Recently started going through a nice little tutorial on gdquest.com that teaches grid based movement in Godot.
Only problem is, the code in the tutorial is currently for Godot 3. And there are quite a few differences between Godot 3 and Godot 4.
Some of it is just changing the names of some functions. Some of it is complete behavior change and requires changing the structure of the code.
Below are the scripts from the tutorial with the changes needed for Godot 4 up through step 3 “Creating the Unit” in the GDQuest tutorial. I will include highlights to the necessary changes and commentary to my own changes.
Grid class
# Represents a grid for a tactical game, size width and height and size of each cell in pixels
# helper functions for calculating coordinates
class_name Grid
extends Resource
# grid rows and columns
# first change here is the use of the @ symbol on export
# the decorators in Godot 4 use an @keyword vs
# just a plain keyword in Godot 3
@export var size := Vector2(30, 30)
@export var cell_size := Vector2(80, 80)
var _half_cell_size = cell_size / 2
# get pixel position from grid coordinates
func calculate_map_position(grid_position: Vector2) -> Vector2:
return grid_position * cell_size + _half_cell_size
# get grid coordinates from pixel position
func calculate_grid_coordinates(map_position: Vector2) -> Vector2:
return (map_position / cell_size).floor()
# check that cursor or move stays inside grid boundaries
# I just renamed vars / rewrote this to be a little clearer about what it is doing
func is_within_bounds(cell_coordinates: Vector2) -> bool:
var inside_x := cell_coordinates.x >= 0 and cell_coordinates.x < size.x
var inside_y := cell_coordinates.y >= 0 and cell_coordinates.y < size.y
return inside_x and inside_y
# Godot 4 was not really liking using clamp as the name here
# since it is a built in func
# so just updated it to be gridclamp
func gridclamp(grid_position: Vector2) -> Vector2:
var clamped_position := grid_position
clamped_position.x = clamp(clamped_position.x, 0, size.x - 1.0)
clamped_position.y = clamp(clamped_position.y, 0, size.y - 1.0)
return clamped_position
func as_index(cell: Vector2) -> int:
return int(cell.x + size.x * cell.y)
Unit class
# again use of the @ symbol on the decorators
@tool
class_name Unit
extends Path2D
@export var grid: Resource = preload("res://resources/Grid.tres")
@export var move_range := 6
# setget no longer works in Godot 4
# there are a couple of new ways to create your getter and setter
# using a function inside of the set: block triggered an infinite loop
# so we write the setter code here
@export var skin: Texture :
set(value):
skin = value
if not _sprite:
# yield has been replaced with await
# and we await the value on the self object
await self.ready
_sprite.texture = value
get:
return _sprite.texture
@export var skin_offset := Vector2.ZERO :
set(value):
skin_offset = value
if not _sprite:
await self.ready
_sprite.position = value
get:
return _sprite.position
@export var move_speed := 600.0
var cell := Vector2.ZERO :
set(value):
cell = grid.gridclamp(value)
get:
return cell
var is_selected := false :
set(value):
is_selected = value
if is_selected:
_anim_player.play("selected")
else:
_anim_player.play("idle")
get:
return is_selected
var _is_walking := false :
set(value):
_is_walking = value
set_process(_is_walking)
get:
return _is_walking
@onready var _sprite: Sprite2D = $PathFollow2D/Sprite
@onready var _anim_player: AnimationPlayer = $AnimationPlayer
@onready var _path_follow: PathFollow2D = $PathFollow2D
signal walk_finished
func _ready() -> void:
set_process(false)
self.cell = grid.calculate_grid_coordinates(position)
position = grid.calculate_map_position(cell)
# function renamed from .editor_hint
if not Engine.is_editor_hint():
curve = Curve2D.new()
var points := [
Vector2(2,2),
Vector2(2,5),
Vector2(8,5),
Vector2(8,7),
]
walk_along(PackedVector2Array(points))
func _process(delta: float) -> void:
# .offset has been renamed to .progress
_path_follow.progress += move_speed * delta
# .unit_offset is now .progress_ratio
if _path_follow.progress_ratio >= 1.0:
self._is_walking = false
_path_follow.progress = 0.0
position = grid.calculate_map_position(cell)
curve.clear_points()
emit_signal("walk_finished")
# PoolVector2Array is now PackedVector2Array
func walk_along(path: PackedVector2Array) -> void:
# there is no path.empty() value
# so we use not path.size() instead
if not path.size():
return
curve.add_point(Vector2.ZERO)
for point in path:
curve.add_point(grid.calculate_map_position(point) - position)
cell = path[-1]
self._is_walking = true
I hope this helps you on your journey of making games.
Keep getting wiser, stronger, and better.