00:00
00:00
3p0ch
If you like hard games try my Daxolissian System series

plasmid @3p0ch

Cat

Scientist

Read the manual & try stuff

So Cal

Joined on 2/13/10

Level:
13
Exp Points:
1,648 / 1,880
Exp Rank:
38,302
Vote Power:
5.48 votes
Audio Scouts
1
Rank:
Portal Security
Global Rank:
23,497
Blams:
50
Saves:
375
B/P Bonus:
8%
Whistle:
Normal
Medals:
4,619
Supporter:
3y 3m 10d

Godot tutorial - All the King's Men - Allies

Posted by 3p0ch - September 8th, 2022


The archer's nodes

Let's start with the archer. Make a res://archer directory and a new scene of type Node2D named Archer. Give it a child nodes of a Sprite which will be for that yellow aiming indicator. In the Sprite node's Texture property, drag in that white_pixel.png. Set Sprite-Offset-Centered to be off. In its Node2D-Transform-Scale property, for now set the Scale to (1000, 10) so we get a sense of what the aiming sprite will look like but be aware that we'll adjust this from code later on. If you look at the sprite in the viewport, you'll notice that it's slightly lower than we want – we'll want the highlighted area to go from the center of the player to the center of the target, but right now the box shows up completely below the y=0 line, so go to Sprite-Offset-Offset and set it to be (0, -0.5) to get it centered vertically. And we want it to be semi-transparent yellow so in CanvasItem-Visibility-Modulate make it yellow with an A (opacity) of 127 (which is 50%). Then save the scene as res://archer/archer.tscn.


The arrow

We'll make a separate Arrow scene for the arrow that the archer will fire, and the archer won't be able to do much until we make that Arrow scene so let's make the Arrow scene now. We'll put it in the res://archer directory so don't make a new directory for it, just make a new scene of type KinematicBody2D named Arrow and give it children of Sprite and CollisionShape2D. Use the arrow.png from my assets for the Sprite and set its Node2D-Transform-Scale to (2, 2) and its Sprite-Offset-Offset to (-5, -5), which should make the arrow's shadow (its "feet") at the arrow tip lie at the Arrow scene's (0, 0) position. Make the CollisionShape2D have a Shape of a new CircleShape2D with radius 4. Then save the scene as res://archer/arrow.tscn and add the following script

extends KinematicBody2D

var speed:float = 2000.0
var lifetime_after_flying:float = 2.0

var flying:bool = true
var collision:KinematicCollision2D

func _ready():
	pass # Replace with function body.

func _physics_process(delta):
	if flying:
		collision = move_and_collide(delta * speed * transform.x)
		if collision:
			flying = false
			if collision.collider.has_method("take_arrow"):
				collision.collider.take_arrow(self)
		if (global_position.x > 1100.0):
			queue_free()
	else:
		lifetime_after_flying -= delta
		if lifetime_after_flying < 0.0:
			queue_free()

The boolean flying will be true if the arrow is in flight and false after it strikes an enemy, since I want the arrow to hang around and stick in enemies for a little bit without moving any more. And we set up a variable called collision that's of type KinematicCollision2D. In _physics_process() if the arrow is flying then we run move_and_collide() to move the arrow a distance of delta * speed in the direction of its transform.x which you might remember from when we were doing the sword swipe. The archers will usually fire the arrow at a little bit of an angle, so the arrows will be rotated and their transform.x will tell you which direction is the arrow's "right" if you account for how it's rotated. And we have collision = move_and_collide(). That tells you that move_and_collide() is actually returning some sort of value when it's called, and if you look in the API reference under KinematicBody2D you'll see that indeed the move_and_collide() method returns something of type KinematicCollision2D which describes the collision if it collided with anything. If you click on the hyperlink for KinematicCollision2D you'll see what properties it has – which is the information about the collision that you'll be able to use. The thing we'll use is collision.collider which is a reference to the node that the arrow collided with, but in the API docs you'll see that there's other data you can get if you want such as the position of the collision. For our purposes, we're just saying that if the arrow collided with something (if the collision variable isn't null) then the arrow will set flying to false and will see if the thing it collided with (collision.collider) and a "take_arrow" method, and if so it will tell the thing it collided with to run its "take_arrow" method. As a parameter for that method, the arrow will include a reference to itself so the node that got hit will be able to do something with the arrow – for example in this game when an enemy gets hit by an arrow I want the arrow to "stick" in the enemy as it continues to walk left, and to make that work correctly I'll make the enemy add the arrow node to itself as a child since child nodes will move around with their parent nodes.


Whenever you have projectiles in your games, make sure they have a finite lifetime. You don't want them to fly off the screen but still persist forever as their coordinates become astronomical. So we'll queue_free() the arrow if it flies past the right edge of the screen, and we'll have a finite lifetime after flying that it will hang around after it gets stuck to an enemy until it disappears.


Go to the Level scene and add some arrows and watch them fly, and if they hit the player or an enemy then they should stop and after a couple seconds disappear. But they do have a bug, and one that you won't be able to notice just yet. We'll get to that in a little bit.


The archer's code

Now for the Archer scene's code

extends Node2D

export var arrow_scene:PackedScene

var aim_time_min:float = 0.5
var aim_time_max:float = 1.0
var reload_time_min:float = 1.0
var reload_time_max:float = 2.0
var x_coord_min:float = -100.0
var x_coord_max:float = -10.0
var y_coord_min:float = 100.0
var y_coord_max:float = 550.0

var aim_time:float = 0
var reload_time:float = rand_range(reload_time_min, reload_time_max)
var target:KinematicBody2D = null
var enemy_array:Array
var arrow_instance:KinematicBody2D

onready var sprite_node = get_node("Sprite")

func _physics_process(delta):
	# Reloading
	if reload_time > 0.0:
		reload_time -= delta
		sprite_node.visible = false
		if not reload_time > 0.0:
			enemy_array = get_tree().get_nodes_in_group("enemy")
			if enemy_array.size():
				global_position.x = rand_range(x_coord_min, x_coord_max)
				global_position.y = rand_range(y_coord_min, y_coord_max)
				target = enemy_array[randi() % enemy_array.size()]
				aim_time = rand_range(aim_time_min, aim_time_max)
				sprite_node.visible = true
	
	# Aiming
	if aim_time > 0.0:
		if not is_instance_valid(target):
			reload_time = rand_range(reload_time_min, reload_time_max)
			aim_time = -1.0
			sprite_node.visible = false
		else:
			sprite_node.scale.x = global_position.distance_to(target.global_position)
			sprite_node.look_at(target.global_position)
			aim_time -= delta
			if not aim_time > 0.0:
				# Firing
				arrow_instance = arrow_scene.instance()
				get_tree().current_scene.add_child(arrow_instance)
				arrow_instance.global_position = global_position
				arrow_instance.look_at(target.global_position)
				reload_time = rand_range(reload_time_min, reload_time_max)

We have an exported variable referring to the Arrow scene, so before doing anything else, click on the root node of the Archer scene where in the Inspector you'll see ScriptVariables-ArrowScene and you should drag arrow.tscn there. The first sets of variables are parameters – we'll want each archer to wait a random time while reloading (before the yellow things pops up) and a random time aiming (while the yellow thing is up before firing), and we'll want to reposition the archer with each shot. The parameters at the top of the script set the ranges for those wait times and the coordinates, and below them are variables where the randomly picked value will be stored. This might be a good point to mention something I usually do. Often your code will need to have parameters that you know full well you'll need to play around with and tweak to make the game "feel" right, like wait times or movement speeds or amount of damage inflicted by a weapon or such. When you're writing code, I strongly recommend that you not just embed numbers like that in the middle of the code like in a _physics_process() function having something like move_and_slide(450.0 * delta * transform.x), and instead make variables to hold any parameters that might need to be tweaked and put them up at the very top of your code like this where it's easy to find them and adjust them while you're playtesting. Especially if a parameter's value might end up being used at multiple different places in the script. My own convention is to first have all the variables that are parameters that I might want to tweak, then have all the rest of the variables that won't need tweaking. Pros might put them in all caps too.


Moving on with the code: it will first check to see if reload_time is positive and if so it will count it down and have the yellow thing be invisible, and when reload_time reaches zero or less it will try to pick an enemy to target. If there are any enemies in play, it will position the archer somewhere to the left off the screen and will pick a target. Let's look at that line

target = enemy_array[randi() % enemy_array.size()]

The variable enemy_array is an array of enemy nodes. With arrays, such as example_array, you refer to array elements by putting brackets afterward and having a number telling it which element in the array you want to look at, so example_array[3] would be the fourth element in example_array. Fourth?!? Yes, arrays start their numbering at zero, so the first three elements of example_array would be example_array[0], example_array[1], and example_array[2]. If the .size() of example_array were 5, then there would not be an example_array[5] element and you would get an error if you tried to use it. For more of a description about arrays see the GDScript basics at https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#array and the API reference at https://docs.godotengine.org/en/stable/classes/class_array.html


In our code, we're looking for index number randi() % enemy_array.size()

What in the world does that mean or do?

That's actually a common way of picking a random element from an array, or in this case picking a random enemy. The randi() function is a built-in function in Godot which generates a random integer between 0 and some very, very large number, and the % operator is the modulo operator. Basically B modulo C (written as B % C) is the remainder if you divide B by C, so for example 17 modulo 5 would be 2 because 17 divided by 5 is 3 with a remainder of 2. With random number generation, using randi() % some_integer will therefore end up giving you a random number from 0 to some_integer minus 1, which is what you want if you're picking a random element from an array. Just check to make sure the array's size isn't zero first or else you'll get a divide by zero error. For more on random number generation, see https://docs.godotengine.org/en/stable/tutorials/math/random_number_generation.html


While the archer is aiming, it continuously checks to make sure the target is still a valid instance. This is something you'll need to do quite commonly – if the archer picked an enemy to target but the player slayed the enemy before the archer actually fired, then the archer would be trying to point to the coordinates of an enemy that no longer exists and you'd have problems. So this uses the is_instance_valid() function to make sure the target is still a valid instance and if not it goes back to reloading. If the target is valid, then it set's the sprite node's width (which is the same as scale.x because the sprite is 1 pixel large but scaled up) to be the distance from the archer to the target, and makes the sprite look_at the target, which handles the aiming effect. Then when time's up, it fires the arrow by instancing the Arrow scene and adding it as a child, not to itself but to get_tree().current_scene (the root scene of the entire Scene Tree). Whenever you instance a node and add it to the scene tree, typically by making it a child of some other node that's already in the scene tree, you'll need to decide what the best parent node for it is. With the sword swipe, it made sense to make it a child of the player since a knight keeps holding his sword while he's swinging it and the sword should move around with the knight. With projectiles, unless you would intend to have the arrow move if the archer were to decide to move, then you probably want to make the arrow be a child of the root scene from get_tree().current.scene instead of being a child of the archer. Then we set the position of the arrow and rotate it by using look_at, and the arrow's script will take care of making it fly forward.


Collision layers and masks (again)

Remember that bug I mentioned? If you test things out with a few archers in the Level (don't worry about where you place them because they'll reposition themselves) you might notice that if the arrow tries to hit an enemy that's far from the archer along the y-axis and the arrow misses, it can actually collide with the top or bottom border of the level, which is obviously not what you want. Do you remember the Dark Arts of Collision Layers and Masks? You might remember that things collide if and only if one of the objects is active in a Layer of some number and the other object is active in a Mask of that same number. You can go to the Level's TopBorder and BottomBorder nodes and set their CollisionObject2D-Collision-Mask to be nothing (but leave Layer 1 selected) and do the same thing for the Arrow scene, and now things will work like you want. Do not attempt to make any sort of intuitive sense out of that, because it can't be done. Just play test it and confirm that it works. Except we still need to add the code to make the arrows stick to the enemies and kill the player.


Autoloads

Before we do that, I'm going to talk about Autoloads because we're about to start using one. An Autoload is a script that Godot will run as soon as the game starts and keep running without it being attached to any instanced nodes in the SceneTree. So it basically can just hang out in the background doing stuff whenever it's needed, and has the very convenient property that it persists even if you change the scene and all of the other nodes go away while the scene is changing. For example if you had a Zelda type game where you move from room to room and each room is a different scene, and the Player node is a child of the Level scene (like with this game), then every time you load a new scene you would reload the Player node (or rather, make a new instance of it for the new scene) and it wouldn't retain memory about the inventory or HP or anything from the previous Player instance. So a common use for Autoloads is to store data that should be kept across scene changes. For this game, we will eventually make an Autoload script that keeps track of which level you've reached and saves progress. But not just yet. Another common use for Autoloads is playing background music when you want the music to keep playing uninterrupted when you change scenes (which we'll also do in this game). And what we're about to do with an Autoload is to write a general-use function that any other script in the game can use.


So without further ado, make a new folder called res://scripts and right click it to make a new script called res://scripts/functions_autoload.gd

Here's the code

extends Node

func reparent(child:Node2D, new_parent:Node = get_tree().current_scene):
	var global_transform = child.global_transform
	child.get_parent().remove_child(child)
	if new_parent:
		new_parent.add_child(child)
	else:
		get_tree().current_scene.add_child(child)
	child.global_transform = global_transform

It defines a function called reparent() which can take two parameters. The first is a Node2D which should be the node that you want to assign to a new parent, and the second parameter is the node that you want to make it a child of. Notice that the second parameter has an equals after it, like if you were to have declared a variable and then assigned something to it. When you write functions, that's how you specify a default value for a parameter if you want it to be optional. So if you say reparent(node_a, node_b) then node_a will become a child of node_b, and if you say reparent(node_c) then node_c will become a child of the current root node for the SceneTree. Now about the reparenting process: the reason I made an Autoload function to do this is because sometimes I do it a lot and the process is a little cumbersome. A node can't have two parents at once, so you need to first unparent it from its current parent node and then reparent it to the new node. A problem is that after the original parent node removes the child node, the child node is no longer in the SceneTree and will forget its current position and orientation (the stuff in global_transform). So we first save the child's global_transform to a variable, then make its parent remove it as a child, then make the new parent add it as a child, and then put the child node back where it originally was in space. I also threw in code to make sure that new_parent isn't null and if so then just reparent to get_tree().current_scene because that made sense to do in a game I wrote a while ago. But the point is that if you find that you need to do something like this a lot in any of your games, and it would make sense to write it as a simple command like reparent() instead of writing out all that code every time you do it, then think about making and Autoload script and setting up a function to do it. Ironically I didn't use reparent() much in this game, but it's still a good teaching point.


Save the script, and to make it get Autoloaded, go to Project (from the top menu bar) – Project Settings and click the Autoload tab. Pick the file for the script you just made and click the Add button, and now you should be able to type FunctionsAutoload in any of your scripts to refer to it, and use FunctionsAutoload.reparent() in your other scripts.


Now go to the Enemy script and make it work with the arrow. Since we set up the arrow script to make stuff that it hits run a take_arrow() method if it has it, we just need to add a take_arrow() function to the end of the Enemy script, and here's the code for that function.

func take_arrow(arrow:KinematicBody2D):
	hp_bar.visible = true
	hp_bar_cooldown = hp_bar_cooldown_max
	FunctionsAutoload.reparent(arrow, self)
	arrow.collision_layer = 0
	if arrow.global_position.y < global_position.y:
		arrow.show_behind_parent = true

A reference to the arrow is passed as a parameter since we set up the Arrow script that way. I'm going to make the enemy's HP bar show up even though it's not losing any health because I think that'll be funny. Then we reparent the arrow to the enemy. If you haven't seen that keyword "self" used before, that's a way for a script to refer to "whatever object I've been loaded by and am running from" which in this case is the enemy node that we attached the script to. We set the arrow's CollisionObject2D-Collision-Layer (which in you can refer to with just "collision_layer" in code – as a tip, if you mouse hover over the name of a property in the Inspector, then it usually shows a name you can use to adjust the property from code) to be 0 so the arrow won't collide with anything any more. Lastly, if the arrow's y coordinate is less than the enemy's y coordinate then the arrow hit the enemy from above, so it should be drawn "behind" the enemy, which can be adjusted with the show_behind_parent property (which can be found in the Inspector under CanvasItem-Visibility).


Making the player die from arrows

For the player, first get the sprite I made called knight_arrowed.png and put it in res://player/sprites. Open the Player scene, click on the AnimatedSprite node, and edit its SpriteFrames so you can add a new animation named arrowed which will just be that picture of the knight dead with an arrow in his back.


Then for the code, since the player's death should make the level end in Defeat, so we'll want this to make the Level scene run that lose() function that we wrote earlier. We'll do that the same way we did with the enemy – by using a signal. At the top of the player script, just under the top extends KinematicBody2D line, add

signal died

and at the end of the script add the new function for take_arrow()

func take_arrow(arrow):
	arrow.queue_free()
	sprite_node.animation = "arrowed"
	collision_layer = 0
	collision_mask = 0
	emit_signal("died")

Then we'll need to make the Level scene connect that signal to its lose() function like so in the level script

func _ready():
	for enemy in get_tree().get_nodes_in_group("enemy"):
		enemy.connect("reached_end", self, "lose")
	get_node("Player").connect("died", self, "lose")

And then you're ready to play test. But if you manage to get the player hit with an arrow, you'll see that he immediately stands back up again and can still move around instead of staying dead, even though the level does end like you want it to. Its _physics_process() function is continuing to run, so it's still doing its usual routine. This raises a common question in game programming of how you want to handle a character's death. Sometimes you'll want to completely get rid of the character's node and replace it with something else like just the sprite of a blood splat. Other times you'll want to leave the character's node there but make it do something different when it's dying or dead (usually doing not very much). We'll take the first of those approaches with Enemy deaths later on and replace them with blood splats, but for the player we'll take the other approach and keep the player node in the game while changing its behavior. First we'll add a boolean called is_alive which will do what you would expect. Add this to the player's variable declarations

var is_alive:bool = true

Then at the end of the take_arrow() function add this line

	is_alive = false

And right after the line with func _physics_process(delta): add code so it looks like this

func _physics_process(delta):
	if not is_alive:
		return
	
	# Cooldowns

You might have encountered the "return" keyword before. Basically it tells the function to immediately stop and maybe return a value if you specify a value for it to return, and it doesn't need to keep running any of the rest of the code in the function. Since _physics_process() is just a function after all, this does what you want. Playtest it and it should work well.


The mage

Now let's make the mage. Make a res://mage folder and make a new scene that will just be a Node2D named Mage, and this will be the simplest hierarchy ever because that will be the only node in the scene and you don't need to adjust any of its parameters, just save it as res://mage/mage.tscn. But like with the archer, you'll need to make a separate scene for its projectile. Make a new scene with a root node of Area2D named Fireball and give it a child Sprite node and a child CollisionShape2D node. We're making the root node of this scene be an Area2D instead of KinematicBody2D because we don't want to make it collide with things, we just want it to fly and burn anything that it overlaps. Add the fireball.png sprite I made to the res://mage folder and put it on the Sprite. It's not entirely clear to me where a fireball's "feet" should be, but I went to its Node2D-Transform and set the Scale to (2, 2) and the Position to (0, -4) so do that. Give the CollisionShape2D a new CapsuleShape2D and edit it to have radius 8 and height 16, then set the CollisionShape2D node's Node2D-Transform-RotationDegrees to 90. Save the scene as res://mage/fireball.tscn. Then add this script

extends Area2D

var speed:float = 600.0

func _ready():
	pass # Replace with function body.

func _physics_process(delta):
	for body in get_overlapping_bodies():
		if body.has_method("take_fireball"):
			body.take_fireball()
	translate(delta * speed * global_transform.x)
	if global_position.x > 1100.0:
		queue_free()

That should be fairly straightforward to understand after you've seen the code for both the swipe and the arrow since it's sort of a combination of the two.


Now for the Mage scene, add this code

extends Node2D

export var fireball_scene:PackedScene

var reload_time_min:float = 2.0
var reload_time_max:float = 4.0
var x_coord_min:float = 0.0
var x_coord_max:float = 1000.0
var y_coord_min:float = -100.0
var y_coord_max:float = -50.0

var reload_time:float = rand_range(reload_time_min, reload_time_max)
var target:KinematicBody2D = null
var enemy_array:Array
var fireball_instance:Area2D

func _physics_process(delta):
	reload_time -= delta
	if not reload_time > 0.0:
		reload_time = rand_range(reload_time_min, reload_time_max)
		enemy_array = get_tree().get_nodes_in_group("enemy")
		if enemy_array.size():
			target = enemy_array[randi() % enemy_array.size()]
			if is_instance_valid(target):
				fireball_instance = fireball_scene.instance()
				get_tree().current_scene.add_child(fireball_instance)
				fireball_instance.global_position.x = rand_range(x_coord_min, x_coord_max)
				fireball_instance.global_position.y = rand_range(y_coord_min, y_coord_max)
				fireball_instance.look_at(target.global_position)

This also has an exported variable of fireball_scene, so after you save the script you want to go to the root node of the Mage scene and look in the Inspector and drag the fireball.tscn scene there. This is pretty similar to the archer except that you don't really care what the Mage node's position is because you'll just spawn the fireball somewhere in the range from x_coord_min to x_coord_max and y_coord_min to y_coord_max anyway. Since the Fireball script is like the Arrow script and tells anything it hits to run the "take_fireball" function if it has it, we'll need to add "take_fireball" functions to the Enemy and Player scripts. Here's the code for the enemy:

func take_fireball():
	hp_bar.visible = true
	hp_bar_cooldown = hp_bar_cooldown_max

Which is not much. For the player, we'll need to add an animation of burning ashes so drag the knight_burned.png files into res://player/sprites and edit the player's AnimatedSprite to add an animation called burned with those two sprites playing at 10 FPS (in this case 15 FPS seemed too fast, and you can have different animations in the same AnimatedSprite play at different speeds, so we'll do that). Then in code for the player we'll tweak how the take_arrow() function works as we're adding the take_fireball() function, so the code will look like this.

func take_arrow(arrow):
	if is_alive:
		arrow.queue_free()
		sprite_node.animation = "arrowed"
		die()

func take_fireball():
	if is_alive:
		sprite_node.animation = "burned"
		die()

func die():
	is_alive = false
	collision_layer = 0
	collision_mask = 0
	emit_signal("died")

First, we're making take_arrow() and take_fireball() check to make sure the player is_alive before they do anything because the return statement that we added to _physics_process() won't stop these other functions from running and we don't want the player to end up with an arrow in the back if he gets arrowed after having previously been fireballed. Also, we'll make the take_arrow() and take_fireball() functions each just do things that are specific for that particular form of death and then call the die() function, which will take care of all the things that should happen when the player dies regardless of his mode of death. Later on we're going to add that ghost when the player dies, and it'll be easier to add once to the die() function than to add it to multiple different functions that each handle different ways of dying, and you could probably imagine that for larger and more complex projects the fewer times you need to repeat code the better.


Add some Mages to the Level scene and test it out, and things should work.


The hound

And finally the hound. Make a res://hound folder and bring in the three hound.png images I made, and make a new scene with a root Area2D node (since the hound wont block arrows or do anything to enemies) named Hound and child AnimatedSprite and CollisionShape2D nodes. Make the AnimatedSprite animate the three hound images, and you'll notice that one image (hound1) had it level while in hound2 it's tilted back and hound3 is tilted forward. So when you make the animation, you can have hound1 be the first frame, then hound2 be the second, then add the hound1 image again to be the third frame, then add hound3 to the animation as the fourth frame. Using the same image multiple times in an animation like that is sometimes a good idea if you want, for example, a player to sit still for a while but occasionally blink, or in this case just re-use an image at multiple points. Set the framerate to something nice like 10 FPS and make sure to turn the Playing property on. Set the Scale to (2, 2) and Position to (0, -10). In the CollisionShape2D make a CapsuleShape2D with radius 5 and height 16, and rotate it 90 degrees. Save the res://hound/hound.tscn scene and add the following script.

extends Area2D

var speed:float = 150.0

var player_node:KinematicBody2D
onready var sprite_node = get_node("AnimatedSprite")

func _ready():
	if get_tree().get_nodes_in_group("player").size():
		player_node = get_tree().get_nodes_in_group("player")[0]

func _physics_process(delta):
	if is_instance_valid(player_node):
		if get_overlapping_bodies().has(player_node):
			player_node.take_hound(self)
		sprite_node.flip_h = player_node.global_position.x < global_position.x
		if to_local(player_node.global_position).length() > 0.1:
			translate(delta * speed * to_local(player_node.global_position).normalized())

This time we're doing things a little differently. First, go to the Player scene and put it in a group named player the same way you put the Enemy scene in a group named enemy. In case you need a reminder: click on the root node to make sure it's selected and in the panel where the Inspector usually sits, click on the Node tab and then the Groups button near the top. Type player and click the Add button. Now back to looking at the hound script: in the _ready() function we're using get_tree().get_nodes_in_group("player") which will return an Array of all nodes in the player group. First we check to see if there are any nodes in the player group (if the size of the array is zero, then the number zero is considered false so the "if" won't run the next line). In this game we can be pretty sure the player node should be there, but I often check to make sure that elements of an array actually exist before I try to access them anyway. In the next line, we assign the first (and hopefully only) member of the array to the variable called player_node.


In _physics_process() we check to see if we're overlapping the player node and if so then we tell the player to run their take_hound() method and pass a reference to the hound as a parameter. The rest of the code makes the hound run toward the player, but let's look closely at it. The to_local() method in a Node2D will convert global coordinates into local coordinates for your node. In this case, we're getting the player's global_position and putting that through the to_local() method, so that will give us a vector pointing from the hound to the player. It's exactly the same as saying (player_node.global_position – global_position) where that second global_position in the parenthesis would be the hound's own global_position. So use whichever coding style seems most intuitive to you. Since that will be a Vector2 from the hound to the player, if its length() is more than a tenth of a pixel, then move the hound toward the player. The to_local(player_node.global_position).normalized() will be a normalized vector (meaning it has length 1) that points from the hound to the player, and multiplying it by delta and speed will move the hound at the appropriate speed.


Since the hound will make the player run its "take_hound" method, we'll need to go add that to the player. First bring in the images of the player getting hounded (named knight_hounded.png) and make a new animation named hounded with them, this time going in order f1, f2, f3, f2 before cycling back to f1. This time it looks all right with the default frame rate of 5 FPS, so leave it at that and edit the code to include the take_hound() function

func take_hound(hound):
	if is_alive:
		hound.queue_free()
		sprite_node.animation = "hounded"
		die()

Test it out, and it should work. So now we have all the key elements of the game, but we need to set things up to have actual level progression, the next topic.


1

Comments

Comments ain't a thing here.