この記事ではタイルマップの基本的な使い方を解説します。
目次
タイルマップの基本的な使い方
基本プロジェクトのダウンロード
今回使用する素材(プロジェクト)を以下からダウンロードします。
TilemapTest
+-- assets: 各種リソース
| +-- fence.png: 柵の画像
| +-- player.png: プレイヤー画像
| +-- switch_gimmick.png: スイッチギミック画像
| +-- tile_set.png: タイルセット画像
|
+-- src: シーンとスクリプト
| +-- Common.gd: 共通スクリプト
| +-- FenceGimmick.tscn: 柵ギミックシーン
| +-- Player.gd: プレイヤースクリプト
| +-- Player.tscn: プレイヤーシーン
| +-- SwitchGimmick.gd: スイッチギミックスクリプト
| +-- SwitchGimmick.tscn: スイッチギミックシーン
|
+-- Main.gd: メインシーンスクリプト
+-- Main.tscn: メインシーン
+-- project.godot: プロジェクトファイル
ここに含まれる「project.godot」を Godot Engine からインポートします。
TileSetリソースの作成
まずはTileSetリソースを作成します。TileSetとはTileMapで使用するタイル画像や切り出し情報などをまとめたリソースです。
TileSetリソースを作るには、”assets” フォルダを右クリックして「新規 > リソース」を選びます。
リソース作成画面が表示されるので、”tileset” と検索エリアに入力して、「TileSet」を選び、作成ボタンを押します。
名前を「tile_set.tres」に変更して保存します。
作成された「tile_set.tres」をダブルクリックすると「タイルセットビュー」が表示されます。
今後、タイルセットを編集する場合は「tile_set.tres」をダブルクリックする、または「タイルセットビュー」をアクティブにする、この2つを忘れないようにしてください(このあたりの挙動を覚えておかないと「編集ができない!」という事態になりやすいので、念のため…)。
それでは、タイルセットの設定を編集します。「タイルセットビューがアクティブ」の状態で、インスペクタをみると、TileSet の項目に Tile Size というものがあります。これをそれぞれ 64 px にします。
次にタイルセットに画像を登録します。ileset.png をタイルセットビューの「タイル」の枠内にドラッグ&ドロップします。
すると以下のようなメッセージが表示されます。これはアトラス座標の切り出しを自動で行うかどうかの確認です。
ここでは「はい」を選び、自動でアトラス座標を作ってもらいます。
すると以下のような表示となります。
説明として、まず左上にある「Setup」「選択」「Paint」はタイルセットビューの状態を表します。上記画像ではセットアップ中なので「Setup」がアクティブになっています。
次に左側はアトラスの設定です。アトラスとは1つの画像に複数の意味を持つ画像を並べるというゲーム開発で主に使われる用語です。特に変更する必要はないですが「Texture Region Size」が 64px となっていて、切り出し単位が 64px になっていることがわかります。
そして右側は切り出した部分の表示です。明るい部分が有効なタイルで、暗くなっている部分は無効なタイルです。
この部分の有効・無効を切り替えるには、上にある「消しゴムボタン」を使います。これが青くなってアクティブだと、左クリックで無効になり、白色で非アクティブだと有効になります。
なお右クリックはメニューのポップアップ表示、右ドラックがスクロールに割り当てられています。(Space+左クリックでスクロールすることはできません)。
- 左クリック:消しゴムが無効ならタイルを有効にする。消しゴムが有効ならタイルを無効にする
- 右クリック:メニューをポップアップ
- 右ドラッグ:スクロール
TileMapノードの作成
作成したタイルセットを元にタイルマップを作っていきます。まずは Mainシーン (Main.tscn) をダブルクリックして開きます。
次に Mainシーンの TileMapノードを追加します。
TileMapノードを追加しました。
次にTileMapのインスペクタの Tile Set の <空> をクリックして「クイックロード」を選びます。
“assets/tile_set.tres” を選んで「開く」を選びます。
これでタイルマップにタイルセットが設定されました。
これでタイルセットからタイルを選択して、左クリックで配置できる…はずですが、左クリックしても配置できないことがよくあります。
原因としては、下の部分のビューのアクティブな設定が「タイルセット」となっている場合は、タイルマップに配置できません。
“TileMap” のビューを選択することで、タイルマップにタイルが配置できるようになります(※ハマりがちなポイントです)。
操作方法は以下のとおりです。
・左クリック:タイルの配置
・右クリック:タイルの消去
・Space+左ドラッグ:スクロール
なお、タイルの配置ツールにはいくつか種類があります。
通常使うのは「鉛筆」「バケツ」「スポイト」あたりですね。簡単な説明を以下に書いておきます。
ツール名 | 説明 | ショートカットキー |
---|---|---|
①選択 | 範囲選択して移動させたりコピーできる | – |
②鉛筆 | 左クリックしたところに配置 | D |
③ライン | 直線上に配置 | L |
④四角形 | 四角形に配置 | R |
⑤バケツ | 塗りつぶし配置 | B |
⑥スポイト | クリックしたタイルをスポイト。 ショートカットキーで使うのが便利 | P |
⑦消しゴム | 左クリックしたタイルを消去。 ラインや四角形やバケツと組み合わせると、 まとめて消せて便利 | E |
⑧ランダム | ランダム配置。 まばらに草などを配置するときに便利 | – |
それでは以下のようにタイルを配置します。
ビューポートの青い線あたりを「壁」タイルで囲み、その中を「床」タイルで塗りつぶします。壁タイルはラインツールで配置し、床タイルはバケツツールで塗りつぶすと楽に配置できます。
次にPlayerシーンを配置します。すでにPlayerは作成済みなので、ドラッグ&ドロップして配置するだけです。
では実行して動作を確認します。
しかし壁をすり抜けて移動できてしまいます。これはタイルにコリジョンが設定されていないためです。
コリジョンを設定するには、タイルセットリソース “tile_set.tres” をダブルクリックします。
そしてインスペクタから TileSet > Physic Layers にある「要素を追加」をクリックします。
すると Collision Layer と Collision Mask が追加されました。
今回はこのままとしますが、正しく使うにはちゃんとIDを振り分ける必要があります。それについては以下のページにまとめています。
【Godot】コリジョンレイヤーとマスクについてこれでタイルセットからコリジョンの設定をすることができます。
コリジョンを設定するにはタイルセットのビューに切り替えます。もし以下のように「TileMap」が選択されたままの場合は、「タイルセット」を選択します。
タイルセットビューが表示されたら「選択」モードにして、壁タイルをクリックします。
すると Base Tileの枠の一番下に「物理」と表示されているのでこれを開きます。
物理の項目を開いていくとポリゴン設定が表示されるので「+」というアイコンをクリックしてコリジョンのポリゴンを設定していきます。
左クリックで1つずつ配置して、一周すると赤く塗りつぶされます。
なお右上の吸着がONになっていると、位置合わせが少しだけ楽できます(デフォルトONのはずですが念のため)。
では実行して壁にめり込まないことを確認します。
タイルマップは複数のレイヤーを組み合わせることができます。
TileMapノードを選択して、インスペクタの “Layer” にある 「要素を追加」をクリックします。
すると “Layer” が 2つに増えました。上のLayerを “Background” 、下のレイヤーを “Object” という名前にしておきます。
そうすると TileMap ビューが有効になっている場合、タイルレイヤーを切り替えることができます。
これを Object レイヤーにして、レバースイッチギミックを配置するとします。
実行すると床の上にレバースイッチが配置されました(特に何も設定していないのですり抜けるだけですが)…。
このようにレイヤーを分けることで、地面の上に草を生やすなどもできるようになります。
ここまでのプロジェクトファイル
ここまでの完成プロジェクトファイルを添付してきます。
タイルマップの応用
ここまででタイルマップの基本的な使い方は終わりです。
個人的にはタイルマップの機能としては背景となるタイルを並べるとか、通過できない壁を置く、といった使い方が基本的な機能だと思っています。
というのもタイルマップでは、Area2D のような侵入を検知するコリジョンを配置できない、ユニークな情報を持つタイルを配置できない(同じタイルであればすべて同じ挙動となる)などの制約があるためです。
そこで、例えばプレイヤーが踏むことのできるスイッチギミックを配置するのであれば、タイルとして配置するのではなく、インスタンス(スイッチギミックシーンを作ってそれを直接配置する)のが結局は良かったりします。
とはいえ、タイルマップには他にも便利な機能があります。
- タイルを消去する
- 「カスタムデータ」でタイルの機能を拡張する
- 配置したタイルの情報を取得する
- 配置したタイル情報を書き換えて、タイルの見た目や機能を入れ替える
これらについて、続けて説明をします。
タイルマップ座標系について
タイルマップでのタイルが配置される座標は「タイルマップ座標系」となります。
通常オブジェクトはスクリーン座標もしくはワールド座標を基準に位置が決まりますが、タイルマップのタイルの位置はマップ座標系が基準となります。
例えば以下の画像におけるスイッチギミックの位置は、マップ座標系では (3, 3) として扱われます。
タイルの消去方法
このことを踏まえて、タイルの消去方法についてです。
実は Common.gd というスクリプトにすでにタイルの消去方法を記述しています。
## 指定の位置にあるタイル消す.
func erase_cell(pos:Vector2, tile_layer:eTileLayer) -> void:
var map_pos = _tile.local_to_map(pos) # ローカル座標をマップ座標に変換する
_tile.erase_cell(tile_layer, map_pos)
TileMap.local_to_map() というのがスクリーン座標またはワールド座標を「マップ座標」に変換する関数です。タイルマップの cell 関数はマップ座標基準で処理を行うため、このような変換をします。
まずは Main.gd を開いて以下のように記述します。
extends Node2D
# ※ここを追加
@onready var _tile = $TileMap # タイルマップ
func _ready() -> void:
# macOSだとサイズがなぜが半分になるので拡大する.
#DisplayServer.window_set_size(Vector2i(1152*2, 648*2))
# ※ここを追加
# タイルマップを自動読み込みモジュールに設定する
Common.setup(_tile)
func _process(delta: float) -> void:
pass
TileMapをCommonモジュールに渡す処理を追加しました。Godot Engine では自動読み込みモジュールにどこからでも使うような変数を渡す方法が実装しやすいので、このやり方をしています。
ちなみに Common.gd は プロジェクト設定の Autoload に設定済みとなります。
それとmacOS用の謎の処理が書かれていますが、これは2023.3.26 現在発生している4Kモニタ環境で発生する不具合で、Windows環境ではおそらく発生しないと思うので無視して問題ないです。
次にPlayer.gd を開いてこの消去処理を呼び出してみます。変更する関数は _physics_process() です。
func _physics_process(delta: float) -> void:
# ※ここを追加する.
if Input.is_action_just_pressed("ui_accept"):
var p = position + _dir * Common.GRID_SIZE
# 前方にあるタイル(Backgroundレイヤー)を消す.
Common.erase_cell(p, Common.eTileLayer.BACKGROUND)
# 移動処理.
_update_move(delta)
# アニメーションの更新.
_update_anim(delta)
“_dir” はキャラクターを向きを表す方向ベクトルなので、それに Common.GRID_SIZE (1マスあたりのサイズ) を掛けることで、前方1マスのタイルの位置が決まります。
では実行して、Spaceキーや Zキーを押してタイルが消えることを確認します。
ちなみに Zキーを決定キーとして扱えるのは、プロジェクト設定の「 インプットマップ > 組み込みアクションを表示」にZキーが登録済みであるためです。
タイルの情報を取得し、別のタイルで置き換える
タイルは消去するだけでなく別のタイルに置き換えることも可能です。Common.gd の set_cell() という関数がその実装となります。
## 指定位置にタイルを設定する.
func set_cell(pos:Vector2, tile_layer:eTileLayer, type:eTileType) -> void:
if not type in TILE_TYPE_TBL:
push_error("指定のtypeは存在しません type:%d"%type)
return
var map_pos = _tile.local_to_map(pos)
var atlas_coords = TILE_TYPE_TBL[type]
_tile.set_cell(tile_layer, map_pos, TILE_SOURCE_ID, atlas_coords)
TileMap.set_cell() という関数がタイルの置き換え処理です。ただこの置き換えには以下のパラメータが必要となります。
- layer:int:タイルレイヤー番号
- coords:Vector2i:マップ座標
- source_id:int:タイルソース番号
- atlas_coords:Vector2i:タイル画像のアトラス座標
- alternative_tile:int:代替タイル番号 (今回は使用しないのでデフォルトの “-1” で問題なし)
ここまでで説明していないのは「タイルソース番号」「タイル画像のアトラス座標」なので、それについて説明します(代替タイル番号は省略)。
タイルソース番号とは、タイルセットのここに表示されている番号です(表示されていない場合は “tile_set.tres” をダブルクリックすると表示されます)。
タイルセットに登録している画像の番号、と言い換えても良いかもしれません。今回は1枚しか登録していないので、タイルソース番号は「0」となります。
タイル画像のアトラス座標とは、タイルセットビューから「選択」を選び、各タイルを選択したときに表示される「Atlas Coords」の値となります。
例えば、カギタイルを選択したときの Atlas Coords は (x, y) = (2, 1) となります。
アトラステクスチャにおける座標系、と解釈しても良いかもしれません。
ということで先程の Common.set_cell() を見ると、TILE_TYPE_TBL からアトラス座標を取得しています。
## 指定位置にタイルを設定する.
func set_cell(pos:Vector2, tile_layer:eTileLayer, type:eTileType) -> void:
if not type in TILE_TYPE_TBL:
push_error("指定のtypeは存在しません type:%d"%type)
return
var map_pos = _tile.local_to_map(pos)
var atlas_coords = TILE_TYPE_TBL[type]
_tile.set_cell(tile_layer, map_pos, TILE_SOURCE_ID, atlas_coords)
TILE_TYPE_TBLの定義は以下のとおりです。
## タイルの種類.
enum eTileType {
NONE = 0, # 何もない.
WALL = 1, # 壁
FLOOR = 2, # 床.
}
## タイルの種類に対応する Atlas Coords
const TILE_TYPE_TBL = {
eTileType.WALL: Vector2i(0, 0), # 壁のアトラス座標.
eTileType.FLOOR: Vector2i(1, 0), # 床のアトラス座標.
}
eTileTypeの説明も同時にしたいので、その定義も含めています。タイルの種類である eTileType の定義に「WALL」「FLOOR」を定義し、TILE_TYPE_TBL にディクショナリ型として、eTileTypeのキーに対応したアトラス座標を格納しています。
このようなテーブルを定義することで、Common.set_cell() を使用するときは、eTileTypeを指定するだけでよくなります(もちろん壁や床以外を書き換える場合は、このテーブルに追加する必要があります)。
Common.set_cell() の説明はここで終わりです。
では、Player.gd に以下の記述を追加して、キャンセルボタンを押したときに「壁」タイルを配置するようにしてみます。
func _physics_process(delta: float) -> void:
if Input.is_action_just_pressed("ui_accept"):
var p = position + _dir * Common.GRID_SIZE
# 前方にあるタイル(Backgroundレイヤー)を消す.
Common.erase_cell(p, Common.eTileLayer.BACKGROUND)
# ※ここを追加する.
if Input.is_action_just_pressed("ui_cancel"):
var p = position + _dir * Common.GRID_SIZE
# 前方にあるタイルを壁にする.
Common.set_cell(p, Common.eTileLayer.BACKGROUND, Common.eTileType.WALL)
# 移動処理.
_update_move(delta)
# アニメーションの更新.
_update_anim(delta)
では実行して動作を確認します。(Xキーで壁を配置します)。
カスタムデータの設定(タイルを取得して判定する)
ここまでで「タイルを消す」「タイルを置き換える」を実装してきましたが、「タイルを取得してどのタイルであるかを判定する」といったことをやりたい場合があります。
タイルの種類を判定するには「カスタムデータ」の仕組みを使う必要があります。
カスタムデータはタイルセットの定義なので、”tile_set.tres” をダブルクリックして、インスペクタから設定します。
TileSet > Custom Data Layers から「要素を追加」をクリックします。
するとカスタムデータの要素が追加されるので、Nameに”type” 、Typeを “int” とします。
これは、カスタムデータの1つのキーの名前に「type」そのデータは int (整数値) であることの定義となります。
今回はレバースイッチのみタイルの種類 (type) を設定していきます。
エディタ下部のビューが「タイルセット」になっていることを確認して、「選択」モードを選びます。
続けてレバースイッチ(OFF)のタイルを選択すると、Base Tile の枠の一番下に「Custom Data」という項目が表示されます。
Custom Dataを開いたところに入力ボックスがあるので「11」と入力します。これはレバースイッチOFFは「11番目」の値とする定義です。
続けてレバースイッチONのタイルを選び、Custom Data を「12」とします。
レバースイッチの両方にCustom Dataを設定すると、以下のようにそれぞれに設定した数値が表示されます。レバースイッチ以外は何も設定していないので「0」です。
これでカスタムデータの設定は完了です。
Custom Dataの設定はタイルデータ上の取り決めなので、スクリプト側でも紐付け用に番号を定義する必要があります。
Common.gd にタイルの種類を追加します。
## タイルの種類.
enum eTileType {
NONE = 0, # 何もない.
WALL = 1, # 壁
FLOOR = 2, # 床.
LEVER_OFF = 11, # レバースイッチOFF
LEVER_ON = 12, # レバースイッチON
}
レバースイッチの定義 (LEVER_OFF / LEVER_ON) を追加しました。
その下にアトラス座標を追加します。
## タイルの種類に対応する Atlas Coords
const TILE_TYPE_TBL = {
eTileType.WALL: Vector2i(0, 0), # 壁のアトラス座標.
eTileType.FLOOR: Vector2i(1, 0), # 床のアトラス座標.
eTileType.LEVER_OFF: Vector2i(5, 2), # レバースイッチOFF.
eTileType.LEVER_ON: Vector2i(6, 2), # レバースイッチON.
}
繰り返しの説明となりますが、このアトラス座標はタイルセットビューから確認できます。
このアトラス座標の設定が大変ですね…。TileSetから自動で取得する方法は探したのですが見つからなかったので、今回の例ではスクリプトに直接定義するようにしています…。
良い方法が見つかったら追記するかもしれません。
最後にPlayer.gd のスクリプトを編集します。前方1マスを指定のタイルで置き換えるコードは以下のとおりです。
func _physics_process(delta: float) -> void:
if Input.is_action_just_pressed("ui_accept"):
var p = position + _dir * Common.GRID_SIZE
# 前方にあるタイルを調べる.
var type = Common.get_cell_type(p)
match type:
Common.eTileType.LEVER_OFF:
# OFFだったらON.
Common.set_cell(p , Common.eTileLayer.OBJECT, Common.eTileType.LEVER_ON)
Common.eTileType.LEVER_ON:
# ONだったらOFF.
Common.set_cell(p, Common.eTileLayer.OBJECT, Common.eTileType.LEVER_OFF)
_:
# それ以外は何もしない.
pass
# 移動処理.
_update_move(delta)
# アニメーションの更新.
_update_anim(delta)
では実行してレバースイッチのON/OFFを決定ボタン(Space / Zキー) で切り替えられることを確認します。
完成プロジェクト
応用編の完成プロジェクトを添付しておきます。
その他
ギミックの実装例
タイルマップとは直接関係ありませんが、スイッチギミックの実装例を紹介します。
今回作成するスイッチギミックは、プレイヤーが上に乗ると扉が開く、というシンプルなものです。
ここで使用するシーンは以下の2つです。
- FenceGimmick.tscn: 柵シーン
- SwitchGimmick.tscn: スイッチシーン
それぞれのシーンは src フォルダに存在します。
TileMapノードをクリックしてタイルマップを表示して、壁を配置していきます。
壁はこのように配置しましたが、だいたい合っていればでOKです。
それと Objectレイヤーのレバースイッチの位置を少し上にずらしておきました。
TileMapの編集モードになっているとタイルの編集と混同してしまいがちなので、いったん Mainノードを選択しておきます。
次にグリッドスナップ(グリッド吸着)を有効にしておきます。
そうしたら「SwitchGimmick.tscn (SwitchGimmick.gdではない)」を配置します。だいたいでOKです。
続けて「FenceGimmick.tscn」を壁のスキマに配置します。
描画順の問題があるので、各ノードをPlayerノードの上に移動させます。
また、ノード名が長いのが気になったので、それぞれ短い名前にしてみました。
プレイヤーから見て、手前のギミックが「Switch1」「Fence1」。そして奥にあるのが「Switch2」「Fence2」となります。
そうしたら「Switch1」ノードを選択します。
インスペクタの SwitchGimmick.gd の Unlock Target の「割り当て」をクリックします。
Fence1ノードを選んでOKを押します。
Fence1が割り当てられました。
同様の操作で、Switch2ノードにもFence2を割り当てます。
では実行して、スイッチギミックを踏むと柵が消えることを確認します。
SwitchGimmick.gd を見るとすぐに分かりますが、Area2D の body_entered() シグナルで、スイッチの見た目を変えて割り当てたオブジェクトを消去しているだけとなります。
extends Area2D
# ===========================
# スイッチギミック.
# ===========================
# ---------------------------------------
# onready.
# ---------------------------------------
@onready var _spr = $Sprite
# ---------------------------------------
# export.
# ---------------------------------------
@export var unlock_target:CollisionObject2D # スイッチを踏んだ時に消すオブジェクト.
# ---------------------------------------
# signal functions.
# ---------------------------------------
func _on_body_entered(body: Node2D) -> void:
# スイッチを踏んだ
if is_instance_valid(unlock_target):
# ターゲットを消す.
unlock_target.queue_free()
_spr.frame = 1 # 踏んだ状態にする.
ちなみに、ON/OFFのサンプルを作ろうと思ったのですが、コリジョンがOFFからONになったときにプレイヤーがコリジョンの場所にいると move_and_slide() を使っても壁にめり込む問題が発生するので、今回はやめました。
もし ON/OFF スイッチを作るのであれば、「プレイヤーと重なっている場合はONにならないようにする」といったプレイヤーがめり込む可能性がないように実装する必要があります。
完成プロジェクト
今回作成したプロジェクトを添付しておきます。
2023.7.5 追記: 一方通行(One way)コリジョンの設定方法
タイルコリジョンに一方通行属性をつけるには、「物理 > Physics Layer > Polygon」のところにある「One Way」にチェックを入れます。
なお、タイルセットにコリジョンを設定するには、Tilesetのインスペクタにある「Physics Layers」の要素を追加しておく必要があります。
そしてよくある一方通行床からの飛び降りは Physics Layers を複数用意して、別の Collision Mask を割り当てておきます。
後はプレイヤー側で飛び降り中は “collision_mask” のビットを下ろすようにすることで床のすり抜けが実装できます。
var oneway_layer = 3
var oneway_bit = int(pow(2, oneway_layer-1)
if _is_fall_through:
# 飛び降り中なのでビットを下げる.
collision_mask &= ~oneway_bit
else:
collision_mask |= oneway_bit
2023.7.8 追記: コリジョンマスクを変更する方法で is_on_floor() が false にならないときの対処法
コリジョンマスクを変更する方法だと is_on_floor() が false にならず、落下処理が正常に行われないことがあります。
例えば上記のように壁に接触している状態で、collision_mask を変更することで通り抜けする方法を行うと、is_on_floor がtrue から変わらず落下処理が正常に行われなくなります(おそらく Y方向への落下がリセットされてしまう)。
これを回避するには「飛び降り」を開始する1フレームだけ X方向への移動を「0」にすると正常に落下できるようになります。