public CollisionMessageData( float thisImpulse, float otherImpulse, SurfaceContact primaryContact, List <SurfaceContact> contacts ) { this.ThisImpulse = thisImpulse; this.OtherImpulse = otherImpulse; this.PrimaryContact = primaryContact; this.Contacts = contacts; }
private void AddForceAndReaction(Vector2 amountAndDir, ForceMode mode, SurfaceContact surfaceContact) { rigidbody2D.AddForce(amountAndDir, mode); if (surfaceContact != null) { var surfaceBody = surfaceContact.Collider.attachedRigidbody; if (surfaceBody != null) { amountAndDir = PhysicsHelper.ConvertToNewtons(amountAndDir, mode, surfaceBody.mass); surfaceBody.AddForce(-amountAndDir * SurfaceReactionFactor, ForceMode2D.Force); } } }
private PhysicsMaterial2D GetSurfaceMaterial(SurfaceContact surfaceContact) { var surfaceMaterial = surfaceContact.Collider.sharedMaterial; if (surfaceMaterial == null) { if (DefaultSurfaceMaterial == null) { DefaultSurfaceMaterial = new PhysicsMaterial2D("DefaultSurfaceMaterial for the Character"); } surfaceMaterial = DefaultSurfaceMaterial; } return(surfaceMaterial); }
private float CalculateFrictionAccelFactor(SurfaceContact surfaceContact) { var surfaceMaterial = GetSurfaceMaterial(surfaceContact); float combinedFriction = PhysicsHelper.CombineFriction( collider2D.sharedMaterial.friction, surfaceMaterial.friction ); float frictionAccelFactor = Mathf.Clamp( Mathf.Sqrt(combinedFriction), MinFrictionAccelFactor, MaxFrictionAccelFactor ); return(frictionAccelFactor); }
private void ProcessCollision(Collision2D collision) { foreach (var contact in collision.contacts) { var normal = contact.normal; var v = ( Vector2 )collider2D.bounds.center - contact.point; /* Fix normal, as Unity (or Box2D) messing it up by randomly swapping * contact.collider and contact.otherCollider * as well as providing opposite normal direction. */ if (Vector2.Dot(v, contact.normal) < 0) { normal = -normal; } if (CheckRelativeVelocity && Vector2.Dot(collision.relativeVelocity, normal) >= 0) { continue; } var surfaceContact = new SurfaceContact(contact.point, normal, collision.collider, Vector2.zero, 0); contacts.Add(surfaceContact); } }
private bool CheckCanClimbStaircase(float horzMoveDir, float stepHeight, float stepDepth, out SurfaceContact staircaseContact) { staircaseContact = null; if (frameContacts.Count == 0) { return(false); } var circleCollider = collider2D as CircleCollider2D; var center = ( Vector2 )circleCollider.bounds.center; var horzMoveDirVector = new Vector2(horzMoveDir, 0); // Seek forward for obstacle to climb on. var steepestContact = frameContacts.WithMin(c => Vector2.Dot(c.Normal, horzMoveDirVector)); if (Vector2.Dot(steepestContact.Normal, horzMoveDirVector) >= 0 || IsWalkable(steepestContact.Normal)) { return(false); } staircaseContact = steepestContact; // Cast up. Debug.DrawRay(center, Vector2.up * stepHeight, Color.cyan); var hit = Spatial.CircleCastFiltered( center, circleCollider.radius, Vector2.up, stepHeight, h => h.rigidbody == this.rigidbody2D, contactsLayerMask ); if (hit.collider != null) { Debug.DrawRay(hit.point, hit.normal * 0.1f, Color.red); return(false); } center += Vector2.up * stepHeight; // Cast forward. Debug.DrawRay(center, horzMoveDirVector * stepDepth, Color.cyan); hit = Spatial.CircleCastFiltered( center, circleCollider.radius, horzMoveDirVector, stepDepth, h => h.rigidbody == this.rigidbody2D, contactsLayerMask ); if (hit.collider != null) { Debug.DrawRay(hit.point, hit.normal * 0.1f, Color.red); return(false); } return(true); }
void FixedUpdate() { TrackContacts(); bool controlWasLost = false; if (controlLossTimer > 0) { if (IsGrounded) { controlLossTimer -= Time.fixedDeltaTime; if (controlLossTimer < 0) { controlLossTimer = 0; } if (controlLossTimer == 0) { controlRegainStartTimestamp = Time.fixedTime; } } controlWasLost = true; } // Allow using item only in certain few states. inventoryUser.CarriedItemIsActive = false; // Allow performing idle action only in truly idle state. bool canPerformIdleAction = false; // Some active actions must be viewed by the camera. bool performedMoveAction = false; bool regainingControl = Time.fixedTime - controlRegainStartTimestamp < ControlRegainDuration; /* TODO: move groups of statements inside if/else into their own methods, * make FixedUpdate () method less "heavy" and more readable. */ if (!IsGrounded && footWeldJoint != null) { /* Sometimes weld joint can move character into position where * no ground contacts can be found by circle trace method. * Such state is not valid since visually character is grounded, * but it's not able to move or jump. */ BreakFootWeld(); } if (regainingControl) { bumpedWhileControlWasLost = false; charController.SetState("ControlRegain", StateMaterials.ControlRegain); } else if (controlWasLost) { BreakFootWeld(); IsPreparingToJump = false; var velocity = rigidbody2D.velocity; bool changeHeading = false; if (IsGrounded) { if (velocity.magnitude > ControlLostChangeHeadingMinVelocity) { changeHeading = true; } } else if (velocity.magnitude > 0) { changeHeading = true; } if (AnyBumpBetweenFrames) { bumpedWhileControlWasLost = true; } if (changeHeading) { Heading = System.Math.Sign(velocity.x); } if (IsGrounded || bumpedWhileControlWasLost) { charController.SetState("ControlLostOnGround", StateMaterials.ControlLostOnGround); } else { charController.SetState("ControlLostMidAir", StateMaterials.ControlLostMidAir); } } else { if (IsPreparingToJump) { if (Time.fixedTime - jumpPreparationStartTimestamp >= JumpPreparationDuration) { IsPreparingToJump = false; if (IsGrounded) { BreakFootWeld(); float jumpDirAngle; float jumpStartVelocity; if (jumpForward) { jumpDirAngle = ForwardJumpDirectionAngle; jumpStartVelocity = ForwardJumpStartVelocity; IsJumpingForward = true; } else { jumpDirAngle = BackJumpDirectionAngle; jumpStartVelocity = BackJumpStartVelocity; IsJumpingBackward = true; } float jumpDirAngleRad = jumpDirAngle * Mathf.Deg2Rad; jumpDir = new Vector2(Mathf.Cos(jumpDirAngleRad), Mathf.Sin(jumpDirAngleRad)); jumpDir.x *= Heading; var flatestFloorContact = FloorContacts.WithMax(c => Vector2.Dot(jumpDir, c.Normal)); float frictionAccelFactor = CalculateFrictionAccelFactor(flatestFloorContact); if (frictionAccelFactor > 1) { frictionAccelFactor = 1; } var velocityChange = jumpDir * jumpStartVelocity; velocityChange.x *= frictionAccelFactor; AddForceAndReaction(velocityChange, ForceMode.VelocityChange, flatestFloorContact); lastJumpTimestamp = Time.fixedTime; stopJumpBounciness = false; performedMoveAction = true; soundPlayer.PlayVariation(JumpSounds); charController.SetState(IsJumpingForward ? "JumpingForward" : "JumpingBackward", StateMaterials.JumpBouncy); } else { // TODO: transfer control to fall-move processing code. EnterStandState(); } } else { charController.SetState("PrepareToJump", StateMaterials.Stand); } } else if (IsJumping) { bool isBouncy = Time.fixedTime - lastJumpTimestamp < JumpBouncinessDuration; if ((BumpTheFloorBetweenFrames || IsGrounded) && !isBouncy) { IsJumpingForward = false; IsJumpingBackward = false; EnterStandState(); } else { // Stop bouncing after first hit. if (BumpTheWallOrCeilingBetweenFrames) { stopJumpBounciness = true; } StateMaterial stateMaterial; if (isBouncy && !stopJumpBounciness) { // Enable character bounciness. float angle = rigidbody2D.velocity.AngleRad(); float t = Mathf.Abs(Mathf.Cos(angle)); stateMaterial = StateMaterial.Lerp(StateMaterials.JumpNormal, StateMaterials.JumpBouncy, t); } else { // Disable bouncing. stateMaterial = StateMaterials.JumpNormal; } if (IsJumpingBackward && Time.fixedTime - lastJumpTimestamp < BackJumpSideAccelerationDuration) { /* Apply little amount of side acceleration so that the character * can climb to the edge of a peak rather than bounce backward and fall down. */ int sign = System.Math.Sign(jumpDir.x); var accel = new Vector2(sign * BackJumpSideAcceleration, 0); rigidbody2D.AddForce(accel, ForceMode.Acceleration); DebugHelper.DrawRay(transform.position, accel, Color.blue, 0, false); } charController.SetState(IsJumpingForward ? "JumpingForward" : "JumpingBackward", stateMaterial); } } else { bool movingDown = Vector2.Dot(rigidbody2D.velocity, -Vector2.up) > 0; float downVelocity = movingDown ? rigidbody2D.velocity.magnitude : 0; // TODO: apply "Fall" animation after some time of being in the air. Velocity should not be accounted. if (!IsGrounded && downVelocity >= FallVelocity) { charController.SetState("Fall", StateMaterials.JumpNormal); } else { bool noMovementPerformed = false; var move = new Vector2(MoveX, MoveY); float moveAmount = move.magnitude; if (moveAmount != 0) { var moveDir = move.normalized; if (moveAmount > 1) { moveAmount = 1; } SurfaceContact surfaceContact = null; Vector2 moveDirAlongSurface = Vector2.zero; float surfaceAcceleration = 0; float gravityResistance = 0; float maxVelocity = 0; bool wantToWalk = false; bool canTurn = false; if (IsGrounded) { canTurn = true; var steepestFloorContact = FloorContacts.WithMin(c => Vector2.Dot(moveDir, c.Normal)); var tangent = Common.RightOrthogonal(steepestFloorContact.Normal); moveDirAlongSurface = Vector2.Dot(moveDir, tangent) > 0 ? tangent : -tangent; bool noObstaclesOnTheWay = frameContacts .All(c => Vector2.Dot(c.Normal, moveDirAlongSurface) >= 0 || // Push dynamic body if it doesn't belong to other character. (c.Collider.attachedRigidbody != null && !IsCharacter(c.Collider)) /* TODO: check mass of the body? Presumably evaluate to false when object can't be moved. * UPD: well, it depends on friction force applied from the floor to an obstacle object, * so everything is much more complicated.. */ ); if (noObstaclesOnTheWay) { wantToWalk = true; surfaceContact = steepestFloorContact; surfaceAcceleration = WalkAcceleration; gravityResistance = WalkGravityResistance; maxVelocity = MaxWalkVelocity; } else { var obstacleContact = frameContacts.First(c => Vector2.Dot(c.Normal, moveDirAlongSurface) < 0); Debug.DrawRay(obstacleContact.Point, obstacleContact.Normal * 0.1f, Color.magenta); } } if (!wantToWalk) { SurfaceContact staircaseContact; bool canClimb = CheckCanClimbStaircase(moveDir.x, ClimbStepHeight, ClimbStepDepth, out staircaseContact); if (canClimb) { canTurn = true; wantToWalk = true; surfaceContact = staircaseContact; float climbMoveDirAngleRad = ClimbMovementDirectionAngle * Mathf.Deg2Rad; var climbDir = new Vector2(Mathf.Cos(climbMoveDirAngleRad), Mathf.Sin(climbMoveDirAngleRad)); climbDir.x *= moveDir.x; moveDirAlongSurface = climbDir; surfaceAcceleration = ClimbAcceleration; gravityResistance = ClimbGravityResistance; maxVelocity = MaxClimbVelocity; } } if (wantToWalk) { BreakFootWeld(); StateMaterial stateMaterial; if (IsCharacter(surfaceContact.Collider)) { stateMaterial = StateMaterials.StepOverCharacter; } else { stateMaterial = StateMaterials.Walk; } var otherRigidbody = surfaceContact.Collider.attachedRigidbody; var platformVelocity = Vector2.zero; if (otherRigidbody != null) { platformVelocity = otherRigidbody.GetPointVelocity(surfaceContact.Point); } var relVelocity = rigidbody2D.velocity - platformVelocity; var velocityAlongSurface = Common.Project(relVelocity, moveDirAlongSurface); float frictionAccelFactor = CalculateFrictionAccelFactor(surfaceContact); surfaceAcceleration *= frictionAccelFactor; var velocityDelta = moveDirAlongSurface * surfaceAcceleration * Time.fixedDeltaTime; var newVelocityAlongSurface = velocityAlongSurface + velocityDelta; Vector2 velocityChange; // DEV: new approach. // TODO: slow down when walking down steep ground. if (newVelocityAlongSurface.magnitude > maxVelocity) { if (newVelocityAlongSurface.magnitude > velocityAlongSurface.magnitude) { if (velocityAlongSurface.magnitude < maxVelocity) { velocityChange = velocityAlongSurface.normalized * maxVelocity - velocityAlongSurface; } else { velocityChange = Vector2.zero; } } else { velocityChange = newVelocityAlongSurface - velocityAlongSurface; } } else { velocityChange = newVelocityAlongSurface - velocityAlongSurface; } // TODO: old approach. //newVelocityAlongSurface = Vector2.ClampMagnitude ( newVelocityAlongSurface, maxVelocity ); //velocityChange = newVelocityAlongSurface - velocityAlongSurface; Debug.DrawRay(transform.position, velocityChange, Color.red, 0, false); AddForceAndReaction(velocityChange, ForceMode.VelocityChange, surfaceContact); AddForceAndReaction(-Physics2D.gravity * gravityResistance, ForceMode.Acceleration, surfaceContact); soundPlayer.PlayVariation(WalkSounds, restart: false); charController.SetState("Walk", stateMaterial); } else { noMovementPerformed = true; } if (canTurn) { var headingScale = headingTransform.localScale; headingScale.x = moveDir.x; headingTransform.localScale = headingScale; } } else { noMovementPerformed = true; } bool readyToJump = Time.fixedTime - lastJumpTimestamp >= JumpCooldown; bool wantsToPerformJump = PerformForwardJump || PerformBackJump; if (wantsToPerformJump && IsGrounded && readyToJump) { jumpPreparationStartTimestamp = Time.fixedTime; IsPreparingToJump = true; /* TODO: disable IsPreparingToJump when control lost. * And maybe in some other cases too. */ jumpForward = PerformForwardJump; } if (noMovementPerformed) { canPerformIdleAction = true; bool performedIdleAction = false; if (IsStandingStill(FootWeldMaxVelocity)) // TODO: use other value instead of FootWeldMaxVelocity? { performedIdleAction = charController.PerformIdleAction(); } if (!performedIdleAction) { EnterStandState(); } } else { performedMoveAction = true; } } } } if (!canPerformIdleAction) { charController.InterruptIdleAction(); } if (performedMoveAction) { CameraEvents.Moved.Restart(); } }
// TODO: make predicate instead of ignorePredicate (accept "true" instead of "false"). public static List <SurfaceContact> GetCircleContacts( Vector2 center, float radius, float minDistance, float maxDistance, System.Func <RaycastHit2D, bool> ignorePredicate = null, int numSweepDirections = 4, int layerMask = Physics2D.DefaultRaycastLayers ) { var contacts = new List <SurfaceContact> (); float angle = 0; float dAngle = Mathf.PI * 2 / numSweepDirections; float offset = radius - minDistance + CastOffset; float radiusSq = radius * radius; float sweepDistance = maxDistance - minDistance + CastOffset * 2; // DEBUG //DebugHelper.DrawCircle ( center, minDistance, new Color ( 1, 0.5f, 0.25f ) ); //DebugHelper.DrawCircle ( center, maxDistance, new Color ( 1, 0.5f, 0.25f ) ); for (int i = 0; i < numSweepDirections; i++, angle += dAngle) { var dir = new Vector2( Mathf.Cos(angle), Mathf.Sin(angle) ); var origin = center - dir * offset; var hits = Physics2D.CircleCastAll(origin, radius, dir, sweepDistance, layerMask); //Debug.DrawRay ( hits [1].point, hits [1].normal, Color.red ); //DebugHelper.DrawCircle ( hits [1].point, 0.1f, Color.yellow ); //Debug.Log ( hits [1].collider ); //DebugHelper.DrawCircle ( origin, radius, Color.yellow ); for (int j = 0; j < hits.Length; j++) { var hit = hits [j]; // TODO: make argument "acceptInnerCollisions" /* TODO: make option to discard inner collisions when outer collisions for the same collider were found. * UPD: or perform this by default? */ // Filter inner collisions and fix their normals. if (hit.distance <= 0) { // Some points can be out of original circle. float distFromCenterSq = Common.DistanceSq(hit.point, center); if (distFromCenterSq > radiusSq) { continue; } /* Some inner collisions are reported outside of bounding box * of other collider. Filter them out as they're quite inaccurate. */ var otherBounds = hit.collider.bounds; var hitPointInZPlane = ( Vector3 )hit.point; hitPointInZPlane.z = otherBounds.center.z; bool hitIsInsideBb = otherBounds.Contains(hitPointInZPlane); if (!hitIsInsideBb) { continue; } /* Now we definitely sure that this is appropriate inner collision * and we can fix its normal. */ var vToCenter = center - hit.point; hit.normal = vToCenter.normalized; } if (ignorePredicate != null && ignorePredicate(hit)) { continue; } var vToPoint = hit.point - center; float distance = vToPoint.magnitude; if (distance < minDistance || distance > maxDistance) { continue; } var dirToPoint = vToPoint.normalized; var contact = new SurfaceContact( hit.point, hit.normal, hit.collider, dirToPoint, distance ); contacts.Add(contact); } } const float sameVertexDistanceThreshold = 0.001f; // TODO: promote to arguments. const float sameVertexDistanceThresholdSq = sameVertexDistanceThreshold * sameVertexDistanceThreshold; for (int i = 0; i < contacts.Count; i++) { var c1 = contacts [i]; bool foundCloseVerices = false; for (int j = contacts.Count - 1; j > i; j--) { var c2 = contacts [j]; if (c1.Collider == c2.Collider && Common.DistanceSq(c1.Point, c2.Point) < sameVertexDistanceThresholdSq ) { contacts.RemoveAt(j); foundCloseVerices = true; } } if (foundCloseVerices) { // Most probable it's a corner collision. Calculate precise normal. c1.Normal = (center - c1.Point).normalized; } } return(contacts); }
public ContactForceFactor(SurfaceContact contact, float factor) { this.Contact = contact; this.Factor = factor; }