# 观前提醒

项目会在 GitHub 中开源,链接:https://github.com/Maikire/Unity/tree/main/UnityFramework/A simple ARPG character framework

# 框架设计

  • 角色状态类:角色的基础信息和基础行为
  • 角色的动画:因为动画参数的名字可能会改变,所以单独做一个类储存名字
  • 角色的移动方式:提供移动的方法,由输入控制或技能系统调用
  • 输入控制:获取玩家的输入

img

# 代码

# 动画信息类

储存动画的名字

AnimationParameter
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";
    }
}

# 角色状态类

基础信息和基础行为

CharacterStatus
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;
        }
    }
}

# 角色的移动方式

  • 在这里实现真正让角色移动的方法,实现各种移动模式的物理和运动计算
  • 由输入系统和技能系统控制 能不能移动

提供移动的方法

CharacterMotor
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);
        }
    }
}

# 输入控制

处理玩家的输入

CharacterInputController
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);
        }
    }
}

# 玩家状态类

  • 继承角色状态类

代码如下:

PlayerStatus
namespace CharacterSystem
{
    /// <summary>
    /// Player
    /// </summary>
    public class PlayerStatus : CharacterStatus
    {
        public override void Die()
        {
            base.Die();
        }
    }
}

# 敌人状态类

  • 继承角色状态类

代码如下:

EnemyStatus
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;
        }
    }
}

# 其他

  • 隐藏鼠标指针

代码如下:

CursorController
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;
        }
    }
}