本文包含内容:
- 类似黑魂的角色控制器(适配鼠标和手柄)
- 以及第三人称相机
参考教程链接: Unity从零开始制作魂类游戏 ?该文章为教程的P1、P2内容
代码架构
?由一个InputSystem PlayerControl和 四个Scripts InputHandle、AnimatorHandle、PlayerLocomotion、CameraHandle组成
PlayerControl(InputSystem):负责处理存储键鼠、手柄的输入。
InputHandle:处理PlayerControl里的数据,把InputSystem里的数据类型转化为float、Vector2这种常用、直接的数据类型。
AnimatorHandle:负责给animator传入数值,改变动画状态
PlayerLocomotion:负责Player的rigidbody.velocity,以及transform.rotation
CameraHandle:负责控制相机的跟随,旋转。
InputHandle处理完输入数据后,其他脚本进行计算和封装。Player的相关函数在PlayerLocomotion的Update中统一执行,CameraHandle则在InputSystem中执行。
?
代码解读
InputSystem 分析
??在PlayerControl中,有Movement控制角色的移动输入,Camera控制相机的旋转输入。 ??在InputHandle对这两个Action进行处理,加工成float、Vector2类型,具体如下:
inputActions.PlayerMovement.Movement.performed += inputActions => movementInput = inputActions.ReadValue<Vector2>();
inputActions.PlayerMovement.Camera.performed += i => cameraInput = i.ReadValue<Vector2>();
👆将PlayerControl中的Movement传给Vector2 movementInput,把Camera传给Vector2 cameraInput中。
private void MoveInput(float delta)
{
horizontal = movementInput.x;
vertical = movementInput.y;
moveAmount = Mathf.Clamp01(Mathf.Abs(horizontal) + Mathf.Abs(vertical));
mouseX = cameraInput.x;
mouseY = cameraInput.y;
}
👆然后在MoveInput中再转化成x轴移动输入horizontal,y轴移动输入vertical,移动速度moveAmount(因为手柄的摇杆输入是有轻按的,可能会小于1,这时候就要让Player用walk动画而非run)。 同理把cameraInput转化成相机的x轴旋转输入mouseX,y轴移动输入mouseY。
Player移动部分分析
我们改变的所有rigidbody、rotation都是在外层的gameobject上改动的,内层模型的位置、旋转等始终不发生变化,只负责动画的播放。
moveDirection = cameraObject.forward * inputHandle.vertical;
moveDirection += cameraObject.right * inputHandle.horizontal;
moveDirection.y = 0.0f;
moveDirection.Normalize();
moveDirection *= speed;
👆用cameraObject.forward等求出Player的移动方向,乘以speed得出位移。关于Transform.forward的用法可以看看这篇文章。 简单来说就以相机的世界坐标为坐标系,得到相机视角下的前后、左右方向,但因为相机会有一些上下倾斜,所以要对y轴归零,最后归一化再乘以speed,就得到一个长度为speed的Vector2位移了。 至于后面的那句 Vector3 projectedVelocity = Vector3.ProjectOnPlane(moveDirection, normalVector); 作用是投影,可能后面才会用到? 最后把求到的Velocity 赋给rigidbody就可以了。
Player旋转处理
private void HandleRotation(float delta)
{
Vector3 targetDir = Vector3.zero;
float moveOverride = inputHandle.moveAmount;
targetDir = cameraObject.forward * inputHandle.vertical;
targetDir += cameraObject.right * inputHandle.horizontal;
targetDir.Normalize();
targetDir.y = 0;
if (targetDir == Vector3.zero)
targetDir = myTransform.forward;
float rs = rotationSpeed;
Quaternion tr = Quaternion.LookRotation(targetDir);
Quaternion targetRotation = Quaternion.Slerp(myTransform.rotation, tr, rs * delta);
myTransform.rotation = targetRotation;
}
首先计算出移动的方向,使用LookRotation函数求出四元数形式的朝向,再用slerp做球形插值。
动画部分分析
public void UpdateAnimatorValue(float verticalMovement,float horizontalMovement)
{
#region Vertical
float v = 0;
if(verticalMovement > 0 && verticalMovement < 0.55f)
{
v = 0.5f;
}
else if(verticalMovement > 0.55f)
{
v = 1;
}
else
{
v = 0;
}
}
👆使用上一步求出的moveAmount,分三级分别播放run、walk、idle动画,反应到手柄上就是轻按摇杆为walk,按到底是run。
相机跟随旋转分析
相机的transform,外层CameraHolder负责相机位置和Y轴旋转,中间的CameraPivot负责X轴旋转,内层Camera本身不受输入影响。
public void FollowTarget(float delta)
{
Vector3 targetPosition = Vector3.Lerp(myTransform.position, targetTransform.position, delta / followSpeed);
myTransform.position = targetTransform.position;
}
👆外层CameraHolder初始位置为(0,0,0),与Player相同,之后每一帧也跟着Player移动,做到跟随效果。
public void HandleCameraRotation(float delta,float mouseXInput,float mouseYInput)
{
lookAngle += (mouseXInput * lookSpeed) / delta;
Vector3 rotation = Vector3.zero;
rotation.y = lookAngle;
Quaternion targetRotation = Quaternion.Euler(rotation);
myTransform.rotation = targetRotation;
}
用lookAngle存储y轴旋转度数,每次赋值给rotation.y,再赋值给CameraHolder.rotation
public void HandleCameraRotation(float delta,float mouseXInput,float mouseYInput)
{
pivotAngle -= (mouseYInput * pivotSpeed) / delta;
pivotAngle = Mathf.Clamp(pivotAngle, minimumPivot, maximumPivot);
rotation = Vector3.zero;
rotation.x = pivotAngle;
targetRotation = Quaternion.Euler(rotation);
cameraPivotTransform.localRotation = targetRotation;
}
z轴同理,不过要加一个clamp()函数,防止出现过高、过低的角度。
?
结果
源代码
PlayerControl
注意Movement和Camera的Action Type设置成Pass Through,Vector2。
InputHandle
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace ASH {
public class InputHandle : MonoBehaviour
{
public float horizontal, vertical,moveAmount,mouseX,mouseY;
PlayerControl inputActions;
CameraHandle cameraHandler;
Vector2 movementInput, cameraInput;
private void Awake()
{
cameraHandler = CameraHandle.singleton;
}
private void FixedUpdate()
{
float delta = Time.deltaTime;
if(cameraHandler != null)
{
cameraHandler.FollowTarget(delta);
cameraHandler.HandleCameraRotation(delta, mouseX, mouseY);
}
}
public void OnEnable()
{
if(inputActions == null)
{
inputActions = new PlayerControl();
inputActions.PlayerMovement.Movement.performed += inputActions => movementInput = inputActions.ReadValue<Vector2>();
inputActions.PlayerMovement.Camera.performed += i => cameraInput = i.ReadValue<Vector2>();
}
inputActions.Enable();
}
private void OnDisable()
{
inputActions.Disable();
}
public void TickInput(float delta)
{
MoveInput(delta);
}
private void MoveInput(float delta)
{
horizontal = movementInput.x;
vertical = movementInput.y;
moveAmount = Mathf.Clamp01(Mathf.Abs(horizontal) + Mathf.Abs(vertical));
mouseX = cameraInput.x;
mouseY = cameraInput.y;
}
}
}
AnimatorHandle
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace ASH
{
public class AnimatorHandle : MonoBehaviour
{
public Animator anim;
int vertical, horizontal;
public bool canRotate;
public void Initialize()
{
anim = GetComponent<Animator>();
vertical = Animator.StringToHash("Vertical");
horizontal = Animator.StringToHash("Horizontal");
}
public void UpdateAnimatorValue(float verticalMovement,float horizontalMovement)
{
#region Vertical
float v = 0;
if(verticalMovement > 0 && verticalMovement < 0.55f)
{
v = 0.5f;
}
else if(verticalMovement > 0.55f)
{
v = 1;
}
else if(verticalMovement < 0 && verticalMovement > -0.55f)
{
v = -0.5f;
}
else if(verticalMovement < -0.55f)
{
v = -1;
}
else
{
v = 0;
}
#endregion
#region Horizontal
float h = 0;
if(horizontalMovement > 0 && horizontalMovement < 0.55f)
{
h = 0.5f;
}
else if(horizontalMovement > 0.55f)
{
h = 1;
}
else if(horizontalMovement < 0&& horizontalMovement > -0.55f)
{
h = -0.5f;
}
else if(horizontalMovement < -0.55f)
{
h = -1;
}
else
{
h = 0;
}
#endregion
anim.SetFloat(vertical, v, 0.1f, Time.deltaTime);
anim.SetFloat(horizontal, h, 0.1f, Time.deltaTime);
}
public void CanRotate()
{
canRotate = true;
}
public void StopRotation()
{
canRotate = false;
}
}
}
PlayerLocomotion
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace ASH
{
public class PlayerLocomotion : MonoBehaviour
{
Transform cameraObject;
InputHandle inputHandle;
Vector3 moveDirection;
[HideInInspector]
public Transform myTransform;
[HideInInspector]
public AnimatorHandle animatorHandle;
public new Rigidbody rigidbody;
public GameObject normalCamera;
[Header("Stats")]
[SerializeField] float moveSpeed = 5.0f;
[SerializeField] float rotationSpeed = 10.0f;
void Start()
{
rigidbody = GetComponent<Rigidbody>();
inputHandle = GetComponent<InputHandle>();
animatorHandle = GetComponentInChildren<AnimatorHandle>();
cameraObject = Camera.main.transform;
myTransform = transform;
animatorHandle.Initialize();
}
private void Update()
{
float delta = Time.deltaTime;
inputHandle.TickInput(delta);
moveDirection = cameraObject.forward * inputHandle.vertical;
moveDirection += cameraObject.right * inputHandle.horizontal;
moveDirection.y = 0.0f;
moveDirection.Normalize();
float speed = moveSpeed;
moveDirection *= speed;
Vector3 projectedVelocity = Vector3.ProjectOnPlane(moveDirection, normalVector);
rigidbody.velocity = moveDirection;
animatorHandle.UpdateAnimatorValue(inputHandle.moveAmount, 0);
if(animatorHandle.canRotate)
{
HandleRotation(delta);
}
}
#region movement
Vector3 normalVector;
Vector3 targetPositon;
private void HandleRotation(float delta)
{
Vector3 targetDir = Vector3.zero;
float moveOverride = inputHandle.moveAmount;
targetDir = cameraObject.forward * inputHandle.vertical;
targetDir += cameraObject.right * inputHandle.horizontal;
targetDir.Normalize();
targetDir.y = 0;
if (targetDir == Vector3.zero)
targetDir = myTransform.forward;
float rs = rotationSpeed;
Quaternion tr = Quaternion.LookRotation(targetDir);
Quaternion targetRotation = Quaternion.Slerp(myTransform.rotation, tr, rs * delta);
myTransform.rotation = targetRotation;
}
#endregion
}
}
CameraHandle
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace ASH
{
public class CameraHandle : MonoBehaviour
{
public Transform targetTransform,
cameraTransform,
cameraPivotTransform;
private Transform myTransform;
private Vector3 cameraTransformPosition;
private LayerMask ignoreLayers;
public static CameraHandle singleton;
public float lookSpeed = 0.1f, followSpeed = 0.1f, pivotSpeed = 0.03f;
private float defaultPosition,lookAngle,pivotAngle;
public float minimumPivot = -35;
public float maximumPivot = 35;
private void Awake()
{
singleton = this;
myTransform = transform;
defaultPosition = cameraTransform.localPosition.z;
ignoreLayers = ~(1 << 8 | 1 << 9 | 1 << 10);
}
public void FollowTarget(float delta)
{
Vector3 targetPosition = Vector3.Lerp(myTransform.position, targetTransform.position, delta / followSpeed);
myTransform.position = targetTransform.position;
}
public void HandleCameraRotation(float delta,float mouseXInput,float mouseYInput)
{
lookAngle += (mouseXInput * lookSpeed) / delta;
Vector3 rotation = Vector3.zero;
rotation.y = lookAngle;
Quaternion targetRotation = Quaternion.Euler(rotation);
myTransform.rotation = targetRotation;
pivotAngle -= (mouseYInput * pivotSpeed) / delta;
pivotAngle = Mathf.Clamp(pivotAngle, minimumPivot, maximumPivot);
rotation = Vector3.zero;
rotation.x = pivotAngle;
targetRotation = Quaternion.Euler(rotation);
cameraPivotTransform.localRotation = targetRotation;
}
}
}
|