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
birthskeleton_death_6.png
skeleton_death_5.png
skeleton_death_4.png
skeleton_death_3.png
skeleton_death_2.png
skeleton_death_1.png
7Off
deathskeleton_death_1.png
skeleton_death_2.png
skeleton_death_3.png
skeleton_death_4.png
skeleton_death_5.png
skeleton_death_6.png
7Off
down_attackskeleton_down_idle_1.png
skeleton_down_attack_1.png
skeleton_down_attack_2.png
3Off
down_idleskeleton_down_idle_1.png
skeleton_down_idle_2.png
1On
down_walkskeleton_down_idle_1.png
skeleton_down_walk_1.png
skeleton_down_idle_1.png
skeleton_down_walk_2.png
5On
left_attackskeleton_left_idle_1.png
skeleton_left_attack_1.png
skeleton_left_attack_2.png
3Off
left_idleskeleton_left_idle_1.png
skeleton_left_idle_2.png
1On
left_walkskeleton_left_idle_1.png
skeleton_left_walk_1.png
skeleton_left_idle_1.png
skeleton_left_walk_2.png
5On
right_attackskeleton_right_idle_1.png
skeleton_right_attack_1.png
skeleton_right_attack_2.png
3Off
right_idleskeleton_right_idle_1.png
skeleton_right_idle_2.png
1On
right_walkskeleton_right_idle_1.png
skeleton_right_walk_1.png
skeleton_right_idle_1.png
skeleton_right_walk_2.png
5On
up_attackskeleton_up_idle_1.png
skeleton_up_attack_1.png
skeleton_up_attack_2.png
3Off
up_idleskeleton_up_idle_1.png
skeleton_up_idle_2.png
1On
up_walkskeleton_up_idle_1.png
skeleton_up_walk_1.png
skeleton_up_idle_1.png
skeleton_up_walk_2.png
5On

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")
	rng.randomize()

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
		$AnimatedSprite.play(animation)
	else:
		# Choose idle animation based on last movement direction and play it
		var animation = get_animation_direction(last_direction) + "_idle"
		$AnimatedSprite.play(animation)

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:
	animates_monster(direction)

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()
	add_child(skeleton)
	
	# 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
	skeleton.arise()

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
	rng.randomize()
	
	# Create skeletons
	for i in range(start_skeletons):
		instance_skeleton()
	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:
		instance_skeleton()
		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
	$AnimatedSprite.play("birth")

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"
		$Timer.start()
	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

Conclusions

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!




47 Comments

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

Testing…

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

Greets!

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!

Pete….

    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!

Anastasia · February 19, 2020 at 6:24 pm

I’m stuck…
firstly i have this line:
var player_relative_position = player.position – position
it says identifier “position” is not declared for the comment scope. it refers to -position

secondly:
var collision = move_and_slide(movement)
method ‘move_and_collide’ is not declared in the current class

Please help!

    Anastasia · February 19, 2020 at 6:29 pm

    move and collide i meant. its the same

    Davide Pesce · February 19, 2020 at 10:10 pm

    The fact that it says move_and_collide isn’t declared makes me think you’re using the wrong type of node for the skeleton. Is it a KinematicBody2D? Is the first line of your skeleton script extends KinematicBody2D?

      Anastasia · February 20, 2020 at 6:41 am

      Yes it is! I used move_and_slide for my player script in the line movement =move and slide() (movement =vector2) And didn’t encounter any problems which is weird.
      Also do you know why I have the position problem?
      Thank you for your time

Anastasia · February 20, 2020 at 7:03 am

no, you were right! for some reason it wasnt! its ok now but the other problem is still there

Anastasia · February 20, 2020 at 7:07 am

Actually it dosent show an error anymore but when i load the game it says (invalid index ‘position’ based on null instances)

extends KinematicBody2D

var player

var rng = RandomNumberGenerator.new()

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

func _ready():
player = get_tree().root.get_node(“res://player.tscn”)
rng.randomize()

func _on_Timer_timeout():
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:
bounce_countdown = bounce_countdown – 1

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)

    Davide Pesce · February 20, 2020 at 8:33 am

    Your issue is in this line:

    player = get_tree().root.get_node("res://player.tscn")

    the get_node argument must be a path to a node, so it should be something like get_node(“Root/Player”) (the exact path depends on the structure of your project). In your case the function cannot find the node and returns null, and consequently cannot access player position variable.

      Anastasia · February 20, 2020 at 8:54 am

      My enemy is a diferent scene from my player… im not sure what the path should be…
      https://ibb.co/BnvT2Zm
      here’s a screenshot!
      Also any idea how i could adapt the spawner to a sprite based workflow? If not thats ok!
      Thanks for your time

        Davide Pesce · February 20, 2020 at 11:49 am

        As far as I see, player is a child of the root node of your main scene. But I don’t see the name of your root node, so I can’t tell you the exact path. It’s something like node/player, just replace node with the name of your root node.

Anastasia · February 20, 2020 at 3:02 pm

Yeah it’s node. Thanks!

Anastasia · February 21, 2020 at 8:48 am

Hello again! It works smoothly, thanks for helping me out!
But one last thing: How can I restrict the monster from going upwards (flying up to the player)?
I’m making a platformer and the usual movement.y += gravity doesn’t seem to work!
Thank you for your time, I won’t me bugging you any longer!

    Davide Pesce · February 21, 2020 at 10:11 am

    The code in this tutorial doesn’t fit very well with a platformer. I would start by making the changes below, but surely you will have to further adapt the code to get the result you want.

    1. declares a new velocity variable and the gravity value at the beginning of the script:

    var velocity : Vector2
    var gravity = 200 #I put a random number, change it to the appropriate value
    

    2. change the _physics_process function like this:

    func _physics_process(delta):
    	velocity.x = direction.x * speed
    	velocity.y += gravity * delta
    	velocity = move_and_slide(velocity, Vector2.UP)
    

    3. remove all bounce code from the _on_Timer_timeout function, as it is written it only makes sense for top-down games:

    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()
    	else
    		# If player is too far, randomly decide whether to stand still or where to move
    		var random_number = rng.randf()
    		if random_number < 0.1:
    			direction = Vector2.RIGHT * rng.randi_range(-1, 1)
    

    Note that now, when enemies reach the end of a platform, they will fall. So, you'll have to add some code to make them jump or go back.

      Anastasia · February 21, 2020 at 10:54 am

      Thank you a lot! I think I’ll manage to make them turn around, I’ll see if I can find some other tutorials or ask on the godot forum but thank you so much again!

Anastasia · February 21, 2020 at 5:46 pm

Hullo! There arent any problems but could yo please be so kind to tell me which line of code influences the speed? increasing the speed variable doesn’t do anything

    Davide Pesce · February 21, 2020 at 6:01 pm

    I realized that there is an error in the code I sent you: in calculating velocity.x you don’t have to multiply by delta (I have already corrected my previous comment). Removed that, you should see the speed change when you change the speed variable.

Anastasia · February 21, 2020 at 5:57 pm

Nevermind! Removing *delta seemed to do the trick for me!

    Davide Pesce · February 21, 2020 at 6:02 pm

    You were faster than me 🙂

Alex · February 29, 2020 at 1:47 pm

Hello David!
Sorry for spamming you with questions, but I cannot figure this out. When I get to the spawner part, I keep getting this error in the “test_position” function:

Invalid call. Nonexistent function ‘world_to_map’ in base ‘Nil’.

My code for tes_position is as follows: func test_position(position: Vector2):
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"))
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"))
return grass_or_sand and no_trees

I downloaded the game from your hithub and the game works perfectly, but even if I just copy and paste the code for test_function from your github to my project, I keep getting the same error in my version of the code. I really have no idea why i get a Nil instance, since, everything in both my skeleton and skeletonspawner script is the same as yours, but the game just crashes each time, and I keep getting the mentioned error. Is there any way you can help me?

    Alex · February 29, 2020 at 1:51 pm

    Forgot to mention that the error occurs in line 2 of the code, at
    var cell_coord = tilemap.world_to_map(position)
    Somehow, the “position” is just recognised as Null.

      Davide Pesce · March 1, 2020 at 9:52 am

      Hi Alex, if you can upload your project to Dropbox or Google Drive and send me the link (here or privately via the contact page) I take a look at it and let you know!

        Alex · March 1, 2020 at 12:07 pm

        Hello again, Davide!
        Here’s the link: https://www.dropbox.com/sh/4i3jajkiduqczty/AAD2Rwp2mtsXCvbipQWXA5XTa?dl=0
        Thank you so much for taking the time to help me, you are an inspiration!
        P.S. I just realized I’ve been calling you by a wrong name, please forgive me. 🙂

          Davide Pesce · March 1, 2020 at 8:27 pm

          There is only one small error in your script: the underscore is missing in the name of the _ready() function.
          Don’t worry about the name, it’s a very common mistake and David is fine anyway!

        Alex · March 1, 2020 at 9:10 pm

        Hahaha, oh my god, I literally spent hours trying to figure out what’s wrong. I will surely not make this mistake again ever, and will be sure to check all instances of a variable from now on, this was a lesson beyond valuable. I can’t beleive that was it. 🙂 Thank you so so much, it all works now!

          Davide Pesce · March 1, 2020 at 9:21 pm

          It’s a mistake easy to make but hard to catch!

Anastasia · March 11, 2020 at 3:00 pm

Hello there! No problem with the code this time, awsome tutorial!
However i cannot test my project with the spawner in the main scene. No errors, no nothing! The editor stops responding and i have to close it
Thanks for your time!

    Davide Pesce · March 12, 2020 at 8:47 am

    Hi Anastasia! It’s difficult to help you without seeing your project. If you can upload it to Dropbox or Google Drive and send me the link (here or privately via the Contact page) I take a look at it and let you know!

      Anastasia · March 12, 2020 at 2:54 pm

      Hello! I… have no idea how to do that… How do I do that?
      https://drive.google.com/drive/folders/1jfPoAkqFV1FD4iDsQ01otnx6tzOf30fo?usp=sharing
      does this work? Its just the project folder

        Davide Pesce · March 13, 2020 at 5:42 pm

        Hi Anastasia, you have a problem when the spawner checks if the position on the map is valid for placing a skeleton. To do this, the test_position function uses the names of the tiles, which you have not set in the tileset.

        Looking at your project, I can only give you one piece of advice: start the tutorials from scratch and follow them step by step. Because the more you go on in the tutorials, the more problems you will have due to the many differences that I found in your project.

          Anastasia · March 15, 2020 at 11:54 am

          Hello! Thanks a lot! I Did that actually, but i might have to redo a Chuck of The work
          Again thanks a lot!

Anastasia · March 15, 2020 at 2:20 pm

Hello! Here I am yet again…
So what did I do wrong this time? no idea. Went through the code like 50 times even started from scrap. Yet my skeletons will not get out of the birth animation! Or start to move! hey just. play birth then stay.
My timer’s autostart is indeed disabled, and the timeout is conected

extends Node2D

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()

func instance_skeleton():
# Instance the skeleton scene and add it to the scene tree
var skeleton = skeleton_scene.instance()
add_child(skeleton)

# 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
skeleton.arise()

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
rng.randomize()

# Create skeletons
for i in range(start_skeletons):
instance_skeleton()
skeleton_count = start_skeletons

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

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

Thanks so much again and i'm very sorry for having to ask something every time. I loved these tutorials so so much but i'm reaaally new to godot so yeahh im not great

    Davide Pesce · March 15, 2020 at 4:09 pm

    Hi Anastasia! Better to do as last time, send me the link to the project so I can have a look at it!

    Davide Pesce · March 16, 2020 at 12:01 pm

    You forgot to connect the animation_finished() signal of AnimatedSprite to the Skelton script, so the timer is never activated.

    I found two other problems in your project:

    1) In the Skeleton.tscn scene, the root node is called Node. This name will be used in subsequent tutorials and must necessarily be Skeleton.

    2) That node is at position (122.033, 53.968). It must be at position (0,0).

      Anastasia · March 16, 2020 at 2:04 pm

      Thank you!
      I moved the skeleton shortly before asking here in order to see if it moves. I’ll put it back
      Thanks a lot again

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.

Close