Removed some of the rst files.
@ -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`
|
@ -9,8 +9,6 @@ About
|
||||
faq
|
||||
troubleshooting
|
||||
list_of_features
|
||||
docs_changelog
|
||||
release_policy
|
||||
complying_with_licenses
|
||||
|
||||
.. history
|
||||
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
Before Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 825 KiB |
Before Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 43 KiB |
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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, you’ve witnessed how intuitive Godot’s 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, you’ll find a few pages to start exploring and
|
||||
build upon what you’ve learned so far.
|
||||
|
||||
But before that, here’s 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 you’re 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 you’ll 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 we’re looking forward to seeing
|
||||
what you achieve using Godot.
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 1016 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 4.1 KiB |