[Untiy2D好项目分享]一个好手感的2D控制器

学习目标:

今天同样在b站上看到一个转载视频这个Unity角色控制器是使用自定义的物理组件制作的,并包含一些隐藏的平台游戏技巧,让玩家体验到优秀的操作手感。

视频地址:

【Unity】好手感从何而来?一款免费的2D角色控制器,附源码_哔哩哔哩_bilibili来自Youtuber Tarodev中文字幕请开启CC字幕视频内源码: https://github.com/Matthew-J-Spencer/Ultimate-2D-Controller额外付费部分: https://www.patreon.com/tarodev了解如何制作一个优秀的玩家控制器。这个Unity角色控制器是使用自定义的物理组件制作的,并包含一些隐藏的平台游戏技巧,让玩家体验到优https://www.bilibili.com/video/BV14S4y1o79L?spm_id_from=333.851.header_right.history_list.click

源码:GitHub - Matthew-J-Spencer/Ultimate-2D-Controller: A great starting point for your 2D controller. Making use of all the hidden tricks like coyote, buffered actions, speedy apex, anti grav apex, etcA great starting point for your 2D controller. Making use of all the hidden tricks like coyote, buffered actions, speedy apex, anti grav apex, etc - GitHub - Matthew-J-Spencer/Ultimate-2D-Controller: A great starting point for your 2D controller. Making use of all the hidden tricks like coyote, buffered actions, speedy apex, anti grav apex, etchttps://github.com/Matthew-J-Spencer/Ultimate-2D-ControllerTarodev is creating Game dev and programming tutorials | PatreonBecome a patron of Tarodev today: Get access to exclusive content and experiences on the world’s largest membership platform for artists and creators.https://www.patreon.com/tarodev

学习内容:

学习好一个2D控制器源码是至关重要的,本素材中作者直接无需添加任何rigibody2D和Collider2D的组件,而是直接用代码来实现玩家的碰撞检测。

创建好一个Ground(用Tile)然后再将它的Layer设置为Ground

创建一个空对象名字叫Player再创建一个空对象叫Viusal给它Audio Source以及新组件Trail Renderer,当你在移动角色的时候该组件会产生一个细的运动轨迹,再创建一个Sprite并给它Sprite Renderer以及Animator

最后给它一个Particals作为总的管理Partical System的再创建移动跳跃落地所需要的Partical System(具体参数源码有)

搞完这些后即可进入编写代码部分:

首先我们要编写的是一个接口负责监控玩家的键盘输入命令,另外一个结构体FrameInput是负责玩家键盘的xy轴的输入,以及射线检测的结构体RayRange.

可以看到我们并没有继承MonoBehaviour,该脚本不需要作为组件挂载,而是给其它类调用

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace TarController
{
    public struct FrameInput
    {
        public float X;
        public bool JumpDown;
        public bool JumpUp;
    }
    public interface IPlayerInterface
    {
        public FrameInput frameInput { get; }
        public Vector3 Velocity { get; }
        public bool FrameJumped { get; }
        public bool FrameLanded { get; }
        public bool Grounded { get; }
        public Vector3 RawMovement { get; }

    }
    public struct RayRange
    {
        public RayRange(float x1,float y1,float x2,float y2,Vector2 dir)
        {
            Start = new Vector2(x1, y1);
            End = new Vector2(x2, y2);
            Dir = dir;
        }
        public readonly Vector2 Start, End, Dir;
    }
}

其次编写的是我们上面提到的能够不使用其它多余组件实现多种移动的CharacterController.cs

完整版如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

namespace TarController
{
    public class CharacterController : MonoBehaviour, IPlayerInterface
    {
        //实现IPlayerInterface的接口
        public FrameInput frameInput { get; private set; }
        public Vector3 Velocity { get; private set; }
        public bool FrameJumped { get; private set; }
        public bool FrameLanded { get; private set; }
        public bool Grounded => collDown;
        public Vector3 RawMovement { get; private set; }

        private Vector3 lastPosition;
        private float currentHorizontalSpeed, currentVerticalSpeed;
        //为了防止游戏开始时碰撞体还没生成需要等0.5s ,nameof函数的string类型
        bool _actived;
        private void Awake()=>Invoke(nameof(Activated),0.5f );
        void Activated() => _actived = true;
        void Update()
        {
            if (!_actived)
                return;
            //计算速度
            Velocity = (transform.position - lastPosition) / Time.deltaTime;
            lastPosition = transform.position;

            CatchInput();
            RunCollisionCheck();

            CalculateWalk();
            CalculateJumpApex();
            CalculateGravity();
            CalculateJump();

            MoveCharacter();
        }

        #region Input System
        private void CatchInput()
        {
            frameInput = new FrameInput
            {
                X = Input.GetAxisRaw("Horizontal"),
                JumpDown = Input.GetButtonDown("Jump"),//按下跳跃
                JumpUp = Input.GetButtonUp("Jump") //按起跳跃
            };
            if (frameInput.JumpDown)
            {
                lastJumpPressed = Time.time;
            }
        }
        #endregion

        #region Collison
        [Header("Collision")]
        [SerializeField] private Bounds characterBounds; //边界长方体
        [SerializeField] private LayerMask groundLayer;
        [SerializeField] private int detectorCount = 3;
        [SerializeField] private float detectorLength = 0.1f;
        [SerializeField, Range(0.1f, 0.3f)] private float rayBuffer = 0.1f;

        private RayRange raysUp, raysDown, raysLeft, raysRight;
        bool collUp, collDown, collLeft, collRight;

        private float timeLeftGrounded;

        private void RunCollisionCheck()
        {
            //生成射线
            CaculateRayRanged();

            //这个状态是在地面上
            FrameLanded = false;
            var groundedCheck = RunDetection(raysDown);
            if(collDown&& !groundedCheck)
            {
                timeLeftGrounded = Time.time;
            }
            //目的是让玩家离开地面一小段距离时还能进行跳跃
            else if(!collDown && groundedCheck)
            {
                coyoteUsable = true;
                FrameLanded = true;
            }
            collDown = groundedCheck;

            collUp = RunDetection(raysUp);
            collLeft = RunDetection(raysLeft);
            collRight = RunDetection(raysRight);

            bool RunDetection(RayRange range)
            {
                return EvaluateRayPositions(range).Any(point => Physics2D.Raycast(point, range.Dir, detectorLength, groundLayer));
            }
        }
        //用射线来判断是否在地面上
        private void CaculateRayRanged()
        {
            var b = new Bounds(transform.position, characterBounds.size);

            raysDown = new RayRange (b.min.x + rayBuffer, b.min.y, b.max.x-rayBuffer, b.min.y,Vector2.down);
            raysUp = new RayRange   (b.min.x + rayBuffer, b.max.y, b.max.x-rayBuffer, b.max.y, Vector2.up);
            raysLeft = new RayRange (b.min.x, b.min.y + rayBuffer, b.min.x, b.max.y - rayBuffer, Vector2.left);
            raysRight = new RayRange(b.max.x, b.min.y + rayBuffer, b.max.x, b.max.y - rayBuffer, Vector2.right);
        }

        //这里不知道什么意思
        private IEnumerable<Vector2> EvaluateRayPositions(RayRange range)
        {
            for (int i = 0; i < detectorCount; i++)
            {
                var t = (float)i / (detectorCount - 1);
                yield return Vector2.Lerp(range.Start, range.End, t);
            }
        }
        private void OnDrawGizmos()
        {
            //Bounds
            Gizmos.color = Color.yellow;
            Gizmos.DrawWireCube(transform.position + characterBounds.center, characterBounds.size);

            //画射线
            if (!Application.isPlaying)
            {
                CaculateRayRanged();
                Gizmos.color = Color.blue;
                foreach (var range in new List<RayRange> { raysUp,raysRight,raysDown,raysLeft})
                {
                    foreach (var point in EvaluateRayPositions(range))
                    {
                        Gizmos.DrawRay(point, range.Dir * detectorLength);
                    }
                }
            }

            if (!Application.isPlaying)
                return;

            //未来的位置
            Gizmos.color = Color.red;
            var move = new Vector3(currentHorizontalSpeed, currentVerticalSpeed) * Time.deltaTime;
            Gizmos.DrawWireCube(transform.position + move, characterBounds.size);
        }
        #endregion

        #region Walk
        /*
         要说明一定,当到最高点apex的时候,允许有一定的调整速度,即apexBonus
         */
        [Header("Walking")]
        [SerializeField] float moveClamp = 13f; //最大速度
        [SerializeField] float accleration = 90f; //加速度
        [SerializeField] float deAccleration = 60f; //减速度
        [SerializeField] float apexBonus = 2f;
        private void CalculateWalk()
        {
            if(frameInput.X != 0)
            {
                currentHorizontalSpeed +=frameInput.X * accleration * Time.deltaTime;
                currentHorizontalSpeed = Mathf.Clamp(currentHorizontalSpeed, -moveClamp, moveClamp);
                //奖励速度,当跳跃到最高点时
                //Mathf.Sign() 如果是0或正数就返回1,负数则返回-1       //最高点apex
                var _apexBouns = Mathf.Sign(frameInput.X) * apexBonus * apexPoints;
                currentHorizontalSpeed += _apexBouns * Time.deltaTime;
            }
            //当你键盘没有输入X的速度就减速
            else
            {
                currentHorizontalSpeed = Mathf.MoveTowards(currentHorizontalSpeed, 0, deAccleration * Time.deltaTime);
            }
            //当碰到墙上时
            if(currentHorizontalSpeed >0 && collRight || currentHorizontalSpeed <0 && collLeft)
            {
                currentHorizontalSpeed = 0;
            }
        }
        #endregion

        #region Jump
        /*
         跳跃分为小跳和大跳
        当按下跳跃的时候y轴的速度等于跳跃高度,同时开始计时离开地面的时间,关闭coyote,正在跳跃为true,早点结束跳跃为false
        当按起跳跃键的时候,判断脚没碰到地面,有y轴方向的速度,早点结束跳跃为true
        当头顶碰到物体的时候直接将y轴速度设置为0

        同时还有一个判断在跳跃最高点,用插值函数作为fallspeed
         */
        [Header("Jumping")]
        [SerializeField] float jumpHeight = 30f;
        [SerializeField] float jumpEarlyGravityModifier = 3;
        [SerializeField] float jumpApexThreshold = 10f; //当跳跃到最高点时有一段反重力和小助推效果
        [SerializeField] float coyoteTImeThreshold = 0.1f;
        [SerializeField] float jumpBuffered = 0.1f; 

        private float apexPoints; //当跳到最高点时值为1
        private float lastJumpPressed; //记录按下跳跃的时间

        private bool coyoteUsable;
        private bool endedjumpEarly = true;

        private bool CanUseCoyote => coyoteUsable && !collDown && coyoteTImeThreshold + timeLeftGrounded > Time.time; //允许玩家在离开平台的几毫秒时间内仍然能跳跃
        private bool HasBufferJump => collDown && jumpBuffered + lastJumpPressed > Time.time; //跳跃缓冲
        private void CalculateJumpApex()
        {
            if (!collDown)
            {
                apexPoints = Mathf.InverseLerp(jumpApexThreshold, 0,Mathf.Abs( Velocity.y));
                fallSpeed = Mathf.Lerp(minFallSpeed, maxFallSpeed, apexPoints);
            }
            else
            {
                apexPoints = 0;
            }
        }
        private void CalculateJump()
        {
            //大跳
            if (frameInput.JumpDown && CanUseCoyote || HasBufferJump)
            {
                currentVerticalSpeed = jumpHeight;
                endedjumpEarly = false;
                coyoteUsable = false;
                timeLeftGrounded = float.MinValue;
                FrameJumped = true;
            }
            else
            {
                FrameJumped = false;
            }
            //小跳(在跳跃的时候很快就松开跳跃键)
            if (frameInput.JumpUp && !collDown && !endedjumpEarly && Velocity.y >0)
            {
                endedjumpEarly = true;
            }
            //如果头顶撞到了
            if (collUp)
            {
                if(currentVerticalSpeed > 0)
                {
                    currentVerticalSpeed = 0;
                }
            }
        }
        #endregion

        #region Gravity
        [Header("Gravity")]
        [SerializeField] float fallClamp = -40f;
        [SerializeField] float minFallSpeed = 80f;
        [SerializeField] float maxFallSpeed = 120f;
        private float fallSpeed;

        /*
          先判断是否在地面上,如果在地面上y轴的速度为0,如果在下降过程中,则再判断是否是小跳,小跳则加快降落速度,否则是正常减速度
          最后再限制一下最大下落速度
         */
        private void CalculateGravity()
        {
            if (collDown)
            {
                if (currentVerticalSpeed < 0)
                    currentVerticalSpeed = 0;
            }
            else
            {
                //判断是否大小跳然后再根据选择下降速度
                var fallingSpeed = endedjumpEarly && currentVerticalSpeed > 0 ? fallSpeed * jumpEarlyGravityModifier : fallSpeed;
                currentVerticalSpeed -= fallingSpeed * Time.deltaTime;
                if (currentVerticalSpeed < fallClamp) currentVerticalSpeed = fallClamp;
            }
        }
        #endregion

        #region Move
        [Header("Move")]
        [SerializeField, Tooltip("Raising this value increases collision accuracy at the cost of performance.")]
        private int freeColliderIterations = 10;

        //我们投射我们的边界来避免被接下来的碰撞体
        private void MoveCharacter()
        {
            var pos = transform.position;
            RawMovement = new Vector3(currentHorizontalSpeed, currentVerticalSpeed);
            var move = RawMovement * Time.deltaTime;
            var furtherPos = move + pos;

            //检测下一个位置的碰撞,如果没有发生碰撞就停止检测
            var hit = Physics2D.OverlapBox(furtherPos, characterBounds.size, 0, groundLayer);
            if(!hit)
            {
                transform.position += move;
                return;
            }

            //否则找个可以移动的点
            var positionToMoveTo = transform.position;

            for (int i = 1; i < freeColliderIterations; i++)
            {
                var t = (float)i / freeColliderIterations;
                var posToTry = Vector2.Lerp(pos, furtherPos, t);

                if (Physics2D.OverlapBox(posToTry, characterBounds.size, 0, groundLayer))
                {
                    transform.position = positionToMoveTo;

                    if(i == 1)
                    {
                        if (currentVerticalSpeed < 0)
                            currentVerticalSpeed = 0;
                        var dir = transform.position - hit.transform.position;
                        transform.position += dir.normalized * move.magnitude;
                    }
                    return;
                }
                positionToMoveTo = posToTry;
            }
        }
        #endregion
    }
}

解析版如下:

首先我们要实现前面提到的接口里面的所有成员(这里是属性)

//实现IPlayerInterface的接口
        public FrameInput frameInput { get; private set; }
        public Vector3 Velocity { get; private set; }
        public bool FrameJumped { get; private set; }
        public bool FrameLanded { get; private set; }
        public bool Grounded => collDown;
        public Vector3 RawMovement { get; private set; }

然后我们currentHorizontalSpeed, currentVerticalSpeed记录我们的xy轴方向上的速度。

这段是作者说明为了防止游戏运行时碰撞体还没有完成生成而延迟0.5s使用Awake()函数

 //为了防止游戏开始时碰撞体还没生成需要等0.5s ,nameof函数的string类型
        bool _actived;
        private void Awake()=>Invoke(nameof(Activated),0.5f );
        void Activated() => _actived = true;
if (!_actived)
                return;
            //计算速度
            Velocity = (transform.position - lastPosition) / Time.deltaTime;
            lastPosition = transform.position;

然后我们分模块解析各个函数的意义,每个模块都用#region 和 #endregion来分块管理

首先是Input System

用Unity中Input类自带的来键盘输入,赋值给我们前面创建的结构体FrameInput,frameInput.JumpDown记录我们最后按下跳跃的时间

#region Input System
        private void CatchInput()
        {
            frameInput = new FrameInput
            {
                X = Input.GetAxisRaw("Horizontal"),
                JumpDown = Input.GetButtonDown("Jump"),//按下跳跃
                JumpUp = Input.GetButtonUp("Jump") //按起跳跃
            };
            if (frameInput.JumpDown)
            {
                lastJumpPressed = Time.time;
            }
        }
        #endregion

然后是Walk模块

用+=来加速,用Mathf.Clamp(限制它的最大速度),而apexBouns则是玩家跳跃到最高点的时候能进行小范围的移动来控制下落点;当你没有输入X方向的输入的时候,Mathf.MoveTowards()来减速,最后当你撞墙的时候你的currentHorizontalSpeed = 0;

#region Walk
        /*
         要说明一定,当到最高点apex的时候,允许有一定的调整速度,即apexBonus
         */
        [Header("Walking")]
        [SerializeField] float moveClamp = 13f; //最大速度
        [SerializeField] float accleration = 90f; //加速度
        [SerializeField] float deAccleration = 60f; //减速度
        [SerializeField] float apexBonus = 2f;
        private void CalculateWalk()
        {
            if(frameInput.X != 0)
            {
                currentHorizontalSpeed +=frameInput.X * accleration * Time.deltaTime;
                currentHorizontalSpeed = Mathf.Clamp(currentHorizontalSpeed, -moveClamp, moveClamp);
                //奖励速度,当跳跃到最高点时
                //Mathf.Sign() 如果是0或正数就返回1,负数则返回-1       //最高点apex
                var _apexBouns = Mathf.Sign(frameInput.X) * apexBonus * apexPoints;
                currentHorizontalSpeed += _apexBouns * Time.deltaTime;
            }
            //当你键盘没有输入X的速度就减速
            else
            {
                currentHorizontalSpeed = Mathf.MoveTowards(currentHorizontalSpeed, 0, deAccleration * Time.deltaTime);
            }
            //当碰到墙上时
            if(currentHorizontalSpeed >0 && collRight || currentHorizontalSpeed <0 && collLeft)
            {
                currentHorizontalSpeed = 0;
            }
        }
        #endregion

跳跃部分分为大跳和小跳,根据你是否过早松开跳跃键来判断。需要注意的是coyote部分的代码使用于当你在台阶上离开地面的几毫秒时间你仍然能进行跳跃

#region Jump
        /*
         跳跃分为小跳和大跳
        当按下跳跃的时候y轴的速度等于跳跃高度,同时开始计时离开地面的时间,关闭coyote,正在跳跃为true,早点结束跳跃为false
        当按起跳跃键的时候,判断脚没碰到地面,有y轴方向的速度,早点结束跳跃为true
        当头顶碰到物体的时候直接将y轴速度设置为0

        同时还有一个判断在跳跃最高点,用插值函数作为fallspeed
         */
        [Header("Jumping")]
        [SerializeField] float jumpHeight = 30f;
        [SerializeField] float jumpEarlyGravityModifier = 3;
        [SerializeField] float jumpApexThreshold = 10f; //当跳跃到最高点时有一段反重力和小助推效果
        [SerializeField] float coyoteTImeThreshold = 0.1f;
        [SerializeField] float jumpBuffered = 0.1f; 

        private float apexPoints; //当跳到最高点时值为1
        private float lastJumpPressed; //记录按下跳跃的时间

        private bool coyoteUsable;
        private bool endedjumpEarly = true;

        private bool CanUseCoyote => coyoteUsable && !collDown && coyoteTImeThreshold + timeLeftGrounded > Time.time; //允许玩家在离开平台的几毫秒时间内仍然能跳跃
        private bool HasBufferJump => collDown && jumpBuffered + lastJumpPressed > Time.time; //跳跃缓冲
        private void CalculateJumpApex()
        {
            if (!collDown)
            {
                apexPoints = Mathf.InverseLerp(jumpApexThreshold, 0,Mathf.Abs( Velocity.y));
                fallSpeed = Mathf.Lerp(minFallSpeed, maxFallSpeed, apexPoints);
            }
            else
            {
                apexPoints = 0;
            }
        }
        private void CalculateJump()
        {
            //大跳
            if (frameInput.JumpDown && CanUseCoyote || HasBufferJump)
            {
                currentVerticalSpeed = jumpHeight;
                endedjumpEarly = false;
                coyoteUsable = false;
                timeLeftGrounded = float.MinValue;
                FrameJumped = true;
            }
            else
            {
                FrameJumped = false;
            }
            //小跳(在跳跃的时候很快就松开跳跃键)
            if (frameInput.JumpUp && !collDown && !endedjumpEarly && Velocity.y >0)
            {
                endedjumpEarly = true;
            }
            //如果头顶撞到了
            if (collUp)
            {
                if(currentVerticalSpeed > 0)
                {
                    currentVerticalSpeed = 0;
                }
            }
        }
        #endregion

然后到了下落部分,同样根据你是大跳还是小跳决定你的下落速度

#region Gravity
        [Header("Gravity")]
        [SerializeField] float fallClamp = -40f;
        [SerializeField] float minFallSpeed = 80f;
        [SerializeField] float maxFallSpeed = 120f;
        private float fallSpeed;

        /*
          先判断是否在地面上,如果在地面上y轴的速度为0,如果在下降过程中,则再判断是否是小跳,小跳则加快降落速度,否则是正常减速度
          最后再限制一下最大下落速度
         */
        private void CalculateGravity()
        {
            if (collDown)
            {
                if (currentVerticalSpeed < 0)
                    currentVerticalSpeed = 0;
            }
            else
            {
                //判断是否大小跳然后再根据选择下降速度
                var fallingSpeed = endedjumpEarly && currentVerticalSpeed > 0 ? fallSpeed * jumpEarlyGravityModifier : fallSpeed;
                currentVerticalSpeed -= fallingSpeed * Time.deltaTime;
                if (currentVerticalSpeed < fallClamp) currentVerticalSpeed = fallClamp;
            }
        }
        #endregion

然后是Collision部分的检测

region Collison
        [Header("Collision")]
        [SerializeField] private Bounds characterBounds; //边界长方体
        [SerializeField] private LayerMask groundLayer;
        [SerializeField] private int detectorCount = 3;
        [SerializeField] private float detectorLength = 0.1f;
        [SerializeField, Range(0.1f, 0.3f)] private float rayBuffer = 0.1f;

        private RayRange raysUp, raysDown, raysLeft, raysRight;
        bool collUp, collDown, collLeft, collRight;

        private float timeLeftGrounded;

        private void RunCollisionCheck()
        {
            //生成射线
            CaculateRayRanged();

            //这个状态是在地面上
            FrameLanded = false;
            var groundedCheck = RunDetection(raysDown);
            if(collDown&& !groundedCheck)
            {
                timeLeftGrounded = Time.time;
            }
            //目的是让玩家离开地面一小段距离时还能进行跳跃
            else if(!collDown && groundedCheck)
            {
                coyoteUsable = true;
                FrameLanded = true;
            }
            collDown = groundedCheck;

            collUp = RunDetection(raysUp);
            collLeft = RunDetection(raysLeft);
            collRight = RunDetection(raysRight);

            bool RunDetection(RayRange range)
            {
                return EvaluateRayPositions(range).Any(point => Physics2D.Raycast(point, range.Dir, detectorLength, groundLayer));
            }
        }
        //用射线来判断是否在地面上
        private void CaculateRayRanged()
        {
            var b = new Bounds(transform.position, characterBounds.size);

            raysDown = new RayRange (b.min.x + rayBuffer, b.min.y, b.max.x-rayBuffer, b.min.y,Vector2.down);
            raysUp = new RayRange   (b.min.x + rayBuffer, b.max.y, b.max.x-rayBuffer, b.max.y, Vector2.up);
            raysLeft = new RayRange (b.min.x, b.min.y + rayBuffer, b.min.x, b.max.y - rayBuffer, Vector2.left);
            raysRight = new RayRange(b.max.x, b.min.y + rayBuffer, b.max.x, b.max.y - rayBuffer, Vector2.right);
        }

        //这里不知道什么意思
        private IEnumerable<Vector2> EvaluateRayPositions(RayRange range)
        {
            for (int i = 0; i < detectorCount; i++)
            {
                var t = (float)i / (detectorCount - 1);
                yield return Vector2.Lerp(range.Start, range.End, t);
            }
        }
        private void OnDrawGizmos()
        {
            //Bounds
            Gizmos.color = Color.yellow;
            Gizmos.DrawWireCube(transform.position + characterBounds.center, characterBounds.size);

            //画射线
            if (!Application.isPlaying)
            {
                CaculateRayRanged();
                Gizmos.color = Color.blue;
                foreach (var range in new List<RayRange> { raysUp,raysRight,raysDown,raysLeft})
                {
                    foreach (var point in EvaluateRayPositions(range))
                    {
                        Gizmos.DrawRay(point, range.Dir * detectorLength);
                    }
                }
            }

            if (!Application.isPlaying)
                return;

            //未来的位置
            Gizmos.color = Color.red;
            var move = new Vector3(currentHorizontalSpeed, currentVerticalSpeed) * Time.deltaTime;
            Gizmos.DrawWireCube(transform.position + move, characterBounds.size);
        }
        #endregion

最后是真正用于移动的Move()

  #region Move
        [Header("Move")]
        [SerializeField, Tooltip("Raising this value increases collision accuracy at the cost of performance.")]
        private int freeColliderIterations = 10;

        //我们投射我们的边界来避免被接下来的碰撞体
        private void MoveCharacter()
        {
            var pos = transform.position;
            RawMovement = new Vector3(currentHorizontalSpeed, currentVerticalSpeed);
            var move = RawMovement * Time.deltaTime;
            var furtherPos = move + pos;

            //检测下一个位置的碰撞,如果没有发生碰撞就停止检测
            var hit = Physics2D.OverlapBox(furtherPos, characterBounds.size, 0, groundLayer);
            if(!hit)
            {
                transform.position += move;
                return;
            }

            //否则找个可以移动的点
            var positionToMoveTo = transform.position;

            for (int i = 1; i < freeColliderIterations; i++)
            {
                var t = (float)i / freeColliderIterations;
                var posToTry = Vector2.Lerp(pos, furtherPos, t);

                if (Physics2D.OverlapBox(posToTry, characterBounds.size, 0, groundLayer))
                {
                    transform.position = positionToMoveTo;

                    if(i == 1)
                    {
                        if (currentVerticalSpeed < 0)
                            currentVerticalSpeed = 0;
                        var dir = transform.position - hit.transform.position;
                        transform.position += dir.normalized * move.magnitude;
                    }
                    return;
                }
                positionToMoveTo = posToTry;
            }
        }
        #endregion
    }

最后我们再添加一个CharacterAnimator.cs用于做动画,控制身体转向,播放声音,播放粒子特效

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;

namespace TarController
{
    public class CharacterAnimator : MonoBehaviour
    {
        [SerializeField] private Animator anim;
        [SerializeField] private AudioSource source;
        [SerializeField] private AudioClip[] clips;
        [SerializeField] private LayerMask groundMasks;
        [SerializeField] private ParticleSystem jumpParticle, lanuchParticle;
        [SerializeField] private ParticleSystem moveParticle, landParticle;
        [SerializeField] private float maxTilt = 0.1f;
        [SerializeField] private float tileSpeed = 1f;
        [SerializeField, Range(1f, 3f)] private float maxIdleSpeed = 2f;

        
        [SerializeField] private float maxParticalFallSpeed = -40f;
        private ParticleSystem.MinMaxGradient currentGradient;

        private IPlayerInterface player;
        private bool playerOnGround;
        private Vector2 movement;

        void Awake() => player = GetComponentInParent<IPlayerInterface>();
        void Update()
        {
            if (player != null)
                return;
            //Flip
            if (player.frameInput.X != 0)
            {
                transform.localScale = new Vector3(player.frameInput.X >0?1:-1, 1, 1);
            }
            //跑步的时候播放身体抖动的动画
            var targetRotVector = new Vector3(0, 0, Mathf.Lerp(-maxTilt, maxTilt, Mathf.InverseLerp(-1, 1, player.frameInput.X)));
            anim.transform.rotation = Quaternion.RotateTowards(anim.transform.rotation,Quaternion.Euler(targetRotVector), tileSpeed * Time.deltaTime);

            //跑步的动画
            anim.SetFloat(IdleSpeedKey, Mathf.Lerp(1, maxIdleSpeed, Mathf.Abs(player.frameInput.X)));
            //降落动画
            if (player.FrameLanded)
            {
                anim.SetTrigger(GroundedKey);
                source.PlayOneShot(clips[Random.Range(0, clips.Length)]);
            }
            //跳跃动画
            if (player.FrameJumped)
            {
                anim.SetTrigger(JumpKey);
                anim.ResetTrigger(GroundedKey);

                //只有在地面上才会播放粒子(也避免在coyote状态下播放)
                if (player.Grounded)
                {
                    SetColor(jumpParticle);
                    SetColor(lanuchParticle);
                    jumpParticle.Play();
                }
            }
            //降落的粒子特效
            if(!playerOnGround && player.Grounded)
            {
                playerOnGround = true;
                moveParticle.Play();
                landParticle.transform.localScale = Vector3.one * Mathf.InverseLerp(0, maxParticalFallSpeed, movement.y);
                SetColor(landParticle);
                landParticle.Play();
            }
            else if(playerOnGround && !player.Grounded)
            {
                playerOnGround = false;
                moveParticle.Stop();
            }

            //检测是否碰到地面
            var groundHit = Physics2D.Raycast(transform.position, Vector3.down, 2, groundMasks);
            if(groundHit&& groundHit.transform.TryGetComponent(out SpriteRenderer sr))
            {
                currentGradient = new ParticleSystem.MinMaxGradient(sr.color * 0.9f, sr.color * 1.2f);
                SetColor(moveParticle);
            }

            movement = player.RawMovement;
        }
        private void OnDisable()
        {
            moveParticle.Stop();
        }
        private void OnEnable()
        {
            moveParticle.Play();
        }
        private void SetColor(ParticleSystem ps)
        {
            var main = ps.main;
            main.startColor = currentGradient;
        }
        #region Animation Keys
        private static readonly int GroundedKey = Animator.StringToHash("Grounded");
        private static readonly int IdleSpeedKey = Animator.StringToHash("IdleSpeed");
        private static readonly int JumpKey = Animator.StringToHash("Jump");
        #endregion
    }
}

在Visual游戏对象的Aniamtor中添加条件 

别忘了修改参数


学习产出:

 

猜你喜欢

转载自blog.csdn.net/dangoxiba/article/details/124605648