5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【コピペで動く】HeliScriptでステート追加が一瞬に!interface式ステートマシン入門

Last updated at Posted at 2025-12-18

はじめに

ついにVketCloudのHeliScriptでinterfaceが使えるようになりました!
今回は、interfaceを活用したNPCステートマシンをサンプルコード付きで解説していきます。

経験者向け情報
有名なゲーム設計書籍キウイ本の第7章(P.98~)に登場する
ステートパターンを参考にHeliScript化したものです。

interfaceって何?

● クラスが持つべきメソッドの宣言
● 異なるクラスでも同じinterfaceを継承することで共通の動作を保証
● 実装の詳細は各クラスで自由に書けるため、柔軟な設計が可能

より詳しい説明はこちら

今回は全ステートクラスがIStateActionを継承し、Enter/Exit/Updateを保証します。

ステートマシンとは?

ゲーム全体、キャラ、UI問わず「状態」を管理する仕組みです。
特に状態の遷移が多岐にわたるキャラクターでは大きな恩恵を得られます。

モーション派生が大量にあるキャラ操作の例

  • 例1:待機 → 通常攻撃
  • 例2:待機 → ダッシュ → ダッシュ攻撃 → ジャンプ → ジャンプ攻撃

コピペで動くサンプルコード(HeliScript)

NPCの実装を例にした実用的な状態遷移サンプルです。

UnityのAnimatorであらわすとこんな感じの遷移をプログラムで実現します。
image.png

[基礎] IStateAction.hs

すべてのステートが必ず持つメソッドを定義する。

// -------------------------------------------
// IStateAction: 各ステートが必ず持つべき関数
// -------------------------------------------
interface IStateAction{
    public void Enter();      // 状態に入った時に1回だけ呼ばれる
    public void Exit();       // 状態を抜けるときに1回だけ呼ばれる
    public IStateAction Update();  // 毎フレーム呼ばれる。次の状態を返す
}

[ステート] Idle.hs

クリックでAction、5秒が経つとWaitへ切り替わる。

// -------------------------------------------
// Idle: NPCの待機状態
// ・5秒放置で[Wait]へ
// ・マウスの左クリックで[Action]へ
// -------------------------------------------
class Idle:IStateAction{
    private float _timeCounter;

    // 初期化処理
    public void Enter(){
        this._timeCounter = 0;
        hsSystemWriteLine("IDLE Enter!!");
    }

    // 終了処理
    public void Exit(){
        hsSystemWriteLine("IDLE Exit!!");
    }
    
    public IStateAction Update(){
        // null → 遷移なし
        IStateAction nextState = null;

        Idle();
        
        // 5秒経過したらWaitへ遷移
        if(this._timeCounter >= 5){
            nextState = new Wait();
        }

        // マウスの左クリックでActionへ移動
        if(hsInputClickButton(HSMouse_Left)){
            nextState = new Action();
        }

        // 毎フレーム時間を加算
        _timeCounter += hsSystemGetDeltaTime();

        return nextState;
    }

    private void Idle(){
        // ここで待機中の処理をする
        // モーションを切り替えたりなど
    }
}

[ステート] Wait.hs
放置時のモーション。

// -------------------------------------------
// Wait: NPCが放置されている状態
// ・放置モーション終了で[Idle]へ
// -------------------------------------------
class Wait:IStateAction{
    private bool _isEndMotion;
    
    public void Enter(){
        _isEndMotion = false;
        hsSystemWriteLine("Wait Enter!!");
    }
    
    public void Exit(){
        hsSystemWriteLine("Wait Exit!!");
    }
    
    public IStateAction Update(){
        IStateAction nextState = null;

        Wait();
        
        // モーション終了 → Idle へ戻る
        if(_isEndMotion){
            nextState = new Idle();
        }

        return nextState;
    }
    
    private void Wait(){
        // 本来はここでモーションの終了を検知する
        // 今回はサンプルのため即座に終了扱いにしている
        _isEndMotion = true;
    }
}

[ステート] Action.hs
ジャンプや攻撃などのアクションを行う場所。

// -------------------------------------------
// Action: NPCがアクション(攻撃など)する状態
// ・3秒経ったら[Idle]へ
// ・もちろん2段目の攻撃派生も作れる
// -------------------------------------------
class Action:IStateAction{
    private float _timeCounter;
    
    public void Enter(){
        this._timeCounter = 0; 
        hsSystemWriteLine("Action Enter!!");
    }
    
    public void Exit(){
        hsSystemWriteLine("Action Exit!!");
    }
    
    public IStateAction Update(){
        IStateAction nextState = null;

        Action();

        // 今回は3秒経ったらIdleへ帰る
        if(_timeCounter >= 3){
            nextState = new Idle();
        }
        
        // 毎フレーム時間を加算
        _timeCounter += hsSystemGetDeltaTime();
        
        return nextState;
    }
    
    private void Action(){
        // ここでアクション(攻撃など)の処理をする
        // 剣を振ったり、銃を撃ったり
    }
}

[コンポーネント] StateManager.hs

Item にアタッチするメイン管理コンポーネント。
最終的には、ここがエントリーポイントとなります。

// -------------------------------------------
// StateManager: 今の状態を管理し、遷移を行うクラス
// ・ステートの Update() が次のステートを返したら遷移
// -------------------------------------------

component StateManager
{
    private IStateAction _stateActor;  // 現在のステートを保持
    
    public StateManager()
    {
        // 初期ステートを登録
        this._stateActor = new Idle();
        this._stateActor.Enter();
    }

    public void Update()
    {
        // 各ステートが遷移先を自動で返してくれる
        IStateAction nextState = _stateActor.Update();
        
        // null以外が返ってきたら遷移
        if(nextState !== null){
            this.ChangeState(nextState);
        }
    }
    
    // 実際のステート遷移処理
    private void ChangeState(IStateAction state){
        this._stateActor.Exit();   // 現ステートの終了処理
        this._stateActor = state;  // 新しいステートへ切り替え
        this._stateActor.Enter();  // 新ステートの初期処理
    }
}

interface を使うメリット

✅ 新しいステート追加が一瞬で済む

これだけで新しい状態が作れる。

[新ステート] SampleState.hs

class SampleState:IStateAction{

    public void Enter(){
        hsSystemWriteLine("SampleState Enter!!");
    }
    
    public void Exit(){
        hsSystemWriteLine("SampleState Exit!!");
    }
    
    public IStateAction Update(){
        IStateAction nextState = null;
        
        Sample();
        
        return nextState;
    }

    private void Sample(){
        // 新しいステートでの処理を定義
    }
}

これだけでOK!
あとは別のステートから派生させてあげつつ、ステートの挙動も書いていきます。


✅ イベントシステムとの相性抜群

疎結合な実装が可能なため、インターフェース式ステートマシンととても相性が良いです。

CallComponentMethod()も使わずに済む!

送信側はただ送信すれば良いので「CallComponentMethod()をするためにItemを取得するためにItemNameを取得して~」という手間が無くなりコードがスッキリします。

イベント機能についてはこちら

イベント処理サンプルコード

/// どこかのステートクラス内部
public void Enter(){
    // イベントリスナー追加
    // 例) NPCをクリックした時のイベント
    hsAddEventListener("Touch", OnTouchMe);
}

public void Exit(){
    // イベントリスナー削除
    hsRemoveEventListener("Touch", OnTouchMe);
}

// 自分がクリックされた場合
private void OnTouchMe(string arg){
    // イベント処理の実行
}

❌ interface抜きで実装すると...

StateManagerにかなりの負担がかかる実装になります。

ステートの終了チェックもしつつ、周辺の状況から適切なステートをUpdateで監視。
ステートを切り替える時もswitch()で大量のステートの羅列が必要。
ステートを1つ増やすとswitch()の羅列が増えてメンテナンス性が下がっていく。

// interface無しの簡単な例
void Update(){
    // 現在のステートから次の派生先を探す
    switch(nowState){
        case "idle":
            if(timer > 5) nowState = "wait";
            if(clicked) nowState = "action";
            break;
            
        case "wait":
            if(motionEnd) nowState = "idle";
            break;
            
        case "action":
            if(motionEnd) nowState = "idle";
            break;
        // 10ステートで100行超える...
    }
}

例えるなら、
● interfaceなし:Managerが目視で手動管理する重労働
● interfaceあり:ステートが自律して次の遷移先を送ってくれる全自動式

ちょっと宣伝

今回紹介したサンプルコードは、実際に下記の記事で実装しました。
サンプルワールドも含めてぜひご覧ください。

🏁 まとめ

今回紹介したステートマシン実装により、「状態」を含む処理が劇的に分かりやすくなります。

  • 状態ごとにクラス分割でき保守性が高い
  • 新しい状態を追加しても壊れにくい
  • イベントシステムとも相性がとても良い

HeliScriptでゲーム的な処理を書く際は、ぜひステートマシンを取り入れてみてください!

執筆者:HIKKY 伊東(ふれふれ)
投稿:2025/12/19

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?