之前作者在前文中向大家介绍了角色控制器组件。默认系统提供了JavaScript脚本的支持,可是我们还是喜欢用C#来编写脚本,本篇文章作者将把角色控制器的所有脚本全部改成C#语言。方便自己也方便大家学习。首先,我们将角色控制器包引入工程中。如下图所示,默认提供的脚本除了MouseLook以外其它的都是JS脚本,本篇文章作者将把它们全部修改成C#。刚好也是答应Unity圣典的站长录制游戏开发视频,视频中我说下一节我将教大家怎么把角色控制器组件的脚本全部改成C#。
首先把CharacterMotor.js修改成C# 它主要设置角色控制的系数,如运动、跳跃、移动、滑动等。第一人称与第三人称主角模型的移动与旋转的角度都最后都是在这里计算的,请大家好好看看这个类, 尤其是UpdateFunction()方法。
CharacterMotor.cs
[代码]c#/cpp/oc代码:
using UnityEngine; using System.Collections; /** * @Author : www.xuanyusong.com */ [RequireComponent(typeof(CharacterController))] [AddComponentMenu("Character/Character Motor")] public class CharacterMotor : MonoBehaviour { // Does this script currently respond to input? public bool canControl = true; public bool useFixedUpdate = true; // For the next variables, @System.NonSerialized tells Unity to not serialize the variable or show it in the inspector view. // Very handy for organization! // The current global direction we want the character to move in. [System.NonSerialized] public Vector3 inputMoveDirection = Vector3.zero; // Is the jump button held down? We use this interface instead of checking // for the jump button directly so this script can also be used by AIs. [System.NonSerialized] public bool inputJump = false; [System.Serializable] public class CharacterMotorMovement { // The maximum horizontal speed when moving public float maxForwardSpeed = 10.0f; public float maxSidewaysSpeed = 10.0f; public float maxBackwardsSpeed = 10.0f; // Curve for multiplying speed based on slope (negative = downwards) public AnimationCurve slopeSpeedMultiplier = new AnimationCurve(new Keyframe(-90, 1), new Keyframe(0, 1), new Keyframe(90, 0)); // How fast does the character change speeds? Higher is faster. public float maxGroundAcceleration = 30.0f; public float maxAirAcceleration = 20.0f; // The gravity for the character public float gravity = 10.0f; public float maxFallSpeed = 20.0f; // For the next variables, @System.NonSerialized tells Unity to not serialize the variable or show it in the inspector view. // Very handy for organization! // The last collision flags returned from controller.Move [System.NonSerialized] public CollisionFlags collisionFlags; // We will keep track of the character's current velocity, [System.NonSerialized] public Vector3 velocity; // This keeps track of our current velocity while we're not grounded [System.NonSerialized] public Vector3 frameVelocity = Vector3.zero; [System.NonSerialized] public Vector3 hitPoint = Vector3.zero; [System.NonSerialized] public Vector3 lastHitPoint = new Vector3(Mathf.Infinity, 0, 0); } public CharacterMotorMovement movement = new CharacterMotorMovement(); public enum MovementTransferOnJump { None, // The jump is not affected by velocity of floor at all. InitTransfer, // Jump gets its initial velocity from the floor, then gradualy comes to a stop. PermaTransfer, // Jump gets its initial velocity from the floor, and keeps that velocity until landing. PermaLocked // Jump is relative to the movement of the last touched floor and will move together with that floor. } // We will contain all the jumping related variables in one helper class for clarity. [System.Serializable] public class CharacterMotorJumping { // Can the character jump? public bool enabled = true; // How high do we jump when pressing jump and letting go immediately public float baseHeight = 1.0f; // We add extraHeight units (meters) on top when holding the button down longer while jumping public float extraHeight = 4.1f; // How much does the character jump out perpendicular to the surface on walkable surfaces? // 0 means a fully vertical jump and 1 means fully perpendicular. public float perpAmount = 0.0f; // How much does the character jump out perpendicular to the surface on too steep surfaces? // 0 means a fully vertical jump and 1 means fully perpendicular. public float steepPerpAmount = 0.5f; // For the next variables, @System.NonSerialized tells Unity to not serialize the variable or show it in the inspector view. // Very handy for organization! // Are we jumping? (Initiated with jump button and not grounded yet) // To see if we are just in the air (initiated by jumping OR falling) see the grounded variable. [System.NonSerialized] public bool jumping = false; [System.NonSerialized] public bool holdingJumpButton = false; // the time we jumped at (Used to determine for how long to apply extra jump power after jumping.) [System.NonSerialized] public float lastStartTime = 0.0f; [System.NonSerialized] public float lastButtonDownTime = -100f; [System.NonSerialized] public Vector3 jumpDir = Vector3.up; } public CharacterMotorJumping jumping = new CharacterMotorJumping(); [System.Serializable] public class CharacterMotorMovingPlatform { public bool enabled = true; public MovementTransferOnJump movementTransfer = MovementTransferOnJump.PermaTransfer; [System.NonSerialized] public Transform hitPlatform; [System.NonSerialized] public Transform activePlatform; [System.NonSerialized] public Vector3 activeLocalPoint; [System.NonSerialized] public Vector3 activeGlobalPoint; [System.NonSerialized] public Quaternion activeLocalRotation; [System.NonSerialized] public Quaternion activeGlobalRotation; [System.NonSerialized] public Matrix4x4 lastMatrix; [System.NonSerialized] public Vector3 platformVelocity; [System.NonSerialized] public bool newPlatform; } public CharacterMotorMovingPlatform movingPlatform = new CharacterMotorMovingPlatform(); [System.Serializable] public class CharacterMotorSliding { // Does the character slide on too steep surfaces? public bool enabled = true; // How fast does the character slide on steep surfaces? public float slidingSpeed = 15f; // How much can the player control the sliding direction? // If the value is 0.5 the player can slide sideways with half the speed of the downwards sliding speed. public float sidewaysControl = 1.0f; // How much can the player influence the sliding speed? // If the value is 0.5 the player can speed the sliding up to 150% or slow it down to 50%. public float speedControl = 0.4f; } public CharacterMotorSliding sliding = new CharacterMotorSliding(); [System.NonSerialized] public bool grounded = true; [System.NonSerialized] public Vector3 groundNormal = Vector3.zero; private Vector3 lastGroundNormal = Vector3.zero; private Transform tr; private CharacterController controller ; void Awake () { controller = GetComponent(); tr = transform; } private void UpdateFunction () { // We copy the actual velocity into a temporary variable that we can manipulate. Vector3 velocity = movement.velocity; // Update velocity based on input velocity = ApplyInputVelocityChange(velocity); // Apply gravity and jumping force velocity = ApplyGravityAndJumping (velocity); // Moving platform support Vector3 moveDistance = Vector3.zero; if (MoveWithPlatform()) { Vector3 newGlobalPoint = movingPlatform.activePlatform.TransformPoint(movingPlatform.activeLocalPoint); moveDistance = (newGlobalPoint - movingPlatform.activeGlobalPoint); if (moveDistance != Vector3.zero) controller.Move(moveDistance); // Support moving platform rotation as well: Quaternion newGlobalRotation = movingPlatform.activePlatform.rotation * movingPlatform.activeLocalRotation; Quaternion rotationDiff = newGlobalRotation * Quaternion.Inverse(movingPlatform.activeGlobalRotation); var yRotation = rotationDiff.eulerAngles.y; if (yRotation != 0) { // Prevent rotation of the local up vector tr.Rotate(0, yRotation, 0); } } // Save lastPosition for velocity calculation. Vector3 lastPosition = tr.position; // We always want the movement to be framerate independent. Multiplying by Time.deltaTime does this. Vector3 currentMovementOffset = velocity * Time.deltaTime; // Find out how much we need to push towards the ground to avoid loosing grouning // when walking down a step or over a sharp change in slope. float pushDownOffset = Mathf.Max(controller.stepOffset, new Vector3(currentMovementOffset.x, 0, currentMovementOffset.z).magnitude); if (grounded) currentMovementOffset -= pushDownOffset * Vector3.up; // Reset variables that will be set by collision function movingPlatform.hitPlatform = null; groundNormal = Vector3.zero; // Move our character! movement.collisionFlags = controller.Move (currentMovementOffset); movement.lastHitPoint = movement.hitPoint; lastGroundNormal = groundNormal; if (movingPlatform.enabled && movingPlatform.activePlatform != movingPlatform.hitPlatform) { if (movingPlatform.hitPlatform != null) { movingPlatform.activePlatform = movingPlatform.hitPlatform; movingPlatform.lastMatrix = movingPlatform.hitPlatform.localToWorldMatrix; movingPlatform.newPlatform = true; } } // Calculate the velocity based on the current and previous position. // This means our velocity will only be the amount the character actually moved as a result of collisions. Vector3 oldHVelocity = new Vector3(velocity.x, 0, velocity.z); movement.velocity = (tr.position - lastPosition) / Time.deltaTime; Vector3 newHVelocity = new Vector3(movement.velocity.x, 0, movement.velocity.z); // The CharacterController can be moved in unwanted directions when colliding with things. // We want to prevent this from influencing the recorded velocity. if (oldHVelocity == Vector3.zero) { movement.velocity = new Vector3(0, movement.velocity.y, 0); } else { float projectedNewVelocity = Vector3.Dot(newHVelocity, oldHVelocity) / oldHVelocity.sqrMagnitude; movement.velocity = oldHVelocity * Mathf.Clamp01(projectedNewVelocity) + movement.velocity.y * Vector3.up; } if (movement.velocity.y < velocity.y - 0.001) { if (movement.velocity.y < 0) { // Something is forcing the CharacterController down faster than it should. // Ignore this movement.velocity.y = velocity.y; } else { // The upwards movement of the CharacterController has been blocked. // This is treated like a ceiling collision - stop further jumping here. jumping.holdingJumpButton = false; } } // We were grounded but just loosed grounding if (grounded && !IsGroundedTest()) { grounded = false; // Apply inertia from platform if (movingPlatform.enabled && (movingPlatform.movementTransfer == MovementTransferOnJump.InitTransfer || movingPlatform.movementTransfer == MovementTransferOnJump.PermaTransfer) ) { movement.frameVelocity = movingPlatform.platformVelocity; movement.velocity += movingPlatform.platformVelocity; } SendMessage("OnFall", SendMessageOptions.DontRequireReceiver); // We pushed the character down to ensure it would stay on the ground if there was any. // But there wasn't so now we cancel the downwards offset to make the fall smoother. tr.position += pushDownOffset * Vector3.up; } // We were not grounded but just landed on something else if (!grounded && IsGroundedTest()) { grounded = true; jumping.jumping = false; SubtractNewPlatformVelocity(); SendMessage("OnLand", SendMessageOptions.DontRequireReceiver); } // Moving platforms support if (MoveWithPlatform()) { // Use the center of the lower half sphere of the capsule as reference point. // This works best when the character is standing on moving tilting platforms. movingPlatform.activeGlobalPoint = tr.position + Vector3.up * (controller.center.y - controller.height*0.5f + controller.radius); movingPlatform.activeLocalPoint = movingPlatform.activePlatform.InverseTransformPoint(movingPlatform.activeGlobalPoint); // Support moving platform rotation as well: movingPlatform.activeGlobalRotation = tr.rotation; movingPlatform.activeLocalRotation = Quaternion.Inverse(movingPlatform.activePlatform.rotation) * movingPlatform.activeGlobalRotation; } } void FixedUpdate () { if (movingPlatform.enabled) { if (movingPlatform.activePlatform != null) { if (!movingPlatform.newPlatform) { Vector3 lastVelocity = movingPlatform.platformVelocity; movingPlatform.platformVelocity = ( movingPlatform.activePlatform.localToWorldMatrix.MultiplyPoint3x4(movingPlatform.activeLocalPoint) - movingPlatform.lastMatrix.MultiplyPoint3x4(movingPlatform.activeLocalPoint) ) / Time.deltaTime; } movingPlatform.lastMatrix = movingPlatform.activePlatform.localToWorldMatrix; movingPlatform.newPlatform = false; } else { movingPlatform.platformVelocity = Vector3.zero; } } if (useFixedUpdate) UpdateFunction(); } void Update () { if (!useFixedUpdate) UpdateFunction(); } private Vector3 ApplyInputVelocityChange (Vector3 velocity) { if (!canControl) inputMoveDirection = Vector3.zero; // Find desired velocity Vector3 desiredVelocity; if (grounded && TooSteep()) { // The direction we're sliding in desiredVelocity = new Vector3(groundNormal.x, 0, groundNormal.z).normalized; // Find the input movement direction projected onto the sliding direction var projectedMoveDir = Vector3.Project(inputMoveDirection, desiredVelocity); // Add the sliding direction, the spped control, and the sideways control vectors desiredVelocity = desiredVelocity + projectedMoveDir * sliding.speedControl + (inputMoveDirection - projectedMoveDir) * sliding.sidewaysControl; // Multiply with the sliding speed desiredVelocity *= sliding.slidingSpeed; } else desiredVelocity = GetDesiredHorizontalVelocity(); if (movingPlatform.enabled && movingPlatform.movementTransfer == MovementTransferOnJump.PermaTransfer) { desiredVelocity += movement.frameVelocity; desiredVelocity.y = 0; } if (grounded) desiredVelocity = AdjustGroundVelocityToNormal(desiredVelocity, groundNormal); else velocity.y = 0; // Enforce max velocity change float maxVelocityChange = GetMaxAcceleration(grounded) * Time.deltaTime; Vector3 velocityChangeVector = (desiredVelocity - velocity); if (velocityChangeVector.sqrMagnitude > maxVelocityChange * maxVelocityChange) { velocityChangeVector = velocityChangeVector.normalized * maxVelocityChange; } // If we're in the air and don't have control, don't apply any velocity change at all. // If we're on the ground and don't have control we do apply it - it will correspond to friction. if (grounded || canControl) velocity += velocityChangeVector; if (grounded) { // When going uphill, the CharacterController will automatically move up by the needed amount. // Not moving it upwards manually prevent risk of lifting off from the ground. // When going downhill, DO move down manually, as gravity is not enough on steep hills. velocity.y = Mathf.Min(velocity.y, 0); } return velocity; } private Vector3 ApplyGravityAndJumping (Vector3 velocity) { if (!inputJump || !canControl) { jumping.holdingJumpButton = false; jumping.lastButtonDownTime = -100; } if (inputJump && jumping.lastButtonDownTime < 0 && canControl) jumping.lastButtonDownTime = Time.time; if (grounded) velocity.y = Mathf.Min(0, velocity.y) - movement.gravity * Time.deltaTime; else { velocity.y = movement.velocity.y - movement.gravity * Time.deltaTime; // When jumping up we don't apply gravity for some time when the user is holding the jump button. // This gives more control over jump height by pressing the button longer. if (jumping.jumping && jumping.holdingJumpButton) { // Calculate the duration that the extra jump force should have effect. // If we're still less than that duration after the jumping time, apply the force. if (Time.time < jumping.lastStartTime + jumping.extraHeight / CalculateJumpVerticalSpeed(jumping.baseHeight)) { // Negate the gravity we just applied, except we push in jumpDir rather than jump upwards. velocity += jumping.jumpDir * movement.gravity * Time.deltaTime; } } // Make sure we don't fall any faster than maxFallSpeed. This gives our character a terminal velocity. velocity.y = Mathf.Max (velocity.y, -movement.maxFallSpeed); } if (grounded) { // Jump only if the jump button was pressed down in the last 0.2 seconds. // We use this check instead of checking if it's pressed down right now // because players will often try to jump in the exact moment when hitting the ground after a jump // and if they hit the button a fraction of a second too soon and no new jump happens as a consequence, // it's confusing and it feels like the game is buggy. if (jumping.enabled && canControl && (Time.time - jumping.lastButtonDownTime < 0.2)) { grounded = false; jumping.jumping = true; jumping.lastStartTime = Time.time; jumping.lastButtonDownTime = -100; jumping.holdingJumpButton = true; // Calculate the jumping direction if (TooSteep()) jumping.jumpDir = Vector3.Slerp(Vector3.up, groundNormal, jumping.steepPerpAmount); else jumping.jumpDir = Vector3.Slerp(Vector3.up, groundNormal, jumping.perpAmount); // Apply the jumping force to the velocity. Cancel any vertical velocity first. velocity.y = 0; velocity += jumping.jumpDir * CalculateJumpVerticalSpeed (jumping.baseHeight); // Apply inertia from platform if (movingPlatform.enabled && (movingPlatform.movementTransfer == MovementTransferOnJump.InitTransfer || movingPlatform.movementTransfer == MovementTransferOnJump.PermaTransfer) ) { movement.frameVelocity = movingPlatform.platformVelocity; velocity += movingPlatform.platformVelocity; } SendMessage("OnJump", SendMessageOptions.DontRequireReceiver); } else { jumping.holdingJumpButton = false; } } return velocity; } void OnControllerColliderHit (ControllerColliderHit hit) { if (hit.normal.y > 0 && hit.normal.y > groundNormal.y && hit.moveDirection.y < 0) { if ((hit.point - movement.lastHitPoint).sqrMagnitude > 0.001 || lastGroundNormal == Vector3.zero) groundNormal = hit.normal; else groundNormal = lastGroundNormal; movingPlatform.hitPlatform = hit.collider.transform; movement.hitPoint = hit.point; movement.frameVelocity = Vector3.zero; } } private IEnumerator SubtractNewPlatformVelocity () { // When landing, subtract the velocity of the new ground from the character's velocity // since movement in ground is relative to the movement of the ground. if (movingPlatform.enabled && (movingPlatform.movementTransfer == MovementTransferOnJump.InitTransfer || movingPlatform.movementTransfer == MovementTransferOnJump.PermaTransfer) ) { // If we landed on a new platform, we have to wait for two FixedUpdates // before we know the velocity of the platform under the character if (movingPlatform.newPlatform) { Transform platform = movingPlatform.activePlatform; yield return new WaitForFixedUpdate(); yield return new WaitForFixedUpdate(); if (grounded && platform == movingPlatform.activePlatform) yield return 1; } movement.velocity -= movingPlatform.platformVelocity; } } private bool MoveWithPlatform () { return ( movingPlatform.enabled && (grounded || movingPlatform.movementTransfer == MovementTransferOnJump.PermaLocked) && movingPlatform.activePlatform != null ); } private Vector3 GetDesiredHorizontalVelocity () { // Find desired velocity Vector3 desiredLocalDirection = tr.InverseTransformDirection(inputMoveDirection); float maxSpeed = MaxSpeedInDirection(desiredLocalDirection); if (grounded) { // Modify max speed on slopes based on slope speed multiplier curve var movementSlopeAngle = Mathf.Asin(movement.velocity.normalized.y) * Mathf.Rad2Deg; maxSpeed *= movement.slopeSpeedMultiplier.Evaluate(movementSlopeAngle); } return tr.TransformDirection(desiredLocalDirection * maxSpeed); } private Vector3 AdjustGroundVelocityToNormal (Vector3 hVelocity, Vector3 groundNormal) { Vector3 sideways = Vector3.Cross(Vector3.up, hVelocity); return Vector3.Cross(sideways, groundNormal).normalized * hVelocity.magnitude; } private bool IsGroundedTest () { return (groundNormal.y > 0.01); } float GetMaxAcceleration (bool grounded) { // Maximum acceleration on ground and in air if (grounded) return movement.maxGroundAcceleration; else return movement.maxAirAcceleration; } float CalculateJumpVerticalSpeed (float targetJumpHeight) { // From the jump height and gravity we deduce the upwards speed // for the character to reach at the apex. return Mathf.Sqrt (2 * targetJumpHeight * movement.gravity); } bool IsJumping () { return jumping.jumping; } bool IsSliding () { return (grounded && sliding.enabled && TooSteep()); } bool IsTouchingCeiling () { return (movement.collisionFlags & CollisionFlags.CollidedAbove) != 0; } bool IsGrounded () { return grounded; } bool TooSteep () { return (groundNormal.y <= Mathf.Cos(controller.slopeLimit * Mathf.Deg2Rad)); } Vector3 GetDirection () { return inputMoveDirection; } void SetControllable (bool controllable) { canControl = controllable; } // Project a direction onto elliptical quater segments based on forward, sideways, and backwards speed. // The function returns the length of the resulting vector. float MaxSpeedInDirection (Vector3 desiredMovementDirection) { if (desiredMovementDirection == Vector3.zero) return 0; else { float zAxisEllipseMultiplier = (desiredMovementDirection.z > 0 ? movement.maxForwardSpeed : movement.maxBackwardsSpeed) / movement.maxSidewaysSpeed; Vector3 temp = new Vector3(desiredMovementDirection.x, 0, desiredMovementDirection.z / zAxisEllipseMultiplier).normalized; float length = new Vector3(temp.x, 0, temp.z * zAxisEllipseMultiplier).magnitude * movement.maxSidewaysSpeed; return length; } } void SetVelocity (Vector3 velocity) { grounded = false; movement.velocity = velocity; movement.frameVelocity = Vector3.zero; SendMessage("OnExternalVelocity"); } // Require a character controller to be attached to the same game object //@script RequireComponent (CharacterController) //@script AddComponentMenu ("Character/Character Motor") }
接着把FPSInputControoler.js修改成C#语言。它用于第一人称控制角色移动,这里会监听主角按下的方向键最后传给CharacterMotor去计算模型的位置与旋转的角度。
FPSInputController.cs
[代码]c#/cpp/oc代码:
using UnityEngine; using System.Collections; /** * @Author : www.xuanyusong.com */ [RequireComponent(typeof(CharacterMotor))] [AddComponentMenu("Character/FPS Input Controller")] public class FPSInputController : MonoBehaviour { private CharacterMotor motor ; // Use this for initialization void Awake () { motor = GetComponent(); } // Update is called once per frame void Update () { // Get the input vector from kayboard or analog stick Vector3 directionVector = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")); if (directionVector != Vector3.zero) { // Get the length of the directon vector and then normalize it // Dividing by the length is cheaper than normalizing when we already have the length anyway var directionLength = directionVector.magnitude; directionVector = directionVector / directionLength; // Make sure the length is no bigger than 1 directionLength = Mathf.Min(1, directionLength); // Make the input vector more sensitive towards the extremes and less sensitive in the middle // This makes it easier to control slow speeds when using analog sticks directionLength = directionLength * directionLength; // Multiply the normalized direction vector by the modified length directionVector = directionVector * directionLength; } // Apply the direction to the CharacterMotor motor.inputMoveDirection = transform.rotation * directionVector; motor.inputJump = Input.GetButton("Jump"); } }
MouseLook.cs因为已经是C#语言所以就不翻译了,然后是PlatFormInputController.cs 把它转成C#语言。它和FPSInputController一样会控制主角,但是它会更加精细的计算模型旋转的插值系数。
PlatformInputController.cs
[代码]c#/cpp/oc代码:
using UnityEngine; using System.Collections; /** * @Author : www.xuanyusong.com */ [RequireComponent(typeof(CharacterController))] [AddComponentMenu("Character/Platform Input Controller")] public class PlatformInputController : MonoBehaviour { public bool autoRotate = true; public float maxRotationSpeed = 360; private CharacterMotor motor ; // Use this for initialization void Awake () { motor = GetComponent(); } // Update is called once per frame void Update () { // Get the input vector from kayboard or analog stick Vector3 directionVector = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0); if (directionVector != Vector3.zero) { // Get the length of the directon vector and then normalize it // Dividing by the length is cheaper than normalizing when we already have the length anyway var directionLength = directionVector.magnitude; directionVector = directionVector / directionLength; // Make sure the length is no bigger than 1 directionLength = Mathf.Min(1, directionLength); // Make the input vector more sensitive towards the extremes and less sensitive in the middle // This makes it easier to control slow speeds when using analog sticks directionLength = directionLength * directionLength; // Multiply the normalized direction vector by the modified length directionVector = directionVector * directionLength; } // Rotate the input vector into camera space so up is camera's up and right is camera's right directionVector = Camera.main.transform.rotation * directionVector; // Rotate input vector to be perpendicular to character's up vector var camToCharacterSpace = Quaternion.FromToRotation(-Camera.main.transform.forward, transform.up); directionVector = (camToCharacterSpace * directionVector); // Apply the direction to the CharacterMotor motor.inputMoveDirection = directionVector; motor.inputJump = Input.GetButton("Jump"); // Set rotation to the move direction if (autoRotate && directionVector.sqrMagnitude > 0.01) { Vector3 newForward = ConstantSlerp( transform.forward, directionVector, maxRotationSpeed * Time.deltaTime ); newForward = ProjectOntoPlane(newForward, transform.up); transform.rotation = Quaternion.LookRotation(newForward, transform.up); } } Vector3 ProjectOntoPlane (Vector3 v, Vector3 normal) { return v - Vector3.Project(v, normal); } Vector3 ConstantSlerp (Vector3 from, Vector3 to, float angle) { float value = Mathf.Min(1, angle / Vector3.Angle(from, to)); return Vector3.Slerp(from, to, value); } }
接着是ThirdPersonCamera 我们把它改成C#语言。它主要控制第三人称视角时摄像机的控制。
ThirdPersonCamera.cs
[代码]c#/cpp/oc代码:
using UnityEngine; using System.Collections; /** * @Author : www.xuanyusong.com */ public class ThirdPersonCamera : MonoBehaviour { public Transform cameraTransform; private Transform _target; public float distance = 7.0f; public float height = 3.0f; public float angularSmoothLag = 0.3f; public float angularMaxSpeed = 15.0f; public float heightSmoothLag = 0.3f; public float snapSmoothLag = 0.2f; public float snapMaxSpeed = 720.0f; public float clampHeadPositionScreenSpace = 0.75f; public float lockCameraTimeout = 0.2f; private Vector3 headOffset = Vector3.zero; private Vector3 centerOffset = Vector3.zero; private float heightVelocity = 0.0f; private float angleVelocity = 0.0f; private bool snap = false; private ThirdPersonController controller; private float targetHeight = 100000.0f; void Awake () { if(!cameraTransform && Camera.main) cameraTransform = Camera.main.transform; if(!cameraTransform) { Debug.Log("Please assign a camera to the ThirdPersonCamera script."); enabled = false; } _target = transform; if (_target) { controller = _target.GetComponent(); } if (controller) { CharacterController characterController = (CharacterController)_target.collider; centerOffset = characterController.bounds.center - _target.position; headOffset = centerOffset; headOffset.y = characterController.bounds.max.y - _target.position.y; } else Debug.Log("Please assign a target to the camera that has a ThirdPersonController script attached."); Cut(_target, centerOffset); } void DebugDrawStuff () { Debug.DrawLine(_target.position, _target.position + headOffset); } float AngleDistance (float a , float b ) { a = Mathf.Repeat(a, 360); b = Mathf.Repeat(b, 360); return Mathf.Abs(b - a); } void Apply (Transform dummyTarget, Vector3 dummyCenter) { // Early out if we don't have a target if (!controller) return; Vector3 targetCenter = _target.position + centerOffset; Vector3 targetHead = _target.position + headOffset; // DebugDrawStuff(); // Calculate the current & target rotation angles float originalTargetAngle = _target.eulerAngles.y; float currentAngle = cameraTransform.eulerAngles.y; // Adjust real target angle when camera is locked float targetAngle = originalTargetAngle; // When pressing Fire2 (alt) the camera will snap to the target direction real quick. // It will stop snapping when it reaches the target if (Input.GetButton("Fire2")) snap = true; if (snap) { // We are close to the target, so we can stop snapping now! if (AngleDistance (currentAngle, originalTargetAngle) < 3.0) snap = false; currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angleVelocity, snapSmoothLag, snapMaxSpeed); } // Normal camera motion else { if (controller.GetLockCameraTimer () < lockCameraTimeout) { targetAngle = currentAngle; } // Lock the camera when moving backwards! // * It is really confusing to do 180 degree spins when turning around. if (AngleDistance (currentAngle, targetAngle) > 160 && controller.IsMovingBackwards ()) targetAngle += 180; currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angleVelocity, angularSmoothLag, angularMaxSpeed); } // When jumping don't move camera upwards but only down! if (controller.IsJumping ()) { // We'd be moving the camera upwards, do that only if it's really high float newTargetHeight = targetCenter.y + height; if (newTargetHeight < targetHeight || newTargetHeight - targetHeight > 5) targetHeight = targetCenter.y + height; } // When walking always update the target height else { targetHeight = targetCenter.y + height; } // Damp the height float currentHeight = cameraTransform.position.y; currentHeight = Mathf.SmoothDamp (currentHeight, targetHeight, ref heightVelocity, heightSmoothLag); // Convert the angle into a rotation, by which we then reposition the camera Quaternion currentRotation = Quaternion.Euler (0, currentAngle, 0); // Set the position of the camera on the x-z plane to: // distance meters behind the target cameraTransform.position = targetCenter; cameraTransform.position += currentRotation * Vector3.back * distance; // Set the height of the camera cameraTransform.position = new Vector3(cameraTransform.position.x,currentHeight,cameraTransform.position.z); // Always look at the target SetUpRotation(targetCenter, targetHead); } void LateUpdate () { Apply (transform, Vector3.zero); } void Cut (Transform dummyTarget , Vector3 dummyCenter) { float oldHeightSmooth = heightSmoothLag; float oldSnapMaxSpeed = snapMaxSpeed; float oldSnapSmooth = snapSmoothLag; snapMaxSpeed = 10000; snapSmoothLag = 0.001f; heightSmoothLag = 0.001f; snap = true; Apply (transform, Vector3.zero); heightSmoothLag = oldHeightSmooth; snapMaxSpeed = oldSnapMaxSpeed; snapSmoothLag = oldSnapSmooth; } void SetUpRotation (Vector3 centerPos,Vector3 headPos) { // Now it's getting hairy. The devil is in the details here, the big issue is jumping of course. // * When jumping up and down we don't want to center the guy in screen space. // This is important to give a feel for how high you jump and avoiding large camera movements. // // * At the same time we dont want him to ever go out of screen and we want all rotations to be totally smooth. // // So here is what we will do: // // 1. We first find the rotation around the y axis. Thus he is always centered on the y-axis // 2. When grounded we make him be centered // 3. When jumping we keep the camera rotation but rotate the camera to get him back into view if his head is above some threshold // 4. When landing we smoothly interpolate towards centering him on screen Vector3 cameraPos = cameraTransform.position; Vector3 offsetToCenter = centerPos - cameraPos; // Generate base rotation only around y-axis Quaternion yRotation = Quaternion.LookRotation(new Vector3(offsetToCenter.x, 0, offsetToCenter.z)); Vector3 relativeOffset = Vector3.forward * distance + Vector3.down * height; cameraTransform.rotation = yRotation * Quaternion.LookRotation(relativeOffset); // Calculate the projected center position and top position in world space Ray centerRay = cameraTransform.camera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 1f)); Ray topRay = cameraTransform.camera.ViewportPointToRay(new Vector3(0.5f, clampHeadPositionScreenSpace, 1f)); Vector3 centerRayPos = centerRay.GetPoint(distance); Vector3 topRayPos = topRay.GetPoint(distance); float centerToTopAngle = Vector3.Angle(centerRay.direction, topRay.direction); float heightToAngle = centerToTopAngle / (centerRayPos.y - topRayPos.y); float extraLookAngle = heightToAngle * (centerRayPos.y - centerPos.y); if (extraLookAngle < centerToTopAngle) { extraLookAngle = 0; } else { extraLookAngle = extraLookAngle - centerToTopAngle; cameraTransform.rotation *= Quaternion.Euler(-extraLookAngle, 0, 0); } } Vector3 GetCenterOffset () { return centerOffset; } }
最后一个是ThirdPersonController我们同样把它修改成C#语言,它主要更新第三人称视角控制主角时播放的各种动画,主角移动,等等。
ThirdPersonController.cs
[代码]c#/cpp/oc代码:
using UnityEngine; using System.Collections; /** * @Author : www.xuanyusong.com */ [RequireComponent(typeof(CharacterController))] public class ThirdPersonController : MonoBehaviour { public AnimationClip idleAnimation ; public AnimationClip walkAnimation ; public AnimationClip runAnimation ; public AnimationClip jumpPoseAnimation; public float walkMaxAnimationSpeed = 0.75f; public float trotMaxAnimationSpeed = 1.0f; public float runMaxAnimationSpeed = 1.0f; public float jumpAnimationSpeed = 1.15f; public float landAnimationSpeed = 1.0f; private Animation _animation; enum CharacterState { Idle = 0, Walking = 1, Trotting = 2, Running = 3, Jumping = 4, } private CharacterState _characterState; // The speed when walking float walkSpeed = 2.0f; // after trotAfterSeconds of walking we trot with trotSpeed float trotSpeed = 4.0f; // when pressing "Fire3" button (cmd) we start running float runSpeed = 6.0f; float inAirControlAcceleration = 3.0f; // How high do we jump when pressing jump and letting go immediately float jumpHeight = 0.5f; // The gravity for the character float gravity = 20.0f; // The gravity in controlled descent mode float speedSmoothing = 10.0f; float rotateSpeed = 500.0f; float trotAfterSeconds = 3.0f; bool canJump = true; private float jumpRepeatTime = 0.05f; private float jumpTimeout = 0.15f; private float groundedTimeout = 0.25f; // The camera doesnt start following the target immediately but waits for a split second to avoid too much waving around. private float lockCameraTimer = 0.0f; // The current move direction in x-z private Vector3 moveDirection = Vector3.zero; // The current vertical speed private float verticalSpeed = 0.0f; // The current x-z move speed private float moveSpeed = 0.0f; // The last collision flags returned from controller.Move private CollisionFlags collisionFlags; // Are we jumping? (Initiated with jump button and not grounded yet) private bool jumping = false; private bool jumpingReachedApex = false; // Are we moving backwards (This locks the camera to not do a 180 degree spin) private bool movingBack = false; // Is the user pressing any keys? private bool isMoving = false; // When did the user start walking (Used for going into trot after a while) private float walkTimeStart = 0.0f; // Last time the jump button was clicked down private float lastJumpButtonTime = -10.0f; // Last time we performed a jump private float lastJumpTime = -1.0f; // the height we jumped from (Used to determine for how long to apply extra jump power after jumping.) private float lastJumpStartHeight = 0.0f; private Vector3 inAirVelocity = Vector3.zero; private float lastGroundedTime = 0.0f; private bool isControllable = true; void Awake () { moveDirection = transform.TransformDirection(Vector3.forward); _animation = GetComponent(); if(!_animation) Debug.Log("The character you would like to control doesn't have animations. Moving her might look weird."); /* public var idleAnimation : AnimationClip; public var walkAnimation : AnimationClip; public var runAnimation : AnimationClip; public var jumpPoseAnimation : AnimationClip; */ if(!idleAnimation) { _animation = null; Debug.Log("No idle animation found. Turning off animations."); } if(!walkAnimation) { _animation = null; Debug.Log("No walk animation found. Turning off animations."); } if(!runAnimation) { _animation = null; Debug.Log("No run animation found. Turning off animations."); } if(!jumpPoseAnimation && canJump) { _animation = null; Debug.Log("No jump animation found and the character has canJump enabled. Turning off animations."); } } void UpdateSmoothedMovementDirection () { Transform cameraTransform = Camera.main.transform; bool grounded = IsGrounded(); // Forward vector relative to the camera along the x-z plane Vector3 forward = cameraTransform.TransformDirection(Vector3.forward); forward.y = 0; forward = forward.normalized; // Right vector relative to the camera // Always orthogonal to the forward vector Vector3 right = new Vector3(forward.z, 0, -forward.x); float v = Input.GetAxisRaw("Vertical"); float h = Input.GetAxisRaw("Horizontal"); // Are we moving backwards or looking backwards if (v < -0.2f) movingBack = true; else movingBack = false; bool wasMoving = isMoving; isMoving = Mathf.Abs (h) > 0.1f || Mathf.Abs (v) > 0.1f; // Target direction relative to the camera Vector3 targetDirection = h * right + v * forward; // Grounded controls if (grounded) { // Lock camera for short period when transitioning moving & standing still lockCameraTimer += Time.deltaTime; if (isMoving != wasMoving) lockCameraTimer = 0.0f; // We store speed and direction seperately, // so that when the character stands still we still have a valid forward direction // moveDirection is always normalized, and we only update it if there is user input. if (targetDirection != Vector3.zero) { // If we are really slow, just snap to the target direction if (moveSpeed < walkSpeed * 0.9f && grounded) { moveDirection = targetDirection.normalized; } // Otherwise smoothly turn towards it else { moveDirection = Vector3.RotateTowards(moveDirection, targetDirection, rotateSpeed * Mathf.Deg2Rad * Time.deltaTime, 1000); moveDirection = moveDirection.normalized; } } // Smooth the speed based on the current target direction float curSmooth = speedSmoothing * Time.deltaTime; // Choose target speed //* We want to support analog input but make sure you cant walk faster diagonally than just forward or sideways float targetSpeed = Mathf.Min(targetDirection.magnitude, 1.0f); _characterState = CharacterState.Idle; // Pick speed modifier if (Input.GetKey (KeyCode.LeftShift) | Input.GetKey (KeyCode.RightShift)) { targetSpeed *= runSpeed; _characterState = CharacterState.Running; } else if (Time.time - trotAfterSeconds > walkTimeStart) { targetSpeed *= trotSpeed; _characterState = CharacterState.Trotting; } else { targetSpeed *= walkSpeed; _characterState = CharacterState.Walking; } moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth); // Reset walk time start when we slow down if (moveSpeed < walkSpeed * 0.3f) walkTimeStart = Time.time; } // In air controls else { // Lock camera while in air if (jumping) lockCameraTimer = 0.0f; if (isMoving) inAirVelocity += targetDirection.normalized * Time.deltaTime * inAirControlAcceleration; } } void ApplyJumping () { // Prevent jumping too fast after each other if (lastJumpTime + jumpRepeatTime > Time.time) return; if (IsGrounded()) { // Jump // - Only when pressing the button down // - With a timeout so you can press the button slightly before landing if (canJump && Time.time < lastJumpButtonTime + jumpTimeout) { verticalSpeed = CalculateJumpVerticalSpeed (jumpHeight); SendMessage("DidJump", SendMessageOptions.DontRequireReceiver); } } } void ApplyGravity () { if (isControllable) // don't move player at all if not controllable. { // Apply gravity bool jumpButton = Input.GetButton("Jump"); // When we reach the apex of the jump we send out a message if (jumping && !jumpingReachedApex && verticalSpeed <= 0.0f) { jumpingReachedApex = true; SendMessage("DidJumpReachApex", SendMessageOptions.DontRequireReceiver); } if (IsGrounded ()) verticalSpeed = 0.0f; else verticalSpeed -= gravity * Time.deltaTime; } } float CalculateJumpVerticalSpeed (float targetJumpHeight) { // From the jump height and gravity we deduce the upwards speed // for the character to reach at the apex. return Mathf.Sqrt(2 * targetJumpHeight * gravity); } void DidJump () { jumping = true; jumpingReachedApex = false; lastJumpTime = Time.time; lastJumpStartHeight = transform.position.y; lastJumpButtonTime = -10; _characterState = CharacterState.Jumping; } void Update() { if (!isControllable) { // kill all inputs if not controllable. Input.ResetInputAxes(); } if (Input.GetButtonDown ("Jump")) { lastJumpButtonTime = Time.time; } UpdateSmoothedMovementDirection(); // Apply gravity // - extra power jump modifies gravity // - controlledDescent mode modifies gravity ApplyGravity (); // Apply jumping logic ApplyJumping (); // Calculate actual motion Vector3 movement = moveDirection * moveSpeed + new Vector3 (0, verticalSpeed, 0) + inAirVelocity; movement *= Time.deltaTime; // Move the controller CharacterController controller = GetComponent (); collisionFlags = controller.Move(movement); // ANIMATION sector if(_animation) { if(_characterState == CharacterState.Jumping) { if(!jumpingReachedApex) { _animation[jumpPoseAnimation.name].speed = jumpAnimationSpeed; _animation[jumpPoseAnimation.name].wrapMode = WrapMode.ClampForever; _animation.CrossFade(jumpPoseAnimation.name); } else { _animation[jumpPoseAnimation.name].speed = -landAnimationSpeed; _animation[jumpPoseAnimation.name].wrapMode = WrapMode.ClampForever; _animation.CrossFade(jumpPoseAnimation.name); } } else { if(controller.velocity.sqrMagnitude < 0.1f) { _animation.CrossFade(idleAnimation.name); } else { if(_characterState == CharacterState.Running) { _animation[runAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0f, runMaxAnimationSpeed); _animation.CrossFade(runAnimation.name); } else if(_characterState == CharacterState.Trotting) { _animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0f, trotMaxAnimationSpeed); _animation.CrossFade(walkAnimation.name); } else if(_characterState == CharacterState.Walking) { _animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0f, walkMaxAnimationSpeed); _animation.CrossFade(walkAnimation.name); } } } } // ANIMATION sector // Set rotation to the move direction if (IsGrounded()) { transform.rotation = Quaternion.LookRotation(moveDirection); } else { Vector3 xzMove = movement; xzMove.y = 0; if (xzMove.sqrMagnitude > 0.001f) { transform.rotation = Quaternion.LookRotation(xzMove); } } // We are in jump mode but just became grounded if (IsGrounded()) { lastGroundedTime = Time.time; inAirVelocity = Vector3.zero; if (jumping) { jumping = false; SendMessage("DidLand", SendMessageOptions.DontRequireReceiver); } } } void OnControllerColliderHit (ControllerColliderHit hit ) { // Debug.DrawRay(hit.point, hit.normal); if (hit.moveDirection.y > 0.01f) return; } float GetSpeed () { return moveSpeed; } public bool IsJumping () { return jumping; } bool IsGrounded () { return (collisionFlags & CollisionFlags.CollidedBelow) != 0; } Vector3 GetDirection () { return moveDirection; } public bool IsMovingBackwards () { return movingBack; } public float GetLockCameraTimer () { return lockCameraTimer; } bool IsMoving () { return Mathf.Abs(Input.GetAxisRaw("Vertical")) + Mathf.Abs(Input.GetAxisRaw("Horizontal")) > 0.5f; } bool HasJumpReachedApex () { return jumpingReachedApex; } bool IsGroundedWithTimeout () { return lastGroundedTime + groundedTimeout > Time.time; } void Reset () { gameObject.tag = "Player"; } }
最后我们用修改的脚本来控制第三人称视角主角的移动,直接上图大家仔细看看监测面板视图中的脚本绑定,这里问题不大,图片如果看不清点击即可查看大图。
脚本终于全部翻译完毕,其实在开发中大家可以去丰富这些脚本,灵活的运用它们。感谢Unity圣典的站长专门花钱买模型让我来写教程与录制视频教程。
下回写点游戏AI的东西,让游戏中的怪物在更加聪明点吧。