【MATLAB】Stateflow テトリスのモデルを頑張って解読する

# この記事は？
一部の界隈では有名な Stateflow の例題モデル、[sf_tetris2](https://jp.mathworks.com/help/stateflow/examples/tetris.html) の解説を行う記事です。

# sf_tetris2 とは？
![tetris.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/49665e9f-e5af-9325-1dd4-77fa8bcfe343.gif)

Simulink の Stateflow と MATLAB のグラフィックス機能を組み合わせることで、**テトリスを再現した~~頭のおかしい~~モデル**です。

MATLAB, Simulink でゲーム制作、といえば必ずこの例題モデルが話題にあがるのですが、結構モデルの構造が複雑なため、MATLAB, Simulink, Stateflow すべてにある程度精通している人じゃないとなかなか中身を紐解けないようなモデルになっています。

そこで、この記事ではこの例題モデルが一体どういう原理で動いているかを簡単に解説したいと思います。

# 全体の構造

sf_tetris2.slx モデルは、それ単体で動いているわけではなく、sf_tetris2_gui.m というスクリプトと連携して動いています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/bb29214d-ac05-770f-be4e-8685a5e7f821.png)

モデルの方でゲームの動きをシミュレーションし、スクリプトでゲーム画面を描画する、という構造になっています。

# sf_tetris2.slx モデルの基本構造
sf_tetris2 の Simulink モデルの構造は非常に単純です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/67afd0b9-2929-ac2b-8b2f-553829f9dfb6.png)

モデルのメインは Stateflow の Chart ブロックです。この中身については後で詳しく解説します。

Chart ブロックの出力は Stop ブロックに繋がっています。テトリスがゲームオーバーになったときは、この出力を 1 にすることでシミュレーションを停止します。

入力は 7 個あり、一つ目は [Uniform Random Number](https://jp.mathworks.com/help/simulink/slref/uniformrandomnumber.html) ブロックが繋がっています。これは次にくるテトリミノを計算するための乱数信号です。
それ以外の入力端子には Left, Right といった Constant ブロックが繋がっていますが、これらはキー入力を検知するための信号で、キー入力が行われるたびに値が変化します。キー入力が行われるたびに値を変化させる方法については後程説明します。

# Stateflow の中身

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/a3b54c3f-5ca4-59ad-6042-17576461a049.png)


第一階層はパラレル構造になっており、WaitingArea -> MainArea -> Draw という実行順序になっています。
一つずつ順に見ていきましょう。

## WaitingArea ステート
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/19528146-8b66-93a6-fc7b-44f9e1ed2f6b.png)

**WainingArea** ステートは、次にくるテトロミノを準備する処理が書いてあります。

まずはシミレーション開始時に entry アクションで`setShapes`関数を実行します。`setShape`関数は初期化関数の一つで、7　つのテトロミノに対応する 16 進数の数値を`shapes`変数に保存します。ちなみにこの 16 進数の数値は、4 x 4 のマス目をのどこにブロックがあるかを表しています。

`setShapes`関数の処理が終わると、サブステート **Idle** がアクティブになり、entry アクションで`createShapeFrom`関数が実行されます。 `createShapeFrom`関数は、Simulink モデルの Uniform Random Number ブロックが生成した乱数を参照し、4 x 4 の 0 or 1 の行列でテトロミノを作成し、shape 変数に格納します。

以降は、Chart 内で`NEW`イベントが発生した時のみ`createShapeFrom`関数が実行されます。この Chart 内では、後述する **MainArea** ステート内の **NewShape** ステートからイベントが発生します。


## MainArea ステート
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/1f600703-14a3-d757-9978-fc52a88cd734.png)

複雑そうに見えますが、実は下のような処理を順に行っているだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482141/abf43d29-f56d-ab17-4b61-fd615b94c8c9.png)

### initArena 関数

**MainArea** ステートのデフォルト遷移で実行され、テトロミノが動くアリーナの初期化を行います。ARENA_WIDTH x ARENA_HEIGHT のマス目を`arena`変数として用意し、テトロミノが動けない場所（壁と床）に 1 を代入しています。後々テトロミノが接地すると、その部分にも 1 が代入されていきます。

### NewShape ステート

**WaitingArea** ステートの **Idle** サブステートに`NEW`イベントを発生させます。その結果、前述した`createShapeFrom`関数が実行され、新しいテトロミノが作成されます。

### Moving ステート

during アクションでシミュレーションの 1 ステップごとにテトロミノを動かします。**DOWN** ステートには一定時間ごとに通常落下する処理が書かれています。それ以外のステートでは`hasChange`関数が使われており、right や left などの Constant ブロックの値が変わった場合のみテトロミノの移動が行われ、`px`,`py`の値が変化します。

### Stopped ステート

**Moving** ステートで 1 ステップごとにテトロミノの位置を計算し、接地したらこのステートに遷移します。テトロミノが接地したときに 1 ステップ分のウェイトをかけるためのステートなので、特に何もアクションが定義されていません。

### GameOver ステート

ブロックが天井に到達していたらこちらのステートに遷移します。Chart ブロックから 1 が出力されるので、Stop ブロックによってシミュレーションが停止します。

### FreezeShape ステート

天井に到達していない場合はこちらのステートに遷移します。`freez`関数によって`arena`変数のテトロミノが置かれた位置に 1 を代入します。

### Score ステート

`isRowMade`関数ですべて埋まった行を判定し、サブステート **ClearCompletedRows** でその行を消します。その後、サブステート **Done** で`calcScore`関数を実行し、消えた行の数に応じてスコアを更新します。

ちなみに、4 行消えたときは特別にサブステート **Tetris** に移動するようになっていますが、"can do some animation here" というコメントしか書いてないので、単に少し多くディレイがかかるだけになっています。

### NextLevel ステート

消えた行数が一定以上になるとレベルが上がります。ちなみに、レベルは **Moving** ステートの通常落下の速度に影響します。

## Draw ステート

実際に figure 上に描画するためのステートです。entry アクションで`sf_tetris2_gui`関数の初期化処理、during アクションで draw 処理を呼び出しています。

ちなみに、この Stateflow は C アクション言語に設定されているため、sf_tetris2_gui.m を呼び出すために [ml 名前空間演算子](https://jp.mathworks.com/help/stateflow/ug/calling-built-in-matlab-functions-and-accessing-workspace-data.html)が使われています。

　　
　　

以上が Stateflow のざっくりとした解説です。 1 つずつ見ていくと、テトリスの処理として当たり前のことをやっているだけだということがわかるかと思います。

次は **Draw** ステートで呼び出されている sf_tetris2_gui の解説に移ります。

# sf_tetris2_gui.m

sf_tetris2_gui はモデルを起動しただけでは開きませんので、

```MATLAB:
>> edit sf_tetris2_gui 
```

によって MATLAB 側で開きます。基本的には、figure の初期化、画面の更新、キー入力検知を行っています。
第一引数の文字列でどの処理を行うかを指定しているので、それぞれの処理内容を簡単に見ていきましょう。

## case 'init'

テトリス用の figure ウィンドウを作成しています。

```MATLAB:sf_tetris2_gui.m_line20-23
tetrisFig = figure('Name', 'Tetris', ...
    'DoubleBuffer', 'on', ...
    'KeyPressFcn', [mfilename, ' keypress'], ...
    'Tag', 'StateflowTetrisFigure');
```
上記のように KeyPressFcn を設定することで、キー入力が行われたときに sf_tetris2_gui('keypress') が実行されるようになっています。

figure 作成の後は、image 関数でアリーナを描画しています。最初はブロックがないので、イメージデータはゼロ行列です。

```MATLAB:sf_tetris2_gui.m-line26
arenaImage = image(zeros(ARENA_HEIGHT+1,ARENA_WIDTH+2));
```

## case 'draw'

Stateflow からアリーナの状況やテトロミノの位置・形状、スコアやレベルのデータを受け取り、figure に反映します。


## case 'keypress'

押されたキーに対応した Constant ブロックの名前を用意し、`set_param`関数によってその Constant ブロックの値を変更します。

```MATLAB:sf_tetris2_gui.m_line118
set_param(nm, 'value', num2str(prevVal+1));
```

値が変更することで、上述した **Moving** ステートの`hasChange`関数が True となり、移動や回転の処理が行われます。



## pauseOrContinue 関数

`set_param`関数で`SimulationCommand`パラメータを制御することで、シミュレーションの一時停止、再開、開始を行います。

```MATLAB:sf_tetris2_gui.m_line125-132
switch status
        case 'running'
            set_param('sf_tetris2', 'SimulationCommand', 'pause');
        case 'paused'
            set_param('sf_tetris2', 'SimulationCommand', 'continue');
        case 'stopped'
            set_param('sf_tetris2', 'SimulationCommand', 'start');
    end
```


# まとめ

sf_tetris21.slx モデルの動作原理をざっくりと解説してみました。

Simulink でのゲーム制作にチャレンジしたい！という方は、このモデルを自分なりにアレンジするところから始めるのもありかもしれませんね。
（ちなみに私の [Simulink ポケモン](https://youtu.be/dz76JAAO0mg)もこのモデルを参考にしてます）

　
※よければ MATLAB でゲーム制作シリーズも読んでみてください
- [0 章：グラフィックスオブジェクト](https://qiita.com/macht/items/05e848f2fa9fab0a538e)
- [1 章：コールバック](https://qiita.com/macht/items/5853875dce0381e33c3a)
- [2 章：メインループ](https://qiita.com/macht/items/de86b46a22f4109df0c1)
- [3 章：アクションゲームでジャンプ](https://qiita.com/macht/items/357ad3201deb1683f010)



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