MATLAB でゲームを作る ～1. コールバック編～

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

# はじめに

MATLAB でゲームを作りたいという至って正常な人のため、今日も記事を書いていきます。


[前回の記事](https://qiita.com/macht/items/05e848f2fa9fab0a538e)では、グラフィックスオブジェクトの基礎を解説しました。今回は、ゲーム作りに必須となる「コールバック」について解説していきたいと思います。


# 前回のおさらい
　本題に入る前に、前回のグラフィックスオブジェクトのおさらいをしてみましょう。

　まずは以下のコマンドで Figure 上に長方形を作成してみます。

```MATLAB:
fig = figure;
ax = axes;
axis equal;
axis([0 10 0 10]);
rec = rectangle('Position',[0 0 1 1]);
```

この長方形を右に 1 だけ移動するにはどうすればよいでしょうか？
・
・
・
[Rectangle のドキュメント](https://jp.mathworks.com/help/matlab/ref/rectangle.html)によると、Rectangle の位置は Position プロパティで決まっています。そのため、一度現在の Position プロパティを取得し、x 座標（1 番目の要素）を 1 増やし、再度 Position プロパティに設定すればいいですね。コードにすると

```MATLAB:
pos = rec.Position;
pos(1) = pos(1) + 1;
rec.Position = pos;
```

こんなかんじです。ドットを使えば簡単に記述できますね。

　
　さて、おさらいとして書いたこのコードですが、rectangle がゲーム内のキャラだと考えれば、これはそのままキャラを動かす処理となります。敵が近づいてくる、アイテムが落ちてくる、といった処理はこのコードと同様の処理を行えばいいわけです~~（長方形が敵とかいつの時代だ）~~。

　しかし、キャラを動かす処理として考えると、まだ大きく抜けている要素があります。それは「ユーザーのアクションに反応して動く」処理です。この rectangle が操作キャラなのであれば、やっぱり十字キーを押した方向に動いて欲しいですよね。

　このような処理を実装するのに必要となるのが、今回の記事のタイトルにもなっている「コールバック」です。

# コールバック

　MATLAB のグラフィックス オブジェクトには、「クリック」や「キー入力」などのユーザー アクションが行われたとき、
それに反応して実行される関数を設定することができます。MATLAB ではこれを「コールバック」と呼びます。

コールバック － ユーザー アクションへのプログラムされた応答
https://jp.mathworks.com/help/matlab/creating_plots/callbacks-programmed-response-to-user-action.html

※本来「コールバック」はもっと広い意味を持ちます。一般的には「イベントハンドラ」の方が意味が近いと思います。

「クリック」や「キー入力」に反応して関数が実行される、という挙動は、いかにもキャラを操作する処理に使えそうですよね（ちなみに私が MATLAB でゲームを作ろうと思っ~~てしまっ~~たのも、このコールバック機能を見つけたのがきっかけです）。

# コールバックを使ってみよう

　それでは**「十字キーの右が押されたら長方形を 1 だけ右に移動する」**という処理を例として、コールバックを用いて実装してみましょう。今回使用するのは Figure の `KeyPressFcn` というコールバック。このコールバックはキーが押されたときに実行されます。

　まず、コールバックを設定するための「ひな型」を用意しましょう。

```MATLAB:
function callback_test() % ただの m　スクリプトではなく、関数として作成していることに注意！！
fig = figure;
ax = axes;
axis equal;
axis([0 10 0 10]);
rec = rectangle('Position',[0 0 1 1]);

fig.KeyPressFcn = @my_callback; % コールバックとして my_callback 関数を設定

  function my_callback(src,data) % src:親のグラフィックスオブジェクト, data:コールバック用のデータ
  
  end
end
```
　
コールバックを設定するときは、対応するプロパティに「[関数ハンドル](https://jp.mathworks.com/help/matlab/matlab_prog/creating-a-function-handle.html)」を代入します。関数ハンドルとは関数のポインタのようなもので、「@関数名」と表記します。ここでは Figure の KeyPressFcn に `@my_callback` を代入していますので、
「何かキーが押されたら my_callback を実行してね！」
と Figure に設定したことになります。

　次に設定したコールバックである my_callback の形を見てみましょう。KeyPressFcn などに設定したコールバックは、通常呼び出されるときに 2 つの引数が渡されます。

　1 つ目はコールバックの親のハンドルです。今回はコールバックを Figure に設定しているので、`src` には Figure のハンドルが格納されています（つまり `src` と `fig` は同じハンドルです）。

```MATLAB:
src = 

  Figure (1) のプロパティ:

      Number: 1
        Name: ''
       Color: [0.9400 0.9400 0.9400]
    Position: [2868 192 560 420]
       Units: 'pixels'

  すべてのプロパティ を表示
```

　2 つ目はコールバックに関するデータです。この引数はコールバック毎に中身が異なっているため、使用するコールバック毎に対応するドキュメントを見る必要があります。今回使っている [KeyPressFcn のドキュメント](https://jp.mathworks.com/help/matlab/ref/matlab.ui.figure-properties.html#buiwuyk-1-WindowKeyPressFcn) を確認すると、`data` には押されたキーの名前などが記録されたオブジェクトが格納されることがわかります。

```MATLAB:
% 十字キーの右を押したときの data の中身
data = 

  KeyData のプロパティ:

    Character: ''
     Modifier: {1×0 cell}
          Key: 'rightarrow'
       Source: [1×1 Figure]
    EventName: 'KeyPress'
```
　
　これら 2 つの引数は必ずコールバックを呼び出す際に渡されるので、引数が 2 個以外の関数をコールバックに設定してしまうと引数の不一致でエラーとなってしまうので注意しましょう。　

　それでは以上の情報をもとに、「十字キーの右が押されたら長方形を 1 だけ右に移動する」処理をコールバックの中に記述していきましょう。必要な処理は、「押されたキーが十字キーの右かどうかの判別」と「長方形を 1 だけ右に移動する」処理の 2 つです。

```MATLAB:
function callback_test()
fig = figure;
ax = axes;
axis equal;
axis([0 10 0 10]);
rec = rectangle('Position',[0 0 1 1]);

fig.KeyPressFcn = @my_callback; % コールバックとして my_callback 関数を設定

  function my_callback(src,data)
    if strcmp(data.Key,'rightarrow')
      pos = rec.Position; % rec は親関数の変数なので、子関数でも使える！
      pos(1) = pos(1) + 1;
      rec.Position = pos;
    end
  end
end
```

　まず、十字キーの右が押されたかどうかを判別するには、`strcmp` という文字列比較の関数を用います。この関数は 2 つの文字列が等しいときは true、それ以外は false を返すので、`data.Key` が `'rightarrow'` と等しいかチェックすることができます。
　次に長方形の移動ですが、これはこの記事の冒頭でやった処理をそのまま記述すれば大丈夫です。「変数 `rec` が `my_callback` 内で宣言されてない！」と思う人がいるかもしれませんが、MATLAB では関数（親）の中で定義されている関数（子）内では親の変数にアクセスすることができます（[入れ子関数](https://jp.mathworks.com/help/matlab/matlab_prog/nested-functions.html)参照）。

　こんなかんじで、コールバックをうまく使うことで、オブジェクトの操作処理を簡単に実装することができます。このコードでは右移動だけですが、elseif をつなげていけば上下左右の移動への拡張もすぐにできるかと思います。興味があれば rectangle 以外のオブジェクトも色々動かしてみてください。

　ちなみに KeyPressFcn に設定したコールバックはキーが押された瞬間にしか実行されないので、「キーを押している間だけ移動」みたいな処理をするには結構工夫が必要になります。詳細は次回の記事で説明します（おそらく）。

#コールバックでよく使うテクニック
コールバックを使う上で、たまに役立つテクニックを 2 つほど紹介します。

###コールバックへの追加引数
　通常コールバックの引数は親のオブジェクトハンドルとコールバックデータの 2 つのみとなっていますが、実装したい処理によっては追加の引数を渡たす必要がある場合があります。例えば、上述した「十字キーの右が押されたら長方形を 1 だけ右に移動する」処理を m スクリプトで書こうとすると、長方形のハンドル `rec` は `my_callback` 関数内からアクセスできないので、引数として渡してあげる必要があります。

　このような場合、MATLAB ではコールバック設定を行う際にセル配列を用いることで、追加の引数を設定することができます。実際に `my_callback` 関数に `rec` を渡してみましょう。

```MATLAB:
fig = figure;
ax = axes;
axis equal;
axis([0 10 0 10]);
rec = rectangle('Position',[0 0 1 1]);

fig.KeyPressFcn = {@my_callback,rec}; % 引数に rec を追加

  function my_callback(src,data,rec_add) % rec_add には rec が渡される
    if strcmp(data.Key,'rightarrow')
      pos = rec_add.Position; % 追加された rec_add を用いることができる
      pos(1) = pos(1) + 1;
      rec_add.Position = pos;
    end
  end
end
```

先ほどは `@my_callback` とだけ設定していたコールバックが `{@my_callback,rec}` に変更されています。このようなセル配列を設定すると、第 1 要素のコールバック関数が呼び出される際に、第 2 要素以降の値が追加の引数としてコールバック関数に渡されます。今回は長方形のハンドル `rec` が追加引数となって `my_callback` 関数の第 3 引数である `rec_add`  に格納されるため、それを用いて移動を行うことができます。

　ただし、注意点として、追加引数に渡される値はコールバックを設定した時点での値で固定されます。例えば以下のコードを見て下さい。

```MATLAB:
fig = figure;
ax = axes;
axis equal;
axis([0 10 0 10]);
rec = 1; % まず rec に 1 が代入される

fig.KeyPressFcn = {@my_callback,rec}; % コールバック設定

rec = rectangle('Position',[0 0 1 1]); % rec に Rectangle のハンドルが代入される

  function my_callback(src,data,rec_add)
    if strcmp(data.Key,'rightarrow')
      pos = rec_add.Position;
      pos(1) = pos(1) + 1;
      rec_add.Position = pos;
    end
  end
end
```

このコードを実行すると、右キーを押したときにエラーになります。なぜなら、コールバックを設定した時点では `rec` に 1 が格納されているので、`my_callback` の `rec_add` には 1 が渡されてしまうからです。上の例はほぼありえないようなコードですが、コールバック関数に値を渡したいときに意外とやってしまいがちなミスなので、注意しましょう。

###無名関数を用いたコールバック
　簡単なコールバック処理を実装するとき、わざわざ関数として宣言するとコードの量が増え、見づらくなってしまいます。こういう場合は MATLAB の「[無名関数](https://jp.mathworks.com/help/matlab/matlab_prog/anonymous-functions.html
)」という機能を用いることで、関数を宣言することなくコールバック処理を実装することができます。

　使い方は実際に使っているコードを見た方が早いかと思いますので、「押されたキーを表示する」というコールバック処理を例としてコードを見ていきましょう。

まず、無名関数を使わない場合はこちら。

```MATLAB:
fig = figure;
fig.KeyPressFcn = @my_callback

function my_callback(src,data)
  disp(data.Dey)
end
```
つぎに、無名関数を使った場合はこちら。

```MATLAB:
fig = figure;
fig.KeyPressFcn = @(src,data) disp(data.Key)
```

関数の宣言がなくなり、コールバック設定の「@関数名」が「@(引数 1, 引数 2) コールバック処理」に変わっており、コード全体として `my_callback` という関数名に関する部分だけがきれいにカットされています。これが"無名"関数たる所以で、関数として宣言せずとも引数および関数内の処理を実装できます。

　ちなみに、この無名関数を用いることでもコールバック関数に追加の引数を渡すことができます。

```MATLAB:
fig = figure;
ax = axes;
axis equal;
axis([0 10 0 10]);
rec = rectangle('Position',[0 0 1 1]);

fig.KeyPressFcn = @(src,data) my_callback(src,data,rec); % 無名関数内で my_callback を呼び出す


  function my_callback(src,data,rec_add)
    if strcmp(data.Key,'rightarrow')
      pos = rec_add.Position;
      pos(1) = pos(1) + 1;
      rec_add.Position = pos;
    end
  end
end
```

このコードでは、キーが押されるとまず無名関数が 2 つの引数で呼び出されます。しかし、無名関数の処理で `my_callback` を `rec` を追加した 3 つの引数で呼び出していますので、結果的にはコールバック処理として 3 つの引数が渡された `my_callback` が実行されます。無名関数が間に立つことで、引数の数の不整合を緩衝できるというわけです。

コールバックなど決められた形の関数のハンドルを設定しなくてはならない場合に無名関数は非常に便利なので、ぜひ覚えておきましょう。~~あとコード内で無名関数を使ってると上級者っぽく見えます。~~

　
　完全に余談ですが、無名関数を使うとクリックしたところに文字を入力できる Figure が 1 行で作れます（Esc キーで文字を確定できます）。

```MATLAB:
>> set(gca,'ButtonDownFcn',@(ax,data) text(ax.CurrentPoint(1,1),ax.CurrentPoint(1,2),'','Editing','on'))
```

MATLAB 芸としてそこそこ受けるかも？


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

- キー入力やクリックなどユーザーアクションに反応して実行される関数を（MATLAB では）コールバックと呼ぶ
- コールバックは各グラフィックス オブジェクトのプロパティとして設定する
- コールバックには「親オブジェクトのハンドル」と「コールバックデータ」の 2 つの引数が渡される
 - KeyPressFcn ではコールバックデータとして押されたキーの情報が格納される
- コールバックに引数を追加で渡したいときは、セル配列または無名関数を用いる
- 簡単なコールバック処理であれば、関数を宣言しなくても無名関数で簡単に実装できる

少しずつですが、ゲーム作りっぽい内容に近づいてきました。次回はループ処理について解説していく予定です。

　
　
　「いいね」が増えると次回の記事が豪華になるかも。