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,638 / 1,880
Exp Rank:
38,468
Vote Power:
5.48 votes
Audio Scouts
1
Rank:
Portal Security
Global Rank:
23,697
Blams:
50
Saves:
371
B/P Bonus:
8%
Whistle:
Normal
Medals:
4,611
Supporter:
3y 2m 22d

3p0ch's News

Posted by 3p0ch - September 9th, 2022


Edit: This tutorial is written for Godot 3.x (where x is approximately 5) and not Godot 4. If you're making a web game, currently I would NOT recommend using Godot 4 because the docs mention that web exports won't run on macOS or iOS devices. Hopefully that will change in the future, but Godot 3 works perfectly well for web exports.


Godot doesn't seem to have great tutorials. The official Godot docs have the example Dodge the Creeps game, but it doesn't seem like it would teach a new game dev enough to be ready to make their own first game.


And I recently finished making All the King's Men for the Brackeys Game Jam, which is a tiny game in Godot that uses a lot of the fundamental concepts needed to make a game while not being overwhelming. So I wrote a tutorial on how I made it.


This is intended for people who have at least installed Godot and finished making Dodge the Creeps and are looking for a good next step, and might also be useful for people who've already made some games in Godot but weren't happy with how things turned out or who just want to see if there are better approaches.


I'm hoping that if you're sharp and go into it with an idea for a game you'd like to make, then this will show you ways of doing things that you can adapt for your purposes (or maybe adapt your idea to fit what can be done, but hopefully stay reasonably true to it). I'm not a professional programmer and might not be doing things the "right" way, but I made Quad so decide for yourself whether I'm qualified to write a tutorial for beginner / intermediate programming in Godot.


That's enough of an intro, if this is for you then it's time to get started with the tutorial.


Tags:

7

Posted by 3p0ch - September 8th, 2022


Implementing the NewGrounds API

You didn't think I was going to leave this out, did you? Go grab the API implementation I wrote at https://3p0ch.newgrounds.com/news/post/1209048

and follow the instructions in it. I already wrote the instructions once on that page, so I'm not going to repeat them here. If you haven't exported your game as an HTML5 and tested it out on NewGrounds yet, go to Project – Export and if you don't have an HTML5 export option available click Add at the top and get it. Export your project to a directory OTHER THAN your actual project directory – make it export to a different directory like /tutorial_export or something and make the .html file be named index.html because that's how NewGrounds rolls. After you've exported the game, go to that export directory where you should see the index.html file, select all files there and put them into a zip archive. Then you can upload that to NewGrounds in your game project page. Preview it to make sure everything works as well online as it did for you in the Godot editor. Then go to the API section and follow the instructions to put the App ID and the AES key into the ngio.gd script in Godot.


To make the leaderboards, I added some more variables to save_handler.gd to keep track of 1) whether the player is doing a full run through the game from the very beginning (I don't want to count it on the leaderboard if they go through the Main scene's Level Select and start at the last level and finish without dying) and the number of defeats so far.

var data = {
	"last_level_unlocked": 0,
}
var level:int = 0
var deaths:int = 0
var doing_full_run:bool = false

In the Main script, make the new_game() function set SaveHandler.doing_full_run to true so it will be active if they've started a new game. In the Level script, make it increment SaveHandler.deaths by 1 whenever it runs the lose() function. And in the Story script, add the following code

func _ready():
	label_node.text = level_text[SaveHandler.level]
	if SaveHandler.level < button_text.size():
		button_node.text = button_text[SaveHandler.level]
		button_node.grab_focus()
	else:
		button_node.visible = false
		Ngio.request("ScoreBoard.postScore", {"id": ###, "value": SaveHandler.deaths})
	button_node.connect("pressed", self, "start_level")

You'll need to set up your scoreboard on NewGrounds in the API section to get the scoreboard's ID, then add it to the code above.


If you'd like to add medals, first set up the medals on the NewGrounds page, then add the Ngio.request calls in your script following the instructions in the ngio.gd script's comments at the top, which will be another exercise for the reader.


Cloudsaving is a little more complicated. The full explanation for how I set up the ngio.gd script to handle cloud saves is at https://3p0ch.newgrounds.com/news/post/1294903 so I'll just show you the code here but you should read about it from that news post.


In main.gd, add this line in the _ready() function

	Ngio.cloud_load(funcref(self, "_on_cloud_loaded"))

And then add this function to the end

func _on_cloud_loaded(loaded_data):
	if not loaded_data == null:
		SaveHandler.data = loaded_data
		get_node("NewContinue/HBoxContainer/Continue").disabled = (SaveHandler.data["last_level_unlocked"] == 0)

That will take care of loading from the cloud. To save to the cloud, just modify the write_save() function in save_handler.gd to

func write_save():
	if OS.has_feature('JavaScript'):
		JavaScript.eval("window.localStorage.setItem(\"" + key + "\", \'" + to_json(data) + "\');")
		Ngio.cloud_save(data)
	else:
		var file = File.new()
		file.open("user://" + key + ".save", File.WRITE)
		file.store_line(to_json(data))
		file.close()


Making your own inheritable classes for stuff like Enemies and Levels

I mentioned in the tutorial that this is an approach that can be pretty useful, so now I'll talk about it in more detail.


The enemies in All the King's Men act pretty much identically. But what if we wanted to have more enemies and give them significantly different characteristics, like if some enemies were able to attack the player, and other enemies had different movement patterns? We would probably need to have completely different _physics_process() functions for each type of enemy, and maybe need to make some helper functions that are specific to one type of enemy. If you were to make a game with 10 different enemy types, then putting everything into one script would get out of hand. On the other hand, if you were to make 10 completely different scripts for each enemy type, a lot of the fundamental code controlling things like making the enemies spawn or making their HP bar deplete or making them die and splatter blood when the HP reaches zero (and maybe even more stuff for more complex games) would be common for all enemies, and it's better not to copy/paste code like that because if you want to change some aspect of it like make the enemies also spawn a ghost when they die then you would have to modify the code in every single enemy's script instead of just having it in one spot where it's easier to maintain.


So to do that, I'll commonly make a base class that will have all the code that will be common for all of the enemies, but isn't a complete set of code and which I don’t intend to directly attach to any enemy scenes. I would make the second line of the script be a class_name command so the script would start with

extends KinematicBody2D
class_name Enemy

Then it would be followed by all the variables, functions (including _ready()), and such that you would want every enemy in the game to have. When you save that script, Godot will register the class name of Enemy, and it will now recognize an Enemy to be a node type just like it recognizes KinematicBody2D or Sprite or AudioStreamPlayer. When you want to make an enemy node that inherits from it, instead of making its script start with extends KinematicBody2D, you would make it start with

extends Enemy

Then it will use any variables and functions that you set up in the base enemy script. Note that you CAN write stuff in a _ready() script in your base enemy.gd script, and write another _ready() function in the inheriting flying_gargoyle.gd script, and it will execute both of them. If you ever need to manually call the "parent's" version of a script when the inheriting script has a function of the same name, though, you can do it by putting a . at the beginning of the function name. You can also check the Godot docs on registering scripts as classes and inheritance.


Avoiding glitchy pauses when you load levels

For this small of a game it's not an issue, but if you start making large and complex levels then you might start noticing that they take a while to load and the game freezes and maybe has the audio glitch out while it's loading. The Godot docs talk about making custom level loaders, but I'll let you in on a secret: that's not the best way of doing it. At least not for web games, maybe console exports are different. But what you're going to want to do is:


1) Make an Autoload that holds all of your large scenes in variables. For example I did this in the actual game and I had a res://scripts/scenes_autoload.gd script with

extends Node

var story = preload("res://story/story.tscn")
var level = preload("res://level/level.tscn")

2) Replace all of your get_tree().change_scene() commands with get_tree().change_scene_to() (include that _to at the end) and instead of using a string with a path pointing to the scene make it load an Autoload variable like so

get_tree().change_scene_to(ScenesAutoload.story)

You might also be able to do get_tree().change_scene_to(preload("scene path")), but in my experience that can sometimes lead to Godot complaining about circular references because of scenes preloading each other, whereas using the Autoload approach has worked well for me.


It seems like the reason this works is the get_tree().change_scene() function needs to read data from a file and get it imported and translated into a PackedScene whenever it's called, whereas if you set up an Autoload like this then the Autoload will load all of the scenes from the file and put them in memory as PackedScenes when the game is initially loading. Player's won't really mind or even notice if the long loading process when the game first loads is a second longer, but they will mind a lot more if it takes a second to complete scene changes while they're playing and things seem to glitch out then.


Routing music through howler.js

Godot has gotten better about playing music in web builds, but I still find it a bit glitchy on Chromium browsers (Chrome and Edge) and I routinely route all my music through howler.js instead of playing it in Godot itself. Sound effects are usually ok to play through Godot without routing through howler, and you'll want to play them through Godot if you're using AudioStreamPlayer2D or AudioStreamPlayer3D nodes that give stereo effects depending on where a sound emitting node is within the game world. I wrote a description of how to route music through howler.js here if you notice that audio playback is enough of an issue to warrant it.

https://3p0ch.newgrounds.com/news/post/1148893


1

Posted by 3p0ch - September 8th, 2022


How I handle sound (not with nodes)

While for the most part I like Godot, the one thing that's still an issue is the quality of music playback in HTML5 exports. I found a way to improve music playback but it's complicated and you might not be comfortable doing it as a beginner, so I'll show you the normal way first and then show you how I do it.


Usually you'll probably want your background music to keep playing continuously when the scene changes, but if you play your music through an AudioStreamPlayer node that's in a scene then the music player will be wiped out whenever the scene changes and the new scene will start playing music from the beginning. That's sometimes also true with sound effects: for example if your Main Menu plays a nice little sound when the player clicks buttons, then the Start New Game button will run into issues because the scene will change right afterward and the sound effect will be cut short while you would probably rather have it persist and finish playing even if that makes it stretch across the scene change. So we're going to use the same trick we used to keep data when the scene changes: we're going to make an Autoload that plays sound.


Making a SoundPlayer autoload

Here's code that you can put in a new script called res://scripts/sound_player.gd and then add to the list of Autoloads in Project – Project Settings – Autoload tab.

extends Node

var new_asp:AudioStreamPlayer

func _ready():
	pause_mode = Node.PAUSE_MODE_PROCESS

func play(stream:AudioStream):
	new_asp = AudioStreamPlayer.new()
	add_child(new_asp)
	new_asp.pause_mode = Node.PAUSE_MODE_PROCESS
	new_asp.stream = stream
	new_asp.connect("finished", new_asp, "queue_free")
	new_asp.play()

It creates a variable called new_asp which will act as a reference to AudioStreamPlayer nodes that you generate from code. Before diving into it too much, you could go to your Main scene and add an AudioStreamPlayer node to its hierarchy like usual and play around with it a little bit to see how it works and what properties it has available to mess with. Drag the file for the background music, good_guys_army.ogg, into the res://main folder and add that to the AudioStreamPlayer's Stream property to test it out. It may also help you understand what this code is doing.


This is pretty much the simplest possible implementation of a player that just has one function, play(). It takes an AudioStream as a parameter. An AudioStream is just the data from a sound or music file, like the data from good_guys_army.ogg when you put it in the AudioStreamPlayer node's Stream property earlier, so when you call this from your own code you could do it like

SoundPlayer.play(preload("res://main/good_guys_army.ogg"))

The preload() function converts the file into the data format that Godot expects, in this case converting the sound file into an AudioStream.


When you call the play() function, the first line creates a new AudioStreamPlayer node and makes the new_asp variable refer to it. The next line adds it as a child to the Autoload. Next line is so music keeps playing even if the game is paused. The next line takes the variable named stream (the variable holding the AudioStream data that we'll use as a parameter when we call the function) and puts it in the new AudioStreamPlayer's Stream property, just like dragging the .ogg file into the AudioStreamPlayer node's Stream property earlier. The next line connects the new AudioStreamPlayer's signal called "finished", which is triggered when the audio is done playing (unless it's set to loop in which case it's never triggered), and connects that to the queue_free() function in the new_asp node. That's how you can force the child AudioStreamPlayer to queue_free() itself when it finishes playing instead of becoming a memory leak. And the last line of course makes the node start playing.


Now if you want to make the background music play continuously throughout the game like I did, you can get rid of that AudioStreamPlayer node you added to the Main scene to test stuff out, and do one of two things.

Option 1: At the end of the main scene's _ready() function add the line

SoundPlayer.play(preload("res://main/good_guys_army.ogg"))

Option 2: Add this variable declaration at the beginning of the Main script

export var bgm:AudioStream

And add this line at the end of the _ready() function

SoundPlayer.play(bgm)

Where the advantage of option 2 is that if you have an exported variable with the AudioStream and you move the file around or rename it within Godot then it will keep track of things, so I prefer option 2 myself. Just remember to drag the sound file into the exported variable for the Main scene.


That's fine if you only want to play one background music track continuously throughout the game, but if you want to change background music then that script won't cut it. The most practical way to do it is probably to take a slightly different approach. You can make an Autoload itself be an AudioStreamPlayer – just start the script with

extends AudioStreamPlayer

And it will work. So you could make a new Autoload script named music_player.gd (this Autoload would be specifically for music, you could use the Autoload we made earlier for sound effects and have both Autoloads running in your game) with code like this, which is a slight adaptation of what I did in Daxolissian System: Quad. We're not going to be doing it in this game, but I'll show you how it works.

extends AudioStreamPlayer

var playing_intro:bool = false
var playing_levels:bool = false
var intro_track:AudioStream
var track_list:Array = []

func _ready():
	pause_mode = Node.PAUSE_MODE_PROCESS
	intro_track = preload("res://music/rip.ogg")
	track_list = [
		preload("res://music/electric_shock_therapy.ogg"),
		preload("res://music/hydrozoa.ogg"),
		preload("res://music/metamorphosis.ogg"),
		preload("res://music/paranormal_pumpkins.ogg"),
		preload("res://music/science_fiction.ogg"),
		preload("res://music/tempest.ogg"),
		preload("res://music/vanish.ogg"),
	]

func play_intro():
	if playing_levels:
		stop()
	playing_intro = true
	playing_levels = false

func play_levels():
	if playing_intro:
		stop()
	playing_intro = false
	playing_levels = true

func stop_playing():
	playing_intro = false
	playing_levels = false
	stop()

func _physics_process(delta):
	if playing_intro and not playing:
		stream = intro_track
		play()
	if playing_levels and not playing:
		stream = track_list[randi() % track_list.size()]
		play()

It declares the variables playing_intro and playing_levels, and sets up the variable intro_track to have the song that plays in the intro (Main menu etc) and the variable track_list to have an array of songs that will randomly play during actual gameplay. I wanted to set things up so that whenever the scene changes, it will let this Autoload know whether it should be playing the intro music or some level music, but if the game is already playing the correct type of music when the scene changes (for example if you finish a level and move onto the next level when it's in the middle of a song) then I wanted Godot to just keep playing that song uninterrupted. It should only interrupt the currently playing song if you transition from a scene that's playing the intro music to a scene that plays level music.


To make that happen: the play_intro() and play_levels() functions will be run whenever the scene changes, and the new scene will execute one or the other depending on what type of music it wants to have playing. If Godot has the wrong type of music playing then it will stop the currently playing music, but if isn't not playing the wrong type of music then we won't stop the AudioStreamPlayer Autoload from continuing to play whatever it's playing. The _physics_process() function does the work of starting new tracks. If playing_intro is true but we're not playing the intro music (either because we just switched to the Main menu, or because we reached the end of the intro track) then we start playing the intro music from the beginning. If playing_levels is true and we're not playing music (either because we just switched to a Level scene, or because we reached the end of a level track and it's time to pick a new one), then it randomly picks a song from the array of level tracks and starts playing it.


Hopefully that was understandable, but if not then don't worry about it because we won't be doing things that complicated in this game. Now on to the rest of the sound for All the King's Men.


Making the game's scenes actually play sound

The Player scene has the lion's share of sound effects, so drag the burn, footstep, nom, splat, and sword wav files into res://player and make the following changes to the script. At the very beginning we're going to add some more variables, including exported variables where you should drag the sound files, and some to keep track of the sound of footsteps and the hound munching.

extends KinematicBody2D

export var sword_audio:AudioStream
export var splat:AudioStream
export var burn:AudioStream
export var nom:AudioStream
export var footstep:AudioStream
signal died

var speed:float = 400.0
var swipe_cooldown_max:float = 0.3
var swipe_display_time:float = 0.05
var strike_knockback_force:float = 200.0
var strike_knockback_cooldown_max:float = 0.5
var hound_chomp_time_max:float = 0.4
var footstep_cooldown_max:float = 0.15

var is_alive:bool = true
var move_vector:Vector2
var swipe_cooldown:float = 0.0
var swipe_vector:Vector2
var swipe_angle:float
var strike_knockback_normalized:Vector2 = Vector2.ZERO
var strike_knockback_cooldown:float = 0.0
var hound_chomp_time:float = 0.0
var footstep_cooldown:float = 0.0

We'll add some code to handle the cooldown for the footstep sound at the beginning of _physics_process()

func _physics_process(delta):
	if swipe_cooldown > 0.0:
		swipe_cooldown -= delta
		if not swipe_cooldown > swipe_cooldown_max - swipe_display_time:
			swipe_pivot.visible = false
			sprite_node.rotation = 0.0
	
	if strike_knockback_cooldown > 0.0:
		strike_knockback_cooldown -= delta
	
	if footstep_cooldown > 0.0:
		footstep_cooldown -= delta

To play the footsteps

		# Movement
		move_vector = speed * Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
		if move_vector.length() > 0.1:
			sprite_node.animation = "run"
			if footstep_cooldown <= 0.0:
				footstep_cooldown = footstep_cooldown_max
				SoundPlayer.play(footstep)

To play the sword swipe sound

		# Swipe
		if mouse_is_clicked and swipe_cooldown <= 0.0:
			swipe_cooldown = swipe_cooldown_max
			swipe_sprite.flip_v = not swipe_sprite.flip_v
			swipe_pivot.visible = true
			SoundPlayer.play(sword_audio)

And at the very end of the _physics_process() code we're going to add an elif that can execute if the player is not still alive (because he's being munched by a hound)

	elif sprite_node.animation == "hounded":
		hound_chomp_time -= delta
		if hound_chomp_time < 0.0:
			hound_chomp_time = hound_chomp_time_max
			SoundPlayer.play(nom)

The arrow splat and the fireball burning sounds are relatively easy

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

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

And all of that code should be pretty straightforward. Most of the rest of the sounds are too, the tough part is getting an Autoload player set up in the first place and understanding how it works. So I'll leave it as an exercise for the reader to add damage.wav to play when an enemy takes damage but doesn't die, kill.wav to play when an enemy does die, arrow.wav to play when an arrow is fired, and fireball.wav to play when a fireball is fired.


Now for some finishing touches.


Show stats in a HUD

Something I haven't talked about yet, which I didn't do in All the King's Men but which you will probably want to do in your own games, is make the HUD show stats like HP or ammo or such. I obviously didn't do it because there's no HP or ammo or such. But for teaching purposes, you can open the Level scene and add a Label node as a child to the Level scene's HUD node and make it show the current level. Note that you will probably want to have the HUD's text fade out when the Overlay becomes opaque at the end of the level - since the HUD node itself isn't a YSort node, its children will be drawn in the order they're shown in the hierarchy where the top child on the list is drawn in the back and the bottom child on the list is drawn in the front, so the Label node should be moved so it's listed before the Overlay node in the hierarchy. Position and set up the Label node how you like, and in the Level scene's code add a variable declaration like

onready var level_node = get_node("HUD/Label")

and in _ready()

level_node.text = "Level: " + str(SaveHandler.level)

Of course if you were showing something like HP that would change while the scene is being played instead of something like Level Number which will be the same value for the entire lifetime of the scene, then you should put it in _physics_process() instead of _ready(). You might have seen the Godot docs talk about doing things like making the player emit a signal whenever they take damage, making the level node which is a parent of both the player node and the HUD node connect that signal from the player to a function in the HUD, and then making the HUD node have a function that updates the player's HP bar in the HUD whenever it receives that signal. Well, you can do that, or you can write one line of code like I just did, so guess which approach I prefer. Not that that makes it the right thing to do. I will also mention that if you're making a more complex game with a bunch of things going on in the HUD and/or if you're making separate scenes for multiple levels, then instead of having the HUD node just be a regular child node in the Level scene like we're doing with this game, you'll want to make a separate scene just for the HUD and add it as a child in each of the Level scenes. And you'll want to do it much earlier in the design of your game instead of adding it as an afterthought like we're doing now.


Enemy splat

Now for some more fun, what's a medieval game without a little gore? Let's add the blood splats for the enemies when they die. Open the Enemy scene and add a CPUParticles2D node (since I suspect that a regular Particles node won't work in a web export, but I might be wrong) and name it Splat. There are a lot of properties for Particle nodes like this, and things are sort of a matter of taste. I'll tell you what I did, but experiment yourself to see how things work

CPUParticles2D-Amount: 20

CPUParticles2D-Time-Lifetime: 2

CPUParticles2D-Time-OneShot: On

CPUParticles2D-Time-Explosiveness: 1

CPUParticles2D-Time-LifetimeRandomness: 0.5

CPUParticles2D-EmissionShape-Shape: Sphere (with radius 8)

CPUParticles2D-Direction: (-1, 0) (with spread 45)

CPUParticles2D-Gravity: (0, 0) (since this is a top-down and not side-view game)

CPUParticles2D-InitialVelocity-Velocity: 800

CPUParticles2D-InitialVelocity-VeloticyRandom: 1

CPUParticles2D-Damping: 5000

CPUParticles2D-Scale-ScaleAmount: 5

CPUParticles2D-Scale-ScaleAmountRandom: 0.5

CPUParticles2D-Color: red


If you turn on the Emitting property at the top, then you should see it in action in the editor's viewport. Then to actually make it play when the enemy dies, add this code to the variable declarations

onready var splat_node = get_node("Splat")

And this to die()

func die(origin:Vector2):
	splat_node.look_at(origin)
	FunctionsAutoload.reparent(splat_node)
	splat_node.emitting = true
	queue_free()

This is unlike the case when the Player dies. The Player node doesn't queue_free() itself immediately, it stays in the scene and the AnimatedSprite of the player's gruesome death keeps playing. But for the enemy we're going to actually queue_free() the enemy's node (which makes things work more easily because the Level node will be counting the number of Enemy nodes still in play and declare Victory when they're all gone). If the Splat node were still a child of the Enemy node when it queue_free()s itself, then the Splat node would also be freed and we wouldn't actually see it play. So we'll use the reparenting function we wrote earlier and reparent the Splat node to the root scene of the scene tree before the Enemy queue_free()s.


Add some scenery

And there's plenty more polish to add, like the player's ghost flying up and fading away when the player dies from any cause, and those flowers and squirrels and such in the background. But since we're nearing the end of the tutorial, those will be left as exercises for the reader to make sure you were paying attention.


The last part of the tutorial will cover the NewGrounds API and some advanced stuff.


1

Posted by 3p0ch - September 8th, 2022


Setting up an autoload for persistent data

Before we set up the Level scene to handle multiple levels, we'll make an Autoload function where we'll store the current level number since that would be a piece of data that we want to have persist when the scene changes. And we'll also want to save the player's progress, which in this case would be the last level that the player has reached. So this will be a good point to talk about saving data.


I've found the most convenient way to save data is to think about all the data that will need to be saved, and put all of that data in a variable of type Dictionary in an Autoload script. I haven't talked about the Dictionary data type yet, and there's a pretty good explanation of it in the Godot API reference at https://docs.godotengine.org/en/stable/classes/class_dictionary.html

But long story short, if you make a dictionary like

var my_dict:Dictionary

Then you can store key/value pairs which are very similar to having properties of a Node. You could say

my_dict.hp_max = 40.0
my_dict.name = "Sigfried"
my_dict.an_array = [12, 16, 27]

func pick_up_potion():
	my_hp = my_dict.hp_max

func talk_to_npc():
	speech_bubble.text = "Hello there, " + my_dict.name

However I more commonly set up initial values for a Dictionary using curly brace notation

var my_dict:Dictionary = {
	"hp_max": 40.0,
	"name": "Sigfried",
	"an_array": [12, 16, 27],
}

And instead of using a dot followed by key name to access a Dictionary element, I usually use bracket syntax.

func pick_up_heart_container():
	my_dict["hp_max"] += 5.0

Because I occasionally find it useful to have a variable with the string of the key that I want to look for, and you can do this

var key_name:String = "hp_max"
print("HP max is: ", my_dict[key_name])

But this won't work

print("HP max is: ", my_dict.key_name)


It's nice to store all the data that needs to be saved in a single Dictionary because the saving and loading process will involve serialization and deserialization, which is a fancy way of saying "converting a complex object like a Dictionary into a String and converting it back again". If you can serialize and deserialize all the data that needs to be saved or loaded in one command, then that makes things easier. And since I do this in practically every game I make, I wrote an Autoload script and use it in all my projects where the only thing I change is the structure of the Dictionary named "data" that will be set up to carry all the relevant data for that game. Save this as res://scripts/save_handler.gd.

extends Node

var key = "3p0ch_all_the_kings_men"

var data = {
	"last_level_unlocked": 0,
}
var level:int = 0

func _ready():
	pause_mode = Node.PAUSE_MODE_PROCESS

func read_save():
	if OS.has_feature('JavaScript'):
		var JSONstr = JavaScript.eval("window.localStorage.getItem('" + key + "');")
		if (JSONstr):
			return parse_json(JSONstr)
		else:
			return null
	else:
		var file = File.new()
		if not file.file_exists("user://" + key + ".save"):
			return null
		file.open("user://" + key + ".save", File.READ)
		return parse_json(file.get_line())

func write_save():
	if OS.has_feature('JavaScript'):
		JavaScript.eval("window.localStorage.setItem(\"" + key + "\", \'" + to_json(data) + "\');")
	else:
		var file = File.new()
		file.open("user://" + key + ".save", File.WRITE)
		file.store_line(to_json(data))
		file.close()

func open_site(url):
	if OS.has_feature('JavaScript'):
		JavaScript.eval("window.open(\"" + url + "\");")
	else:
		print("Could not open site " + url + " without an HTML5 build")

func switchToSite(url):
	if OS.has_feature('JavaScript'):
		JavaScript.eval("window.open(\"" + url + "\", \"_parent\");")
	else:
		print("Could not switch to site " + url + " without an HTML5 build")

The variable named data is the Dictionary of data that we want to save, which in this case is just the last level that the player reached, but in your own games could be much more complicated and include any permanent upgrades the player collected or number of battleships they amassed in their fleet. It can even include an array or even another dictionary if you want. The variable named level is outside of the data Dictionary, it's a standalone variable for the particular run that the player is playing that we'll use to keep track of what level they're on. We don't want to save it to disk because if the player plays through then game once and has their progress saved, and then decides to start a new game from the beginning, then we don't need to unlock any more levels. But we will want to keep track of where the player is in their current run by using a variable in an Autoload that won't disappear when the scene changes, so we'll keep the current level number in this Autoload but not in the data Dictionary that will be saved.


The _ready() function is worth talking about even though it's not really necessary for this game because I didn't implement a Pause feature. If you want your game to be pauseable, then you'll probably go to the top menu bar in Godot and choose Project – Project Settings – pick the Input Map tab, and make a new action called pause and assign a key like P to it. Then in one of your scripts if you have a command like this

if Input.is_action_just_pressed("pause"):
	get_tree().paused = not get_tree().paused

Then it looks like the SceneTree will toggle its property called paused whenever you press the pause button. But there's a problem – when the game is paused the nodes will stop running their scripts, so when the player tries to press P again to unpause this script won't be running and the game won't restart.


When you pause the game, you want to stop things like the player and enemies but you want to have other things that keep running, such as a script that will handle unpausing the game. Nodes have a property that you can set in the Inspector at the bottom called PauseMode, which normally is "Inherit" which means it will do whatever its parent node does, but can be set to "Process" if you want that node to keep running its scripts even when the SceneTree is paused (which is what you would want to do if you make a full pause screen pop up), or can be set to Stop if you want to explicitly tell that node to stop running its code when the SceneTree is paused. That code above to toggle the pause state should work fine if you put it in the script of a node where PauseMode is set to Process.


In this case, the save_handler.gd script is something that I want to keep running even when the game is paused, especially if a pause menu has a button like "Save Game" or if we want to save any options that are set while the player is in the pause screen. So I put that in its _ready() function and have left it there since.


The other functions of read_save(), write_save(), open_site(), and switch_to_site() pretty much do what they say, but I'll point out a few things. The read_save() function doesn't just take the saved data and put it directly into the data variable, it passes it as a return value for the function which will be null if no saved data was successfully read (so you can use that as a condition in an "if" statement and it will evaluate to true only if some data was actually read). That allows you to do things in your code like

data_from_disk = SaveHandler.read_save()
if not data_from_disk:
	continue_button.disabled = true
if player_clicked_continue_button:
	SaveHandler.data = data_from_disk
	start_the_game()
elif player_clicked_new_game_button:
	# Just start with the default data in SaveHandler.data and don't assign data_from_disk
	start_the_game()

I'll also point out that the functions use

if OS.has_feature('JavaScript'):

Which will check if the game is being run from a platform that supports JavaScript (e.g. an HTML5 export that's being played from a browser) and allows you to run different code in a web build than when you're testing things out in the Godot editor. In this case, for web builds I want to save the data to the browser's LocalStorage under a key that I set at the top to be 3p0ch_all_the_kings_men. This gets a little complicated and outside the scope of what you need to know to make Godot games so don't worry too much about it if you're not familiar with it, just be aware that you should change that key at the very top to be something unique for your game, such as your_user_name_your_game_name.


After you've changed the var key = "" line to be something unique for your game, save the script and add it to the list of Autoloads like you did earlier: go to Project – Project Settings – Autoload tab, pick save_handler.gd, and click the Add button. Now all of your other scripts should be able to read and write to

SaveHandler.level
SaveHandler.data["last_level_unlocked"]

(or alternatively SaveHandler.data.last_level_unlocked, but like I said I prefer bracket notation)

And run the methods

SaveHandler.read_save()
SaveHandler.write_save()


Making things change with different levels

Now that we have a place to keep track of the current level, we can start setting things up to make the difficulty progress and have more "allies" around the further you get into the game. Just like I mentioned with Enemies, there are a couple of ways of doing that with Levels.

1) Make a completely different scene for each level in the game. The different scenes might all have the same script attached to them if they all use the same logic and check for the same win or lose conditions and the only difference between them is the level layout.

2) Make one scene and just change the number of enemies that get spawned depending on what level we're on, which is what we'll do in this game.

3) Use class inheritance which I talked about with the Enemies earlier but still haven't shown you yet – I promise I'll get around to it though. This might be useful if you want to write some code that every level will use, similar to how our current Level code handles pausing and making the scene fade out after a win or lose condition has been met, but have custom code for each level so they can have different win or lose conditions like killing all the enemies in some levels or reaching a goal in other levels.


The level scene

We're going with approach #2, so open the Level scene and get ready for a moderate redesign because we're going to take a different approach to adding enemies and allies to the scene. If you have enemies and archers and mages and hounds added to the hierarchy, delete them all and just leave the Ground, Borders, Player, and HUD with its children. Then add the following exported variables which will hold references to the scenes for the enemies, archers, mages, and hounds that we'll instance.

extends YSort

export var enemy_scene:PackedScene
export var archer_scene:PackedScene
export var mage_scene:PackedScene
export var hound_scene:PackedScene

Save the scene so those variables appear in the Inspector when you click on the root Level node, and drag the appropriate .tscn files into those variables. I should mention that it is possible to do this

export var enemy_scene:PackedScene = preload("res://enemy/enemy.tscn")

But the problem is that if you ever decide to change the game's file structure or scene names so the scene you want is no longer at res://enemy/enemy.tscn then you'll have to go back and fix that, whereas if you make an exported variable and drag the scene to it in the Inspector then Godot will keep track of things even if you move files around, so I usually do it that way.


After you've added the scenes to the variables in the Inspector, here's the code for the level with all of the changes that we'll make up to the _ready() function (we won't mess with any of the other functions, so leave your current physics_process and lose functions as they are).

extends YSort

export var enemy_scene:PackedScene
export var archer_scene:PackedScene
export var mage_scene:PackedScene
export var hound_scene:PackedScene

var level_enemies:Dictionary = {
	0: {"enemy":  5, "archer": 0, "mage": 0, "hound": 0},
	1: {"enemy":  5, "archer": 2, "mage": 0, "hound": 0},
	2: {"enemy":  5, "archer": 6, "mage": 0, "hound": 0},
	3: {"enemy":  5, "archer": 2, "mage": 2, "hound": 0},
	4: {"enemy":  5, "archer": 2, "mage": 4, "hound": 0},
	5: {"enemy":  5, "archer": 2, "mage": 2, "hound": 1},
	6: {"enemy":  5, "archer": 4, "mage": 4, "hound": 1},
}

var level_won:bool = false
var level_lost:bool = false
var pause_until_fade:float = 2.0
var fade_duration_max:float = 1.0

var fade_duration:float = 0.0
var enemy_instance:KinematicBody2D
onready var outcome_node = get_node("HUD/Outcome")
onready var overlay_node = get_node("HUD/Overlay")

func _ready():
	for i in level_enemies[SaveHandler.level]["enemy"]:
		enemy_instance = enemy_scene.instance()
		add_child(enemy_instance)
		enemy_instance.position.x = 1050.0 + 100.0 * i
		enemy_instance.position.y = rand_range(100.0, 550.0)
	for enemy in get_tree().get_nodes_in_group("enemy"):
		enemy.connect("reached_end", self, "lose")
	for i in level_enemies[SaveHandler.level]["archer"]:
		add_child(archer_scene.instance())
	for i in level_enemies[SaveHandler.level]["mage"]:
		add_child(mage_scene.instance())
	for i in level_enemies[SaveHandler.level]["hound"]:
		add_child(hound_scene.instance())
	get_node("Player").connect("died", self, "lose")

We added a variable called level_enemies which is a Dictionary, and it's a Dictionary of Dictionaries so is an example of a relatively complex data structure that we'll walk through. A Dictionary is a set of key / value pairs, and in this case the first entry in the level_enemies dictionary has a key of 0 and the value is {"enemy": 5, "archer": 0, "mage": 0, "hound": 0}. When you see things like that enclosed in curly brackets, that tells you you're looking at a Dictionary, so the value in level_enemies[0] is that Dictionary which four elements – for enemy, archer, mage, and hound – which are the numbers of each thing that should be present in the level. And you can access those values by typing level_enemies[0]["enemy"] and that would give you the value of 5 which is the number of enemies in level 0. And level_enemies[1]["archer"] is 2 which is the number of archers in level 1.


And now the _ready() function has "for" loops that will make the appropriate number of instances of enemies, archers, mages, and hounds. If the line setting up each "for" loop seems difficult to decipher, let's look at the one for Enemies.

for i in level_enemies[SaveHandler.level]["enemy"]:

Since SaveHandler.level has an integer telling you the current level, then if the player were say on level 2 that would be the same as

for i in level_enemies[2]["enemy"]:

And that should make sense now that we discussed it in the previous paragraph. Since level_enemies[2]["enemy"] is 5, that's the same as

for i in 5:

Which will run the loop while having the variable i take value 0 for the first iteration, then 1, then 2, then 3, and then 4 (so the loop runs 5 times). In the loop for the enemies, we make an instance of the Enemy scene and add it to the SceneTree by adding it as a child of Level, and then we position it with an x coordinate of 1050 + 100 * i (which spreads out the enemies – remember that for the first enemy i=0, for the second i=1, etc.) and a random y coordinate. I didn't worry about giving the archers or mages a position like that since they reposition themselves randomly anyway, and I just left the hound with its default position of (0, 0) instead of bothering to give it a random position.


If you playtest it now it should run level 0 all right and spawn five enemies, but you'll notice that the enemies are either all slimes or all goblins and not a mix. Remember that when we were placing enemies in the Level manually we were clicking a box to set its is_goblin variable to true or false, but now that the enemies are being spawned from code we can't do that. But we can make the Enemy code randomly select whether to be a slime or a goblin when it spawns. Go to the Enemy code and add this line where you're declaring variables with parameters

var odds_of_being_a_goblin:float = 0.3

Then erase the export var is_goblin:bool line (since we don't want it to be an exported variable any more) and add this

var is_goblin:bool = false

And add this line to the _ready() function

	if randf() < odds_of_being_a_goblin:
		is_goblin = true

The randf() function is a built-in function in Godot that generates a random floating point number from 0 to 1. And now if you playtest it you should (probably) get some slimes and some goblins. While we're in the Enemy script, you might notice during playtesting that if enemies are coming in from the right edge of the screen and you're impatient about pouncing on them and slashing them, then you can knock them back off the right edge of the screen and have to wait for them to come back into play. I didn't like that, and wanted to make the enemies stay in play after they entered and not get knocked back out. It might be possible to do that by messing with those invisible borders in the Level scene, but I've had enough of dealing with the necromancy of collision layers and masks so instead I added this to the end of _physics_process() and for the tutorial it's worth making the point that just because you know one way of solving a problem doesn't mean you need to or should stick with the same approach all the time.

	if movement_vector.x > 0.0 and global_position.x > 1024.0:
		global_position.x = 1024.0


So Level number 0 works, and if you want to test out the other levels you could go to the SaveHandler script and change the value of the variable named level there, but we'll need to make the game increment the level when you win and if applicable increment the last_level_unlocked and save your progress. Fortunately that's pretty easy, just make this modification to the Level script.

func _physics_process(delta):
	if (not level_lost) and (not level_won):
		if (get_tree().get_nodes_in_group("enemy").size() < 1):
			level_won = true
			outcome_node.visible = true
			SaveHandler.level += 1
			if SaveHandler.level > SaveHandler.data["last_level_unlocked"]:
				SaveHandler.data["last_level_unlocked"] = SaveHandler.level
				SaveHandler.write_save()
	else:
		if pause_until_fade > 0.0:
			pause_until_fade -= delta
		else:
			fade_duration += delta
			if fade_duration < fade_duration_max:
				overlay_node.modulate.a = fade_duration / fade_duration_max
			else:
				get_tree().change_scene("res://story/story.tscn")


The story scene

We'll also do a similar thing to the Story scene so it shows appropriate text depending on which level the player is currently on. Here's the updated code.

extends Node2D

var level_text:Array = [
	"Sir Knight, the goblins of the East are laying seige.\n\n" + \
	"Pray thee, vanquish the vile invaders lest they reach our farmlands.\n\n" + \
	"Move with WASD / arrows\nand use thy mouse to smite them!",
	
	"Alas, more fiends are upon us. But have no fear, thou needst not fight alone. I shall dispatch the archers to aid you!",
	
	"Yet more goblins approach.\n\n" + \
	"Thou shalt have the aid of all of our archers!",
	
	"They continue to press on our borders.\n\n" + \
	"Mages, gather at once to assist!",
	
	"Their ranks still swell...\n\n" + \
	"Muster every mage in the wizard tower for our defense!",
	
	"Even against every man in the kingdom, they continue their assault.\n\n" + \
	"If men aren't enough, so be it. Release the hounds!",
	
	"The last of the enemy approaches!\n\n" + \
	"Everyone in the kingdom who still draws breath, throw all that you have at them!",
	
	"Thank you for your service, sir Knight.\n\n" + \
	"But everyone in the kingdom was fighting, so I guess you didn't really need to do much yourself now did you?\n\n" + \
	"Well, give them your thanks for helping and bugger off now.",
]

var button_text:Array = [
	" A'ight ",
	" A'ight ",
	" Um, it's really not necessary m'lord... ",
	" I hope they're better than the archers ",
	" I really can handle this on my own, sire ",
	" Oh for the love of... ",
	" Something something shadow of death ",
]

onready var label_node = get_node("Label")
onready var button_node = get_node("CenterContainer/Button")

func _ready():
	label_node.text = level_text[SaveHandler.level]
	if SaveHandler.level < button_text.size():
		button_node.text = button_text[SaveHandler.level]
		button_node.grab_focus()
	else:
		button_node.visible = false
	button_node.connect("pressed", self, "start_level")

func start_level():
	get_tree().change_scene("res://level/level.tscn")

It sets up an array named level_text that has the text for each level. It might look a little strange, but there are actually seven values in the array (for levels 0 through 6) plus another value to be shown after the player has finished all the levels, even though the block for that array takes up a lot of lines. The \ character at the end of a line tells Godot that the next line should be considered part of the same line as the previous line but you're breaking them up for clarity to make all the code fit on your screen, and the combination of + \ at the end of the line does that while the + will concatenate the two strings from that line and the next line. The commas will actually separate the different elements in the array. The button_text array is easier to read and is essentially the same thing. In _ready() it sets the label to show the level_text for the current level (the number in SaveHandler.level) which might be 7 if the player has finished the last level. So when it tries to set the text for the button, first it sees if SaveHandler.level is less than the size of the button_text array: if so then it sets the button text appropriately, and if SaveHandler.level isn't less than the size of the button_text array (remember that if the last element is index number 6 then the size of the array is 7) then it makes the button_node invisible. You might wonder: why is the "if" statement checking whether SaveHandler.level < button_text.size() instead of simply saying if SaveHandler.level < 7? The answer is that while I was programming the game I wasn't sure how many levels I was going to make, so writing it that way would make it work even if the number of levels changes. You might also have noticed the button_node.grab_focus() line; it allows the player to just press Enter or Space to quickly restart the level since the button will have focus for keyboard input.


Main menu node hierarchy and dealing with containers

Now that we're saving progress, we'll need to have a Main Menu where you can choose New Game or Continue, and if you continue then have a level selection. Make a new folder of res://main and a new scene with a root node of type Sprite and name the root node Main, and from my assets get title.png for the Sprite. We're about to work with Container nodes, and I will give you fair warning that even if you follow this tutorial and read all the Godot docs you can find on UI design, in practice they will probably mess you up good. Don't consider yourself any less of a programmer if they do. And if you just plan to make web games where you can manually control the size and position of everything and don't need to worry about having things resize properly if people play it with different size viewports and different aspect ratios, then you're free to not use them at all. But for this tutorial we'll do some simple ones.


When the game starts we'll want to show the New Game and Continue buttons, and have some text saying this is for Brackeys Game Jam. And if the player chooses Continue, we'll want to make that go away and have a level selection come up. So we'll make one child of Main that has New Game and Continue and another child of Main that has the Level Section and we'll adjust which one of them is visible based on what the player does.


First give the Main node a child VBoxContainer node and name it NewContinue. Then give NewContinue a child HBoxContainer node and it can keep its the default name. Give the HBoxContainer node two children that are each Button nodes with one named New and one named Continue, and give the NewContinue node another child that's a Label node with the following text (including an Enter so it's on two lines).

Made in a week for Brackeys Game Jam

Jam theme: "You're not alone"

Since you've set up the Story scene already, you should be able to set up the Button nodes and Label node to look how you want and to respond when the mouse is initially clicked instead of waiting for a click and release (if that's how you want it to work), and I'm mostly going to focus on how the Container nodes (VBox and HBox) behave. The HBox container with the two Button children will keep them aligned in a row with the first child in the hierarchy on the left and the second child in the hierarchy on the right. Try going to one of the buttons nodes and manually changing its Control-Rect-Position so it's no longer nicely aligned in the row and then run the scene using that icon in the top right of the arrow in the director's thingy. How do you like that? Godot will let you know who the boss is, and now you're starting to see why container nodes can make a novice pull their hair out.


With Container nodes, you wont be controlling things like you're used to any more. You can set the position and size of the outermost Container node but it will control the placement of all of its children. In this case set the NewContinue node's Control-Rect-Position to be (440, 425). Set the HBoxContainer's Alignment to be Center and its Control-ThemeOverrides-Constants-Separation to be 100. You can have a little bit of control by changing the child nodes' properties, and for the Button nodes you could try changing their SizeFlags (under Control) and seeing what happens. If your scene looks like mine in the actual game, then adjusting the Horizontal flags and having them on in different combinations will have the most noticeable effects.


Once those look like they're set up nicely, you can hide the NewContinue node (click they eye icon in the hierarchy) and give Main a new child of type HBoxContainer and name it Levels. Give the Levels node a child Label node with the text Level, and another child node of type Button named 0 with text 0. After you set the Control-ThemeOverrides-Font on the 0 node (and its Colors if desired), you can copy/paste nodes in the hierarchy, so copy the 0 node and paste it to the Levels node to give it another Button child, and keep doing that until you have Button nodes for levels 0 through 6 and adjust their Text appropriately. In this case I thought it looked nicest if I set the Levels node's Control-Rect-Position to (440, 425) and its MinSize-x to 550, Control-ThemeOverrides-Constants-Separation to 20, and turned on each of the children's Control-SizeFlags-Horizontal Fill and Expand flags. When you're done, make the Levels node be hidden and show the NewContinue node again.


Main menu code

When you're done, save the scene as res://main/main.tscn and add this code.

extends Sprite

func _ready():
	randomize()
	
	if SaveHandler.read_save():
		SaveHandler.data = SaveHandler.read_save()
	if SaveHandler.data["last_level_unlocked"] == 0:
		get_node("NewContinue/HBoxContainer/Continue").disabled = true
	
	get_node("NewContinue/HBoxContainer/New").connect("pressed", self, "new_game")
	get_node("NewContinue/HBoxContainer/Continue").connect("pressed", self, "continue_game")

func new_game():
	SaveHandler.level = 0
	get_tree().change_scene("res://story/story.tscn")

func continue_game():
	for level in 7:
		get_node("Levels/" + str(level)).disabled = (level > SaveHandler.data["last_level_unlocked"])
		get_node("Levels/" + str(level)).connect("pressed", self, "start_at_level", [level])
	get_node("NewContinue").visible = false
	get_node("Levels").visible = true

func start_at_level(level):
	SaveHandler.level = level
	get_tree().change_scene("res://story/story.tscn")

The randomize() function should be run once when the game starts because if you don't use it then when Godot generates random numbers it will always generate the same series of random numbers each time you run the game, but after it executes that randomize() function it will start making its random numbers randomly. You only need to run it once, so the _ready() function of the main scene is a good place for it.


Next it checks for saved data. I wrote the SaveHandler.read_save() function so it will return null if there's no save data present, so that first "if" statement will only execute the next line if there was data saved (meaning that SaveHandler.read_save() is not null since logic statements like "if" consider null to be false). If there's not any save data found, then SaveHandler.data will remain the default value that we set it to when we made the save_handler script. Then if the player hasn't unlocked any levels after level 0 it will make the Continue button be disabled.


It connects the Continue button's "pressed" event to continue_game, and let's look at that function closely. It runs a "for" loop that will make the variable named level take on each value from 0 to 6. Then it has get_node() statements that might look weird. Normally you would expect to see something like get_node("Levels/0") to get the button for level 0, but here we have the expression get_node("Levels/" + str(level)). The str(level) will give you the level number in String format, and the plus symbol when used with strings will concatenate them or join them together. So the first time it goes through the loop with level = 0 that will be get_node("Levels/" + str(0)) which is get_node("Levels/0"), and the second time through the loop it will be get_node("Levels/1"), etc, so this loop will end up acting on each of the level buttons. That's one slick way to do it; another way to loop through nodes would be if you have a parent_node and want to do something to each of its children in a loop, you could say

for child_node in parent_node.get_children():
	# Do something with child_node

But in this case I want to have the variable named level with the level number to make things easier for the next steps. First, the loop will go through all the level buttons and make them disabled if they're above the last_level_unlocked, and will connect each level button to the start_at_level() function. Next it will connect the buttons' "pressed" signals to start_at_level().


This is also an example of making a signal pass a value when it runs a function. In this case, we're going to write a single start_at_level() script that will start at a different level depending on which of the level buttons have been pressed, so the start_at_level() script will need to know which level to start at. Some signals will automatically pass parameters when they're connected to functions – for example when we were making the player's swipe work by connecting an on_body_entered signal to the swipe, we connected it to a function and that function took a parameter which was a reference to the body that entered. For the "pressed" signal, Godot doesn't automatically pass any parameters like that. But you can tell it to pass parameters to the function that the signal is connected to by adding a fourth parameter in the .connected() command, and that fourth parameter should be an Array. In this case we made the fourth parameter in the .connect() be [level], so the start_at_level function will receive a parameter with the number for the level that we want to start.


In the start_at_level() function itself, it sets the value of the SaveHandler script's level variable to be whichever level number the button passed to it when it called the function, and then goes to the Story scene which will read that level number from SaveHandler and show the appropriate story for that level.


Now that we have a proper Main scene, you can go to Project – Project Settings – General tab – Application – Run and make that be the Main scene that runs when the game starts. And we're getting there, this is starting to look almost like a finished game! Except there's still no sound, so let's handle that next.


1

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

Posted by 3p0ch - September 8th, 2022


The level boundary's collision layers and masks

Open the Level scene, and before we add a script to it we'll address an issue that you might have noticed – right now the enemies can't walk off the left edge of the screen because we set up a border there to keep the player from walking off the screen, and of course we need to fix that before we add code to make the level end in Defeat when an enemy walks off the left edge. So now we delve into the Dark Arts of Collision Layers and Collision Masks which no one really understands, even though the Godot docs attempt to explain it at https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-layers-and-masks yet fail as expected for something this arcane. My own pitiful attempt to explain it is: if two objects A and B overlap, the collision will be detected if 1) A's Mask includes a number where B has a Layer, OR 2) A's Layers includes a number where B has a Mask. I know full well that it makes no sense. But I do know that things end up working if you go to the Player scene and set its root node's CollisionObject2D-Collision-Layer to include both 1 and 2 and set its CollisionObject2D-Collision-Mask to include both 1 and 2, then go to the Level's LeftBorder and RightBorder and set each of them to include ONLY 2 (and NOT 1) for both the CollisionObject2D-Collision-Layer and CollisionObject2D-Collision-Mask, while the Enemy still has the default of just 1 for both the Layer and Mask. I would say something about the player colliding with the left and right borders on layer 2 while the enemies are only in layer 1 and therefore don't collide with the left and right borders which are only in layer 2, but it would be incomprehensible gibberish. Still, if you make those changes and playtest it, then the enemies will be able to walk off the screen while the player can't so things will work like you want.


Text overlays

Now let's move on to things that can be understood by human beings. We're going to want to be able to display a "Victory" or "Defeat" message when the level is won or lost, and further down the line we're going to want to have the level fade in and out on transitions instead of just popping instantaneously. An in general, when you're making more complex games, you'll usually want to have some sort of HUD that shows the player's stats like their HP and maybe ammo and maybe inventory and maybe a minimap of the level, so it's common to have a HUD node that has all of that. That's the approach I took with this game and is a good teaching point, so in your Level scene make a child node that's a Control node and name it HUD. In your own game, you might decide you want to save the HUD as its own independent scene instead of having it just be a child node within the Level node, especially if you have many levels in your game which each have their own .tscn file but all need the same HUD. In that case you would want to make a res://hud directory and save the HUD scene there, and then add it as a child in all of the Level scenes. That's how I did it in my Daxolissian System games anyway.


Since the root node of the scene is a YSort node, and since we want the HUD to appear in front of everything else in the scene, on the HUD node set its Control-Rect-Position property to be (0, 600) so it thinks it's at the very bottom of the screen and will draw itself in front of everything else. We can set its child nodes to have different y-coordinates including negative y-coordinates so they end up on-screen, but since the y-coordinate of the HUD node will be what dictates which nodes get drawn in front of which other nodes, we can use shenanigans like setting a large y-coordinate for the root node to ensure that the HUD gets drawn in front of everything else. You might be wondering why I didn't set the Z index of the HUD node so it shows up in front of everything else? Well, the Z index is a property of Node2Ds and not Control nodes (which is what the HUD is) so tough luck and just deal with this workaround.


In preparation for making the Victory and Defeat text, we need to go get an appropriate font to show them (and other text). Go download it from https://www.1001freefonts.com/prince-valiant.font where you can see that it is indeed a free font that you can use in your game, and rename it prince_valiant.ttf to make things easier. Then in Godot make a res://font directory and put it there. With Godot there's a bit more to it: right click the res://font directory and choose New Resource and make a new DynamicFont named valiant_large.tres (since this will be larger than your usual text) and edit it to have DynamicFont-Settings-Size 80, OutlineSize 5, and OutlineColor black. Then in DynamicFont-Font drag the prince_valiant.ttf.


In the Level scene, give the HUD node a child node of type Label, and make sure that the Label node is a child of the HUD node and not a child of the root Level node. Give that Label node a name of Outcome. For now, let's set it up to show "Victory" because I'm an eternal optimist – set its Text property to be the text Victory and set its Align property to be Center, then under Control set Control-Rect-Position to (0, -400) and Control-Rect-MinSize to (1024, 0). I prefer adjusting MinSize instead of Size because it tends to do what I want more reliably; if you just adjust the Size of a node that inherits from Control then sometimes it ignores it and just does what it wants. In Control-ThemeOverrides-Colors set the FontColor to be yellow (for Victory, the default) and for Control-ThemeOverrides-Fonts drag in the valiant_large.tres font that you made earlier. If that looks nice in the viewport, then in the hierarchy click the eye icon next to Level/HUD/Outcome to make it invisible (since you only want to show it after the level is won or lost).


Make an enemy group

Before writing the code for the Level scene, let's also assign the Enemy scene to a group with name "enemy". We'll want to do this because in order to tell whether Victory has been achieved, the Level scene will need to check whether there are still any enemies in play, and a reliable way to tell how many nodes of a specific type are in play is to use get_nodes_in_group() and see how many nodes are found. To add the Enemy scene to an "enemy" group, open the Enemy scene and click the root node, and in the window on the right click on the Node tab (next to the Inspector tab), then the Groups button on the right near the top. Type "enemy" (without the quotes) in the box and click the Add button, and now the root node of the Enemy scene is in the "enemy" group.


Code for winning and losing a level

Now for code: add this script to the root node of the Level scene

extends YSort

var level_won:bool = false
var level_lost:bool = false

onready var outcome_node = get_node("HUD/Outcome")

func _ready():
	pass

func _physics_process(delta):
	if (not level_lost) and (not level_won):
		if (get_tree().get_nodes_in_group("enemy").size() < 1):
			level_won = true
			outcome_node.visible = true

func lose():
	if (not level_lost) and (not level_won):
		outcome_node.text = "Defeat"
		outcome_node.set("custom_colors/font_color", Color.purple)
		level_lost = true
		outcome_node.visible = true

The booleans level_won and level_lost will be set to false initially and will be set to true once the win or lose conditions are met. Part of the reason for doing that is that I don't want to immediately queue_free() the Level once a win or lose condition is met – I want it to stick around for a couple seconds so the player can see the Victory or Defeat text and, once we add the "allies", be able to tell that they were defeated by an arrow in the back by seeing their corpse and ghost floating up. Suppose the player defeats the last of the enemies, and the level is still running while it's showing Victory, and during that time the hound comes and eats the player? I don't want it to change to a Defeat in that case, so adding the check of whether the level has already been won or lost before checking the win or lose conditions will prevent that from happening. The other thing that we'll do later is make the scene wait and fade to black a little bit after the Victory or Defeat, so in physics_process() near the beginning we can check whether the level has been won or loss and if so then run code to handle making the scene fade out. In general, I've found it useful to have booleans like that to tell if the win or lose conditions for a level have been met yet so I pretty much always do it that way.


We'll check for the win condition in _physics_process(): if the level has not already been won or lost then we'll use get_tree().get_nodes_in_group("enemy") and check its size to see how many enemies are in play, and if it's less than 1 we'll set the level to won and we'll make the node with the victory text visible. Let's look more closely at that command though. When you're starting off learning Godot and seeing commands like this being used, it might at first seem like they just sort of magically work but you're not completely sure why, so you when you see an example you'll just copy/paste it into your own game's code and try to change it as little as possible while hoping it still does what you expect.. So instead, let's take a moment to break things down and see how it works. The method get_tree() (which is run by the Level node and can be found in the API as a method for Nodes) will return the SceneTree that the node is in (the game's active SceneTree), and the SceneTree in the API reference has the method get_nodes_in_group() where if you have a name of a group inside the parenthesis it will return an Array with all the nodes in that group where each element of the array is a node in that group. If you look at the Array in the API reference, you'll find that is has a size() method which returns an integer telling you how many elements are in the array. The SceneTree might still seem like a nebulous thing, and if you'd like to get more of a description of it, then on the API reference page for SceneTree there's a section of Tutorials on the SceneTree with links you can click to see a more descriptive explanation of them.


For the lose condition: instead of just having the Level scene check for enemies walking off the left edge of the screen, we'll plan to make the Enemy scene emit a signal when it walks off the left edge of the screen, and then we'll connect that signal to the lose() function we just added. In the lose() method, if the win or lose conditions have not already been set, then we'll change the outcome node's text and color to be Defeat and make it visible, and set the level_lost boolean to true. Since we haven't added the signal to the enemy and connected it to that function yet, it won't actually be called if you playtest the game right now, but you can test the game and make sure the Victory screen appears if you kill all the enemies.


Making the enemy emit a signal for defeat

Now let's go to the Enemy code and add the signal. First, near the top of the script, insert a line declaring the signal named "reached_end"

extends KinematicBody2D

signal reached_end
export var is_goblin:bool = true

Then in _physics_process() just after the player moves make it emit the signal if it reaches an x coordinate of 0 or less

	move_and_slide(movement_vector)
	if global_position.x < 0:
		emit_signal("reached_end")

In the Level code, set up the _ready() function like this

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

That will use a "for" loop. Remember that get_tree().get_nodes_in_group("enemy") will give you an Array of all of the nodes in the group "enemy", and with Arrays you can use "for" loops to run commands with each of the different elements in the array. Between the "for" and "in" of that command you can specify a name to refer to the current element in the array that the "for" loop is looking at – basically think of it like declaring a new variable called "enemy" and for each element in the array of nodes it will run the following block of code, in this case attaching that enemy's "reached_end" signal to the "lose" function that's found in self (the Level's code).


If setting up signals seemed like a hassle, you could have used other approaches like in the Level's _physics_process() function writing a for loop to make it loop through all the enemies found with get_tree().get_nodes_in_group("enemy") and check their global_position.x to see if it's less than zero. But in this game I know that there will be multiple different things that can lead to Defeat – an enemy could reach the end, or the player could get hit by an arrow, or the player could be burned to a crisp, or the player could be mauled by a hound. So it seemed like things would be cleaner if I have a lose() function that handles all the stuff that the Level should do when the game is lost and use signals to connect the various different things that could make the level be lost. And using signals can be useful when you're programming in general, so it's worth seeing an example like this of how to set up a custom signal. Go ahead and play test the game and the Defeat should show up properly if you let an enemy through.


Setting up a "story" scene

Now things work, but in the actual game once the level is won or lost we won't want it to just sit there with "Victory" or "Defeat" showing forever. In this game I had a separate screen with the king talking to the player to give the game a little bit of a story, so let's make that Story scene and transition back and forth between it and the Level scene.


Make a directory res://story and make a new scene with a root node being a Sprite named Story. From the assets I made, drag background.png into the res://story directory, drag that to the Story node's Texture property, and turn off Sprite-Offset-Centered. Add a child Sprite node and go download an asset pack including the picture of the king from https://kiddolink.itch.io/fantasy-npc-non-playable-characters-pack. Rename the picture of the sketch drawing of the king "king.png" and drag it into res://story, then drag that into the child sprite's Texture and position it so it looks nice (I used Sprite-Offset-Centered off and Node2D-Transform-Position (0, 130)). To the root Story node, add another child node of type Label. In the Label node's Text property, type the following text for the first level (it's fine for the Text to have multiple lines)

Sir Knight, the goblins of the East are laying seige.

Pray thee, vanquish the vile invaders lest they reach our farmlands.

Move with WASD / arrows
and use thy mouse to smite them!

The default font looks horrible, and the DynamicFont resource we made earlier would be too large for this text box. So go back to the res://font folder and right click on it to make a New Resource and choose DynamicFont named valiant_medium.tres. Set it to have Size 36, OutlineSize 3, OutlineColor black, and UseFilter on, and in DynamicFont-Font-FontData drag the prince_valiant.ttf. Back in the Story scene's Label node, in Control-ThemeOverrides-Fonts drag that new font you just made and you'll get a better sense of how things look. Under the Label properties set Align to Center, Valign to Center, and Autowrap On. To position it, in Control-Rect set Position to (400, 50) and MinSize to (600, 400). (You might need to click that Undo button next to the Size property after you set MinSize to make things look right.) For the button: first add a child node to Story of type CenterContainer and set its Control-Rect to have Position (400, 500) and MinSize (600, 100). Then give that CenterContainer node a child node of type Button – usually you'll probably want to use a TextureButton instead of just a Button because that lets you make slick graphics for each of the button's states (idle, mouse hover, pressed, disabled, etc.) but a regular Button will work for a game jam or a tutorial. The CenterContainer node just serves to keep the Button node centered to align underneath the text since that's how Godot''s UI design works – the CenterContainer is a container type of node, and containers are used to keep their child nodes arranged how you want. I won't go into detail here, there's a huge discussion on UI design and Control nodes in the Godot docs at https://docs.godotengine.org/en/stable/tutorials/ui/index.html but for now just type some text in the Button node's Text property like " A'ight " (where I added a couple of spaces on each side to have some padding on the button's borders) and in Control-ThemeOverrides-Fonts drag the new font you made, then in Control-ThemeOverrides-Colors set all of the colors to yellow. When I make buttons I tend to like to set the BaseButton-ActionMode to be ButtonPress so things happen immediately when the player clicks the button instead of waiting for click and release, which would also mean you wouldn't need to worry about setting up additional font colors for the Pressed state (or making new pictures for the Pressed state if you use a TextureButton instead of a plain old Button). Save the scene as res://story/story.tscn.


I'll mention that I've been using ThemeOverrides a lot. If you're making a larger game, you'll probably want to make use of Themes where you can set up default fonts for text boxes, colors for buttons, and in general decide how you want your UI elements to look by default, so you could just tell all of your UI nodes to use that Theme and not have to set all of the individual properties in ThemeOverrides every time you add a UI node. That's also covered in the Godot docs' UI section linked above. Also, notice that I intentionally misspelled seige to serve as a teaching point: Godot didn't give you any indication of a misspelled word and doesn't have any internal spelling or grammar checking, so when you're writing a lot of text you'll probably want to type it in a word processing program and then paste it into Godot.


Transitioning between the story and level scenes

Now add some code to the Story scene to make it switch to the Level scene when you click the button. You could do this

extends Node2D

onready var button_node = get_node("CenterContainer/Button")

func _ready():
	button_node.connect("pressed", self, "start_level")

func start_level():
	get_tree().change_scene("res://level/level.tscn")

This will work, but be aware that the command of get_tree().change_scene("res://level/level.tscn") is inefficient and if you're going to load a large and complex level then the game might hang temporarily during the loading process and look janky. After we introduce Autoloads, I'll show you a different way to do it which worked reliably for me.


Now that we can transition from the Story scene to the Level scene, we'll also need to set up the Level scene to go back to the Story scene after a level is won or lost. First in the Level scene let's add a way to make the scene fade in and out. On the HUD node, add a child TextureRect and name it Overlay. From my assets, drag white_pixel.png to res://level and then drag it to the Overlay node's Texture property, turn on Expand, and set Control-Rect to have Position (0, -600) and MinSize (1024, 600). The screen should now be covered in white, so next set the property CanvasItem-Visibility-Modulate to have a color of black (which is what we'll want to fade to) and an A value (opacity) of zero, and then the scene will look like normal again.


A bug that I promise you'll encounter and need to be aware of

But I've intentionally laid a trap for you, and one which many Godot devs fall prey to. Try playtesting your game and you'll be in for a surprise because now that player's sword swipe is no longer working! So what broke? The issue is in how the mouse click is being handled. When we added the Overlay node, it covered the entire screen. Control nodes tend to be UI elements like Buttons that might consume mouse clicks, and that's exactly what's going on here. Godot is making the mouse clicks be detected by the Overlay node which isn't doing anything with them, instead of passing them on to the Player node so it can swipe the sword. If you had a complex UI with a menu on top of other things then that would be what you want – if a player clicks a button on the UI then you don't want other things underneath it to also respond to the mouse click. But in this case it's not what we want and it's breaking our game. To fix it, in the Overlay node's properties set Control-Mouse-Filter to Ignore, and now it will ignore mouse input so it will be detected by the Player and the game will work again. Believe me, this happens a lot and will mess you up if you don't realize what's going on.


Fading out the level

Now let's expand the Level's code to handle transitions and making the Overlay fade the level in and out.

extends YSort

var level_won:bool = false
var level_lost:bool = false
var pause_until_fade:float = 2.0
var fade_duration_max:float = 1.0

var fade_duration:float = 0.0
onready var outcome_node = get_node("HUD/Outcome")
onready var overlay_node = get_node("HUD/Overlay")

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

func _physics_process(delta):
	if (not level_lost) and (not level_won):
		if (get_tree().get_nodes_in_group("enemy").size() < 1):
			level_won = true
			outcome_node.visible = true
	else:
		if pause_until_fade > 0.0:
			pause_until_fade -= delta
		else:
			fade_duration += delta
			if fade_duration < fade_duration_max:
				overlay_node.modulate.a = fade_duration / fade_duration_max
			else:
				get_tree().change_scene("res://story/story.tscn")

func lose():
	if (not level_lost) and (not level_won):
		outcome_node.text = "Defeat"
		outcome_node.set("custom_colors/font_color", Color.purple)
		level_lost = true
		outcome_node.visible = true

Now we've set up the variables pause_until_fade, fade_duration_max, and fade_duration, and in the _physics_process() function we added an else: block that will run if the level has been either lost or won. In that else block: first we see if the pause_until_fade has reached zero yet and if not we subtract delta from it, and if the pause_until_fade has reached zero then we count up fade_duration. If it hasn't reached fade_duration_max, the we set the Overlay node's Modulate property's opacity to be fade_duration / fade_duration max (making the overlay gradually become more opaque as fade_duration gets closer to fade_duration_max), and when fade_duration reaches fade_duration max we switch to the Story scene. As an aside, if you wanted to make it so the level would just restart instead of going back to the Story scene when the player is defeated, you could do that with get_tree().reload_current_scene() and I'll leave that as an exercise for the reader. Go ahead and test it out, and this is starting to feel a little more like a game. Next we'll add our "allies".


1

Posted by 3p0ch - September 8th, 2022


This game has two types of enemies – slimes and goblilns – that are overall pretty similar but with minor differences (their hp and movement speed, and of course their sprites). In broader terms, it's common for games to have different types of enemies that are in many ways the same (they have hp that gets drained when the player attacks them and they go poof when the hp is depleted) and in other ways different (how they move, how they attack the player, etc). There are a few different ways to go about making the enemies.


1) Make a completely new scene and script for each different type of enemy. This will work, and most beginners will probably do it that way, and you'll probably want to make your first game or two that way to keep things simple to understand when you're just starting out. But you'll have to re-write a lot of the "groundwork" code for each enemy. While copy/pasting code can be done, in general you'll want to avoid doing it if possible – if you have a game where the enemies spawn into the middle of the scene and you want to change the spawning process for all enemies to play a glittery animation when they spawn, or be added as a potential target for the player's multi-strike attack when they spawn, then it's better to have the spawning code that's common to all enemies be in one place instead of having to go and edit it for each of the different enemies.


2) Make a single Enemy scene and script that controls every type of enemy. That's what I did in this game, but only because the enemies are very similar, and I wouldn't recommend doing this as your usual approach because if you make a lot of different enemies with a lot of different behaviors then putting all the code into a single script would make it get hideously complicated. But for this simple game, I'll show you how I did it.


3) Make a base Enemy class that handles the stuff that all (or almost all) enemies in the game should do, and then make separate SpecificEnemy scenes that extend Enemy and do things that are unique to that enemy type. This is what I usually do in more complex games like Daxolissian System, so I'll also show you how to do that but much later.


The enemy node hierarchy

First for approach #2: make a res://enemy folder. Go to the pack of sprites that you got earlier and drag all of the slime_run_anim and goblin_run_anim sprites into the res://enemy folder. Make a new scene with a root node of KinematicBody2D and name it Enemy. Give it child nodes of an AnimatedSprite and a CollisionShape2D. In the AnimatedSprite, for its Frames property make a new SpriteFrames and edit it to have one animation call slime with all of the slime sprites and another animation called goblin with all of the goblin sprites, and set the speed for each of them to be 15 FPS. In the AnimatedSprite's Node2D-Transform set the Scale to (2, 2) and adjust the position so the "feet" are at the center of the Enemy node (both while the slime animation is playing and while the goblin animation is playing – you can turn the AnimatedSprite's Playing property on and switch the Animation property to see how they look in the viewport). I did it by setting the Node2D-Transform-Position to (0, -8). Lastly, since the sprites are facing right but we want the enemies to run toward the left, set the AnimatedSprite's FlipH property to On. Then for the ColisionShape2D set its shape to a CapsuleShape2D and edit it to have a radius of 6 and height of 14 or so, and set the CollisionShape2D's Node2D-RotationDegrees to 90. Make sure the AnimatedSprite's Animation property is set to slime (we'll set things up so that the slime is the default enemy type) and save the scene as res://enemy/enemy.tscn.


The enemy code

Now for the code: attach a script to the root Enemy node and give it the following code.

extends KinematicBody2D

export var is_goblin:bool

var speed_min:float = 50.0
var speed_max:float = 100.0
var goblin_speed_multiplier:float = 0.8
var slime_hp:int = 2
var goblin_hp:int = 4

var speed:float
var hp:int
var movement_vector:Vector2 = Vector2.ZERO

onready var sprite_node = get_node("AnimatedSprite")

func _ready():
	hp = slime_hp
	speed = rand_range(speed_min, speed_max)
	if is_goblin:
		hp = goblin_hp
		sprite_node.animation = "goblin"
		speed *= goblin_speed_multiplier

func _physics_process(delta):
	movement_vector = Vector2(-speed, 0)
	move_and_slide(movement_vector)

Before I describe what that code does, save it and go to the Level scene and add a few Enemy nodes to the center of it. If you click on an Enemy node in the Level's hierarchy, then in the Inspector you'll see that it has a property called IsGoblin that you can click to turn on. It doesn't look like it does anything to the enemy when you're looking at the Level scene in the editor, but if you play the game then any enemies where you turned on IsGoblin should end up being goblins. When you declare variables with the keyword "export" in front, then you can set them for each instance in the editor like that, which can be handy if you want to manually set parameters for specific enemies within the levels of your games. If you go back to the enemy.gd script and change that line to

export var is_goblin:bool = true

then it will by default have a value of true, and you can try adding a few more enemies to the level to see it work and that you should instead manually uncheck it if you want the enemy to not be a goblin.


The actual code so far is pretty simple. We'll want the enemies to each have different random speeds instead of all moving at the exact same speed, so we have the speed_min and speed_max variables and in _ready() we set this instance's actual speed to be a random number between them with the line

speed = rand_range(speed_min, speed_max)

We also have variables defining the slime's hp and the goblin's hp, and another variable just called hp which will be the hp for the specific enemy instance depending on whether it's a slime or a goblin. In _ready() it first sets the hp and speed to be the values for a slime, then if is_goblin is true it changes them (and the animation that the AnimatedSprite is playing) to be the values for goblins, including multiplying the speed by a multiplier so goblins are usually a little slower than slimes. In _physics_process() we just have the enemy move left according to whatever its speed is. You might be wondering why there are two lines there instead of just

move_and_slide(Vector2(-speed, 0))

and the reason is that we'll be doing some more complicated stuff later to modify the movement_vector before actually moving the enemy.


Testing and tweaking - HP bars

If you play the game now, the enemies walk left and you can see in the Output panel that it's properly detecting collisions when the enemies get hit with the sword. So next let's add code to make the enemies actually get damaged when you swipe them. The HP bar will be useful to have as a visible indicator, so in the Enemy scene add a new child node of type ProgressBar and name it HPBar. Set its ProgressBar-Percent-Visible property to false (unchecked), Range-Step to 1, and Range-Value to 50 (so when we look at it in the viewport we can see how it looks when it's 50% filled). To give it intuitive colors, under Control-ThemeOverrides-Styles make a new StyleBoxFlat for both the foreground and background and edit them to be a nice shade of green for the foreground and red for the background. Set Control-Rect-Position to (-20, -40) and Size to (40, 6) to get it centered a little above the enemy's head, then save the Enemy scene. This was pretty quick and dirty, and you'll probably want to use a TextureProgress node instead of the humble ProgressBar node to be fancier in your own games.


Now for the code, we'll add some variables to handle the cooldown for making the HP bar visible (I'll only want it to be visible briefly after an enemy is struck and then disappear), a reference to the HPBar node with the hp_bar variable, a command in _ready() to set the hp bar to full health, and a function called take_damage() that will run when the enemy gets hit. With game design, it's pretty common to have a weapon or anything that inflicts damage 1) look for collisions with any targets that it can inflict damage on, and 2) tell its target to run a method to actually take the damage, while the target is responsible for carrying the code saying what it will do when it gets damaged. The enemy script will be

extends KinematicBody2D

export var is_goblin:bool = true

var speed_min:float = 50.0
var speed_max:float = 100.0
var goblin_speed_multiplier:float = 0.8
var slime_hp:int = 2
var goblin_hp:int = 4
var hp_bar_cooldown_max:float = 2.0

var speed:float
var hp:int
var movement_vector:Vector2 = Vector2.ZERO
var hp_bar_cooldown:float = 0.0

onready var sprite_node = get_node("AnimatedSprite")
onready var hp_bar = get_node("HPBar")

func _ready():
	hp = slime_hp
	speed = rand_range(speed_min, speed_max)
	if is_goblin:
		hp = goblin_hp
		sprite_node.animation = "goblin"
		speed *= goblin_speed_multiplier
	hp_bar.max_value = hp

func _physics_process(delta):
	if hp_bar_cooldown > 0.0:
		hp_bar_cooldown -= delta
	else:
		hp_bar.visible = false
	
	movement_vector = Vector2(-speed, 0)
	move_and_slide(movement_vector)

func take_damage():
	hp -= 1
	if hp <= 0:
		queue_free()
	hp_bar.value = hp
	hp_bar.visible = true
	hp_bar_cooldown = hp_bar_cooldown_max

We'll also need to go back to the Player script and make the swipe actually inflict damage instead of just printing stuff to the Output window. To do it, we know that get_overlapping_bodies will give you an array of all of the bodies that the swipe overlaps. Those bodies could be an enemy, or could be the player, or could be an invisible border of the level. So we'll look at each body the swipe overlaps and test to see whether it has the take_damage() method that we just added to the enemy, and if so then we'll tell the body to run its take_damage() method. So the player's code will now have

	# Swipe
	if mouse_is_clicked and swipe_cooldown <= 0.0:
		swipe_cooldown = swipe_cooldown_max
		swipe_sprite.flip_v = not swipe_sprite.flip_v
		swipe_pivot.visible = true
		for body in swipe_area.get_overlapping_bodies():
			if body.has_method("take_damage"):
				body.take_damage()


Testing and tweaking - Knockback from hits

You should be able to test that out now and it should work. But it seems sort of bland, doesn't it? Let's add some knockback effect so if there's a hit then the player and enemy (or enemies) will be pushed apart some. Here's the code with knockback added for the player.

extends KinematicBody2D

var speed:float = 400.0
var swipe_cooldown_max:float = 0.3
var swipe_display_time:float = 0.05
var strike_knockback_force:float = 200.0
var strike_knockback_cooldown_max:float = 0.5

var move_vector:Vector2
var swipe_cooldown:float = 0.0
var last_mouse_position:Vector2 = Vector2.ZERO
var mouse_is_clicked:bool = false
var strike_knockback_normalized:Vector2 = Vector2.ZERO
var strike_knockback_cooldown:float = 0.0

onready var swipe_pivot = get_node("SwipePivot")
onready var swipe_area = get_node("SwipePivot/Swipe")
onready var swipe_sprite = get_node("SwipePivot/Swipe/Sprite")
onready var sprite_node = get_node("AnimatedSprite")

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

func _physics_process(delta):
	# Cooldowns
	if swipe_cooldown > 0.0:
		swipe_cooldown -= delta
		if not swipe_cooldown > swipe_cooldown_max - swipe_display_time:
			swipe_pivot.visible = false
			sprite_node.rotation = 0.0
	
	if strike_knockback_cooldown > 0.0:
		strike_knockback_cooldown -= delta
	
	# Movement
	move_vector = speed * Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	if move_vector.length() > 0.1:
		sprite_node.animation = "run"
	else:
		sprite_node.animation = "idle"
	if move_vector.x > 0.01:
		sprite_node.flip_h = false
	if move_vector.x < -0.01:
		sprite_node.flip_h = true
	if strike_knockback_cooldown > 0.0:
		move_vector += strike_knockback_normalized * strike_knockback_force * (strike_knockback_cooldown / strike_knockback_cooldown_max)
	move_and_slide(move_vector)
	
	# Point the sword toward the mouse
	swipe_pivot.look_at(last_mouse_position)
	
	# Swipe
	if mouse_is_clicked and swipe_cooldown <= 0.0:
		swipe_cooldown = swipe_cooldown_max
		swipe_sprite.flip_v = not swipe_sprite.flip_v
		swipe_pivot.visible = true
		for body in swipe_area.get_overlapping_bodies():
			if body.has_method("take_damage"):
					body.take_damage(global_position)
					strike_knockback_normalized = (swipe_pivot.global_position - swipe_area.global_position).normalized()
					strike_knockback_cooldown = strike_knockback_cooldown_max

func _unhandled_input(event):
	if event is InputEventMouseMotion:
		last_mouse_position = event.position
	if event is InputEventMouseButton:
		if event.button_index == BUTTON_LEFT:
			if event.pressed:
				mouse_is_clicked = true
			else:
				mouse_is_clicked = false

Don't try to run the game just yet because it won't work until we do a little more with the Enemy too, but I'll describe what this is doing. Now when a swipe occurs, if it hits any bodies that will take damage (e.g. enemies) then the player's strike_knockback_cooldown will be set to max and Godot will calculate the strike_knockback_normalized vector, which is a vector pointing from the swipe's area node toward the swipe's pivot node, which is the direction we want the player to be pushed. If you're not familiar with vector math, basically if you want a vector giving you the direction (and distance) from point A to point B, then take the coordinates of point B and subtract the coordinates of point A and that's the vector you want. If that's completely alien to you, check Godot's docs on vector math at https://docs.godotengine.org/en/stable/tutorials/math/vector_math.html and try to absorb what you can because it can be useful for programming games.


Another important thing to notice is that I added a parameter for the take_damage() call, so now instead of just body.take_damage() it's body.take_damage(global_position). That's because when an enemy gets hit, if it's going to take knockback, then it won't be enough just to tell the enemy that it got hit and you'll also need to let the enemy know where the hit came from so it can be knocked back in the correct direction. That global_position in the parenthesis is the player's global_position (because this is the script for the Player scene so any bare property names like that are the player's properties). Notice that I'm using global_position and not just position, and this is a good point to talk about local versus global coordinates. Remember when we were setting up the SwipePivot node and set its position property to be (0, -8): that tells us the SwipePivot's position relative to its parent node (the root Player node). If the player moves around on the battlefield, the SwipePivot's position property will not change, it will always have its position be (0, -8) (unless we intentionally move SwipePivot's position relative to the Player node, but we won't). So if you even want to use the SwipePivot's position, you'll have to think for a bit: do you want the SwipePivot's position on the screen accounting for the fact that the player moved, which is its global_position, or do you just want the SwipePivot's position relative to the Player node, which is its position (also called local position). 99% of the time you'll really want its global_position, so I've gotten into the habit of just using .global_position all the time unless I specifically know that I need its local position in which case I'll use .position. But that distinction between global and local coordinates is highly likely to mess you up at some point in game development, so whenever it seems like your code just isn't working like it should, ask yourself whether you're accidentally using local coordinates when you want to be using global coordinates or vice-versa.


As a more minor side note, the syntax in the line to calculate strike_knockback_normalized might also look a little funny, but I want a normalized vector (meaning its length is set to 1) pointing from the swipe area to the swipe pivot. If you have a Vector2 then you can use its .normalized() method, but I don't want to calculate swipe_pivot.global_position.normalized() and/or swipe_area.global_position.normalized(), I want to subtract them and then normalize that vector. So the parentheses force the subtraction to happen first which gives you a Vector2, and then you can normalize() that Vector2, even though it might look a little funny to have a period coming right after a parenthesis like that.


Now we'll modify the Enemy script to take knockback and handle the parameter that gets passed in the take_damage() function.

extends KinematicBody2D

export var is_goblin:bool = true

var speed_min:float = 50.0
var speed_max:float = 100.0
var goblin_speed_multiplier:float = 0.8
var slime_hp:int = 2
var goblin_hp:int = 4
var strike_knockback_force:float = 800.0
var strike_knockback_cooldown_max:float = 0.5
var hp_bar_cooldown_max:float = 2.0

var speed:float
var hp:int
var movement_vector:Vector2 = Vector2.ZERO
var strike_knockback_cooldown:float = 0.0
var strike_knockback_normalized:Vector2 = Vector2.ZERO
var hp_bar_cooldown:float = 0.0

onready var sprite_node = get_node("AnimatedSprite")
onready var hp_bar = get_node("HPBar")

func _ready():
	hp = slime_hp
	speed = rand_range(speed_min, speed_max)
	if is_goblin:
		hp = goblin_hp
		sprite_node.animation = "goblin"
		speed *= goblin_speed_multiplier
	hp_bar.max_value = hp

func _physics_process(delta):
	if strike_knockback_cooldown > 0.0:
		strike_knockback_cooldown -= delta
	if hp_bar_cooldown > 0.0:
		hp_bar_cooldown -= delta
	else:
		hp_bar.visible = false
	
	movement_vector = Vector2(-speed, 0)
	if strike_knockback_cooldown > 0.0:
		movement_vector += strike_knockback_normalized * strike_knockback_force * strike_knockback_cooldown / strike_knockback_cooldown_max
	move_and_slide(movement_vector)

func take_damage(origin:Vector2):
	hp -= 1
	if hp <= 0:
		queue_free()
	hp_bar.value = hp
	hp_bar.visible = true
	hp_bar_cooldown = hp_bar_cooldown_max
	strike_knockback_normalized = (global_position - origin).normalized()
	strike_knockback_cooldown = strike_knockback_cooldown_max

The take_damage() function has been modified so now it takes a parameter which I'm calling origin (as in the origin of the strike that just hit the enemy) and I'm using static typing to make sure that it's a Vector2 to help avoid bugs. The strike_knockback_normalized and strike_knockback_cooldown work like in the player's script and they similarly affect the movement before the move_and_slide() command, albeit with more oomph than the player because I set strike_knockback_force to be larger with the enemy script. Go ahead and playtest.


That makes it feel a little more like a video game, but we've still got some work to do on the fundamental game mechanics because we'll want the enemies to be able to walk off the left edge of the screen and lead to a Defeat, and we'll want the level to recognize when all of the enemies are dead and lead to a Victory, and do a few more things which we'll deal with when we get there. These sorts of things – telling win the Win Condition or Lose Condition of a level have been met – are best handled by the Level in the next post.


1

Posted by 3p0ch - September 8th, 2022


The level's node hierarchy

So far our player is just walking around on a gray background, so let's make a level to put him in. In Godot make a new folder of res://level and make a new scene (on the main menu bar: Scene – New Scene) with the root node being a YSort node. It will handle that sorting that we just talked about – its children will be drawn so anything with a smaller y-coordinate in its Position property (meaning that it's higher on the screen: remember that the top of the screen has y coordinate 0 and the bottom has a y coordinate of the number of pixels in the screen) will be drawn behind anything with a larger y-coordinate. Name that root node Level, and give it a child of a Sprite node and name that child Ground. The assets I made for the game can be downloaded from

https://www.newgrounds.com/dump/download/233a38b7d038de5619489482c8ff1ac3

Put the grass.png file in the res://level folder and then drag that to the Ground node's Texture property, and under Offset make sure that Centered is unchecked so you'll end up with the ground being at position (0, 0) and drawn covering the screen area. I made that 1024 x 600 pixels to cover the default screen size in Godot – if it's not covering the entire screen then from the main menu bar at the top of Godot go to Project – Project Settings – Display – Window and adjust the Width and Height to 1024 x 600. The Ground's position of (0, 0) works well with the YSort node because since the ground's position y-coordinate is 0 it will be drawn behind pretty much everything. Then add an instance of Player to the Level node: in the Godot file structure find res://player/player.tscn and click and drag it up to the Level node in the scene's hierarchy to make a Player instance in the scene. Then move the player so he's kind of in the center of the screen.


Lastly, we won't want the player to run off the edge of the screen so we'll add some collision objects that won't have sprites attached to them but will be there to keep him from running off the edge. Make a child StaticBody2D called LeftBorder, give it a child CollisionShape2D, and set the CollisionShape2D's Shape to be a RectangleShape2D with Extents of x: 10 and y: 300 (half the height of the screen, so the rectangle reaches 300 pixels up and down to cover the entire height). Then move the LeftBorder (StaticBody2D) node by setting its Node2D-Transform-Position to (0, 300) and you're done with the left border. Make the RightBorder similarly but at position (1024, 300), and make the BottomBorder with the RectangleShape2D using Extents of x: 512 (half the screen width) and y: 10 and position it at (512, 600). For the top border, remember how the player's hitbox is set up – we want that border to be a little thicker so if the player walks all they way up they won't have their head up above the top of the screen, so make its Extents be x: 512 and y: 40. You should be able to run that scene (save it as res://level/level.tscn if you haven't already) and have the player able to run around within the screen area. While we're at it, for now let's make the Level scene be the starting scene for the game (when you run an export or when you click the play button in the upper right that's not on a director's thingy) – in Project – Project Settings – General tab – Application – Run, set Main Scene to res://level/level.tscn.


Now for teaching points: the YSort node is a common way to handle 2D node sorting – deciding which nodes should be drawn in front of or behind others – for games with this sort of camera angle where you might have a guy walking around a tree like in the example above. But if you're making a different type of game then you should be aware there are different ways of handling layering if you don't use a YSort node. If you click on anything that inherits from Node2D (like the Ground or Player or the Borders) then in the inspector under Node2D you'll see a Z Index. Remember the Godot API reference? Check what the z-index does by looking at the Node2D entry (since z-index is in the Node2D group in the inspector so it's a property of Node2Ds) or click this link https://docs.godotengine.org/en/stable/classes/class_node2d.html#class-node2d-property-z-index Basically you can give the nodes numbers there so nodes with higher numbers will be drawn in front of nodes with lower numbers. If you're making a side-scroller then the background could have z-index 0, player and walls and enemies etc could have z-index 1, and any foreground effects like fog or such could have z-index 2. Also be aware that if two nodes have the same z-index then Godot uses the following rules: child nodes are drawn in front of their parent nodes (although you can change that by setting a node's Show Behind Parent property under CanvasItem to true), and the order in the hierarchy in the top left matters because "sibling" nodes will have the top node in the hierarchy list drawn furthest toward the back and nodes lower in the hierarchy list drawn in front.


The sword swipe

How should we handle swinging the sword? This is actually much more complicated than it seems. Stop and think about it for a minute before reading how I do it, and take a look at the game again and pay attention to how he's swinging the sword in the direction where the mouse is pointing.


If you did the "Your first 2D game" tutorial, then you might think of spawning the sword similar to how you spawn an enemy in that example – by making an instance of a scene. That's not how I did it. But since this is a tutorial and the purpose is to show you how stuff works in Godot, let's try that approach.


Approach 1: Instancing a scene for each swipe

Make a new scene, and your first question will be what type of node you want the sword swipe to be. KinematicBody? StaticBody? Area? Sprite? AnimatedSprite? In this case, we want to be able to detect collisions or overlaps with enemies, so a natural choice would be something that handles physics like KinematicBody / StaticBody / Area. And in my opinion, I just wanted to be able to detect if the sword overlaps an enemy that got hit but I didn't want to make Godot's physics engine forcibly push apart the sword node with whatever enemy got hit (I'll handle that myself from code because I know how I want to handle the pushback for both player and enemy) so I made it an Area2D node. If you want a detailed discussion of what the different nodes do and when you should pick which type of node, then it's at https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html But for now just make a new scene with a root node of Area2D named Swipe and give it child nodes of Sprite and CollisionShape2D. Drag swipe.png into the res://player folder and make that be the Texture for the Sprite. Remember that we scaled up the player by 2, so for the Sprite node under Node2D-Transform-Scale set x and y to 2. Then for the CollisionShape2D make a new CapsuleShape2D and edit it to overlap the sprite nicely. Save that as res://player/swipe.tscn. On the root node of the swipe scene, add a script with just the following code.

extends Area2D

var lifetime:float = 0.1

func _ready():
	connect("body_entered", self, "_on_body_entered")

func _physics_process(delta):
	lifetime -= delta
	if lifetime < 0.0:
		queue_free()

func _on_body_entered(body):
	print("body_entered signal, overlap with: ", body)

The print() will make Godot show things in the bottom center of the editor if you click on Output, in this case which body triggered the body_entered signal, and the rest of the code just makes the swipe get rid of itself after 0.1 seconds.


Now go to your player.tscn scene and open its code (either in the hierarchy clicking on the script icon next to the root Player node, or in the top center of the screen in Godot click on Script and then select the player.gd script that you saved earlier). Add a new line just above the speed variable declaration of

export var swipe_scene:PackedScene

and save the script. Since that line starts with "export", in the Inspector you should see a new category of Script Variables at the top that has Swipe Scene, which should look eerily similar to things like Node2D-Transform-Position. Drag swipe.tscn into that variable, and then add some more code to end up with this.

extends KinematicBody2D

export var swipe_scene:PackedScene
var speed:float = 400.0
var swipe_cooldown_max:float = 0.5
var player_to_swipe_distance:float = 26.0

var move_vector:Vector2
var swipe_instance:Area2D
var swipe_cooldown:float = -1.0
var last_mouse_position:Vector2 = Vector2.ZERO

onready var sprite_node = get_node("AnimatedSprite")

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

func _physics_process(delta):
	# Cooldowns
	if swipe_cooldown > 0.0:
		swipe_cooldown -= delta
	
	# Movement
	move_vector = speed * Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	if move_vector.length() > 0.1:
		sprite_node.animation = "run"
	else:
		sprite_node.animation = "idle"
	if move_vector.x > 0.01:
		sprite_node.flip_h = false
	if move_vector.x < -0.01:
		sprite_node.flip_h = true
	move_and_slide(move_vector)

func _unhandled_input(event):
	if event is InputEventMouseMotion:
		last_mouse_position = event.position
	if event is InputEventMouseButton:
		if (event.button_index == BUTTON_LEFT) and event.pressed and (swipe_cooldown <= 0.0):
			swipe_instance = swipe_scene.instance()
			add_child(swipe_instance)
			swipe_instance.look_at(last_mouse_position)
			swipe_instance.translate(player_to_swipe_distance * swipe_instance.global_transform.x)

We added variables swipe_cooldown_max and swipe_cooldown to handle a cooldown so you have to wait a tiny bit between swipes, and we made a swipe_instance variable that will be a reference to the swipe object that gets instanced. Remember that a scene like swipe_scene and an instance like swipe_instance are different types of things – a scene is like an abstract blueprint and only one swipe scene should exist in the game, while an instance or object is a more concrete thing that can show up in the scene tree and interact with stuff and potentially have multiple copies running around. The func _unhandled_input(event) function gets called any time the player does any sort of input, from pressing a key to moving or clicking the mouse to pressing a gamepad button, unless something else in the game "handles" the input first. The parameter named event is an object describing the event, so event might be a mouse motion or a mouse button press or a key press etc. In this case, the code is doing two things. First it checks to see if the input is a mouse motion, and if so it updates the variable last_mouse_position to be the coordinates on the screen where the mouse is. Why in blazes are we doing that? Because there isn't a command along the lines of current_mouse_position() that you can just call any time you feel like it. So the next best thing you can do is constantly check for mouse movement and if the mouse moves then store its position in a variable.


Second, it checks to the if the input is a mouse button press, and if so it checks to see if it's a left mouse button press (not a right mouse button press or the mouse wheel), whether the button was actually just pressed (because when you release the mouse button Godot also treats that as a mouse button event, but it has event.pressed set to false instead of true), and it makes sure that we're not still waiting on the cooldown for the next swipe. If all of those conditions are met, it makes an instance of the swipe and adds it to the scene tree as a child of the player, then makes it "look at" or point to (where the positive x-axis direction will point to the coordinate you indicate – that's why the sword swipe picture has the sword swinging right), and makes it move a little bit away from the player instead of sitting smack on top of the center of the player (which would normally happen with add_child). Taking a closer look at that line with swipe_instance.translate: it uses swipe_instance.global_transform.x. The global_transform of a Node2D is a description of how it's oriented, and the global_transform.x is a unit vector (meaning it has length 1) that tells you which way the Node2D's "right" is now pointing if you account for how it's rotated. In this case, since the swipe was rotated to point toward the mouse, swipe_instance.global_transform.x tells you which way the swipe_instance considers "right" (or "forward" since that's how the sprite was made) and nudges it in that direction multiplied by player_to_swipe_distance. If things are still unclear, try playing the game with and without that line commented out to keep it from being run (by putting a # character at the beginning of that line) to see what it's doing and why.


At this point, you might be asking yourself how you're supposed to remember all these input events and that you should check to see if an event is an InputEventMouseMotion or InputEventMouseButton and what properties like .pressed you should be checking for. Remember how I told you that you'll need to refer to the API a lot? Check InputEvent, and specifically InputEventWithModifiers (because keyboard and mouse input can have modifiers) and InputEventMouse and things that are inherited by it. Especially if you're thinking of making any games that might want to check for whether the player double-clicked on something without needing to write a lot of unnecessary code.


Try testing things out by playing the Level scene. It works like we want, sort of. The first thing you might notice is that if you move the mouse around the player then you can tell that the swipe is rotating around a pivot point at the player's feet (since we intentionally set things up to have the player's (0, 0) be there) instead of the center of the player which would seem more natural. That would be easy enough to fix by moving the swipe up a bit more, and I'll leave that as an exercise for the reader. Next look at the output window as you're playing to see what the swipe is colliding with. Sometimes it will collide with the player, and that's not really a problem because when we write the code to handle inflicting damage we can make the swipe not damage the player. And if you walk up to the StaticBody2Ds that you set up as borders then you'll see that you can swipe them and they'll be detected just as you would expect, so far so good.


Before moving on, I'll mention another approach you might want to take with your games. In this case the swipe sprite has the sword in it, and it makes sense for the swipe sprite to move around with the player if the player is moving, which is what will happen if the swipe is a child of the player. But what if you made the sprite be a swoosh of glowing crimson air that the sword passed through, and it would make more sense to have the swipe's sprite stay stationary instead of moving around with the player as the player moves before it disappears? In that case, you would want the swipe to be a child of whatever the root node for the scene is, instead of making it a child of the player (and therefore moving around with the player since child nodes move with their parent node). To do that, you could make these changes

			get_tree().current_scene.add_child(swipe_instance)
			swipe_instance.position = position

The get_tree() method gives you a reference to the scene tree, and its current_scene property is the root for the tree, so get_tree().current_scene.add_child() will add it to the root of the current scene tree. For the next line: on the right hand side if you just see a naked "position" without anything else then Godot will look for a position property of the object that the script is attached to, in this case the Player scene. The swipe_instance.position is of course the position of the swipe instance, so this will set the swipe_instance's position to be the same as the player's position (which will be nudged again in the next line). That's important to do in this case because when we did the get_tree().current_scene.add_child(swipe_instance), the swipe_instance was added as a child at the root scene's position (0, 0) which isn't where we want it since the player swiped his sword after all. We didn't worry about that earlier since when we added the swipe_instance as a child of the player it was added to the scene at the player's position.


Approach 2: Making the swipe a permanent child of the player

While that approach worked, it wasn't the approach I took in the game. Before we dive into it, try adding this line to the very end of your current player.gd script.

			print("Swipe instance overlapping bodies: ", swipe_instance.get_overlapping_bodies())

You'll notice that when you play, it doesn't work and doesn't jive with what you see from the body_entered signal. Hold that thought, and now we're going to wipe out the current swipe implementation and replace it with what I actually used in the game. In this case you're doing a tutorial, but when you're working on your own game if you ever feel like doing a major overhaul then be aware that there's not a great way to undo things if you decide it was a bad idea after all and you want your original design back. The simplest thing to do would be to make a .zip of your entire project's directory before you make any major changes so you'll have the option of scrapping your changes and going back to that .zipped version. Or use could use more advanced version control approaches described at https://docs.godotengine.org/en/stable/tutorials/best_practices/version_control_systems.html but I haven't used that approach myself.


We're going to make the swipe just be a child node within the Player scene, and no longer be its own Swipe scene. Open the Player scene and make a new child Node2D named SwipePivot. Then remember how you added the Player scene as a child in the Level scene by opening Level and dragging the player.tscn file to the root Level node in the hierarchy? Drag the swipe.tscn scene and drop it on the new SwipePivot node so it's a child of SwipePivot. Then right click the Swipe node in the hierarchy and click Make Local so it will now just be a child node without any sort of connection to the swipe.tscn scene. Detach the script from the Swipe node (by right clicking and picking remove script or by clicking the script with an X in the upper right of the scene hierarchy). Save the Player scene, and now you can delete the swipe.tscn scene and the swipe.gd script since everything will be done in the Player scene and by player.gd.


The SwipePivot node serves as a convenient way to make the sword pivot around the center of the player instead of around the player's feet when you move the mouse around while swiping. First, to see what happens if you don't make use of the SwipePivot node: try just going to the Swipe node and setting its Node2D-Transform-Position to (24, -8) which is kind of where you want it if the player is looking forward and swiping. Then play with the slider under Node2D-RotationDegrees, and it doesn't really do what you want since it just rotates the swipe around the Swipe node's position at (24, -8). Instead, set the SwipePivot's Node2D-Transform-Position to (0, -8) so its crosshair in the scene view is about on the player's shoulder, and set the Swipe node's position to (24, 0). Now you can use the SwipePivot's Node2D-RotationDegrees to rotate the swipe around like you want. Then in the hierarchy click the eye symbol to the right of SwipePivot so it and its children won't normally be visible – we'll make it appear and disappear from code in response to mouse clicks.


Next, edit the player's script to be this.

extends KinematicBody2D

var speed:float = 400.0
var swipe_cooldown_max:float = 0.3
var swipe_display_time:float = 0.05

var move_vector:Vector2
var swipe_cooldown:float = 0.0
var last_mouse_position:Vector2 = Vector2.ZERO

onready var swipe_pivot = get_node("SwipePivot")
onready var swipe_area = get_node("SwipePivot/Swipe")
onready var swipe_sprite = get_node("SwipePivot/Swipe/Sprite")
onready var sprite_node = get_node("AnimatedSprite")

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

func _physics_process(delta):
	# Cooldowns
	if swipe_cooldown > 0.0:
		swipe_cooldown -= delta
		if not swipe_cooldown > swipe_cooldown_max - swipe_display_time:
			swipe_pivot.visible = false
			sprite_node.rotation = 0.0
	
	# Movement
	move_vector = speed * Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	if move_vector.length() > 0.1:
		sprite_node.animation = "run"
	else:
		sprite_node.animation = "idle"
	if move_vector.x > 0.01:
		sprite_node.flip_h = false
	if move_vector.x < -0.01:
		sprite_node.flip_h = true
	move_and_slide(move_vector)
	
	# Point the sword toward the mouse
	swipe_pivot.look_at(last_mouse_position)

func _unhandled_input(event):
	if event is InputEventMouseMotion:
		last_mouse_position = event.position
	if event is InputEventMouseButton:
		if (event.button_index == BUTTON_LEFT) and event.pressed and (swipe_cooldown <= 0.0):
			swipe_cooldown = swipe_cooldown_max
			swipe_sprite.flip_v = not swipe_sprite.flip_v
			swipe_pivot.visible = true

Briefly, it now has a command at the end of _physics_process() that will point the SwipePivot node toward the last mouse position on every frame (whether it's visible or not) and _unhandled_input() will make it visible and set the swipe_cooldown in response to clicks. As a little bonus, it also flips the swipe's sprite vertically each time you swipe so he slashes in opposite directions on each swipe. Test it out, and it should pretty much act like you want. The only thing you might not like about it is that the player has to press and release the mouse button with each swipe of the sword, while I think it would be better if they could just keep the mouse button held down and continuously slash whenever the cooldown is up. You can do that by making a boolean variable to keep track of whether the mouse is pressed that will be updated whenever an InputEventMouseButton happens. Add it to the list of declared variables near the top

var move_vector:Vector2
var swipe_cooldown:float = 0.0
var last_mouse_position:Vector2 = Vector2.ZERO
var mouse_is_clicked:bool = false

Then make the _unhandled_input(event) function update mouse_is_clicked whenever the left mouse button is pressed or released

func _unhandled_input(event):
	if event is InputEventMouseMotion:
		last_mouse_position = event.position
	if event is InputEventMouseButton:
		if event.button_index == BUTTON_LEFT:
			if event.pressed:
				mouse_is_clicked = true
			else:
				mouse_is_clicked = false

And finally put the code to actually swipe at the end of _physics_process()

	# Point the sword toward the mouse
	swipe_pivot.look_at(last_mouse_position)
	
	# Swipe
	if mouse_is_clicked and swipe_cooldown <= 0.0:
		swipe_cooldown = swipe_cooldown_max
		swipe_sprite.flip_v = not swipe_sprite.flip_v
		swipe_pivot.visible = true

You can test that out and it should work nicely. Next add collision detection to the swipe with the get_overlapping_bodies() method like so

	# Swipe
	if mouse_is_clicked and swipe_cooldown <= 0.0:
		swipe_cooldown = swipe_cooldown_max
		swipe_sprite.flip_v = not swipe_sprite.flip_v
		swipe_pivot.visible = true
		for body in swipe_area.get_overlapping_bodies():
			print("The swipe overlaps with: ", body)

You should be able to see in the editor that the swipe overlaps the player and if you walk towards a border of the screen and swipe then it detects the screen's border. But why did it work now when it wasn't working when we tried to use get_overlapping_bodies() when we were instancing the swipe from a PackedScene? This is a common way people get tripped up with the physics engine. The physics stuff like determining what overlaps with what is just calculated once during the physics step that's run with physics_process, and if a node gets added to the scene then all that information won't be updated until the instanced node does its physics_process. It doesn't matter that we were checking for overlap from within the Player node's physics_process because the Swipe was instanced and overlaps were being checked immediately afterward; the swipe didn't have time to do its own physics before we started checking for collisions. But with this new approach the Swipe node is a permanent child of Player (whether it's visible or not) so it will have its physics be processed continuously and its collision data will be up to date when we check it. But be aware that similar problems with physics can crop up if you do things like make a huge change in a node's position and check for overlaps immediately afterward – the physics engine might not have updated the overlap data between the time you changed the position and the time you check for overlaps. Usually you'll want to use the move_and_slide or move_and_collide methods instead of just changing a physics object's Position property because they're really made for this sort of thing and checking for collisions as the object is in the process of moving.


So everything works like we want, and we can move on to making some enemies (the slimes and goblins, not our "allies" yet).


1

Posted by 3p0ch - September 8th, 2022


The player's node hierarchy

I usually start games by making the player, and we'll do that now. The player will be a scene with a root node of a KinematicBody2D with child nodes including an AnimatedSprite for the player's sprite and a CollisionShape2D for his hitbox. You can name the root node Player (and leave the other nodes with their default names), make a directory called res://player, and save the scene there as player.tscn. To add the sprites, download them from https://o-lobster.itch.io/simple-dungeon-crawler-16x16-pixel-pack and since we'll have a lot of sprites for the player make a subdirectory within your Godot project of res://player/sprites to put the relevant sprites into: in this case just the knight's run and idle sprites (not the spritesheets). Click the AnimatedSprite node and in its Frames property make a new SpriteFrames, then click it and edit the SpriteFrames to include one animation called idle and another animation called run. Add the relevant sprites to the idle and run animations and give both animations a speed of 15 FPS (which I used) or whatever feels right to you – click on the AnimatedSprite node and set its Playing property to On to see it in action. For the CollisionShape2D, in its Shape property give it a new CapsuleShape2D and edit the new CapsuleShape2D to get the size about right – you'll notice it's pointing up/down when you want it left/right (if you think that's surprising then just bear with me for now, I'll explain in more detail later), so click on the CollisionShape2D node again and in its properties under Node2D and Transform there will be a Rotation Degrees property that you can set to 90 so the capsule is aligned to point horizontally. But don't worry too much about getting it just the right size for now because I'll go into more detail about it soon.


The player's code - Movement

After that's set up, it's time to start coding. Click on the root node and attach a script to it (go ahead and use the default name which should be res://player/player.gd if you've already saved the scene as res://player/player.tscn) and give it the following code so the guy can move around.

extends KinematicBody2D

var speed:float = 400.0

var move_vector:Vector2

onready var sprite_node = get_node("AnimatedSprite")

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

func _physics_process(delta):
	# Movement
	move_vector = speed * Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	if move_vector.length() > 0.1:
		sprite_node.animation = "run"
	else:
		sprite_node.animation = "idle"
	if move_vector.x > 0.01:
		sprite_node.flip_h = false
	if move_vector.x < -0.01:
		sprite_node.flip_h = true
	move_and_slide(move_vector)

That's a pretty common way to handle movement for a top-down game like this with keyboard controls and similar to how it's done in the Your First Game tutorial. If you'd like to check out other ways of moving a character around, the Godot docs talk about it at https://docs.godotengine.org/en/stable/tutorials/2d/2d_movement.html


Introduction to the API reference

However, the tutorial didn't use the Input.get_vector() method. if you're not already familiar with Input.get_vector() then you can find out how it works in the Godot docs' API reference. If you haven't looked there before then now's the time to get started because you'll need to go there a lot as a Godot dev, and it might be confusing at first but it's important so I'll talk about it in detail now in case it's new to you. From the Godot docs at https://docs.godotengine.org/en/stable/ scroll down to the bottom of the index on the left and under CLASS REFERENCE expand the Godot API tree to see a ton of Classes. Briefly look through that first one, @GDScript, to see built-in functions like rand_range() which we'll use later. If you click on it, you'll see the entry

float rand_range ( float from, float to )

and you should interpret that as meaning that if you use the rand_range() function then it will take two parameters (which it calls "from" and "to") which should be floating point values and it's output will be a floating point value (that's what the "float" at the very beginning means). So if you want to use it in your own code, you could write a command like

my_floating_point_var = rand_range(my_min_value, my_max_value)

When you start writing your own code and including functions that you haven't seen used in a tutorial or other code before, the API reference is the place to look to see a description of what it does and how it should be structured to work. Now if you wanted to see how that Input.get_vector() method works, in the API listing scroll down to Input and click on it (don't expand its tree, just click on it) and look through its Methods and find get_vector and click it. There you can see that it returns a Vector2 (so Godot will be able to handle move_vector = Input.get_vector(), phew!) and the parameters when you call get_vector() should each be a String with the name of the axis for negative and positive x and y values, which are set up in the Input Map that we'll come back to in just a minute. But first, a couple more examples of looking things up in the API reference. Suppose you didn't know how move_and_slide works. In this example it's not like Input.get_vector() where it's easy to tell that we're looking in the Input class for the method get_vector(). What's going on here is the player.gd script is of a type KinematicBody2D (that's what the very first line of code starting with extends tells you) so the move_and_slide method is in the KinematicBody2D class. Scroll down and find KinematicBody2D in the API reference and click on it to see how move_and_slide works and what parameters can be used with it, and also notice that it returns a value of a Vector2 which you might not have known about but will be helpful if you make a game where you need to preserve vertical velocity when the player jumps (and thankfully we don't have to worry about it for this game but be aware that it's there and should be thought about if you do make a game that involves jumping). As you look through the API reference listing for KinematicBody2D you might be thinking "Wait a minute, there's not a lot here. Aren't there a lot more things that you can do with a KinematicBody2D, like look_at()?" This is the last thing I'll point out for now that might trip you up when you're using the API reference at first – the look_at() method can be used by KinematicBody2D, but it's actually a method that belongs to Node2D. A KinematicBody2D "inherits" from a Node2D, so a KinematicBody2D will have all of the properties and methods that a Node2D has plus some more that are specifically for KinematicBody2D and not for all Node2Ds. In this case, if you scroll up to the top of the page for KinematicBody2D you'll see that it inherits from things including Node2D, and if you click on Node2D to see its API reference page you'll find the look_at() method. And that's enough of the API reference for now.


A note about static variable typing

Also, you might not have seen people declare variables like this

var speed:float
var move_vector:Vector2

where the variable's name is followed by a colon followed by the type of variable that it should be. I strongly recommend that you do that, which is called static typing. It will make Godot throw errors if you try to assign a value other than that type to a variable. For example if you had the following erroneous line in your code

move_vector = speed * delta

where speed is a floating point value of 400.0 and delta is the frame rate, then if you didn't use static typing Godot would blithely calculate 400.0 times 0.0167 = 6.68 and put that floating point value into move_vector, which is clearly not what you wanted because move_vector should be a vector and not a float. And it wouldn't show up as an error until later in the code when you try to use move_vector in the move_and_slide() function where Godot is expecting to get a vector saying where the player should move but instead just got the floating point value of 6.68. Then you would have to rummage through your code looking for where things went wrong, whereas if you declared the variable with

var move_vector:Vector2

then Godot would tell you right away exactly where the problem is since you tried to assign a float to a variable that should be a Vector2. This is a simple example, but the more complex your code is and the more abstract the variables become, the harder it gets to track down those bugs. So I recommend using typed variables unless you have a good reason not to. For more details about static typing, see https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/static_typing.html and hopefully their description is more understandable after you've seen this explanation.


Test and tweak

Now let's test out the player and see how it looks: make sure the tab for the player scene is selected at the top of the center viewport, then click the button in the top right to play the selected scene (the movie director thingy with the play button in the middle). The first thing you'll notice is that the player in the top left can move with arrows but not WASD, so you should set it up to move with either WASD or arrows. Stop playing and go to Project – Project settings – Input map and set ui_left, ui_right, ui_up, and ui_down to be controllable with WASD too. Now try playing it again, and it seems ok but I'd like the player to be bigger. So stop playing and click on the AnimatedSprite node and look under Node2D, Transform, Scale and set it to 2 for both x and y. (Remember how I was talking about inheritance earlier? The inspector is helping to teach you that the Transform, which has Scale, is a property of Node2D.) But now the hitbox in the CollisionShape2D node also needs to be made larger. Do you have any vague memories of seeing somewhere in the Godot docs that you should never adjust the Scale of a hitbox? Well, here's your reminder: do NOT just adjust the scale of the CollisionShape2D under Node2D-Transform-Scale the same way you did with the AnimatedSprite. Instead you want to select the CollisionShape2D node and edit the Shape at the very top and adjust its dimensions to something like Radius 6 and Height 16.


Hitboxes in 45° view games

The last point for now is getting the AnimatedSprite and the CollisionShape2D aligned properly. When you're making games like this where the "camera" is sort of looking at the scene from above at a 45-or-so degree angle, think about how things work in the real world. If you're standing in a room looking at a tall tree and a short person, then who will look like they're "in front" from your perspective? See the picture below. Whoever's "feet" are lowest in your field of view will be the one standing closest to you. Even if the tree's center is higher in your field of view than the guy's center, if the position in your field of view where the tree contacts the earth is lower than where the guy's feet touch the earth, then the tree is closer to you. Godot can automatically "sort" things from back to front like this so anything lower on the screen is drawn in front of anything higher on the screen, but remember that the position of the objects should be the position of their "feet" to make this work right. The other concept to be aware of is how large the hitbox should be. Suppose the player in the figure below were to walk straight left – would he collide with the tree? No, he would walk "behind" it from your field of view. But if the guy were a little bit lower in your field of view so his feet lined up with the tree's "feet" and he walked left then he would smack into the tree, and if he were to walk even lower than that then he would be walking "in front of" the tree from your perspective. So the tree's hitbox shouldn't cover the entire tree's sprite, it should only cover a small area around its "feet" where collisions should happen, and same for the guy. In the picture below, you would want the hitboxes to be something like the yellow areas. And if you remember to do all those things – make the "position" of the tree and the guy be their feet instead of their centers, and make the hitboxes cover a reasonable area around their feet – then the guy will be able to walk around the tree and be properly drawn in front of or behind it. Remember that this is only applicable for games where the camera is sort of looking down at an angle and is not applicable for platformers like Mario or Sonic where the camera is looking directly from the side and the hitbox should probably cover most of the sprite.

iu_746482_3205434.png

Putting that into practice with our player, the root node of the scene (the KinematicBody2D) should still have its position property (under Node2D-Transform-Position) be (0, 0) because we haven't messed with it, and this is the "official" position of the player so that (0, 0) is where the feet should end up centered. The AnimatedSprite should be positioned so that the center of its feet are at about (0, 0), and in this case after I set the scale to 2 I set the Node2D-Transform-Position to be (0, -11) so go ahead and set that now. The hitbox CollisionShape2D should have its Node2D-Transform-Position be at (0, 0) and its Shape set to be a CapsuleShape2D with Radius about 6 and Height about 16 while the CollisionShape2D's Rotation Degrees (under Node2D-Transform) is 90.


In the next post, we'll go over making the Level scene and the sword swipes.


3

Posted by 3p0ch - August 9th, 2022


I've updated my Godot implementation for the NewGrounds API to handle cloud saves, and using it will be a little tricky especially if you're not used to using a FuncRef so here's the TL DR on how it works. (Admittedly a little rushed because it seems like this would be useful for the Mobile Game Jam if players want to be able to play both from phone and computer and keep their save data.)


NewGrounds' cloud save system will give your game some number of save slots that can each store a string with some maximum size. NewGrounds might change the number of save slots and maximum size over time, so the specific numbers aren't built into the Godot API implementation and you'll want to check your game project's page and look in the API section to see what the current values are.


You'll probably want to make a Dictionary specifically designed to hold all of the data that should be cloudsaved and keep it in an AutoLoad so it's persistent when you change scenes and every script can access it – I'll include an example of the save_handler.gd script I've been using at the end of this post. While the cloud technically just stores a string in each save slot, the procedure for converting an object to a string to transmit over the internet and then be converted back to an object again (known as serializing and deserializing with JSON) is built into Godot and I set up the API implementation to handle that process for you, so you can blithely consider a slot to be something that holds a Dictionary and not worry about those details.


You can send a request to cloud save a variable (like a complex Dictionary) with the command

Ngio.cloud_save(data_to_be_saved, slot_number)

The slot_number is optional and if you leave it out it will save in slot #1. THERE IS NO SLOT #0 – SAVE SLOT NUMBERING STARTS AT #1 and if you use multiple save slots then that will probably mess you up at some point so consider yourself warned. Once you send the request to save the data, it will take some time for the request to be sent and the data to actually be saved. You'll probably want to just send the request and hope that it succeeds without going to inordinate measures to make sure it worked, but if you do want to show some indication that the cloud save was successful you can use a FuncRef like so

Ngio.cloud_save(data_to_be_saved, slot_number, funcref(self, "_on_cloudsave_complete"))

and have an _on_cloudsave_complete() function in your script like this

func _on_cloudsave_complete(result):
	print("in _on_cloudsave_complete with result ", result)

It should accept one parameter that has the results that the NewGrounds API sends from the cloud save attempt. If you don't include that third parameter of a FuncRef when you call Ngio.cloud_save() then by default it will just print that result to the browser's Developer Tools window in the Console tab where you can check it for debugging purposes. Because I know you've been checking there for error messages to debug your HTML5 games this whole time, right?


You can send a request to load cloud data from a single slot with the command

Ngio.cloud_load(funcref(self, "_on_cloud_loaded"), slot_number)

The slot number is again optional and the default is slot #1. Since it will take time for the cloud data to be transferred over the internet and loaded, you can't just have your code stop at that line and wait until the data is loaded and then in the next line of code start using the data (or else it will seem like the game is frozen during the loading process for what could be seconds or longer). Instead you need to have the first parameter of the cloud_load() call be a FuncRef pointing to a function that will handle the saved data once it finally gets in from the internet, and a simple function could be something like

func _on_cloud_loaded(loaded_data):
	my_dict_of_save_data = loaded_data

It should take one parameter which will be the same as whatever you originally sent in your request to save the data. In practice you'll probably want to get more sophisticated and just before calling Ngio.cloud_load() make some visual cue letting the player know that a cloud load is being attempted, and then in the referenced function give the player an indication that the load is complete, and also check that the loaded data is what you expect it to be with something like

if loaded_data == null:
	print("Could not load data from that slot, was anything saved there in the first place?")
elif loaded_data is Dictionary:
	print("Load successful, here's your data: ", loaded_data)
else:
	handle a messup

The results that the NewGrounds API sends from the load request will also show up the browser's Developer Tools window in the Console pane to help with debugging.


You can also send a request to load from all slots with a single command

Ngio.cloud_load_all(funcref(self, "_on_cloud_loaded"))

With a function that handles each slot of data as it comes in like

func _on_cloud_loaded(slot_number, loaded_data):
	if loaded_data == null:
		Could not load data from that slot
	else:
		Do something with the loaded_data from slot_number


If you want to clear the data from a save slot, just use this command because it's simple enough that I didn't bother to write my own function to streamline it.

Ngio.request("CloudSave.clearSlot", {"id": slot_number})

You can also add a FuncRef as a third parameter in that call, but you probably don't need to.


Lastly, by default I made it show results from any API requests to cloudsave in your browser's Developer Tools – Console tab because my Spidey senses tell me that you'll probably need to do some debugging. But once your game is ready to publish, you can (if you like) change show_cloudsave_results near the top of the Ngio.gd script to false so players can't look there to see where the cloudsaved data lives.


And here's the save_handler script I usually use. It includes functions to save locally either in LocalStorage if you're playing from web or your hard drive if you're playing from the Godot editor, and you might want to keep those because saving and loading locally is faster than having to go through the cloud and could be another backup.

extends Node

var key = "3p0ch_cloud_save_test"

var data = {
	"boolean_test": true,
	"int_test": 12,
	"float_test": 4.3,
	"string_test": "testing_funcref_default",
	"array_test": [
		1,
		2.3,
		"four",
		[5, "six"],
	],
	"dictionary_test": {
		"seven": 8,
		9: "ten",
	},
}

func read_save():
	if OS.has_feature('JavaScript'):
		var JSONstr = JavaScript.eval("window.localStorage.getItem('" + key + "');")
		if (JSONstr):
			return parse_json(JSONstr)
		else:
			return null
	else:
		var file = File.new()
		if not file.file_exists("user://" + key + ".save"):
			return null
		file.open("user://" + key + ".save", File.READ)
		return parse_json(file.get_line())

func write_save():
	if OS.has_feature('JavaScript'):
		JavaScript.eval("window.localStorage.setItem(\"" + key + "\", \'" + to_json(data) + "\');")
	else:
		var file = File.new()
		file.open("user://" + key + ".save", File.WRITE)
		file.store_line(to_json(data))
		file.close()

func open_site(url):
	if OS.has_feature('JavaScript'):
		JavaScript.eval("window.open(\"" + url + "\");")
	else:
		print("Could not open site " + url + " without an HTML5 build")

func switchToSite(url):
	if OS.has_feature('JavaScript'):
		JavaScript.eval("window.open(\"" + url + "\", \"_parent\");")
	else:
		print("Could not switch to site " + url + " without an HTML5 build")

Tags:

11