最终效果
文章目录
前言
之前做过InputSystem+有限状态机控制俯视角人物控制:
【unity实战】使用unity的新输入系统InputSystem+有限状态机设计一个玩家状态机控制——实现玩家的待机 移动 闪避 连击 受击 死亡状态切换
这次来实现一个2d平台平台跳跃玩家控制器,平台人物控制要做的好,要考虑的问题会有很多,比如移动加速减速 多段跳 冲锋 冲锋残影 滑墙 蹬墙跳 土狼时间 预输入 攀爬 爬楼梯
素材
角色
https://clembod.itch.io/warrior-free-animation-set
环境
https://assetstore.unity.com/packages/2d/environments/pixel-art-platformer-village-props-166114
目录结构
动画配置
既然用了有限状态机,这里就不进行连线和参数配置了
检测脚本
// 检测脚本
public class PhysicsCheck : MonoBehaviour
{
[Header("地面检测")]
public Transform groundCheckPoint;
public float groundCheckRadius;
public LayerMask groundLayer;
public bool isTouchGround;
[Header("墙壁检测")]
public Transform wallCheckPoint;
public float wallCheckLength;
public LayerMask wallLayer;
public bool isTouchWall;
[Header("攀爬检测")]
public Transform climbCheckPoint;
public float climbCheckLength;
public bool isCheckClimb;
private void Update()
{
Check();
}
public void Check()
{
// 检测是否接触地面
isTouchGround = Physics2D.OverlapCircle(groundCheckPoint.position, groundCheckRadius, groundLayer);
// 检测是否接触墙壁
isTouchWall = Physics2D.Raycast(wallCheckPoint.position, transform.right * transform.localScale.x, wallCheckLength, wallLayer);
// 攀爬检测
bool isClimbTouchWall = Physics2D.Raycast(climbCheckPoint.position, transform.right * transform.localScale.x, climbCheckLength, wallLayer);
if(!isClimbTouchWall && isTouchWall){
isCheckClimb = true;
}else{
isCheckClimb = false;
}
}
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
// 在 Scene 视图中绘制检测范围
Gizmos.DrawWireSphere((Vector2)groundCheckPoint.position, groundCheckRadius);
Gizmos.DrawLine(wallCheckPoint.position, wallCheckPoint.position + Vector3.right * transform.localScale.x * wallCheckLength);
Gizmos.DrawLine(climbCheckPoint.position, climbCheckPoint.position + Vector3.right * transform.localScale.x * climbCheckLength);
}
}
配置
状态机
IState抽象基类,定义了所有状态类的基本结构
//抽象基类,定义了所有状态类的基本结构
public interface IState
{
void OnEnter();// 进入状态时的方法
void OnUpdate();// 更新方法
void OnFixedUpdate();// 固定更新方法
void OnExit();// 退出状态时的方法
}
定义状态类型枚举
// 定义状态类型枚举
public enum StateType
{
Idle, //待机
Move, //移动
Dash, //冲锋
MeleeAttack, //近战攻击
Hit, //受击
Death,//死亡
Jump,//跳跃
Fall, //下落
Land,//落地
WallSlide,//墙壁滑行
WallJump, //蹬墙跳
WallJumpFall, //蹬墙跳下落状态
MeleeAttack1,//一段近战攻击状态
MeleeAttack2,//二段近战攻击状态
ClimbBegin,//开始攀爬
Climbing,//攀爬中
ClimbEnd,//攀爬结束
CoyoteTime,//土狼时间
}
定义属性参数
[Serializable]
public class Parameter
{
[Header("属性")]
[HideInInspector] public Animator animator;
[HideInInspector] public SpriteRenderer sr;
[HideInInspector] public Rigidbody2D rb;
[HideInInspector] public PhysicsCheck check;
[Header("受伤与死亡")]
[HideInInspector] public bool isHurt; // 是否受伤
[HideInInspector] public bool isDead; // 是否死亡
}
有限状态机类
public class FSM : MonoBehaviour
{
private IState currentState; // 当前状态接口
protected Dictionary<StateType, IState> states = new Dictionary<StateType, IState>(); // 状态字典,存储各种状态
protected virtual void Awake(){
TransitionState(StateType.Idle); // 初始状态为Idle
}
protected virtual void OnEnable()
{
currentState.OnEnter();
}
protected virtual void Update()
{
currentState.OnUpdate();
}
protected virtual void FixedUpdate()
{
currentState.OnFixedUpdate();
}
// 状态转换方法
public void TransitionState(StateType type)
{
if (currentState != null)
currentState.OnExit();// 先调用退出方法
currentState = states[type]; // 更新当前状态为指定类型的状态
currentState.OnEnter(); // 调用新状态的进入方法
}
// 翻转角色
public virtual void FlipTo() {}
}
玩家有限状态机
玩家状态基类
public class PlayerState : IState
{
protected PlayerFSM manager;
protected PlayerParameter parameter;
protected string animationName;
protected AnimatorStateInfo animatorStateInfo; // 动画状态信息
//状态切换计时器
float stateStartTime;
protected float StateDuration => Time.time - stateStartTime;
public PlayerState(PlayerFSM manager, string animationName)
{
this.manager = manager;
this.parameter = manager.parameter;
this.animationName = animationName;
}
public virtual void OnEnter()
{
stateStartTime = Time.time;
//将当前动画平滑过渡到名为animationName的动画状态,并且过渡持续时间为 0.1 秒
parameter.animator.CrossFade(animationName, 0.1f);
}
public virtual void OnUpdate() {
animatorStateInfo = parameter.animator.GetCurrentAnimatorStateInfo(0);// 获取当前动画状态信息
if(parameter.rb.velocity.y < 0.1f && parameter.check.isTouchGround){
parameter.currentJumpNum = 0;//接触地面重置跳跃次数
}
}
public virtual void OnFixedUpdate() { }
public virtual void OnExit() { }
//动画是否播放完成
public bool PlayComplete(){
// 当前动画是否播放了95%
if (animatorStateInfo.normalizedTime >= .95f && animatorStateInfo.IsName(animationName)) return true;
return false;
}
//移动
public void Move()
{
// 根据输入方向移动角色
SetVelocity(parameter.inputDirection.x * parameter.currentSpeed, parameter.rb.velocity.y);
//翻转
manager.FlipTo();
}
//设置速度
public void SetVelocity(float setVelocityX, float setVelocityY){
parameter.rb.velocity = new Vector2(setVelocityX, setVelocityY);
}
}
玩家参数
public class PlayerParameter : Parameter
{
[Header("移动")]
public float normalSpeed = 6f; // 默认移动速度
public float jumpSpeed = 4f; // 跳跃时的移动速度
public float acceration = 40f;//加速过渡值
public float deceleration = 40f;//减速过渡值
[HideInInspector] public float Gravity;//重力
[HideInInspector] public int facingDirection = 1; // 角色面向的方向,1右 -1左
[HideInInspector] public Vector2 inputDirection; // 输入的移动方向
[HideInInspector] public float currentSpeed; // 当前移动速度
//如果不触壁有输入 或者 触墙反方向移动才可以移动
public bool isMove => (check.isTouchWall && inputDirection.x == -facingDirection) ||
(!check.isTouchWall && inputDirection.x != 0);
[Header("跳跃")]
public float jumpForce;//跳跃力
public int jumpNum = 1;//跳跃次数
[HideInInspector] public int currentJumpNum;//当前跳跃次数
public bool isClickJump;//是否按下跳跃
public bool isJump => isClickJump && currentJumpNum < jumpNum;
public bool isClickStopJump;
public bool isFall => (rb.velocity.y < 0.1f && !check.isTouchGround) || (isClickStopJump && !check.isTouchGround);
[Header("墙壁滑行")]
public float wallSlideSpeed = -1f;//滑行速度
public float wallSlideSpeedUp = -10f;//加速下滑
public bool isWallSlide => check.isTouchWall && !check.isTouchGround && facingDirection == inputDirection.x;
[Header("蹬墙跳")]
public float wallJumpForce; // 蹬墙跳时施加的力
[Header("近战攻击")]
public Vector2 attackMovePostion;//移动补偿
[HideInInspector] public bool isClickMeleeAttack;//是否点击近战攻击
[Header("冲锋")]
public Vector2 dashPostion;//冲锋速度
public float dashTime = 0.5f; //冲锋时间
public float dashCD = 1f; //冲锋CD
public GameObject shadowPrefab; //冲锋残影
[HideInInspector] public bool isClickDash;
[HideInInspector] public bool isDashing;//是否正在冲锋
[HideInInspector] public bool isWaitDash;//是否CD冷却
public bool isDash => isClickDash && !isDashing && !isWaitDash;
[Header("攀爬")]
[HideInInspector] public bool isClimbing;
public bool isClimb => check.isCheckClimb && inputDirection.y == 0;
[Header("土狼时间")]
public float coyoteTime = 0.1f;
[Header("预输入")]
public float waitJumpInputBufferTime = 0.2f;//跳跃输入缓冲时间
public bool HasJumpInputBuffer;//是否存在跳跃缓冲
}
玩家有限状态机类
public class PlayerFSM : FSM
{
public PlayerParameter parameter; // 状态机参数
protected override void Awake()
{
parameter.rb = GetComponent<Rigidbody2D>();
parameter.animator = GetComponent<Animator>();
parameter.sr = GetComponent<SpriteRenderer>();
parameter.check = GetComponent<PhysicsCheck>();
parameter.Gravity = parameter.rb.gravityScale;
// 初始化各个状态,并添加到状态字典中
states.Add(StateType.Idle, new PlayerIdleState(this, "Idle"));
states.Add(StateType.Move, new PlayerMoveState(this, "Run"));
states.Add(StateType.Jump, new PlayerJumpState(this, "Jump"));
states.Add(StateType.Fall, new PlayerFallState(this, "Fall"));
states.Add(StateType.Land, new PlayerLandState(this, "Land"));
states.Add(StateType.WallSlide, new PlayerWallSlideState(this, "WallSlide"));
states.Add(StateType.WallJump, new PlayerWallJumpState(this, "Jump"));
states.Add(StateType.WallJumpFall, new PlayerWallJumpFallState(this, "Fall"));
states.Add(StateType.MeleeAttack1, new PlayerMeleeAttack1State(this, "MeleeAttack1"));
states.Add(StateType.MeleeAttack2, new PlayerMeleeAttack2State(this, "MeleeAttack2"));
states.Add(StateType.Dash, new PlayerDashState(this, "Dash"));
states.Add(StateType.ClimbBegin, new PlayerClimbBeginState(this, "ClimbBegin"));
states.Add(StateType.Climbing, new PlayerClimbingState(this, "Climbing"));
states.Add(StateType.CoyoteTime, new PlayerCoyoteTimeState(this, "Run"));
base.Awake();
}
protected override void Update()
{
base.Update();
}
// 翻转角色
public override void FlipTo()
{
if (parameter.inputDirection.x < 0)
{
parameter.facingDirection = -1;
transform.localScale = new Vector3(-Mathf.Abs(transform.localScale.x), transform.localScale.y, transform.localScale.z);
}
if (parameter.inputDirection.x > 0)
{
parameter.facingDirection = 1;
transform.localScale = new Vector3(Mathf.Abs(transform.localScale.x), transform.localScale.y, transform.localScale.z);
}
}
public void OnStartCoroutine(IEnumerator name){
StartCoroutine(name);
}
}
玩家控制脚本
public class PlayerController : PlayerFSM {
InputSystem inputSystem;
protected override void Awake() {
base.Awake();
// 初始化输入控制
inputSystem = new InputSystem();
inputSystem.Player.Move.performed += Move;
inputSystem.Player.Move.canceled += StopMove;
inputSystem.Player.Jump.started += Jump;
inputSystem.Player.Jump.canceled += StopJump;
inputSystem.Player.MeleeAttack.started += MeleeAttack;
// inputSystem.Player.MeleeAttack.canceled += StopMeleeAttack;
inputSystem.Player.Dash.started += Dash;
inputSystem.Player.Dash.canceled += StopDash;
SwitchActionMap(inputSystem.Player); // 切换到游戏操作的输入映射
}
void OnDisable()
{
// 禁用所有输入
inputSystem.Disable();
}
// 切换操作映射
public void SwitchActionMap(InputActionMap actionMap)
{
inputSystem.Disable(); // 禁用当前的输入映射
actionMap.Enable(); // 启用新的输入映射
}
//移动
public void Move(InputAction.CallbackContext context)
{
parameter.inputDirection = inputSystem.Player.Move.ReadValue<Vector2>();
}
//停止移动
public void StopMove(InputAction.CallbackContext context)
{
parameter.inputDirection = Vector2.zero;
}
// 跳跃
public void Jump(InputAction.CallbackContext context)
{
parameter.isClickJump = true;
parameter.isClickStopJump = false;
//预输入计时
StopCoroutine(PreInputExitTime());
StartCoroutine(PreInputExitTime());
}
IEnumerator PreInputExitTime()
{
parameter.HasJumpInputBuffer = true;
yield return new WaitForSeconds(parameter.waitJumpInputBufferTime); // 等待
parameter.HasJumpInputBuffer = false;
}
//停止移动
public void StopJump(InputAction.CallbackContext context)
{
parameter.isClickStopJump = true;
parameter.isClickJump = false;
}
//近战攻击
public void MeleeAttack(InputAction.CallbackContext context)
{
parameter.isClickMeleeAttack = true;
}
private void Dash(InputAction.CallbackContext context)
{
parameter.isClickDash = true;
}
private void StopDash(InputAction.CallbackContext context)
{
parameter.isClickDash = false;
}
}
配置
定义人物不同状态
待机
public class PlayerIdleState : PlayerState
{
public PlayerIdleState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
parameter.isClickMeleeAttack = false;
}
public override void OnUpdate()
{
base.OnUpdate();
if (parameter.isDash)
{
manager.TransitionState(StateType.Dash);
}
if (parameter.isMove)
{
manager.TransitionState(StateType.Move);
}
if (parameter.isJump)
{
manager.TransitionState(StateType.Jump);
}
if (parameter.isClickMeleeAttack)
{
manager.TransitionState(StateType.MeleeAttack1);
}
parameter.currentSpeed = Mathf.MoveTowards(parameter.currentSpeed, 0f, parameter.deceleration * Time.deltaTime);
}
public override void OnFixedUpdate() {
base.OnFixedUpdate();
SetVelocity(parameter.currentSpeed * parameter.facingDirection, parameter.rb.velocity.y);//减速
}
public override void OnExit() {
base.OnExit();
}
}
移动
public class PlayerMoveState : PlayerState
{
public PlayerMoveState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
parameter.isClickMeleeAttack = false;
}
public override void OnUpdate()
{
base.OnUpdate();
if (parameter.isDash)
{
manager.TransitionState(StateType.Dash);
}
if (!parameter.isMove)
{
manager.TransitionState(StateType.Idle);
}
if (parameter.isJump)
{
manager.TransitionState(StateType.Jump);
}
if (parameter.isFall)
{
manager.TransitionState(StateType.CoyoteTime);
}
if (parameter.check.isTouchWall && !parameter.check.isTouchGround)
{
manager.TransitionState(StateType.WallSlide);
}
if(parameter.isClickMeleeAttack){
manager.TransitionState(StateType.MeleeAttack1);
}
parameter.currentSpeed = Mathf.MoveTowards(parameter.currentSpeed, parameter.normalSpeed, parameter.acceration * Time.deltaTime);//加速
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
Move();
}
public override void OnExit()
{
base.OnExit();
}
}
跳跃
public class PlayerJumpState : PlayerState
{
public PlayerJumpState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
parameter.HasJumpInputBuffer = false;
parameter.isClickJump = false;
parameter.currentJumpNum ++;
Debug.Log(parameter.currentJumpNum);
SetVelocity(parameter.rb.velocity.x, parameter.jumpForce);// 设置跳跃速度
}
public override void OnUpdate()
{
base.OnUpdate();
if (parameter.isClimb)
{
manager.TransitionState(StateType.ClimbBegin);
}
if (parameter.isDash)
{
manager.TransitionState(StateType.Dash);
}
if (parameter.isFall)
{
manager.TransitionState(StateType.Fall);
}
if (parameter.isWallSlide)
{
manager.TransitionState(StateType.WallSlide);
}
if (parameter.rb.velocity.y < 0.1f && parameter.check.isTouchGround)
{
manager.TransitionState(StateType.Land);
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
Move();
}
public override void OnExit()
{
base.OnExit();
}
}
下落状态
public class PlayerFallState : PlayerState
{
public PlayerFallState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
parameter.currentSpeed = parameter.jumpSpeed;
SetVelocity(parameter.rb.velocity.x, 0);
}
public override void OnUpdate()
{
base.OnUpdate();
if (parameter.isClimb)
{
manager.TransitionState(StateType.ClimbBegin);
}
if (parameter.isDash)
{
manager.TransitionState(StateType.Dash);
}
if (parameter.check.isTouchGround)
{
manager.TransitionState(StateType.Land);
}
if (parameter.isJump)
{
manager.TransitionState(StateType.Jump);
}
if (parameter.isWallSlide)
{
manager.TransitionState(StateType.WallSlide);
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
Move();
}
public override void OnExit() {
base.OnExit();
}
}
落地状态
public class PlayerLandState : PlayerState
{
public PlayerLandState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
}
public override void OnUpdate()
{
base.OnUpdate();
if (PlayComplete())
{
manager.TransitionState(StateType.Idle);
}
if (parameter.HasJumpInputBuffer || parameter.isJump)
{
manager.TransitionState(StateType.Jump);
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
}
public override void OnExit() {
base.OnExit();
}
}
墙壁滑行状态
public class PlayerWallSlideState : PlayerState
{
public PlayerWallSlideState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
parameter.rb.gravityScale = 0;
SetVelocity(0, 0);//速度清0
}
public override void OnUpdate()
{
base.OnUpdate();
if (parameter.isClimb)
{
manager.TransitionState(StateType.ClimbBegin);
}
if (parameter.check.isTouchGround)
{
manager.TransitionState(StateType.Idle);
}
if (parameter.isClickJump)
{
manager.TransitionState(StateType.WallJump);
}
if (!parameter.check.isTouchWall)
{
manager.TransitionState(StateType.Fall);
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
//应用滑墙速度 按下加速下滑
if (parameter.inputDirection.y < 0)
{
SetVelocity(0, parameter.wallSlideSpeedUp);// 限制垂直速度以应用墙壁滑行速度
}
else
{
SetVelocity(0, parameter.wallSlideSpeed);
}
}
public override void OnExit()
{
base.OnExit();
parameter.rb.gravityScale = parameter.Gravity;
}
}
蹬墙跳状态
public class PlayerWallJumpState : PlayerState
{
public PlayerWallJumpState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
// parameter.isWallJump = true;
// parameter.isClickJump = false;
parameter.currentSpeed = parameter.normalSpeed;
SetVelocity(parameter.wallJumpForce * -parameter.facingDirection, parameter.jumpForce);// 设置跳跃速度
}
public override void OnUpdate()
{
base.OnUpdate();
if (parameter.isDash)
{
manager.TransitionState(StateType.Dash);
}
if (parameter.isFall)
{
manager.TransitionState(StateType.WallJumpFall);
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
// 输入方向一定是面朝反方向才可控制方向,禁止单墙跳,且确保不输入可以从墙上弹开
if(parameter.inputDirection.x == -parameter.facingDirection) Move();
}
public override void OnExit()
{
base.OnExit();
}
}
蹬墙跳下落状态
public class PlayerWallJumpFallState : PlayerState
{
public PlayerWallJumpFallState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
}
public override void OnUpdate()
{
base.OnUpdate();
if (parameter.isDash)
{
manager.TransitionState(StateType.Dash);
}
if (parameter.check.isTouchGround)
{
manager.TransitionState(StateType.Land);
}
if (parameter.isWallSlide)
{
manager.TransitionState(StateType.WallSlide);
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
//可以翻转
manager.FlipTo();
}
public override void OnExit() {
base.OnExit();
}
}
一段近战攻击状态
public class PlayerMeleeAttack1State : PlayerState
{
public PlayerMeleeAttack1State(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
parameter.isClickMeleeAttack = false;
//移动补偿
SetVelocity(parameter.attackMovePostion.x * parameter.facingDirection, parameter.attackMovePostion.y);
}
public override void OnUpdate()
{
base.OnUpdate();
if (parameter.isDash)
{
manager.TransitionState(StateType.Dash);
}
if (PlayComplete())
{
if (parameter.isClickMeleeAttack)
{
manager.TransitionState(StateType.MeleeAttack2);
}else{
manager.TransitionState(StateType.Idle);
}
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
}
public override void OnExit()
{
base.OnExit();
//攻击执行完可以转向,比如第二段攻击往后打,手感更好
manager.FlipTo();
}
}
二段近战攻击状态
public class PlayerMeleeAttack2State : PlayerState
{
public PlayerMeleeAttack2State(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
parameter.isClickMeleeAttack = false;
// parameter.currentSpeed = parameter.meleeAttackSpeed;
//移动补偿
SetVelocity(parameter.attackMovePostion.x * parameter.facingDirection, parameter.attackMovePostion.y);
}
public override void OnUpdate()
{
base.OnUpdate();
if (parameter.isDash)
{
manager.TransitionState(StateType.Dash);
}
if (PlayComplete())
{
manager.TransitionState(StateType.Idle);
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
// Move();
}
public override void OnExit()
{
base.OnExit();
}
}
冲锋状态
public class PlayerDashState : PlayerState
{
public PlayerDashState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
// parameter.isDashing = true;
parameter.isWaitDash = true;
manager.OnStartCoroutine(DashExitTime());
manager.OnStartCoroutine(DashWaitTime());
}
public override void OnUpdate()
{
base.OnUpdate();
if (parameter.isWallSlide)
{
manager.TransitionState(StateType.WallSlide);
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
SetVelocity(parameter.dashPostion.x * parameter.facingDirection, parameter.dashPostion.y);
//生成影子
ObjectPool.GetObject(parameter.shadowPrefab);
}
public override void OnExit()
{
base.OnExit();
// parameter.isDashing = false;
}
IEnumerator DashExitTime()
{
yield return new WaitForSeconds(parameter.dashTime); // 等待
manager.TransitionState(StateType.Idle);
}
IEnumerator DashWaitTime()
{
yield return new WaitForSeconds(parameter.dashCD); // 等待
parameter.isWaitDash = false;
}
}
土狼时间状态
public class PlayerCoyoteTimeState : PlayerState
{
public PlayerCoyoteTimeState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
parameter.rb.gravityScale = 0;
}
public override void OnUpdate()
{
base.OnUpdate();
if (parameter.isJump)
{
manager.TransitionState(StateType.Jump);
}
if (StateDuration >= parameter.coyoteTime || !parameter.isMove)
{
manager.TransitionState(StateType.Fall);
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
}
public override void OnExit()
{
base.OnExit();
parameter.rb.gravityScale = parameter.Gravity;
}
}
攀爬开始状态
public class PlayerClimbBeginState : PlayerState
{
public PlayerClimbBeginState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
parameter.rb.gravityScale = 0;
SetVelocity(0, 0);
}
public override void OnUpdate()
{
base.OnUpdate();
if (PlayComplete())
{
manager.TransitionState(StateType.Climbing);
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
}
public override void OnExit()
{
base.OnExit();
parameter.rb.gravityScale = parameter.Gravity;
}
}
攀爬进行状态
public class PlayerClimbingState : PlayerState
{
public PlayerClimbingState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
parameter.isClimbing = true;
parameter.rb.gravityScale = 0;
SetVelocity(0, 0);
}
public override void OnUpdate()
{
base.OnUpdate();
//按下掉落
if (parameter.inputDirection.y < 0)
{
manager.TransitionState(StateType.WallSlide);
}
//按上
if (parameter.inputDirection.y > 0)
{
parameter.isClickStopJump = false;//保证执行完整的长跳
manager.TransitionState(StateType.Jump);
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
}
public override void OnExit()
{
base.OnExit();
parameter.rb.gravityScale = parameter.Gravity;
parameter.isClimbing = false;
}
}
功能细节解析
计时器
这里使用了两种计时器方式,其实可以酌情选择一种即可
一种是在状态切换会自动重置时间的计时器,我在土狼时间里就用到了它
# 计时器,写在了PlayerState里
//状态切换计时器
float stateStartTime;
protected float StateDuration => Time.time - stateStartTime;
# 在OnEnter,及状态切换会自动重置时间
stateStartTime = Time.time;
一种是携程的方式,写在了PlayerFSM里,冲锋时间和CD就用到了它
public void OnStartCoroutine(IEnumerator name){
StartCoroutine(name);
}
移动撞墙停止移动
如果不触壁有输入 或者 触墙反方向移动才可以移动
//如果不触壁有输入 或者 触墙反方向移动才可以移动
public bool isMove => (check.isTouchWall && inputDirection.x == -facingDirection) ||
(!check.isTouchWall && inputDirection.x != 0);
效果
移动加速和减速
如果我们只是简单的在玩家按下移动方向移动角色,松开按键停止角色,那么手感会非常生硬,现实中没有物体可以在瞬间实现加速和减速的过程。所有我们应该给玩家的移动添加加速和减速过程,这个过程可以很短,但只要有就会产生不一样的感觉。
这里使用的是Mathf.MoveTowards,会将速度从指定速度缓慢加减到目标的速度
# 加速
parameter.currentSpeed = Mathf.MoveTowards(parameter.currentSpeed, parameter.normalSpeed, parameter.acceration * Time.deltaTime);//加速
# 减速
parameter.currentSpeed = Mathf.MoveTowards(parameter.currentSpeed, 0f, parameter.deceleration * Time.deltaTime);
效果
长跳和短跳
状态机里要实现长跳和短跳非常简单,只要玩家在松开跳跃按键时,马上且为落地状态即可,记得进入落地状态要清除y轴的速度
public bool isFall => (rb.velocity.y < 0.1f && !check.isTouchGround) || (isClickStopJump && !check.isTouchGround);
效果
多段跳
新增跳跃次数和当前跳跃次数变量,isJump 时进行跳跃次数判定即可
public int jumpNum = 1;//跳跃次数
[HideInInspector] public int currentJumpNum;//当前跳跃次数
public bool isClickJump;//是否按下跳跃
public bool isJump => isClickJump && currentJumpNum < jumpNum;
修改jumpNum = 2
,二段跳效果
跳跃优化——土狼时间
跳跃是平台游戏至关重要的一个功能,在平台游戏的游玩过程中,玩家基本上全程都是在跳的,也因此对跳跃手感的优化我们必须要注重。
土狼时间就是其中的一种跳跃优化,它是对玩家离开平台时的可跳跃时间的一种延长,具体的表现为,当玩家角色离开平台时,在一小段时间里,虽然玩家已经不再有触地判定了,但玩家角色不会马上进入掉落状态,并且在这时候玩家依然可以起跳,这就是所谓的土狼时间。
介绍
至于为什么叫这个名字(土狼时间)呢?其实这个名词来自于华纳兄弟动画公司,一部古老的动画系列影片《Looney Tunes》
里面有一个角色叫Wale E. Coyote的土狼,在影片中经常会出现这么有趣的一幕,歪心土狼跑着跑着就离开了悬崖
但却没有马上往下掉(我们熟知的《猫和老鼠》也经常有这样的一幕),于是后来的游戏开发者们就将这个类似的功能称为土狼时间了
实现
这里我把土狼时间设置为一个新的状态,在进行这个状态时,玩家的重力设置被设置为0,退出时回复重力
在土狼时间里,按下跳跃键会进行跳跃
if (parameter.isJump)
{
manager.TransitionState(StateType.Jump);
}
土狼时间结束,或者松开移动键,进入掉落状态
if (StateDuration >= parameter.coyoteTime || !parameter.isMove)
{
manager.TransitionState(StateType.Fall);
}
不使用土狼时间,玩家在掉落过程是不允许起跳的
为了能看出效果,我先把coyoteTime
土狼时间设置为1
coyoteTime
土狼时间可以根据自己的感觉设定,这里我设置为0.1
预输入
介绍
在大部分强调操作的游戏类中,玩家对输普遍存在着这么一个问题,那就是对玩家按键时机的处理,由于玩家个体之间存在一定的差异性,比如玩家的反应时间,玩家对输入设备的熟悉程度,玩家对游戏机制的理解等等,都会造成玩家输入不可能百分之百准确。
拿我们这个简单的小项目来说,想要触发跳跃这个动作,我们只需要按下跳跃键,但这是有条件限制的,那就是我们的角色必须处于地面上才行。
这时候就产生了一个问题,当玩家在处于掉落状态时,由于上述的一些原因,玩家可能会因为判断错误而提前按下跳跃键,而这时候如果还没产生触地判定,那么这一次的跳跃输入就会被忽略,导致玩家并不会跳起来,这时候玩家就会骂了:我明明按下了跳跃键却跳不起来,垃圾游戏!
,这就是玩家们所谓的“吞键
”了,这种情况是很容易让玩家对我们制作的游戏幸生厌恶感的,那么我们应该如何处理这种情况呢?
既然触发条件比较严格,那么很明显的我们应该稍微放宽跳跃功能的触发条件,当玩家提前按下跳跃键时,在规定的时间内如果玩家触地我们也应该触发跳跃功能,一般的我们把这个机制称为玩家输入的预输入窗口,英文叫Input Buffer,也就是输入缓冲。
实现
新增变量,规定跳跃输入缓冲时间和是否存在跳跃缓冲
[Header("预输入")]
public float waitJumpInputBufferTime = 0.5f;//跳跃输入缓冲时间
public bool HasJumpInputBuffer;//是否存在跳跃缓冲
跳跃落地状态切换为跳跃状态,多加一层判断,如果存在跳跃缓冲时也可以起跳
if (parameter.HasJumpInputBuffer || parameter.isJump)
{
manager.TransitionState(StateType.Jump);
}
跳跃开始时,将跳跃缓冲设置为false
parameter.HasJumpInputBuffer = false;
玩家跳跃输入,添加预输入计时,超过跳跃输入缓冲时间,则将跳跃缓冲设置为false
// 跳跃
public void Jump(InputAction.CallbackContext context)
{
parameter.isClickJump = true;
parameter.isClickStopJump = false;
//预输入计时
StopCoroutine(PreInputExitTime());
StartCoroutine(PreInputExitTime());
}
IEnumerator PreInputExitTime()
{
parameter.HasJumpInputBuffer = true;
yield return new WaitForSeconds(parameter.waitJumpInputBufferTime); // 等待
parameter.HasJumpInputBuffer = false;
}
效果
攻击
多段攻击
这里我动画没有进行连线,所以每一个段攻击我都添加为一个新状态
通过animatorStateInfo.normalizedTime判断动画播放进度
//动画是否播放完成
public bool PlayComplete(){
// 当前动画是否播放了95%
if (animatorStateInfo.normalizedTime >= .95f && animatorStateInfo.IsName(animationName)) return true;
return false;
}
如果动画播放95%之前再次按下攻击键则进入下一段攻击
if (PlayComplete())
{
if (parameter.isClickMeleeAttack)
{
manager.TransitionState(StateType.MeleeAttack2);
}else{
manager.TransitionState(StateType.Idle);
}
}
效果
攻击移动补偿和攻击突进
攻击时我们不希望玩家还能移动,但是简单粗暴的禁止移动又会影响我们的攻击操作手感,在死亡细胞等游戏中,攻击时会朝前方以一个较小的速度移动,这样可以一定程度的补偿攻击时无法移动的缺陷
public Vector2 attackMovePostion;//移动补偿
//移动补偿
SetVelocity(parameter.attackMovePostion.x * parameter.facingDirection, parameter.attackMovePostion.y);
如果想的话甚至可以把比如最后一段攻击改为突进技能
攻击转向
我们肯定不希望玩家在攻击过程还能转向,但是简单的禁止攻击转向是不可取的,一般我们希望每一段攻击切换的间隙可以进行转向
我们可以在每一段攻击结束时调用FlipTo,控制转向
public override void OnExit()
{
base.OnExit();
//攻击执行完可以转向,比如第二段攻击往后打,手感更好
manager.FlipTo();
}
效果
攻击速度
可以通过改变攻击动画的播放速度来实现攻速控制
比如增加3倍攻速
parameter.animator.speed = 3;
记得退出时将速度设置回去
parameter.animator.speed = 1;
如果是重武器,比如大锤,你可以酌情把攻速降低,以达到更好的效果
马上改变攻击方向
比如我正在向右跑,但是又想向左边发起攻击
进入攻击状态时设置转向即可,其实前面移动补偿已经实现了
public override void OnEnter()
{
base.OnEnter();
parameter.isClickMeleeAttack = false;
//移动补偿
SetVelocity(parameter.attackMovePostion.x * parameter.facingDirection, parameter.attackMovePostion.y);
}
滑墙
简单滑墙
滑墙的实现很简单,判断玩家触碰墙壁,不在地面且x输入方向为玩家面向方向则进入滑墙状态
public bool isWallSlide => check.isTouchWall && !check.isTouchGround && facingDirection == inputDirection.x;
记得在进入滑墙状态时,清除玩家重力和速度
parameter.rb.gravityScale = 0;
SetVelocity(0, 0);//速度清0
退出时恢复重力
parameter.rb.gravityScale = parameter.Gravity;
效果
按下加快下滑
//应用滑墙速度 按下加速下滑
if (parameter.inputDirection.y < 0)
{
SetVelocity(0, parameter.wallSlideSpeedUp);
}
else
{
SetVelocity(0, parameter.wallSlideSpeed);
}
效果
蹬墙跳
简单蹬墙跳
这里我把蹬墙跳设置为一个新的状态,在滑墙状态下点击跳跃按键进入蹬墙跳状态
if (parameter.isClickJump)
{
manager.TransitionState(StateType.WallJump);
}
今天蹬墙跳状态时,设置往角色面向的反方向跳跃
SetVelocity(parameter.wallJumpForce * -parameter.facingDirection, parameter.jumpForce);// 设置跳跃速度
禁止单面蹬墙跳
禁止单面蹬墙跳重点在于控制玩家在空中的移动
这里我加入一个新的状态叫蹬墙跳下落状态,当蹬墙跳满足下落条件时,进入蹬墙跳下落状态,蹬墙跳下落状态和普通下落状态的区别就是不可以控制角色角色移动。但是可以控制转向
if (parameter.isFall)
{
manager.TransitionState(StateType.WallJumpFall);
}
我不希望蹬墙跳可以单面上墙,且确保不输入点击跳跃可以从墙上弹开,
// 输入方向一定是面朝反方向才可控制方向,禁止单墙跳,且确保不输入可以从墙上弹开
if(parameter.inputDirection.x == -parameter.facingDirection) Move();
效果
攀爬
攀爬上墙
攀爬实现的原理就是在玩家的墙壁检测上再加一个头部墙壁检测,当头部检测为false,墙壁检测为true,则进入攀爬状态
if(!isClimbTouchWall && isTouchWall){
isCheckClimb = true;
}else{
isCheckClimb = false;
}
进入攀爬状态需要把重力和速度都清0,退出时还原重力
parameter.rb.gravityScale = 0;
SetVelocity(0, 0);
效果
攀爬结束
攀爬状态,如果玩家按下掉落和按上跳上墙(这里上墙其实可以另外加一个攀爬上墙状态会更好,这里没用攀爬上墙动画,就没有做了)
//按下掉落
if (parameter.inputDirection.y < 0)
{
manager.TransitionState(StateType.WallSlide);
}
//按上跳上
if (parameter.inputDirection.y > 0)
{
parameter.isClickStopJump = false;//保证执行完整的长跳
manager.TransitionState(StateType.Jump);
}
效果
冲锋
冲锋状态
定义冲锋的速度时间和CD
[Header("冲锋")]
public Vector2 dashPostion;//冲锋速度
public float dashTime = 0.5f; //冲锋时间
public float dashCD = 1f; //冲锋CD
[HideInInspector] public bool isClickDash;
[HideInInspector] public bool isDashing;//是否正在冲锋
[HideInInspector] public bool isWaitDash;//是否CD冷却
public bool isDash => isClickDash && !isDashing && !isWaitDash;
进入冲锋状态时,启动携程,冲锋结束进入待机状态和冲锋CD结束isWaitDash = false才可以再次发起冲锋
manager.OnStartCoroutine(DashExitTime());
manager.OnStartCoroutine(DashWaitTime());
IEnumerator DashExitTime()
{
yield return new WaitForSeconds(parameter.dashTime); // 等待
manager.TransitionState(StateType.Idle);
}
IEnumerator DashWaitTime()
{
yield return new WaitForSeconds(parameter.dashCD); // 等待
parameter.isWaitDash = false;
}
冲锋残影
参考:
效果
冲锋滑墙
冲锋和待机状态同时加入滑墙状态切换,保证冲锋结束和途中都可以上墙
if (parameter.isWallSlide)
{
manager.TransitionState(StateType.WallSlide);
}
效果
爬楼梯
绘制梯子,给梯子配置触发器区域和层级
修改PhysicsCheck新增梯子检测
[Header("梯子检测")]
public Transform ladderCheckPoint;
public float ladderCheckRadius;
public LayerMask ladderLayer;
public bool isTouchLadder;
[HideInInspector] public Transform ladderTransform;
// 检测是否接触梯子
Collider2D collider = Physics2D.OverlapCircle(ladderCheckPoint.position, ladderCheckRadius, ladderLayer);
if (collider != null)
{
ladderTransform = collider.transform;
isTouchLadder = true;
}
else
{
isTouchLadder = false;
ladderTransform = null;
}
配置
修改PlayerParameter,新增梯子参数
[Header("梯子")]
public float ladderSpeed;//爬梯子速度
public bool isLadder => check.isTouchLadder && inputDirection.y != 0;
新增梯子状态
public class PlayerLadderState : PlayerState
{
public PlayerLadderState(PlayerFSM manager, string animationName) : base(manager, animationName) { }
public override void OnEnter()
{
base.OnEnter();
parameter.rb.gravityScale = 0;
SetVelocity(0, 0);//速度清0
//固定玩家x轴位置在梯子中间
manager.transform.position = new Vector2(parameter.check.ladderTransform.position.x, manager.transform.position.y);
}
public override void OnUpdate()
{
base.OnUpdate();
if ((parameter.check.isTouchGround && parameter.inputDirection.y <= 0) || !parameter.check.isTouchLadder)
{
manager.TransitionState(StateType.Idle);
}
if (parameter.isClickJump)
{
manager.TransitionState(StateType.Jump);
}
}
public override void OnFixedUpdate()
{
base.OnFixedUpdate();
//没有上下移动,停止暂停玩家动画播放
if(parameter.inputDirection.y == 0){
parameter.animator.speed = 0;
SetVelocity(0, 0);
}else{
parameter.animator.speed = 1;
SetVelocity(0, parameter.ladderSpeed * parameter.inputDirection.y);
}
}
public override void OnExit()
{
base.OnExit();
parameter.animator.speed = 1;
parameter.rb.gravityScale = parameter.Gravity;
}
}
新增状态切换,比如待机,下落,移动都可以切换为爬梯子状态
if(parameter.isLadder){
manager.TransitionState(StateType.Ladder);
}
效果
结束
注意,这里主要是对2d平台控制的一些探究,所以射击的状态会很多,实际项目使用可能不会需要这多状态,而且有一些本身就存在手感冲突,比如多段跳 长短跳和攀爬之间,实际项目还是得按需要进行选择
源码
很遗憾源码我并不想
免费
分享,我也建议大家能自己手动去敲代码
,逐步实现和理解每一块功能。项目实现所涉及的主要功能思路和代码我也已经毫无保留
的分享在文章中了,当然,如果你真的需要的话,源码我也放出来了,收个辛苦费,就当作你对我不断创作的支持。力量随微,心暖人。您的每一次支持都是我创作的最大动力!!!
整理好了我会放上来
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,以便我第一时间收到反馈,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,出于兴趣爱好,最近开始自学unity,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!php是工作,unity是生活!如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~