最終効果:
コンテンツ
アイデアを書く
ステートマシンは次のとおりです。
アイデアをコーディングし、onEnter、onExit、onUpdateの3つの関数を含む一般的な状態インターフェイスを記述します。次に、他の状態は、異なる状態に従ってインターフェイスを実装するだけで済みます。
辞書に格納されているさまざまな状態を含む有限状態マシンのスクリプトを記述します。状態マシンは常に1つの状態にしかなれず、各状態の切り替えや回転など、各状態に共通の機能があります。 。
各ステートスクリプトにはonEnter関数とonExit関数があります。ステートマシンスクリプトでは、状態切り替えを実行するときに、現在の状態のonExit関数と新しい状態のonEnter関数を呼び出します。ステートマシンの更新では、対応する状態のonUpdate関数を引き続き実行します。
アイドル状態を例にとると、アイドル状態の更新機能では、プレイヤーはアイドルタイムの時間滞在し、時間が経過するとパトロール状態に切り替わります。このとき、有限状態で状態切り替え機能があります。ステートマシンFSMが呼び出されます。
基本的なコードフレームワーク
最初に状態のインターフェイスを記述し、次の状態でインターフェイスを実装する必要があります。
public interface IState_
{
void OnEnter();
void OnUpdate();
void OnExit();
}
アニメーションコントローラーで敵のアニメーションコントローラーを作成します。
アニメーションコントローラ自体はステートマシンであるため、接続を設定する必要はありません。スクリプトを使用して切り替えを制御するだけです。
パトロールと追跡のポイントを設定します。
次に、単純な状態制御マシンのフレームワークを記述します。
さまざまな状態の列挙、TransititionState、状態が切り替わるときに実行する必要のある操作、フリップステアリング機能、およびパトロール時間や追跡時間などのすべての機能に含まれるパラメーターが含まれています。
さらに、ステートマシンは、辞書を使用してここに格納されているすべての状態を含む一般的なステートマシンとして機能します。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum StateType_
{
Idle,Patrol,Chase,React,Attack
}
[Serializable]
public class Parameter_
{
public int health;
public float moveSpeed;
public float chaseSpeed;
public float idleTime;
public Transform[] patrolPoints;
public Transform[] chasePoints;
}
public class FSM_ : MonoBehaviour
{
public Animator animator;
private IState_ currentState;
private Dictionary<StateType_, IState_> states = new Dictionary<StateType_, IState_>();
public Parameter_ parameter;
void Start()
{
states.Add(StateType_.Idle, new IdleState_(this));
states.Add(StateType_.Patrol, new PatrolState_(this));
TransititionState(StateType_.Idle);
animator = GetComponent<Animator>();
}
private void Update()
{
currentState.OnUpdate();
}
public void TransititionState(StateType_ type)
{
if (currentState != null)
currentState.OnExit();
currentState = states[type];
currentState.OnEnter();
}
public void FlipTo(Transform target)
{
if (target != null)
{
if (transform.position.x > target.position.x)
{
transform.localScale = new Vector3(-1, 1, 1);
}
else if (transform.position.x < target.position.x)
{
transform.localScale = new Vector3(1, 1, 1);
}
}
}
}
次に、待機状態関数を作成する例としてIdleStateを取り上げます。
IdleStateには、待機時間、ターゲットポイント、アニメーターなどの情報が必要です。これらのパラメーターは別のスクリプトに配置してから、敵にフックする必要があります。便宜上、これらのパラメーターはFSMスクリプトに配置されるため、IdleState状態を使用する場合は、これらのパラメーターを使用する必要があります。ここでは、FSMをこれらの状態に渡すことによってインスタンス化されます。(ここでの状態切り替え関数もFSMスクリプトに実装されています。この関数を便利に使用するために、
コードは次のように表示されます。
IdleStateは、待機時間が経過すると、自動的に別の状態に切り替わることに気付きます。
public class IdleState_ : IState_
{
private FSM_ manager;
private Parameter_ parameter;
private float timer;
public IdleState_(FSM_ manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public void OnEnter()
{
parameter.animator.Play("Idle");
}
public void OnUpdate()
{
timer += Time.deltaTime;
if (timer >= parameter.idleTime)
{
manager.TransititionState(StateType_.Patrol);
}
}
public void OnExit()
{
timer = 0;
}
}
敵のステートマシンがここに実装されています。他の敵にステートマシンのコードを使用する場合は、一般的な部分を保持してから、他のキャラクターのステートマシンを使用してこのステートマシンを継承できます。
パトロール状況
次に、パトロールステータスを書き込みます。
全体は上記と同様ですが、
public class PatrolState_ : IState_
{
private FSM_ manager;
private Parameter_ parameter;
private int patrolPosition;//巡逻到第几个点了
public PatrolState_(FSM_ manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public void OnEnter()
{
parameter.animator.Play("Walk");
}
public void OnUpdate()
{
manager.FlipTo(parameter.patrolPoints[patrolPosition]);//让敌人始终朝向巡逻点的方向
manager.transform.position = Vector2.MoveTowards(manager.transform.position, parameter.patrolPoints[patrolPosition].position, parameter.moveSpeed * Time.deltaTime);
if (Vector2.Distance(manager.transform.position, parameter.patrolPoints[patrolPosition].position)<0.1f) {
manager.TransititionState(StateType_.Idle);
}
}
public void OnExit()
{
patrolPosition++;
if (patrolPosition >= parameter.patrolPoints.Length)
{
patrolPosition = 0;
}
}
}
ここにバグがあります。アニメーターがgetcomponentを使用して自動的に取得できなかった理由はわかりませんが、手動でしか取得できません。当面は理由がわかりません。
導入後の効果は以下のとおりです。
追跡状態
子オブジェクトを敵に追加して、敵の目として機能させます。敵の視野に入ると、追跡状態に切り替わります。
private void OnTriggerEnter2D(Collider2D collision)
{
parameter.target = collision.transform;
}
次に、追跡状態の関数を記述します。
public class ChaseState_ : IState_
{
private FSM_ manager;
private Parameter_ parameter;
private float timer;
public ChaseState_(FSM_ manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public void OnEnter()
{
parameter.animator.Play("Walk");
}
public void OnUpdate()
{
manager.FlipTo(parameter.target);
if (parameter.target != null)
{
manager.transform.position = Vector2.MoveTowards(manager.transform.position, parameter.target.position, parameter.chaseSpeed * Time.deltaTime);
}
if(parameter.target==null ||
manager.transform.position.x < parameter.chasePoints[0].position.x ||
manager.transform.position.x > parameter.chasePoints[1].position.x)
{
manager.TransititionState(StateType_.Patrol);
}
if (true)//进入攻击范围,执行攻击)
{
}
}
public void OnExit()
{
timer = 0;
}
}
プレイヤーが敵の攻撃範囲に入ると、敵は攻撃状態に切り替わります。
攻撃状況
攻撃範囲に入った状態は、プレイヤーからの距離だけで達成できますが、手動で測定するのは面倒です。ここでは、範囲検出機能を使用して、敵の攻撃ポイントと攻撃範囲半径を設定します。 parameterこれらの2つのパラメーターを追加します。
private void OnTriggerEnter2D(Collider2D collision)
{
parameter.target = collision.transform;
}
private void OnTriggerExit(Collider other)
{
if (other.CompareTag("Player"))
{
parameter.target = null;
}
}
円の範囲を視覚的に確認するために、ペイントを使用して円を描くことができます。
private void OnDrawGizmos()
{
Gizmos.DrawWireSphere(parameter.attackPoint.position, parameter.attackAreaRadius);
}
次に、追跡の更新で攻撃に切り替えるための条件を追加し、攻撃状態を切り替えます。
if (Physics2D.OverlapCircle(parameter.attackPoint.position,parameter.attackAreaRadius,parameter.targetLayer))//进入攻击范围,执行攻击)
//注意,此处需要添加层级,以防止敌人检测到其他的物体则也进入攻击
{
manager.TransititionState(StateType_.Attack);
}
反応ステータスを追加する
敵がパトロール時にプレイヤーを攻撃する状態に入ることができるようにするには、idleStateとpatrolStateにコードを追加します。
ターゲットがあり、ターゲットが追跡範囲外にない場合は、ターゲットを追跡します。ターゲットを確認して追跡範囲外にある場合でも、追跡しません。
反応状態は実際には比較的単純で、アニメーションを再生し、アニメーションが完了すると追跡状態に戻ります。
攻撃状況はこれに似ています
public class AttackState_ : IState_
{
private FSM_ manager;
private Parameter_ parameter;
private AnimatorStateInfo animStateInfo;//动画状态信息
public AttackState_(FSM_ manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public void OnEnter()
{
parameter.animator.Play("Attack");
}
public void OnUpdate()
{
animStateInfo = parameter.animator.GetCurrentAnimatorStateInfo(0);
if (animStateInfo.normalizedTime >= .95f)//当动画进度接近1的时候,认为动画接近完成了
{
manager.TransititionState(StateType_.Chase);
}
}
public void OnExit()
{
}
}
このようにして、スタンディングガード、パトロール、リアクション、追跡、攻撃の状態切り替えが実現されます。
要約すると、ロジックは実際には非常に単純です。最初に単純なガードパトロールロジックを実装し、ガードパトロール相互切り替えのロジックの前に優先度の判断条件を追加し、ガード中にターゲットが見つかった場合は反応状態に切り替えます。パトロール(トリガーによって実行)、リアクション状態は単にリアクションアニメーションを再生することです。プレイ後、プレイヤーはすぐに追跡アニメーションに入り、追跡アニメーションが十分に近づくと(球によって判断される)、攻撃に入ることができます。州。各攻撃後、自動的に追跡状態になります。まだ攻撃範囲内にある場合は、すぐに再び攻撃状態になります。攻撃範囲を超える場合は攻撃を追跡し、追跡範囲を超える場合は追跡します。 、パトロールに戻ります。
このようなステートマシンでは、新しい状態が表示された場合、新しい状態を登録してスイッチング条件を書き込むだけで済みます。
ヒット状態
攻撃状態は特殊であり、上記の様々な状態では攻撃状態の判定が優先されるため、上記すべての状態のonupdate機能では、攻撃された場合に攻撃状態に切り替わるように組み込む必要があります。 。
ダメージチェックを追加します。
すべての州で以下を追加します。
if (parameter.getHit)
{
manager.TransititionState(StateType_.Hit);
}
攻撃状態と死状態:
public class HitState_ : IState_
{
private FSM_ manager;
private Parameter_ parameter;
private AnimatorStateInfo animStateInfo;//动画状态信息
public HitState_(FSM_ manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public void OnEnter()
{
parameter.animator.Play("Hit");
parameter.health--;
}
public void OnUpdate()
{
animStateInfo = parameter.animator.GetCurrentAnimatorStateInfo(0);
if (parameter.health <= 0)
{
manager.TransititionState(StateType_.Death);
}
if (animStateInfo.normalizedTime >= .95f)
{
parameter.target = GameObject.FindWithTag("Player").transform;
manager.TransititionState(StateType_.Chase);
}
}
public void OnExit()
{
parameter.getHit = false;
}
}
public class DeathState_ : IState
{
private FSM manager;
private Parameter parameter;
public DeathState_(FSM manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public void OnEnter()
{
parameter.animator.Play("Dead");
}
public void OnUpdate()
{
}
public void OnExit()
{
}
}