Removed some of the rst files.

This commit is contained in:
Relintai 2022-03-19 20:28:23 +01:00
parent 23ea9d5dc2
commit 4acaef7d25
211 changed files with 0 additions and 6193 deletions

View File

@ -1,221 +0,0 @@
.. _doc_docs_changelog:
Documentation changelog
=======================
The documentation is continually being improved. The release of version 3.2
includes many new tutorials, many fixes and updates for old tutorials, and many updates
to the :ref:`class reference <toc-class-ref>`. Below is a list of new tutorials
added since version 3.1.
.. note:: This document only contains new tutorials so not all changes are reflected,
many tutorials have been substantially updated but are not reflected in this document.
New tutorials since version 3.1
-------------------------------
Project workflow
^^^^^^^^^^^^^^^^
- :ref:`doc_android_custom_build`
2D
^^
- :ref:`doc_2d_sprite_animation`
Audio
^^^^^
- :ref:`doc_recording_with_microphone`
- :ref:`doc_sync_with_audio`
Math
^^^^
- :ref:`doc_beziers_and_curves`
- :ref:`doc_interpolation`
Inputs
^^^^^^
- :ref:`doc_input_examples`
Internationalization
^^^^^^^^^^^^^^^^^^^^
- :ref:`doc_localization_using_gettext`
Shading
^^^^^^^
- Your First Shader Series:
- :ref:`doc_introduction_to_shaders`
- :ref:`doc_your_first_canvasitem_shader`
- :ref:`doc_your_first_spatial_shader`
- :ref:`doc_your_second_spatial_shader`
- :ref:`doc_visual_shaders`
Networking
^^^^^^^^^^
- :ref:`doc_webrtc`
VR
^^
- :ref:`doc_vr_starter_tutorial_part_one`
- :ref:`doc_vr_starter_tutorial_part_two`
Plugins
^^^^^^^
- :ref:`doc_android_plugin`
- :ref:`doc_inspector_plugins`
- :ref:`doc_visual_shader_plugins`
Multi-threading
^^^^^^^^^^^^^^^
- :ref:`doc_using_multiple_threads`
Creating content
^^^^^^^^^^^^^^^^
Procedural geometry series:
- :ref:`Procedural geometry <toc-procedural_geometry>`
- :ref:`doc_arraymesh`
- :ref:`doc_surfacetool`
- :ref:`doc_meshdatatool`
- :ref:`doc_immediategeometry`
Optimization
^^^^^^^^^^^^
- :ref:`doc_using_multimesh`
- :ref:`doc_using_servers`
Legal
^^^^^
- :ref:`doc_complying_with_licenses`
New tutorials since version 3.0
-------------------------------
Step by step
^^^^^^^^^^^^
- :ref:`doc_signals`
- :ref:`doc_exporting_basics`
Scripting
^^^^^^^^^
- :ref:`doc_gdscript_static_typing`
Project workflow
^^^^^^^^^^^^^^^^
Best Practices:
- :ref:`doc_introduction_best_practices`
- :ref:`doc_what_are_godot_classes`
- :ref:`doc_scene_organization`
- :ref:`doc_scenes_versus_scripts`
- :ref:`doc_autoloads_versus_internal_nodes`
- :ref:`doc_node_alternatives`
- :ref:`doc_godot_interfaces`
- :ref:`doc_godot_notifications`
- :ref:`doc_data_preferences`
- :ref:`doc_logic_preferences`
2D
^^
- :ref:`doc_2d_lights_and_shadows`
- :ref:`doc_2d_meshes`
3D
^^
- :ref:`doc_csg_tools`
- :ref:`doc_animating_thousands_of_fish`
- :ref:`doc_controlling_thousands_of_fish`
Physics
^^^^^^^
- :ref:`doc_ragdoll_system`
- :ref:`doc_soft_body`
Animation
^^^^^^^^^
- :ref:`doc_2d_skeletons`
- :ref:`doc_animation_tree`
GUI
^^^
- :ref:`doc_gui_containers`
Viewports
^^^^^^^^^
- :ref:`doc_viewport_as_texture`
- :ref:`doc_custom_postprocessing`
Shading
^^^^^^^
- :ref:`doc_converting_glsl_to_godot_shaders`
- :ref:`doc_advanced_postprocessing`
Shading Reference:
- :ref:`doc_introduction_to_shaders`
- :ref:`doc_shading_language`
- :ref:`doc_spatial_shader`
- :ref:`doc_canvas_item_shader`
- :ref:`doc_particle_shader`
Plugins
^^^^^^^
- :ref:`doc_making_main_screen_plugins`
- :ref:`doc_spatial_gizmo_plugins`
Platform-specific
^^^^^^^^^^^^^^^^^
- :ref:`doc_customizing_html5_shell`
Multi-threading
^^^^^^^^^^^^^^^
- :ref:`doc_thread_safe_apis`
Creating content
^^^^^^^^^^^^^^^^
- :ref:`doc_making_trees`
Miscellaneous
^^^^^^^^^^^^^
- :ref:`doc_jitter_stutter`
- :ref:`doc_running_code_in_the_editor`
- :ref:`doc_change_scenes_manually`
- :ref:`doc_gles2_gles3_differences`
Compiling
^^^^^^^^^
- :ref:`doc_optimizing_for_size`
- :ref:`doc_compiling_with_script_encryption_key`
Engine development
^^^^^^^^^^^^^^^^^^
- :ref:`doc_binding_to_external_libraries`

View File

@ -9,8 +9,6 @@ About
faq
troubleshooting
list_of_features
docs_changelog
release_policy
complying_with_licenses
.. history

View File

@ -1,149 +0,0 @@
.. _doc_release_policy:
Godot release policy
====================
Godot's release policy is in constant evolution. What is described below is
intended to give a general idea of what to expect, but what will actually
happen depends on the choices of core contributors, and the needs of the
community at a given time.
Godot versioning
----------------
Godot loosely follows `Semantic Versioning <https://semver.org/>`__ with a
``major.minor.patch`` versioning system, albeit with an interpretation of each
term adapted to the complexity of a game engine:
- The ``major`` version is incremented when major compatibility breakages happen
which imply significant porting work to move projects from one major version
to another.
For example, porting Godot projects from Godot 2.1 to Godot 3.0 required
running the project through a conversion tool, and then performing a number
of further adjustments manually for what the tool could not do automatically.
- The ``minor`` version is incremented for feature releases which do not break
compatibility in a major way. Minor compatibility breakage in very specific
areas *may* happen in minor versions, but the vast majority of projects
should not be affected or require significant porting work.
The reason for this is that as a game engine, Godot covers many areas such
as rendering, physics, scripting, etc., and fixing bugs or implementing new
features in a given area may sometimes require changing the behavior of a
feature, or modifying the interface of a given class, even if the rest of
the engine API remains backwards compatible.
.. tip::
Upgrading to a new minor version is therefore recommended for all users,
but some testing is necessary to ensure that your project still behaves as
expected in a new minor version.
- The ``patch`` version is incremented for maintenance releases which focus on
fixing bugs and security issues, implementing new requirements for platform
support, and backporting safe usability enhancements. Patch releases are
backwards compatible.
Patch versions may include minor new features which do not impact the
existing API, and thus have no risk of impacting existing projects.
.. tip::
Updating to new patch versions is therefore considered safe and strongly
recommended to all users of a given stable branch.
We call ``major.minor`` combinations *stable branches*. Each stable branch
starts with a ``major.minor`` release (without the ``0`` for ``patch``) and is
further developed for maintenance releases in a Git branch of the same name
(for example patch updates for the 3.3 stable branch are developed in the
``3.3`` Git branch).
.. note::
As mentioned in the introduction, Godot's release policy is evolving, and
earlier Godot releases may not have followed the above rules to the letter.
In particular, the 3.2 stable branch received a number of new features in
3.2.2 which would have warranted a ``minor`` version increment.
Release support timeline
------------------------
Stable branches are supported *at minimum* until the next stable branch is
released and has received its first patch update. In practice, we support
stable branches on a *best effort* basis for as long as they have active users
who need maintenance updates.
Whenever a new major version is released, we make the previous stable branch a
long-term supported release, and do our best to provide fixes for issues
encountered by users of that branch who cannot port complex projects to the new
major version. This was the case for the 2.1 branch, and will be the case for
the latest 3.x stable branch by the time Godot 4.0 is released.
In a given minor release series, only the latest patch release receives support.
If you experience an issue using an older patch release, please upgrade to the
latest patch release of that series and test again before reporting an issue
on GitHub.
+-------------+----------------------+--------------------------------------------------------------------------+
| **Version** | **Release date** | **Support level** |
+-------------+----------------------+--------------------------------------------------------------------------+
| Godot 4.0 | ~2022 (see below) | |unstable| *Current focus of development (unstable).* |
+-------------+----------------------+--------------------------------------------------------------------------+
| Godot 3.5 | Q2 2022 | |supported| *Beta.* Receives new features as well as bug fixes while |
| | | under development. |
+-------------+----------------------+--------------------------------------------------------------------------+
| Godot 3.4 | November 2021 | |supported| Receives fixes for bugs, security and platform support |
| | | issues, as well as backwards-compatible usability enhancements. |
+-------------+----------------------+--------------------------------------------------------------------------+
| Godot 3.3 | April 2021 | |partial| Receives fixes for security and platform support issues only. |
+-------------+----------------------+--------------------------------------------------------------------------+
| Godot 3.2 | January 2020 | |eol| No longer supported as fully superseded by the compatible 3.3 |
| | | release (last update: 3.2.3). |
+-------------+----------------------+--------------------------------------------------------------------------+
| Godot 3.1 | March 2019 | |eol| No longer supported (last update: 3.1.2). |
+-------------+----------------------+--------------------------------------------------------------------------+
| Godot 3.0 | January 2018 | |eol| No longer supported (last update: 3.0.6). |
+-------------+----------------------+--------------------------------------------------------------------------+
| Godot 2.1 | July 2016 | |eol| No longer supported (last update: 2.1.6). |
+-------------+----------------------+--------------------------------------------------------------------------+
| Godot 2.0 | February 2016 | |eol| No longer supported (last update: 2.0.4.1). |
+-------------+----------------------+--------------------------------------------------------------------------+
| Godot 1.1 | May 2015 | |eol| No longer supported. |
+-------------+----------------------+--------------------------------------------------------------------------+
| Godot 1.0 | December 2014 | |eol| No longer supported. |
+-------------+----------------------+--------------------------------------------------------------------------+
.. |supported| image:: img/supported.png
.. |partial| image:: img/partial.png
.. |eol| image:: img/eol.png
.. |unstable| image:: img/unstable.png
**Legend:**
|supported| Full support
|partial| Partial support
|eol| No support (end of life)
|unstable| Development version
Pre-release Godot versions aren't intended to be used in production and are
provided for testing purposes only.
.. _doc_release_policy_when_is_next_release_out:
When is the next release out?
-----------------------------
While Godot contributors aren't working under any deadlines, we have
historically had one major or minor release per year, with several maintenance
updates between each.
Starting with Godot 3.3, we aim to accelerate our development cycles for minor
releases, so you can expect a new minor release every 3 to 6 months.
Maintenance (patch) releases will be released as needed with potentially very
short development cycles, to provide users of the current stable branch with
the latest bug fixes for their production needs.
As for the upcoming Godot 4.0, we can only say that we aim for a **2022**
release, but any closer estimate is likely to be hard to uphold. Alpha builds
will be published as soon as the main features for Godot 4.0 are finalized.

View File

@ -1,77 +0,0 @@
.. _doc_your_first_2d_game_project_setup:
Setting up the project
======================
In this short first part, we'll set up and organize the project.
Launch Godot and create a new project.
.. image:: img/new-project-button.png
.. tabs::
.. tab:: GDScript
Download :download:`dodge_assets.zip <files/dodge_assets.zip>`.
The archive contains the images and sounds you'll be using
to make the game. Extract the archive and move the ``art/``
and ``fonts/`` directories to your project's directory.
.. tab:: C#
Download :download:`dodge_assets.zip <files/dodge_assets.zip>`.
The archive contains the images and sounds you'll be using
to make the game. Extract the archive and move the ``art/``
and ``fonts/`` directories to your project's directory.
Ensure that you have the required dependencies to use C# in Godot.
You need the .NET Core 3.1 SDK, and an editor such as VS Code.
See :ref:`doc_c_sharp_setup`.
.. tab:: GDNative C++
Download :download:`dodge_assets_with_gdnative.zip
<files/dodge_assets_with_gdnative.zip>`.
The archive contains the images and sounds you'll be using
to make the game. It also contains a starter GDNative project
including a ``SConstruct`` file, a ``dodge_the_creeps.gdnlib``
file, a ``player.gdns`` file, and an ``entry.cpp`` file.
Ensure that you have the required dependencies to use GDNative C++.
You need a C++ compiler such as GCC or Clang or MSVC that supports C++14.
On Windows you can download Visual Studio 2019 and select the C++ workload.
You also need SCons to use the build system (the SConstruct file).
Then you need to `download the Godot C++ bindings <https://github.com/godotengine/godot-cpp>`_
and place them in your project.
Your project folder should look like this.
.. image:: img/folder-content.png
This game is designed for portrait mode, so we need to adjust the size of the
game window. Click on *Project -> Project Settings* to open the project settings
window and in the left column, open the *Display -> Window* tab. There, set
"Width" to ``480`` and "Height" to ``720``.
.. image:: img/setting-project-width-and-height.png
Also, scroll down to the bottom of the section and, under the "Stretch" options,
set ``Mode`` to "2d" and ``Aspect`` to "keep". This ensures that the game scales
consistently on different sized screens.
.. image:: img/setting-stretch-mode.png
Organizing the project
~~~~~~~~~~~~~~~~~~~~~~
In this project, we will make 3 independent scenes: ``Player``, ``Mob``, and
``HUD``, which we will combine into the game's ``Main`` scene.
In a larger project, it might be useful to create folders to hold the various
scenes and their scripts, but for this relatively small game, you can save your
scenes and scripts in the project's root folder, identified by ``res://``. You
can see your project folders in the FileSystem dock in the lower left corner:
.. image:: img/filesystem_dock.png
With the project in place, we're ready to design the player scene in the next lesson.

View File

@ -1,100 +0,0 @@
.. _doc_your_first_2d_game_player_scene:
Creating the player scene
=========================
With the project settings in place, we can start working on the
player-controlled character.
The first scene will define the ``Player`` object. One of the benefits of
creating a separate Player scene is that we can test it separately, even before
we've created other parts of the game.
Node structure
~~~~~~~~~~~~~~
To begin, we need to choose a root node for the player object. As a general
rule, a scene's root node should reflect the object's desired functionality -
what the object *is*. Click the "Other Node" button and add an :ref:`Area2D
<class_Area2D>` node to the scene.
.. image:: img/add_node.png
Godot will display a warning icon next to the node in the scene tree. You can
ignore it for now. We will address it later.
With ``Area2D`` we can detect objects that overlap or run into the player.
Change the node's name to ``Player`` by double-clicking on it. Now that we've
set the scene's root node, we can add additional nodes to give it more
functionality.
Before we add any children to the ``Player`` node, we want to make sure we don't
accidentally move or resize them by clicking on them. Select the node and click
the icon to the right of the lock; its tooltip says "Makes sure the object's
children are not selectable."
.. image:: img/lock_children.png
Save the scene. Click Scene -> Save, or press :kbd:`Ctrl + S` on Windows/Linux
or :kbd:`Cmd + S` on macOS.
.. note:: For this project, we will be following the Godot naming conventions.
- **GDScript**: Classes (nodes) use PascalCase, variables and
functions use snake_case, and constants use ALL_CAPS (See
:ref:`doc_gdscript_styleguide`).
- **C#**: Classes, export variables and methods use PascalCase,
private fields use _camelCase, local variables and parameters use
camelCase (See :ref:`doc_c_sharp_styleguide`). Be careful to type
the method names precisely when connecting signals.
Sprite animation
~~~~~~~~~~~~~~~~
Click on the ``Player`` node and add an :ref:`AnimatedSprite
<class_AnimatedSprite>` node as a child. The ``AnimatedSprite`` will handle the
appearance and animations for our player. Notice that there is a warning symbol
next to the node. An ``AnimatedSprite`` requires a :ref:`SpriteFrames
<class_SpriteFrames>` resource, which is a list of the animations it can
display. To create one, find the ``Frames`` property in the Inspector and click
"[empty]" -> "New SpriteFrames". Click again to open the "SpriteFrames" panel:
.. image:: img/spriteframes_panel.png
On the left is a list of animations. Click the "default" one and rename it to
"walk". Then click the "New Animation" button to create a second animation named
"up". Find the player images in the "FileSystem" tab - they're in the ``art``
folder you unzipped earlier. Drag the two images for each animation, named
``playerGrey_up[1/2]`` and ``playerGrey_walk[1/2]``, into the "Animation Frames"
side of the panel for the corresponding animation:
.. image:: img/spriteframes_panel2.png
The player images are a bit too large for the game window, so we need to scale
them down. Click on the ``AnimatedSprite`` node and set the ``Scale`` property
to ``(0.5, 0.5)``. You can find it in the Inspector under the ``Node2D``
heading.
.. image:: img/player_scale.png
Finally, add a :ref:`CollisionShape2D <class_CollisionShape2D>` as a child of
``Player``. This will determine the player's "hitbox", or the bounds of its
collision area. For this character, a ``CapsuleShape2D`` node gives the best
fit, so next to "Shape" in the Inspector, click "[empty]"" -> "New
CapsuleShape2D". Using the two size handles, resize the shape to cover the
sprite:
.. image:: img/player_coll_shape.png
When you're finished, your ``Player`` scene should look like this:
.. image:: img/player_scene_nodes.png
Make sure to save the scene again after these changes.
In the next part, we'll add a script to the player node to move and animate it.
Then, we'll set up collision detection to know when the player got hit by
something.

View File

@ -1,543 +0,0 @@
.. _doc_your_first_2d_game_coding_the_player:
Coding the player
=================
In this lesson, we'll add player movement, animation, and set it up to detect
collisions.
To do so, we need to add some functionality that we can't get from a built-in
node, so we'll add a script. Click the ``Player`` node and click the "Attach
Script" button:
.. image:: img/add_script_button.png
In the script settings window, you can leave the default settings alone. Just
click "Create":
.. note:: If you're creating a C# script or other languages, select the language
from the `language` drop down menu before hitting create.
.. image:: img/attach_node_window.png
.. note:: If this is your first time encountering GDScript, please read
:ref:`doc_scripting` before continuing.
Start by declaring the member variables this object will need:
.. tabs::
.. code-tab:: gdscript GDScript
extends Area2D
export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.
.. code-tab:: csharp
using Godot;
using System;
public class Player : Area2D
{
[Export]
public int Speed = 400; // How fast the player will move (pixels/sec).
public Vector2 ScreenSize; // Size of the game window.
}
.. code-tab:: cpp
// A `player.gdns` file has already been created for you. Attach it to the Player node.
// Create two files `player.cpp` and `player.hpp` next to `entry.cpp` in `src`.
// This code goes in `player.hpp`. We also define the methods we'll be using here.
#ifndef PLAYER_H
#define PLAYER_H
#include <AnimatedSprite.hpp>
#include <Area2D.hpp>
#include <CollisionShape2D.hpp>
#include <Godot.hpp>
#include <Input.hpp>
class Player : public godot::Area2D {
GODOT_CLASS(Player, godot::Area2D)
godot::AnimatedSprite *_animated_sprite;
godot::CollisionShape2D *_collision_shape;
godot::Input *_input;
godot::Vector2 _screen_size; // Size of the game window.
public:
real_t speed = 400; // How fast the player will move (pixels/sec).
void _init() {}
void _ready();
void _process(const double p_delta);
void start(const godot::Vector2 p_position);
void _on_Player_body_entered(godot::Node2D *_body);
static void _register_methods();
};
#endif // PLAYER_H
Using the ``export`` keyword on the first variable ``speed`` allows us to set
its value in the Inspector. This can be handy for values that you want to be
able to adjust just like a node's built-in properties. Click on the ``Player``
node and you'll see the property now appears in the "Script Variables" section
of the Inspector. Remember, if you change the value here, it will override the
value written in the script.
.. warning:: If you're using C#, you need to (re)build the project assemblies
whenever you want to see new export variables or signals. This
build can be manually triggered by clicking the word "Mono" at the
bottom of the editor window to reveal the Mono Panel, then clicking
the "Build Project" button.
.. image:: img/export_variable.png
The ``_ready()`` function is called when a node enters the scene tree, which is
a good time to find the size of the game window:
.. tabs::
.. code-tab:: gdscript GDScript
func _ready():
screen_size = get_viewport_rect().size
.. code-tab:: csharp
public override void _Ready()
{
ScreenSize = GetViewportRect().Size;
}
.. code-tab:: cpp
// This code goes in `player.cpp`.
#include "player.hpp"
void Player::_ready() {
_animated_sprite = get_node<godot::AnimatedSprite>("AnimatedSprite");
_collision_shape = get_node<godot::CollisionShape2D>("CollisionShape2D");
_input = godot::Input::get_singleton();
_screen_size = get_viewport_rect().size;
}
Now we can use the ``_process()`` function to define what the player will do.
``_process()`` is called every frame, so we'll use it to update elements of our
game, which we expect will change often. For the player, we need to do the
following:
- Check for input.
- Move in the given direction.
- Play the appropriate animation.
First, we need to check for input - is the player pressing a key? For this game,
we have 4 direction inputs to check. Input actions are defined in the Project
Settings under "Input Map". Here, you can define custom events and assign
different keys, mouse events, or other inputs to them. For this game, we will
map the arrow keys to the four directions.
Click on *Project -> Project Settings* to open the project settings window and
click on the *Input Map* tab at the top. Type "move_right" in the top bar and
click the "Add" button to add the ``move_right`` action.
.. image:: img/input-mapping-add-action.png
We need to assign a key to this action. Click the "+" icon on the right, then
click the "Key" option in the drop-down menu. A dialog asks you to type in the
desired key. Press the right arrow on your keyboard and click "Ok".
.. image:: img/input-mapping-add-key.png
Repeat these steps to add three more mappings:
1. ``move_left`` mapped to the left arrow key.
2. ``move_up`` mapped to the up arrow key.
3. And ``move_down`` mapped to the down arrow key.
Your input map tab should look like this:
.. image:: img/input-mapping-completed.png
Click the "Close" button to close the project settings.
.. note::
We only mapped one key to each input action, but you can map multiple keys,
joystick buttons, or mouse buttons to the same input action.
You can detect whether a key is pressed using ``Input.is_action_pressed()``,
which returns ``true`` if it's pressed or ``false`` if it isn't.
.. tabs::
.. code-tab:: gdscript GDScript
func _process(delta):
var velocity = Vector2.ZERO # The player's movement vector.
if Input.is_action_pressed("move_right"):
velocity.x += 1
if Input.is_action_pressed("move_left"):
velocity.x -= 1
if Input.is_action_pressed("move_down"):
velocity.y += 1
if Input.is_action_pressed("move_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite.play()
else:
$AnimatedSprite.stop()
.. code-tab:: csharp
public override void _Process(float delta)
{
var velocity = Vector2.Zero; // The player's movement vector.
if (Input.IsActionPressed("move_right"))
{
velocity.x += 1;
}
if (Input.IsActionPressed("move_left"))
{
velocity.x -= 1;
}
if (Input.IsActionPressed("move_down"))
{
velocity.y += 1;
}
if (Input.IsActionPressed("move_up"))
{
velocity.y -= 1;
}
var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");
if (velocity.Length() > 0)
{
velocity = velocity.Normalized() * Speed;
animatedSprite.Play();
}
else
{
animatedSprite.Stop();
}
}
.. code-tab:: cpp
// This code goes in `player.cpp`.
void Player::_process(const double p_delta) {
godot::Vector2 velocity(0, 0);
velocity.x = _input->get_action_strength("move_right") - _input->get_action_strength("move_left");
velocity.y = _input->get_action_strength("move_down") - _input->get_action_strength("move_up");
if (velocity.length() > 0) {
velocity = velocity.normalized() * speed;
_animated_sprite->play();
} else {
_animated_sprite->stop();
}
}
We start by setting the ``velocity`` to ``(0, 0)`` - by default, the player
should not be moving. Then we check each input and add/subtract from the
``velocity`` to obtain a total direction. For example, if you hold ``right`` and
``down`` at the same time, the resulting ``velocity`` vector will be ``(1, 1)``.
In this case, since we're adding a horizontal and a vertical movement, the
player would move *faster* diagonally than if it just moved horizontally.
We can prevent that if we *normalize* the velocity, which means we set its
*length* to ``1``, then multiply by the desired speed. This means no more fast
diagonal movement.
.. tip:: If you've never used vector math before, or need a refresher, you can
see an explanation of vector usage in Godot at :ref:`doc_vector_math`.
It's good to know but won't be necessary for the rest of this tutorial.
We also check whether the player is moving so we can call ``play()`` or
``stop()`` on the AnimatedSprite.
``$`` is shorthand for ``get_node()``. So in the code above,
``$AnimatedSprite.play()`` is the same as
``get_node("AnimatedSprite").play()``.
.. tip:: In GDScript, ``$`` returns the node at the relative path from the
current node, or returns ``null`` if the node is not found. Since
AnimatedSprite is a child of the current node, we can use
``$AnimatedSprite``.
Now that we have a movement direction, we can update the player's position. We
can also use ``clamp()`` to prevent it from leaving the screen. *Clamping* a
value means restricting it to a given range. Add the following to the bottom of
the ``_process`` function (make sure it's not indented under the `else`):
.. tabs::
.. code-tab:: gdscript GDScript
position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
.. code-tab:: csharp
Position += velocity * delta;
Position = new Vector2(
x: Mathf.Clamp(Position.x, 0, ScreenSize.x),
y: Mathf.Clamp(Position.y, 0, ScreenSize.y)
);
.. code-tab:: cpp
godot::Vector2 position = get_position();
position += velocity * (real_t)p_delta;
position.x = godot::Math::clamp(position.x, (real_t)0.0, _screen_size.x);
position.y = godot::Math::clamp(position.y, (real_t)0.0, _screen_size.y);
set_position(position);
.. tip:: The `delta` parameter in the `_process()` function refers to the *frame
length* - the amount of time that the previous frame took to complete.
Using this value ensures that your movement will remain consistent even
if the frame rate changes.
Click "Play Scene" (:kbd:`F6`, :kbd:`Cmd + R` on macOS) and confirm you can move
the player around the screen in all directions.
.. warning:: If you get an error in the "Debugger" panel that says
``Attempt to call function 'play' in base 'null instance' on a null
instance``
this likely means you spelled the name of the AnimatedSprite node
wrong. Node names are case-sensitive and ``$NodeName`` must match
the name you see in the scene tree.
Choosing animations
~~~~~~~~~~~~~~~~~~~
Now that the player can move, we need to change which animation the
AnimatedSprite is playing based on its direction. We have the "walk" animation,
which shows the player walking to the right. This animation should be flipped
horizontally using the ``flip_h`` property for left movement. We also have the
"up" animation, which should be flipped vertically with ``flip_v`` for downward
movement. Let's place this code at the end of the ``_process()`` function:
.. tabs::
.. code-tab:: gdscript GDScript
if velocity.x != 0:
$AnimatedSprite.animation = "walk"
$AnimatedSprite.flip_v = false
# See the note below about boolean assignment.
$AnimatedSprite.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite.animation = "up"
$AnimatedSprite.flip_v = velocity.y > 0
.. code-tab:: csharp
if (velocity.x != 0)
{
animatedSprite.Animation = "walk";
animatedSprite.FlipV = false;
// See the note below about boolean assignment.
animatedSprite.FlipH = velocity.x < 0;
}
else if (velocity.y != 0)
{
animatedSprite.Animation = "up";
animatedSprite.FlipV = velocity.y > 0;
}
.. code-tab:: cpp
if (velocity.x != 0) {
_animated_sprite->set_animation("right");
_animated_sprite->set_flip_v(false);
// See the note below about boolean assignment.
_animated_sprite->set_flip_h(velocity.x < 0);
} else if (velocity.y != 0) {
_animated_sprite->set_animation("up");
_animated_sprite->set_flip_v(velocity.y > 0);
}
.. Note:: The boolean assignments in the code above are a common shorthand for
programmers. Since we're doing a comparison test (boolean) and also
*assigning* a boolean value, we can do both at the same time. Consider
this code versus the one-line boolean assignment above:
.. tabs::
.. code-tab :: gdscript GDScript
if velocity.x < 0:
$AnimatedSprite.flip_h = true
else:
$AnimatedSprite.flip_h = false
.. code-tab:: csharp
if (velocity.x < 0)
{
animatedSprite.FlipH = true;
}
else
{
animatedSprite.FlipH = false;
}
Play the scene again and check that the animations are correct in each of the
directions.
.. tip:: A common mistake here is to type the names of the animations wrong. The
animation names in the SpriteFrames panel must match what you type in
the code. If you named the animation ``"Walk"``, you must also use a
capital "W" in the code.
When you're sure the movement is working correctly, add this line to
``_ready()``, so the player will be hidden when the game starts:
.. tabs::
.. code-tab:: gdscript GDScript
hide()
.. code-tab:: csharp
Hide();
.. code-tab:: cpp
hide();
Preparing for collisions
~~~~~~~~~~~~~~~~~~~~~~~~
We want ``Player`` to detect when it's hit by an enemy, but we haven't made any
enemies yet! That's OK, because we're going to use Godot's *signal*
functionality to make it work.
Add the following at the top of the script, after ``extends Area2D``:
.. tabs::
.. code-tab:: gdscript GDScript
signal hit
.. code-tab:: csharp
// Don't forget to rebuild the project so the editor knows about the new signal.
[Signal]
public delegate void Hit();
.. code-tab:: cpp
// This code goes in `player.cpp`.
// We need to register the signal here, and while we're here, we can also
// register the other methods and register the speed property.
void Player::_register_methods() {
godot::register_method("_ready", &Player::_ready);
godot::register_method("_process", &Player::_process);
godot::register_method("start", &Player::start);
godot::register_method("_on_Player_body_entered", &Player::_on_Player_body_entered);
godot::register_property("speed", &Player::speed, (real_t)400.0);
// This below line is the signal.
godot::register_signal<Player>("hit", godot::Dictionary());
}
This defines a custom signal called "hit" that we will have our player emit
(send out) when it collides with an enemy. We will use ``Area2D`` to detect the
collision. Select the ``Player`` node and click the "Node" tab next to the
Inspector tab to see the list of signals the player can emit:
.. image:: img/player_signals.png
Notice our custom "hit" signal is there as well! Since our enemies are going to
be ``RigidBody2D`` nodes, we want the ``body_entered(body: Node)`` signal. This
signal will be emitted when a body contacts the player. Click "Connect.." and
the "Connect a Signal" window appears. We don't need to change any of these
settings so click "Connect" again. Godot will automatically create a function in
your player's script.
.. image:: img/player_signal_connection.png
Note the green icon indicating that a signal is connected to this function. Add
this code to the function:
.. tabs::
.. code-tab:: gdscript GDScript
func _on_Player_body_entered(body):
hide() # Player disappears after being hit.
emit_signal("hit")
# Must be deferred as we can't change physics properties on a physics callback.
$CollisionShape2D.set_deferred("disabled", true)
.. code-tab:: csharp
public void OnPlayerBodyEntered(PhysicsBody2D body)
{
Hide(); // Player disappears after being hit.
EmitSignal(nameof(Hit));
// Must be deferred as we can't change physics properties on a physics callback.
GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
}
.. code-tab:: cpp
// This code goes in `player.cpp`.
void Player::_on_Player_body_entered(godot::Node2D *_body) {
hide(); // Player disappears after being hit.
emit_signal("hit");
// Must be deferred as we can't change physics properties on a physics callback.
_collision_shape->set_deferred("disabled", true);
}
Each time an enemy hits the player, the signal is going to be emitted. We need
to disable the player's collision so that we don't trigger the ``hit`` signal
more than once.
.. Note:: Disabling the area's collision shape can cause an error if it happens
in the middle of the engine's collision processing. Using
``set_deferred()`` tells Godot to wait to disable the shape until it's
safe to do so.
The last piece is to add a function we can call to reset the player when
starting a new game.
.. tabs::
.. code-tab:: gdscript GDScript
func start(pos):
position = pos
show()
$CollisionShape2D.disabled = false
.. code-tab:: csharp
public void Start(Vector2 pos)
{
Position = pos;
Show();
GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
}
.. code-tab:: cpp
// This code goes in `player.cpp`.
void Player::start(const godot::Vector2 p_position) {
set_position(p_position);
show();
_collision_shape->set_disabled(false);
}
With the player working, we'll work on the enemy in the next lesson.

View File

@ -1,180 +0,0 @@
.. _doc_your_first_2d_game_creating_the_enemy:
Creating the enemy
==================
Now it's time to make the enemies our player will have to dodge. Their behavior
will not be very complex: mobs will spawn randomly at the edges of the screen,
choose a random direction, and move in a straight line.
We'll create a ``Mob`` scene, which we can then *instance* to create any number
of independent mobs in the game.
Node setup
~~~~~~~~~~
Click Scene -> New Scene and add the following nodes:
- :ref:`RigidBody2D <class_RigidBody2D>` (named ``Mob``)
- :ref:`AnimatedSprite <class_AnimatedSprite>`
- :ref:`CollisionShape2D <class_CollisionShape2D>`
- :ref:`VisibilityNotifier2D <class_VisibilityNotifier2D>`
Don't forget to set the children so they can't be selected, like you did with
the Player scene.
In the :ref:`RigidBody2D <class_RigidBody2D>` properties, set ``Gravity Scale``
to ``0``, so the mob will not fall downward. In addition, under the
:ref:`CollisionObject2D <class_CollisionObject2D>` section, click the ``Mask`` property and uncheck the first
box. This will ensure the mobs do not collide with each other.
.. image:: img/set_collision_mask.png
Set up the :ref:`AnimatedSprite <class_AnimatedSprite>` like you did for the
player. This time, we have 3 animations: ``fly``, ``swim``, and ``walk``. There
are two images for each animation in the art folder.
Adjust the "Speed (FPS)" to ``3`` for all animations.
.. image:: img/mob_animations.gif
Set the ``Playing`` property in the Inspector to "On".
We'll select one of these animations randomly so that the mobs will have some
variety.
Like the player images, these mob images need to be scaled down. Set the
``AnimatedSprite``'s ``Scale`` property to ``(0.75, 0.75)``.
As in the ``Player`` scene, add a ``CapsuleShape2D`` for the collision. To align
the shape with the image, you'll need to set the ``Rotation Degrees`` property
to ``90`` (under "Transform" in the Inspector).
Save the scene.
Enemy script
~~~~~~~~~~~~
Add a script to the ``Mob`` like this:
.. tabs::
.. code-tab:: gdscript GDScript
extends RigidBody2D
.. code-tab:: csharp
public class Mob : RigidBody2D
{
// Don't forget to rebuild the project.
}
.. code-tab:: cpp
// Copy `player.gdns` to `mob.gdns` and replace `Player` with `Mob`.
// Attach the `mob.gdns` file to the Mob node.
// Create two files `mob.cpp` and `mob.hpp` next to `entry.cpp` in `src`.
// This code goes in `mob.hpp`. We also define the methods we'll be using here.
#ifndef MOB_H
#define MOB_H
#include <AnimatedSprite.hpp>
#include <Godot.hpp>
#include <RigidBody2D.hpp>
class Mob : public godot::RigidBody2D {
GODOT_CLASS(Mob, godot::RigidBody2D)
godot::AnimatedSprite *_animated_sprite;
public:
void _init() {}
void _ready();
void _on_VisibilityNotifier2D_screen_exited();
static void _register_methods();
};
#endif // MOB_H
Now let's look at the rest of the script. In ``_ready()`` we play the animation
and randomly choose one of the three animation types:
.. tabs::
.. code-tab:: gdscript GDScript
func _ready():
$AnimatedSprite.playing = true
var mob_types = $AnimatedSprite.frames.get_animation_names()
$AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
.. code-tab:: csharp
public override void _Ready()
{
var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
animSprite.Playing = true;
string[] mobTypes = animSprite.Frames.GetAnimationNames();
animSprite.Animation = mobTypes[GD.Randi() % mobTypes.Length];
}
.. code-tab:: cpp
// This code goes in `mob.cpp`.
#include "mob.hpp"
#include <RandomNumberGenerator.hpp>
#include <SpriteFrames.hpp>
void Mob::_ready() {
godot::Ref<godot::RandomNumberGenerator> random = godot::RandomNumberGenerator::_new();
random->randomize();
_animated_sprite = get_node<godot::AnimatedSprite>("AnimatedSprite");
_animated_sprite->_set_playing(true);
godot::PoolStringArray mob_types = _animated_sprite->get_sprite_frames()->get_animation_names();
_animated_sprite->set_animation(mob_types[random->randi() % mob_types.size()]);
}
First, we get the list of animation names from the AnimatedSprite's ``frames``
property. This returns an Array containing all three animation names: ``["walk",
"swim", "fly"]``.
We then need to pick a random number between ``0`` and ``2`` to select one of
these names from the list (array indices start at ``0``). ``randi() % n``
selects a random integer between ``0`` and ``n-1``.
.. note:: You must use ``randomize()`` if you want your sequence of "random"
numbers to be different every time you run the scene. We're going to
use ``randomize()`` in our ``Main`` scene, so we won't need it here.
The last piece is to make the mobs delete themselves when they leave the screen.
Connect the ``screen_exited()`` signal of the ``VisibilityNotifier2D`` node and
add this code:
.. tabs::
.. code-tab:: gdscript GDScript
func _on_VisibilityNotifier2D_screen_exited():
queue_free()
.. code-tab:: csharp
public void OnVisibilityNotifier2DScreenExited()
{
QueueFree();
}
.. code-tab:: cpp
// This code goes in `mob.cpp`.
void Mob::_on_VisibilityNotifier2D_screen_exited() {
queue_free();
}
This completes the `Mob` scene.
With the player and enemies ready, in the next part, we'll bring them together
in a new scene. We'll make enemies spawn randomly around the game board and move
forward, turning our project into a playable game.

View File

@ -1,452 +0,0 @@
.. _doc_your_first_2d_game_the_main_game_scene:
The main game scene
===================
Now it's time to bring everything we did together into a playable game scene.
Create a new scene and add a :ref:`Node <class_Node>` named ``Main``. Ensure you
create a Node, **not** a Node2D. Click the "Instance" button and select your
saved ``Player.tscn``.
.. image:: img/instance_scene.png
Now, add the following nodes as children of ``Main``, and name them as shown
(values are in seconds):
- :ref:`Timer <class_Timer>` (named ``MobTimer``) - to control how often mobs
spawn
- :ref:`Timer <class_Timer>` (named ``ScoreTimer``) - to increment the score
every second
- :ref:`Timer <class_Timer>` (named ``StartTimer``) - to give a delay before
starting
- :ref:`Position2D <class_Position2D>` (named ``StartPosition``) - to indicate
the player's start position
Set the ``Wait Time`` property of each of the ``Timer`` nodes as follows:
- ``MobTimer``: ``0.5``
- ``ScoreTimer``: ``1``
- ``StartTimer``: ``2``
In addition, set the ``One Shot`` property of ``StartTimer`` to "On" and set
``Position`` of the ``StartPosition`` node to ``(240, 450)``.
Spawning mobs
~~~~~~~~~~~~~
The Main node will be spawning new mobs, and we want them to appear at a random
location on the edge of the screen. Add a :ref:`Path2D <class_Path2D>` node
named ``MobPath`` as a child of ``Main``. When you select ``Path2D``, you will
see some new buttons at the top of the editor:
.. image:: img/path2d_buttons.png
Select the middle one ("Add Point") and draw the path by clicking to add the
points at the corners shown. To have the points snap to the grid, make sure "Use
Grid Snap" and "Use Snap" are both selected. These options can be found to the
left of the "Lock" button, appearing as a magnet next to some dots and
intersecting lines, respectively.
.. image:: img/grid_snap_button.png
.. important:: Draw the path in *clockwise* order, or your mobs will spawn
pointing *outwards* instead of *inwards*!
.. image:: img/draw_path2d.gif
After placing point ``4`` in the image, click the "Close Curve" button and your
curve will be complete.
Now that the path is defined, add a :ref:`PathFollow2D <class_PathFollow2D>`
node as a child of ``MobPath`` and name it ``MobSpawnLocation``. This node will
automatically rotate and follow the path as it moves, so we can use it to select
a random position and direction along the path.
Your scene should look like this:
.. image:: img/main_scene_nodes.png
Main script
~~~~~~~~~~~
Add a script to ``Main``. At the top of the script, we use ``export
(PackedScene)`` to allow us to choose the Mob scene we want to instance.
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
export(PackedScene) var mob_scene
var score
.. code-tab:: csharp
public class Main : Node
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
#pragma warning disable 649
// We assign this in the editor, so we don't need the warning about not being assigned.
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public int Score;
}
.. code-tab:: cpp
// Copy `player.gdns` to `main.gdns` and replace `Player` with `Main`.
// Attach the `main.gdns` file to the Main node.
// Create two files `main.cpp` and `main.hpp` next to `entry.cpp` in `src`.
// This code goes in `main.hpp`. We also define the methods we'll be using here.
#ifndef MAIN_H
#define MAIN_H
#include <AudioStreamPlayer.hpp>
#include <CanvasLayer.hpp>
#include <Godot.hpp>
#include <Node.hpp>
#include <PackedScene.hpp>
#include <PathFollow2D.hpp>
#include <RandomNumberGenerator.hpp>
#include <Timer.hpp>
#include "hud.hpp"
#include "player.hpp"
class Main : public godot::Node {
GODOT_CLASS(Main, godot::Node)
int score;
HUD *_hud;
Player *_player;
godot::Node2D *_start_position;
godot::PathFollow2D *_mob_spawn_location;
godot::Timer *_mob_timer;
godot::Timer *_score_timer;
godot::Timer *_start_timer;
godot::AudioStreamPlayer *_music;
godot::AudioStreamPlayer *_death_sound;
godot::Ref<godot::RandomNumberGenerator> _random;
public:
godot::Ref<godot::PackedScene> mob_scene;
void _init() {}
void _ready();
void game_over();
void new_game();
void _on_MobTimer_timeout();
void _on_ScoreTimer_timeout();
void _on_StartTimer_timeout();
static void _register_methods();
};
#endif // MAIN_H
We also add a call to ``randomize()`` here so that the random number
generator generates different random numbers each time the game is run:
.. tabs::
.. code-tab:: gdscript GDScript
func _ready():
randomize()
.. code-tab:: csharp
public override void _Ready()
{
GD.Randomize();
}
.. code-tab:: cpp
// This code goes in `main.cpp`.
#include "main.hpp"
#include <SceneTree.hpp>
#include "mob.hpp"
void Main::_ready() {
_hud = get_node<HUD>("HUD");
_player = get_node<Player>("Player");
_start_position = get_node<godot::Node2D>("StartPosition");
_mob_spawn_location = get_node<godot::PathFollow2D>("MobPath/MobSpawnLocation");
_mob_timer = get_node<godot::Timer>("MobTimer");
_score_timer = get_node<godot::Timer>("ScoreTimer");
_start_timer = get_node<godot::Timer>("StartTimer");
// Uncomment these after adding the nodes in the "Sound effects" section of "Finishing up".
//_music = get_node<godot::AudioStreamPlayer>("Music");
//_death_sound = get_node<godot::AudioStreamPlayer>("DeathSound");
_random = (godot::Ref<godot::RandomNumberGenerator>)godot::RandomNumberGenerator::_new();
_random->randomize();
}
Click the ``Main`` node and you will see the ``Mob Scene`` property in the Inspector
under "Script Variables".
You can assign this property's value in two ways:
- Drag ``Mob.tscn`` from the "FileSystem" panel and drop it in the ``Mob``
property .
- Click the down arrow next to "[empty]" and choose "Load". Select ``Mob.tscn``.
Next, select the ``Player`` node in the Scene dock, and access the Node dock on
the sidebar. Make sure to have the Signals tab selected in the Node dock.
You should see a list of the signals for the ``Player`` node. Find and
double-click the ``hit`` signal in the list (or right-click it and select
"Connect..."). This will open the signal connection dialog. We want to make a
new function named ``game_over``, which will handle what needs to happen when a
game ends. Type "game_over" in the "Receiver Method" box at the bottom of the
signal connection dialog and click "Connect". Add the following code to the new
function, as well as a ``new_game`` function that will set everything up for a
new game:
.. tabs::
.. code-tab:: gdscript GDScript
func game_over():
$ScoreTimer.stop()
$MobTimer.stop()
func new_game():
score = 0
$Player.start($StartPosition.position)
$StartTimer.start()
.. code-tab:: csharp
public void GameOver()
{
GetNode<Timer>("MobTimer").Stop();
GetNode<Timer>("ScoreTimer").Stop();
}
public void NewGame()
{
Score = 0;
var player = GetNode<Player>("Player");
var startPosition = GetNode<Position2D>("StartPosition");
player.Start(startPosition.Position);
GetNode<Timer>("StartTimer").Start();
}
.. code-tab:: cpp
// This code goes in `main.cpp`.
void Main::game_over() {
_score_timer->stop();
_mob_timer->stop();
}
void Main::new_game() {
score = 0;
_player->start(_start_position->get_position());
_start_timer->start();
}
Now connect the ``timeout()`` signal of each of the Timer nodes (``StartTimer``,
``ScoreTimer`` , and ``MobTimer``) to the main script. ``StartTimer`` will start
the other two timers. ``ScoreTimer`` will increment the score by 1.
.. tabs::
.. code-tab:: gdscript GDScript
func _on_ScoreTimer_timeout():
score += 1
func _on_StartTimer_timeout():
$MobTimer.start()
$ScoreTimer.start()
.. code-tab:: csharp
public void OnScoreTimerTimeout()
{
Score++;
}
public void OnStartTimerTimeout()
{
GetNode<Timer>("MobTimer").Start();
GetNode<Timer>("ScoreTimer").Start();
}
.. code-tab:: cpp
// This code goes in `main.cpp`.
void Main::_on_ScoreTimer_timeout() {
score += 1;
}
void Main::_on_StartTimer_timeout() {
_mob_timer->start();
_score_timer->start();
}
// Also add this to register all methods and the mob scene property.
void Main::_register_methods() {
godot::register_method("_ready", &Main::_ready);
godot::register_method("game_over", &Main::game_over);
godot::register_method("new_game", &Main::new_game);
godot::register_method("_on_MobTimer_timeout", &Main::_on_MobTimer_timeout);
godot::register_method("_on_ScoreTimer_timeout", &Main::_on_ScoreTimer_timeout);
godot::register_method("_on_StartTimer_timeout", &Main::_on_StartTimer_timeout);
godot::register_property("mob_scene", &Main::mob_scene, (godot::Ref<godot::PackedScene>)nullptr);
}
In ``_on_MobTimer_timeout()``, we will create a mob instance, pick a random
starting location along the ``Path2D``, and set the mob in motion. The
``PathFollow2D`` node will automatically rotate as it follows the path, so we
will use that to select the mob's direction as well as its position.
When we spawn a mob, we'll pick a random value between ``150.0`` and
``250.0`` for how fast each mob will move (it would be boring if they were
all moving at the same speed).
Note that a new instance must be added to the scene using ``add_child()``.
.. tabs::
.. code-tab:: gdscript GDScript
func _on_MobTimer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instance()
# Choose a random location on Path2D.
var mob_spawn_location = get_node("MobPath/MobSpawnLocation")
mob_spawn_location.offset = randi()
# Set the mob's direction perpendicular to the path direction.
var direction = mob_spawn_location.rotation + PI / 2
# Set the mob's position to a random location.
mob.position = mob_spawn_location.position
# Add some randomness to the direction.
direction += rand_range(-PI / 4, PI / 4)
mob.rotation = direction
# Choose the velocity for the mob.
var velocity = Vector2(rand_range(150.0, 250.0), 0.0)
mob.linear_velocity = velocity.rotated(direction)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
.. code-tab:: csharp
public void OnMobTimerTimeout()
{
// Note: Normally it is best to use explicit types rather than the `var`
// keyword. However, var is acceptable to use here because the types are
// obviously Mob and PathFollow2D, since they appear later on the line.
// Create a new instance of the Mob scene.
var mob = (Mob)MobScene.Instance();
// Choose a random location on Path2D.
var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
mobSpawnLocation.Offset = GD.Randi();
// Set the mob's direction perpendicular to the path direction.
float direction = mobSpawnLocation.Rotation + Mathf.Pi / 2;
// Set the mob's position to a random location.
mob.Position = mobSpawnLocation.Position;
// Add some randomness to the direction.
direction += (float)GD.RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
mob.Rotation = direction;
// Choose the velocity.
var velocity = new Vector2((float)GD.RandRange(150.0, 250.0), 0);
mob.LinearVelocity = velocity.Rotated(direction);
// Spawn the mob by adding it to the Main scene.
AddChild(mob);
}
.. code-tab:: cpp
// This code goes in `main.cpp`.
void Main::_on_MobTimer_timeout() {
// Create a new instance of the Mob scene.
godot::Node *mob = mob_scene->instance();
// Choose a random location on Path2D.
_mob_spawn_location->set_offset((real_t)_random->randi());
// Set the mob's direction perpendicular to the path direction.
real_t direction = _mob_spawn_location->get_rotation() + (real_t)Math_PI / 2;
// Set the mob's position to a random location.
mob->set("position", _mob_spawn_location->get_position());
// Add some randomness to the direction.
direction += _random->randf_range((real_t)-Math_PI / 4, (real_t)Math_PI / 4);
mob->set("rotation", direction);
// Choose the velocity for the mob.
godot::Vector2 velocity = godot::Vector2(_random->randf_range(150.0, 250.0), 0.0);
mob->set("linear_velocity", velocity.rotated(direction));
// Spawn the mob by adding it to the Main scene.
add_child(mob);
}
.. important:: Why ``PI``? In functions requiring angles, Godot uses *radians*,
not degrees. Pi represents a half turn in radians, about
``3.1415`` (there is also ``TAU`` which is equal to ``2 * PI``).
If you're more comfortable working with degrees, you'll need to
use the ``deg2rad()`` and ``rad2deg()`` functions to convert
between the two.
Testing the scene
~~~~~~~~~~~~~~~~~
Let's test the scene to make sure everything is working. Add this ``new_game``
call to ``_ready()``:
.. tabs::
.. code-tab:: gdscript GDScript
func _ready():
randomize()
new_game()
.. code-tab:: csharp
public override void _Ready()
{
NewGame();
}
.. code-tab:: cpp
// This code goes in `main.cpp`.
void Main::_ready() {
new_game();
}
Let's also assign ``Main`` as our "Main Scene" - the one that runs automatically
when the game launches. Press the "Play" button and select ``Main.tscn`` when
prompted.
You should be able to move the player around, see mobs spawning, and see the
player disappear when hit by a mob.
When you're sure everything is working, remove the call to ``new_game()`` from
``_ready()``.
What's our game lacking? Some user interface. In the next lesson, we'll add a
title screen and display the player's score.

View File

@ -1,438 +0,0 @@
.. _doc_your_first_2d_game_heads_up_display:
Heads up display
================
The final piece our game needs is a User Interface (UI) to display things like
score, a "game over" message, and a restart button.
Create a new scene, and add a :ref:`CanvasLayer <class_CanvasLayer>` node named
``HUD``. "HUD" stands for "heads-up display", an informational display that
appears as an overlay on top of the game view.
The :ref:`CanvasLayer <class_CanvasLayer>` node lets us draw our UI elements on
a layer above the rest of the game, so that the information it displays isn't
covered up by any game elements like the player or mobs.
The HUD needs to display the following information:
- Score, changed by ``ScoreTimer``.
- A message, such as "Game Over" or "Get Ready!"
- A "Start" button to begin the game.
The basic node for UI elements is :ref:`Control <class_Control>`. To create our
UI, we'll use two types of :ref:`Control <class_Control>` nodes: :ref:`Label
<class_Label>` and :ref:`Button <class_Button>`.
Create the following as children of the ``HUD`` node:
- :ref:`Label <class_Label>` named ``ScoreLabel``.
- :ref:`Label <class_Label>` named ``Message``.
- :ref:`Button <class_Button>` named ``StartButton``.
- :ref:`Timer <class_Timer>` named ``MessageTimer``.
Click on the ``ScoreLabel`` and type a number into the ``Text`` field in the
Inspector. The default font for ``Control`` nodes is small and doesn't scale
well. There is a font file included in the game assets called
"Xolonium-Regular.ttf". To use this font, do the following:
1. Under **Theme overrides > Fonts** click on the empty box and select "New DynamicFont"
.. image:: img/custom_font1.png
2. Click on the "DynamicFont" you added, and under **Font > FontData**,
choose "Load" and select the "Xolonium-Regular.ttf" file.
.. image:: img/custom_font2.png
Set the "Size" property under ``Settings``, ``64`` works well.
.. image:: img/custom_font3.png
Once you've done this on the ``ScoreLabel``, you can click the down arrow next
to the Font property and choose "Copy", then "Paste" it in the same place
on the other two Control nodes.
.. note:: **Anchors and Margins:** ``Control`` nodes have a position and size,
but they also have anchors and margins. Anchors define the origin -
the reference point for the edges of the node. Margins update
automatically when you move or resize a control node. They represent
the distance from the control node's edges to its anchor.
Arrange the nodes as shown below. Click the "Layout" button to set a Control
node's layout:
.. image:: img/ui_anchor.png
You can drag the nodes to place them manually, or for more precise placement,
use the following settings:
ScoreLabel
~~~~~~~~~~
- *Layout* : "Top Wide"
- *Text* : ``0``
- *Align* : "Center"
Message
~~~~~~~~~~~~
- *Layout* : "HCenter Wide"
- *Text* : ``Dodge the Creeps!``
- *Align* : "Center"
- *Autowrap* : "On"
StartButton
~~~~~~~~~~~
- *Text* : ``Start``
- *Layout* : "Center Bottom"
- *Margin* :
- Top: ``-200``
- Bottom: ``-100``
On the ``MessageTimer``, set the ``Wait Time`` to ``2`` and set the ``One Shot``
property to "On".
Now add this script to ``HUD``:
.. tabs::
.. code-tab:: gdscript GDScript
extends CanvasLayer
signal start_game
.. code-tab:: csharp
public class HUD : CanvasLayer
{
// Don't forget to rebuild the project so the editor knows about the new signal.
[Signal]
public delegate void StartGame();
}
.. code-tab:: cpp
// Copy `player.gdns` to `hud.gdns` and replace `Player` with `HUD`.
// Attach the `hud.gdns` file to the HUD node.
// Create two files `hud.cpp` and `hud.hpp` next to `entry.cpp` in `src`.
// This code goes in `hud.hpp`. We also define the methods we'll be using here.
#ifndef HUD_H
#define HUD_H
#include <Button.hpp>
#include <CanvasLayer.hpp>
#include <Godot.hpp>
#include <Label.hpp>
#include <Timer.hpp>
class HUD : public godot::CanvasLayer {
GODOT_CLASS(HUD, godot::CanvasLayer)
godot::Label *_score_label;
godot::Label *_message_label;
godot::Timer *_start_message_timer;
godot::Timer *_get_ready_message_timer;
godot::Button *_start_button;
godot::Timer *_start_button_timer;
public:
void _init() {}
void _ready();
void show_get_ready();
void show_game_over();
void update_score(const int score);
void _on_StartButton_pressed();
void _on_StartMessageTimer_timeout();
void _on_GetReadyMessageTimer_timeout();
static void _register_methods();
};
#endif // HUD_H
The ``start_game`` signal tells the ``Main`` node that the button
has been pressed.
.. tabs::
.. code-tab:: gdscript GDScript
func show_message(text):
$Message.text = text
$Message.show()
$MessageTimer.start()
.. code-tab:: csharp
public void ShowMessage(string text)
{
var message = GetNode<Label>("Message");
message.Text = text;
message.Show();
GetNode<Timer>("MessageTimer").Start();
}
.. code-tab:: cpp
// This code goes in `hud.cpp`.
#include "hud.hpp"
void HUD::_ready() {
_score_label = get_node<godot::Label>("ScoreLabel");
_message_label = get_node<godot::Label>("MessageLabel");
_start_message_timer = get_node<godot::Timer>("StartMessageTimer");
_get_ready_message_timer = get_node<godot::Timer>("GetReadyMessageTimer");
_start_button = get_node<godot::Button>("StartButton");
_start_button_timer = get_node<godot::Timer>("StartButtonTimer");
}
void HUD::_register_methods() {
godot::register_method("_ready", &HUD::_ready);
godot::register_method("show_get_ready", &HUD::show_get_ready);
godot::register_method("show_game_over", &HUD::show_game_over);
godot::register_method("update_score", &HUD::update_score);
godot::register_method("_on_StartButton_pressed", &HUD::_on_StartButton_pressed);
godot::register_method("_on_StartMessageTimer_timeout", &HUD::_on_StartMessageTimer_timeout);
godot::register_method("_on_GetReadyMessageTimer_timeout", &HUD::_on_GetReadyMessageTimer_timeout);
godot::register_signal<HUD>("start_game", godot::Dictionary());
}
This function is called when we want to display a message
temporarily, such as "Get Ready".
.. tabs::
.. code-tab:: gdscript GDScript
func show_game_over():
show_message("Game Over")
# Wait until the MessageTimer has counted down.
yield($MessageTimer, "timeout")
$Message.text = "Dodge the\nCreeps!"
$Message.show()
# Make a one-shot timer and wait for it to finish.
yield(get_tree().create_timer(1), "timeout")
$StartButton.show()
.. code-tab:: csharp
async public void ShowGameOver()
{
ShowMessage("Game Over");
var messageTimer = GetNode<Timer>("MessageTimer");
await ToSignal(messageTimer, "timeout");
var message = GetNode<Label>("Message");
message.Text = "Dodge the\nCreeps!";
message.Show();
await ToSignal(GetTree().CreateTimer(1), "timeout");
GetNode<Button>("StartButton").Show();
}
.. code-tab:: cpp
// This code goes in `hud.cpp`.
// There is no `yield` in GDNative, so we need to have every
// step be its own method that is called on timer timeout.
void HUD::show_get_ready() {
_message_label->set_text("Get Ready");
_message_label->show();
_get_ready_message_timer->start();
}
void HUD::show_game_over() {
_message_label->set_text("Game Over");
_message_label->show();
_start_message_timer->start();
}
This function is called when the player loses. It will show "Game Over" for 2
seconds, then return to the title screen and, after a brief pause, show the
"Start" button.
.. note:: When you need to pause for a brief time, an alternative to using a
Timer node is to use the SceneTree's ``create_timer()`` function. This
can be very useful to add delays such as in the above code, where we
want to wait some time before showing the "Start" button.
.. tabs::
.. code-tab:: gdscript GDScript
func update_score(score):
$ScoreLabel.text = str(score)
.. code-tab:: csharp
public void UpdateScore(int score)
{
GetNode<Label>("ScoreLabel").Text = score.ToString();
}
.. code-tab:: cpp
// This code goes in `hud.cpp`.
void HUD::update_score(const int p_score) {
_score_label->set_text(godot::Variant(p_score));
}
This function is called by ``Main`` whenever the score changes.
Connect the ``timeout()`` signal of ``MessageTimer`` and the ``pressed()``
signal of ``StartButton`` and add the following code to the new functions:
.. tabs::
.. code-tab:: gdscript GDScript
func _on_StartButton_pressed():
$StartButton.hide()
emit_signal("start_game")
func _on_MessageTimer_timeout():
$Message.hide()
.. code-tab:: csharp
public void OnStartButtonPressed()
{
GetNode<Button>("StartButton").Hide();
EmitSignal("StartGame");
}
public void OnMessageTimerTimeout()
{
GetNode<Label>("Message").Hide();
}
.. code-tab:: cpp
// This code goes in `hud.cpp`.
void HUD::_on_StartButton_pressed() {
_start_button_timer->stop();
_start_button->hide();
emit_signal("start_game");
}
void HUD::_on_StartMessageTimer_timeout() {
_message_label->set_text("Dodge the\nCreeps");
_message_label->show();
_start_button_timer->start();
}
void HUD::_on_GetReadyMessageTimer_timeout() {
_message_label->hide();
}
Connecting HUD to Main
~~~~~~~~~~~~~~~~~~~~~~
Now that we're done creating the ``HUD`` scene, go back to ``Main``. Instance
the ``HUD`` scene in ``Main`` like you did the ``Player`` scene. The scene tree
should look like this, so make sure you didn't miss anything:
.. image:: img/completed_main_scene.png
Now we need to connect the ``HUD`` functionality to our ``Main`` script. This
requires a few additions to the ``Main`` scene:
In the Node tab, connect the HUD's ``start_game`` signal to the ``new_game()``
function of the Main node by typing "new_game" in the "Receiver Method" in the
"Connect a Signal" window. Verify that the green connection icon now appears
next to ``func new_game()`` in the script.
In ``new_game()``, update the score display and show the "Get Ready" message:
.. tabs::
.. code-tab:: gdscript GDScript
$HUD.update_score(score)
$HUD.show_message("Get Ready")
.. code-tab:: csharp
var hud = GetNode<HUD>("HUD");
hud.UpdateScore(Score);
hud.ShowMessage("Get Ready!");
.. code-tab:: cpp
_hud->update_score(score);
_hud->show_get_ready();
In ``game_over()`` we need to call the corresponding ``HUD`` function:
.. tabs::
.. code-tab:: gdscript GDScript
$HUD.show_game_over()
.. code-tab:: csharp
GetNode<HUD>("HUD").ShowGameOver();
.. code-tab:: cpp
_hud->show_game_over();
Finally, add this to ``_on_ScoreTimer_timeout()`` to keep the display in sync
with the changing score:
.. tabs::
.. code-tab:: gdscript GDScript
$HUD.update_score(score)
.. code-tab:: csharp
GetNode<HUD>("HUD").UpdateScore(Score);
.. code-tab:: cpp
_hud->update_score(score);
Now you're ready to play! Click the "Play the Project" button. You will be asked
to select a main scene, so choose ``Main.tscn``.
Removing old creeps
~~~~~~~~~~~~~~~~~~~
If you play until "Game Over" and then start a new game right away, the creeps
from the previous game may still be on the screen. It would be better if they
all disappeared at the start of a new game. We just need a way to tell *all* the
mobs to remove themselves. We can do this with the "group" feature.
In the ``Mob`` scene, select the root node and click the "Node" tab next to the
Inspector (the same place where you find the node's signals). Next to "Signals",
click "Groups" and you can type a new group name and click "Add".
.. image:: img/group_tab.png
Now all mobs will be in the "mobs" group. We can then add the following line to
the ``new_game()`` function in ``Main``:
.. tabs::
.. code-tab:: gdscript GDScript
get_tree().call_group("mobs", "queue_free")
.. code-tab:: csharp
// Note that for calling Godot-provided methods with strings,
// we have to use the original Godot snake_case name.
GetTree().CallGroup("mobs", "queue_free");
.. code-tab:: cpp
get_tree()->call_group("mobs", "queue_free");
The ``call_group()`` function calls the named function on every node in a
group - in this case we are telling every mob to delete itself.
The game's mostly done at this point. In the next and last part, we'll polish it
a bit by adding a background, looping music, and some keyboard shortcuts.

View File

@ -1,79 +0,0 @@
.. _doc_your_first_2d_game_finishing_up:
Finishing up
============
We have now completed all the functionality for our game. Below are some
remaining steps to add a bit more "juice" to improve the game experience.
Feel free to expand the gameplay with your own ideas.
Background
~~~~~~~~~~
The default gray background is not very appealing, so let's change its color.
One way to do this is to use a :ref:`ColorRect <class_ColorRect>` node. Make it
the first node under ``Main`` so that it will be drawn behind the other nodes.
``ColorRect`` only has one property: ``Color``. Choose a color you like and
select "Layout" -> "Full Rect" so that it covers the screen.
You could also add a background image, if you have one, by using a
``TextureRect`` node instead.
Sound effects
~~~~~~~~~~~~~
Sound and music can be the single most effective way to add appeal to the game
experience. In your game assets folder, you have two sound files: "House In a
Forest Loop.ogg" for background music, and "gameover.wav" for when the player
loses.
Add two :ref:`AudioStreamPlayer <class_AudioStreamPlayer>` nodes as children of
``Main``. Name one of them ``Music`` and the other ``DeathSound``. On each one,
click on the ``Stream`` property, select "Load", and choose the corresponding
audio file.
To play the music, add ``$Music.play()`` in the ``new_game()`` function and
``$Music.stop()`` in the ``game_over()`` function.
Finally, add ``$DeathSound.play()`` in the ``game_over()`` function.
Keyboard shortcut
~~~~~~~~~~~~~~~~~
Since the game is played with keyboard controls, it would be convenient if we
could also start the game by pressing a key on the keyboard. We can do this with
the "Shortcut" property of the ``Button`` node.
In a previous lesson, we created four input actions to move the character. We
will create a similar input action to map to the start button.
Select "Project" -> "Project Settings" and then click on the "Input Map"
tab. In the same way you created the movement input actions, create a new
input action called ``start_game`` and add a key mapping for the :kbd:`Enter`
key.
In the ``HUD`` scene, select the ``StartButton`` and find its *Shortcut*
property in the Inspector. Select "New Shortcut" and click on the "Shortcut"
item. A second *Shortcut* property will appear. Select "New InputEventAction"
and click the new "InputEventAction". Finally, in the *Action* property, type
the name ``start_game``.
.. image:: img/start_button_shortcut.png
Now when the start button appears, you can either click it or press :kbd:`Enter`
to start the game.
And with that, you completed your first 2D game in Godot.
.. image:: img/dodge_preview.gif
You got to make a player-controlled character, enemies that spawn randomly
around the game board, count the score, implement a game over and replay, user
interface, sounds, and more. Congratulations!
There's still much to learn, but you can take a moment to appreciate what you
achieved.
And when you're ready, you can move on to :ref:`doc_your_first_3d_game` to learn
to create a complete 3D game from scratch, in Godot.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@ -1,76 +0,0 @@
.. _doc_your_first_2d_game:
Your first 2D game
==================
In this step-by-step tutorial series, you will create your first complete 2D
game with Godot. By the end of the series, you will have a simple yet complete
game of your own, like the image below.
|image0|
You will learn how the Godot editor works, how to structure a project, and build
a 2D game.
.. note:: This project is an introduction to the Godot engine. It assumes that
you have some programming experience already. If you're new to
programming entirely, you should start here: :ref:`doc_scripting`.
The game is called "Dodge the Creeps!". Your character must move and avoid the
enemies for as long as possible. Here is a preview of the final result:
You will learn to:
- Create a complete 2D game with the Godot editor.
- Structure a simple game project.
- Move the player character and change its sprite.
- Spawn random enemies.
- Count the score.
And more.
You'll find another series where you'll create a similar game but in 3D. We
recommend you to start with this one, though.
**Why start with 2D?**
3D games are much more complex than 2D ones. It would be best if you stuck to 2D
until you understood the game development process and how to use Godot well.
You can find a completed version of this project at this location:
- https://github.com/godotengine/godot-demo-projects
Prerequisites
-------------
This step-by-step tutorial is intended for beginners who followed the complete
:ref:`Getting Started <toc-learn-step_by_step>`.
If you're an experienced programmer, you can find the complete demo's source
code here: `Godot demo projects
<https://github.com/godotengine/godot-demo-projects>`__.
We prepared some game assets you'll need to download so we can jump straight to
the code.
You can download them by clicking the link below.
:download:`dodge_assets.zip <files/dodge_assets.zip>`.
Contents
--------
.. toctree::
:maxdepth: 1
:name: toc-learn-first_2d_game
01.project_setup
02.player_scene
03.coding_the_player
04.creating_the_enemy
05.the_main_game_scene
06.heads_up_display
07.finishing-up
.. |image0| image:: img/dodge_preview.gif

View File

@ -1,164 +0,0 @@
.. _doc_first_3d_game_game_area:
Setting up the game area
========================
In this first part, we're going to set up the game area. Let's get started by
importing the start assets and setting up the game scene.
We've prepared a Godot project with the 3D models and sounds we'll use for this
tutorial, linked in the index page. If you haven't done so yet, you can download
the archive here: `Squash the Creeps assets
<https://github.com/GDQuest/godot-3d-dodge-the-creeps/releases/tag/1.0.0>`__.
Once you downloaded it, extract the .zip archive on your computer. Open the
Godot project manager and click the *Import* button.
|image1|
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|
Click *Import & Edit* to open the project in the editor.
|image3|
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|
There are two 3D models, ``player.glb`` and ``mob.glb``, some materials that
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
*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
a node to the scene, you can press :kbd:`Ctrl + a` (or :kbd:`Cmd + a` on macOS).
|image5|
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*.
|image6|
A warning sign next to the *CollisionShape* appears because we haven't defined
its shape. If you click the icon, a popup appears to give you more information.
|image7|
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*.
|image8|
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 its *Extents* to ``30`` on the X axis, ``1`` for the Y axis, and ``30`` for
the Z axis.
|image9|
.. note::
In 3D, translation and size units are in meters. The box's total size is
twice its extents: ``60`` by ``60`` meters on the ground plane and ``2``
units tall. The ground plane is defined by the X and Z axes, while the Y
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.
|image10|
In the *Inspector*, click on the field next to *Mesh* and create a *CubeMesh*
resource to create a visible cube.
|image11|
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|
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),
and click and drag down on the Y axis. It's the green arrow in the move gizmo.
|image13|
.. 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|
Move the ground down ``1`` meter. A label in the bottom-left corner of the
viewport tells you how much you're translating the node.
|image15|
.. note::
Moving the *Ground* node down moves both children along with it.
Ensure you move the *Ground* node, **not** the *MeshInstance* or the
*CollisionShape*.
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
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|
At this point, your project should look like this.
|image17|
That's our starting point. In the next part, we will work on the player scene
and base movement.
.. |image1| image:: img/01.game_setup/01.import_button.png
.. |image2| image:: img/01.game_setup/02.browse_to_project_folder.png
.. |image3| image:: img/01.game_setup/03.import_and_edit.png
.. |image4| image:: img/01.game_setup/04.start_assets.png
.. |image5| image:: img/01.game_setup/05.main_node.png
.. |image6| image:: img/01.game_setup/06.staticbody_node.png
.. |image7| image:: img/01.game_setup/07.collision_shape_warning.png
.. |image8| image:: img/01.game_setup/08.create_box_shape.png
.. |image9| image:: img/01.game_setup/09.box_extents.png
.. |image10| image:: img/01.game_setup/10.mesh_instance.png
.. |image11| image:: img/01.game_setup/11.cube_mesh.png
.. |image12| image:: img/01.game_setup/12.cube_resized.png
.. |image13| image:: img/01.game_setup/13.move_gizmo_y_axis.png
.. |image14| image:: img/01.game_setup/14.select_mode_icon.png
.. |image15| image:: img/01.game_setup/15.translation_amount.png
.. |image16| image:: img/01.game_setup/16.turn_on_shadows.png
.. |image17| image:: img/01.game_setup/17.project_with_light.png

View File

@ -1,177 +0,0 @@
.. _doc_first_3d_game_player_scene_and_input:
Player scene and input actions
==============================
In the next two lessons, we will design the player scene, register custom input
actions, and code player movement. By the end, you'll have a playable character
that moves in eight directions.
.. TODO: add player animated gif?
.. player_movement.gif
Create a new scene by going to the Scene menu in the top-left and clicking *New
Scene*. Create a *KinematicBody* node as the root and name it *Player*.
|image0|
Kinematic 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
their movement. You will see how we use the node's unique features when we code
the jump and squash mechanics.
.. seealso::
To learn more about the different physics node types, see the
:ref:`doc_physics_introduction`.
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 *Spatial* 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.
|image1|
This should instantiate the model as a child of *Pivot*. You can rename it to
*Character*.
|image2|
.. note::
The ``.glb`` files contain 3D scene data based on the open-source GLTF 2.0
specification. They're a modern and powerful alternative to a proprietary format
like FBX, which Godot also supports. To produce these files, we designed the
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.
|image3|
It will be the shape the physics engine uses to collide with the environment, so
we want it to better fit the 3D model. Shrink it a bit by dragging the orange
dot in the viewport. My sphere has a radius of about ``0.8`` meters.
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.
|image5|
Save the scene as ``Player.tscn``.
With the nodes ready, we can almost get coding. But first, we need to define
some input actions.
Creating input actions
----------------------
To move the character, we will listen to the player's input, like pressing the
arrow keys. In Godot, while we could write all the key bindings in code, there's
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…*.
|image6|
At the top, there are multiple tabs. Click on *Input Map*. This window allows
you to add new actions at the top; they are your labels. In the bottom part, you
can bind keys to these actions.
|image7|
Godot projects come with some predefined actions designed for user interface
design, which we could use here. But we're defining our own to support gamepads.
We're going to name our actions ``move_left``, ``move_right``, ``move_forward``,
``move_back``, and ``jump``.
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.
|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*.
|image10|
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.
|image12|
Let's now add support for a gamepad's left joystick. Click the "+" button again
but this time, select *Joy Axis*.
|image13|
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.
|image14|
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,
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.
|image16|
Leave the default values and click the *Add* button.
|image17|
Your jump input action should look like this.
|image18|
That's all the actions we need for this game. You can use this menu to label any
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
.. |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
.. |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
.. |image17| image:: img/02.player_input/14.add_jump_button.png
.. |image18| image:: img/02.player_input/14.jump_input_action.png

View File

@ -1,413 +0,0 @@
.. _doc_first_3d_game_player_movement:
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
it. In the popup, set the *Template* to *Empty* before pressing the *Create*
button.
|image0|
Let's start with the class's properties. We're going to define a movement speed,
a fall acceleration representing gravity, and a velocity we'll use to move the
character.
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
# How fast the player moves in meters per second.
export var speed = 14
# The downward acceleration when in the air, in meters per second squared.
export var fall_acceleration = 75
var velocity = Vector3.ZERO
.. code-tab:: csharp
public class Player : KinematicBody
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
// 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.
[Export]
public int FallAcceleration = 75;
private Vector3 _velocity = Vector3.Zero;
}
These are common properties for a moving body. The ``velocity`` is a 3D vector
combining a speed with a direction. Here, we define it as a property because
we want to update and reuse its value across frames.
.. note::
The values are quite different from 2D code because distances are in meters.
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
using the global ``Input`` object, in ``_physics_process()``.
.. tabs::
.. code-tab:: gdscript GDScript
func _physics_process(delta):
# We create a local variable to store the input direction.
var direction = Vector3.ZERO
# We check for each move input and update the direction accordingly.
if Input.is_action_pressed("move_right"):
direction.x += 1
if Input.is_action_pressed("move_left"):
direction.x -= 1
if Input.is_action_pressed("move_back"):
# Notice how we are working with the vector's x and z axes.
# In 3D, the XZ plane is the ground plane.
direction.z += 1
if Input.is_action_pressed("move_forward"):
direction.z -= 1
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// We create a local variable to store the input direction.
var direction = Vector3.Zero;
// We check for each move input and update the direction accordingly
if (Input.IsActionPressed("move_right"))
{
direction.x += 1f;
}
if (Input.IsActionPressed("move_left"))
{
direction.x -= 1f;
}
if (Input.IsActionPressed("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 += 1f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.z -= 1f;
}
}
Here, we're going to make all calculations using the ``_physics_process()``
virtual function. Like ``_process()``, it allows you to update the node every
frame, but it's designed specifically for physics-related code like moving a
kinematic or rigid body.
.. seealso::
To learn more about the difference between ``_process()`` and
``_physics_process()``, see :ref:`doc_idle_and_physics_processing`.
We start by initializing a ``direction`` variable to ``Vector3.ZERO``. Then, we
check if the player is pressing one or more of the ``move_*`` inputs and update
the vector's ``x`` and ``z`` components accordingly. These correspond to the
ground plane's axes.
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
call its ``normalize()`` method.
.. tabs::
.. code-tab:: gdscript GDScript
#func _physics_process(delta):
#...
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(translation + direction, Vector3.UP)
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Spatial>("Pivot").LookAt(Translation + 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.
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
in the viewport instead.
In 3D, the property that contains a node's position is ``translation``. By
adding the ``direction`` to it, we get a position to look at that's one meter
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.
.. tabs::
.. code-tab:: gdscript GDScript
func _physics_process(delta):
#...
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)
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
// Ground velocity
_velocity.x = direction.x * Speed;
_velocity.z = direction.z * Speed;
// Vertical velocity
_velocity.y -= FallAcceleration * delta;
// Moving the character
_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 - ...``.
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.
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 ``KinematicBody.move_and_slide()``. It's a powerful
method of the ``KinematicBody`` 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.
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.
Here is the complete ``Player.gd`` code for reference.
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
# How fast the player moves in meters per second.
export var speed = 14
# The downward acceleration when in the air, in meters per second squared.
export var fall_acceleration = 75
var velocity = Vector3.ZERO
func _physics_process(delta):
var direction = Vector3.ZERO
if Input.is_action_pressed("move_right"):
direction.x += 1
if Input.is_action_pressed("move_left"):
direction.x -= 1
if Input.is_action_pressed("move_back"):
direction.z += 1
if Input.is_action_pressed("move_forward"):
direction.z -= 1
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(translation + 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)
.. code-tab:: csharp
public class Player : KinematicBody
{
// 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.
[Export]
public int FallAcceleration = 75;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
// We create a local variable to store the input direction.
var direction = Vector3.Zero;
// We check for each move input and update the direction accordingly
if (Input.IsActionPressed("move_right"))
{
direction.x += 1f;
}
if (Input.IsActionPressed("move_left"))
{
direction.x -= 1f;
}
if (Input.IsActionPressed("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 += 1f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.z -= 1f;
}
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Spatial>("Pivot").LookAt(Translation + direction, Vector3.Up);
}
// Ground velocity
_velocity.x = direction.x * Speed;
_velocity.z = direction.z * Speed;
// Vertical velocity
_velocity.y -= FallAcceleration * delta;
// Moving the character
_velocity = MoveAndSlide(_velocity, Vector3.Up);
}
}
Testing our player's movement
-----------------------------
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*
tab at the top of the editor to do so.
|image1|
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
Child Scene*.
|image2|
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 *Position3D*, name it *CameraPivot*,
and add a *Camera* node as a child of it. Your scene tree should look like this.
|image3|
Notice the *Preview* checkbox that appears in the top-left when you have the
*Camera* selected. You can click it to preview the in-game camera projection.
|image4|
We're going to use the *Pivot* to rotate the camera as if it was on a crane.
Let's first split the 3D view to be able to freely navigate the scene and see
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).
|image5|
On the bottom view, select the *Camera* and turn on camera preview by clicking
the checkbox.
|image6|
In the top view, move the camera about ``19`` units on the Z axis (the blue
one).
|image7|
Here's where the magic happens. Select the *CameraPivot* and rotate it ``45``
degrees around the X axis (using the red circle). You'll see the camera move as
if it was attached to a crane.
|image8|
You can run the scene by pressing :kbd:`F6` and press the arrow keys to move the
character.
|image9|
We can see some empty space around the character due to the perspective
projection. In this game, we're going to use an orthographic projection instead
to better frame the gameplay area and make it easier for the player to read
distances.
Select the *Camera* again and in the *Inspector*, set the *Projection* to
*Orthogonal* and the *Size* to ``19``. The character should now look flatter and
the ground should fill the background.
|image10|
With that, 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
.. |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
.. |image4| image:: img/03.player_movement_code/05.camera_preview_checkbox.png
.. |image5| image:: img/03.player_movement_code/06.two_viewports.png
.. |image6| image:: img/03.player_movement_code/07.camera_preview_checkbox.png
.. |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

View File

@ -1,332 +0,0 @@
.. _doc_first_3d_game_designing_the_mob_scene:
Designing the mob scene
=======================
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.
Create a scene with, once again, a *KinematicBody* node as its root. Name it
*Mob*. Add a *Spatial* 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*.
|image0|
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*.
|image2|
In the *Inspector*, assign a *BoxShape* to the *Shape* property.
|image3|
We should change its size to fit the 3D model better. You can do so
interactively by clicking and dragging on the orange dots.
The box should touch the floor and be a little thinner than the model. Physics
engines work in such a way that if the player's sphere touches even the box's
corner, a collision will occur. If the box is a little too big compared to the
3D model, you may die at a distance from the monster, and the game will feel
unfair to the players.
|image4|
Notice that my box is taller than the monster. It is okay in this game because
we're looking at the scene from above and using a fixed perspective. Collision
shapes don't have to match the model exactly. It's the way the game feels when
you test it that should dictate their form and size.
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.
Once a monster leaves the screen, we don't need it anymore, so we can 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.
.. note::
When you keep instancing an object in games, 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.
When working with GDScript, you don't need to worry about this. The main
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`.
Select the *Mob* node and add a *VisibilityNotifier* as a child of it. Another
box, pink this time, appears. When this box completely leaves the screen, the
node will emit a signal.
|image5|
Resize it using the orange dots until it covers the entire 3D model.
|image6|
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
and call the function from there.
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``.
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
# 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(velocity)
.. code-tab:: csharp
public class Mob : KinematicBody
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed = 18;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
MoveAndSlide(_velocity);
}
}
Similarly to the player, we move the mob every frame by calling
``KinematicBody``\ '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
and leave the screen, even if it were to hit an obstacle.
We need to define another function to calculate the start 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
``player_position`` as its arguments.
We first position the mob at ``start_position``. Then, we turn it towards the
player using the ``look_at()`` method and randomize the angle by rotating a
random amount around the Y axis. Below, ``rand_range()`` outputs a random value
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):
translation = start_position
# We turn the mob so it looks at the player.
look_at(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))
.. code-tab:: csharp
// We will call this function from the Main scene
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
Translation = startPosition;
// We turn the mob so it looks at the player.
LookAt(playerPosition, Vector3.Up);
// And rotate it randomly so it doesn't move exactly toward the player.
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.
.. tabs::
.. code-tab:: gdscript GDScript
func initialize(start_position, player_position):
# ...
# We calculate a random speed.
var random_speed = rand_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.
velocity = velocity.rotated(Vector3.UP, rotation.y)
.. code-tab:: csharp
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
// ...
// We calculate a random speed.
float randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
// We calculate a forward velocity that represents the speed.
_velocity = Vector3.Forward * randomSpeed;
// We then rotate the vector based on the mob's Y rotation to move in the direction it's looking
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
}
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*.
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.
|image9|
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.
.. tabs::
.. code-tab:: gdscript GDScript
func _on_VisibilityNotifier_screen_exited():
queue_free()
.. code-tab:: csharp
// We also specified this function name in PascalCase in the editor's connection window
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
Our monster is ready to enter the game! In the next part, you will spawn
monsters in the game level.
Here is the complete ``Mob.gd`` script for reference.
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
# 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(velocity)
func initialize(start_position, player_position):
translation = start_position
look_at(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()
.. code-tab:: csharp
public class Mob : KinematicBody
{
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed = 18;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
MoveAndSlide(_velocity);
}
// We will call this function from the Main scene
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
Translation = startPosition;
LookAt(playerPosition, Vector3.Up);
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
var randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
_velocity = Vector3.Forward * randomSpeed;
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
}
// We also specified this function name in PascalCase in the editor's connection window
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}
.. |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

View File

@ -1,360 +0,0 @@
.. _doc_first_3d_game_spawning_monsters:
Spawning monsters
=================
In this part, we're going to spawn monsters along a path randomly. By the end,
you will have monsters roaming the game board.
|image0|
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
nice little box.
Go to *Project -> Project Settings*.
|image1|
In the left menu, navigate down to *Display -> Window*. On the right, set the
*Width* to ``720`` and the *Height* to ``540``.
|image2|
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.
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
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
viewport.
|image3|
Adding placeholder cylinders
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Let's add the placeholder meshes. Add a new *Spatial* 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.
|image4|
In the *Inspector*, assign a *CylinderMesh* to the *Mesh* property.
|image5|
Set the top viewport to the top orthogonal view using the menu in the viewport's
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*
menu in the toolbar and clicking *View Grid*.
|image7|
You now want to move the cylinder along the ground plane, looking at the camera
preview in the bottom viewport. I recommend using grid snap to do so. You can
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
corner.
|image9|
We're going to create copies of the mesh and place them around the game area.
Press :kbd:`Ctrl + D` (:kbd:`Cmd + D` on macOS) to duplicate the node. You can also right-click
the node in the *Scene* dock and select *Duplicate*. Move the copy down along
the blue Z axis until it's right outside the camera's preview.
Select both cylinders by pressing the :kbd:`Shift` key and clicking on the unselected
one and duplicate them.
|image10|
Move them to the right by dragging the red X axis.
|image11|
They're a bit hard to see in white, aren't they? Let's make them stand out by
giving them a new material.
In 3D, materials define a surface's visual properties like its color, how it
reflects light, and more. We can use them to change the color of a mesh.
We can update all four cylinders at once. Select all the mesh instances in the
*Scene* dock. To do so, you can click on the first one and Shift click on the
last one.
|image12|
In the *Inspector*, expand the *Material* section and assign a *SpatialMaterial*
to slot *0*.
|image13|
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
the background, like a bright orange.
|image14|
We can now use the cylinders as guides. Fold them in the *Scene* dock by
clicking the grey arrow next to them. Moving forward, you can also toggle their
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
the *Add Point* tool, the icon with the green "+" sign.
|image16|
.. note:: You can hover any icon to see a tooltip describing the tool.
Click in the center of each cylinder to create a point. Then, click the *Close
Curve* icon in the toolbar to close the path. If any point is a bit off, you can
click and drag on it to reposition it.
|image17|
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.
|image19|
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.
We first export a variable to the *Inspector* so that we can assign ``Mob.tscn``
or any other monster to it.
Then, as we're going to spawn the monsters procedurally, we want to randomize
numbers every time we play the game. If we don't do that, the monsters will
always spawn following the same sequence.
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
export (PackedScene) var mob_scene
func _ready():
randomize()
.. code-tab:: csharp
public class Main : Node
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
#pragma warning disable 649
// We assign this in the editor, so we don't need the warning about not being assigned.
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public override void _Ready()
{
GD.Randomize();
}
}
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.
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*.
|image21|
In the *Inspector*, set its *Wait Time* to ``0.5`` seconds and turn on
*Autostart* so it automatically starts when we run the game.
|image22|
Timers emit a ``timeout`` signal every time they reach the end of their *Wait
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
double-click the ``timeout`` signal.
|image23|
Connect it to the *Main* node.
|image24|
This will take you back to the script, with a new empty
``_on_MobTimer_timeout()`` function.
Let's code the mob spawning logic. We're going to:
1. Instantiate the mob scene.
2. Sample a random position on the spawn path.
3. Get the player's position.
4. Add the mob as a child of the *Main* node.
5. Call the mob's ``initialize()`` method, passing it the random position and
the player's position.
.. tabs::
.. code-tab:: gdscript GDScript
func _on_MobTimer_timeout():
# Create a Mob instance and add it to the scene.
var mob = mob_scene.instance()
# Choose a random location on Path2D.
# 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()
var player_position = $Player.transform.origin
add_child(mob)
mob.initialize(mob_spawn_location.translation, player_position)
.. code-tab:: csharp
// We also specified this function name in PascalCase in the editor's connection window
public void OnMobTimerTimeout()
{
// Create a mob instance and add it to the scene.
Mob mob = (Mob)MobScene.Instance();
// Choose a random location on Path2D.
// We stire the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
AddChild(mob);
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
}
Above, ``randf()`` produces a random value between ``0`` and ``1``, which is
what the *PathFollow* node's ``unit_offset`` expects.
Here is the complete ``Main.gd`` script so far, for reference.
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
export (PackedScene) var mob_scene
func _ready():
randomize()
func _on_MobTimer_timeout():
var mob = mob_scene.instance()
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
mob_spawn_location.unit_offset = randf()
var player_position = $Player.transform.origin
add_child(mob)
mob.initialize(mob_spawn_location.translation, player_position)
.. code-tab:: csharp
public class Main : Node
{
#pragma warning disable 649
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public override void _Ready()
{
GD.Randomize();
}
public void OnMobTimerTimeout()
{
Mob mob = (Mob)MobScene.Instance();
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
AddChild(mob);
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
}
}
You can test the scene by pressing :kbd:`F6`. You should see the monsters spawn and
move in a straight line.
|image25|
For now, they bump and slide against one another when their paths cross. We'll
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
.. |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
.. |image6| image:: img/05.spawning_mobs/07.top_view.png
.. |image7| image:: img/05.spawning_mobs/08.toggle_view_grid.png
.. |image8| image:: img/05.spawning_mobs/09.toggle_grid_snap.png
.. |image9| image:: img/05.spawning_mobs/10.place_first_cylinder.png
.. |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
.. |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
.. |image17| image:: img/05.spawning_mobs/18.close_path.png
.. |image18| image:: img/05.spawning_mobs/19.path_result.png
.. |image19| image:: img/05.spawning_mobs/20.spawn_nodes.png
.. |image20| image:: img/05.spawning_mobs/20.mob_scene_property.png
.. |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
.. |image25| image:: img/05.spawning_mobs/25.spawn_result.png

View File

@ -1,353 +0,0 @@
.. _doc_first_3d_game_jumping_and_squashing_monsters:
Jumping and squashing monsters
==============================
In this part, we'll add the ability to jump, to 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
the world of :ref:`physics layers
<doc_physics_introduction_collision_layers_and_masks>`.
Controlling physics interactions
--------------------------------
Physics bodies have access to two complementary properties: layers and masks.
Layers define on which physics layer(s) an object is.
Masks control the layers that a body will listen to and detect. This affects
collision detection. When you want two bodies to interact, you need at least one
to have a mask corresponding to the other.
If that's confusing, don't worry, we'll see three examples in a second.
The important point is that you can use layers and masks to filter physics
interactions, control performance, and remove the need for extra conditions in
your code.
By default, all physics bodies and areas are set to both layer and mask ``0``.
This means they all collide with each other.
Physics layers are represented by numbers, but we can give them names to keep
track of what's what.
Setting layer names
~~~~~~~~~~~~~~~~~~~
Let's give our physics layers a name. Go to *Project -> Project Settings*.
|image0|
In the left menu, navigate down to *Layer Names -> 3D Physics*. You can see a
list of layers with a field next to each of them on the right. You can set their
names there. Name the first three layers *player*, *enemies*, and *world*,
respectively.
|image1|
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
*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.
|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
creatures from falling.
Note that you can click the "..." button on the right side of the properties to
see a list of named checkboxes.
|image4|
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.
|image5|
Then, open the *Mob* scene by double-clicking on ``Mob.tscn`` and select the
*Mob* node.
Set its *Collision -> Layer* to "enemies" and unset its *Collision -> Mask*,
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"
mask.
.. note::
The mobs don't need to mask the "world" layer because they only move
on the XZ plane. We don't apply any gravity to them by design.
Jumping
-------
The jumping mechanic itself requires only two lines of code. Open the *Player*
script. We need a value to control the jump's strength and update
``_physics_process()`` to code the jump.
After the line that defines ``fall_acceleration``, at the top of the script, add
the ``jump_impulse``.
.. tabs::
.. code-tab:: gdscript GDScript
#...
# Vertical impulse applied to the character upon jumping in meters per second.
export var jump_impulse = 20
.. code-tab:: csharp
// Don't forget to rebuild the project so the editor knows about the new export variable.
// ...
// Vertical impulse applied to the character upon jumping in meters per second.
[Export]
public int JumpImpulse = 20;
Inside ``_physics_process()``, add the following code before the line where we
called ``move_and_slide()``.
.. tabs::
.. code-tab:: gdscript GDScript
func _physics_process(delta):
#...
# Jumping.
if is_on_floor() and Input.is_action_just_pressed("jump"):
velocity.y += jump_impulse
#...
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_velocity.y += JumpImpulse;
}
// ...
}
That's all you need to jump!
The ``is_on_floor()`` method is a tool from the ``KinematicBody`` class. It
returns ``true`` if the body collided with the floor in this frame. That's why
we apply gravity to the *Player*: so we collide with the floor instead of
floating over it like the monsters.
If the character is on the floor and the player presses "jump", we instantly
give them a lot of vertical speed. In games, you really want controls to be
responsive and giving instant speed boosts like these, while unrealistic, feel
great.
Notice that the Y axis is positive upwards. That's unlike 2D, where the Y axis
is positive downward.
Squashing monsters
------------------
Let's add the squash mechanic next. We're going to make the character bounce
over monsters and kill them at the same time.
We need to detect collisions with a monster and to differentiate them from
collisions with the floor. To do so, we can use Godot's :ref:`group
<doc_groups>` tagging feature.
Open the scene ``Mob.tscn`` again and select the *Mob* node. Go to the *Node*
dock on the right to see a list of signals. The *Node* dock has two tabs:
*Signals*, which you've already used, and *Groups*, which allows you to assign
tags to nodes.
Click on it to reveal a field where you can write a tag name. Enter "mob" in the
field and click the *Add* button.
|image7|
An icon appears in the *Scene* dock to indicate the node is part of at least one
group.
|image8|
We can now use the group from the code to distinguish collisions with monsters
from collisions with the floor.
Coding the squash mechanic
~~~~~~~~~~~~~~~~~~~~~~~~~~
Head back to the *Player* script to code the squash and bounce.
At the top of the script, we need another property, ``bounce_impulse``. When
squashing an enemy, we don't necessarily want the character to go as high up as
when jumping.
.. tabs::
.. code-tab:: gdscript GDScript
# Vertical impulse applied to the character upon bouncing over a mob in
# meters per second.
export var bounce_impulse = 16
.. code-tab:: csharp
// Don't forget to rebuild the project so the editor knows about the new export variable.
// Vertical impulse applied to the character upon bouncing over a mob in meters per second.
[Export]
public int BounceImpulse = 16;
Then, at the bottom of ``_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.
In every iteration of the loop, we check if we landed on a mob. If so, we kill
it and bounce.
With this code, if no collisions occurred on a given frame, the loop won't run.
.. tabs::
.. code-tab:: gdscript GDScript
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
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
for (int index = 0; index < GetSlideCount(); index++)
{
// We check every collision that occurred this frame.
KinematicCollision collision = GetSlideCollision(index);
// If we collide with a monster...
if (collision.Collider is Mob mob && mob.IsInGroup("mob"))
{
// ...we check that we are hitting it from above.
if (Vector3.Up.Dot(collision.Normal) > 0.1f)
{
// If so, we squash it and bounce.
mob.Squash();
_velocity.y = BounceImpulse;
}
}
}
}
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:`KinematicBody<class_KinematicBody>` class and are related to
``move_and_slide()``.
``get_slide_collision()`` returns a
:ref:`KinematicCollision<class_KinematicCollision>` 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")``.
.. 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
that is perpendicular to the plane where the collision occurred. The dot product
allows us to compare it to the up direction.
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
the Mob class.
Open the script ``Mob.gd`` by double-clicking on it in the *FileSystem* dock. At
the top of the script, we want to define a new signal named ``squashed``. And at
the bottom, you can add the squash function, where we emit the signal and
destroy the mob.
.. tabs::
.. code-tab:: gdscript GDScript
# Emitted when the player jumped on the mob.
signal squashed
# ...
func squash():
emit_signal("squashed")
queue_free()
.. code-tab:: csharp
// Don't forget to rebuild the project so the editor knows about the new signal.
// Emitted when the played jumped on the mob.
[Signal]
public delegate void Squashed();
// ...
public void Squash()
{
EmitSignal(nameof(Squashed));
QueueFree();
}
We will use the signal to add points to the score in the next lesson.
With that, you should be able to kill monsters by jumping on them. You can press
:kbd:`F5` to try the game and set ``Main.tscn`` as your project's main scene.
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
.. |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
.. |image7| image:: img/06.jump_and_squash/09.groups_tab.png
.. |image8| image:: img/06.jump_and_squash/10.group_scene_icon.png

View File

@ -1,467 +0,0 @@
.. _doc_first_3d_game_killing_the_player:
Killing the player
==================
We can kill enemies by jumping on them, but the player still can't die.
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
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.
|image0|
In the *Inspector*, assign a cylinder shape to it.
|image1|
Here is a trick you can use to make the collisions only happen when the
player is on the ground or close to it. You can reduce the cylinder's
height and move it up to the top of the character. This way, when the
player jumps, the shape will be too high up for the enemies to collide
with it.
|image2|
You also want the cylinder to be wider than the sphere. This way, the
player gets hit before colliding and being pushed on top of the
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
cannot detect the area. The complementary *Monitoring* property allows
it to detect collisions. Then, remove the *Collision -> Layer* and sets
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*.
|image4|
The *MobDetector* will emit ``body_entered`` when a *KinematicBody* or a
*RigidBody* 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
a ``die()`` function that helps us put a descriptive label on the code.
.. tabs::
.. code-tab:: gdscript GDScript
# Emitted when the player was hit by a mob.
# Put this at the top of the script.
signal hit
# And this function at the bottom.
func die():
emit_signal("hit")
queue_free()
func _on_MobDetector_body_entered(_body):
die()
.. code-tab:: csharp
// Don't forget to rebuild the project so the editor knows about the new signal.
// Emitted when the player was hit by a mob.
[Signal]
public delegate void Hit();
// ...
private void Die()
{
EmitSignal(nameof(Hit));
QueueFree();
}
// We also specified this function name in PascalCase in the editor's connection window
public void OnMobDetectorBodyEntered(Node body)
{
Die();
}
Try the game again by pressing :kbd:`F5`. If everything is set up correctly,
the character should die when an enemy runs into it.
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
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
reaction.
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.
.. tabs::
.. code-tab:: gdscript GDScript
func _on_Player_hit():
$MobTimer.stop()
.. code-tab:: csharp
// We also specified this function name in PascalCase in the editor's connection window
public void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
}
If you try the game now, the monsters will stop spawning when you die,
and the remaining ones will leave the screen.
You can pat yourself in the back: you prototyped a complete 3D game,
even if it's still a bit rough.
From there, we'll add a score, the option to retry the game, and you'll
see how you can make the game feel much more alive with minimalistic
animations.
Code checkpoint
---------------
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``.
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
export(PackedScene) var mob_scene
func _ready():
randomize()
func _on_MobTimer_timeout():
# Create a Mob instance and add it to the scene.
var mob = mob_scene.instance()
# Choose a random location on Path2D.
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
# And give it a random offset.
mob_spawn_location.unit_offset = randf()
var player_position = $Player.transform.origin
add_child(mob)
mob.initialize(mob_spawn_location.translation, player_position)
func _on_Player_hit():
$MobTimer.stop()
.. code-tab:: csharp
public class Main : Node
{
#pragma warning disable 649
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public override void _Ready()
{
GD.Randomize();
}
public void OnMobTimerTimeout()
{
// Create a mob instance and add it to the scene.
var mob = (Mob)MobScene.Instance();
// Choose a random location on Path2D.
// We stire the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
AddChild(mob);
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
}
public void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
}
}
Next is ``Mob.gd``.
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
# 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
var velocity = Vector3.ZERO
func _physics_process(_delta):
move_and_slide(velocity)
func initialize(start_position, player_position):
translation = start_position
look_at(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 squash():
emit_signal("squashed")
queue_free()
func _on_VisibilityNotifier_screen_exited():
queue_free()
.. code-tab:: csharp
public class Mob : KinematicBody
{
// Emitted when the played jumped on the mob.
[Signal]
public delegate void Squashed();
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed = 18;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
MoveAndSlide(_velocity);
}
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
Translation = startPosition;
LookAt(playerPosition, Vector3.Up);
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
float randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
_velocity = Vector3.Forward * randomSpeed;
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
}
public void Squash()
{
EmitSignal(nameof(Squashed));
QueueFree();
}
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}
Finally, the longest script, ``Player.gd``.
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
# Emitted when a mob hit the player.
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
var velocity = Vector3.ZERO
func _physics_process(delta):
var direction = Vector3.ZERO
if Input.is_action_pressed("move_right"):
direction.x += 1
if Input.is_action_pressed("move_left"):
direction.x -= 1
if Input.is_action_pressed("move_back"):
direction.z += 1
if Input.is_action_pressed("move_forward"):
direction.z -= 1
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(translation + direction, Vector3.UP)
velocity.x = direction.x * speed
velocity.z = direction.z * speed
# Jumping.
if is_on_floor() and Input.is_action_just_pressed("jump"):
velocity.y += jump_impulse
velocity.y -= fall_acceleration * delta
velocity = move_and_slide(velocity, Vector3.UP)
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
func die():
emit_signal("hit")
queue_free()
func _on_MobDetector_body_entered(_body):
die()
.. code-tab:: csharp
public class Player : KinematicBody
{
// Emitted when the player was hit by a mob.
[Signal]
public delegate void Hit();
// 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.
[Export]
public int FallAcceleration = 75;
// Vertical impulse applied to the character upon jumping in meters per second.
[Export]
public int JumpImpulse = 20;
// Vertical impulse applied to the character upon bouncing over a mob in meters per second.
[Export]
public int BounceImpulse = 16;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
var direction = Vector3.Zero;
if (Input.IsActionPressed("move_right"))
{
direction.x += 1f;
}
if (Input.IsActionPressed("move_left"))
{
direction.x -= 1f;
}
if (Input.IsActionPressed("move_back"))
{
direction.z += 1f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.z -= 1f;
}
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Spatial>("Pivot").LookAt(Translation + direction, Vector3.Up);
}
_velocity.x = direction.x * Speed;
_velocity.z = direction.z * Speed;
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_velocity.y += JumpImpulse;
}
_velocity.y -= FallAcceleration * delta;
_velocity = MoveAndSlide(_velocity, Vector3.Up);
for (int index = 0; index < GetSlideCount(); index++)
{
KinematicCollision collision = GetSlideCollision(index);
if (collision.Collider is Mob mob && mob.IsInGroup("mob"))
{
if (Vector3.Up.Dot(collision.Normal) > 0.1f)
{
mob.Squash();
_velocity.y = BounceImpulse;
}
}
}
}
private void Die()
{
EmitSignal(nameof(Hit));
QueueFree();
}
public void OnMobDetectorBodyEntered(Node body)
{
Die();
}
}
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
.. |image4| image:: img/07.killing_player/05.body_entered_signal.png
.. |image5| image:: img/07.killing_player/06.player_hit_signal.png

View File

@ -1,475 +0,0 @@
.. _doc_first_3d_game_score_and_replay:
Score and replay
================
In this part, we'll add the score, music playback, and the ability to restart
the game.
We have to keep track of the current score in a variable and display it on
screen using a minimal interface. We 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
edit your User Interface (UI).
Add a *Label* node and rename it to *ScoreLabel*.
|image0|
In the *Inspector*, set the *Label*'s *Text* to a placeholder like "Score: 0".
|image1|
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 *Custom Colors* and click the black box next to *Font Color* to
tint the text.
|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
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
theme resource in *Theme -> Theme*.
|image5|
Click on it to open the theme editor In the bottom panel. It gives you a preview
of how all the built-in UI widgets will look with your theme resource.
|image6|
By default, a theme only has one property, the *Default Font*.
.. seealso::
You can add more properties to the theme resource to design complex user
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*.
|image7|
Expand the *DynamicFont* by clicking on it and expand its *Font* section. There,
you will see an empty *Font Data* field.
|image8|
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
``Montserrat-Medium.ttf`` file we included in the project onto the *Font Data*.
The text will reappear in the theme preview.
The text is a bit small. Set the *Settings -> Size* to ``22`` pixels to increase
the text's size.
|image9|
Keeping track of the score
--------------------------
Let's work on the score next. Attach a new script to the *ScoreLabel* and define
the ``score`` variable.
.. tabs::
.. code-tab:: gdscript GDScript
extends Label
var score = 0
.. code-tab:: csharp
public class ScoreLabel : Label
{
private int _score = 0;
}
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.
Instead, we have to make the connection from the code every time we spawn a
monster.
Open the script ``Main.gd``. If it's still open, you can click on its name in
the script editor's left column.
|image10|
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.
.. tabs::
.. code-tab:: gdscript GDScript
func _on_MobTimer_timeout():
#...
# We connect the mob to the score label to update the score upon squashing one.
mob.connect("squashed", $UserInterface/ScoreLabel, "_on_Mob_squashed")
.. code-tab:: csharp
public void OnMobTimerTimeout()
{
// ...
// We connect the mob to the score label to update the score upon squashing one.
mob.Connect(nameof(Mob.Squashed), GetNode<ScoreLabel>("UserInterface/ScoreLabel"), nameof(ScoreLabel.OnMobSquashed));
}
This line means that when the mob emits the ``squashed`` signal, the
*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.
There, we increment the score and update the displayed text.
.. tabs::
.. code-tab:: gdscript GDScript
func _on_Mob_squashed():
score += 1
text = "Score: %s" % score
.. code-tab:: csharp
public void OnMobSquashed()
{
_score += 1;
Text = string.Format("Score: {0}", _score);
}
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()``
function.
.. seealso::
You can learn more about string formatting here: :ref:`doc_gdscript_printf`.
You can now play the game and squash a few enemies to see the score
increase.
|image11|
.. note::
In a complex game, you may want to completely separate your user interface
from the game world. In that case, you would not keep track of the score on
the label. Instead, you may want to store it in a separate, dedicated
object. But when prototyping or when your project is simple, it is fine to
keep your code simple. Programming is always a balancing act.
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
rectangle with a uniform color and will serve as an overlay to darken the
screen.
To make it span over the whole viewport, you can use the *Layout* menu in the
toolbar.
|image12|
Open it and apply the *Full Rect* command.
|image13|
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.
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
*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.
|image15|
Next, add a *Label* as a child of *Retry* and give it the *Text* "Press Enter to
retry."
|image16|
To move it and anchor it in the center of the screen, apply *Layout -> Center*
to it.
|image17|
Coding the retry option
~~~~~~~~~~~~~~~~~~~~~~~
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
the game. Add this line to the ``_ready()`` function.
.. tabs::
.. code-tab:: gdscript GDScript
func _ready():
#...
$UserInterface/Retry.hide()
.. code-tab:: csharp
public override void _Ready()
{
// ...
GetNode<Control>("UserInterface/Retry").Hide();
}
Then, when the player gets hit, we show the overlay.
.. tabs::
.. code-tab:: gdscript GDScript
func _on_Player_hit():
#...
$UserInterface/Retry.show()
.. code-tab:: csharp
public void OnPlayerHit()
{
//...
GetNode<Control>("UserInterface/Retry").Show();
}
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.
If the player pressed the predefined ``ui_accept`` input action and *Retry* is
visible, we reload the current scene.
.. tabs::
.. code-tab:: gdscript GDScript
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 override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed("ui_accept") && GetNode<Control>("UserInterface/Retry").Visible)
{
// This restarts the current scene.
GetTree().ReloadCurrentScene();
}
}
The function ``get_tree()`` gives us access to the global :ref:`SceneTree
<class_SceneTree>` object, which allows us to reload and restart the current
scene.
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
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.
You can use the autoload feature to have Godot load a node or a scene
automatically at the start of the game, outside the current scene. You can also
use it to create globally accessible objects.
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*.
|image19|
We included a music soundtrack in the ``art/`` directory, ``House In a Forest
Loop.ogg``. Click and drag it onto the *Stream* property in the *Inspector*.
Also, turn on *Autoplay* so the music plays automatically at the start of the
game.
|image20|
Save the scene as ``MusicPlayer.tscn``.
We have to register it as an autoload. Head to the *Project -> Project
Settings…* menu and click on the *Autoload* tab.
In the *Path* field, you want to enter the path to your scene. Click the folder
icon to open the file browser and double-click on ``MusicPlayer.tscn``. Then,
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.
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:
*Remote* and *Local*.
|image22|
The *Remote* tab allows you to visualize the node tree of your running game.
There, you will see the *Main* node and everything the scene contains and the
instantiated mobs at the bottom.
|image23|
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
make the game both look and feel much nicer.
Here is the complete ``Main.gd`` script for reference.
.. tabs::
.. code-tab:: gdscript GDScript
extends Node
export (PackedScene) var mob_scene
func _ready():
randomize()
$UserInterface/Retry.hide()
func _unhandled_input(event):
if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
get_tree().reload_current_scene()
func _on_MobTimer_timeout():
var mob = mob_scene.instance()
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
mob_spawn_location.unit_offset = randf()
var player_position = $Player.transform.origin
add_child(mob)
mob.connect("squashed", $UserInterface/ScoreLabel, "_on_Mob_squashed")
mob.initialize(mob_spawn_location.translation, player_position)
func _on_Player_hit():
$MobTimer.stop()
$UserInterface/Retry.show()
.. code-tab:: csharp
public class Main : Node
{
#pragma warning disable 649
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public override void _Ready()
{
GD.Randomize();
GetNode<Control>("UserInterface/Retry").Hide();
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed("ui_accept") && GetNode<Control>("UserInterface/Retry").Visible)
{
GetTree().ReloadCurrentScene();
}
}
public void OnMobTimerTimeout()
{
Mob mob = (Mob)MobScene.Instance();
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
AddChild(mob);
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
mob.Connect(nameof(Mob.Squashed), GetNode<ScoreLabel>("UserInterface/ScoreLabel"), nameof(ScoreLabel.OnMobSquashed));
}
public void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
GetNode<Control>("UserInterface/Retry").Show();
}
}
.. |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
.. |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
.. |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
.. |image13| image:: img/08.score_and_replay/11.full_rect_option.png
.. |image14| image:: img/08.score_and_replay/12.anchors_updated.png
.. |image15| image:: img/08.score_and_replay/13.retry_color_picker.png
.. |image16| image:: img/08.score_and_replay/14.retry_node.png
.. |image17| image:: img/08.score_and_replay/15.layout_center.png
.. |image18| image:: img/08.score_and_replay/16.new_scene.png
.. |image19| image:: img/08.score_and_replay/17.music_player_node.png
.. |image20| image:: img/08.score_and_replay/18.music_node_properties.png
.. |image21| image:: img/08.score_and_replay/19.register_autoload.png
.. |image22| image:: img/08.score_and_replay/20.scene_dock_tabs.png
.. |image23| image:: img/08.score_and_replay/21.remote_scene_tree.png

View File

@ -1,558 +0,0 @@
.. _doc_first_3d_game_character_animation:
Character animation
===================
In this final lesson, we'll use Godot's built-in animation tools to make our
characters float and flap. You'll learn to design animations in the editor and
use code to make your game feel alive.
|image0|
We'll start with an introduction to using the animation editor.
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 animation player node.
The *Animation* dock appears in the bottom panel.
|image1|
It features a toolbar and the animation drop-down menu at the top, a track
editor in the middle that's currently empty, and filter, snap, and zoom options
at the bottom.
Let's create an animation. Click on *Animation -> New*.
|image2|
Name the animation "float".
|image3|
Once you created the animation, the timeline appears with numbers representing
time in seconds.
|image4|
We want the animation to start playback automatically at the start of the game.
Also, it should loop.
To do so, you can click the button with an "A+" icon in the animation toolbar
and the looping arrows, respectively.
|image5|
You can also pin the animation editor by clicking the pin icon in the top-right.
This prevents it from folding when you click on the viewport and deselect the
nodes.
|image6|
Set the animation duration to ``1.2`` seconds in the top-right of the dock.
|image7|
You should see the gray ribbon widen a bit. It shows you the start and end of
your animation and the vertical blue line is your time cursor.
|image8|
You can click and drag the slider in the bottom-right to zoom in and out of the
timeline.
|image9|
The float animation
-------------------
With the animation player node, you can animate most properties on as many nodes
as you need. Notice the key icon next to properties in the *Inspector*. You can
click any of them to create a keyframe, a time and value pair for the
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.
Select the *Character* and click the key icon next to *Translation* in the
*Inspector*. Do the same for *Rotation Degrees*.
|image10|
Two tracks appear in the editor with a diamond icon representing each keyframe.
|image11|
You can click and drag on the diamonds to move them in time. Move the
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``.
|image13|
Create a keyframe for both properties and shift the translation key to ``0.7``
seconds by dragging it on the timeline.
|image14|
.. note::
A lecture on the principles of animation is beyond the scope of this
tutorial. Just note that you don't want to time and space everything evenly.
Instead, animators play with timing and spacing, two core animation
principles. You want to offset and contrast in your character's motion to
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,
create a key for both properties.
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.
|image15|
You can see that the engine interpolates between your keyframes to produce a
continuous animation. At the moment, though, the motion feels very robotic. This
is because the default interpolation is linear, causing constant transitions,
unlike how living things move in the real world.
We can control the transition between keyframes using easing curves.
Click and drag around the first two keys in the timeline to box select them.
|image16|
You can edit the properties of both keys simultaneously in the *Inspector*,
where you can see an *Easing* property.
|image17|
Click and drag on the curve, pulling it towards the left. This will make it
ease-out, that is to say, transition fast initially and slow down as the time
cursor reaches the next keyframe.
|image18|
Play the animation again to see the difference. The first half should already
feel a bit bouncier.
Apply an ease-out to the second keyframe in the rotation track.
|image19|
Do the opposite for the second translation keyframe, dragging it to the right.
|image20|
Your animation should look something like this.
|image21|
.. note::
Animations update the properties of the animated nodes every frame,
overriding initial values. If we directly animated the *Player* node, it
would prevent us from moving it in code. This is where the *Pivot* node
comes in handy: even though we animated the *Character*, we can still move
and rotate the *Pivot* and layer changes on top of the animation in a
script.
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
to offset it.
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.
|image22|
In ``_physics_process()``, after the line where we check the ``direction``
vector, add the following code.
.. tabs::
.. code-tab:: gdscript GDScript
func _physics_process(delta):
#...
#if direction != Vector3.ZERO:
#...
$AnimationPlayer.playback_speed = 4
else:
$AnimationPlayer.playback_speed = 1
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
if (direction != Vector3.Zero)
{
// ...
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 4;
}
else
{
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 1;
}
}
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
can make the character arc when jumping using the following line of code. Add it
at the end of ``_physics_process()``.
.. tabs::
.. code-tab:: gdscript GDScript
func _physics_process(delta):
#...
$Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
.. code-tab:: csharp
public override void _PhysicsProcess(float delta)
{
// ...
var pivot = GetNode<Spatial>("Pivot");
pivot.Rotation = new Vector3(Mathf.Pi / 6f * _velocity.y / JumpImpulse, pivot.Rotation.y, pivot.Rotation.z);
}
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.
Open the *Mob* scene, select the animation player node and open the float animation.
Next, click on *Animation -> Copy*. Then Open ``Player.tscn`` and open its animation
player. Click *Animation -> Paste*. That's it; all monsters will now play the float
animation.
We can change the playback speed based on the creature's ``random_speed``. Open
the *Mob*'s script and at the end of the ``initialize()`` function, add the
following line.
.. tabs::
.. code-tab:: gdscript GDScript
func initialize(start_position, player_position):
#...
$AnimationPlayer.playback_speed = random_speed / min_speed
.. code-tab:: csharp
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
// ...
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = randomSpeed / MinSpeed;
}
And with that, you finished coding your first complete 3D game.
**Congratulations**!
In the next part, we'll quickly recap what you learned and give you some links
to keep learning more. But for now, here are the complete ``Player.gd`` and
``Mob.gd`` so you can check your code against them.
Here's the *Player* script.
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
# Emitted when the player was hit by a mob.
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
var velocity = Vector3.ZERO
func _physics_process(delta):
var direction = Vector3.ZERO
if Input.is_action_pressed("move_right"):
direction.x += 1
if Input.is_action_pressed("move_left"):
direction.x -= 1
if Input.is_action_pressed("move_back"):
direction.z += 1
if Input.is_action_pressed("move_forward"):
direction.z -= 1
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(translation + direction, Vector3.UP)
$AnimationPlayer.playback_speed = 4
else:
$AnimationPlayer.playback_speed = 1
velocity.x = direction.x * speed
velocity.z = direction.z * speed
# Jumping
if is_on_floor() and Input.is_action_just_pressed("jump"):
velocity.y += jump_impulse
velocity.y -= fall_acceleration * delta
velocity = move_and_slide(velocity, Vector3.UP)
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
$Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
func die():
emit_signal("hit")
queue_free()
func _on_MobDetector_body_entered(_body):
die()
.. code-tab:: csharp
public class Player : KinematicBody
{
// Emitted when the player was hit by a mob.
[Signal]
public delegate void Hit();
// 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.
[Export]
public int FallAcceleration = 75;
// Vertical impulse applied to the character upon jumping in meters per second.
[Export]
public int JumpImpulse = 20;
// Vertical impulse applied to the character upon bouncing over a mob in meters per second.
[Export]
public int BounceImpulse = 16;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
var direction = Vector3.Zero;
if (Input.IsActionPressed("move_right"))
{
direction.x += 1f;
}
if (Input.IsActionPressed("move_left"))
{
direction.x -= 1f;
}
if (Input.IsActionPressed("move_back"))
{
direction.z += 1f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.z -= 1f;
}
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Spatial>("Pivot").LookAt(Translation + direction, Vector3.Up);
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 4;
}
else
{
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 1;
}
_velocity.x = direction.x * Speed;
_velocity.z = direction.z * Speed;
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_velocity.y += JumpImpulse;
}
_velocity.y -= FallAcceleration * delta;
_velocity = MoveAndSlide(_velocity, Vector3.Up);
for (int index = 0; index < GetSlideCount(); index++)
{
KinematicCollision collision = GetSlideCollision(index);
if (collision.Collider is Mob mob && mob.IsInGroup("mob"))
{
if (Vector3.Up.Dot(collision.Normal) > 0.1f)
{
mob.Squash();
_velocity.y = BounceImpulse;
}
}
}
var pivot = GetNode<Spatial>("Pivot");
pivot.Rotation = new Vector3(Mathf.Pi / 6f * _velocity.y / JumpImpulse, pivot.Rotation.y, pivot.Rotation.z);
}
private void Die()
{
EmitSignal(nameof(Hit));
QueueFree();
}
public void OnMobDetectorBodyEntered(Node body)
{
Die();
}
}
And the *Mob*'s script.
.. tabs::
.. code-tab:: gdscript GDScript
extends KinematicBody
# 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
var velocity = Vector3.ZERO
func _physics_process(_delta):
move_and_slide(velocity)
func initialize(start_position, player_position):
translation = start_position
look_at(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()
.. code-tab:: csharp
public class Mob : KinematicBody
{
// Emitted when the played jumped on the mob.
[Signal]
public delegate void Squashed();
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed = 18;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
MoveAndSlide(_velocity);
}
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
Translation = startPosition;
LookAt(playerPosition, Vector3.Up);
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
float randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
_velocity = Vector3.Forward * randomSpeed;
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = randomSpeed / MinSpeed;
}
public void Squash()
{
EmitSignal(nameof(Squashed));
QueueFree();
}
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}
.. |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
.. |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
.. |image15| image:: img/09.adding_animations/14.play_button.png
.. |image16| image:: img/09.adding_animations/15.box_select.png
.. |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
.. |image20| image:: img/09.adding_animations/19.ease_in_second_translation_key.png
.. |image21| image:: img/09.adding_animations/20.float_animation.gif
.. |image22| image:: img/09.adding_animations/21.script_icon.png

View File

@ -1,42 +0,0 @@
.. _doc_first_3d_game_going_further:
Going further
=============
You can pat yourself on the back for having completed your first 3D game with
Godot.
In this series, we went over a wide range of techniques and editor features.
Hopefully, youve witnessed how intuitive Godots scene system can be and
learned a few tricks you can apply in your projects.
But we just scratched the surface: Godot has a lot more in store for you to save
time creating games. And you can learn all that by browsing the documentation.
Where should you begin? Below, youll find a few pages to start exploring and
build upon what youve learned so far.
But before that, heres a link to download a completed version of the project:
`<https://github.com/GDQuest/godot-3d-dodge-the-creeps>`_.
Exploring the manual
--------------------
The manual is your ally whenever you have a doubt or youre curious about a
feature. It does not contain tutorials about specific game genres or mechanics.
Instead, it explains how Godot works in general. In it, you will find
information about 2D, 3D, physics, rendering and performance, and much more.
Here are the sections we recommend you to explore next:
1. Read the :ref:`Scripting section <toc-scripting-core-features>` to learn essential programming features youll use
in every project.
2. The :ref:`3D <toc-learn-features-3d>` and :ref:`Physics <toc-learn-features-physics>` sections will teach you more about 3D game creation in the
engine.
3. :ref:`Inputs <toc-learn-features-inputs>` is another important one for any game project.
You can start with these or, if you prefer, look at the sidebar menu on the left
and pick your options.
We hope you enjoyed this tutorial series, and were looking forward to seeing
what you achieve using Godot.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Some files were not shown because too many files have changed in this diff Show More