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. Dovresti continuare a creare giochi 2D fino a quando non avrai una buona comprensione del processo di sviluppo di un videogioco e di come usare Godot.

Impostare il progetto

Avvia Godot e crea un nuovo progetto. Poi scarica dodge_assets.zip. Contiene le immagini e i suoni che userai per fare il gioco. 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 di Godot. Se non hai ancora letto Scene e nodi, fallo ora per ricevere la spiegazione su come preparare un progetto ed utilizzare l'editor.

Questo gioco è progettato per la modalità portrait, quindi abbiamo bisogno di modificare la dimensione della finestra di gioco. Clicca su Progetto -> Impostazioni Progetto -> Display -> Finestra ed imposta "Larghezza" su 480 e "Altezza" su 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 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, identificata 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, 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.

Con il nodo Area2D possiamo rilevare gli oggetti che si sovrappongono o scontrano con il giocatore. Rinomina il nodo in Player facendo doppio click sopra di esso. Ora che abbiamo impostato la scena principale, 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

Salva la scena. Clicca su Scena -> Salva, oppure premi Ctrl + S su Windows/Linux o Cmd + S su Mac.

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 AnimatedSprite come figlio. Il nodo AnimatedSprite si occuperà dell'aspetto e delle animazioni del nostro giocatore. Nota che ci sarà un segnale di allarme vicino al nodo. Un AnimatedSprite richiede una risorsa SpriteFrames, la quale è una lista di animazioni che può mostrare. Per crearne una, cerca la proprietà Frames nell'Inspector e clicca "[vuoto]" -> "Nuovo SpriteFrames". Clicca ancora 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 "walk". Ora clicca sul tasto "Aggiungi" per creare una seconda animazione chiamata "up". Trova le immagini del giocatore nella scheda "FileSystem" - si trovano nella cartella `` art`` che hai decompresso in precedenza. 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 per l'animazione corrispondente:

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

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 "Aggiungi 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 definite 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 un pulsante è premuto 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 destra e giù 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 in diagonale 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 se il giocatore si sta muovendo così possiamo chiamare play() o stop() sul nodo 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.

Ora che abbiamo una direzione di movimento, possiamo aggiornare la posizione del player. Possiamo inoltre usare clamp() per prevenire che il player esca dallo schermo. Fare il clamping di un valore significa restringerlo ad un certo intervallo. Aggiungi il seguente codice sul fondo della funzione _process() (assicurati che non sia indentato sotto 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.

Clicca su "Avvia Scena" (F6) e verifica di poter muovere il giocatore sullo schermo in tutte le direzioni.

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

questo probabilmente significa che hai scritto il nome del nodo AnimatedSprite in modo sbagliato. I nomi dei nodi sono case-sensitive e $NomeNodo deve corrispondere al nome che vedi nello 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

Le assegnazioni booleane nel codice qui sopra sono comuni abbreviazioni per i programmatori. Siccome stiamo facendo una comparazione (booleano) a anche assegnando un valore booleano, possiamo fare entrambi allo stesso momento. Considera questo codice contro l'assegnamento booleano in un'unica riga mostrato sopra:

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.

Quando sei sicuro che il movimento stia funzionando correttamente, aggiungi questa linea a _ready(), così che il player sarà nascosto quando il gioco inizia:

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.

Aggiungi 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 il nodo 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 personalizzato "hit". Poiché i nemici saranno nodi RigidBody2D, vogliamo usare il segnale body_entered(body: Node). Questo segnale viene emesso quando un corpo entra a contatto con il giocatore. Clicca "Connetti.." e appare la finestra "Connetti un Segnale". Non abbiamo bisogno di cambiare nessuna delle impostazioni quindi clicca "Connetti" nuovamente. Godot creerà automaticamente una funzione nello script del tuo player.

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

Disabilitare la forma di collisione dell'area può causare un errore se si usa mentre il motore sta elaborando le collisioni. Usando `set_deferred() si dice a Godot di aspettare a disabilitare la forma di collisione, fino a quando non è sicuro farlo.

L'ultimo pezzo è aggiungere una funzione che chiamiamo per resettare il player quando inizia 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 creare i nemici che il nostro giocatore dovrà schivare. Il loro comportamento non sarà molto complesso: i mob si generano in modo casuale ai bordi dello schermo, scelgono una direzione randomica e si muovono in linea retta.

Creeremo una scena Mob, che possiamo poi istanziare per creare un qualsiasi numero di mob indipendenti nel gioco.

Nota

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

Impostazione dei nodi

Clicca Scena -> Nuova Scena e aggiungi i seguenti nodi:

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 :ref: AnimatedSprite <class_AnimatedSprite> come hai fatto per il giocatore. Questa volta abbiamo 3 animazioni: `` fly``, `` swim`` e `` walk``. Ci sono due immagini per ogni animazione nella cartella grafica.

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

../../_images/mob_animations.gif

Imposta 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).

Come nella scena Player, aggiungi un CapsuleShape2D per la collisione. Per allineare la forma con l'immagine, dovrai impostare la proprietà Gradi di rotazione su 90 (sotto "Trasforma" nell'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

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

L'ultimo pezzo è quello di fare in modo che i mob si cancellino loro stessi quando escono dallo schermo. Collegare il segnale screen_exited() del nodo ``VisibilityNotifier2D` e aggiungere questo codice:

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. Aggiungi un nodo Path2D chiamato MobPath` come figlio di Main`. Quando selezioni Path2D`, vedrai 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" and "Use Snap" are both selected. These options can be found to the left of the "Lock" button, appearing as a magnet next to some dots and intersecting lines, respectively.

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

La tua scena dovrebbe essere così:

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

  • Trascinare ''Mob.tscn'' dal pannello "FileSystem" e rilasciarlo nella proprietà ''Mob''.
  • Cliccare la freccia rivolta verso il basso accanto a "[empty]" e scegliere "Load". Selezionare Mob.tscn`.

Next, select the Player node in the Scene dock, and access the Node dock on the sidebar. Make sure to have the Signals tab selected in the Node dock.

You should see a list of the signals for the Player node. Find and double-click the hit signal in the list (or right-click it and select "Connect..."). This will open the signal connection dialog. 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 signal connection dialog 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();
}

Collega 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.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 += GD.RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
    mobInstance.Rotation = direction;

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

Importante

Perché PI? 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.

Testare la scena

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: qualcosa per visualizzare 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''. Esso 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 deve mostrare 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''.
  • Label denominato ''Message''.
  • :ref:'Button <class_Button>' denominato ''StartButton''.
  • :ref:'Timer <class_Timer>' denominato ''MessageTimer''.

Fare clic su ScoreLabel e digitare un numero nel campo Testo nell'Inspector. Il tipo di font predefinito per i nodi Control è piccolo e non scala bene. C'è un file di font incluso nelle risorse di gioco chiamato "Xolonium-Regular.ttf". Per utilizzare questo font, eseguire le operazioni seguenti:

  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"

Messaggio

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

StartButton

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

Questa funzione è chiamata quando si desidera visualizzare temporaneamente un messaggio, ad esempio "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.

Collega il segnale timeout() di MessageTimer e il segnale pressed() di StartButton e aggiungi il seguente codice alle nuove funzioni:

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: