Twoja pierwsza gra

Przegląd

Ten samouczek pomoże ci stworzyć pierwszy projekt w Godot. Dowiesz się, jak działa edytor Godot, jak ustawić strukturę projektu i jak zbudować grę 2D.

Informacja

Projekt ten jest wstępem do obsługi silnika Godot. Zakłada on, że masz już pewne doświadczenie w programowaniu. Jeśli jesteś początkującym programistą, powinieneś zacząć od: Skrypty.

Gra nazywa się „Dodge the Creeps!” Twoja postać musi się poruszać i unikać wrogów jak najdłużej. Poniżej znajduje się końcowy efekt:

../../_images/dodge_preview.gif

Dlaczego 2D? Gry 3D są znacznie bardziej złożone niż 2D. Powinieneś trzymać się 2D, dopóki dobrze nie zrozumiesz procesu tworzenia gry.

Konfiguracja projektu

Uruchom Godota i stwórz nowy projekt. Następnie pobierz dodge_assets.zip - obrazy i dźwięki, których będziesz używać do tworzenia gry. Rozpakuj te pliki do folderu projektu.

Informacja

W tym samouczku zakładamy, że znasz edytora. Jeśli nie przeczytałeś Sceny i węzły, zrób to teraz, aby uzyskać wyjaśnienie dotyczące konfiguracji projektu i korzystania z edytora.

Ta gra będzie korzystać z trybu portretowego (pionowego), więc musimy dostosować rozmiar okna gry. Kliknij Projekt -> Ustawienia projektu -> Wyświetlanie -> Okno i ustaw „Szerokość” na 480 oraz „Wysokość” na 720.

Organizacja projektu

W tym projekcie powstaną 3 niezależne sceny: Player, Mob i HUD, które połączymy w scenę Main. W większym projekcie, może być użyteczne, aby istniały różne foldery do przechowywania różnych scen i ich skryptów, ale w tej stosunkowo małej grze, można zapisać sceny i skrypty w folderze głównym, zwanym res://. Foldery projektów są widoczne w Panelu systemu plików w lewym górnym rogu:

../../_images/filesystem_dock.png

Scena Player (Gracz)

Pierwszą sceną, którą stworzymy, będzie Player. Jedną z zalet stworzenia oddzielnej sceny Player jest to, że możemy ją przetestować oddzielnie, nawet zanim utworzyliśmy inne części gry.

Struktura węzłów

Aby rozpocząć, kliknij przycisk „Dodaj/Twórz nowy węzeł” i dodaj węzeł Area2D do sceny.

../../_images/add_node.png

Za pomocą Area2D możemy wykrywać obiekty, które zachodzą na siebie. Zmień jego nazwę na Player klikając na nazwę węzła. Jest to główny węzeł sceny. Możemy dodać dodatkowe węzły do gracza, aby zwiększyć jego funkcjonalność.

Przed dodaniem dzieci do węzła Player chcemy się upewnić, że nie przesunęliśmy ich przypadkowo lub nie zmieniliśmy ich wielkości klikając na nie. Zaznacz węzeł i kliknij ikonę po prawej stronie zamka; jego podpowiedź brzmi „Upewnij się, że dzieci nie są wybieralne.”

../../_images/lock_children.png

Zapisz scenę. Kliknij Scena -> Zapisz lub naciśnij Ctrl+S w Windowsie/Linuksie lub Command+S na komputerze Mac.

Informacja

Dla tego projektu, będziemy korzystać z konwencji nazewnictwa, typowych dla Godota.

  • GDScript: Classes (nodes) use PascalCase, variables and functions use snake_case, and constants use ALL_CAPS (See GDScript style guide).
  • C#: Klasy, eksportowane zmienne i metody używają PascalCase, pola prywatne używają _camelCase, natomiast zmienne lokalne i parametry korzystają z camelCase (Więcej na C# style guide). Zwróć szczególną uwagę na nazwy metod przy łączeniu sygnałów.

Animacja węzła Sprite

Click on the Player node and add an 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 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”. This should automatically open the SpriteFrames panel.

../../_images/spriteframes_panel.png

Po lewej stronie znajduje się lista animacji. Kliknij „default” i zmień jego nazwę na „right”. Następnie kliknij przycisk „Dodaj”, aby utworzyć drugą animację o nazwie „up”. Przeciągnij dwa obrazy dla każdej animacji o nazwie playerGrey_up[1/2] i playerGrey_walk[1/2], w panelu „Klatki animacji”:

../../_images/spriteframes_panel2.png

Obrazy graczy są zbyt duże, aby zmieściły się w oknie gry, więc musimy je pomniejszyć. Kliknij na węzeł AnimatedSprite i ustaw właściwość Scale na (0.5, 0.5). W inspektorze można ją znaleźć pod nagłówkiem Node2D.

../../_images/player_scale.png

Finally, add a 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:

../../_images/player_coll_shape.png

Po zakończeniu, scena Player powinna wyglądać w ten sposób:

../../_images/player_scene_nodes.png

Poruszanie graczem

Teraz musimy dodać funkcjonalność, której nie możemy uzyskać z wbudowanego węzła, więc dodamy skrypt. Kliknij na węzeł Player, a następnie kliknij przycisk „Dodaj skrypt”:

../../_images/add_script_button.png

W oknie ustawień skryptu możesz pozostawić ustawienia domyślne. Kliknij na „Utwórz”:

Informacja

Jeśli tworzysz skrypt C# lub w innych językach, wybierz język z menu rozwijanego Język przed naciśnięciem przycisku Utwórz.

../../_images/attach_node_window.png

Informacja

Jeśli po raz pierwszy spotykasz się z GDScriptem, przeczytaj Skrypty zanim przejdziesz dalej.

Zacznij od zadeklarowania zmiennych, których ten obiekt będzie potrzebował:

extends Area2D

export var speed = 400  # How fast the player will move (pixels/sec).
var screen_size  # Size of the game window.
public class Player : Area2D
{
    [Export]
    public int Speed = 400; // How fast the player will move (pixels/sec).

    private Vector2 _screenSize; // Size of the game window.
}

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.

Ostrzeżenie

Jeżeli używasz C#m musisz (prze)budować projekt, jeśli chcesz zobaczyć nowowyeksportowane zmienne czy sygnały. Takie przebudowanie może zostać uruchomione ręcznie - w tym celu otwórz Panel Mono klikając na tekst „Mono”, znajdujący się na dole okna edytora, a następnie kliknij przycisk „Zbuduj projekt”.

../../_images/export_variable.png

Funkcja _ready() jest wywoływana, gdy węzeł wchodzi do drzewa sceny, co jest dobrym momentem, aby ustalić rozmiar okna gry:

func _ready():
    screen_size = get_viewport_rect().size
public override void _Ready()
{
    _screenSize = GetViewport().GetSize();
}

Teraz możemy użyć funkcji _process() do zdefiniowania, co gracz będzie robił. _process() jest wywoływany co każda klatkę, więc użyjemy go do aktualizacji elementów naszej gry, które, jak się spodziewamy, będą się często zmieniać. W przypadku naszego gracza, musimy zrobić, co następuje:

  • Sprawdzić dane wejściowe.
  • Przesunąć gracza w określonym kierunku.
  • Odtworzyć odpowiednią animację.

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 demo, we will use the default events that are assigned to the arrow keys on the keyboard.

Za pomocą Input.is_action_pressed() możesz sprawdzić który klawisz jest wciśnięty. Naciśnięty klawisz zwraca true a zwraca false gdy nie jest wciśnięty.

func _process(delta):
    var velocity = Vector2()  # The player's movement vector.
    if Input.is_action_pressed("ui_right"):
        velocity.x += 1
    if Input.is_action_pressed("ui_left"):
        velocity.x -= 1
    if Input.is_action_pressed("ui_down"):
        velocity.y += 1
    if Input.is_action_pressed("ui_up"):
        velocity.y -= 1
    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite.play()
    else:
        $AnimatedSprite.stop()
public override void _Process(float delta)
{
    var velocity = new Vector2(); // The player's movement vector.

    if (Input.IsActionPressed("ui_right"))
    {
        velocity.x += 1;
    }

    if (Input.IsActionPressed("ui_left"))
    {
        velocity.x -= 1;
    }

    if (Input.IsActionPressed("ui_down"))
    {
        velocity.y += 1;
    }

    if (Input.IsActionPressed("ui_up"))
    {
        velocity.y -= 1;
    }

    var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");

    if (velocity.Length() > 0)
    {
        velocity = velocity.Normalized() * Speed;
        animatedSprite.Play();
    }
    else
    {
        animatedSprite.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 than if it just moved horizontally.

Możemy temu zapobiec, jeśli znormalizujemy prędkość, co oznacza, że ustawimy jej długość na 1 i pomnożymy ją przez pożądaną prędkość. Oznacza to rezygnację z szybszego ruchu po przekątnej.

Wskazówka

Jeśli nigdy wcześniej nie używałeś matematyki wektorowej lub potrzebujesz przypomnienia, możesz zobaczyć wyjaśnienie użycia wektorów w Godot Wektory. Dobrze jest wiedzieć, ale nie będzie to konieczne dla reszty tego poradnika.

Sprawdzamy również, czy gracz porusza się, abyśmy mogli uruchomić lub zatrzymać animację AnimatedSprite.

Wskazówka

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.

$ jest skrótem od get_node(). Tak więc w kodzie powyżej, $AnimatedSprite.play() jest taki sam jak get_node("AnimatedSprite").play().

Now that we have a movement direction, we can update the player’s position and use clamp() to prevent it from leaving the screen by adding the following to the bottom of the _process function:

position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
Position += velocity * delta;
Position = new Vector2(
    x: Mathf.Clamp(Position.x, 0, _screenSize.x),
    y: Mathf.Clamp(Position.y, 0, _screenSize.y)
);

Wskazówka

Clamp oznacza ograniczenie go do określonego zakresu.

Click „Play Scene” (F6) and confirm you can move the player around the screen in all directions. The console output that opens upon playing the scene can be closed by clicking Output (which should be highlighted in blue) in the lower left of the Bottom Panel.

Ostrzeżenie

Jeśli w panelu „Debugger” pojawi się błąd odnoszący się do „null instance”(braku instancji), prawdopodobnie oznacza to, że nazwa węzła została błędnie wpisana. W nazwach węzłów wielkość liter ma znaczenie i $NodeName lub get_node("NodeName") musi odpowiadać nazwie widocznej w drzewie sceny.

Wybieranie animacji

Teraz, kiedy gracz może się poruszać, musimy zmienić, w której animacji, w zależności od kierunku, jest obecnie AnimatedSprite. Mamy animację „right”(prawo), która powinna być obracana poziomo za pomocą właściwości flip_h dla ruchu lewostronnego, oraz animację „up”, która powinna być obracana pionowo za pomocą flip_v dla ruchu w dół. Umieśćmy ten kod na końcu naszej funkcji _process():

if velocity.x != 0:
    $AnimatedSprite.animation = "right"
    $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
if (velocity.x != 0)
{
    animatedSprite.Animation = "right";
    // See the note below about boolean assignment
    animatedSprite.FlipH = velocity.x < 0;
    animatedSprite.FlipV = false;
}
else if(velocity.y != 0)
{
    animatedSprite.Animation = "up";
    animatedSprite.FlipV = velocity.y > 0;
}

Informacja

The boolean assignments in the code above are a common shorthand for programmers. Consider this code versus the shortened boolean assignment above:

if velocity.x < 0:
    $AnimatedSprite.flip_h = true
else:
    $AnimatedSprite.flip_h = false
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. When you’re sure the movement is working correctly, add this line to _ready(), so the player will be hidden when the game starts:

hide()
Hide();

Preparing for collisions

Chcemy, aby Player wykrył, kiedy został uderzony przez wroga, ale nie zrobiliśmy jeszcze żadnych wrogów! To w porządku, ponieważ będziemy korzystać z funkcji Godot signal (sygnał), aby to zadziałało.

W górnej części skryptu, po extends Area2d: dodaje się następujący tekst:

signal hit
// Don't forget to rebuild the project so the editor knows about the new signal.

[Signal]
public delegate void Hit();

Definiuje on niestandardowy sygnał zwany „hit”(trafieniem), który nasz gracz wyemituje (wyśle), kiedy zderzy się z wrogiem. Do wykrywania kolizji użyjemy Area2D. Wybierz węzeł Player i kliknij zakładkę „Węzeł” obok zakładki Inspektor, aby zobaczyć listę sygnałów, które odtwarzacz może emitować:

../../_images/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( Object body ) signal; this will be emitted when a body contacts the player. Click „Connect..” and then „Connect” again on the „Connecting Signal” window. We don’t need to change any of these settings - Godot will automatically create a function in your player’s script. This function will be called whenever the signal is emitted - it handles the signal.

Wskazówka

Podczas podłączania sygnału, zamiast domyślnie utworzenie funkcję, możesz również podać nazwę już istniejącej funkcji, z którą chcesz powiązać sygnał.

Dodaj ten kod do funkcji:

func _on_Player_body_entered(body):
    hide()  # Player disappears after being hit.
    emit_signal("hit")
    $CollisionShape2D.set_deferred("disabled", true)
public void OnPlayerBodyEntered(PhysicsBody2D body)
{
    Hide(); // Player disappears after being hit.
    EmitSignal("Hit");
    GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("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.

Informacja

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() allows us to have Godot wait to disable the shape until it’s safe to do so.

Ostatnim elementem dla naszego gracza jest dodanie funkcji, którą możemy wywołać, aby zresetować gracza podczas uruchamiania nowej gry.

func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false
public void Start(Vector2 pos)
{
    Position = pos;
    Show();
    GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
}

Enemy scene

Teraz nadszedł czas, aby zrobić wrogów, których nasz gracz będzie musiał unikać. Ich zachowanie nie będzie zbyt skomplikowane: tłumy będą się pojawiać losowo na krawędziach ekranu i poruszać w losowym kierunku po linii prostej, a następnie usuwać się, gdy znikają z ekranu.

Zbudujemy to w scenie ``Mob``(Przeciwnik), której możemy utworzyć instancje czyli utworzyć dowolną wolną liczbę niezależnych przeciwników w grze.

Node setup

Kliknij Scena -> Nowa scena i stwórzmy Mob.

Scena Mob będzie korzystać z następujących węzłów:

  • RigidBody2D (o nazwie Mob)
    • : ref: «AnimatedSprite <class_AnimatedSprite>»
    • CollisionShape2D
    • VisibilityNotifier2D (o nazwie Visibility)

Nie zapomnij ustawić dzieci tak, aby nie mogły być wybierane, jak to miało miejsce w przypadku sceny z graczem.

We właściwościach RigidBody2D ustaw Gravity Scale``(Skala Grawitacji) na ``0, tak aby przeciwnik nie spadł w dół. Ponadto w sekcji ``PhysicsBody2D``(Ciało fizyczne 2D) kliknij na właściwość ``Mask``(Maska) i odznaczyć pierwsze pole wyboru. Zapewni to, że przeciwnicy nie będą się zderzać.

../../_images/set_collision_mask.png

Skonfiguruj AnimatedSprite tak jak zrobiłeś to dla gracza. Tym razem mamy 3 animacje: fly, swim i walk. Ustawić właściwość Odtwarzanie w inspektorze na „Włączony” i dostosować ustawienie „Prędkość na sekundę”, jak pokazano poniżej. Jedną z tych animacji wybierzemy losowo, tak aby przeciwnicy mieli jakąś różnorodność.

../../_images/mob_animations.gif

fly powinno wyświetlać się w 3 FPS, a swim i walk w 4 FPS.

Podobnie jak sprite gracza, też i sprite przeciwników muszą być zmniejszone. Ustaw właściwość AnimatedSprite - Skala na (0,75, 0,75).

Podobnie jak w przypadku sceny Player, dodaj CapsuleShape2D dla kolizji. Aby wyrównać kształt z spritem, należy ustawić właściwość Rotation Degrees``(Kąt) na ``90 pod Node2D.

Enemy script

Dodaj skrypt do Mob i dodaj do niego następujące zmienne:

extends RigidBody2D

export var min_speed = 150  # Minimum speed range.
export var max_speed = 250  # Maximum speed range.
var mob_types = ["walk", "swim", "fly"]
public class Mob : RigidBody2D
{
    // Don't forget to rebuild the project so the editor knows about the new export variables.

    [Export]
    public int MinSpeed = 150; // Minimum speed range.

    [Export]
    public int MaxSpeed = 250; // Maximum speed range.

    private String[] _mobTypes = {"walk", "swim", "fly"};
}

When we spawn a mob, we’ll pick a random value between min_speed and max_speed for how fast each mob will move (it would be boring if they were all moving at the same speed). We also have an array containing the names of the three animations, which we’ll use to select a random one. Make sure you’ve spelled these the same in the script and in the SpriteFrames resource.

Spójrzmy teraz na resztę skryptu. W ready() losowo wybieramy jeden z trzech typów animacji:

func _ready():
    $AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
// C# doesn't implement GDScript's random methods, so we use 'System.Random' as an alternative.
static private Random _random = new Random();

public override void _Ready()
{
    GetNode<AnimatedSprite>("AnimatedSprite").Animation = _mobTypes[_random.Next(0, _mobTypes.Length)];
}

Informacja

Musisz użyć randomize(), jeśli chcesz, aby sekwencja liczb „losowych” różniła się za każdym razem, gdy uruchamiasz scenę. Będziemy używać randomize() w naszej scenie Main, więc nie będziemy tego tutaj potrzebować. randi() % n jest standardowym sposobem uzyskania losowej liczby całkowitej pomiędzy 0 i n-1.

Ostatnim krokiem jest zmuszenie przeciwników do samodzielnego usunięcia się po opuszczeniu ekranu. Trzeba podłączyć funkcję screen_exited() z sygnałem Visibility i dodać ten kod:

func _on_Visibility_screen_exited():
    queue_free()
public void OnVisibilityScreenExited()
{
    QueueFree();
}

To kończy tworzenie sceny Przeciwnika.

Main scene

Teraz nadszedł czas, aby to wszystko połączyć. Utwórz nową scenę i dodaj Węzeł o nazwie Main. Kliknij przycisk „Instancjuj” i wybierz zapisany Player.tscn.

../../_images/instance_scene.png

Informacja

Zobacz Tworzenie instancji aby dowiedzieć się więcej o instancjach.

Now, add the following nodes as children of Main, and name them as shown (values are in seconds):

  • Czasomierz (o nazwie MobTimer) - do kontroli częstotliwości tworzenia przeciwników
  • Czasomierz (o nazwie ScoreTimer) - aby co sekundę zwiększać liczbę punktów
  • Czasomierz (o nazwie StartTimer) - aby podać opóźnienie przed rozpoczęciem
  • Position2D (o nazwie StartPosition) - wskazuje pozycję startową gracza

Ustawić właściwość Czas oczekiwania każdego z węzłów Timer w następujący sposób:

  • MobTimer: 0.5
  • ScoreTimer: 1
  • StartTimer: 2

Dodatkowo, ustaw właściwość Jedno Przejście w StartTimer na „Włączony” i ustaw właściwość Position z węzła StartPosition na (240, 450).

Spawning mobs

Węzeł główny Main będzie się tworzył nowych przeciwników. Chcemy, aby pojawiały się one w przypadkowym miejscu na krawędzi ekranu. Dodaj węzeł Path2D o nazwie MobPath jako dziecko Main. Po wybraniu Path2D na górze edytora pojawi się kilka nowych przycisków:

../../_images/path2d_buttons.png

Wybierz środkowy („Dodaj punkt”) i narysuj ścieżkę, klikając, aby dodać punkty w wyświetlonych rogach. Aby punkty były wyrównane do siatki, sprawdź, czy zaznaczona jest opcja „Wyrównaj do siatki”. Opcję tę można znaleźć pod przyciskiem „Opcje przyciągania” po lewej stronie przycisku „Zablokuj”, pojawiającym się jako trzy pionowe kropki.

../../_images/draw_path2d.gif

Ważne

Narysuj ścieżkę zgodnie z ruchem zegara albo twoje przeciwnicy będą się tworzyli na zewnątrz zamiast do wewnątrz!

Po umieszczeniu punktu 4 na obrazku, kliknij przycisk „Zamknij krzywą”, a krzywa zostanie zamknięta.

Teraz, gdy ścieżka jest zdefiniowana, dodaj węzeł PathFollow2D <class_PathFollow2D>`jako dziecko ``MobPath` i nazwij go MobSpawnLocation. Węzeł ten będzie automatycznie obracał się i podążał ścieżką, tak abyśmy mogli go wykorzystać do wybrania losowego położenia i kierunku wzdłuż ścieżki.

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.

extends Node

export (PackedScene) var Mob
var score

func _ready():
    randomize()
public class Main : Node
{
    // Don't forget to rebuild the project so the editor knows about the new export variable.

    [Export]
    public PackedScene Mob;

    private int _score;

    // We use 'System.Random' as an alternative to GDScript's random methods.
    private Random _random = new Random();

    public override void _Ready()
    {
    }

    // We'll use this later because C# doesn't support GDScript's randi().
    private float RandRange(float min, float max)
    {
        return (float)_random.NextDouble() * (max - min) + min;
    }
}

Przeciągnij Mob.tscn z panelu „System plików” i upuść go we właściwość Mob pod zmiennymi skryptu węzła Main.

Następnie kliknij na Player i podłącz sygnał hit. Chcemy stworzyć nową funkcję o nazwie game_over, która będzie obsługiwać to, co musi się zdarzyć, gdy gra się skończy. Wpisz „game_over” w polu „Funkcje w węźle” u dołu okna „Łączenie sygnału”. Dodaj następujący kod, jak również funkcję new_game, aby skonfigurować wszystko, co jest skonfigurowane dla nowej gry:

func game_over():
    $ScoreTimer.stop()
    $MobTimer.stop()

func new_game():
    score = 0
    $Player.start($StartPosition.position)
    $StartTimer.start()
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();
}

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.

func _on_StartTimer_timeout():
    $MobTimer.start()
    $ScoreTimer.start()

func _on_ScoreTimer_timeout():
    score += 1
public void OnStartTimerTimeout()
{
    GetNode<Timer>("MobTimer").Start();
    GetNode<Timer>("ScoreTimer").Start();
}

public void OnScoreTimerTimeout()
{
    _score++;
}

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.

Nowa instancja musi być dodana do sceny za pomocą add_child().

Now click on MobTimer in the scene window then head to inspector window, switch to node view then click on timeout() and connect the signal.

Dodaj poniższy kod:

func _on_MobTimer_timeout():
    # Choose a random location on Path2D.
    $MobPath/MobSpawnLocation.set_offset(randi())
    # Create a Mob instance and add it to the scene.
    var mob = Mob.instance()
    add_child(mob)
    # Set the mob's direction perpendicular to the path direction.
    var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
    # Set the mob's position to a random location.
    mob.position = $MobPath/MobSpawnLocation.position
    # Add some randomness to the direction.
    direction += rand_range(-PI / 4, PI / 4)
    mob.rotation = direction
    # Set the velocity (speed & direction).
    mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
    mob.linear_velocity = mob.linear_velocity.rotated(direction)
public void OnMobTimerTimeout()
{
    // Choose a random location on Path2D.
    var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
    mobSpawnLocation.SetOffset(_random.Next());

    // Create a Mob instance and add it to the scene.
    var mobInstance = (RigidBody2D)Mob.Instance();
    AddChild(mobInstance);

    // 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.
    mobInstance.Position = mobSpawnLocation.Position;

    // Add some randomness to the direction.
    direction += RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
    mobInstance.Rotation = direction;

    // Choose the velocity.
    mobInstance.SetLinearVelocity(new Vector2(RandRange(150f, 250f), 0).Rotated(direction));
}

Ważne

W przypadku funkcji wymagających kąta, GDScript używa radianów, a nie stopni. Jeśli jest tobie łatwiej pracować ze stopniami, trzeba będzie użyć deg2rad() i rad2deg() do konwersji między tymi dwoma funkcjami.

HUD

Ostatnim elementem, którego potrzebuje nasza gra jest interfejs użytkownika: interfejs do wyświetlania takich informacji, jak wynik, komunikat „game over”(koniec gry) i przycisk restartu. Utwórz nową scenę i dodaj węzeł CanvasLayer o nazwie HUD. „HUD” oznacza „Heads-up display” - wyświetlacz informacyjny, który pojawia się jako nakładka na widoku gry i informuje o np. liczbie punktów, punktów życia.

Węzeł CanvasLayer pozwala nam narysować nasze elementy interfejsu użytkownika na warstwie będącą przed resztą gry, tak aby wyświetlane przez niego informacje nie były zakryte żadnymi elementami gry, takimi jak gracz lub przeciwnik.

HUD wyświetla następujące informacje:

  • Wynik, zmieniany przez ScoreTimer.
  • Wiadomości takie jak „Koniec Gry” lub „Przygotuj Się!”
  • Przycisk „Start” rozpoczyna grę.

Podstawowym węzłem elementów interfejsu użytkownika jest Control. Aby utworzyć nasz interfejs użytkownika, będziemy używać dwóch typów węzła ref:Control <class_Control> - Label oraz Button.

Utwórz je jako dzieci węzła HUD:

  • Label nazwany ScoreLabel.
  • Label nazwany MessageLabel.
  • Przycisk Button o nazwie StartButton.
  • Timer o nazwie 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 for each of the three Control nodes:

  1. W zakładce „Czcionki niestandardowe” wybierz opcję „Nowa czcionka dynamiczna”
../../_images/custom_font1.png
  1. Click on the „DynamicFont” you added, and under „Font/Font Data”, choose „Load” and select the „Xolonium-Regular.ttf” file. You must also set the font’s Size. A setting of 64 works well.
../../_images/custom_font2.png

Informacja

Kotwice i marginesy: Węzły Control mają położenie i wielkość, ale także kotwice i margiesy. Kotwice określają początek - punkt odniesienia dla krawędzi węzła. Marginesy są aktualizowane automatycznie po przesunięciu lub zmianie rozmiaru węzła control. Przedstawiają one odległość od krawędzi węzła control do jego punktu zakotwiczenia. Więcej informacji na ten temat można znaleźć w temacie Twórz interfejs z węzłami kontrolującymi.

Uporządkuj węzły w sposób pokazany poniżej. Kliknąć przycisk „Zakotwicz”, aby ustawić dla węzła Control kotwicę:

../../_images/ui_anchor.png

Węzły można przeciągnąć ręcznie lub, aby uzyskać bardziej precyzyjne rozmieszczenie, należy użyć następujących ustawień:

ScoreLabel

  • Text : 0
  • Layout : „Top Wide”
  • Align : „Center”

MessageLabel

  • Text : Dodge the Creeps!
  • Layout : „HCenter Wide”
  • Align : „Center”

StartButton

  • Text : Start
  • Layout : „Center Bottom”
  • Margin :
    • Góra: -200
    • Dół: -100

Teraz dodaj ten skrypt do HUD:

extends CanvasLayer

signal start_game
public class HUD : CanvasLayer
{
    // Don't forget to rebuild the project so the editor knows about the new signal.

    [Signal]
    public delegate void StartGame();
}

Sygnał start_game informuje węzeł Main o naciśnięciu przycisku.

func show_message(text):
    $MessageLabel.text = text
    $MessageLabel.show()
    $MessageTimer.start()
public void ShowMessage(string text)
{
    var messageLabel = GetNode<Label>("MessageLabel");
    messageLabel.Text = text;
    messageLabel.Show();

    GetNode<Timer>("MessageTimer").Start();
}

Ta funkcja jest wywoływana, gdy chcemy tymczasowo wyświetlić komunikat, taki jak „Przygotuj się”. W MessageTimer ustaw Czas oczekiwania na 2 i ustaw właściwość Jednokrotny na „Włączone”.

func show_game_over():
    show_message("Game Over")
    yield($MessageTimer, "timeout")
    $MessageLabel.text = "Dodge the\nCreeps!"
    $MessageLabel.show()
    yield(get_tree().create_timer(1), 'timeout')
    $StartButton.show()
async public void ShowGameOver()
{
    ShowMessage("Game Over");

    var messageTimer = GetNode<Timer>("MessageTimer");
    await ToSignal(messageTimer, "timeout");

    var messageLabel = GetNode<Label>("MessageLabel");
    messageLabel.Text = "Dodge the\nCreeps!";
    messageLabel.Show();

    GetNode<Button>("StartButton").Show();
}

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.

Informacja

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 delay, such as in the above code, where we want to wait a little bit of time before showing the „Start” button.

func update_score(score):
    $ScoreLabel.text = str(score)
public void UpdateScore(int score)
{
    GetNode<Label>("ScoreLabel").Text = score.ToString();
}

This function is called by Main whenever the score changes.

Podłączyć Timeout() do sygnału MessageTimer i naciśnięty() do sygnału StartButton.

func _on_StartButton_pressed():
    $StartButton.hide()
    emit_signal("start_game")

func _on_MessageTimer_timeout():
    $MessageLabel.hide()
public void OnStartButtonPressed()
{
    GetNode<Button>("StartButton").Hide();
    EmitSignal("StartGame");
}

public void OnMessageTimerTimeout()
{
    GetNode<Label>("MessageLabel").Hide();
}

Podłączenie HUD do Main

Teraz, kiedy już stworzyłeś scenę HUD, zapisz ją i wróć do Main`. Stwórz instancję HUD w Main tak, jak zrobiłeś to Player scenę i umieść ją na dole drzewa. Pełne drzewo powinno wyglądać tak, więc upewnij się, że niczego nie przegapiłeś:

../../_images/completed_main_scene.png

Teraz musimy podłączyć funkcję HUD do naszego skryptu Main. Wymaga to paru poprawek do sceny Main:

W zakładce Węzeł należy podłączyć sygnał HUD’a start_game do funkcji new_game().

W new_game(), zaktualizuj wyświetlany wynik i pokaż komunikat „Przygotuj się”:

$HUD.update_score(score)
$HUD.show_message("Get Ready")
var hud = GetNode<HUD>("HUD");
hud.UpdateScore(_score);
hud.ShowMessage("Get Ready!");

W game_over() musimy wywołać odpowiednią funkcję w HUD:

$HUD.show_game_over()
GetNode<HUD>("HUD").ShowGameOver();

Na koniec dodaj to do on_on_ScoreTimer_timeout(), aby wyświetlacz był zsynchronizowany ze zmieniającym się wynikiem:

$HUD.update_score(score)
GetNode<HUD>("HUD").UpdateScore(_score);

Teraz jesteś gotowy do gry! Kliknąć przycisk „Uruchom projekt”. Zostaniesz poproszony o wybranie głównej sceny, więc wybierz Main.tscn.

Removing old creeps

If you play until „Game Over” and then start a new game the creeps from the previous game are still on screen. It would be better if they all disappeared at the start of a new game.

We’ll use the start_game signal that’s already being emitted by the HUD node to remove the remaining creeps. We can’t use the editor to connect the signal to the mobs in the way we need because there are no Mob nodes in the Main scene tree until we run the game. Instead we’ll use code.

Start by adding a new function to Mob.gd. queue_free() will delete the current node at the end of the current frame.

func _on_start_game():
    queue_free()
public void OnStartGame()
{
    QueueFree();
}

Then in Main.gd add a new line inside the _on_MobTimer_timeout() function, at the end.

$HUD.connect("start_game", mob, "_on_start_game")
GetNode("HUD").Connect("StartGame", mobInstance, "OnStartGame");

This line tells the new Mob node (referenced by the mob variable) to respond to any start_game signal emitted by the HUD node by running its _on_start_game() function.

Finishing up

Ukończyliśmy już wszystkie funkcjonalności naszej gry. Poniżej przedstawiamy kilka kroków, które należy jeszcze podjąć, aby dodać trochę więcej funkcji w celu poprawy wrażeń z gry. Zapraszamy do rozszerzenia rozgrywki o własne pomysły.

Tło

Domyślnie szare tło nie jest zbyt atrakcyjne, więc zmieńmy jego kolor. Jednym ze sposobów na to jest użycie węzła ColorRect. Stwórz pierwszy węzeł pod Main tak, aby był rysowany za innymi węzłami. ColorRect ma tylko jedną właściwość: Kolor. Wybierz kolor, który chcesz i przeciągnij ColorRect tak, aby zakrył ekran.

You could also add a background image, if you have one, by using a Sprite node.

Sound effects

Dźwięk i muzyka mogą być najskuteczniejszym sposobem na uatrakcyjnienie wrażeń z gry. W folderze zasobów do gry znajdują się dwa pliki dźwiękowe: „House In a Forest Loop.ogg” na muzykę w tle i „gameover.wav” odtwarzaną w razie przegranej.

Dodaj dwa węzły AudioStreamPlayer jako dzieci Main. Nazwij jedną z nich muzykę, a drugą DeathSound. Na każdym z nich kliknij na właściwość Stream, wybierz opcję „Wczytaj” i wybierz odpowiedni plik audio.

Aby odtwarzać muzykę, dodaj $Music.play() w funkcji new_game() i $Music.stop( ) w funkcji game_over().

Na koniec dodaj $DeathSound.play() do funkcji game_over().

Skrót klawiszowy

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. One way to do this is using the „Shortcut” property of the Button node.

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 „InputEvent”. Finally, in the Action property, type the name ui_select. This is the default input event associated with the spacebar.

../../_images/start_button_shortcut.png

Now when the start button appears, you can either click it or press the spacebar to start the game.

Project files

You can find a completed version of this project at these locations: