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

ボールとラケットとの2つを動かす

ボールが壁に反射し続けるアニメーションに加えて, 「キーで左右に動かせるラケットに当たっても,ボールが反射する」 という動きを今回の目標としましょう.

プログラムの設計を考える

ボールのアニメーションをするプログラムは, おおよそ次のように書けました.

while (1) {
  ボールを1ステップ動かす
  delay()で待つ
}

ボールとラケットの2つのものが動作するプログラムも同様に,

while (1) {
  ボールを1ステップ動かす
  ラケットを1ステップ動かす
  delay()で待つ
}
のように書けますが, このループの中でやることは今後ますます増えていきますから, 手に負えなくならないようにプログラムの構造をこのあたりで 考えておきましょう.たとえば以下のようにします. ball.c はballに関する処理だけを記述するモジュール(処理単位) として他となるべく独立させ,他に公開するインターフェースだけを ball.hに書いて,その他は隠すようにします. racket.c と racket.hも同様です.

main関数は,ball.h で公開される「ボールを1ステップ動かす」関数, racket.h で公開された「ラケットを1ステップ動かす」関数を利用して, たとえば以下のように書くことにします.

#include "gba.h"
#include "ball.h"
#include "racket.h"

// delay()関数などを定義...

int main(void)
{
  // 画面を初期化
  // タイマーを初期化
  while (1) {
    ball_step();
    racket_step();
    delay(INTERVAL);
  }
}
racket.h には racket_step() 関数の宣言を
extern void racket_step(void);
のように書いておきます.また,ball_step()も同様にball.hの中に書いておきます. 箱を描くのに必要な struct box の定義や,draw_box(), move_box()などの関 数も,box.c というモジュールに分け,box.hにインターフェースを書いてお きます.

他のモジュールのデータの読み書き

モジュールがそれぞれ完全に独立していれば, 他のモジュールでやっていることを気にする必要がないのですが, 全体で一つのプログラムを動かしているわけですから, モジュール間でのデータのやりとりがどうしても必要になります.

今回の課題では,ballモジュールとracketモジュールの間で, ボールがラケットに当たったかどうかを判定する必要があります.

この判定は,どちらのモジュールが行なっても基本的にかまわないのですが, ここではracketモジュールが判定するとして説明します. racketモジュールのプログラム racket_step() の中からボールの位置を 問い合わせたり,ボールの速度(dy)を読み書きする必要があるわけですが, 直接 ball モジュールの変数を読み書きするのではなく,ballモジュールが 用意したインターフェース関数を利用して読み書きするようにします. 具体的には,ball.h に以下のようなインターフェースを記述します.

extern int ball_get_dy(void);             // ボールのy方向の速度を返す.
extern void ball_set_dy(int new_dy);      // ボールのy方向の速度をセットする.
extern struct box *ball_get_box(void);    // ボールの箱の位置を返す.
extern void ball_step(void);              // アニメーションの1ステップを行なう.
そして,ball.cの内容は以下のようになります.
#include "gba.h"
#include "box.h"
#include "ball.h"

static int dx, dy;            /* ボールの現在の速度 */
static struct box b;          /* ボールの箱の現在の位置 */

int ball_get_dy(void) { return dy; }
void ball_set_dy(int new_dy) { dy = new_dy; }
struct box *ball_get_box(void) { return &b; }

void ball_step(void)
{
  ...
}
static と宣言しておくと,このファイルの外からは参照できない 変数となります. 他のファイルと名前がぶつかる心配もいらなくなります.

このように,インターフェース関数だけでモジュールのデータを読み書き するように作ると,以下のような利点があります.

また,今回のプログラムではball_step()やracket_step()の実行中に, 他のモジュールの実行に切り替わることがないのでよいのですが, 割り込みを使ってプログラムの実行を強制的に切り替える処理 (プリエンプティブな並行処理)を行なうような場合は, 変数の読み書きに排他制御が必要となります. インターフェース関数経由で読み書きするようにしておいた方が, あとで排他制御を付け加えるのも簡単です.

練習

ラケットとボールの衝突判定

ラケットとボールが衝突したかどうかを判定するには, 2つの箱が画面上で重なっているかどうかを判定する関数を作っておくと便利です.

int cross(struct *b1, struct *b2)
{
  // b1とb2の領域が重なっていれば1,重なっていなければ0を返す.
}
重なっているかどうかを判定する条件は, まず x座標については となります.y座標についても同様に上端と下端で判定します.

overlap

箱に関するモジュールのインターフェース box.h は次のようになります.

struct box { ... };
extern void draw_box(struct box *b, int x, int y, hword color);
extern void move_box(struct box *b, int x, int y, hword color);
extern int cross(struct box *b1, struct box *b2);

全体をまとめるMakefileを以下のように作っておくと良いでしょう.

AS     = as-arm
CC     = gcc-arm
LIBGCC = `gcc-arm -print-libgcc-file-name`
CFLAGS = -Wall -O -fno-builtin -fomit-frame-pointer -finhibit-size-directive \
	-fno-ident

all: racket.bin

racket.bin: ball.o box.o crt.o main.o racket.o
	ld-arm -o racket.out -T gcc.ls \
	  crt.o ball.o box.o main.o racket.o ${LIBGCC}
	objcopy-arm -O binary racket.out racket.bin

clean:
	rm -f *.o *.s *.out *.bin

ball.o: gba.h box.h ball.h
box.o: gba.h box.h
main.o: gba.h ball.h box.h racket.h
racket.o: gba.h box.h ball.h racket.h