無料・軽快な 2D / 3D 用のゲームエンジン Godot Engine 4 で、タイルマップを実現する TileMapLayer の各タイルと、キャラクターが接触しているタイル群を判定するために、タイルサイズで割って得られるセルの座標の端数切り上げと端数切り下げの結果が必要になり、自作した関数群を紹介します。
TileMapLayer.local_to_map 関数に (1, 1) を足した場合は、端数切り上げと似た結果ですが、キャラクターがタイル1個分にぴったりはまっていた場合に誤差が生じる例も紹介します。
※ GodotEngine 4.3 を使用しています。.NET 版ではありません。
※スクリプトは自己責任でご使用ください。
前回の記事
前回は、TileMapLayer の各タイルを指定するセルの座標と、Node2D で用いられるローカル座標とグローバル座標の関係と、その変換に便利な関数について紹介しました。
キャラクターが重なっているタイルの取得に必要な機能
キャラクターが重なっている TileMapLayer の全てのタイルを取得するにあたって、当たり判定領域の外接矩形(領域を全て含められる最小の四角形)とタイルサイズの割り算の結果の端数の切り下げ切り上げが必要になりました。
local_to_map では端数を切り下げた値しか得られません。
しかし local_to_map に (1, 1) を毎回重ねると、ぴったり1タイル分のときに縦横1ずつ余計にタイルを取得してしまいます。
スクリプト
グローバル座標をセル座標に変換する自作の関数は以下です。
to_cell_pos_ceil 関数は、端数を切り上げてグローバル座標をセルの座標に変換します。
to_cell_pos_floor 関数は、端数を切り捨ててグローバル座標をセルの座標に変換します。
※ to_cell_pos_floor はおそらく local_to_map 関数と同様の結果になります。
to_cell_pos 関数は、整数にする前の小数でセルの座標を返す処理で、前述の2つの関数の共通部分の処理を行います。
※詳細はコメント文をご覧ください。
# グローバル座標の位置を、tilmepa_layer のセルの位置 (小数切り上げで整数) に変換します。
#
# TileMapLayer.local_to_map(local_pos) + Vector2i(1, 1) という処理(仮に処理 A)と似ていますが、
# 引数によって処理が異なる場合があります。
# 例えば、TileMapLayer.global_posiion = (0, 0), TileMapLayer.tile_set.tile_size = (16, 16) の場合、
# local_pos が (20, 47) だった場合、本関数では (1, 2) で、処理 A でも (1, 2) と同じ値が返されます。
# しかし、 local_pos が (32, 48) だった場合、本関数では (1, 2) ですが、処理 A では (2, 3) になってしまいます。
func to_cell_pos_ceil(global_pos: Vector2, tilemap_layer: TileMapLayer) -> Vector2i:
return Vector2i(to_cell_pos(global_pos, tilemap_layer).ceil())
# グローバル座標の位置を、tilmepa_layer のセルの位置 (小数切り捨てで整数) に変換します。
# おそらく、TileMapLayer.local_to_map と同じ結果が得られます。
func to_cell_pos_floor(global_pos: Vector2, tilemap_layer: TileMapLayer) -> Vector2i:
return Vector2i(to_cell_pos(global_pos, tilemap_layer).floor())
# グローバル座標の位置を、tilmepa_layer のセルの位置(小数)に変換します。
# to_cell_pos_ceil, to_ceil_pos_floor 関数の共通の処理です。
func to_cell_pos(global_pos: Vector2, tilemap_layer: TileMapLayer) -> Vector2:
# tilemap_layer のローカル座標に変換。
# 例えば tilemap_layer.global_position = (-16, 32), global_pos = (64, 80) だった場合
# local_pos には (64 - (-16), 80 - 32) = (80, 48) に変換されます。
var local_pos = tilemap_layer.to_local(global_pos)
# tilemap_layer のタイル(セル)サイズを取得します。
var cell_size = Vector2(tilemap_layer.tile_set.tile_size)
# local_pos = (80, 48) で cell_size = (16, 16) の場合はセル座標 (5, 3) が返されます。
# セル座標が (4.8, 2.1) のような場合は切り上げられて (5, 3) が返されます。
return local_pos / cell_size
※この関数を使わなくても、 TileMapLayer の local_to_map 関数の引数のローカル座標 local_pos をあらかじめ $TileMapLayer.tile_set.tile_size で割った結果を ceil() して、再び tile_size を掛けることで、同じ結果が得られます。
自作関数のテスト
前述のグローバル座標からタイルマップのセル座標に変換する to_cell_pos_floor 関数のテストコードです
func _ready():
var global_pos = Vector2(64, 80)
var cell_pos = to_cell_pos_floor(global_pos, $TileMapLayer)
print("global_pos = ", global_pos)
print("$TileMapLayer.global_position = ", $TileMapLayer.global_position)
print("$TileMapLayer.tile_set.tile_size = ", $TileMapLayer.tile_set.tile_size)
print("cell_pos = ", cell_pos)
下パネルの出力に、引数の値を表した3行と、4行目にその結果の正しい値が出力されました。
global_pos = (64, 80)
$TileMapLayer.global_position = (-16, 32)
$TileMapLayer.tile_set.tile_size = (16, 16)
cell_pos = (5, 3)
to_cell_pos_ceil 関数については、次の章で、TileMapLayer.local_to_map 関数の結果に (1, 1) を足したものとの比較で、テスト結果を紹介します。
TileMapLayer.local_to_map 関数との違い
TileMapLayer.local_to_map でローカル座標からセルの座標に変換できますが、切り捨てて整数に変換されます。
切り上げの結果を求める際にすでに整数になっているため小数部分が 0 なのかどうか判定できず、すべての結果に Vector2i(1, 1) を加算すると、倍数ぴったりの入力の際に不要に縦横1マスずつ多い座標に変換されてしまいます。
func _ready():
$TileMapLayer.global_position = Vector2(0, 0)
print("local_to_map(20, 47) + Vector2i(1, 1) = ", $TileMapLayer.local_to_map(Vector2(20, 47)) + Vector2i(1, 1))
print("to_cell_pos_ceil(Vector2(20, 47), $TileMapLayer) = ", to_cell_pos_ceil(Vector2(20, 47), $TileMapLayer))
print("local_to_map(32, 48) + Vector2i(1, 1) = ", $TileMapLayer.local_to_map(Vector2(32, 48)) + Vector2i(1, 1))
print("to_cell_pos_ceil(Vector2(32, 48), $TileMapLayer) = ", to_cell_pos_ceil(Vector2(32, 48), $TileMapLayer))
print("local_to_map(33, 49) + Vector2i(1, 1) = ", $TileMapLayer.local_to_map(Vector2(33, 49)) + Vector2i(1, 1))
print("to_cell_pos_ceil(Vector2(33, 49), $TileMapLayer) = ", to_cell_pos_ceil(Vector2(33, 49), $TileMapLayer))
結果として、タイルサイズ (16, 16) の倍数で、商の余りがでないタイルとぴったり収まる位置にいる場合の値 (32, 48) では、TileMapLayer.local_to_map の結果に Vector2i(1, 1) を足した場合と、 to_cell_pos_ceil 関数では結果が異なり、 local_to_map の結果に Vector2i(1, 1) を足した場合は、1マス分余計になります。
local_to_map(20, 47) + Vector2i(1, 1) = (2, 3)
to_cell_pos_ceil(Vector2(20, 47), $TileMapLayer) = (2, 3)
local_to_map(32, 48) + Vector2i(1, 1) = (3, 4)
to_cell_pos_ceil(Vector2(32, 48), $TileMapLayer) = (2, 3)
local_to_map(33, 49) + Vector2i(1, 1) = (3, 4)
to_cell_pos_ceil(Vector2(33, 49), $TileMapLayer) = (3, 4)
キャラクターと重なったと判定された枠を表示するテスト
以下の TileMapLayer.local_to_map の戻り値に強制的に (1, 1) を加算した場合、キャラクターがタイル1個にぴったり収まっている状態では、先ほどの print 文の出力と同じく、縦横1マスずつ多い範囲が接触していると誤判定されます。
自作関数の to_cell_pos_ceil 関数では、キャラクターがタイル1個分ぴったりの範囲の場合、切り捨てた結果と切り上げた結果が同じになるため、余計なタイルの誤判定は発生しません。
ノードの縮小や、地面と設置したときに起きるわずかなずれにより、ぴったり1タイル分に収めることが難しかったので、強制的にキャラクターの RectangleShape2D.size を1タイル分(例では 16 × 16) に設定して、キャラクターの位置 global_position も16の倍数にぴったりなるように調整して、テストしました。
# 当たり判定領域の外接矩形を取得して、body の scale に合わせて拡大縮小します。
var rect = shape.get_rect()
rect.position *= body.scale
rect.size *= body.scale
# デバッグ用に16の倍数に設定
rect.position = Vector2(-8, -8)
rect.size = Vector2(16, 16)
#print("Collision shape rect: ", rect) # Debug
まとめ
今回は、無料・軽快な 2D / 3D 用のゲームエンジン Godot Engine 4 で、タイルマップを実現する TileMapLayer の各タイルと、キャラクターが接触しているタイル群を判定するために、タイルサイズで割って得られるセルの座標の端数切り上げと端数切り下げの結果が必要になり、自作した関数群を紹介しました。
TileMapLayer.local_to_map 関数に (1, 1) を足した場合は、端数切り上げと似た結果ですが、キャラクターがタイル1個分にぴったりはまっていた場合に誤差が生じる例も紹介しました。
参照サイト Thank You!
- Godot Engine – Free and open source 2D and 3D game engine
- Using TileSets — Godot Engine (4.x)の日本語のドキュメント
- TileMapLayer — Godot Engine (4.x)の日本語のドキュメント
記事一覧 → Compota-Soft-Press
コメント