プレイヤーなどの状態を自動遷移
##この記事について
プレイヤーの状態(ステート)を自動遷移したくて作りました。プレイヤーに関しては現状この案以上のものは思いついてないです。
こんな感じのUnityのAnimator的遷移をある程度自動で行うものです。
ポイントとしては、
- アクションの追加が楽
- 自動で遷移
- 行数が減る
という感じで、プレイヤーやボスなどの複雑な遷移や条件がある場合有効です。
要件
- (入力/当たり判定に応じてプレイヤーの)状態が変わる
- アクションは変わる(増える/減る)可能性がある
- アクションの数は31以下である
- 状態遷移のための変数は31以下である
作成&解説
Transition構造体を作る
遷移するための情報を入れた構造体です。
情報は3つ、
- 前のアクション
- 次のアクション
- 遷移条件
ただし、これらのintはboolの塊として扱います。
例えば275という数字が与えられた場合、2進数換算して100010101です。これをTrue,False,False...として扱うという意味です。
/// <summary>
/// 遷移用の構造体
/// 各種変数はbit単位で扱い、boolの配列とみなす
/// </summary>
public struct Transition {
public Transition(int before, int after, int condition) {
beforeIndex = before;
afterIndex = after;
conditionIndex = condition;
}
public int beforeIndex;
public int afterIndex;
public int conditionIndex;
}
Actionの基底クラスを作る
public abstract class ActionBase {
public Player.ActionIndex actionIndex { get; set; }
/// <summary>
/// 他のアクションからこれになった時に呼ばれる関数
/// </summary>
/// <param name="before">前のアクション</param>
public abstract void OnStart(ActionBase before, Player player);
/// <summary>
/// 他のアクションになる関数
/// </summary>
/// <param name="after">次のアクション</param>
public abstract void OnEnd(ActionBase after, Player player);
/// <summary>
/// 毎フレーム呼ばれるやーつ
/// </summary>
/// <param name="player"></param>
public abstract void Update(Player player);
}
Playerを作る
public class Player : MonoBehaviour {
//Qiitaではregionできないのでコメントアウト
//#region アクション、条件まわり
[Flags]
public enum ActionIndex {
IDLE = 1 << 0,
DUSH = 1 << 1,
JUMP = 1 << 2,
FALL = 1 << 3,
LAND = 1 << 4,
SLIDE = 1 << 5,
VAULT = 1 << 6,
CLIMB = 1 << 7,
TRANPOLINE_JUMP = 1 << 8,
STYLE_UP = 1 << 9,
STYLE_DOWN = 1 << 10,
STYLE_RIGHT = 1 << 11,
MAX = 1 << 12
}
public ActionIndex nowAction { get; private set; } = ActionIndex.IDLE;
public enum ConditionIndex {
IS_TAP = 1 << 0,
IS_HOLD = 1 << 1,
IS_FLICK_UP = 1 << 2,
IS_FLICK_RIGHT = 1 << 3,
IS_FLICK_DOWN = 1 << 4,
IS_GROUND = 1 << 5,
IS_TRIGGER_GROUND = 1 << 6,
IS_FALLING = 1 << 7,
IS_COLLISION_WALL = 1 << 8,
IS_TRIGGER_WALL = 1 << 9,
IS_COLLISION_HURDLE = 1 << 10,
IS_TRIGGER_HURDLE = 1 << 11,
IS_COLLISION_SLOPE = 1 << 12,
IS_TRIGGER_SLOPE = 1 << 13,
IS_COLLISION_TRANMPOLINE = 1 << 14,
IS_INTERVAL_END = 1 << 15,
IS_TIME_END = 1 << 16,
CONDITION_MAX = 1 << 17
}
private List<Transition> transitions = new List<Transition>();
public ConditionIndex conditions = 0;
private List<ActionBase> actions = new List<ActionBase>();
//#endregion
void Start() {
actions.Add(new Idle { actionIndex = ActionIndex.IDLE });
actions.Add(new Dush { actionIndex = ActionIndex.DUSH });
actions.Add(new Jump { actionIndex = ActionIndex.JUMP });
actions.Add(new Fall { actionIndex = ActionIndex.FALL });
actions.Add(new Landing { actionIndex = ActionIndex.LAND });
actions.Add(new Slide { actionIndex = ActionIndex.SLIDE });
actions.Add(new Vault { actionIndex = ActionIndex.VAULT });
actions.Add(new Climbing { actionIndex = ActionIndex.CLIMB });
actions.Add(new TrampolineJump { actionIndex = ActionIndex.TRANPOLINE_JUMP });
actions.Add(new StyleUp { actionIndex = ActionIndex.STYLE_UP });
actions.Add(new StyleDown { actionIndex = ActionIndex.STYLE_DOWN });
actions.Add(new StyleRight { actionIndex = ActionIndex.STYLE_RIGHT });
conditions = 0;
//遷移条件の追加
transitions.Add(new Transition((int)(ActionIndex.IDLE), (int)ActionIndex.VAULT, (int)(ConditionIndex.IS_COLLISION_HURDLE))); //通常遷移
transitions.Add(new Transition((int)(ActionIndex.IDLE), (int)ActionIndex.VAULT, (int)(ConditionIndex.IS_TRIGGER_HURDLE | ConditionIndex.IS_FLICK_RIGHT))); //複数条件
transitions.Add(new Transition((int)(ActionIndex.IDLE | ActionIndex.IDLE.Dush), (int)ActionIndex.VAULT, (int)(ConditionIndex.IS_COLLISION_HURDLE))); //複数のbeforeから遷移
}
//Rigidbodyを使うのでFixedUpdate
void FixedUpdate(){
InputCheck();
//全ての条件を満たしているかどうかチェック
bool IsTrans(int condition) {
return (condition & (int)conditions) == (int)conditions;
}
//今のアクションがTransitionの中に含まれているか確認
bool HasIndex(int index) {
return index % (int)nowAction > 0;
}
for (int i = 0; i < transitions.Count; i++) {
var transition = transitions[i];
if (HasIndex(transition.beforeAction) && nowAction != transtion.afterIndex)) && IsTrans(transitions[i].conditionIndex)) {
var beforeAction = actions[(int)nowAction];
var afterAction = actions[(int)transitions[i].afterIndex];
beforeAction.OnEnd(afterAction, this);
afterAction.OnChanged(beforeAction, this);
nowAction = transitions[i].afterIndex;
intervalTimer = 0;
break;
}
}
//アクションのアップデートを呼ぶ
nowAction.Update(this);
}
//入力チェック
private void InputCheck() {
//サンプルなので簡易版
conditions[(int)Mathf.Log((int)ConditionIndex.IS_TAP, 2)] = Input.GetKeyDown(KeyCode.Space);
}
}
Actionクラスを作る
//ActionBaseを継承する
public class Jump : ActionBase {
private float animationTimer = 0;
public override void OnStart(ActionBase before, Player player) {
//開始時の処理
}
public override void Update(Player player) {
//毎フレーム行われる処理
if(player.playerRigidbody.velocity.y <= 0) {
//外部から条件をtrueにしたいときはこうする
player.conditions |= ConditionIndex.IS_TIME_END;
//falseにする場合は player.conditions &= ~ConditionIndex.IS_TIME_END;
}
}
public override void OnEnd(ActionBase after, Player player) {
//終了時の処理
}
}
まとめ
条件などを設定するだけで自動で遷移するようになりました。とても便利なので何回か使っています。
また、この仕組みにすることでファイルがかなり分割されているので、同時に複数人でプレイヤーのプログラムを組むことができます。また、Playerクラスを編集すること自体はとても少なく、仕様が変わったタイミングでのみ編集すればよいだけなのでとても楽です。
応用例
Actionの中にActionの入れ子構造(ビヘイビアツリーやBlendTreeなど)
Transitionを編集しやすくする(エディター拡張など要ツール作成)
改善点
enumの列挙が大変
012...とすると逆にTransitionの追加で大変
途中を削除した場合次以降全て編集する必要あり
##謝辞
最初は定数クラスを作ったり、非効率な面が複数あったのですがalbireoさんに指摘していただいたもっと良い方法を採用した結果大幅に改善できました。ありがとうございます。