Tkinterでフィールドマップをつくる

# はじめに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625723/097a2476-4ec7-a184-87dc-e0bbc3746537.png)

Tkinterで、フィールマップをマップチップを用いて作成する方法について解説します。

# マップチップとは

>マップチップとは、ドット絵で画像を作成する際、画像サイズの小さい最小単位の部品をパズルのように組み合わせて画像を製作する手法、またその最小単位の部品のこと。
>
>容量が今のゲームよりもはるかに少なかった2Dゲームの時代、いかに少ないファイルサイズで効率よく街やフィールドのマップを作るかということに苦心していた頃に使われていた手法であり、ハードの進歩により必須となる局面は減ったとはいえ今でも現役の手法であり、特に2Dの自作ゲームを製作する際に重要なテクニックとして重用されている。
>
>ファミコンなどの旧時代のグラフィック画像を例に見ると、樹木やレンガ、海の波の模様や屋根瓦といった背景の模様などを1マスの小さな単位として扱い、一定の規則に従った配列の繰り返しで構成することで描画していることがよくわかる。
>(ピクシブ百科事典より)

https://dic.pixiv.net/a/%E3%83%9E%E3%83%83%E3%83%97%E3%83%81%E3%83%83%E3%83%97

# なぜマップチップを使うのか
 <dl>
  <dt>マップを効率的に開発できる</dt>
  <dd>特に大きなゲームの場合、大量のマップが必要になります。マップを一枚の画像にしたものを何十枚用意するより、マップチップを使いまわす方が効率的に開発できます。</dd>
  <dt>容量を削減できる</dt>
  <dd>マップチップを使用することで、同じチップを繰り返し利用することができるので、容量の削減につながります。</dd>
  <dt>一貫性を確保できる</dt>
  <dd>マップチップを使用することで、ゲーム内のグラフィックスやデザインの一貫性を保つことができます。同じセットのチップを使用することで、ゲーム内のすべての場所が統一感を持ち、プレイヤーの没入感が向上します。</dd>
  <dt>修正が容易である</dt>
  <dd>マップチップを使用することで、マップを修正したり拡張したりするのが容易になります。単一のチップを修正することで、ゲーム内の複数の場所に変更が反映されます。</dd>
</dl>

# 作ってみる

## 画像を表示する
Tkinterで画像を表示するには、`create_image`メゾットを使用します。
```Python
canvas.create_image(x, y, anchor, image)
```


| 変数名 | 説明 | 型 | デフォルト値 | 備考 |
|---|---|---|---|---|
| `x` | 画像の位置 (X座標) | int | 0 | 基準点の座標 |
| `y` | 画像の位置 (Y座標) | int | 0 | 基準点の座標 |
| `anchor` | 画像の基準点 | str | "center" | "center", "nw", "n", "ne", "e", "se", "s", "sw", "w" から選択 |
| `image` | 表示する画像 | PhotoImage | None | `PhotoImage` オブジェクト |


以下のサイトに詳しく説明があります。

https://water2litter.net/rum/post/python_tkinter_canvas_create_image/

:::note warn
**imageには`PhotoImage` オブジェクトを指定する必要**があります。`PhotoImage` オブジェクトは以下で作成できます。
```Python:
img = tk.PhotoImage(file=xxx.png)
```
**ファイルを直接指定するだけではない**ことに注意してください。
:::

### サンプルコード

```Python:画像を表示するサンプルコード
import tkinter as tk

# ウィンドウの作成
root = tk.Tk()
root.title("画像を表示するサンプルコード")

# 画像の読み込み
image_path = "picture.png"
img = tk.PhotoImage(file=image_path)

# キャンバスの作成
canvas = tk.Canvas(root, width=img.width(), height=img.height())
canvas.pack()

# 画像をキャンバスに表示
canvas.create_image(0, 0, anchor=tk.NW, image=img)

# ウィンドウの表示
root.mainloop()
```

:::note info
**pytonファイルと同じフォルダ内**に`picture.png`という名前の画像を置いてください。名前が一致していればなんでも良いですが、今回は以下の画像を使用しました。
![picture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625723/16c8efad-21bf-4076-ad55-1aa8850bc0b8.png)
:::

### 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625723/2fa5a0a2-a4aa-a34b-9004-e1b54bb80e7d.png)

## 画像を拡大して表示する
あまりにも小さいのでこの画像を拡大して表示してみます。整数倍の拡大であれば簡単にできます。
```Python
img = img.zoom(x, y)
```

| 変数名 | 説明 | 型 | 備考 |
|---|---|---|---|
| `x` | x方向の拡大率 | int |  |
| `y` | y方向の拡大率 | int | 未指定の場合は`x`と同じ値になる |

逆に縮小したい場合は`img.subsample()`を利用すると整数倍で縮小できます。

### サンプルコード

```Python:画像を拡大表示するサンプルコード
import tkinter as tk

# ウィンドウの作成
root = tk.Tk()
root.title("画像を拡大表示するサンプルコード")

# 画像の読み込み
image_path = "picture.png"
img = tk.PhotoImage(file=image_path)
# 画像を4倍に拡大する
img = img.zoom(4)

# キャンバスの作成
canvas = tk.Canvas(root, width=img.width(), height=img.height())
canvas.pack()

# 画像をキャンバスに表示
canvas.create_image(0, 0, anchor=tk.NW, image=img)

# ウィンドウの表示
root.mainloop()
```

### 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625723/deaae8d2-078c-878e-8a95-74fabbb6b8ca.png)

## 一枚の画像から切り取って表示する
以下のタイルセット(マップチップを1枚のシートにしたもの)から、欲しい部分を切り取って表示します。ファイル名は`mapchip.png`としています。
![mapchip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625723/531cf654-c6e9-8ab6-29d5-a3287089c97b.png)

:::note info
画像の切り取りにはPillow(PIL)が必要です。コマンドプロンプトで以下のコマンドを入力してインストールしてください。
```
$ pip install Pillow
```
:::

`crop`メゾットを使用することで画像を切り取って表示できます。

```Python
img = img.crop((left, top, right, bottom))
```

| 変数名 | 説明 | 型 |
|---|---|---|
| `left` | 切り取る領域の左端の座標 | int |
| `top` | 切り取る領域の上端の座標 | int |
| `right` | 切り取る領域の右端の座標 | int |
| `bottom` | 切り取る領域の下端の座標 | int |

:::note warn
`img.crop(left, top, right, bottom)`ではなく`img.crop((left, top, right, bottom))`であることに注意してください。あくまで4要素のタプルを1つ与えています。
:::

### サンプルコード

```Python:画像を切り取って表示するサンプルコード
import tkinter as tk
from PIL import Image, ImageTk

# 定数
MAGNIFICATION = 16 #拡大率
CHIP_WIDTH = 16 #マップチップの幅
CHIP_HEIGHT = 16 #マップチップの高さ

# ウィンドウの作成
root = tk.Tk()
root.title("画像を切り取り表示するサンプルコード")

# 画像の読み込み
image_path = "mapchip.png"
img = Image.open(image_path)

# 画像のサイズを取得
img_width = img.width
img_height = img.height

# 画像の拡大
img = img.resize((img_width * MAGNIFICATION, img_height * MAGNIFICATION))

# 画像のサイズを再取得
img_width = img.width
img_height = img.height

# 画像を切り取って表示(今回は左から2番目)
i = 2 # 左から何番目
j = 1 # 上から何番目
chip = img.crop((CHIP_WIDTH * MAGNIFICATION * (i - 1), CHIP_HEIGHT * MAGNIFICATION * (j - 1), CHIP_WIDTH * MAGNIFICATION * i, CHIP_HEIGHT * MAGNIFICATION * j))
chip = ImageTk.PhotoImage(chip)

# キャンバスの作成
canvas = tk.Canvas(root, width=chip.width(), height=chip.height())
canvas.pack()

# 画像をキャンバスに表示
canvas.create_image(0, 0, anchor=tk.NW, image=chip)

# ウィンドウの表示
root.mainloop()
```

### 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625723/0e8aa8a4-531b-23f3-bd3b-eb6871e28652.png)

## マップを表示する
以上を組み合わせてマップを表示する簡単なプログラムを作成しました。

### サンプルコード
```Python:マップを表示するサンプルコード
import tkinter as tk
from PIL import Image, ImageTk

# 定数
MAGNIFICATION = 4 #拡大率
CHIP_WIDTH = 16 #マップチップの幅
CHIP_HEIGHT = 16 #マップチップの高さ

# マップ
MAP = [
    [3,3,3,3,3,3],
    [3,3,4,4,3,3],
    [3,4,1,1,1,3],
    [3,4,1,1,1,3],
    [3,3,3,4,3,3],
    [3,3,3,3,3,3],
]

# マップの要素数
MAP_WIDTH = len(MAP[0])
MAP_HEIGHT = len(MAP)

# ウィンドウの作成
root = tk.Tk()
root.title("マップを表示するサンプルコード")

# 画像の読み込み
image_path = "mapchip.png"
img = Image.open(image_path)

# 画像のサイズを取得
img_width = img.width
img_height = img.height

# 画像の拡大
img = img.resize((img_width * MAGNIFICATION, img_height * MAGNIFICATION))

# 画像のサイズを再取得
img_width = img.width
img_height = img.height

#画像にマップチップが縦横何枚あるかを取得
img_width_in_chips = img_width // (CHIP_WIDTH * MAGNIFICATION)
img_height_in_chips = img_height // (CHIP_HEIGHT * MAGNIFICATION)

# 画像を切り取って配列に格納
chips = [ImageTk.PhotoImage(img.crop((CHIP_WIDTH * MAGNIFICATION * (i % img_width_in_chips), CHIP_HEIGHT * MAGNIFICATION * (i // img_width_in_chips), CHIP_WIDTH * MAGNIFICATION * (i % img_width_in_chips + 1), CHIP_HEIGHT * MAGNIFICATION * ((i // img_width_in_chips) + 1)))) for i in range(img_width_in_chips * img_height_in_chips)]


# キャンバスの作成
canvas = tk.Canvas(root, width=CHIP_WIDTH * MAGNIFICATION * MAP_WIDTH, height=CHIP_WIDTH * MAGNIFICATION * MAP_HEIGHT)
canvas.pack()

# マップをキャンバスに表示
for i in range(MAP_HEIGHT):
    for j in range(MAP_WIDTH):
        canvas.create_image(CHIP_WIDTH * MAGNIFICATION * j, CHIP_HEIGHT * MAGNIFICATION * i, anchor=tk.NW, image=chips[MAP[i][j]])

# ウィンドウの表示
root.mainloop()
```

### 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625723/1b126c5f-fe7b-37a3-544f-e9bdacaf5017.png)

# まとめ
Tkinterでフィールドマップを表示する方法について解説しました。PILを用いて画像を切り取り、並べて配置することでマップを表示できました。