UDN
Search public documentation:

UnrealScriptStatesJP
English Translation
中国翻译
한국어

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

UE3 ホーム > UnrealScript > UnrealScript 言語のリファレンス > UnrealScript のステート

UnrealScript のステート


概要


「ステート」(状態) の概念は、歴史的に見ると、ゲームが「Pong」(卓球ゲーム) の段階を経て進化して以降、ゲームプログラマーたちに常に使用されてきました。ステート (「ステート マシン プログラミング」とも呼ばれる) は、複雑なオブジェクトのビヘイビアを管理する自然な方法です。しかしながら、UnrealScript 以前について言えば、ステートは言語レベルではサポートされていませんでした。そのため、開発者自身がオブジェクトのステートに基づく C/C++ の switch 文を作成する必要がありました。そのようなコードを書いたり更新したりするのは大変な作業でした。

UnrealScript は言語レベルでステートをサポートしています。

UnrealScript では、ワールド内の各アクタが、常にただ 1 つのステート (状態) にあります。ステートは、アクタが実行すべきアクションを反映します。たとえば、移動ブラシは、StandOpenTimed や BumpOpenTimed のようなステートをいくつかもっています。ポーンは、「Dying」 (瀕死)、「Attacking」 (攻撃中)、「Wandering」 (歩き回る) のようなステートをいくつかもっています。

UnrealScript では、特定のステートの中に置く関数とコードを書くことができます。それらの関数は、アクタがそのステートにあるときにのみ呼び出されます。たとえば、モンスターのスクリプトを書きながら、SeePlayer 関数を処理する方法を検討しているとしましょう。移動中に他のプレーヤと出会うと攻撃するように設定するとします。また、すでにそのプレーヤを攻撃している場合は、中断することなく攻撃を続けるように設定するとします。

以上を簡単に実現するには、いくつかのステートを定義して (「歩き回り」と「攻撃中」)、各ステートで異なるバージョンの Touch を記述します。UnrealScript では、このような方法を取ることが可能です。

ステートについてより深く学ぶ前に、ステートの主な利点 2 つと、厄介な問題 1 つを理解しておく必要があります。

  • 利点 1 : ステートを使用することによって、ステート固有の関数を簡単に書くことができます。そのため、アクタの行なっていることに基づいて、同じ関数を異なるやり方で処理することができます。
  • 利点 2 : ステートを使用することによって、特別な「ステートコード」を書くことができます。そのためには、通常の UnrealScript コマンドに加え、「潜在関数」として知られているいくつかの特殊な関数を使用します。潜在関数とは、「ゆっくり」 (すなわちノンブロッキングで) 実行される関数で、一定のゲーム時間 ( game time ) が経過した後に値を返します。これによって、時間ベースのプログラミングを行うことが可能になります。このことは、C、C++、Java にはない大きな利点です。すなわち、考えたとおりにコードを書くことができるのです。たとえば、スクリプトをあたかも次のように書くことができます。「このドアを開ける。2 秒待つ。このサウンドエフェクトを再生する。そのドアを開ける。モンスターをリリースして、プレーヤを攻撃させる」というように。つまり、単純で直線的なコードを書くことができます。時間ベースによるコード実行管理の詳細部分は、UnrealEngine が処理します。
  • 問題点: 子クラスの中で行うのと同じように、複数のステートの中で ( Touch のような) 関数をオーバーライドしたため、どの Touch 関数をどのような状況で呼び出すべきかといった問題が多数生じ、それを正確に把握しておかなければなりません。UnrealScript には、このプロセスを明確に定めた規則がありますが、この問題は、クラスとステートの複雑な階層を作成する場合に認識しておくべきものです。

次は、ステートの例として TriggerLight スクリプトから引用したものです。

  // Trigger turns the light on.
  state() TriggerTurnsOn
  {
     function Trigger( actor Other, pawn EventInstigator )
     {
        Trigger = None;
        Direction = 1.0;
        Enable( 'Tick' );
     }
  }
  
  // Trigger turns the light off.
  state() TriggerTurnsOff
  {
     function Trigger( actor Other, pawn EventInstigator )
     {
        Trigger = None;
        Direction = -1.0;
        Enable( 'Tick' );
     }
  }
  

ここでは、2 つの異なるステート ( TriggerTurnsOn と TriggerTurnsOff ) を宣言しています。それぞれのステートにおいて異なるバージョンの Trigger 関数を記述しています。ステートを使わずに実装することもできますが、ステートを使用することによって、モジュール性と拡張性の高いコードが書けます。UnrealScript では、既存のクラスを簡単にサブクラス化し、新しいステートを付け加え、新しい関数を追加することができます。ステートなしでこれを実装しようとすると、完成したコードを後で拡張することが難しくなるはずです。

ステートは編集可能 ( editable ) として宣言することができます。つまり、アクタのステートを「Unreal」エディタでユーザーが設定することも、しないことも可能なのです。編集可能なステートを宣言するには、次のようにします。

  state() MyState
  {
     ...
  }
  
  

編集不可能なステートを宣言するには、次のようにします。

  state MyState
  {
     ...
  }
  
  

また、auto キーワードを使用することによって、アクタの自動ステート (つまり初期ステート) を指定することができます。これによって、新しいアクタが初めてアクティブになると、必ずこのステートになります。

  auto state MyState
  {
     ...
  }
  
  

ステートラベルと潜在関数


関数の他にも、ステートは、1 つ以上のラベルを持つことができます。ラベルは、UnrealScript コードの前に付けます。例 :

  auto state MyState
  {
  Begin:
     `log( "MyState has just begun!" );
     Sleep( 2.0 );
     `log( "MyState has finished sleeping" );
     goto('Begin');
  }
  
  

上にあるステートのコードは、 ”MyState has just begun!” ( MyState が開始しました!) というメッセージを表示し、そこから 2 秒間休止し、さらに、 "MyState has finished sleeping" ( MyState のスリープが終了しました!) というメッセージを表示します。この例で面白いのは、潜在関数 Sleep の呼び出しです。この関数呼び出しは、すぐに値を返さず、一定のゲーム時間が経過した後に値を返しています。潜在関数は、ステートコード内からのみ呼び出すことができ、関数内から呼び出すことはできません。潜在関数を使用すると、時間の経過がかかわる複雑なイベントの連鎖をうまく管理することができます。

すべてのステートコードは、ラベルの定義から始まります。上記の例では、ラベルが「Begin」と名付けられています。ステートコードにおいて、ラベルは便利なエントリポイントとなります。ステートコードではあらゆるラベル名を使用することができますが、Begin ラベルは特別です。この Begin ラベルは、そのステートコードにおけるデフォルトの開始点となります。

すべてのアクタで、次の 3 つの主要な潜在関数を利用することができます。

  • Sleep( float Seconds ) は、ステートの実行を一定時間休止した後で再開させます。
  • FinishAnim() は、現在再生中のアニメーションシーケンスが終了するのを待ってから再開させます。この関数を使用することによって、アニメーション主導のスクリプト (実行がメッシュアニメーションによって制御されているスクリプト) を簡単に書けるようになります。たとえば、AI スクリプトのほとんどはアニメーション主導です (時間主導ではなく)。これは、なめらかなアニメーションが AI システムの主要な目標であるからです。
  • FinishInterpolation() は、現在の InterpolationPoint の動作が終了するのを待ってから再開します。

Pawn クラスは、アクション (例 : ワールド内の移動や短時間の動作など) にとって重要な潜在関数をいくつか定義します。使用方法に関する説明については、別途提供されている AI のドキュメントを参照してください。

ステートコードを書く場合、次の 3 つのネイティブ UnrealScript 関数が特に役立ちます。

  • ステートコード内の Goto('LabelName') 関数(C/C++/Basic の goto に相等) は、指定されたラベルにおいてステートコードの実行を継続します。
  • ステート内の特別な Stop コマンドは、ステートコードの実行を停止します。ステートコードの実行が再開されるのは、新しいステートになるか、現在のステート内で新しいラベルに入った場合です。
  • GotoState 関数は、アクタを新しいステートにします。オプションとして、指定されたラベルで実行を継続させます。(ラベルを指定しない場合、デフォルトは Begin ラベルとなります)。ステートコード内から GotoState を呼び出し、直接、目的のステートへ行くことができます。また、アクタ内のどの関数内からでも GotoState を呼び出すことができますが、すぐには有効になりません。実行がステートコードに戻るまでは有効にならないためです。

以下に、今まで説明したステートのコンセプトの実際例を示します。

  // This is the automatic state to execute.
  auto state Idle
  {
  	// When touched by another actor...
  	function Touch( actor Other )
  	{
  		`log( "I was touched, so I'm going to Attacking" );
  		GotoState( 'Attacking' );
  		`log( "I have gone to the Attacking state" );
  	}
  Begin:
  	`log( "I am idle..." );
  	sleep( 10 );
  	goto 'Begin';
  }
  
  // Attacking state.
  state Attacking
  {
  Begin:
  	`log( "I am executing the attacking state code" );
  	...
  }
  

このプログラムを実行して、アクタに手をつけに行くと、次のように表示されます。

  I am idle...
  I am idle...
  I am idle...
  I was touched, so I'm going to Attacking
  I have gone to the Attacking state
  I am executing the attacking state code
  
  

GotoState には理解しておくべき重要なことがあります。すなわち、関数内から GotoState を呼び出すと、直ちに目的の場所へ行くのではなく、実行がステートコードに戻ったときに目的の場所に行くということです。

ステートの継承とスコープの規則


UnrealScript では、既存のクラスをサブクラス化する場合、変数、関数、ステートのすべてを新しいクラスがその親クラスから継承します。これは良く知られていることです。

しかし、UnrealScript プログラミングモデルにステート抽象概念が追加されたことによって、継承とスコープ規則に新たな規則が追加されることになります。完全な継承の規則は、次のようになります。

  • 新しいクラスは、その親クラスからすべての変数を継承する。
  • 新しいクラスは、その親クラスの非ステート関数をすべて継承する。継承された非ステート関数は、どれもオーバーライドすることができる。また、完全に新しい非ステート関数を追加することができる。
  • 新しいクラスは、親クラスのステートをすべて継承する。これには、ステート内の関数とラベルも含まれている。継承されたステート関数は、どれもオーバーライドすることができる。また、継承されたステートラベルは、どれもオーバーライドすることができ、新たなステート関数および新たなステートラベルを追加することができる。

上記すべてのオーバーライドを例示すると、次のようになります。

  // Here is an example parent class.
  class MyParentClass extends Actor;
  
  // A non-state function.
  function MyInstanceFunction()
  {
  	`log( "Executing MyInstanceFunction" );
  }
  
  // A state.
  state MyState
  {
  	// A state function.
  	function MyStateFunction()
  	{
  		`log( "Executing MyStateFunction" );
  	}
  // The "Begin" label.
  Begin:
  	`log("Beginning MyState");
  }
  
  // Here is an example child class.
  class MyChildClass extends MyParentClass;
  
  // Here I'm overriding a non-state function.
  function MyInstanceFunction()
  {
  	`log( "Executing MyInstanceFunction in child class" );
  }
  
  // Here I'm redeclaring MyState so that I can override MyStateFunction.
  state MyState
  {
  	// Here I'm overriding MyStateFunction.
  	function MyStateFunction()
  	{
  		`log( "Executing MyStateFunction" );
  	}
  // Here I'm overriding the "Begin" label.
  Begin:
  	`log( "Beginning MyState in MyChildClass" );
  }
  

グローバルに実装されている関数が、1 つ以上のステート、および、1 つ以上の親クラスに存在する場合、どのバージョンの関数がどのような状況で呼び出されるかを知っておく必要があります。このような複雑な状況を解決してくれるスコープの規則は、次のとおりです。

*オブジェクトがあるステートに存在し、そのステートのどこかに (アクタのクラスか、親クラスのいずれかに) 関数の実装が存在する場合は、最派生のステートバージョンの関数が呼び出される。

  • そうでない場合は、最派生の非ステートバージョンの関数が呼び出される。

高度なステートのプログラミング


親クラス内にある同じ名前のステートをオーバーライドしないのであれば、extends キーワード (オプション) を使用すると、現在のクラス内において既存ステートに基づいてステートを拡張させることができます。これが役立つのは、たとえば、多くの機能を共有する類似したステートのグループ (例 : MeleeAttacking や RangeAttacking など) が存在する場合です。そのような場合は、基本となる Attacking ステートを次のように宣言することができます。

  // Base Attacking state.
  state Attacking
  {
  	// Stick base functions here...
  }
  
  // Attacking up-close.
  state MeleeAttacking extends Attacking
  {
  	// Stick specialized functions here...
  }
  
  // Attacking from a distance.
  state RangeAttacking extends Attacking
  {
  	// Stick specialized functions here...
  }
  

ステート内で ignores 指定子 (オプション) を使用することによって、そのステートにある間は、関数を無視することができます。この場合の構文は次のとおりです。

  // Declare a state.
  state Retreating
  {
  	// Ignore the following messages...
  	ignores Touch, UnTouch, MyFunction;
  
  	// Stick functions here...
  }
  

state 変数 (name 型の変数) によって、アクタがどのようなステートにあるかを知ることができます。

GotoState('') を使用することによって、アクタを「ステートなし」(no state) にすることも可能です。アクタが「ステートなし」の場合、グローバル (非ステート) 関数だけが呼び出されます。

GotoState コマンドを使用してアクタのステートを設定する場合、エンジンは EndState()BeginState() という 2 つの特別な通知関数を呼び出すことができます。(ただし、これらの関数が定義済みである場合)。 EndState は、新しいステートが開始される直前に現在のステートで呼び出されます。 BeginState は、新しいステートが開始された直後に呼び出されます。これらの関数は、ステートが必要とするであろうステート固有の初期化やクリーンアップを行うために都合の良い場所となります。

ステート スタッキング

通常のステート変更では、あるステートから別のステートへ入ると、既に終了している前回のステートに戻ることはできません。ステート スタッキングを利用すると、これが可能になります。 PushState 関数を呼び出すことによって、新しいステートに変わり、その新しいステートがスタックの最上部に置かれます。現在のステートは、凍結されます。 PopState を呼び出すと、前のステートがリストアされ、 PushState が呼び出された地点から実行が再開されます。 PushState 関数は、可能な場合潜在関数として機能します (ステートコード内部においてのみ)。したがって、 PushState を関数外部から呼び出す場合は、コード実行の動作が異なります。PushState を関数から呼び出してもコードの実行を妨害しません (関数内部から呼び出された GotoState と同様です)。一方、PushState をステートコード内部から呼び出すと、子クラスがポップされるまでコードの実行が休止されます (この点においても関数内部から呼び出さた GotoState と同様です)。

ステートをスタックに置けるのは一度だけです。同じステートを2回スタックに置こうとすると、失敗します。 PushState は、 GotoState と同じように機能し、ステート名とステートのエントリポイントのためのラベル (オプション) を引数に取ります。新しいステートは PushedState イベントを受け取り、現在のステートは PausedState イベントを受け取ります。 PopState の呼び出し後は、現在のステートは PoppedState イベントを受けとり、新たなステート (スタック上で次に位置していたステート) は、 ContinuedState を受けとります。

  state FirstState
  {
  	function Myfunction()
  	{
  		doSomething();
  		PushState('SecondState');
  		// this will be executed immediately since we're inside of a function (no latent functionality)
  		JustPushedSecondState();
     }
  
  Begin:
  	doSomething();
  	PushState('SecondState');
  	// this will be executed once SecondState is popped since we're inside of a state code block (latent functionality)
  	JustPoppedSecondState();
  }
  
  state SecondState
  {
  	event PushState()
  	{
  		// we got pushed, push back
  		PopState();
  	}
  }
  

IsInState 関数を使用することによって、あるステートがスタック上に存在するか否かをチェックすることができます。この関数は、ステートの名前だけをチェックするため、親ステートのチェックには使用することができません。例 :

  state BaseState
  {
     ...
  }
  
  state ExtendedState extends BaseState
  {
     ...
  }
  
  

アクティブなステートが ExtendedState である場合、 IsInState('BaseState') が false を返します。もちろん、 BaseState がスタック上にある場合は、 IsInState('BaseState', true) を呼びだすと true が返されます。