If you followed all the tutorials in this series, at this point all you have is a character that can move freely on a map and a simple GUI. Of course, you must be satisfied to have achieved this result (especially if it’s the first time you create a game), but where is the real fun of an RPG if there are no monsters to hunt for?

In this tutorial, we are going to add monsters to our game. We will give them a very basic artificial intelligence: all they can do is chasing the player along the map when it’s close, or otherwise move randomly.

We will create the monster object (which, by the way, will be a skeleton) as a separate scene, which we’ll instantiate in multiple copies within the main scene. As a last thing, we’ll also see how to create a spawner, i.e. a node that takes care of automatically adding monsters to the scene.

Preparatory steps

Let’s open the project and pick up where we left off in the last tutorial.

Since we will add two new entities, we will create the respective folders in the FileSystem panel. Under the Entities folder, create the Skeleton and SkeletonSpawner folders:

Entities folder structure

Now, download the images that we will use for the skeleton by pressing the button below.

Download “SimpleRPG Skeleton Frames” skeleton_frames.zip – 14 KB

Once downloaded, import them into the Skeleton folder. Then, select all the images and, in the Import panel, deselect Flags → Filter and re-import them.

That done, we can move on to creating the skeleton scene.

Creating the skeleton scene

Create a new scene by clicking on the Scenes → New Scene menu:

Create new scene

We need to add a root node to this new scene. Since our monster will move and collide with other nodes on the map, we will use a KinematicBody2D as the root node, just like we did for the player. So, in the Scenes panel, click on the Custom Node button and add a KinematicBody2D node.

Add a custom node as scene root

Rename the node to Skeleton. You will see a warning that indicates that the collision shape is missing. We will add it later.

To draw the monster on the screen, add an AnimatedSprite node to Skeleton. With the AnimatedSprite node selected, in the Inspector, click next to the Frames property and select new SpriteFrames to create the resource that will contain the skeleton animations.

Create new SpriteFrames

Click on the newly created resource to open the SpriteFrames editor and create the animations listed in the table below. If you don’t remember how to do it, take a look at the 2D Sprite Animation tutorial.

NameFramesSpeed (FPS)Loop

For now, set down_idle as the default animation. Then, set the Z Index property of AnimatedSprite to 1.

Add a CollisionShape2D node to Skeleton and, in the Inspector, set the Shape property to a new RectangleShape2D resource. Then, resize the rectangle to cover the skeleton body.

Resize the rectangle to cover the skeleton

The last thing to add to Skeleton is a Timer node. We will use this timer to periodically check the state of the skeleton and decide its behavior (chasing the player or move randomly). With Timer selected, in the Inspector set Wait Time to 0.25 seconds and enable Autostart.

Now click on Scene → Save Scene or press Control + S to save the scene. Call it Skeleton.tscn and save it in the Entities/Skeleton folder.

Skeleton Artificial Intelligence

The Artificial Intelligence of the skeleton will be very simple. If the player is within a certain distance, the skeleton will move towards it. Otherwise, it will randomly decide whether to stay still, change movement direction or continue in the current direction. If the skeleton comes into contact with an obstacle, it will change direction casually in an attempt to get around it.

Attach a script to the Skeleton node. Call it Skelton.gd and save it in the Entitites/Skeleton folder, then open it.

First, let’s add all the variables we’ll need:

# Node references
var player

# Random number generator
var rng = RandomNumberGenerator.new()

# Movement variables
export var speed = 25
var direction : Vector2
var last_direction = Vector2(0, 1)
var bounce_countdown = 0

This is the list of variables and what they are used for:

  • player: it is a reference to the Player node. We will need it to get the player’s current position.
  • rng: is an object of type RandomNumberGenerator. As the name says, it’s a class for generating pseudo-random numbers. The new() method is used to create an object from a class.
  • speed: it’s the movement speed of the skeleton. The export keyword makes this variable visible in the Inspector.
  • direction: it’s the current movement direction of the skeleton.
  • last_direction: it’s the last direction of movement before stopping. It’s used to choose the correct idle animation.
  • bounce_countdown: when the skeleton hits an obstacle, it changes direction for a certain amount of time to try to go around it. This variable is the countdown used to restores the default behavior.

When the skeleton enters the scene tree, we need to do two things:

  • get the Player node reference
  • Initialize the random number generator

We must perform these operations within the _ready() function:

func _ready():
	player = get_tree().root.get_node("Root/Player")

The get_tree() function returns the current node hierarchy as a SceneTree object. Using the SceneTree root property, we get a reference to the root Viewport, to which we can request a node reference by passing its path to the get_node() function.

The randomize() method of RandomNumberGenerator use a time-based seed to initialize the random number generator.

As we said earlier, we want to use the timer to periodically decide the behavior of the skeleton. Basically, we want to execute a function every time the timer is triggered. To do this, we need to connect the Timer timeout() signal to our script.

In the Node panel, double-click on timeout(), select the Skeleton node and press Connect. The function to manage the timer signal will be automatically created.

Replace the function code with this:

func _on_Timer_timeout():
	# Calculate the position of the player relative to the skeleton
	var player_relative_position = player.position - position
	if player_relative_position.length() <= 16:
		# If player is near, don't move but turn toward it
		direction = Vector2.ZERO
		last_direction = player_relative_position.normalized()
	elif player_relative_position.length() <= 100 and bounce_countdown == 0:
		# If player is within range, move toward it
		direction = player_relative_position.normalized()
	elif bounce_countdown == 0:
		# If player is too far, randomly decide whether to stand still or where to move
		var random_number = rng.randf()
		if random_number < 0.05:
			direction = Vector2.ZERO
		elif random_number < 0.1:
			direction = Vector2.DOWN.rotated(rng.randf() * 2 * PI)
	# Update bounce countdown
	if bounce_countdown > 0:
		bounce_countdown = bounce_countdown - 1

The first thing this function does is calculate the player’s position relative to the skeleton. This vector will be used to determine the distance from the player, and to set the direction of movement and the orientation of the skeleton.

Then, the function checks the distance of the skeleton from the player. If it’s within 16 pixels, the skeleton is in contact with the player and doesn’t move (direction is set to zero), but the variable last_direction is set to turn the skeleton towards the player.

If the distance is greater than 16, but within 100 pixels, then the skeleton chases the player, setting its movement direction towards it.

In addition to the distance, the function checks if bouce_countdown is equal to 0. If it is not, the default behavior is ignored and the skeleton moves along the last direction (which, as we will see later, is set after calling move_and_collide() if there is a collision).

The last possibility is that the player is too far away from the monster. In that case, the randf() function is used to generate a random number between 0 and 1. If the number is less than 0.05, the skeleton stops; if it’s greater than 0.05 and less than 0.1, the skeleton will move along a randomly generated direction. This direction is obtained by rotating Vector2.DOWN by a random angle between 0 and 2π radians (0 to 360°). For any other value of the random number, the skeleton maintains the previous behavior.

So, in practice, at each Timer timeout, there is a 5% chance that the skeleton will stop and 5% probability that it will change direction of movement (as long as the player is out of range).

The last function we need to implement to complete the skeleton A.I. is _physics_process(). As we have already seen in the Player movement tutorial, this is the function in which it’s recommended to put code related to physics, including the movement of physical bodies. Enter this code in the script:

func _physics_process(delta):
	var movement = direction * speed * delta
	var collision = move_and_collide(movement)
	if collision != null and collision.collider.name != "Player":
		direction = direction.rotated(rng.randf_range(PI/4, PI/2))
		bounce_countdown = rng.randi_range(2, 5)

In _physics_process(), the function move_and_collide() is used to move the skeleton, exactly the same way we used it to move the player. In this case, however, we also use its return value (a KinematicCollision2D object) to determine if there has been a collision with a body other than Player.

If this happens, the current direction of movement is rotated by a randomly generated angle obtained using the randf_range() function. This angle has a value between π/4 and π/2 radians (45° to 90°). In addition, an integer number between 2 and 5 is generated randomly (using the randi_range()) function, to be used as a countdown for the “bounce” on the obstacle.

Now that the A.I. is completed, instantiate some monsters in the scene dragging Skeleton.tscn on the Root node, and try running the game to test them. If you want, you can play with the values inside the previous functions, to fine-tune skeletons behavior.

Animating skeleton sprite

To animate the skeletons, we will use two functions that we will copy almost exactly from Player:

func get_animation_direction(direction: Vector2):
	var norm_direction = direction.normalized()
	if norm_direction.y >= 0.707:
		return "down"
	elif norm_direction.y <= -0.707:
		return "up"
	elif norm_direction.x <= -0.707:
		return "left"
	elif norm_direction.x >= 0.707:
		return "right"
	return "down"

func animates_monster(direction: Vector2):
	if direction != Vector2.ZERO:
		last_direction = direction
		# Choose walk animation based on movement direction
		var animation = get_animation_direction(last_direction) + "_walk"
		# Play the walk animation
		# Choose idle animation based on last movement direction and play it
		var animation = get_animation_direction(last_direction) + "_idle"

The only modified function is animates_monster(), and there are only a few changes compared to the version used in Player:

  • the name of the function.
  • the name of the AnimatedSprite node.
  • the removal of the line that sets the FPS of the animation based on the speed (skeletons move at a constant speed).

For details on how these functions work, please refer to 2D Sprite Animation tutorial.

There are other animations besides idle and walk, for example birth, which should not be interrupted by the execution of the animates_monster() function. In order not to interrupt them, we need a variable that tells us if one of these animations is playing:

# Animation variables
var other_animation_playing = false

Finally, let’s add this code to _physics_process() to play walk and idle animations:

# Animate skeleton based on direction
if not other_animation_playing:

Run the game now to see skeletons idle and walk animations.

Skeleton Spawner

To test skeletons, we have manually added them to our scene. However, during the game we want to use a system that automatically generates the player’s enemies, so that they never fall below a certain number even after they have been killed. So, we are going to create a so-called spawner, i.e. a node that handles the instantiation of monsters when necessary.

First of all, remove all the skeletons you had manually added to the main scene.

Then, open the Skeleton.tscn scene, select the Timer node and in the Inspector disable Autostart. We will start the timer from the script when we instantiate the skeletons. We can’t have the timer active right away, because the skeletons would start moving before completing the spawning procedure.

Create a new scene for our spawner by clicking Scene → New Scene. Having the spawner in a separate scene will allow us, if we wish, to instantiate more than one spawners in the main scene.

The root node of this scene can be simply a Node2D, so in the Scene panel click on 2D Scene:

Create a 2D scene

Rename this node to SkeletonSpawner and add a Timer node to it. With Timer selected, enable Autostart in the Inspector and leave Wait Time at 1 second.

Save the scene with the name SkeletonSpawner.tscn inside the Entities/SkeletonSpawner folder. Now attach a script on the SkeletonSpawner node and save it as SkeletonSpawner.gd in the same folder as before.

Spawner script

Let’s start by adding all the variables we’ll need:

# Nodes references
var tilemap
var tree_tilemap

# Spawner variables
export var spawn_area : Rect2 = Rect2(50, 50, 700, 700)
export var max_skeletons = 40
export var start_skeletons = 10
var skeleton_count = 0
var skeleton_scene = preload("res://Entities/Skeleton/Skeleton.tscn")

# Random number generator
var rng = RandomNumberGenerator.new()

Here’s how we’ll use these variables:

  • tilemap, tree_tilemap: in these variables we will store the references to the tilemaps. When we place the monsters randomly, we will use tilemaps to find out if there is an obstacle in the chosen position.
  • spawn_area: it’s a rectangle that represents the area in which we want the monsters to appear.
  • max_skeletons: it’s the maximum number of skeletons that can be on the map at the same time. Note that this value is for single spawner.
  • start_skeletons: it’s the number of skeletons created at the beginning of the game. Note that this value is for single spawner.
  • skeleton_count: it’s the number of skeletons (created by this spawner) currently present on the map.
  • skeleton_scene: it’s the resource that represents the Skeleton scene. The preload() function returns a resource from the filesystem that is loaded during script parsing.
  • rng: a RandomNumberGenerator object, used to randomly generate the positions where to place new skeletons.

The spawner will create new skeletons at two different times: at game start and at each timer timeout. Since we will need the code to instantiate skeletons on more than one occasion, we should write it down inside a function:

func instance_skeleton():
	# Instance the skeleton scene and add it to the scene tree
	var skeleton = skeleton_scene.instance()
	# Place the skeleton in a valid position
	var valid_position = false
	while not valid_position:
		skeleton.position.x = spawn_area.position.x + rng.randf_range(0, spawn_area.size.x)
		skeleton.position.y = spawn_area.position.y + rng.randf_range(0, spawn_area.size.y)
		valid_position = test_position(skeleton.position)

	# Play skeleton's birth animation

First, the function instantiates skeleton_scene using the instance() function. This function returns a scene of type Skeleton, which we add to the node hierarchy using the add_child() function.

Now, we need to place the skeleton at a random point within the spawn area. However, not all positions on the map are valid for the placement of a skeleton. For example, we cannot place the monster in the water or on an obstacle. If we get an invalid position, we need to regenerate it until we get a valid one.

To do this, we use a while loop. The while statement is a control flow statement that allows code to be executed repeatedly, based on a given boolean condition. In our case, we will repeat the code inside while as long as valid_position is false (and, consequently, the condition not valid_position is true). Inside the while loop, we generate a random position and check it with the function test_position(), which we will create shortly. If test_position() returns true, then the position is valid, otherwise it is not.

The last line of instance_skeleton() calls the arise() function of Skeleton. We will create this function later. It will simply play the skeleton’s birth animation.

Add the test_position() function to the script:

func test_position(position : Vector2):
	# Check if the cell type in this position is grass or sand
	var cell_coord = tilemap.world_to_map(position)
	var cell_type_id = tilemap.get_cellv(cell_coord)
	var grass_or_sand = (cell_type_id == tilemap.tile_set.find_tile_by_name("Grass")) || (cell_type_id == tilemap.tile_set.find_tile_by_name("Sand"))
	# Check if there's a tree in this position
	cell_coord = tree_tilemap.world_to_map(position)
	cell_type_id = tree_tilemap.get_cellv(cell_coord)
	var no_trees = (cell_type_id != tilemap.tile_set.find_tile_by_name("Tree"))
	# If the two conditions are true, the position is valid
	return grass_or_sand and no_trees

This function takes as argument the position of the map to be checked. tilemap‘s world_to_map() method returns the cell coordinates corresponding to the given local position. We use that coordinates as the argument of the get_cellv() function, which returns an index that identifies the type of tile in that cell. To find out the index corresponding to a certain type of tile, we use the find_tile_by_name() function of TileSet. We then compare the indexes to know if the cell is of type Grass or Sand (the only ones that are valid for positioning skeletons).

We repeat the same procedure with tree_tiles, this time to check that there are no trees in that position. If both tests are successful, the position is valid and the function returns true.

Skeletons instantiation

We said earlier that we need to instantiate skeletons on two occasions. The first is at startup, so we need to enter the instantiation code in the _ready() function, together with other initialization code:

func _ready():
	# Get tilemaps references
	tilemap = get_tree().root.get_node("Root/TileMap")
	tree_tilemap = get_tree().root.get_node("Root/Tree TileMap")
	# Initialize random number generator
	# Create skeletons
	for i in range(start_skeletons):
	skeleton_count = start_skeletons

The first two lines retrieve the references to the main scene tilemaps, used in the test_position() method. The following line initialize the random number generator.

The code immediately afterwards is the one that actually handles monsters instantiation. The for statement is a control flow statement used to iterate through a range. The range() function, when used with only one argument n, returns all the integers between 0 and n-1. So, in our case, the for loop executes the instance_skeleton() function a number of times equal to the value of start_skeletons, thus instantiating that number of skeletons.

Also, we want to create a new skeleton every second, unless the skeleton count is already equal to or greater than max_skeletons.

Go to the Node panel and connect SkeletonSpawner‘s Timer timeout() signal to the script. The function to handle the signal will be automatically added to the script. Replace the code with this:

func _on_Timer_timeout():
	# Every second, check if we need to instantiate a skeleton
	if skeleton_count < max_skeletons:
		skeleton_count = skeleton_count + 1

Completing Skeleton script

Now that we’re done with the spawner, let’s go back to Skeleton and add the missing function arise() to the script:

func arise():
	other_animation_playing = true

If we were to add the spawner to the main scene and try the game now, we would see that, after reproducing the birth animation, the skeletons would remain stuck in its last frame. This is due to the fact that, at the end of the animation, we have to do two more things:

  • start the timer to enable Artificial Intelligence
  • set to false the other_animation_playing variable to allow walk and idle animations to play.

So, connect the animation_finished() signal of AnimatedSprite to the script and replace the automatically created function with this code:

func _on_AnimatedSprite_animation_finished():
	if $AnimatedSprite.animation == "birth":
		$AnimatedSprite.animation = "down_idle"
	other_animation_playing = false

Using the spawner

Open the main scene and drag SkeletonSpawner.tscn to the Root node. To observe more easily how the spawner works, with SkeletonSpawner selected, in the Inspector set the size of the Spawn Area to 200, 100.

Set Spawn Area size to 200×100

Run the game to see the spawner in action.

This is not a safe place!

When you have tested that everything is working, remember to reset Spawn Area to the default values, clicking on the reset icon:

Reset Spawn Area size


In this tutorial we had learned a lot of useful things that you’ll often meet when developing games in Godot:

  • How to instantiate scenes, either manually or by code.
  • How to get nodes from the node hierarchy, using get_tree() and get_node() functions.
  • How to use while and for loops
  • How to generate random numbers
  • How to use Timers

You can try what we’ve done so far by clicking here:

In the next tutorial we will add sword fighting to the game, so you can finally kill your enemies! But be careful, because they can kill you too!


Ben · November 14, 2019 at 2:18 am

Glad the next part is out! This one was fun. Nice job on the skeleton sprite and spawn animation, they look great. Watching them spawn in is super satisfying.

I could be wrong, but I think in get_animation_direction(), you have an extra return “down” at the bottom. I copied my code from the Player scene and it did not have this, and it ran without issue.

Some other questions:

Any reason for the name disparity between Player’s “Sprite” and Skeleton’s “AnimatedSprite”? If they’re the same type, why change the name?

Is it actually necessary to create a RandomNumberGenerator object in each script? Other tutorials I’ve seen just call randomize() to initialize the RNG but never make the object.

Is there any way to get the spawn area for SkeletonSpawner from a property of the map? As in, could you do something along the lines of spawn_area = tilemap.rect.size? I know tilemap doesn’t have a rect property, but is there a similar way to achieve what I’m talking about?

    Davide Pesce · November 14, 2019 at 9:47 am

    Hi Ben, thanks for the support!

    I try to answer you point by point:

    I could be wrong, but I think in get_animation_direction(), you have an extra return “down” at the bottom. I copied my code from the Player scene and it did not have this, and it ran without issue.

    The last return is executed only when direction is a zero vector. It should never happen, but I prefer the function to handle every possibility (you never know).

    Any reason for the name disparity between Player’s “Sprite” and Skeleton’s “AnimatedSprite”? If they’re the same type, why change the name?

    For the player we initially used a Sprite node, and then replaced it with an AnimatedSprite in a later tutorial. When replacing a node, Godot keeps the old name (and thus for the player the name remained Sprite).
    For the skeletons we created the node from scratch, so it was called by its default name (AnimatedSprite).

    Is it actually necessary to create a RandomNumberGenerator object in each script? Other tutorials I’ve seen just call randomize() to initialize the RNG but never make the object.

    In Godot there are two ways to generate random numbers:

    – using the built-in functions
    – using a RandomNumberGenerator object as in this tutorial

    Sometimes it’s useful to have multiple random number generators with different seeds, for example when a map is generated procedurally and you want a repeatable result. In that case, multiple RandomNumberGenerator objects are used. Maybe the use of RandomNumberGenerator is overkill for this little project, but it’s nice to know how to use it.

    Is there any way to get the spawn area for SkeletonSpawner from a property of the map? As in, could you do something along the lines of spawn_area = tilemap.rect.size? I know tilemap doesn’t have a rect property, but is there a similar way to achieve what I’m talking about?

    You can use the get_used_rect() function to get the TileMap rectangle, but this rectangle is in grid coordinates and cannot be used directly. To get the rectangle in global coordinates, you have to multiply all its components by the size of the single cell, obtained with the cell_size property:

    var tilemap_rect : Rect2 = tilemap.get_used_rect()
    spawn_area.position.x = tilemap_rect.position.x * tilemap.cell_size.x
    spawn_area.position.y = tilemap_rect.position.y * tilemap.cell_size.y
    spawn_area.size.x = tilemap_rect.size.x * tilemap.cell_size.x
    spawn_area.size.y = tilemap_rect.size.y * tilemap.cell_size.y

    This code also works for non-square cells.

      Ben · November 16, 2019 at 8:16 pm

      Not sure what the markup is for quotes, but I’ll also respond point-by-point:

      Re: return “down”
      Ah, got it. I didn’t realize return ends the function immediately (though it seems obvious now), so I thought the last one would override anything done in the if statements.

      Re: Sprite/Animated Sprite
      Whoops, I guess I just forgot that that happened.

      Re: RNG
      “Sometimes it’s useful to have multiple random number generators with different seeds, for example when a map is generated procedurally and you want a repeatable result.”
      This is cool, thanks for the explanation.

      Re: get map rect
      Awesome. I figured there had to be some way to get the map, but didn’t know what it was.

      Thanks again! Looking forward to Part 11.

        Davide Pesce · November 17, 2019 at 10:40 pm

        Not sure what the markup is for quotes

        You can use standard html tags inside comments, like <blockquote> for quoting text or <code> to insert code.

        Ah, got it. I didn’t realize return ends the function immediately (though it seems obvious now), so I thought the last one would override anything done in the if statements.

        I actually forgot to explain how return works, I’m going to add it right away!

        Thanks again for the feedback!

Ben · November 18, 2019 at 1:11 pm


I actually forgot to explain how return works, I’m going to add it right away!

Good idea, that will help people who have never encountered it before (or people like me who just didn’t understand every detail of its operation).

Jim Storey · January 24, 2020 at 3:58 am

Awesome tutorial thanks. If anyone else can’t see the skeletons after dragging them on the root node, mine were hidden and stuck at zero, zero. I clicked on the skeleton under the root node and changed the transform to 30,30.
Thanks again David for amazing work, much appreciated, I’m about to send this link to my son as he wants to do game programming.

    Davide Pesce · January 24, 2020 at 9:47 am

    I am glad you liked my tutorials, I hope they can help your son!

Syl · January 29, 2020 at 10:22 pm


Thxs for your tutorials, they’re so helpful for my curent project, but i’m stuck with ‘the rise of the undead’: dunno where to put the skeleton.tscn at the end of the ‘skeleton artificial intelligence’ part.
How to dragg the skeleton.tscn into the root node and instantiate the skeletons? They don’t appears and wherever i put the skeleton.tscn, the root node isn’t found.

    Davide Pesce · January 29, 2020 at 10:42 pm

    Hi Syl!

    To instantiate a Skeleton, drag the Skeleton.tscn file from the FileSystem panel and drop it on the Root node of the main scene. The Skeleton is added as the last child of root at position 0,0. Alternatively, first select the Root node of the main scene and then drag the skeleton.tscn file on the map where you want to place the Skeleton.

    If you don’t see it, you may need to set Skeleton’s Z Index property to 1 to display it above the TileMap.

      Syl · January 29, 2020 at 10:58 pm

      Cheers! That works fine. Didn’t know that we could instantiate that way. No need of autoload then?

        Davide Pesce · January 29, 2020 at 11:31 pm

        No, there is no need.

Peter Januarius · February 7, 2020 at 7:17 am

Great tutorials!

I’ve just done #11 and simply can’t get my skeletons to follow my player. I’ve compared the code, used debug statements but just can’t see where it is is different or wrong. My skeletons seem to take the random direction path even though the proximity check passes:

elif player_relative_position.length() <= 100 and bounce_countdown == 0:
# If player is within range, move toward it
print("Player is within range…")
direction = player_relative_position.normalized()

Pretty stuck at the moment.. sigh!


    Davide Pesce · February 7, 2020 at 8:56 am

    Hi Pete,

    the first thing you could try is to download the project from GitHub and check if it works for you. If so, try comparing the code with yours. Otherwise we will try to investigate further!

Leave a Reply

Your email address will not be published. Required fields are marked *

By continuing to browse this Website, you consent to the use of cookies. More information

This Website uses:

By continuing to browse this Website without changing the cookies settings of your browser or by clicking "Accept" below, you consent to the use of cookies.