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

Im Fenster mit den Skripteinstellungen können Sie die Standardeinstellungen beibehalten. Klicken Sie einfach auf "Erstellen":

Bemerkung

Wenn Sie ein C#-Skript oder andere Sprachen verwenden wollen, wählen Sie die Sprache aus dem Auswahlmenü "Sprache", bevor Sie auf Erstellen klicken.

../../_images/attach_node_window.png

Bemerkung

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

Beginnen Sie, indem Sie die Member-Variablen deklarieren, die dieses Objekt benötigt:

extends Area2D

export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.

Wenn Sie das Schlüsselwort export für die erste Variable speed verwenden, ermöglicht es Ihnen dessen Wert im Inspektor einzustellen. Dies kann für Werte nützlich sein, die Sie wie die integrierten Eigenschaften eines Nodes anpassen möchten. Klicken Sie auf den Node Player, und die Eigenschaft wird jetzt im Bereich "Script Variables" des Inspektors angezeigt. Denken Sie daran, wenn Sie den Wert hier ändern, wird der im Skript verwendete Wert überschrieben.

Warnung

Wenn Sie C# verwenden, müssen Sie die Projekt-Bausteine neu übersetzen, um neue Exportvariablen oder Signale sichtbar zu machen. Das kann manuell durch einen Klick auf das Wort "Mono" im unteren Fensterbereich geschehen, es wird das Mono-Panel eingeblendet und ein anschließender Klick auf "Build Project" führt die Aktion aus.

../../_images/export_variable.png

Die Funktion _ready() wird aufgerufen, wenn ein Node in den Szenenbaum eintritt, was ein guter Zeitpunkt ist, um die Größe des Spielfensters zu ermitteln:

func _ready():
    screen_size = get_viewport_rect().size

Jetzt können wir die Funktion _process() verwenden, um festzulegen, was der Spieler tun soll. _process() wird mit jedem Frame aufgerufen, deshalb werden wir es verwenden, um Elemente unseres Spiels zu aktualisieren, von denen wir erwarten, dass sie sich häufig ändern werden. Für den Spieler müssen wir Folgendes tun:

  • Auf Eingabe prüfen.

  • In die angegebene Richtung bewegen.

  • Die entsprechende Animation abspielen.

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.

Bemerkung

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.

Sie können herausfinden, ob eine Taste gedrückt wird, indem Sie Input.is_action_pressed() benutzen, welches true zurückgibt, wenn eine Taste gedrückt wird und false wenn nicht.

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

Wir fangen damit an, die velocity (Geschwindigkeit) auf (0,0) zu setzen - standardmäßig sollte sich der Spieler nicht bewegen. Dann überprüfen wir jede Eingabe und addieren/subtrahieren von der velocity, um eine Gesamtrichtung zu erhalten. Wenn Sie beispielsweise rechts und runter gleichzeitig halten, ist der resultierende velocity-Vektor (1, 1). In diesem Fall, da wir eine horizontale und vertikale Bewegung addieren, würde sich der Spieler schneller bewegen, als wenn er sich nur horizontal bewegen würde.

Wir können das verhindern, indem wir die Geschwindigkeit normalisieren, was bedeutet, dass wir ihre Länge auf 1 festlegen, anschließend multiplizieren wir mit der gewünschten Geschwindigkeit. Das sorgt dafür, dass keine schnelle diagonale Bewegung mehr stattfindet.

Tipp

Wenn Sie noch nie zuvor Vektor-Mathematik verwendet haben oder eine Auffrischung benötigen, finden Sie eine Erklärung zur Vektorverwendung in Godot unter Vektor-Mathematik. Es ist gut zu wissen, wird aber für den Rest dieser Anleitung nicht notwendig sein.

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

Tipp

$ 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.

Jetzt, da wir eine Bewegungsrichtung haben, können wir die Position des Spielers aktualisieren und mit clamp() verhindern, dass er den Bildschirm verlässt. clamp() beschränkt dabei einen Wert auf einen angegebenen Bereich. Am Ende der _process Funktion fügen wir also folgendes hinzufügen:

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

Tipp

Der Parameter delta in der Funktion `__process ()`bezieht sich auf die Frame-Länge - die Zeit, die der vorherige Frame für die Fertigstellung benötigt hat. Durch die Verwendung dieses Werts wird sichergestellt, dass Ihre Bewegung auch dann konstant bleibt, wenn sich die Bildrate ändert.

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

Warnung

Wenn ein Fehler im "Debugger" Bereich auftaucht, der sagt

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.

Animationen

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

Bemerkung

Die booleschen Zuweisungen im obigen Code sind eine gebräuchliche Abkürzung für Programmierer. Da wir einen Vergleichstest (boolescher Wert) durchführen und auch einen booleschen Wert zuweisen, können wir beide gleichzeitig durchführen. Betrachten Sie diesen Code im Vergleich zur obigen einzeiligen booleschen Zuweisung:

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

Spielen Sie die Szene erneut ab und überprüfen Sie, ob die Animationen in jeder Richtung korrekt sind.

Tipp

Ein allgemeiner Fehler ist hier, die Namen der Animationen falsch zu benennen. Die Animationsnamen in dem SpriteFrames Bereich müssen mit dem, was im Code geschrieben steht, übereinstimmen. Wenn die Animation "Walk" heißt, dann muss auch ein großgeschriebenes "W" im Code stehen.

Spielen Sie die Szene noch einmal ab und überprüfen, ob die Animationen in jede Richtung korrekt sind. Wenn Sie sich sicher sind, dass die Bewegung korrekt funktioniert, fügen Sie diese Zeile zu _ready() hinzu, damit der Spieler zu Beginn des Spiels ausgeblendet wird:

hide()

Vorbereitung auf Kollisionen

Wir wollen, dass der Player erkennt, wann er von einem Feind getroffen wird, aber wir haben uns noch keine Feinde erstellt! Das ist in Ordnung, denn wir werden Godots Signal-Funktionalität nutzen, damit es funktioniert.

Fügen Sie folgendes oben im Skript nach extends Area2D hinzu:

signal hit

Dies definiert ein benutzerdefiniertes Signal namens "hit", das wir von unserem Spieler aussenden (send out) lassen, wenn es mit einem Gegner kollidiert. Wir werden Area2D verwenden, um die Kollision zu erkennen. Wählen Sie den Node Player und klicken auf die Registerkarte "Node" neben der Registerkarte Inspektor, um die Liste der Signale zu sehen, die der Spieler ausgeben kann:

../../_images/player_signals.png

Beachten Sie, dass unser benutzerdefiniertes "hit"-Signal auch da ist! Da unsere Feinde RigidBody2D Nodes sein werden, brauchen wir das Signal body_entered(body: Node); dieses wird ausgesendet, wenn ein Körper den Spieler berührt. Klicken Sie auf "Verbinden..." und dann, im "Signal verbinden"-Fenster, wieder auf "Verbinden". Wir müssen keine dieser Einstellungen ändern - Godot erstellt automatisch eine Funktion im Skript des Spielers. Diese Funktion wird immer dann aufgerufen, wenn das Signal ausgelöst wird - sie handhabt das Signal.

../../_images/player_signal_connection.png

Beachten Sie das grüne Symbol, was anzeigt, dass das Signal mit dieser Funktion verbunden ist. Fügen Sie diesen Code zu der Funktion hinzu:

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)

Jedes Mal, wenn ein Feind den Spieler trifft, wird das Signal ausgesendet. Wir müssen die Kollision des Spielers deaktivieren, damit das Treffer-Signal nicht mehr als einmal ausgelöst wird.

Bemerkung

Das Deaktivieren der Kollisionsform während der Kollisionsberechnung der Engine kann einen Fehler auslösen. Die Verwendung von set_deferred() weist Godot an, mit dem Deaktivieren der Form bis zu einem sicheren Zeitpunkt zu warten.

Das letzte was wir tun müssen ist eine Funktion hinzuzufügen, die wir aufrufen können, um den Spieler beim erneuten Start des Spiels zurückzusetzen.

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

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