player controller and “collision detection”, part #4 of godot-roguelike series
Sun 23 October 2016 2D Godot roguelike Unity3D , 0 comments
Mon 24 October 2016

disclaimer

Although I have quite a bit of programming experience in high level languages, I may not be qualified to teach you anything about game development as I am learning myself and I probably do not have a sense of how code should be written just yet. So please keep in mind that I am very new at this as well and don’t take it as the definite way to do it. It is only my way

updates

2016-10-24 18:35: corrected a mistake in explaining the collision masks and physics layers. Initially I had mentioned that the physics objects will interact with each other if they are placed in the same physics layer, but that’s not true, the layer mask determines what gets detected, the explanation has be updated

overview

with Godot v2.1.stable.officiali-godot
project files

Before you continue make sure you have the project files from the last part as you will need them here if you want to follow along!

OK, time has arrived to implement the player character’s movement and collisions. We’ll be using FSMs (Finite State Machines) for this. There are a lot of ways this can be done, ranging from dumping everything in the _input() function with loads and loads of flag variables (for me using flags is a no no! like… don’t use them… ever! In my opinion one should have 0 flags or as close to 0 as possible! Don’t take my word for it, hear it from the masters) checking for states, etc. This would be a complete mess. Don’t do it! There are other patterns out there, stuff that you’ll have to search on your own, because here we’ll be using FSM as previously mentioned. I found that in the case of a simple turn base game such as this one a FSM works pretty well and as we shall see states can be shared between entities, that is, some of the states will be used by both player and AI which is really the holy grail of programming design, having reusable code & objects. But be aware that we might end up falling into an extreme, where we want everything to be fully optimized etc. Don’t be fooled, optimizations are nice, but you might end up falling through the rabbit hole and loosing track of what’s the real objective, that is to have a fully functional game. Optimizations can come later through refactoring and reimplementation so don’t get too hung up on this at the early stage. Speaking of which, our next task is to refactor some code since the Game node from the main scene should really be only a mediator for different bits of the game. It will also be the ‘engine’ of the game, that is it will drive the turn base system.

Note that I’m not going to cover what FSMs are here, there are people far better prepared to explain how they work, just follow the link and read the introduction to FSMs before moving on so that you have an understanding of what’s happening here!

overview

With that out of the way, let’s start off with refactoring the code. Also there’s one thing I forgot to implement in the earlier parts of the tutorial, and that is to export a variable to the inspector so that we can choose the zoom level of the window. As you’ll see this is very very simple and very neat!

refactoring the code

At the moment all of the functionality is dumped inside of the script attached to the Game node of the main scene. This isn’t really ideal. We know that we have a ‘board’ of some sorts so it might be a good idea to create a separate object (node) just for that. This will also be used later when we want to implement level transitions as we’ll have to recreate the board each time. It just makes things neater.

This is a really painless process since we were so careful to create the game in an organized way. To start off, make sure you have Game.tscn opened, then create a new node of type Node. Call it Board. We will be moving all of the ‘board’ related functionality into this node as you can imagine. Next add a script to it by right clicking the new node and selecting Add Script from the menu. Save the script in res://Scripts/Board.gd. Now we’re ready to move the code around.

Move everything from the Game.gd script to Board.gd except the first extends Node line, the _ready() and __set_screen() functions. Don’t forget to leave intact the extends Node line in Board.gd as well. This is a requirement. At the end of this operation Game.gd should contain this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
extends Node

func _ready():
    self.fetch_tiles('res://Scenes/TileSet.tscn', 'Exit')
    self.make_board()
    self.__set_screen()

func __set_screen():
    var board_size = (2 * (self.perim_thickness + Vector2(1, 1)) \
                     + self.inner_grid_size) * self.__tile_collection.tile_size
    self.get_tree().set_screen_stretch(SceneTree.STRETCH_MODE_2D, \
                                       SceneTree.STRETCH_ASPECT_KEEP, board_size)
    OS.set_window_size(2 * board_size)

Click here to reveal the full Board.gd file

extends Node


export var inner_grid_size = Vector2(8, 8)

export var perim_thickness = Vector2(1, 1)
export var count_obstacles = Vector2(2, 7)
export var count_items = Vector2(3, 6)
export var count_enemies = Vector2(2, 2)
var __tile_collection
var __grid


func fetch_tiles(tile_set_filepath, size_node_name):
    self.__tile_collection = TileCollection.new()

    var tile_set = load(tile_set_filepath).instance()


    for node in tile_set.get_children():
        self.__tile_collection.item[node.get_name().to_lower()] = node
    self.__tile_collection.tile_size = (tile_set \
                                        .get_node(size_node_name) \
                                        .get_texture() \
                                        .get_size())



func make_board():
    randomize()

    self.__grid = self.__make_grid()
    self.__add_base_tiles()

    self.__add_other_tiles(self.__tile_collection.item.obstacles, count_obstacles)
    self.__add_other_tiles(self.__tile_collection.item.items, count_items)
    self.__add_other_tiles(self.__tile_collection.item.enemies, count_enemies)


    self.__add_tile(self.__tile_collection.item.exit, Vector2(self.inner_grid_size.x, -1))
    self.__add_tile(self.__tile_collection.item.player, Vector2(-1, self.inner_grid_size.y))


func __make_grid():
    var grid = []
    for x in range(self.inner_grid_size.x):
        for y in range(self.inner_grid_size.y):
            grid.push_back(Vector2(x, y))
    return grid


func __add_base_tiles():
    var bounds = {
        xmin = -self.perim_thickness.x - 1, xmax = self.inner_grid_size.x + self.perim_thickness.x,
        ymin = -self.perim_thickness.y - 1, ymax = self.inner_grid_size.y + self.perim_thickness.y
    }

    for x in range(bounds.xmin, bounds.xmax + 1):
        for y in range(bounds.ymin, bounds.ymax + 1):
            var tile
            if (x == bounds.xmin or x == bounds.xmax or y == bounds.ymin or y == bounds.ymax):

                tile = self.__rand_tile(self.__tile_collection.item.walls)
            else:

                tile = self.__rand_tile(self.__tile_collection.item.floors)
            self.__add_tile(tile, Vector2(x, y))


func __add_other_tiles(tile_set, count=Vector2(1, 1)):
    var n = int(rand_range(count.x, count.y))

    for i in range(n):

        var idx = int(rand_range(0, self.__grid.size()))
        var xy = self.__grid[idx]
        var tile = self.__rand_tile(tile_set)
        self.__add_tile(tile, xy)
        self.__grid.remove(idx)


func __add_tile(tile, xy):
    var tile_dup = tile.duplicate()
    tile_dup.set_pos((self.perim_thickness + Vector2(1, 1) + xy) * self.__tile_collection.tile_size)
    self.add_child(tile_dup)


func __rand_tile(tile_set):
    var tiles = tile_set.get_children()
    var r = int(rand_range(0, tiles.size()))
    return tiles[r]


class TileCollection:
    var item = {}
    var tile_size
Note that I have omitted the documentation and comments from the source files. {: .info}

We’re not quite done yet, if you try to run the project right now it will give an error because inside of our Game.gd script we are trying to call some non-existent functions. They have been moved to the Board.gd script. Let’s fix it. Before the _ready() function from Game.gd add this line:

onready var __board = self.get_node('Board')

This will make a variable that will hold a reference to the Board node so we can manipulate it (call functions and set it up). While we’re at it let’s also change our hard coded value for the zoom level and instead make a variable that is accessible from the inspector panel so we can tweak it as needed. Add this line before the var __board line:

export var window_zoom = 1

Now all we have to do is call the functions on the __board object instead of just self. Just replace self. with self.__board where we referred to the variables of the ‘board’.

Click here to reveal the full Game.gd file

extends Node


export var window_zoom = 1
onready var __board = self.get_node('Board')


func _ready():
    self.__board.fetch_tiles('res://Scenes/TileSet.tscn', 'Exit')
    self.__board.make_board()
    self.__set_screen()


func __set_screen():
    var board_size = (2 * (self.__board.perim_thickness + Vector2(1, 1)) \
                      + self.__board.inner_grid_size) * self.__board.__tile_collection.tile_size
    self.get_tree().set_screen_stretch(SceneTree.STRETCH_MODE_2D, \
                                        SceneTree.STRETCH_ASPECT_KEEP, board_size)
    # NOTE also the change here. instead of the hard coded 2, now we have
    # `self.window_zoom` which can be set from the inspector, try it, go to the inspector
    # and change the `Window Zoom` variable and see what happens when you run the project!
    OS.set_window_size(self.window_zoom * board_size)

There’s one little detail that we need to take care of. Remember our convention about public and private variables? Well, at the moment we’re calling self.__board.__tile_collection in the __set_screen() function inside Game.gd. This breaks the contract we made for ourselves about the member variables. Let’s fix it: replace __tile_collection with tile_collection in both Game.gd and Board.gd. A simple way to do this is to use the search & replace feature of the text editor. Select Search > Replace (or press CTRL + R) and fill the first text box with __tile_collection and the second one with tile_collection and press the Replace All button. Do this for both Game.gd and Board.gd. If you try running the project right now it should work as expected.

finally, the player!

We’ll divide this into two parts, first we’ll be creating a separate scene for the Player object to keep things neat and organized then we’ll look at implementing the code that drives the Player object through FSM and finally we’ll look at how to actually implement this FSM.

creating the Player scene

Fig. 01. Player scene node structure. Note the exclamation mark, that’s an icon on which we can hover with the mouse to get extra information from Godot, try it!

Let’s start off by adding the required nodes to the Player object so that we can detect collisions. We’ll first create a separate scene just for the Player node so that we’ll have things organized. This isn’t required but it’s never a good idea to have everything in one scene, the more we have our assets split up into logical components the better it will be for the health of the project.

Make sure you’re in the TileSet scene and right click on the Player node and select Save Branch as Scene from the menu. Save it in the res://Scenes folder with the given name. It’s safe to remove the Player node from the TileSet scene so just do that, we’ll next instantiate it from the newly created Player scene. Right click on the TileSet root node, select Instance Child Scene and choose the Player scene file from res://Scenes.

You might think this is redundant but it isn’t, we’ll be modifying the Player scene next and all of the modifications will be carried over to the TileSet scene file! This also has another benefit. Should we ever need to have the player object in another scene file, say for example we’ll have a special tutorial section in our game and we don’t want to use the board, but just the Player object to exemplify something. Then in this tutorial scene all we have to do is instantiate the Player scene and it will have all of the functionality that we pack into it. Pretty cool, right?

the Area2D and CollisionShape2D nodes

This node is a container node for other CollisionShape2D nodes and is used to detect collisions with other objects. As I said, to detect, not to react so in other words, this node doesn’t prevent objects from overlapping and it has no physics. Other things that it can do is to alter physics parameters (such as gravity, damping, etc.) in certain areas. But the question is, why is it a container node only? Why not have an associated shape with it from the start? Consider you want to have the player trigger an event if he’s located in two or more places in the map/game. It would be less than ideal to have multiple Area2D nodes just for this. That’s why this is just a container, it can contain multiple CollisionShape2D nodes spread around wherever we’d like so we don’t have to duplicate objects/functionality! That’s very handy!

So now we know that Area2D is a container for CollisionShape2D nodes that can be placed anywhere really. This CollisionShape2D does nothing except that it defines the places where the Area2D node operates in. The CollisionShape2D needs a Shape resource which can be set through the Inspector panel. Godot comes with a predefined list of useful shapes: CapsuleShape2D, CircleShape2D, ConcavePolygonSahpe2D, ConvexPolygonShape2D, LineShape2D, RayShape2D, RectangleShape2D and finally SegmentShape2D. As you can see there’s quite a lot of options to choose from, accommodating all cases, simple to complex.

Now, it’s useful to know that these nodes don’t have a physical representation in the game so how can we debug such a thing?! Godot has us covered:

Moving on, open up the Player scene and right click on the Player node and select Add Child Node (or hit CTRL + A), search for Area2D and instantiate it. Now, with Area2D selected add a CollisionShape2D node. Finally make sure the Player node is selected and add 4 RayCast2D nodes and rename them as follows: RayCast2DRight, RayCast2DDown, RayCast2DLeft, RayCast2DUp. You should have the tree structure from Fig. 01 now.

the RayCast2D node

This node is useful for detecting collisions with other objects as you can imagine. It’s basically a segment that points in some direction that detects when it overlaps another Area2D or any other node inheriting from PhysicsBody2D. It doesn’t have a physical representation in the game and so in order to see it when debugging we need to turn on Visible Collision Shapes as explained in the Area2D and CollisionShape2D explanation

Fig. 02. RayCast2D nodes set-up. Make certain you have the same options.

Now that we have the nodes let’s set them up:

  1. Player: make sure this node is selected first and then go to the Node panel (the tab near the Inspector tab) and select Groups (near the Signals button). Here we can specify which groups the selected node is associated with. This is very handy for collision detections etc. For example, we’ll have the Player node in it’s separate player group and the two Enemy nodes associated with the group enemy so when we detect collisions instead of querying for every possible enemy name, we’ll just ask if it’s part of the enemy group and immediately know what needs to be done. Awesome! Refer to Fig. 03 if you have trouble finding the panel.
  2. Area2D: set its position to (16, 16) in the inspector. This being a child node of the Player AnimatedSprite node, this position will be relative to this node, which means that if the Player root node will move, then Area2Ds position will still be (16, 16). This is exactly what we want as we’d like all of the children nodes to move along with the parent node
  3. CollisionShape2D: in the inspector press on <null> at the Shape property and select New RectangleShape2D from the menu then press the > arrow to edit this rectangular shape and input (16, 16) in its Extents property. Why 16 and not 32 (the size of the sprite in pixels) you ask? That’s because the Extents property works with halves as you might have guessed so we need 32/2 = 16 here.
  4. select all of the RayCast2D objects and modify their position to (16, 16), just like Area2D. Next, with them still selected make sure Enabled is ticked in the inspector (they are turned off by default!). Tick the second and third check boxes for Layer Mask and untick the first one, finally make sure that only Area is ticked under Type Mask. Check Fig. 02 if unsure.
  5. RayCast2DRight: set Cast To to (32, 0)
  6. RayCast2DDown: set Cast To to (0, 32)
  7. RayCast2DLeft: set Cast To to (-32, 0)
  8. RayCast2DUp: set Cast To to (0, -32)

We’ll get to why there needs to be four RayCast2D nodes here in the next part: the implementation of the FSM.

collision layers and masks

In Godot the interacting objects (Area2D, RayCast2D, StaticBody2D etc.) can be placed on phyiscs layers and can have defined additional masks which define which physics layers the object will scan for detecting collisions. “A contact is detected if object A is in any of the layers that object B scans, or object B is in any layer scanned by object A”. Taken word for word from Godot documentation. There are 32 layers in total.

Fig. 03. Associating groups with nodes

Fig. 04. Associating groups with nodes

We’re all done with setting up the Player scene. But before moving on, let’s do one more thing, let’s create some actual obstacles so that our Player object has something to interact with, also to limits movement so it doesn’t go off screen.

In the TileSet scene we’ll be adding Area2D with the CollisionShape2D to all objects except for floor tiles. This gives me the opportunity to show to you another cool little feature. Since we already created the Player object Area2D with the proper structure and settings, let’s reuse that. But how?! It’s in a different scene altogether! Godot has us covered! Open up the tree so you can see the TileSet/Walls/1 node, and right click on it then select Merge From Scene, navigate to the res://Scenes/Player.tscn scene and open it. From the new menu just double click on the Area2D node. BAM! We have our Area2D along with the CollisionShape2D with appropriate values placed under the TileSet/Walls/1 node! Perfect! Now we can repeat this process for all of the nodes except for the nodes underthe Florors node. Or, you can duplicate the newly created Area2D node by selecting it and hitting CTRL + D then moving it under the right place. With the duplication operation you’ll have to rename Area2D1 to Area2D after you’ve placed it under the right node. This isn’t mandatory but it keeps things nice and clean! No matter the way you chose to do it, the tree structure should look like the one in Fig. 04!

Note that I’ve omitted showing the Obstacles and Enemies structure in Fig. 04 as it would have made it too large, but they should have exactly the same structure as the nodes under Walls for example.

If you leave things as they are and play the game you’ll notice that the Player object isn’t interacting with any of the obstacles… what gives?! Remember when we set the Layer Mask on those RayCast2D nodes? (Fig. 02) Well, it’s because of that, the Area2D nodes we just created are on physics layer 1 but the RayCast2D objects are instructed to interact only with the objects located in physics layers 2 and 3. Let’s correct this. Select all of the Area2D nodes (except for the ones under Enemy) by holding down CTRL while clicking on them in the Scene panel and put them on physics layer 3 in the Inspector panel. Do the same for the Area2D nodes under Enemies, but place them on physics layer 2 instead. This separation will come handy later.

Now we’re finally able to move on to some coding!

Area2D & CollisionShape2D size and placement

Now I know what you’re thinking, we went through all of that trouble to calculate the tile size based on the sprite dimension and here we are, setting up things manually, this position at (16, 16), that size at (32, 0) etc.! This is madness you say! Well no, not really, we are doing it this way for now just to have something to play with, in the next part of the tutorial we will be updating all of these dimensions from script and you’ll see how easy it is! But you’ll have to be patient because this part is long enough as it is so I’ll cover it in the next part!

implementing the FSM

The Player will be able to do these actions: move and chop (bushes, not enemies, there will be no way to attack enemies in this one, sorry, Unity tutorial’s idea). Let’s think about this for a minute, the Player can be in an Idle state, it can have a Moving state and finally we should have an Inactive state. Since we’re talking about a turn based game, the Player should be in this Inactive state whenever the enemies are moving for example or at the end of the level when the Player reaches the exit sign so the user doesn’t accidentally move the object around. So we have only 3 states to take care of.

Our state objects will need this interface:

  1. an enter() method for setting up any number of things needed for the functionality of the state. This will be called on the new state whenever a transition to this new state happens
  2. an input() method which will be called whenever the Player object processes the input events
  3. an update() method which will be called every physics frame in order to check collisions and implement any other necessary actions

Fig. 05 shows the logic behind the state transitions. Note that we’ll have to send some info from the Idle state to the Moving state about the user input somehow, because depending on which key the user will press, the movement of the Player needs to reflect the desired direction. So there is some interdependency here, just something to keep in mind when designing these systems.

Fig. 05. States and their possible transitions. Note that there’s no way to transition from the Inactive state to any other state!

The way we’re going to build these state objects will be very generic, this way both the Player object and the Enemy AI can use the same states as they are identical. Except for the Idle state because the Idle state is responsible for checking user input in case of the Player object and it will be responsible for starting off the AI logic once the Enemys turn begins so it makes sense to have these as separate objects. But the Inactive and the Moving states are identical and we’ll reuse them for the AI later.

We’ll need to store these states somewhere. We can either instantiate them from the script, or we can create nodes with this functionality. Let’s do the second as it we’ll have the added bonus of being able to manipulate some parameters directly from the Inspector panel if need be. And since they’ll be used by both the Player and the AI, the Game scene seems to be a reasonable place to have them. Open up the Game.tscn scene if it isn’t opened and make sure the root Game node is selected. Create a new node of type Node and call it States. This will basically be the container for our states. Now create three more nodes under States with the following names: IdlePlayer, Inactive and Moving.

Now for the coding let’s start by creating a Base state. This doesn’t need to be part of the scene tree as it will be used just for inheriting so make sure you’re in the Script view (F2) and go to File > New to create a new script and save it as res://Scripts/States/Base.gd. You’ll have to create the res://Scripts/States folder. Put this inside of it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
extends Node

onready var __parent = self.get_node('..')

func enter(entity):
    pass

func input(entity, event):
    pass

func update(entity, delta_time):
    pass

As you can see it pretty much does nothing except get the __parent node (in this case it will be States). Why is this useful? Well, some states will make use only of the enter() method for example, but the Player object doesn’t know this, it will blindly try to call enter(), input() and update() whenever necessary. This means that these methods need to exist in all states even though some will do nothing. But having a Base class we’ll have less typing to do and also less risk of making a mistake. Whenever we duplicate gode we risk of making mistakes, that’s just how things are.

Godot‘s _input, _process & _fixed_process exposed API

As I have explained in the previous parts of the tutorial, Godot nodes have some methods that we can call, such as the _ready() method. It’s time to learn about the others we’ll be using in 99% of our projects:

  • _input(event): this function is called whenever Godot receives any input (mouse move, mouse click, joystick button pressed/release, key pressed/released etc.) it takes one parameter, the event which has information about what event took place as you can imagine, for example in case of the mouse move it stores information about the position of the mouse etc.
  • _process(delta_time): this function is called on every rendering frame and gets a parameter that represents how much time (in seconds) has passed from the last frame. All logic that wouldn’t interact with the physics engine or any processing that doesn’t need to be called at a fixed frame rate can go in here. It can also be used for querying for user input every frame instead of waiting for the user to trigger the _input() function.
  • _fixed_process(delta_time): this function is exactly like _process() except it’s called for every physics frame so Godot tries to call this one at a fixed rate of 60 frames per seconds. This is useful for anything involving physics as you guessed.

Unlike the _ready() function, all of these other functions are not called by default by Godot. In order to have them ‘activated’ we need to turn them on by calling: set_process_input(true), set_process(true) and set_fixed_process(true). They can of course be turned off just as well, by providing false to these functions instead. So this is cool because they can be ‘activated’ whenever we need and turned off otherwise so Godot won’t process them needlessly!

Let’s move on to the actual states. The simplest next one is the Inactive state so let’s take care of it. Right click on the Inactive state, select Add Script and save it at res://Scripts/States/Inactive.gd. It will only contain this:

1
2
3
4
5
6
7
8
extends 'Base.gd'  # <-- note this line, this is how we inherit from our
                   #     custom `Base` class, by specifying the (relative
                   #     or absolute - using `res://`) path to the `extends`
                   #     keyword. Very intuitive!

func enter(entity):
    entity.set_fixed_process(false)
    entity.set_process_input(false)

All that this states does is to turn off the processing that would be turned on by other states’ enter() method. For example the Moving state as we shall see will need to use the _fixed_process() function in order to drive the physical movement of the Player object, but in the Inactive state there’s no need for it so when entering the Inactive state we just turn them off. Moving on, let’s look at the IdlePlayer state.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
extends 'Base.gd'

func enter(entity):
    entity.set_fixed_process(false)
    entity.set_process_input(true)

func input(entity, event):
    var delta_xy
    if event.is_action_pressed('ui_right'):
        delta_xy = entity.UNIT_RIGHT
    elif event.is_action_pressed('ui_down'):
        delta_xy = entity.UNIT_DOWN
    elif event.is_action_pressed('ui_left'):
        delta_xy = entity.UNIT_LEFT
    elif event.is_action_pressed('ui_up'):
        delta_xy = entity.UNIT_UP
    if event.is_action_pressed('ui_right') or event.is_action_pressed('ui_down') \
       or event.is_action_pressed('ui_left') or event.is_action_pressed('ui_up'):
        var state = self.__parent.get_node('Moving')
        state.delta_xy = delta_xy
        entity.transition_to(state)

This one is fairly simple as well. On entering the state, _fixed_process() is turned off (remember that the Moving state has a transition to Idle, check Fig. 04 and moving the object needs to be done in _fixed_process()) and the _input() function is activated.

Next is the input() method (remember that this is called inside of the _input() function from the Player object) is called where we resolve the user input and transition to the Moving state if necessary. First we define a variable delta_xy which we’ll use to send information to the Moving state in case the user input is valid. So if the user presses the appropriate buttons using (checked by event.is_action_pressed()) we get the Moving state (remember the Base.gd class references the container States node) and set its delta_xy member variable to the direction associated with the key press (these constants: UNIT_RIGHT, UNIT_DOWN, etc. are defined in the Player script, they’re just unit vectors pointing in one of the 4 directions, up, down, left, right). This will be used in the Moving state to drive the Player movement in the right direction. Finally we transition to the Moving state (using the entity.transition_to() method which is defined in the Player script). So all of this happens only if the user input is valid, if not, this function basically does nothing and gets triggered again when the user initiates the input.

input actions in Godot

Godot has an action input system that abstracts the need to check specific key or button presses. This is done as you saw in the code above through the event.is_action_pressed method. There’s also the counterpart event.is_action_released() method for checking when the user releases the key or button for example.

Check the system out by going to Scene > Project Settings and by looking at the Input Map tab. Here you can see the default actions defined by Godot and we’re also able to define our own actions if need be. As you can see, a number of keys and joystick buttons are grouped together into an action such as ui_left for example. So instead of manually checking what key was pressed inside the _input() function, we can check directly for the action. This is way better than hard coding our keys and buttons and it makes it much more convenient to change key mappings from a Settings screen for example inside our game! This is great! Don’t ever hard code these things inside of your game, it’s a bad idea!

Finally, here’s the last state, the Moving state which is a bit more complex:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
extends 'Base.gd'

export var duration = 0.25
var __time_elapsed = 0
var __pos_start
var delta_xy
onready var __board = self.get_node('/root/Game/Board')


func enter(entity):
    entity.set_process_input(false)
    entity.set_fixed_process(true)
    self.__time_elapsed = 0
    self.__pos_start = entity.get_pos()
    if entity.ray_casts[delta_xy].is_colliding():
        var state_name
        if entity.is_in_group('player'):
            state_name = 'IdlePlayer'
        elif entity.is_in_group('enemy'):
            state_name = 'IdleEnemy'
        entity.transition_to(self.__parent.get_node(state_name))


func update(entity, delta_time):
    var delta_xy_px = self.delta_xy * self.__board.tile_collection.tile_size
    var pos = self.__pos_start.linear_interpolate(self.__pos_start + delta_xy_px, \
                                                  self.__time_elapsed/self.duration)
    if self.__time_elapsed > self.duration:
        pos = self.__pos_start + delta_xy_px
        var state_name
        if entity.is_in_group('player'):
            state_name = 'IdlePlayer'
        elif entity.is_in_group('enemy'):
            state_name = 'IdleEnemy'
        entity.transition_to(self.__parent.get_node(state_name))
    self.__time_elapsed += delta_time
    entity.set_pos(pos)

This being a turn based game there’s no need to bother with physics here, with speed and acceleration and so forth so instead it makes more sense to define the duration in which the Player object moves from one tile to the other, that’s line 3: export var duration = 0.25. See this export? Remember what it does? This is the cool thing about having states as nodes instead of just having them in code! This value can now be changed in the Inspector tab. Now you might argue that this isn’t such a big deal, you could have as well changed the value in code and it would be fine. Yes, but I’m giving you here alternatives and think about it when you’ll want to make plugins and extensions for Godot, then you’ll want to expose properties in the interface so that developers will easily play with the values. How about making a FSM plugin for Godot? Well before doing that maybe you’d like to check the already existing one first :)

Moving on, we have the __time_elapsed variable to keep track of the elapsed time since movement initiation. __post_start is a variable used in calculating the trajectory and location of the Player object once the movement starts, delta_xy is a public variable, it’s the one we set in the IdlePlayer state. And finally __board is a reference to the Board object we created at the beginning which we’ll need in order to get the tile_size property to be able to calculate the location of the Player.

The enter() function initializes some important things:

  • it disables _input() processing as not to trigger it when the user taps the button multiple times by mistake or because he wants to
  • _fixed_process() is turned on, this is where the on screen movement actually takes place
  • __time_elapsed is reset to 0
  • __pos_start is initialized to the position of the Player object before the movement takes place
  • now, remember the ray casts that we set up on the Player object in the previous part? This is where they’re used, we’re checking if they’re colliding with any of the Area2D nodes from the TileSet scene given the direction in which the user wants to move and if it collides then we just jump back to the Idle state, thus canceling the movement (this ray_casts Dictionary is defined in the Player script). Remember what the enter() function from the IdlePlayer state does. It turns on _input() and it disables __fixed_process(), pretty simple isn’t it? As you can see this here also checkes the group of the entity parameter in order to jump to the appropriate Idle state. If it’s the Player it jumps to IdlePlayer, if it’s the AI it jumps to IdleEnemy

Finally we have the update() function which is called inside of _fixed_process() on the Player object.

We start by converting the unit vectors set in IdlePlayer to vectors in pixel units since we need to move a delta_xy amount in a given direction, but in pixels not some other arbitrary units. Next, we simply calculate the position at which the Player will be this frame by interpolating between __pos_start and __pos_start + delta_xy_px based on the elapsed time. This should be simple enough to understand. __time_elapsed/duration will return a value between 0 and 1, when the value is 0, the returned position will be __pos_start, when the value is 1, the pos will be set to __pos_start + delta_xy_px. Refer to Fig. 05 for a diagram. In each call to _fixed_process() we need to check if __time_elapsed is greater than duration, in which case we do two things, first update the position to be exactly at __pos_start + delta_xy_px as to not introduce rounding errors due to the nature of the limited representation of real numbers in computers and secondly, since __time_elapsed is larger than duration it means that the movement has finished so we instruct the Player object to move back to the IdlePlayer state.

If the transition hasn’t finished and (__time_elapsed > duration evaluates to false) then we need to increase __time_elapsed with the delta_time given to us by Godot in the _fixed_process() function and finally update the position of the Player object to the new location based on the interpolated value between __pos_start and __post_start + delta_xy_px.

Fig. 06. Exemplifying interpolation.

why four RayCast2D objects?!

Before moving on I’d like to address the reason why this design includes four RayCast2D nodes on the Player object. Godot has input handling, rendering and physics processing done internally through servers, they aren’t servers in the sense of web-servers, think about them as independent systems instead.

Now, the RayCast2D object has a method called cast_to() which can be used to point the ray in a different direction and then evaluate the collision but the problem is, since the input is handled in a separate system from the physics system, the cast_to() needs a few frames to propagate the modification in to the physics engine. So it isn’t as simple as calling cast_to() inside of _input() and then check for collisions.

Because of this limitation I ended up using four RayCast2D objects pointing in the allowed four directions as this will not need any calls to cast_to(). This idea was given to me by a member of ever increasing and great Godot community: Lars Kokemohr, dean of engineering at the School4Games

At the time of this writing there is an unresolved issue on github considering exactly this limitation! Check out this Godot github issue for more info on the matter and if you want to get involved!

using the states with the Player object

Once again, before moving on go check FSMs out first otherwise you won’t understand much of what’s happening here!

This part is going to be very simple really. All we have to do is think about the implementation needed to change the Players states and process these states. That’s all, we don’t even care about what the states are doing at this stage, we just know that we want to work with a FSM, and that’s enough information to complete the Player script. How cool is that?

So to start off, right click on the Player node, choose Add Script and save it as res://Scripts/Actor.gd. We’ll find out why Actor.gd and not Player.gd later.

There are a couple of things we need to take care of first. We need that we want the Player to move one tile at a time only horizontally and vertically, we also know what the Player needs to have a state that needs to be processed when it is active. Let’s define the necessary variables and constants:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
extends AnimatedSprite

const UNIT_RIGHT = Vector2(1, 0)
const UNIT_DOWN = Vector2(0, 1)
const UNIT_LEFT = Vector2(-1, 0)
const UNIT_UP = Vector2(0, -1)
onready var ray_casts = {
    UNIT_RIGHT: self.get_node('RayCast2DRight'),
    UNIT_DOWN: self.get_node('RayCast2DDown'),
    UNIT_LEFT: self.get_node('RayCast2DLeft'),
    UNIT_UP: self.get_node('RayCast2DUp')
}
onready var __states = self.get_node('/root/Game/States')
onready var __state = self.__states.get_node('IdlePlayer')

First we create some constants useful when resolving the user input. Depending on what key the user presses, these constants will define the direction in which the Player will move. As you can see they’re defined in unit vectors instead of vectors with dimension (say the dimension of the sprite), this we if we want to change the dimensions of the image everything will still work as expected! We’re trying to keep everything as change-agnostic as possible so we won’t have a hard time modifying things later on if need be.

Next we have the __state variable which is a private variable (as defined by our convention). This will be assigned to the appropriate state.

Finally we have a Dictionary which stores the RayCast2D child nodes with appropriate keys depending on where they point at. We’ll be using this Dictionary to check for collisions depending on the key the user presses, that’s why each RayCast2D is associated with a direction.

Now on to the functions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func transition_to(state):
    self.__state = state
    self.__state.enter(self)

func _ready():
    self.__state.enter(self)

func _input(event):
    self.__state.input(self, event)

func _fixed_process(delta_time):
    self.__state.update(self, delta_time)

That’s all there is to the Player script! Wow, right? All we’re doing is entering an initial state in the _ready() function and then just delegating work to that state in the _input() and _fixed_process() functions. Now you know why we called this script Actor.gd instead of Player.gd, because it’s so generic that we are going to reuse it for the AI enemies as well!

The functions are pretty much self explanatory, but let’s go over them one by one anyway:

  1. the transition_to() function takes a state parameter and sets the internal variable __state to it then triggers the enter() method from this given state. This enter() method is a way to set-up the state and possibly includes clean-up code etc., just like it explains in gameprogrammingpatterns’ FSM
  2. as I mentioned in the previous parts of this tutorial series, the _ready() function gets called by Godot automatically when the node enters the scene tree. This is usually the place to set things up so here we are just triggering the enter() method of the initial state
  3. _input() & _fixed_process() are explained earlier, go check it out if you haven’t done so yet

closing remarks

That’s it for this part! At the end of it you should have a working Player object that you can move around, it should take care of collisions and it should be smart enough to know not to receive input from the user after the move action has been already initiated. Hope this was helpful to you, as always, let me know what you think in the comments section and if you spot any mistakes give me a shout!

project files

Get the zipped finished project for this part with annotated scripts and see if you could follow along! Or if just want to consult them.