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:

../../_images/add_script_button.png

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

Informacja

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

../../_images/attach_node_window.png

Informacja

If this is your first time encountering GDScript, please read Scripting languages before continuing.

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.

Użycie słowa kluczowego export przy pierwszej zmiennej speed pozwala na ustawienie jej wartości w inspektorze. Może to być przydatne w przypadku wartości, które mają być dostosowywane w taki sam sposób, jak wbudowane właściwości węzła. Kliknij na węzeł Player i zauważ, że wyeksportowana właściwość pojawiła się w sekcji "Zmienne skryptu" Inspektora. Pamiętaj - jeżeli zmienisz wartość zmiennej tutaj, nadpisze ona wartość ustawioną w skrypcie.

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

Teraz możemy użyć funkcji _process() do zdefiniowania, co gracz będzie robił. _process() jest wywoływany co 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 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.

../../_images/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".

../../_images/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:

../../_images/input-mapping-completed.png

Click the "Close" button to close the project settings.

Informacja

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.

Za pomocą Input.is_action_pressed() możesz sprawdzić, czy klawisz jest wciśnięty. Naciśnięty klawisz zwraca true. Wartość false zostanie zwrócona, gdy klawisz nie jest wciśnięty.

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()

Zaczniemy od ustawienia velocity na (0, 0) - domyślnie gracz nie powinien się poruszać. Sprawdzamy każde wejście i dodajemy/odejmujemy od velocity``(prędkość), aby uzyskać kierunek całkowity. Na przykład, jeśli przytrzymasz jednocześnie ``prawo i dół`, otrzymany wektor velocity będzie ``(1, 1) ``. W tym przypadku, ponieważ dodajemy ruch poziomy i pionowy, gracz poruszałby się szybciej niż gdyby po prostu poruszał się poziomo.

Możemy tego uniknąć, jeśli znormalizujemy prędkość, co oznacza ustawienie jej length (długości) 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 Godocie (Wektory). Ogólnie dobrze jest to wiedzieć, jednak wiedza ta nie będzie konieczna dla reszty tego poradnika.

We also check whether the player is moving so we can call play() or stop() on the AnimatedSprite.

Wskazówka

$ is shorthand for get_node(). So in the code above, $AnimatedSprite.play() is the same as get_node("AnimatedSprite").play().

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.

Teraz, gdy mamy kierunek ruchu, możemy zaktualizować położenie gracza. Możemy również użyć clamp(), aby zapobiec opuszczenia ekranu przez naszą postać. Klampowanie wartości oznacza ograniczenie jej do danego zakresu. Dodaj na dole funkcji _process podany fragment kodu (upewnij się przy tym, że nie zostanie on wciągnięty pod else):

position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)

Wskazówka

Parametr delta w funkcji _process() odnosi się do długości ramki - czasu, jaki zajęło wykonanie poprzedniej ramki. Użycie tej wartości zapewnia, że ruch pozostanie spójny nawet w przypadku zmiany ilości klatek.

Click "Play Scene" (F6, Cmd + R on macOS) and confirm you can move the player around the screen in all directions.

Ostrzeżenie

Jeśli otrzymasz błąd w panelu "Debugger", mówiący

Próba wywołania funkcji 'play' w podstawowej 'instancji null' w instancji o wartości null

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.

Wybieranie animacji

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:

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

Informacja

Przypisania logiczne w powyższym kodzie są częstym skrótem dla programistów. Ponieważ robimy test porównawczy (boolean), a także przypisanie wartości logicznej boolean, możemy wykonać obie te czynności w tym samym czasie. Rozważmy pokazany tu kod w porównaniu do powyższego jednowierszowego przypisania logicznego:

if velocity.x < 0:
    $AnimatedSprite.flip_h = true
else:
    $AnimatedSprite.flip_h = false

Odtwórz scenę ponownie i sprawdź, czy wszystkie animacje są odtwarzane poprawnie w każdym z kierunków.

Wskazówka

Częstym błędem jest tu podanie niepoprawnych nazw animacji. Nazwy animacji w panelu SpriteFrames muszą odpowiadać nazwom wpisanym w kodzie. Jeśli nazywamy animację "Walk", musimy również użyć nazwy pisanej z wielkiej litery "W" w kodzie.

Gdy upewnisz się, że mechanika poruszania się działa poprawnie, dodaj pokazaną linijkę do _ready(), aby ukryć naszego gracza przy starcie gry:

hide()

Przygotowanie do kolizji

Chcemy, aby Player wykrywał, kiedy zostanie uderzony przez wroga, jednak nie mamy jeszcze żadnych wrogów! Nie ma się jednak co tym przejmować, ponieważ korzystać będziemy z Godotowej funkcji sygnału (signal).

W górnej części skryptu, po extends Area2d, dodajmy następujący kod:

signal 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. Wybierzmy węzeł Player i kliknijmy zakładkę "Węzeł" obok zakładki Inspektor, aby zobaczyć listę sygnałów, które nasz gracz może emitować:

../../_images/player_signals.png

Zauważ, że nasz niestandardowy sygnał "hit" również się tutaj znajduje! Ponieważ nasi wrogowie będą węzłami RigidBody2D, chcemy sygnału body_entered(body: Node); będzie on emitowany, gdy ciało zetknie się z graczem. Kliknijmy przycisk "Połącz...", by otworzyć okno "Połącz sygnał do metody". Nie ma potrzeby zmieniać tu żadnego z domyślnych ustawień - po prostu ponownie kliknijmy "Połącz", a Godot automatycznie utworzy w skrypcie gracza funkcję _on_Player_body_entered.

../../_images/player_signal_connection.png

Należy zwrócić uwagę na zielony symbol oznaczający, że do funkcji podłączony jest sygnał. Dodajmy do stworzonej przed chwilą funkcji ten kod:

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)

Za każdym razem, gdy wróg uderzy w gracza, wysłany zostanie sygnał. Po tym zdarzeniu musimy wyłączyć kolizje gracza, aby nie wywołać więcej niż jednego sygnału hit.

Informacja

Wyłączenie obszaru kształtu kolizji może spowodować błąd, jeśli nastąpi to w środku procesu przetwarzania kolizji przez silnik. Użycie ``set_deferred()```powie Godotowi, aby czekał na wyłączenie kształtu do momentu, aż bezpiecznie będzie można to zrobić.

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

func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false

With the player working, we'll work on the enemy in the next lesson.