本篇文章主要列举了第三人称的多种控制方式。
一、官方实例的第三人称控制方式。
该控制方式比较复杂,但是却写得很好很完善,并且运用了新的动画系统。大家可以下载官方的角色控制包来使用,附上图一张,不多说。
二、老版官方的第三人称控制方式。
大家应该知道老版的第三人称控制方式是用JavaScript脚本写的,可能大家拿过来还不太好用,但是这里我们把它改写成C#脚本(PS:参照雨松的修改),这样用起来就方便多了,而且用的是经典版的动画系统,满足了很多人的需求。
在unity中,新版的mecanim动画系统出现,虽然说很实用,在某些方面解决了很多人的需求,但这并不意味着可以替代原版经典的动画系统,所以到现在为止,两种动画都是通用的。
using UnityEngine;
using System.Collections;
[RequireComponent(typeof(CharacterController))]
public class ThirdPersonController111 : 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<Animation>();
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<CharacterController>();
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";
}
}
图一张:
三、根据需求,自己写自己需要的控制方式。
在本期训练营中,主角超级玛丽我才用了一种比较简洁的控制方式,因为这种方式已经能够满足需求,该种方式就是前后左右移动的方式。该方式不需要添加charactercontroller,只需添加胶囊体就可。(PS:不过该方式有个缺点就是必须朝向固定,也就是只能朝向Z轴正方向)
代码如下:
using UnityEngine;
using System.Collections;
public class MarioMove : MonoBehaviour
{
public float speed = 5.0f;
public static bool isGround;
public static bool IsAllowJump;
[SerializeField]
float m_StationaryTurnSpeed = 180;
[SerializeField]
float m_MovingTurnSpeed = 360;
float m_ForwardAmount;
float m_TurnAmount;
Vector3 m_GroundNormal;
private Vector3 m_Move;
private Transform m_Cam;
private Vector3 m_CamForward;
// Use this for initialization
void Start()
{
// get the transform of the main camera
if (Camera.main != null)
{
m_Cam = Camera.main.transform;
}
else
{
Debug.LogWarning(
"Warning: no main camera found. Third person character needs a Camera tagged \"MainCamera\", for camera-relative controls.");
// we use self-relative controls in this case, which probably isn't what the user wants, but hey, we warned them!
}
}
void OnCollisionEnter(Collision collision)
{
//if (collision.collider.tag == "Ground")
if (collision.collider.tag != null)
{
isGround = true;
IsAllowJump = true;
}
else
{
isGround = false;
}
}
// Update is called once per frame
void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
GetComponent<Rigidbody>().MovePosition(transform.position - new Vector3(h, 0, v) * speed * Time.deltaTime);
if (isGround == true && Input.GetButton("Jump"))
{
if (IsAllowJump == true)
{
transform.GetComponentInChildren<Animation>().CrossFade("jump");
GetComponent<Rigidbody>().MovePosition(transform.position - new Vector3(-h * 0.1f, -0.15f, -v * 0.1f));
}
}
else if (Input.GetButtonUp("Jump"))
{
IsAllowJump = false;
}
else
{
if (Input.GetAxis("Vertical") > 0.5f ||
Input.GetAxis("Vertical") < -0.5f ||
Input.GetAxis("Horizontal") > 0.5f ||
Input.GetAxis("Horizontal") < -0.5f)
{
transform.GetComponentInChildren<Animation>().CrossFade("run");
}
else if ((Input.GetAxis("Vertical") > 0.0f && Input.GetAxis("Vertical") < 0.5f) ||
(Input.GetAxis("Vertical") > -0.5f && Input.GetAxis("Vertical") < 0.0f) ||
(Input.GetAxis("Horizontal") > 0.0f && Input.GetAxis("Horizontal") < 0.5f) ||
(Input.GetAxis("Horizontal") < 0.0f && Input.GetAxis("Horizontal") > -0.5f))
{
transform.GetComponentInChildren<Animation>().CrossFade("walk");
}
else
{
transform.GetComponentInChildren<Animation>().CrossFade("idle");
}
}
if (m_Cam != null)
{
// calculate camera relative direction to move:
m_CamForward = Vector3.Scale(m_Cam.forward, new Vector3(1, 0, 1)).normalized;
m_Move = v * m_CamForward + h * m_Cam.right;
}
else
{
// we use world-relative directions in the case of no main camera
m_Move = v * Vector3.forward + h * Vector3.right;
}
Move(m_Move);
}
public void Move(Vector3 move)
{
// convert the world relative moveInput vector into a local-relative
// turn amount and forward amount required to head in the desired
// direction.
if (move.magnitude > 1f) move.Normalize();
move = transform.InverseTransformDirection(move);
//CheckGroundStatus();
move = Vector3.ProjectOnPlane(move, m_GroundNormal);
m_TurnAmount = Mathf.Atan2(move.x, move.z);
m_ForwardAmount = move.z;
ApplyExtraTurnRotation();
}
void ApplyExtraTurnRotation()
{
// help the character turn faster (this is in addition to root rotation in the animation)
float turnSpeed = Mathf.Lerp(m_StationaryTurnSpeed, m_MovingTurnSpeed, m_ForwardAmount);
transform.Rotate(0, m_TurnAmount * turnSpeed * Time.deltaTime, 0);
}
}
在跳跃的代码部分,这样写的目的是实现了按跳跃键的时间长短跳的高度不同,和大家小时候玩的超级玛丽游戏的感觉很像。
附图一张: