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

テーマ.7-2 ペイントゲームを製作しよう

加速度センサーを利用してゲームを製作しよう!

チャプター1このレッスンで学ぶこと

このレッスンでは、前のレッスンより少し複雑なゲームプログラムの製作に取り組みます。また、リストや辞書を使うプログラムを短くまとめるときに便利な「内包表記」について新たに学習します。

新しいPython文法の学習

リストには「1, 2, 3, 4, 5, …」や「2, 4, 6, 8, 10, ….」のように、ある一定の規則で並ぶ値を用意して格納するときに便利な「内包表記」という書き方があります。ここでは、簡単なサンプルプログラムを通して、この内包表記について学習しましょう。

2. 1 リストの内包表記

内包表記の基本は、式とfor文とシーケンスなデータを組み合わせて次のように書きます。

[ 変数1を使用した式  for 変数1 in range()関数やリスト、タプルなどのシーケンスなデータ ]

例えば、0~9の数字を格納したリストを作成したい場合は、次のように書くことができます。

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

このプログラムを実行すると、しっかりとリストに1~9までの数字が格納されていることが確認できます。

(実行結果)

式では様々な演算を行うことができ、例えば【 サンプルコード 2-1-1 】で、式「num」を「num * 3」に変えると、3の倍数の並びになります。

【 サンプルコード 2-1-2 】
(実行結果)

内包表記はこれだけでなく、複数のfor文を組み合わせたたり、if文と組み合わせたりすることができます。それぞれサンプルコードを例に見ていきましょう。

■ for文を複数組み合わせた内包表記の書き方

複数のfor文を組み合わせることで、複数のシーケンスなデータにもとづいた値を順番に格納していくことができます。例えば、2つのfor文を組み合わせる場合は次のように書きます。

[ 変数1と変数2を使用した式 for 変数1 in シーケンスなデータ for 変数2 in シーケンスなデータ ]

では実際に、「11, 12, 21, 22, 31, 32」という数字の並びを、2つのfor文を組み合わせてリストに格納してみましょう。

【 サンプルコード 2-2-1 】
(実行結果)

【 サンプルコード 2-2-1 】を内包表記を使用せずに書くと、以下のようなコードになります。

変数iと変数jの2重のループになっており、i * 10で10の桁を、jで1の桁を表して、リストに数値を追加しています。

appendの利点

  • 簡単にリストに要素を追加できる。
  • リストの末尾に追加されるため、リストの順序を保持したまま新しい要素を追加できる。

これがappendメソッドの基本的な使い方と特性です。

■ if文を組み合わせた内包表記の書き方

for文の後に続けてif文を書くことで、条件式によって、シーケンスなデータから取り出した値から式を評価して、結果をリストに格納するかどうかを決めることができます。

[ 変数1を使用した式 for 変数1 in シーケンスなデータ if 式を評価するかどうかを決める条件式]

例えば、数値が格納されたあるリストから、5より大きい値を取り出したい場合、次のように書くことができます。

【 サンプルコード 2-3-1 】
(実行結果)

【 サンプルコード 2-3-1 】を内包表記を使用せずに書くと、以下のようなコードになります。

内包表記を使用する事で記述するコードがシンプルになり、読みやすくなるだけでなく、処理速度が速くなるなどのメリットもあります。

2. 2 辞書の内包表記

内包表記はリストだけでなく、辞書にも適用できます。(残念ながらタプルには適用できません。)基本の書き方は次の通りで、キーと値をそれぞれシーケンスなデータの要素を使用した式で決めることができます。

{ キーを決める式:値を決める式 for 変数 in シーケンスなデータ }

例えば、リストに格納している各要素へ、順番に「2000-」から始まる番号をキーとして付与したい場合、以下のように書くことができます。

【 サンプルコード 2-4-1 】
(実行結果)

そして、リストのときと同様に、複数のfor文を組み合わせたたり、if文と組み合わせたりすることもできます。

■ 2つのfor文を使用した内包表記

例えば以下のサンプルコードでは、「1または2」と「4または5」の組み合わせの掛け算の式をキーに、その計算結果を値にもつ辞書を、2つのfor文を組み合わせて作成しています。

【 サンプルコード 2-4-2 】
(実行結果)

■ if文を使用した内包表記

また、次のサンプルコードではif文と組み合わせることで、リスト内から5文字以上の単語のみを取り出し、その単語をキーに文字数を要素にもつ辞書を作成しています。

【 サンプルコード 2-4-3 】
(実行結果)

内包表記は慣れるまで少し、時間が掛かるかもしれませんが、使いこなせるようになると、Pythonらしいプログラムが書けるようになります。もし、これから先にリストや辞書を使ったプログラムを作成する場合は、積極的に内包表記が利用できないか考えるようにしましょう。ここまでできたらクリック

ペイントゲームの製作

ここからは、Studuino:bitのLEDディスプレイと内蔵の加速度センサーを使用した「ペイントゲーム」を製作します。まずは次の動画を見て、このゲームの動作を確認しましょう。

【 ゲームのプレイ動画 】

3. 1 ペイントゲームの遊び方

このゲームでは、5×5マスのLEDディスプレイを1枚の画用紙に見立てます。プレイヤーはその上で筆に見立てた赤色のLEDを移動させ、通ったマスに色を塗ります。筆はStuduino:bitを傾けた方向へ移動するので、上手に操作して制限時間の10秒以内にすべてのマスを塗りつぶすことができればゲームクリアとなります。

3. 2 ゲームプログラムの作成

このゲームには、主に次の機能が必要になります。

【 ペイントゲームに必要な機能 】
  • Studuino:bitを傾けた向きに筆(赤色のLED)を移動する機能
  • 筆が通ったマスのLEDを緑色に点灯する機能
  • すべてのマスを塗りつぶすことができたかどうかを判定する機能
  • 経過した時間を確認する機能

これらの機能はgame()関数としてまとめ、次の流れで処理を行います。

【 プログラムの主な処理の流れ 】

では、それぞれの機能を順番に作成していきましょう。

■ 使用するモジュールやオブジェクトのインポート

準備として、プログラムの中で使用する各種モジュールやオブジェクトをインポートしておきましょう。ボタンAは押すとゲームを開始するという設定で使用します。また、前のレッスンでStuduino:bit内に保存したsoundモジュールを今回も使用します。

もし、Studuino:bit内からsoundモジュールを削除している場合は、以下のリンク先からダウンロードしてもう一度Studuino:bitに保存しましょう。

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

sound.py

■ Studuino:bitを傾けた向きに筆を移動する機能/筆が通ったマスのLEDを緑色に点灯する機能

筆はゲーム開始時にLEDディスプレイの中央に配置します。また、移動後の座標は現在の位置から求めるため、変数brush_xbrush_yを用意して、座標を記録するようにします。

ゲームの主な処理は、game()関数にまとめます。この関数を宣言し、冒頭で筆の初期位置として中央の座標(x=2y=2)をbrush_xbrush_yに入れて、LEDを赤色に点灯させましょう。

追加【5行目~8行目】

続けて、Studuino:bitを傾けることで、筆を前後左右の4つの方向へ移動できるようにします。

これら4つの方向へ移動できるようにすると、その組み合わせで斜め方向へも移動できるようになります。

左右の傾きは加速度センサーのX軸の値を、前後の傾きはY軸の値を調べることで分かります。

【 加速度センサーの各軸の方向 】

では、加速度センサーの値を確認するため、新しくプログラムを作成し、以下のコードをコピーして実行しましょう。

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

確認した値から、左や右、前や後ろに傾いたと判定する条件を考えましょう。この先では、次のように条件を決めたものとしてプログラムを作成していきます。

向き判定の条件
左に傾いているX軸の値が-2より小さい
右に傾いているX軸の値が2より大きい
前に傾いているY軸の値が-2より小さい
後ろに傾いているY軸の値が2より大きい

元のプログラムに戻り、分岐処理を書いていきます。筆がLEDディスプレイの端まで移動している場合(X軸やY軸の値が「0」や「4」のとき)は、座標を変更しないように条件を組み合わせましょう。

追加【10行目~19行目】

16行目がelif文ではなくif文になっていることに注意してください。ここをelif文にしてしまうと、X軸方向とY軸方向へ同時に移動させることできなくなり、斜めの移動が行えなくなります。

次に、移動した先のLEDを赤色に点灯させます。移動していない場合は、点灯を行う必要がないため、あらかじめ移動前の座標を記録しておき、それと見比べて変更があった場合のみコードが実行されるようにしましょう。

追加【11行目、12行目、22行目~24行目】

そして、移動前のマスはLEDを緑色に点灯します。

追加【24行目】

最後に、筆が移動する速さを設定します。この速さは次の移動を行うまでに待つ時間の長さで決まります。ここでは100ミリ秒としておき、あとでゲームをプレイしてみてから調整するようにしてください。

追加【25行目】
※ インデントの位置に注意してください。

ここまでのプログラムを実行して動作を確認します。画面上部の「実行」をクリックした後、ターミナルからgame()関数を実行しましょう。

■ すべてのマスを塗りつぶすことができたかどうかを判定する機能

制限時間内にすべてのマスを通り、LEDを緑色に点灯できればゲームクリアとなります。ここでは、そのための判定を行う処理を書いていきましょう。

ここでは、1つ1つのマスに対して、通ったかどうかをTrueまたはFalseのブール値で管理するようにします。こうすることで、すべてのマスがTrueになった時点で塗りつぶしが完了したと判定できます。

game()関数のはじめに、それぞれのマスのX座標とY座標のタプルをキーとし、TrueまたはFalseのブール値を値とする辞書paperを用意します。初期状態では、上の左の図のような配置となるように、以下のように内包表記を利用してコードを書きましょう。

追加【9行目、10行目】

このように辞書のキーにはタプルを指定することもできます。また、内包表記を利用したことで、コードを短くまとめることができました。

次に、筆の移動先になるマスの値をTrueに変更するコードを追加します。キーがX座標とY座標のタプルとなっているため、呼び出すときも[]内にタプルを指定します。

追加【27行目】

塗りつぶしが完了したかどうかの判定は、筆の移動後に行います。辞書paperの値を1つずつ確認し、1つでもFalseがあれば、塗りつぶしが完了していないと判断します。塗りつぶしが完了した場合はゲーム終了として、外側のwhile文を抜けるようにします。

追加【30行目~33行目】

上で追加したコードでは、30行目のfor文の中で、順番に辞書paperの値がFalseになっているかどうかを調べ、1つ目が見つかった時点で32行目のbreak文でこのfor文を抜けるようになっています。この場合、33行目のelse文は処理されませんので、12行目のwhile文の次のループに移ります。反対に、32行目のbreak文が終わりまで実行されなかった場合、33行目のelse文が処理され、34行目のbreak文で12行目のwhile文を抜けます。

最後に、12行目のwhile文を抜ける前に、現在の筆の位置を示すマスを緑色に点灯させるコードを追加して、LEDディスプレイの全面が緑色に点灯するようにします。

追加【34行目】

ここまでのプログラムを実行して動作を確認しましょう。何度か動作を確認する場合は、game()関数を実行する前にdisplay.clear()を実行して、LEDディスプレイの表示をリセットしておきましょう。

■ 時間の経過を確認する機能

ここでは、時間の経過を計測して、制限時間内にゲームをクリアできたかどうかを判定する機能を追加していきます。

まずは、定数として制限時間を定義します。はじめは簡単にクリアできるように10000ミリ秒(10秒)としておきましょう。

追加【5行目】

次に、game()関数のはじめで、timeモジュールのticks_ms()関数で現在の時間を取得して変数start_timeに格納するコードを追加します。そしてwhile文の条件を「経過時間(time.ticks_diff(time.ticks_ms(), start_time))が制限時間(LIMIT_TIME)を下回っている」に変更します。

追加・変更【14行目、15行目】

これで、制限時間を超える前に塗りつぶしを完了できた場合は、break文でこのwhile文が強制的に終了され、制限時間を超えた場合は正常にwhile文を抜けることになります。このことを利用して、次のようにコードを書くことで、ゲームクリアに成功した場合と失敗した場合に分けて処理を行うことができます。

追加【39行目~45行目】

39行目のelse文は15行目のwhile文が38行目のbreak文で強制終了されなかった場合(クリア失敗の場合)のみ実行されます。そして、42行目のreturn文でgame()関数を抜けます。return文は通常関数の戻り値を渡す場合に使用しますが、このように戻り値がなくても使用することができます。return文が実行されると、その後ろに続く関数内のコードは実行されないため、43行目以降は、成功の場合のみ実行されることになります。

これでゲームの主な機能ができました。折角なので最後に、ターミナルからgame()関数を実行するのではなく、ボタンAを押してカウントダウン後にゲームを開始できるようにコードを追加しましょう。

追加【47行目~54行目】

完成したプログラムは以下のようになります。プログラムを実行してうまくいかない場合は、これと見比べて誤りがないかどうかを確認しましょう。

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

課題:ゲームの難易度を変更できる機能の追加

市販のシューティングゲームやアクションゲームの中には、ゲームの難易度をプレイヤーが選択できるものがあります。今回製作したペイントゲームも様々な方法でゲームの難易度を変更することができます。その中でも簡単な方法が、「制限時間を変える」ことです。つまり、定数LIMIT_TIMEの値を変更するだけで難易度が調整できます。

しかし、このままでは事前に難易度は調整できても、プレイヤーに難易度を選択させることはできません。そこでこの課題では、ゲームの開始前に、「Easy」「Normal」「Hard」の3つのモードからプレイヤーが難易度を選択できるように【 サンプルコード 3-2-2 】を改造します。さらに、プレイヤーの選択した難易度が分かるように、ゲーム中のLEDの点灯色もそれぞれ次のように変えましょう。

【 各難易度の制限時間 】
モード(難易度)制限時間
Easy(やさしい)10000ミリ秒(10秒)
Normal(ふつう)8000ミリ秒(8秒)
Hard(むずかしい)6000ミリ秒(6秒)
【 各モード選択時のLEDの点灯色 】

また、モードはボタンBを押して選択します。初期選択は「Easy」とし、main()関数の中に選択を切り替える処理を書きましょう。

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

【 サンプルコード 3-2-2 】に、次の2つの役割をもつコードを追加します。

  • ボタンBを押してモードを選択する
  • 選択されたモードで制限時間とLEDの点灯色を設定する

それでは、順番に書いていきましょう。

■ ボタンBを押してモードを選択する

プログラムのはじめに、3つのモードと、それぞれのモードの制限時間とLEDの点灯色(筆で塗りつぶす色)を定数として定義します。

定数名データ型
MODEタプル("Easy", "Normal", "Hard")
LIMIT_TIME辞書それぞれのモードの制限時間 {"Easy":10000, "Normal":8000, "Hard":6000}
PAINT_COLOR辞書それぞれのモードのLEDの点灯色 {"Easy":(31, 0, 0), "Normal":(0, 31, 0), "Hard":(31, 0, 31)}

そして、変数selected_modeを定義して、現在選択中のモードを格納するようにします。また、button_bオブジェクトのインポートも忘れないように注意してください。

追加・変更【1行目、5行目~8行目】

次に、main()関数内でボタンBが押されると、選択中のモードを切り替える処理を書いていきます。ここでは、タプルがもつ「要素の値からインデックスを調べる」index()メソッドを利用します。このメソッドで、現在選択中のモードのタプルMODE内でのインデックスを調べ、その次のインデックスを求めます。ただし、最後のインデックスの場合は「0」に戻さなければいけないことに注意してください。また、変更後のモードがプレイヤーに分かるようにdisplayオブジェクトのscroll()メソッドで表示します。

追加【 42行目、43行目、49行目~53行目 】

selected_modeはグローバル変数のため、main()関数内で値を変更する場合は、42行目のようにglobal宣言が必要です。また、初期選択がどのモードになっているのかが分かるように、main()関数の呼び出し直後にもLEDディスプレイに表示するようにしておきましょう。

■ 選択されたモードで制限時間とLEDの点灯色を設定する

モードの選択ができるようになったので、後は選択されたモードによって制限時間とLEDの点灯色が切り替わるようにプログラムを変更します。【 サンプルコード 3-2-2 】からLIMIT_TIMELIMIT_TIME[selected_mode]に、(0, 31, 0)PAINT_COLOR[selected_mode]へそれぞれ変更しましょう。これでプログラムの完成です。

【 サンプルコード 4-1-1 】
変更【18行目、32行目、40行目】

おわりに

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

このレッスンでは、リストや辞書の「内包表記」について新たに学習しました。内包表記を利用することで、Pythonらしくプログラムを簡潔にまとめることができるようになります。

また、前回に引き続きゲーム製作として「ペイントゲーム」の作成に取り組みました。少し構造が複雑になりましたが、課題で取り組んだように難易度の設定ができることで、より長くプレイヤーに楽しんでもらえるゲームになります。

この先のレッスンで作成するゲームや前のレッスンで作成したゲームでも、ぜひこの難易度設定を導入できないか考えてみてください。

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

次回のレッスンでは、さらに構造が複雑なゲームプログラムに取り組みます。ビデオカメラで撮影した動画やゲームの映像で用いられる「フレームの処理」の考え方を応用した「複数オブジェクトの同時制御手法」を紹介します。

TOP