Pyxel シューティングゲーム(前編)

Pyxel サンプルコードからゲーム開発の基礎を学ぶ

 Python向けレトロゲームエンジン「Pyxel(ピクセル)」を使ってのゲーム作成を通してプログラミングを学習します。
 Pyxelのサンプルコード 09_shooter.py 「画面遷移のあるシューティングゲーム」を写して,キャラクターが画面上を動く仕組みや衝突判定の方法を見てみましょう。

 公式サイトのサンプル
 デモ https://kitao.github.io/pyxel/wasm/examples/09_shooter.html
 コード pyxel/09_shooter.py at main · kitao/pyxel · GitHub

作成するゲーム

 09_shooter.py はスマホにもピッタリな縦画面のシューティングゲームですが,本記事ではこれを横にスクロールするタイプに改造したいと思います。

 サンプルコードからの変更は以下になります。
 ・画面を横長にする。
 ・背景を流れる星の方向を横方向にする。
 ・ドット絵をサンプルリソースファイルから取得する。
 ・自機が撃つ弾の飛ぶ方向を横向きにする。
 ・画面右から敵が出現する。
 

  

シューティングゲームのコード例

 ※リソースファイルはPyxelのサンプルを使用します。以下の記事を参照してください。 
 Pyxel サンプルリソースを使う - 勉強ボックス管理者ブログ

 ※ソースファイルの内容(list10_01 ~ list10_07)
 シューティングゲーム01 - Google ドライブ

 

01 Appクラスをつくる

 本ブログのコード例ではPythonのメイン処理からpyxel.run()命令を直接呼び出す記述としていましたが,公式サイトでもすすめられているAppクラスを使用した記述にしてみましょう。

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)
list10_01.py

import pyxel

class App:
    
    def __init__(self):
        pyxel.init(240, 160, title="Pyxel Shooter r")
        pyxel.load("sample.pyxres")
        pyxel.run(self.update, self.draw)

    def update(self):
        return

    def draw(self):
        pyxel.cls(0)
        return

App()

・class App:
 Appクラス定義の中にPyxel命令を入れています。インスタンス作成時に呼び出されるメソッド__init__(コンストラクタと呼ばれます)の処理で,pyxel画面の初期化とリソースファイルの読み込み,そして繰り返し実行される更新と描画のメソッドを指定してpyxelの処理を開始します。

・App()
 Pythonのメイン処理でApp()が実行されると,Appクラスのインスタンスが作られます。
 特にこの後Appクラスのインスタンスを使って何かするコードでなければ,obj = App() のようなインスタンスの代入処理は不要です。

 以降,Appクラスのupdate()メソッドとdraw()メソッドがフレームごとに実行されていくことがわかれば,クラスで書かれたPyxelのサンプルコードの処理も読み解くことができると思います。

 

02 背景に流れる星を描画する + 条件式(三項演算子

 サンプルコードのBackgroundクラスを移植して,背景に流れる星を描画させましょう。

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)
list10_02.py 抜粋

class Background:
・・・

    def update(self):
        for i, (x, y, speed) in enumerate(self.stars):
            x -= speed
            if x <= 0:
                x += pyxel.width
            self.stars[i] = (x, y, speed)

    def draw(self):
        for (x, y, speed) in self.stars:
            pyxel.pset(x, y, STAR_COLOR_HIGH if speed > 1.8 else STAR_COLOR_LOW)

実行結果
 

・Backgroundクラスのupdate()
 100個の星の位置を更新しています。左方向に移動させたいのでx座標の値を減らしています。画面外に移動した星は右側に戻しています。

・pyxel.pset(x, y, STAR_COLOR_HIGH if speed > 1.8 else STAR_COLOR_LOW)
 pyxel.pset(x, y, col)は1ドットの点を打てる命令です。
 色番号を指定するcolの引数のところが見慣れない記述になっていますが,これはPythonの「条件式(三項演算子)」と呼ばれる文法です。

Python 言語リファレンス 6.13. 条件式 (Conditional Expressions) より>

x if C else y

 x if C else y という式は,という式は最初に x ではなく条件 C を評価します。 C が true の場合 x が評価され値が返されます。 それ以外の場合には y が評価され返されます。
 

STAR_COLOR_HIGH if speed > 1.8 else STAR_COLOR_LOW

 speedの値が1.8より大きい場合 STAR_COLOR_HIGH の色になる
 それ以外の場合 STAR_COLOR_LOW の色になる

 ※条件式(三項演算子)を使うと,if~else文を1行にまとめて記述できるメリットがあります。自分でコードを考えて書くときは普通のif文で書いて問題ありません。慣れてきたら使ってみましょう。

 

03 自機を追加する

 本説明ではプレイヤーが操作する機体を「自機」と呼ぶことにします。サンプルコードでは自機のドット絵がコード内に記述されていましたが,サンプルリソースファイルのイメージバンク0の座標を指定して表示するようにします。

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)
list10_03.py 抜粋

class Player:
・・・
    def update(self):
        if pyxel.btn(pyxel.KEY_LEFT):
            self.x -= PLAYER_SPEED
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.x += PLAYER_SPEED
        if pyxel.btn(pyxel.KEY_UP):
            self.y -= PLAYER_SPEED
        if pyxel.btn(pyxel.KEY_DOWN):
            self.y += PLAYER_SPEED
        self.x = max(self.x, 0)
        self.x = min(self.x, pyxel.width - self.w)
        self.y = max(self.y, 0)
        self.y = min(self.y, pyxel.height - self.h)

    def draw(self):
        pyxel.blt(self.x, self.y, 0, 32,40, self.w, self.h, 0)

実行結果
 

・max()関数,min()関数
 Pythonの組み込み関数です。max()関数は引数の中の最大のものを返します。min()関数は引数の中で最小のものを返します。(引数にリストを指定することもできます)
 self.x = max(self.x, 0) ・・・ self.xが負の値になったときは0に戻されます。
 self.x = min(self.x, pyxel.width - self.w) ・・・ 自機が右側画面外に出ないようにします。

・def update_play_scene(self):
 本ゲームは画面遷移(タイトル画面→プレイ画面→ゲームオーバー画面)があります。update()メソッドで画面ごとの更新処理を呼び出す作りにするため,プレイ画面用の更新処理として作成します。

 

04 自機が弾を撃つ処理を追加する

 スペースキーを押すと自機が弾を撃ちます。弾用のクラスを追加して,インスタンスをリストに入れてまとめて処理しています。

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)
list10_04.py 抜粋

bullets = []

def update_list(list):
・・・
def draw_list(list):
・・・
def cleanup_list(list):
・・・
class Player:
    def update(self):
  ・・・
        if pyxel.btnp(pyxel.KEY_SPACE):
            Bullet(
                self.x + PLAYER_WIDTH - BULLET_WIDTH / 2,
                self.y + (PLAYER_HEIGHT - BULLET_HEIGHT) / 2
            )
            pyxel.play(0, 0)
・・・
class Bullet:
    def __init__(self, x, y):
  ・・・
        bullets.append(self)

    def update(self):
  ・・・
    def draw(self):
        pyxel.rect(self.x, self.y, self.w, self.h, BULLET_COLOR)

class App:
    def __init__(self):
  ・・・
        pyxel.sound(0).set("a3a2c1a1", "p", "7", "s", 5)

実行結果
 

・リスト用の関数群
 update_list(list),draw_list(list),cleanup_list(list)は,引数で指定されたリストのインスタンスを次々処理する(メソッドを呼び出す)関数です。今は弾だけですが,後の手順で敵や爆発の表示なども同じ関数で処理します。

 list.pop(i)はリストのインデックス i の要素を取り除くメソッドです。リストの長さもその場で短くなります。

 ※bulletsはグローバル変数と同じ場所で定義されたリストですが,global文なしで各関数やクラスのメソッドから変更されています。これは,グローバルで定義されたリストのメソッド呼び出しは値の参照と同じでどこでもできるという理由のようです。(関数から bullets = [] のような代入を行う場合にはglobal文での指定が必要になります)


・pyxel.btnp(pyxel.KEY_SPACE)
 自機の移動に使ったpyxel.btn()はキーが押されているかどうかの判定なので,方向キーを押し続けて移動ができます。弾の発射のpyxel.btnp()は,そのフレームにキーが押されたかどうか,を判定するので押し続けた場合は最初の1回だけTrueになります。
 ※押し続けたときにも連射させたいときは pyxel.btnp(pyxel.KEY_SPACE,15,15) などで調整してみましょう。

 btnp(key, [hold], [repeat])
 そのフレームにkeyが押されたらTrue、押されなければFalseを返します。
 holdとrepeatを指定すると、holdフレーム以上ボタンを押し続けた時にrepeatフレーム間隔でTrueが返ります。

・pyxel.sound(0).set("a3a2c1a1", "p", "7", "s", 5)
 弾発射時の効果音を設定しています。
 "a3a2c1a1"は音程を指定しています。a3はラ,a2はa3より1オクターブ下のラ。
 参考 Pyxel サウンドクラスの音程一覧 - 勉強ボックス管理者ブログ


後編の記事で敵を出現させます。kinutani.hateblo.jp

関連記事

 kinutani.hateblo.jp