情報システム実験 K-13 組み込みオペレーティングシステム
No.6

ゲームの状態の管理

タスク

前回は,プログラムをいくつかの独立したモジュールに分ける方法を 説明しました.これらのモジュールbox, ball, racketのうち,main関数から定期的に 呼び出されて,処理を少しずつ進行させるもの(ballとracket)を,今後はタスク と呼ぶことにします.また,タスクが呼び出される時間間隔をティックと 呼ぶことにします.

ゲームの進行役タスク

今までに作った2つのタスク(ball, racket)は, 毎回おなじ処理をするだけでした. しかし,実際のゲームでは「ボールを落した」「ゲームオーバー」「一面クリアした」 などによって動きが変化します.

崩すブロックはまだ登場していませんが,まずは「ボールを落したら停止して, STARTボタンを押すと初期画面からやりなおし」という状態変化を付け加えてみましょう.

状態遷移

ゲームの状態は,以下の4つのうちのどれかであるとします.

START
初期画面を表す状態です.STARTキーが押されると RUNNING状態に移ります.
RUNNING
ボールやラケットが動いている状態です. ボールが落ちるとDEAD状態に移ります.
DEAD
ボールが落ちてしまった状態です. アニメーションは停止しています.STARTキーが押されると RESTART状態に移ります.
RESTART
再び初期画面に戻った状態です.次のティックにはRUNNING状態に移ります.

ゲームの状態が今どうなっているのかを管理するgameタスクを新たに作りましょう. このタスクも独立したモジュールとして作り,そのインターフェース(game.h)は 以下のようにします.

1  enum state {START, RUNNING, DEAD, RESTART};
2  
3  extern void game_step(void);             // 1ティックの動作を行なう.
4  extern enum state game_get_state(void);  // 今の状態を問い合わせる.
5  extern void game_set_state(enum state);  // 状態を変更する.

1行目の宣言は, 新たな型である「enum state型」を宣言しています.この型の変数は 値としてSTART, RUNNING, DEAD, RESTARTのいずれかを取ります. (実際のプログラム実行中は整数の値で表され,STARTが0,RUNNINGが1, DEAD が2, ...となります.)

それぞれのタスクを呼び出すmain関数のループはgame_step()を加えて 以下のようになります.

  while (1) {
    ball_step();
    racket_step();
    game_step();
    delay(INTERVAL);
  }

ボールを扱うタスクball_step()の動作は,今の状態を意識して次のように 書きます.

...
#include "game.h"
...
void ball_step(void)
{
  switch (game_get_state()) {
  case START:
    ボールの位置,速度を初期状態にし,ボールを表示する.
    break;
  case RUNNING:
    ボールのアニメーションを1ステップ行なう.
    if (ボールが落ちた)
      game_set_state(DEAD);
    break;
  case DEAD:
    何もしない.
    break;
  case RESTART:
    現在のボールを画面から消し,
    ボールの位置,速度を初期状態にし,ボールを表示する.
    break;
  }
}

racketモジュールも同じようにゲームの状態で場合分けして処理するように書き換えます. また,gameモジュールは以下のように書けます.

#include "game.h"

static enum state current_state;  // 現在の状態

enum state game_get_state(void) { return current_state; }
void game_set_state(enum state new_state) {
  current_state = new_state;
}

void game_step(void)
{
  switch (game_get_state()) {
  case START:
    if (STARTキーが押された)
      game_set_state(RUNNING);
    break;
  ...
  case RESTART:
    /* 次のティックはRUNNING状態にする.*/
    game_set_state(RUNNING);
    break;
  }
}

練習

ブロックの表示とボールとの衝突判定

いよいよ残った部分,ブロックを扱うblockタスクを作っていきましょう. 例によって,block.h と block.c を作ります.block.h のインターフェースは 以下のようになります.

  extern void block_step(void);

最初は,簡単のためにブロックを一つだけ表示し, そのブロックにボールが当たったらクリア(ゲーム終了) というバージョンを作ってみましょう.

ゲームの状態は1個増えて下図のようになります. 新たな状態であるCLEAR状態では,DEAD状態と同様に アニメーションを停止してSTARTキーが押されるのを待ちます. STARTキーが押されると,START状態に移るようにします.

state2

game.h のenum stateの定義にCLEARを加え, また各々のタスクのswitch文でCLEAR状態を扱う選択肢を加えましょう. gameタスクでは,CLEAR状態でSTARTキーが押されるとSTART状態に移るようにします. その他のタスクでは,DEAD状態と同様に,単に何もしないようにすれば良いでしょう. main関数にも忘れずにblock_step()の呼び出しを加えます.

  while (1) {
    ball_step();
    racket_step();
    block_step();
    game_step();
    delay(INTERVAL);
  }

blockタスクの本体である,block.c の中の block_step() 関数を作りましょう. これも,他のタスクと同様に,今のゲーム状態によってswitch文で分岐するようにします.

#include "gba.h"
#include "ball.h"
#include "game.h"
#include "box.h"
#include "block.h"

static な変数の定義

void block_step(void)
{
  switch (game_get_state()) {
  case START:
    ブロックを表示する.
    break;
  case RUNNING:
    if (ボールがブロックとぶつかっている)
      game_set_state(CLEAR);
    break;
  ...
  }
}

ブロックはboxで表し,ボールとぶつかっているかどうかの判定は racketの時と同様にcross()を使うと簡単でしょう.

練習