Let's learn Godot together

Make games! Discuss those games here.

Moderators: Bob the Hamster, marionline, SDHawk

User avatar
Mogri
Super Slime
Posts: 4678
Joined: Mon Oct 15, 2007 6:38 pm
Location: Austin, TX
Contact:

Re: Let's learn Godot together

Post by Mogri »

OK, so

If I run the scene, it works just fine. If I run the game, I get your error. This is because you have the same script attached to label.tscn, which is your Main Scene. It's a Sprite2D as well, but it doesn't have a Timer child.

You aren't actually using label.tscn for anything, as far as I can tell. So you can either delete it or go to Project Settings > Application > Run and set the Main Scene to node_2d.tscn. (Or, I suppose, you could give label.tscn a Timer child, but that probably doesn't accomplish what you're trying to do.)

In general, I recommend "Run Current Scene" over "Run Project" for debugging. It starts you from what you're looking at instead of from the start of the game. It may not make much difference now, but it'll be very helpful if you start making a larger project.
User avatar
Mogri
Super Slime
Posts: 4678
Joined: Mon Oct 15, 2007 6:38 pm
Location: Austin, TX
Contact:

Re: Let's learn Godot together

Post by Mogri »

Main menu, continued

I've done a little more wiring up of my main menu. I haven't changed anything in the scene editor, so it still looks like this:
Image

But I have messed around with the script a bit. Here's what I've got right now:

Code: Select all

extends Control

@export var load_menu_scene: PackedScene
@export var options_menu_scene: PackedScene

signal start_new_game

var focused_control: Control


func has_saved_game():
	return FileAccess.file_exists("user://savegame.save")


func open_submenu(submenu_scene: PackedScene):
	var submenu = submenu_scene.instantiate()
	add_child(submenu)
	$Menu.hide()
	submenu.connect("close_menu", _on_close_menu)


func _ready():
	if (has_saved_game()):
		$Menu/LoadGameButton.grab_focus()
	else:
		$Menu/NewGameButton.grab_focus()
		$Menu/LoadGameButton.disabled = true


func _on_quit_button_pressed():
	get_tree().quit()


func _on_options_button_pressed():
	focused_control = $Menu/OptionsButton
	open_submenu(options_menu_scene)


func _on_load_game_button_pressed():
	focused_control = $Menu/LoadGameButton
	open_submenu(load_menu_scene)


func _on_new_game_button_pressed():
	emit_signal("start_new_game")


func _on_close_menu():
	$Menu.show()
	focused_control.grab_focus()
It's not too complicated, but let's run through it briefly.

@export var my_scene: PackedScene is the tutorial-recommended way of getting a reference to an external scene that we want to be able to instance from this one. I grab a reference to the load menu and the options menu so I can pop them up.

I've created two helper methods. has_saved_game lets me know whether to enable the Load Game button, and open_submenu allows me to pop up new menus. The latter also hides the main menu. I found that if you don't do this, then you can still interact with those buttons, such as by navigating to them from the controls in the submenu.

In _ready, I put the keyboard/controller focus on the Load Game button if there's a saved game; otherwise, I disable that button and focus New Game.

Clicking on Options or Load Game spawns the appropriate submenu. If the player closes the submenu, we want to refocus the button that got him there, so we remember that with focused_control. You may notice that there's a lot of var_name: VarType in my script. This is called "type hinting," and it's a good practice that also allows the script editor to give you better feedback. For example, when I type "focused_control.gr" in my script, the editor says, "Oh! I know that variable is a Control node, so I'll suggest grab_focus and grow_horizontal" (among other options). Without the type hint, the editor will do its best but will generally be less helpful.

Right now, OptionsMenu and LoadMenu aren't doing much. They're identical scenes with just a Cancel button. Here's the script I'm currently using for both:

Code: Select all

extends Control

signal close_menu

func _ready():
	$CancelButton.grab_focus()


func _on_cancel_button_pressed():
	queue_free()
	emit_signal("close_menu")
We saw queue_free() in "Your first 2D game" as well. It just means "unload this scene at your earliest convenience." The close_menu signal is handled in my open_submenu function in the main menu script: when the main menu receives that signal, it invokes _on_close_menu to make the main menu reappear and give it focus.


That's it! Nothing super game-changing today, but "create an instance of a scene and connect its signal to my function" is an important skill to add to our toolbox.
User avatar
MorpheusKitami
Slime Knight
Posts: 218
Joined: Wed Nov 30, 2016 8:23 pm
Location: Somewhere in America

Re: Let's learn Godot together

Post by MorpheusKitami »

Mogri wrote: Mon Oct 23, 2023 5:49 pm OK, so

If I run the scene, it works just fine. If I run the game, I get your error. This is because you have the same script attached to label.tscn, which is your Main Scene. It's a Sprite2D as well, but it doesn't have a Timer child.

You aren't actually using label.tscn for anything, as far as I can tell. So you can either delete it or go to Project Settings > Application > Run and set the Main Scene to node_2d.tscn. (Or, I suppose, you could give label.tscn a Timer child, but that probably doesn't accomplish what you're trying to do.)

In general, I recommend "Run Current Scene" over "Run Project" for debugging. It starts you from what you're looking at instead of from the start of the game. It may not make much difference now, but it'll be very helpful if you start making a larger project.
Ah, I see what happened now. I was following the tutorials from the start, and used the label scene to start with, but when I started into scripting and made the new scene the icon character must have been in linked to the old one, and then it automatically got linked to the script. Damn, it takes talent for me to suck that badly. I appreciate your help and patience.
User avatar
Mogri
Super Slime
Posts: 4678
Joined: Mon Oct 15, 2007 6:38 pm
Location: Austin, TX
Contact:

Re: Let's learn Godot together

Post by Mogri »

No problem! There's a decent chance that Godot is doing its merry best to help you screw things up, as it tends to be overzealous in updating references when you change things that are linked to by other things. There's a method to the madness, surely, but I haven't figured it all out yet.
User avatar
Mogri
Super Slime
Posts: 4678
Joined: Mon Oct 15, 2007 6:38 pm
Location: Austin, TX
Contact:

Re: Let's learn Godot together

Post by Mogri »

Your first 3D game

Time to dive into the last tutorial. There's a starter pack for this, so download and import that instead of creating a new project.

The first thing that jumps out at me is that we have to define the dimensions for the MeshInstance and the CollisionShape separately. Seems like there ought to be a better way. You could create your own component to do it more easily, but why doesn't it already exist?


The first step is just creating the floor and a light. The second step creates the player scene. I had a hard time figuring out how to navigate the 3D scene and get the collision model to line up. I eventually found a clumsy way to move around by alternating right-click and middle-click. Surely, there's a better way that I'm missing.

We also set up our inputs in this step. Again, sure would be nice to have some presets here so we could just make a quick "left" action, and so on.


Step 3 is the player script. This introduces us to _physics_process, distinct from the _process we've seen because it happens at a fixed rate to enable smooth game physics.

If you didn't un-hide the character model in the last step, the Player will be invisible when you instantiate it in your Main scene.


Step 4, creating the Mob, is very similar to other things we've already done, so let's move on to step 5. This introduces a couple of very valuable features: duplicating nodes and editing more than one node at once. Very handy! I'd love to say this handles my complaint about MeshInstance and CollisionShape, but selecting multiple nodes shows their lowest common ancestor, which is Node3D for those two, and their dimensions exist as properties of MeshInstance and CollisionShape themselves. Ah well. I get the feeling that you do less and less of this by hand as you go.

The traffic jam you get by running the game at the end of this step is great.


Step 6 adds the jumping and squashing actions. Here, we go into more depth on layers and masks, which we explored only briefly in the 2D game. They don't do a great job explaining them here. My understanding is that layer means "this object exists on this layer" and mask means "this object collides with things on this layer." An object with a mask but no layers would be pushed around by other things but would not produce any collisions of its own.

Running the game at the end of this step gives me weird behaviors when I let the mobs push me around. I'm often credited with squashing them when they run into me. Maybe I did something wrong with my mob collision.


Step 7 adds player death, which may effectively solve that problem anyway. Make sure you use body_entered and not area_entered for your signal here.


Step 8 adds the HUD. I was tempted to skip this, because we already did it in the 2D tutorial, but there's some good stuff here! Notably, it introduces themes. I already discovered these in my own tinkering, as seen above, but I learned that you can place a theme on a Control node to have it apply to all of the descendants.

Weirdly, you can't edit the default font for a theme in the theme editor. You have to do it in the inspector.

We also learn how to connect a signal from an instance created in-script, a technique I discovered in my last update. It's almost as though the tutorial is useful or something.

I appreciate the side note in this tutorial that mentions this really isn't how you'd structure a larger game.

Finally, the tutorial teaches us about autoloads and tells us that there's a way to examine a running game from within the engine. Very handy!


I'm out of time for today, so I'll have to revisit the last two sections later. I have to say I have a much more positive impression after this tutorial.
User avatar
Mogri
Super Slime
Posts: 4678
Joined: Mon Oct 15, 2007 6:38 pm
Location: Austin, TX
Contact:

Re: Let's learn Godot together

Post by Mogri »

Finishing your first 3D game

It turns out the last two parts are really small. The animations tutorial is really cool -- you get a lot of bang for your buck without altering any of the models. 3D modeling intimidates me, and I hope I don't have to do much or any of it in order to make a nice first-person dungeon crawler. 😬

The last page is a collection of links to other topics that are good for novice users. Due to the nature of the game I'd like to make, most of these are not interesting to me, but as the page says, "Inputs is an important one for any project." Let's take a peek.


Using InputEvent: An important takeaway from this page is the node graph halfway through, which shows the order in which nodes receive events. Otherwise, this is mostly a technical explanation of things we've already looked at, although the tutorials didn't really talk much about _unhandled_input, so I'd recommend reading about that. At the end is an example of how to generate your own input events, which is fairly situational but nice to have.


Input examples: _input(event) versus Input.isActionPressed is an important distinction, and the article doesn't really talk about why you'd prefer one over the other. Consider their example of jump versus move_right. When the user presses the jump button, you definitely want the player to jump; in contrast, it's fine to just check whether the user is currently holding right. If we check whether the user is currently pressing jump, then it's possible for the user to press and release the button too quickly, and the game will skip the jump action. That's true for movement as well, but movement is continuous while jump is more binary. Put another way, if you need to know how long a button is held down, you need Input.isActionPressed; if you need to know when a button is pressed, use _input.


Mouse and input coordinates is short and platform-specific. If your game should work with a non-mouse input, you can probably skip this article even if you want the mouse to be able to do stuff, too. The UI stuff will handle that for you.


Customizing the mouse cursor is more interesting as long as you want to support mouse. Not terribly difficult, though, and not really a priority.


Controllers, gamepads, and joysticks: Recommended reading. This tells you how to handle analog controls (such as a joypad), add controller vibration, and deal with the weird inconsistencies between gamepad and keyboard.


Handling quit requests is a safe skip if you know about get_tree().quit() and don't care about preventing the OS from closing your game (e.g. because the user clicked on the X).


This is about as much as we're going to get from guided learning. I'm going back to my experiments after this.
User avatar
Mogri
Super Slime
Posts: 4678
Joined: Mon Oct 15, 2007 6:38 pm
Location: Austin, TX
Contact:

Re: Let's learn Godot together

Post by Mogri »

Tile-based movement in 3D

I'm super pleased to have figured this out! I'm sure I'll need to tweak it down the line, but as a proof of concept, it's working really well.

Before we can move in 3D, we need to have a world to move in. I created a new 3D scene called Dungeon and gave it a floor and a ceiling. They're both MeshInstance3D, and they're identical except that ceiling has a y-position of 8m. (Side note: I'm not sure if these are meant to represent actual meters. If they are, I guess I'm creating a giant dungeon for giants.) I've used a PlaneMesh and just made them really large. Because PlaneMesh is one-sided, I rotated it 180° on the z-axis. Nothing else really matters here, but I did color them.

Traveling through a featureless void is not good visual feedback. I created a Wall scene and put a couple instances of it in my Dungeon. Wall is a MeshInstance3D that uses a 10m x 10m x 1m BoxMesh. (I suppose it only needs to be 8m tall, but I did a lot of tweaking as I went, so not everything is entirely consistent.) To create a corner out of walls, I placed one at (5, 5, 0) with 90° y rotation and the other at (0, 5, 5) with no rotation. Someday, we will dynamically load in walls, but this is good enough for now.

As the 3D tutorial taught, we need a camera to see anything. I could just use a camera node, but I want the camera to hang behind the point of rotation a bit, so I created a Marker3D named CameraPivot to house my Camera3D. CameraPivot is 4m off the ground (that's y), while Camera is -3m in the x direction and rotated -90°. (Why did I do this? I don't remember, but changing it now breaks everything.)

We also want the player to carry a light source. I added an OmniLight3D to CameraPivot, offsetting it by -2m x and 1m y to give the impression that it's being held. I set Omni > Range to 30m and dragged the Attenuation to the right a bit. Attenuation affects how sharply the light falls off with distance; at my value of about 0.5, it stays solid for a decent radius.

At this point, I fooled around with this stuff to remove the ambient light and do a few other things, but our world is good to go even without all that.

Let's look at the script I attached to Dungeon. We want to do a few things here:
  • If the user taps a direction, he moves one tile (or turns 90°) in that direction.
  • If the user holds a direction, he continues moving (or turning) seamlessly from the previous action.
  • The user cannot turn and move at the same time; this isn't a first-person shooter.
We'll worry about walls some other time. For now, let's start with movement.

Code: Select all

var move_duration: float = 0
var move_direction: Vector3

const TILE_SIZE = 10
const MOVE_TIME = .5
const DIR_FORWARD = Vector3(1, 0, 0)
const DIR_BACK = Vector3(-1, 0, 0)
const DIR_LEFT = Vector3(0, 0, -1)
const DIR_RIGHT = Vector3(0, 0, 1)
We initialize a couple of variables: move_duration is how much longer we'll be moving, and move_direction is... well, you know. We also set up some constants. Our tiles are 10m², and moving to the next tile should take 0.5 seconds.

Code: Select all

func _process(delta):
	if (move_duration < delta && turn_duration == 0):
		if (Input.is_action_pressed("move_forward") && (move_direction == DIR_FORWARD || move_duration == 0)):
			move_duration += MOVE_TIME
			move_direction = DIR_FORWARD
		if (Input.is_action_pressed("move_back") && (move_direction == DIR_BACK || move_duration == 0)):
			move_duration += MOVE_TIME
			move_direction = DIR_BACK
		if (Input.is_action_pressed("move_left") && (move_direction == DIR_LEFT || move_duration == 0)):
			move_duration += MOVE_TIME
			move_direction = DIR_LEFT
		if (Input.is_action_pressed("move_right") && (move_direction == DIR_RIGHT || move_duration == 0)):
			move_duration += MOVE_TIME
			move_direction = DIR_RIGHT
This is more repetitive than I'd like -- there's probably a cleaner way to do this, but I'm not familiar enough with Godot paradigms to do anything fancier. Here's what this does:
  • If we're turning or we still have more moving to do, don't process move inputs.
  • If we're holding down the direction we're currently moving or we're not currently moving, add MOVE_TIME to move_duration and set move_direction appropriately.
Here's the code to actually move:

Code: Select all

	if (move_duration > 0):
		var move_length = min(move_duration, delta)
		$CameraPivot.position += move_direction.rotated(DIR_FORWARD, $CameraPivot.rotation.x)\
			* move_length * TILE_SIZE / MOVE_TIME
		move_duration -= move_length
move_length is how far we're moving this frame. It's usually going to be delta, but if move_duration is lower than delta, we use that value instead.

We move CameraPivot in the appropriate direction by an amount equal to move_length times tile size divided by the total move time. This means that over the course of MOVE_TIME, we'll move a total of TILE_SIZE. (The backslash indicates that the current line continues to the next line.)

Finally, we deduct how much we moved from how far we need to move. With that, we're done with movement! Turning ends up being fairly similar.

Code: Select all

var turn_duration: float = 0
var turn_rotation: float = 0

const TURN_TIME = .3
const TURN_LEFT = PI * .5
const TURN_RIGHT = PI * -.5
const TURN_AROUND = PI
turn_rotation is the total amount of rotation to perform over the TURN_TIME duration. Since we're using radians, half pi is 90 degrees.

Code: Select all

	if (turn_duration < delta && move_duration == 0):
		if (Input.is_action_pressed("turn_left") && (turn_rotation == TURN_LEFT || turn_duration == 0)):
			turn_duration += TURN_TIME
			turn_rotation = TURN_LEFT
		if (Input.is_action_pressed("turn_right") && (turn_rotation == TURN_RIGHT || turn_duration == 0)):
			turn_duration += TURN_TIME
			turn_rotation = TURN_RIGHT
		if (Input.is_action_pressed("turn_around") && (turn_rotation == TURN_AROUND || turn_duration == 0)):
			turn_duration += TURN_TIME
			turn_rotation = TURN_AROUND
	
	if (turn_duration > 0):
		var turn_length = min(turn_duration, delta)
		$CameraPivot.rotation.y += turn_rotation * turn_length / TURN_TIME
		turn_duration -= turn_length
Very similar to movement above. It may seem unusual that you can turn 180 degrees in the same time as 90, but it's a genre convention; the "turn around" command should execute quickly.

That's all! Here's how it looks:

Image

And here's the full script:

Code: Select all

extends Node3D

var move_duration: float = 0
var move_direction: Vector3
var turn_duration: float = 0
var turn_rotation: float = 0

const TILE_SIZE = 10
const MOVE_TIME = .5
const DIR_FORWARD = Vector3(1, 0, 0)
const DIR_BACK = Vector3(-1, 0, 0)
const DIR_LEFT = Vector3(0, 0, -1)
const DIR_RIGHT = Vector3(0, 0, 1)
const TURN_TIME = .3
const TURN_LEFT = PI * .5
const TURN_RIGHT = PI * -.5
const TURN_AROUND = PI

func _process(delta):
	if (move_duration < delta && turn_duration == 0):
		if (Input.is_action_pressed("move_forward") && (move_direction == DIR_FORWARD || move_duration == 0)):
			move_duration += MOVE_TIME
			move_direction = DIR_FORWARD
		if (Input.is_action_pressed("move_back") && (move_direction == DIR_BACK || move_duration == 0)):
			move_duration += MOVE_TIME
			move_direction = DIR_BACK
		if (Input.is_action_pressed("move_left") && (move_direction == DIR_LEFT || move_duration == 0)):
			move_duration += MOVE_TIME
			move_direction = DIR_LEFT
		if (Input.is_action_pressed("move_right") && (move_direction == DIR_RIGHT || move_duration == 0)):
			move_duration += MOVE_TIME
			move_direction = DIR_RIGHT
	
	if (turn_duration < delta && move_duration == 0):
		if (Input.is_action_pressed("turn_left") && (turn_rotation == TURN_LEFT || turn_duration == 0)):
			turn_duration += TURN_TIME
			turn_rotation = TURN_LEFT
		if (Input.is_action_pressed("turn_right") && (turn_rotation == TURN_RIGHT || turn_duration == 0)):
			turn_duration += TURN_TIME
			turn_rotation = TURN_RIGHT
		if (Input.is_action_pressed("turn_around") && (turn_rotation == TURN_AROUND || turn_duration == 0)):
			turn_duration += TURN_TIME
			turn_rotation = TURN_AROUND
	
	if (move_duration > 0):
		var move_length = min(move_duration, delta)
		$CameraPivot.position += move_direction.rotated(DIR_FORWARD, $CameraPivot.rotation.x)\
			* move_length * TILE_SIZE / MOVE_TIME
		move_duration -= move_length
	
	if (turn_duration > 0):
		var turn_length = min(turn_duration, delta)
		$CameraPivot.rotation.y += turn_rotation * turn_length / TURN_TIME
		turn_duration -= turn_length
User avatar
Bob the Hamster
Lord of the Slimes
Posts: 7684
Joined: Tue Oct 16, 2007 2:34 pm
Location: Hamster Republic (Ontario Enclave)
Contact:

Re: Let's learn Godot together

Post by Bob the Hamster »

Very nice!
User avatar
Mogri
Super Slime
Posts: 4678
Joined: Mon Oct 15, 2007 6:38 pm
Location: Austin, TX
Contact:

Re: Let's learn Godot together

Post by Mogri »

Transitions

Now that I have a MainMenu scene and a Dungeon scene, I can make the "New Game" button switch from one to the other. This is very easy:

Code: Select all

@export var new_game_scene: PackedScene

func _on_new_game_button_pressed():
	get_tree().change_scene_to_packed(new_game_scene)
If you prefer, you can use get_tree().change_scene_to_file("res://path/to/scene.tscn") instead. This seems nicer to me.

Now, clicking New Game will instantly plop us into the dungeon. A nice fade would add a lot of polish to this transition. Let's create a Fader scene.

The Fader scene is super simple. The root node is a Control. It has two children: a ColorRect, anchored to the entire screen with the color black, and an AnimationPlayer. We create a new "fade" animation and connect the Color property from ColorRect to "fade." At t=0, set the color to #00000000 (that's eight zeroes, meaning "fully black, but also fully transparent" -- you can also drag Alpha to 0). At t=1, set the color to normal black. Tinker with the easing as desired.

Connect AnimationPlayer's "animation_finished" signal back to Fader. Here's the script for Fader:

Code: Select all

extends Control

signal fade_finished

func fade_out():
	show()
	set_focus_mode(Control.FOCUS_ALL)
	grab_focus()
	$AnimationPlayer.play("fade")

func fade_in():
	show()
	set_focus_mode(Control.FOCUS_ALL)
	grab_focus()
	$AnimationPlayer.play_backwards("fade")

func _on_animation_player_animation_finished(anim_name):
	fade_finished.emit()
	if ($Overlay.color.a == 0):
		# Hide Fader after fading in (i.e. when the alpha is 0)
		hide()
Why the show/hide? Because Fader is a Control, it blocks mouse input when it covers the screen. This is desirable because we don't want the user clicking on, say, Load Game as we're fading out to start a new game. When Fader is hidden, it no longer blocks the mouse. grab_focus handles the keyboard/controller inputs, since only one Control can have focus at a time, and set_focus_mode allows that to happen (normally, a naked Control can't grab focus). The fade_finished signal will allow scenes using Fader to wait for it to finish.

Let's wire it up to MainMenu. First, we need to add an instance of Fader to MainMenu and anchor it to the full screen.

Code: Select all

func _ready():
	$Menu/LoadGameButton.disabled = !has_saved_game
	$Fader.fade_in()
	await $Fader.fade_finished
	if (has_saved_game()):
		$Menu/LoadGameButton.grab_focus()
	else:
		$Menu/NewGameButton.grab_focus()


func _on_new_game_button_pressed():
	$Fader.fade_out()
	await $Fader.fade_finished
	get_tree().change_scene_to_packed(new_game_scene)
await tells Godot to hold off on processing the rest of the script until it receives the given signal. While Godot is multithreaded and other scripts can continue to run, nothing else in this script will be able to do anything while you're awaiting, so consider carefully whether await or connecting the signal to a handler is more appropriate.

We can add Fader to Dungeon as well:

Code: Select all

var player_can_act: bool = false

func _ready():
	$Fader.fade_in()
	await $Fader.fade_finished
	player_can_act = true


func _process(delta):
	if (!player_can_act):
		return
	# ...
Why add player_can_act instead of set_process(false)? Because turning off _process does it for all children as well.

Next: It would be nice to be able to return to the main menu. Let's create a new scene PauseMenu using a Control node. I'll give it a fullscreen ColorRect with a translucent background, then I'll add a Panel and give it a nice style. In the Panel, I add a VBoxContainer like I did with MainMenu, and I put my buttons in there with spacers in between. I also add a Fader instance but make it invisible by default -- this will let me transition back to the main menu without needing to use the main scene's Fader. When I'm done, it looks like this:

Image

Here's my PauseMenu script. I'll wire up the other buttons later.

Code: Select all

extends Control

@export var main_menu_scene: PackedScene

signal menu_closed

func _ready():
	$Panel/VBoxContainer/ContinueButton.grab_focus()


func _on_continue_button_pressed():
	menu_closed.emit()
	queue_free()


func _on_quit_button_pressed():
	$Fader.fade_out()
	await $Fader.fade_finished
	get_tree().change_scene_to_packed(main_menu_scene)
Simple enough, right? Wrong! get_tree().change_scene works great on the first go, but if you ever try to change into a scene that was previously loaded, nothing happens. How do we get around this? It took me probably an hour to figure this out.

Rather than keeping the PackedScene from the start, we need to load it as we use it:

Code: Select all

func _on_quit_button_pressed():
	$Fader.fade_out()
	await $Fader.fade_finished
	var main_menu_scene = load("res://scenes/menu/main_menu.tscn")
	get_tree().change_scene_to_packed(main_menu_scene)
It's still fine to use our @export var for scenes we want to instance, but it won't work for change_scene calls, and I still don't really know why. We have to do this in the MainMenu script as well (see the start of this update).

Let's wrap up by allowing the user to open PauseMenu from the Dungeon script:

Code: Select all

func _process(delta):
	if (!player_can_act):
		return
	
	if (Input.is_action_just_pressed("open_menu")):
		var pause_menu = pause_menu_scene.instantiate()
		pause_menu.connect("menu_closed", _on_close_pause_menu)
		player_can_act = false
		add_child(pause_menu)
		return
	#...


func _on_close_pause_menu():
	player_can_act = true
We've still got a couple of problems:
  • There's no way to open the menu using the mouse. We haven't enabled mouse controls for moving around the dungeon, either, so I'm not too fussed about this yet.
  • I wired up ui_cancel and open_menu as shortcuts to the Continue button. On the controller, those are different buttons, but on the keyboard, Esc does both. But open_menu is also the input to open the menu, so it reopens immediately. I'll have to see if there's a clever way to solve this or if I just need to put a timeout on how quickly the menu can reopen.
But those will have to wait for another day. This has already been a lot.
User avatar
Mogri
Super Slime
Posts: 4678
Joined: Mon Oct 15, 2007 6:38 pm
Location: Austin, TX
Contact:

Re: Let's learn Godot together

Post by Mogri »

Dialogue

In video games, characters like to talk. A means of handling dialogue is a really good thing to have.

It's also surprisingly nuanced. Here are the table stakes for a dialogue system -- if you can't offer these features, you're not a serious contender:
  • Branching conversation. Pick an option from a list.
  • Localization. Bare minimum, you need to support Godot's built-in localization.
  • Basic scripting. If Johnny is happy, say X; otherwise, say Y.
That's not nothing! It's actually quite a lot, which is why I have no desire to create my own. Fortunately, there are a lot of plugins out there for dialogue in Godot.

Dialegume is a nice one in terms of the features it offers out of the box, which include character portraits, animations, and colored text. It was the first plugin I looked at. I liked the look of it, but I decided against it because the format for writing dialogue is needlessly verbose (it's XML).

I seriously considered Bonnie, which is a fork of Clyde, which was named after the Pac-Man ghost. It's got a very flexible dialogue language that is very lightweight at the same time. Bonnie only acts as an interpreter/manager; you need to provide your own UI and interface to actually display the dialogue. This is actually a good thing, as it means Bonnie doesn't exert any control over how you have to present things.

The uncreatively-named "Dialogue Manager" is my pick. It has a very similar feature set to Bonnie, but it's got just a bit more in the way of features and a lot more in the way of documentation. I think Bonnie's dialogue syntax is a bit nicer (though this one clearly shares a lot of influences), but Dialogue Manager has an in-editor syntax checker and the ability to run a test scene on a specific dialogue file. Dialogue Manager also allows you to explicitly invoke outside code, whereas Bonnie only allows you to emit specific signals.

I'm not going to get into the weeds on implementation here. Dialogue Manager has a robust series of video tutorials that walk you through the whole thing, and I don't feel any need to duplicate that effort. It gets my endorsement, and I'm sure you'll see it in action in a future post.
User avatar
Mogri
Super Slime
Posts: 4678
Joined: Mon Oct 15, 2007 6:38 pm
Location: Austin, TX
Contact:

Re: Let's learn Godot together

Post by Mogri »

Revisiting the to-do list

Here's what I set out to learn:
Mogri wrote:
  • UI stuff. Menus, dialogue, and so on.
  • Support for various control schemes. Keyboard, controller, mouse, whatever.
  • Mod support. Including localization.
  • Data-driven design. Mod support is pretty useless if you've hardcoded your entire game.
  • Save/load. How hard is it to convert the current game state to/from a file?
Here's what I've learned on these topics:

UI & Control schemes

We've spent a lot of time on the topic of UI. I'm satisfied with what I learned. Godot has a nice variety of built-in UI components that work well across control methods, which answers that topic nicely as well.


Localization

The localization documentation is good enough for me. Community add-ons such as the dialogue managers mentioned last update do a good job of integrating the built-in localization support.


Mod support

In the same way you can load("res://path/to/file") to grab something from the game files, you can load("user://path/to/file") to access something from the filesystem. Super simple!

However: I don't see that there's a way to add a localization in this way. This isn't a dealbreaker; you could set up a workflow where you expose localizations via GitHub and feed user-submitted translations back into your game. It's less convenient, of course, and it also means that mods containing text will be language-specific.


Data-driven design

If you want to split hairs, everything in Godot is data-driven. When you create a scene, it comes out of a file that looks like this. You wouldn't want to write that by hand, but it's hardly incomprehensible.

We peeked into the dialogue files last time, and I'm happy with how the add-on integrates with the Godot editor to supply syntax checking. What about arbitrary game data? Well, there's a nice-looking add-on for that called Datalogue. I haven't tried it yet, but I like how it balances structure and flexibility, and the query system means I can pretty easily allow user mods to add or override game data.


Save/load

This is one I was dreading, but Godot makes it pretty simple. The short version is:
  • Any instanced node that needs to be saved gets added to a node group. The documentation calls the group "Persist."
  • When we save, we grab all "Persist" nodes and serialize them. Each of our Persist nodes must have a "save()" function that is responsible for serializing itself. (This is the hard part, but it really boils down to figuring out which variables need to be saved and stuffing them in a dictionary.)
  • The serialized game data gets stuffed into a file. When we load, we use that data to re-instance all of those nodes and add them to the scene tree appropriately.
Now, this is great for saving an exact snapshot of an in-progress game, but eagle-eyed readers may have noticed the major limitation: it only works for instanced nodes. Then again, the documentation has you create your save file from scratch; it's not a prescribed format. This means you can alter the save file to include the details that you need.

In particular, though, the documentation's method assumes you're already in the proper top-level scene. This suggests that we might be better off avoiding get_tree().change_scene altogether in favor of a single top-level scene that persists throughout the game and instances all of the sub-scenes, except that their method also assumes that you never have a Persist node as a descendant of a Persist node!

Here's my suggestion to work around all of this -- completely untested, mind:
  • As part of each node's save() function, add a "node_type" field that indicates whether the node is an instance, the root scene, or neither. (Auto-loading singletons should be neither.)
  • When loading, load nodes in order by the length of the parent path, which should ensure that parents load before their descendants. If a node is the root scene, use get_tree().change_scene; if it's an instance, use the existing instantiation logic instead; otherwise, skip the instantiation logic.
That should do it!


What's next

That pretty much wraps up my investigation. I'm satisfied with what Godot has to offer, and maybe I'll even continue making a game.

If you have any topics you're still itching to learn about, let me know, and I might just do the legwork for you.
User avatar
Mogri
Super Slime
Posts: 4678
Joined: Mon Oct 15, 2007 6:38 pm
Location: Austin, TX
Contact:

Re: Let's learn Godot together

Post by Mogri »

Heads up: the current version of Datalogue is incompatible with Godot 4.1. I fixed it locally and threw up a PR, but since my fix might break 4.0 compatibility, it might not get merged anytime soon. You can, however, download my fixed version here.
User avatar
Bob the Hamster
Lord of the Slimes
Posts: 7684
Joined: Tue Oct 16, 2007 2:34 pm
Location: Hamster Republic (Ontario Enclave)
Contact:

Re: Let's learn Godot together

Post by Bob the Hamster »

I wonder how Godot handles compatibility breaks? Does it have any feature to do things conditionally depending on its own version?
User avatar
Mogri
Super Slime
Posts: 4678
Joined: Mon Oct 15, 2007 6:38 pm
Location: Austin, TX
Contact:

Re: Let's learn Godot together

Post by Mogri »

Good question! In this case, it's breaking because (among other reasons) Directory was replaced by DirAccess, so there's a compile-time error. If there's a way to support both versions, it would have to include a separate script depending on version, and I'm not sure if that would actually work.
lennyhome
Slime Knight
Posts: 115
Joined: Fri Feb 14, 2020 6:07 am

Re: Let's learn Godot together

Post by lennyhome »

Has anybody managed to make a playable game in Godot yet? If your computer can run Godot 4 then it can probably run Unreal, so why bother? The people behind Godot are beyond sketchy and the code base is a giant bug.
I wonder how Godot handles compatibility breaks?
It doesn't lol. If you started development with version 3.x or 4.0 you're already on your own.
Post Reply