見出し画像

4.とりあえず動かそう!Pyxelでゲーム作り+ちょこっとPython:ゲームの説明

第4回はミニゲームのプログラムの説明をします。文法ではなく、何をしているかを書いています。

クリックするとジャンプして爆弾をよけるミニゲーム。bomb-jump

ChatGPTにコードを貼り付けて、解説してもらいました。
VS Code などで見たい方は GitHub のリンクを貼っておきます。


GitHubのリンク

https://github.com/programma-py/first-pyxel

ワンクリックで遊ぶPyxelミニゲームのしくみ

このゲームでは、プレイヤーが自動で右へ進み、爆弾が左へ流れてきます。クリックまたはスペースキーでジャンプし、爆弾をよけます。ぶつかると爆発してゲームオーバーになります。クリックひとつで遊べるゲームです。

  • 最初のクリックでゲーム開始

  • ゲーム中のクリックでジャンプ

  • 爆弾に当たるとゲームオーバー

  • ゲームオーバー後のクリックでリスタート

このプログラムは、大きく分けると次の部品でできています。

画像データの定義
↓
音の設定
↓
Player クラス
↓
Bomb クラス
↓
App クラス
↓
App() でゲーム開始

1. 画像データを辞書でまとめる

最初に、キャラクターの画像データを定義しています。

PUTIT_GREEN = {                             # キャラクターの画像データ
    "sprites": {
        "IDLE": [(0,  0, 16, 15, 16, 2),    # 立ち止まっている状態
                 (0, 16, 16, 15, 16, 2)],
        "JUMP":  [(0, 0, 0, 15, 16, 2),     # ジャンプしている状態
                 (0, 16, 0, 15, 16, 2)]
    },
    "state_to_sprite": {                    # 状態と画像データの対応
        "idle": "IDLE",
        "run": "JUMP",
        "jump": "JUMP",
        "down": "IDLE"
    }
}

Pyxelでは、画像を表示するときに pyxel.blt() を使います。
画像そのものを直接指定するのではなく、イメージバンクのどこにある画像を使うかを数字で指定します。

(イメージバンク番号, X座標, Y座標, 幅, 高さ, 透明色)
(0, 0, 16, 15, 16, 2)

これは、
イメージバンク 0 番、X座標 0、Y座標 16、幅 15、高さ 16、透明色 2
という意味です。
数字をそのままプログラムの中に書き続けると、あとで見たときに何の画像かわからなくなります。
そこで、画像データを `PUTIT_GREEN` という辞書にまとめています。

透明色について

透明色(とうめいしょく)とは、「この色は表示しない」と指定するための色です。透明色に指定した部分は画面に描かれず、後ろにある背景や画像がそのまま見えます。

状態と画像を対応させる

プレイヤーには、いくつかの状態があります。
idle:まだ動いていない
run:走っている
jump:ジャンプしている
down :倒れている、ゲームオーバー
という意味です。そして、状態に合わせて使う画像を変えています。

"state_to_sprite": {
    "idle": "IDLE",
    "run": "JUMP",
    "jump": "JUMP",
    "down": "IDLE"
}

たとえば、プレイヤーの状態が "idle" のときは、画像グループ "IDLE" を使います。
プレイヤーの状態が "jump" のときは、画像グループ "JUMP" を使います。
"run" のときも "JUMP" の画像を使いまわしています。
このように、「今の状態がこれなら、この画像を使う」という対応表を作っておくと、プログラムが読みやすくなります。

2.効果音の設定

def setup_sounds():
    pyxel.sound(0).set("c3", "t", "7", "n", 4)          # 歩く音
    pyxel.sound(1).set("c3g3c4", "p", "7", "n", 12)     # ジャンプ音
    pyxel.sound(2).set("c2c1c2c1", "n", "7", "n", 12)   # 爆発音

ここでは、ゲーム中に鳴らす音を3つ作っています。あとで、次のようにして音を鳴らします。

pyxel.play(0, 0)

この場合、最初の 0 はチャンネル番号、次の 0 はサウンド番号です。

3. Playerクラス

class Player:

クラスは、ざっくり言うと、キャラクターを作るための設計図です。
プレイヤーは、次のような情報を持っています。

x       横の位置
y       縦の位置
vy      縦方向の速度
state   現在の状態
width   幅
height  高さ
base_y  地面に立っているときのY座標

resetで最初の状態に戻す

def reset(self):
    self.x = 0
    self.y = self.base_y
    self.vy = 0
    self.state = "idle"

`reset()` は、プレイヤーを最初の状態に戻す命令です。
最初は、X座標は0、Y座標は地面の上、縦の速さは0、状態は `"idle"` になります。
ゲームを最初から始めるときや、リスタートするときに使います。

updateでプレイヤーを動かす

`update()` は、ゲーム中に毎フレーム呼ばれる処理です。

def update(self):

ここで、プレイヤーの状態に合わせて動きを変えています。

idle状態

if self.state == "idle":
    if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
        self.state = "run"
    return

`idle` は、まだゲームが始まっていない状態です。
クリックされたら、状態を `"run"` に変えます。

self.state = "run"

これでゲームが始まります。

run状態

if self.state == "run":
    self.x += 0.5

`run` のときは、少しずつ右に進みます。

self.x += 0.5

これは、X座標を少しずつ増やしているということです。
X座標が増えると、キャラクターは右に動きます。

if self.x > pyxel.width - self.width:
    self.x = 0

画面の右端まで行ったら、左端に戻します。

クリックしたらジャンプ

if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
    self.vy = -6
    self.state = "jump"

ゲーム中にクリックすると、ジャンプします。
ここで大事なのは、

self.vy = -6

です。
Pyxelでは、上に行くほどY座標が小さくなります。
なので、上にジャンプさせたいときは、Y方向の速さをマイナスにします。

 jump状態

if self.state == "jump":
    self.x += 0.5
    self.y += self.vy
    self.vy += 0.4

ジャンプ中も、少しずつ右に進みます。

self.y += self.vy

で、縦方向に動かします。

self.vy += 0.4

で、だんだん下向きの力を加えます。これは、重力のようなものです。
最初は `vy = -6` なので上に進みます。でも、毎回 `0.4` ずつ増えるので、だんだん上昇が弱くなり、やがて落ちてきます。

地面に着いたらrunに戻る

if self.y >= self.base_y:
    self.y = self.base_y
    self.vy = 0
    self.state = "run"

ジャンプして落ちてきたら、地面より下に行かないようにします。

self.y = self.base_y

で、地面の上にぴったり戻します。
そして、状態を `"run"` に戻します。

drawでプレイヤーを描く

def draw(self):

`draw()` は、画面に絵を表示する処理です。

sprite_name = PUTIT_GREEN["state_to_sprite"][self.state]
frames = PUTIT_GREEN["sprites"][sprite_name]

ここでは、今の状態を見る、その状態に合う画像名を調べる、実際の画像データを取り出す、ということをしています。
たとえば、状態が `"idle"` なら、

PUTIT_GREEN["state_to_sprite"]["idle"]

で `"IDLE"` が取り出されます。

PUTIT_GREEN["sprites"]["IDLE"]

IDLE用の画像データを取り出します。

アニメーションのしくみ

frame = (pyxel.frame_count // 12) % len(frames)

これは、画像を順番に切り替えてアニメーションさせるための計算です。
`pyxel.frame_count` は、ゲームが始まってから何フレーム経ったかを表します。
// 12 としているので、12フレームごとに画像が切り替わります。
% len(frames) は、画像の枚数を超えたら最初に戻すための計算です。
たとえば画像が2枚なら、`0, 1, 0, 1, 0, 1` のように繰り返されます。

// は、あまりのある割り算の商の部分 13 // 3 = 4
% は、あまりのある割り算のあまりの部分 13 % 3 = 1

4. Bombクラス

爆弾を表すクラスが `Bomb` です。

class Bomb:

爆弾にも `self.state` があります。
爆弾の状態は、次の2つです。
wait 爆発前、explode 爆発中 

爆弾を複数出すしくみ

このゲームでは、爆弾が3つ出ます。

self.bomb1 = Bomb()
self.bomb2 = Bomb(self.bomb1)
self.bomb3 = Bomb(self.bomb2)

1つ目の爆弾は、普通に作ります。

self.bomb1 = Bomb()

2つ目の爆弾は、1つ目の爆弾を目印にして作ります。

self.bomb2 = Bomb(self.bomb1)

3つ目の爆弾は、2つ目の爆弾を目印にして作ります。

self.bomb3 = Bomb(self.bomb2)

この目印になる爆弾を、プログラムでは `leader` と呼んでいます。

def __init__(self, leader=None):
    self.leader = leader

`leader` がない爆弾は、最初の爆弾です。
`leader` がある爆弾は、前の爆弾から少し離れた場所に置きます。

爆弾同士の間隔をランダムにする

self.spawn_min_x = 70
self.spawn_max_x = 80

これは、爆弾どうしの間隔です。

self.x = self.leader.x + pyxel.rndi(self.spawn_min_x, self.spawn_max_x)

この部分で、前の爆弾より70から80ピクセル右に置いています。
`pyxel.rndi(70, 80)` は、70から80までの整数をランダムに選びます。
そのため、爆弾の間隔が毎回少し変わります。ぴったり同じ間隔で出るより、ゲームらしくなります。

爆弾を左に動かす

self.x -= 1

爆弾は左に動きます。
プレイヤーが右に進んでいるようにも見えますが、実際には爆弾も左に流れています。
横スクロールゲームでは、このように、プレイヤーを動かす、敵や背景を動かす、の両方を使って走っている感じを作ることができます。

爆弾が画面外に出たら戻す

if self.x < 0:
    if self.leader is None:
        self.x = 210
    else:
        self.x = self.leader.x + pyxel.rndi(self.spawn_min_x, self.spawn_max_x)
    return True

爆弾が画面の左に出ていったら、右側に戻します。
1つ目の爆弾は、

self.x = 210

で、かなり右のほうに戻します。
2つ目、3つ目の爆弾は、前の爆弾から70〜80ピクセル離れた位置に置きます。

return True

を返します。これは、「この爆弾は無事によけられました」という合図として使っています。

スコアを増やす

Appクラスの中で、爆弾の更新をしています。

if enemy.update():
    self.score += 1

`enemy.update()` が `True` を返したら、スコアを1増やします。
つまり、爆弾が画面外に出たら、よけたことになって点数が入ります。

if self.score > self.high_score:
    self.high_score = self.score

今のスコアが最高得点を超えたら、ハイスコアを更新します。

5. 当たり判定

ゲームでとても大事なのが、当たり判定です。

def check_collision(self, player, enemy):
    if (player.x < enemy.x + enemy.width and 
        enemy.x < player.x + player.width and
        layer.y < enemy.y + enemy.height and
        enemy.y < player.y + player.height):
        return True
    else:
        return False

これは、プレイヤーと爆弾の四角形が重なっているかを調べています。
キャラクターの画像を、四角い箱だと考えます。
プレイヤーにも、爆弾にも、左、右、上、下があります。
この2つの四角形が重なったら、当たったことにします。
少し難しく見えますが、考え方はシンプルです。
「プレイヤーの四角と、爆弾の四角が重なっているか?」を調べているだけです。

if self.check_collision(self.player, enemy):
    enemy.explode()
    self.player.state = "down"

プレイヤーと爆弾が当たったら、爆弾を爆発状態にします。

enemy.explode()

そして、プレイヤーの状態を `"down"` にします。

self.player.state = "down"

プレイヤーが `"down"` になると、ゲームオーバーになります。

6. Appクラスはゲーム全体の管理役

class App:

`App` クラスは、ゲーム全体を管理しています。
主な役割は次のとおりです。

  • Pyxelの初期化

  • 画像データの読み込み

  • プレイヤーを作る

  • 爆弾を作る

  • スコアを管理する

  • 毎フレームの更新

  • 画面の描画

Pyxelの初期化

pyxel.init(160, 120)
pyxel.load("my_resource.pyxres")

ここで、Pyxelの画面を作っています。

pyxel.init(160, 120)

は、横160ピクセル、縦120ピクセルの画面を作るという意味です。

pyxel.load("my_resource.pyxres")

は、画像などが入ったリソースファイルを読み込んでいます。

ゲームループ

pyxel.run(self.update, self.draw)

Pyxelでは、この1行でゲームが動き続けます。

self.update

は、ゲームの中身を更新する処理です。

self.draw

は、画面に描く処理です。
Pyxelは、updateする、drawする、updateする、drawする、という流れを何度も繰り返します。
これをゲームループといいます。

ゲーム全体の状態の流れ

このゲームの流れは、次のようになっています。

タイトル状態
   ↓ クリック
走る
   ↓ クリック
ジャンプ
   ↓ 地面に着く
走る
   ↓ 爆弾に当たる
ゲームオーバー
   ↓ クリック
リスタート

プログラムでは、この流れを `state` という変数で管理しています。
`state` を使うと、「今は何をしているところか?」がわかりやすくなります。

文字を描く処理

def text_draw(self, text, x, y, color1, color2):
    pyxel.rect(x - 2, y - 2, len(text) * 4 + 4, 8, color2)
    pyxel.text(x, y, text, color1)

この関数は、文字の後ろに四角を描いてから、文字を表示しています。

pyxel.rect(...)

で背景の四角を描きます。

pyxel.text(...)

で文字を描きます。
文字だけだと背景にまぎれて読みにくいことがあります。
そこで、文字の後ろに四角を置いて、読みやすくしています。

drawで画面全体を描く

def draw(self):
    pyxel.cls(1)

`pyxel.cls(1)` は、画面を色番号1で塗りつぶします。
そのあとで、

self.player.draw()

でプレイヤーを描きます。
ゲーム中やゲームオーバー中なら、爆弾も描きます。

if self.player.state in ("run", "jump", "down"):
    for enemy in self.enemies:
        enemy.draw()

`for enemy in self.enemies:` は、爆弾リストの中にある爆弾を1つずつ取り出す処理です。
このゲームでは、爆弾が3つあります。

self.enemies = [self.bomb1, self.bomb2, self.bomb3]

このリストに入れておくことで、3つの爆弾をまとめて処理できます。

ゲーム開始前とゲーム中で表示を変える

ゲーム開始前やゲームオーバー中は、次の文字を表示します。

CLICK OR TAP TO START

ゲーム中は、次の文字を表示します。

CLICK OR TAP TO JUMP

プレイヤーの状態によって、表示するメッセージを変えています。

if self.player.state == "idle" or self.player.state == "down":

これは、「プレイヤーが待機中、またはゲームオーバー中なら」という意味です。

elif self.player.state in ("run", "jump"):

これは、「プレイヤーが走っている、またはジャンプしているなら」という意味です。

7. このプログラムで学べること

このプログラムには、ゲーム作りの基本がたくさん入っています。
特に大事なのは、次の5つです。

状態で動きを分ける

self.state = "idle"
self.state = "run"
self.state = "jump"
self.state = "down"

ゲームでは、キャラクターが今どんな状態かを管理することが大切です。

クラスでキャラクターをまとめる
Player` クラスと `Bomb` クラスを作ることで、プレイヤーと爆弾の情報を整理しています。

辞書で画像データを管理する
画像の座標を辞書にまとめることで、プログラムが読みやすくなります。

updateとdrawを分ける
`update()` は動きの計算。
`draw()` は画面に描く処理。
この2つを分けるのは、ゲームプログラミングの基本です。

当たり判定でゲームらしくする
プレイヤーと爆弾が重なったらゲームオーバー。
これによって、ゲームにルールが生まれます。

まとめ

このプログラムは、見た目は小さなワンクリックゲームですが、中にはゲーム作りの基本がたくさん入っています。

  • キャラクターの状態管理

  • スプライト画像の切り替え

  • ジャンプの動き

  • 敵キャラクターの出現

  • スコア計算

  • 当たり判定

  • ゲームオーバーとリスタート

クリックひとつで遊べるシンプルなゲームでも、プログラムの中ではいろいろな仕組みが動いています。まずはこの形を理解しておくと、次に、

  • 背景をスクロールさせる

  • 爆弾の数を増やす

  • スピードをだんだん速くする

  • アイテムを出す

といった改造もしやすくなります。

関連ページ・書籍

とりあえず動かそう!Pyxelでゲーム作り+ちょこっとPython シリーズ
第1回 はじめの一歩
第2回 何かを表示してみよう
第3回 ミニゲーム
第4回 ゲームの説明 ← このページ

公式レファレンス
【公式】準備ゼロで使えるPyxel開発環境『Pyxel Code Maker』
Pyxel Code Maker マニュアル
Pyxel Editor マニュアル
Pyxel ユーザーガイド
Pyxel API Reference

関連書籍

いいなと思ったら応援しよう!