見出し画像

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 から読み込む必要はありません。今回の記事はあくまでゲームプログラム の学習用途となります。

いいなと思ったら応援しよう!