BioErrorLog Tech Blog

試行錯誤の記録

初心者がPythonでゼロからゲームを作ってみた | デザインから実装まで

Pythonでゲームを自作してみました。 ゲーム制作にあたって行った準備から、実装時にハマった注意点、出来上がったゲームのデザインまでを記録します。

はじめに

何かを作りたい

何かを作ってみたいと思いました。

ここ最近、初心者ながらコードの書き方がだんだんわかってきたので、さっそく何か新しいものを作りたくなったのです。

プログラミングで作るもの、として真っ先に思い付いたのが、ゲームでした。 私はそこまで大のゲーム好きという訳ではありませんが、ゲームは身近でイメージしやすく、作るのも楽しそうです。

そんなわけで、ゲームを自作することに決めました。


なぜPythonを選んだのか

このゲーム作りにPythonを選んだ理由は、Pythonがとても書きやすい言語だと思ったからです。

Pythonは文法がシンプルで、一つの実装にごちゃごちゃとコードを書く必要がありません。

例えばJavaとPythonを比べてみると、「ゲームウィンドウを表示する」という同じ目的でコードを書いても、必要なコード量が全然違います。

かわりにPythonは比較的処理が遅く、ゲーム制作用としては不人気のようです。 しかし、コードを簡単に書けるというのは、私のような初心者にはとても重要なことです。

また、Unityのようなゲームエンジンを使うことも考えましたが、今回は自分の手でゲームを作ることが目的だったので、不採用としました。

Pythonにはゲーム制作用のライブラリ"pygame"があります。 今回はこれを使って、ゲームを自作しました。


作業環境

Pythonバージョン情報 ↓

Python 3.7.2  
pygame 1.9.5

Windows10上で開発しました。

※Pythonのインストール方法
はじめてのPython | Windows環境構築 - Atom - BioErrorLog Tech Blog


ゲーム概要

こちらが、最終的なゲームの概要です。


ゲーム自体はitch.ioで配布しています。
もしよければ軽く触ってみてください:


ソースコードはGitHubに置いています。
ソースコードからゲームを実行するには、以下の手順をご参考ください。

# pygameをインストール
pip install pygame

# ソースコードをクローン
git clone https://github.com/bioerrorlog/CellForRest_Pygame.git

# ゲームを実行
cd CellForRest_Pygame/game/
python CellForRest.py


何を作ったのか、どのようにして作ったのか、どこにつまずいたのかを、順を追ってこの記事内で書き出していきます。

なお、私はプログラミングを始めて間もない初心者ですので、私の書いたコードが最善のやり方であることはまずないと思います。 書いてるときは常に、もう少しマシなやり方はないものかと苦悩してました。

かといって、悩み続けていてもゲームは完成しません。 初心者がとりあえず動くものを作ることを目的に書き上げたものだと思ってもらえれば幸いです。

それでは、ここまでの過程を一つ一つ振り返っていきます。


[関連記事] Devlog #1 ChatGPTを使ったゲームを作る実験をはじめる


Pythonでゲームを作る

Python / pygameの基本を知る

チュートリアルをやってみる

まずは、Pythonでのゲーム制作の基本を調べるところから始めました。

ゲームを作りたい、といきなり思い立っても、作り方が全く分かりません。 どうやってウィンドウを表示すればいいのか、動く画面はどうやって実装するのかなど、ゲーム作りの基本を一つも知らなかったのです。

そこでまずは、ネット上にあるチュートリアルを通して最低限の基本を身につけることにしました。 私は文章よりも動画で見る方が好きなので、Youtubeで「Python game dev」などで検索して、一番再生されてそうなsentdex氏のチュートリアル動画をやりました。

英語の動画になってしまいますが、ソースコードと書き方の手順がわかりやすく、あまり聞き取れなくても問題なく基本を追えるチュートリアルとなっていました。 また、ネットの情報は圧倒的に英語のものが多いので、早いうちに英語に慣れるという意味でもちょうどいいチュートリアルだったと思います。

チュートリアルを通して、

・ゲームウィンドウの表示
・画像の表示
・ゲームループの実装
・スタート画面 -> ゲーム画面/メニュー画面の流れ
・テキスト/ボタン表示の実装
・マウス/キーボードイベントの検知

など、基本となるゲーム実装のやり方を知ることができました。

私のように知識ゼロの状態からゲームを作ろうと思っている方は、どれでもいいのでチュートリアルをさらっとやることをお勧めします。 自分で一つのゲームを作ることに対して、勇気と希望を抱くことができます。


参考になったゲーム制作関連の情報

次に、制作を進める中で参考になった情報を紹介します。 早めに知っていればもっとスムーズに作れたなあと思ったので、ご参考までに。


1.初心者のためのpygameガイド

これは、pygame公式の初心者ガイドの日本語訳です。 初心者が知っておくべき13の心得が紹介されています。

私も後で説明しますが、処理スピードの話などは特に重要なポイントでしたので、早めに目を通しておくと良いかと思います。


2.Game Programming Patterns

ゲームプログラミングについてのデザインパターンが書かれた記事(英語)です。 日本語訳されたものは書籍として出版されていますが、英語のhtml版はネットで公開されています。

私には内容が高度に感じたのであまり真面目には読んでませんが、暇なときに読んでみると、たまに重要なヒントが得られました。 英語に慣れる練習にもなります。


3.redditのpygame板

redditは、海外の有名な掲示板です。 そこのpygame板では、毎日pygameユーザーによる議論が行われています。

もちろん、本当は日本語のpygameコミュニティーがあればよかったのですが、動いている日本のpygameコミュニティーを私は見つけられませんでした。 代わりにこのredditに入って、同じくゲームを作っている人を見てました。 モチベーションを高めるためにも、何かしらのコミュニティーを探すといいと思います。

振り返って特に参考になったと感じるのはこの3つです。 とはいえ、恐らくまだまだネット上にはたくさんの有益な情報が溢れていると思いますので、あくまで参考までにどうぞ。


[関連記事] Godot EngineでBoids Flockingシミュレーションを実装する | 人工生命


ゲームデザインを考える

デザインはどんどん変わっていった

ある程度ゲームのつくり方が理解できたら、つぎは自分がどんなゲームを作るのかを考えました。

とはいっても、作り始める前に考えていたデザインは、ほとんど跡形もなく変更してしまいました。 実装を進める中で、やっぱりデザインを変更しよう、別の方法で実装しよう、などとやってるうちに、当初作っていたものとは完全に別物になったわけです。

逆に今思えば、作り始める前にゲーム性を確定しようとするのは愚かだったと思います。 私はプログラミングの経験が浅く、どのような実装が難しいのかを把握しきれていません。 さらにはゲーム制作も初めてなので、どんなゲームにしたいのかも、現物を見ないとイメージできません。

なので、気楽にスケッチを描き始めるような感覚で、気軽に実装を進めることにしました。 まず書き始め、気に入らなければ躊躇なく変更し、頭の中のイメージに近づけていきます。

恐らくは個人が趣味で作るときのみに許される、自由で楽しい作り方です。 おかげで楽しくゲームを作り上げることができました。


最終的なゲームデザイン

さて、肝心の最終的なゲームの形ですが、クッキークリッカーのようなものに落ち着きました。

クッキークリッカーとは、Wikipediaの説明を借りると、

画面に現れるクッキーを1回クリックするごとに1枚(アイテムで1クリックあたりの枚数を増やすことができる)クッキーを焼くことができる。焼いたクッキーはクッキーの生産施設購入費用に充てることができ、次第に大量のクッキーが手に入るようになる仕組みをとっている。

つまり、クッキーを増やすだけのシンプルなゲームです。
ゲームを進めるにつれ指数的にクッキーが増えていく様子には、ある種のレベルアップ中毒のようなものがあり、私もえらくハマったのを覚えています。


今回出来上がったのは、3つのステージを行き来しながら、クッキーならぬ「Leaf」という値を増やしていくゲームです(Fig. 1)。

Fig. 1 ゲームの全体像
スタート画面からゲームをスタートし、「Cellステージ」「Treeステージ」「Caveステージ」をボタンで行き来しながらゲームを進めていきます。


Cellステージ(Fig. 2)、Treeステージ(Fig. 3)、Caveステージ(Fig. 4)には、それぞれ異なった役割があります。

Cellステージから順に、軽くゲーム画面を説明します。

Fig. 2 Cellステージ
Cellステージでは、手動クリックによる採取によって「Leaf」が獲得されます。

①: ステージ移動ボタン
  ->ほかのステージに移動する。
②: Leaf
  ->「Leaf」の残高が表示される。
③: Gate
  ->クリックで「Cell」を生成する。
  ->「Cell」の生成には「Leaf」を消費する。
④: Cell
  ->うねうね動き回る緑色のやつ
  ->ある程度動いた後に出芽して「Bud」になる。
  ->出芽する前に1回分裂して「Cell」を生む。
⑤: Bud
  ->「Cell」の成れ果て
  ->クリックで採取できる。
  ->採収すると「Leaf」が獲得される。


Fig. 3 Treeステージ
Treeステージにある「Tree」のレベルに応じて、「Leaf」が自動的に増えていきます。 その増加幅は「Human」の数に応じて倍増します。
※語呂の関係で、ステージ遷移ボタンでの名称は「Tree」ではなく「Forest」となっています。

①: ステージ移動ボタン
  ->ほかのステージに移動する。
②: Leaf
  ->「Leaf」の残高が表示される。
③: Tree
  ->レベルに応じて「Leaf」を自動加算する。
  ->レベルに応じて大きくなる。
  ->レベルアップには「Leaf」を消費する。
④: House
  ->クリックで「Human」を生成する。
  ->「Human」の生成には「Leaf」を消費する。
⑤: Human
  ->数に応じて「Tree」の効果が倍増する。
  ->地面を動き回る。


Fig. 4 Caveステージ
Caveステージにある「BlueGem」のレベルに応じて、Cellステージでの「Leaf」獲得量が倍増します。

①: ステージ移動ボタン
  ->ほかのステージに移動する。
②: Leaf
  ->「Leaf」の残高が表示される。
③: BlueGem
  ->上下にゆらゆら揺れる。
  ->レベルに応じてCellステージでの「Leaf」獲得量が倍増する。
  ->レベルアップには「Leaf」を消費する。
  ->レベルに応じて周りの青いやつが増える。

今回出来上がったゲームは以上のような感じです。


[関連記事] 人工生命をつくりたい - 思うところとアプローチのメモ | ALife


実装する

それではここから、ソースコードについての注意点を書き出していきます。 どのようなことに苦労したのか、将来の自分に向けてメモを残すつもりでやっていきます。

ソースコードはGitHubに置いてあります。

ソースコードの全体像

まずは、ソースコード全体の構造(Fig. 5)をまとめます。

Fig. 5 ソースコード全体像
gameInit()メソッドによってスタート画面を表示し、スタート画面でStartボタンが押されるとgameLoop()メソッドが開始します。gameLoop()メソッドの中ではゲームループがまわり続け、ゲームが進行していきます。

ゲームループの中では、3つのステージそれぞれに対応するLayerManagerクラスのオブジェクトが動いています。LayerManagerクラスは、それぞれのステージで登場するキャラクターのオブジェクトを生成・保持・削除します。

button()メソッドやtextDisplay()メソッドなど、ゲームを通して使用するメソッドはクラス外で宣言し、各クラスが使用するようにしました。

全体像はざっとこんな感じです。 ここからは部分部分について、注意が必要だったところのメモを残していきます。


ゲームループ

ゲームループとは、ゲームを静止画ではなく動画として表現するための仕組みです。

一つのwhileループ内でゲーム処理と描画処理を実行し、ループを繰り返します。

今回私は次のように実装しました。
以下gameLoop()メソッド部分のみの切り抜きです。

# Game loop: event.get() -> update() -> draw() -> display.update() -> clear()
# Close button: Quit game
# m button: Menu
# Left click: Inform each game layer via setMouseEventUp()
def gameLoop():
    global intro
    intro = False
    game_exit = False

    cellLayerManager = CellLayerManager()
    treeLayerManager = TreeLayerManager()
    caveLayerManager = CaveLayerManager()

    while not game_exit:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                gameQuit()
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_m:
                    menu()
            elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
                # cellLayerManeger uses pygame.mouse.get_pressed()
                treeLayerManager.setMouseEventUp(True)
                caveLayerManager.setMouseEventUp(True)

        cellLayerManager.update()
        treeLayerManager.update()
        caveLayerManager.update()

        if cell_layer == True:
            cellLayerManager.draw()
        elif tree_layer == True:
            treeLayerManager.draw()
        elif cave_layer == True:
            caveLayerManager.draw()

        pygame.display.update()
        clock.tick(30)

        cellLayerManager.clear()
        treeLayerManager.clear()
        caveLayerManager.clear()

流れとしては、各ステージに対応したLayerManagerクラスのオブジェクトをそれぞれ一つずつ生成してからループに入り、

1.イベントの取得: pygame.event.get()
2.更新処理: update()
3.描画処理: draw()
4.ディスプレイ更新: pygame.display.update()
5.更新後処理: clear()

これをループで繰り返しました。

イベントの取得では、キーボードやマウスのボタンイベントを取得します。 ここについては少し注意が必要でしたので、別途後述したいと思います。

update()をループごとに必ず一度呼び出されるメソッドにして、各ステージの数値更新が主に行わせました。 一方draw()は3つあるステージのうち表示中のステージのみで実行されるようにしました。

pygameによるディスプレイの更新では、clock.tick()でfpsを指定できます。 fpsとはframes per secondのことで、一秒間に何回画面を更新するかの単位です。
今回私は30fpsにしました。 ゲームが進んで処理オブジェクトが増えても、30fpsを下ることはあまりなかったからです。 逆にこれを60fpsとかに設定してしまうと、処理オブジェクトが増えていったときに処理の低下が目立ってしまいます。

更新後処理clear()も定義して、ディスプレイの更新後に値を初期化するときなどに使いました。


3つのLayerManagerクラスに以上のupdate()メソッド、draw()メソッド、 clear()メソッドを必ず持たせて、それらをゲームループでフレームごとに順番に呼び出させるという訳です。

以上が、今回作ったゲームループでした。


クリックイベントが検出されない原因と対処

開発途中に、なぜかマウスのクリックが検出されないという事態にハマりました。 左クリックを検出するコードを書いたのに、2回に1回ほどしか認識されなかったのです。 この原因と対処法について、メモを残します。

まず、マウスやキーボードの入力を検出するには、二つの方法があります。
初心者のためのpygameガイドには、次のように書いてあります。

最初の方法は、入力デバイスの状態を直接チェックすることだ。 これは、たとえば pygame.mouse.get_pos() や pygame.key.get_pressed() なんかを呼ぶことで実現できる。 これは その関数を呼んだ時点での 入力デバイスの状態を 教えてくれるだろう。

このようなpygame.mouse.get_pressed()などを使うやり方は、クリックをしている間ずっと検出し続けるので、検出漏れはありません。 しかし一方で、一度のクリックで何回分もクリックが検出されてしまうので、場合によっては使い勝手がよくありません。

例えば、ボタン押下でレベルアップを行う処理を行う場合、1回だけレベルアップしたつもりが、何回も(限界まで)レベルアップが行われてしまう、ということになります。


1回のクリックで1回のクリック検出を行いたいときは、二つ目の方法で実装する必要があります。

2番目の方法は SDL のイベントキューを使うことだ。 このキューはイベントのリスト -- イベントが 検出されると、リストに追加される -- になっている。 そしてこれらのイベントは取り出されるとリストから消える。

こちらはpygame.event.get()を利用する方法です。 これを使うときに注意しなければいけないのは、pygame.event.get()を呼び出すたびに"イベントが取り出されてリストから消える"ということです。

つまり、複数の場所でpygame.event.get()を書いてしまい、一つのゲームループの中で複数回pygame.event.get()を呼び出してしまうと、検出漏れが起きてしまうということです。

そこで、今回私は次のようにゲームループの中で1回pygame.event.get()を呼び出し、その結果を各LayerManagerに渡すというやり方をしました。

def gameLoop():
・
・中略
・
    while not game_exit:
        for event in pygame.event.get():
            if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
                # cellLayerManeger uses pygame.mouse.get_pressed()
                treeLayerManager.setMouseEventUp(True)
                caveLayerManager.setMouseEventUp(True)

for event in pygame.event.get():とすることで、各マウスクリックやキーボードのボタン押下状態を一つずつ取り出して検証することができます。 今回の場合だと、event.type == pygame.MOUSEBUTTONUPでマウスボタンがUPしたことを検出し、event.button == 1でそれが左クリックであることを認識しています。

それを各LayerManagerに作成したsetMouseEventUp()メソッドにTrueとして渡しているのはとてもダサいですが、動くのでまあ良しとしました。

同様のやり方でキーボード押下や閉じるボタンの処理を記述することができます。 詳しくはソースコードや、pygameのドキュメントをご参考ください。


画像ファイルの読み込み

画像ファイルの読み込みにも、すこし注意が必要でした。

当初、私は単純に次のようにして画像を読み込んでいました。

img = pygame.image.load('img.png')

しかし、これはあまり上手いやり方ではありません。 convert()を使っていないからです。

初心者のためのpygameガイドにも、次のようにあります。

最初に surface.convert() の説明を読んだとき、 ぼくはそれがそんなに気にかけるほどのものだとは思わなかった。 「自分は png しか使わないから、ぼくが扱うものはすべて 同一の形式になるはずだ、だから convert() なんか必要ない」 -- でもこれは激しく間違っていることがわかった。

私もまったく同じ状況でした。

convert()の機能は、pygameドキュメントによると、

何も引数を定しなかった場合は、SurfaceはディスプレイのSurfaceと同じピクセル形式になります。この方法を行うと、常に使用ディスプレイ環境での画像描写に最も高速化された形式となります。画像描写処理を行う前には、Surface.convert()命令でピクセル形式を最適な状態に変更するとよいでしょう。

あまりよく分かりませんが、「ピクセル形式」をディスプレイと同じに変えることで、画像描写処理を高速化してくれるそうです。

convert()を適用するには、次のようにして画像を読み込みます。

img = pygame.image.load('img.png').convert()

末尾に.convert()をつけるだけです。 ちなみに、png画像などで透明度を保持する必要がある場合は.convert_alpha()を使います。

私の場合、背景もpng画像で表示していたため、このconvert()/convert_alpha()を適用することで画像描写処理が劇的に改善しました。 fpsで言えば、大体3倍以上の違いが出ました。

画像の読み込み時にはconvert()を忘れないことが大切です。


キャラクターの作画と描画

キャラクターの作画と描画処理についても、少し書いておきます。

まず、絵描きツールは、次の二つを使いました。

Inkscape
無料のベクターグラフィックツール。
ペイントツールでフリーハンドで書くよりも、Inkscapeで図形や線・曲線を組み合わせて描いた方が絵心の無さがごまかせるので、こちらをメインに使っていました。

FireAlpaca
無料のペイントツール。
こちらでキャラクターそのものを書くことは少なかったですが、レイヤーエフェクトをかけるのがとても簡単なので時折使っていました。

基本的にはInkscapeでキャラクターを描き、FireAlpacaでエフェクトをかける、ということをしていました。

エフェクトをかける、というと大げさですが、画像全体の色味を少し変えるという程度の話です。 マウスオーバー時にキャラクターが少し明るくなるような仕様にしたかったため、各キャラクターに色違いの画像を用意しました(Fig. 6)。

Fig. 6 マウスオーバー時に表示する画像を変える

マウスオーバー時に表示画像を入れ替える描画処理は、CellTreeなどのキャラクタークラスのdraw()メソッドに、次のようにして実装しました。

def draw(self):
    mouse = pygame.mouse.get_pos()
    if self.x+self.width > mouse[0] > self.x and self.y+self.height > mouse[1] > self.y:
        game_display.blit(self.act_img, (self.x, self.y))
    else:
        game_display.blit(self.inact_img, (self.x, self.y))

mouse = pygame.mouse.get_pos()でマウス位置を取得し、X方向mouse[0]とY方向mouse[1]が画像位置内に収まっていればマウスオーバー用の画像act_imgを、そうでなければ元の画像inact_imgblit()で表示させるのです。

このキャラクタークラスのdraw()メソッドはLayerManagerクラスのdraw()メソッドが呼び出し、そして今度はゲームループがLayerManagerクラスのdraw()メソッドを呼び出すことで、画面にキャラクターが描画されるというやり方をとりました。

反省点

今回のこのプロジェクトの目的は、とりあえず何かを作ってみる、ということでした。 なので、出来上がったものにはたくさんの反省点があります。

まず、ゲームバランスの調整をほとんどやってないことです。 作ることが目的だったために、誰かに遊ばれることを想定していません。 なので、ゲームの肝となるゲームバランス調整には、ほとんど時間を割いていません。

加えて、このゲームにはセーブ機能がありません。 このような放置系ゲームにおいては致命的でしょう。 pygameでセーブ機能を実装するのは少し厄介そうだったので、断念しました。 志が湧いてきたら、挑戦してみるのも面白いかもしれません。

コードについては見返すたびに反省点が浮かびますが、まずはソースコードを一つのファイルに詰め込んだのはよくなかったと思います。 参考にしたゲーム制作チュートリアルがオブジェクト指向でなかったため、そのまま中途半端な感じになってしまいました。 各クラス間の依存関係を薄くして、それぞれファイルを分けた方が見やすいですし、再利用もしやすいでしょう。

また、今回はクラス継承という機能を利用しませんでした。 似たようなクラスがあるにも関わらず、それぞれが特に親クラスを持っていないため、共通項が取れずにコードが読みずらくなっています。

次また何か作るときは、改善していきたいです。


おわりに

今回は、Pythonでゲームを自作してみました。

プログラミング練習のようなものではなく、自分の頭で考えて作った初めてのものになります。 おかげでいろいろと面白い経験ができました。

思えば結構な時間がかかりました。
朝にコーヒーを飲みながらのコーディングは、ささやかな楽しみでもありました。

今回の記事はかなり長いものになってしまいました。
やりたいことは他にもたくさんあります。
この辺でこのゲームに区切りをつけ、次に行きたいと思います。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

YouTube

初心者のための pygame ガイド

A Newbie Guide to pygame — pygame v2.5.0 documentation

Table of Contents · Game Programming Patterns

Reddit - Dive into anything

クッキークリッカー - Wikipedia

event - Pygameドキュメント 日本語訳

surface - Pygameドキュメント 日本語訳

Draw Freely | Inkscape

フリー ペイントツール(Mac/Windows 両対応)FireAlpaca[ファイア アルパカ]