Need help about grid system

Godot Version

4.4

Question

Hello everyone, I’m new to the Godot Engine universe.

I’m currently working on a third-person farm management game, and I have a specific requirement for one of the gameplay elements: I’d like to have a grid of tiles that can be manipulated by the player.

Let me explain: in my game, I want to have an area of ​​50x20 tiles, for example (which can be expanded later by the player purchasing other plots, but for now, I’m starting with a single plot).

This grid will be composed of 1x1 cells, each of which will have different states (empty, planted, watered, ready to be harvested, etc.) depending on the tool selected in my hotbar.

The difficulty lies in a “selector” that I’d like to display over my grid as soon as a tile is hovered over. This selector would be 1x1 by default but could change size depending on the currently selected tool and its level (tools can be upgraded to increase their action area, speed, etc.).

First question: Is this grid system, with the selector, the different cell states, etc., feasible on Godot?

Second question: If it is feasible, what is the best way to achieve this functionality?

Also, if you have any tutorials or other resources that could be useful in my case, I would greatly appreciate them.

Thank you in advance for your help!

Yes a grid is feasible, you can make extensive use of the snapped function to place objects and your selector.

This seems like a perfect use for TileMapLayer, unless there’s something I’m missing.

Edit: TileMapLayer for the display, probably an array or an array of dictionaries for the cells:

var FieldData: Array = []
var FieldSize: Vector2i

enum SEED { none, wheat, corn, melon, pumpkin, barley }

func field_prepare(size: Vector2i) -> void:
    var index: int = 0

    FieldSize = size

    for y: int  in size.y:
        for x: int in size.x:
            FieldData[index].append({ "seed": SEED.none, "growth": 0.0, "water": 0.0 })
            index += 1

func water(cell: Vector2i, amount: float) -> float:
    var index = cell.x + (FieldSize.x * cell.y)
    FieldData[index]["water"] += amount
    return FieldData[index]["water"]

func plant(cell: Vector2i, seed: SEED) -> void:
    var index = cell.x + (FieldSize.x * cell.y)
    FieldData[index]["seed"]    = seed
    FieldData[index]["growth"] = 0.0

[...]
1 Like

Hi, thanks for your answer! I think you miss something, TileMapLayer is only for 2D right?
I’m on 3D scene.

There’s a GridMap which is a 3D equivalent to TileMapLayer, and the array code above extends to a third dimension if you add an inner Z loop.

But your description above implied 2D (“I have an area of 50x20 tiles…”), so I assumed 2D. :slight_smile:

TileMapLayer lives in a 2D scene, but you can layer things so there’s a 2D scene behind a 3D scene if you like. You could have a TileMapLayer ground with 3D plants growing on top; just make sure you set it up so that the “ground” is at the far clipping plane, or add a depth impostor so stuff that’s “below” the ground doesn’t draw on top of it.

Hi!
Update: The gridmap seems to be the solution I was looking for!

I’ve come up with a decent solution for managing the grid and the different sizes the selector can take. It’s not perfect; I still need to fix a few points, but it’s satisfactory for now.

extends Node3D

@onready var Grid = $GridMap
@onready var GridCamera = $Camera3D

@onready var hotbar = get_tree().get_root().get_node("Main/CanvasLayer/HotBar")

@export var selector_scene: PackedScene
@export var empty_scene: PackedScene
@export var selector_size: int = 1 
var selector_instance: Node3D
var empty_instance: Node3D
var current_tool = "" 

func _on_tool_changed(tool_id):
	current_tool = tool_id
	if current_tool == 'watering':
		selector_size = 2
	elif current_tool == 'hoe':
		selector_size = 4
	else:
		selector_size = 1
	
func _ready():
	hotbar.connect("current_tool", Callable(self, "_on_tool_changed"))
	if selector_scene:
		selector_instance = selector_scene.instantiate()
		selector_instance.get_node_or_null('MeshInstance3D').mesh.size = Vector2(selector_size,selector_size) 
		add_child(selector_instance)

func _process(_delta):
	var mouse_pos = get_viewport().get_mouse_position()
	var from = GridCamera.project_ray_origin(mouse_pos)
	var to = from + GridCamera.project_ray_normal(mouse_pos) * 1000

	var space_state = get_world_3d().direct_space_state
	var result = space_state.intersect_ray(PhysicsRayQueryParameters3D.create(from, to))

	if result:
		var hit_local = Grid.to_local(result.position) 
		var cell = Grid.local_to_map(hit_local)
		
		var offset = (selector_size - 1) / 2.0
		var centered_cell = Vector3i(
			round(cell.x - offset),
			0,
			round(cell.z - offset)
		)

		var cell_local_pos = Grid.map_to_local(centered_cell)
		var cell_global_pos = Grid.to_global(cell_local_pos)
		selector_instance.get_node_or_null('MeshInstance3D').mesh.size = Vector2(selector_size,selector_size) 
		selector_instance.global_position = cell_global_pos
		selector_instance.global_position.y = 0.1
		if selector_size % 2 == 0:
			selector_instance.global_position.x -= (selector_size - 1) / 2.0
			selector_instance.global_position.z += (selector_size - 1) / 2.0

		
func _input(event):
	if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
		if empty_scene:
			var mouse_pos = get_viewport().get_mouse_position()
			var from = GridCamera.project_ray_origin(mouse_pos)
			var to = from + GridCamera.project_ray_normal(mouse_pos) * 1000

			var space_state = get_world_3d().direct_space_state
			var result = space_state.intersect_ray(PhysicsRayQueryParameters3D.create(from, to))

			if result:
				var hit_local = Grid.to_local(result.position) 
				var cell = Grid.local_to_map(hit_local)
				
				var offset = (selector_size - 1) / 2.0

				var centered_cell = Vector3i(
					round(cell.x - offset),
					0,
					round(cell.z - offset)
				)
				var local_pos = Grid.map_to_local(centered_cell)
				var global_pos = Grid.map_to_local(centered_cell)
				if selector_size % 2 == 0:
					global_pos.x -= (selector_size - 1) / 2.0
					global_pos.z += (selector_size - 1) / 2.0
				var instance = empty_scene.instantiate()
				var mesh_node = instance.get_node_or_null("MeshInstance3D")
				if mesh_node and mesh_node.mesh is BoxMesh:
					var new_mesh := BoxMesh.new()
					new_mesh.size = Vector3(selector_size, 0.1, selector_size)			
					mesh_node.mesh = new_mesh
	
				instance.global_position = global_pos
				add_child(instance)

		

Now I need to focus on managing the different states!

Thanks for your help!