PythonでTiledマップエディタのデータを読み込む
今回は、汎用の2Dマップエディタ「Tiledマップエディタ」をPythonで読み込む方法を紹介します。
例えばこのように Tiled に配置されたデータを読み込みます。
■まずは日本語化
Tiled を日本語化していない場合は、以下の手順で日本語化します。
メニューから「Preference...」を選んで設定画面を開きます。
「Interface」>「Language」>「Japanese」を選びます。
すると表示設定が「日本語」になります。
■マップデータの新規作成
新規作成画面から「新しいマップ...」を選んで、マップデータを新規作成します。
すると「新しいマップ」の情報を指定する画面が表示されます。
マップの大きさを「16タイル x 16タイル」、タイルの大きさを「8px x 8px」に設定して、「ファイル名をつけて保存...」を選びます。
ファイル名は「map」として保存します。
マップが作成されたので、次にタイルセットを作成します。右下にある「New Tileset...」をクリックします。
新しいタイルセットの設定画面が表示されます。
名前を「back」、以下のファイルをダウンロードして "tileset.png" をパスに設定します。
そして透過色を設定します。スポイトボタンを押すと透過色の設定ができるので、#7e2553 になるよう、うまく設定します。
MacOSX環境では、なぜか変な場所をクリックすると設定できました。タイルセットを設定すると、タイルセットファイル "back.tsx" を選んだ状態になるので、"map.tmx" タブを選んでマップデータの編集に戻ります。
タイルセットの表示が小さいので、倍率を「800%」くらいにして大きくします。
そうしたら、タイルセットをクリックして、マップデータに配置してきます。
読み込みテスト用なので好きなものを配置して問題ありません。配置したら、map.tmx を保存します。
データを作成するのが面倒な場合は以下のファイルをダウンロードします。
なお配置したチップデータは以下の番号に割り振られます。
"0" 始まりではなく、 "1" 始まりであることに注意します。Tiledは何も配置していない状態を "0" として扱うためです。
■Pythonで読み込む
読み込みは以下のコードで行うことができます。
import pyxel
import os
import xml.etree.ElementTree as ET
# 2次元配列管理
class Array2D:
def __init__(self, width=0, height=0):
self.create(width, height)
self.outofrange = -1 # 領域外を指定したときの値
def create(self, width, height):
# 作成
self.width = width # 幅
self.height = height # 高さ
self.vals = [0] * width * height # 値
def to_idx(self, x, y):
# 座標をインデックスに変換する
return x + (y * self.width)
def check(self, x, y):
# 領域内チェック
if 0 <= x < self.width:
if 0 <= y < self.height:
# 領域内
return True
return False # 領域外
def check_from_idx(self, idx):
# 領域内チェック (インデックス指定)
return 0 <= idx < (self.width * self.height)
def get(self, x, y):
# 値の取得
if self.check(x, y) == False:
return self.outofrange
return self.get_from_idx(self.to_idx(x, y))
def set(self, x, y, v):
# 値の設定
if self.check(x, y) == False:
return
return self.set_from_idx(self.to_idx(x, y), v)
def get_from_idx(self, idx):
# 値の取得 (インデックス指定)
if self.check_from_idx(idx) == False:
return self.outofrange
return self.vals[idx]
def set_from_idx(self, idx, v):
# 値の設定 (インデックス指定)
if self.check_from_idx(idx) == False:
return
self.vals[idx] = v
def count(self, v):
# 指定の値の存在数をカウントする
ret = 0
for j in range(self.height):
for i in range(self.width):
if self.get(i, j) == v:
ret += 1
return ret
def search(self, v):
# 指定の値を検索する。最初に見つかった値を返す
for j in range(self.height):
for i in range(self.width):
if self.get(i, j) == v:
return i, j
# 存在しない
return -1, -1
def choice(self, v):
# 指定の値の座標をランダムで取得する
list = []
for j in range(self.height):
for i in range(self.width):
if self.get(i, j) == v:
list.append((i, j))
if len(list) == 0:
# 存在しない
return -1, -1
return random.choices(list)
def fill(self, v):
# 全てを指定の値で埋める
self.foreach(lambda x, y, val: self.set(x, y, v))
def foreach(self, func):
# 繰り返し処理を行う
for j in range(self.height):
for i in range(self.width):
func(i, j, self.get(i, j))
def dump(self):
# デバッグ出力
print("[Array2D] (w,h)=(%d,%d)"%(self.width, self.height))
for j in range(self.height):
s = ""
for i in range(self.width):
s = s + "%d,"%self.get(i, j)
print(s)
# Tiled Map Editor読み込みクラス
class Tiled:
def __init__(self, path=""):
if path != "":
self.load(path)
def load(self, path):
# *.tmx ファイル読み込み
elem = ET.parse(path)
root = elem.getroot()
self.name = os.path.basename(path)
self.width = int(root.get("width"))
self.height = int(root.get("height"))
self.tilewidth = int(root.get("tilewidth"))
self.tileheight = int(root.get("tileheight"))
self.layers = []
for child in root:
if child.tag == "layer":
w = int(child.get("width"))
h = int(child.get("height"))
layer = Array2D(w, h)
for data in child:
if data.tag == "data":
idx = 0
text = data.text
for line in text.split("\n"):
for v in line.split(","):
if v.strip() == "":
break
layer.set_from_idx(idx, int(v))
idx += 1
self.layers.append(layer)
def dump(self):
# デバッグ出力
print("Tiled dump.")
print("filename = %s"%self.name)
print("(width, height) = (%d, %d)"%(self.width, self.height))
print("(tile_width, tile_height) = (%d, %d)"%(self.tilewidth, self.tileheight))
for i, layer in enumerate(self.layers):
print("layer[%d] (width, height) = (%d, %d)"%(i, layer.width, layer.height))
layer.dump()
tiled = Tiled()
tiled.load("map.tmx")
tiled.dump()
読み込んだマップデータの格納には、「2次元配列管理クラスの実装について」で紹介した Array2D クラスを使用しています。読み込みは「xml.etree.ElementTree」を使って XML を上から順番に解析しています。
Tiled のデータについて少し説明すると、"width / height" はマップチップが縦横に並んでいる数です。"tilewidth / tileheight" はタイルセットの1つのチップの幅と高さです。
width / height はマップデータに並んでいるチップの数
tilewidth / tileheight はタイルの幅と高さ
なお今回の読み込み例では、自由な位置にオプジェクトを配置できる "オブジェクトレイヤー" の読み込みは考慮していません。
■Pyxelで表示する
おまけとして読み込んだデータを Pyxel で表示してみます。
import math
class App:
def __init__(self):
pyxel.init(160, 120, fps=60)
pyxel.image(0).load(0, 0, "tileset.png")
self.tiled = Tiled("map.tmx")
self.tiled.dump()
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
for layer in self.tiled.layers:
for j in range(layer.height):
for i in range(layer.width):
# 値を取得する
val = layer.get(i, j)
if val == 0:
# 何も配置していない
continue
val -= 1
w = self.tiled.tilewidth
h = self.tiled.tileheight
x = i * w
y = j * h
u = (val % 5) * w
v = math.floor(val / 5) * h
pyxel.blt(x, y, 0, u, v, w, h, 2)
App()
タイル読み込みは、Tiledクラス / Array2Dクラスを使うとして、Appクラスのみを記述しています。
描画する画像の UVを求めるのに "5" を使用していますが、これはチップ画像の並んでいる横の数です(チップ画像の幅 40px ÷ 1つあたりの幅 8px = 5)
今回作成したプログラムとデータ一式は以下からダウンロード可能となります。
ちなみに Pyxel にはタイルエディターが付属しているのでわざわざ Tiled から読み込む必要はありません。今回の記事はあくまでゲームプログラム の学習用途となります。