Il tuo primo gioco

Panoramica

Questo tutorial ti guiderà nella creazione del tuo primo progetto in Godot. Imparerai come funziona l’editor di Godot, come strutturare un progetto e come sviluppare un gioco 2D.

Nota

Questo progetto è un’introduzione a Godot Engine. Si suppone che tu abbia già una minima esperienza di programmazione, altrimenti ti consigliamo di iniziare da qui: Scripting.

Il gioco si chiama «Dodge the Creeps!». Il tuo personaggio deve muoversi ed evitare i nemici per il maggior tempo possibile. Questa è una dimostrazione del risultato finale:

../../_images/dodge_preview.gif

Why 2D? 3D games are much more complex than 2D ones. You should stick to 2D until you have a good understanding of the game development process and how to use Godot.

Impostare il progetto

Launch Godot and create a new project. Then, download dodge_assets.zip. This contains the images and sounds you’ll be using to make the game. Unzip these files in your project folder.

Nota

For this tutorial, we will assume you are familiar with the Godot editor. If you haven’t read Scene e nodi, do so now for an explanation of setting up a project and using the editor.

This game is designed for portrait mode, so we need to adjust the size of the game window. Click on Project -> Project Settings -> Display -> Window and set «Width» to 480 and «Height» to 720.

Sempre in questa sezione, sotto le opzioni «Stretch», impostare Mode a «2d» e Aspect a «keep». Questo assicura che il gioco sia scalabile in modo coerente su schermi di dimensioni diverse.

Organizzare il progetto

In this project, we will make 3 independent scenes: Player, Mob, and HUD, which we will combine into the game’s Main scene. In a larger project, it might be useful to create folders to hold the various scenes and their scripts, but for this relatively small game, you can save your scenes and scripts in the project’s root folder, identified by res://. You can see your project folders in the FileSystem Dock in the lower left corner:

../../_images/filesystem_dock.png

Scena del Player

La prima scena che faremo andrà a definire l’oggetto Player. Uno dei benefici di creare una scena separata per l’oggetto Player è che potremo testarla separatamente, anche prima di aver creato le altre parti del gioco.

Struttura del nodo

Per iniziare, dobbiamo scegliere un nodo radice per l’oggetto giocatore. Come regola generale, il nodo radice di una scena dovrebbe riflettere la funzionalità desiderata dell’oggetto - ciò che l’oggetto è. Fare clic sul pulsante «Altro nodo» e aggiungere un nodo Area2D <class_Area2D>` alla scena.

../../_images/add_node.png

Godot mostrerà un’icona di avvertimento accanto al nodo nell’albero della scena. Per ora lo puoi ignorare. Ce ne occuperemo successivamente.

With Area2D we can detect objects that overlap or run into the player. Change the node’s name to Player by double-clicking on it. Now that we’ve set the scene’s root node, we can add additional nodes to give it more functionality.

Prima di aggiungere figli al nodo Player, vogliamo assicurarci di non muoverli o ridimensionarli accidentalmente cliccandoci sopra. Seleziona il nodo e fai click sull’icona a destra del lucchetto; la descrizione del comando dice «Accerta che i figli dell’oggetto non siano selezionabili.»

../../_images/lock_children.png

Save the scene. Click Scene -> Save, or press Ctrl + S on Windows/Linux or Cmd + S on macOS.

Nota

Per questo progetto seguiremo le convenzioni di denominazione di Godot.

  • GDScript: Le classi (nodi) usano PascalCase, le variabili e le funzioni snake_case e le costanti usano ALL_CAPS (Vedi GDScript style guide).
  • C#: Le classi, le variabili export e i metodi usano PascalCase, i campi privati usano _camelCase, le variabili locali e i parametri usano camelCase (Vedi C# style guide). Bisogna dare attenzione a scrivere il nome dei metodi correttamente quando si connettono i segnali.

Animazione 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». Click again to open the «SpriteFrames» panel:

../../_images/spriteframes_panel.png

On the left is a list of animations. Click the «default» one and rename it to «walk». Then click the «New Animation» button to create a second animation named «up». Find the player images in the «FileSystem» tab - they’re in the art folder you unzipped earlier. Drag the two images for each animation, named playerGrey_up[1/2] and playerGrey_walk[1/2], into the «Animation Frames» side of the panel for the corresponding animation:

../../_images/spriteframes_panel2.png

Le immagini del giocatore sono un po” troppo grandi per la finestra di gioco, quindi dobbiamo rimpicciolirle. Fai clic sul nodo AnimatedSprite e imposta la proprietà Scale a (0.5, 0.5). La si può trovare all’interno dell” Inspector nella sezione Node2D.

../../_images/player_scale.png

Infine, aggiungi un CollisionShape2D come figlio di Player. Questo determinerà la «hitbox» del giocatore, in altre parole i limiti dell’area di collisione. Per questo personaggio, un nodo CapsuleShape2D è la scelta migliore, quindi vicino a «Shape» nel Inspector, clicca «[empty]»» -> «New CapsuleShape2D». Usando le due maniglie di dimensione, ridimensiona la forma in modo da coprire la sprite:

../../_images/player_coll_shape.png

Quando hai finito, la tua scena Player dovrebbe essere simile a questa:

../../_images/player_scene_nodes.png

Assicurati di salvare di nuovo la scena dopo queste modifiche.

Muovere il giocatore

Now 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

Nella finestra di impostazioni dello script, puoi lasciare le impostazioni di default. Clicca «Crea»:

Nota

Se stai creando uno script C# o in altri linguaggi, scegli il linguaggio dal menu a tendina linguaggio prima di cliccare crea.

../../_images/attach_node_window.png

Nota

Se è la prima volta che ti imbatti in GDScript, leggi Scripting prima di continuare.

Inizia dichiarando le variabili membro che questo oggetto avrà bisogno:

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

Usando la parola chiave export sulla prima variabile speed ci permette di impostare il suo valore dall’Ispettore. Questo può essere utile per valori che vorresti poter modificare come le proprietà dei nodi. Clicca sul nodo Player e vedrai che la proprietà è ora apparsa nella sezione «Variabili di Script» dell’ispettore. Ricorda, se cambi il valore qui, si sovrascriverà al valore scritto nello script.

Avvertimento

Se stai usando C#, devi (ri)costruire l’assemblaggio del progetto ogni volta che vuoi vedere le nuove variabili esportate o i segnali. Questa costruzione può essere attivata facendo click sulla parola «Mono» in basso, nella finestra dell” editor, per rivelare il panello Mono, quindi fare click sul pulsante «Build Project».

../../_images/export_variable.png

La funzione _ready() viene chiamata quando un nodo entra nello scene tree, un momento ideale per andare a ricavare le dimensioni della finestra di gioco :

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

Ora possiamo usare la funzione _process() per definire cosa farà il player. La funzione _process() viene chiamata ad ogni frame, quindi la useremo per aggiornare gli elementi del nostro gioco che ci aspettiamo che cambino spesso. Per il player dobbiamo fare il seguente:

  • Controlla per input.
  • Muovi nella direzione assegnata.
  • Esegui l’animazione appropriata.

Prima di tutto, dobbiamo controllare per l’input - il giocatore sta premendo un pulsante? Per questo gioco abbiamo 4 direzioni di input da controllare. Le Azioni di Input sono definire nelle Impostazioni di Progetto sotto «Mappa Input». Qui puoi definire degli eventi personalizzati ed assegnargli pulsanti differenti, eventi del mouse, o altri tipi di input. Per questa demo useremo gli eventi default che sono assegnati alle frecce direzionali della tastiera.

You can detect whether a key is pressed using Input.is_action_pressed(), which returns true if it’s pressed or false if it isn’t.

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 diagonally than if it just moved horizontally.

We can prevent that if we normalize the velocity, which means we set its length to 1, then multiply by the desired speed. This means no more fast diagonal movement.

Suggerimento

Se non hai mai usato la matematica vettoriale prima d’ora, oppure hai bisogno di una rinfrescata, puoi vedere una spiegazione sull’uso dei vettori in Godot a v:ref:doc_vector_math. È buona cosa da sapere, ma non sarà necessario per il resto di questo tutorial.

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

$ è la versione breve per get_node(). Quindi nel codice sopra, $AnimatedSprite.play() è uguale a get_node("AnimatedSprite").play().

Suggerimento

In GDScript, $ ritorna il percorso relativo del nodo che ci serve dal nodo corrente, altrimenti ritorna null se il nodo non è stato trovato. Siccome AnimatedSprite è figlio del nodo corrente, possiamo usare $AnimatedSprite.

Now that we have a movement direction, we can update the player’s position. We can also use clamp() to prevent it from leaving the screen. Clamping a value means restricting it to a given range. Add the following to the bottom of the _process function (make sure it’s not indented under the else):

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

Suggerimento

Il parametro delta della funzione _process() si riferisce alla lunghezza del frame - il tempo di esecuzione del frame precedente. Usare questo valore garantisce che il movimento sia proporzionale al frame precedente, in caso di variazione del frame rate.

Click «Play Scene» (F6) and confirm you can move the player around the screen in all directions.

Avvertimento

Se si ottiene un errore nel pannello «Debugger» che dice

`` Tentativo di chiamare la funzione “play” nella base “istanza nulla” su un’istanza nulla``

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.

Scegliere le animazioni

Ora che il giocatore si può muovere, abbiamo bisogno di cambiare l’animazione eseguita da AnimatedSprite in base alla direzione. Abbiamo l’animazione «right», che deve essere specchiata orizzontalmente usando la proprietà flip_h per ottenere il movimento a sinistra, e un animazione «up», la quale deve essere capovolta verticalmente con flip_v per ottenere il movimento verso il basso. Aggiungiamo questo pezzo di codice alla fine della funzione _process():

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

Nota

The boolean assignments in the code above are a common shorthand for programmers. Since we’re doing a comparison test (boolean) and also assigning a boolean value, we can do both at the same time. Consider this code versus the one-line 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;
}

Riprodurre la scena e verificare che le animazioni siano corrette in ciascuna delle direzioni.

Suggerimento

Un errore comune è quello di sbagliare i nomi delle animazioni. I nomi delle animazioni nel pannello SpriteFrames devono corrispondere a quello che si digita nel codice. Se avete chiamato l’animazione `"Walk"`, dovete anche usare una «W» maiuscola nel codice.

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

Preparazione per le collisioni

Vogliamo che il Player rilevi quando viene colpito da un nemico, ma non abbiamo ancora creato nemici! Non c’è problema, perché useremo la funzionalità segnali di Godot per fare in modo che funzioni.

Add the following at the top of the script, after extends Area2D:

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

[Signal]
public delegate void Hit();

Questo definisce un segnale personalizzato chiamato «hit» che il giocatore emetterà quando entra a contatto con un nemico. Useremo Area2D``per rilevare la collisione. Seleziona il nodo ``Player e fai click sull’etichetta «Node», vicino a «Inspector», per vedere la lista di segnali che il giocatore può emettere:

../../_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(body: Node) signal. This signal will be emitted when a body contacts the player. Click «Connect..» and the «Connect a Signal» window appears. We don’t need to change any of these settings so click «Connect» again. Godot will automatically create a function in your player’s script.

../../_images/player_signal_connection.png

Notare l’icona verde che indica che a questa funzione è collegato un segnale. Aggiungere questo codice alla funzione:

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);
}

Ogni volta che un nemico colpirà il giocatore, il segnale verrà emesso. Bisogna disabilitare la collisione del giocatore per non innescare il segnale hit più di una volta.

Nota

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

The last piece is to add a function we can call to reset the player when starting a new game.

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

Scena del nemico

Now it’s time to make the enemies our player will have to dodge. Their behavior will not be very complex: mobs will spawn randomly at the edges of the screen, choose a random direction, and move in a straight line.

We’ll create a Mob scene, which we can then instance to create any number of independent mobs in the game.

Nota

Vedi :ref:` Istanziamento<doc_instancing>` per saperne di più sull’istanziamento.

Impostazione dei nodi

Click Scene -> New Scene and add the following nodes:

Non dimenticare di impostare i nodi figli in modo che non possano essere selezionati, come hai fatto con la scena del Giocatore.

Nelle proprietà di RigidBody2D, imposta Gravity Scale a 0 in modo che il nemico non cada verso il basso. Inoltre, sotto la sezione PhysicsBody2D, clicca sulla proprietà Mask e deseleziona la prima casella. Questo farà in modo che i nemici non collidano tra di loro.

../../_images/set_collision_mask.png

Set up the AnimatedSprite like you did for the player. This time, we have 3 animations: fly, swim, and walk. There are two images for each animation in the art folder.

Regolare «Speed (FPS)» su `3 per tutte le animazioni.

../../_images/mob_animations.gif

Impostare la proprietà `Playing` nell’Inspector su «On».

Selezioneremo una di queste animazioni a caso in modo che i nemici abbiano una certa varietà.

Come per le immagini del giocatore, anche quelle del nemico dovranno essere rimpicciolite. Imposta la proprietà Scale dell” AnimatedSprite a (0.75, 0.75).

As in the Player scene, add a CapsuleShape2D for the collision. To align the shape with the image, you’ll need to set the Rotation Degrees property to 90 (under «Transform» in the Inspector).

Salva Scena.

Script del nemico

Aggiungi uno script a Mob e le seguenti variabili membro:

extends RigidBody2D

export var min_speed = 150  # Minimum speed range.
export var max_speed = 250  # Maximum speed range.
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.

}

Quando viene generato un nemico, sceglieremo un valore casuale tra min_speed` e max_speed` per regolare la velocità con cui ogni nemico si muoverà (sarebbe noioso se si muovessero tutti alla stessa velocità).

Ora guardiamo il resto della sceneggiatura. In _ready() scegliamo casualmente uno dei tre tipi di animazione:

func _ready():
    var mob_types = $AnimatedSprite.frames.get_animation_names()
    $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()
{
    var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
    var mobTypes = animSprite.Frames.GetAnimationNames();
    animSprite.Animation = mobTypes[_random.Next(0, mobTypes.Length)];
}

Per prima cosa, otteniamo l’elenco dei nomi delle animazioni dalla proprietà frames di AnimatedSprite. Questo restituisce un Array contenente tutti e tre i nomi delle animazioni: ["walk", "swim", "fly"].

Dobbiamo quindi scegliere un numero casuale tra 0 e 2 per selezionare uno di questi nomi dalla lista (gli indici degli array iniziano con 0). randi() % n seleziona un numero intero casuale tra 0 e n-1.

Nota

You must use randomize() if you want your sequence of «random» numbers to be different every time you run the scene. We’re going to use randomize() in our Main scene, so we won’t need it here.

The last piece is to make the mobs delete themselves when they leave the screen. Connect the screen_exited() signal of the VisibilityNotifier2D node and add this code:

func _on_VisibilityNotifier2D_screen_exited():
    queue_free()
public void OnVisibilityNotifier2DScreenExited()
{
    QueueFree();
}

Questo completa la scena Mob.

Main Scene

Ora e” il momento di riunire tutto insieme. Creare una nuova scena e aggiungere un Nodo denominato Main.

../../_images/instance_scene.png

Ora, aggiungi i seguenti nodi come figli di Main, e rinominali come mostrato (i valori sono in secondi):

  • Timer (chiamato MobTimer) - per controllare quanto spesso i mob vengano generati
  • Timer (chiamato ScoreTimer) - per aumentare il punteggio ogni secondo
  • Timer (chiamato``StartTimer``) - per definire un ritardo prima di iniziare
  • Posizione 2D (chiamato``StartPosition``) - per indicare la posizione di partenza del giocatore

Impostare la proprietà Wait Time di ciascuno dei nodi Timer come segue:

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

Inoltre, imposta la proprietà «One Shot» di StartTimer` su «On» e imposta la Posizione` del nodo StartPosition` su (240, 450)`.

Generare Mobs

Il nodo principale genererà nuovi mob, e vogliamo che appaiano in una posizione casuale sul bordo dello schermo. Aggiungere un nodo :ref:Path2D <class_Path2D> chiamato MobPath` come figlio di Main`. Selezionando Path2D`, si vedranno alcuni nuovi pulsanti nella parte superiore dell’editor:

../../_images/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» is selected. This option can be found to the left of the «Lock» button, appearing as a magnet next to some intersecting lines.

../../_images/grid_snap_button.png

Importante

Disegnare il percorso in ordine orario, o i tuoi nemici verranno generati verso l’esterno invece che verso l’interno!

../../_images/draw_path2d.gif

Dopo aver posizionato il punto ““4”” nell’immagine, fare clic sul pulsante «Chiudi curva» e la curva sarà completa.

Ora che il percorso è definito, aggiungere un nodo :ref:”PathFollow2D <class_PathFollow2D>” come figlio di “”MobPath”” e denominarlo “”MobSpawnLocation”“. Questo nodo ruoterà automaticamente e seguirà il percorso mentre si muove, quindi possiamo usarlo per selezionare una posizione e una direzione casuali lungo il percorso.

Your scene should look like this:

../../_images/main_scene_nodes.png

Script principale

Aggiungi uno script a `` Main``. Nella parte superiore dello script, utilizziamo `` export (PackedScene) `` per permetterci di scegliere la scena Mob che vogliamo istanziare.

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;
    }
}

Clicca sul nodo Main` e vedrai la proprietà Mob nell’Inspector sotto «Script Variables».

È possibile assegnare il valore di questa proprietà in due modi:

  • Drag Mob.tscn from the «FileSystem» panel and drop it in the Mob property .
  • Cliccare la freccia rivolta verso il basso accanto a «[empty]» e scegliere «Load». Selezionare Mob.tscn`.

Next, click on the Player and connect the hit signal. 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 «Connect a Signal» window 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:

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(), creeremo un’istanza mob, sceglieremo una posizione di partenza casuale lungo il Path2D, e metteremo in movimento il mob. Il nodo PathFollow2D ruoterà automaticamente mentre segue il percorso, quindi lo useremo per selezionare la direzione e la posizione del mob.

Si noti che una nuova istanza deve essere aggiunta alla scena utilizzando “”add_child()”“.

func _on_MobTimer_timeout():
    # Choose a random location on Path2D.
    $MobPath/MobSpawnLocation.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.Offset = _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.LinearVelocity = new Vector2(RandRange(150f, 250f), 0).Rotated(direction);
}

Importante

Why PI? In functions requiring angles, GDScript uses radians, not degrees. 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

Proviamo la scena per assicurarci che tutto funzioni. Aggiungere questo a _ready():

func _ready():
    randomize()
    new_game()
    public override void _Ready()
    {
        NewGame();
    }
}

Assegniamo anche Main` come nostra «Scena principale» - quella che viene eseguita automaticamente al lancio del gioco. Premi il pulsante «Play» e seleziona Main.tscn` quando richiesto.

Dovresti essere in grado di muovere il giocatore, vedere i mob che vengono generati e vedere il giocatore scomparire quando viene colpito da un mob.

Quando si è sicuri che tutto funzioni, rimuovere la chiamata a new_game() da _ready().

HUD

L’ultimo pezzo di cui il nostro gioco ha bisogno è un’interfaccia utente: un’interfaccia per visualizzare cose come il punteggio, un messaggio «game over» e un pulsante di riavvio. Creare una nuova scena e aggiungere un nodo :ref:”CanvasLayer <class_CanvasLayer>” denominato “”HUD”“. «HUD» sta per «heads-up display», un display informativo che appare come una sovrapposizione sulla parte superiore della vista di gioco.

Il nodo :ref:”CanvasLayer <class_CanvasLayer>” ci consente di disegnare i nostri elementi dell’interfaccia utente su un livello sopra il resto del gioco, in modo che le informazioni visualizzate non vengano coperte da elementi di gioco come il giocatore o i mob.

The HUD needs to display the following information:

  • Punteggio, modificato da “”ScoreTimer”“.
  • Un messaggio, ad esempio «Game Over» o «Get Ready!»
  • Un pulsante «Start» per iniziare il gioco.

Il nodo di base per gli elementi dell’interfaccia utente è :ref:”Controllo <class_Control>”. Per creare l’interfaccia utente, useremo due tipi di :ref:”Controlla <class_Control>” nodi: :ref:”Label <class_Label>” e :ref:”Button <class_Button>”.

Creare quanto segue come elementi figlio del nodo “”HUD”“:

  • :ref:”Label <class_Label>” denominato “”ScoreLabel”“.
  • Label named Message.
  • :ref:”Button <class_Button>” denominato “”StartButton”“.
  • :ref:”Timer <class_Timer>” denominato “”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:

  1. In «Caratteri personalizzati», scegli «Nuovo DynamicFont»
../../_images/custom_font1.png
  1. Fare clic su «DynamicFont» aggiunto e in «Font/Font Data», scegliere «Carica» e selezionare il file «Xolonium-Regular.ttf». È inoltre necessario impostare il tipo di carattere “”Size”. Un’impostazione di ““64”” funziona bene.
../../_images/custom_font2.png

Una volta fatto questo sul ScoreLabel, è possibile fare clic sulla freccia rivolta verso il basso accanto alla proprietà DynamicFont e scegliere «Copia», quindi «Incolla» nello stesso punto degli altri due nodi Controllo.

Nota

I nodi «Ancoraggi e Margini»: «Controllo» hanno una posizione e una dimensione, ma hanno anche ancoraggi e margini. Gli ancoraggi definiscono l’origine, ovvero il punto di riferimento per gli spigoli del nodo. I margini si aggiornano automaticamente quando si sposta o si ridimensiona un nodo di controllo. Rappresentano la distanza dai bordi del nodo di controllo all’ancoraggio. Vedere :ref:”doc_design_interfaces_with_the_control_nodes” per ulteriori dettagli.

Arrange the nodes as shown below. Click the «Layout» button to set a Control node’s layout:

../../_images/ui_anchor.png

È possibile trascinare i nodi per posizionarli manualmente o, per un posizionamento più preciso, utilizzare le seguenti impostazioni:

ScoreLabel

  • Layout : «Top Wide»
  • Testo : 0
  • Allinea: «Centro»

Message

  • Layout : «HCenter Wide»
  • Text : Dodge the Creeps!
  • Allinea: «Centro»
  • Autowrap : «On»

Pulsante Start

  • Testo : Start
  • *Layout *: «Center Bottom»
  • Margine :
    • In alto: “”-200”“
    • In basso: “”-100”“

On the MessageTimer, set the Wait Time to 2 and set the One Shot property to «On».

Ora aggiungi questo script a “”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();
}

Il segnale “”start_game”” indica al nodo “”Main”” che il pulsante è stato premuto.

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

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

This function is called when we want to display a message temporarily, such as «Get Ready».

func show_game_over():
    show_message("Game Over")
    # Wait until the MessageTimer has counted down.
    yield($MessageTimer, "timeout")

    $Message.text = "Dodge the\nCreeps!"
    $Message.show()
    # Make a one-shot timer and wait for it to finish.
    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 message = GetNode<Label>("Message");
    message.Text = "Dodge the\nCreeps!";
    message.Show();

    await ToSignal(GetTree().CreateTimer(1), "timeout");
    GetNode<Button>("StartButton").Show();
}

Questa funzione viene chiamata quando il giocatore perde. Mostra «Game Over» per 2 secondi, poi ritorna alla schermata del titolo e, dopo una breve pausa, mostra il pulsante «Start».

Nota

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 add delays such as in the above code, where we want to wait some 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();
}

Questa funzione viene chiamata da “”Main”” ogni volta che il punteggio cambia.

Connect the timeout() signal of MessageTimer and the pressed() signal of StartButton and add the following code to the new functions:

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

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

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

Collegamento di HUD a Main

Now that we’re done creating the HUD scene, go back to Main. Instance the HUD scene in Main like you did the Player scene. The scene tree should look like this, so make sure you didn’t miss anything:

../../_images/completed_main_scene.png

Ora dobbiamo collegare la funzionalità “”HUD”” al nostro script “”Main”“. Ciò richiede alcune aggiunte alla scena “”Main”“:

Nella scheda Node, collegare il segnale start_game dell’HUD alla funzione new_game() del nodo principale digitando «new_game» nel «Receiver Method» nella finestra «Connect a Signal». Verificare che l’icona verde di connessione appaia ora accanto a func new_game() nello script.

In new_game(), aggiornare la visualizzazione del punteggio e mostrare il messaggio «Get Ready»:

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

In “”game_over()”” è necessario chiamare la funzione “”HUD”” corrispondente:

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

Infine, aggiungere questo valore a “”_on_ScoreTimer_timeout()”” per mantenere la visualizzazione sincronizzata con il punteggio modificato:

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

Ora sei pronto per giocare! Fare clic sul pulsante «Riproduci il progetto». Ti verrà chiesto di selezionare una scena principale, quindi scegli “”Main.tscn”.

Rimozione old creeps

If you play until «Game Over» and then start a new game right away, the creeps from the previous game may still be on the screen. It would be better if they all disappeared at the start of a new game. We just need a way to tell all the mobs to remove themselves. We can do this with the «group» feature.

Nella scena Mob, selezionare il nodo radice e cliccare sulla scheda «Nodo» accanto all’Inspector (lo stesso posto dove si trovano i segnali del nodo). Accanto a «Segnali», cliccare su «Gruppi» e si può digitare un nuovo nome per il gruppo e cliccare su «Aggiungi».

../../_images/group_tab.png

Ora tutti i mob saranno nel gruppo «mob». Possiamo quindi aggiungere la seguente riga alla funzione game_over() nella funzione Main:

get_tree().call_group("mobs", "queue_free")
GetTree().CallGroup("mobs", "queue_free");

La funzione call_group()` chiama la funzione cosi denominata su ogni nodo di un gruppo - cosi facendo diciamo ad ogni mob di cancellarsi.

Rifinitura

Ora abbiamo completato tutte le funzionalità del nostro gioco. Di seguito sono riportati alcuni passaggi rimanenti per aggiungere un po” più «succo» per migliorare l’esperienza di gioco. Sentitevi liberi di espandere il gameplay con le vostre idee.

Sfondo

The default gray background is not very appealing, so let’s change its color. One way to do this is to use a ColorRect node. Make it the first node under Main so that it will be drawn behind the other nodes. ColorRect only has one property: Color. Choose a color you like and select «Layout» -> «Full Rect» so that it covers the screen.

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

Effetti Sonori

Suono e musica possono essere il modo più efficace per aggiungere fascino all’esperienza di gioco. Nella cartella delle risorse di gioco sono presenti due file audio: «House In a Forest Loop.ogg» per la musica di sottofondo e «gameover.wav» per quando il giocatore perde.

Aggiungere due nodi AudioStreamPlayer <class_AudioStreamPlayer>``come figli di ``Main`. Chiamane uno Music e l’altro DeathSound. Per ognuno di essi, clicca sulla proprietà Stream, seleziona «Load» e scegli il file audio corrispondente.

Per riprodurre la musica, aggiungete $Music.play() nella funzione new_game() e $Music.stop() nella funzione game_over().

Dopodichè , aggiungi “”$DeathSound.play()”” nella funzione “”game_over()”“.

Keyboard shortcut

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. We can do this with 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 «InputEventAction». 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 Space to start the game.

Files del progetto

È possibile trovare una versione completa di questo progetto in queste posizioni: