#6 PythonでRPGを作ろう!基礎が固まるコマンドラインで動くゲーム開発 | 初心者向け

python_rpg

皆さんこんにちは、「ゲームじゃないものは何か?」と聞かれると、実は世の中全部ゲームで出来ているのじゃないだろうか?という怖いループに陥っていしまう小幡です。人生良い事もあれば、悪い事もある、というのが最近の口癖です。

前回の記事は以下リンクからどうぞ。

前回行った事は以下の内容です。

  1. マップクラスを作成する
  2. メソッドの違いを確認する
  3. メイン関数でマップクラスを使う

メソッドの解説で記事の半分くらいのスペースとなってしまいましたが、クラスやらメソッドやらに出会うと嫌でも気を使わなければならない存在だと感じています。1人で全てまとめる個人制作のゲームを作るだけならば、クラスを1つも用意せずに作業しても良いかもしれませんが、Pythonの基礎を固める事を目標としていますので、不必要な方は読み飛ばしてもらった方が良いと思います。

ということで、前回までに作成したプログラムを実行すると、半角英文字がマップとして表示されるところまで来ました。

今回は、この少し見ずらいマップを列挙型を使って、全角文字に変換してみましょう。

列挙型とは?

Pythonの公式ドキュメントを見ても、わからない事が多いと思いますが、列挙型のドキュメントは量が少なく初心者でも比較的理解できる内容になっている様な印象です。

列挙型は、一意の定数値に束縛された識別名 (メンバー) の集合です。

https://docs.python.org/ja/3/library/enum.html

以上の公式ドキュメントからわかるように、「低数値とメンバーが紐づいているんだなぁ」ということがわかると思います。そしてその集合体であるということもわかるかと思います。

そして、概要説明でもお伝えした通り、「半角文字列を全角文字列に変換する」ということがしたいので、公式ドキュメントでいう所の「定数値に束縛(紐づける)させる」ということをすれば、うまく行きそうだというイメージを持ってもらえれば、列挙型のイメージはおおむね出来たようなものです。

さらに「集合」は何を示すかというと、今回の場合は「クラス」が1つの「集合」となります。それはこれから示すように列挙型を継承した新しいクラスを作成するからでもあります。

ここまでのイメージを持った状態で実際に使うMapItemクラスを見てみましょう。

MapItemクラスを作成する

今回の列挙型ではenumモジュールのEnumクラスを使用しますので、以下のコードをmap.pyの1行目に記述することが必要です。

from import enum import Enum

まずは最終的な形のコードを以下に示します。map.pyにクラスを作成している訳ですが、これはまだまだ改良の余地があると考えて居ます。

注意

列挙型を使いたいという理由だけで、ざっくりとした設計になっています。もっと良いコーディングがあると思いますので、自分なりに改良して貰えればと思います。

class MapItem(str, Enum):
    """マップアイテム一覧
    """
    def __new__(cls, value, map_item, title, description):
        obj = str.__new__(cls, value)
        obj._value_ = value
        obj.map_item = map_item
        obj.title = title
        obj.description = description
        return obj

    BLOCK = 'B', '#', '壁', '侵入不可エリア(壁)'
    EMPTY = 'E', ' ', '空地', '何もないフィールド'
    PLAYER = 'P', 'P ', 'プレイヤー', 'プレイヤーの現在位置'
    GOAL = 'G', 'G ', 'ゴール', 'ゴールの位置'
    WEAPON = 'W', '剣', '勇者の剣', '武器/勇者の剣/持っているだけで攻撃力アップ:'
    SIELD = 'S', '盾', '勇者の盾', '防具/勇者の盾/持っているだけで防御力アップ'
    HERBS = 'H', '薬', '薬草', '道具/薬草/使うとHPがすこし回復する'

それでは詳しく見ていきましょう。

class MapItem(str, Enum):

まずは1行目です。クラスMapItemをstr型とEnum型(列挙型)を継承して宣言しています。2つのクラスを継承するというのは、難しいと感じるかもしれませんが、「文字列を使う、列挙型だ」と理解してもらえれば良いと思います。ではなぜstr型も継承しなければならないのか?という疑問が浮かぶかと思いますが、これは、先ほどの公式ドキュメントを読み返すとわかるかもしれませんが、「一意の定数値」に紐づけるものだというのが肝です。

つまりBLOCK = 1, ITEM = 2, など定数の集合です。「一意」ということで、数値を使うのが基本ですが、strクラスの力を借りて文字列型に紐づけてしまおうという魂胆です。

簡単にまとめると、Enumクラスを継承しつつ、strクラスも継承して文字列を扱えるようにしますというイメージです。

def __new__(cls, value, map_item, title, description):

4行目は__new__そ使用しています。これは__init__と似ていますが、必ずインスタンスを作成して返却されるものというイメージで大丈夫だと思います。つまりインスタンスを生成するために存在するクラスという認識です。逆に考えるとインスタンスを生成しないで利用するということは無いということです。後述しますが__init__と違い、必ず末行にはreturnで自分自身を返却します。

この__new__はかなり難解です。1度で理解できるものではないと思いますので、気にせず先に進みましょう。

引数には、宣言する定数に与えたい要素を付与するイメージです。’E’(value)と言う半角英文字に’ '(map_item)という全角スペースを付与すると同時にそのtitle(タイトル)とdescription(説明)も付与しているという事になります。これにより最初に紹介した少し見ずらいマップで表示されていた’E’に複数の意味を持たせることができるようになります。

この仕組みを実現するために以下の様なインスタンス生成するための処理を記述しています。

5行目でも__new__が使われていますね。ここでstr型のサブクラスのオブジェクト(obj)を生成しているイメージです。その後オブジェクトのメンバにそれぞれ引数の値を代入していることになります。メンバーに値を代入したオブジェクトをreturnすることでインスタンス生成が完了します。

6行目では”obj._value_”に値を代入していますが、この”_value_”はプライベートな値で、普段使う事はないと思います。今回は”__new__”で生成しているオブジェクト内だから行っているという特殊ルールのような作業です。他の値はobjのメンバになりますが、これだけは特別扱いしているイメージです。ここの値には、一意な半角英数字を文字列として入れておきます。ここで入れた文字列が登場した時に、以下の値が入手可能になるということです。

7・8・9行目は、それぞれobjにメンバとして値を入れています。”map_item”はフィールドマップに変換して表示される全角文字列を入れます。”title”はアイテム使用画面などで表示させたいそのobjの名前を入れます。そして”description”は説明ですので、そのobjの説明を入れます。

これもすぐに全部理解することは難しいと思いますが、とにかくこのようにオブジェクトに様々な情報を持たせることができることは非常に便利ですし、Enum型で作成していることにより、様々な恩恵を受ける事ができますが、その話はまた今度にしておきます。今回は1つの定数にたくさんの情報を与えているということを覚えて貰えれば十分だと考えて居ます。

続けて12行目から18行目は列挙型クラスの中身である定数を定義しています。

BLOCK = 'B', '#', '壁', '侵入不可エリア(壁)'

BLOCKを例に説明します。

これは全て大文字である変数ということでも表している様に、一意の変数ということで定数です。小文字で書いてもコードとしては成り立ちますが、コーディング規約的には不適切ですので定数は全て大文字で書きましょう。

代入しているものは、左から右までの順で’B’, ‘#’, ‘壁’, ‘侵入不可エリア(壁)’が、先ほど作成した”__new__”の上から下の”value”, “map_item”, “title”, “description”に対応しているイメージです。

  • B -> value
  • # -> map_item
  • 壁 -> title
  • 侵入不可エリア(壁) -> description

この構造は、次に説明する実際の動きを見て貰った方が理解できるかもしれません。

もっと機能拡張したい時は?

今回の場合はわかりやすい様に、半角英字1文字に全角文字1文字を定義しましたが、例えば武器の種類が30個などに増やしたい場合があると思います。その時はどうすれば良いでしょうか?

その時に最も単純な方法で思いつくのは、定数に番号を与えてみることです。今までは’W’をウェポンのWとして扱ってきました。ウェポンなので武器ですね。マップ表示には「剣」にしようとしました。マップ表示は「剣」のままでも良いかもしれませんが、取得する時は色んな剣があったほうがゲームとして面白いでしょう。そういう場合は’W01’=>’こんぼう’、’W02’=>’ひのきのぼう’というように、番号を与えてあげることで、titleを変更することができたりします。

さらにほんの少しだけ発展させるとしたら、今回まとめてアイテムやフィールド要素、フィールドマップの2次元配列を外部ファイルに書き出すという選択肢も出てくるかと思います。pythonと言えばCSVの入出力の記事をよく見ますし、フィールドマップもコンマで区切った簡単な作りですので、簡単に切り出すことが出来そうです。

このように、最初に提示したコードはあくまでも例ですので、いくらでも機能拡張できますので是非オリジナルのアイテムをたくさん作成してみてください。そうすることで列挙型の使い方にも慣れていくことができると思います。

列挙型を呼び出す

さて、列挙型の仕組みがわかったところで、前回作成した2次元配列の半角英字を列挙型の”map_item”で置換することを想定して実際に列挙型を呼び出してみましょう

例ではマップにある”W”を”剣”を変換してみます。つまり半角英字から全角文字列に置換してみましょう。以下の様なコードになります。

weapon = 'W'
print(MapItem(weapon).map_item)

このprint関数の結果は”剣”になるわけですが、順を追って説明してみます。

まず”MapItem()”でクラスのコンストラクタを使っていることがわかるかと思います。普通のクラスであればコンストラクタでインスタンスを生成する訳ですが、それに”weapon”を入れて渡してあげる事によって”__new__”される時に、”value”に相当する定数に変換され新しいオブジェクトが生成されます。

つまり”w”が定数WEAPONになり、それぞれのメンバが付与されるというイメージです。

そして定数WEAPONをもとにインスタンスが生成された後に、”.map_item”によってインスタンスのメンバを呼びだしているというイメージです。インスタンスの生成やら、定数の適用やら、メンバの呼び出しなどを一回で行っているという事ですね。

2次元配列を列挙型で置換する

以上の呼び出し方法を使って本題に入っていきます。

ようやく本題です。やることは単純な文字列の置き換えですので、これをどのようにコードにすれば上手く行くかを考えてみましょう。

私が実装したコードは以下になります。map.pyのMapクラスのshowメソッドを更新しています。

    def show(self):
        """マップを表示
        """
        # 二次元配列の文字列を、コマンドライン表示用に変換
        for array in self.map_lists:
            map = ''
            for string in array:
                map = map + MapItem(string).map_item
            print(map)

前回までのコードと何が変わったのか?というほど変化が少ないと思います。これは列挙型で準備を整えていたから変更点が少なく出来たと言っても良いでしょう。

さて、実際に変更したコードは8行目です。

map = map + MapItem(string).map_item

前回までは”string”をそのまま”map”に結合して再代入していましたが、今回は先ほど説明したとおり最終的にMapItemクラスの”map_item”を呼び出しています。

以上の様に列挙型をすることで、期待通り指定した文字列に変換して”map”に格納することができると思います。結果を実際に標準出力したフィールドマップが以下です。

python_rpg
ああえあ

移動可能な範囲を全角空文字、プレイヤーの現在位置をPと表示しており、取得できるアイテムは薬、剣、盾、と表示させています。

インスタンスを生成することについて

今回は列挙型をpythonの基礎固めのために無理やり使った感じがあります。実際に実装する手法かと聞かれるとNOだと思います。FORループを単純に2重にしていることや、MapItemクラスのインスタンスをフィールドマップがある分だけ生成しているので、処理量が少ないとは言えません。

しかし私は、最初は気にしなくて良いと考えて居ます。とにかく1つ完成できるように学習を進めて良ければ良いと思います。作業になれてきたときに、「もっと見やすいコードにするためには?」「処理が速いコードとは?」という事を考えて行けば良いと思います。

それはいつなのか?と聞かれたら、一通りのコードができるようになり、個人制作ではなくなって、テストコードもガシガシ書くようになったときかもしれないと答えるかもしれません。

Pythonを採用しているプロジェクトでメモリの開放や、処理速度を極端に早くしたいということを目指しているプロジェクトは存在しないと考えています。そのプロジェクトはC言語などで書く必要があると考えるからです。

Pythonでメモリを気にし始めるのはビッグデータの処理やAIの研究が序盤を過ぎた頃からではないでしょうか?と考えて居たりします。

しかしながら制作しているものがプログラムである以上、軽量で高速に動作するものは好まれますので、適宜調整が必要だとも思います。

まとめ

今回は二次元配列の半角英字を列挙型を使用して全角文字列に変換してみました。

列挙型の定義は少し複雑なので、慣れるまでは一意の定数であるという事を忘れずに作業すると良いかもしれません。

またコードの書き方もたくさんの方法があるということも頭に入れておきたいところです。今回のコードも決して100点満点のコードではありません。たくさんのコードに触れてより良いコーディングを目指してもらえればと考えています。

さて、マップの表示はこのくらいにして、次回はマップの移動について考えていきたいと思います。

最後まで読んで頂きありがとうございました。また次回もお会いできることを楽しみにしております。

以下のDiscordサーバでも質問を受け付けています。お気軽に声をかけて頂けると幸いです!

この記事を書いた人

小幡 知弘

1990年茨城県神栖市生まれ
2013年大阪芸術大学卒業
Python×Webエンジニア