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

plasmid @3p0ch

Cat

Scientist

Read the manual & try stuff

So Cal

Joined on 2/13/10

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

Godot tutorial - All the King's Men - NewGrounds API and advanced stuff

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

Comments

Comments ain't a thing here.