2D Topdown Grid Based Movement Troubles

Godot Version

Godot v4.2

Question

I am attempting to make a grid based movement system for a game I’m working on and I keep hitting roadblocks. The intention is to somewhat emulate the feel of old Pokemon games with their tile-based world. I have cobbled together some code but I really can’t seem to figure out how to get the character to move smoothly and without interruption.

extends CharacterBody2D

@export_category("Foundational")
@export var PlayerCamera: Camera2D
@export var sprDefault: AnimatedSprite2D

@onready var ray: RayCast2D = $RayCast2D
@onready var PlayerSprite: AnimatedSprite2D = sprDefault

var player_speed = 4
var moving = false
const tile_size = 16
const inputs = {
	"player_right": Vector2.RIGHT,
	"player_left": Vector2.LEFT,
	"player_down": Vector2.DOWN,
	"player_up": Vector2.UP,
}

func _unhandled_input(event):
	if moving:
		return
	for dir in inputs.keys():
		if event.get_action_strength(dir):
			$PlayerSprite.play(dir)
			move(dir)

func move(dir):
	ray.target_position = inputs[dir] * tile_size
	ray.force_raycast_update()
	if !ray.is_colliding():
		var tween = create_tween()
		tween.tween_property(self, "position",
			position + inputs[dir] * tile_size, 0.8/player_speed).set_trans(Tween.TRANS_LINEAR)
		moving = true
		await tween.finished
		moving = false

This is my player code as is. It successfully follows the grid although its not exactly what I’m looking for. The player stutters as they walk (which stutters the camera as well) and the player will freeze for a moment before moving every time a movement key is pressed causing it to feel clunky and just awful.

What approach should I be coming at this from? Is there a way to fix this block or do I have to rewrite the whole thing? I just need a grid based movement solution that feels smooth while still being locked to the grid. Any help appreciated!

1 Like

You need to disconnect the input from the move code because you will not be able to move again until you press or release a button. The input code should manage a direction then in a process or physics function check if moving and call move when not moving with the present direction.

Move should also check if there is no direction in case player isn’t inputting and character isn’t moving

I’m not sure I understand? How would I go about doing that?

I modified the quoted code, it probably won’t compile and you need to add inputmap actions. But I hope you get the gist. You could also normalize the direction so they always equal 1 or 0 or -1

I also setup the input to only have one direction at a time. And the last direction to be pressed will take over.

I attempted to modify your example to compile but It gives an error I dont understand

I’m going to be honest I am still very confused on what this code even means, and it doesn’t seem to work.

var dir : Vector2
func _unhandled_input(event):
	if event.is_action("player_up") or event.is_action("player_down"):
		dir = Vector2.ZERO
		dir.y = Input.get_axis("player_up","player_down")
		if dir.y == 0.0: # key was released let's check if left or right is still held
			dir.x = Input.get_axis("player_left","player_right")
	elif event.is_action("player_left") or event.is_action("player_right"):
		dir = Vector2.ZERO
		dir.x = Input.get_axis("player_left","player_right")
		if dir.x == 0.0:
			dir.y = Input.get_axis("player_up","player_down")
	  
			
func _process(_delta):
			move(dir)

func move(dir):
	if moving or dir == Vector2.ZERO:
		$PlayerSprite.play(dir)
		return
	ray.target_position = inputs[dir] * tile_size
	ray.force_raycast_update()
	if !ray.is_colliding():
		var tween = create_tween()
		tween.tween_property(self, "position",
			position + inputs[dir] * tile_size, 0.8/player_speed).set_trans(Tween.TRANS_LINEAR)
		moving = true
		await tween.finished
		moving = false

It doesn’t like that the global class variable dir is the same as the parameter variable in move called dir.

I think it should scope okay, but it could definitely lead to confusion for you the coder. I would suggest naming them differently. _dir is a general naming convention for class member variables. I don’t like “_duck” naming scheme personally, because Godot allows you to hide warnings for unused parameters in the same way…

another option is to remove the parameter and let the function access the class member directly. This should be fine but it is generally a bad practice as it will become harder to debug later as your codebase growsand it is not easy to write unit tests for, but is okay for early prototypes.

I’m not really concerned with a lot of the stuff you are mentioning, I just want working smooth movement locked to a grid. This isn’t for a huge project or anything. I am very much still lost, I ended up reverting the projects code back to what I originally wrote cause I just couldn’t get your suggestion to work.

1 Like

Changing gears then, I’m concerned by this and what it means. If you hold multiple buttons this will cause multiple moves in a single frame.

Have you tried using var as a global variable? i think it would work and be less laggy. :star_struck:

I got that line from a tutorial somewhere, I am extremely willing to change it for something more efficient and readable.

The easiest way to get a direction vector is using the Input Singleton method.
Input.get_vector(negative x action, position x action, negative y action, positive y action)

The downside is that it doesn’t work for grid like movement like Pokemon. So if you are holding two keys at forming a diagonal it will return a normalized vector of (x=0.785,y=0.785).

We have to scheme a way to only walk in the cardinal directions only, and no diagonals.

The current setup works to keep a cardinal directions, but if two keys are pressed to for a diagonal it will pass three vectors over two frames; first frame, right is pressed and move((x=1.0, y=0.0)) is called; then down is pressed some other frame and right is still held: move( (x=1.0, y=0.0) ) and move((x=0.0, y=1.0)) are called during the same for-loop.

My previous goal was to separate the input from the move function so that it is no longer being called, and we can focus on just making a single cardinal direction that makes sense, as we will need to make some design choices.

The input design choices

A player can mash all four keys at once, or two keys in opposite direction (up and down, left and right). What should this do for movement? Should it cancel our to zero or should the newest key to be pressed be the direction the player walks?

The second design choice, if two keys are pressed in a diagonal how should the player move here as well? Should the newest input move the player in that direction? Or should up/down be prioritized over left/right or vice versa?

using the input

Once we have decided on how input is recognized. We should then move and animate the player. The player should walk a whole grid space before moving to the next grid space.

In this script it awaits the finish of the tween animation during the grid move and it was guarded by the moving Boolean in the input function.

While a character is moving and the moving guard is true, the player could change key directions.

Currently if the player changes key directions during the move animation, the input is dropped by the moving guard. And when the animation ends and moving is false. The player is holding key but the character is staying still.

another design question

This is another design question, should the player continue to move multiple grid spaces if a key is held?

If we want the character to keep moving, then we need to always be monitoring changes of input, and only when the character stops moving, check what the current input is and move in that direction again. This cycle should be repeated over and over.

If you want to have the character stop and “latch” onto a grid space until a new key is pressed. We need to work the code in another way. Almost like it already is now.

So before we can continue can you answer the movement design questions to the desired behavior you are after?

1 Like

My apologizes for the lack of extended context, this is my first time posting to the forums like this. I appreciate your patience!

I am trying to very closely emulate the movement style of old Pokemon (Like Pokemon Emerald or Gold) or the sorta feel you’d get from an RPGMaker game. So that means no diagonal movement and no “latching” onto a grid space. Holding down the key should keep the player moving smoothly in that direction, and you should be able to change direction easily so that it doesn’t feel clunky. The code I originally had lacks most of the stuff I listed, all it really does correctly is keep you locked to a grid when you move. It should also just prioritize the newest key pressed to keep things smooth.

1 Like

I just watched the two tutorials here.
He is showing how to do pokemon style movement and it is pretty easy to understand and follow.
It is for GODOT 3.x though so you will have to convert some code to 4.x (as well as some editor functionality). But it looks like very trivial conversion.

1 Like

i remember i have achieved this by using tweens instead, i cant find which game it was right now, but it goes like this; constantly check your player’s global position and if the x or y axis aren’t clamped to your grid size (lets say 16px) then the position will slowly go towards your direction. and if the input stops, your current global_position will be clamped as well for the last time and another tween will play out. you may need to save the vector to which you are facing.

The instructions in the tutorial worked p well for what I was trying to go for! for the sake of anyone else who might be struggling here is the result I reached after watching some of the videos and adapting it to 4.x

class_name Player extends CharacterBody2D

@export_category("Foundational")
@export var PlayerCamera: Camera2D
@export var sprDefault: Sprite2D

@onready var ray: RayCast2D = $CollisionRay
@onready var PlayerSprite: Sprite2D = sprDefault
@onready var AnimTree: AnimationTree = $AnimationTree
@onready var AnimState = AnimTree.get("parameters/playback")

@export_category("Movement")
@export var walk_speed = 6.0

const TILE_SIZE = 16

var startPos = Vector2(0, 0) # the starting position of a movement
var inputDir = Vector2(0, 0) # what direction you input to move
var isMoving = false # if the player is currently in motion
var moveTilePercent = 0.0 # percent moved to next tile

func _ready():
	# setup startPos var and make player idle and looking downwards
	startPos = position
	AnimTree.set("parameters/Idle/blend_position", Vector2(0,1))
	
func _physics_process(delta):
	# code for handling if player is moving or not, and what animations to play
	if isMoving == false:
		# if player is not moving, check for input
		process_player_input()
	elif inputDir != Vector2.ZERO:
		# if player is moving, animate movement and call movement function
		AnimState.travel("Walk")
		move(delta)
	else:
		# if player is not moving, set idle animation and reset isMoving
		AnimState.travel("Idle")
		isMoving = false

func process_player_input():
	# finds what direction the player is pressing
	if inputDir.y == 0:
		inputDir.x = int(Input.get_action_strength("player_right")) - int(Input.get_action_strength("player_left"))
	if inputDir.x == 0:
		inputDir.y = int(Input.get_action_strength("player_down")) - int(Input.get_action_strength("player_up"))
	
	# sets up animation trees and triggers isMoving
	if inputDir != Vector2.ZERO:
		AnimTree.set("parameters/Idle/blend_position", inputDir)
		AnimTree.set("parameters/Walk/blend_position", inputDir)
		startPos = position
		isMoving = true
	else:
		AnimState.travel("Idle")

func move(delta):
	# uses a raycast to check the tile the player is attempting to move to
	ray.target_position = Vector2(inputDir * TILE_SIZE / 2)
	ray.force_raycast_update() # NOTE if you don't force update the raycast this breaks!
	
	if !ray.is_colliding():
		moveTilePercent += walk_speed * delta
		# resets startPos to new location after moveTilePercent reaches 1.0
		# (meaning that a full movement between tiles has occured, and is finished)
		if moveTilePercent >= 1.0:
			position = startPos + (TILE_SIZE * inputDir)
			moveTilePercent = 0.0
			isMoving = false
		else: # if player is still moving, keep doing that! lol
			position = startPos + (TILE_SIZE * inputDir * moveTilePercent)
	else: # this code prevents the player from getting stuck when facing a wall
		moveTilePercent = 0.0
		isMoving = false