Kevin's Data Analytics Blog

データサイエンティスト、AIエンジニアを目指す方に向けて情報発信していきます。

Pythonでレトロ2Dゲームを作ってみた|Pyxel|ゲームプログラミング

PythonのPyxelというライブラリを使って、レトロゲームを作ってみました。
youtu.be
おにぎりくんが爆弾を避けながらビールを獲得していくというシンプルなゲームです。※おにぎりくんは、親戚の子供向けに作成した架空のキャラクターです。

Pyxel紹介

Pythonのライブラリの1つです。ゲームを作るために必要な基本的な機能が用意されているので、ゲームプログラミング初心者の方でも、短時間でゲームを実装できます。ゲーム内で使用するアイコンや音楽も自分で作ることができます。
Pyxelのサイト:
https://github.com/kitao/pyxel/blob/master/README.ja.md

ソースコード

サイトに公開されているサンプルコードを基に、見よう見まねで実装しました。オブジェクト指向プログラミングの練習にもなりました。
処理が冗長だったり、わかりづらかったりする部分はあるかと思いますが、一旦、作りたい仕様は実現できたので良しとします。

# -*- coding: utf-8 -*-

from collections import deque, namedtuple
from random import randint
import pyxel
import time

Point = namedtuple("Point", ["w", "h"])

UP = Point(-16, 16)
DOWN = Point(16, 16)
RIGHT = Point(-16, 16)
LEFT = Point(16, 16)

class App:
    def __init__(self):
        pyxel.init(160, 120, caption="Onigiri-kun Loves Beer")
        pyxel.load("my_resource.pyxres")
        self.direction = RIGHT

        #START FLAG
        self.START = False

        #GAMEOVER FLAG
        self.GAMEOVER = False
        self.end_bgm_flg = 1

        # Score
        self.score = 0
        self.items_got = 0
        self.bombs_got = 0
        self.total_items = 0

        # Starting Point
        self.player_x = 37
        self.player_y = 38
        self.player_vy = 0
        self.item = [((i+4) * 60, randint(6, 104), True) for i in range(4)]
        self.sp_item = [((i+1) * 950, randint(6, 104), True) for i in range(2)]
        self.bomb = [((i+3) * 77, randint(6, 104), True) for i in range(3)]
        self.timebar = 99

        pyxel.playm(0, loop=True)
        pyxel.run(self.update, self.draw)

    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

        # enter key to start
        if pyxel.btn(pyxel.KEY_ENTER) or pyxel.btn(pyxel.GAMEPAD_1_START):
            self.START = True

        if self.timebar < 0:
            self.GAMEOVER = True

        if self.GAMEOVER is True:
            pyxel.stop()

        if self.GAMEOVER and (pyxel.btn(pyxel.KEY_ENTER or pyxel.btn(pyxel.GAMEPAD_1_START))) :
            self.reset()

        if not self.START or self.GAMEOVER:
            return

        self.update_player()

        for i, v in enumerate(self.item):
            self.item[i] = self.update_item(*v)

        for i, v in enumerate(self.sp_item):
            self.sp_item[i] = self.update_sp_item(*v)

        for i, v in enumerate(self.bomb):
            self.bomb[i] = self.update_bomb(*v)

        self.timebar -= 0.2415

    def update_player(self):
        if pyxel.btn(pyxel.KEY_LEFT) or pyxel.btn(pyxel.GAMEPAD_1_LEFT):
            self.player_x = max(self.player_x - 4, 0)
            self.direction = LEFT

        if pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.GAMEPAD_1_RIGHT):
            self.player_x = min(self.player_x + 4, pyxel.width - 16)
            self.direction = RIGHT

        if pyxel.btn(pyxel.KEY_UP) or pyxel.btn(pyxel.GAMEPAD_1_UP):
            self.player_y = max(self.player_y - 4, 0)
            self.direction = UP

        if pyxel.btn(pyxel.KEY_DOWN) or pyxel.btn(pyxel.GAMEPAD_1_DOWN):
            self.player_y = min(self.player_y + 4, pyxel.height - 16)
            self.direction = DOWN

    def draw(self):

        if self.GAMEOVER:
            if self.end_bgm_flg == 1:
                if (self.bomb[0][2] is False and abs(self.bomb[0][0] - self.player_x) < 48 and abs(self.bomb[0][1] - self.player_y) < 48) \
                       or (self.bomb[1][2] is False and abs(self.bomb[1][0] - self.player_x) < 48 and abs(self.bomb[1][1] - self.player_y) < 48) \
                       or (self.bomb[2][2] is False and abs(self.bomb[2][0] - self.player_x) < 48 and abs(self.bomb[2][1] - self.player_y) < 48):
                    pyxel.play(3, 6)
                else:
                    pyxel.play(3, 7)

                time.sleep(1)
                self.end_bgm_flg = 0

            MESSAGE =\
"""
     FINISH
 
PUSH ENTER RESTART
"""
            pyxel.text(51, 40, MESSAGE, 1)
            pyxel.text(50, 40, MESSAGE, 7)
            return

        # bg color
        pyxel.cls(12)

        # time bar
        pyxel.rect(
            50,
            2,
            90 if self.timebar > 90
            else self.timebar,
            4,
            11 if self.timebar > 60
            else 10 if self.timebar > 40
            else  9 if self.timebar > 20
            else  8
          )

        # draw item
        for x, y, is_active in self.item:
            if is_active:
                pyxel.blt(x, y, 0, 16, 0, 16, 16, 12)

        # draw special item
        for x, y, is_active in self.sp_item:
            if is_active:
                pyxel.blt(x, y, 0, 16, 16, 16, 16, 12)

        # draw bomb
        for x, y, is_active in self.bomb:
            if is_active:
                pyxel.blt(x, y, 0, 32, 0, 16, 16, 12)

        # draw player
        pyxel.blt(
            self.player_x,
            self.player_y,
            0,
            0,
            16 if (self.item[0][2] is False and abs(self.item[0][0] - self.player_x) < 12 and abs(self.item[0][1] - self.player_y) < 12)
                 or (self.item[1][2] is False and abs(self.item[1][0] - self.player_x) < 12 and abs(self.item[1][1] - self.player_y) < 12)
                 or (self.item[2][2] is False and abs(self.item[2][0] - self.player_x) < 12 and abs(self.item[2][1] - self.player_y) < 12)
                 or (self.item[3][2] is False and abs(self.item[3][0] - self.player_x) < 12 and abs(self.item[3][1] - self.player_y) < 12)
            else 32 if (self.bomb[0][2] is False and abs(self.bomb[0][0] - self.player_x) < 24 and abs(self.bomb[0][1] - self.player_y) < 24)
                       or (self.bomb[1][2] is False and abs(self.bomb[1][0] - self.player_x) < 24 and abs(self.bomb[1][1] - self.player_y) < 24)
                       or (self.bomb[2][2] is False and abs(self.bomb[2][0] - self.player_x) < 24 and abs(self.bomb[2][1] - self.player_y) < 24)
            else 48 if (self.sp_item[0][2] is False and abs(self.sp_item[0][0] - self.player_x) < 48 and abs(self.sp_item[0][1] - self.player_y) < 48)
                       or (self.sp_item[1][2] is False and abs(self.sp_item[1][0] - self.player_x) < 48 and abs(self.sp_item[1][1] - self.player_y) < 48)
            else 0,
            self.direction[0],
            self.direction[1],
            12,
          )

        # print score
        s = "Score: {:>3}".format(self.score)
        pyxel.text(5, 4, s, 1)
        pyxel.text(4, 4, s, 7)
        s = "Beer: {:>1}".format(self.items_got) + " Bomb: {:>1}".format(self.bombs_got)
        pyxel.text(5, 110, s, 1)
        pyxel.text(4, 110, s, 7)

        if not self.START:

            START_TEXT1 ="PUSH ENTER KEY"
            START_TEXT2 =": PLAYER"
            START_TEXT3 =": 100 pt / time +++"
            START_TEXT4 =":  10 pt / time +"
            START_TEXT5 =": -50 pt / time ---"

            pyxel.text(50, 23, START_TEXT1, 1)
            pyxel.text(49, 23, START_TEXT1, 7)

            pyxel.text(56, 43, START_TEXT2, 1)
            pyxel.text(55, 43, START_TEXT2, 7)

            pyxel.blt(37, 55, 0, 16, 16, 16, 16, 5)
            pyxel.text(56, 61, START_TEXT3, 1)
            pyxel.text(55, 61, START_TEXT3, 7)

            pyxel.blt(37, 73, 0, 16, 0, 16, 16, 5)
            pyxel.text(56, 79, START_TEXT4, 1)
            pyxel.text(55, 79, START_TEXT4, 7)

            pyxel.blt(37, 91, 0, 32, 0, 16, 16, 5)
            pyxel.text(56, 97, START_TEXT5, 1)
            pyxel.text(55, 97, START_TEXT5, 7)

            return

    def update_item(self, x, y, is_active):
        if is_active and abs(x - self.player_x) < 12 and abs(y - self.player_y) < 12:
            is_active = False
            self.score += 10
            self.items_got += 1
            self.timebar += 3
            self.player_vy = min(self.player_vy, -8)
            pyxel.play(3, 4)
            self.total_items += 1

        if self.GAMEOVER is False:
            x -= 3

        if x < -10:
            if is_active is True:
                self.total_items += 1
            if self.timebar < 0:
                x = 999999999999999
            else:
                x += 240
            y = randint(6, 104)
            is_active = True

        return (x, y, is_active)

    def update_sp_item(self, x, y, is_active):
        if is_active and abs(x - self.player_x) < 12 and abs(y - self.player_y) < 12:
            is_active = False
            self.score += 100
            self.timebar += 10
            self.items_got += 1
            self.player_vy = min(self.player_vy, -8)
            pyxel.play(3, 5)
            self.total_items += 1

        if self.GAMEOVER is False:
            x -= 5

        if x < -10:
            if is_active is True:
                self.total_items += 1
            if self.timebar < 0:
                x = 999999999999999
            else:
                x += 1900
            y = randint(6, 104)
            is_active = True

        return (x, y, is_active)


    def update_bomb(self, x, y, is_active):
        if is_active and abs(x - self.player_x) < 12 and abs(y - self.player_y) < 12:
            is_active = False
            self.score -= 50
            self.timebar -= 50
            self.bombs_got += 1
            self.player_vy = min(self.player_vy, -8)
            pyxel.play(3, 6)

        if self.GAMEOVER is False:
            x -= 2

        if x < -10:
            if self.timebar < 0:
                x = 999999999999999
            else:
                x += 231
            y = randint(6, 104)
            is_active = True

        return (x, y, is_active)

    def reset(self):
        #GAMEOVER FLAG
        self.GAMEOVER = False

        # Score
        self.score = 0
        self.items_got = 0
        self.bombs_got = 0
        self.total_items = 0

        # Starting Point
        self.player_x = 42
        self.player_y = 60
        self.player_vy = 0
        self.item = [((i+5) * 60, randint(6, 104), True) for i in range(4)]
        self.sp_item = [((i+1) * 950, randint(6, 104), True) for i in range(2)]
        self.bomb = [((i+4) * 77, randint(6, 104), True) for i in range(3)]
        self.timebar = 99
        self.end_bgm_flg = 1

        pyxel.playm(0, loop=True)
App()

プログラム実行ファイル

実行ファイルは、Google ドライブ上にzipファイル形式で公開してます。以下のリンクから、zipファイルをダウンロードできます。
origiri-kun_loves_beer_v1.0.zip - Google ドライブ

ダウンロードの完了後、解凍ソフト等を使用して、zipファイルを展開します。
Google ドライブなのでファイル改ざんの可能性は無いと思いますが、念のため、ウイルスソフトでファイルをスキャンすることをお勧めします。
exeファイルをダブルクリックすると、ゲームが起動されます。是非遊んでみてください。

まとめ

投稿が遅くなりましたが、夏休みの成果物として、プログラムのコードおよびプログラム実行ファイルを公開しました。
また、実際に作業している様子を動画にしていますので、Pyxelを使って実際にゲームを作ってみたいという方の参考になれば幸いです。
youtu.be

最後まで読んでいただき、ありがとうございました。