MATLAB でゲームを作る ～2. メインループ編～

MATLAB でゲーム制作シリーズ
- [0 章：グラフィックスオブジェクト](https://qiita.com/macht/items/05e848f2fa9fab0a538e)
- [1 章：コールバック](https://qiita.com/macht/items/5853875dce0381e33c3a)
- 2 章：メインループ（この記事）
- [3 章：アクションゲームでジャンプ](https://qiita.com/macht/items/357ad3201deb1683f010)

# はじめに

「MATLAB いじるのってゲームみたいなものだし、もはや MATLAB でゲームを作る必要ないのでは？」
という真理からは目を背けつつ、今日も元気に MATLAB でゲームを作っていきます。

[前回の記事](https://qiita.com/macht/items/5853875dce0381e33c3a)では、コールバックの設定方法を紹介し、実際に十字キーが押されたときにオブジェクトを移動する処理を作りました。

今回は、メインループとコールバックを組み合わせることで、「キーが押されている間だけ移動」などのより複雑な動作を作っていきます。


# メインループとは

　メインループとは、端的に言ってしまえば

```MATLAB:
while(1)
  % 毎フレームの処理
end
```

これです。ctrl + C を押さない限り永遠に同じ処理が繰り返されます。さすがにこれは解説不要でしょう。

　もちろん、実際には永遠に繰り替えされてしまっては困るので、何らかの break 条件が設定されます。MATLAB のゲーム作りで使う場合は、大体以下のようなメインループを使用します。

```MATLAB:
fig = figure;
while(isgraphics(fig))
  % 毎フレームの処理
  pause(sec); % sec 秒ポーズする。ポーズが必要なければ drawnow に変更可能。
end
```

まず、break 条件に[初回の記事](https://qiita.com/macht/items/05e848f2fa9fab0a538e)で紹介した `isgraphics` 関数を用いました。これによって Figure が閉じられた時点でループが break されますので、ゲームにキレて Figure を close するような短気な人がプレイヤーでも安心です~~（MATLAB ごと閉じる人は知らん）~~。

さらに、ループ内にポーズ処理も加えました。1 フレームの処理が速すぎるとゲームにならないので、ポーズ処理で時間を調整します。ちなみに昔のゲームの中には無駄なループを回して時間を調整してたものもあり、新しいハードに移したら爆速になってまともにプレイできない、みたいな現象が結構あったらしいです。

また、MATLAB はグラフィックスオブジェクトに何かしら変更があったとき、すぐには Figure を更新してくれず、すぐに更新させるためにはこの `pause` 関数や `drawnow` 関数などを使用する必要があります。時間調整が必要な場合は `pause` 関数、必要ない場合は `drawnow` を忘れずに入れるようにしましょう。


# メインループとコールバックの組み合わせ

　さて、このメインループ処理ですが、前回の記事で紹介したコールバックと相性がいいです。コールバックでユーザーのアクションに反応してメインループ内で使用している変数の値を変える、といった処理ができます。マイコンをいじったことをある人なら「ボタンが押されたときに処理を変える」というプログラムを 1 度は書いたことがあるかと思いますが、あれとほとんど同じです。

　実際に前回の記事で作った「十字キーの右が押されたら長方形を右に 1 だけ移動する」プログラムを改造し、「**一番直近に押された十字キーの方向に長方形が移動し続ける**」という処理を実装していきましょう。

　まず、前回のプログラムを全方向のキーに対応させてみます。

```MATLAB:
function mainloop_test()
fig = figure;
ax = axes;
axis equal;
axis ij; % y 軸の向きを反転
axis([0 10 0 10]);
rec = rectangle('Position',[0 0 1 1]);

fig.KeyPressFcn = @my_callback;

  function my_callback(src,data) % 全方向に対応
    if strcmp(data.Key,'rightarrow')
      pos = rec.Position;
      pos(1) = pos(1) + 1;
      rec.Position = pos;
    elseif strcmp(data.Key,'leftarrow')
      pos = rec.Position;
      pos(1) = pos(1) - 1;
      rec.Position = pos;
    elseif strcmp(data.Key,'uparrow')
      pos = rec.Position;
      pos(2) = pos(2) - 1;
      rec.Position = pos;
    elseif strcmp(data.Key,'downarrow')
      pos = rec.Position;
      pos(2) = pos(2) + 1;
      rec.Position = pos;
    end
  end
end
```

一応関数名を今回の記事用に変えています。内容的には y 軸の向きを反転したのと、`elseif` 処理を加えて全方向に対応しただけですね。もちろんこれだけだと、キーが押されたときに移動するだけですので、さらに改造していきます。「移動し続ける」ということは、もはやキーが押されたときのコールバックで移動処理をする必要はなく、メインループ内で定期的に移動していけばよいはずです。コールバックでしなければならないのは、その移動の方向を決めることです。

　これを踏まえて、コードを以下のように改造します。

```MATLAB:
function mainloop_test()
fig = figure;
ax = axes;
axis equal;
axis ij;
axis([0 10 0 10]);
rec = rectangle('Position',[0 0 1 1]);
vx = 0; % x 軸方向の速度
vy = 0; % y 軸方向の速度

fig.KeyPressFcn = @my_callback;

while(isgraphics(fig))
  pos = rec.Position;
  pos(1) = pos(1) + vx;
  pos(2) = pos(2) + vy;
  rec.Position = pos;
  pause(1/60);
end

  function my_callback(src,data)
    if strcmp(data.Key,'rightarrow')
      vx = 0.1;
      vy = 0;
    elseif strcmp(data.Key,'leftarrow')
      vx = -0.1;
      vy = 0;
    elseif strcmp(data.Key,'uparrow')
      vx = 0;
      vy = -0.1;
    elseif strcmp(data.Key,'downarrow')
      vx = 0;
      vy = 0.1;
    end
  end
end
```

新たに `vx` と `vy` という移動速度を表す変数を用意しました。メインループ内では 1 フレームに `vx`、`vy` だけに移動する処理、コールバックでは押されたキーに応じて `vx` と `vy` を変更する処理が行われています。

　これだけでも動かしてみると結構面白いのですが、長方形が画面外に出て行ってしまうのが少し気になりますね。移動はメインループが行っているので、メインループ内を改造して画面外に出ていかないようにしてみましょう。

```MATLAB:
function mainloop_test()
fig = figure;
ax = axes;
axis equal;
axis ij;
axis([0 10 0 10]);
rec = rectangle('Position',[0 0 1 1]);
vx = 0; % x 軸方向の速度
vy = 0; % y 軸方向の速度

fig.KeyPressFcn = @my_callback;

while(isgraphics(fig))
  pos = rec.Position;
  pos(1) = min(max(pos(1) + vx,0),9); % min,max で範囲を制限
  pos(2) = min(max(pos(2) + vy,0),9);
  rec.Position = pos;
  pause(1/60);
end

  function my_callback(src,data)
    if strcmp(data.Key,'rightarrow')
      vx = 0.1;
      vy = 0;
    elseif strcmp(data.Key,'leftarrow')
      vx = -0.1;
      vy = 0;
    elseif strcmp(data.Key,'uparrow')
      vx = 0;
      vy = -0.1;
    elseif strcmp(data.Key,'downarrow')
      vx = 0;
      vy = 0.1;
    end
  end
end
```

やり方はいろいろあると思いますが、今回は `min, max` 処理を入れ、位置が座標軸の範囲から出ないようにしました（壁にぶつかって止まってるイメージです）。ちなみに最大値を 10 ではなく 9 に制限しているのは、幅の分の 1 が余分にあるからです。


# 応用：キーが押されている間だけ動く

　さて、メインループとコールバックを組み合わせるといろいろできるということがわかったところで、冒頭で例として挙げた「キーが押されている間だけ動く」処理に改造していきましょう。

※以降のプログラムは、チャタリングが多いキーボードだとうまく動かないことがあるので、気を付けてください。

　前回の記事でも説明した通り、MATLAB には「キーが押されているかどうか」を判定する機能がありません。そのため、代わりに「キーが離れたときのコールバック」である `WindowKeyReleaseFcn` プロパティを使っていきます。直感的にはキーが離れたら速度が 0 になるように設定すれば、キーが押されたときだけ動くはずです。早速プログラムを改造してみます。

```MATLAB:
function mainloop_test()
fig = figure;
ax = axes;
axis equal;
axis ij;
axis([0 10 0 10]);
rec = rectangle('Position',[0 0 1 1]);
vx = 0; % x 軸方向の速度
vy = 0; % y 軸方向の速度

fig.WindowKeyPressFcn = @wkpf;
fig.WindowKeyReleaseFcn = @wkrf;

while(isgraphics(fig))
  pos = rec.Position;
  pos(1) = min(max(pos(1) + vx,0),9);
  pos(2) = min(max(pos(2) + vy,0),9);
  rec.Position = pos;
  pause(1/60);
end

  function wkpf(src,data)
    if strcmp(data.Key,'rightarrow')
      vx = 0.1;
      vy = 0;
    elseif strcmp(data.Key,'leftarrow')
      vx = -0.1;
      vy = 0;
    elseif strcmp(data.Key,'uparrow')
      vx = 0;
      vy = -0.1;
    elseif strcmp(data.Key,'downarrow')
      vx = 0;
      vy = 0.1;
    end
  end
  function wkrf(src,data)
    vx = 0;
    vy = 0;
  end
end
```

`WindowKeyPressFcn` に `wkpf` 関数、`WindowKeyReleaseFcn` に `wkrf` 関数をコールバックとして設定しました。`wkpf` 内では同様に速度変更の処理、`wkrf` 関数内では速度を 0 にする処理を記述しています。実際試してみると、キーを離したときにちゃんと止まることが確認できるかと思います。

完成！やったね！

　……とはなりません。これ、十字キーを押しながら適当なキーを押して離すと移動が止まってしまいます。`WindowKeyReleaseFcn` はどんなキーが離れても実行されるコールバックですので、当たり前っちゃ当たり前ですね。マ〇オでいうならジャンプボタンを押すと横移動がなくなってしまうということです~~（なんというクソゲー）~~。さすがにこのままでは使い物になりません。

　じゃあ `wkrf` 内で十字キー以外を弾けばいいかというと、それも十分ではありません。例えば、右から下に方向転換するときを考えると、右を離す前に下を押してしまうと右を離した時点で下方向の移動も止まってしまいます。これを防ぐには、「直前に押された十字キーと同じキーが離れた場合だけ速度を 0 にする」という処理に変更する必要があります。

```MATLAB:
function mainloop_test()
fig = figure;
ax = axes;
axis equal;
axis ij;
axis([0 10 0 10]);
rec = rectangle('Position',[0 0 1 1]);
vx = 0; % x 軸方向の速度
vy = 0; % y 軸方向の速度

fig.WindowKeyPressFcn = @wkpf;
fig.WindowKeyReleaseFcn = @wkrf;

currentKey = '';

while(isgraphics(fig))
  pos = rec.Position;
  pos(1) = min(max(pos(1) + vx,0),9);
  pos(2) = min(max(pos(2) + vy,0),9);
  rec.Position = pos;
  pause(1/60);
end

  function wkpf(src,data)
    if strcmp(data.Key,'rightarrow')
      vx = 0.1;
      vy = 0;
      currentKey = data.Key;
    elseif strcmp(data.Key,'leftarrow')
      vx = -0.1;
      vy = 0;
      currentKey = data.Key;
    elseif strcmp(data.Key,'uparrow')
      vx = 0;
      vy = -0.1;
      currentKey = data.Key;
    elseif strcmp(data.Key,'downarrow')
      vx = 0;
      vy = 0.1;
      currentKey = data.Key;
    end
  end
  function wkrf(src,data)
    if strcmp(currentKey,data.Key)
      vx = 0;
      vy = 0;
    end
  end
end
```

十字キーが押されたら、キー名を `currentKey` 変数に保存しておきます。キーが離れたときは、離れたキー（`data.Key`）が `currentKey` と同じ時だけ速度を 0 にします。これによって、変なタイミングで止まってしまうのを防ぐことができます。

　ちなみにこのプログラムはそのままシューティングゲームの自機操作に使えますし、上下方向をなくせば横アクションゲームにも使えます。「決定キーを押した長さでジャンプの距離が変わる」みたいな処理にも応用できそうですね。「キーが押されている間だけ〇〇」という処理は非常に汎用性が高いので、しっかり抑えておきましょう。

# まとめ

　今回の記事のまとめです。

- `isgraphics` 関数をメインループの break 条件にすると、figure が閉じたときにループから抜けることができる
- グラフィックスをすぐに更新するためには、`drawnow` や `pause` が必要
- 「〇〇し続ける」ような処理はメインループで行う。コールバックはその処理内の変数等を変更するために使うことが多い
- 「キーが押されている間だけ〇〇」という処理を作るには、`WindowKeyPressFcn` と `WindowKeyReleaseFcn` を組み合わせ、直近の方向キーと離れたキーが同じかどうか判別する必要がある


　次回の記事からは、そろそろアクションゲームに特化した話をしていきたいなと思っています。「ジャンプする」「オブジェクトに乗る」みたいな処理を実際に作っていく予定です。
　
　
　
　「いいね」が増えると次回の記事が豪華になるかも。