Pythonロボティクスコース レッスン 28
テーマ.7-3 卓球ゲームを製作しよう
複数のオブジェクトを同時に制御しよう!
チャプター1
このレッスンで学ぶこと
ビデオカメラで撮影した動画やゲームの映像では、一定の間隔でフレーム(静止画やコマ)を差し替えることで滑らかな動きを表現しています。このレッスンでは、この「フレームを一定の間隔で差し替える」制御方法を参考に、複数のオブジェクトを共通の時間軸上で制御する手法を紹介します。また、レッスンの後半では、新たに学んだ手法を応用して、Studuino:bitのLEDディスプレイを使った卓球ゲームを製作します。
チャプター2
複数のオブジェクトを共通の時間軸上で制御する方法
Pythonの文法ではありませんが、今回は複数のオブジェクトを共通の時間軸(「タイムライン」とも呼ばれます)上で制御する手法について新たに学びます。
2. 1 ゲームにおける複数のオブジェクトの同時制御
市販されているゲームでは、当たり前のように同時に複数のキャラクターや背景などのオブジェクトが画面の中で動いています。しかし、実際にこういったゲームを作成するとなると、とても高度な技術が求められます。
まずは簡単な例として、次のケースを考えてみましょう。
ある村のマップに、村人として「Brian(ブライアン)」と「Anne(アン)」という2人のキャラクターを用意します。そして、Brianには3秒に1回の頻度で、Anneは6秒に1回の頻度で繰り返しメッセージを表示させるとしましょう。では、このプログラムはどのように作成すれば良いでしょうか。
ここでは、それぞれのキャラクターを用意するために、次の原型となるクラスを使います。
【 キャラクターの原型を定義したクラス 】
class Character():
def __init__(self, name): # コンストラクタ
self.name = name # キャラクター名
def speak(self): # キャラクタ―名とメッセージを表示するメソッド
print("{}:Hi!".format(self.name))
1つの方法としては、1秒ずつ時間の経過をカウントし、経過時間が3の倍数と6の倍数のときに、それぞれのキャラクターのオブジェクトからspeak()
メソッドを実行するやり方が考えられます。実際にこのコードを書いたものが次のプログラムになります。
【 サンプルコード 2-1-1 】
※ ある数の倍数になっているかどうかは、「%」演算子で割り算の余りを求め、その余りが0であることを調べることで判断できます。
import time
class Character():
def __init__(self, name):
self.name = name
def speak(self):
print("{}:Hi!".format(self.name))
Brian = Character("Brian")
Anne = Character("Anne")
count = 0 # カウンター。1秒経過するごとに1ずつ増やす
while True:
time.sleep_ms(1000) # 1秒が経過
count += 1 # 経過した時間をカウント
if count % 3 == 0: # 経過時間が3の倍数(3秒に1回)
Brian.speak()
if count % 6 == 0: # 経過時間が6の倍数(6秒に1回)
Anne.speak()
このプログラムのポイントは、「1秒毎に進む時間軸」を設定し、それに沿って2つのオブジェクトを制御しているところにあります。
これによって、さらにもう1人別のキャラクターを追加したい場合でも、少しの変更を加えるだけで済みます。下のプログラムは実際に5秒に1回の頻度でメッセージを表示するキャラクター「Thomas(トーマス)」を追加したものです。
【 サンプルコード 2-1-2 】
追加【12行目、22行目、23行目】
import time
class Character():
def __init__(self, name):
self.name = name
def speak(self):
print("{}:Hi!".format(self.name))
Brian = Character("Brian")
Anne = Character("Anne")
Thomas = Character("Thomas") # Thomasを新たに作成
count = 0
while True:
time.sleep_ms(1000)
count += 1
if count % 3 == 0:
Brian.speak()
if count % 6 == 0:
Anne.speak()
if count % 5 == 0: # 経過時間が5の倍数(5秒に1回)
Thomas.speak()
これは簡単な例でしたが、このように共通の時間軸に沿ってオブジェクトを制御することで、ゲーム全体の進行も管理することができます。
では、ここからはさらにもう少し高度な内容に踏み込んでいきます。【 サンプルコード 2-1-1 】では、1秒という単位で時間軸を進めていましたが、今度はビデオカメラの動画やゲームの映像でも使われている「フレームレート」を基準にしてオブジェクトを制御する方法を見ていきましょう。
2. 2 フレームレートとは?
ビデオカメラで撮影した動画やゲームの映像は、パラパラ漫画のようにいくつものフレーム(静止画やコマ)を高速で切り替えることで滑らかな動きを表現しています。このときのフレームを切り替える速さのことを「フレームレート」といいます。
フレームレートは、1秒あたりに表示するの静止画の枚数を指していて、「fps(frames per second) = フレーム毎秒」という単位で表されます。
例として、次のキャラクターが歩行する動画で考えてみましょう。この動画は4つの静止画を一定の間隔で切り替えることで、歩行しているように見せています。
この動画のフレームレートを変更すると、歩行する速さが変化します。
- 5fps(1秒あたりに5枚分の画像が切り替わる)
- 10fps(1秒あたりに10枚分の画像が切り替わる)
- 20fps(1秒あたりに20枚分の画像が切り替わる)
上の例では動きの速さを切り替えていますが、一般的にはより滑らかな動きを表現したいときに、フレームレートを高く設定します。ビデオカメラで撮影する場合は「30fps」と「60fps」の2つの設定が選べるようになっていることが多く、60fpsの方がより滑らかな映像となります。ただし、その分静止画の枚数が増える(2倍になる)ため、データ量は多くなります。
2. 3 フレームにまとめて複数のオブジェクトを制御する
ゲーム中のあるシーンを1つのフレームとして捉えみましょう。このフレームの中には、複数のオブジェクトが収まっています(下の画像では2人のキャラクター)。フレームは設定したフレームレートで次のフレームに切り替わり、オブジェクトはこのフレームの進行に合わせてあらかじめ登録された動作を行います。
このように、1つのフレームに複数のオブジェクトを収めて、共通の時間軸上で制御する手法がゲーム製作において使われることがあります。実際にこの手法を使って、【 サンプルコード 2-1-1 】と同じ動作を行うプログラムを書いてみましょう。
■ プログラムで用意するオブジェクト
このプログラムでは次のオブジェクトを作成します。
- フレームの進行を制御するオブジェクト
設定されたフレームレートに従ってフレームを進行させるオブジェクト - キャラクターのオブジェクト
2人のキャラクターを制御するオブジェクト
また、フレーム上で動作するキャラクターのオブジェクトには、フレームを進める時間軸とは別にそれぞれ独自の時間軸を持たせます。
この独自の時間軸はフレームの進行に合わせて進みます。そして、この独自の時間軸上において特定の位置に「イベント」として、動作(メソッド)を登録しています。こうすることで、フレームが進み、その位置に到達すると、登録した動作が行われるようになります。
また、この独自の時間軸が終点まで到達したあとは、また始点へ位置を戻すことで同じ動作を繰り返すこともできます。
では、これらのオブジェクトを作成するためのクラスを順番に定義して、プログラムを作成していきましょう。
■ フレームの進行を制御するクラスの定義
このクラスは、Ticker
という名前で定義します。Ticker
クラスには、次のプロパティとメソッドを用意します。
※ 「ticker」は英語で懐中時計のようにカチカチと鳴るものを表します。ここでは、カチカチと決まった間隔でフレームを次へと進めていく役割からこの名前を付けています。
【 Ticker
クラスのプロパティ 】
interval_time
プロパティ
次のフレームへ進めるまでの時間の間隔(単位はミリ秒)。フレームレート(fps)から計算。objs
プロパティ
フレーム上で制御するオブジェクトを格納するリスト。
【 Ticker
クラスのメソッド 】
register()
メソッドobjs
プロパティにオブジェクトを追加するための処理。ticks()
メソッドobjs
プロパティに格納されている各オブジェクトが持つ独自の時間軸を進めるメソッドを実行する処理。
それでは、順番にこれらのプロパティとメソッドを定義していきましょう。
まずは、Ticker
クラスを宣言し、コンストラクタ(__init__()
メソッド)を定義します。このコンストラクタでは、引数としてフレームレート(fps
)を受け取り、「1000/fps
」の計算式でフレームレートからフレーム間の時間の間隔(単位:ms)に変換し、inverval
プロパティに格納します。また、空のリストとしてobjs
プロパティを用意します。
※interval
プロパティは、time
モジュールのsleep_ms()
関数の引数として渡すため、round()
関数で計算結果を整数値に丸めています。
class Ticker:
def __init__(self, fps): # コンストラクタ
self.interval_time = round(1000/fps) # 単位はミリ秒
self.objs = [] # フレーム上で制御するオブジェクトを格納するリスト
次に、フレーム上で制御するオブジェクトを登録するregister()
メソッドを定義します。このメソッドは引数として複数(1個でも可)のオブジェクトを受け取り、objs
プロパティへリストのappend()
メソッドを使って順番に格納します。
追加【6行目~8行目】
class Ticker:
def __init__(self, fps):
self.interval_time = round(1000/fps)
self.objs = []
def register(self, *objs): # 複数のオブジェクトを引数として受け取るため、可変長な位置引数として定義
for obj in objs: # このobjsは「引数のobjs」であることに注意
self.objs.append(obj) # このobjsは「インスタンスのobjsプロパティ」であることに注意
最後にticks()
メソッドを定義します。このメソッドでは、あとでフレーム上のオブジェクトを作成するための原型となるスーパークラスTickObj
内で定義するticks()
メソッドを実行します。objs
プロパティに格納されているオブジェクト順番に取り出してticks()
メソッドを実行するコードを書きましょう。
追加【1行目、12行目~15行目】
import time
class Ticker:
def __init__(self, fps):
self.interval_time = round(1000/fps)
self.objs = []
def register(self, *objs):
for obj in objs:
self.objs.append(obj)
def ticks(self):
time.sleep_ms(self.interval_time) # 前にこのメソッドが実行されてから、フレームレートで設定された1フレーム分だけ間隔をあけてから下のコードを実行する
for obj in self.objs:
obj.ticks()
このメソッドはwhile
文を使った無限ループ内で繰り返し実行されることを想定しているため、time
モジュールのsleep_ms()
メソッドでinterval_time
プロパティの時間だけ実行の間隔を空けるようにしています。
■ フレーム上のオブジェクトに必要な機能をまとめたスーパークラスの定義
フレーム上で制御するオブジェクトには共通して必要なプロパティやメソッドがあります。そこで、それらをまとめたスーパークラスTickObj
を用意し、これを継承することで各オブジェクト用のクラスを定義します。
【 TickObj
クラスのプロパティ 】
position
プロパティ
オブジェクトがもつ時間軸上の現在の位置。初期値は「0」。end
プロパティ
時間軸の終点の位置。repeat
プロパティ
時間軸の終点に達したあとに先頭に戻り繰り返すかどうかを表すブール値。初期設定はTrue
。timeline
プロパティ
時間軸上の各位置で行うイベントをまとめた辞書。
【 TickObj
クラスのメソッド 】
add_event()
メソッド
時間軸上の位置を指定して、イベントをtimeline
プロパティに登録する処理。remove_event()
メソッドtimeline
プロパティから指定された位置のイベントを削除する処理。ticks()
メソッド
時間軸上の位置を次へ進める処理。reset_position()
メソッドposition
プロパティを「0」に戻す処理。
それでは順番にこれらのプロパティとメソッドを定義していきましょう。
まずは、TickObj
を宣言し、コンストラクタ(__init__()
メソッド)を定義します。
追加【18行目~23行目】
class TickObj:
def __init__(self, end, repeat=True):
self.position = 0 # 初期位置として0を格納
self.end = end # 時間軸の終点
self.repeat = repeat # 先頭へ戻り繰り返すかどうかを表すブール値
self.timeline = {n+1: [] for n in range(end)} # 時間軸上のイベントを管理する辞書
※ 上記の23行目のように内包表記を利用してコードを書くことで、timeline
プロパティには以下のような辞書が格納されます。
{1: [], 2: [], 3: [], ... , endの数値: []}
コンストラクタでは引数として、end
プロパティとrepeat
プロパティに格納する値を取ります。また、コンストラクタ内ではposition
プロパティは初期位置として0を格納します。そして、timeline
プロパティには、内包表記を使って時間軸上の1から終点までの位置をキーとして、要素に空のリストを持つ辞書を格納します。空のリストを持たせるのは、同じ位置に複数のイベントを登録できるようにするためです。
次に、add_event()
メソッドとremove_event()
メソッドを定義します。
add_event()
メソッドは引数として、イベント(メソッド)とそのイベントを行う時間軸上の位置を受け取り、timeline
プロパティへ格納します。
反対に、remove_event()
メソッドでは引数として、イベントを削除したい時間軸上の位置を受け取り、timeline
プロパティから要素を削除します。
追加【25行目、26行目、28行目、29行目】
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() # 指定した位置のイベントを全て削除
続けて、reset_position()
メソッドを定義します。このメソッドでは、position
プロパティを時間軸の先頭である0に戻します。
追加【31行目、32行目】
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 # 初期位置へ戻す
最後にticks()
メソッドを定義します。このメソッドでは、最初にposition
プロパティの数値を1増やし時間軸上の位置を1つ先へ進めます。そして、その位置に登録されているイベントを順番に実行します。さらに、もしrepeat
プロパティがTrue
でかつ位置が終点に達していれば、reset_position()
メソッドを実行して、初期位置に戻します。
追加【34行目~40行目】
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() # event にはメソッドのオブジェクトが格納されているため、()を付けることで実行できる。
if self.repeat and self.position == self.end:
self.reset_position() # 繰り返しの設定になっていてかつ終点に達していれば、初期位置へ戻す
これで、TickObj
クラスの定義ができました。早速このクラスを継承してキャラクターのクラスを定義しましょう。
■ キャラクターのオブジェクト作成用クラスの定義
キャラクターのオブジェクトの原型となるクラスをCharacter
として定義します。このクラスは、上で定義したTickObj
クラスを継承します。そして、【 サンプルコード 2-1-1 】と同じようにキャラクターの名前を格納するname
プロパティと、メッセージを表示するspeak()
メソッドを新たに追加します。
はじめに、name
プロパティを追加するために、コンストラクタの__init__()
メソッドをオーバーライドして、新たな引数として付け加えます。内部でsuper()
関数を使いスーパークラスの__init__()
メソッドを実行するようにしましょう。speak()
メソッドはスーパークラスにはありませんので、【 サンプルコード 2-1-1 】と同じコードをそのまま流用します。
追加【42行目~48行目】
class Character(TickObj):
def __init__(self, name, end, repeat=True):
super().__init__(end, repeat=repeat)
self.name = name
def speak(self):
print("{} speaks.".format(self.name))
■ 各オブジェクトの作成とメソッドの実行
最後に、ここまでで用意した各クラスを使用してオブジェクトを作成し、プログラムを完成させます。まずは、Ticker
クラスのオブジェクトを作成します。ここでは、【 サンプルコード 2-1-1 】に合わせてfpsを「1」(1秒で1フレーム進む)に設定しましょう。
追加【50行目】
ticker = Ticker(1)
次に2人のキャラクターのオブジェクトを作成します。そして、それぞれのオブジェクトにadd_event()
メソッドで、以下の図で示す位置へspeak()
メソッドをイベントとして登録します。
追加【51行目~54行目】
※ メソッドを引数として渡すときは、()
を付けないことに注意してください。
ticker = Ticker(1)
Brian = Character("Brian", 3) # 時間軸の終点は3
Anne = Character("Anne", 6) # 時間軸の終点は6
Brian.add_event(3, Brian.speak) # 3の位置に自身のspeak()メソッドを登録
Anne.add_event(6, Anne.speak) # 6の位置に自身のspeak()メソッドを登録
最後に50行目で作成したticker
オブジェクトのregister()
メソッドで2人のキャラクターのオブジェクトを登録します。これですべての準備ができたので、あとはwhile
文を使った無限ループ内で繰り返しticker
オブジェクトのticks()
メソッドを実行することで、フレームの進行に合わせてキャラクターのオブジェクトが制御されます。
追加【56行目~58行目】
ticker = Ticker(1)
Brian = Character("Brian", 3)
Anne = Character("Anne", 6)
Brian.add_event(3, Brian.speak)
Anne.add_event(6, Anne.speak)
ticker.register(Brian, Anne) # キャラクターのオブジェクトをフレームへ追加
while True:
ticker.ticks() # フレームを次へ進める
完成したプログラムが以下になります。自分の書いたコードを見比べて、誤りがないかを確認し、実際にプログラムを実行してみましょう。【 サンプルコード 2-1-1 】と同じ結果になれば成功です。
【 サンプルコード 2-3-1 】
import time
class Ticker: # フレームの進行を管理するクラス
def __init__(self, fps):
self.fps = fps # フレームレート
self.ticks_time_ms = round(1000/fps) # フレーム間の間隔(単位:ms)
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() # TickObjクラスのticks()メソッド
class TickObj: # フレーム上で制御するオブジェクトのスーパークラス
def __init__(self, end, repeat=True):
self.position = 0 # オブジェクトが独自に持つ時間軸上の現在位置。初期位置は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): # 初期位置の0へ戻す
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() # 繰り返しの設定になっていて、かつ終点に達している場合は先頭へ戻る
class Character(TickObj): # TickObjを継承したキャラクターの原型となるクラス
def __init__(self, name, end, repeat=True): # スーパークラスのコンストラクタをオーバーライド
super().__init__(end, repeat=repeat) # スーパークラスのコンストラクタを実行
self.name = name # 新たに追加したプロパティを定義
def speak(self):
print("{} speaks.".format(self.name))
ticker = Ticker(1)
Brian = Character("Brian", 3) # 終点が「3」の時間軸を設定
Anne = Character("Anne", 6) # 終点が「6」の時間軸を設定
Brian.add_event(3, Brian.speak) # speakメソッドを「3」の位置に登録
Anne.add_event(6, Anne.speak) # speakメソッドを「6」の位置に登録
ticker.register(Brian, Anne) # キャタクターのオブジェクトを登録
while True:
ticker.ticks()
【 サンプルコード 2-3-1 】は、【 サンプルコード 2-1-1 】と比べると、複雑なコードになっています。そのため、なぜ同じ動作をさせているだけなのに、ここまで複雑なコードを書かなければいけないのかと疑問に思うことでしょう。しかし、より多くの処理を行わなければならないゲームのプログラムを書くと、この手法が力を発揮します。そこで実際に、次のチャプターでは【 サンプルコード 2-3-1 】を流用して卓球ゲームを製作し、その実力を確認してみましょう。
チャプター3
卓球ゲームの製作
このチャプターでは、卓球ゲームを製作します。まずは下のプレイ動画を見て、ゲームの動作を確認しましょう。
【 卓球ゲームのプレイ動画 】
3. 1 ゲームの遊び方
このゲームは2人で行います。プレイヤーはボタンA側とボタンB側に分かれ、飛んでくるボールに合わせてタイミング良くボタンを押します。ボタンを押すとラケットが表示され、ラケットがボールと重なると、相手側に打ち返すことができます。勝敗は、先にボールを打ち返せなかったプレイヤーの負けとなります。
3. 2 ゲームに必要なオブジェクトとクラスの確認
では次に、このゲームの製作で必要なオブジェクトと、そのオブジェクトを作成するためのクラスを確認しましょう。
【 ゲームに必要なオブジェクト 】
変数名 | 説明 |
---|---|
ball | ボール |
racket_a | ボタンA側のラケット |
racket_b | ボタンB側のラケット |
ticker | 【 サンプルコード 2-3-1 】で作成したTicker クラスのオブジェクト |
【 オブジェクト作成に必要なクラス 】
クラス名 | 説明 |
---|---|
Ball | ボールのオブジェクト(ball )を作成するためのクラス。 【 サンプルコード 2-3-1 】で作成したTickObj クラスを継承 |
Racket | ラケットのオブジェクト(racket_a 、racket_b )を作成するためのクラス。 【 サンプルコード 2-3-1 】で作成したTickObj クラスを継承 |
Racket
クラスはボールA側とボールB側のオブジェクトを作成するため、引数としてボタン名の"A"
または"B"
を受け取るようにします。
3. 3 2つのクラスの定義
では、【 サンプルコード 2-3-1 】で作成したクラスとTickObj
クラスを使用して2つのクラスBall
とRacket
を先に作成していきましょう。
準備として、新しいプログラムファイルを作成して、【 サンプルコード 2-3-1 】から以下の40行目までを複製して貼り付けましょう。
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()
■ Ball
クラスの定義
Ball
クラスはTickObj
クラスを継承して、以下のプロパティとメソッドを追加します。
【 追加するプロパティ 】
プロパティ名 | 内容 |
---|---|
x | LEDディスプレイ上のボールの位置を表すX座標の値 |
y | LEDディスプレイ上のボールの位置を表すY座標の値 |
image | ボールをLEDディスプレイに表示するためのイメージ (※StuduinoBitImageクラスのインスタンス) |
color | ボールの表示色を表すタプル |
direction | 現在ボールが飛んでいる方向を表す変数 |
RIGHT 、LEFT | ボールが飛ぶ方向を表す定数(クラスメンバ変数) |
【 追加またはオーバーライドするメソッド 】
メソッド名 | 内容 |
---|---|
__init__() | ※ スーパークラスからオーバーライド。 プロパティの追加やadd_event() メソッドでイベントの登録をする処理 |
move() | direction プロパティにもとづいてボールの位置を横方向(LEDディスプレイのX軸方向)に移動する処理 |
change_direction() | ボールの飛ぶ方向を反対向きに変える処理 |
get_image() | ボールを表示するためのイメージを返す処理 |
reset() | 各プロパティを初期値に戻す処理 |
そして、Ball
クラスには次の図で表す時間軸を設定します。また、ボールの移動は繰り返し行うため、TickObj
クラスから継承したrepeat
プロパティはTrue
を設定します。
【 Ball
クラスの時間軸 】
では順番にコードを書いていきましょう。まずは、RIGHT
プロパティとLEFT
プロパティを定義します。この2つはボールの飛ぶ方向を表すための定数(クラスメンバ変数)で、"right"
や"left"
のような文字列の代わりとして使用します。
追加【42行目~44行目】
class Ball(TickObj):
RIGHT = 1
LEFT = 2
次に、__init__()
メソッドをオーバーライドして、各プロパティの初期値を設定します。
まず、ボールが飛ぶ方向として、direction
プロパティはLEFT
変数とRIGHT
変数のいずれかをランダムに選び設定します。また、ボールをLEDディスプレイに表示させるためのイメージも用意する必要があります。そのため、先頭でrandom
モジュールとStuduinoBitImage
(Image
)クラスをインポートしましょう。
追加【2行目、3行目】
import time
import random
from pystubit.board import Image
そして、__init__()
メソッドを定義して、それぞれプロパティに次のように値を設定しましょう。また、あとで定義するmove()
メソッドを時間軸の6の位置に登録する処理もここに書いておきます。
追加【48行目~55行目】
class Ball(TickObj):
RIGHT = 1
LEFT = 2
def __init__(self):
super().__init__(6) # 終点は6の位置。repeat引数は初期値がTrueのため省略。
self.x = 2 # ゲーム開始時のボールの位置は、LEDディスプレイの中心点(2, 2)
self.y = 2 self.image = Image(5, 5) # 5×5マスの空のイメージを作成
self.color = (31, 20, 0) # オレンジ色
self.direction = random.choice((self.RIGHT, self.LEFT)) # 最初に飛ぶ方向をランダムに決定
self.add_event(6, self.move) # 時間軸上の6の位置にmove()メソッドを登録
続けて、ボールの位置を変化させるmove()
メソッドを定義します。飛ぶ向きがRIGHT
の場合は、x
の値を1増やし、反対にLEFT
の場合は、x
の値を1減らします。
追加【57行目~61行目】
def __init__(self):
super().__init__(6)
self.x = 2
self.y = 2
self.image = Image(5, 5)
self.color = (31, 20, 0)
self.direction = random.choice((self.RIGHT, self.LEFT))
self.add_event(6, self.move)
def move(self):
if self.direction == self.RIGHT:
self.x += 1 # LEDディスプレイの右側へボールを移動する
else:
self.x -= 1 # LEDディスプレイの左側へボールを移動する
さらに続けて、ボールの飛ぶ方向を変えるchange_direction()
メソッドを定義します。現在の方向がRIGHT
の場合はLEFT
を、LEFT
の場合はRIGHT
に変更します。また、方向を変更したあとは、reset_position()
メソッドを実行して、時間軸上の位置を「0」に戻しておきましょう。
追加【63行目~68行目】
def move(self):
if self.direction == self.RIGHT:
self.x += 1
else:
self.x -= 1
def change_direction(self): # 飛ぶ方向を変更するメソッド
if self.direction == self.RIGHT:
self.direction = self.LEFT
else:
self.direction = self.RIGHT
self.reset_position() # このメソッドはスーパークラスの「TickObj」で定義済み
今度はLEDディスプレイ上に表示するためのイメージを取得するget_image()
メソッドを定義します。このメソッドでは、image
プロパティのイメージを一度リセットして、現在のボール位置のLEDのみをcolor
プロパティの色で点灯するように設定します。そして、この設定を行ったイメージを戻り値として返します。
追加【70行目~75行目】
def change_direction(self):
if self.direction == self.RIGHT:
self.direction = self.LEFT
else:
self.direction = self.RIGHT
self.reset_position()
def get_image(self):
for x in range(0, 5): # ボールが移動する行のLEDをすべて消灯に変更する
self.image.set_pixel(x, self.y, 0)
if self.x >= 0 and self.x <= 4: # 現在のボールが存在している位置のLEDを点灯に設定する
self.image.set_pixel_color(self.x, self.y, self.color)
return self.image
最後に、各プロパティを初期値に戻すreset()
メソッドを定義します。このメソッドは、ゲームを再プレイするときに呼び出します。次のようにコードを書きましょう。
追加【77行目~80行目】
def get_image(self):
for x in range(0, 5):
self.image.set_pixel(x, self.y, 0)
if self.x >= 0 and self.x <= 4:
self.image.set_pixel_color(self.x, self.y, self.color)
return self.image
def reset(self):
self.x = 2 # ボールの位置をLEDディスプレイの中心に戻す
self.direction = random.choice((self.RIGHT, self.LEFT)) # 再度飛ぶ方向をランダムに選び設定する
self.reset_position() # 時間軸上の位置を「0」に戻す
■ Racket
クラスの定義
次はRacket
クラスを定義します。このクラスもTickObj
を継承します。そして、以下のプロパティとメソッドを追加します。
【 追加するプロパティ 】
プロパティ名 | 内容 |
---|---|
image | ラケットをLEDディスプレイに表示するためのイメージ。 (StuduinoBitImageクラスのインスタンス) |
is_valid | ラケットをLEDディスプレイへ表示するかどうかを表すブール値。 表示を有効にする場合はTrue を、無効にする場合はFalse を格納します。 |
※ 「valid」は英語で「有効な」の意味を表します。反対に、「invalid」は「無効な」の意味を表します。
【 追加またはオーバーライドするメソッド 】
メソッド名 | 内容 |
---|---|
__init__() | スーパークラスからオーバーライド。 プロパティの追加やadd_event() メソッドでイベントの登録をする処理 |
swing() | ラケットを表示するために時間軸の進行を再開する処理 |
valid() | ラケットの表示を有効にする処理 |
invalid() | ラケットの表示を無効にする処理 |
get_image() | ラケットを表示するためのイメージを返す処理 |
reset() | 各プロパティを初期値に戻す処理 |
そして、Rakcet
クラスには次の図で表す時間軸を設定します。ラケットはプレイヤーがボタンを押したときに表示を1度だけ行うため、repeat
プロパティはFalse
を設定します。また、valid()
メソッドが実行されて、invalid()
メソッドが実行されるまでの間だけLEDディスプレイ上に表示します。さらに、位置が時間軸上の終点に到達するまではボタンを押しても再表示されないようにします。
【 Racket
クラスの時間軸 】
では順番にコードを書いていきましょう。まずは、__init__()
メソッドを定義します。Racket
クラスからはボタンA側とボタンB側の2つのラケットのオブジェクトを作成するため、引数としてボタンの名前("A"
または"B"
)を受け取ります。
また、初期位置は「0」ではなく、終点を設定します。これによって、時間軸の進行が停止の状態でスタートさせることができます。もし初期位置を「0」にした場合、プログラムの実行直後に必ずラケットが表示されてしまいます。
追加【82行目~92行目】
class Racket(TickObj):
def __init__(self, button):
super().__init__(15, repeat=False)
if button == "A": # ボタンA側に表示する青色のラケットのイメージを作成
self.image = Image("10000:10000:10000:10000:10000:", color=(0, 31, 0))
else: # ボタンB側に表示する緑色のラケットのイメージを作成
self.image = Image("00001:00001:00001:00001:00001:", color=(0, 0, 31))
self.is_valid = False # 最初は非表示
self.position = self.end # 初期位置を「0」ではなく終点にすることで、時間軸の進行が停止した状態でスタート
self.add_event(1, self.valid) # イベントの登録
self.add_event(5, self.invalid) # イベントの登録
次に、ラケットを表示するために時間軸の進行を再開するswing()
メソッドを定義します。このメソッドはposition
プロパティの値が終点を表すend
プロパティと等しいときだけreset_position()
メソッドで時間軸の位置を「0」に戻します。TickObj
のticks()
メソッドの定義の通り、時間軸は位置が終点でなければ、自動的に1ずつ進行するようになっています。
追加【94行目~96行目】
class Racket(TickObj):
def __init__(self, button):
super().__init__(15, repeat=False)
if pos == "A":
self.image = Image("10000:10000:10000:10000:10000:", color=(0, 31, 0))
else:
self.image = Image("00001:00001:00001:00001:00001:", color=(0, 0, 31))
self.is_valid = False
self.position = self.end
self.add_event(1, self.valid)
self.add_event(5, self.invalid)
def swing(self):
if self.position == self.end: # 位置が終点でない場合は、ボタンを押してもラケットを表示しない
self.reset_position()
続けて、ラケットの表示が有効か無効か切り替えるためのvalid()
メソッドとinvalid()
メソッドを定義します。以下のようにコードを書きましょう。
追加【98行目~102行目】
def swing(self):
if self.position == self.end:
self.reset_position()
def valid(self):
self.is_valid = True
def invalid(self):
self.is_valid = False
今度は、ラケットのイメージを返すget_image()
メソッドを定義します。以下のようにコードを書きましょう。
追加【104行目、105行目】
def invalid(self):
self.is_valid = False
def get_image(self):
return self.image
最後に、各プロパティを初期値に戻すreset()
メソッドを次のように定義しましょう。
追加【107行目~109行目】
def get_image(self):
return self.image
def reset(self):
self.is_valid = False
self.position = self.end
3. 4 ゲームのメインとなる処理の作成
ここまででオブジェクトの作成に必要なクラスがすべて定義できました。ここからは、プログラムのメイン処理を書いていきます。また、プログラムの見通しを良くするため、以下の関数を用意してまとめていきます。
関数名 | 内容 |
---|---|
main() | メイン処理全体をまとめた(囲んだ)関数 |
reset() | ゲームを初期状態にリセットする処理をまとめた関数 |
start_game() | ゲームを1回プレイする処理をまとめた関数 |
reset()
関数とstart_game()
関数は、main()
関数の中で定義します。では、これらの関数を組み合わせた全体の処理がどのような流れになるのかを下の図で確認しましょう。
【 プログラム全体の処理の流れ 】
ゲーム開始時に行うカウントダウンや、失敗したときの音は、テーマ.7-1から使用しているsound
モジュールを利用します。もし、Studuino:bit上から削除している場合は、以下のリンク先からダウンロードして、もう一度Studuino:bitに保存しましょう。
※ リンクの上にカーソルを合わせて右クリックし、「名前を付けてリンク先を保存」を選択してください。
それでは、順を追って作成していきましょう。
■ main()
関数の定義
まずは、main()
関数から定義していきます。その前にプログラムの先頭で使用するdisplay
、button_a
、button_b
の3つのオブジェクトとsound
モジュールをインポートしましょう。
追加・変更【3行目、4行目】
import time
import random
from pystubit.board import display, button_a, button_b, Image
import sound
次に定数FPS
として、このゲームのフレームレートを設定します。この値はあとで変更することもできますので、ここでは一旦「60fps」としておきましょう。
追加【6行目】
import time
import random
from pystubit.board import button_a, button_b, display, Image
import sound
FPS = 60 class Ticker:
では、main()
関数を定義します。用意したクラスからticker
、racket_a
、racket_b
、ball
の4つのオブジェクトを作成します。そして、ticker
オブジェクトのregister()
メソッドでフレームの時間軸上に3つのオブジェクトを登録しましょう。
追加【114行目~119行目】 最後に追加
def main():
ticker = Ticker(FPS)
racket_a = Racket("A")
racket_b = Racket("B")
ball = Ball()
ticker.register(racket_a, racket_b, ball)
ゲームはボタンAとボタンBの両方を押すと開始になります。while
文を使った無限ループ内で両方のボタンが押されているかどうかを調べ、両方とも押された場合は順番に関数を実行するようにします。
※ start_game()
関数とreset()
関数はあとで定義します。
追加【121行目~125行目】
def main():
ticker = Ticker(FPS)
racket_a = Racket("A")
racket_b = Racket("B")
ball = Ball()
ticker.register(racket_a, racket_b, ball)
while True:
if button_a.is_pressed() and button_b.is_pressed():
sound.countdown() # カウントダウン
start_game() # ゲームの開始
reset() # ゲーム終了後はリセット(※次で定義)
■ reset()
関数の定義
reset()
関数はmain()
関数の外からは呼び出すことがないため、main()
関数の中で定義します。reset()
関数では、racket_a
、racket_b
、ball
の3つのオブジェクトのreset()
メソッドを実行します。以下のようにコードを書きましょう。
追加挿入【121行目~124行目】
def main():
ticker = Ticker(FPS)
racket_a = Racket("A")
racket_b = Racket("B")
ball = Ball()
ticker.register(racket_a, racket_b, ball
def reset():
racket_a.reset()
racket_b.reset()
ball.reset()
while True:
if button_a.is_pressed() and button_b.is_pressed():
sound.countdown()
start_game()
reset()
■ start_game()
関数の定義
start_game()
関数も同じくmain()
関数の外からは呼び出すことがないため、main()
関数の中で定義します。まずは、フレームを次々に進めるためのコードを書きましょう。
追加挿入【126行目~128行目】
def main():
ticker = Ticker(FPS)
racket_a = Racket("A")
racket_b = Racket("B")
ball = Ball()
ticker.register(racket_a, racket_b, ball)
def reset():
racket_a.reset()
racket_b.reset()
ball.reset()
def start_game():
while True:
ticker.ticks() # フレームを進める
while True:
if button_a.is_pressed() and button_b.is_pressed():
sound.countdown()
start_game()
reset()
次にボタンAとボタンBがそれぞれ押された場合に、swing()
メソッドを実行します。これで時間軸の位置が終点に達しているときだけ位置が「0」に戻り、ラケットの表示が行われます。
追加【130行目~133行目】
def start_game():
while True:
ticker.ticks()
if button_a.is_pressed():
racket_a.swing() # positonプロパティの値を「0」にしてタイムラインを進める
if button_b.is_pressed():
racket_b.swing() # positonプロパティの値を「0」にしてタイムラインを進める
続けて、LEDディスプレイ上に表示するイメージを用意します。まずは、ball
オブジェクトからボールのイメージを取得し変数screen
に格納します。
追加【135行目】
if button_a.is_pressed():
racket_a.swing()
if button_b.is_pressed():
racket_b.swing()
screen = ball.get_image()
さらに、StuduinoBitImage
でオーバーライドしている+
演算子の処理で、racket_a
やracket_b
から得られるラケットのイメージを合成していきます。ただし、このイメージの合成はis_valid
プロパティがTrue
に設定されているときのみ行います。そして、合成が完了したイメージはLEDディスプレイに表示します。
追加【136行目~140行目】
if button_a.is_pressed():
racket_a.swing()
if button_b.is_pressed():
racket_b.swing()
screen = ball.get_image()
if racket_a.is_valid: # 表示が有効な場合のみイメージを合成
screen += racket_a.get_image()
if racket_b.is_valid:
screen += racket_b.get_image()
display.show(screen, delay=0) # 合成が完了したイメージを表示
また、ラケットのイメージとボールのイメージに重なりがあるときは、ball
オブジェクトのchange_direction()
メソッドを実行してボールの飛ぶ方向を変更します。ただし、ボールが飛んできている方向も加味する必要があり、ボタンA側のラケットはボールが左方向へ飛んでいるときだけ、ボタンB側はボールが右方向へ飛んでいるときだけ、この変更を行います。
追加【138行目、139行目、142行目、143行目】
screen = ball.get_image()
if racket_a.is_valid:
screen += racket_a.get_image()
if ball.x == 0 and ball.direction == Ball.LEFT: # ボールのx座標の値が0のときにA側のラケットと重なりがある
ball.change_direction()
if racket_b.is_valid:
screen += racket_b.get_image()
if ball.x == 4 and ball.direction == Ball.RIGHT: # ボールのx座標の値が4のときにB側のラケットと重なりがある
ball.change_direction()
display.show(screen, delay=0)
最後に、ボールがLEDディスプレイの外に出てしまっているかどうかを判定します。LEDディスプレイの右側(x > 4
)に出た場合はボタンA側のプレイヤーの勝利となり、左側(x < 0
)に出た場合はボタンB側のプレイヤーの勝利となります。その勝者をLEDディスプレイに表示しましょう。また、これで1回のゲームが終わりとなるため、break
文で無限ループを抜けます。
追加【146行目~150行目】
screen = ball.get_image()
if racket_a.is_valid:
screen += racket_a.get_image()
if ball.x == 0 and ball.direction == Ball.LEFT:
ball.change_direction()
if racket_b.is_valid:
screen += racket_b.get_image()
if ball.x == 4 and ball.direction == Ball.RIGHT:
ball.change_direction()
display.show(screen, delay=0)
if ball.x < 0 or ball.x > 4: # LEDディスプレイの外側にボールが出た場合
sound.failed() # soundモジュールの失敗音を鳴らす
winner = "A" if ball.x > 4 else "B" # 条件文でコードを簡略化
display.scroll("{} wins.".format(winner), delay=30)
break # 127行目のwhile文のループを抜ける
一連の処理を開始するために、main()
関数を関数の外側で実行しましょう。これでプログラムの完成です。
追加【159行目】
while True:
if button_a.is_pressed() and button_b.is_pressed():
sound.countdown()
start_game()
reset()
main()
長い道のりで大変だったと思いますが、チャプター2で紹介した制御方法を使わなければ、もっとコードが複雑なものになっていたでしょう。では、最後に自分の書いたコードと以下のサンプルコードを見比べて誤りがないかを確認して、プログラムを実行しましょう。
【 サンプルコード 3-4-1 】
import time
import random
from pystubit.board import button_a, button_b, display, Image
import sound
FPS = 60 # フレームレート。1秒間で60フレーム切り替わる。
class Ticker: # 【 サンプルコード 2-3-1 】と同様
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: # 【 サンプルコード 2-3-1 】と同様
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()
class Ball(TickObj): # ボールのオブジェクトのためのクラス
RIGHT = 1 # ボールの移動方向を表す定数
LEFT = 2
def __init__(self):
super().__init__(6) # 時間軸の終点は「6」
self.x = 2 # ボールの初期位置の座標は(2,2)
self.y = 2
self.image = Image(5, 5) # 空のイメージ
self.color = (31, 20, 0) # オレンジ色
self.direction = random.choice((self.RIGHT, self.LEFT)) # 最初に飛ぶ方向はランダムに設定
self.add_event(6, self.move) # 時間軸の「6」の位置にイベントを追加
def move(self): # 現在の移動方向へX座標を変化させる
if self.direction == self.RIGHT:
self.x += 1
else:
self.x -= 1
def change_direction(self): # ボールの移動方向を現在とは別の方向へ変える
if self.direction == self.RIGHT:
self.direction = self.LEFT
else:
self.direction = self.RIGHT
self.reset_position() # 変更後は時間軸の位置をリセットする
def get_image(self): # ボールを表示するためのイメージを返す
for x in range(0, 5): # 一度イメージを空にする
self.image.set_pixel(x, self.y, 0)
if self.x >= 0 and self.x <= 4: # 現在のボールの位置にあるLEDを点灯に設定
self.image.set_pixel_color(self.x, self.y, self.color)
return self.image
def reset(self): # ゲームを再プレイする前の設定リセット用
self.x = 2
self.direction = random.choice((self.RIGHT, self.LEFT))
self.reset_position()
class Racket(TickObj): # ラケットのオブジェクトのためのクラス
def __init__(self, button):
super().__init__(15, repeat=False) # 時間軸の終点は「15」。繰り返しは「なし」。
if button == "A": # 使用するボタンによって表示イメージを変える
self.image = Image("10000:10000:10000:10000:10000:", color=(0, 31, 0))
else:
self.image = Image("00001:00001:00001:00001:00001:", color=(0, 0, 31))
self.is_valid = False # 最初は表示設定を無効にしておく
self.position = self.end # 時間軸が自動的に進まないように初期位置を終点に設定
self.add_event(1, self.valid) # 時間軸の「1」の位置へイベントを登録
self.add_event(5, self.invalid) # 時間軸の「5」の位置へイベントを登録
def swing(self): # 時間軸の位置を0にして進行するように変える
if self.position == self.end:
self.reset_position()
def valid(self): # 表示設定を有効にする
self.is_valid = True
def invalid(self): # 表示設定を無効にする
self.is_valid = False
def get_image(self):
return self.image
def reset(self): # ゲームを再プレイする前の設定リセット用
self.is_valid = False
self.position = self.end
def main(): # ゲームのメイン処理
ticker = Ticker(FPS)
racket_a = Racket("A")
racket_b = Racket("B")
ball = Ball()
ticker.register(racket_a, racket_b, ball)
def reset(): # 再プレイする前の設定のリセット
racket_a.reset()
racket_b.reset()
ball.reset()
def start_game(): # ゲーム中の処理
while True:
ticker.ticks() # 次のフレームへ進める
if button_a.is_pressed(): # ボタンを押すとラケットを振る
racket_a.swing()
if button_b.is_pressed():
racket_b.swing()
screen = ball.get_image() # LEDディスプレイに表示するイメージの作成
if racket_a.is_valid: # 表示設定が有効な場合のみイメージを合成
screen += racket_a.get_image()
if ball.x == 0 and ball.direction == Ball.LEFT:
ball.change_direction() # ボールと重なっている場合は打ち返す
if racket_b.is_valid: # 表示設定が有効な場合のみイメージを合成
screen += racket_b.get_image()
if ball.x == 4 and ball.direction == Ball.RIGHT:
ball.change_direction() # ボールと重なっている場合は打ち返す
display.show(screen, delay=0) # 作成したイメージの表示
if ball.x < 0 or ball.x > 4: # LEDディスプレイの外側にボールが出た場合は勝敗を判定する
sound.failed() # soundモジュールの失敗音
winner = "A" if ball.x > 4 else "B" # 勝者をLEDディスプレイに表示
display.scroll("{} wins.".format(winner), delay=30)
break # 127行目の無限ループを抜ける
while True: # ボタンAとボタンBの同時押しでゲームをスタート
if button_a.is_pressed() and button_b.is_pressed():
sound.countdown()
start_game()
reset()
main()
3. 5 フレームレートを変更する
【 サンプルコード 3-4-1 】では、フレームレートを「60fps」に設定していましたが、この値を変更すると、ゲームの進行の速さを切り替えることができます。例えば90fpsに変更すると、ボールが少し早く移動するようになります。30fpsに変更すると、反対にボールが少し遅く移動するようになります。
- フレームレートを
90fps
に設定 ➡ ボールの移動が早くなる
import time
import random
from pystubit.board import button_a, button_b, display, Image
import sound
FPS = 90 # 90fps = 11msで1フレーム進む
- フレームレートを
30fps
に設定 ➡ ボールの移動が遅くなる
import time
import random
from pystubit.board import button_a, button_b, display, Image
import sound
FPS = 30 # 30fps = 33msで1フレーム進む
数値上では、30fpsと90fpsで比べると90fpsの方が3倍の速さでボールが移動する設定になっていますが、実際にはそうはなりません。これはStuduino:bitのLEDディスプレイの表示処理に時間が掛かるためです。そこで次のチャプターでは、フレームレートを変えずにボールの移動速度を2倍にする課題に取り組んでみましょう。
チャプター4
課題:ボールが移動する速さを一定の確率で変える
【 サンプルコード 3-4-1 】でもゲームとして楽しむことはできますが、ずっと同じ速さでボールが飛び交うので、どこか単調になってしまい、やや盛り上がりに欠けています。そこで、この課題では、打ち返したときに20%の確率でボールが2倍の速さで移動するように機能を追加して、もっと盛り上がるゲームに改良してみましょう。
【 機能を追加した卓球ゲームのプレイ動画 】
4. 1 プログラムの作成例
この課題はやや難しいので、以下の内容を参考にしてコードを書いてみましょう。
■ ボールを2倍の速さで飛ばすには?
ボールの速さは時間軸上の別の位置にmove()
メソッドを登録するだけで簡単に変えることができます。例えば「3」の位置に追加すると、2倍の速さになります。
反対に2倍の速さにした状態から元の速さに戻すときは、「3」の位置のイベントを削除します。
この時間軸の「3」の位置へのmove()
メソッドの登録と削除を繰り返し行うことで、ゲーム中のボールの速さを変化させます。
■ 20%の確率でボールを速く飛ばすプログラム
では実際に、方向が変わるときに20%の確率でボールが速く移動するプログラムへと変更していきましょう。
まずは、定数としてボールの移動速度を表すSLOW
とFAST
をBall
オブジェクトのコードの先頭で定義します。
追加【50行目、51行目】
class Ball(TickObj):
RIGHT = 1
LEFT = 2
SLOW = 3 # ボールが遅く移動(通常の速さ)
FAST = 4 # ボールが速く移動
ボールに「速さ」の情報を追加するため、新たにspeed
プロパティを用意します。__init__()
メソッドで初期値としてSLOW
を設定しましょう。
追加【60行目】
def __init__(self):
super().__init__(6)
self.x = 2
self.y = 2
self.image = Image(5, 5)
self.color = (31, 20, 0)
self.direction = random.choice((self.RIGHT, self.LEFT))
self.speed = self.SLOW # 最初は遅く設定
self.add_event(6, self.move)
次に速さを変更するためのメソッドをchange_speed()
として定義します。このメソッドでは、引数に速さ(SLOW
またはFAST
)を受け取り、現在設定されている速さから変更があれば、時間軸上にmove()
メソッドを登録したり、削除したりする処理を行います。
追加【70行目~76行目】
def change_direction(self):
if self.direction == self.RIGHT:
self.direction = self.LEFT
else:
self.direction = self.RIGHT
self.reset_position()
def change_speed(self, speed):
if self.speed != speed: # 今の速さから変更があった場合
if speed == self.SLOW: # 「速い」から「遅い」へ変更されるとき
self.remove_event(3) # イベントを削除
else: # 「遅い」から「早い」へ変更されるとき
self.add_event(3, self.move) # イベントを登録
self.speed = speed # 新しく設定された速さをプロパティに格納
ボールの速さは、移動方向を変えるchange_direction()
メソッド内で変更します。このとき「20%の確率」を設定するために、random
モジュールのrandint()
メソッドを利用します。randint()
メソッドで「1~5」の範囲から数字をランダムに選び、その数字が3の場合のみ「速く移動する」設定にし、それ以外の数字では「遅く移動する」設定にします。
def change_direction(self):
if self.direction == self.RIGHT:
self.direction = self.LEFT
else:
self.direction = self.RIGHT
self.reset_position()
if random.randint(1, 5) == 3: # 5つ中1つだけなので、確率は20%となります。
self.change_speed(self.FAST) # 速く移動する設定へ
else:
self.change_speed(self.SLOW) # 遅く移動する設定へ
これでプログラムの変更が完了しました。以下が完成したサンプルプログラムです。プログラムを実行してうまくいかなかった場合は、こちらのBall
クラスの定義と見比べて誤りがないかを確認してください。
【 サンプルコード 4-1-1 】
import time
import random
from pystubit.board import button_a, button_b, display, Image
import sound
FPS = 60
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()
class Ball(TickObj): # この行以下のBallクラスの定義を確認
RIGHT = 1
LEFT = 2
SLOW = 3
FAST = 4
def __init__(self):
super().__init__(6)
self.x = 2
self.y = 2
self.image = Image(5, 5)
self.color = (31, 20, 0)
self.direction = random.choice((self.RIGHT, self.LEFT))
self.speed = self.SLOW
self.add_event(6, self.move)
def move(self):
if self.direction == self.RIGHT:
self.x += 1
else:
self.x -= 1
def change_direction(self):
if self.direction == self.RIGHT:
self.direction = self.LEFT
else:
self.direction = self.RIGHT
self.reset_position()
if random.randint(1, 5) == 3:
self.change_speed(self.FAST)
else:
self.change_speed(self.SLOW)
def change_speed(self, speed):
if self.speed != speed:
if speed == self.SLOW:
self.remove_event(3)
else:
self.add_event(3, self.move)
self.speed = speed
def get_image(self):
for x in range(0, 5):
self.image.set_pixel(x, self.y, 0)
if self.x >= 0 and self.x <= 4:
self.image.set_pixel_color(self.x, self.y, self.color)
return self.image
def reset(self):
self.x = 2
self.direction = random.choice((self.RIGHT, self.LEFT))
self.change_speed(self.SLOW)
self.reset_position()
class Racket(TickObj):
def __init__(self, button):
super().__init__(15, repeat=False)
if button == "A":
self.image = Image("10000:10000:10000:10000:10000:", color=(0, 31, 0))
else:
self.image = Image("00001:00001:00001:00001:00001:", color=(0, 0, 31))
self.is_valid = False
self.position = self.end
self.add_event(1, self.valid)
self.add_event(5, self.invalid)
def swing(self):
if self.position == self.end:
self.reset_position()
def valid(self):
self.is_valid = True
def invalid(self):
self.is_valid = False
def get_image(self):
return self.image
def reset(self):
self.is_valid = False
self.position = self.end
def main():
ticker = Ticker(FPS)
racket_a = Racket("A")
racket_b = Racket("B")
ball = Ball()
ticker.register(racket_a, racket_b, ball)
def reset():
racket_a.reset()
racket_b.reset()
ball.reset()
def start_game():
while True:
ticker.ticks()
if button_a.is_pressed():
racket_a.swing()
if button_b.is_pressed():
racket_b.swing()
screen = ball.get_image()
if racket_a.is_valid:
screen += racket_a.get_image()
if ball.x == 0 and ball.direction == Ball.LEFT:
ball.change_direction()
if racket_b.is_valid:
screen += racket_b.get_image()
if ball.x == 4 and ball.direction == Ball.RIGHT:
ball.change_direction()
display.show(screen, delay=0)
if ball.x < 0 or ball.x > 4:
sound.failed()
winner = "A" if ball.x > 4 else "B"
display.scroll("{} wins.".format(winner), delay=30)
break
while True:
if button_a.is_pressed() and button_b.is_pressed():
sound.countdown()
start_game()
reset()
main()
チャプター5
おわりに
5. 1 このレッスンのまとめ
このレッスンでは、ゲーム製作におけるテクニックとして、ビデオカメラで撮影した動画やゲームの映像で使われる「フレームの処理」を参考に、複数のオブジェクトを共通の時間軸上で同時に制御する手法について学びました。
また、レッスンの後半では学んだ手法の応用として、卓球ゲームを製作しました。LEDディスプレイをゲーム画面と考えると、画面上で複数のオブジェクト(ボールとラケット)の描画を同時に制御するという少し高度な処理を行ったことになります。
今回学習した手法は次のレッスンのゲーム製作でも使用しますので、もし途中で分からなくなったときは、このレッスンに戻って復習をしてください。
5. 2 次のレッスンについて
次回のレッスンでは、「レーシングゲーム」を製作します。ここまでは、ゲーム中に登場するオブジェクトが固定されていましたが、次のレーシングゲームでは、ゲーム中に新たなオブジェクトを出現させたり、反対に描画する必要がなくなったオブジェクトを削除したりするなど、さらに高度な処理を扱います。