mirror of
https://github.com/godotengine/godot-docs.git
synced 2026-01-05 22:09:56 +03:00
441 lines
14 KiB
ReStructuredText
441 lines
14 KiB
ReStructuredText
.. _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``.
|
|
(The reason we are using Node instead of Node2D is because this node will
|
|
be a container for handling game logic. It does not require 2D functionality itself.)
|
|
|
|
Click the **Instance** button (represented by a chain link icon) and select your saved
|
|
``player.tscn``.
|
|
|
|
.. image:: img/instance_scene.webp
|
|
|
|
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:`Marker2D <class_Marker2D>` (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.webp
|
|
|
|
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 Smart 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.webp
|
|
|
|
.. 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.webp
|
|
|
|
Main script
|
|
~~~~~~~~~~~
|
|
|
|
Add a script to ``Main``. At the top of the script, we use
|
|
``@export var mob_scene: PackedScene`` to allow us to choose the Mob scene we want
|
|
to instance.
|
|
|
|
.. tabs::
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
extends Node
|
|
|
|
@export var mob_scene: PackedScene
|
|
var score
|
|
|
|
.. code-tab:: csharp
|
|
|
|
using Godot;
|
|
|
|
public partial class Main : Node
|
|
{
|
|
// Don't forget to rebuild the project so the editor knows about the new export variable.
|
|
|
|
[Export]
|
|
public PackedScene MobScene { get; set; }
|
|
|
|
private 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
|
|
|
|
// 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();
|
|
}
|
|
|
|
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" dock and drop it in the **Mob Scene**
|
|
property.
|
|
- Click the down arrow next to "[empty]" and choose "Load". Select ``mob.tscn``.
|
|
|
|
Next, select the instance of the ``Player`` scene under ``Main`` 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". You are aiming to have the ``hit`` signal
|
|
emitted from ``Player`` and handled in the ``Main`` script. 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<Marker2D>("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_score_timer_timeout():
|
|
score += 1
|
|
|
|
func _on_start_timer_timeout():
|
|
$MobTimer.start()
|
|
$ScoreTimer.start()
|
|
|
|
.. code-tab:: csharp
|
|
|
|
private void OnScoreTimerTimeout()
|
|
{
|
|
_score++;
|
|
}
|
|
|
|
private 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_mob_timer_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_mob_timer_timeout():
|
|
# Create a new instance of the Mob scene.
|
|
var mob = mob_scene.instantiate()
|
|
|
|
# Choose a random location on Path2D.
|
|
var mob_spawn_location = get_node("MobPath/MobSpawnLocation")
|
|
mob_spawn_location.progress_ratio = randf()
|
|
|
|
# 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 += randf_range(-PI / 4, PI / 4)
|
|
mob.rotation = direction
|
|
|
|
# Choose the velocity for the mob.
|
|
var velocity = Vector2(randf_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
|
|
|
|
private 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.
|
|
Mob mob = MobScene.Instantiate<Mob>();
|
|
|
|
// Choose a random location on Path2D.
|
|
var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
|
|
mobSpawnLocation.ProgressRatio = GD.Randf();
|
|
|
|
// 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_progress_ratio((real_t)_random->randf());
|
|
|
|
// 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 ``deg_to_rad()`` and ``rad_to_deg()`` 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():
|
|
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.
|
|
|
|
.. tip:: If you had already set another scene as the "Main Scene", you can right
|
|
click ``main.tscn`` in the FileSystem dock and select "Set As Main Scene".
|
|
|
|
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.
|