教程源地址:https://www.udemy.com/course/2d-rpg-alexdev/
本章节实现了敌人弓箭手的制作
Enemy_Archer.cs
核心功能
-
状态机管理敌人的行为
- 定义了多个状态对象(如
idleState
、moveState
、attackState
等),通过状态机管理敌人的状态切换。 - 状态包括空闲、移动、攻击、跳跃、受击眩晕、死亡等。
- 定义了多个状态对象(如
-
攻击逻辑
- 使用
AnimationSepcoalAttackTrigger
方法生成箭矢。 - 创建箭矢对象时,调用
Arrow_Controller.SetUpArrow
为其设置速度和伤害信息。 - 箭矢会根据面向方向(
facingDir
)飞向目标。
- 使用
-
地形检测
GroundBenhundCheck
方法:检查敌人背后的地面是否存在,通过Physics2D.BoxCast
实现。WallBehind
方法:检测敌人背后的墙壁是否存在,通过Physics2D.Raycast
实现。- 这些方法用于辅助弓箭手的移动或跳跃逻辑,确保其行为合理。
-
受击与死亡
CanBeStunned
方法:判断弓箭手是否能被眩晕,并切换到stunnedState
。Die
方法:当弓箭手死亡时,切换到deadState
。
-
可视化调试
OnDrawGizmos
方法:在 Unity 编辑器中绘制背后地面检测区域,帮助开发者直观了解地形检测的范围。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//2024.12.11
public class Enemy_Archer : Enemy
{
[Header("弓箭手特殊信息")]//Archer specific info
[SerializeField] private GameObject arrowPrefab;
[SerializeField] private float arrowSpeed;
[SerializeField] private int arrowDamage;
public Vector2 jumpVelocity;
public float jumpCooldown;
public float safeDistance;//安全距离
[HideInInspector]public float lastTimeJumped;
[Header("额外的碰撞检查")]//Additional collision check
[SerializeField] private Transform groundBehindCheck;
[SerializeField] private Vector2 groundBehindCheckSize;
#region States
public ArcherIdleState idleState { get; private set; }
public ArcherMoveState moveState { get; private set; }
public ArcherBattleState battleState { get; private set; }
public ArcherAttackState attackState { get; private set; }
public ArcherDeadState deadState { get; private set; }
public ArcherStunnedState stunnedState { get; private set; }
public ArcherJumpState ArcherJumpState { get; private set; }
#endregion
protected override void Awake()
{
base.Awake();
idleState = new ArcherIdleState(this, stateMachine, "Idle", this);
moveState = new ArcherMoveState(this, stateMachine, "Move", this);
battleState = new ArcherBattleState(this, stateMachine, "Idle", this);
attackState = new ArcherAttackState(this, stateMachine, "Attack", this);
deadState = new ArcherDeadState(this, stateMachine, "Move", this);
stunnedState = new ArcherStunnedState(this, stateMachine, "Stun", this);
ArcherJumpState = new ArcherJumpState(this, stateMachine, "Jump", this);
}
protected override void Start()
{
base.Start();
stateMachine.Initialize(idleState);
}
public override bool CanBeStunned()
{
if (base.CanBeStunned())
{
stateMachine.ChangeState(stunnedState);
return true;
}
return false;
}
public override void Die()
{
base.Die();
stateMachine.ChangeState(deadState);
}
public override void AnimationSepcoalAttackTrigger()
{
GameObject newArrow = Instantiate(arrowPrefab, attackCheck.position, Quaternion.identity);
newArrow.GetComponent<Arrow_Controller>().SetUpArrow(arrowSpeed * facingDir, stats);
}
public bool GroundBenhundCheck() => Physics2D.BoxCast(groundBehindCheck.position, groundBehindCheckSize, 0, Vector2.zero, 0, whatIsGround);
public bool WallBehind() => Physics2D.Raycast(wallCheck.position, Vector2.right * -facingDir, wallCheckDistance +2, whatIsGround);
protected override void OnDrawGizmos()
{
base.OnDrawGizmos();
Gizmos.DrawWireCube(groundBehindCheck.position, groundBehindCheckSize);
}
}
ArcherMoveState.cs
using System.Collections;
using UnityEngine;
public class ArcherMoveState : ArcherGroundState
{
public ArcherMoveState(Enemy _enemyBase, EnemyStateMachine _stateMachine, string _animBoolName, Enemy_Archer _enemy) : base(_enemyBase, _stateMachine, _animBoolName, _enemy)
{
}
public override void Enter()
{
base.Enter();
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
enemy.SetVelocity(enemy.moveSpeed * enemy.facingDir, enemy.rb.velocity.y);
if (enemy.IsWallDetected() || !enemy.IsGroundDetected())//撞墙或者没有路反转
{
enemy.Flip();
stateMachine.ChangeState(enemy.idleState);
}
}
}
Arrow_Controller.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//2024.12.11
public class Arrow_Controller : MonoBehaviour
{
[SerializeField] private int damage;//箭矢造成的伤害值
[SerializeField] private string targetLayerName = "Player";//箭矢的目标层
[SerializeField] private float xVelocity;
[SerializeField] private Rigidbody2D rb;
[SerializeField] private bool canMove;
[SerializeField] private bool flipped;
private CharacterStats myStats;
private void Update()
{
if(canMove)
rb.velocity = new Vector2(xVelocity, rb.velocity.y);
}
public void SetUpArrow(float _speed,CharacterStats _myStats)
{
xVelocity = _speed;
myStats = _myStats;
}
private void OnTriggerEnter2D(Collider2D collision)
{
if(collision.gameObject.layer == LayerMask.NameToLayer(targetLayerName))
{
collision.GetComponent<CharacterStats>().TakeDamage(damage);//箭的伤害
myStats.DoDamage(collision.GetComponent<CharacterStats>());//弓箭手的额外属性
StuckInto(collision);
}
else if (collision.gameObject.layer == LayerMask.NameToLayer("Ground"))
{
StuckInto(collision);
}
}
private void StuckInto(Collider2D collision)//射中目标
{
GetComponentInChildren<ParticleSystem>().Stop();//停止粒子系统
GetComponent<Collider2D>().enabled = false;
canMove = false;
rb.isKinematic = true;//刚体为运动
rb.constraints = RigidbodyConstraints2D.FreezeAll;
transform.parent = collision.transform;//箭矢的父物体为碰撞物体
Destroy(gameObject, Random.Range(5,7));
}
public void FlipArrow()
{
if (flipped)
return;
xVelocity = xVelocity * -1;
flipped = true;
transform.Rotate(0,180,0);
targetLayerName = "Enemy";
}
}
ArcherBattleState.cs
using System.Collections;
using UnityEngine;
//2024.12.11
public class ArcherBattleState : EnemyState
{
private Transform player;
private Enemy_Archer enemy;
private int moveDir;
public ArcherBattleState(Enemy _enemyBase, EnemyStateMachine _stateMachine, string _animBoolName, Enemy_Archer _enemy) : base(_enemyBase, _stateMachine, _animBoolName)
{
this.enemy = _enemy;
}
public override void Enter()
{
base.Enter();
//player = GameObject.Find("Player").transform;
player = PlayerManager.instance.player.transform;
if (player.GetComponent<PlayerStats>().isDead)
stateMachine.ChangeState(enemy.moveState);
}
public override void Update()
{
base.Update();
if (enemy.IsPlayerDetected())
{
stateTimer = enemy.battleTime;
if (enemy.IsPlayerDetected().distance < enemy.safeDistance)//小于安全距离
{
if (CanJump())
stateMachine.ChangeState(enemy.ArcherJumpState);//跳走
}
if (enemy.IsPlayerDetected().distance < enemy.attackDistance)
{
if (CanAttack())
stateMachine.ChangeState(enemy.attackState);
}
}
else
{
if (stateTimer < 0 || Vector2.Distance(player.transform.position, enemy.transform.position) > 10)//超过距离或者时间到了
stateMachine.ChangeState(enemy.idleState);
}
BattleStateFlipController();
}
private void BattleStateFlipController()
{
if (player.position.x > enemy.transform.position.x && enemy.facingDir == -1)
enemy.Flip();
else if (player.position.x < enemy.transform.position.x && enemy.facingDir == 1)
enemy.Flip();
}
public override void Exit()
{
base.Exit();
}
private bool CanAttack()
{
if (Time.time >= enemy.lastTimeAttack + enemy.attackCoolDown)
{
enemy.attackCoolDown = Random.Range(enemy.minAttackCoolDown, enemy.maxAttackCoolDown);
enemy.lastTimeAttack = Time.time;
return true;
}
else
return false;
}
private bool CanJump()
{
if(enemy.GroundBenhundCheck() == false || enemy.WallBehind() ==true)
return false;
if (Time.time >= enemy.lastTimeJumped + enemy.jumpCooldown)
{
enemy.lastTimeJumped = Time.time;
return true;
}
return false;
}
}
ArcherGroundState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//2024.12.11
public class ArcherGroundState : EnemyState
{
protected Transform player;
protected Enemy_Archer enemy;
public ArcherGroundState(Enemy _enemyBase, EnemyStateMachine _stateMachine, string _animBoolName,Enemy_Archer _enemy) : base(_enemyBase, _stateMachine, _animBoolName)
{
enemy = _enemy;
}
public override void Enter()
{
base.Enter();
player = PlayerManager.instance.player.transform;//p63 3:43改
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
if (enemy.IsPlayerDetected() || Vector2.Distance(enemy.transform.position, player.transform.position) < enemy.agroDistance)
{
stateMachine.ChangeState(enemy.battleState);
}
}
}
ArcherIdleState.cs
using System.Collections;
using UnityEngine;
public class ArcherIdleState : ArcherGroundState
{
public ArcherIdleState(Enemy _enemyBase, EnemyStateMachine _stateMachine, string _animBoolName, Enemy_Archer _enemy) : base(_enemyBase, _stateMachine, _animBoolName, _enemy)
{
}
public override void Enter()
{
base.Enter();
stateTimer = enemy.idleTime;
}
public override void Exit()
{
base.Exit();
AudioManager.instance.PlaySFX(24, enemy.transform);
}
public override void Update()
{
base.Update();
if (stateTimer < 0)
{
stateMachine.ChangeState(enemy.moveState);
}
}
}
ArcherStunnedState.cs
using System.Collections;
using UnityEngine;
//2024.12.11
public class ArcherStunnedState : EnemyState
{
private Enemy_Archer enemy;
public ArcherStunnedState(Enemy _enemyBase, EnemyStateMachine _stateMachine, string _animBoolName, Enemy_Archer enemy) : base(_enemyBase, _stateMachine, _animBoolName)
{
this.enemy = enemy;
}
public override void Enter()
{
base.Enter();
enemy.fx.InvokeRepeating("RedColorBlink", 0, .1f); //这行代码使用 InvokeRepeating 方法,每隔 0.1 秒调用一次 RedColorBlink 方法。
stateTimer = enemy.stunDuration;
rb.velocity = new Vector2(-enemy.facingDir * enemy.stunDirection.x, enemy.stunDirection.y);
}
public override void Exit()
{
base.Exit();
enemy.fx.Invoke("CancelColorChange", 0);//Invoke 方法用于在指定的延迟时间后调用某个方法。在这里,延迟时间为 0
}
public override void Update()
{
base.Update();
if (stateTimer < 0)
stateMachine.ChangeState(enemy.idleState);
}
}
ArcherAttackState.cs
using System.Collections;
using UnityEngine;
public class ArcherAttackState : EnemyState
{
private Enemy_Archer enemy;
public ArcherAttackState(Enemy _enemyBase, EnemyStateMachine _stateMachine, string _animBoolName, Enemy_Archer _enemy) : base(_enemyBase, _stateMachine, _animBoolName)
{
this.enemy = _enemy;
}
public override void Enter()
{
base.Enter();
}
public override void Exit()
{
base.Exit();
enemy.lastTimeAttack = Time.time;
}
public override void Update()
{
base.Update();
enemy.SetZeroVelocity();
if (triggerCalled)
stateMachine.ChangeState(enemy.battleState);
}
}