Hey everyone! Welcome to my first Godot Engine tutorial since the release of 3.1 stable. Today we’re going to be covering grid-based movement, or RPG-styled movement. This means the player won’t simply snap to the next space but instead keep moving smoothly in a specified interval. We’ll also cover collision using a RayCast2D and the new TileSet editor from 3.1.
We’ll just be using the built in icon.png that comes with every new Godot project so don’t worry about any downloads. If you want to see the source, however, that can be found below.
Creating the player scene
First we’re going to start with a regular Node as our root. Under it we’ll create a new Sprite using the icon.png that comes with every new project. Just drag it from the FileSystem tab into the middle of the screen and rename it to “player”. We will be handling our collisions manually so we’re using a Sprite over the usual body nodes. In the Inspector, turn Centered off (under Offset). Save the scene as “test.tscn”.
Next add a script to our player node. Give it an empty template and save it as “player.gd”. We’ll start the script with a few variables.
extends Sprite var speed = 256 # big number because it's multiplied by delta var tile_size = 64 # size in pixels of tiles on the grid var last_position = Vector2() # last idle position var target_position = Vector2() # desired position to move towards var movedir = Vector2() # move direction
speed and tile_size are pretty self explanatory. Just make sure you set tile_size to whatever the width and height your player sprite is. When the player is moving, last_position is where he’ll be moving from and target_position is where he’ll end up. movedir is the direction he’ll go based off of our input (that we’ll set in a bit).
Our _ready() function is just going to have a few that makes sure the player is already snapped to the grid when the game starts. You could do this manually when creating your scene but it’s nice to have a backup.
func _ready(): position = position.snapped(Vector2(tile_size, tile_size)) # make sure player is snapped to grid last_position = position target_position = position
Now we need a function that will set our movedir based on our currently held keys. Create a new function called get_movedir() and add the following:
# GET DIRECTION THE PLAYER WANTS TO MOVE func get_movedir(): var LEFT = Input.is_action_pressed("ui_left") var RIGHT = Input.is_action_pressed("ui_right") var UP = Input.is_action_pressed("ui_up") var DOWN = Input.is_action_pressed("ui_down") movedir.x = -int(LEFT) + int(RIGHT) # if pressing both directions this will return 0 movedir.y = -int(UP) + int(DOWN) if movedir.x != 0 && movedir.y != 0: # prevent diagonals movedir = Vector2.ZERO
If you’ve seen my Zelda-like tutorials you know what’s up. First we create four variables for the four directions we can move. These are made as booleans. In the line after that we take LEFT and RIGHT and turn them into integers, make LEFT negative, and set movedir.x to that. That way if only the left arrow key is being held, we get movedir.x = -1 or Vector2(-1,0) which goes left. If both left and right are held, they add together to make 0 so the player does not move. It’s the same deal with UP and DOWN. The last two lines just check if you’re holding diagonal directions and set movedir to Vector2(0,0). As you can see I’m using some of Godot 3.1’s new Vector2 constants.
Our _process(delta) will have one section for idle and one section for moving.
func _process(delta): # MOVEMENT position += speed * movedir * delta if position.distance_to(last_position) >= tile_size: # if we've moved further than one space position = target_position # snap the player to the intended position # IDLE if position == target_position: get_movedir() last_position = position # record the player's current idle position target_position += movedir * tile_size # if key is pressed, get new target (also shifts to moving state)
The idle code has to be below the movement code (so that variables are only updated after any movement has taken place) but for the sake of simplicity I’ll explain it first. The if statement if position == target_position checks to see if the player is where they intended to go. We only want the player to be able to move if they are already in the middle of the tile. Otherwise you’d be able to change directions mid-movement which breaks the entire grid effect. We also set our last_position here which will be used in the movement code. Finally, we set our target_position based off of any keys we were just pressing. If no keys are pressed, that will remain the same.
Above all that is the movement code. We add speed * movedir * delta to our position in order to actually move the player. If movedir is Vector2(0,0) no movement will happen. Multiplying by delta (the time since the last frame) keeps our movement consistent with different frame rates. You can read more about delta timing here. The if statement underneath takes the distance between our current position and our last idle position. If it’s more than one tile, we snap to the grid. When we hold down the key the player will move smoothly, but if we just tap it they’ll move until they’re locked to the grid.
If we run the game now we should be snapped to the grid and be able to move around with the arrow keys. The only thing left to add is collisions, so hop onto the next page and we’ll do that.