Improve My Gam Jam #26


I will be working on DecaDream, submitted for the Godot Wild Jam #55 that was run a week ago. There I received feedback that I would be honoring in this Improve My Game Jam #26.

Decadream draws inspiration from Vampire Survivors and Brotato. It is an action roguelike, uses mainly geometry for its visuals and has you playing as a triangle that has to defeat squares in order to gain sides up until being a decahedron.

In this devlog I:

  • Improved the enemy placement: enemies were placed instantly on a random point, sometimes spawning on top of the player.
  • Added a gameplay mode that lets you play for an unlimited amount of time.
  • Improved the menu feel: menus were not stylized at all, so they did not fit the overall visuals.
  • Added a settings menu: I wanted to experiment with user settings, so I implemented volume control.

Enemy Placement

The enemy placement feeling unfair is something I had to address right away. I implemented the same solution used on “Brotato”. They use red Xs to show where the enemies will spawn. Those change their transparency to “blink”, and after a while the enemies spawn on top. Here is the starting point on the left, and the result on the right, both with reduced frame rate:

Before the enemies were not annouced

Now the enemies are announced before appearing with little squares that blink

I have in place an “EnemySpawner” that:

. Selects a random point given top-left and bottom-right limits. . Selects a random number of instances, between one and five, to spawn. . For each instance, it randomly offsets its position from the initial selected point. . For each instance, it adds them to the world.

The “EnemySpawner” spawns PackedScenes, so it can spawn anything as it is a standalone Scene. Instead of passing the “Enemy” scene to the “EnemySpawner”, I passed a “SpawnMarker” scene instead.

The “SpawnMarker” scene receives a PackedScene, in this case the “Enemy” scene, and adds it to the world in its position after a certain period of time. Before spawning the “Enemy” scene, a Tween changes the transparency of the “SpawnMarker” sprite back and forth. The “SpawnMarker” scene looks like this:

extends Node2D

@export var spawn_scene: PackedScene

func _ready() -> void:
	var tween := get_tree().create_tween()
	tween.tween_property(self, "modulate:a", 0.2, 0.6).set_trans(Tween.TRANS_LINEAR)
	tween.chain().tween_property(self, "modulate:a", 1, 0.1).set_trans(Tween.TRANS_LINEAR)
	tween.chain().tween_property(self, "modulate:a", 0.2, 0.1).set_trans(Tween.TRANS_LINEAR)
	tween.chain().tween_property(self, "modulate:a", 1, 0.1).set_trans(Tween.TRANS_LINEAR)
	tween.chain().tween_property(self, "modulate:a", 0.2, 0.05).set_trans(Tween.TRANS_LINEAR)
	tween.chain().tween_property(self, "modulate:a", 1, 0.05).set_trans(Tween.TRANS_LINEAR)
	tween.chain().tween_property(self, "modulate:a", 0.2, 0.05).set_trans(Tween.TRANS_LINEAR)
	tween.chain().tween_callback(spawn)

func spawn() -> void:
	if spawn_scene == null:
		push_warning("Need a PackedScene to spawn!")
	else:
		var scene := spawn_scene.instantiate()
		scene.global_position = global_position
		get_parent().add_child(scene)
	queue_free()

In the “EnemySpawner”, the spawn code looks like this:

var n_instances: int = randi_range(0, 5)
for n in range(n_instances):
	var spawn_marker: Node2D = SpawnMarker.instantiate()
	spawn_marker.global_position = Vector2(
		randf_range(spawn_center.x - spawn_offset, spawn_center.x + spawn_offset),
		randf_range(spawn_center.y - spawn_offset, spawn_center.y + spawn_offset),
	)
	spawn_marker.spawn_scene = Enemy
	entities.add_child(spawn_marker)

And with these changes, the enemy placement now has a gives the player a visual indicator where the enemies will be placed, giving time to react.

Infinite Game Mode

To introduce an infinite mode I had a problem due to how I change from scene to scene.

The game is organized into into different scenes, where each corresponds to one “view” of the game: “game”, “main menu”, “settings menu”, “credits menu”, etc. The way I change between them makes impossible to set a variable to the next scene that will be used. So instead of changing the way I change from scene to scene, I introduced a global object.

I created the global object, called GameState, that holds one single variable, called infinite_mode, which is a boolean. When the player selects the infinite mode in the main menu, the infinite_mode variable is set to true. From the game, the conditions for winning are only triggered if infinite_mode is false.

Menu Feel

The menus were not customized to match the overall visuals of the game, so they felt out of place.

In Godot the UI elements can have a Theme. The theme specifies values for different elements of the UI components, like colors, font, sizes, etc. I started by creating a theme and saving it as standalone, so I could add reuse it in the different menu scenes I have.

At some point, I thought “there must be a better way”, and it fact there is. In the project settings you can define a default theme for the whole project, so now I do not need to add the theme to every new menu I create.

Here is how the main menu looks like, before on the left, after on the right:

Before the menus were not customized

Now the menus are customized to fit the overall style

Settings Menu

I wanted to experiment with user settings and volume adjustment, so I implemented a menu settings screen with persistent configuration.

First of all I added multiple audio buses to my game. Godot has the “Master” bus, and I created one for music, called “Music”, and one for effects, called “Effects”. Each AudioStreamPlayer node can be associated to one of the audio buses, so I changed mine accordingly. Changing their volume from code looks like this:

var master_bus_index := AudioServer.get_bus_index("Master")
AudioServer.set_bus_volume_db(bus_index, -5)

var music_bus_index := AudioServer.get_bus_index("Music")
AudioServer.set_bus_volume_db(music_bus_index, -10)

var effects_bus_index := AudioServer.get_bus_index("Effects")
AudioServer.set_bus_volume_db(effects_bus_index, -5)

For the settings menu I used multiple HSliders. As the sliders have minimum and maximum avlues, you can use dB directly without any conversion. In my case, I configured the sliders from -30 to 0, so I can set the value directly into the bus dB value.

The settings menu with sliders

While it worked well, the value of the sliders was not saved anywhere, so the volumes were reset on every startup. To store the values I used the ConfigFile class in the user://configuration.cfg, which is translated to C:/Users/<user>/AppData/Roaming/Godot/app_userdata/<Game Name>/configuration.cfg. The configuration file looks like this:

[audio]
master_volume = 0
music_volume = 0
effects_volume = 0

Then on Godot I created a Configuration global object, that reads these values on application startup and sets the appropiate values for each of the audio buses. Then the Configuration global variable is accessible from anywhere, so I can read and save new values on any point in the game code.

The Configuration.gd looks like this:

extends Node

var config := ConfigFile.new()
var config_path := ProjectSettings.globalize_path("user://configuration.cfg")


func _ready() -> void:
	print("Loading configuration from '%s'" % config_path)
	var err := config.load(config_path)

	if err != OK:
		if err == ERR_FILE_NOT_FOUND:
			push_warning("'%s' was not found, creating one." % config_path)
			config.save(config_path)
		else:
			push_error("Something went wrong loading '%s'. Error code '%s'" % [config_path, err])

	# Here I read the volume configuration and set the buses volumes


func get_value(section: String, key: String, default: Variant = null) -> Variant:
	return config.get_value(section, key, default)


func set_value(section: String, key: String, value: Variant) -> void:
	config.set_value(section, key, value)
	config.save(config_path)

Another interesting thing is the Callable.bind() method. In the settings screen, where the user interacts with the sliders, I connected the different sliders drag_ended signal, to a single function. Using bind I could pass references to the slider, bus name and configuration key, without having to create a function for each of the sliders. It looks like this:

func _ready() -> void:
	master_volume_slider.drag_ended.connect(
		on_volume_slider_drag_ended.bind("Master", master_volume_slider, "master_volume")
	)


func on_volume_slider_drag_ended(
	value_changed: bool, bus_name: String, volume_slider: Slider, configuration_key: String
) -> void:
	if not value_changed:
		return

	var slider_value := volume_slider.value
	var bus_index := AudioServer.get_bus_index(bus_name)
	Configuration.set_value("audio", configuration_key, slider_value)
	if slider_value == volume_slider.min_value:
		AudioServer.set_bus_mute(bus_index, true)
	else:
		AudioServer.set_bus_mute(bus_index, false)
		AudioServer.set_bus_volume_db(bus_index, slider_value)

This way the function receives all that it needs and do not need to access variable out of its body.

Files

decadream-v1.1.zip Play in browser
Mar 28, 2023

Leave a comment

Log in with itch.io to leave a comment.