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,不然根本无法检测到碰撞。在这里卡了将近一个钟……
运行后功能正常,但落地动画的延迟较高,等待后续进行优化。

浙公网安备 33010602011771号