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

テーマ.7-3 卓球ゲームを製作しよう

複数のオブジェクトを同時に制御しよう!

このレッスンで学ぶこと

ビデオカメラで撮影した動画やゲームの映像では、一定の間隔でフレーム(静止画やコマ)を差し替えることで滑らかな動きを表現しています。このレッスンでは、この「フレームを一定の間隔で差し替える」制御方法を参考に、複数のオブジェクトを共通の時間軸上で制御する手法を紹介します。また、レッスンの後半では、新たに学んだ手法を応用して、Studuino:bitのLEDディスプレイを使った卓球ゲームを製作します。

複数のオブジェクトを共通の時間軸上で制御する方法

Pythonの文法ではありませんが、今回は複数のオブジェクトを共通の時間軸(「タイムライン」とも呼ばれます)上で制御する手法について新たに学びます。

2. 1 ゲームにおける複数のオブジェクトの同時制御

市販されているゲームでは、当たり前のように同時に複数のキャラクターや背景などのオブジェクトが画面の中で動いています。しかし、実際にこういったゲームを作成するとなると、とても高度な技術が求められます。

まずは簡単な例として、次のケースを考えてみましょう。

ある村のマップに、村人として「Brian(ブライアン)」と「Anne(アン)」という2人のキャラクターを用意します。そして、Brianには3秒に1回の頻度で、Anneは6秒に1回の頻度で繰り返しメッセージを表示させるとしましょう。では、このプログラムはどのように作成すれば良いでしょうか。

ここでは、それぞれのキャラクターを用意するために、次の原型となるクラスを使います。

【 キャラクターの原型を定義したクラス 】

1つの方法としては、1秒ずつ時間の経過をカウントし、経過時間が3の倍数と6の倍数のときに、それぞれのキャラクターのオブジェクトからspeak()メソッドを実行するやり方が考えられます。実際にこのコードを書いたものが次のプログラムになります。

【 サンプルコード 2-1-1 】
※ ある数の倍数になっているかどうかは、「%」演算子で割り算の余りを求め、その余りが0であることを調べることで判断できます。

このプログラムのポイントは、「1秒毎に進む時間軸」を設定し、それに沿って2つのオブジェクトを制御しているところにあります。

これによって、さらにもう1人別のキャラクターを追加したい場合でも、少しの変更を加えるだけで済みます。下のプログラムは実際に5秒に1回の頻度でメッセージを表示するキャラクター「Thomas(トーマス)」を追加したものです。

【 サンプルコード 2-1-2 】
追加【12行目、22行目、23行目】

これは簡単な例でしたが、このように共通の時間軸に沿ってオブジェクトを制御することで、ゲーム全体の進行も管理することができます。

では、ここからはさらにもう少し高度な内容に踏み込んでいきます。【 サンプルコード 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()関数で計算結果を整数値に丸めています。

次に、フレーム上で制御するオブジェクトを登録するregister()メソッドを定義します。このメソッドは引数として複数(1個でも可)のオブジェクトを受け取り、objsプロパティへリストのappend()メソッドを使って順番に格納します。

追加【6行目~8行目】

最後にticks()メソッドを定義します。このメソッドでは、あとでフレーム上のオブジェクトを作成するための原型となるスーパークラスTickObj内で定義するticks()メソッドを実行します。objsプロパティに格納されているオブジェクト順番に取り出してticks()メソッドを実行するコードを書きましょう。

追加【1行目、12行目~15行目】

このメソッドは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行目】
※ 上記の23行目のように内包表記を利用してコードを書くことで、timelineプロパティには以下のような辞書が格納されます。

コンストラクタでは引数として、endプロパティとrepeatプロパティに格納する値を取ります。また、コンストラクタ内ではpositionプロパティは初期位置として0を格納します。そして、timelineプロパティには、内包表記を使って時間軸上の1から終点までの位置をキーとして、要素に空のリストを持つ辞書を格納します。空のリストを持たせるのは、同じ位置に複数のイベントを登録できるようにするためです。

次に、add_event()メソッドとremove_event()メソッドを定義します。

add_event()メソッドは引数として、イベント(メソッド)とそのイベントを行う時間軸上の位置を受け取り、timelineプロパティへ格納します。

反対に、remove_event()メソッドでは引数として、イベントを削除したい時間軸上の位置を受け取り、timelineプロパティから要素を削除します。

追加【25行目、26行目、28行目、29行目】

続けて、reset_position()メソッドを定義します。このメソッドでは、positionプロパティを時間軸の先頭である0に戻します。

追加【31行目、32行目】

最後にticks()メソッドを定義します。このメソッドでは、最初にpositionプロパティの数値を1増やし時間軸上の位置を1つ先へ進めます。そして、その位置に登録されているイベントを順番に実行します。さらに、もしrepeatプロパティがTrueでかつ位置が終点に達していれば、reset_position()メソッドを実行して、初期位置に戻します。

追加【34行目~40行目】

これで、TickObjクラスの定義ができました。早速このクラスを継承してキャラクターのクラスを定義しましょう。

■ キャラクターのオブジェクト作成用クラスの定義

キャラクターのオブジェクトの原型となるクラスをCharacterとして定義します。このクラスは、上で定義したTickObjクラスを継承します。そして、【 サンプルコード 2-1-1 】と同じようにキャラクターの名前を格納するnameプロパティと、メッセージを表示するspeak()メソッドを新たに追加します。

はじめに、nameプロパティを追加するために、コンストラクタの__init__()メソッドをオーバーライドして、新たな引数として付け加えます。内部でsuper()関数を使いスーパークラスの__init__()メソッドを実行するようにしましょう。speak()メソッドはスーパークラスにはありませんので、【 サンプルコード 2-1-1 】と同じコードをそのまま流用します。

追加【42行目~48行目】

■ 各オブジェクトの作成とメソッドの実行

最後に、ここまでで用意した各クラスを使用してオブジェクトを作成し、プログラムを完成させます。まずは、Tickerクラスのオブジェクトを作成します。ここでは、【 サンプルコード 2-1-1 】に合わせてfpsを「1」(1秒で1フレーム進む)に設定しましょう。

追加【50行目】

次に2人のキャラクターのオブジェクトを作成します。そして、それぞれのオブジェクトにadd_event()メソッドで、以下の図で示す位置へspeak()メソッドをイベントとして登録します。

追加【51行目~54行目】
※ メソッドを引数として渡すときは、()を付けないことに注意してください。

最後に50行目で作成したtickerオブジェクトのregister()メソッドで2人のキャラクターのオブジェクトを登録します。これですべての準備ができたので、あとはwhile文を使った無限ループ内で繰り返しtickerオブジェクトのticks()メソッドを実行することで、フレームの進行に合わせてキャラクターのオブジェクトが制御されます。

追加【56行目~58行目】

完成したプログラムが以下になります。自分の書いたコードを見比べて、誤りがないかを確認し、実際にプログラムを実行してみましょう。【 サンプルコード 2-1-1 】と同じ結果になれば成功です。

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

【 サンプルコード 2-3-1 】は、【 サンプルコード 2-1-1 】と比べると、複雑なコードになっています。そのため、なぜ同じ動作をさせているだけなのに、ここまで複雑なコードを書かなければいけないのかと疑問に思うことでしょう。しかし、より多くの処理を行わなければならないゲームのプログラムを書くと、この手法が力を発揮します。そこで実際に、次のチャプターでは【 サンプルコード 2-3-1 】を流用して卓球ゲームを製作し、その実力を確認してみましょう。

卓球ゲームの製作

このチャプターでは、卓球ゲームを製作します。まずは下のプレイ動画を見て、ゲームの動作を確認しましょう。

【 卓球ゲームのプレイ動画 】

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_aracket_b)を作成するためのクラス。 【 サンプルコード 2-3-1 】で作成したTickObjクラスを継承

RacketクラスはボールA側とボールB側のオブジェクトを作成するため、引数としてボタン名の"A"または"B"を受け取るようにします。

3. 3 2つのクラスの定義

では、【 サンプルコード 2-3-1 】で作成したクラスとTickObjクラスを使用して2つのクラスBallRacketを先に作成していきましょう。

準備として、新しいプログラムファイルを作成して、【 サンプルコード 2-3-1 】から以下の40行目までを複製して貼り付けましょう。

■ Ballクラスの定義

BallクラスはTickObjクラスを継承して、以下のプロパティとメソッドを追加します。

【 追加するプロパティ 】
プロパティ名内容
xLEDディスプレイ上のボールの位置を表すX座標の値
yLEDディスプレイ上のボールの位置を表すY座標の値
imageボールをLEDディスプレイに表示するためのイメージ (※StuduinoBitImageクラスのインスタンス)
colorボールの表示色を表すタプル
direction現在ボールが飛んでいる方向を表す変数
RIGHTLEFTボールが飛ぶ方向を表す定数(クラスメンバ変数)
【 追加またはオーバーライドするメソッド 】
メソッド名内容
__init__()※ スーパークラスからオーバーライド。 プロパティの追加やadd_event()メソッドでイベントの登録をする処理
move()directionプロパティにもとづいてボールの位置を横方向(LEDディスプレイのX軸方向)に移動する処理
change_direction()ボールの飛ぶ方向を反対向きに変える処理
get_image()ボールを表示するためのイメージを返す処理
reset()各プロパティを初期値に戻す処理

そして、Ballクラスには次の図で表す時間軸を設定します。また、ボールの移動は繰り返し行うため、TickObjクラスから継承したrepeatプロパティはTrueを設定します。

【 Ballクラスの時間軸 】

では順番にコードを書いていきましょう。まずは、RIGHTプロパティとLEFTプロパティを定義します。この2つはボールの飛ぶ方向を表すための定数(クラスメンバ変数)で、"right""left"のような文字列の代わりとして使用します。

追加【42行目~44行目】

次に、__init__()メソッドをオーバーライドして、各プロパティの初期値を設定します。

まず、ボールが飛ぶ方向として、directionプロパティはLEFT変数とRIGHT変数のいずれかをランダムに選び設定します。また、ボールをLEDディスプレイに表示させるためのイメージも用意する必要があります。そのため、先頭でrandomモジュールとStuduinoBitImageImage)クラスをインポートしましょう。

追加【2行目、3行目】

そして、__init__()メソッドを定義して、それぞれプロパティに次のように値を設定しましょう。また、あとで定義するmove()メソッドを時間軸の6の位置に登録する処理もここに書いておきます。

追加【48行目~55行目】

続けて、ボールの位置を変化させるmove()メソッドを定義します。飛ぶ向きがRIGHTの場合は、xの値を1増やし、反対にLEFTの場合は、xの値を1減らします。

追加【57行目~61行目】

さらに続けて、ボールの飛ぶ方向を変えるchange_direction()メソッドを定義します。現在の方向がRIGHTの場合はLEFTを、LEFTの場合はRIGHTに変更します。また、方向を変更したあとは、reset_position()メソッドを実行して、時間軸上の位置を「0」に戻しておきましょう。

追加【63行目~68行目】

今度はLEDディスプレイ上に表示するためのイメージを取得するget_image()メソッドを定義します。このメソッドでは、imageプロパティのイメージを一度リセットして、現在のボール位置のLEDのみをcolorプロパティの色で点灯するように設定します。そして、この設定を行ったイメージを戻り値として返します。

追加【70行目~75行目】

最後に、各プロパティを初期値に戻すreset()メソッドを定義します。このメソッドは、ゲームを再プレイするときに呼び出します。次のようにコードを書きましょう。

追加【77行目~80行目】

■ 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行目】

次に、ラケットを表示するために時間軸の進行を再開するswing()メソッドを定義します。このメソッドはpositionプロパティの値が終点を表すendプロパティと等しいときだけreset_position()メソッドで時間軸の位置を「0」に戻します。TickObjticks()メソッドの定義の通り、時間軸は位置が終点でなければ、自動的に1ずつ進行するようになっています。

追加【94行目~96行目】

続けて、ラケットの表示が有効か無効か切り替えるためのvalid()メソッドとinvalid()メソッドを定義します。以下のようにコードを書きましょう。

追加【98行目~102行目】

今度は、ラケットのイメージを返すget_image()メソッドを定義します。以下のようにコードを書きましょう。

追加【104行目、105行目】

最後に、各プロパティを初期値に戻すreset()メソッドを次のように定義しましょう。

追加【107行目~109行目】

3. 4 ゲームのメインとなる処理の作成

ここまででオブジェクトの作成に必要なクラスがすべて定義できました。ここからは、プログラムのメイン処理を書いていきます。また、プログラムの見通しを良くするため、以下の関数を用意してまとめていきます。

関数名内容
main()メイン処理全体をまとめた(囲んだ)関数
reset()ゲームを初期状態にリセットする処理をまとめた関数
start_game()ゲームを1回プレイする処理をまとめた関数

reset()関数とstart_game()関数は、main()関数の中で定義します。では、これらの関数を組み合わせた全体の処理がどのような流れになるのかを下の図で確認しましょう。

【 プログラム全体の処理の流れ 】

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

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

sound.py

それでは、順を追って作成していきましょう。

■ main()関数の定義

まずは、main()関数から定義していきます。その前にプログラムの先頭で使用するdisplaybutton_abutton_bの3つのオブジェクトとsoundモジュールをインポートしましょう。

追加・変更【3行目、4行目】

次に定数FPSとして、このゲームのフレームレートを設定します。この値はあとで変更することもできますので、ここでは一旦「60fps」としておきましょう。

追加【6行目】

では、main()関数を定義します。用意したクラスからtickerracket_aracket_bballの4つのオブジェクトを作成します。そして、tickerオブジェクトのregister()メソッドでフレームの時間軸上に3つのオブジェクトを登録しましょう。

追加【114行目~119行目】  最後に追加

ゲームはボタンAとボタンBの両方を押すと開始になります。while文を使った無限ループ内で両方のボタンが押されているかどうかを調べ、両方とも押された場合は順番に関数を実行するようにします。

※ start_game()関数とreset()関数はあとで定義します。
追加【121行目~125行目】

■ reset()関数の定義

reset()関数はmain()関数の外からは呼び出すことがないため、main()関数の中で定義します。reset()関数では、racket_aracket_bballの3つのオブジェクトのreset()メソッドを実行します。以下のようにコードを書きましょう。

追加挿入【121行目~124行目】

■ start_game()関数の定義

start_game()関数も同じくmain()関数の外からは呼び出すことがないため、main()関数の中で定義します。まずは、フレームを次々に進めるためのコードを書きましょう。

追加挿入【126行目~128行目】

次にボタンAとボタンBがそれぞれ押された場合に、swing()メソッドを実行します。これで時間軸の位置が終点に達しているときだけ位置が「0」に戻り、ラケットの表示が行われます。

追加【130行目~133行目】

続けて、LEDディスプレイ上に表示するイメージを用意します。まずは、ballオブジェクトからボールのイメージを取得し変数screenに格納します。

追加【135行目】

さらに、StuduinoBitImageでオーバーライドしている+演算子の処理で、racket_aracket_bから得られるラケットのイメージを合成していきます。ただし、このイメージの合成はis_validプロパティがTrueに設定されているときのみ行います。そして、合成が完了したイメージはLEDディスプレイに表示します。

追加【136行目~140行目】

また、ラケットのイメージとボールのイメージに重なりがあるときは、ballオブジェクトのchange_direction()メソッドを実行してボールの飛ぶ方向を変更します。ただし、ボールが飛んできている方向も加味する必要があり、ボタンA側のラケットはボールが左方向へ飛んでいるときだけ、ボタンB側はボールが右方向へ飛んでいるときだけ、この変更を行います。

追加【138行目、139行目、142行目、143行目】

最後に、ボールがLEDディスプレイの外に出てしまっているかどうかを判定します。LEDディスプレイの右側(x > 4)に出た場合はボタンA側のプレイヤーの勝利となり、左側(x < 0)に出た場合はボタンB側のプレイヤーの勝利となります。その勝者をLEDディスプレイに表示しましょう。また、これで1回のゲームが終わりとなるため、break文で無限ループを抜けます。

追加【146行目~150行目】

一連の処理を開始するために、main()関数を関数の外側で実行しましょう。これでプログラムの完成です。

追加【159行目】

長い道のりで大変だったと思いますが、チャプター2で紹介した制御方法を使わなければ、もっとコードが複雑なものになっていたでしょう。では、最後に自分の書いたコードと以下のサンプルコードを見比べて誤りがないかを確認して、プログラムを実行しましょう。

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

3. 5 フレームレートを変更する

【 サンプルコード 3-4-1 】では、フレームレートを「60fps」に設定していましたが、この値を変更すると、ゲームの進行の速さを切り替えることができます。例えば90fpsに変更すると、ボールが少し早く移動するようになります。30fpsに変更すると、反対にボールが少し遅く移動するようになります。

  • フレームレートを90fpsに設定 ➡ ボールの移動が早くなる
  • フレームレートを30fpsに設定 ➡ ボールの移動が遅くなる

数値上では、30fpsと90fpsで比べると90fpsの方が3倍の速さでボールが移動する設定になっていますが、実際にはそうはなりません。これはStuduino:bitのLEDディスプレイの表示処理に時間が掛かるためです。そこで次のチャプターでは、フレームレートを変えずにボールの移動速度を2倍にする課題に取り組んでみましょう。

課題:ボールが移動する速さを一定の確率で変える

【 サンプルコード 3-4-1 】でもゲームとして楽しむことはできますが、ずっと同じ速さでボールが飛び交うので、どこか単調になってしまい、やや盛り上がりに欠けています。そこで、この課題では、打ち返したときに20%の確率でボールが2倍の速さで移動するように機能を追加して、もっと盛り上がるゲームに改良してみましょう。

【 機能を追加した卓球ゲームのプレイ動画 】

4. 1 プログラムの作成例

この課題はやや難しいので、以下の内容を参考にしてコードを書いてみましょう。

■ ボールを2倍の速さで飛ばすには?

ボールの速さは時間軸上の別の位置にmove()メソッドを登録するだけで簡単に変えることができます。例えば「3」の位置に追加すると、2倍の速さになります。

反対に2倍の速さにした状態から元の速さに戻すときは、「3」の位置のイベントを削除します。

この時間軸の「3」の位置へのmove()メソッドの登録と削除を繰り返し行うことで、ゲーム中のボールの速さを変化させます。

■ 20%の確率でボールを速く飛ばすプログラム

では実際に、方向が変わるときに20%の確率でボールが速く移動するプログラムへと変更していきましょう。

まずは、定数としてボールの移動速度を表すSLOWFASTBallオブジェクトのコードの先頭で定義します。

追加【50行目、51行目】

ボールに「速さ」の情報を追加するため、新たにspeedプロパティを用意します。__init__()メソッドで初期値としてSLOWを設定しましょう。

追加【60行目】

次に速さを変更するためのメソッドをchange_speed()として定義します。このメソッドでは、引数に速さ(SLOWまたはFAST)を受け取り、現在設定されている速さから変更があれば、時間軸上にmove()メソッドを登録したり、削除したりする処理を行います。

追加【70行目~76行目】

ボールの速さは、移動方向を変えるchange_direction()メソッド内で変更します。このとき「20%の確率」を設定するために、randomモジュールのrandint()メソッドを利用します。randint()メソッドで「1~5」の範囲から数字をランダムに選び、その数字が3の場合のみ「速く移動する」設定にし、それ以外の数字では「遅く移動する」設定にします。

これでプログラムの変更が完了しました。以下が完成したサンプルプログラムです。プログラムを実行してうまくいかなかった場合は、こちらのBallクラスの定義と見比べて誤りがないかを確認してください。

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

おわりに

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

このレッスンでは、ゲーム製作におけるテクニックとして、ビデオカメラで撮影した動画やゲームの映像で使われる「フレームの処理」を参考に、複数のオブジェクトを共通の時間軸上で同時に制御する手法について学びました。

また、レッスンの後半では学んだ手法の応用として、卓球ゲームを製作しました。LEDディスプレイをゲーム画面と考えると、画面上で複数のオブジェクト(ボールとラケット)の描画を同時に制御するという少し高度な処理を行ったことになります。

今回学習した手法は次のレッスンのゲーム製作でも使用しますので、もし途中で分からなくなったときは、このレッスンに戻って復習をしてください。

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

次回のレッスンでは、「レーシングゲーム」を製作します。ここまでは、ゲーム中に登場するオブジェクトが固定されていましたが、次のレーシングゲームでは、ゲーム中に新たなオブジェクトを出現させたり、反対に描画する必要がなくなったオブジェクトを削除したりするなど、さらに高度な処理を扱います。

TOP