123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- .. _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.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" 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 ``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.
- .. 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.
|