Godot Engine 4.0 Documentation - First 3D Game

This article is the result of Google Translate's English translation, and DrGraph added some corrections on this basis. English original page:

Your first 3D game — Godot Engine (stable) documentation in English

Your first 3D game¶

In this step-by-step tutorial series, you'll create your first full 3D game using Godot. By the end of this series, you'll have yourself a simple but finished project like the animated gif below.

The game we'll write here is similar to your first 2D game , but with one twist: you can now jump, and your goal is to squish minions. This way, you can both recognize the patterns you learned in the previous tutorial and build on them with new code and functionality.

You will learn:

  • 3D coordinates are handled using the jump mechanism.

  • Use kinematic bodies to move 3D characters and detect when and how they collide.

  • Use the physical layer and a group to detect interactions with specific entities.

  • Write a basic procedural game by instantiating monsters periodically.

  • Design a motion animation and change its speed at runtime.

  • Draw user interface on 3D game.

and more.

This tutorial is for beginners who have followed the full Getting Started series. [We'll go slow at first to elaborate] and be brief as we follow up with similar steps. If you're an experienced programmer, you can browse the source code for the full demo here: Squash the Creep source code .

NOTE: You can follow this series without finishing the 2D series. However, if you're new to game development, we recommend starting in 2D. 3D game code is always more complex, while 2D series will give you a more comfortable base.

We prepared some game assets so that we can jump directly to the code. You can download them here: Squash the Creeps assets .

We'll start by making a basic prototype for the player's movements. Then we'll add monsters that we'll randomly spawn around the screen. After that, we'll implement the jumping and crushing mechanics before improving the game with some nice animations. We'll end with a scoring and retry screen.

Setting up the play area¶

In the first part, we will set up the game area. Let's start by importing the starting assets and setting up the game scene.

We've prepared a Godot project with the 3D models and sounds we'll be using in this tutorial, linked in the index page. If you haven't done so already, you can download the archive here: Squash the Creeps assets .

After downloading, extract the .zip archive to your computer. Open the Godot Project Manager and click the Import button.

In the import popup, enter the full path to the newly created directory  squash_the_creeps_start/. You can click the browse button on the right to open a file browser and navigate to project.godotthe files contained in the folder.

Click Import & Edit to open the project in the editor.

The startup project contains an icon and two folders: art/and fonts/. There you'll find the art assets and music we'll be using in the game.

There are two 3D models, player.glband mob.glb, some materials belonging to these models, and a music track.

Set playable area¶

We'll create our main scene with a normal node as its root. In  the Scene  dock, click the Add Child Node button represented by the "+" icon in the upper left corner , then double-click the Node . Name the node Main. Another way to rename a node is to right-click on the node and select Rename (or F2). Alternatively, to add a node to the scene, you can press Ctrl+a (or Cmd+a on macOS).

Press Ctrl+s (Cmd+s on macOS) to save the scene as main.tscn.

We'll start by adding a floor to keep the character from falling. To create static colliders like floors, walls or ceilings, you can use the StaticBody3D node. They require a CollisionShape3D child node to define the collision area. With the nodes selected Main, add a StaticBody3D  node, then a CollisionShape3D . Rename StaticBody3DGround to .

Your scene tree should look like this

A warning sign will appear next to CollisionShape3D because we haven't defined its shape yet. If you click on the icon, a popup will appear giving you more information.

To create the shape, select the CollisionShape3D node, go to the Inspector  and click in the <empty> field next to the Shape property . Create a new BoxShape3D .

The box shape is perfect for flat floors and walls. Its thickness allows it to reliably block fast-moving objects.

A wireframe of a box appears in the viewport with three orange dots. You can click and drag them to interactively edit the extents of the shapes. We can also set dimensions precisely in the inspector. Click on BoxShape3D to expand the resource. Set its size to X 60, Y , and 2Z.60

Collision shapes are invisible. We need to add a visual floor to go with it. Select this Groundnode and add a MeshInstance3D as its child node.

In the Inspector, click in the field next to Mesh and create a BoxMesh  asset to create a visible box.

Again, it's too small by default. Click the box icon to expand the resource and set its size to 60, , 2and 60.

You should see a wide gray slab covering the grid and the blue and red axes in the viewport.

We're going to move the ground down so we can see the floor mesh. Select  Groundthe node, hold down the Ctrl key to turn on grid snapping, then click and drag down the Y axis. It's the green arrow in the mobile gadget.

Note: If you cannot see the 3D object manipulators shown in the image above, make sure Selection Mode is active in the toolbar above the view .

Move the ground 1meters down so that there is a visible editor grid. A label in the lower left corner of the viewport tells you how much the node has been panned.

Note: Moving the Ground node down will move both child nodes at the same time. Make sure to move the Ground node, not the MeshInstance3D or  CollisionShape3D .

Ultimately, Groundthe transform.position.y should be -1

Let's add a directional light so our scene isn't all gray. Select Main the node and add the child node DirectionalLight3D .

We need to move and rotate the DirectionalLight3D node. Move the manipulator up by clicking and dragging its green arrow, then click and drag the red arc to rotate it about the X axis until the ground is lit.

In the Inspector , turn on Shadow -> Enabled by clicking on the checkbox .

At this point your project should look like this.

This is our [new starting point]. In the next part, we'll deal with the player scene and base movement.

Player Scene and Input Actions¶

In the next two lessons, we'll design the player scene, register custom input actions, and code player movement. By the end, you'll have a playable character that can move in eight directions.

Create a new scene by going to the Scene menu in the upper left corner and clicking New Scene .

Create a CharacterBody3D node as the root node

Name the CharacterBody3D as Player. The character body is in addition to the regions and rigid bodies used in the 2D game tutorial. Like Rigidbodies, they can move and collide with the environment, but instead of being controlled by the physics engine, you determine their motion. You'll see how we use the unique capabilities of nodes when coding the jump and squeeze mechanics.

Reference: To learn more about the different physical node types, see  Introduction to Physics .

We will now create a basic rig for our character's 3D model. This will allow us to rotate the model later via code while the animation is playing.

Add a Node3D node as Player的a child node and name itPivot

Then, in the file system dock, double-click to expand the folder and drag and drop art/it under the .player.glb文件Pivot

This should instantiate the model as Pivot. You can rename it to Character.

Note: These .glbfiles contain 3D scene data based on the open source GLTF 2.0 specification. They are a modern and powerful alternative to proprietary formats like FBX which Godot also supports. To generate these files, we designed the model in Blender 3D and exported it to GLTF.

As with the various physics nodes, we need a collision shape for our character to collide with the environment. Select the Playernode again and add a child node  CollisionShape3D . On the Inspector's Shape property, add a new SphereShape3D .

The wireframe of the sphere appears below the character.

This will be the shape the physics engine will use to collide with the environment, so we want it to fit the 3D model better. Zoom out a bit by dragging the orange dot in the viewport. The radius of my sphere is about 0.8meters.

Then, move the shape up so that its bottom roughly aligns with the grid plane.

You can toggle the visibility of the model by clicking Pivotor the eye icon next to the node.Character

Save the scene asplayer.tscn

With our nodes ready, we can almost start coding. But first, we also need to define some input actions.

Create an input action¶

To move the character, we'll listen for player input, such as pressing an arrow key. In Godot, while we can write all keybindings in code, there is a powerful system that allows you to assign labels to groups of keys and buttons. This simplifies our scripts and makes them more readable.

This system is input mapping. To access its editor, go to the project menu and select project settings .

At the top, there are multiple tabs. Click the input map 【Input Map】 . This window allows you to add new actions at the top; they are your tabs. At the bottom, you can bind keys to these actions.

The Godot project comes with some predefined actions designed for user interface design, we can use them here. But we are defining our own gamepad.

We'll name our actions move_leftmove_rightmove_forwardmove_back and jump.

To add an action, write its name in the top bar and press Enter.

Create the following five actions:

To bind a key or button to an action, click the "+" button to its right. This is for move_left. Press the left arrow key and click OK .

Also bind the A key to move_left动作.

Now let's add support for the left joystick of the gamepad. Click the "+" button again, but this time choose Manual Selection -> Joypad Axes .

Select the negative X axis of the left joystick.

Leave other values ​​as default and press OK

Note: If you want your controller to have different input actions, you should use the device option in the additional options. Device 0 corresponds to the first gamepad plugged in, device 1 corresponds to the second gamepad plugged in, and so on.

Do the same for other input operations. For example, bind the right arrow D and the positive axis of the left stick to move_right. After binding all keys, your interface should look like this.

The last thing to set is jumpaction. Bind the Space key and the A key of the gamepad.

Your jump input action should look like this.

That's all the moves we need in this game. You can use this menu to label any key and button group in your project.

In the next section, we'll write the code and test the player's movement.

Moving Player Using Code¶

Time to code! We will use the input action created in the previous section to move the character.

Right-click the Playernode and select Attach Script to add a new script to it. In the popup, set the template to empty before pressing the create button  .

Let's start with the properties of the class. We'll define a movement velocity, a fall acceleration that represents gravity, and a velocity that we'll use to move the character.

extends CharacterBody3D

# How fast the player moves in meters per second.
@export var speed = 14
# The downward acceleration when in the air, in meters per second squared.
@export var fall_acceleration = 75

var target_velocity = Vector3.ZERO

These are common properties of moving objects. target_velocityis a 3D vector that combines speed and direction. Here we define it as a property because we want to update and reuse its value across frames.

NOTE: These values ​​are very different from QR codes because distances are in meters. In 2D, a thousand units (pixels) might only correspond to half the width of the screen, while in 3D it's one kilometer.

Let's encode the motion. We first _physics_process()中calculate the input direction vector using the global object Input.

func _physics_process(delta):
    # We create a local variable to store the input direction.
    var direction = Vector3.ZERO

    # We check for each move input and update the direction accordingly.
    if Input.is_action_pressed("move_right"):
        direction.x += 1
    if Input.is_action_pressed("move_left"):
        direction.x -= 1
    if Input.is_action_pressed("move_back"):
        # Notice how we are working with the vector's x and z axes.
        # In 3D, the XZ plane is the ground plane.
        direction.z += 1
    if Input.is_action_pressed("move_forward"):
        direction.z -= 1

Here, we will use _physics_process() virtual functions for all calculations. As in _process(), it allows you to update nodes every frame, but it's designed specifically for physics-related code, such as moving kinematics or rigid bodies.

参考:_process()To learn more about the difference between and _physics_process(), see Idle and Physical Processing .

We start by initializing a variable directionto Vector3.ZERO. Then, we check to see if the player has pressed one or more inputs and update the vector and components move_*accordingly . These correspond to the axes of the ground plane.xz

These four conditions give us eight possibilities, eight possible directions.

If the player presses W and D at the same time, the length of the vector will be approx 1.4. But if they press a key, it will have a length of 1. We want the vector to be of the same length, not move faster diagonally. To do this, we can call its normalize()methods.

#func _physics_process(delta):
    #...

    if direction != Vector3.ZERO:
        direction = direction.normalized()
        $Pivot.look_at(position + direction, Vector3.UP)

Here, we only normalize the vector if the length of the direction is greater than zero, which means the player is pressing the arrow key.

In this case we also get Pivotthe node and call its look_at()methods. This method gets a position in space to view in global coordinates and up. In this case we can use Vector3.UPconstants.

Note: A node's local coordinates, eg position, are relative to its parent node. Global coordinates, for example global_position, relative to the world's main axis as you can see it in the viewport.

In 3D, the attribute that contains the position of the node is position. By adding to it direction, we get a Player1 meter distance to look at.

Then, we update the velocity. We have to calculate ground speed and fall speed separately. [Be sure to pay attention to the indentation] so that these lines are _physics_process()inside the function, but outside the condition we just wrote above.

func _physics_process(delta):
    #...
    if direction != Vector3.ZERO:
        #...

    # Ground Velocity
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Vertical Velocity
    if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

    # Moving the Character
    velocity = target_velocity
    move_and_slide()

CharacterBody3D.is_on_floor()The function returns true if the body collided with the floor during this frame . That's why we only Playerapply gravity to it when it's in the air.

For vertical velocity, we subtract the descent acceleration times the delta time per frame. This line of code will cause our character to fall every frame, as long as it doesn't land on or collide with the floor.

The physics engine can only detect interactions with walls, floors, or other objects during a given frame if movement and collisions occur. We'll use this property later to write jump code.

On the last line, we call a powerful method of CharacterBody3D.move_and_slide(),这the CharacterBody3Dclass that allows you to move your character smoothly. If it hits a wall mid-movement, the engine tries to smooth it out for you. It uses the velocity value inherent to CharacterBody3D

That's all the code needed to move the character on the floor.

Here is the full Player.gdcode for reference.

extends CharacterBody3D

# How fast the player moves in meters per second.
@export var speed = 14
# The downward acceleration when in the air, in meters per second squared.
@export var fall_acceleration = 75

var target_velocity = Vector3.ZERO


func _physics_process(delta):
    var direction = Vector3.ZERO

    if Input.is_action_pressed("move_right"):
        direction.x += 1
    if Input.is_action_pressed("move_left"):
        direction.x -= 1
    if Input.is_action_pressed("move_back"):
        direction.z += 1
    if Input.is_action_pressed("move_forward"):
        direction.z -= 1

    if direction != Vector3.ZERO:
        direction = direction.normalized()
        $Pivot.look_at(position + direction, Vector3.UP)

    # Ground Velocity
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Vertical Velocity
    if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

    # Moving the Character
    velocity = target_velocity
    move_and_slide()

Testing our player's actions¶

We'll put our Player in Mainthe scene for testing. To do this, we need to instantiate the player, then add the camera. Unlike 2D, in 3D, if your viewport doesn't have a camera pointing at something, you won't see anything.

Save Playerthe scene and open Mainthe scene. You can do this by clicking on the Main tab at the top of the editor .

If you closed the scene before, go to the file system dock and double-click  main.tscnto reopen it.

To instantiate Player, right click on Mainthe node and select Instance Child Scene .

In the popup, double-click player.tscn. The character should appear in the center of the viewport.

Add camera¶

Next let's add the camera. Just like we did with the Player 's Pivot , we'll create a basic rig. Right-click the Mainnode again and select  Add Child Node . Create a new Marker3D and name it CameraPivot. CameraPivotSelect and add a child node Camera3D to it . Your scene tree should look like this.

When you've selected a camera , notice the preview checkbox that appears in the upper left corner. You can click it to preview the in-game camera projection.

We will use Pivot to rotate the camera like it is on a crane. Let's first split the 3D view so that we can freely navigate the scene and see what the camera sees.

In the toolbar just above the viewports, click View , then click 2 Viewports . You can also press Ctrl + 2 (Cmd + 2 on macOS).

In the bottom view, select your Camera3D and open the camera preview by clicking the checkbox.

In top view, moves the camera 19unit up along the Z axis (blue axis).

This is where the magic happens. Select CameraPivot and rotate degrees around the X axis -45(use the red circle). You'll see the camera move as if hanging from a crane.

You can run the scene by pressing F6 and pressing the arrow keys to move the character.

We can see some empty space around the character due to perspective projection. In this game, we'll use an orthographic projection instead to better frame the game area and make it easier for the player to read distances.

Select Camera again and in the Inspector , set Projection to  Orthogonal and Size to 19. The character should now look flatter, and the ground should fill the background.

Note: When using an orthographic camera in Godot 4, the quality of directional shadows depends on the camera's Far value. The higher the Far value, the farther the camera can see. However, higher Far values ​​also reduce shadow quality, since shadow rendering must cover a greater distance.

If directional shadows look too blurry after switching to an orthographic camera, reduce the camera's Far property to a lower value, eg  100. Don't reduce this Far property too much, or distant objects will start to disappear.

Test your scene and you should be able to move in all 8 directions without glitching on the floor!

Finally, we have both player movement and vision. Next, we'll deal with the monsters.

Design MOB scene¶

In this part, you'll code the monster, which we'll call the mob. In the next lesson we'll randomly generate them around the playable area.

Let's design the monster itself in the new scene. The node structure will player.tscnbe similar to the scene.

Create a scene again with the CharacterBody3D node as its root. name it  Mob. Add a child node Node3D , named as Pivot. and drag and drop the file mob.glbfrom the FileSystem dock to Pivotadd the 3D model of the monster to the scene.

mobYou can rename the newly created node to Character.

We need a collision shape to make our body work. Right-click Mobthe root node of the scene and click Add Child Node .

Add CollisionShape3D .

In the Inspector , assign a BoxShape3D to the Shape property.

We should resize it to better fit the 3D model. You can do this interactively by clicking and dragging the orange dot.

The box should touch the floor and be a little thinner than the model. The way the physics engine works is that if the player's sphere touches a corner of the box, there will be a collision. If the box is a little bigger than the 3D model, you may die far away from the monster, and the game will give the player a sense of unfairness.

Note that my box is taller than the monster. No problem in this game because we are looking at the scene from above and using a fixed perspective. The collision shape does not have to exactly match the model. The feel of the game should dictate their form and size as you test it.

Remove off-screen monsters¶

We'll be spawning monsters periodically throughout the game levels. If we're not careful, their number could grow to infinity, and we don't want that. Each creature instance has a memory and processing cost that we don't want to pay for when the creature is offscreen.

Once a monster is off screen, we don't need it anymore, so we should delete it. Godot has a node VisibleOnScreenNotifier3D that detects when an object leaves the screen, which we'll use to destroy our mob.

Note: When you are constantly instantiating an object, you can use a technique to avoid the cost of creating and destroying instances all the time, called pooling. It consists of pre-creating an array of objects and reusing them over and over again.

You don't need to worry about this when using GDScript. The main reason to use pools is to avoid freezing in garbage collected languages ​​like C# or Lua. GDScript uses a different technique to manage memory, namely reference counting, and it doesn't have that caveat. You can learn more about it here: Memory management .

Select this Mobnode and add a child node VisibleOnScreenNotifier3D . Another box appears, this time pink. The node emits a signal when the box is completely off screen.

Resize it using the orange dots until it covers the entire 3D model.

Encode MOB's Motion¶

Let's implement the monster's motion. We will do this in two steps. First, we'll Mobwrite a script on , defining a function that initializes the monster. Then we will main.tscncode the random generation mechanism in the scene and call the function from there.

Append the script to Mob.

Here's the mobile code to start with. We define two properties min_speed and max_speedto define a random velocity range, which we will use later CharacterBody3D.velocity.

extends CharacterBody3D

# Minimum speed of the mob in meters per second.
@export var min_speed = 10
# Maximum speed of the mob in meters per second.
@export var max_speed = 18


func _physics_process(_delta):
    move_and_slide()

Similar to the player, we CharacterBody3D.move_and_slide()move the mob every frame by calling a function. This time, we don't update every frame velocity; we want the monster to move at a constant speed and leave the screen, even if it hits an obstacle.

We need to define another function to do the calculation CharacterBody3D.velocity. This function will turn the monster towards the player and randomize its movement angle and speed.

This function will take the mob's spawn position start_positionand  player_positionas its parameter.

We position the monster at start_positionand use methods look_at_from_position()to turn it towards the player and randomize the angle by randomly rotating it around the Y axis by an angle. The following code outputs a random value between randf_range()radians -PI/4and radians .PI/4

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -90 and +90 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

We got a random position, now we need a random_speed. function randi_range()will be useful as it provides random int value which we will use min_speed与max_speedrandom_speedJust an integer we just use to multiply our CharacterBody3D.velocity. After applying random_speed, we make the Vector3 velocity vector CharacterBody3D.velocity向the player rotates.

func initialize(start_position, player_position):
    # ...

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

Leaving the screen¶

We still need to destroy the mobs when they go off screen. To do this, we connect the signal of the VisibleOnScreenNotifier3D node to the .screen_exitedMob

Click the 3D tab at the top of the editor to return to the 3D viewport. You can also press Ctrl + F2 (Alt + 2 on macOS).

Select the VisibleOnScreenNotifier3D node, then navigate to the node dock on the right side of the interface. Double click on screen_exited()the signal.

Connect the signal toMob

This will bring you back to the script editor and add a new function for you  _on_visible_on_screen_notifier_3d_screen_exited(). Call the method within that function queue_free() . This function destroys the instance it was called on.

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

Our monster is ready to enter the game! In the next section, you'll spawn monsters in the game level.

Here is the full Mob.gdscript for reference.

extends CharacterBody3D

# Minimum speed of the mob in meters per second.
@export var min_speed = 10
# Maximum speed of the mob in meters per second.
@export var max_speed = 18

func _physics_process(_delta):
    move_and_slide()

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -90 and +90 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

Spawn monsters¶

In this part, we will randomly spawn monsters along a path. By the end, you'll see monsters roaming the game board.

main.tscnDouble-click to open Mainthe scene in the file system .

Before drawing the path, we want to change the game resolution. Our game has a default window size 1152x648. We're going to set it up as 720x540,a nice little box.

Go to Project -> Project Settings .

In the left menu, navigate down to Display -> Window . On the right,  set the width to 720and the height to 540.

Create Build Path¶

Just like you did in the 2D game tutorial, you'll design a path and use a  PathFollow3D node to sample random positions on it.

But in 3D, drawing paths is a bit more complicated. We want it to wrap around the game view so the monsters appear offscreen. But if we draw a path, we won't see it from the camera preview.

To find the limits of the view, we can use some grid of placeholders. Your viewport should still be split in two, with the camera preview at the bottom. If this is not the case, press Ctrl + 2 (Cmd + 2 on macOS) to split the view in two. Select the Camera3D node and click the preview checkbox in the bottom viewport .

Adding a placeholder cylinder¶

Let's add the placeholder grid. Add a new Node3D as  Maina child of node and name it Cylinders. We'll use this to group the cylinders. Select Cylindersand add child node MeshInstance3D

In the Inspector , assign CylinderMesh to the Mesh property.

Use the menu in the upper left corner of the viewport to set the top viewport to a top orthographic view. Alternatively, you can press the 7 key on your keyboard.

Grids can be distracting. You can toggle this by going to the View menu in the toolbar and clicking View Grid .

You now want to move the cylinder along the ground plane, see the camera preview in the bottom viewport. I recommend using grid snapping to do this. You can toggle it by clicking the magnet icon in the toolbar or pressing Y.

Move the cylinder so that it is outside the view of the top left camera.

We'll create copies of the meshes and place them around the play area. Press Ctrl + D (Cmd + D on macOS) to duplicate the node. You can also right-click a node in the scene dock and select Duplicate . Move the copy down the blue Z axis until it is just outside the camera preview.

Select both cylinders by Shift-clicking the unselected cylinders and duplicating them.

Move them to the right by dragging the red X-axis.

White is kind of ugly, isn't it? Let's make them stand out by giving them a new material.

In 3D, a material defines the visual properties of a surface, such as its color, the way it reflects light, and so on. We can use them to change the color of the grid.

We can update all four cylinders at once. Selects all mesh instances in the scene dock. You can do this by clicking the first one, then Shift-clicking the last one.

In the Inspector , expand the Material section and assign StandardMaterial3D to slot  0 .

Click on the sphere icon to open the material resource. You can preview the material and a long list of sections filled with properties. You can use them to create all kinds of surfaces, from metal to rock or water.

Expand the Albedo section.

Set the color to something that contrasts with the background, such as bright orange.

We can now use the cylinder as a guide. Click the gray arrow next to them to collapse them into the scene dock. Going forward, you can also toggle the visibility of Cylinders by clicking the eye icon next to them.

Add child node Path3D to Mainnode. In the toolbar, four icons appear. Click on the Add Point tool, the icon with the green "+" sign.

You can hover over any icon to see a tooltip describing that tool.

Click the center of each cylinder to create a point. Then, click the Close Curve icon in the toolbar to close the path. If any point is a bit off, you can click and drag it to reposition it.

Your path should look like this.

To sample random positions on it, we need a PathFollow3D node. Add a  PathFollow3D as Path3D. Rename these two nodes to SpawnPathand ,  respectively SpawnLocation. It's more descriptive of what we're going to do with them.

With this, we can write the code for the generation mechanism.

Randomly Generated Monsters¶

Right click on the Mainnode and attach a new script to it.

We start by exporting a variable to the Inspector so we can mob.tscn assign it or any other monster.

extends Node

@export var mob_scene: PackedScene

We want mobs to spawn at regular intervals. To do this, we need to go back to the scene and add a timer. Before that though, we need to  mob.tscnassign the file to mob_scenethe property above (otherwise it's empty!)

Return to the 3D screen and select Mainthe node. Drag from the FileSystem dock mob.tscnto the Mob Scene slot in the Inspector .

Add a new Timer node as a child of Main. name it MobTimer.

In the Inspector , set its Wait Time to 0.5seconds and turn on  Autostart so it starts automatically when we run the game.

The timer emits a signal timeoutevery time it reaches the end of the wait time . By default, they are automatically restarted and signaled in a loop. We can connect to this signal from the master node to spawn monsters 0.5every second .

With MobTimer still selected, go to the node dock on the right and double click on the signal timeout.

Connect it to the master node.

This will bring you back to the script with a new empty  _on_mob_timer_timeout()function.

Let's write the MOB generation logic. we want:

  1. Instantiate the MOB scene.

  2. Sample at random positions along the generated path.

  3. Get the player's position.

  4. Call the creature's initialize()method, passing it the random position and the player's position.

  5. Add the creature as a child of the main node .

func _on_mob_timer_timeout():
    # Create a new instance of the Mob scene.
    var mob = mob_scene.instantiate()

    # Choose a random location on the SpawnPath.
    # We store the reference to the SpawnLocation node.
    var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
    # And give it a random offset.
    mob_spawn_location.progress_ratio = randf()

    var player_position = $Player.position
    mob.initialize(mob_spawn_location.position, player_position)

    # Spawn the mob by adding it to the Main scene.
    add_child(mob)

Above, randf()a random value between 0 and 1 is generated, which is what the PathFollow node expects: 0 is the start of the path and 1 is the end of the path. The path we set wraps around the camera's viewport, so any random value between 0 and 1 progress_ratiois a random position at the edge of the viewport!

main.gdHere's the full script so far, for reference.

extends Node

@export var mob_scene: PackedScene


func _on_mob_timer_timeout():
    # Create a new instance of the Mob scene.
    var mob = mob_scene.instantiate()

    # Choose a random location on the SpawnPath.
    # We store the reference to the SpawnLocation node.
    var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
    # And give it a random offset.
    mob_spawn_location.progress_ratio = randf()

    var player_position = $Player.position
    mob.initialize(mob_spawn_location.position, player_position)

    # Spawn the mob by adding it to the Main scene.
    add_child(mob)

You can test the scene by pressing F6. You should see the monster spawn and move in a straight line.

They now collide and slide against each other when their paths cross. We will address this issue in the next section.

Jumping and Squeezing Monsters¶

In this part, we'll add the ability to jump and squeeze the monster. In the next lesson, we'll make the player die when the monster hits the ground.

First, we have to change some settings related to physics interaction. Enter the world of the physical layer.

Controlling Physical Interactions¶

Physical entities can access two complementary properties: layers [layers] and mask [mask]. A layer defines on which physical layer an object is located.

Masks control the layers the body will listen to and detect. This affects collision detection. When you want two objects to interact, you need at least one object to have a corresponding mask to the other.

If this confuses you, don't worry, we'll see three examples in a moment.

The important point is that you can use layers and masks to filter physical interactions, control performance, and eliminate the need for extra conditions in your code.

By default, all physics volume and region layers and masks are set to 1. That means they all collide with each other.

Physical layers are represented by numbers, but we can give them names to keep track of what is what.

Set layer name¶

Let's give the physical layer a name. Go to Project -> Project Settings .

In the left menu, navigate down to Layer Names -> 3D Physics . You can see a list of layers on the right, with a field next to each layer. You can set their names there. Name the first three layers player , enemy and world respectively .

Now, we can assign them to our physical nodes.

Assigning Layers and Masks¶

In the main scene, select Groundthe node. In the Inspector , expand  the Collision section. There, you can treat your nodes' layers and masks as a grid of buttons.

The ground is part of the world, so we want it to be part of the third layer. Click the lit button to close the first layer and open the third layer. Then, close the mask by clicking it .

As mentioned earlier, the Mask property allows the node to listen for interactions with other physics objects, but we don't need it for collisions. GroundDoesn't need to listen to anything; it's just there to prevent mobs from falling.

Note that you can click the "..." button to the right of the property to see a list of named checkboxes.

Next is Playerand Mob. Open by double- clicking a file in the file system dock player.tscn.

Select the Player node and set its Collision -> Mask to "enemies" and "world". You can leave the default Layer property unchanged, since the first layer is the "player" layer.

Then, double-click to open the Mobmob.tscn scene and select  Mobthe node.

Set its Collision -> Layer to "enemies" and unset its Collision -> Mask so the mask is empty.

These settings mean that the monsters will move around each other. If you want the monsters to collide and slide with each other, turn on the " enemy" mask.

Note: Creatures don't need to mask the "world" layer, since they only move on the XZ plane. We don't put any gravity on them by design.

Jump¶ _

The jumping mechanism itself requires only two lines of code. Open the player  script. We need a value to control the strength of the jump and update  _physics_process()to encode the jump.

After the defined fall_accelerationline, at the top of the script, add the jump_impulse.

#...
# Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20

Inside _physics_process(), add the following code before the code block move_and_slide().

func _physics_process(delta):
    #...

    # Jumping.
    if is_on_floor() and Input.is_action_just_pressed("jump"):
        target_velocity.y = jump_impulse

    #...

That's all you need to jump!

This is_on_floor()method is a tool from this class CharacterBody3D. trueIt returns if the body collides with the floor in this frame. That's why we apply gravity to the Player : so we collide with the floor instead of floating on it like a monster.

If the character is on the floor and the player presses "jump", we immediately give them a lot of vertical velocity. In gaming, you really want the controls to be responsive and provide an instant speed boost like this, which isn't practical, but feels great.

Note that the Y axis is positive upwards. This is different from 2D, where the Y axis is positive down.

Squash Monsters¶

Next let's add the squash mechanic. We want the character to bounce on monsters and kill them at the same time.

We need to detect collisions with monsters and distinguish them from collisions with the floor. For this, we can use Godot's group tagging feature.

Open the scene again mob.tscnand select the Mob node. Go to the node dock on the right to see a list of signals . The Node dock has two tabs: Signals , which you already use, and Groups  , which allow you to assign labels to nodes .

Click it to reveal a field where you can write the tag name. Enter "mob" in the field and click the Add button.

An icon appears in the scene dock indicating that the node belongs to at least one group.

We can now use groups in code to differentiate between collisions with monsters and collisions with the floor.

Encoding Squeeze Mechanism¶

Go back to the Player script to write the squeeze and bounce code.

At the top of the script, we need another property bounce_impulse. When squashing an enemy, we don't necessarily want the character to fly as high as it does when jumping.

# Vertical impulse applied to the character upon bouncing over a mob in
# meters per second.
@export var bounce_impulse = 16

Then, after the Jumping_physics_process() code block we added above , add the following loop. When used  , Godot sometimes moves the body multiple times in succession to smooth out the character's motion. So we have to go through all possible collisions.move_and_slide()

On each iteration of the loop, we check to see if we've landed on a mob. If so, we kill it and bounce.

With this code, the loop doesn't run if there is no collision on a given frame.

func _physics_process(delta):
   #...

   # Iterate through all collisions that occurred this frame
   for index in range(get_slide_collision_count()):
       # We get one of the collisions with the player
       var collision = get_slide_collision(index)

       # If the collision is with ground
       if (collision.get_collider() == null):
           continue

       # If the collider is with a mob
       if collision.get_collider().is_in_group("mob"):
           var mob = collision.get_collider()
           # we check that we are hitting it from above.
           if Vector3.UP.dot(collision.get_normal()) > 0.1:
               # If so, we squash it and bounce.
               mob.squash()
               target_velocity.y = bounce_impulse

That's a lot of new features. Here's some more information about them.

Functions get_slide_collision_count()and get_slide_collision()both come from the CharacterBody3D class and are compatible with  move_and_slide()相关.

get_slide_collision()Returns a  KinematicCollision3D object that contains information about where and how the collision occurred. For example, we use its get_colliderproperties is_in_group()to check if we collided with a "mob" by calling: collision.get_collider().is_in_group("mob")

Note: This is_in_group()method is available on every Node .

To check if we landed on a monster, we use the vector dot product: Vector3.UP.dot(collision.get_normal()) > 0.1. A collision normal is a 3D vector perpendicular to the plane on which the collision occurred. The dot product allows us to compare it to the up direction.

For a dot product, the angle between the two vectors is less than 90 degrees when the result is greater than 0. A value above 0.1tells us that we are roughly above the monster.

We're calling an undefined function mob.squash(), so we have to add it to the Mob class.

Open the script by double-clicking it in the file system dock Mob.gd. At the top of the script, we're going to define a squashednew signal called . At the bottom, you can add the squash function, where we can send a signal and destroy the mob.

# Emitted when the player jumped on the mob.
signal squashed

# ...


func squash():
    squashed.emit()
    queue_free()

We'll use this signal to add points to the score in the next lesson.

With it, you should be able to kill monsters by jumping on them. You can try the game by pressing F5 and set it main.tscnas the main scene of the project.

However, the player does not die yet. We'll deal with that in the next section.

Killing Players¶

We can kill enemies by jumping on them, but the player still can't die. Let's get this out of the way.

We want to detect being hit by an enemy differently than squishing them. We want the player to die while moving on the floor, but not in the air. We can use vector math to distinguish between these two types of collisions. However, we will use the Area3D node, which works with collision boxes.

Hitbox with Area node¶

Go back to player.tscnthe scene and add a new child node Area3D . Name it  MobDetector Add a CollisionShape3D node as its child.

In the Inspector , give it a Cylinder shape.

This is a trick you can use to make collisions only happen when the player is on or close to the ground. You can lower the height of the cylinder and move it up to the top of the character. This way, when the player jumps, the shape will be so tall that enemies cannot collide with it.

You also want the cylinder to be wider than the sphere. This way, the player is hit before colliding and being pushed to the top of the monster's hitbox.

The wider the cylinder, the easier it is for the player to be killed.

Next , select the MobDetectornode again and turn off its Monitorable property in the Inspector . This makes the area undetectable to other physical nodes. The complementary Monitoring property allows it to detect collisions. Then, remove Collision -> Layer and set the mask to the "enemies" layer.

They emit a signal when a zone detects a collision. We'll connect one to Playerthe node. Select MobDetectorand go to the Nodes tab  of the Inspector , double click on the signal and connect it tobody_enteredPlayer

MobDetector will fire when a CharacterBody3D or  RigidBody3D node comes in . Since it only shields the "enemy" physical layer, it will only detect nodes. ​​​​​​​​body_enteredMob

On the code side, we're going to do two things: emit a signal, which we'll use later to end the game and destroy the player. We can wrap these operations in a die()function, which helps us put descriptive labels on our code.

# Emitted when the player was hit by a mob.
# Put this at the top of the script.
signal hit


# And this function at the bottom.
func die():
    hit.emit()
    queue_free()


func _on_mob_detector_body_entered(body):
    die()

Try the game again by pressing F5. If everything is set up correctly, the character should die when an enemy hits the collider. Note that no Player, the following line

var player_position = $Player.position

gives an error because there is no $Player!

Also note that enemies that collide with the player and die depend on  the size and position of Playerthe and Mobcollider shapes. You may need to move and resize them to get a compact gaming feel.

End Game¶

Signals we can use Playerto hitend the game. All we need to do is connect it to Maina node and stop MobTimerreacting.

Open main.tscn, select Playerthe node, and in the Node  dock, connect its hitsignal to Mainthe node.

Get the timer in the function and stop it _on_player_hit().

func _on_player_hit():
    $MobTimer.stop()

If you try the game now, the mobs will stop spawning after you die and the rest will go off screen.

You can give yourself credit for making a complete 3D game prototype, even if it's a bit rough.

From there, we'll add a score, the option to retry the game, and you'll see how minimalistic animations can be used to make the game feel more alive.

Code Checkpoints¶

MainBelow is the full script for the , Moband Playernode for reference. You can use them to compare and check your code.

from main.gdthe beginning

extends Node

@export var mob_scene: PackedScene


func _on_mob_timer_timeout():
    # Create a new instance of the Mob scene.
    var mob = mob_scene.instantiate()

    # Choose a random location on the SpawnPath.
    # We store the reference to the SpawnLocation node.
    var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
    # And give it a random offset.
    mob_spawn_location.progress_ratio = randf()

    var player_position = $Player.position
    mob.initialize(mob_spawn_location.position, player_position)

    # Spawn the mob by adding it to the Main scene.
    add_child(mob)

func _on_player_hit():
    $MobTimer.stop()

Next is Mob.gd.

extends CharacterBody3D

# Minimum speed of the mob in meters per second.
@export var min_speed = 10
# Maximum speed of the mob in meters per second.
@export var max_speed = 18

# Emitted when the player jumped on the mob
signal squashed

func _physics_process(_delta):
    move_and_slide()

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -90 and +90 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

func squash():
    squashed.emit()
    queue_free() # Destroy this node

Finally, the longest script Player.gd:

extends CharacterBody3D

signal hit

# How fast the player moves in meters per second
@export var speed = 14
# The downward acceleration while in the air, in meters per second squared.
@export var fall_acceleration = 75
# Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20
# Vertical impulse applied to the character upon bouncing over a mob
# in meters per second.
@export var bounce_impulse = 16

var target_velocity = Vector3.ZERO


func _physics_process(delta):
    # We create a local variable to store the input direction
    var direction = Vector3.ZERO

    # We check for each move input and update the direction accordingly
    if Input.is_action_pressed("move_right"):
        direction.x = direction.x + 1
    if Input.is_action_pressed("move_left"):
        direction.x = direction.x - 1
    if Input.is_action_pressed("move_back"):
        # Notice how we are working with the vector's x and z axes.
        # In 3D, the XZ plane is the ground plane.
        direction.z = direction.z + 1
    if Input.is_action_pressed("move_forward"):
        direction.z = direction.z - 1

    # Prevent diagonal moving fast af
    if direction != Vector3.ZERO:
        direction = direction.normalized()
        $Pivot.look_at(position + direction, Vector3.UP)

    # Ground Velocity
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Vertical Velocity
    if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

    # Jumping.
    if is_on_floor() and Input.is_action_just_pressed("jump"):
        target_velocity.y = jump_impulse

    # Iterate through all collisions that occurred this frame
    # in C this would be for(int i = 0; i < collisions.Count; i++)
    for index in range(get_slide_collision_count()):
        # We get one of the collisions with the player
        var collision = get_slide_collision(index)

        # If the collision is with ground
        if (collision.get_collider() == null):
            continue

        # If the collider is with a mob
        if collision.get_collider().is_in_group("mob"):
            var mob = collision.get_collider()
            # we check that we are hitting it from above.
            if Vector3.UP.dot(collision.get_normal()) > 0.1:
                # If so, we squash it and bounce.
                mob.squash()
                target_velocity.y = bounce_impulse

    # Moving the Character
    velocity = target_velocity
    move_and_slide()

# And this function at the bottom.
func die():
    hit.emit()
    queue_free()

func _on_mob_detector_body_entered(body):
    die()

See you in the next lesson, adding scores and replay options.

Score and Replay¶

In this part, we'll add scoring, music playback, and the ability to restart the game.

We have to keep track of the current score in a variable and display it on screen using a minimal interface. We'll use text labels to do this.

In the main scene, add Maina new child node Control and name it UserInterface. You will automatically enter the 2D screen where you can edit the user interface (UI).

Add a Label node and name itScoreLabel

In the Inspector , set the Label 's text as a placeholder, such as "Score: 0".

Also, the text defaults to white, just like our game's background. We need to change its color to see it at runtime.

Scroll down to Theme Overrides , expand Colors  and enable Font Color in order to color the text black (contrasted with the white 3D scene)

Finally, click and drag the text in the viewport away from the upper left corner.

This UserInterfacenode allows us to group UI into one branch of the scene tree and use theme resources that will be propagated to all its children. We'll use this to set our game's font.

Creating UI Themes¶

Select the node again UserInterface. In the Inspector, create a new theme resource in Theme -> Theme .

Click it to open the theme editor in the bottom panel. It allows you to preview how all built-in UI widgets and your theme resources will look.

By default, a theme has only one property, Default Font .

See: You can add more properties to theme resources to design complex user interfaces, but that's beyond the scope of this series. To learn more about creating and editing themes, see Introduction to GUI Skins .

This requires a font file, just like the font files on your computer. Two common font file formats are TrueType fonts (TTF) and OpenType fonts (OTF).

In the FileSystem  dock, expand fontsthe directory and click on the file we included in the project Montserrat-Medium.ttfand drag it  onto the Default Font . The text will reappear in the theme preview.

Text is a bit small. Set the default font size to 22pixels to increase the size of the text.

Tracking scores¶

Next let's look at fractions. Attach a new script to ScoreLabeland define scorevariables.

extends Label

var score = 0

Every time we squash a monster, the score should increase by 1. We can use their squashedsignals to know when something is happening. However, because we instantiate the mob from code, our 通过editor cannot connect the mob signal to it ScoreLabel.

Instead, we have to make the connection from code every time a monster is spawned.

Open the script main.gd. If it's still open, you can click on its name in the left column of the script editor.

Alternatively, you can double-click the file in the file systemmain.gd dock.

At the bottom of the function _on_mob_timer_timeout(), add the following lines:

func _on_mob_timer_timeout():
    #...
    # We connect the mob to the score label to update the score upon squashing one.
    mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind())

What this line means is that when the creature emits a signal squashedScoreLabelthe node will receive the signal and call the function _on_mob_squashed().

Return to ScoreLabel.gdthe script to define _on_mob_squashed() the callback function.

There we increment the score and update the displayed text.

func _on_mob_squashed():
    score += 1
    text = "Score: %s" % score

The second line uses the value of the variable scorein place of the placeholder %s. When using this feature, Godot will automatically convert the value to a string literal, which is convenient for outputting text in labels or using functions print().

See: You can learn more about string formatting here: GDScript formatting strings . In C#, consider using string interpolation with "$" .

You can now play the game and squash some enemies to see your score increase.

Note: In complex games, you may want to completely separate the user interface from the game world. In that case you don't keep track of the score on the label. Instead, you probably want to store it in a separate dedicated object. But when prototyping or when your project is simple, it's good to keep your code simple. Programming is always a balancing act.

Replay the game¶

We will now add the ability to play again after dying. When the player dies, we will display a message on the screen and wait for input.

Back in main.tscnthe scene, select UserInterfacethe node, add a child node ColorRect , and name it Retry. This node fills a rectangle with a uniform color and will be used as an overlay to darken the screen.

To make it span the entire viewport, you can use the Anchor Presets menu in the toolbar.

Open it and apply the Full Rect command.

Nothing happened. Well, almost nothing; just four green pushpins moving to the corners of the selection box.

This is because UI nodes (all nodes with a green icon) use anchors and margins relative to their parent's bounding box. Here, UserInterfacethe size of the nodes is small and Retryconstrained by it.

Select UserInterfaceand apply Anchor Preset -> Full Rect . The  Retrynode should now span the entire viewport.

Let's change its color to darken the play area. Select Retryand in  the Inspector , set its Color to a dark and transparent color. To do this, in the color picker, drag the A slider to the left. It controls the alpha channel of the color, that is, its opacity/transparency.

Next, add a Label as a child Retryand give it the text  "Press Enter to retry". To move it and anchor it to the center of the screen, apply Anchor Preset -> Center to it.

Coding Replay Options¶

We can now use code to Retryshow and hide nodes when the player dies and plays the game again.

Open the script main.gd. First, we want to hide the overlay when the game starts. Add this line to _ready()the function.

func _ready():
    $UserInterface/Retry.hide()

We then display the overlay when the player is hit.

func _on_player_hit():
    #...
    $UserInterface/Retry.show()

Finally, Retrywe need to listen for player input when the node is visible, and restart the game if they press enter. For this, we use the built-in  _unhandled_input()callback, which fires on any input.

If the player presses a predefined ui_acceptinput action and Retryit is visible, we will reload the current scene.

func _unhandled_input(event):
    if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
        # This restarts the current scene.
        get_tree().reload_current_scene()

This function get_tree()gives us access to the global SceneTree object, which allows us to reload and restart the current scene.

Add music¶

To add music that plays continuously in the background, we'll use another feature in Godot: autoloading .

To play audio, all you need to do is add an AudioStreamPlayer node to your scene and attach an audio file to it. When you start a scene, it can play automatically. However, when you reload the scene, as if we were playing it again, the audio node resets and the music starts playing from the beginning.

You can use the autoload feature to have Godot automatically load a node or scene other than the current one when the game starts. You can also use it to create globally accessible objects.

Create a new scene by going to the "Scenes" menu and clicking "New Scene" or by using the + icon  next to the currently open scene .

Click the Other Node button to create an AudioStreamPlayer and rename it to  MusicPlayer.

art/We included the music soundtrack in the catalog House In a Forest Loop.ogg,click and drag it onto the Stream property in the Inspector . Also, turn on Autoplay to automatically play music when the game starts.

Save the scene as MusicPlayer.tscn.

We have to register it for autoloading. Go to the Project -> Project Settings… menu and click on the Autoload tab.

In the Path field, you enter the path to the scene. Click the folder icon to open the file browser and double-click MusicPlayer.tscn. Then, click the Add button on the right to register the node.

MusicPlayer.tscnNow load into any scene you open or play. So if you run the game now, the music will play automatically in any scene.

Before we wrap up this lesson, let's take a quick look at how it works. When you run the game, your Scene  dock changes to give you two tabs:  Remote and Local .

The Remote tab allows you to visualize the node tree of the running game. There you'll see the main node and everything the scene contains and the instanced creature at the bottom.

At the top is the autoload node MusicPlayerand the root node , which is your game's viewport.

That's what this lesson is about. In the next part, we'll add an animation to make the game look and feel better.

Here is the full main.gdscript for reference.

extends Node

@export var mob_scene: PackedScene

func _ready():
    $UserInterface/Retry.hide()


func _on_mob_timer_timeout():
    # Create a new instance of the Mob scene.
    var mob = mob_scene.instantiate()

    # Choose a random location on the SpawnPath.
    # We store the reference to the SpawnLocation node.
    var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
    # And give it a random offset.
    mob_spawn_location.progress_ratio = randf()

    var player_position = $Player.position
    mob.initialize(mob_spawn_location.position, player_position)

    # Spawn the mob by adding it to the Main scene.
    add_child(mob)

    # We connect the mob to the score label to update the score upon squashing one.
    mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind())

func _on_player_hit():
    $MobTimer.stop()
    $UserInterface/Retry.show()

func _unhandled_input(event):
    if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
        # This restarts the current scene.
        get_tree().reload_current_scene()

Character animation¶

In this final lesson, we'll use Godot's built-in animation tools to make our character levitate and flap. You'll learn to design animations in the editor and use code to bring your game to life.

We'll start with an introduction to using the animation editor.

Using the animation editor¶

The engine comes with tools for authoring animations in the editor. You can then use code to play and control them at runtime.

Open the player scene, select Playerthe node, and add an AnimationPlayer node.

The animation dock appears in the bottom panel.

It has a toolbar and animation dropdown at the top, a currently empty track editor in the middle, and filtering, snapping, and zooming options at the bottom.

Let's create an animation. Click Animation -> New .

Name the animation "float".

Once you've created an animation, a timeline will appear with numbers representing the time in seconds.

We want the animation to start playing automatically when the game starts. Also, it should loop.

You can do this by clicking the button with the "A+" icon and the cycle arrow respectively in the animation toolbar.

You can also pin the animation editor by clicking the pin icon in the upper right corner. This prevents it from collapsing when you click in the viewport and deselect the node.

Set the animation duration to 1.2seconds in the upper right corner of the dock.

You should see the gray ribbon widen a bit. It shows you where the animation starts and ends, and the vertical blue line is your time cursor.

You can click and drag the slider in the lower right to zoom in and out of the timeline.

Float animation¶

Using animation player nodes, you can animate most properties on as many nodes as you want. Note the key icon next to the property in the Inspector. You can click on any of them to create keyframe, time, and value pairs for the corresponding property. A keyframe is inserted in the timeline at the location of the time cursor.

Let's insert our first key. Here we will animate the position and rotation of the node Character.

Select Characterand expand the Transform section in the Inspector . Click on the key icon next to Position and Rotation .

For this tutorial, just create the default selected RESET Track(s)

Two tracks appear in the editor with a diamond icon for each keyframe.

You can click and drag the diamonds to move them in time. Move the position key to 0.2seconds and the rotation key to 0.1seconds.

Move the time cursor to seconds by clicking and dragging on the gray timeline 0.5.

In the Inspector , set the Y axis of Position to meters and the X axis of Rotation to .0.658

Create a keyframe for both properties

0.7 Now, move the position keyframe to seconds by dragging on the timeline.

Note: A lecture on animation principles is beyond the scope of this tutorial. Note that you don't want to schedule time and space evenly. Instead, animators use time and intervals, two core animation principles. You want to offset and contrast your characters' movements so they feel alive.

Moves the time cursor to the end of the animation, in 1.2seconds. Set Y Position to Approximate 0.35and X Rotation to -9Degrees. Create a key again for these two properties.

You can preview the result by clicking the play button or pressing Shift+D. Click the stop button S or press to stop playback.

You can see the engine interpolate between keyframes to produce a continuous animation. For now, though, the action feels very robotic. This is because the default interpolation is linear, resulting in constant transitions, unlike how creatures move in the real world.

We can use easing curves to control the transition between keyframes.

Click and drag the first two keys in the timeline to frame select them.

You can edit the properties of both keys at the same time in the Inspector, where you can see an Easing property.

Click and drag the curve to pull it to the left. This will ease it out, that is, the transition is fast initially and slows down as the time cursor reaches the next keyframe.

Play the animation again to see the difference. The first half should already feel a bit snappy.

Applies ease-out to the second keyframe in the rotation track.

Do the opposite for the second position keyframe, dragging it to the right.

Your animation should look like this.

Note: The animation updates the properties of the animation node every frame, overwriting the initial value. If we animate the Player node directly , it prevents us from moving it in code. This is where the Pivot node comes in handy: even if we animate the Character , we can still move and rotate the Pivot in script and change layers on top of the animation.

Player creatures will now float if you play the game!

If the creature is a little too close to the ground, you can Pivotmove up to counteract it.

Control animations in code¶

We can use code to control animation playback based on player input. Let's change the animation speed when the character moves.

A script opened by clicking the script icon next to it Player.

In _physics_process(), after we check the rows of the vector direction , add the following code.

func _physics_process(delta):
    #...
    if direction != Vector3.ZERO:
        #...
        $AnimationPlayer.speed_scale = 4
    else:
        $AnimationPlayer.speed_scale = 1

This code makes it so that we multiply the playback speed by when the player moves  4. When they stop, we reset them to normal.

We mentioned that Pivotthe could layer transforms on top of the animation. We can use the following line of code to make a character arc while jumping. Add it at the end _physics_process().

func _physics_process(delta):
    #...
    $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse

Animated creatures¶

This is another nice trick for animations in Godot: as long as you use a similar node structure, you can copy them into different scenes.

For example, both scenes Moband Playerscenes have PivotCharacternode, so we can reuse animations between them.

Open the Player scene, select the AnimationPlayer node and turn on the "floating" animation. Next, click Animation > Duplicate . Then open mob.tscn, create an AnimationPlayer child node and select it. Click Animation > Paste  and make sure the button with the "A+" icon (autoplay on load) and the loop arrow (animation loop) are also turned on in the animation editor in the bottom panel. That's it; all mobs will now play the floating animation.

We can change the playback speed according to the creature random_speed. Open Mob 's script and add the following line at the end of the function initialize().

func initialize(start_position, player_position):
    #...
    $AnimationPlayer.speed_scale = random_speed / min_speed

With this, you have coded your first full 3D game.

congratulations !

In the next section, we'll quickly review what you've learned and give you some links to continue learning more. But for now, here are the full ones Player.gdMob.gdso you can check your code against them.

Here is the player script.

extends CharacterBody3D

signal hit

# How fast the player moves in meters per second.
@export var speed = 14
# The downward acceleration while in the air, in meters per second squared.
@export var fall_acceleration = 75
# Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20
# Vertical impulse applied to the character upon bouncing over a mob
# in meters per second.
@export var bounce_impulse = 16

var target_velocity = Vector3.ZERO


func _physics_process(delta):
    # We create a local variable to store the input direction
    var direction = Vector3.ZERO

    # We check for each move input and update the direction accordingly
    if Input.is_action_pressed("move_right"):
        direction.x = direction.x + 1
    if Input.is_action_pressed("move_left"):
        direction.x = direction.x - 1
    if Input.is_action_pressed("move_back"):
        # Notice how we are working with the vector's x and z axes.
        # In 3D, the XZ plane is the ground plane.
        direction.z = direction.z + 1
    if Input.is_action_pressed("move_forward"):
        direction.z = direction.z - 1

    # Prevent diagonal movement being very fast
    if direction != Vector3.ZERO:
        direction = direction.normalized()
        $Pivot.look_at(position + direction,Vector3.UP)
        $AnimationPlayer.speed_scale = 4
    else:
        $AnimationPlayer.speed_scale = 1

    # Ground Velocity
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Vertical Velocity
    if not is_on_floor(): # If in the air, fall towards the floor
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

    # Jumping.
    if is_on_floor() and Input.is_action_just_pressed("jump"):
        target_velocity.y = jump_impulse

    # Iterate through all collisions that occurred this frame
    # in C this would be for(int i = 0; i < collisions.Count; i++)
    for index in range(get_slide_collision_count()):
        # We get one of the collisions with the player
        var collision = get_slide_collision(index)

        # If the collision is with ground
        if (collision.get_collider() == null):
            continue

        # If the collider is with a mob
        if collision.get_collider().is_in_group("mob"):
            var mob = collision.get_collider()
            # we check that we are hitting it from above.
            if Vector3.UP.dot(collision.get_normal()) > 0.1:
                # If so, we squash it and bounce.
                mob.squash()
                target_velocity.y = bounce_impulse

    # Moving the Character
    velocity = target_velocity
    move_and_slide()

    $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse

# And this function at the bottom.
func die():
    hit.emit()
    queue_free()

func _on_mob_detector_body_entered(body):
    die()

There's also a script from The  Mob .

extends CharacterBody3D

# Minimum speed of the mob in meters per second.
@export var min_speed = 10
# Maximum speed of the mob in meters per second.
@export var max_speed = 18

# Emitted when the player jumped on the mob
signal squashed

func _physics_process(_delta):
    move_and_slide()

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -90 and +90 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

    $AnimationPlayer.speed_scale = random_speed / min_speed

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

func squash():
    squashed.emit()
    queue_free() # Destroy this node​​​​​​​

Going further¶

You can take comfort in the fact that you've made your first 3D game using Godot.

In this series, we've covered a wide range of techniques and editor features. Hopefully you've seen how intuitive Godot's scene system is, and learned some tricks you can apply to your projects.

But we've only scratched the surface: Godot saves you time creating games and so much more. You can learn all about this by browsing the documentation.

Where should you start? Below, you'll find a few pages to start exploring and building on what you've learned so far.

But before that, here is a link to download the full version of the project:  https://github.com/godotengine/godot-3d-dodge-the-creeps .

Browse the manual¶

Whenever you have questions or are curious about a feature, the manual is your ally. It does not contain tutorials on specific game types or mechanics. Instead, it explains how Godot works in general. In it you'll find information on 2D, 3D, physics, rendering and performance.

Here are the sections we suggest you explore next:

  1. Read the scripting section to learn about the basic programming functions you will use in each project.

  2. The 3D and Physics section will teach you more about creating 3D games in the engine .

  3. Input is another important input for any game project.

You can start with these, or, if you prefer, check out the sidebar menu on the left and choose your option.

We hope you enjoyed this tutorial series, and we look forward to seeing what you achieve with Godot.

Guess you like

Origin blog.csdn.net/drgraph/article/details/130814519
Recommended