Pythonロボティクスコース レッスン 29

テーマ.7-4 レーシングゲームを制作しよう

高度な処理を扱うゲームを製作しよう!

このレッスンで学ぶこと

この回では、前回のレッスンで学習した制御手法を使用して、より高度な処理を扱うゲーム製作を行います。

前回のレッスンの振り返り

前のレッスンでは、カメラで撮影した動画やゲームの映像処理で用いられている考え方を参考にして、フレームレートで定義した時間単位をもつ時間軸上で複数のオブジェクトを同時に制御する手法について学びました。

【 学習した制御手法の特徴 】
  • 複数のオブジェクトを配置したフレームを設定する。
  • フレームの処理はフレームレート(fps) で設定した時間単位で進める。
  • フレームに配置したオブジェクトはそれぞれ独自の時間軸を持っている。
  • 各オブジェクトの時間軸上には、あらかじめイベント(オブジェクトのメソッド)が登録されており、時間がそこまで進むとそのイベントが実行される。

また、この制御の仕組み実現するために、プログラム内でTickerクラスとTickObjクラスの2つのクラスを定義しました。Tickerはフレームの進行を制御するクラスで、TickObjはフレームに配置するオブジェクトに最低限必要な機能を定義したスーパークラスです。各オブジェクトのクラスは、このTickObjクラスを継承して作成しました。

【 テーマ.7-3で作成したクラス定義のコード 】

このレッスンでも引き続き、このコードを使用してゲームの製作を行います。まずは、新しくファイルを作成して、上のコードをコピーして貼り付けましょう。そして、ゲーム製作に入る前に少しだけこのコードを改良しておきたいと思います。

2. 1 コードを改良する

Tickerクラスでは、フレームにオブジェクトを配置するためにregister()メソッドを定義していました。しかし、オブジェクトは配置するだけでなく、反対にフレームから取り除きたいケースもあります。そこで新たに、remove()メソッドを定義して、指定されたオブジェクトを削除できるようにしましょう。

■ フレームに配置したオブジェクトをID値で管理する

以前のレッスンで、Pythonでは作成したオブジェクトに必ず固有の番号(ID値)が割り振られるということを説明しました。この割り振られた番号を調べるときは、標準関数のid()を使用します。

実際に、コピーしたコードを実行して、ターミナルエリアでTickObjクラスのインスタンスを作成し、そのID値を調べてみましょう。

※ ID値はオブジェクトを作成するたびに違う値が設定されるため、上のサンプルコードと同じ番号になるとは限りません。

今はフレームに配置したオブジェクトをリストのプロパティobjsで管理していますが、これをID値をキーにした辞書で管理するように変更します。これによって、ID値でオブジェクトを検索して削除できるようになります。まずは、コードを次のように変更しましょう。

変更【7行目、11行目、15行目】

次に新たにremove()メソッドを定義します。このメソッドは引数として複数のオブジェクトを受け取り、順番にid()関数でID値を調べて、一致する要素をobjsプロパティから削除します。

追加【13行目~15行目】

これでフレームからオブジェクトが削除できるようになりました。

■ オブジェクトの時間軸上に設定するイベントをID値で管理する

Pythonでは関数やメソッドもID値を持っています。そこで、オブジェクトがもつ時間軸上に設定するイベント(メソッド)もID値で管理するように変更しておきましょう。

追加・変更【27行目、30行目、32行目、33行目、41行目】

これでコードが改良できました。このコードを使って、次のチャプターから「レーシングゲーム」を製作していきましょう。

レーシングゲーム機の組み立て

以下の組立説明書を確認して、レーシングゲームで使用する機体を組み立てましょう。

3. 1 組み立てに必要なパーツ

【 パーツ一覧 】
  • Studuino:bit×1
  • 電池ボックス×1
  • ブロック基本四角(白)×1
  • ブロック基本四角(グレー)×4
  • ブロック基本四角(黒)×1
  • ブロック基本四角(赤)×6
  • ブロックハーフA(グレー)×2
  • ブロックハーフB(グレー)×1
  • ブロックハーフC(白)×7
  • ブロックハーフD(白)×5
  • ステー×2
  • 回転軸×3
【 アーテックブロックの形状 】

3. 2 組立説明書

以下のリンク先から組立説明書を確認しましょう。

レーシングゲーム機の組立説明書ここまでできたらクリック

レーシングゲームの作成

ここからは、レーシングゲームのプログラムを作成していきます。まずは、次のプレイ動画と遊び方を見てどのようなゲームになっているのかを確認しましょう。

【 ゲームのプレイ動画 】
【 ゲームの遊び方 】
  1. Aボタンを押してゲームを開始します。
  2. ゲーム開始後から敵(赤系や青系の色のドット)がLEDディスプレイの上側から登場し、一定のフレーム数おきに下へ1マスずつ移動してきます。
  3. プレイヤーはレーサー(緑色のドット)を操作し、この移動してくる敵を避けます。
  4. レーサーはハンドルを傾けた向きに移動します。(ハンドルを傾けた向きは、Studuino:bitの加速度センサーで判別します。)
  5. 制限時間の30秒が経過するまで、敵を避け続けることができればゲームクリアです。

4. 1 ゲーム中に登場するオブジェクト

このゲームでは、以下のオブジェクトが登場します。これらのオブジェクトを作成するためのクラスはTickObjを継承して定義します。

名前役割
レーサー緑色のドットでプレイヤーが操作するオブジェクト
エンジンエンジン音としてブザーから一定の間隔で低い音を鳴らすオブジェクト
赤系や青系のドットで、ランダムな位置から出現し、レーサーに迫ってくるオブジェクト

レーサーやエンジンのオブジェクトはフレームに1つだけ配置します。一方で敵は、一定の間隔で新しいオブジェクトを作成し、フレームに配置します。そして、敵のオブジェクトはLEDディスプレイの外側へ移動した場合、処理する必要がなくなるため、フレームから取り除きます。

4. 2 各クラスの定義

LEDディスプレイ上に表示するオブジェクトを作成するためのクラスは、すべてTickObjのサブクラスとして定義します。

【 定義するクラスの一覧 】
クラス名役割
Racerレーサーのオブジェクト作成用クラス
Engineエンジンのオブジェクト作成用クラス
Enemy敵のオブジェクト作成用クラス

では、それぞれのクラスを定義していきましょう。

■ Racerクラスの定義

RacerクラスはTickObjを継承して、次のプロパティとメソッドを新たに追加します。

Racerクラスで追加するプロパティ】
プロパティ名内容
accelerometerStuduino:bitの加速度センサーを扱うStuduinoBitAccelerometerクラスのインスタンス
xLEDディスプレイ上のX座標
yLEDディスプレイ上のY座標
colorLEDディスプレイ上に表示するときの色の値
【 Racerクラスで追加またはオーバーライドするメソッド 】
プロパティ名内容
__init__()新たに追加したプロパティの初期値を設定します。
※ スーパークラスのメソッドをオーバーライド
move()加速度センサーの値から傾きを判断して、X座標の位置を変更します。
reset()プロパティを初期値に戻します。

まずは、__init__()メソッドを定義して、プロパティの初期値を設定します。スーパークラスのメソッドをオーバーライドするため、それと同じ引数を受け取る必要がある点に注意してください。

追加【46行目~53行目】

次に、加速度センサーの値から傾きを判定して、横方向(X軸方向)に移動するmove()メソッドを定義しましょう。加速度センサーの値は重力の影響から、Studuino:bitを右に傾けると大きくなり、左に傾けると小さくなります。

追加【55行目~60行目】

最後に、プロパティを初期状態に戻すreset()メソッドを定義します。レーサーを初期位置の(2, 4)に設定し、スーパークラスから継承した時間軸の位置を元に戻す(0にする)メソッドreset_position()を実行します。

追加【62行目~65行目】

■ Engineクラスの定義

EngineクラスはTickObjを継承して、次のプロパティとメソッドを新たに追加します。Racerクラスとはちがい、__init__()メソッドはオーバーライドしません。

Engineクラスで追加するプロパティ】
プロパティ名内容
buzzerStuduino:bitのブザー扱うStuduinoBitBuzzerクラスのインスタンス
【 Engineクラスで追加するメソッド 】
プロパティ名内容
on()ブザーから低い音("C3")を鳴らします。
off()ブザーの音を止めます。
reset()プロパティを初期値に戻します。

では、それぞれのプロパティとメソッドを次のように定義しましょう。

追加【67行目~77行目】

■ Enemyクラスの定義

EnemyクラスはTickObjを継承して、次のプロパティとメソッドを新たに追加します。

Enemyクラスで追加するプロパティ】
プロパティ名内容
xLEDディスプレイ上のX座標(ランダムに決定)
yLEDディスプレイ上のY座標
colorLEDディスプレイ上に表示するときの色の値(ランダムに決定)
【 Enemyクラスで追加またはオーバーライドするメソッド 】
プロパティ名内容
__init__()新たに追加したプロパティの初期値を設定します。</br>※ スーパークラスのメソッドをオーバーライド
move()Y座標の位置を1ずつ変更します。

敵が出現するX座標の位置とその表示色はランダムに決定します。それには乱数を利用するため、プログラムの先頭でrandomモジュールをインポートしましょう。

追加【2行目】

Enemyクラスは次のように定義します。colorプロパティはレーサーの色を緑色に設定しているため、緑の強さ(G値)は0に設定し、赤の強さ(R値)と青の強さ(B値)を乱数を使って設定しましょう。以下のコードでは、random.randint(1, 31)のように、1きざみの乱数を発生させてしまうと、あまり色の違いが出ないこともあるため、random.randint(1, 5) * 6とし、6きざみの乱数を発生させるようにしています。

追加【80行目~89行目】

4. 3 メインの処理を行うプログラムの作成

ここまでで、すべてのクラスが定義できたので、ここからはゲームのメインとなる処理を書いていきます。このメインの処理は次の4つの関数にまとめます。

【 定義する関数 】
関数名内容
main()ゲームのメイン処理全体をまとめた関数 (※以下の関数もすべてこの関数内で定義します。)
reset()ゲームの状態をリセットする処理をまとめた関数
clear_screen()LEDディスプレイへ表示するイメージがもつLEDの点灯状態をすべて消灯に変更する関数
start_game()ゲーム中の進行を行う処理をまとめた関数

そして、これらの関数を組み合わせて全体を次の流れで処理していきます。

【 ゲームの処理の流れ 】

後で詳しく説明しますが、start_game()関数内でフレームの時間軸を次に進めた回数をカウントする点に注意してください。この回数は一定の間隔で敵を出現させる処理を行うために利用します。

ゲーム開始時に行うカウントダウンや、失敗したときの音は、テーマ.7-1から使用しているsoundモジュールを利用します。もし、Studuino:bit上から削除している場合は、以下のリンク先からダウンロードしてもう一度Studuino:bitに保存しましょう。

※ リンクの上にカーソルを合わせて右クリックし、「名前を付けてリンク先を保存」を選択してください。

sound.py

また、ゲーム内で登場する各オブジェクトには、それぞれ次のように時間軸とイベントを設定します。

【 各オブジェクトの時間軸と設定するイベント 】

それでは、順を追ってコードを書いていきましょう。

■ フレームレートと制限時間の設定

このゲームのフレームレートと制限時間を定数としてプログラムの先頭で定義します。はじめは、フレームレートを60fps、制限時間を30秒としておきましょう。

追加【4行目、5行目】

■ オブジェクトと変数の作成

まずは、main()関数内で使用する外部ファイルのオブジェクトとモジュールをインポートします。

追加【96行目~98行目】

次にレーサーとエンジンのオブジェクトを作成してイベントの登録を行い、フレームに配置します。また、ディスプレイに表示するイメージと、繰り返し作成する敵のオブジェクトを管理するリストも用意しましょう。

追加【100行目~108行目】

■ reset()関数の定義

ゲームを初期状態に戻すreset()関数は次のように定義します。racerオブジェクトとengineオブジェクトのreset()メソッドを実行するだけでなく、フレームから敵オブジェクトを削除したり、現在のディスプレイの表示をクリアしたりする処理もここに記述します。

追加【110行目~117行目】

■ clear_screen()関数の定義

ディスプレイに表示するイメージ上のLEDをすべて消灯に設定するclear_screen()関数は次のように定義します。for文とrange()関数を組み合わせて、ディスプレイの左上(x=0, y=0)から右下(x=4, y=4)まで順番に消灯の設定を行いましょう。

追加【119行目~122行目】

■ start_game()関数の定義

続けて、メイン処理の中核にあたるstart_game()関数を定義します。ゲーム開始時にまずは現在の時間を取得します。そして、フレームの時間軸を進めた回数を記録する変数と、敵を出現させる頻度として、何回フレームを進めるごとに敵を出現させるかを定めた変数を定義します。

追加【124行目~127行目】
※ 「appearance」は「出現」、「frequency」は「頻度」の意味があります。

次に制限時間が経過するまで繰り返す処理を用意して、始めにフレームの進行と進めた回数をカウントします。

追加【129行目~131行目】

カウントした回数がenemy_appearance_frequencyに達していれば、敵を出現させます。新しい敵のオブジェクトを作成し、イベントの登録を行ってからフレームに配置しましょう。また、作成した敵のオブジェクトはリストenemiesにも追加します。

追加【132行目~137行目】

次にLEDディスプレイから外れてしまった敵のオブジェクトを削除する処理を書きます。LEDディスプレイから外れたことは、敵のオブジェクトをがもつyプロパティ(Y軸上の位置)が「4」を超えているかどうかで判断できます。

追加【139行目~145行目】

今度は、LEDディスプレイにレーサーと敵オブジェクトが乗ったイメージを表示する処理を書きます。StuduinoBitImageクラスのset_pixel_colorメソッドを使って、ひとつずつ設定していきましょう。

追加【147行目~151行目】

前のレッスンで製作した卓球ゲームとちがい、ひとつひとつのオブジェクトにイメージ(StuduinoBitImageクラスのインスタンス)を持たせていません。これは、「+」演算子によるイメージ同士の合成を行う処理に時間が掛かり、ゲームの進行が設定したフレームレートより遅くなってしまうのを防ぐためです。このゲームは前回よりも速くLEDディスプレイの表示を切り替える必要があるため、このような書き方にしています。

そして最後に、レーサーと敵が接触しているかどうかを判定します。フラグとしてis_overlappedを定義し、レーサーと敵のX座標とY座標が一致していれば、これをTrueにします。もし判定の結果is_overlappedTrueであれば、クリア失敗として変数is_clearedFalseを格納します。そして、制限時間の経過を待たずにbreak文で129行目のwhile文を抜けます。

追加【153行目~160行目】

また、break文ではなく、制限時間が経過して正常にwhile文を抜けた場合はクリア成功となるため、while文に対するelse文でis_clearedTrueを格納します。

追加【162行目、163行目】

これで1回のゲームは終了です。最後にゲームの結果を音とLEDディスプレイの表示でプレイヤーに知らせます。

追加【165行目~172行目】

■ ボタンAを押してゲームを開始する処理

ゲームはボタンAを押して開始します。そこで、そのことがわかるようにLEDディスプレイにAの文字を表示しておきます。そして、ボタンAが押されると、カウントダウンを行い、start_game()関数を実行してゲームを開始します。ゲームが終了したあとは、少し時間を空けてから状態をリセットして、再びLEDディスプレイにAの文字を表示しましょう。

追加【174行目~181行目】

これでメインの処理ができました。あとはこの関数の外側で関数を呼び出して実行すればレーシングゲームを楽しむことができます。

追加【184行目】

■ 動作を確認する

200行近くコードを書いてきたので少し大変だったと思いますが、これでプログラムの完成です。実行して動作を確認してみましょう。もしうまくいかない場合、下の完成プログラムと見比べて、誤りがないかを確かめてください。

【 サンプルコード 4-3-1 】

課題:ゲームの難易度設定を調整しよう

作成したレーシングゲームの難易度は主に次の要素と関係しています。

行番号要素難易度との関係
4行目フレームレートFPSフレームレートが高いほどゲームの進行スピードが上がり、難易度が高くなります。
5行目制限時間LIMIT_TIME制限時間が長いほどゲームクリアの難易度が高くなります。
100行目、101行目レーサーのオブジェクトracerの時間軸の設定move()メソッドを実行するまでのサイクルが短いほどレーサーを素早く動かすことができます。
127行目敵の出現頻度enemy_appearance_frequency数値が小さいほど敵の出現頻度が高くなります。
133行目、134行目敵のオブジェクトnew_enemyの時間軸の設定move()メソッドを実行するまでのサイクルが短いほど敵が速くレーサーに接近してきます。

この5つの要素を調整することで、ゲームの難易度を変更することができます。色々と試してみて、自分好みの難易度になるようにプログラムを改良してみましょう。

おわりに

6. 1 このレッスンのまとめ

このレッスンでは、テーマ.7-3で学習した手法を使用して、より高度なゲームプログラムの作成に取り組みました。テーマ7を通してゲームの製作を行ってきましたが、シンプルなゲームでもこれだけ複雑なプログラムを書かなければならないことが分かったのではないかと思います。ゲーム製作のテーマはこれで終わりですが、ぜひ今度は自分自身でゲーム内容を考えて制作することに挑戦してみてください。

6. 2 次のレッスンについて

次のテーマからは、「ロボットアーム」の製作に取り組みます。複数のモーターの動きやセンサーを制御して、ものを識別して正確に運べるようなロボットアームを開発していきましょう。

TOP