• 2D
  • Tile-based positioning of objects

  • [deleted]

I'm working on a tile-based game, where objects on the map (characters, items, etc.) are positioned by tile instead of by pixel coordinates. That is, objects would have a tile position variable that controls their real position. I'm wondering how to best implement this, especially for building scenes in the editor.

When working in the editor I assume the most common method is to turn on "Snap to Grid" and set the grid size to my tile size, and for the objects to initialize their tile position variables in their _ready methods. I'd have to make sure that snapping is configured right, while I was hoping to come up with something a bit more automatic.

In Godot 2.1 I discovered an interesting alternative approach using the method CanvasItem.edit_set_state. It got called when the object is moved in the editor (and the object's script is set to tool mode). I was therefore able to override edit_set_state to both snap the object to the nearest tile when it was moved in the editor and also update its tile position variable while doing so. edit_set_state no longer exists in Godot 3.0 though.

How do others handle tile-based placement of objects in tile-based games?

15 days later

if you're using a tilemap, you can use this function I made (for this exact purpose) to make a given scene copy itself to every instance of a given tile.

extends TileMap
#clones scenes in given array scenepaths to every instance of given cell_ID
func associate(tileset,cell_name,scenepaths):
	var cell_ID = nametoid(tileset.find_tile_by_name(cell_name)
	var ObjectLocales = get_used_cells_by_id(cell_ID)
	for object in ObjectLocales:
		for scene in scenepaths:
			var ObjectItem = load(scene).instance()
			add_child(ObjectItem)
			var realPos = map_to_world(object)
			ObjectItem.position = realPos

This is what it looks like in usage:

func _ready():
	var tiles = load("res://Tiles/Objects.tres")
	for i in tiles.get_tiles_ids():
		print(i," ",tiles.tile_get_name(i))
	associate(tiles,"Candela",["res://Tiles/Objects/Lights/Candela/CandelaStuff.tscn"])
	associate(tiles,"Torch",["res://Tiles/Objects/Lights/Torch/TorchStuff.tscn"])
	associate(tiles,"Campfire",["res://Tiles/Objects/Lights/Campfire/CampfireStuff.tscn"])

I used it here to copy a scene containing particles and light sources to every instance of these light sources. Multiple scenes can be copied too, because the scenepaths variable takes an array of strings containing the paths to each scene.

If you want to convert between tile coordinates and global pixel coordinates, use map_to_world() and world_to_map().

  • [deleted]

  • Edited

So what you're doing is making a TileSet whose tile types represent game objects, while your levels have a TileMap using this TileSet where you draw the game object tiles on. Then when the level loads this game object TileMap is used to load the real game objects and set their positions.

One drawback is having to update the game object TileSet everytime I add a new game object or modify a game object's appearance. I also plan on having multi-tile game objects (e.g. a dragon that's 2x2 tiles) and I'm not sure if a TileSet can support such a tile type.

Meanwhile, I've recently tried making a "tile object" editor plugin. There's a main tile object script with tile position properties that update its true position, and an editor plugin to handle its positioning in the editor.

# tile_object.gd
tool
extends Node2D

# Base size of tiles in pixels
export(Vector2) var tile_size = Vector2(16, 16) setget set_tile_size

# Dimensions of object in tiles
export(Vector2) var tile_dimensions = Vector2(1, 1) setget set_tile_dimensions

# Position of object in tiles
# Origin is top-left
export(Vector2) var tile_pos = Vector2(0, 0) setget set_tile_pos

func _ready():
	if get_parent() is TileMap:
		set_tile_size(get_parent().cell_size)

	var tile = position / tile_size
	set_tile_pos(Vector2(round(tile.x), round(tile.y)))

func set_tile_size(value):
	tile_size = value
	_update_position()
	if Engine.editor_hint:
		update()

func set_tile_dimensions(value):
	tile_dimensions = value
	if Engine.editor_hint:
		update()

func set_tile_pos(value):
	tile_pos = value
	_update_position()

func _update_position():
	position = tile_pos * tile_size

func _draw():
	if Engine.editor_hint:
		var rect = Rect2(Vector2(), tile_dimensions * tile_size)
		draw_rect(rect, Color(1, 0, 1), false)
# tile_object_plugin.gd
tool
extends EditorPlugin

const TileObject = preload("res://addons/tile_object/tile_object.gd")

var _current = null

func _enter_tree():
	add_custom_type("TileObject", "Node2D", TileObject, preload("res://addons/tile_object/tile_object_icon.png"))

func _exit_tree():
	remove_custom_type("TileObject")

func handles(object):
	return object is TileObject

func edit(object):
	if _current != object:
		_current = object

func make_visible(visible):
	if not visible:
		_current = null

func clear():
	_current = null

func forward_canvas_gui_input(event):
	if _current != null:
		if (event is InputEventMouseButton) and (not event.pressed):
			assert(_current is TileObject)
			var origin_tile = _current.position / _current.tile_size
			_current.tile_pos = Vector2(round(origin_tile.x), round(origin_tile.y))

Drawbacks are: Update's to a TileObject's tile_pos property caused by dragging doesn't appear in the inspector until I deselect and reselect the TileObject. Creating a node that subclasses TileObject is awkward. I add a TileObject as a root node, clear its script, and make a new script where I have to make sure I select the plugin script "tile_object.gd" as the parent. Not the type "TileObject", but the actual script inside the addon folder.