Update "Your first 3D game" tutorial for Godot 4 (#6243)
* FULLY REMADE FOR 4.0 Co-authored-by: Johannes Loepelmann <johannes@loepelmann.de>
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
BIN
getting_started/first_3d_game/img/01.game_setup/11.box_mesh.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
BIN
getting_started/first_3d_game/img/04.mob_scene/10.node_dock.webp
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 6.7 KiB |