シューティングゲームを作ろう!

注意点

テキストをちゃんと読んでから質問してください。
テキストをしっかり読まずに、図やテキストを流し読みをして分かった気になって、テキストを読めば分かることを質問してくる方が非常に多いです。

エディタ(導入必須)

繰り返しになりますが、xyzzy、Emacsなどのエディタの導入をお願いします。
特にWindows の方は xyzzy を必ず使って下さい。

エディタの導入方法はこちらです。

プログラミングが苦手で、上手くいかず質問してくる人ほど xyzzy を使っていないケースが多く、非常に困ります
自力でできず、TA・SAに質問しようという人は、必ず xyzzy を使うようにして下さい。

配列とfor文を利用して、簡単なシューティングゲームを作成する

まず、本日の講義資料をダウンロードしてください。
第7回資料(Windows用)
第7回資料(Mac用)

今回はまず最初に、プレイヤー(自機)がショット(弾)を発射して、ショットがぶつかったら敵が死ぬ、というシューティングゲームの基本的なサンプルプログラムを見ていきます。
Sample01フォルダ直下にある、Game.javaファイルを開いて下さい。

このプログラムでは、画面上部に敵(赤色の四角形)が表示され、左右に動いています。
画面下部にはプレイヤー(青色の四角形)がいて、左右キーで右左に移動することができます。
そしてZキーを押すとショットを出し、そのショットが当たると敵が消えます。


図:Sample01の実行画面。赤い四角が敵、青い四角がプレイヤー、真ん中の円い玉がショット。

プレイヤーと敵の動きについてはこれまでの回で学んだ知識で理解できると思いますので、ここではショットを撃ち出す方法について解説していきます。

まずはショット関連の変数定義を見て下さい。

/********* 変数定義はこちらに *********/
// 型 変数名; の順に書いて定義する

(中略)

// ショットのX座標
int[] shot_x = new int[10];
// ショットのY座標
int[] shot_y = new int[10];
// ショットが生きているか、死んでいるかのフラグ
boolean[] shot_alive_flag = new boolean[10];
// ショットのサイズ
int shot_size;
// ショットのY方向の速度
int shot_speed_y;

// 次に有効にする弾番号
int next_alive_shot_num;

ショット用として、大きさ10の配列を定義しています。
これまでの回で扱った配列と同じように、ショットが有効かどうかを表すフラグも定義されています。

定義した変数は、以下のように初期化します。

/********* 初期化の手順はこちらに *********/
public void initGame()
{
(中略)

// ショットの初期化処理
for(int i=0; i<10; i=i+1)
{
// ショットの座標を初期化する
shot_x[i] = 0;
shot_y[i] = 0;
// ショットのフラグを初期化する
shot_alive_flag[i] = false;
}

// ショットの速度を初期化する
shot_speed_y = -20;

// ショットのサイズを初期化する
shot_size = 24;

// 次に有効にするショット番号を初期化する
next_alive_shot_num = 0;
}

ショットの座標は、updateGame内でショットを発射するたびにプレイヤーの座標に合わせて初期化されるため、ここでは適当な値 (0, 0) で初期化しています。
最初は1つもショットが発射されていない状態なので、shot_alive_flag は全て false です。

次に有効にするショット番号を表す next_alive_shot_num は、配列の先頭である 0 で初期化しています。
この next_alive_shot_num が、配列を利用してショットを発射するために重要な変数となります。

 

それでは、更新処理を見ていきましょう。
まずは、updateGame 内でショットを発射している部分です。

// Zキーを押すと、ショットを発射する
if(gc.isKeyPushed(gc.KEY_Z))
{
// ショットを有効に
shot_alive_flag[next_alive_shot_num] = true;

// ショットの初期座標をプレイヤーの中央位置にセット
shot_x[next_alive_shot_num] = player_x + player_w/2 - shot_size/2;
shot_y[next_alive_shot_num] = player_y + player_h/2 - shot_size/2;

// 次のショットの番号へ。
next_alive_shot_num = next_alive_shot_num + 1;

// ショットは10個までなので、それを超えたら0に戻る(使い回す)
if(next_alive_shot_num >= 10)
{
// 次のショットの番号を先頭の 0 へ
next_alive_shot_num = 0;
}
}

ショットが発射されたら、ショットのフラグ shot_alive_flag を有効な状態へと書き換え、ショットの座標をプレイヤーの中央位置に合わせています。

その後が重要なポイントです。
ショットが発射されたら next_alive_shot_num を次の番号へと書き換えています。
こうすることで、最初にショットが発射されたら配列 0 番、次に発射されたら 1 番、というように、次々と新しいショットを発射できるのです。

配列サイズは10なので、次の番号へと書き換えた結果 next_alive_shot_num が10を超えたら、next_alive_shot_num を配列の先頭である 0 へと再び戻しています。
つまり、配列 9 番の次に発射されるショットは配列 0 番となります。
9番が発射される頃には、0 番のショットは画面外に出ているか消えているので、もう一度使いまわしても問題ない、というわけです。

続いて、ショットの更新処理を見ていきましょう。

// ショット更新用のfor文
for(int i=0; i<10; i=i+1)
{
// 有効(true)なショットのみ実行
if(shot_alive_flag[i])
{
// Y方向に shot_speed_y ずつ進める
shot_y[i] = shot_y[i] + shot_speed_y;

// ショットが画面の上を越えた場合
if(shot_y[i] < -shot_size)
{
// ショットのフラグを false に
shot_alive_flag[i] = false;
}

// もし敵がまだ生きていれば
if(enemy_alive_flag)
{
// 敵と、有効なショットの当たり判定を行う
if( gc.checkHitRect(enemy_x, enemy_y, enemy_w, enemy_h, shot_x[i], shot_y[i], shot_size, shot_size) )
{
// ぶつかった敵のフラグを false に
enemy_alive_flag = false;

// ショットのフラグも false に
shot_alive_flag[i] = false;
}
}
}
}

見て分かるとおり、これまでの配列を使った更新処理とあまり変わっていません。
フラグを調べて、有効なショットについてのみ、移動や当たり判定を行っています。
また、画面外に出たショットはこれ以上更新する必要がないので、フラグを無効な状態へと書き換えています。

 

ショットを発射する方法についての説明は以上で終わりです。
描画処理についてはこれまでの配列とほぼ同じ処理なので、ここで説明はしません。
これまでの授業をちゃんと理解していれば、プログラムの内容は理解できると思います。

カウンタ変数を利用して、一定時間ごとに敵を出す

続いて、時間が経つ毎に次々と敵を出現させる方法を見ていきましょう。
敵をたくさん出せるようになれば、かなり本格的なシューティングゲームに近づけることができます。

それでは、Sample02フォルダ直下にあるGame.javaを開いて下さい。

まずは、変数定義で変更された点について見ていきます

/********* 変数定義はこちらに *********/
// 型 変数名; の順に書いて定義する

(中略)
// 敵のX座標
int[] enemy_x = new int[10];
// 敵のY座標
int[] enemy_y = new int[10];
// 敵の幅
int enemy_w;
// 敵の高さ
int enemy_h;
// 敵の速度
int enemy_speed_y;
// 敵が生きているかどうかのフラグ
boolean[] enemy_alive_flag = new boolean[10];;

// 次に有効にする敵の番号
int next_alive_enemy_num;

// 時間を表す変数
int time;

プレイヤー関係の変数と、ショット関係の変数については何も変更されていません。
変更されたのは、敵関連の変数です。
敵をたくさん出すために、Sample01では単一の変数だった敵関連の変数が、Sample02では配列へと変更されています。

加えて、次に有効にする敵の番号を表す変数 next_alive_enemy_num が追加されました。
これは、さきほどショットを発射するために利用した next_alive_shot_num と同じような働きをする変数です。
一定時間ごとに敵を出す処理は、ショットと同じような仕組みの処理となります。

さらに、時間を表すカウンタ変数 time を用意しました。
この変数には、現在の経過時間がセットされます。

 

それでは、これらの変数の初期化について見ていきましょう。

    /********* 初期化の手順はこちらに *********/
public void initGame()
{
(中略)

// 敵の初期化処理
for(int i=0; i<10; i=i+1)
{
// 敵の座標を初期化する
enemy_x[i] = 0;
enemy_y[i] = 0;
// 敵が生きているかどうかのフラグを初期化
enemy_alive_flag[i] = false;
}

// 敵の幅・高さを初期化する
enemy_w = 100;
enemy_h = 20;
// 敵の速度を初期化
enemy_speed_y = 5;

// 次に有効にする敵の番号を初期化
next_alive_enemy_num = 0;

// 時間を表す変数を初期化
time = 0;
}

プレイヤー関係の変数と、ショット関係の変数については何も変更されていないので、ここでも省略しています。

ショットと同様に、敵の座標は敵出現時に初期化されるので、ここでは適当な値(0, 0)をセットしています。
最初は1体も敵が出現していない状態なので、enemy_alive_flag は全て false です。
next_alive_enemy_num は、配列の先頭である 0 で初期化しています。
time は経過時間を表すカウンタ変数なので、当然 0 で初期化されます。

 

続いては更新処理です。
太字の箇所に注目して下さい。

/********* 物体の移動等の更新処理はこちらに *********/
public void updateGame()
{
// 左キーが押され続けている場合に真
if(gc.isKeyPress(gc.KEY_LEFT))
{
// プレイヤーを左に動かす
player_x = player_x - 5;
}
// 右キーが押され続けている場合に真
if(gc.isKeyPress(gc.KEY_RIGHT))
{
// プレイヤーを右に動かす
player_x = player_x + 5;
}
// 1秒(30フレーム)毎に敵を生成する if(time % 30 == 0) { // 敵を有効に enemy_alive_flag[next_alive_enemy_num] = true; // 敵の初期座標をランダムにセット enemy_x[next_alive_enemy_num] = gc.rand(0, 640-enemy_w); enemy_y[next_alive_enemy_num] = 0; // 次の敵の番号へ。 next_alive_enemy_num = next_alive_enemy_num + 1; // 敵は10個までなので、それを超えたら0に戻る(使い回す) if(next_alive_enemy_num >= 10) { // 次の敵の番号を先頭の 0 へ next_alive_enemy_num = 0; } } // 敵更新用のfor文 for(int i=0; i<10; i=i+1) { // 有効(true)な敵のみ実行 if(enemy_alive_flag[i]) { // Y方向に enemy_speed_y ずつ進める enemy_y[i] = enemy_y[i] + enemy_speed_y; // 敵が画面の下を越えた場合 if(enemy_y[i] > 480) { // 敵のフラグを false に enemy_alive_flag[i] = false; } } } // Zキーを押すと、ショットを発射する if(gc.isKeyPushed(gc.KEY_Z)) { // ショットを有効に shot_alive_flag[next_alive_shot_num] = true; // ショットの初期座標をプレイヤーの中央位置にセット shot_x[next_alive_shot_num] = player_x + player_w/2 - shot_size/2; shot_y[next_alive_shot_num] = player_y + player_h/2 - shot_size/2; // 次のショットの番号へ。 next_alive_shot_num = next_alive_shot_num + 1; // ショットは10個までなので、それを超えたら0に戻る(使い回す) if(next_alive_shot_num >= 10) { // 次のショットの番号を先頭の 0 へ next_alive_shot_num = 0; } } // ショット更新用のfor文 for(int i=0; i<10; i=i+1) { // 有効(true)なショットのみ実行 if(shot_alive_flag[i]) { // Y方向に shot_speed_y ずつ進める shot_y[i] = shot_y[i] + shot_speed_y; // ショットが画面の上を越えた場合 if(shot_y[i] < -shot_size) { // ショットのフラグを false に shot_alive_flag[i] = false; } // 全ての敵に対してショットとの当たり判定を行う for(int j=0; j<10; j=j+1) { // 敵のフラグがtrueなら if(enemy_alive_flag[j]) { // 敵と、有効なショットの当たり判定を行う if( gc.checkHitRect(enemy_x[j], enemy_y[j], enemy_w, enemy_h, shot_x[i], shot_y[i], shot_size, shot_size) ) { // ぶつかった敵のフラグを false に enemy_alive_flag[j] = false; // ショットのフラグも false に shot_alive_flag[i] = false; // ショットが消えたので、敵との当たり判定を行うfor文から抜ける break; } } } } } // 時間を加算する time = time + 1; }

まず最初の太字の、// 1秒(30フレーム)毎に敵を生成する とコメントが書いてある場所に注目してください。
経過時間を保持する time は、updateGame関数の最後で 1 ずつ加算されています。
この time に対して余りを求める % 記号を利用することで、30で割った余りが0の時、つまり30回( 1 秒)に1回だけこの if 文が true となります。

if 文の中身は、新しく敵を出現させる処理です。乱数を利用して、毎回バラバラな位置に敵を出現させています。
この敵出現処理は、新しくショットを発射する処理とほぼ同じ方法となっています。見比べながら理解してみて下さい。
すぐ下にある敵更新用のfor 文についても、これまで配列を利用した時と同じように、フラグが true な敵だけを更新しています。

続いて、ショット更新用のfor文の中にある太字の箇所を見てください。// 全ての敵に対してショットとの当たり判定を行う とコメントが書いてある場所です。
全ての敵と全てのショットとの当たり判定を行うため、for 文の中にもう一つ for 文を書いています。
外側の for 文で i を利用しているので、その内側にあるこの for 文では同じ変数名は使えません。
そのため、内側の for 文では j を利用しています。

このfor文がどのように動くかというと、 まず i=0; の時に、shot_alive_flag[i] が true かどうかを調べます。
true なら、
i=0, j=0
i=0, j=1
i=0, j=2
i=0, j=3
i=0, j=4
i=0, j=5
i=0, j=6
i=0, j=7
i=0, j=8
i=0, j=9
の順に実行され、j が10になったら内側のfor文を抜けて、i が加算されて 1 となります。
shot_alive_flag[i] が false なら if 文の中身は実行されないので、その後すぐ i が加算されて 1 となります。

それ以降も同様に、 i=1, j=0 i=1, j=1 (中略) i=1, j=9 といった感じで、i が 10 になって外側の for 文が終了するまで続けられます。

それでは、内側の for 文の中身に注目してください。
enemy_alive_flag[j] が true なら、ショットと敵の当たり判定を行っています。
つまり、フラグが true なショットと、フラグが true な敵の、全ての組み合わせをチェックしているのです。

当たり判定が true になった場合には、ショットと敵の両方のフラグを false にした後で、break 命令を呼び出しています。
break は、現在の for 文を強制的に終了させる命令です。
内側の for 文の中に書いてあるので、内側の for 文が強制的に終了され、次の処理として外側の for 文の i が加算されます。

break は、ショットが消えたのに、それ以降の敵と当たり判定が行われるのを防ぐために書かれています。
たとえば、i=0, j=2 のショットと敵がぶつかって、両方のフラグが false になったとします。
その際に、break文 で終了しないと、i=0 のショットは false なのに、j=3, j=4 ... と処理が実行されてしまって困るというわけです。

 

一定時間ごとに敵を出す方法についての説明は、以上で終わりです。
描画処理についてはこれまでの配列とほぼ同じ処理なので、ここで説明はしません。
これまでの授業をちゃんと理解していれば、プログラムの内容は理解できると思います。

データのセーブ・ロード

GameCanvas では、データをセーブデータに保存し、後で保存したデータを読み込むことができます。
セーブデータは、"savedata.dat" というファイル名で作成されます。

それでは、実際の使い方を見てみましょう。
Sample03 フォルダ直下にあるGame.javaを開いて下さい。

Sample03 は、テニスゲームのプログラムです。
ラケットで打ち返した回数をスコア(点数)として画面に表示しています。
また、今までゲームをプレイした中で一番良かった点数(スコア)も、ハイスコアとして合わせて画面に表示しています。
そして、ゲームを終了する際にハイスコア情報を保存し、ゲームを起動した時に保存したハイスコア情報を読み込んでいます。

まずは変数定義を見てください。
変数定義には、スコアとハイスコアを保持する変数がそれぞれ定義されています。

/********* 変数定義はこちらに *********/
// 型 変数名; の順に書いて定義する

(中略)

// 打ち返した回数(スコア)
int score;

// 今までで一番よかったスコア(ハイスコア)
int high_score;

続いては初期化処理です。
スコアを初期状態の 0 点で初期化しています。
ハイスコアはスコアとは異なり、 0 で初期化する代わりに、0 番にセーブしたデータを読み込んでいます。
セーブデータが無い場合には 0 が返されるので、セーブデータが無い場合には 0 で初期化され、セーブデータがある場合にはその値で初期化されることになります。

/********* 初期化の手順はこちらに *********/
public void initGame()
{
(中略)

// スコアを0点に初期化する
score = 0;

// ハイスコアを0番から読み込む
high_score = gc.load(0);
}

更新処理では、ラケットとボールが当る毎にスコアを加算しています。
そして、加算した後のスコアがハイスコアより大きいかを調べ、大きければハイスコアに現在のスコアをセットしています。
これにより、記録が更新される度にハイスコアが更新されるようになっています。
(下記では一部あえて誤った記述がなされている箇所がありますので、プログラムの意味を考えた上で修正して下さい。)

/********* 物体の移動等の更新処理はこちらに *********/
public void updateGame()
{
(中略)

// ボールと、ラケットが当たった場合に真
if(gc.checkHitRect(racket_x, racket_y, 100, 20, ball_x, ball_y, 24, 24))
{
// ボールのY方向の速度を反転させる
ball_speed_y = -ball_speed_y;

// スコアを加算
score = score + 1;

// ハイスコアより大きければ
if(high_score > score)
{
// ハイスコアを新記録で更新
high_score = score;
}
}
}

描画処理では、スコアとハイスコアを画面に表示しています。

/********* 画像の描画はこちらに *********/
public void drawGame()
{
(中略)

// 色を黒にセット
gc.setColor(0, 0, 0);
// スコアとハイスコアを描画
gc.drawString("Score:" + score, 0, 0);
gc.drawString("HighScore:" + high_score, 0, 32);
}

最後は終了処理です。
finalGame() は、ゲームウインドウの右上の×ボタンを押してゲームを終了した場合に実行される処理です。
ここでは、終了した時にハイスコアを 0 番にセーブしています。

/********* 終了時の処理はこちらに *********/
public void finalGame()
{
// ハイスコアを0番にセーブ
gc.save(0, high_score);
}

メソッドを定義する

※ 難しい項目ですので、腕に自信のある方以外は読み飛ばしてもらってかまいません。

メソッドを使うと、複数の命令を1つにまとめることができます。
実は、今までの講義で使ってきた gc.drawImage などの gc. で始まる命令は全てメソッドです。
GameCanvas 側で用意されたこれらのメソッドは、複雑な処理をひとつにまとめ、簡単にゲームを作れるように工夫されています。

では、メソッドの作り方を学んでいきましょう。 メソッドを使いこなすことで、より良いプログラムが書けるようになります。
メソッドの自作に挑戦してみましょう。

Sample04 フォルダ直下にあるGame.javaを開いて下さい。
このプログラムは、画面を青く塗りつぶすだけの単純なものです。

    (中略)

/********* 画像の描画はこちらに *********/
public void drawGame()
{
// 青く塗りつぶすメソッドを呼ぶ
clearBlue();
}

/********* 終了時の処理はこちらに *********/
public void finalGame() {}


// 青で画面を塗りつぶすメソッドを定義
void clearBlue()
{
// 色を青にセット
gc.setColor(0, 0, 255);
// 画面を塗りつぶす
gc.fillRect(0, 0, 640, 480);
}

一番下で、clearBlue というメソッドを定義しています。
このメソッドの中には、2 つの命令が記述されています。

定義したメソッド clearBlue は、drawGame 内で呼ばれています。
clearBlue が呼ばれると、clearBlue の中身が上から順に実行されます。
(ここでは、色を青にセットし、その色で画面を塗りつぶしている)

 

もう一つ具体例を見てみましょう。
Sample05 フォルダ直下にあるGame.javaを開いて下さい。

Sample05 は、Sample03 を元に変更を加えたものです。
ボールが画面の下を超えた場合の初期化処理を、メソッドとしてまとめています。

一番下に、resetGameValue というメソッドを定義しています。
このメソッドの中に、ゲーム開始時に毎回戻さなければいけない変数の初期化処理を記述します。

// ゲームスタート時の状態に初期化するメソッドを定義
void resetGameValue()
{
// ボールの座標を初期化する
ball_x = 0;
ball_y = 100;

// ラケットの座標を初期化する
racket_x = 270;
racket_y = 450;

// 点数を初期化する
score = 0;
}

そして、このresetGameValueメソッドを、必要に応じて適宜呼び出しています。

/********* 初期化の手順はこちらに *********/
public void initGame()
{
(中略)

// ゲームスタート時の状態に初期化するメソッドを呼び出す
resetGameValue();
}

/********* 物体の移動等の更新処理はこちらに *********/
public void updateGame()
{
(中略)

// ボールが画面の下を越えた場合
if(ball_y > 480)
{
// ゲーム開始の状態に初期化するメソッドを呼び出す
resetGameValue();
}

(中略)
}

resetGameValue メソッドは、initGame 内と、updateGame 内でボールが画面の下を超えた場合に呼ばれています。
これにより、ボールが画面の下を超えた場合に、もう一度最初の状態から始められるようになりました。

このように、いくつかの場所に同じような処理がまとまっている場合に、メソッドを使うと非常に便利です。
これをメソッドを使わずに書くと、次のように同じ内容を複数の箇所に書かなければならず、非常に面倒です。

/********* 初期化の手順はこちらに *********/
public void initGame()
{
(中略)

// ボールの座標を初期化する
ball_x = 0;
ball_y = 100;

// ラケットの座標を初期化する
racket_x = 270;
racket_y = 450;

// 点数を初期化する
score = 0;
}

/********* 物体の移動等の更新処理はこちらに *********/
public void updateGame()
{
(中略)

// ボールが画面の下を越えた場合
if(ball_y > 480)
{
// ボールの座標を初期化する
ball_x = 0;
ball_y = 100;

// ラケットの座標を初期化する
racket_x = 270;
racket_y = 450;

// 点数を初期化する
score = 0;
}

(中略)
}

また、処理を変更しなければならなくなった場合にも、複数の場所を同時に書き換えることになります。
書き換える場所が増えてくると書き換え忘れることもあるでしょう。こうしてバグが生まれてしまうわけです。
こうならないためにも、メソッドを活用することをお薦めします。

メソッドの引数

メソッドを呼び出す際にカッコの中に書く数字のことを、引数と言います。
例えば、gc.setColor(0, 0, 255); は、3 つの引数を持つメソッドです。
Sample04, Sample05 で作ったメソッドは、カッコの中になにもないので、引数無しのメソッドです。

引数を活用したメソッドを定義してみましょう。
Sample06 フォルダ直下にあるGame.javaを開いて下さい。
引数に渡した数値を使って、画面を塗りつぶすメソッドを定義しています。

    (中略)

/********* 画像の描画はこちらに *********/
public void drawGame()
{
// 画面を塗りつぶすメソッドを呼ぶ
// ここでは、赤で塗りつぶしている
clearWindow(255, 0, 0);
}

(中略)

// 引数に渡した数値を使って、画面を塗りつぶすメソッドを定義
void clearWindow(int r, int g, int b)
{
// 色を青にセット
gc.setColor(r, g, b);
// 画面を塗りつぶす
gc.fillRect(0, 0, 640, 480);
}

定義された clearWindow メソッドには、int 型の3つの引数 r, g, b があります。
そして、drawGame では、255, 0, 0 の引数で clearWindow を呼び出しています。

ここで呼び出された clearWindow の引数はそれぞれ、r は 255、g は 0、b は 0 です。
つまり、gc.setColor(r, g, b); は、gc.setColor(255, 0, 0); として実行されます。

メソッドの戻り値

メソッドは、値を返すこともできます。この返す値を、戻り値といいます。
例えば、gc.rand は、int 型の値を返すメソッドです。

それでは、戻り値の例を説明します。
Sample07 フォルダ直下にあるGame.javaを開いて下さい。
このプログラムでは、引数に与えた数字を 2 乗して返すメソッドを定義しています。

/********* 物体の移動等の更新処理はこちらに *********/
public void updateGame()
{
value = square(7);
}

(中略)

// 引数に渡した数値を2乗して返すメソッドを定義
int square(int n)
{
return n * n;
}

square という、int 型の引数 1 つをとり、2乗した値を返すメソッドを定義しています。
return 値; と書くと、その値が返されます。
return された時点でメソッドは終了するので、return より下に命令を書くことはできません。注意しましょう。

square の前に書いてある int は、戻り値の型です。
ここでは、int 型の値を返すと定義しています。
なお、ここに void と書くと、値を返さないメソッドとなります。
その場合は、return 文を書かなくてもエラーにはなりません。

updateGame 内で、square() を呼んでいます。
このメソッドの中では、n * n で引数を二乗して、return でその結果を返しています。

課題

これまでの授業で学んだ内容の集大成として、シューティングゲームを作成しましょう。
以下の内容を全て満たした課題プログラムを作ってください。

また、余裕のある方は、メソッドを積極的に使っていきましょう。


図:課題の実行画面(タイトル)


図:課題の実行画面(一時停止)


図:課題の実行画面(ゲーム画面)


図:課題の実行画面(ゲームオーバー)


図:課題の実行画面(結果表示)

戻る