ゲーム制作において、かなり悩ましい問題はこの敵の移動処理です。今回はゲームのタイプに関わらない移動処理について、そのおおまかな考え方について私の実装方法を解説していきます。なお、移動とともに繋がりの深い表示と消去については、日を改めて記事化できればと考えています。


  • 直進と方向転換
まず敵の位置を X,Y とします。そして、敵の進行方向を dir で表します。昔のゲームでは上下左右の4方向が大多数でした。これは X と Y をそれぞれ ±1 すれば位置が変更出来るため簡単だったからです。8方向だと斜め方向は ±0.707( 1÷√2 ) じゃないと斜め方向に速度が速くなります。この辺りは三角関数と弾幕で解説していますので、興味があれば参照してください。

さて、移動方向はどうやって決めれば良いでしょうか。大きく分けて、以下の3つの方法があります。
  1. ランダム
  2. あらかじめ決められた方向に
  3. プレイヤーの位置に対して
それぞれ説明します。


 1. ランダム

方向を乱数で決めます。完全に乱数で決めると、多くの場合は殆どその場から動かず、その場でプルプルしているだけとなります。現在の移動方向から左右だけに限定すると、多少はマシになりますが、それでもおそらくかなり狭い範囲でウロウロしているだけでしょう。この問題を解決するには、後述する直進カウンタを組み合わせる必要があります。

あるいは、動きたい方向が選択されるようにランダム値を調整すると、なんとなくその方法に近づくようになります。例えば、だんだん右に移動したい場合は、以下のような判定で実装します。

■ C#

int u = 4, d = 4, l = 3, r = 5;
int max = u + d + l + r;
var rnd = new Random(); var dir = rnd.Next(0, max);

if (dir < u) MoveCheck(Dir.Up); else if (dir < u + d) MoveCheck(Dir.Down); else if (dir < u + d + l) MoveCheck(Dir.Left); else MoveCheck(Dir.Right);


 2. あらかじめ決められた方向に

シューティングゲームの敵に多い動きですが、あらかじめ決められた動きをする処理となります。例えば、画面右側から出現して、画面中央まで進み、その後クルンと向きを変えて画面範囲外に離脱…みたいな動きの事です。パターンプレーが可能になりますので、敵をなぎ倒す爽快感を演出する時の、敵の動きは大体これですね。これを出現テーブル移動スクリプトで管理します。

この決められた動きを司るスクリプトについては、それだけで一つの記事が書けてしまいますので、ここでは詳細は省きます。


 3. プレイヤーの位置に対して

自分の座標とプレイヤーの座標を比較して方向を決めます。あるいは現在位置と移動方向ベクトルを参照して移動方向を決めます。必ずしも近づくだけではありません。例えば、横方向に移動している際に、プレイヤーが同じ Y 軸線上にいれば必ず上(あるいは右)に曲がるというのも、この範疇となります。

個人的には、真後ろに反転移動するのは最終手段だと考えています。進行方向に対しての「左右」どちらに曲がれるかを判定して、それが叶わない場合は直進を継続するのが良いでしょう。曲がりたい方向に曲がれず、直進も出来ない場合は、逆方法に移動できないか確認して、それでもダメな場合は反転させます。なぜならその場所は行き止まりだからです。

上記 1,2,3 の動きを一定時間毎に切り替える事で、なんとなく気がつくとプレイヤーに近づいているような敵の動きが作れます。また、直線上にプレイヤーが見えた時だけ、プレイヤーめがけて直線的に動くなんて動作も作れたりします。
※ SFC互換機ですがなんとハンディタイプです。そしてデザインがPS-Vitaっぽいというよく分かんないヤツですねw

  • どれだけ進むか / 直進カウンタ
テーブルで動いている場合を除き、殆どの敵の動きにはこのどけだけ進むかを管理する直進カウンタが用いられます。この直進カウンタが 0 より大きい場合は、カウンタ値を -1 して直進だけ処理します。その際は、衝突判定と画面範囲外判定だけは常に必要で、何かに衝突したらそれに応じた対応をします。プレイヤーなら相手にダメージ、弾なら被弾処理、画面範囲外なら存在消去処理などです。C# で構造体定義すると、以下のような感じになるかと思います。

■ C#

enum Dir { Up, Down, Left, right } // 上下左右 struct Charactor { public Dir dir; // 方向 public Point position; // 現在の位置 public int cntAhead; // 直進カウンタ }

この直進カウンタは、曲がったタイミングで毎回初期化します。そして、以後は直進カウンタ値が 0 より大きければ…と同様の判定と処理を行います。この初期化で入れ込む値が小さければ小さいほどせわしなく動きます。値が大きいとあまり曲がらなくなります。

直進カウンタ値が小さいとかなりクイックに向きを合わせてきますが、直進カウンタ値が大きいと大きな半径で位置を合わせてきます。あまりにも小さいとゲームにならなくなる可能性もあります。そのため、この直進カウンタの初期値は、敵の性格付けゲームバランス調整に使われます。ベータテスト時の要調整項目だったりするのです。故に初期値はだいたい敵のタイプ別にデータとして持っていたりします。

また、直進カウンタ値が常に同じ値だと、動きが幾何学的すぎてつまらない事があります。ミサイルや敵機などの場合は、直進カウンタの初期値は固定で良いと思いますが、生物的な敵の場合は、直進カウンタの初期値には乱数で変化を付けると良いです。例えば、いつも最低6キャラ進んでから曲がるとして、これを 5 ~ 8 キャラと乱数で変化させると、プレイヤーに「うわっ」と思わせる事が出来ます。極端に直進カウンタの初期値を変化させると、ほぼランダムな動きと変わらなくなる可能性がありますのでオススメしません。この辺りはデザイナーの感性に因るかと思います。


  • どうやって曲がるか
先にも記載したとおり、曲がる方向は基本的には進行方向に対して左右です。そのため、例えば右に曲がりたいとすると、こんな感じの処理を記述する事が多いと思います。
※ プレイヤーの方向に曲がる処理を想定

■ C#

switch (enemy.dir) { case Dir.Up: MoveCheck(Dir.Right); break; case Dir.Down: MoveCheck(Dir.Left); break; case Dir.Left: MoveCheck(Dir.Up); break; case Dir.Right: MoveCheck(Dir.Down); break; }
■ Z80

; 方向は上下左右で 0,1,2,3 と定義 ld a, (enemy.Dir) ; Acc 方向 cp 2 ; 上下と左右に分岐判定 jr c, .updn ; CF=1 上下移動中 ; 左右に移動中 jr z, .L ; ZF=1 左に移動中 call ChkMvD ; 下に移動できるか jr .cont ; 処理の継続 .L call CjlMvU ; 上に移動できるか jr .cont ; 処理の継続 ; 上下に移動中 .updn or a ; 更に方向判定 jr z, .U ; ZF=1 上に移動中 call ChkMvL ; 左に移動できるか jr .cont ; 処理の継続 .U call ChkMvR ; 右に移動できるか jr .cont ; 処理の継続

上記の記述方法は直感的で分かりやすいのですが、それは4方向だからです。この方向が8方向以上になると急に面倒になります。そこで、方向に関しては右側を方向 0 の基準方向として、反時計方向周りにグルッと回るように定義してやると、プログラムが一気に簡単になります。例えば、8方向だと以下のようになります。

■ C#

enum Dir { R, UR, U, UL, L, DL, D, DR, Max } var nextDir = (Dir)(((int)enemy.dir - 1) % (int)Dir.Max); MoveCheck(nextDir);

■ Z80

; 方向は右から反時計方向に 0,1,2,...7 と定義 ld a, (enemy.Dir) ; Acc 方向 dec a ; Acc 方向を右側に and 00000111b ; 桁を8方向に丸める add a, a ; 2倍 ld a, chktbl ; テーブル下位アドレス加算 ld l, a ld h, chktbl / 256 ; HL ジャンプテーブル ld a, (hl) inc l ld h, (hl) ; HL 移動先チェックルーチンアドレス ld de, .cont push de ; 戻り先をスタックに送る jp (hl) ; チェックルーチンに飛ぶ ; 移動先チェックルーチンは256境界内前提とする chktbl: dw ChkMvR, ChkMvUR, ChkMvU, ChkMvUL dw ChkMvL, ChkMvDL, ChkMvD, ChkMvDR

C# だとめちゃくちゃ短いですね。Z80 は逆に少し重くなりましたが、この考え方で実装すると、32方向でも64方向でも、このルーチンのままで動作します(移動先チェックは大変ですが…)。

※ 2022/05/25 1300 追記
上記の処理だと正確には進行方向に対して右斜め前を確認しています。真右をチェックしたい場合は、-1 では無くて -2 の位置を確認します。

以上、何かの参考になれば幸いです。