Unity2D Day0 初步实现角色操作

DEMO构思

少女坠落到殖民地,与大量涌入的侵染无人机进行战斗;

快节奏横板清版过关

攻击手段

玩家主要武器:“斩舰刀”,用于格挡/弹反攻击

其他武器:侵染无人机掉落的各类人类制式装备(如各类枪械)

这些武器无法装填,打空后直接丢掉。但是每个类别可以储存很多把

特殊技能:可以装备3个特殊技能(飞弹,激光,大口径炮等),消耗MP使用

基本操作

走,跑,滑铲(翻滚),格挡,弹反

ad行走,w跳跃,s蹲下,空格翻滚,shift进入奔跑

无武器时:左键斩舰刀攻击,右键格挡

手持武器时:左键武器射击,右键斩舰刀攻击,r丢掉手中武器

铺设底层

首先创建控制角色的状态机。对几个基类进行创建:

//Player.cs
 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 
 public class Player : MonoBehaviour
 {
   // Start is called before the first frame update
   void Start()
   {
     
   }
 
   // Update is called once per frame
   void Update()
   {
     
   }
 }
//PlayerState.cs
 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 
 public abstract class PlayerState
 {
 
   public string Name { get; }
   public PlayerStateMachine StateMachine { get; }
 
   public Player Player { get; }
   public abstract void OnEnter();
   public abstract void OnExit();
   public abstract void OnUpdate();
 }
using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 
 public class PlayerStateMachine 
 {
 }

Player:玩家类,之后会将玩家大部分相关数据和操作放在这里

PlayerState:玩家当前状态的基类

PlayerStateMachine:玩家状态机,用于切换状态和根据状态更新玩家回响形态了

不像虚幻,unity没有状态机这种集成好的功能,因此需要自己编写一套。不过虚幻状态机我也用不太来,之前也是自己写的所以问题不大了……

接下来对玩家状态进行分析。既然是基础的操作,那么先把移动做出来:

Idle:静止态,等待操作

Move:移动态

Sprint:冲刺,比移动更快

接下来完善玩家类,状态机和三种状态:

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

 public class PlayerStateIdle : PlayerState
 {
   public PlayerStateIdle(string name, PlayerStateMachine stateMachine, Player player) : base(name, stateMachine, player)
   {
   }

   public override void OnEnter()
   {
   }

   public override void OnExit()
   {
   }

   public override void OnUpdate()
   {
   }
 }
using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;

 public class PlayerStateMove : PlayerState
 {
   public PlayerStateMove(string name, PlayerStateMachine playerStateMachine, Player player):base(name, playerStateMachine, player)
   {

   }
   public override void OnEnter()
   {
   }

   public override void OnExit()
   {
   }

   public override void OnUpdate()
   {
   }
 }
using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;

 public class PlayerStateSprint : PlayerState
 {
   public PlayerStateSprint(string name, PlayerStateMachine stateMachine, Player player) : base(name, stateMachine, player)
   {
   }

   public override void OnEnter()
   {
   }

   public override void OnExit()
   {
   }

   public override void OnUpdate()
   {
   }
 }
using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;

 public class Player : MonoBehaviour
 {
   public PlayerStateMachine StateMachine;
   private void Awake()
   {
     StateMachine = new PlayerStateMachine();
   }
   // Start is called before the first frame update
   void Start()
   {
     StateMachine.Init(this);
   }

   // Update is called once per frame
   void Update()
   {
     
   }
   private void FixedUpdate()
   {
     if (StateMachine != null)
     {
       StateMachine.OnUpdate();
     }
   }
 }
using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;

 public class PlayerStateMachine 
 {
   public PlayerStateIdle PlayerStateIdle;
   public PlayerStateMove PlayerStateMove;
   public PlayerStateSprint PlayerStateSprint;

   public PlayerStateMachine()
   {
   }

   public PlayerState PlayerStateNow{ get; private set; }
   public void Init(Player player)
   {
     PlayerStateIdle = new PlayerStateIdle("Idle", this, player);
     PlayerStateMove = new PlayerStateMove("Move", this, player);
     PlayerStateSprint = new PlayerStateSprint("Sprint", this, player);
     PlayerStateNow = PlayerStateIdle;
   }
   public void ChangePlayerState(PlayerState playerState) 
   {
     PlayerStateNow.OnExit();
     PlayerStateNow = playerState;
     PlayerStateNow.OnEnter();
   }
   public void OnUpdate()
   {
     if (PlayerStateIdle != null)
     {
       PlayerStateIdle.OnUpdate();
     }
   }

 }

思来想去还是把状态机更新放进FixedUpdate里面了。根据实际时间来对状态机进行更新,避免帧数问题影响弹反等操作。

接下来先去场景里新建一个Player空物体,把Player脚本挂上去。编译运行无报错。

然后将之前搞的素材放进游戏,再创建一个动画管理器(虽然计划中所有动作都只有一帧,但还是创建一个方便进行管理)

管理器的状态设置:

再给平台和人物整上碰撞体:

给人物整上刚体:

碰撞设置为持续,插值开上,再锁定z轴的旋转

完善角色的操作部分:

using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 public class Player : MonoBehaviour
 {
   public PlayerStateMachine StateMachine;
   public Animator Anim { get; private set; }
   public Rigidbody2D rb { get; private set; }
   [Header("Movement")]
   public float MovementSpeed = 8.0f;
   private void Awake()
   {
     StateMachine = new PlayerStateMachine();
     Anim=GetComponentInChildren<Animator>();
     rb = GetComponent<Rigidbody2D>();
   }
   // Start is called before the first frame update
   void Start()
   {
     StateMachine.Init(this);
   }
   // Update is called once per frame
   void Update()
   {
   }
   public void setVelocity(float x, float y)
   {
     rb.velocity=new Vector2 (x,y);
   }
   private void FixedUpdate()
   {
     if (StateMachine != null)
     {
       StateMachine.OnUpdate();
     }
   }
   public bool isFacingRight = true;
   public void Flip()
   {
     isFacingRight = !isFacingRight;
     transform.Rotate(0, 180, 0);
   }
   public bool CheckShouldFlip(float xInput)
   {
     return xInput > 0 && !isFacingRight || xInput < 0 && isFacingRight;
   }
 }

更新PlayerState基类,加入更多的通用变量:

using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 public abstract class PlayerState
 {
   public float xInput;
   protected PlayerState()
   {
   }
   public PlayerState(string name, PlayerStateMachine stateMachine, Player player)
   {
     Name = name;
     StateMachine = stateMachine;
     Player = player;
   } 
   public string Name { get; }
   public PlayerStateMachine StateMachine { get; }
   protected Rigidbody2D rb;
   public Player Player { get; }
   public virtual void OnEnter()
   {
     Player.Anim.SetBool(Name, true);
     rb = Player.rb;
   }
   public virtual void OnExit()
   {
     Player.Anim.SetBool(Name, false);
   }
   public virtual void OnUpdate()
   {
     xInput = Input.GetAxis("Horizontal");
     if (Player.CheckShouldFlip(xInput))
     {
       Player.Flip();
     }
   }
 }

在编辑器中运行,已经可以左右移动和正常翻转朝向。

接下来进行跑步状态的完善

using System;
 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 public class PlayerStateMove : PlayerState
 {
   public PlayerStateMove(string name, PlayerStateMachine playerStateMachine, Player player):base(name, playerStateMachine, player)
   {
   }
   public override void OnEnter()
   {
     base.OnEnter();
   }
   public override void OnExit()
   {
     base.OnExit();
   }
   public override void OnUpdate()
   {
     base.OnUpdate();
     Player.setVelocity(xInput *Player.MovementSpeed, rb.velocity.y);
     if (xInput == 0.0)
     {
       StateMachine.ChangePlayerState(StateMachine.PlayerStateIdle);
     }
     else
     {
       //Player.Log("speed:"+Player.rb.velocity.magnitude);
       if (Math.Abs( Player.rb.velocity.x )> 3.0)
       {
         StateMachine.ChangePlayerState(StateMachine.PlayerStateSprint);
       }
     }
   }
 }
using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 public class PlayerStateSprint : PlayerState
 {
   public PlayerStateSprint(string name, PlayerStateMachine stateMachine, Player player) : base(name, stateMachine, player)
   {
   }
   public override void OnEnter()
   {
     base.OnEnter();
   }
   public override void OnExit()
   {
     base.OnExit();
   }
   public override void OnUpdate()
   {
     base.OnUpdate();
     Player.setVelocity(xInput * Player.MovementSpeed, rb.velocity.y);
     if (Mathf.Abs( Player.rb.velocity.x )<3.0)
     {
       StateMachine.ChangePlayerState(StateMachine.PlayerStateMove);
     }
   }
 }

在移动速度超过3.0时自动切换至跑步。

接下来完成跳跃。因为要区分空中和地面上的状态,所以重新创建两个基类:PlayerStateInAir和PlayerStateOnGround,并把Move,Sprint和Idle重新继承自PlayerStateOnGround。目前的想法是Jump仍然为ground状态,在下一次update后直接切换至InAir,然后再在InAir里调用碰撞检测来辨别地面:

using System;
 using System.Collections;
 using System.Collections.Generic;
 using Unity.VisualScripting;
 using UnityEngine;
 public class Player : MonoBehaviour
 {
   public PlayerStateMachine StateMachine;
   public Animator Anim { get; private set; }
   public Rigidbody2D rb { get; private set; }
   private static Logger logger;
   [Header("Movement")]
   public float MovementSpeed = 8.0f;
   public float JumpForce = 12f;
   [Header("Collision")]
   [SerializeField] private Transform GroundCheccker;
   [SerializeField] private LayerMask whatIsGround;
   private void Awake()
   {
     StateMachine = new PlayerStateMachine();
     Anim=GetComponentInChildren<Animator>();
     rb = GetComponent<Rigidbody2D>();
     logger = new Logger(new MyLogHandler());
   }
   public static void Log(string s)
   {
     logger.Log("[PLAYER]", s);
   }
   // Start is called before the first frame update
   void Start()
   {
     StateMachine.Init(this);
   }
   // Update is called once per frame
   void Update()
   {
   }
   public void setVelocity(float x, float y)
   {
     rb.velocity=new Vector2 (x,y);
   }
   private void FixedUpdate()
   {
     if (StateMachine != null)
     {
       StateMachine.OnUpdate();
     }
   }
   public bool isFacingRight = true;
   public void Flip()
   {
     isFacingRight = !isFacingRight;
     transform.Rotate(0, 180, 0);
   }
   public bool CheckShouldFlip(float xInput)
   {
     return xInput > 0 && !isFacingRight || xInput < 0 && isFacingRight;
   }
   private void OnDrawGizmos()
   {
     //Gizmos.DrawSphere(GroundCheccker.position, radius: 1);
   }
   bool isGrounded = true;
   public bool CheckOnGround()
   {
     bool isHit = Physics2D.CircleCast(
       GroundCheccker.position,
       radius:1f,
       Vector2.down,
       0.1f,
       whatIsGround.value
     );
     // 调试可视化
     Debug.DrawRay(GroundCheccker.position, Vector3.down * 1.1f, isHit ? Color.green : Color.red, 0.1f);
     return isHit;
   }
 }
 public class MyLogHandler : ILogHandler
 {
   public void LogFormat(LogType logType, UnityEngine.Object context, string format, params object[] args)
   {
     Debug.unityLogger.logHandler.LogFormat(logType, context, format, args);
   }
   public void LogException(Exception exception, UnityEngine.Object context)
   {
     Debug.unityLogger.LogException(exception, context);
   }
 }
using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 public class PlayerStateInAir : PlayerState
 {
   public PlayerStateInAir(string name, PlayerStateMachine stateMachine, Player player) : base(name, stateMachine, player)
   {
   }
   public override void OnEnter()
   {
     base.OnEnter();
   }
   public override void OnExit()
   {
     base.OnExit();
   }
   public override void OnUpdate()
   {
     base.OnUpdate();
     if (xInput != 0)
     {
       Player.setVelocity(xInput * Player.MovementSpeed/2, rb.velocity.y);
     }
     if (rb.velocity.y == 0)
     {
       if (Player.CheckOnGround())
       {
         StateMachine.ChangePlayerState(StateMachine.PlayerStateIdle);
       }
     }
   }
 }
using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 public class PlayerStateJump : PlayerStateOnGround
 {
   public PlayerStateJump(string name, PlayerStateMachine stateMachine, Player player) : base(name, stateMachine, player)
   {
   }
   public override void OnEnter()
   {
     base.OnEnter();
     Player.setVelocity(rb.velocity.x, Player.JumpForce);
   }
   public override void OnExit()
   {
     base.OnExit();
   }
   public override void OnUpdate()
   {
     base.OnUpdate();
     if (!Player.isGrounded)
     {
       StateMachine.ChangePlayerState(StateMachine.InAir);
     }
   }
 }

注意这里碰撞检测一定要用Physics2D,不然根本无法检测到碰撞。在这里卡了将近一个钟……

运行后功能正常,但落地动画的延迟较高,等待后续进行优化。

posted @ 2025-06-09 13:34  国土战略局特工  阅读(26)  评论(0)    收藏  举报