I’m building a 2d platformer in Godot, and it’s my first time with the engine.

I’ve followed the First 2D Game tutorial as well as a few others (Brackey’s videos and some others) and have a decent grasp on building out a level as well as decent physics for how I want my player character to control.

So far, everything was made underneath one main Scene called “Game” and I’m at the step where I want to start having a Main Menu, a Level Select Menu, and logic on starting/completing a level rather than just one big “Game” Scene with a bunch of stuff I slapped together to test out the platforming.

I’d love some input on how I should be structuring my project and changing scenes around. I’ve found examples in tutorials but they all differ on some details and I’m not sure what is best. I’ve built a simple Main Menu and Level Select Menu and am able to go in and start the “Game” scene from it.

This is a big ask, I admit :).


  1. I’ve created a new main Scene titled “Main”. It’s jsut a plain Node with this script so far:
class_name Main extends Node


var main_menu_scene: PackedScene = preload("res://scenes/main_menu.tscn")

func _ready() -> void:
	add_child(main_menu_scene.instantiate())

func exit() -> void:
	get_tree().quit()

I’ve done this partly to follow an example that I found, but also due to how I want the background to work. In some Valve games, the Main Menu background is based upon the level you’re at in the campaign. I want that. If you start the game up the first time, I want the background to be the 1st level background. If you’re on world 10, I want it to remember and show that background. When the level launches, the background can stay the same as it was on the Main Menu.

I don’t have backgrounds started in at all, yet.

  1. My Main Menu scene is a PanelContainer. Above, Main loads the MainMenu scene and adds it as a child. This works but I don’t know if it’s the best way. Script:
class_name MainMenu extends PanelContainer

@onready var main_scene: Main = get_parent()
var level_select_menu_scene: PackedScene = load("res://scenes/level_select_menu.tscn")
var options_menu_scene: PackedScene = load("res://scenes/options_menu.tscn")

func _on_play_button_pressed() -> void:
	main_scene.remove_child(self)
	main_scene.add_child(level_select_menu_scene.instantiate())

func _on_options_button_pressed() -> void:
	main_scene.remove_child(self)
	main_scene.add_child(options_menu_scene.instantiate())

func _on_exit_button_pressed() -> void:
	main_scene.exit()

This lets you exit (works by calling the Main scene function) go to Options, and go to the Level Select scene as well.

  1. Next is the Level Select Menu. It’s similar to the MainMenu. Script:
class_name LevelSelectMenu extends PanelContainer


@onready var main_scene: Main = get_parent()
var main_menu_scene: PackedScene = load("res://scenes/main_menu.tscn")

func _on_back_button_pressed() -> void:
	main_scene.remove_child(self)
	main_scene.add_child(main_menu_scene.instantiate())

func _on_level_1_button_pressed() -> void:
	main_scene.remove_child(self)
	main_scene.add_child(load("res://scenes/game.tscn").instantiate())

func _on_level_2_button_pressed() -> void:
	# main_scene.remove_child(self)
	pass

Here you can go back (works) to the MainMenu, or move forward and start the “Game” scene I mentioned above. I intend to build a few levels and stick them in here in a similar fashion, replacing game.tscn. That will get me to the pre-pre-pre-alpha stage of my game and I hope to get feedback on how the platforming “feels” from there.

My thought is that the Main scene would handle the backgrounds, and always be the main parent Node in the Scene Tree. The Menus and Levels get swapped out as the child node of Main.


Questions:

Does this look ok to any of you Godot veterans?

Any code smells?

All of my scenes are in a /scenes directory. All of my scripts are in a /scripts directory. No sub-directories in either. This is quickly getting messy and I’d also love tips on folder structure.

  • Jayjader
    link
    fedilink
    English
    arrow-up
    8
    ·
    20 days ago

    Instead of

    main_scene.remove_child(self)
    

    I would “simply” do

    queue_free() # you can also write `self.queue_free()` if you really prefer it
    

    I would also emit custom signals ( play_requested, options_requested, level_requested(level_number), etc) in your “child” scenes and hook them up to functions in the Main scene that would take care of instantiating the required scene and doing a add_child(the_scene). This would mean moving most of your load() calls (and variables storing their results) into your Main scene.

    There is an adage that is often repeated in the Godot community; “call down (but/and) signal up”. Here’s some more in-depth explanations of why this is generally considered a best/better practice:

    As for file/folder structure, what tends to work for me is to group things by how often they are used together, more than grouping them by what they are. In your project, I could for example have a folder containing the scene and scripts for the level select, another one containing the scene(s) and script(s) for level 1, and so on.

    • spacemanspiffy@lemmy.worldOP
      link
      fedilink
      English
      arrow-up
      3
      ·
      edit-2
      20 days ago

      This is great. Really appreciate you reading my post and writing all of this up.

      Reading some of your linked stuff now.

      Call down/signal up sounds a lot like my time with Vue.js :).

  • KoboldCoterie@pawb.social
    link
    fedilink
    English
    arrow-up
    5
    ·
    edit-2
    20 days ago

    I wouldn’t call myself a veteran by any sense, and it’d be helpful if you posted a screenshot of your scene tree so we can see what you’re working with. A few notes:

    Generally speaking it’s recommended to keep scenes and their associated scripts in the same directory. You can subdivide your /scenes directory further, as well, to organize things, but really your structure is up to you and whatever works for you is fine.

    Consider making one “Main Menu” scene, and adding all of the sub-menus as children. Rather than adding and removing children, you can just hide / show them as needed. The increased resource usage to keep them all in the scene tree at once is extremely minimal and should have exactly zero impact. This also makes it easier to, for example, show the menu over a paused level if the user presses Escape (or whatever button you use), and it makes it easier to assign references to the sub-menus, using exported variables.

    (If you add @export before a variable declaration, the variable shows up in the Godot editor when you select the node the script is attached to, and you can assign values there. This creates dynamic references to e.g. scenes, and if you move the scene in your folder structure or the scene tree later, it will automatically be updated. With the ‘hard coded’ paths you’re using currently, you’ll need to manually go and update those references any time your file structure changes.)

    I’d use a simple state machine to manage this. A very basic implementation is to have a “states” enum with your various states (e.g. MAIN_MENU, OPTIONS, LEVEL_SELECT, etc.), and a “current_state” variable. Make a ‘change_state’ function that has 3 components:

    1. ‘Exit’ the current state. This is where you run any code that needs to happen when a state ends - for example, when you exit Options, you want to hide the Options menu node, etc.
    2. Change the state, where you actually set the state variable to the new value
    3. ‘Enter’ the new state, where you include code related to the new state starting - for example, for Options, this is where you Show the window (and maybe run an initialization function inside the options menu script, if needed).

    It’s a very compact way to keep all of the code in one place and make it very easy to add new states or functionality later. For the project as a whole, you might want to consider a more robust state machine if it warrants it.

    Edit: here’s a quick and dirty example of what a (very basic) state machine might look like for a single thing like this:

    enum States {
    	NONE,
    	MAIN_MENU,
    	OPTIONS,
    	LEVEL_SELECT
    }
    var current_state = States.MAIN_MENU
    
    func change_state(new_state:int) -> void:
    	# Exit state
    	match current_state:
    		States.MAIN_MENU:
    			main_menu.hide()
    		States.OPTIONS:
    			options.hide()
    			options.save_options_to_disk()
    		States.LEVEL_SELECT:
    			level_select.hide()
    			set_next_level(level_select.chosen_level)
    	
    	# Set state
    	current_state = new_state
    	
    	# Enter state
    	match current_state:
    		States.MAIN_MENU:
    			main_menu.show()
    		States.OPTIONS:
    			options.show()
    		States.LEVEL_SELECT:
    			level_select.show()
    

    You can use a similar state machine to handle (for example) player actions - have states for Jumping, Running, Walking, Standing, Climbing, etc., and use the state machine to both start and stop animations as needed, and to gate input (to prevent, for instance, jumping while already jumping.)

    Edit #2: Without seeing the rest of your code it’s hard to say if this applies, but just as a note: If you’re removing scenes from the scene tree, then re-instantiating them the next time you need them, you’re potentially creating a memory leak. You either want to store references to the scenes after you remove them, and just re-add them to the scene tree, or you want to free them (via queue_free()) which actually removes them entirely and frees up the memory they were using, then reinstantiate them later. (Removing children doesn’t actually remove the scene from memory.)

    • spacemanspiffy@lemmy.worldOP
      link
      fedilink
      English
      arrow-up
      5
      ·
      20 days ago

      This is immensely helpful, thank you!

      I do indeed want a pause screen with similar behavior so that tidbit is perfect.

  • unit327@lemmy.zip
    link
    fedilink
    arrow-up
    3
    ·
    17 days ago

    Honestly I wouldn’t sweat too much about your scene structure. Just make the thing and when something starts becoming painful to use, then it might be time to reorganise things.

    The incremental search feature on the file system tab in godot is a godsend for finding things in messy projects. Which all projects end up being to one degree or another.

    If you want some sort of example structure to follow, you could take a look at how the godot-xr-tools / godot-xr-template stuff is done, though it might be overkill. Obviously that is for VR stuff but most of the structure is general.

    https://github.com/GodotVR/godot-xr-template/

  • Björn@swg-empire.de
    link
    fedilink
    arrow-up
    3
    ·
    20 days ago

    Sorry, I didn’t read through all of your code. But the beauty of Godot is that you can take any node and its children and save it as its own scene to be reused in other scenes. That makes refactoring easier when you discover later that you actually want to set up everything differently.

  • Daisy (she/her)@lemmy.ml
    link
    fedilink
    arrow-up
    2
    ·
    19 days ago

    Hi, I have a few games under my belt now, but by no means an expert. I am quite fond of the change_scene_to_packed method on the scene tree. It clears the current main scene and replaces it with your new scene while maintaining any singletons (autoloaded scenes). Feel free to look at how I did it for last years GMTK game jam: https://git.techiedamien.xyz/TechieDamien/Ouroboros/src/branch/master/Scripts/main_menu.gd

    You can also clone it and edit it to see how it works. Feel free to ask me any questions about it.