GameMaker 日本語掲示板

オブジェクト(struct)ベースのStateパターン

9 コメント
views
17 フォロー

GameMakerでのStateパターンの実装は、チュートリアル動画などでは下記のように、functionを変数に持つ形が紹介されています。
これを、オブジェクトベースにできないかと考えたので紹介したいと思います。また、こういうやり方が「ありかなしか」が判断できないので、プログラミングに詳しい方の意見をいただけたら助かります。


■チュートリアルで紹介されてるよくある形
(stateの関数の内容は省略)

create event:
state = scr_player_walk; // ← 関数(function)

step event:
state();



■オブジェクト(struct)ベースで実装する形
このケースではidleとwalkのステートを60step毎に交互に切り替えます。
プロジェクトは、1つのスクリプトと1つのオブジェクト(とスプライト)で構成されます。


■スクリプト: "player_states"

function idle(_obj) constructor
{
	obj = _obj;
	sprite = spr_player_idle;
	imgspd = 0.01;
	cnt = 0;
	
	init = function()
	{
		cnt = 0;
		obj.sprite_index = sprite;
		obj.image_speed = imgspd;
	}
	
	loop = function()
	{
		if (cnt++ == 60) end_state();
	}
	
	end_state = function()
	{
		with(obj) scr_state_change(new walk(id));
	}
}


function walk(_obj) constructor
{
	obj = _obj;
	sprite = spr_player_walk;
	imgspd = 0.2;
	cnt = 0;
	
	init = function()
	{
		cnt = 0;
		obj.sprite_index = sprite;
		obj.image_speed = imgspd;
	}
	
	loop = function()
	{
		with (obj) x += 2;
		if (cnt++ == 60) end_state();
	}
	
	end_state = function()
	{
		with(obj) scr_state_change(new idle(id));
	}
}



■ オブジェクト:o_player
create event

state = undefined;

state_change = function (_state)
{
	if (!is_undefined(state)) delete state;
	state = _state;
	state.init();
}

state_change(new idle(id));



step event

state.loop();



どうでしょう。この方法ありでしょうか?
もしこれが「あり」なら、この方法にはメリットがあると思います。

  • state毎にインスタンス変数を持てる
  • state毎にメソッドを持てる
  • state開始時に1回だけ実行する処理もスッキリ実装できる

疑問なのは、stateオブジェクトのメソッドで呼び出した側を変更していいのか?という辺り。「こういう実装は問題を生みがちなのでやらない方がいい」などということはありますか?
以上です。ご意見をお待ちしています。

asa
作成: 2022/07/08 (金) 12:30:25
最終更新: 2022/07/08 (金) 13:52:58
通報 ...
1
hayate212 2022/07/08 (金) 17:27:08

idle や walk といったステートは状態の実装のみを責務として、ステートの遷移は実装を切り分けるのはどうでしょうか?
ステート遷移が複雑になるほどステートの内部も複雑化してしまう気がしまして・・・

また、切り分けることでステートを自由に組み合わせることが出来るようになるため使いまわしも用意になるかなと
例えばマグロ🐟は walk <-> attack の遷移しか出来ないことをステートを使いまわした上で表現出来るようになります

2

遷移に係わる部分は各stateの、

stateを終了する条件の部分

if (cnt++ == 60) end_state();

と、遷移先のstateを決めて遷移する部分

end_state = function()
{
  with(obj) scr_state_change(new idle(id));
}



これを切り分けてstateの外に実装するのがよいということでしょうか。

3
生高橋 2022/07/08 (金) 17:59:24 修正

自分もこんな感じでポーズメニューをつくりました。
自分の知識が浅いからなのか思ったよりも複雑なった印象を受けました。

GameMakerは他の言語と違ってポリモーフィズムをする際の制約がないんですよね。
インターフェースがないのでstateごとに同名の関数の存在が保証されないです。
新しいstateを追加したときに関数の作り忘れが起きてしまったら一気にバグります。
まぁコピペすればいいだけなんですが。

ただし、新しい関数を追加したり名前や引数を変更してしまったら他のstateも全部変更が必要です。
stateが少なければこれもまぁ問題ないかもしれません。
他の言語ではVisualStudioを使っていると名前の一括変換などの機能があるので楽なんですよね。

そして、特定のステートのみの共通な処理や他のstateのデータを利用するとなったとき若干やっかいです。
無理に共通化したりすると他のstateにも影響をうけます。
ここらへんから自分は複雑になってきた印象があります。

ただ自分はまだ設計が浅いのでもしかしたらもっとちゃんと設計すればそれらもスッキリ解決できるんじゃないかと勉強中です。

4

stateをオブジェクトにすることでかえって複雑になる可能性があるということですかね。

5

遷移する度にインスタンスの破棄とインスタンス化をするのが負荷になる可能性があるという指摘も頂きました。
いろいろ考えるべきことがありますね~

6

別口でご回答させていただきましたが、為念こちらにも。

gameMakerは学んだことがなく知識や仕様理解は暗いですが、気になった点としてインスタンスの生成を上げさせていただきました。
もう1点、idleとwalk内で同様の処理が記述されていることも、冗長になりがちな点と、コピペミス等で不具合が発生する原因になりそうであったため、少し気になりました。

上記2点をとりあえず解決するという目的で、自分ならこうするかな? という観点でコードを記載させていただきます。

先に述べました通り、gameMakerには疎いため、文法等に不備等がありましたらご容赦ください。(withってなんやレベルでした)
あくまで考え方のひとつとして見ていただければと思います。

function character(_obj) constructor {
	obj = _obj;
	cnt;

	current_motion;
	idle = new motion(obj, spr_player_idle, 60, 0.01);
	walk = new motion(obj, spr_player_walk, 60, 0.2, 2);
	
	init = function() {
		cnt = 0;
		idle.next_motion = walk;
		walk.next_motion = idle;
		current_motion = idle;
	}
	
	loop = function() {
		if (cnt >= current_motion.limit) {
			current_motion = current_motion.next_motion();
			cnt = 0;
			obj.sprite_index = current_motion.sprite;
			obj.image_speed = current_motion.image_speed;
		}
		current_motion.action();
		cnt++;
	}
}
function idle(_obj, _sprite, _image_speed, _limit) constructor
{
	obj = _obj;
	sprite = _sprite;
	image_speed = _image_speed;
	limit = _limit;

	next_motion;
	
	action = function() {
	}
}
function walk(_obj, _sprite, _imgspd, _limit, _speed) constructor
{
	obj = _obj;
	sprite = _sprite;
	image_speed = _image_speed;
	limit = _limit;
	speed = _speed;
	next_motion;

	action = function() {
		with (obj) x += speed;
	}
}

以下、メモ書きです。

それっぽくかんがえたこと

  • idleとwalkのインスタンスを別の構造体の中に持たせてみた
      newしないで切り替えできるように
      (ここだけ見ると構成的にo_playerに持たせてもよさそうな?)
      ついでにidleとwalkで共通していた処理も1か所書けばいいようになった気がする
  • 現在のモーション(idleかwalk)を、current_motionに保持
      何かしら実行するときはこれだけ見てればいい
      gameMakerで出来るかどうかは知見のなさ故に不明瞭ですが、たぶんできるやろの精神
  • idleとwalkに、次の遷移先を持たせる
      本当はアドレス指定よりKey値で指定できる形が望ましいと思いますが、とりあえずの形で
      切り替えるときはloopで60cntした時だったところを、limit変数にしてみた
      limitもidleとかに持たせたのでidleは60f, walkは40fとか個別指定もできるかも?
  • idleとwaikからマジックナンバーを消してみた
      全く別のobjからもidleやwalkが使える
      移動量とかも全部インスタンス生成時に設定できるからmotionで指定するとかできそうな気がする
      汎用性は低そうなのでそこまでする必要があるかどうかは不明
  • ソースよりコメントに時間がかかってしまった
      長文になってしまい申し訳ございませぬ・・・
7

友人さん ありがとうございます。

characterに各motionインスタンスを全部持たせて、init()とloop()もそっちに移動、
motionはloopから呼ばれるactionのロジックだけにするという構造なんですね。

いろいろ勉強させていただきます。
サンプルコードから詳しい説明まで、ありがとうございます!!

8
hayate212 2022/07/15 (金) 10:10:18

何に関心(AI,メニュー遷移,etc...)を持って実装をするかで大きく構造が変わるなと思い
オブジェクトの Sprite を管理するステートマシンを実装してみました

asa さんが最初に提示した実装例から以下のようなことを考えました

  • ステート内に遷移条件が実装されている
    • 開発が進むにつれステート内部が複雑になっていきそう
  • ステート内に挙動とアニメーション管理が実装されている
    • 前方への攻撃アニメーションを再生しつつ後退する場合、後退が行われた瞬間に攻撃アニメーションはキャンセルされる?
      • 場合によっては前方への攻撃アニメーションを残しつつ後退することも考えられるので、挙動=アニメーションではないはず
  • 新たにジャンプステートを実装する場合、ジャンプしながらの横移動処理はジャンプステートにも実装される?
    • 実装の重複は修正漏れ考えると避けたい・・・

そこでアニメーションの管理のみを行うステートマシンを実装しました。
以下が仕様です。

  • アニメーションステート(以下ステート)は Sprite を必ずひとつ持つ
  • 遷移条件構造体は遷移条件関数と遷移先のステート名を持つ
    • 遷移条件関数は遷移条件を満たしたかどうかの Bool 値を必ず返す
  • ステートをステートマシンに登録する際には以下の情報が必要
    • ステート名
    • ステート
    • 遷移条件構造体の配列
  • 毎ステップ、条件付きループ(for)で遷移条件構造体の配列の中から順番に遷移条件関数を実行し true が返った場合、遷移条件構造体の遷移先ステート名を元にステートを現在のものから置き換え、ループを抜ける
  • 毎ステップの最後に現在のステートから Sprite をオブジェクトに割り当てる

これにより挙動とアニメーション管理を分離出来ます。

*上記仕様をもとに実装しステートマシンを使用している部分
画像1

9

ステート毎に終了条件(遷移条件)が違い遷移先も条件で複数変わるので、仕方なくステート内にそれらを書いていましたが、ステートをインスタンス化するときにこういう方法でfunctionを渡すことができるんですね。

あと、進行方向とアニメの向きが必ずしも一致しないというのはその通りですね。後退しながら攻撃したり、体を対象の方に向けながら歩き回ったり。
ステートを、アニメの開始から終わりまでという単位で考えていたのでステートとアニメを一体化して捉えていましたが分けたらスッキリするなら分けたいですね。

考え方とサンプルコードで勉強になります。参考にさせて頂きます。
ありがとうございます!