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

Perchè 2D? I giochi in 3D sono molto più complessi di quelli 2D. Ti consigliamo di continuare a creare giochi 2D fino a quando non avrai una buona comprensione del processo di sviluppo di un videogioco.

Impostare il progetto

Avvia Godot e crea un nuovo progetto. Poi scarica le immagini e i suoni che verranno usati nella creazione del gioco dodge_assets.zip. Estrai i file nella cartella del progetto appena creato.

Nota

Per questo tutorial si da per scontato che tu abbia già familiarità con l’editor. Se non hai ancora letto Scene e nodi, ti consigliamo di farlo subito per ricevere la spiegazione su come preparare un progetto ed utilizzare l’editor.

Questo gioco utilizzarà la modalità portrait, quindi abbiamo bisogno di modificare la dimensione della finestra di gioco. Clicca su Progetto -> Impostazioni del Progetto -> Display -> Finestra ed imposta «Larghezza» su 480 e «Altezza» su 720.

Organizzare il progetto

In questo progetto faremo 3 scene indipendenti: Player, Mob, e HUD, che combineremo nella scena Main del nostro gioco. In un progetto più grande, potrebbe essere utile creare delle cartelle per contenere le varie scene e i loro script, ma per questo piccolo gioco puoi salvare le tue scene e script nella cartella principale del progetto, riferita come res://. Puoi vedere la tua cartella del progetto nella FileSystem Dock nell’angolo in basso a sinistra:

../../_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, fai click sul tasto «Aggiungi/Crea un Nuovo Nodo» e aggiungi un nodo 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.

Con il nodo Area2D possiamo rilevare gli oggetti che si sovrappongono al giocatore. Rinomina il nodo in Player cliccandoci sopra. Questo è il nodo principale della scena. Possiamo aggiungere ulteriori nodi al giocatore per aggiungere altre funzionalità.

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

Clicca su il nodo Player e aggiungi un nodo figlio AnimatedSprite. Il nodo AnimatedSprite si occuperà dell’aspetto e delle animazioni del nostro giocatore. Noterete che ci sarà un segnale di allarme vicino al nodo. Un AnimatedSprite richiede una risorsa SpriteFrames, la quale è una lista di animazioni che possono essere esposte. Per crearne una, cercate la proprietà Frames nell’Inspector e cliccate «[vuoto]» -> «Nuovo SpriteFrames». Dopodichè, sulla stessa proprietà, clicca <SpriteFrames> e poi su «Apri Editor» per aprire il pannello «SpriteFrames».

../../_images/spriteframes_panel.png

Sulla sinistra c’è una lista di animazioni. Clicca su quella con scritto «default» e rinominala in «right». Ora clicca sul tasto «Aggiungi» per creare una seconda animazione chiamata «up». Trascina le due immagini per ogni animazione, chiamate playerGrey_up[1/2] e playerGrey_walk[1/2], all’interno della sezione «Frames animazione» del pannello:

../../_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

Muovere il giocatore

Ora abbiamo bisogno di aggiungere alcune funzionalità che non abbiamo con un nodo pre-esistente, quindi aggiungeremo uno script. Clicca il nodo Player e clicca il bottone «Add Script»:

../../_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.

Puoi rilevare quando il giocatore sta premendo un pulsante usando Input.is_action_pressed(), che ritorna true se è premuto o false se non lo è.

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

Iniziamo impostando la velocity a (0, 0) - di default il giocatore non dovrebbe muoversi. Poi controlliamo ogni input e aggiungiamo/sottraiamo da velocity per ottenere una direzione globale. Per esempio, se tieni premuto right e down allo stesso tempo, il vettore velocity che otterremo sarà (1, 1). In questo caso, dato che stiamo sommando un movimento orizzontale e uno verticale, il giocatore si muoverà più velocemente rispetto al solo movimento orizzontale.

Possiamo prevenire ciò normalizzando la velocità, che significa impostare la sua lunghezza a 1, dopodiché possiamo moltiplicare il vettore per la velocità desiderata. In questo modo possiamo evitare i movimenti diagonali a velocità maggiore.

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.

Possiamo anche controllare quando il giocatore si sta muovendo così da far partire o fermare l’animazione del nodo AnimatedSprite.

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.

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

Ora che abbiamo una direzione di movimento, possiamo aggiornare la posizione del player. Possiamo inoltre usare clamp() per impedirgli di uscire dallo schermo. Aggiungendo il seguente codice sul fondo della funzione _process():

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.

Clicca «Play Scene» (F6) e conferma che riesci a muovere il giocatore sullo schermo in tutte le direzioni. La console di output che si aprirà all’avvio della scena può essere chiusa facendo click su Output (il quale dovrebbe essere evidenziato in blu) in basso a sinistra del pannello inferiore.

Avvertimento

Se nel pannello «Debugger» ricevi un errore che fa riferimento a una «null instance» significa che probabilmente hai sbagliato a scrivere il nome del nodo. I nomi dei nodi sono case-sensitive (distinguono tra maiuscole e minuscole) $NodeName e get_node("NodeName") devono corrispondere esattamente al nome che vedi nell’albero di scena.

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 = "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";
    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

L’assegnazione di booleane nel codice qui sopra è un’abbreviazione comune usata dai programmatori. Paragona il codice precedente con il seguente:

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

Avvia nuovamente la scena e per ogni direzione controlla che le animazioni siano corrette. Terminato il controllo aggiungi la seguente linea di codice a _ready(), così che il giocatore sia nascosto all’avvio del gioco:

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.

Aggiungere quanto di seguito in cima allo script, dopo 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

Come puoi notare la lista contiene anche il nostro segnale «hit». Poiché i nemici saranno nodi RigidBody2D, vogliamo usare il segnale body_entered( Object body ); questo segnale viene emesso quando un corpo entra a contatto con il giocatore. Clicca «Connect..» e poi di nuovo «Connect» nella finestra «Connecting Signal». Non abbiamo bisogno di cambiare nessuna delle impostazioni - Godot creerà automaticamente una funzione nello script del giocatore. Questa funzione sarà invocata ogni volta che il segnale viene emesso - gestisce il segnale.

Suggerimento

Quando connetti un segnale, invece che Godot crei una funzione per te, puoi assegnare il nome di una funzione già esistente che vuoi connettere al segnale.

Aggiungi 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

Disabilitare la forma di collisione dell’area può causare un errore se si verifica nel mezzo dell’elaborazione di collisione del motore. Usando `set_deferred() ci permette di avere Godot mettere in pausa per disabilitare la forma fino a quando non è sicuro di farlo.

L’ultimo pezzo da aggiungere al nostro giocatore è una funzione che possiamo chiamare per resettarlo quando si comincia una nuova partita.

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

Ora è il momento di rendere i nemici che il nostro giocatore dovrà schivare. Il loro comportamento non sarà molto complesso: i mob si riprodurranno in modo casuale ai bordi dello schermo e si muoveranno in una direzione casuale in linea retta, per poi sparire quando escono dallo schermo.

Costruiremo una scena Mob, che possiamo poi instanziare per creare un qualsiasi numero di mob nel gioco.

Impostazione dei nodi

Vai su Scena -> Nuova Scena e creeremo i Mob.

La scena del nemico userà i seguenti nodi:

  • RigidBody2D (chiamato Mob)
    • AnimatedSprite
    • CollisionShape2D
    • VisibilityNotifier2D (chiamato Visibility)

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

Imposta AnimatedSprite come hai fatto per il giocatore. Questa volta abbiamo 3 animazioni: fly, swim e walk. Imposta la proprietà Playing nell’Inspector su «On» e regola la proprietà «Speed (FPS)» come mostrato sotto. Selezioneremo casualmente una di queste animazione in modo che i nemici abbiamo un po” di varietà.

../../_images/mob_animations.gif

fly andrebbe impostato a 3 FPS, mentre swim e walk a 4 FPS.

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

Come nella scena Player, aggiungi un CapsuleShape2D per la collisione. Per allineare la forma con l’immagine, è necessario impostare la proprietà Rotation Degrees su 90 sotto Node2D.

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

Quando viene generato un mob, sceglieremo un valore casuale tra min_speed` e max_speed` per quanto velocemente ogni mob si muoverà (sarebbe noioso se si muovessero tutti alla stessa velocità). Inoltre, abbiamo anche un array contenente i nomi delle tre animazioni, che useremo per selezionarne una casuale. Assicurati di averle scritte nello script e nella risorsa SpriteFrames.

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

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

Nota

È necessario utilizzare randomize() se si desidera che la sequenza di numeri «casuali» sia diversa ogni volta che si esegue la scena. Useremo randomize() nella nostra scena Main, quindi non ne avremo bisogno qui. randi() % n è il modo standard per ottenere un numero intero casuale tra 0 e n-1.

L’ultimo pezzo è quello di fare in modo che i mob spariscono quando lasciano lo schermo. Collegare il segnale screen_exited() del nodo ``Visibilità` e aggiungere questo codice:

func _on_Visibility_screen_exited():
    queue_free()
public void OnVisibilityScreenExited()
{
    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

Nota

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

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/draw_path2d.gif

Importante

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

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.

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

Trascinare “”Mob.tscn”” dal pannello «FileSystem» e rilasciarlo nella proprietà “”Mob”” sotto le variabili di script del nodo “”Main”“.

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 «Connecting Signal» window. Add the following code, as well as a new_game function to 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();
}

Collegare ora il segnale “”timeout()”” di ciascuno dei nodi Timer (“”StartTimer”“, “”ScoreTimer”” e “”MobTimer”“) allo script principale. “”StartTimer”” avvierà gli altri due timer. “”ScoreTimer”” incrementerà il punteggio di 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.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));
}

Importante

Nelle funzioni che richiedono angoli, GDScript utilizza i radianti, non i gradi. Se si ha più familiarità con i gradi, è necessario utilizzare le funzioni “”deg2rad()”” e “”rad2deg()”” per eseguire la conversione tra i due.

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.

L’HUD visualizza le seguenti informazioni:

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

Fare clic su “”ScoreLabel”” e digitare un numero nel campo «Text» nella finestra di ispezione. Il tipo di carattere predefinito per i nodi “”Controllo”” è piccolo e non è scalabile bene. C’è un file di font incluso nelle risorse di gioco chiamato «Xolonium-Regular.ttf». Per utilizzare questo tipo di carattere, eseguire le operazioni seguenti per ognuno dei tre nodi “”Control”“:

  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

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

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

MessageLabel (Etichetta Messaggio)

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

Pulsante Start

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

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):
    $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();
}

Questa funzione viene chiamata quando si desidera visualizzare temporaneamente un messaggio, ad esempio «Get Ready». Sul MessageTimer, impostare il Tempo di attesa su 2 e impostare la proprietà One Shot su «On».

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

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

Quando è necessario sospendere per un breve periodo di tempo, un’alternativa all’utilizzo di un nodo Timer consiste nell’utilizzare la funzione “”create_timer()”” di SceneTree. Questo può essere molto utile per ritardare, come nel codice sopra, dove vogliamo aspettare un po “di tempo prima di mostrare il pulsante «Start».

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.

Collegare il segnale “”timeout()”” di “”MessageTimer”” e il segnale “”pressed()”” di “”“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();
}

Collegamento di HUD a Main

Ora che abbiamo finito di creare la scena “”HUD”“, salvarla e tornare a “”Main”“. Istanza della scena “”HUD”” in “”Main”” come hai fatto la scena “”Player”“, e posizionarlo nella parte inferiore dell’albero. L’albero completo dovrebbe essere simile a questo, quindi assicurati di non perdere nulla:

../../_images/completed_main_scene.png

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

In the Node tab, connect the HUD’s start_game signal to the new_game() function of the Main node.

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

Se si gioca fino a «Game Over» e poi si inizia una nuova partita, i creeps del gioco precedente sono ancora sullo schermo. Sarebbe meglio se scomparissero tutti all’inizio di una nuova partita.

Useremo il segnale “”start_game”” che è già stato emesso dal nodo “”HUD”” per rimuovere i creep rimanenti. Non possiamo usare l’editor per collegare il segnale ai mob nel modo di cui abbiamo bisogno, perché non ci sono nodi “”Mob”” nell’albero della scena “”Main”” fino a quando non eseguiamo il gioco. Invece useremo il codice.

Iniziare aggiungendo una nuova funzione a “”Mob.gd”“. “”queue_free()”” eliminerà il nodo corrente alla fine del frame corrente.

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

Quindi in “”Main.gd”” aggiungere una nuova riga all’interno della funzione “”_on_MobTimer_timeout()”“, alla fine.

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

Questa linea dice al nuovo nodo Mob (a cui fa riferimento la variabile mob) di rispondere a qualsiasi segnale start_game emesso dal nodo HUD eseguendo la sua funzione _on_start_game().

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

Lo sfondo grigio di default non è un gran che, quindi cambiamo il colore. Un modo per farlo è usando il nodo ColorRect. Mettendo il nodo come primo figlio di Main questo verrà disegnato dietro gli altri nodi. ColorRect``ha una sola proprietà: ``Color. Scegli un colore che ti piace e aumenta la grandezza di ColorRect in modo da coprire lo schermo.

Puoi anche aggiungere un immagine come sfondo, se ne hai una, usando un nodo ti tipo Sprite.

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()”“.

Scorciatoie da tastiera

Dato che nel gioco si utilizzano i comandi della tastiera, sarebbe anche comodo iniziare il gioco premendo un tasto della tastiera. Un modo per farlo è usare la proprietà «Shortcut» del nodo Button.

Nella scena “”HUD”“, seleziona “”StartButton”” e cerca la proprietà “Scorciatoie” nell’Inspector. Seleziona quindi «Nuova scorciatoia» e clicca sul «Scelta rapida». Verrà visualizzata la relativa proprietà . Seleziona «Nuovo InputEventAction» e clicca sul nuovo «InputEvent». Infine, nella proprietà «Action», digita il nome “”ui_select”“.Questo è l’InputEvent predefinito predefinito associato alla barra spaziatrice.

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