[Unity2D角色控制器]Ultimate 2D Controller代码拆解学习笔记

链接

原项目(github)链接:Matthew-J-Spencer Ultimate-2D-Controller

Itch上的官方配套展示:tarodev.itch.io/ultimate-2d-controller

unitypackage加载后的文件结构

Tarodev 2D Controller/
├── _Scripts/                          # 存放脚本代码的文件夹
│   ├── PlayerAnimator.cs             # 控制玩家动画状态的脚本
│   ├── PlayerController.cs           # 控制玩家移动和物理行为的主脚本
│   └── ScriptableStats.cs           # 使用 ScriptableObject 存储玩家的参数(如速度、跳跃力等)
├── Animation/                        # 存放动画资源的文件夹
│   ├── Idle.anim                     # 玩家站立时的动画
│   ├── Jump.anim                     # 玩家跳跃时的动画
│   ├── Land.anim                     # 玩家落地时的动画
│   └── Visual.controller             # 动画状态机控制器,用于切换动画状态
├── Audio/                            # 存放音效文件的文件夹
│   ├── Splat 1.wav                   # 撞击或落地时的音效(1)
│   ├── Splat 2.wav                   # 撞击或落地时的音效(2)
│   └── Splat 3.wav                   # 撞击或落地时的音效(3)
├── Demo/                             # 示例场景及演示资源
│   ├── Scene.unity                   # 示例场景文件,展示控制器的使用
│   └── Square.png                    # 示例中的方块贴图或辅助图像
├── Materials/                        # 材质与物理材质
│   ├── Dust.mat                      # 用于尘土粒子的材质
│   ├── Player.physicsMaterial2D      # 玩家用的2D物理材质(如摩擦力、弹性等)
│   └── Trail.mat                     # 跟踪或拖尾效果使用的材质
├── Prefabs/                          # 预制体文件夹
│   └── Player Controller.prefab      # 玩家控制器的预设,组合了动画、脚本、碰撞器等
├── Sprites/                          # 2D图像资源
│   ├── Circle.png                    # 圆形精灵,可用于角色或特效
│   └── Player.png                    # 玩家角色的主要图像
└── Stat Presets/                     # ScriptableObject 参数预设
    └── Player Controller.asset       # 玩家控制器使用的参数配置文件(ScriptableStats 的实例)
 

示例对象

代码拆解


ScriptableStats.cs:

(做了部分汉化注释)

using UnityEngine;

namespace TarodevController
{
    [CreateAssetMenu]
    public class ScriptableStats : ScriptableObject
    {
        [Header("LAYERS")]
        // 图层
        [Tooltip("Set this to the layer your player is on")]
        // 将此设置为玩家所在的图层
        public LayerMask PlayerLayer;

        [Header("INPUT")]
        // 输入
        [Tooltip("Makes all Input snap to an integer. Prevents gamepads from walking slowly. Recommended value is true to ensure gamepad/keyboard parity.")]
        // 将所有输入四舍五入为整数。防止手柄缓慢移动。建议开启以确保手柄和键盘输入效果一致。
        public bool SnapInput = true;

        [Tooltip("Minimum input required before you mount a ladder or climb a ledge. Avoids unwanted climbing using controllers")]
        // 攀爬梯子或翻越台阶前需要的最小垂直输入,避免手柄漂移导致的意外攀爬
        [Range(0.01f, 0.99f)]
        public float VerticalDeadZoneThreshold = 0.3f;

        [Tooltip("Minimum input required before a left or right is recognized. Avoids drifting with sticky controllers")]
        // 识别左右移动前需要的最小水平输入,避免手柄漂移
        [Range(0.01f, 0.99f)]
        public float HorizontalDeadZoneThreshold = 0.1f;

        [Header("MOVEMENT")]
        // 移动
        [Tooltip("The top horizontal movement speed")]
        // 最高水平移动速度
        public float MaxSpeed = 14;

        [Tooltip("The player's capacity to gain horizontal speed")]
        // 玩家水平速度增长的能力
        public float Acceleration = 120;

        [Tooltip("The pace at which the player comes to a stop")]
        // 玩家停止输入时地面上的减速速度
        public float GroundDeceleration = 60;

        [Tooltip("Deceleration in air only after stopping input mid-air")]
        // 空中停止输入后仅在空中生效的减速速度
        public float AirDeceleration = 30;

        [Tooltip("A constant downward force applied while grounded. Helps on slopes")]
        // 落地时向下的恒定作用力,帮助角色在斜坡上稳定
        [Range(0f, -10f)]
        public float GroundingForce = -1.5f;

        [Tooltip("The detection distance for grounding and roof detection")]
        // 检测地面和头顶碰撞的射线长度
        [Range(0f, 0.5f)]
        public float GrounderDistance = 0.05f;

        [Header("JUMP")]
        // 跳跃
        [Tooltip("The immediate velocity applied when jumping")]
        // 跳跃时立即施加的初始向上速度
        public float JumpPower = 36;

        [Tooltip("The maximum vertical movement speed")]
        // 允许的最大自由落体速度
        public float MaxFallSpeed = 40;

        [Tooltip("The player's capacity to gain fall speed. a.k.a. In Air Gravity")]
        // 空中重力加速度程度
        public float FallAcceleration = 110;

        [Tooltip("The gravity multiplier added when jump is released early")]
        // 提前释放跳跃键时施加的额外重力倍数
        public float JumpEndEarlyGravityModifier = 3;

        [Tooltip("The time before coyote jump becomes unusable. Coyote jump allows jump to execute even after leaving a ledge")]
        // “科约特时间”持续时长,在离开平台后仍可短暂跳跃
        public float CoyoteTime = .15f;

        [Tooltip("The amount of time we buffer a jump. This allows jump input before actually hitting the ground")]
        // 可缓存的跳跃输入时长,允许在即将着地前按下跳跃键
        public float JumpBuffer = .2f;
    }
}

PlayerController.cs:

(加了中文注释)

主要功能:

可控制的跳跃:例如提前释放跳跃键,通过在玩家释放跳跃的时向角色添加额外向下的力来实现。(代码默认设置为4倍的重力)

跳跃缓冲:可以让你在真正落地之前排队进行下一次跳跃。(通过比较当前时间刻 和 跳跃按下的时间刻+最大缓冲时间 来获取是否处在有跳跃缓冲的状态)

Coyote Time(土狼时间):允许玩家在离开平台后仍能执行跳跃(时间仅为几毫秒),(通过检测角色何时离开地面并且是否在coyote time阈值内按下跳跃键来实现的)。

(PS:Coyote Time的名字源于经典动画《乐一通》(Looney Tunes)中角色冲出平台后仍能在空中奔跑的夸张效果。)

using System;
using UnityEngine;

namespace TarodevController
{
    /// <summary>
    /// Tarodev 提供的 2D 玩家控制器
    /// </summary>
    [RequireComponent(typeof(Rigidbody2D), typeof(Collider2D))]//挂载Rigidbody2D和Collider2D组件
    public class PlayerController : MonoBehaviour, IPlayerController
    {
        [SerializeField] private ScriptableStats _stats;        // 配置的行为参数
        private Rigidbody2D _rb;                                // 刚体组件引用
        private CapsuleCollider2D _col;                         // 碰撞器组件引用
        private FrameInput _frameInput;                         // 当前帧输入
        private Vector2 _frameVelocity;                         // 计算后帧速度
        private bool _cachedQueryStartInColliders;               // 缓存 Physics2D.queriesStartInColliders 原值

        #region Interface

        public Vector2 FrameInput => _frameInput.Move;         // 对外暴露的移动输入
        public event Action<bool, float> GroundedChanged;     // 地面状态改变事件,参数:是否着地,上一次离地时的垂直速度
        public event Action Jumped;                           // 跳跃事件

        #endregion

        private float _time;                                     // 游戏运行时间计数

        private void Awake()
        {
            // 缓存所有组件引用
            _rb = GetComponent<Rigidbody2D>();
            _col = GetComponent<CapsuleCollider2D>();

            // 缓存物理查询设置
            _cachedQueryStartInColliders = Physics2D.queriesStartInColliders;
        }

        private void Update()
        {
            _time += Time.deltaTime;//累计时间
            GatherInput();//采集玩家输入
        }

        /// <summary>
        /// 收集玩家输入,并处理跳跃缓冲与死区
        /// </summary>
        private void GatherInput()
        {
            _frameInput = new FrameInput
            {
                JumpDown = Input.GetButtonDown("Jump") || Input.GetKeyDown(KeyCode.C),     // 跳跃按下
                JumpHeld = Input.GetButton("Jump") || Input.GetKey(KeyCode.C),            // 跳跃持续
                Move = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"))// 水平与垂直移动输入
            };

            // 输入死区处理(可选)让
            if (_stats.SnapInput)
            {
                _frameInput.Move.x = Mathf.Abs(_frameInput.Move.x) < _stats.HorizontalDeadZoneThreshold//小于一定值则归零,防止因为误触而移动
                    ? 0 : Mathf.Sign(_frameInput.Move.x);//Mathf.Sign(x)返回x的正负号 这里用来规范到整数
                _frameInput.Move.y = Mathf.Abs(_frameInput.Move.y) < _stats.VerticalDeadZoneThreshold
                    ? 0 : Mathf.Sign(_frameInput.Move.y);
            }

            // 跳跃输入缓冲
            if (_frameInput.JumpDown)
            {
                _jumpToConsume = true;          //是否有输入待处理
                _timeJumpWasPressed = _time;    //记录跳跃按下时刻
            }
        }

        private void FixedUpdate()
        {
            // 物理更新:检测碰撞、处理跳跃、方向、重力,最后应用速度
            CheckCollisions();//碰撞检测
            HandleJump();//跳跃处理
            HandleDirection();//水平移动处理
            HandleGravity();//重力处理
            ApplyMovement();//应用速度
        }

        #region Collisions

        private float _frameLeftGrounded = float.MinValue;       // 离地时间戳
        private bool _grounded;                                   // 是否着地

        /// <summary>
        /// 检测地面与天花板碰撞,并触发相应事件
        /// </summary>
        private void CheckCollisions()
        {
            // 禁止从内部开始查询碰撞,以避免自碰撞
            Physics2D.queriesStartInColliders = false;

            // 向下检测地面,向上检测天花板
            /*
            Physics2D.CapsuleCast(
                _col.bounds.center,        // 胶囊中心点(角色中心)
                _col.size,                 // 胶囊尺寸(和碰撞体一样大)
                _col.direction,            // 胶囊朝向(垂直 or 水平)
                0,                         // 胶囊体旋转角度(这里不旋转)
                Vector2.down,              // 投射方向 ↓(检测“地面”)
                _stats.GrounderDistance,   // 投射距离(短短的一点点)
                ~_stats.PlayerLayer        // 探测哪些层级(除了玩家自己)
            );
            */
            // 地面
            bool groundHit = Physics2D.CapsuleCast(
                _col.bounds.center, _col.size, _col.direction,
                0, Vector2.down, _stats.GrounderDistance, ~_stats.PlayerLayer
            );
            //天花板
            bool ceilingHit = Physics2D.CapsuleCast(
                _col.bounds.center, _col.size, _col.direction,
                0, Vector2.up, _stats.GrounderDistance, ~_stats.PlayerLayer
            );

            // 撞到天花板时,限制向上速度
            if (ceilingHit)
                _frameVelocity.y = Mathf.Min(0, _frameVelocity.y);

            // 处理落地事件
            if (!_grounded && groundHit)//如果之前状态是不在地面,并且下方检测到地面
            {
                _grounded = true;//修改状态为在在地面
                _coyoteUsable = true;//土狼跳跃(离开地面后仍然能起跳)//可用
                _bufferedJumpUsable = true;//跳跃缓冲可用 
                _endedJumpEarly = false;//提前松开跳跃
                GroundedChanged?.Invoke(true, Mathf.Abs(_frameVelocity.y));//触发落地事件广播(用于动画控制),传递落地时候的速度
            }
            // 离地事件
            else if (_grounded && !groundHit)//如果之前状态是在地面,并且下方没有检测到地面
            {
                _grounded = false;//不再着地
                _frameLeftGrounded = _time;//记录离地时间戳
                GroundedChanged?.Invoke(false, 0);//触发离地事件(用于动画控制),传递上一次离地时的速度
            }

            // 恢复原查询设置
            Physics2D.queriesStartInColliders = _cachedQueryStartInColliders;
        }

        #endregion

        #region Jumping

        private bool _jumpToConsume;                             // 是否有跳跃输入待处理
        private bool _bufferedJumpUsable;                        // 缓冲跳跃是否可用
        private bool _endedJumpEarly;                            // 是否提前松开跳跃
        private bool _coyoteUsable;                              // 是否可使用 coyote jump
        private float _timeJumpWasPressed;                       // 跳跃按下时刻

        // 跳跃缓冲有效
        //(使用属性,每次访问都是重新计算)
        private bool HasBufferedJump => _bufferedJumpUsable && _time < _timeJumpWasPressed + _stats.JumpBuffer;//是在落地的一瞬间_bufferedJumpUsable为true,才计算这个状态。
        // Coyote 时间内可跳
        private bool CanUseCoyote => _coyoteUsable && !_grounded && _time < _frameLeftGrounded + _stats.CoyoteTime;

        /// <summary>
        /// 处理跳跃输入、缓冲与 coyote jump
        /// </summary>
        private void HandleJump()
        {
            // 检测跳跃提前结束,用于调整重力
            if (!_endedJumpEarly && !_grounded && !_frameInput.JumpHeld && _rb.velocity.y > 0)//没有标记为提前结束,并且不在地面上,并且跳跃键没有一直按住,并且速度向上。
                _endedJumpEarly = true;

            // 无待处理跳跃且缓冲已过期,直接返回
            if (!_jumpToConsume && !HasBufferedJump)//如果没有待处理的跳跃则返回
                return;

            if (_grounded || CanUseCoyote)//如果可以有跳跃需处理 在地面上或者满足土狼跳(刚离开地面没多久)的条件则执行跳跃
                ExecuteJump();

            _jumpToConsume = false;//清空跳跃请求状态
        }

        /// <summary>
        /// 执行跳跃逻辑,重置相关状态
        /// </summary>
        private void ExecuteJump()
        {
            // 重置相关状态
            _endedJumpEarly = false;//将提前结束标记清空
            _timeJumpWasPressed = 0;//将跳跃按下时刻清空
            _bufferedJumpUsable = false;//缓冲跳跃状态为不可用
            _coyoteUsable = false;//土狼跳状态为不可用
            _frameVelocity.y = _stats.JumpPower;//设置跳跃速度
            //跳跃事件广播(用于角色动画代码)
            Jumped?.Invoke();
        }

        #endregion

        #region Horizontal

        /// <summary>
        /// 处理水平移动和减速
        /// </summary>
        private void HandleDirection()
        {
            if (_frameInput.Move.x == 0)//停止水平输入后减速
            {
                // 无输入时进行减速处理
                float decel = _grounded ? _stats.GroundDeceleration : _stats.AirDeceleration;//判断是否在地面,选择对应减速度。
                _frameVelocity.x = Mathf.MoveTowards(_frameVelocity.x, 0, decel * Time.fixedDeltaTime);//减速
                //MoveTowards(current, target, maxDelta):将current向target靠拢,但不超过maxDelta的步长。
            }
            else//有水平输入时加速
            {
                // 有输入时加速至最大速度
                _frameVelocity.x = Mathf.MoveTowards(//加速
                    _frameVelocity.x,
                    _frameInput.Move.x * _stats.MaxSpeed,//方向×最大速度大小
                    _stats.Acceleration * Time.fixedDeltaTime//加速度
                );
            }
        }

        #endregion

        #region Gravity

        /// <summary>
        /// 处理重力效果,包括着地时的下压力和空中重力
        /// </summary>
        private void HandleGravity()
        {
            if (_grounded && _frameVelocity.y <= 0f)//如果在地面且y轴速度为0或负 则施加额外力,辅助贴地
            {
                // 着地时施加额外下压力,帮助角色贴地
                _frameVelocity.y = _stats.GroundingForce;
            }
            else//处理在空中的速度
            {
                // 空中下落重力
                float gravity = _stats.FallAcceleration;
                // 提前松开跳跃时增加重力加速度
                if (_endedJumpEarly && _frameVelocity.y > 0)
                    gravity *= _stats.JumpEndEarlyGravityModifier;

                _frameVelocity.y = Mathf.MoveTowards(//下落速度计算,不超过最大下落速度
                    _frameVelocity.y,
                    -_stats.MaxFallSpeed,//最大下落速度
                    gravity * Time.fixedDeltaTime
                );
            }
        }

        #endregion

        /// <summary>
        /// 将计算后的速度应用到刚体
        /// </summary>
        private void ApplyMovement() => _rb.velocity = _frameVelocity;

#if UNITY_EDITOR
        private void OnValidate()
        {
            // 在编辑器中提醒绑定 Stats
            if (_stats == null)
                Debug.LogWarning("Please assign a ScriptableStats asset to the Player Controller's Stats slot", this);
        }
#endif
    }

    // 玩家输入结构体
    public struct FrameInput
    {
        public bool JumpDown;   // 跳跃按下
        public bool JumpHeld;   // 跳跃持续
        public Vector2 Move;    // 水平与垂直移动输入
    }

    // 玩家控制接口
    public interface IPlayerController
    {
        event Action<bool, float> GroundedChanged;  // 地面状态事件
        event Action Jumped;                        // 跳跃事件
        Vector2 FrameInput { get; }                 // 当前移动输入(只读属性)
    }
}

PlayerAnimanitor.cs

(加了中文注释)

using TarodevController;
using UnityEngine;

namespace TarodevController
{
    /// <summary>
    /// 非常基础的角色动画示例,演示如何根据角色状态驱动动画、翻转、倾斜及粒子效果。
    /// </summary>
    public class PlayerAnimator : MonoBehaviour
    {
        [Header("References")] 
        [SerializeField]
        private Animator _anim;                        // 引用 Animator 组件,用于控制动画状态机

        [SerializeField] private SpriteRenderer _sprite; // 精灵渲染器,用于左右翻转

        [Header("Settings")] 
        [SerializeField, Range(1f, 3f)]
        private float _maxIdleSpeed = 2;               // 空闲时动画速度的最大倍率

        [SerializeField] private float _maxTilt = 5;   // 最大倾斜角度(度数)
        [SerializeField] private float _tiltSpeed = 20; // 倾斜插值速度

        [Header("Particles")] 
        [SerializeField] private ParticleSystem _jumpParticles;   // 跳跃时的粒子特效
        [SerializeField] private ParticleSystem _launchParticles; // 起跳发射效果
        [SerializeField] private ParticleSystem _moveParticles;   // 移动持续粒子特效
        [SerializeField] private ParticleSystem _landParticles;   // 落地粒子特效

        [Header("Audio Clips")] 
        [SerializeField]
        private AudioClip[] _footsteps;               // 脚步音频列表,用于随机播放脚步声

        private AudioSource _source;                   // 音频源组件
        private IPlayerController _player;             // 角色控制接口
        private bool _grounded;                        // 当前是否在地面
        private ParticleSystem.MinMaxGradient _currentGradient; // 粒子颜色渐变

        private void Awake()
        {
            // 缓存组件引用
            _source = GetComponent<AudioSource>();// 音频源组件
            _player = GetComponentInParent<IPlayerController>();// 角色控制接口
        }

        private void OnEnable()
        {
            // 订阅跳跃与落地事件
            _player.Jumped += OnJumped;//跳跃事件
            _player.GroundedChanged += OnGroundedChanged;//着陆状态改变

            // 启动移动粒子
            _moveParticles.Play();
        }

        private void OnDisable()
        {
            // 取消订阅,防止内存泄漏
            _player.Jumped -= OnJumped;
            _player.GroundedChanged -= OnGroundedChanged;

            // 停止移动粒子
            _moveParticles.Stop();
        }

        private void Update()
        {
            if (_player == null) return;

            // 检测地面下方颜色,用于粒子染色
            DetectGroundColor();

            // 根据水平输入翻转精灵
            HandleSpriteFlip();

            // 根据输入强度调节空闲动画速度及移动粒子大小
            HandleIdleSpeed();

            // 根据地面状态和平移输入倾斜角色
            HandleCharacterTilt();
        }

        /// <summary>
        /// 根据输入方向翻转精灵
        /// </summary>
        private void HandleSpriteFlip()
        {
            if (_player.FrameInput.x != 0)
                _sprite.flipX = _player.FrameInput.x < 0;
        }

        /// <summary>
        /// 调整空闲动画速度和移动粒子缩放
        /// </summary>
        private void HandleIdleSpeed()
        {
            float inputStrength = Mathf.Abs(_player.FrameInput.x);//(开启SnapInput时为1)
            // 空闲时速率在 1 到 _maxIdleSpeed 之间根据inputStrength插值
            _anim.SetFloat(IdleSpeedKey, Mathf.Lerp(1, _maxIdleSpeed, inputStrength));
            // 移动粒子根据输入强度缩放
            _moveParticles.transform.localScale = Vector3.MoveTowards(
                _moveParticles.transform.localScale,
                Vector3.one * inputStrength,
                2 * Time.deltaTime
            );
        }

        /// <summary>
        /// 角色倾斜效果,跑动时身体向前倾
        /// </summary>
        private void HandleCharacterTilt()
        {
            // 如果在地面,根据输入角度生成旋转,否则回正
            Quaternion target = _grounded
                ? Quaternion.Euler(0, 0, _maxTilt * _player.FrameInput.x)
                : Quaternion.identity;
            // 平滑插值当前 transform.up(决定了当前动画对象的朝向)
            _anim.transform.up = Vector3.RotateTowards(
                _anim.transform.up,
                target * Vector2.up,
                _tiltSpeed * Time.deltaTime,
                0f
            );
        }

        /// <summary>
        /// 跳跃时触发动画与粒子效果
        /// </summary>
        private void OnJumped()//跳跃事件
        {
            // 播放跳跃动画
            _anim.SetTrigger(JumpKey);
            _anim.ResetTrigger(GroundedKey);

            if (_grounded) // 仅在非土狼跳时播放粒子
            {
                //设置粒子颜色
                SetColor(_jumpParticles);
                SetColor(_launchParticles);
                //播放粒子
                _jumpParticles.Play();
                _launchParticles.Play();
            }
        }

        /// <summary>
        /// 落地状态改变时触发动画、声音、粒子
        /// </summary>
        private void OnGroundedChanged(bool grounded, float impact)
        {
            //获取当前是否在地面
            _grounded = grounded;

            if (grounded)
            {
                DetectGroundColor();              // 更新粒子颜色
                SetColor(_landParticles);         // 应用颜色梯度

                _anim.SetTrigger(GroundedKey);    // 播放落地动画
                // 随机脚步声
                _source.PlayOneShot(_footsteps[Random.Range(0, _footsteps.Length)]);
                _moveParticles.Play();            // 恢复移动粒子

                // 根据冲击强度缩放落地粒子
                // Mathf.InverseLerp 反向插值 返回的是一个 0到1之间的比例值,表示 value 在范围 [a, b] 中的位置。
                float scale = Mathf.InverseLerp(0, 40, impact);//落地的时候会impact传过来y轴速度,不在地面的时候传过来0
                _landParticles.transform.localScale = Vector3.one * scale;//缩放粒子
                _landParticles.Play();            // 播放落地粒子
            }
            else
            {
                // 离地时停止移动粒子
                _moveParticles.Stop();
            }
        }

        /// <summary>
        /// 向下射线检测地面颜色,用于粒子染色
        /// </summary>
        private void DetectGroundColor()
        {
            var hit = Physics2D.Raycast(transform.position, Vector3.down, 2);//射线检测,以当前位置为起点,向下检测2的距离
            if (!hit || hit.collider.isTrigger) return;//如果没有碰到东西或者碰到的东西是触发器,则不处理

            // 如果碰撞物带有 SpriteRenderer,则获取其颜色
            if (hit.transform.TryGetComponent(out SpriteRenderer r))
            {
                Color color = r.color;
                // 生成渐变色用于粒子
                _currentGradient = new ParticleSystem.MinMaxGradient(color * 0.9f, color * 1.2f);//在原颜色的基础上有一定的亮度变化
                SetColor(_moveParticles);
            }
        }

        /// <summary>
        /// 设置粒子系统的起始颜色
        /// </summary>
        private void SetColor(ParticleSystem ps)
        {
            var main = ps.main;//获取粒子系统的主模块
            main.startColor = _currentGradient;//设置粒子颜色
        }

        // Animator 参数的哈希值,优化性能
        //StringToHash 方法将 Animator 参数名(字符串)转换成一个整数哈希值。
        private static readonly int GroundedKey = Animator.StringToHash("Grounded");
        private static readonly int IdleSpeedKey = Animator.StringToHash("IdleSpeed");
        private static readonly int JumpKey = Animator.StringToHash("Jump");
    }
}

有话要说

花了几个小时把这个角色控制器的代码读了一遍,受益匪浅。他介绍的原视频中还提到了定点修改和边缘检测的功能,但我并没有在源码中找到。但通过可控的跳跃、跳跃缓冲、土狼时间、以及会变色的粒子效果和Q弹的角色动画已经得到了非常不错的手感和体验。感谢大佬对代码的开源。
这篇笔记没有把代码拆细,如果带来了观感上的不适请在评论区留言,我也会继续完善这篇笔记的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值