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

ボールやバーを動かす

ブロック崩しゲームでは, ブロックを崩すボールやボールを打ち返すバーを動かす必要があります. 何か(オブジェクト)を動かすには, そのオブジェクトを表示して, 消して, 動かしたい方向に少しずれだ場所にまた表示する, というのを繰り返すことで動いているように見せることができます.

四角を表示する

まずオブジェクトとして, ボールやバーとなる四角を表示する関数を作ってみましょう. 四角は, 下図のように, 基準となる点 (x, y) とそこからの幅 (width) と高さ (height) で表すことができます.

これをそのまま構造体にすると次のようになります.

1  struct box {
2          int     x, y;
3          int     width, height;
4  };

この四角を (x, y) に書く関数 draw_box は次のようになります.

 1  void
 2  draw_box(struct box *b, int x, int y, hword color)
 3  {
 4          hword   *base, *d;
 5          int     w, h;
 6
 7          /* Base point is at (x, y). */
 8          base = (hword*)VRAM + LCD_WIDTH * y + x;
 9
10          /* Draw box from (x, y). */
11          for (h = b->height; h > 0; h--) {
12                  d = base;
13                  for (w = b->width; w > 0; w--)
14                          *(d++) = color;
15                  base += LCD_WIDTH;
16          }
17
18          /* Set the current position. */
19          b->x = x;
20          b->y = y;
21  }

描画に関しては, x, y で与えられたドットから, 横に width 分, 縦に height 分だけドットを打っていくだけです. 19,20行目で, 最後に表示した座標 (x, y) を構造体のメンバ x, y で憶えておくようにしています. このようにすることで, 後でこの四角を消す時には, 構造体に憶えさせた座標を使うことができます.

動かす

動かすには,

  1. 前に書いた四角を黒で塗りつぶして消す
  2. 新しい場所に四角を書く
を十分に速く繰り返すと,人の目には動いているように見えます.

1  void
2  move_box(struct box *b, int x, int y, hword color)
3  {
4          draw_box(b, b->x, b->y, COLOR_BLACK);
5          draw_box(b, x, y, color);
6  }

move_box を単純に繰り返し呼び出すと, 1ドットずつ動かしていっても, 四角はものすごく(動くというよりば飛ぶように)速く動いてしまいます. ゆっくり動かすには時間調整が必要なのですが,それが次の話題です.

練習

速度の調整

move_box でボールは動かせるようになりましたが, 単純に繰り返し呼び出すと動きが速すぎてゲームになりません. 速度の調整をするには, move_box を呼び出す時間の間隔を調整する必要があります.

同じだけずらして表示するというのを繰り返す場合, 下図の上の場合と下の場合では, 上の場合のように半分の間隔で呼び出せば, 半分の速度で動くということになります. つまり,呼び出す間隔を調整することで,動く速度を調整できるわけです.

間隔を空ける最も簡単な方法は, その時間を無駄に消費することです. ただ無駄に消費するのではなく, 空けたい間隔を調整できるように引数を与えることができないと困ります. 下の wait() 関数は,52ページリスト22の82〜87行目で使われているものです.

void
wait(int val) {
        int     i, j;

        for (i = 0; i < val; i++)
                for (j = 0; j < val; j++)
                        ;
}

for 文を2回重ねて,ただループをまわしています. 引数の val が大きければ,それだけの多くの回数ループがまわりますので, 長い時間が消費されることになります.

練習

速度の調整(ちょっと上級編)

上の wait 関数で,時間調整をするという目的は達成できます. しかし,引数として渡す値と, wait 関数が消費してくれる時間の関係がわかりにくいという欠点があります. そこで,ハードウェアとして用意されているタイマを使用する方法があります. タイマの仕様は60〜62ページに書いてありますが, 簡単には一定時間毎にカウントアップするタイマレジスタが4つ用意されています. ここでは,その1つタイマ0を使って時間調整をしてみます.

タイマを使うには, そのためのマクロが定義されている ~kumikomios/CDROM/chapter-3/gba.h を使用しますので, cp してきてください.

60ページの図1を見てください. タイマ制御レジスタの下位2ビットを 11 にセットすると, タイマ間隔は約61マイクロ秒になります. ということは 61 × 16 = 976 ですので, 16カウントアップするのを待てば約1ミリ秒待つことができるということになります.

1          /* Initialize Timer 0. */
2          gba_register(TMR_COUNT0) = 0;
3          gba_register(TMR_CTRL0) = TMR_ENABLE + TMR_1024CLOCK;

意識して気をつけなければいけないのはタイマレジスタのオーバフローです. タイマレジスタは16ビット幅ですので, 最大値は 0xffff (65535) となります. その次にカウントアップするとオーバフローして, 初期値(上の初期化方法だと 0)に戻ってしまいます. 0 からカウントアップしてオーバフローするまでの時間は,たった

 61マイクロ秒 × 216 = 3997696マイクロ秒 ≒ 4秒

です. オーバフローしないという前提ではプログラミングできません.

もう1つ関連して気をつけないといけないのは変数のデータ型です. Cのプログラムでは int は 32 ビットですので, 0xffff (65535) に 1 を足しても 0 には戻らずに 0x10000 (65536) になってしまいます. 例えば 0xffff (65535) から1ミリ秒待つにはタイマが 16 カウントアップするのを待てば良いわけですが, タイマの値は 0xffff の次は 0 ですので, 16 カウントアップした後の値は 0xf (15) ということになります. しかし,int では 0xffff (65535) に 16 を足すと 0x1000f (65551) になってしまいます.

そこで, タイマレジスタ同じ 16 ビットの 0 〜 0xffff の値をとるデータ型を使うことで, 問題を簡単にすることができます. そうすれば,0xffff (65535) に 16 を足すとは 0xf (15) になってくれます. そのようなデータ型は unsigned short で, gba.h で hword と typedef され, gba_register の値の型として使われています. これを使うことで,(一応は)オーバフローを処理に含めなくて良くなります.

以上のように考え作成した delay 関数は,次のようになります.

1  void
2  delay(hword val)
3  {
4          val += gba_register(TMR_COUNT0);
5          while (val != gba_register(TMR_COUNT0))
6                  ;
7  }

練習