Pythonロボティクスコース レッスン 29
テーマ.7-4 レーシングゲームを制作しよう
高度な処理を扱うゲームを製作しよう!
チャプター1
このレッスンで学ぶこと
この回では、前回のレッスンで学習した制御手法を使用して、より高度な処理を扱うゲーム製作を行います。
チャプター2
前回のレッスンの振り返り
前のレッスンでは、カメラで撮影した動画やゲームの映像処理で用いられている考え方を参考にして、フレームレートで定義した時間単位をもつ時間軸上で複数のオブジェクトを同時に制御する手法について学びました。
【 学習した制御手法の特徴 】
- 複数のオブジェクトを配置したフレームを設定する。
- フレームの処理はフレームレート(fps) で設定した時間単位で進める。
- フレームに配置したオブジェクトはそれぞれ独自の時間軸を持っている。
- 各オブジェクトの時間軸上には、あらかじめイベント(オブジェクトのメソッド)が登録されており、時間がそこまで進むとそのイベントが実行される。
また、この制御の仕組み実現するために、プログラム内でTicker
クラスとTickObj
クラスの2つのクラスを定義しました。Ticker
はフレームの進行を制御するクラスで、TickObj
はフレームに配置するオブジェクトに最低限必要な機能を定義したスーパークラスです。各オブジェクトのクラスは、このTickObj
クラスを継承して作成しました。
【 テーマ.7-3で作成したクラス定義のコード 】
import time
class Ticker:
def __init__(self, fps):
self.fps = fps
self.ticks_time_ms = round(1000/fps)
self.objs = []
def register(self, *objs):
for obj in objs:
self.objs.append(obj)
def ticks(self):
time.sleep_ms(self.ticks_time_ms)
for obj in self.objs:
obj.ticks()
class TickObj:
def __init__(self, end, repeat=True):
self.position = 0
self.end = end
self.repeat = repeat
self.timeline = {n+1: [] for n in range(end)}
def add_event(self, pos, event):
self.timeline[pos].append(event)
def remove_event(self, pos):
self.timeline[pos].clear()
def reset_position(self):
self.position = 0
def ticks(self):
if self.position < self.end:
self.position += 1
for event in self.timeline[self.position]:
event()
if self.repeat and self.position == self.end:
self.reset_position()
このレッスンでも引き続き、このコードを使用してゲームの製作を行います。まずは、新しくファイルを作成して、上のコードをコピーして貼り付けましょう。そして、ゲーム製作に入る前に少しだけこのコードを改良しておきたいと思います。
2. 1 コードを改良する
Ticker
クラスでは、フレームにオブジェクトを配置するためにregister()
メソッドを定義していました。しかし、オブジェクトは配置するだけでなく、反対にフレームから取り除きたいケースもあります。そこで新たに、remove()
メソッドを定義して、指定されたオブジェクトを削除できるようにしましょう。
■ フレームに配置したオブジェクトをID値で管理する
以前のレッスンで、Pythonでは作成したオブジェクトに必ず固有の番号(ID値)が割り振られるということを説明しました。この割り振られた番号を調べるときは、標準関数のid()
を使用します。
実際に、コピーしたコードを実行して、ターミナルエリアでTickObj
クラスのインスタンスを作成し、そのID値を調べてみましょう。
>>> obj = TickObj(10) >>> print(id(obj)) 1065455472
※ ID値はオブジェクトを作成するたびに違う値が設定されるため、上のサンプルコードと同じ番号になるとは限りません。
今はフレームに配置したオブジェクトをリストのプロパティobjs
で管理していますが、これをID値をキーにした辞書で管理するように変更します。これによって、ID値でオブジェクトを検索して削除できるようになります。まずは、コードを次のように変更しましょう。
変更【7行目、11行目、15行目】
import time
class Ticker:
def __init__(self, fps):
self.fps = fps
self.ticks_time_ms = round(1000/fps)
self.objs = {} # 辞書に変更
def register(self, *objs):
for obj in objs:
self.objs[id(obj)] = obj # ID値をキーにしてオブジェクトを格納
def ticks(self):
time.sleep_ms(self.ticks_time_ms)
for obj in self.objs.values(): # 辞書のvalues()メソッドでオブジェクトだけを取り出す
obj.ticks()
次に新たにremove()
メソッドを定義します。このメソッドは引数として複数のオブジェクトを受け取り、順番にid()
関数でID値を調べて、一致する要素をobjs
プロパティから削除します。
追加【13行目~15行目】
import time
class Ticker:
def __init__(self, fps):
self.fps = fps
self.ticks_time_ms = round(1000/fps)
self.objs = {}
def register(self, *objs):
for obj in objs:
self.objs[id(obj)] = obj
def remove(self, *objs): # 指定されたオブジェクトをフレームから削除する
for obj in objs:
del self.objs[id(obj)] # del文で要素を指定して削除
def ticks(self):
time.sleep_ms(self.ticks_time_ms)
for obj in self.objs.values():
obj.ticks()
これでフレームからオブジェクトが削除できるようになりました。
■ オブジェクトの時間軸上に設定するイベントをID値で管理する
Pythonでは関数やメソッドもID値を持っています。そこで、オブジェクトがもつ時間軸上に設定するイベント(メソッド)もID値で管理するように変更しておきましょう。
追加・変更【27行目、30行目、32行目、33行目、41行目】
class TickObj:
def __init__(self, end, repeat=True):
self.position = 0
self.end = end
self.repeat = repeat
self.timeline = {n+1: {} for n in range(end)} # 「n+1: {}」で辞書に変更
def add_event(self, pos, event):
self.timeline[pos][id(event)] = event # ID値をキーにしてイベントを格納
def remove_event(self, pos, event): # 引数にイベントを追加
del self.timeline[pos][id(event)] # del文で要素を指定して削除
def reset_position(self):
self.position = 0
def ticks(self):
if self.position < self.end:
self.position += 1
for event in self.timeline[self.position].values(): # 辞書のvalues()メソッドでイベントだけを取り出す
event()
if self.repeat and self.position == self.end:
self.reset_position()
これでコードが改良できました。このコードを使って、次のチャプターから「レーシングゲーム」を製作していきましょう。
チャプター3
レーシングゲーム機の組み立て
以下の組立説明書を確認して、レーシングゲームで使用する機体を組み立てましょう。
3. 1 組み立てに必要なパーツ
【 パーツ一覧 】
- Studuino:bit×1
- 電池ボックス×1
- ブロック基本四角(白)×1
- ブロック基本四角(グレー)×4
- ブロック基本四角(黒)×1
- ブロック基本四角(赤)×6
- ブロックハーフA(グレー)×2
- ブロックハーフB(グレー)×1
- ブロックハーフC(白)×7
- ブロックハーフD(白)×5
- ステー×2
- 回転軸×3
【 アーテックブロックの形状 】
3. 2 組立説明書
以下のリンク先から組立説明書を確認しましょう。
レーシングゲーム機の組立説明書ここまでできたらクリック
チャプター4
レーシングゲームの作成
ここからは、レーシングゲームのプログラムを作成していきます。まずは、次のプレイ動画と遊び方を見てどのようなゲームになっているのかを確認しましょう。
【 ゲームのプレイ動画 】
【 ゲームの遊び方 】
- Aボタンを押してゲームを開始します。
- ゲーム開始後から敵(赤系や青系の色のドット)がLEDディスプレイの上側から登場し、一定のフレーム数おきに下へ1マスずつ移動してきます。
- プレイヤーはレーサー(緑色のドット)を操作し、この移動してくる敵を避けます。
- レーサーはハンドルを傾けた向きに移動します。(ハンドルを傾けた向きは、Studuino:bitの加速度センサーで判別します。)
- 制限時間の30秒が経過するまで、敵を避け続けることができればゲームクリアです。
4. 1 ゲーム中に登場するオブジェクト
このゲームでは、以下のオブジェクトが登場します。これらのオブジェクトを作成するためのクラスはTickObj
を継承して定義します。
名前 | 役割 |
---|---|
レーサー | 緑色のドットでプレイヤーが操作するオブジェクト |
エンジン | エンジン音としてブザーから一定の間隔で低い音を鳴らすオブジェクト |
敵 | 赤系や青系のドットで、ランダムな位置から出現し、レーサーに迫ってくるオブジェクト |
レーサーやエンジンのオブジェクトはフレームに1つだけ配置します。一方で敵は、一定の間隔で新しいオブジェクトを作成し、フレームに配置します。そして、敵のオブジェクトはLEDディスプレイの外側へ移動した場合、処理する必要がなくなるため、フレームから取り除きます。
4. 2 各クラスの定義
LEDディスプレイ上に表示するオブジェクトを作成するためのクラスは、すべてTickObj
のサブクラスとして定義します。
【 定義するクラスの一覧 】
クラス名 | 役割 |
---|---|
Racer | レーサーのオブジェクト作成用クラス |
Engine | エンジンのオブジェクト作成用クラス |
Enemy | 敵のオブジェクト作成用クラス |
では、それぞれのクラスを定義していきましょう。
■ Racer
クラスの定義
Racer
クラスはTickObj
を継承して、次のプロパティとメソッドを新たに追加します。
【Racer
クラスで追加するプロパティ】
プロパティ名 | 内容 |
---|---|
accelerometer | Studuino:bitの加速度センサーを扱うStuduinoBitAccelerometer クラスのインスタンス |
x | LEDディスプレイ上のX座標 |
y | LEDディスプレイ上のY座標 |
color | LEDディスプレイ上に表示するときの色の値 |
【 Racer
クラスで追加またはオーバーライドするメソッド 】
プロパティ名 | 内容 |
---|---|
__init__() | 新たに追加したプロパティの初期値を設定します。 ※ スーパークラスのメソッドをオーバーライド |
move() | 加速度センサーの値から傾きを判断して、X座標の位置を変更します。 |
reset() | プロパティを初期値に戻します。 |
まずは、__init__()
メソッドを定義して、プロパティの初期値を設定します。スーパークラスのメソッドをオーバーライドするため、それと同じ引数を受け取る必要がある点に注意してください。
追加【46行目~53行目】
class Racer(TickObj):
from pystubit.board import accelerometer # accelerometerオブジェクトをインポートし、プロパティとして追加
def __init__(self, end, repeat=True): # スーパークラスのメソッドと同じ引数を受け取る
super().__init__(end, repeat) # スーパークラスのメソッドの呼び出し
self.x = 2 # 初期位置はLEDディスプレイの下部中央の(2, 4)
self.y = 4
self.color = (0, 31, 0) # レーサーは緑色で表示
次に、加速度センサーの値から傾きを判定して、横方向(X軸方向)に移動するmove()
メソッドを定義しましょう。加速度センサーの値は重力の影響から、Studuino:bitを右に傾けると大きくなり、左に傾けると小さくなります。
追加【55行目~60行目】
class Racer(TickObj):
from pystubit.board import accelerometer
def __init__(self, end, repeat=True):
super().__init__(end, repeat)
self.x = 2
self.y = 4
self.color = (0, 31, 0)
def move(self):
val = self.accelerometer.get_x() # x軸方向の加速度を取得
if val > 2 and self.x < 4: # 右に傾けたとき。※既に右端まで到達している場合(x=4)は除く
self.x += 1 # 右へ移動
if val < -2 and self.x > 0: # 左に傾けたとき。※既に左端まで到達している場合(x=0)は除く
self.x -= 1 # 左へ移動
最後に、プロパティを初期状態に戻すreset()
メソッドを定義します。レーサーを初期位置の(2, 4)に設定し、スーパークラスから継承した時間軸の位置を元に戻す(0にする)メソッドreset_position()
を実行します。
追加【62行目~65行目】
class Racer(TickObj):
from pystubit.board import accelerometer
def __init__(self, end, repeat=True):
super().__init__(end, repeat)
self.x = 2
self.y = 4
self.color = (0, 31, 0)
def move(self):
val = self.accelerometer.get_x()
if val > 2 and self.x < 4:
self.x += 1
if val < -2 and self.x > 0:
self.x -= 1
def reset(self):
self.x = 2 # 初期位置の(2, 4)に戻す
self.y = 4
self.reset_position() # 時間軸上の位置をリセットする
■ Engine
クラスの定義
Engine
クラスはTickObj
を継承して、次のプロパティとメソッドを新たに追加します。Racer
クラスとはちがい、__init__()
メソッドはオーバーライドしません。
【Engine
クラスで追加するプロパティ】
プロパティ名 | 内容 |
---|---|
buzzer | Studuino:bitのブザー扱うStuduinoBitBuzzer クラスのインスタンス |
【 Engine
クラスで追加するメソッド 】
プロパティ名 | 内容 |
---|---|
on() | ブザーから低い音("C3" )を鳴らします。 |
off() | ブザーの音を止めます。 |
reset() | プロパティを初期値に戻します。 |
では、それぞれのプロパティとメソッドを次のように定義しましょう。
追加【67行目~77行目】
class Engine(TickObj):
from pystubit.board import buzzer # buzzerオブジェクトをインポートし、プロパティとして追加
def on(self):
self.buzzer.on("C3") # buzzerオブジェクトはプロパティとしてインポートしているため、selfを付けることに注意
def off(self):
self.buzzer.off()
def reset(self):
self.reset_position()
■ Enemy
クラスの定義
Enemy
クラスはTickObj
を継承して、次のプロパティとメソッドを新たに追加します。
【Enemy
クラスで追加するプロパティ】
プロパティ名 | 内容 |
---|---|
x | LEDディスプレイ上のX座標(ランダムに決定) |
y | LEDディスプレイ上のY座標 |
color | LEDディスプレイ上に表示するときの色の値(ランダムに決定) |
【 Enemy
クラスで追加またはオーバーライドするメソッド 】
プロパティ名 | 内容 |
---|---|
__init__() | 新たに追加したプロパティの初期値を設定します。</br>※ スーパークラスのメソッドをオーバーライド |
move() | Y座標の位置を1ずつ変更します。 |
敵が出現するX座標の位置とその表示色はランダムに決定します。それには乱数を利用するため、プログラムの先頭でrandom
モジュールをインポートしましょう。
追加【2行目】
import time
import random
Enemy
クラスは次のように定義します。color
プロパティはレーサーの色を緑色に設定しているため、緑の強さ(G値)は0に設定し、赤の強さ(R値)と青の強さ(B値)を乱数を使って設定しましょう。以下のコードでは、random.randint(1, 31)
のように、1きざみの乱数を発生させてしまうと、あまり色の違いが出ないこともあるため、random.randint(1, 5) * 6
とし、6きざみの乱数を発生させるようにしています。
追加【80行目~89行目】
class Enemy(TickObj):
def __init__(self, end, repeat=True):
super().__init__(end, repeat)
self.x = random.randint(0, 4)
self.y = 0
self.color = (random.randint(1, 5) * 6, 0, random.randint(1, 5) * 6)
# randint(1, 5)で「1~5」の乱数を発生させ、それに6を掛けることで、6きざみの乱数を発生させています。
def move(self):
self.y += 1
4. 3 メインの処理を行うプログラムの作成
ここまでで、すべてのクラスが定義できたので、ここからはゲームのメインとなる処理を書いていきます。このメインの処理は次の4つの関数にまとめます。
【 定義する関数 】
関数名 | 内容 |
---|---|
main() | ゲームのメイン処理全体をまとめた関数 (※以下の関数もすべてこの関数内で定義します。) |
reset() | ゲームの状態をリセットする処理をまとめた関数 |
clear_screen() | LEDディスプレイへ表示するイメージがもつLEDの点灯状態をすべて消灯に変更する関数 |
start_game() | ゲーム中の進行を行う処理をまとめた関数 |
そして、これらの関数を組み合わせて全体を次の流れで処理していきます。
【 ゲームの処理の流れ 】
後で詳しく説明しますが、start_game()
関数内でフレームの時間軸を次に進めた回数をカウントする点に注意してください。この回数は一定の間隔で敵を出現させる処理を行うために利用します。
ゲーム開始時に行うカウントダウンや、失敗したときの音は、テーマ.7-1から使用しているsound
モジュールを利用します。もし、Studuino:bit上から削除している場合は、以下のリンク先からダウンロードしてもう一度Studuino:bitに保存しましょう。
※ リンクの上にカーソルを合わせて右クリックし、「名前を付けてリンク先を保存」を選択してください。
また、ゲーム内で登場する各オブジェクトには、それぞれ次のように時間軸とイベントを設定します。
【 各オブジェクトの時間軸と設定するイベント 】
それでは、順を追ってコードを書いていきましょう。
■ フレームレートと制限時間の設定
このゲームのフレームレートと制限時間を定数としてプログラムの先頭で定義します。はじめは、フレームレートを60fps、制限時間を30秒としておきましょう。
追加【4行目、5行目】
import time
import random
FPS = 60 # フレームレート
LIMIT_TIME = 30000 # 制限時間(単位:ミリ秒)
class Ticker:
■ オブジェクトと変数の作成
まずは、main()
関数内で使用する外部ファイルのオブジェクトとモジュールをインポートします。
追加【96行目~98行目】
def main():
from pystubit.board import display, button_a, Image
import sound
次にレーサーとエンジンのオブジェクトを作成してイベントの登録を行い、フレームに配置します。また、ディスプレイに表示するイメージと、繰り返し作成する敵のオブジェクトを管理するリストも用意しましょう。
追加【100行目~108行目】
def main():
from pystubit.board import display, button_a, Image
import sound
racer = Racer(4) # レーサーのオブジェクト
racer.add_event(4, racer.move)
engine = Engine(2) # エンジンのオブジェクト
engine.add_event(1, engine.on)
engine.add_event(2, engine.off)
ticker = Ticker(FPS) # フレームの進行を制御するオブジェクト
ticker.register(racer, engine)
screen = Image(5, 5) # LEDディスプレイの表示する空のイメ―ジ
enemies = [] # 敵のオブジェクトを管理するリスト
■ reset()
関数の定義
ゲームを初期状態に戻すreset()
関数は次のように定義します。racer
オブジェクトとengine
オブジェクトのreset()
メソッドを実行するだけでなく、フレームから敵オブジェクトを削除したり、現在のディスプレイの表示をクリアしたりする処理もここに記述します。
追加【110行目~117行目】
def reset():
racer.reset() # レーサーの設定をリセット
engine.reset() # エンジンの設定をリセット
for enemy in enemies: # フレームから敵オブジェクトを削除
ticker.remove(enemy)
enemies.clear() # 敵オブジェクトを管理するリストを空にする
clear_screen() # ディスプレイに表示するイメージをクリア
display.clear() # 現在のディスプレイの表示をクリア
■ clear_screen()
関数の定義
ディスプレイに表示するイメージ上のLEDをすべて消灯に設定するclear_screen()
関数は次のように定義します。for
文とrange()
関数を組み合わせて、ディスプレイの左上(x=0, y=0)から右下(x=4, y=4)まで順番に消灯の設定を行いましょう。
追加【119行目~122行目】
def clear_screen():
for x in range(5):
for y in range(5):
screen.set_pixel(x, y, 0)
■ start_game()
関数の定義
続けて、メイン処理の中核にあたるstart_game()
関数を定義します。ゲーム開始時にまずは現在の時間を取得します。そして、フレームの時間軸を進めた回数を記録する変数と、敵を出現させる頻度として、何回フレームを進めるごとに敵を出現させるかを定めた変数を定義します。
追加【124行目~127行目】
def start_game():
start_time = time.ticks_ms() # 現在の時間を取得
count = 0 # フレームの時間軸を進めた回数を記録する変数
enemy_appearance_frequency = 10 # 敵を出現させる頻度
※ 「appearance」は「出現」、「frequency」は「頻度」の意味があります。
次に制限時間が経過するまで繰り返す処理を用意して、始めにフレームの進行と進めた回数をカウントします。
追加【129行目~131行目】
def start_game():
start_time = time.ticks_ms()
count = 0
enemy_appearance_frequency = 10
while time.ticks_diff(time.ticks_ms(), start_time) < LIMIT_TIME:
ticker.ticks() # 次のフレームへ進める
count += 1 # 回数をカウントする
カウントした回数がenemy_appearance_frequency
に達していれば、敵を出現させます。新しい敵のオブジェクトを作成し、イベントの登録を行ってからフレームに配置しましょう。また、作成した敵のオブジェクトはリストenemies
にも追加します。
追加【132行目~137行目】
def start_game():
start_time = time.ticks_ms()
count = 0
enemy_appearance_frequency = 10
while time.ticks_diff(time.ticks_ms(), start_time) < LIMIT_TIME:
ticker.ticks()
count += 1
if count == enemy_appearance_frequency: # 出現頻度の回数に達した
new_enemy = Enemy(3) # 新しい敵オブジェクトを作成
new_enemy.add_event(3, new_enemy.move) # イベントを登録
ticker.register(new_enemy) # フレームに配置
enemies.append(new_enemy) # リストに追加
count = 0 # 回数をリセット
次にLEDディスプレイから外れてしまった敵のオブジェクトを削除する処理を書きます。LEDディスプレイから外れたことは、敵のオブジェクトをがもつy
プロパティ(Y軸上の位置)が「4」を超えているかどうかで判断できます。
追加【139行目~145行目】
while time.ticks_diff(time.ticks_ms(), start_time) < LIMIT_TIME:
ticker.ticks()
count += 1
if count == enemy_appearance_frequency:
new_enemy = Enemy(3)
new_enemy.add_event(3, new_enemy.move)
ticker.register(new_enemy)
enemies.append(new_enemy)
count = 0
del_list = [] # 削除対象の敵オブジェクトを格納するリスト
for enemy in enemies:
if enemy.y > 4: # LEDディスプレイから外れた場合
ticker.remove(enemy) # フレームから削除
del_list.append(enemy) # リストの削除対象に追加
for enemy in del_list: # 削除対象をリストから削除
enemies.remove(enemy) # 要素で検索して削除
今度は、LEDディスプレイにレーサーと敵オブジェクトが乗ったイメージを表示する処理を書きます。StuduinoBitImageクラスのset_pixel_color
メソッドを使って、ひとつずつ設定していきましょう。
追加【147行目~151行目】
del_list = []
for enemy in enemies:
if enemy.y > 4:
ticker.remove(enemy)
del_list.append(enemy)
for enemy in del_list:
enemies.remove(enemy)
clear_screen() # 以前のイメージをクリアする
screen.set_pixel_color(racer.x, racer.y, racer.color) # レーサーの表示を設定
for enemy in enemies: # 敵オブジェクトの表示を設定
screen.set_pixel_color(enemy.x, enemy.y, enemy.color)
display.show(screen, delay=0) # 設定が完了したイメージを表示
前のレッスンで製作した卓球ゲームとちがい、ひとつひとつのオブジェクトにイメージ(StuduinoBitImageクラスのインスタンス)を持たせていません。これは、「+
」演算子によるイメージ同士の合成を行う処理に時間が掛かり、ゲームの進行が設定したフレームレートより遅くなってしまうのを防ぐためです。このゲームは前回よりも速くLEDディスプレイの表示を切り替える必要があるため、このような書き方にしています。
そして最後に、レーサーと敵が接触しているかどうかを判定します。フラグとしてis_overlapped
を定義し、レーサーと敵のX座標とY座標が一致していれば、これをTrue
にします。もし判定の結果is_overlapped
がTrue
であれば、クリア失敗として変数is_cleared
にFalse
を格納します。そして、制限時間の経過を待たずにbreak
文で129行目のwhile
文を抜けます。
追加【153行目~160行目】
clear_screen()
screen.set_pixel_color(racer.x, racer.y, racer.color)
for enemy in enemies:
screen.set_pixel_color(enemy.x, enemy.y, enemy.color)
display.show(screen, delay=0)
is_overlapped = False # レーサーが敵と接触したかどうかの判定フラグ
for enemy in enemies: # すべての敵とレーサーが接触していないかを確認する
if racer.x == enemy.x and racer.y == enemy.y: # X座標とY座標の両方が一致していれば接触ありと判定
is_overlapped = True
break # 接触があった時点で判定のループを抜ける
if is_overlapped: # 接触があった場合はクリア失敗
is_cleared = False
break # 制限時間の経過を待たず129行目のwhile文を抜ける
また、break
文ではなく、制限時間が経過して正常にwhile
文を抜けた場合はクリア成功となるため、while
文に対するelse
文でis_cleared
にTrue
を格納します。
追加【162行目、163行目】
is_overlapped = False
for enemy in enemies:
if racer.x == enemy.x and racer.y == enemy.y:
is_overlapped = True
break
if is_overlapped:
is_clear = False
break
else: # while文を正常に抜けた場合の処理。インデントの位置に注意
is_cleared = True
これで1回のゲームは終了です。最後にゲームの結果を音とLEDディスプレイの表示でプレイヤーに知らせます。
追加【165行目~172行目】
else:
is_cleared = True
engine.off() # エンジン音のブザーが鳴っている可能性があるため、ここで止める。
time.sleep_ms(1000) # 結果の表示まで少し間を空ける
if is_cleared:
display.show(Image.HEART, color=(0, 31, 0)) # 成功の場合は緑色でハートを表示
sound.success() # soundモジュールで定義した成功音
else:
display.show(Image.SKULL, color=(31, 0, 0)) # 失敗の場合は赤色でドクロマークを表示
sound.failed() # soundモジュールで定義した失敗音
■ ボタンAを押してゲームを開始する処理
ゲームはボタンAを押して開始します。そこで、そのことがわかるようにLEDディスプレイにAの文字を表示しておきます。そして、ボタンAが押されると、カウントダウンを行い、start_game()
関数を実行してゲームを開始します。ゲームが終了したあとは、少し時間を空けてから状態をリセットして、再びLEDディスプレイにAの文字を表示しましょう。
追加【174行目~181行目】
display.show("A", delay=0, color=(31, 31, 31))
while True:
if button_a.is_pressed():
sound.countdown()
start_game()
time.sleep_ms(2000) # 次のゲームを開始できるようにするまで少し時間を空ける
reset()
display.show("A", delay=0, color=(31, 31, 31))
これでメインの処理ができました。あとはこの関数の外側で関数を呼び出して実行すればレーシングゲームを楽しむことができます。
追加【184行目】
display.show("A", delay=0, color=(31, 31, 31))
while True:
if button_a.is_pressed():
sound.countdown()
start_game()
time.sleep_ms(2000)
reset()
display.show("A", delay=0, color=(31, 31, 31))
main() # main()関数の呼び出し
■ 動作を確認する
200行近くコードを書いてきたので少し大変だったと思いますが、これでプログラムの完成です。実行して動作を確認してみましょう。もしうまくいかない場合、下の完成プログラムと見比べて、誤りがないかを確かめてください。
【 サンプルコード 4-3-1 】
import time
import random
FPS = 60
LIMIT_TIME = 30000
class Ticker:
def __init__(self, fps):
self.fps = fps
self.ticks_time_ms = round(1000/fps)
self.objs = {}
def register(self, *objs):
for obj in objs:
self.objs[id(obj)] = obj
def remove(self, *objs):
for obj in objs:
del self.objs[id(obj)]
def ticks(self):
time.sleep_ms(self.ticks_time_ms)
for obj in self.objs.values():
obj.ticks()
class TickObj:
def __init__(self, end, repeat=True):
self.position = 0
self.end = end
self.repeat = repeat
self.timeline = {n+1: {} for n in range(end)}
def add_event(self, pos, event):
self.timeline[pos][id(event)] = event
def remove_event(self, pos, event):
del self.timeline[pos][id(event)]
def reset_position(self):
self.position = 0
def ticks(self):
if self.position < self.end:
self.position += 1
for event in self.timeline[self.position].values():
event()
if self.repeat and self.position == self.end:
self.reset_position()
class Racer(TickObj):
from pystubit.board import accelerometer
def __init__(self, end, repeat=True):
super().__init__(end, repeat)
self.x = 2
self.y = 4
self.color = (0, 31, 0)
def move(self):
val = self.accelerometer.get_x()
if val > 2 and self.x < 4:
self.x += 1
if val < -2 and self.x > 0:
self.x -= 1
def reset(self):
self.x = 2
self.y = 4
self.reset_position()
class Engine(TickObj):
from pystubit.board import buzzer
def __init__(self, end, repeat=True):
super().__init__(end, repeat)
def on(self):
self.buzzer.on("C3")
def off(self):
self.buzzer.off()
def reset(self):
self.reset_position()
class Enemy(TickObj):
def __init__(self, end, repeat=True):
super().__init__(end, repeat)
self.x = random.randint(0, 4)
self.y = 0
self.color = (random.randint(1, 5) * 6, 0, random.randint(1, 5) * 6)
def move(self):
self.y += 1
def main():
from pystubit.board import display, button_a, Image
import sound
racer = Racer(4)
racer.add_event(4, racer.move)
engine = Engine(2)
engine.add_event(1, engine.on)
engine.add_event(2, engine.off)
ticker = Ticker(FPS)
ticker.register(racer, engine)
screen = Image(5, 5)
enemies = []
def reset():
racer.reset()
engine.reset()
for enemy in enemies:
ticker.remove(enemy)
enemies.clear()
clear_screen()
display.clear()
def clear_screen():
for x in range(5):
for y in range(5):
screen.set_pixel(x, y, 0)
def start_game():
start_time = time.ticks_ms()
count = 0
enemy_appearance_frequency = 10
while time.ticks_diff(time.ticks_ms(), start_time) < LIMIT_TIME:
ticker.ticks()
count += 1
if count == enemy_appearance_frequency:
new_enemy = Enemy(3)
new_enemy.add_event(3, new_enemy.move)
ticker.register(new_enemy)
enemies.append(new_enemy)
count = 0
del_list = []
for enemy in enemies:
if enemy.y > 4:
ticker.remove(enemy)
del_list.append(enemy)
for enemy in del_list:
enemies.remove(enemy)
clear_screen()
screen.set_pixel_color(racer.x, racer.y, racer.color)
for enemy in enemies:
screen.set_pixel_color(enemy.x, enemy.y, enemy.color)
display.show(screen, delay=0)
is_overlapped = False
for enemy in enemies:
if racer.x == enemy.x and racer.y == enemy.y:
is_overlapped = True
break
if is_overlapped:
is_cleared = False
break
else:
is_cleared = True
engine.off()
time.sleep_ms(1000)
if is_cleared:
display.show(Image.HEART, color=(0, 31, 0))
sound.success()
else:
display.show(Image.SKULL, color=(31, 0, 0))
sound.failed()
display.show("A", delay=0, color=(31, 31, 31))
while True:
if button_a.is_pressed():
sound.countdown()
start_game()
time.sleep_ms(2000)
reset()
display.show("A", delay=0, color=(31, 31, 31))
main()
チャプター5
課題:ゲームの難易度設定を調整しよう
作成したレーシングゲームの難易度は主に次の要素と関係しています。
行番号 | 要素 | 難易度との関係 |
---|---|---|
4行目 | フレームレートFPS | フレームレートが高いほどゲームの進行スピードが上がり、難易度が高くなります。 |
5行目 | 制限時間LIMIT_TIME | 制限時間が長いほどゲームクリアの難易度が高くなります。 |
100行目、101行目 | レーサーのオブジェクトracer の時間軸の設定 | move() メソッドを実行するまでのサイクルが短いほどレーサーを素早く動かすことができます。 |
127行目 | 敵の出現頻度enemy_appearance_frequency | 数値が小さいほど敵の出現頻度が高くなります。 |
133行目、134行目 | 敵のオブジェクトnew_enemy の時間軸の設定 | move() メソッドを実行するまでのサイクルが短いほど敵が速くレーサーに接近してきます。 |
この5つの要素を調整することで、ゲームの難易度を変更することができます。色々と試してみて、自分好みの難易度になるようにプログラムを改良してみましょう。
チャプター6
おわりに
6. 1 このレッスンのまとめ
このレッスンでは、テーマ.7-3で学習した手法を使用して、より高度なゲームプログラムの作成に取り組みました。テーマ7を通してゲームの製作を行ってきましたが、シンプルなゲームでもこれだけ複雑なプログラムを書かなければならないことが分かったのではないかと思います。ゲーム製作のテーマはこれで終わりですが、ぜひ今度は自分自身でゲーム内容を考えて制作することに挑戦してみてください。
6. 2 次のレッスンについて
次のテーマからは、「ロボットアーム」の製作に取り組みます。複数のモーターの動きやセンサーを制御して、ものを識別して正確に運べるようなロボットアームを開発していきましょう。