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, 150, 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!

Did you enjoy this article? Then consider buying me a coffee! Your support will help me cover site expenses and pay writers to create new blog content.


91 Comments

Avatar

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?

    Avatar

    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.

      Avatar

      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.

        Avatar

        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!

Avatar

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

Avatar

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.

    Avatar

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

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

Avatar

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.

    Avatar

    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.

      Avatar

      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?

        Avatar

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

        No, there is no need.

Avatar

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….

    Avatar

    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!

    Avatar

    Rifai · August 21, 2020 at 4:45 pm

    i have same problem, did you find the problem?

      Avatar

      Davide Pesce · August 21, 2020 at 7:43 pm

      Most of the time the issue is in Player’s child nodes: check that all have position (0,0). Many wrote to me with similar problems – very often they had accidentally moved the Sprite node instead of Player.

        Avatar

        Rifai · August 24, 2020 at 4:44 am

        Thanks David,
        I checked my player sprites’s position (0,0)
        I thought the skeleton couldn’t find the exact positon of the player. My skeleton follow the player but it keep distance from player and not get close like on your videos.
        When player try to approach the skeleton, the skeleton run away and when player stop the skeleton stop and start following again when player move

        Avatar

        Rifai · August 24, 2020 at 4:52 am

        I forgot to mention, the problem just when skeleton is spawing and not chase player. The skeleton work well when I placed manually.

          Avatar

          Davide Pesce · August 24, 2020 at 8:03 am

          If you upload the project to Dropbox or Drive and send me the link, I’ll check it out asap.

        Avatar

        Rifai · August 26, 2020 at 7:37 am

        Hi Davide,

        Recently I ve found that the Skeleton’s position coordinate in x,y use coordinate of spawn area, that is why the Skeleton keep distance from Player. I use print(player_relative_position) on func _on_Timer_timeout, but still I don’t know how to solve it.

          Avatar

          Mark · March 18, 2021 at 7:29 am

          Thank you so much! I’m at the potion dropping part of the tutorial(two tutorials ahead) and for days I could not figure out why my potions weren’t dropping. Then I noticed they were dropping but near the player start point. I went through the source on Github and compared to mine and couldn’t find anything. Was about to give up and ask Davide for help when I noticed my skeletons were acting like yours. I ran the same fix, and now my skeletons are chasing me, and dropping their potions where they die.
          Now into more!
          And thanks Davide for the wonderful tutorials.

        Avatar

        Rifai · August 26, 2020 at 7:54 am

        Hi Davide, finally I aware my mistake. I set value in SkeletonSpawner’s transform, which should set (x = 0, y =- 0), because the Player checking Spawner’s coordiante, that is why Skeleton keeping distance from Player. Lol dumb me.
        Anyway your tutorial is awesome, now I can continue the next tutorial.

          Avatar

          Davide Pesce · August 26, 2020 at 1:04 pm

          I’m glad you solved the problem yourself, debugging is a key part of gamedev!

Avatar

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!

    Avatar

    Anastasia · February 19, 2020 at 6:29 pm

    move and collide i meant. its the same

    Avatar

    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?

      Avatar

      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

Avatar

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

Avatar

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)

    Avatar

    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.

      Avatar

      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

        Avatar

        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.

Avatar

Anastasia · February 20, 2020 at 3:02 pm

Yeah it’s node. Thanks!

Avatar

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!

    Avatar

    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.

      Avatar

      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!

Avatar

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

    Avatar

    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.

Avatar

Anastasia · February 21, 2020 at 5:57 pm

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

    Avatar

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

    You were faster than me 🙂

Avatar

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?

    Avatar

    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.

      Avatar

      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!

        Avatar

        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. 🙂

          Avatar

          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!

        Avatar

        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!

          Avatar

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

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

    Avatar

    Numair · June 17, 2020 at 8:40 am

    Hi David,
    I am receiving the same error but my _ready() function is just fine. Also I checked my skeleton and SkeletonSpawner scripts with yours on github and there’s nothing wrong in those also. Everything was working fine until I created SkeletonSpawner script.

    Error: Attempt to call function ‘world_to_map’ in base ‘null instance’ on a null instance.

      Avatar

      Davide Pesce · June 17, 2020 at 7:17 pm

      Hi Numari, if you get this error, it means that the variables tilemap or tree_tilemap are null. This can only happen if you have called the tilemaps differently from the tutorial, if they are in a different position in the node structure, or if you gave a different name to the root node.

        Avatar

        Numair · June 19, 2020 at 10:34 am

        Thanks David
        The problem was indeed the Tree Tilemap. The name on the tilemap was ‘Tree Tilemap’ instead of ‘Tree TileMap’. It solved the error however I’m stuck at godot screen whenever I launch. The game works fine if I remove the SkeletonSpawner from the main scene.
        Please help.

          Avatar

          Davide Pesce · June 22, 2020 at 2:47 pm

          If you upload your project to Drive or Dropbox and send me the link, I will take a look at it as soon as possible.

          Avatar

          Numair · June 23, 2020 at 7:06 am

          This is the link: https://drive.google.com/drive/folders/1ybqyYJbknheR7fYmrvT9P5gW__CBQ_ii?usp=sharing

          Please help as soon as possible.

          Avatar

          Davide Pesce · June 30, 2020 at 1:18 pm

          Hi Numair, sorry if it took me a while but I don’t have much free time these days.
          Your problem is in the tileset, you have not given the correct names to the various tiles. So,the test_position() function in SkeletonSpawner.gd never returns true and execution hangs in the instance_skeleton() while loop. Go back to tutorial 7 and carefully follow all the steps, including naming the tiles.

Avatar

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!

    Avatar

    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!

      Avatar

      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

        Avatar

        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.

          Avatar

          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!

Avatar

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

    Avatar

    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!

    Avatar

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

      Avatar

      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

Avatar

Gatheesha · April 18, 2020 at 7:27 am

hey man! Awesome tutorial
I only did the ‘skeleton Ai’ part. When I load up the game I get this error ‘Invalid get index ‘position’ (on base: ‘Nil’).’
like in anastasia’s case I checked my node path and its fine

heres the enemy code>

“`extends KinematicBody2D

# 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

func _ready():
var player = get_tree().root.get_node(“Level/YSort/Player”)
rng.randomize()
print_debug(“haha player is good”)

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() <= 160:
# If player is near, don't move but turn toward it
direction = Vector2.ZERO
print_debug("haha found ya")
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)“`

and a screenshot https://cdn.discordapp.com/attachments/487973837494026241/700970444207030313/unknown.png

    Avatar

    Davide Pesce · April 18, 2020 at 9:22 am

    Hi Gatheesha,
    if you try to put at the bottom of the _ready function this statement: print(player.name) – does the node name appear in the output panel or do you get an error?

      Avatar

      Gatheesha · April 18, 2020 at 9:35 am

      no, I tested it earlier and thats why I said the node path was fine

        Avatar

        Davide Pesce · April 18, 2020 at 9:45 am

        Okay, I’ve seen where the problem is now! In the _ready function you put var in front of player, so you are declaring a new local variable with the same name! Remove var and you’re good to go

          Avatar

          Gatheesha · April 18, 2020 at 12:18 pm

          Super thanks Davide! It works perfectly anyway I had to use ‘if’ instead of ‘elif’ in here >
          elif player_relative_position.length() <= 100 and bounce_countdown == 0:
          # If player is within range, move toward it

          thanks you again for your time

Avatar

David · April 26, 2020 at 10:00 pm

Hi! I am experiencing difficulties with the spawner currently 🙁 I got my skeletons to work fine, but for some reason the spawner isnt working properly and spawning in the skeletons. Going through other replies, I assume you need to see my project hands on: so here is the link. https://drive.google.com/open?id=1EzBwq2FxP5yd3PF-YObBj_raF19vs2uC

    Avatar

    Davide Pesce · April 27, 2020 at 11:34 am

    Hi David, I must see the entire project, not only the project.godot file!

      Avatar

      David · April 27, 2020 at 5:21 pm

      Lol! my bad, here, this should be correct https://drive.google.com/open?id=1Kbeh-t2hUGBQAWrBboZ4M_ezD9OBlN4U

        Avatar

        Davide Pesce · April 28, 2020 at 10:30 am

        Hi David,

        unlike how I did it in the tutorial, you created two different tilesets for the two tilemaps. As a result, you also had to change the code that use the reference to the right tileset. So this line:

        var no_trees = (cell_type_id != tilemap.tile_set.find_tile_by_name("Tree"))

        must become:

        var no_trees = (cell_type_id != tree_tilemap.tile_set.find_tile_by_name("Tree"))

        Anyway, there are some problems in your tilesets, so I recommend you to remake them following the tilemaps tutorial from scratch.

        Also, you should reset Skeleton, Skeleton’s AnimatedSprite and SkeletonSpawner Z-Index to 0, otherwise the skeletons are drawn above the trees.

          Avatar

          David · April 28, 2020 at 7:02 pm

          Hi! Thanks for the help–I redid the tileset tutorial and now the game will actually open up, but unfortunately the spawner still refuses to actually spawn in skeletons. I think it might have something to do with the line “for i in range(start_skeletons), as the debugger says that the variable “i” is never declared. Here’s the new link: https://drive.google.com/drive/folders/1Ye5PHyuNPKanUaGYdg-4zhi1zhyXiu2e?usp=sharing

          Avatar

          Davide Pesce · April 28, 2020 at 10:43 pm

          I think it might have something to do with the line “for i in range(start_skeletons), as the debugger says that the variable “i” is never declared.

          Actually I guess it says it’s declared but not used. You can safely ignore it.

          There are two problems:

          1) Skeletons are actually created, but you don’t see them! This is because in the order of the nodes, SkeletonSpawner must be after the tilemaps (the nodes are drawn in order, unless you modify the Z-Index, but I advise you to leave the Z-Index of the skeletons at 0)

          2) You have to set SkeletonSpawner’s Transform → Position to (0,0), otherwise the skeletons are created in a different position from where you would expect.

Avatar

David · April 29, 2020 at 3:49 am

AHHH!!! Thank you so much for the help! Can’t wait to complete the rest of this incredible series!

Avatar

Anon · July 7, 2020 at 3:40 pm

Hi,

Your tutorial has been a lot of help to me in making AI for my game. I’ve just been having a couple of issues with getting the correct sprite animations playing in the correct directions. Currently, only the left and right animations play (the right seems to play when walking in any direction except left). Also, the idle animations don’t play as they should when the monster is close to the player.

I’ve made some changes here and there, and added things but here is my script:

extends KinematicBody2D

export (int) var speed = 10

# Node references
var player

var timer

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

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

var movement
var velocity = Vector2.ZERO

const MAX_SPEED = 85

var current_collide

var state = “idle”

var char_direction = Vector2()
var facing_direction = “”

var is_diagonal = false

#Sighted the player
var sighted

var anim_player = null
var currentAnim = “”
var newAnim = “”
var anim = “”

var playing = false

var state_machine

var dir
var player_pos

func _ready():
timer = get_node(“Timer”)
player = get_tree().get_current_scene().get_node(“Objects/Player”)
rng.randomize()

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 sighted == 1:
# If player is near, don't move but turn toward it
direction = Vector2.ZERO
last_direction = player_relative_position.normalized()
sighted == 1
elif player_relative_position.length() <= 500 and bounce_countdown == 0:
if sighted == 1:
# If player is within range, move toward it
direction = player_relative_position.normalized()
sighted = 1
elif bounce_countdown == 0:
sighted == 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 _process(delta):

var norm_direction = direction.normalized()

if direction == Vector2.ZERO:

if norm_direction.x >= -0.707:
$AnimatedSprite.play(“right_idle”)
$Reflection.play(“right_idle_ref”)
facing_direction = “right”

elif norm_direction.x = 0.707:
$AnimatedSprite.play(“down_idle”)
$Reflection.play(“down_idle_ref”)
facing_direction = “down”

elif norm_direction.y = -0.707:
$AnimatedSprite.play(“right_walk”)
$Reflection.play(“right_walk_ref”)
facing_direction = “right”

elif norm_direction.x <= -0.707:
$AnimatedSprite.play("left_walk")
$Reflection.play("left_walk_ref")
facing_direction = "left"

elif norm_direction.y = 0.707:
$AnimatedSprite.play(“down_walk”)
$Reflection.play(“down_walk_ref”)
facing_direction = “down”
pass

func _physics_process(delta):

movement = direction * MAX_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)

move_and_collide(velocity * delta)

velocity = move_and_slide(velocity)

if char_direction.abs().x > 0 and char_direction.abs().y > 0:
is_diagonal = true
else:
is_diagonal = false

if is_diagonal == true:
global_position = global_position.round()

func _on_SightAreaDown_body_entered(body):
if $SightAreaDown.visible:
if body.name == “Player”:
sighted = 1
pass

func _on_SightAreaDown_body_exited(body):
if $SightAreaDown.visible:
if body.name == “Player”:
sighted = 0
pass

Thanks again for the tutorial and I hope you can figure out what the issues could be 🙂

    Avatar

    Davide Pesce · July 10, 2020 at 8:09 am

    If you can upload your project to Drive or Dropbox and send me the link, I take a look at it. Sometimes the problem is not in the code but in other parts.

    P.S. the comments before being published go through a moderation queue, otherwise there would be thousands of spam messages below each article. It takes me some time to scroll it! I deleted your duplicate messages.

      Avatar

      Anon · July 11, 2020 at 1:48 pm

      Hi Davide

      Thank you so much for the help. I’m a bit embarrassed that the problems were so simple. Everything is working as it should now, thank you 🙂

Avatar

Christian C · September 5, 2020 at 5:00 am

I absolutely love your tutorial Davide. I did a bunch of other Godot tutorials before yours, but this is the first one where I feel like I’m actually learning new things.

I do have an issue I haven’t seen anyone else bring up, and it’s definitely not the code as I compare/contrasted with yours on github and made sure they were identical.

The skeleton sprites disappear when they touch the player, but only when the player’s y position is less than the skeleton’s (so when the player appears above them on the map). They vanish, but then when the player moves away they come back. It doesn’t do this when the player touches them from any other direction, it’s so baffling. I have no idea what could be causing this, do you have any ideas?

    Avatar

    Davide Pesce · September 5, 2020 at 1:53 pm

    I’ve never had a problem like this, have you tried playing with the z-indexes of the sprites to see if anything changes?

      Avatar

      Christian C · September 5, 2020 at 7:10 pm

      This was actually my fault – I somehow forgot to save the skeleton’s up-idle animation, so there were no sprites there when the skeleton was in that position. Your tutorial is making me understand how many different things go into simple games, and forgetting even one tiny detail can lead to so many unexpected results. Thanks again, sorry for wasting your time with a silly problem!

Avatar

Erik · September 15, 2020 at 7:16 pm

Hey Davide,

First off, thanks so much for the very detailed tutorials. Been running through them like a gauntlet these past few days and enjoying learning about Godot through them.

But, of course, I come with a problem. For some reason, my spawner only spawns Skeletons behind trees. I found this out by tweaking the spawn area down to smaller and smaller chunks and noticing when there’s only a few trees available, they all just spawn on top of each other, often push each other on to water tiles or whatever. When there’s no trees in the spawn area, the game literally will not launch. I’ll hit play and the player will “open” but it’s just on my dock, doesn’t actually open a window or anything.

What could I send your way to help figure out why it’s only spawning basically as close to trees as possible? I’ll keep trying to troubleshoot on my own, but this has been a thing for a couple hours now!

Avatar

Erik · September 15, 2020 at 7:28 pm

OK, of course right after I posted, I figured it out.

I had named the tree tiles in the TileMap nodes “Trees” and not “Tree.” back in part 7. So when

var no_trees = (cell_type_id != tilemap.tile_set.find_tile_by_name(“Tree”))

ran, it of course always came back as not a Tree tile because there was no such thing as a Tree tile.

    Avatar

    Davide Pesce · September 16, 2020 at 9:36 am

    Hi Erik, I’m glad you managed to solve it yourself!

Avatar

Balázs · September 29, 2020 at 9:12 pm

Hi Davide,

thank you for these great tutorials, it is really easy and enjoyable to learn Godot from them. However, I have a question that I can’t figure out currently and I hope you can help me.

What I can’t interpret and understand right now is when we are in the 5% chance interval and the skeletons should stand still, they usually do it for a pretty long time (it can been seen on the 2nd video, too). Theoretically, from the code it shouldn’t follow and also we have a 0.25 timer.

So what I can only think of is that when we call the $AnimatedSprite.play(animation) function it is not called asynchronously from the timer and the timer can only signal when the animation (specifically the idle animation with 1 fps speed) is finished. Am I right?

    Avatar

    Davide Pesce · October 3, 2020 at 10:08 am

    Hi Balázs, if the problem you are observing is that skeletons don’t move, I think it’s due to a problem with colliders. The skeletons somehow get “stuck” to nearby obstacles. This problem also occurs to me, but I have never had time to investigate it thoroughly. I will look at it as soon as I can.

      Avatar

      Balázs · October 3, 2020 at 6:06 pm

      Hi, thank you for your response!

      In fact, I thought of the case, when the skeletons are in the idle “state” without contacting any colliders for a pretty long time, but it was just my misunderstanding of the code (I usually play with Godot at nights after 8 hours of programming at work and I’m a bit tired then… 🙂 ). So, when a skeleton changes it’s state to idle (5% chance for that), it can remain in idle state for a long time when the player is far away (>100 and just observing the skeleton), because in that case there is only a 5% change for the skeleton to turn around and move (elif random_number < 0.1…)

Avatar

Zainab · October 17, 2020 at 11:40 am

Hello!
I have been trying to change the spawn area to the map size but i am struggling to do so.
Can you expand on this: placed in the ready funciton
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

because i get errors like:
Invalid call. Nonexistent function get_used_rect in base Nil

    Avatar

    Zainab · October 17, 2020 at 11:41 am

    Also sorry about this, but if my spawn area variable is like this:
    export var spawn_area : Rect2 = Rect2(720,1280,720,1280)

    the game wont load but when its smaller it will.. what does this mean?

    Avatar

    Davide Pesce · October 18, 2020 at 9:16 am

    If you get this error, it means that tilemap is null. You must get a reference to the tilemap before you can use it (using for example the get_node function)

Avatar

Kavya · February 28, 2021 at 12:03 pm

thanks for such an amazing tuitor

i am new to godot and i want to make a game where other players who play this game can interact with the player rather than AI can you tell how can i do this

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