はじめに
ついにVketCloudのHeliScriptでinterfaceが使えるようになりました!
今回は、interfaceを活用したNPCステートマシンをサンプルコード付きで解説していきます。
経験者向け情報
有名なゲーム設計書籍キウイ本の第7章(P.98~)に登場する
ステートパターンを参考にHeliScript化したものです。
interfaceって何?
● クラスが持つべきメソッドの宣言
● 異なるクラスでも同じinterfaceを継承することで共通の動作を保証
● 実装の詳細は各クラスで自由に書けるため、柔軟な設計が可能
より詳しい説明はこちら!
今回は全ステートクラスがIStateActionを継承し、Enter/Exit/Updateを保証します。
ステートマシンとは?
ゲーム全体、キャラ、UI問わず「状態」を管理する仕組みです。
特に状態の遷移が多岐にわたるキャラクターでは大きな恩恵を得られます。
モーション派生が大量にあるキャラ操作の例
- 例1:待機 → 通常攻撃
- 例2:待機 → ダッシュ → ダッシュ攻撃 → ジャンプ → ジャンプ攻撃
コピペで動くサンプルコード(HeliScript)
NPCの実装を例にした実用的な状態遷移サンプルです。
UnityのAnimatorであらわすとこんな感じの遷移をプログラムで実現します。

[基礎] 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!
あとは別のステートから派生させてあげつつ、ステートの挙動も書いていきます。
✅ イベントシステムとの相性抜群
疎結合な実装が可能なため、インターフェース式ステートマシンととても相性が良いです。
イベント機能についてはこちら
イベント処理サンプルコード
/// どこかのステートクラス内部
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