脚本
新建脚本
选择对象 > 监视器 > 添加组件
或者在文件夹中新建script文件
给对象添加组件,选择新建的脚本PlayerMove
功能
移动
获取水平移动输入,提供水平方向速度
public class PlayerMove : MonoBehaviour
{
private Rigidbody2D rb;
[SerializeField]private float speed = 5.0f;
void Start()
{
//获取组件
rb = GetComponent<Rigidbody2D>();
}
void Update()
{
//获取输入
var xInput = Input.GetAxis("Horizontal");
//修改速度
rb.linearVelocity = new Vector2(xInput * speed, rb.linearVelocity.y);
}
}
跳跃
默认空格为跳跃键,获取空格输入,水平速度保持不变,获取一个向上的速度
public class PlayerMove : MonoBehaviour
{
private Rigidbody2D rb;
[SerializeField]private float speed = 5.0f;
void Start()
{
rb = GetComponent<Rigidbody2D>();
}
void Update()
{
//Input
//Input.GetKey(KeyCode.Space)持续监测
if (Input.GetKeyDown(KeyCode.Space))
{
rb.linearVelocity = new Vector2(rb.linearVelocity.x, speed);
}
}
}
地面/墙体监测
角色作为父级,创建两个空的子对象WallCheck和GroundCheck
player脚本声明以下成员
[Header("Collision info")]
[SerializeField]private Transform groundCheck;
[SerializeField]private Transform wallCheck;
[SerializeField]private float groundCheckRadius = 0.2f;
[SerializeField]private float wallCheckRadius = 0.2f;
[SerializeField]private LayerMask whatIsGround;
在Inspector中绑定groundCheck和wallCheck
将地面图层设置为Ground(初次使用需要添加图层Ground)
Inspector中将Player的whatIsGround设置为Ground
绘图方法OnDrawGizmos
private void OnDrawGizmos()
{
//Gizmos.DrawLine(groundCheck.position, new Vector3(groundCheck.position.x, groundCheck.position.y + groundCheckRadius));//向上画线
Gizmos.DrawLine(groundCheck.position, new Vector3(groundCheck.position.x, groundCheck.position.y - groundCheckRadius));//向下画线
//Gizmos.DrawLine(wallCheck.position, new Vector3(wallCheck.position.x - wallCheckRadius, wallCheck.position.y));//向左画线
Gizmos.DrawLine(wallCheck.position, new Vector3(wallCheck.position.x + wallCheckRadius, wallCheck.position.y));//向右画线
}
调整划线的位置直到与地面/墙面接触
player.cs
public bool IsGroundedDetected()
{
return Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckRadius, whatIsGround);
}
playerGroundedState.cs
public override void Update()
{
base.Update();
//如果人物在地面上,且按下跳跃键,那么切换到跳跃状态
if (jumpInput && player.IsGroundedDetected())
{
stateMachine.ChangeState(player.jumpState);
}
}
冲刺
player.cs
[Header("Dash info")]
public float dashSpeed = 8f;
public float dashTime = 1f;
public float dashTimer;
public float dashCoolDown = 2f;
public bool isDashCoolDown ;
可以从任意状态转换到dash状态(包括空中)
playerState.cs
protected float stateTimer;
public virtual void Update()
{
dashInput = Input.GetKeyDown(KeyCode.LeftShift);
if (player.dashTimer > 0)
player.dashTimer -= Time.deltaTime;
else
{
player.dashTimer = player.dashCoolDown;
player.isDashCoolDown = false;
}
if(dashInput && !player.isDashCoolDown)
{
stateMachine.ChangeState(player.dashState);
}
}
playerDashState.cs
public override void Enter()
{
base.Enter();
player.isDashCoolDown = true;
//开始计时(冲刺持续时间)
stateTimer = player.dashTime;
//设置dash冷却计时器,冷却时间结束后,可以再次dash
player.dashTimer = player.dashCoolDown;
}
public override void Update()
{
base.Update();
//设置dash速度,冲刺时保持角色y轴不变
player.SetVelocity(player.facingDir * player.dashSpeed, 0);
//如果dash时间结束,那么切换到idle状态
stateTimer -= Time.deltaTime;
if (stateTimer <= 0)
{
stateMachine.ChangeState(player.idleState);
}
}
动画中添加事件
添加事件
Player.cs
public void AnimationTrigger() => stateMachine.currentState.AnimationFinishTrigger();
PlayerState.cs
protected bool triggerCalled;
public virtual void Enter()
{
triggerCalled = false;
}
public virtual void AnimationFinishTrigger()
{
triggerCalled = true;
}
PlayerAnimationTriggers.cs
public class PlayerAnimationTriggers : MonoBehaviour
{
private Player player => GetComponentInParent<Player>();
private void AnimationTrigger() => player.AnimationTrigger();
}
PlayerAttackState.cs
public override void Update()
{
//如果触发器被调用(动画播放完毕),那么切换到idle状态
if (triggerCalled)
{
stateMachine.ChangeState(player.idleState);
}
}
将脚本PlayerAnimationTriggers.cs绑定到Animator上
点击事件查看Inspector
Function > PlayerAnimationTriggers > Methods > AnimationTrigger
同理playerAttack2 ,playerAttack3都要加
保存
有限状态机
工作流程:Player脚本绑定在角色身上,通过Awake和Start函数完成对成员的构造、获取组件以及初始化,通过Update持续监测当前状态(调用PlayerState类/派生类的Update方法)是否需要改变为其他状态(PlayerStateMachine脚本通过ChangeState方法将currentState修改为其他状态)
在PlayerState类的Enter方法中设定将Animator对应的flag设定为true,此时Animator会从Entry转换到某一个clip,然后在Exit方法中设定为false,此时Animator退出当前clip回到默认clip(Idle)
Player
using System;
using UnityEngine;
public class Player : MonoBehaviour
{
public PlayerStateMachine stateMachine { get; private set; }
public PlayerIdleState idleState { get; private set; }
public PlayerMoveState moveState { get; private set; }
public Rigidbody2D rb { get; private set; }
public Transform tf { get; private set; }
public Animator anim { get; private set; }
public float moveSpeed = 5f;
private void Awake()
{
stateMachine = new PlayerStateMachine();
idleState = new PlayerIdleState("idle", stateMachine, this);
moveState = new PlayerMoveState("move", stateMachine, this);
rb = GetComponent<Rigidbody2D>();
tf = GetComponent<Transform>();
anim = GetComponentInChildren<Animator>();
}
private void Start()
{
stateMachine.Initialize(idleState);
}
private void Update()
{
stateMachine.currentState.Update();
}
}
PlayerStateMachine
using UnityEngine;
public class PlayerStateMachine
{
public PlayerState currentState { get; private set; }
public void Initialize(PlayerState startState)
{
currentState = startState;
currentState.Enter();
}
public void ChangeState(PlayerState newState)
{
currentState.Exit();
currentState = newState;
currentState.Enter();
}
}
PlayerState
using UnityEngine;
using System.Collections;
public class PlayerState
{
protected PlayerStateMachine stateMachine;
protected Player player;
private string animBoolName;
public PlayerState(string animBoolName, PlayerStateMachine stateMachine, Player player)
{
this.animBoolName = animBoolName;
this.stateMachine = stateMachine;
this.player = player;
}
public virtual void Enter()
{
player.anim.SetBool(animBoolName, true);
}
public virtual void Update()
{
}
public virtual void Exit()
{
player.anim.SetBool(animBoolName, false);
}
}
PlayerIdleState
using UnityEngine;
public class PlayerIdleState : PlayerState
{
public PlayerIdleState(string animBoolName, PlayerStateMachine stateMachine, Player player) : base(animBoolName, stateMachine, player)
{
//调用基类的构造函数
}
public override void Enter()
{
base.Enter();
}
public override void Update()
{
base.Update();
////跳出状态接口
if (Input.GetKeyDown(KeyCode.Space))
{
stateMachine.ChangeState(player.moveState);
}
}
public override void Exit()
{
base.Exit();
}
}
PlayerMoveState
using UnityEngine;
public class PlayerMoveState : PlayerState
{
public PlayerMoveState(string animBoolName, PlayerStateMachine stateMachine, Player player) : base(animBoolName, stateMachine, player)
{
//调用基类的构造函数
}
public override void Enter()
{
base.Enter();
}
public override void Update()
{
base.Update();
//跳出状态接口
if (Input.GetKeyDown(KeyCode.Space))
{
stateMachine.ChangeState(player.idleState);
}
}
public override void Exit()
{
base.Exit();
}
}
添加其他状态
-
设置动画和对应过渡参数(可选)
-
一般继承PlayerState,在player.cs中添加对应state公有成员
-
Awake()中初始化state,如:
wallSlideState = new PlayerWallSlideState("wallSlide", stateMachine, this);
animBoolName取决于Animator中入口参数
-
考虑哪个状态可以过渡到目标状态,在Update中添加监测条件
if(player.IsWallDetected() && !player.IsGroundedDetected(1.0f)) { stateMachine.ChangeState(player.wallSlideState); //取消当前帧Update后续程序段 return; }
行为树
需要一个良好的支持基础
不然太难用了。。
以下是半成品
using Script.Entity.Enemy.BehaviourTree;
using Script.Entity.Enemy.BehaviourTree.Action;
using Script.Entity.Enemy.BehaviourTree.Condition;
namespace Script.Entity.Enemy.FalseKnight
{
public class FalseKnightEnemy : Base.Enemy
{
private EnemyBtNode root;
public FalseKnightIdleState IdleState { get; private set; }
public FalseKnightMoveState MoveState { get; private set; }
public FalseKnightAttackState AttackState { get; private set; }
public FalseKnightHitState HitState { get; private set; }
protected override void Awake()
{
base.Awake();
IdleState = new FalseKnightIdleState("idle", StateMachine, this);
MoveState = new FalseKnightMoveState("move", StateMachine, this);
AttackState = new FalseKnightAttackState("attack", StateMachine, this);
HitState = new FalseKnightHitState("hit", StateMachine, this);
}
protected override void Start()
{
base.Start();
StateMachine.Initialize(IdleState);
BuildBehaviorTree();
}
private void BuildBehaviorTree()
{
// 创建主选择器
var mainSelector = new BtSelector();
// 死亡检查 - 最高优先级
mainSelector.AddChild(new BtSequence(
new BtConditionNode(() => isDead),
new BtChangeStateNode(StateMachine, DeadState)
));
// 受击检查
mainSelector.AddChild(new BtSequence(
new BtConditionNode(() => IsKnockedBack),
new BtChangeStateNode(StateMachine, HitState)
));
// 战斗行为选择器
var combatSelector = new BtSelector();
combatSelector.AddChild(new BtSequence(
new BtConditionNode(() => IsObjectDetected(whatIsPlayer)),
new BtConditionNode(() => IsObjectInAttackRange(whatIsPlayer)),
new BtConditionNode(() => StateMachine.CurrentState == AttackState),
new BtConditionNode(() => !isAttacking), // 攻击动画完成
new BtConditionNode(() => AttackState.coolDownTimer > 0f),
new BtChangeStateNode(StateMachine, IdleState)
));
// 攻击序列 - 检测到玩家且在攻击范围内且可以攻击
combatSelector.AddChild(new BtSequence(
new BtConditionNode(() => IsObjectDetected(whatIsPlayer)),
new BtConditionNode(() => IsObjectInAttackRange(whatIsPlayer)),
new BtConditionNode(() => !isAttacking),
new BtConditionNode(() => AttackState.coolDownTimer <= 0f),
new BtChangeStateNode(StateMachine, AttackState)
));
// 追击序列 - 检测到玩家但不在攻击范围内
combatSelector.AddChild(new BtSequence(
new BtConditionNode(() => IsObjectDetected(whatIsPlayer)),
new BtConditionNode(() => !IsObjectInAttackRange(whatIsPlayer)),
new BtConditionNode(() => !isAttacking),
new BtConditionNode(() => AttackState.coolDownTimer <= 0f),
new BtActionNode(() => SetMovingDir(GetObjectDirection())),
new BtChangeStateNode(StateMachine, MoveState)
));
// 巡逻/空闲行为选择器
var patrolSelector = new BtSelector();
// 巡逻移动 - 没有检测到玩家且空闲时间结束
patrolSelector.AddChild(new BtSequence(
new BtConditionNode(() => !IsObjectDetected(whatIsPlayer)),
new BtConditionNode(() => StateMachine.CurrentState == IdleState),
new BtConditionNode(() => IdleState.StateTimer <= 0f),
new BtChangeStateNode(StateMachine, MoveState)
));
// 停止巡逻回到空闲
patrolSelector.AddChild(new BtSequence(
new BtConditionNode(() => MoveState.StateTimer <= 0f || IsWallDetected() || !IsGroundedDetected()),
new BtChangeStateNode(StateMachine, IdleState)
));
mainSelector.AddChild(combatSelector);
mainSelector.AddChild(patrolSelector);
root = mainSelector;
}
protected override void Update()
{
base.Update();
StateMachine.CurrentState.Update();
root?.Execute();
}
public override void SetDead(float time =1f)
{
base.SetDead(5f);
}
}
}
Comments