Operation KiWi

一生使える言語はPythonだと信じてる

Python3でRPGを作る時に使えそうな技術の断片 -マップビューア作成-

f:id:sakage24:20170628200017p:plain

前回の記事では、2次元配列を用いたワールドマップを作成しました。今回はファイルの整理、マップビューアを作成しましょう。

2017/07/02追記

浅いコピーをしろ…みたいに書いていましたが、深いコピーをしないと後々おかしな挙動が起こることが分かりました!!該当のコードは修正しましたが、ミスったコードを使っている方もいるかもしれませんので、打ち消し線を付けた上で、残しておきます。すみません。

前回のあらすじ

www.kiwi-bird.xyz

今回やること

  1. パッケージを作成しよう
  2. WorldMapクラスを、作成したパッケージ内の別のファイルに引っ越ししよう
  3. マップビューアを作成しよう

今回は以上3つの作業を行っていきます。頑張っていきましょう!!

パッケージを作成する

Oh…我々のアプリケーション…思い返せば、初めは10行にも満たないコードから始まり、発展してスタンドアロンアプリになり、いよいよもって大規模アプリへの道へと羽ばたこうとしています。さあパッケージ化しましょう。

…引用機能を使いたかっただけです。別に大したことしないのでご安心をw

このまま一つのファイルに書いていくと管理が大変なのとブログ的にもちょっと辛いので、パッケージ化をしてコードを分割、保守しましょう。

作り方

  1. ディレクトリを作成する。(例:sources/)
  2. 作成したディレクトリに__init__.pyを作成する(空のファイルで平気)。

簡単ですね。PyCharmを使っている方は以下の手順でも簡単に作成出来ます。

File→New-Python Package→好きなディレクトリ名を入力

出来ましたでしょうか?作業ディレクトリが、こんな感じのファイル構成になっていればオッケーです。

work_dir/
  • run.py
  • sources/
    • __init__.py

WorldMapクラスを、作成したパッケージ内の別のファイルに引っ越ししよう

引越し作業

  1. sourcesディレクトリにmaps.pyを作成する。
  2. maps.pyWorldMapクラスを切り取って貼り付ける。
  3. 変更に伴ってrun.py内を少し書き換える。

やってみよう

作成&貼り付け

run.py
# run.py
from sources.maps import WorldMap  # 追記
import pprint


pp = pprint.PrettyPrinter()
world_map = WorldMap()
world_map.set_village(y=2, x=4)
world_map.set_village(y=8, x=3)
pp.pprint(world_map.get_world_map())
sources/maps.py
# maps.py
class WorldMap(object):
    # MAPの高さ
    __height = 10
    # MAPの横幅
    __width = 10
    # 草原
    __grass = 0
    # 村
    __village = 1
    # height * widthの2次元配列
    __world_map = list()

    def __init__(self):
        WorldMap.__world_map = [[0 for x in range(WorldMap.__width)] for y in range(WorldMap.__height)]

# ↓値は直接操作せず、ゲッター、セッターを定義して使う(任意)↓

    @classmethod
    def get_height(cls):
        return cls.__height

    @classmethod
    def set_height(cls, n):
        cls.__height = n

    @classmethod
    def get_width(cls):
        return cls.__width

    @classmethod
    def set_width(cls, n):
        cls.__width = n

    @classmethod
    def get_world_map(cls):
        return cls.__world_map

    @classmethod
    def get_rect(cls):
        return cls.__height, cls.__width

    @classmethod
    def set_grass(cls, y, x):
        cls.__world_map[y][x] = cls.__grass

    @classmethod
    def set_village(cls, y, x):
        cls.__world_map[y][x] = cls.__village

run.pyを書き換える

簡単です。引越し先のsources/maps.pyからWorldMapクラスが欲しいのですから、その通りimportしてあげれば良いのです。

# run.py
import pprint
from sources.maps import WorldMap # 追加

実行結果

[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

前回の最後と変わりなく出力されれば成功です。これでクラス管理が楽になって(ブログ的にも)大助かりですw

マップビューアを作ろう

マップビューアって何?造語?

私が名付けました。母体となるworldMap().__world_mapの2次元配列を実際に書き換えること無く、コピーしてそれを書き換えて表示する機能を作っていきます。

やること

  1. sourcesディレクトリにview.pyを作成しよう
  2. view.pyViewFilterクラスを作成しよう
  3. ViewFilterクラスを作り込んでいこう

構成

work_dir/
  • run.py
  • sources/
    • __init__.py
    • maps.py
    • view.py # 今ココ

サクッとやっていきましょう!!

sourcesディレクトリにview.pyを作成しよう

view.pyViewFilterクラスを作成しよう

省略します。

ViewFilterクラスを作り込んでいこう

ViewFilterクラスはWorldMapクラスの2次元配列をコピーして中身を書き換えてユーザに表示するクラスです。そのために必要な情報が3つあります。

  • WorldMap.__world_map浅いコピー深いコピー
  • WorldMap.__heightの値
  • WorldMap.__widthの値

突然ですがちょっと一息…浅いコピーと深いコピーについて

Python公式ドキュメントに詳細があります。

Pythonにおけるリスト操作には罠が仕掛けられています。リストをそのまま変数に代入すると、値では無く参照渡しが行われます。気づかずにやってしまうと予期しない挙動をしたりするので十分に注意が必要です。

以下に具体例を示します。コマンドプロンプトからインタプリタで試してみて下さい。

>>> x = [1, 2, 3]
>>> y = [4, 5, 6]
>>> x = y
>>> x
[4, 5, 6]
>>> y
[4, 5, 6]
>>> x.append(7)
>>> x
[4, 5, 6, 7]
>>> y
[4, 5, 6, 7] # !?
>>> id(x)
1977306513992
>>> id(y)
1977306513992

xにappend()したはずなのにyにも入っています。id()で確認すると、x, yが同じIDを指してしまっています。

これを回避するにはcopy()(浅いコピー)もしくはcopy.deepcopy()(深いコピー)をする必要があります。

>>> x = [1,2,3]
>>> y = x.copy()
>>> x
[1, 2, 3]
>>> y
[1, 2, 3]
>>> x.append(4)
>>> x
[1, 2, 3, 4]
>>> y
[1, 2, 3]
>>> id(x)
2647847010888
>>> id(y)
2647847011528

出来ました!両者は別のidを指しているため、別のオブジェクトであることが分かりますね。これ知らないでいると、とんでもない挙動に遭遇したりしますw

それでは、続きを行っていきましょう!!

ViewFilterクラスの作成

# view.py
class ViewFilter(object):
    def __init__(self, v, rect):
        self._view = v
        self._view_height = rect[0]
        self._view_width = rect[1]

    def create_view(self):
        for i in range(self._view_height):
            for j in range(self._view_width):
                if self._view[i][j] == 0:
                    self._view[i][j] = "ー"
                elif self._view[i][j] == 1:
                    self._view[i][j] = "村"
        return self._view

create_view()関数でfor文をネストして、対応する数値と文字列を入れ替えています。浅いコピー深いコピーをしているので、WorldMapクラスの値を書き換えることもありません。

run.pyからViewFilterクラスの呼び出し

# run.py
import pprint
import copy
from sources.maps import WorldMap
from sources.view import ViewFilter # 追加

pp = pprint.PrettyPrinter()
world_map = WorldMap()
world_map.set_village(y=2, x=4)
world_map.set_village(y=8, x=3)
map_viewer = ViewFilter(v=(copy.deepcopy(world_map.get_world_map())), rect=world_map.get_rect()) # 追加
pp.pprint(map_viewer.create_view()) # 変更

インスタンス生成時に2次元配列の浅いコピー※ミスです!!深いコピーをして下さい!!

とマップの高さ、横幅を渡しています。

実行結果

[['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '村', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '村', '・', '・', '・', '・', '・', '・'],
 ['・', '・', '・', '・', '・', '・', '・', '・', '・', '・']]

出来ました!!!なんかエグいのは気にしないでください。って書いてありますからひと目で分かりますねw

そうしたら今回は一旦終わりです!!6000文字!!!

終わり

次回は座標管理についてです。

あ、入門 Python 3をまだ買っていない人は早く買うようにw

ソースコード

www.dropbox.com

続き

www.kiwi-bird.xyz