個人開発者日記[3日目] ゲーム性を付加していく

2日目の記事では、Pyxelを使ってドット絵を描画し、プレイヤーの入力で操作ができるようにしました。

これに手を加えて、「遊べる」ゲームとして形にしていきます。サンプルゲームの「スネークゲーム」を参考にしています。

当たり判定

ゲームといえば、当たり判定が必要です。基本的に、何かと何かがぶつかったらイベントが起きるのがゲームです。

図形の場合、描画の起点となる座標から幅・高さをもって描画されるので、当たり判定を実装するには衝突する二つの図形の幅・高さ同士が接触しているかどうかを計算する必要があります。また、実際には描画する図形よりも当たり判定が小さかったり、大きかったりすることもあります。ちょっとややこしいですね。

最も単純な当たり判定

当たり判定ロジックを単純化させるために、衝突するオブジェクトとして描画する図形を1ピクセルの点(ドット)にしてみます。こうすると、衝突しているかどうかはその点の座標が一致しているかどうかを見ればいいので、当たり判定が非常に簡単です。サンプルゲームの07_snake.pyが参考になります。

デフォルトだと非常に小さい点になってしまうので、画面を拡大表示させることで1ドットがそれなりの大きさになるマス目にします。

    def __init__(self):
        pyxel.init(
            40, 30, title="secondgame", fps=20, display_scale=20
        )
# 以下略

これで、ある程度大きな1ドットを画面上で動かせるようになりました。

次に、衝突の対象となるドットも画面に配置してみます。原理理解が目的なので、オブジェクトにせず手続き型っぽく作っていきます。

    def __init__(self):
# 略
        self.apple_x = 20
        self.apple_y = 15 

# 略

    def draw(self):
# 略
        pyxel.pset(self.apple_x, self.apple_y,8)

これで、appleを示す赤いドットが20,15の位置に表示されました。 次に、自機のドットとappleの赤いドットが衝突したら、appleのドットが消えるようにします。

    def __init__(self):
# 略
        self.apple_eaten = False
# 略
    def update(self):
# 略
        if self.x == self.apple_x and self.y == self.apple_y:
            self.apple_eaten = True

    def draw(self):
#略
        if not self.apple_eaten:
            pyxel.pset(self.apple_x, self.apple_y,8)

x座標とy座標が自機・りんごで一致したらフラグを立てて表示処理を実行しない、という単純な処理です。

これで、非常に単純な当たり判定を実装することができました。

ゲーム性を追加する

当たり判定の実装はできましたが、これではゲームとして成立していません。

「スネークゲーム」を参考に、ゲーム性を付加していきましょう。

スコア表示を追加する

古いアーケードゲームファミコンのゲームでは、アイテムを集めたり敵を倒したりしてスコアをどれだけ集められるかを競う、というものが多いです。

そこで、このゲームにもスコアを導入します。

その前に、今はappleが一回取ったら消えるようになっているので、取ったらまた別の場所に再出現するようにしてみます。こちらも、サンプルのスネークゲームのロジックを参考にします。

    def __init__(self):
        pyxel.init(
#略
        # このフラグは使わないのでコメントアウトしておきます
        # self.apple_eaten = False

        pyxel.run(self.update, self.draw)
#略
    def update(self):
        if self.x == self.apple_x and self.y == self.apple_y:
            # 座標が一致したら、appleの座標を移動(再出現)させるように
            self.apple_x = pyxel.rndi(0, pyxel.width - 1)
            self.apple_y = pyxel.rndi(0, pyxel.height - 1)

    def draw(self):
        pyxel.cls(6)
        pyxel.pset(self.x, self.y,1)
        # 以下の条件分岐も不要なのでコメントアウトします
        # if not self.apple_eaten:
        pyxel.pset(self.apple_x, self.apple_y,8)

これだけでもだいぶゲームらしくなりましたね。 次に、appleと衝突したら加算するスコアを加算し、画面に表示する処理を追加します。

    def __init__(self):
#略
        # スコア領域と被らないように右にずらす
        self.x = 30
        self.score = 0
#略
    def update(self):
#略
        if self.x == self.apple_x and self.y == self.apple_y:
            self.score += 100
#略

    def draw(self):
#略
        pyxel.rect(0, 0, 18, 8, 0)
        pyxel.text(1, 1, f"{self.score}", 7)
        # スコア表示で見えなくならないように、自機を最後に描画させる
        pyxel.pset(self.x, self.y,1)

これで、appleと衝突するたびにスコアが100加算されるようになりました。

ゲームの終了条件

これで最低限ゲームとしての体を成したわけですが、このままでは目的もなければ終わりもありません。

ゲームには、ゲームオーバーやクリアなど、終わりになる条件が必要です。今回は、ゲームオーバーとタイムアップによるゲーム終了を実装してよりゲームらしさをつけくわえてみます。

まずは、ゲームオーバーの条件を設定します。単純さのため、林檎と同時に生成される毒林檎を間違えて食べたらゲームオーバー、としてみます。

    def __init__(self):
# 腐った林檎の座標
        self.rotten_apple_x = pyxel.rndi(19, pyxel.width - 1)
        self.rotten_apple_y = pyxel.rndi(9, pyxel.height - 1)
        self.score = 0
        self.game_over = False
# 略
    def update(self):
        if self.x == self.apple_x and self.y == self.apple_y:
 # 略
# 林檎を取ったら腐った林檎も再配置する
            self.rotten_apple_x = pyxel.rndi(19, pyxel.width - 1)
            self.rotten_apple_y = pyxel.rndi(9, pyxel.height - 1)
# 腐った林檎とぶつかったらゲームオーバーフラグを立てる
        if self.x == self.rotten_apple_x and self.y == self.rotten_apple_y:
            self.game_over = True
# 略
    def draw(self):
# ゲームオーバーフラグが立っていたら、画面表示を変える
        if self.game_over:
            pyxel.cls(0)
            pyxel.text(4, 11, "GAMEOVER", 8)
        else:
# 元々の処理を記載するので省略

これで、緑の腐った林檎を食べたらゲームオーバーになり、ゲーム性が増しましたね。

最後に、時間制限を設けて、ゲームオーバーせずにタイムアップしたらスコアが表示されるようにしてみましょう。

    def __init__(self):
        pyxel.init(
# 略
# 秒数カウント用の変数 初期値十秒
        self.time_up = False
        self.timer = 10

# 略
    def update(self):
# ゲームオーバーまたはタイムアップ後は処理をしないように
        if self.game_over or self.time_up:
            return

# 略
# 20フレームでカウントしたら一秒経ったとみなしてタイマーのカウントを1減らす
        if pyxel.frame_count != 0 and pyxel.frame_count % 20 == 0 and not self.game_over:
            self.timer -= 1
            if self.timer <= 0:
                self.time_up = True
# 略
    def draw(self):
        if self.game_over:
            pyxel.cls(0)
            pyxel.text(4, 11, "GAMEOVER", 8)
# タイムアップ後のスコア表示
        elif self.time_up:
            pyxel.cls(0)
            pyxel.text(0, 11, f"SCORE:{self.score} ", 7)
# 略

これで、ゲームオーバーと、時間切れによるゲーム終了の処理ができて、だいぶゲームらしくなりました。 実はこのゲームには潜在的なバグがいくつか残されていますが、サンプルゲームなのであえてそのままにしておきます。

今回作ったゲームもGithubリポジトリにアップロードして、ブラウザ上でプレイできるように公開しておきました。

github.com

https://naritsan.github.io/pyxel_firstgame/pages/firstgame_v2.html

まとめ

昨日のただ動くだけだったものから、遊べるゲームを作るところまで勧められました。 明日は、ドットではなく図形による当たり判定を実装していきたいと思います。