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(); } }
public void DebugDraw(Color color) { DebugHelper.DrawRay(Point, Normal * NormalLength, color, 0, false); DebugHelper.DrawCircle(Point, 0.01f, Color.yellow); }