Update "Your first 3D game" tutorial for Godot 4 (#6243)

* FULLY REMADE FOR 4.0

Co-authored-by: Johannes Loepelmann <johannes@loepelmann.de>
This commit is contained in:
TheYellowArchitect
2023-01-11 20:52:51 +00:00
committed by GitHub
parent 7c194d9d7b
commit 41252827ca
100 changed files with 700 additions and 609 deletions

View File

@@ -14,23 +14,23 @@ the archive here: `Squash the Creeps assets
Once you downloaded it, extract the .zip archive on your computer. Open the
Godot project manager and click the *Import* button.
|image1|
.. image:: img/01.game_setup/01.import_button.png
In the import popup, enter the full path to the freshly created directory
``squash_the_creeps_start/``. You can click the *Browse* button on the right to
open a file browser and navigate to the ``project.godot`` file the folder
contains.
|image2|
.. image:: img/01.game_setup/02.browse_to_project_folder.png
Click *Import & Edit* to open the project in the editor.
|image3|
.. image:: img/01.game_setup/03.import_and_edit.png
The start project contains an icon and two folders: ``art/`` and ``fonts/``.
There, you will find the art assets and music we'll use in the game.
|image4|
.. image:: img/01.game_setup/04.start_assets.png
There are two 3D models, ``player.glb`` and ``mob.glb``, some materials that
belong to these models, and a music track.
@@ -38,44 +38,47 @@ belong to these models, and a music track.
Setting up the playable area
----------------------------
We're going to create our main scene with a plain *Node* as its root. In the
We're going to create our main scene with a plain :ref:`Node <class_Node>` as its root. In the
*Scene* dock, click the *Add Node* button represented by a "+" icon in the
top-left and double-click on *Node*. Name the node "Main". Alternatively, to add
top-left and double-click on *Node*. Name the node ``Main``. Alternatively, to add
a node to the scene, you can press :kbd:`Ctrl + a` (or :kbd:`Cmd + a` on macOS).
|image5|
.. image:: img/01.game_setup/05.main_node.png
Save the scene as ``Main.tscn`` by pressing :kbd:`Ctrl + s` (:kbd:`Cmd + s` on macOS).
We'll start by adding a floor that'll prevent the characters from falling. To
create static colliders like the floor, walls, or ceilings, you can use
*StaticBody* nodes. They require *CollisionShape* child nodes to
define the collision area. With the *Main* node selected, add a *StaticBody*
node, then a *CollisionShape*. Rename the *StaticBody* as *Ground*.
create static colliders like the floor, walls, or ceilings, you can use :ref:`StaticBody3D <class_StaticBody3D>` nodes. They require :ref:`CollisionShape3D <class_CollisionShape3D>` child nodes to
define the collision area. With the ``Main`` node selected, add a :ref:`StaticBody3D <class_StaticBody3D>`
node, then a :ref:`CollisionShape3D <class_CollisionShape3D>`. Rename the :ref:`StaticBody3D <class_StaticBody3D>` to ``Ground``.
|image6|
.. image:: img/01.game_setup/adding_static_body3D.webp
A warning sign next to the *CollisionShape* appears because we haven't defined
Your scene tree should look like this
.. image:: img/01.game_setup/06.staticbody_node.png
A warning sign next to the :ref:`CollisionShape3D <class_CollisionShape3D>` appears because we haven't defined
its shape. If you click the icon, a popup appears to give you more information.
|image7|
.. image:: img/01.game_setup/07.collision_shape_warning.png
To create a shape, with the *CollisionShape* selected, head to the *Inspector*
and click the *[empty]* field next to the *Shape* property. Create a new *Box
Shape*.
To create a shape, select the :ref:`CollisionShape3D <class_CollisionShape3D>` node, head to the *Inspector*
and click the *<empty>* field next to the *Shape* property. Create a new *Box
Shape3D*.
|image8|
.. image:: img/01.game_setup/08.create_box_shape3D.jpg
The box shape is perfect for flat ground and walls. Its thickness makes it
reliable to block even fast-moving objects.
A box's wireframe appears in the viewport with three orange dots. You can click
and drag these to edit the shape's extents interactively. We can also precisely
set the size in the inspector. Click on the *BoxShape* to expand the resource.
set the size in the inspector. Click on the *BoxShape3D* to expand the resource.
Set its *Extents* to ``30`` on the X axis, ``1`` for the Y axis, and ``30`` for
the Z axis.
|image9|
.. image:: img/01.game_setup/09.box_extents.webp
.. note::
@@ -85,80 +88,70 @@ the Z axis.
axis represents the height.
Collision shapes are invisible. We need to add a visual floor that goes along
with it. Select the *Ground* node and add a *MeshInstance* as its child.
with it. Select the ``Ground`` node and add a :ref:`MeshInstance <class_MeshInstance3D>` as its child.
|image10|
.. image:: img/01.game_setup/10.mesh_instance3d.png
In the *Inspector*, click on the field next to *Mesh* and create a *CubeMesh*
In the *Inspector*, click on the field next to *Mesh* and create a *BoxMesh*
resource to create a visible cube.
|image11|
.. image:: img/01.game_setup/11.box_mesh.webp
Once again, it's too small by default. Click the cube icon to expand the
resource and set its *Size* to ``60``, ``2``, and ``60``. As the cube
resource works with a size rather than extents, we need to use these values so
it matches our collision shape.
|image12|
.. image:: img/01.game_setup/12.cube_resized.png
You should see a wide grey slab that covers the grid and blue and red axes in
the viewport.
We're going to move the ground down so we can see the floor grid. Select the
*Ground* node, hold the :kbd:`Ctrl` key down to turn on grid snapping (:kbd:`Cmd` on macOS),
``Ground`` node, hold the :kbd:`Ctrl` key down to turn on grid snapping (:kbd:`Cmd` on macOS),
and click and drag down on the Y axis. It's the green arrow in the move gizmo.
|image13|
.. image:: img/01.game_setup/13.move_gizmo_y_axis.png
.. note::
If you can't see the 3D object manipulator like on the image above, ensure
the *Select Mode* is active in the toolbar above the view.
|image14|
.. image:: img/01.game_setup/14.select_mode_icon.png
Move the ground down ``1`` meter. A label in the bottom-left corner of the
Move the ground down ``1`` meter, in order to have a visible editor grid. A label in the bottom-left corner of the
viewport tells you how much you're translating the node.
|image15|
.. image:: img/01.game_setup/15.translation_amount.png
.. note::
Moving the *Ground* node down moves both children along with it.
Ensure you move the *Ground* node, **not** the *MeshInstance* or the
*CollisionShape*.
Ensure you move the *Ground* node, **not** the *MeshInstance3D* or the
*CollisionShape3D*.
Let's add a directional light so our scene isn't all grey. Select the *Main*
node and add a *DirectionalLight* as a child of it. We need to move it and
rotate it. Move it up by clicking and dragging on the manipulator's green arrow
Ultimately, ``Ground``'s transform.position.y should be -1
.. image:: img/01.game_setup/ground_down1meter.webp
Let's add a directional light so our scene isn't all grey. Select the ``Main``
node and add a child node :ref:`DirectionalLight <class_DirectionalLight3D>`.
.. image:: img/01.game_setup/create_directional_light3d.webp
We need to move and rotate the :ref:`DirectionalLight <class_DirectionalLight3D>` node.
Move it up by clicking and dragging on the manipulator's green arrow
and click and drag on the red arc to rotate it around the X axis, until the
ground is lit.
In the *Inspector*, turn on *Shadow -> Enabled* by clicking the checkbox.
|image16|
.. image:: img/01.game_setup/16.turn_on_shadows.webp
At this point, your project should look like this.
|image17|
.. image:: img/01.game_setup/17.project_with_light.webp
That's our starting point. In the next part, we will work on the player scene
and base movement.
.. |image1| image:: img/01.game_setup/01.import_button.png
.. |image2| image:: img/01.game_setup/02.browse_to_project_folder.png
.. |image3| image:: img/01.game_setup/03.import_and_edit.png
.. |image4| image:: img/01.game_setup/04.start_assets.png
.. |image5| image:: img/01.game_setup/05.main_node.png
.. |image6| image:: img/01.game_setup/06.staticbody_node.png
.. |image7| image:: img/01.game_setup/07.collision_shape_warning.png
.. |image8| image:: img/01.game_setup/08.create_box_shape.png
.. |image9| image:: img/01.game_setup/09.box_extents.png
.. |image10| image:: img/01.game_setup/10.mesh_instance.png
.. |image11| image:: img/01.game_setup/11.cube_mesh.png
.. |image12| image:: img/01.game_setup/12.cube_resized.png
.. |image13| image:: img/01.game_setup/13.move_gizmo_y_axis.png
.. |image14| image:: img/01.game_setup/14.select_mode_icon.png
.. |image15| image:: img/01.game_setup/15.translation_amount.png
.. |image16| image:: img/01.game_setup/16.turn_on_shadows.png
.. |image17| image:: img/01.game_setup/17.project_with_light.png

View File

@@ -11,13 +11,18 @@ that moves in eight directions.
.. player_movement.gif
Create a new scene by going to the Scene menu in the top-left and clicking *New
Scene*. Create a *CharacterBody3D* node as the root and name it *Player*.
Scene*.
|image0|
Create a :ref:`CharacterBody3D <class_CharacterBody3D>` node as the root
.. image:: img/02.player_input/add_character_body3D.webp
Name the :ref:`CharacterBody3D <class_CharacterBody3D>` to ``Player``.
Character bodies are complementary to the area and rigid bodies used in the 2D
game tutorial. Like rigid bodies, they can move and collide with the
environment, but instead of being controlled by the physics engine, you dictate
environment, but instead of being controlled by the physics engine, **you** dictate
their movement. You will see how we use the node's unique features when we code
the jump and squash mechanics.
@@ -29,14 +34,18 @@ the jump and squash mechanics.
For now, we're going to create a basic rig for our character's 3D model. This
will allow us to rotate the model later via code while it plays an animation.
Add a *Node3D* node as a child of *Player* and name it *Pivot*. Then, in the
FileSystem dock, expand the ``art/`` folder by double-clicking it and drag and
drop ``player.glb`` onto the *Pivot* node.
Add a :ref:`Node3D <class_Node3D>` node as a child of ``Player`` and name it ``Pivot``
.. image:: img/02.player_input/adding_node3D.webp
Then, in the FileSystem dock, expand the ``art/`` folder
by double-clicking it and drag and
drop ``player.glb`` onto ``Pivot``.
|image1|
This should instantiate the model as a child of *Pivot*. You can rename it to
*Character*.
This should instantiate the model as a child of ``Pivot``.
You can rename it to ``Character``.
|image2|
@@ -48,9 +57,12 @@ This should instantiate the model as a child of *Pivot*. You can rename it to
model in `Blender 3D <https://www.blender.org/>`__ and exported it to GLTF.
As with all kinds of physics nodes, we need a collision shape for our character
to collide with the environment. Select the *Player* node again and add a
*CollisionShape*. In the *Inspector*, assign a *SphereShape* to the *Shape*
property. The sphere's wireframe appears below the character.
to collide with the environment. Select the ``Player`` node again and add a child node
:ref:`CollisionShape3D <class_CollisionShape3D>`. In the *Inspector*, on the *Shape* property, add a new :ref:`SphereShape3D <class_SphereShape3D>`.
.. image:: img/02.player_input/add_capsuleshape3d.webp
The sphere's wireframe appears below the character.
|image3|
@@ -63,11 +75,11 @@ Then, move the shape up so its bottom roughly aligns with the grid's plane.
|image4|
You can toggle the model's visibility by clicking the eye icon next to the
*Character* or the *Pivot* nodes.
``Character`` or the ``Pivot`` nodes.
|image5|
Save the scene as ``Player.tscn``.
Save the scene as ``Player.tscn``
With the nodes ready, we can almost get coding. But first, we need to define
some input actions.
@@ -81,7 +93,7 @@ a powerful system that allows you to assign a label to a set of keys and
buttons. This simplifies our scripts and makes them more readable.
This system is the Input Map. To access its editor, head to the *Project* menu
and select *Project Settings*.
and select *Project Settings*.
|image6|
@@ -101,52 +113,45 @@ To add an action, write its name in the bar at the top and press Enter.
|image8|
Create the five actions. Your window should have them all listed at the bottom.
Create the following five actions:
|image9|
To bind a key or button to an action, click the "+" button to its right. Do this
for ``move_left`` and in the drop-down menu, click *Key*.
for ``move_left``. Press the left arrow key and click *OK*.
|image10|
.. image:: img/02.player_input/left_inputmap.webp
This option allows you to add a keyboard input. A popup appears and waits for
you to press a key. Press the left arrow key and click *OK*.
|image11|
Do the same for the A key.
Bind also the :kbd:`A` key, onto the action ``move_left``.
|image12|
Let's now add support for a gamepad's left joystick. Click the "+" button again
but this time, select *Joy Axis*.
but this time, select *Manual Selection -> Joypad Axes*.
|image13|
.. image:: img/02.player_input/left_inputmap.webp
The popup gives you two drop-down menus. On the left, you can select a gamepad
by index. *Device 0* corresponds to the first plugged gamepad, *Device 1*
corresponds to the second, and so on. You can select the joystick and direction
you want to bind to the input action on the right. Leave the default values and
press the *Add* button.
Select the negative X axis of the left joystick.
|image14|
.. image:: img/02.player_input/left_joystick_select.webp
Leave the other values as default and press *OK*
.. note::
If you want controllers to have different input actions, you should use the Devices option in Additional Options. Device 0 corresponds to the first plugged gamepad, Device 1 corresponds to the second plugged gamepad, and so on.
Do the same for the other input actions. For example, bind the right arrow, D,
and the left joystick's right axis to ``move_right``. After binding all keys,
and the left joystick's positive axis to ``move_right``. After binding all keys,
your interface should look like this.
|image15|
We have the ``jump`` action left to set up. Bind the Space key and the gamepad's
A button. To bind a gamepad's button, select the *Joy Button* option in the menu.
The final action to set up is the ``jump`` action. Bind the Space key and the gamepad's
A button.
|image16|
Leave the default values and click the *Add* button.
|image17|
Your jump input action should look like this.
|image18|
@@ -157,21 +162,18 @@ groups of keys and buttons in your projects.
In the next part, we'll code and test the player's movement.
.. |image0| image:: img/02.player_input/01.new_scene.png
.. |image1| image:: img/02.player_input/02.instantiating_the_model.png
.. |image1| image:: img/02.player_input/02.instantiating_the_model.webp
.. |image2| image:: img/02.player_input/03.scene_structure.png
.. |image3| image:: img/02.player_input/04.sphere_shape.png
.. |image4| image:: img/02.player_input/05.moving_the_sphere_up.png
.. |image5| image:: img/02.player_input/06.toggling_visibility.png
.. |image5| image:: img/02.player_input/06.toggling_visibility.webp
.. |image6| image:: img/02.player_input/07.project_settings.png
.. |image7| image:: img/02.player_input/07.input_map_tab.png
.. |image8| image:: img/02.player_input/07.adding_action.png
.. |image9| image:: img/02.player_input/08.actions_list_empty.png
.. |image10| image:: img/02.player_input/08.create_key_action.png
.. |image11| image:: img/02.player_input/09.keyboard_key_popup.png
.. |image12| image:: img/02.player_input/09.keyboard_keys.png
.. |image13| image:: img/02.player_input/10.joy_axis_option.png
.. |image14| image:: img/02.player_input/11.joy_axis_popup.png
.. |image15| image:: img/02.player_input/12.move_inputs_mapped.png
.. |image16| image:: img/02.player_input/13.joy_button_option.png
.. |image15| image:: img/02.player_input/12.move_inputs_mapped.webp
.. |image16| image:: img/02.player_input/13.joy_button_option.webp
.. |image17| image:: img/02.player_input/14.add_jump_button.png
.. |image18| image:: img/02.player_input/14.jump_input_action.png
.. |image18| image:: img/02.player_input/14.jump_input_action.webp

View File

@@ -6,7 +6,7 @@ Moving the player with code
It's time to code! We're going to use the input actions we created in the last
part to move the character.
Right-click the *Player* node and select *Attach Script* to add a new script to
Right-click the ``Player`` node and select *Attach Script* to add a new script to
it. In the popup, set the *Template* to *Empty* before pressing the *Create*
button.
@@ -23,10 +23,10 @@ character.
# 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.
# The downward acceleration while in the air, in meters per second squared.
@export var fall_acceleration = 75
var velocity = Vector3.ZERO
var target_velocity = Vector3.ZERO
.. code-tab:: csharp
@@ -37,7 +37,7 @@ character.
// How fast the player moves in meters per second.
[Export]
public int Speed = 14;
// The downward acceleration when in the air, in meters per second squared.
// The downward acceleration while in the air, in meters per second squared.
[Export]
public int FallAcceleration = 75;
@@ -45,7 +45,7 @@ character.
}
These are common properties for a moving body. The ``velocity`` is a 3D vector
These are common properties for a moving body. The ``velocity`` is a :ref:`3D vector <class_Vector3>`
combining a speed with a direction. Here, we define it as a property because
we want to update and reuse its value across frames.
@@ -55,7 +55,7 @@ we want to update and reuse its value across frames.
While in 2D, a thousand units (pixels) may only correspond to half of your
screen's width, in 3D, it's a kilometer.
Let's code the movement now. We start by calculating the input direction vector
Let's code the movement. We start by calculating the input direction vector
using the global ``Input`` object, in ``_physics_process()``.
.. tabs::
@@ -124,7 +124,7 @@ These four conditions give us eight possibilities and eight possible directions.
In case the player presses, say, both W and D simultaneously, the vector will
have a length of about ``1.4``. But if they press a single key, it will have a
length of ``1``. We want the vector's length to be consistent. To do so, we can
length of ``1``. We want the vector's length to be consistent, and not move faster diagonally. To do so, we can
call its ``normalize()`` method.
.. tabs::
@@ -135,7 +135,7 @@ call its ``normalize()`` method.
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(translation + direction, Vector3.UP)
$Pivot.look_at(position + direction, Vector3.UP)
.. code-tab:: csharp
@@ -146,30 +146,30 @@ call its ``normalize()`` method.
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Node3D>("Pivot").LookAt(Translation + direction, Vector3.Up);
GetNode<Node3D>("Pivot").LookAt(position + direction, Vector3.Up);
}
}
Here, we only normalize the vector if the direction has a length greater than
zero, which means the player is pressing a direction key.
In this case, we also get the *Pivot* node and call its ``look_at()`` method.
In this case, we also get the ``Pivot`` node and call its ``look_at()`` method.
This method takes a position in space to look at in global coordinates and the
up direction. In this case, we can use the ``Vector3.UP`` constant.
.. note::
A node's local coordinates, like ``translation``, are relative to their
parent. Global coordinates are relative to the world's main axes you can see
A node's local coordinates, like ``position``, are relative to their
parent. Global coordinates, like ``global_position`` are relative to the world's main axes you can see
in the viewport instead.
In 3D, the property that contains a node's position is ``translation``. By
In 3D, the property that contains a node's position is ``position``. By
adding the ``direction`` to it, we get a position to look at that's one meter
away from the *Player*.
away from the ``Player``.
Then, we update the velocity. We have to calculate the ground velocity and the
fall speed separately. Be sure to go back one tab so the lines are inside the
``_physics_process()`` function but outside the condition we just wrote.
``_physics_process()`` function but outside the condition we just wrote above.
.. tabs::
.. code-tab:: gdscript GDScript
@@ -179,14 +179,17 @@ fall speed separately. Be sure to go back one tab so the lines are inside the
if direction != Vector3.ZERO:
#...
# Ground velocity
velocity.x = direction.x * speed
velocity.z = direction.z * speed
# Vertical velocity
velocity.y -= fall_acceleration * delta
# Moving the character
velocity = move_and_slide(velocity, 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()
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
@@ -202,29 +205,28 @@ fall speed separately. Be sure to go back one tab so the lines are inside the
_velocity = MoveAndSlide(_velocity, Vector3.Up);
}
For the vertical velocity, we subtract the fall acceleration multiplied by the
delta time every frame. Notice the use of the ``-=`` operator, which is a
shorthand for ``variable = variable - ...``.
The ``CharacterBody3D.is_on_floor()`` function returns ``true`` if the body collided with the floor in this frame. That's why
we apply gravity to the ``Player`` only while he is in the air.
This line of code will cause our character to fall in every frame. This may seem
strange if it's already on the floor. But we have to do this for the character
to collide with the ground every frame.
For the vertical velocity, we subtract the fall acceleration multiplied by the
delta time every frame.
This line of code will cause our character to fall in every frame, as long he is not on the floor, or collides with it.
The physics engine can only detect interactions with walls, the floor, or other
bodies during a given frame if movement and collisions happen. We will use this
property later to code the jump.
On the last line, we call ``CharacterBody3D.move_and_slide()``. It's a powerful
On the last line, we call ``CharacterBody3D.move_and_slide()`` which is a powerful
method of the ``CharacterBody3D`` class that allows you to move a character
smoothly. If it hits a wall midway through a motion, the engine will try to
smooth it out for you.
smooth it out for you. It uses the *velocity* value native to the :ref:`CharacterBody3D <class_CharacterBody3D>`
The function takes two parameters: our velocity and the up direction. It moves
the character and returns a leftover velocity after applying collisions. When
hitting the floor or a wall, the function will reduce or reset the speed in that
direction from you. In our case, storing the function's returned value prevents
the character from accumulating vertical momentum, which could otherwise get so
big the character would move through the ground slab after a while.
.. OLD TEXT: The function takes two parameters: our velocity and the up direction. It moves
.. the character and returns a leftover velocity after applying collisions. When
.. hitting the floor or a wall, the function will reduce or reset the speed in that
.. direction from you. In our case, storing the function's returned value prevents
.. the character from accumulating vertical momentum, which could otherwise get so
.. big the character would move through the ground slab after a while.
And that's all the code you need to move the character on the floor.
@@ -240,7 +242,7 @@ Here is the complete ``Player.gd`` code for reference.
# The downward acceleration when in the air, in meters per second squared.
@export var fall_acceleration = 75
var velocity = Vector3.ZERO
var target_velocity = Vector3.ZERO
func _physics_process(delta):
@@ -255,15 +257,21 @@ Here is the complete ``Player.gd`` code for reference.
if Input.is_action_pressed("move_forward"):
direction.z -= 1
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(translation + direction, Vector3.UP)
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(position + direction, Vector3.UP)
velocity.x = direction.x * speed
velocity.z = direction.z * speed
velocity.y -= fall_acceleration * delta
velocity = move_and_slide(velocity, 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()
.. code-tab:: csharp
public class Player : CharacterBody3D
@@ -321,11 +329,11 @@ Here is the complete ``Player.gd`` code for reference.
Testing our player's movement
-----------------------------
We're going to put our player in the *Main* scene to test it. To do so, we need
We're going to put our player in the ``Main`` scene to test it. To do so, we need
to instantiate the player and then add a camera. Unlike in 2D, in 3D, you won't
see anything if your viewport doesn't have a camera pointing at something.
Save your *Player* scene and open the *Main* scene. You can click on the *Main*
Save your ``Player`` scene and open the ``Main`` scene. You can click on the *Main*
tab at the top of the editor to do so.
|image1|
@@ -333,21 +341,20 @@ tab at the top of the editor to do so.
If you closed the scene before, head to the *FileSystem* dock and double-click
``Main.tscn`` to re-open it.
To instantiate the *Player*, right-click on the *Main* node and select *Instance
To instantiate the ``Player``, right-click on the ``Main`` node and select *Instance
Child Scene*.
|image2|
In the popup, double-click *Player.tscn*. The character should appear in the
In the popup, double-click ``Player.tscn``. The character should appear in the
center of the viewport.
Adding a camera
~~~~~~~~~~~~~~~
Let's add the camera next. Like we did with our *Player*\ 's *Pivot*, we're
going to create a basic rig. Right-click on the *Main* node again and select
*Add Child Node* this time. Create a new *Marker3D*, name it *CameraPivot*,
and add a *Camera* node as a child of it. Your scene tree should look like this.
going to create a basic rig. Right-click on the ``Main`` node again and select
*Add Child Node*. Create a new :ref:`Marker3D <class_Marker3D>`, and name it ``CameraPivot``. Select ``CameraPivot`` and add a child node :ref:`Camera3D <class_Camera3D>` to it. Your scene tree should look like this.
|image3|
@@ -363,9 +370,11 @@ what the camera sees.
In the toolbar right above the viewport, click on *View*, then *2 Viewports*.
You can also press :kbd:`Ctrl + 2` (:kbd:`Cmd + 2` on macOS).
|image11|
|image5|
On the bottom view, select the *Camera* and turn on camera preview by clicking
On the bottom view, select your :ref:`Camera3D <class_Camera3D>` and turn on camera Preview by clicking
the checkbox.
|image6|
@@ -397,10 +406,12 @@ the ground should fill the background.
|image10|
With that, we have both player movement and the view in place. Next, we will
Test your scene and you should be able to move in all 8 directions and not glitch through the floor!
Ultimately, we have both player movement and the view in place. Next, we will
work on the monsters.
.. |image0| image:: img/03.player_movement_code/01.attach_script_to_player.png
.. |image0| image:: img/03.player_movement_code/01.attach_script_to_player.webp
.. |image1| image:: img/03.player_movement_code/02.clicking_main_tab.png
.. |image2| image:: img/03.player_movement_code/03.instance_child_scene.png
.. |image3| image:: img/03.player_movement_code/04.scene_tree_with_camera.png
@@ -410,4 +421,5 @@ work on the monsters.
.. |image7| image:: img/03.player_movement_code/08.camera_moved.png
.. |image8| image:: img/03.player_movement_code/09.camera_rotated.png
.. |image9| image:: img/03.player_movement_code/10.camera_perspective.png
.. |image10| image:: img/03.player_movement_code/11.camera_orthographic.png
.. |image10| image:: img/03.player_movement_code/13.camera3d_values.webp
.. |image11| image:: img/03.player_movement_code/12.viewport_change.webp

View File

@@ -7,28 +7,33 @@ In this part, you're going to code the monsters, which we'll call mobs. In the
next lesson, we'll spawn them randomly around the playable area.
Let's design the monsters themselves in a new scene. The node structure is going
to be similar to the *Player* scene.
to be similar to the ``Player.tscn`` scene.
Create a scene with, once again, a *CharacterBody3D* node as its root. Name it
*Mob*. Add a *Node3D* node as a child of it, name it *Pivot*. And drag and drop
the file ``mob.glb`` from the *FileSystem* dock onto the *Pivot* to add the
monster's 3D model to the scene. You can rename the newly created *mob* node
into *Character*.
Create a scene with, once again, a :ref:`CharacterBody3D <class_CharacterBody3D>` node as its root. Name it
``Mob``. Add a child node :ref:`Node3D <class_Node3D>`, name it ``Pivot``. And drag and drop
the file ``mob.glb`` from the *FileSystem* dock onto the ``Pivot`` to add the
monster's 3D model to the scene.
.. image:: img/04.mob_scene/drag_drop_mob.webp
You can rename the newly created ``mob`` node
into ``Character``.
|image0|
We need a collision shape for our body to work. Right-click on the *Mob* node,
We need a collision shape for our body to work. Right-click on the ``Mob`` node,
the scene's root, and click *Add Child Node*.
|image1|
Add a *CollisionShape*.
Add a :ref:`CollisionShape3D <class_CollisionShape3D>`.
|image2|
In the *Inspector*, assign a *BoxShape* to the *Shape* property.
|image3|
In the *Inspector*, assign a *BoxShape3D* to the *Shape* property.
.. image:: img/01.game_setup/08.create_box_shape3D.jpg
We should change its size to fit the 3D model better. You can do so
interactively by clicking and dragging on the orange dots.
@@ -52,15 +57,15 @@ Removing monsters off-screen
We're going to spawn monsters at regular time intervals in the game level. If
we're not careful, their count could increase to infinity, and we don't want
that. Each mob instance has both a memory and a processing cost, and we don't
want to pay for it when the mob's outside the screen.
want to pay for it when the mob is outside the screen.
Once a monster leaves the screen, we don't need it anymore, so we can delete it.
Once a monster leaves the screen, we don't need it anymore, so we should delete it.
Godot has a node that detects when objects leave the screen,
*VisibilityNotifier*, and we're going to use it to destroy our mobs.
:ref:`VisibleOnScreenNotifier3D <class_VisibileOnScreenNotifier3D>`, and we're going to use it to destroy our mobs.
.. note::
When you keep instancing an object in games, there's a technique you can
When you keep instancing an object, there's a technique you can
use 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.
@@ -69,9 +74,9 @@ Godot has a node that detects when objects leave the screen,
reason to use pools is to avoid freezes with garbage-collected languages
like C# or Lua. GDScript uses a different technique to manage memory,
reference counting, which doesn't have that caveat. You can learn more
about that here :ref:`doc_gdscript_basics_memory_management`.
about that here: :ref:`doc_gdscript_basics_memory_management`.
Select the *Mob* node and add a *VisibilityNotifier* as a child of it. Another
Select the ``Mob`` node and add a child node :ref:`VisibleOnScreenNotifier3D <class_VisibileOnScreenNotifier3D>`. Another
box, pink this time, appears. When this box completely leaves the screen, the
node will emit a signal.
@@ -85,17 +90,16 @@ Coding the mob's movement
-------------------------
Let's implement the monster's motion. We're going to do this in two steps.
First, we'll write a script on the *Mob* that defines a function to initialize
the monster. We'll then code the randomized spawn mechanism in the *Main* scene
First, we'll write a script on the ``Mob`` that defines a function to initialize
the monster. We'll then code the randomized spawn mechanism in the ``Main.tscn`` scene
and call the function from there.
Attach a script to the *Mob*.
Attach a script to the ``Mob``.
|image7|
Here's the movement code to start with. We define two properties, ``min_speed``
and ``max_speed``, to define a random speed range. We then define and initialize
the ``velocity``.
and ``max_speed``, to define a random speed range, which we will later use to define ``CharacterBody3D.velocity``.
.. tabs::
.. code-tab:: gdscript GDScript
@@ -107,11 +111,9 @@ the ``velocity``.
# Maximum speed of the mob in meters per second.
@export var max_speed = 18
var velocity = Vector3.ZERO
func _physics_process(_delta):
move_and_slide(velocity)
move_and_slide()
.. code-tab:: csharp
@@ -126,24 +128,22 @@ the ``velocity``.
[Export]
public int MaxSpeed = 18;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
MoveAndSlide(_velocity);
MoveAndSlide();
}
}
Similarly to the player, we move the mob every frame by calling
``CharacterBody3D``\ 's ``move_and_slide()`` method. This time, we don't update
the ``velocity`` every frame: we want the monster to move at a constant speed
Similarly to the player, we move the mob every frame by calling the function
``CharacterBody3D.move_and_slide()``. This time, we don't update
the ``velocity`` every frame; we want the monster to move at a constant speed
and leave the screen, even if it were to hit an obstacle.
We need to define another function to calculate the start velocity. This
We need to define another function to calculate the ``CharacterBody3D.velocity``. This
function will turn the monster towards the player and randomize both its angle
of motion and its velocity.
The function will take a ``start_position``, the mob's spawn position, and the
The function will take a ``start_position``,the mob's spawn position, and the
``player_position`` as its arguments.
We position the mob at ``start_position`` and turn it towards the player using
@@ -154,13 +154,14 @@ between ``-PI / 4`` radians and ``PI / 4`` radians.
.. tabs::
.. code-tab:: gdscript GDScript
# We will call this function from the Main scene.
func initialize(start_position, player_position):
# We position the mob and turn it so that it looks at the player.
look_at_from_position(start_position, player_position, Vector3.UP)
# And rotate it randomly so it doesn't move exactly toward the player.
rotate_y(rand_range(-PI / 4, 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)
# In this rotation^, the mob will move directly towards the player
# so we rotate it randomly within range of -90 and +90 degrees.
rotate_y(randf_range(-PI / 4, PI / 4))
.. code-tab:: csharp
// We will call this function from the Main scene
@@ -172,12 +173,8 @@ between ``-PI / 4`` radians and ``PI / 4`` radians.
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
}
We then calculate a random speed using ``rand_range()`` once again and we use it
to calculate the velocity.
We start by creating a 3D vector pointing forward, multiply it by our
``random_speed``, and finally rotate it using the ``Vector3`` class's
``rotated()`` method.
We got a random position, now we need a ``random_speed``. ``randi_range()`` will be useful as it gives random int values, and we will use ``min_speed`` and ``max_speed``.
``random_speed`` is just an integer, and we just use it to multiply our ``CharacterBody3D.velocity``. After ``random_speed`` is applied, we rotate ``CharacterBody3D.velocity`` Vector3 towards the player.
.. tabs::
.. code-tab:: gdscript GDScript
@@ -185,11 +182,12 @@ We start by creating a 3D vector pointing forward, multiply it by our
func initialize(start_position, player_position):
# ...
# We calculate a random speed.
var random_speed = rand_range(min_speed, max_speed)
# 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 vector based on the mob's Y rotation to move in the direction it's looking.
# 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)
.. code-tab:: csharp
@@ -210,26 +208,25 @@ Leaving the screen
------------------
We still have to destroy the mobs when they leave the screen. To do so, we'll
connect our *VisibilityNotifier* node's ``screen_exited`` signal to the *Mob*.
connect our :ref:`VisibleOnScreenNotifier3D <class_VisibleOnScreenNotifier3D>` node's ``screen_exited`` signal to the ``Mob``.
Head back to the 3D viewport by clicking on the *3D* label at the top of the
editor. You can also press :kbd:`Ctrl + F2` (:kbd:`Alt + 2` on macOS).
|image8|
Select the *VisibilityNotifier* node and on the right side of the interface,
navigate to the *Node* dock. Double-click the *screen_exited()* signal.
Select the :ref:`VisibleOnScreenNotifier3D <class_VisibleOnScreenNotifier3D>` node and on the right side of the interface,
navigate to the *Node* dock. Double-click the ``screen_exited()`` signal.
|image9|
Connect the signal to the *Mob*.
Connect the signal to the ``Mob``
|image10|
This will take you back to the script editor and add a new function for you,
``_on_VisibilityNotifier_screen_exited()``. From it, call the ``queue_free()``
method. This will destroy the mob instance when the *VisibilityNotifier* \'s box
leaves the screen.
``_on_visible_on_screen_notifier_3d_screen_exited()``. From it, call the ``queue_free()``
method. This function destroy the instance it's called on.
.. tabs::
.. code-tab:: gdscript GDScript
@@ -256,29 +253,33 @@ Here is the complete ``Mob.gd`` script 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
# 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
var velocity = Vector3.ZERO
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)
# In this rotation^, the mob will move directly towards the player
# so we rotate it randomly within range of -90 and +90 degrees.
rotate_y(randf_range(-PI / 4, PI / 4))
func _physics_process(_delta):
move_and_slide(velocity)
func initialize(start_position, player_position):
look_at_from_position(start_position, player_position, Vector3.UP)
rotate_y(rand_range(-PI / 4, PI / 4))
var random_speed = rand_range(min_speed, max_speed)
velocity = Vector3.FORWARD * random_speed
velocity = velocity.rotated(Vector3.UP, rotation.y)
func _on_VisibilityNotifier_screen_exited():
queue_free()
# 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()
.. code-tab:: csharp
public class Mob : CharacterBody3D
@@ -318,11 +319,10 @@ Here is the complete ``Mob.gd`` script for reference.
.. |image0| image:: img/04.mob_scene/01.initial_three_nodes.png
.. |image1| image:: img/04.mob_scene/02.add_child_node.png
.. |image2| image:: img/04.mob_scene/03.scene_with_collision_shape.png
.. |image3| image:: img/04.mob_scene/04.create_box_shape.png
.. |image4| image:: img/04.mob_scene/05.box_final_size.png
.. |image5| image:: img/04.mob_scene/06.visibility_notifier.png
.. |image6| image:: img/04.mob_scene/07.visibility_notifier_bbox_resized.png
.. |image7| image:: img/04.mob_scene/08.mob_attach_script.png
.. |image8| image:: img/04.mob_scene/09.switch_to_3d_workspace.png
.. |image9| image:: img/04.mob_scene/10.node_dock.png
.. |image10| image:: img/04.mob_scene/11.connect_signal.png
.. |image9| image:: img/04.mob_scene/10.node_dock.webp
.. |image10| image:: img/04.mob_scene/11.connect_signal.webp

View File

@@ -8,7 +8,7 @@ you will have monsters roaming the game board.
|image0|
Double-click on ``Main.tscn`` in the *FileSystem* dock to open the *Main* scene.
Double-click on ``Main.tscn`` in the *FileSystem* dock to open the ``Main`` scene.
Before drawing the path, we're going to change the game resolution. Our game has
a default window size of ``1024x600``. We're going to set it to ``720x540``, a
@@ -27,7 +27,7 @@ Creating the spawn path
-----------------------
Like you did in the 2D game tutorial, you're going to design a path and use a
*PathFollow* node to sample random locations on it.
:ref:`PathFollow3D <class_PathFollow3D>` node to sample random locations on it.
In 3D though, it's a bit more complicated to draw the path. We want it to be
around the game view so monsters appear right outside the screen. But if we draw
@@ -36,7 +36,7 @@ a path, we won't see it from the camera preview.
To find the view's limits, we can use some placeholder meshes. Your viewport
should still be split into two parts, with the camera preview at the bottom. If
that isn't the case, press :kbd:`Ctrl + 2` (:kbd:`Cmd + 2` on macOS) to split the view into two.
Select the *Camera* node and click the *Preview* checkbox in the bottom
Select the :ref:`Camera3D <class_Camera3D>` node and click the *Preview* checkbox in the bottom
viewport.
|image3|
@@ -44,9 +44,8 @@ viewport.
Adding placeholder cylinders
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Let's add the placeholder meshes. Add a new *Node3D* node as a child of the
*Main* node and name it *Cylinders*. We'll use it to group the cylinders. As a
child of it, add a *MeshInstance* node.
Let's add the placeholder meshes. Add a new :ref:`Node3D <class_Node3D>` as a child of the
``Main`` node and name it ``Cylinders``. We'll use it to group the cylinders. Select ``Cylinders`` and add a child node :ref:`MeshInstance3D <class_MeshInstance3D>`
|image4|
@@ -59,7 +58,7 @@ top-left corner. Alternatively, you can press the keypad's 7 key.
|image6|
The grid is a bit distracting for me. You can toggle it by going to the *View*
The grid may be distracting. You can toggle it by going to the *View*
menu in the toolbar and clicking *View Grid*.
|image7|
@@ -70,7 +69,7 @@ toggle it by clicking the magnet icon in the toolbar or pressing Y.
|image8|
Place the cylinder so it's right outside the camera's view in the top-left
Move the cylinder so it's right outside the camera's view in the top-left
corner.
|image9|
@@ -101,16 +100,21 @@ last one.
|image12|
In the *Inspector*, expand the *Material* section and assign a *StandardMaterial3D*
to slot *0*.
In the *Inspector*, expand the *Material* section and assign a :ref:`StandardMaterial3D <class_StandardMaterial3D>` to slot *0*.
|image13|
.. image:: img/05.spawning_mobs/standard_material.webp
Click the sphere icon to open the material resource. You get a preview of the
material and a long list of sections filled with properties. You can use these
to create all sorts of surfaces, from metal to rock or water.
Expand the *Albedo* section and set the color to something that contrasts with
Expand the *Albedo* section.
.. image:: img/05.spawning_mobs/albedo_section.webp
Set the color to something that contrasts with
the background, like a bright orange.
|image14|
@@ -121,7 +125,7 @@ visibility by clicking the eye icon next to *Cylinders*.
|image15|
Add a *Path* node as a child of *Main*. In the toolbar, four icons appear. Click
Add a child node :ref:`Path3D <class_Path3D>` to ``Main`` node. In the toolbar, four icons appear. Click
the *Add Point* tool, the icon with the green "+" sign.
|image16|
@@ -138,9 +142,9 @@ Your path should look like this.
|image18|
To sample random positions on it, we need a *PathFollow* node. Add a
*PathFollow* as a child of the *Path*. Rename the two nodes to *SpawnPath* and
*SpawnLocation*, respectively. It's more descriptive of what we'll use them for.
To sample random positions on it, we need a :ref:`PathFollow3D <class_PathFollow3D>` node. Add a
:ref:`PathFollow3D <class_PathFollow3D>` as a child of the ``Path3d``. Rename the two nodes to ``SpawnPath`` and
``SpawnLocation``, respectively. It's more descriptive of what we'll use them for.
|image19|
@@ -149,7 +153,7 @@ With that, we're ready to code the spawn mechanism.
Spawning monsters randomly
--------------------------
Right-click on the *Main* node and attach a new script to it.
Right-click on the ``Main`` node and attach a new script to it.
We first export a variable to the *Inspector* so that we can assign ``Mob.tscn``
or any other monster to it.
@@ -189,14 +193,14 @@ always spawn following the same sequence.
We want to spawn mobs at regular time intervals. To do this, we need to go back
to the scene and add a timer. Before that, though, we need to assign the
``Mob.tscn`` file to the ``mob_scene`` property.
``Mob.tscn`` file to the ``mob_scene`` property above (otherwise it's null!)
Head back to the 3D screen and select the *Main* node. Drag ``Mob.tscn`` from
Head back to the 3D screen and select the ``Main`` node. Drag ``Mob.tscn`` from
the *FileSystem* dock to the *Mob Scene* slot in the *Inspector*.
|image20|
Add a new *Timer* node as a child of *Main*. Name it *MobTimer*.
Add a new :ref:`Timer <class_Timer>` node as a child of ``Main``. Name it ``MobTimer``.
|image21|
@@ -210,7 +214,7 @@ Time*. By default, they restart automatically, emitting the signal in a cycle.
We can connect to this signal from the *Main* node to spawn monsters every
``0.5`` seconds.
With the *MobTimer* still selected, head to the *Node* dock on the right and
With the *MobTimer* still selected, head to the *Node* dock on the right, and
double-click the ``timeout`` signal.
|image23|
@@ -220,7 +224,7 @@ Connect it to the *Main* node.
|image24|
This will take you back to the script, with a new empty
``_on_MobTimer_timeout()`` function.
``_on_mob_timer_timeout()`` function.
Let's code the mob spawning logic. We're going to:
@@ -234,20 +238,21 @@ Let's code the mob spawning logic. We're going to:
.. tabs::
.. code-tab:: gdscript GDScript
func _on_MobTimer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instantiate()
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.unit_offset = randf()
# 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.transform.origin
mob.initialize(mob_spawn_location.translation, player_position)
var player_position = $Player.position
mob.initialize(mob_spawn_location.position, player_position)
add_child(mob)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
.. code-tab:: csharp
@@ -263,7 +268,7 @@ Let's code the mob spawning logic. We're going to:
// And give it a random offset.
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
Vector3 playerPosition = GetNode<Player>("Player").position;
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
AddChild(mob);
@@ -271,31 +276,39 @@ Let's code the mob spawning logic. We're going to:
}
Above, ``randf()`` produces a random value between ``0`` and ``1``, which is
what the *PathFollow* node's ``unit_offset`` expects.
what the *PathFollow* node's ``progress_ratio`` expects:
0 is the start of the path, 1 is the end of the path.
The path we have set is around the camera's viewport, so any random value between 0 and 1
is a random position alongside the edges of the viewport!
Here is the complete ``Main.gd`` script so far, for reference.
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
extends Node
@export var mob_scene: PackedScene
@export var mob_scene: PackedScene
func _ready():
randomize()
func _ready():
randomize()
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()
func _on_MobTimer_timeout():
var mob = mob_scene.instantiate()
var player_position = $Player.position
mob.initialize(mob_spawn_location.position, player_position)
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
mob_spawn_location.unit_offset = randf()
var player_position = $Player.transform.origin
mob.initialize(mob_spawn_location.translation, player_position)
add_child(mob)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
.. code-tab:: csharp
@@ -318,7 +331,7 @@ Here is the complete ``Main.gd`` script so far, for reference.
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
Vector3 playerPosition = GetNode<Player>("Player").position;
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
AddChild(mob);
@@ -335,7 +348,7 @@ address this in the next part.
.. |image0| image:: img/05.spawning_mobs/01.monsters_path_preview.png
.. |image1| image:: img/05.spawning_mobs/02.project_settings.png
.. |image2| image:: img/05.spawning_mobs/03.window_settings.png
.. |image2| image:: img/05.spawning_mobs/03.window_settings.webp
.. |image3| image:: img/05.spawning_mobs/04.camera_preview.png
.. |image4| image:: img/05.spawning_mobs/05.cylinders_node.png
.. |image5| image:: img/05.spawning_mobs/06.cylinder_mesh.png
@@ -346,7 +359,7 @@ address this in the next part.
.. |image10| image:: img/05.spawning_mobs/11.both_cylinders_selected.png
.. |image11| image:: img/05.spawning_mobs/12.four_cylinders.png
.. |image12| image:: img/05.spawning_mobs/13.selecting_all_cylinders.png
.. |image13| image:: img/05.spawning_mobs/14.spatial_material.png
.. |image13| image:: img/05.spawning_mobs/14.multi_material_selection.webp
.. |image14| image:: img/05.spawning_mobs/15.bright-cylinders.png
.. |image15| image:: img/05.spawning_mobs/16.cylinders_fold.png
.. |image16| image:: img/05.spawning_mobs/17.points_options.png
@@ -357,5 +370,5 @@ address this in the next part.
.. |image21| image:: img/05.spawning_mobs/21.mob_timer.png
.. |image22| image:: img/05.spawning_mobs/22.mob_timer_properties.png
.. |image23| image:: img/05.spawning_mobs/23.timeout_signal.png
.. |image24| image:: img/05.spawning_mobs/24.connect_timer_to_main.png
.. |image24| image:: img/05.spawning_mobs/24.connect_timer_to_main.webp
.. |image25| image:: img/05.spawning_mobs/25.spawn_result.png

View File

@@ -3,7 +3,7 @@
Jumping and squashing monsters
==============================
In this part, we'll add the ability to jump, to squash the monsters. In the next
In this part, we'll add the ability to jump and squash the monsters. In the next
lesson, we'll make the player die when a monster hits them on the ground.
First, we have to change a few settings related to physics interactions. Enter
@@ -51,21 +51,20 @@ Now, we can assign them to our physics nodes.
Assigning layers and masks
~~~~~~~~~~~~~~~~~~~~~~~~~~
In the *Main* scene, select the *Ground* node. In the *Inspector*, expand the
In the *Main* scene, select the ``Ground`` node. In the *Inspector*, expand the
*Collision* section. There, you can see the node's layers and masks as a grid of
buttons.
|image2|
The ground is part of the world, so we want it to be part of the third layer.
Click the lit button to toggle off the first *Layer* and toggle on the third
one. Then, toggle off the *Mask* by clicking on it.
Click the lit button to toggle **off** the first *Layer* and toggle **on** the third
one. Then, toggle **off** the *Mask* by clicking on it.
|image3|
As I mentioned above, the *Mask* property allows a node to listen to interaction
with other physics objects, but we don't need it to have collisions. The
*Ground* doesn't need to listen to anything; it's just there to prevent
As mentioned before, the *Mask* property allows a node to listen to interaction
with other physics objects, but we don't need it to have collisions. ``Ground`` doesn't need to listen to anything; it's just there to prevent
creatures from falling.
Note that you can click the "..." button on the right side of the properties to
@@ -73,17 +72,17 @@ see a list of named checkboxes.
|image4|
Next up are the *Player* and the *Mob*. Open ``Player.tscn`` by double-clicking
Next up are the ``Player`` and the ``Mob``. Open ``Player.tscn`` by double-clicking
the file in the *FileSystem* dock.
Select the *Player* node and set its *Collision -> Mask* to both "enemies" and
"world". You can leave the default *Layer* property as the first layer is the
"player" one.
"world". You can leave the default *Layer* property as it is, because the first layer is the
"player" layer.
|image5|
Then, open the *Mob* scene by double-clicking on ``Mob.tscn`` and select the
*Mob* node.
``Mob`` node.
Set its *Collision -> Layer* to "enemies" and unset its *Collision -> Mask*,
leaving the mask empty.
@@ -91,7 +90,7 @@ leaving the mask empty.
|image6|
These settings mean the monsters will move through one another. If you want the
monsters to collide with and slide against each other, turn on the "enemies"
monsters to collide with and slide against each other, turn **on** the "enemies"
mask.
.. note::
@@ -125,8 +124,7 @@ the ``jump_impulse``.
[Export]
public int JumpImpulse = 20;
Inside ``_physics_process()``, add the following code before the line where we
called ``move_and_slide()``.
Inside ``_physics_process()``, add the following code before the ``move_and_slide()`` codeblock.
.. tabs::
.. code-tab:: gdscript GDScript
@@ -136,7 +134,7 @@ called ``move_and_slide()``.
# Jumping.
if is_on_floor() and Input.is_action_just_pressed("jump"):
velocity.y += jump_impulse
target_velocity.y = jump_impulse
#...
@@ -149,7 +147,7 @@ called ``move_and_slide()``.
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_velocity.y += JumpImpulse;
_velocity.y = JumpImpulse;
}
// ...
@@ -222,7 +220,7 @@ when jumping.
[Export]
public int BounceImpulse = 16;
Then, at the bottom of ``_physics_process()``, add the following loop. With
Then, after the **Jumping** codeblock we added above in ``_physics_process()``, add the following loop. With
``move_and_slide()``, Godot makes the body move sometimes multiple times in a
row to smooth out the character's motion. So we have to loop over all collisions
that may have happened.
@@ -237,17 +235,24 @@ With this code, if no collisions occurred on a given frame, the loop won't run.
func _physics_process(delta):
#...
for index in range(get_slide_count()):
# We check every collision that occurred this frame.
var collision = get_slide_collision(index)
# If we collide with a monster...
if collision.collider.is_in_group("mob"):
var mob = collision.collider
# ...we check that we are hitting it from above.
if Vector3.UP.dot(collision.normal) > 0.1:
# If so, we squash it and bounce.
mob.squash()
velocity.y = bounce_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
.. code-tab:: csharp
@@ -275,22 +280,22 @@ With this code, if no collisions occurred on a given frame, the loop won't run.
That's a lot of new functions. Here's some more information about them.
The functions ``get_slide_count()`` and ``get_slide_collision()`` both come from
the :ref:`CharacterBody3D<class_CharacterBody3D>` class and are related to
The functions ``get_slide_collision_count()`` and ``get_slide_collision()`` both come from
the :ref:`CharacterBody3D <class_CharacterBody3D>` class and are related to
``move_and_slide()``.
``get_slide_collision()`` returns a
:ref:`KinematicCollision3D<class_KinematicCollision3D>` object that holds
information about where and how the collision occurred. For example, we use its
``collider`` property to check if we collided with a "mob" by calling
``is_in_group()`` on it: ``collision.collider.is_in_group("mob")``.
``get_collider`` property to check if we collided with a "mob" by calling
``is_in_group()`` on it: ``collision.get_collider(index).is_in_group("mob")``.
.. note::
The method ``is_in_group()`` is available on every :ref:`Node<class_Node>`.
To check that we are landing on the monster, we use the vector dot product:
``Vector3.UP.dot(collision.normal) > 0.1``. The collision normal is a 3D vector
``Vector3.UP.dot(collision.get_normal()) > 0.1``. The collision normal is a 3D vector
that is perpendicular to the plane where the collision occurred. The dot product
allows us to compare it to the up direction.
@@ -298,7 +303,7 @@ With dot products, when the result is greater than ``0``, the two vectors are at
an angle of fewer than 90 degrees. A value higher than ``0.1`` tells us that we
are roughly above the monster.
We are calling one undefined function, ``mob.squash()``. We have to add it to
We are calling one undefined function, ``mob.squash()``, so we have to add it to
the Mob class.
Open the script ``Mob.gd`` by double-clicking on it in the *FileSystem* dock. At
@@ -343,11 +348,11 @@ With that, you should be able to kill monsters by jumping on them. You can press
However, the player won't die yet. We'll work on that in the next part.
.. |image0| image:: img/06.jump_and_squash/02.project_settings.png
.. |image1| image:: img/06.jump_and_squash/03.physics_layers.png
.. |image2| image:: img/06.jump_and_squash/04.default_physics_properties.png
.. |image3| image:: img/06.jump_and_squash/05.toggle_layer_and_mask.png
.. |image1| image:: img/06.jump_and_squash/03.physics_layers.webp
.. |image2| image:: img/06.jump_and_squash/04.default_physics_properties.webp
.. |image3| image:: img/06.jump_and_squash/05.toggle_layer_and_mask.webp
.. |image4| image:: img/06.jump_and_squash/06.named_checkboxes.png
.. |image5| image:: img/06.jump_and_squash/07.player_physics_mask.png
.. |image6| image:: img/06.jump_and_squash/08.mob_physics_mask.png
.. |image5| image:: img/06.jump_and_squash/07.player_physics_mask.webp
.. |image6| image:: img/06.jump_and_squash/08.mob_physics_mask.webp
.. |image7| image:: img/06.jump_and_squash/09.groups_tab.png
.. |image8| image:: img/06.jump_and_squash/10.group_scene_icon.png

View File

@@ -9,14 +9,15 @@ Let's fix this.
We want to detect being hit by an enemy differently from squashing them.
We want the player to die when they're moving on the floor, but not if
they're in the air. We could use vector math to distinguish the two
kinds of collisions. Instead, though, we will use an *Area* node, which
kinds of collisions. Instead, though, we will use an :ref:`Area3D <class_Area3D>` node, which
works well for hitboxes.
Hitbox with the Area node
-------------------------
Head back to the *Player* scene and add a new *Area* node. Name it
*MobDetector*. Add a *CollisionShape* node as a child of it.
Head back to the ``Player.tscn`` scene and add a new child node :ref:`Area3D <class_Area3D>`. Name it
``MobDetector``
Add a :ref:`CollisionShape3D <class_CollisionShape3D>` node as a child of it.
|image0|
@@ -38,8 +39,8 @@ monster's collision box.
The wider the cylinder, the more easily the player will get killed.
Next, select the *MobDetector* node again, and in the *Inspector*, turn
off its *Monitorable* property. This makes it so other physics nodes
Next, select the ``MobDetector`` node again, and in the *Inspector*, turn
**off** its *Monitorable* property. This makes it so other physics nodes
cannot detect the area. The complementary *Monitoring* property allows
it to detect collisions. Then, remove the *Collision -> Layer* and set
the mask to the "enemies" layer.
@@ -47,14 +48,14 @@ the mask to the "enemies" layer.
|image3|
When areas detect a collision, they emit signals. We're going to connect
one to the *Player* node. In the *Node* tab, double-click the
``body_entered`` signal and connect it to the *Player*.
one to the ``Player`` node. Select ``MobDetector`` and go to *Inspector*'s *Node* tab, double-click the
``body_entered`` signal and connect it to the ``Player``
|image4|
The *MobDetector* will emit ``body_entered`` when a *CharacterBody3D* or a
*RigidBody* node enters it. As it only masks the "enemies" physics
layers, it will only detect the *Mob* nodes.
The *MobDetector* will emit ``body_entered`` when a :ref:`CharacterBody3D <class_CharacterBody3D>` or a
:ref:`RigidBody3D <class_RigidBody3D>` node enters it. As it only masks the "enemies" physics
layers, it will only detect the ``Mob`` nodes.
Code-wise, we're going to do two things: emit a signal we'll later use
to end the game and destroy the player. We can wrap these operations in
@@ -74,7 +75,7 @@ a ``die()`` function that helps us put a descriptive label on the code.
queue_free()
func _on_MobDetector_body_entered(_body):
func _on_mob_detector_body_entered(_body):
die()
.. code-tab:: csharp
@@ -100,30 +101,37 @@ a ``die()`` function that helps us put a descriptive label on the code.
}
Try the game again by pressing :kbd:`F5`. If everything is set up correctly,
the character should die when an enemy runs into it.
the character should die when an enemy runs into the collider. Note that without a ``Player``, the following line
However, note that this depends entirely on the size and position of the
*Player* and the *Mob*\ 's collision shapes. You may need to move them
.. tabs::
.. code-tab:: gdscript GDScript
var player_position = $Player.transform.origin
gives error because there is no $Player!
Also note that the enemy colliding with the player and dying depends on the size and position of the
``Player`` and the ``Mob``\ 's collision shapes. You may need to move them
and resize them to achieve a tight game feel.
Ending the game
---------------
We can use the *Player*\ 's ``hit`` signal to end the game. All we need
to do is connect it to the *Main* node and stop the *MobTimer* in
We can use the ``Player``\ 's ``hit`` signal to end the game. All we need
to do is connect it to the ``Main`` node and stop the ``MobTimer`` in
reaction.
Open ``Main.tscn``, select the *Player* node, and in the *Node* dock,
connect its ``hit`` signal to the *Main* node.
Open ``Main.tscn``, select the ``Player`` node, and in the *Node* dock,
connect its ``hit`` signal to the ``Main`` node.
|image5|
Get and stop the timer in the ``_on_Player_hit()`` function.
Get the timer, and stop it, in the ``_on_player_hit()`` function.
.. tabs::
.. code-tab:: gdscript GDScript
func _on_Player_hit():
func _on_player_hit():
$MobTimer.stop()
.. code-tab:: csharp
@@ -147,7 +155,7 @@ animations.
Code checkpoint
---------------
Here are the complete scripts for the *Main*, *Mob*, and *Player* nodes,
Here are the complete scripts for the ``Main``, ``Mob``, and ``Player`` nodes,
for reference. You can use them to compare and check your code.
Starting with ``Main.gd``.
@@ -155,35 +163,32 @@ Starting with ``Main.gd``.
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
extends Node
export(PackedScene) var mob_scene
@export var mob_scene: PackedScene
func _ready():
randomize()
func _ready():
randomize()
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()
func _on_MobTimer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instantiate()
var player_position = $Player.position
mob.initialize(mob_spawn_location.position, player_position)
# Choose a random location on the SpawnPath.
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
# And give it a random offset.
mob_spawn_location.unit_offset = randf()
# Communicate the spawn location and the player's location to the mob.
var player_position = $Player.transform.origin
mob.initialize(mob_spawn_location.translation, player_position)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
func _on_Player_hit():
$MobTimer.stop()
# Spawn the mob by adding it to the Main scene.
add_child(mob)
func _on_player_hit():
$MobTimer.stop()
.. code-tab:: csharp
public class Main : Node
@@ -210,7 +215,7 @@ Starting with ``Main.gd``.
mobSpawnLocation.UnitOffset = GD.Randf();
// Communicate the spawn location and the player's location to the mob.
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
Vector3 playerPosition = GetNode<Player>("Player").position;
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
// Spawn the mob by adding it to the Main scene.
@@ -228,40 +233,42 @@ Next is ``Mob.gd``.
.. tabs::
.. code-tab:: gdscript GDScript
extends CharacterBody3D
extends CharacterBody3D
# Emitted when the player jumped on the mob.
signal squashed
# 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
# 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
var velocity = Vector3.ZERO
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)
# In this rotation^, the mob will move directly towards the player
# so we rotate it randomly within range of -90 and +90 degrees.
rotate_y(randf_range(-PI / 4, PI / 4))
func _physics_process(_delta):
move_and_slide(velocity)
func initialize(start_position, player_position):
look_at_from_position(start_position, player_position, Vector3.UP)
rotate_y(rand_range(-PI / 4, PI / 4))
var random_speed = rand_range(min_speed, max_speed)
velocity = Vector3.FORWARD * random_speed
velocity = velocity.rotated(Vector3.UP, rotation.y)
# 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():
emit_signal("squashed")
queue_free()
func _on_VisibilityNotifier_screen_exited():
queue_free()
emit_signal("squashed")
queue_free() # Destroy this node
.. code-tab:: csharp
public class Mob : CharacterBody3D
@@ -306,71 +313,90 @@ Next is ``Mob.gd``.
}
}
Finally, the longest script, ``Player.gd``.
Finally, the longest script, ``Player.gd``:
.. tabs::
.. code-tab:: gdscript GDScript
extends CharacterBody3D
# Emitted when a mob hit the player.
signal hit
signal hit
# 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
# 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
# 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
@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 velocity = Vector3.ZERO
var target_velocity = Vector3.ZERO
func _physics_process(delta):
var direction = Vector3.ZERO
func _physics_process(delta):
# We create a local variable to store the input direction
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
# 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
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(translation + direction, Vector3.UP)
# Prevent diagonal moving fast af
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(position + direction,Vector3.UP)
velocity.x = direction.x * speed
velocity.z = direction.z * speed
# Ground Velocity
target_velocity.x = direction.x * speed
target_velocity.z = direction.z * speed
# Jumping.
if is_on_floor() and Input.is_action_just_pressed("jump"):
velocity.y += jump_impulse
# Vertical Velocity
if not is_on_floor(): # If in the air, fall towards the floor
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
velocity.y -= fall_acceleration * delta
velocity = move_and_slide(velocity, Vector3.UP)
# Jumping.
if is_on_floor() and Input.is_action_just_pressed("jump"):
target_velocity.y = jump_impulse
for index in range(get_slide_count()):
var collision = get_slide_collision(index)
if collision.collider.is_in_group("mob"):
var mob = collision.collider
if Vector3.UP.dot(collision.normal) > 0.1:
mob.squash()
velocity.y = bounce_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
func die():
emit_signal("hit")
queue_free()
# 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()
func _on_MobDetector_body_entered(_body):
die()
# And this function at the bottom.
func die():
emit_signal("hit")
queue_free()
func _on_mob_detector_body_entered(body):
die()
.. code-tab:: csharp
public class Player : CharacterBody3D
@@ -464,6 +490,6 @@ See you in the next lesson to add the score and the retry option.
.. |image0| image:: img/07.killing_player/01.adding_area_node.png
.. |image1| image:: img/07.killing_player/02.cylinder_shape.png
.. |image2| image:: img/07.killing_player/03.cylinder_in_editor.png
.. |image3| image:: img/07.killing_player/04.mob_detector_properties.png
.. |image3| image:: img/07.killing_player/04.mob_detector_properties.webp
.. |image4| image:: img/07.killing_player/05.body_entered_signal.png
.. |image5| image:: img/07.killing_player/06.player_hit_signal.png

View File

@@ -9,11 +9,11 @@ the game.
We have to keep track of the current score in a variable and display it on
screen using a minimal interface. We will use a text label to do that.
In the main scene, add a new *Control* node as a child of *Main* and name it
*UserInterface*. You will automatically be taken to the 2D screen, where you can
In the main scene, add a new child node :ref:`Control <class_Control>` to ``Main`` and name it
``UserInterface``. You will automatically be taken to the 2D screen, where you can
edit your User Interface (UI).
Add a *Label* node and rename it to *ScoreLabel*.
Add a :ref:`Label <class_Label>` node and name it ``ScoreLabel``
|image0|
@@ -24,28 +24,25 @@ In the *Inspector*, set the *Label*'s *Text* to a placeholder like "Score: 0".
Also, the text is white by default, like our game's background. We need to
change its color to see it at runtime.
Scroll down to *Theme Overrides*, and expand *Colors* and click the black box next to *Font Color* to
tint the text.
Scroll down to *Theme Overrides*, and expand *Colors*
and enable *Font Color* in order to tint the text to black
(which contrasts well with the white 3D scene)
|image2|
Pick a dark tone so it contrasts well with the 3D scene.
|image3|
Finally, click and drag on the text in the viewport to move it away from the
top-left corner.
|image4|
The *UserInterface* node allows us to group our UI in a branch of the scene tree
The ``UserInterface`` node allows us to group our UI in a branch of the scene tree
and use a theme resource that will propagate to all its children. We'll use it
to set our game's font.
Creating a UI theme
-------------------
Once again, select the *UserInterface* node. In the *Inspector*, create a new
Once again, select the ``UserInterface`` node. In the *Inspector*, create a new
theme resource in *Theme -> Theme*.
|image5|
@@ -63,11 +60,11 @@ By default, a theme only has one property, the *Default Font*.
interfaces, but that is beyond the scope of this series. To learn more about
creating and editing themes, see :ref:`doc_gui_skinning`.
Click the *Default Font* property and create a new *DynamicFont*.
Click the *Default Font* property and create a new :ref:`FontVariation <class_FontVariation>`
|image7|
Expand the *DynamicFont* by clicking on it and expand its *Font* section. There,
Expand the :ref:`FontVariation <class_FontVariation>` by clicking on it and expand its *Font* section. There,
you will see an empty *Font Data* field.
|image8|
@@ -75,7 +72,7 @@ you will see an empty *Font Data* field.
This one expects a font file like the ones you have on your computer. Two common
font file formats are TrueType Font (TTF) and OpenType Font (OTF).
In the *FileSystem* dock, Expand the ``fonts`` directory and click and drag the
In the *FileSystem* dock, expand the ``fonts`` directory and click and drag the
``Montserrat-Medium.ttf`` file we included in the project onto the *Font Data*.
The text will reappear in the theme preview.
@@ -87,7 +84,7 @@ the text's size.
Keeping track of the score
--------------------------
Let's work on the score next. Attach a new script to the *ScoreLabel* and define
Let's work on the score next. Attach a new script to the ``ScoreLabel`` and define
the ``score`` variable.
.. tabs::
@@ -105,8 +102,8 @@ the ``score`` variable.
}
The score should increase by ``1`` every time we squash a monster. We can use
their ``squashed`` signal to know when that happens. However, as we instantiate
monsters from the code, we cannot do the connection in the editor.
their ``squashed`` signal to know when that happens. However, because we instantiate
monsters from the code, we cannot connect the mob signal to the ``ScoreLabel`` via the editor.
Instead, we have to make the connection from the code every time we spawn a
monster.
@@ -119,16 +116,16 @@ the script editor's left column.
Alternatively, you can double-click the ``Main.gd`` file in the *FileSystem*
dock.
At the bottom of the ``_on_MobTimer_timeout()`` function, add the following
line.
At the bottom of the ``_on_mob_timer_timeout()`` function, add the following
line:
.. tabs::
.. code-tab:: gdscript GDScript
func _on_MobTimer_timeout():
func _on_mob_timer_timeout():
#...
# We connect the mob to the score label to update the score upon squashing one.
mob.connect("squashed", $UserInterface/ScoreLabel, "_on_Mob_squashed")
# We connect the mob to the score label to update the score upon squashing one.
mob.squashed.connect($UserInterface/ScoreLabel._on_Mob_squashed.bind())
.. code-tab:: csharp
@@ -140,7 +137,7 @@ line.
}
This line means that when the mob emits the ``squashed`` signal, the
*ScoreLabel* node will receive it and call the function ``_on_Mob_squashed()``.
``ScoreLabel`` node will receive it and call the function ``_on_Mob_squashed()``.
Head back to the ``ScoreLabel.gd`` script to define the ``_on_Mob_squashed()``
callback function.
@@ -164,13 +161,19 @@ There, we increment the score and update the displayed text.
The second line uses the value of the ``score`` variable to replace the
placeholder ``%s``. When using this feature, Godot automatically converts values
to text, which is convenient to output text in labels or using the ``print()``
to string text, which is convenient to output text in labels or using the ``print()``
function.
.. seealso::
You can learn more about string formatting here: :ref:`doc_gdscript_printf`.
.. note::
If you get an error when you squash a mob
check your capital letters in the signal "_on_Mob_squashed"
You can now play the game and squash a few enemies to see the score
increase.
@@ -190,8 +193,8 @@ Retrying the game
We'll now add the ability to play again after dying. When the player dies, we'll
display a message on the screen and wait for input.
Head back to the *Main* scene, select the *UserInterface* node, add a
*ColorRect* node as a child of it and name it *Retry*. This node fills a
Head back to the ``Main.tscn`` scene, select the ``UserInterface`` node, add a
child node *ColorRect*, and name it ``Retry``. This node fills a
rectangle with a uniform color and will serve as an overlay to darken the
screen.
@@ -204,27 +207,27 @@ Open it and apply the *Full Rect* command.
|image13|
Nothing happens. Well, almost nothing: only the four green pins move to the
Nothing happens. Well, almost nothing; only the four green pins move to the
corners of the selection box.
|image14|
This is because UI nodes (all the ones with a green icon) work with anchors and
margins relative to their parent's bounding box. Here, the *UserInterface* node
has a small size and the *Retry* one is limited by it.
margins relative to their parent's bounding box. Here, the ``UserInterface`` node
has a small size and the ``Retry`` one is limited by it.
Select the *UserInterface* and apply *Layout -> Full Rect* to it as well. The
*Retry* node should now span the whole viewport.
Select the ``UserInterface`` and apply *Layout -> Full Rect* to it as well. The
``Retry`` node should now span the whole viewport.
Let's change its color so it darkens the game area. Select *Retry* and in the
Let's change its color so it darkens the game area. Select ``Retry`` and in the
*Inspector*, set its *Color* to something both dark and transparent. To do so,
in the color picker, drag the *A* slider to the left. It controls the color's
alpha channel, that is to say, its opacity.
Alpha channel, that is to say, its opacity/transparency.
|image15|
Next, add a *Label* as a child of *Retry* and give it the *Text* "Press Enter to
retry."
Next, add a :ref:`Label <class_Label>` as a child of ``Retry`` and give it the *Text*
"Press Enter to retry."
|image16|
@@ -236,7 +239,7 @@ to it.
Coding the retry option
~~~~~~~~~~~~~~~~~~~~~~~
We can now head to the code to show and hide the *Retry* node when the player
We can now head to the code to show and hide the ``Retry`` node when the player
dies and plays again.
Open the script ``Main.gd``. First, we want to hide the overlay at the start of
@@ -274,11 +277,11 @@ Then, when the player gets hit, we show the overlay.
GetNode<Control>("UserInterface/Retry").Show();
}
Finally, when the *Retry* node is visible, we need to listen to the player's
Finally, when the ``Retry`` node is visible, we need to listen to the player's
input and restart the game if they press enter. To do this, we use the built-in
``_unhandled_input()`` callback.
``_unhandled_input()`` callback, which is triggered on any input.
If the player pressed the predefined ``ui_accept`` input action and *Retry* is
If the player pressed the predefined ``ui_accept`` input action and ``Retry`` is
visible, we reload the current scene.
.. tabs::
@@ -310,7 +313,7 @@ Adding music
To add music that plays continuously in the background, we're going to use
another feature in Godot: :ref:`autoloads <doc_singletons_autoload>`.
To play audio, all you need to do is add an *AudioStreamPlayer* node to your
To play audio, all you need to do is add an :ref:`AudioStreamPlayer <class_AudioStreamPlayer>` node to your
scene and attach an audio file to it. When you start the scene, it can play
automatically. However, when you reload the scene, like we do to play again, the
audio nodes are also reset, and the music starts back from the beginning.
@@ -323,8 +326,8 @@ Create a new scene by going to the *Scene* menu and clicking *New Scene*.
|image18|
Click the *Other Node* button to create an *AudioStreamPlayer* and rename it to
*MusicPlayer*.
Click the *Other Node* button to create an :ref:`AudioStreamPlayer2D <class_AudioStreamPlayer2D>` and rename it to
``MusicPlayer``.
|image19|
@@ -346,8 +349,8 @@ click the *Add* button on the right to register the node.
|image21|
If you run the game now, the music will play automatically. And even when you
lose and retry, it keeps going.
``MusicPlayer.tscn`` now loads 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, here's a quick look at how it works under the
hood. When you run the game, your *Scene* dock changes to give you two tabs:
@@ -361,7 +364,7 @@ instantiated mobs at the bottom.
|image23|
At the top are the autoloaded *MusicPlayer* and a *root* node, which is your
At the top are the autoloaded ``MusicPlayer`` and a *root* node, which is your
game's viewport.
And that does it for this lesson. In the next part, we'll add an animation to
@@ -372,38 +375,42 @@ Here is the complete ``Main.gd`` script for reference.
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
extends Node
@export var mob_scene: PackedScene
@export var mob_scene: PackedScene
func _ready():
randomize()
$UserInterface/Retry.hide()
func _ready():
randomize()
$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()
func _unhandled_input(event):
if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
get_tree().reload_current_scene()
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_MobTimer_timeout():
var mob = mob_scene.instantiate()
# We connect the mob to the score label to update the score upon squashing one.
mob.squashed.connect($UserInterface/ScoreLabel._on_Mob_squashed.bind())
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
mob_spawn_location.unit_offset = randf()
var player_position = $Player.transform.origin
mob.initialize(mob_spawn_location.translation, player_position)
add_child(mob)
mob.connect("squashed", $UserInterface/ScoreLabel, "_on_Mob_squashed")
func _on_Player_hit():
$MobTimer.stop()
$UserInterface/Retry.show()
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()
.. code-tab:: csharp
public class Main : Node
@@ -434,7 +441,7 @@ Here is the complete ``Main.gd`` script for reference.
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
Vector3 playerPosition = GetNode<Player>("Player").position;
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
AddChild(mob);
@@ -451,14 +458,14 @@ Here is the complete ``Main.gd`` script for reference.
.. |image0| image:: img/08.score_and_replay/01.label_node.png
.. |image1| image:: img/08.score_and_replay/02.score_placeholder.png
.. |image2| image:: img/08.score_and_replay/02.score_custom_color.png
.. |image2| image:: img/08.score_and_replay/02.score_custom_color.webp
.. |image3| image:: img/08.score_and_replay/02.score_color_picker.png
.. |image4| image:: img/08.score_and_replay/02.score_label_moved.png
.. |image5| image:: img/08.score_and_replay/03.creating_theme.png
.. |image6| image:: img/08.score_and_replay/04.theme_preview.png
.. |image7| image:: img/08.score_and_replay/05.dynamic_font.png
.. |image8| image:: img/08.score_and_replay/06.font_data.png
.. |image9| image:: img/08.score_and_replay/07.font_size.png
.. |image7| image:: img/08.score_and_replay/05.dynamic_font.webp
.. |image8| image:: img/08.score_and_replay/06.font_data.webp
.. |image9| image:: img/08.score_and_replay/07.font_size.webp
.. |image10| image:: img/08.score_and_replay/08.open_main_script.png
.. |image11| image:: img/08.score_and_replay/09.score_in_game.png
.. |image12| image:: img/08.score_and_replay/10.layout_icon.png

View File

@@ -17,7 +17,7 @@ Using the animation editor
The engine comes with tools to author animations in the editor. You can then use
the code to play and control them at runtime.
Open the player scene, select the player node, and add an *AnimationPlayer* node.
Open the player scene, select the ``Player`` node, and add an :ref:`AnimationPlayer <class_AnimationPlayer>` node.
The *Animation* dock appears in the bottom panel.
@@ -78,13 +78,16 @@ corresponding property. The keyframe gets inserted where your time cursor is in
the timeline.
Let's insert our first keys. Here, we will animate both the translation and the
rotation of the *Character* node.
rotation of the ``Character`` node.
Select the *Character* and click the key icon next to *Translation* in the
*Inspector*. Do the same for *Rotation Degrees*.
Select the ``Character`` and in the *Inspector* expand the *Transform* section. Click the key icon next to *Position*, and *Rotation*.
|image10|
.. image:: img/09.adding_animations/curves.webp
For this tutorial, just create RESET Track(s) which is the default choice
Two tracks appear in the editor with a diamond icon representing each keyframe.
|image11|
@@ -95,12 +98,19 @@ translation key to ``0.2`` seconds and the rotation key to ``0.1`` seconds.
|image12|
Move the time cursor to ``0.5`` seconds by clicking and dragging on the gray
timeline. In the *Inspector*, set the *Translation*'s *Y* axis to about
``0.65`` meters and the *Rotation Degrees*' *X* axis to ``8``.
timeline.
.. image:: img/09.adding_animations/timeline_05_click.webp
In the *Inspector*, set the *Position*'s *Y* axis to ``0.65`` meters and the *Rotation*' *X* axis to ``8``.
|image13|
Create a keyframe for both properties and shift the translation key to ``0.7``
Create a keyframe for both properties
.. image:: img/09.adding_animations/second_keys_both.webp
Now, move the position keyframe to ``0.7``
seconds by dragging it on the timeline.
|image14|
@@ -114,9 +124,11 @@ seconds by dragging it on the timeline.
make them feel alive.
Move the time cursor to the end of the animation, at ``1.2`` seconds. Set the Y
translation to about ``0.35`` and the X rotation to ``-9`` degrees. Once again,
position to about ``0.35`` and the X rotation to ``-9`` degrees. Once again,
create a key for both properties.
.. image:: img/09.adding_animations/animation_final_keyframes.webp
You can preview the result by clicking the play button or pressing :kbd:`Shift + D`.
Click the stop button or press :kbd:`S` to stop playback.
@@ -170,7 +182,7 @@ Your animation should look something like this.
If you play the game, the player's creature will now float!
If the creature is a little too close to the floor, you can move the *Pivot* up
If the creature is a little too close to the floor, you can move the ``Pivot`` up
to offset it.
Controlling the animation in code
@@ -179,7 +191,7 @@ Controlling the animation in code
We can use code to control the animation playback based on the player's input.
Let's change the animation speed when the character is moving.
Open the *Player*'s script by clicking the script icon next to it.
Open the ``Player``'s script by clicking the script icon next to it.
|image22|
@@ -216,7 +228,7 @@ vector, add the following code.
This code makes it so when the player moves, we multiply the playback speed by
``4``. When they stop, we reset it to normal.
We mentioned that the pivot could layer transforms on top of the animation. We
We mentioned that the ``Pivot`` could layer transforms on top of the animation. We
can make the character arc when jumping using the following line of code. Add it
at the end of ``_physics_process()``.
@@ -242,10 +254,10 @@ Animating the mobs
Here's another nice trick with animations in Godot: as long as you use a similar
node structure, you can copy them to different scenes.
For example, both the *Mob* and the *Player* scenes have a *Pivot* and a
*Character* node, so we can reuse animations between them.
For example, both the ``Mob`` and the ``Player`` scenes have a ``Pivot`` and a
``Character`` node, so we can reuse animations between them.
Open the *Player* scene, select the animation player node and open the "float" animation.
Open the ``Player.tscn`` scene, select the ``AnimationPlayer`` node and open the "float" animation.
Next, click on **Animation > Copy**. Then open ``Mob.tscn`` and open its animation
player. Click **Animation > Paste**. That's it; all monsters will now play the float
animation.
@@ -282,71 +294,90 @@ Here's the *Player* script.
.. tabs::
.. code-tab:: gdscript GDScript
extends CharacterBody3D
extends CharacterBody3D
# Emitted when the player was hit by a mob.
signal hit
signal hit
# How fast the player moves in meters per second.
@export var speed = 14
# The downward acceleration when in the air, in meters per second per second.
@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
# 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
@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 velocity = Vector3.ZERO
var target_velocity = Vector3.ZERO
func _physics_process(delta):
var direction = Vector3.ZERO
func _physics_process(delta):
# We create a local variable to store the input direction
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
# 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
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(translation + direction, Vector3.UP)
$AnimationPlayer.playback_speed = 4
else:
$AnimationPlayer.playback_speed = 1
# Prevent diagonal movement being very fast
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(position + direction,Vector3.UP)
$AnimationPlayer.playback_speed = 4
else:
$AnimationPlayer.playback_speed = 1
velocity.x = direction.x * speed
velocity.z = direction.z * speed
# Ground Velocity
target_velocity.x = direction.x * speed
target_velocity.z = direction.z * speed
# Jumping
if is_on_floor() and Input.is_action_just_pressed("jump"):
velocity.y += jump_impulse
# Vertical Velocity
if not is_on_floor(): # If in the air, fall towards the floor
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
velocity.y -= fall_acceleration * delta
velocity = move_and_slide(velocity, Vector3.UP)
# Jumping.
if is_on_floor() and Input.is_action_just_pressed("jump"):
target_velocity.y = jump_impulse
for index in range(get_slide_count()):
var collision = get_slide_collision(index)
if collision.collider.is_in_group("mob"):
var mob = collision.collider
if Vector3.UP.dot(collision.normal) > 0.1:
mob.squash()
velocity.y = bounce_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)
$Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
# 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
func die():
emit_signal("hit")
queue_free()
# Moving the Character
velocity = target_velocity
move_and_slide()
$Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
func _on_MobDetector_body_entered(_body):
die()
# And this function at the bottom.
func die():
emit_signal("hit")
queue_free()
func _on_mob_detector_body_entered(body):
die()
.. code-tab:: csharp
public class Player : CharacterBody3D
@@ -449,42 +480,44 @@ And the *Mob*'s script.
.. tabs::
.. code-tab:: gdscript GDScript
extends CharacterBody3D
extends CharacterBody3D
# Emitted when the player jumped on the mob.
signal squashed
# 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
# 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
var velocity = Vector3.ZERO
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)
# In this rotation^, the mob will move directly towards the player
# so we rotate it randomly within range of -90 and +90 degrees.
rotate_y(randf_range(-PI / 4, PI / 4))
func _physics_process(_delta):
move_and_slide(velocity)
# 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.playback_speed = random_speed / min_speed
func initialize(start_position, player_position):
look_at_from_position(start_position, player_position, Vector3.UP)
rotate_y(rand_range(-PI / 4, PI / 4))
var random_speed = rand_range(min_speed, max_speed)
velocity = Vector3.FORWARD * random_speed
velocity = velocity.rotated(Vector3.UP, rotation.y)
$AnimationPlayer.playback_speed = random_speed / min_speed
func squash():
emit_signal("squashed")
queue_free()
func _on_VisibilityNotifier_screen_exited():
queue_free()
func _on_visible_on_screen_notifier_3d_screen_exited():
queue_free()
func squash():
emit_signal("squashed")
queue_free() # Destroy this node
.. code-tab:: csharp
public class Mob : CharacterBody3D
@@ -533,21 +566,21 @@ And the *Mob*'s script.
.. |image0| image:: img/squash-the-creeps-final.gif
.. |image1| image:: img/09.adding_animations/01.animation_player_dock.png
.. |image2| image:: img/09.adding_animations/02.new_animation.png
.. |image2| image:: img/09.adding_animations/02.new_animation.webp
.. |image3| image:: img/09.adding_animations/03.float_name.png
.. |image4| image:: img/09.adding_animations/03.timeline.png
.. |image5| image:: img/09.adding_animations/04.autoplay_and_loop.png
.. |image6| image:: img/09.adding_animations/05.pin_icon.png
.. |image7| image:: img/09.adding_animations/06.animation_duration.png
.. |image8| image:: img/09.adding_animations/07.editable_timeline.png
.. |image9| image:: img/09.adding_animations/08.zoom_slider.png
.. |image10| image:: img/09.adding_animations/09.creating_first_keyframe.png
.. |image11| image:: img/09.adding_animations/10.initial_keys.png
.. |image12| image:: img/09.adding_animations/11.moving_keys.png
.. |image13| image:: img/09.adding_animations/12.second_keys_values.png
.. |image14| image:: img/09.adding_animations/13.second_keys.png
.. |image7| image:: img/09.adding_animations/06.animation_duration.webp
.. |image8| image:: img/09.adding_animations/07.editable_timeline.webp
.. |image9| image:: img/09.adding_animations/08.zoom_slider.webp
.. |image10| image:: img/09.adding_animations/09.creating_first_keyframe.webp
.. |image11| image:: img/09.adding_animations/10.initial_keys.webp
.. |image12| image:: img/09.adding_animations/11.moving_keys.webp
.. |image13| image:: img/09.adding_animations/12.second_keys_values.webp
.. |image14| image:: img/09.adding_animations/13.second_keys.webp
.. |image15| image:: img/09.adding_animations/14.play_button.png
.. |image16| image:: img/09.adding_animations/15.box_select.png
.. |image16| image:: img/09.adding_animations/15.box_select.webp
.. |image17| image:: img/09.adding_animations/16.easing_property.png
.. |image18| image:: img/09.adding_animations/17.ease_out.png
.. |image19| image:: img/09.adding_animations/18.ease_out_second_rotation_key.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB