Tile-based positioning of objects

AaronAaron Posts: 7Member

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?

Tagged:

Tags :

Answers

  • o0Rh0mbus0oo0Rh0mbus0o Posts: 3Member

    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().

  • AaronAaron Posts: 7Member
    edited December 2018

    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.

Sign In or Register to comment.