Learn 3D Graphics

Game Development with Godot + Blender

Learn the basics of creating a 3D game in the Godot game engine with assets created in Blender.

Godot Workspace, Concepts & Projects

Godot is an open source Game Engine designed by game developers for game developers. It is free to use - no matter how much money you make from your games. No accounts or other apps are needed!

Godot is already installed on the lab's computers, so there's no special setup necessary. Intallation is really easy though, if you choose to get Godot at home.

Godot uses it's own scripting language - called GDScript by default. This is an object based programming language - again created by game developers for game developers. While there is a learning curve (especially if you are new to programming), it's about as easy as it gets due to the high level of integration with Godot.

It is possible to use other common coding languages such as C#, but it requires additional setup.

Scene Tree

The Scene Tree (top left), displays all the assets in the specific scene in a "node" format. Nodes are the smallest building blocks that make up the scene.

The tree shows the parent/child relationship between different nodes. Making a change to a parent always impacts the child. At the same time, children can be adjusted independently from the parent.

New nodes can be created/added/removed from the Scene Tree

File System

The File System (bottom left) gives access to all of the assets/files for the project.

The Inspector

The Inspector (right side) shows the properties for the active node. The Node tab behind it also gives access to signals and groups.

Viewport

The Viewport has mulitple modes: 2D View, 3D View, Script Editor, and an Asset Library. Most often we will go back and forth between the 3D View, where we can see the virtual world of our game and the Script Editor where we will code different behaviors for our assets.

Bottom Panel

  1. The very first time opening Godot, it will ask if you want to load a project from the asset library, since there are no available projects - choose "Cancel" Image is missing
  2. On the top left, click on "New Project" Image is missing
  3. Select a location to save the project:
    1. Click on Browse, and choose an easy to find directory - ie. Documents Image is missing
    2. Click on "Create Folder" and make a folder for the game (the project name will automatically update to the folder name) Image is missing
    3. Click on "Choose Current Folder" Image is missing
  4. Choose a Renderer (this can be changed at any time later on, but may require changes to scenes)
    • Forward+ - for Desktop, great for complex scenes & advanced graphics though slow for simple scenes (the tutorial is based upon this Renderer)
    • Mobile - for Desktop/Mobile, great for simple scenes but has more graphics limitations & is not as strong for complex scenes
    • Compatibility - for Desktop/Mobile/Web, best for simple scenes and old/low-end devices, worst for advanced graphics
  5. Click on "Create & Edit" Image is missing

Setting up proper organization from the start will allow for:

  • Easily finding assets (especially for complex games or after a long time away from the game)
  • Easier collaboration between team members
  • Easily sharing or reusing parts of the game

There are many ways to organize a project. This way should scale up reasonably well as your games get more complex.

Creating Folders in the File System panel

  1. Right-click in the File System panel and choose "New Folder":
    • For folders at the top level, right-click in the empty space of the panel (or on res://) Example image is missing
    • For sub-folders, right-click on an existing folder Example image is missing
  2. Give each folder a descriptive name, without spaces Example image is missing
  3. Click on "Ok"

Starting Folders

  1. Create folders for: initial organization: the "Player", "Assets", "UI", and "Levels":
    1. Player
      • (if planning a multi-player game, this would be Players & each player would have a sub-folder)
    2. Assets
      • (for re-usable assets that could be used in multiple scenes/levels; could include sub-folders for different categories of assets)
    3. UI
      • (for the interface, including any fonts)
    4. Levels
      • (each level might have its own sub-folder containing any art, scripts, or other assets unique to that level)
    Example image is missing
  1. In the top menu, go to "Editor" → "Editor Settings" Image is missing
  2. In the "General" tab, under "FileSystem", go to "Import"
  3. Next to "Blender 3 Path", type: /Applications/Blender.app/Contents/MacOS/ and then press return/enter Image is missing
  4. Choose "Save & Restart" at the bottom left

Models from Blender can be added to Godot to use as parts of the game. Any animated actions (even shape key animations) that have pushed to the NLA Editor, will be available to use in Godot with the help of a little scripting.

Depending on the specific materials, they might might automatically be applied on the Godot end, or there might be additional setup. Some developers prefer to work with materials directly in Godot while others prefer to bake the Blender materials into image textures.

The standard format for models in Godot is glb/glTF. We'll actually just use the .blend file itself (it gets converted to glb automatically by Godot) so that any saved changes on the Blender side will automatically update in Godot.

  1. Create (and rig if necessary) any objects (player, obstacles & etc.) like normal — with each object in its own file
  2. Keep the models at the center of the grid/world origin
  3. Create any animations as different actions:
    1. Actions should be "in place" (ie. walking or jumping in place) and not change the model's location
    2. Actions should be named descriptively (ie. run, jump, idle or etc.) and pushed to the NLA editor (Non-Linear Animation)
      • For normal keyframes, use the Action Editor mode of the Dope Sheet
      • For shape keys, use the Shape Key Editor mode of the Dope Sheet
  4. Apply any Transforms (Ctrl+A)
  5. Apply modifiers
  6. Preparing Materials for Export:
    1. The Principled BSDF Shader will automatically be applied to the object in Godot - no additional changes are needed (this is currently the only material that works this way)
    2. For any other materials, they need to be "baked" into an image texture:
      1. Create a U/V map of the mesh (if one hasn't been created already)
        1. In Edit mode, select the entire mesh with "A"
        2. Press "U" and choose one of the unwrap optons (Smart UV Project is recommended)
        3. In the "UV Editing" workspace, adjust the map
      2. Create an image to bake the material onto:
        1. In the "UV Editing" workspace, click on "+ New" at the top of the "UV Editor" panel
        2. Give the image a name and choose "OK"
        3. Save the image using the "Image" menu → "Save" at the top of the UV Editor (save the image directly into the Assets/Materials folder for the game project)
      3. "Connect" the image to each material (this step can get much more complicated depending on the complexity of the material — there are some great video tutorials if you need a better result):
        1. In the "Shading" workspace, add an image texture node to the active material (Shift+A → Texture → Image Texture)
        2. Place the node to the side (not directly connected to the material nodes)
        3. Open the blank, saved image Image is missing
        4. In the "Materials" tab of the properties panel, switch to the next material & repeat the previous steps so that the empty saved image is to the side of each material's nodes Image is missing
        5. Repeat for any additional materials
      4. Bake the materials onto the image:
        1. In the properties panel, switch to the "Render" tab of the properties panel, set the render engine to "Cycles"
        2. Optionally, under "Light Paths", check "Fast GI Approximation" to use ambient lighting Image is missing
        3. The AO distance can be increased as needed
        4. Still in the "Render" tab, expand the "Bake" subsection and click on "Bake" Image is missing
      5. Switch the material(s) to use that baked image texture rather than the original color(s)
    3. Save the file in the game folder:
      1. In the File menu, choose "Save" — or "Save As" if the file has already been saved
      2. In the Save dialog, select a location:
        1. Use the sidebar of the Save dialog to navigate to your game folder
        2. Once the game directory is open, create and/or open the relevant directory:
          • Player — for the player model
          • Assets — for assets that will be reused in multiple levels (these could be organized in further sub-folders such as "Obstacles", "Plants" or etc.)
          • Level folders (ie. Levels/Level1) — for assets unique to that Level (if there are a lot of models unique to level, they might be placed in an "Art" or "Models" sub-folder)
      3. Choose a descriptive name - ie. playermodel.blend
      4. Click on "Save Blender File"

GDScript is the default scripting language in Godot. Here's some basic vocabulary and syntax information to get started. More specifics will follow later.

  • Class — a category or collection of related parts/objects that act as a blueprint to help create your own version in the related category
    • Extending the class gives the script access to methods, functions, and properties from that class or other classes inherited by the class
  • Add # in front of a line to write comments:
    • For organization/clarity
    • For reminders of what the code does or how it works
    • To temporarily disable lines of code while troubleshooting
  • Methods - default procedures/actions
  • Properties - attributes
  • Functions - lists of directions, each with a specific and unique name/identifier:
    • Functions begin with:
      
      # Functions might be built-in or might be completely custom
      # Parameters are temporary variables(changeable, custom data) specific to a function that pass information to that function
      # If there are multiple parameters, they are listed with commas between (param1, param2)
      # Some functions have no parameters and the () will be left empty
      
      func function_name(parameter):
      
      
    • Use the tab key to indent any lines inside the function:
      
      # Indenting shows the hierarchy - or what is part of the same block of code
      
      func function_name(parameter):
      	some code telling the function what to do will go here...
      	any code at this same indent level is part of the function...
      
      
  • Variables are used to set changeable data in a script:
    • Variables can be named anything, but should use _ instead of a space when using a name wth multiple words
    • Global variables are written outside of functions and can be used by any following function
    • Local variables are written inside a function and can only be used by that function
    • Variables have a variety of types:
      • string - text (ie. string : 'some name')
      • float - decimal numbers (ie. float : '1.0')
      • int - whole numbers (ie. int : '1')
      • str - converts numbers to a string
    • Variables are written as:
      • var variable_name = value (this syntax provides no hints if the wrong type is used elsewhere in the code)
      • var variable_name : variable_type = value (if the wrong type is used with this variable elsewhere in the code, this gives a hint about what is wrong)
      • var variable_name : = value (Godot guesses the type based on the value and provides hints like above when the wrong type is used)
    • Adding @export in front of the variable makes it public - so that the variable can be seen and changed in Godot's Inspector for easy testing purposes
  1. Open your project in Godot — this should open an empty scene (or press CMD+N to start a new scene if needed)
  2. In the empty scene, click on "+ Other Node" in the Scene Tree example image is missing
  3. Search for "character" and choose "CharacterBody3D" (this node is great for players as it can be setup to move/collide and you have control over the movement) example image is missing
  4. Double-click on the CharacterBody3D node and rename as Player example image is missing
  5. Save the scene with CMD+S or Scene → Save
    1. Double-click on the Player folder to select it example image is missing
    2. Rename if necessary (the scene will automatically be named based on the parent node - ie. player)
    3. Click on "Save" example image is missing
  6. Add the ability to rotate properly with future animations:
    1. Right-click on the node and choose "Add Child Node" from the context menu example image is missing
    2. Search for "node3" and choose "Node3D" example image is missing
    3. Double-click on the Node3D node to rename it "Pivot" (it is helpful to name things as you are going to make the function more clear) example image is missing
  7. Import the player model by itself:
    1. In the FileSystem, expand the "Player" directory (if the player model hasn't already been saved in this directory, move it there using Finder)
    2. Double-click on the player.blend file to access the Import dialog example image is missing
    3. Use the Scene tab to select the different assets in file
    4. On the right sidebar, check "Skip Import" for any models/assets that aren't part of the player — ie. references, lights, camera or etc. example image is missing
    5. Adjust other settings if needed
    6. Click on "Reimport" at the bottom of the window example image is missing
  8. Add the player model as a child of the Pivot node:
    1. Drag the player blend file from the FileSystem panel over "Pivot" in the SceneTree and release to make the player model Pivot's child example image is missing
    2. Alternately, create a placeholder object such as a cylinder to use temporarily as a player object (New Child Node → search for CSGCylinder3D ) and then replace it later in the process. It is easy to delete the placeholder and then add in the actual player model in the same place. In that case, the model's exact size/position and the collision shape that gets added in the next step, may need adjustment
  9. Notice the warning icon to the right of the Player node - whenever you see this, it lets you know that something is missing or incorrect in the node setup - clicking on it brings up a hint about how to fix the problem - in this case, the node needs a collider
  10. Fix the warning by setting up a collision shape:
    1. Right-click on the Player node and choose "Add Child Node" from the context menu example image is missing
    2. Search for "collision"
    3. Choose "CollisionShape3D" and click "Create" example image is missing
    4. Notice another warning - in this case, the collision shape needs a shape set
    5. In the "Inspector", next to shape, click on empty then set a shape that makes sense for your model: example image is missing
      • Capsule is often used for players - it works well for stairs but easily falls off the edge of platforms
      • Cubes aren't great with stairs but are less like to fall off the edge of a platform
      • Other shapes have similar strengths and weaknesses - experiment to see what works best for your model and desired gameplay
      • Keep in mind that you can add more than one collision object to the player to improve the collision if needed
    6. Correct the size/position of the shape to fit the player (sometimes it works better to go slightly smaller):
      1. Tap on the name of the shape (ie. CapsuleShape3D or etc.) to access/change the size settings example image is missing (or just click and drag on the red circles of the collision shape in the 3D window)
      2. Use the Transform → Position settings example image is missing (or use the triangular handles of the Transform widget) to adjust the location as needed
  11. Move the Player node to rest on top of the grid:
    1. In the Scene panel, select the Player node
    2. In the 3D window, change the view to look at the front or side by clicking on either X or Z in the View widget
    3. Use the Transform → Position settings (or use the triangular green handle of the Transform widget) to adjust the Y location as needed
    4. Keeping the Collision shape in particular just above the grid will prevent the player from falling through the floor/ground later on
    example image is missing
  12. Add a "player" group to the root "Player" node for easy identification of the player in future interactions:
    1. Make sure the main Player node is active
    2. Switch the right sidebar to the "Node" tab
    3. With "Groups" active, type "player" and then click on "Add" Adding a group called player image is missing
    4. Notice the group indicator next to the node name Group Indicator image is missing
  13. Save the Scene again with CMD+S

A great feature in Godot is the ability to easily choose the input for each game. There can be multiple types of input setup for the same action.

  1. In the Project menu, go to "Project Settings" example image is missing
  2. Click on the "Input Map" tab
  3. Create new input actions for "move_forward", "move_back", "turn_left", "turn_right", and "jump":
    (the names themselves could be anything, but generally it's helpful to be descriptive of the action and not include any spaces — whatever the names are, they'll be used in the movement script later on):
    1. For each action, in the "Add New Action" field, type the name of the action & then press return/enter example image is missing
    2. Link each action name to specific inputs by clicking on the + icon to the far right of the input name and then: example image is missing
      1. For keyboard inputs, press the key that should match the command (ie. W for move_forward) and then choose "OK"
      2. To set a joypad input, click on the + button, expand "Joypad Axes" or "Joypad Button", click on an axis or button option, and then choose "OK" (ie. Joypad Axis 1 for move_forward)
    3. Repeat as needed for additional keyboard inputs (ie. move_forward might have W, the up arrow, and Joypad Axis 1 all set as inputs
    4. At minimum, set:
      1. Forwards/backwards movement: up/down arrow keys and W/S
      2. Turning left/right: left/right arrow keys and A/D
      3. Jump: space
    example image is missing
  4. Close the Project Settings window

Keep in mind that the input map alone won't make your player move. Scripting needs to be added to control how the player moves based on the actions/input.

This script sets up basic tank controls (where forwards/backwards input controls forwards/backwards movement and left/right input controls the direction the player faces).

  1. Create the Script file:
    1. In the Scene panel, right-click on the Player node and choose "Attach Script" example image is missing
    2. Adjust the settings if necessary
      1. By default this saves in the same location as the player object
      2. By default the name is the same as the node name
      3. Make sure "Template" is set to "Empty"
    3. Click on "Create" example image is missing
  2. Looking in the Script view of the central panel, there should be a nearly empty script with the line: example image is missing
    
    # extend gives access to any properties, methods, or functions that are available to the class or inherited by the class
    # CharacterBody3D is the class of the CharacterBody3D node
    extends CharacterBody3D
    
    
  3. Add public global variables for the movement speed and turning speed:
    
    extends CharacterBody3D
    
    # Global variables with type hinting - since these are written with decimals, the type is float # The variables could be named anything, but need to be written the same way wherever the variable is used # These can be accessed by any functions that follow & due to @export are visible in the Inspector @export var move_speed : float = 14.0 @export var turn_speed : float = 4.0
  4. Add a global variable to describe velocity (speed+direction)
    
    @export var turn_speed : float = 4.0
    
    # Vector3 is a default class which gives coordinates for the X, Y, and Z axi # This sets the initial velocity value to 0 for each axi var target_velocity : Vector3 = Vector3.ZERO
  5. Add the start of a movement function:
    
    var target_velocity : Vector3 = Vector3.ZERO
    
    # _physics_process is a default function for working with physics # Using delta as a parameter will help keep speeds consistent across different devices or operating systems func _physics_process(delta):
  6. Add local variables for movement and turn direction inside the function:
    
    func _physics_process(delta):
    # Use the tab key to indent blocks of code to show the hierarchy
    # Since this variable is indented, it is inside the physics function, making it local
    # This will represent the direction for all of Vector3's axi, with a starting value of 0, type is Vector3
    	var move_direction : Vector3 = Vector3.ZERO
    # This sets another local variable that will represent the direction of the player rotation
    	var turn_direction : float = 0.0
    
    
  7. Link the input to the direction variables:
    
    	var turn_direction : float = 0.0
    
    # This automatically sets a positive value (1.0) for the first input and a negative value (-1.0) for the second input # The input names must be the same as the ones set in the Input Map # Later this will help determine the direction of movement or rotation for each key/input # get_action_strength is a method of the Input class # This only changes the Z vector for the movement direction move_direction.z = Input.get_action_strength("move_back") - Input.get_action_strength("move_forward") turn_direction = Input.get_action_strength("turn_left") - Input.get_action_strength("turn_right")
  8. Make the player turn along the Y axis (this is the vertical direction):
    
    	turn_direction = Input.get_action_strength("turn_left") - Input.get_action_strength("turn_right")
    
    # rotation_degrees is a default property of Vector3 # The product of the multiplication operation gets added to the object's current rotation # Clockwise vs. counter-clockwise is determined by the positive/negative of turn_direction # turn_speed determines how quickly the rotation happens rotation_degrees.y += turn_direction * turn_speed
  9. Set the target velocity:
    
    	rotation_degrees.y += turn_direction * turn_speed
    
    # This changes the value of the target_velocity variable to the product of the multiplication operation # The backwards/forwards direction is determined by the positive/negative of move_direction # globel_transform and basis are properties of the class Node3D and work together to share the rotation of an object # without them, the object can't move and rotate at the same time; speed should be obvious ;) target_velocity = move_direction.z * global_tranform.basis.z * move_speed #This sets the velocity (a default property of the Vector3 class) to match the value for target_velocity velocity = target_velocity
  10. Make velocity move the player:
    
    	velocity = target_velocity
    
    # This allows velocity to actually move our player - and allows for collisions with other CharacterBody3D or RigidBody objects # This is a default method of CharacterBody3D move_and_slide()

We can't test the script until our player has been instantiated in a scene for our level — we need a surface to move along. This will happen in the next tab for the "Main" scene/level. At that point, you can preview the game and make adjustments to the speed variables as needed.

This adds player gravity and a jump to the player movement script above. While it is possible to do a more simple jump script with fewer variables, this gives much more predictable control.

  1. Add global, public variables for the jump & amount of gravity
    
    @export var turn_speed : float = 4.0
    
    # These should be added towards the top of the script, after the variables for speed # To represent how high the jump gets @export var jump_height : float = 100.0 # To represent the time the jump takes while ascending @export var time_up : float = 0.9 # To represent the time the jump takes while descending @export var time_down : float = 0.6 # To help control an arcing rotation for the player while jumping # Lower numbers will make a more dramatic rotation later while higher numbers will be more subtle @export var jump_rotation : float = 10.0
  2. Set variables for the jump's velocity and gravity impact:
    
    @export var jump_rotation : float = 10.0
    
    # Global variables that are ready to be accessed for the jump # Using the variables for jump height & time to help determine the velocity @onready var jump_velocity : float = (2.0 * jump_height) / time_up # To represent when gravity has more impact, using the result of the formula using the jump height, the jump's upward time and a negative number @onready var high_gravity_impact : float = (-2.0 * jump_height) / (time_up * time_up) # To represent when gravity has less impact, using the result of the formula using the jump height, the jump's downward time and a negative number @onready var low_gravity_impact : float = (-2.0 * jump_height) / (time_down * time_down)
    var target_velocity : Vector3 = Vector3.ZERO
  3. Begin setting up vertical gravity in the physics processing function:
    
    	target_velocity = move_direction.z * global_transform.basis.z * move_speed
    
    # This should be inside the _physics_process function, directly before velocity = target_velocity # The gravity impact variable is multiplied by delta and then subtracted from the Y target_velocity variable # The result is the Y velocity decreases differing amounts depending upon a custom function for gravity's impact that will be added later on # Including the delta parameter makes sure the jump will be consistent across different OS or frame rates target_velocity.y += gravity_impact() * delta
    velocity = target_velocity
  4. Directly afterwards, still before velocity = target_velocity, set the jump input to change the Y velocity:
    
    	target_velocity.y += gravity_impact() * delta
    
    # If statements make code run based upon conditions being met # is_on_floor is a method of CharacterBody3D and is_action_just_pressed is a method of the Input class # In this case, if the player is colliding with the floor/ground and the jump input is pressed, then the Y velocity changes to the jump_velocity variable # Notice target_velocity.y is indented to show it is part of the if statement if is_on_floor and Input.is_action_just_pressed("jump"): target_velocity.y += jump_velocity
    velocity = target_velocity
  5. Set a new and custom function to setup the jump rotation:
    
    	move_and_slide()
    
    # This is a new function, placed after the end of the _physics_process function, so it has no tabbing/indent at the start of the function
    # Keep in mind that the exact order of the custom functions doesn't matter
    func set_jump_rotation(): # If the player is in the air (not on the floor/ground) AND there is negative Y velocity if not is_on_floor() and velocity.y < 0.0: # Subtract from the Pivot node's X rotation (default property) with the formula below # When the player is falling, the jump rotation will angle downwards # Use a $ next to the name to make changes to other nodes in the scene $Pivot.rotation.x -= PI / (-jump_rotation / 3.5) * velocity.y / jump_velocity # Elif is used when there are other possiblities beyond the statement's answer being just true or false # If the previous is not true, and instead the player is not on the ground (ie. in the air) with positive Y velocity elif not is_on_floor() and velocity.y > 0.0: # Add to the Pivot node's X rotation with the formula below # When there is upwards velocity, the player will tilt back # Since the upwards velocity is set for a longer time, the rotation should look more gradual $Pivot.rotation.x += PI / jump_rotation * velocity.y / jump_velocity # Or Else, neither of the other options are true (the player must be on the floor/ground) else: # When on the ground, the Pivot node's x rotation goes back to 0 $Pivot.rotation.x = 0.0
  6. Call the jump rotation function in the physics processing function, just after move_and_slide:
    
    	move_and_slide()
    
    # This calls the jump_rotation function set_jump_rotation()
    func set_jump_rotation():
  7. Add an entirely new and custom function to control the impact of gravity:
    
    		$Pivot.rotation.x = 0.0
    
    # This is a new function, so it has no tabbing/indent at the start of the function # This can be placed after set_jump_rotation() which is at the end of the _physics_process function # Custom functions can use any name, as long as they are written the same way wherever they are accessed by the script func gravity_impact(): # This IF Else statement determines when gravity has more or less impact due to the vertical velocity # If the Y velocity is less than 0, share the high_gravity_impact variable # In this case, the player is falling so gravity should have a high impact if target_velocity.y < 0.0: return high_gravity_impact # Otherwise, when the Y velocity is not less than 0, share the variable representing a smaller gravity impact else: return low_gravity_impact
  8. Save the scene with CMD+S (or Scene → Save Scene)
    • The scene will automatically be named with the name of the first node, though it can be changed
    • The location will be the Project folder by default — for simple games this is fine, though it can also be helpful to make a new folder to keep reusable assets like the player

Again, we can't test the script until our player has been instantiated in a scene for our level. At that point, you may want to adjust the values for the global, public variables.

Levels Setup

  1. Make a sub-folder for the first/main level:
    1. Right-click on the "Levels" folder in the FileSystem Panel
    2. Under "+ Create New", choose "Folder" Example from context menu is missing
    3. Give the folder a descriptive name, ie. Level1 Example naming dialog is missing
    4. Choose "Ok" to make the folder
  2. From the 3D View, make a new scene with CMD+N (or Scene → New Scene)
  3. Add a "Node" node
    1. Click on "+ Other Node" in the Scene Tree Example from context menu is missing
    2. Search for "node" and choose it Example from context menu is missing
    3. Double-click on the node to rename it to Level 1 Example from context menu is missing
  4. Save the Scene with CMD+S:
    1. Make sure to select the "Level1" directory for the location
    2. Leave the default name - it should be "level_1.tscn"
    Example from context menu is missing
  5. Give the level a ground:
    1. In the Scene Tree panel, select the "Level 1" node
    2. Click the "+" icon at the top of the SceneTree Example from context menu is missing
    3. Search for and add a "StaticBody3D" node (this node in combination with a collider shape stops the player from falling through floors or going through walls) Example from context menu is missing
    4. Rename the "StaticBody3D" node to a descriptive name like "Ground" Example from context menu is missing
    5. Add a model for the ground:
      1. From Blender:
        1. If you have made a ground/environment for the game in Blender, find it in the FileSystem panel (if it's not already there, use Finder to move the model into the Assets folder if it will be reused, or the level 1 folder if it is just for that level)
        2. To disable lights, cameras or etc., double-click on the model and adjust the Import settings as needed before clicking on "Reimport"
        3. Drag and drop the Blend file over the "Ground" node Example from context menu is missing
        4. In the SceneTree, right-click on the blender model's node and check "Editable Children" towrds the bottom of the context menu Example from context menu is missing
      2. If not using a blend file, create a simple platform directly in Godot:
        1. With the "Ground" node selected, click the "+" icon to to search for and add a "MeshInstance3D" node Example from context menu is missing
        2. In the "Inspector" panel, click on the "empty" field next to "Mesh" and choose "New BoxMesh" Example from context menu is missing
        3. Click on the box icon next to "Mesh" to toggle open and change the size settings Example from context menu is missing
          1. Y — vertical axis in Godot, set to 2 as a starting point
          2. X & Z — set to 80 as a starting point
          Example from context menu is missing
    6. Adjust the "Ground" node's location to line up with the X/Z axi:
      1. Switch to a side view by clicking X or Z in the 3D window's view widget Example from context menu is missing
      2. In the SceneTree select the "Ground" node
      3. In select mode, drag the green handle of the move widget to reposition verticall Example from context menu is missing
    7. If there will be any lighting unique to the level, follow the upcoming steps to add the lighting
    8. Repeat as needed for any additional levels, but increase the level number each time (ie. use a Level2 folder and Level 2 node for the second level)

Collisions in Godot rely on a few things - PhysicsBodies (such as CharacterBody3D or RigidBody3D), collision shapes, and collision layers/masks. A physicsbody with a collision shape, can interact with other collision shapes as long as the layers of the objects are set to interact. For each collision object you can choose a "layer" the object is located on and any "masks" layers that can interact with the object. If you don't set any layers/masks, EVERYTHING interacts, which lowers game performance.

Keep in mind that in some cases the collision shape needs to fit the model perfectly to get the right interaction. However sometimes it is helpful to deliberately make the collision shape bigger or smaller — either to control the difficulty level, or to prevent certain interactions.

  1. Layer Setup:
    1. In the menu, go to Project → Project Settings Example from the menu is missing
    2. With the "General" tab active, search for "Physics" and then in the sidebar, choose "3D Physics" under "Layer Names"
    3. Add some layer names:
      • Layer 1: Player — for the player(s)
      • Layer 2: World — for the objects that make up the world
      • Add more as needed (Enemy, Collect or etc.)
      Example is missing
    4. Close out of Project Settings
  2. Decide what sort of collision shape makes sense for the model:
    • Complex/accurate collision shapes can be important for complex items/objects that need precise interactions with other physicsbodies — such as an uneven/irregular ground, a cave or etc.
    • Simple collision shapes are best when the object is simple — like a ball or cube or some basic player models
    • Simple collision shapes can also be helpful when only part of an object needs to interact with the player — like the lower end of a tree trunk, a hurtbox, or etc.
  3. Add collision shapes to each model:
    • If only a simple collision shape is needed:
      1. With the object's main node selected (this example uses the Ground node, but with a simple platform as a mesh) selected, click the "+" icon to to search for and add a "CollisionShape3D" node Example from the add child dialog is missing
      2. Click on the "CollisionShape3D" node Example from the SceneTree is missing
      3. In the Inspector, next to shape, click on the "empty" field to choose a collision shape that suits the ground model — ie. a "New BoxShape3D" Example from the Inspector is missing
      4. Tap on the shape's icon to expand the size options
      5. Set X,Y, and Z to the same sizes as the 3DMeshInstance Example of resizing is missing
    • If using a complex form that needs a complex and accurate collision shape:
      1. Select the model's mesh node (this example also shows the ground, but with a more detailed Blender mesh)
      2. Click on the "Mesh" button in the top right of the 3D window's menu Example is missing
      3. Choose a "Create _____ Collision Sibling" option that best fits the mesh object
        • Create Trimesh Collision Sibling — most processing heavy but best detail for high complexity meshes Example is missing
        • Create Single Convex Collision Sibling — good performance for small objects Example is missing
        • Create Simplified Convex Collision Sibling — good performance but ignores small details Example is missing
        • Create Multiple Convex Collision Siblings — lower performance but better for models with medium complexity Example is missing
      4. In the SceneTree panel, drag and drop the CollisionShape3D node to make it a child of the model's parent node (for the ground this would be the StaticBody3D node called "Ground") Example is missing
  4. Assign layers and masks for each object that has a collision shape:
    1. Select the collision shape node's parent (CharacterBody3D, RigidBody3D, Area3D, or StaticBody3D nodes...) Example is missing
    2. In the Inspector, expand "Collision"
    3. Choose the relevant layer for the object (if you don't remember what the layers are names, hovering over a layer will show the name in a tooltip):
      • Player (Layer 1) — for the player object Example is missing
      • World (Layer 2) — for the objects that make up the world Example is missing
      • & etc.
    4. Add "Mask" layers if the object should interact with other objects
      • Player — in most cases, the player should have masks enabled for every other layer where there are objects — (if there are multiple players, the player layer should also be masked for the players) Example is missing
      • Environment — most of the environment probably doesn't need any masks
      • Trigger/Sensor objects (such as doors, portals, coins, lava, a goal object or etc.) — should have the player layer as a mask Example is missing
      • Enemies (atm, there aren't any in this tutorial) — should have the player, environment, and enemies layers as masks
  5. Save the Scene with CMD+S or Scene → Save

When adding other scenes into the current scene, you instantiate or make an instance of the child scene. Multiple instances of the same scene can be positioned differently and have some unique settings. However certain parts remain linked, so changes to the original scene's model, script, material, and collision shape will impact all the instances. This saves a lot of time when working with identical objects and when reusing assets across multiple levels. This is similar to "Prefabs" if you have used Unity.

  1. Setup the obstacle scenes:
    1. From the 3D view, Create a new scene with CMD+N or Scene → New Scene
    2. In the SceneTree panel, click on the "+" to search for and add a "StaticBody3D" node Find staticbody image is missing
    3. Rename the node descriptively to match the model(ie. obstacle, tree or etc...) Rename root node image is missing
    4. Save the scene in either the level folder if the obstacle is unique to that level, or if the obstacle is a shared asset, save it within an appropriate folder inside the Assets folder(ie. Assets/Obstacles, Assets/Nature or etc.) Save location image is missing
    5. Add a mesh/model for the obstacle:
      1. From Blender:
        1. If the Blender model is not already saved in that same directory, move it there using Finder
        2. Back in Godot's FileSystem panel, find the obstacle model from Blender and double-click on it to adjust the import settings as needed
        3. Drag/drop the blend file from the FileSystem, over the parent node in the SceneTree Drag/drop image is missing
      2. From Godot:
        1. With the root StaticBody3D node selected, click on the + icon at the top of the SceneTree to search for and add a MeshInstance3D node Find MeshInstance3D node image is missing
        2. In the Inspector, set the Mesh shape of your choice Shape choosing image is missing
        3. Click on the shape icon to access the size settings and change as needed Shape size image is missing
    6. Create a collision shape to fit the model, following the same steps as in the Collision section above
    7. In a side view, select the root StaticBody3D node and slide it so that the bottom of the obstacle is resting on the X/Z axis, and centered along Y Aligning image is missing
    8. Save the scene with CMD+S or Scene → Save Scene
  2. If it is helpful, make copies as a starting point for each different obstacle model:
    1. From the Scene menu, use "Save Scene As" to save a copy of the obstacle as a new scene Find MeshInstance3D node image is missing
    2. Use a new descriptive name (ie. pine_tree or corner_wall) and relevent location (Level1/Obstacles, Assets/Nature, or etc.) Save As dialog image is missing
    3. Rename the parent StaticBody3D node to match the new model Renaming image is missing
    4. Delete the old model with CMD+delete and replace it with the new Blender model
    5. Adjust or recreate the CollisionShape so that it fits the new model, as needed Updated scene image is missing
    6. In a side view, select the root StaticBody3D node and slide it so that the bottom of the obstacle is resting on the X/Z axis, and centered along Y Aligning image is missing
    7. Save and repeat as needed
  3. Back in the Level scene, instantiate the obstacles:
    1. In the SceneTree panel, click on the "+" icon to search for and add a "Node3D" node (this will act as a parent for all the obstacles — and will allow you to easily move/hide them all at once) Node3D image is missing
    2. Rename it to "Obstacles", or some other descriptive name Renaming Node3D image is missing
    3. With the "Obstacles" node selected, click on the link icon at the top of the SceneTree panel and choose one of the obstacle scenes to open/instantiate Instancing image is missing
    4. Position/scale/rotate as needed with the widget or the Inspector's Transform settings
    5. Repeat as needed for any other obstacle scenes
    6. Duplicate (CMD+D) selected obstacles and rename/position/scale/rotate as needed Duplicating image is missing
  4. Save with CMD+S or Scene → Save Scene

Global Lighting/Environment

When using a main game scene (where levels are added and removed as needed), if the sun and/or world environment are set in that main scene, they can be considered global and will apply to all the levels. The benefit is only needing to setup the sun and environment the one time. With this approach, changes can still be programmed for individual levels through scripting.

Local Lighting/Environment

Alternatively, the sun and world environment can be set locally each level scene. This way it is easy to have different looks or times of day for the different levels without a need for scripting. With enough levels, this becomes time consuming.

Other lighting, like from lamps, streetlights or etc., most often needs to be set in the levels themselves.

  1. Adding Lighting:
    1. Adding the sun:
      1. In the 3D View, click on the ⋮ menu, next to the sun and world toggle buttons at the top of the panel
      2. Adjust the sun settings as needed
      3. Select "Add Sun to Scene" — which adds the sun as a DirectionalLight3D node Sun Settings image is missing
      4. With that DirectionalLight3D node selected, in the Inspector, enable any layers the light should work on Light Layers image is missing
    2. Adding other lighting:
      1. In the SceneTree panel, choose a parent node (ie. the main parent node or etc.) click on the + add one of the Light3D nodes as a child: Find 3D light nodes image is missing
        • OmniLight3D — is a light source that emits in all directions like a candle or lightbulb; changes to location impact how the light displays OmniLight3D image is missing
        • Spotlight3D — emits a directional cone of light, like a spotlight; both rotation and location impact how the light displays Spotlight3D image is missing
        • DirectionalLight3D (this is the same as the sun from above) — is a distant light source like the sun; changing rotation changes the "time of day" but changes to location have no impact DirectionalLight3D image is missing
      2. When using multiple lights, make sure to name them descriptively
      3. In the Inspector, enable any layers the light should work on
      4. Adjust the light location/direction as necessary (keep in mind a lot of customization can be done in the Inspector)
  2. Adding the World Environment:
    1. In the 3D View, click on the ⋮ menu, next to the sun and world toggle buttons at the top of the panel
    2. Under "Preview Environment", set an initial "Sky Color"
    3. Set an initial "Ground Color"
    4. Choose "Add Environment to Scene" World Settings image is missing
    5. With the "WorldEnvironment" node selected, click on the "Environment" toggle at the top of the Inspector
    6. Adjust the sky further by clicking on the sky toggle, and then the ProceduralSkyMaterial toggle:
      • Adjust up to 5 colors for different sections of the world environment
      • Click an drag on the curves to change the spread of the sky/ground gradients
      More World Settings image is missing
    7. Customize other settings as needed

A Functional Game Menu

  1. With the 3D view active, create a new scene with CMD+N or Scene → New Scene
  2. Click on "User Interface" in the SceneTree to add an UI control node (this automatically switches to the 2D View) First UI node image is missing
  3. To Zoom In/Out in 2D, use CMD+ or CMD- or the zoom buttons in the top left of the window 2D Zoom image is missing
  4. Rename the node to "Menu" Rename first UI node image is missing
  5. Save the scene with CMD+S and choose the UI folder for the location Saving the UI scene image is missing
  6. With the Menu node selected, switch the sidebar to the Node tab and under groups, add a group called "menu" (this is going to help control menu visibility later) Add a menu group image is missing
  7. Add a node for the menu background:
    • To add a solid background color:
      1. Click on the SceneTree's "+" icon to find and add a "ColorRect" node Adding a solid background image is missing
      2. Rename the new node to "Background" Rename descriptively image is missing
      3. Click on the "Anchor" settings in the 2D Viewer's top menu Anchor settings icon image is missing
      4. Choose "Full Rect" in the bottom right corner to fill the UI screen Fill the space image is missing
      5. Pull on the green outer anchor handles to control the alignment if the screen is a different scale or the red inner handles for the size of the rectangle
        • Keep equal amounts of space on each side to keep the UI content centered
        • Expand the green anchors on one side to keep the content anchored to the opposite side
        Size and alignment handles image is missing
      6. In the Inspector, click on the color field and choose a color that suits your game aesthetic and then click out of the color picker window Choose your own color image is missing
    • To add a procedural gradient as a background:
      1. Click on the SceneTree's "+" icon to find and add a "TextRect" node Add a texture background image is missing
      2. Rename the new node to "Background" Rename descriptively image is missing
      3. Click on the "Anchor" settings in the 2D Viewer's top menu Anchor settings icon image is missing
      4. Choose "Full Rect" in the bottom right corner to fill the UI screen Set alignment to fill image is missing
      5. Pull on the green anchor handles to control the alignment if the screen is a different scale
        • Keep equal amounts of space on each side to keep the UI content centered
        • Expand the anchors on one side to keep the content anchored to the opposite side
        Size and alignment handles image is missing
      6. In the Inspector, click on the empty field next to "Texture" and choose one "NewGradientTexture2D" Add a gradient image is missing
      7. Click on the now filled gradient texture field to access the gradient settings Expand the gradient settings image is missing
      8. Click on the gradient color to access most settings: Expand the gradient settings image is missing
        • Double-click on a colorstop dial to access a colorpicker — change the color and then click outside of the picker window to exit it
        • Click on an empty spot on the bottom edge of the gradient to add a new color stop
        • Drag the colorstop dials to control color placement Expand the gradient settings image is missing
        • Under Fill, choose the gradient type Gradient type image is missing
        • Towards the top of the gradient settings, there is a large square with some small handles:
          • Adjust the square handle to change the angle Gradient angle handle image is missing
          • Adjust the diamond handle to change the origin of the gradient Gradient origin handle image is missing
        • Adjust other settings as needed, including the Expand and Stretch modes Other gradient settings image is missing
    • Images can also be added as TextureRect nodes. There are a large number of texture types that can be added — experiment if you'd like, specific directions may be added later
  8. Add a VBox to stack UI elements vertically:
    1. Select the "Background" node
    2. Click on the SceneTree's "+" icon to find and add a "VBoxContainer" node Add a Vbox image is missing
    3. Click on the anchor setting and choose an alignment - ie. Center Horizontal center anchoring image is missing
  9. Add a label for the game's name:
    1. Select the "VboxContainer" node
    2. Click on the SceneTree's "+" icon to find and add a "Label" node Add a label image is missing
    3. Double-click on the name in the SceneTree and rename it to "Title" Rename descriptively image is missing
    4. In the Inspector, under "Text" type the name of the game Set the title image is missing
    5. Underneath the text field, adjust the various type/alignment settings and etc. as needed — such as setting the text to "Uppercase" Change text options image is missing
    6. In the anchor settings, choose an alignment Set anchor image is missing
    7. Change Font settings under "Theme Overrides"
      • Fonts for the game should be saved in an UI/Fonts Folder:
        1. With Finder, in the "Go" menu, choose "Computer" Access computer directory image is missing
        2. Then go to "Macintosh HD" → "System" → Library" → "Fonts" Font directory image is missing
        3. Copy any needed fonts with CMD+C
        4. Navigate to the Project's UI folder or UI/Fonts and paste copied fonts with CMD+V Copy/paste fonts image is missing
      • Back in Godot's Inspector, under "Theme Overrides", next to "Fonts", click on the "empty" field and choose "Load" Menu to load image is missing
      • Choose a font from the UI or UI/Fonts folder
      • Next to "Font Size" increase the number of pixels - ie. 100px Adjust font override settings image is missing
      • Under "Colors", next to "Font Color", click on the color to choose a custom font color Override the font color image is missing
      • Put the title label in a group:
        1. Switch the sidebar to the "Node" tab and then click on "Groups"
        2. In the empty field, type a group name "menu-start", to represent the menu on the game's start & press "Add" to give the label this group name(this group will help control visibility in the menu later on) Adding a group image is missing
        3. When a group has been added, there is a visual indicator in the SceneTree Group indicator image is missing
  10. Add margin below the title label to keep other design elements from crowding the title:
    1. Select the VBoxContainer node
    2. Click on the SceneTree's "+" icon to find and add a "MarginContainer" node Adding margin is missing
    3. In the Inspector under "Layout", add a value for "Y" - ie. 50px (this will add vertical space between the label and the play/quit buttons) Adding vertical margin image is missing
    4. Change the Inspector view to that panel's "Node" tab and click on "Groups"
    5. Add the same "menu-start" group by typing and adding "menu-start" Adding a group image is missing
  11. Add a label for some simple directions:
    1. In the SceneTree, select the "Title" node and press CMD+D to duplicate
    2. Rename the duplicated "Title2" node to "Directions" Rename descriptively image is missing
    3. Drag/drop the node to move it below the "MarginContainer" node Reposition the directions node image is missing
    4. In the Inspector, under "Text" change the text to some directions or a challenge (ie. "Reach the goal before time runs out" or "Can you reach the goal in time?") Add directions image is missing
    5. Adjust other type settings, font-size, and anchoring as needed Adjust font settings image is missing
    6. Change the Inspector view to that panel's "Node" tab and click on "Groups"
    7. Make sure the same "menu-start" group is assigned by typing and adding "menu-start" Adding a group image is missing
  12. Add a label for a message when the game ends:
    1. In the SceneTree, select the "Directions" node and press CMD+D to duplicate
    2. Rename the duplicated "Directions2" node to "Message" Rename descriptively image is missing
    3. In the Inspector, under "Text" change the text to a temporary placeholder message (ie. ie. "Did the player win or lose?") Temporary message image is missing
    4. Adjust other type settings, font-size, and anchoring as needed Message settings image is missing
    5. Back in the "Node" tab under "Groups", delete the menu-start Group Remove the group image is missing
    6. In the empty add field, type a new group name "menu-end" (to control visibility in the menu for the game's end) & press enter to give the label this group name Add the group image is missing
  13. Add margin below the message:
    1. Select the MarginContainer node in the SceneTree and press CMD+D to Duplicate
    2. Drag/drop to move the MarginContainer2 node below the Message node Add the group image is missing
    3. In the Inspector under "Layout", change the "Y" value if needed
    4. Change the Inspector view to that panel's "Node" tab and click on "Groups"
    5. Delete any active groups Remove the group image is missing
  14. Add buttons to play or quit:
    1. Select the "VboxContainer" node
    2. Click on the SceneTree's "+" icon to find and add a "HBoxContainer" node to keep the buttons side by side Add an Hbox image is missing
    3. With the "HBoxContainer" node selected, find and add a "Button" node Add a button image is missing
    4. Rename the Node to "PlayButton" Rename descriptively image is missing
    5. In the Inspector, under "Text" type "Play" or something similar Update button text image is missing
    6. Change font, font size, and font colors for the different button states under "Theme Overrides" Update button text colors image is missing
    7. Customize the default button shape and background:
      1. Under "Styles", click on the empty field next to "Normal" and create a "New StyleBoxFlat" Adding a button style text image is missing
      2. Click on the new "StyleBoxFlat" to access the settings
      3. Choose a BG color and adjust the other settings as needed Button style settings image is missing
      4. Click on "StyleBoxFlat" to collapse the settings
    8. Customize the settings for the Hover state of the button:
      1. Drag the "StyleBoxFlat" into the empty field for "Hover" state — this creates a linked copy Linked copy image is missing
      2. Click on the down arrow to the right to access the menu and choose "Make Unique" — so any changes only impact this button state Make unique image is missing
      3. Click on the "StyleBoxFlat" to access the settings Toggle to show/hide image is missing
      4. Choose a BG color and adjust the other settings as needed
      5. Preview the current scene with "CMD+R" to see how the button interactions look
      6. When finished making customizations, click on "StyleBoxFlat" to collapse the settings
      7. Repeat for other button states as needed
    9. With the "HBoxContainer" node selected, find and add a "MarginContainer" node Add a margin container image is missing
    10. In the Inspector under "Layout", add a value for "X" - ie. 50px (this will add horizontal space between the play and quit buttons) Add horizontal margin image is missing
    11. With the "PlayButton" node active, press CMD+D to duplicate the node
    12. Rename the new button to "QuitButton" Rename descriptively image is missing
    13. In the Inspector, under "Text", type "Quit" or something similar Type quit image is missing
  15. Save the scene with "CMD+S"

What Are Signals?

Signals allow nodes to send messages to other nodes. A node emits a signal, based on something happening (a button press, collision, a timer running out, a certain score or etc.). When the signal is emitted, any nodes that are set to listen for that signal, will call a function in response.

In Godot, nodes "call down" the SceneTree to access child or descendent nodes. When a child or descendent node needs to communicate back up, signals should used instead. This is often phrased as "call down, signal up".

This tutorial creates global signals that can be emitted by any scene AND connected to any scene. This way the same signals can be reused across multiple nodes without duplicating that code unnecessarily. This also allows signals to be accessed without needing to know which node the signal is from in instanced scenes.

Keep in mind signals are often created, emitted and responded to locally within a single scene/node. Visit Godot's manual to learn more.

Creating a Global Signal Script

  1. Create a global script to use for signals:
    1. From the "Project Menu", open "Project Settings" Project Settings in the menu image is missing
    2. Switch to the "Autoload" tab
    3. To the right of "Node Name", write the name: global_signals and click on "Add" Adding an autoload image is missing
    4. In the "Create Script" pop-up, set the path to the Gameplay folder Autoload save location image is missing
    5. Set the template to "Object:Empty" and choose Create Autoload new script dialog image is missing
  2. From the FileSystem panel, find the global_signals.gd script and double-click on it to open Open the autoload script dialog image is missing
  3. Add signals in the script to play and quit the game:
    
    # extends Node gives access to any properties, methods, or functions that are available to the class or inherited by the Node class
    extends Node
    
    # Using "signal" followed by a name at the top of a script, allows a custom signal to be declared # Signal names are usually descriptive and use underscores instead of spaces # Signal for pressing a play button signal play_pressed
    # Signal for pressing a quit button signal quit_pressed
  4. Save the script with CMD+S

There can be multiple autoload scripts. Whatever they are used for, they can be accessed by any scene. In some game engines, using autoloads is considered a bad practice because of performance costs. This is not actually a problem for Godot.

Emitting Signals & the Menu Script

After a signal has been declared, it needs to be set to emit, based on something happening — in this case, a button press.

  1. Open the Menu scene
  2. Create a script:
    1. In the SceneTree panel, with the root node called "Menu" selected, press on the new script button at the top right of the panel Create a menu script image is missing
    2. In the "Create Script" pop-up, set the path to the Menu folder, if it's not already selected
    3. Set the template to "Object:Empty" & choose "Create" New menu script dialog image is missing
  3. Connect the play button to the script:
    1. In the SceneTree, select the play button
    2. Change from the Inspector tab to the "Node"
    3. Make sure "Signals" is active
    4. Under "BaseButton", double-click on "pressed()" Connect default pressed signal image is missing
    5. In the Signal Connection pop-up window, make sure the root node called "Menu" is selected, since it has the script, and then click on "Connect" Connect signal dialog image is missing
    6. A connected signal icon will display next to the button in the SceneTree Connected signal icon image is missing
    7. An empty function will be automatically created in the menu script:
    8. 
      # extends Control gives access to any properties, methods, or functions that are available to the class or inherited by the Control class
      extends Control
      
      # This function was automatically created by connecting the default button press signal func _on_play_pressed(): pass # Replace with function body.
  4. Delete the placeholder "pass" and the following comment
  5. At the same level of indenting, emit the play signal instead:
    
    func _on_play_pressed():
    # The name of the global script is written first in Pascal Case (_ is stripped out, no spaces, and each word starts with a capital letter)
    # emit_signal() is a default method of the Object class used to emit signals by name
    # the exact name of the play signal (that's been declared in the GlobalSignals script) gets added inside quotes
    	GlobalSignals.emit_signal("play_pressed")
    
    
  6. Notice the green connected signal icon in the script side too Connected signal icon image is missing
  7. Connect the quit signal and create it's function:
    1. In the SceneTree, select the quit button
    2. In the Signals tab, under "BaseButton", double-click on "pressed()" Connect the default press signal image is missing
    3. In the Signal Connection pop-up window, make sure the root node called "Menu" is selected and then click on "Connect" Connect signal dialog image is missing
    4. Connect the correct signal in the new function:
      
      	GlobalSignals.emit_signal("play_pressed")
      
      # This function was automatically created by connecting the default button press signal func _on_quit_pressed(): # Again, the name of the emitted signal should match the quit signal from the GlobalSignals script GlobalSignals.emit_signal("quit_pressed")
  8. Save the script with CMD+S

This is for the main game scene. Menus, Levels, the player and etc. will all be loaded into this main scene as they are needed and removed or hidden when they are no longer necessary. A benefit to this approach is it minimizes unnecessary code duplication across scenes. Another helpful thing is that player data (ie. health, score or etc.) automatically persists across the whole game.

Many beginners set scene changes in the levels themselves instead. This is fine for simple games but doesn't scale up as well as a game gets more complex. Player data won't automatically persist across levels, as each scene will have it's own player.

  1. From the 3D View, make a new scene with CMD+N (or Scene → New Scene)
  2. Add a "Node" node
    1. Click on "+ Other Node" in the Scene Tree
    2. Search for "Node" and choose it Add a Node node image is missing
    3. Double-click on the node to rename it to "Game" Rename Node node image is missing
  3. Save the Scene with CMD+S and choose a relevant location — such as the "Gameplay" folder Save location image is missing
  4. For games where the levels are going to share the same world environment and/or main light source, follow the earlier directions to set the environment/lighting in this main game scene — keep in mind that the world environment will override any world environments in the levels themselves
  5. Add player scenes as an instance:
    1. Make sure the "Game" node is selected
    2. In the SceneTree panel, click on the link icon and then search for the player scene in the pop-up window Instantiate the player image is missing
    3. Double-click on the player scene to add it as an instance, or press "Open"
  6. Repeat the last step to add the Menu scene as an instance Instantiate the menu image is missing
  7. Add a camera as a child of the player (so the camera follows the player):
    1. With the "Player" node selected, press the "+" icon at the top left of the SceneTree to find and add a "Camera3D" node Add the camera image is missing
    2. In the 3D View's top menubar, click on "View" to switch to one of the "2 Viewport" views Enable 2 viewports image is missing
    3. With the Camera3D node selected, in one of the views, toggle on "Camera Preview" Camera Preview image is missing
    4. Use the widget in the other window to adjust the camera's view to show the player Adjust the camera view image is missing

This sets up a script to start or quit the game from the menu scene and to hide the menu scene when it's not needed. More functionality will be added to the script in the future.

  1. In the new "Game" scene, access the SceneTree and click on the "Game" node
  2. Click on the "Script" icon to create/attach a script: Add script image is missing
    1. Adjust the settings if necessary
      1. By default this saves in the same location as the main game scene
      2. By default the name is the same as the node name
      3. Make sure "Template" is set to "Empty"
      Script dialog image is missing
    2. Click on "Create"
  3. Looking in the Script view of the central panel, there should be a nearly empty script with the line: example image is missing
    
    # extend gives access to any properties, methods, or functions that are available to the class or inherited by the Node class
    extends Node
    
    
  4. Create global variables that can be accessed anywhere in the script:
    
    extends Node
    # @export gives access in the Inspector; PackedScene is a class that is used with scenes
    # This sets a variable to represent the first level, using PackedScene as the type
    @export var first_level : PackedScene
    # @onready is used to assign variables when the node is ready but before the default _ready() function
    # $ + a path allows you to access children from the scene tree - since the player is a direct child of the root node, only the name is needed for the path
    # So this creates a variable at the start to represent the player, using the type Node, and set to the player instance
    @onready var player_instance : Node = $Player
    # Creates a variable to represent the current level as a Node, without an initial value
    var current_level : Node
    # Creates a variable to represent the next level as a String, without an initial value
    var next_level : String
    
    
  5. After the global variable, create a _ready() function and use it to connect the play/quit signals to upcoming custom functions:
    
    	var next_level : String
    
    # this default function is called when the node enters the scene tree for the first time func _ready(): # The following connects signals from the global signal script to the correct functions # Format is: Autoload_Script_Name.signal_name.connect(function_name) # Connects a play_pressed signal from the global script to a custom function to set the starting level of the game GlobalSignals.play_pressed.connect(set_starting_level) # Connects a quit_pressed signal from the global script to a custom function to quit the game GlobalSignals.quit_pressed.connect(quit_game)
  6. After the end of the ready function, create a new custom function to toggle the visibility of the menu on/off:
    
    	GlobalSignals.quit_pressed.connect(quit_game)
    
    # A custom function to turn menu visibility on/off func menu_visibility_toggle(): # Create a local variable to represent the menu node # get_tree() is a default method of the Node class that accesses the scene tree as a whole # get_first_node_in_group() is a default method of the SceneTree class that looks for the first node in a specific group # This accesses the scene tree to find the first (in this case only) node in the menu group, with the type set to node var ui_menu : Node = get_tree().get_first_node_in_group("menu") # When .visible is added to a node, it makes the node visible # ! is used for "not", as in the node is not visible # Having the visible state equal to the not visible state creates a toggle that turns visbility on/off ui_menu.visible = !ui_menu.visible
  7. After the end of the menu visibility function, create a new, custom function to set the starting level for the game:
    
    	ui_menu.visible = !ui_menu.visible
    
    # This custom function is called when the play signal is emitted func set_starting_level(): # resource_path is a default property of the Resource class that gets the path of a resource, such as a PackedScene # This sets the next level variable to the first level's path next_level = first_level.resource_path # Run a custom level changing function - the function will be created next change_level()
  8. After the end of the set_starting_level() function, create a function to change the level when other processing is done:
    
    	change_level()
    
    # The custom function to change levels, but only when ready func change_level(): # Call deferred delays the function until processing for the current frame is done, or the node is idle # This prevents issues as the game gets more complex # The name of the upcoming deferred function goes in the () call_deferred("_deferred_change_level")
  9. After the end of the change_level() function, create the deferred function to really change the level:
    
    	call_deferred("_deferred_change_level")
    
    # The deferred custom function for changing levels func _deferred_change_level(): # Set the current level variable to load and instance the next level current_level = load(next_level).instantiate() # Add the just instanced node as a child to give it a place in the scene tree (otherwise it won't be visible) add_child(current_level) # Run the custom menu visibility function to hide the menu menu_visibility_toggle()
  10. After the end of the deferred level changing function, add a quit function:
    
    	menu_visibility_toggle()
    
    # The custom quit function func quit_game(): # quit is a default method of the SceneTree class # Accessing then quiting the scene tree quits the game get_tree().quit()
  11. Save the scene with "CMD+S"
  12. Click on the root node "Game" in the SceneTree and add the first level into it's field in the Inspector:
    1. Find the level_1.tscn in the FileSystem panel Find the level image is missing
    2. Drag/drop the level scene into into the Inspector's first level field Drag/drop the level image is missing
  13. Save the scene with "CMD+S"

Fixing Issues

The game needs to be tested regularly to make sure that it functions as expected. To preview the 3D view of the game, there must be a camera in the scene. Now that our player and camera are instanced with the first level, gameplay can be tested.

Testing the Menu:

  1. Play the main scene with "CMD+R" or the play current scene button in the very top right of the window — if a dialog asks for a default scene, choose the main Game scene Script dialog image is missing
  2. When the menu opens, try pressing the "Quit" button — the preview window whould close
  3. Preview again with CMD+R and press the "Play" button — the menu should hide, level 1 should appear, and the player should be there...though there's likely a problem with the player position and/or movement.

If the menu buttons don't work:

  • Check that the signal names are consistent in the game management script, the signal script, and the menu script
  • Make sure the buttons are connected to the button press signal from the sidebar
  • Check that each buttonpress is actually recognized:
    1. Back in the menu script, inside the function for the button that doesn't work, add a message using print():
      
      func _on_play_pressed():
      # print() sends a message to the Output Console in Godot's bottom panel — it can be helpful when troubleshooting
      	print("Play button was pressed")
      
      
    2. Preview the game with "CMD+R", press the problem button, and look for the message in the Console — if the message is missing, the button probably isn't connected
  • Check that the signal was actually recieved by the connected function:
    1. In game management script, find the quit or set_starting_level function
    2. Add a message inside the function:
      
      func quit_game(): 
      # use print() to send messsages to console to help troubleshoot
      	print("Quit signal was received")
      
      
    3. Preview the game with "CMD+R", press the problem button, and look for the message in the Console — if the message is missing, the signal probably isn't connected properly
  • Check that indenting, spelling and names are correct and consistent

At this point, the menu should be visible at the start of the game, pressing play should go to the first level and hide the menu, and pressing quit should quit the game. However there is probably a big problem once you press play and the level loads...

At this stage the player likely falls upon playing for 2 reasons. First, since the player is instanced from the start, due to gravity it has been falling the entire time the menu has been open. Even if that wasn't the case, the player may or not be at the right height for the ground when the level loads. To fix the first problem, we'll pause processing of the SceneTree whenever the menu is active. Processing will resume whenever the menu is hidden.

  1. Change the processing mode of the Menu Scene:
    1. Open the "menu.tscn" file
    2. In the SceneTree panel, select the root node, called "Menu" Menu node selected image is missing
    3. In the Inspector panel, towards the bottom, expand "Process"
    4. Change the mode from "Inherit" to either "Always" or "When Paused"
      • Inherit — uses the parent node's setting
      • Always — processes all the time, even when the SceneTree is paused (use this one if you want players to be able to pause midgame and then un-pause from a future "paused" view of the menu scene)
      • When Paused — only processes when the SceneTree is paused (use this one if there will be no mid-game pausing)
      • Disabled — no processing
      Set the process type image is missing
  2. Back at the end of the main "Game" script, add a new game pausing toggle function:
    	
    	get_tree().quit()
    
    # A new custom function to turn game processing on and off # This is being added as its own function to make it easier to pause gameplay mid-game later on func game_paused_toggle(): # .paused is a default property of the SceneTree class that can be used to pause all processing in the SceneTree (except for shaders) # Beware, pausing the tree even disables buttons and keyboard inputs # Setting a true state = a false state creates a toggle that switches back and forth, in this case between paused and unpaused get_tree().paused = !get_tree().paused
  3. Run the new game_paused_toggle() function at the end of the ready function, after the signals:
    
    	GlobalSignals.quit_pressed.connect(quit_game)
    # Since processing is on by default, this pauses processing when the game first opens
    	game_paused_toggle()
    
    
  4. Run the game_paused_toggle() function at the end of the menu_visibility_toggle() function:
    
    	ui_menu.visible = !ui_menu.visible
    # Whenever the menu is hidden, SceneTree processing is unpaused
    # Whenever the menu is visible, SceneTree processing is paused
    	game_paused_toggle()
    
    

To fix the second potential reason for the player falling at the level start, lets add a spawn point to set the initial player position on each level. This will make sure the player begins at the right location/elevation on each level, instead of where the player ended in the last level.

  1. Create a spawn point scene to use across all the levels:
    1. From the 3D View, use CMD+N to make a new scene
    2. Press on "3D Scene" in the SceneTree to make a Node3D as the root node Make a Node3D image is missing
    3. Rename the node "PlayerSpawnPoint" Rename the Node3D image is missing
    4. In the sidebar's Node tab under "Groups", add a "playerspawn" group (this group will be accessed in the script to find the correct spawning node) Make a group image is missing
    5. With the PlayerSpawnNode active, click on the SceneTree's "+" button to search for and add a Marker3D node Add a marker3D image is missing
    6. Save the scene with CMD+S and save somewhere within the assets folder Save location image is missing
  2. Instance and position the spawning scene in each level:
    1. In each level, with the ground node selected, click on the SceneTree's link button to search for and add the spawn point as an instance Instance the spawnpoint scene image is missing
    2. Position the spawn point where the player should start Set the spawn location image is missing
  3. Switch back to the Script view, with the Game script active
  4. At the end, add a new function to set the player start/spawn position, so they don't fall at the start:
    
    	get_tree().paused = !get_tree().paused
    
    # player spawning is being added as its own function to make it easier to add player re-spawning later func player_spawn(): # Sets a custom variable for the player spawning point and uses the scene tree to access the first (in this case only) node in the playerspawn group var player_spawn_loc = get_tree().get_first_node_in_group("playerspawn") # global_position is a property of Node3D that uses Vector3 values # match the player variable's global position to the spawn point's global position to move the player to the spawn point player_instance.global_position = player_spawn_loc.global_position
  5. Reference the player spawn function inside the end of the change_level_deferred() function:
    
    	menu_visibility_toggle()
    	
    # Run the player_spawn function to move the player to the starting location
    	player_spawn()
    
    
    
  6. Save with CMD+S

Now when testing again with CMD+R, the player should start from the spawn point and be movable with the arrow keys and/or the WASD keys. Spacebar should make the player jump, if you've coded a jump.

If the player still falls:

  • Make sure the player and ground collision is setup properly — including collision shapes, layers, and masks
  • Check that the spawn point node and player node are set to the correct groups
  • Check that the game management script uses the correct indenting, spelling, and names

If the player is on the ground but won't move

  • Make sure the player and ground collision is setup properly — including collision shapes, layers, and masks
  • Make sure the Input Map is setup in Project Settings
  • Check if the Transform → Scale settings have been changed on the player/ground/colliders (physics tends to be most accurate when the Transform scale is set to 1.0)
  • Check that the player's script uses the correct indenting, spelling, and names
  • Check the direction of the mesh's normals (face direction)
  • Add print() messages in strategic places to see if the code is being triggered
  • As a last resort, change the physics engine to the 3rd party Jolt engine (sometimes this gives better results, at least for certain types of physics interactions):
    1. Change the view to the AssetLib
    2. Search for "Jolt" and select "Godot Jolt" Find jolt image is missing
    3. Leave the default settings in the Install dialog and click on "Install" Jolt install options image is missing
    4. Close and restart
    5. Back in Godot, go to the Project menu and open "Project Settings"
    6. Toggle on "Advanced Settings" in the top right
    7. Go to Physics → 3D and change the default game engine to "JoltPhysics3D" Enable jolt image is missing
    8. Save, close and then restart Godot

Level Changing

Portals or Doorways are commonly used to move from one level to another.

  1. Begin by creating a signal to represent entering the portal:
    1. Open the Global Signals script
    2. After the last signal, add a signal for entering the portal:
      
      signal quit_pressed
      
      
      # Signal for entering a portal to change levels
      signal portal_entered
      
      
    3. Save the changes to the script with CMD+S
  2. Switch to the 3D View
  3. Use CMD+N to create a new scene to use for a reusable portal
  4. Click on "+ Other Node" to search for and add an "Area3D" node (Area3D's are passed through by physicsbodies and can sense when physicsbodies enter/exit their collision shape) Add new Area3D node image is missing
  5. Rename the Area3D to "Portal" Rename Area3D node image is missing
  6. Save the scene with "CMD+S" and save the portal scene in a new Portal folder inside of the Assets folder Save Scene image is missing
  7. Add a mesh to use as the portal/doorway (follow the steps under "Adding Obstacles as Instances" to add either a .Blend model or a MeshInstance3D)
  8. With the root node "Portal" selected, click on the "+" to add a CollisionShape Add collider image is missing
  9. In the Inspector tab, choose a shape for the CollisionShape — in most cases a "BoxShape3D" will work well Set collider shape image is missing
  10. Adjust the size/location of the collision shape to fit the area in which the player should be able to enter the portal - this is not necessarily the size of the mesh Set collider size/location image is missing
  11. Create a script that sends a signal when the player enters the portal:
    1. Make sure the root node called "Portal" is selected in the SceneTree Add a script image is missing
    2. Click on the "+ Script" button to make a new empty script called "Portal" and make sure it is set to the Portal directory Script dialog image is missing
    3. Make sure the root node called "Portal" is actively selected in the SceneTree
    4. Switch the right sidebar to the "Node" tab, with "Signals" active
    5. Double-click on "body_entered(body: Node3D)" Default Area3D signals image is missing
    6. From the Signal Connection pop-up window, select the root node "Portal" and then click on "Connect" Connect a signal image is missing
    7. This should automatically create a new function in the Portal Script — replace the pass placeholder with the following:
      
      # extends Area3D gives access to any properties, methods, or functions that are available to the class or inherited by the Area3D class
      extends Area3D
      
      # This default function was created by connecting the signal from the sidebar # It recognizes when a physicsbody enters the connected Area3D node # body is a default parameter that represents the physicsbody that enters the Area3D func _on_body_entered(body): # In the case there are multiple moving physicsbodies that could enter the Area3D (like moving enemies) # it is helpful to make sure the player's body is the only one that triggers the portal # is_in_group is a default method of the node class that recognizes if a node is in a specific group or not # So if the physicsbody is in the player group: if body.is_in_group("player"): # Then emit the portal_entered signal from the Global Signals script GlobalSignals.emit_signal("portal_entered") # Also add a temporary check to see if the portal is entered by the player # print() sends messages to the console - in this case the string is combined with the name of the body for the message print("Portal was entered by ", body)
    8. Save the script with "CMD+S"
    9. Add the portal as an instance in every level except for the last level:
      1. Click on the SceneTree panel's link icon to search for and add the portal as an instance Add portal scene as an instance image is missing
      2. Set the location, scale, and rotation of the portal as needed using the widget or the transform settigns Portal instance example image is missing
      3. Save the changes to the level with CMD+S
  12. Go back to the main Game scene and play the game with CMD+R
  13. When testing the portal, look for the message in the Output console at the bottom of the Godot window:
    • If the message is missing, make sure the script's spelling/indentation is correct, the right names are used, the player is assigned to a group called player, and that the Area3D has a connected on_body_entered signal
    • If the message is there, delete or comment out the print() message from the portal script and save the change with CMD+S before moving on to the next step

This is part 2 of writing the game management script. It needs some adjusting to use the portals to actually change levels. Keep in mind this largely relies on consistent level naming/organization and upon changing levels in consecutive order.

  1. Open the Game.gd script
  2. Add a new global variable before the _ready() function:
    
    var next_level: String
    
    # Sets a variable to represent the last level, as a node
    var last_level : Node
    
    func ready():
    
    
  3. In the _ready() function, connect the portal entered signal:
    1. After the last signal, at the same level of indentation, connect the signal:
      
      	GlobalSignals.quit_pressed.connect(quit_game)
      	
      # Connects a "portal" signal to a custom changing level forward function
      	GlobalSignals.portal_entered.connect(set_next_level)
      	
      	game_paused_toggle()
      
      
  4. At the start of the set_starting_level() function, set the last_level to null:
    
    func set_starting_level():
    
    # When starting the game from the start, there's no previous level	
    	last_level = null
    	
    	next_level = first_level.resource_path
    
    
  5. After the end of "set_starting_level()" function, add a new function for setting the next level:
    
    	change_level()
    	
    # A custom function to figure out the next level before changing levels
    func set_next_level():
    # get_name() is a property of the Node class that is used to find the name of a node
    # to_int() is a method of the Node class used to convert a string to an integer, with any non-numerical characters stripped out
    # This sets a variable to represent the level number by getting level node's name turned to an integer
    	var current_num : int = current_level.get_name().to_int()
    # scene_file_path is a property of the Node class that accesses the absolute file path of an instanced scene
    # replace(something, with_some_thing_else) is a method of the String class
    # Set the next level to the path of the current level, where the current number(s) are replaced with the current number + 1
    	next_level = current_level.scene_file_path.replace(str(current_num), str(current_num + 1))
    # Run the custom level changing function
    	change_level()
    
    func change_level():
    
    
  6. After the "set_next_level()" function, add a function for deleting the old level (this will be accessed by the change level function):
    
    	change_level()
    	
    # A custom function to delete old levels
    func delete_old_level():
    # remove_child() is a default method of the Node Class that removes a node from the SceneTree, making it an orphan node
    	remove_child(last_level)
    # queue_free() is a default method of the Node class that waits for the end of the current frame to delete the node and its children
    # This completely deletes the old level's node
    	last_level.queue_free()
    
    func change_level():
    
    
  7. Adjust the next_level_deferred() function so that when loading level 1, the menu is hidden, but when loading higher levels, the older level is deleted first:
    1. After add_child(current_level) but before the menu_visibility_toggle(), add:
      
      	add_child(current_level)
      	
      # If the last level variable has a value (is not null)
      	if last_level != null:
      # So call the custom delete old level function
      		delete_old_level()
      # Otherwise, there should be a visible menu to hide
      	else:
      # Tab the following menu_visibility_toggle() inward so it is part of the Else statement	
      		menu_visibility_toggle()
      
      	player_spawn()
      
      
    2. At the end of this function, but still inside of it, set the last level to be the current level:
      
      	player_spawn()
      		
      # This is so that next time the function runs, what is now the current level will be the last level	
      	last_level = current_level
      
      
  8. Save the game with "CMD+S" and then test the portal functionality with "CMD+R"
  9. Follow the troubleshooting steps listed in the sidebar if there is a problem

Testing Additional Levels

When testing other levels, you can play through from one level to the next. Sometimes turning off the visibility of the obstacles/enemies can help speed up moving from one level to another. Just make sure to turn the visibility back on later.

Alternatively, in the Inspector for the Game scene, you can temporarily switch out the first level to be the level you want to test. Just make sure to switch back to the correct first level later.

Lose Conditions

For this tutorial, the game is lost when the level times out, or when the player falls off the ground into an Area3D "sensor".

When time runs out, the player will lose the game.

  1. Create a timer scene:
    1. From the 3D View, create a new scene with "CMD+N"
    2. Click on the "+ Other Node" in the SceneTree
    3. Search for and add a "Control" node Add a control node image is missing
    4. Rename the node to "Level_Timer" Rename the control node image is missing
    5. Save the scene with "CMD+S" and save within the UI folder (perhaps in a timer folder) Rename the control node image is missing
  2. Add a label to display the countdown time:
    1. Click on the "+" button to at the top of the SceneTree panel to search for and add a "Label" node Add a label node image is missing
    2. Rename the label to "CountdownLabel" Rename the label node image is missing
    3. In the Inspector:
      1. Add placeholder text for the label — such as 00:00 Placeholder text image is missing
      2. Under "Theme Overrides", change the font, font size, and color as needed Customize the label image is missing
    4. From the 2D View, adjust the location of the label as needed
    5. Add the timer:
      1. Make sure the root "Level_Timer" node is active
      2. Click on the "+" button to at the top of the SceneTree panel to search for and add a "Timer" node Add a timer node image is missing
      3. Rename the label to "CountdownTimer" Rename the timer node image is missing
      4. In the Inspector, check "One Shot", so that the timer only runs 1 time Check 'One Shot' image is missing
    6. Create a script for the timer:
      1. Make sure the root "Level_Timer" node is active
      2. Click on the "+ Script" button at the top of the SceneTree panel to create an empty script Create an empty image is missing
      3. Save the script
    7. Begin the timer script:
      1. In the timer script, add global variables:
        
        # extend gives access to any properties, methods, or functions that are available to the class or inherited by the Control class
        extends Control
        # A public variable, available in the inspector, that sets the amount of time to complete the level
        # 10 is just a placeholder to represent 10 seconds — this can be overridden in each level as needed
        @export var amount_of_time : int = 10
        # Sets a variable to represent the label, leaving an empty value for a just moment
        # $ + nodepath lets you find a node in the SceneTree by the name/path
        @onready var label : Node = 
        # Sets a variable to represent the timer node, leaving an empty value for a just moment
        @onready var timer : Timer = 
        
        
      2. Add the CountdownLabel as the value for the label variable:
        1. Drag/drop the countdown label's node from the SceneTree to the end of the label variable in the script Drag the label image is missing
      3. Add the CountdownTimer as the value for the timer variable:
        1. Drag/drop the countdown timer's node from the SceneTree to the end of the label variable in the script Drag the timer image is missing
      4. After the global variables, create a _ready() function to begin the timer:
        
        # Called when the node and its children are ready — they've entered the scene tree
        func _ready() :
        # wait_time is a property of the Timer class that is the initial time where the timer starts counting down
        # Set the wait_time to use the amount_of_time variable's value
        	timer.wait_time = amount_of_time
        # start() is a method of the Timer class that begins the timer
        	timer.start()
        
        
      5. Add a function to find out how much time is left in minutes and seconds:
        
        # A custom function to find out how much time is left in minutes and seconds
        func time_left_in_level() :
        # This creates a variable to represent the time remaining for the timer
        # time_left is a default property of the Timer class - it is how much time the timer has left in seconds
        	var time_remaining = timer.time_left
        # This creates a variable to represent the number of minutes left
        # floor() is a default method that rounds downward to the nearest whole number
        # Dividing a number of seconds (the time_remaining value) by 60 and then rounding that result gets the number of minutes
        	var minute = floor(time_remaining / 60)
        # This creates a variable to represent the seconds left in the current minute
        # In this context, % is being used to get the remainder of time_remaining being divided by 60
        # int() converts the time_remaining value to an integer
        	var second = int(time_remaining) % 60
        # This returns the time left in minutes and seconds whenever the function runs
        	return [minute, second]
        
        
      6. Create a _process(delta) function to replace the text in the label with the time remaining:
        
        # _process(delta) is a default function that runs every frame
        # using the delta parameter adds consitency for time across different systems/devices
        # Timer nodes don't need to factor in delta, so an _ is added to the front of the parameter to ignore it
        func _process(_delta) :
        # In a string, % is used to create a placeholder to set where something will be replaced
        # 02 is setting a number of places and d sets the number as a decimal, giving the string 00:00 as a format
        # After a string with % placeholders, whatever follows the % will replace the placeholders in the string
        # text is a default property of the Label class
        # The label's text gets changed to the minute:second values returned in the time_left_in_level() function
        	label.text = "%02d:%02d" % time_left_in_level()
        
        
    8. Save the scene with "CMD+S"
    9. Add the timer scene as an instance to each level:
      1. In each level, at the top of the SceneTree, click on the link icon
      2. Search for and choose the timer.tscn
      Instance the timer image is missing
    10. Preview the game from the Game scene with "CMD+R" to confirm the timer works as expected
  1. In the Global Signals script, add a new signal for running out of time:
    
    # A custom signal for running out of time
    signal portal_entered
    
    signal out_of_time
    
    
  2. Connect the default timeout signal to a timeout function:
    1. Select the CountdownTimer node in the SceneTree
    2. In the sidebar's Node tab, under "Signals", double-click on "timeout()" Connect the timeout signal image is missing
    3. From the signal connection dialog, choose the root node called "Level Timer" then click on "Connect" Connect signal dialog image is missing
  3. In the Timer Script's new timeout function, emit the out of time signal:
    
    	label.text = "%02d:%02d" % time_left_in_level()
    
    # A function created from the connected timer's time out signal func _on_countdown_timer_timeout(): # Use GlobalSignals to emit the signal for running out of time GlobalSignals.emit_signal("out_of_time")
    game_paused_toggle()
  4. In the main "Game" script' _ready() function, connect the out_of_time signal to a new time out function:
    
    	GlobalSignals.portal_entered.connect(set_next_level)
    # Connects an "out_of_time" signal to a custom time_out function that will be added later
    	GlobalSignals.out_of_time.connect(time_out)
    
    
  5. At the end of the script, add that time_out function, with pass as a temporary placeholder:
    
    	player_instance.global_position = player_spawn_loc.global_position
    
    # A custom function to set the time out message and then run a game over function func time_out(): # This is a temporary placeholder to stop any warnings pass
  6. Save with CMD+S

Area3D nodes can be used as "sensors" that detect when a physicsbody3D enters/exits. This is great

  1. Create a new signal for when the player dies in the GlobalSignals script:
    
    # A custom signal for when the player dies
    signal out_of_time
    signal player_died
    
    
  2. Save changes to the script with "CMD+S"
  3. Create a new scene to use as a sensor that recognizes when the player falls off of the terrain/ground:
    1. With the 3D View active, press "CMD+N" to make a new scene
    2. Click on "+ Other Node" to search for and add an "Area3D" node Add an area3d node image is missing
    3. Rename the node to FallSensor Rename the area3d node image is missing
    4. With the FallSensor node selected, click on the + at the top of the SceneTree to add a child node then search for and add a "CollisionShape3D" node Add a collision3d node image is missing
    5. In the Inspector, set the collision shape to a new "BoxShape3D"
    6. Click on the BoxShape3D in the Inspector to expand the size settings
    7. Set the size to be much larger than the ground in any scene — perhaps X: 100, Y: 1, and Z:100 Set the collision shape/size image is missing
    8. Under Collision, set the Layer/Mask:
      • Layer = 2 (or whatever layer number is for the world or etc.)
      • Mask = 1 (or whatever layer number is for the player)
      Collision layer/mask image is missing
    9. Save the Scene (CMD+S) and save it in within the assets folder — perhaps in a new FallSensor folder Save the scene image is missing
  4. Make a script that recognizes when the player exits the Area3D:
    1. With the root node "FallSensor" selected, click on the SceneTree's new script button
    2. Create an empty script, saved in the same location as the FallSensor scene
    Create a script image is missing
  5. Connect the Area3D's default exit signal to the script:
    1. With the root node "FallSensor" selected in the SceneTree, change the right sidebar to the "Node" tab
    2. Under Signals → Area3D, double-click on "body_exited(body: Node3D)" Connect the exited signal image is missing
    3. In the signal connection window, make sure the root node "Fallsensor" is connected, then click on connect Signal dialog image is missing
  6. In the new _on_body_exited() function, emit the player_death signal:
    
    # This gives access to the methods and properties available to the Area3D class
    extends Area3D
    
    # This function was created by connecting the _on_body_exited signal from Area3D # The body parameter will be unused, so an _ is added in front to disable it # This function will run whenever a Physicsbody3D (like the player) exits through the Area3D # In this case exit is preferrable to enter, to prevent the sensor being triggered unintentionally by the player on scene changes func _on_body_exited(_body): # The global signals script is accessed in order to emit the player_death signal GlobalSignals.emit_signal("player_death")
  7. Save the script with "CMD+S"
  8. In the main "Game" script's _ready() function, connect the player_death signal to a player_died function:
    
    	GlobalSignals.out_of_time.connect(time_out)
    # Connects a "player_death" signal to a custom player_died function that will be added later
    	GlobalSignals.player_death.connect(player_died)
    
    
  9. At the end of the script, add that new player_died function, with pass as a temporary placeholder:
    
    	pass
    
    # A custom function to set the player died message and then run a game over function func player_died(): # This is just a temporary placeholder to prevent warnings pass
  10. Save the script with CMD+S
  11. Add the FallSensor as an instance to any levels where the player can fall off the ground/terrain:
    1. Open the level scene file
    2. In the SceneTree, click on the link icon to search for and add the FallSensor scene Add an instance image is missing
    3. From the 3D View, adjust the position of FallSensor node to be somewhere below the ground
    4. Adjust the size and location as needed
    5. "CMD+S" to save the scene
    6. Repeat for other levels

Win Conditions

For this tutorial, the game is won when the player reaches a goal, using collision.

  1. Create a new signal for when the player reaches the goal in the GlobalSignals script:
    
    # A custom signal for reaching the goal and winning
    signal player_death
    signal goal_reached
    
    
  2. Save changes to the script with "CMD+S"
  3. Connect the signal in the main Game script, at the end of the other signals in the _ready() function:
    
    	GlobalSignals.out_of_time.connect(time_out)
    # Connects a "goal_reached" signal to a custom function for winning the game, that will be added later
    	GlobalSignals.goal_reached.connect(game_won)
    
    
  4. At the end of the script, create the game_won function, with pass as a temporary placeholder for now:
    
    	pass
    
    # A custom function to set the player won message and then run a game over function func game_won(): # This is just a temporary placeholder to stop warnings pass
  5. Create a Goal object scene:
    1. From the 3D View, press "CMD+N" to create a new scene
    2. Click on "+ Other Node" to search for and add a "StaticBody3D" node
    3. Rename the node to Goal
    4. Save the scene with "CMD+S" and choose/create a relevent location to save — perhaps Assets/Goal
    5. Follow the directions under "Adding Obstacles as Instances" in the "Levels" tab to add either a MeshInstance3D or a Blender model
    6. With the "Goal" node selected, add a CollisionShape3D, using the steps under "Adding Collision" in the Levels tab
    7. After the collision shape and size are set, make sure to set the collision Layer/Mask in the Inspector (with the root "Goal" node selected):
      • Layer = 2 (or whatever layer number is for the world or goal or etc.)
      • Mask = 1 (or whatever layer number is for the player)
    8. Switch the right sidebar to the "Node" tab, and go to "Groups"
    9. With the root "Goal" node still selected, click in the empty field to create/add a group called "goal"
    10. Save the scene with "CMD+S"
  6. Add the Goal scene as an instance in the final level:
    1. With the final level scene open, click on the main parent/root node (Level #...)
    2. At the top of the SceneTree panel click on the link icon to search for/add the "Goal" scene as an instance
    3. Adjust the scale/location/rotation of the goal as needed using the 3D View controls, or the Inspector's transform settings for the instanced "Goal" node
  7. Adjust the Player's script to recognize collisions with the goal:
    1. Open the Player script
    2. Add to the end of the _physics_process(delta) function:
      
      	set_jump_rotation()
      # A "for loop" iterates/repeats through the code for each item in a list/array/dictionary
      # get_slide_collision_count() returns the number of collisions/changes in direction in the last call from the move_and_slide() method of CharacterBody3D
      # i is a temporary variable to represent each item in get_slide_collision_count as the loop executes
      	for i in get_slide_collision_count():
      # This sets a collision variable called collision to represent the collision data from the temporary variable, using the get_slide_collision method with a type of KinematicCollision3D
      		var collision : KinematicCollision3D = get_slide_collision(i)
      # get_collider() is a default method that finds the colliding body from an index
      # This looks to see if there is a collision with a colliding body (it is not null), and if the collision is the goal group
      		if collision.get_collider().is_in_group("goal"):
      # Access the GlobalSignals script to emit the goal_reached signal		
      			GlobalSignals.emit_signal("goal_reached")
      # otherwise ignore this	
      		else:
      			pass
      
      func set_jump_rotation():

Game Over

This uses the previously connected signals for reaching the goal, timing out, and falling off the terrain to set different win/lose messages.

  1. At the top of the script, with the other @onready variables, add a variable to represent the game over message:
    
    @onready var player_instance : Node = $Player
    # A variable that accesses the SceneTree to set the game over message to use the first (only) item in the menu-end group
    @onready var game_over_message : Label = get_tree().get_first_node_in_group("menu-end")
    
    var current_level : Node
  2. In the _ready() function, turn visbility of the game over message off at the game's start, after the signal connections and before the game_pause_toggle():
    
    	GlobalSignals.goal_reached.connect(game_won)
    # At the start of the game, the menu shouldn't show any game over message
    # .visible controls visibility, and setting the visibility to false hides the label
    	game_over_message.visible = false
    	game_paused_toggle()
    
    
  3. Adjust the time out function by replacing pass:
    
    # A custom function to set the out of time message and then run a game over function
    func out_of_time():
    # This sets a game over message variable to use the string — choose your own message
    # .text accesses the text
    	game_over_message.text = "Sorry, you ran out of time :("
    # Run a custom game over function — it will be created a little later
    	game_over()
    
    
  4. Adjust the player died function by replacing pass:
    
    # A custom function to set the player died message and then run a game over function
    func player_died():
    # This sets a game over message variable to use the string — choose your own message
    # .text accesses the text
    	game_over_message.text = "Sorry, you died :("
    # Run the custom game over function — it is coming soon
    	game_over()
    
    
  5. Adjust the player won function by replacing pass:
    
    # A custom function to set the player won message and then run a game over function
    func game_won():
    # This sets a game over message variable to use the string — choose your own message
    # .text accesses the text
    	game_over_message.text = "Amazing, you won!!!"
    # Run the custom game over function — it will be created next
    	game_over()
    
    

After the game over messages are set, those functions trigger the game over function. This turns menu visibility back on, controls which parts of the menu display upon the end of the game, and removes the last level to reset the game.

  1. At the end of the script, create the game over function:
    
    	game_over()
    
    # A custom function to run when the game ends func game_over(): # Like the level changing function, this needs to use call_deferred to make sure processing is finished call_deferred("_deferred_game_over")
  2. Create the deferred game over function:
    
    	call_deferred("_deferred_game_over")
    
    # The deferred game over function func _deferred_game_over(): # A "for" loop that sets a temporary variable called "ui" for items in the menu-start group, accessed from the SceneTree for ui in get_tree().get_nodes_in_group("menu-start"): # Turns the visibility off for any nodes in the group ui.visible = false # Use the game over message visiblity to true game_over_message.visible = true # Use the custom menu visibility function to toggle the menu back to visible menu_visibility_toggle() # Use the custom function to delete the last level played delete_old_level()
  3. Save the game with CMD+S
  4. Preview with CMD+R and test the game:
    • Does the time running out open the menu with the time out message?
    • Does falling off the terrain open the menu with the player died message?
    • Does reaching the goal open the menu with the player won message?
    • Does the play button from the game over screen start the game over from the beginning?
  5. If there are any issues, use the standard troubleshooting steps — make sure names/spelling are correct/consistent, make sure indenting is correct, use print() in strategic places...

The process of creating/exporting a game app is called building. There are some settings that should be set before building the playable game app.

Some project Settings need to be adjusted before exporting the game.

  1. Open the "Project Settings" window from the Project menu
  2. Under "Config", set the final game name and version number Game name and version image is missing
  3. If using your own game icon, choose the icon (make sure the image is saved in the Game Project folder ahead of time — or Godot's icon will be used) Game icon image is missing
  4. Under "Run", click on the folder icon to the right of "Main Scene" to choose the main game scene Game scene image is missing
  5. Optionally, there are some settings under "Boot" that can be customized
  6. Under "Display", there are a variety of "Window" settings that can be customized:
    1. Size:
      • Mode: in most cases should be either Fullscreen or Maximized Size Mode image is missing
      • Resizable: check if the window size can be changed Resizable image is missing
      • Borderless: keep on to prevent black bars on the sides if the screen has a different aspect ratio to the camera Borderless image is missing
    2. Stretch:
      • Mode: set to "canvas_items" so the UI size automatically adjusts to the screen/window size Stretch Mode image is missing
      • Aspect: adjust as needed to control how the camera size adjusts to different screen sizes — expand fills the empty space Set aspect ratio image is missing
    3. Depending on the game, other settings might be adjusted

Godot uses "Export Templates" for each OS in order to export to the different platforms. They are not installed by default, so you need to go through the download process for each platform you want to build for.

  1. Open the Export window from the Project menu Export in the menu image is missing
  2. Next to "Presets" click on "Add" and choose "macOS" Select macOS image is missing
  3. The first time exporting to a new platform, the export template needs to downloaded, so click on "Manage Export Templates" at the bottom of the window Select manage export templates image is missing
  4. Choose "Download and Install" from the Export Template Manager Dialog (if it's a slow internet day, ask for the template to be airdropped to your computer and then choose that file instead) Select macOS image is missing
  5. Close the download window when the installation is finished
  6. Repeat for other platforms as needed
  1. Once the template is installed, go back to adjust the Export Settings:
    1. Select the platform on the left Select macOS image is missing
    2. Next to "Export Path", click on the folder icon to choose and/or create a directory for a build location Export location image is missing
    3. Next to "Bundle Identifier", give a name (if this was going to be sold in an app store, this would need to be unique — in this case a placeholder is fine, ie. abrhs2024.game) Bundle identifier image is missing
    4. With macOS preset selected, choose "Export Project" NOT the other export options Export project image is missing
    5. For the first build and other test builds, leave "Debug" on — turn off for a final build/release — then press save to actually export the game Export dialog image is missing
  1. Testing locally on the same mac:
    1. To run on macOS, double-click on the DMG to open DMG image is missing
    2. Drag and drop the app and debug log into the parent directory
    3. Double-click on the app to play it — on your own account it should run without issue
    4. If there are issues, run the game from the debug file (named with .command at the end of the name, ie. Game_Name.command) Run the debug version image is missing
    5. Look at the log that opens with the app to see the error messages from the game, and use that as a starting point for making changes back in Godot Error log image is missing
  2. Running on other macs:
    1. Airdrop or download the DMG to the new mac
    2. Double-click on the DMG to open and drag/drop the app somewhere easy to access on the computer
    3. Opening the first time could fail and bring up a warning about an "unknown developer", since we are not participating in the "Apple Developer Program"
    4. If opening fails, close the warning dialog and then right-click on the app and choose 'Open' fromt he context menu
    5. When a new warning dialog opens, click on the "Open Anyways" option — after this the app should always open normally on this computer

If you've exported the game for other platforms, the general process is the same though the actual files generated might vary. If there are multiple files, just make sure they are always stored/kept together. Zip the build directory when sharing to another computer. Always extract the files to the same location. If certain files get separated, the game might not run.