# 观前提醒
项目会在 GitHub 中开源,链接:https://github.com/Maikire/Unity/tree/main/UnityFramework/A simple ARPG character framework
# 框架设计
- 角色状态类:角色的基础信息和基础行为
- 角色的动画:因为动画参数的名字可能会改变,所以单独做一个类储存名字
- 角色的移动方式:提供移动的方法,由输入控制或技能系统调用
- 输入控制:获取玩家的输入

# 代码
# 动画信息类
储存动画的名字
using System; | |
using UnityEngine; | |
namespace CharacterSystem | |
{ | |
// 可序列化,将当前对象 “嵌入” 到脚本后,可以在编辑器中显示属性 | |
// 对应的,这将生成一个对象,不需要手动 new AnimationParameter () | |
/// <summary> | |
/// AnimationParameter | |
/// </summary> | |
[Serializable] | |
public class AnimationParameter | |
{ | |
[Tooltip("走路")] | |
public string Walk = "Walk"; | |
[Tooltip("跑步")] | |
public string Run = "Run"; | |
[Tooltip("跳跃")] | |
public string Jump = "Jump"; | |
[Tooltip("二次跳跃")] | |
public string DoubleJump = "DoubleJump"; | |
[Tooltip("滑行")] | |
public string Slide = "Slide"; | |
[Tooltip("贴墙跳跃")] | |
public string WallJump = "WallJump"; | |
[Tooltip("坠落")] | |
public string Fall = "Fall"; | |
[Tooltip("冲刺")] | |
public string Dash = "Dash"; | |
[Tooltip("闲置")] | |
public string Idle = "Idle"; | |
[Tooltip("受伤")] | |
public string Injure = "Injure"; | |
[Tooltip("死亡")] | |
public string Die = "Die"; | |
[Tooltip("复活")] | |
public string Revive = "Revive"; | |
} | |
} |
# 角色状态类
基础信息和基础行为
using UnityEngine; | |
namespace CharacterSystem | |
{ | |
/// <summary> | |
/// CharacterStatus | |
/// </summary> | |
public abstract class CharacterStatus : MonoBehaviour | |
{ | |
[Tooltip("动画信息")] | |
public AnimationParameter PlayerAnimationParameter; | |
[Tooltip("最大生命值")] | |
public float MaxHP = 500; | |
[Tooltip("生命值")] | |
public float HP = 500; | |
[Tooltip("最大魔力")] | |
public float MaxMP = 500; | |
[Tooltip("魔力")] | |
public float MP = 500; | |
[Tooltip("防御力")] | |
public float Defense = 5; | |
[Tooltip("攻击力")] | |
public float AttackPower = 10; | |
/// <summary> | |
///true: 死亡 | |
/// </summary> | |
protected bool isDie = false; | |
public void Start() | |
{ | |
HP = MaxHP; | |
MP = MaxMP; | |
} | |
/// <summary> | |
/// 受伤 | |
/// </summary> | |
/// <param name="damage"> 伤害值 & lt;/param> | |
public virtual void Damage(float damage) | |
{ | |
if (isDie) return; | |
float temp = damage - Defense; | |
if (temp > 0) | |
{ | |
HP -= temp; | |
} | |
if (HP <= 0) | |
{ | |
Die(); | |
} | |
} | |
/// <summary> | |
/// 死亡 | |
/// </summary> | |
public virtual void Die() | |
{ | |
isDie = true; | |
} | |
} | |
} |
# 角色的移动方式
- 在这里实现真正让角色移动的方法,实现各种移动模式的物理和运动计算
- 由输入系统和技能系统控制 能不能移动
提供移动的方法
using UnityEngine; | |
namespace CharacterSystem | |
{ | |
/// <summary> | |
/// 移动 | |
/// </summary> | |
[RequireComponent(typeof(CharacterController))] | |
public class PlayerMovement : MonoBehaviour | |
{ | |
[Tooltip("移动速度")] | |
public float moveSpeed = 4f; | |
[Tooltip("跳跃高度")] | |
public float jumpHeight = 1f; | |
[Tooltip("旋转速度")] | |
public float rotationSpeed = 15; | |
[Tooltip("重力")] | |
public float gravity = -30f; | |
[Tooltip("地面检测半径")] | |
public float groundCheckRadius = 0.3f; | |
[Tooltip("地面检测图层")] | |
public LayerMask groundLayer = ~0; | |
[Tooltip("允许的最大跳跃次数")] | |
public int maxJumpCount = 2; | |
/// <summary> | |
/// 当前跳跃次数 | |
/// </summary> | |
private int jumpCount = 0; | |
/// <summary> | |
/// 是否在地面上 | |
/// </summary> | |
public bool IsGrounded { get { return isGrounded; } } | |
/// <summary> | |
/// 是否在地面上 | |
/// </summary> | |
private bool isGrounded; | |
/// <summary> | |
/// 待处理的移动输入 | |
/// </summary> | |
private Vector3 pendingMoveInput; | |
/// <summary> | |
/// 速度 | |
/// </summary> | |
private Vector3 velocity; | |
private CharacterController controller; | |
private Transform cameraTransform; | |
private void Start() | |
{ | |
controller = this.GetComponent<CharacterController>(); | |
cameraTransform = this.GetComponentInChildren<Camera>().transform; | |
rotationSpeed *= 10; | |
velocity.y = -10f; | |
} | |
private void Update() | |
{ | |
CheckGrounded(); | |
ApplyGravity(); | |
ApplyMovement(); | |
pendingMoveInput = Vector3.zero; | |
} | |
/// <summary> | |
/// 移动角色 | |
/// </summary> | |
/// <param name="direction"></param> | |
public void Move(Vector3 direction) | |
{ | |
pendingMoveInput = Vector3.ClampMagnitude(direction, 1f); | |
} | |
/// <summary> | |
/// 旋转角色和摄像机 | |
/// </summary> | |
/// <param name="direction"></param> | |
public void Look(Vector3 direction) | |
{ | |
if (direction.sqrMagnitude > 1f) | |
{ | |
direction.Normalize(); | |
} | |
Vector3 targetRotY = this.transform.eulerAngles + Vector3.up * direction.y; | |
this.transform.rotation = Quaternion.Slerp( | |
this.transform.rotation, | |
Quaternion.Euler(targetRotY), | |
rotationSpeed * Time.deltaTime | |
); | |
float eulerX = cameraTransform.eulerAngles.x; | |
if (eulerX > 180f) | |
{ | |
eulerX -= 360f; | |
} | |
Vector3 targetRotX = Vector3.right * (eulerX + direction.x) + Vector3.up * cameraTransform.eulerAngles.y; | |
targetRotX.x = Mathf.Clamp(targetRotX.x, -90f, 90f); | |
cameraTransform.rotation = Quaternion.Slerp( | |
cameraTransform.rotation, | |
Quaternion.Euler(targetRotX), | |
rotationSpeed * Time.deltaTime | |
); | |
} | |
/// <summary> | |
/// 旋转视角 | |
/// </summary> | |
/// <param name="direction"></param> | |
public void Look(Vector3 direction, int a = 1) | |
{ | |
// Y 轴 | |
Vector3 targetRotY = this.transform.eulerAngles + Vector3.up * direction.y; | |
this.transform.rotation = Quaternion.Slerp( | |
this.transform.rotation, | |
Quaternion.Euler(targetRotY), | |
rotationSpeed * Time.deltaTime | |
); | |
// X 轴 | |
float ex = cameraTransform.eulerAngles.x; | |
if (ex > 180) | |
{ | |
ex -= 360; | |
} | |
Vector3 targetRotX = Vector3.right * (ex + direction.x) + Vector3.up * cameraTransform.eulerAngles.y; | |
if (targetRotX.x <= -90) | |
{ | |
targetRotX.x = -90; | |
} | |
else if (targetRotX.x >= 90) | |
{ | |
targetRotX.x = 90; | |
} | |
cameraTransform.rotation = Quaternion.Slerp( | |
cameraTransform.rotation, | |
Quaternion.Euler(targetRotX), | |
rotationSpeed * Time.deltaTime | |
); | |
} | |
/// <summary> | |
/// 跳跃 | |
/// </summary> | |
public void Jump() | |
{ | |
if (isGrounded) | |
{ | |
jumpCount = 0; | |
} | |
else if (jumpCount == 0) | |
{ | |
// 如果角色未跳跃且不在地面上(例如走下边缘掉落),消耗掉第一次跳跃 | |
jumpCount = 1; | |
} | |
if (jumpCount < maxJumpCount) | |
{ | |
velocity.y = Mathf.Sqrt(-2f * gravity * jumpHeight); | |
jumpCount++; | |
isGrounded = false; | |
} | |
} | |
/// <summary> | |
/// 检查角色是否在地面上 | |
/// </summary> | |
private void CheckGrounded() | |
{ | |
isGrounded = | |
controller.isGrounded || | |
Physics.CheckSphere(this.transform.position, groundCheckRadius, groundLayer); | |
if (isGrounded && velocity.y < 0f) | |
{ | |
velocity.y = -10f; | |
jumpCount = 0; | |
} | |
} | |
/// <summary> | |
/// 应用重力 | |
/// </summary> | |
private void ApplyGravity() | |
{ | |
if (!isGrounded) | |
{ | |
velocity.y += gravity * Time.deltaTime; | |
} | |
} | |
/// <summary> | |
/// 应用移动输入 | |
/// </summary> | |
private void ApplyMovement() | |
{ | |
Vector3 move = pendingMoveInput; | |
if (move.sqrMagnitude > 0f) | |
{ | |
move = Quaternion.Euler(0f, this.transform.eulerAngles.y, 0f) * move; | |
} | |
velocity.x = move.x * moveSpeed; | |
velocity.z = move.z * moveSpeed; | |
Vector3 displacement = velocity * Time.deltaTime; | |
controller.Move(displacement); | |
} | |
} | |
} |
# 输入控制
处理玩家的输入
using UnityEngine; | |
using UnityEngine.InputSystem; | |
namespace CharacterSystem | |
{ | |
/// <summary> | |
/// 角色控制 | |
/// </summary> | |
[RequireComponent(typeof(PlayerMovement))] | |
public class PlayerInputController : MonoBehaviour | |
{ | |
private PlayerMovement playerMovement; | |
private PlayerInput playerInput; | |
private InputActionAsset actionAsset; | |
private InputActionMap actionMap_Player; | |
private InputActionMap actionMap_UI; | |
private InputAction moveAction; | |
private InputAction lookAction; | |
private InputAction jumpAction; | |
private InputAction escapeAction; | |
protected void Awake() | |
{ | |
playerMovement = this.GetComponent<PlayerMovement>(); | |
playerInput = this.GetComponent<PlayerInput>(); | |
} | |
protected void Start() | |
{ | |
Config(); | |
AddEvent(); | |
} | |
protected void Update() | |
{ | |
if (actionMap_Player.enabled) | |
{ | |
Move(); | |
Look(); | |
} | |
} | |
private void OnDestroy() | |
{ | |
RemoveEvent(); | |
} | |
/// <summary> | |
/// 配置 | |
/// </summary> | |
private void Config() | |
{ | |
actionAsset = playerInput.actions; | |
actionMap_Player = actionAsset.FindActionMap("Player", false); | |
actionMap_UI = actionAsset.FindActionMap("UI", false); | |
foreach (var item in actionAsset.actionMaps) | |
{ | |
item.Disable(); | |
} | |
actionMap_Player.Enable(); | |
//actionMap_UI.Enable(); | |
moveAction = actionMap_Player.FindAction("Move", false); | |
lookAction = actionMap_Player.FindAction("Look", false); | |
jumpAction = actionMap_Player.FindAction("Jump", false); | |
escapeAction = actionMap_Player.FindAction("Escape", false); | |
} | |
/// <summary> | |
/// 添加事件 | |
/// </summary> | |
private void AddEvent() | |
{ | |
jumpAction.performed += OnJump; | |
escapeAction.performed += OnEscape; | |
} | |
private void RemoveEvent() | |
{ | |
jumpAction.performed -= OnJump; | |
escapeAction.performed -= OnEscape; | |
} | |
/// <summary> | |
/// 移动 | |
/// </summary> | |
public void Move() | |
{ | |
Vector2 moveVector = moveAction.ReadValue<Vector2>(); | |
// 如果只需要水平方向的移动,就只取 x 轴 | |
//Vector2 direction = Vector2.right * moveVector; | |
Vector3 direction = new Vector3(moveVector.x, 0, moveVector.y); | |
playerMovement.Move(direction); | |
} | |
/// <summary> | |
/// 旋转视角 | |
/// </summary> | |
/// <param name="context"></param> | |
private void Look() | |
{ | |
Vector2 lookVector = lookAction.ReadValue<Vector2>(); | |
Vector3 direction = new Vector3(-lookVector.y, lookVector.x, 0); | |
direction.Normalize(); | |
playerMovement.Look(direction, 2); | |
} | |
/// <summary> | |
/// 跳跃 | |
/// </summary> | |
/// <param name="context"></param> | |
private void OnJump(InputAction.CallbackContext context) | |
{ | |
playerMovement.Jump(); | |
} | |
/// <summary> | |
/// ESC | |
/// </summary> | |
/// <param name="context"></param> | |
private void OnEscape(InputAction.CallbackContext context) | |
{ | |
CursorController.Instance.Escape(playerInput.currentControlScheme); | |
} | |
} | |
} |
# 玩家状态类
- 继承角色状态类
代码如下:
namespace CharacterSystem | |
{ | |
/// <summary> | |
/// Player | |
/// </summary> | |
public class PlayerStatus : CharacterStatus | |
{ | |
public override void Die() | |
{ | |
base.Die(); | |
} | |
} | |
} |
# 敌人状态类
- 继承角色状态类
代码如下:
using System.Collections; | |
using UnityEngine; | |
namespace CharacterSystem | |
{ | |
/// <summary> | |
/// EnemyStatus | |
/// </summary> | |
public class EnemyStatus : CharacterStatus | |
{ | |
[Tooltip("受伤声音")] | |
public AudioClip HurtAudio; | |
private SpriteRenderer sprite; | |
private AudioSource audioSource; | |
private void Awake() | |
{ | |
sprite = this.GetComponentInChildren<SpriteRenderer>(); | |
audioSource = this.GetComponent<AudioSource>(); | |
} | |
public override void Damage(float damage) | |
{ | |
base.Damage(damage); | |
this.StartCoroutine(Hurt()); | |
} | |
/// <summary> | |
/// Hurt | |
/// </summary> | |
/// <returns></returns> | |
private IEnumerator Hurt() | |
{ | |
sprite.color = new Color(0.5f, 0.5f, 0.5f); | |
audioSource.PlayOneShot(HurtAudio); | |
yield return new WaitForSeconds(0.06f); | |
sprite.color = Color.white; | |
} | |
} | |
} |
# 其他
- 隐藏鼠标指针
代码如下:
using Common; | |
using UnityEngine; | |
namespace CharacterSystem | |
{ | |
/// <summary> | |
/// 鼠标指针控制 | |
/// </summary> | |
public class CursorController : MonoSingleton<CursorController> | |
{ | |
/// <summary> | |
///true: 打开 UI | |
/// </summary> | |
private bool isUI = false; | |
private void Start() | |
{ | |
Cursor.visible = false; | |
Cursor.lockState = CursorLockMode.Locked; | |
} | |
/// <summary> | |
/// ESC | |
/// </summary> | |
/// <param name="currentControlScheme"></param> | |
public void Escape(string currentControlScheme) | |
{ | |
if (currentControlScheme == "Keyboard&Mouse") | |
{ | |
if (isUI) | |
{ | |
Cursor.visible = false; | |
Cursor.lockState = CursorLockMode.Locked; | |
} | |
else | |
{ | |
Cursor.visible = true; | |
Cursor.lockState = CursorLockMode.None; | |
} | |
} | |
isUI = !isUI; | |
} | |
} | |
} |