Creare una life bar (barra della vita) su Godot Engine “procedurale”



Buy Me a Coffee at ko-fi.com

Sto lavorando al mio gioco e ho avuto diversi spunti su tutorial che potrebbero essere utili per chi sta affrontando lo stesso viaggio.

In questo post cercherò di guidarvi nella creazione di una barra della vita “procedurale” (termine che potrebbe risultare un po’ improprio ma che vi da l’idea della dinamicità)

Godot offre già dei nodi che si possono usare per questo scopo, si chiamano progress bar. Il problema arriva nel momento in cui volete usare delle tacche invece di una barra classica.

Nel mio caso ho una barra della vita in pixel art, ogni tacca corrisponde praticamente a una vita, e la vita massima del giocatore può aumentare asseconda se si trovano nuove vite o se si acquistano dei potenziamenti. 

Questa è la progression bar del mio gioco.

Come ho approcciato la sua creazione ? Ecco qua.

La prima cosa è che dobbiamo avere ciascuna parte che costituisce la barra ben separata.

Avremo quindi:

Un segmento dell’inizio della barra 

14x11 px

Un segmento centrale (questo segmento si ripeterà)

10x11 px

Un segmento finale che conclude la barra

14x11 px

Un segmento che corrisponde alla vita disponibile, costituirà la tacca di vita 

11x5 px


Questa sarà un operazione chirurgica, quindi è fondamentale che le dimensioni in larghezza le conosciate benissimo, perché è con quello che lavoreremo, faremo un po’ di matematica.

Set-Up

Per prima cosa lavoreremo con il background della barra di vita. 
Creiamo una nuova scena. 
Come root node possiamo scegliere o un Sprite2D o un Node2D (tendenzialmente potete usare un qualsiasi nodo che possa fare da contenitore)
Io ho usato un Node2D.

Bisogna aggiungere i tre pezzi del background. 
Il primo pezzo è l’inizio. 
Il secondo pezzo è la parte centrale, questa parte sarà quella che si estenderà e si accorcerà a seconda del bisogno
Il terzo pezzo è la parte finale.

Aggiungete anche il segmento che costituisce la vita.

Noterete che ciascun elemento è sfuocato, se vogliamo che tutto si veda bene e che la nostra pixel art ritorni ben chiara dobbiamo cambiare il filtro applicato alla nostra texture. 
Selezionate i 3 Sprite2D, andate nella sezione Texture e sotto filtro scegliete Nearest .


Sul pezzo centrale, nella stessa sezione troverete anche la voce repeat  qui scegliete Enable. Questo permetterà alla texture di ripetersi quando si aumenta la dimensione della regione.

Attivate quindi Region sul Sprite centrale. Una volta attivata non vedrete piu niente, cio è dovutto al fatto che dovete impostare le dimensioni del vostro Sprite. 




Potete sia usare il pulsante Edit Region oppure inserirli manualmente  questi valori. Io li conosco bene questi valori, nel caso non li sapete potete trovarli o su windows oppure ancora piu semplicemente cliccando sulla voce Texture. Verra visualizzata la texture con le relative informazioni.


Procedo quindi con inserire su w (width) il valore della larghezza 10 px, su h (height) il valore dell'altezza 11 px. E il nostro sprite sara di nuovo visibile.



Ripetete la stessa procedura anche per anche per il segmento della vita. Anche esso ha si ripetera e ha bisogno quindi della Region attiva.

🎯 Alcuni accorgimenti:

- Il primo sprite dovete fare il modo che coincida con l'origine, che inizi proprio dove c'è l'origine della scena.

Questo pezzo di texture non si modificherà nel tempo ma è importante nei calcoli matematici che faremo per il posizionamento degli altri pezzi.

- La parte finale della texture così com'è non funzionerebbe bene, bisogna cambiarli l'offset. Bisogna fare in modo che aggiungiamo 7 pixel, nel mio caso, sull'asse delle X. Questo valore è la metà della texture.


Dovreste ritrovarvi con una situazione come questa.



Il setup è pronto. È il momento di scrivere un po’ di codice.


Il Codice

Aggiungete al Node2D uno script vuoto.

Useremo diverse keyword che ci daranno delle funzionalità molto utili. In particolar modo useremo :
@tool

Questa keyword è una delle keyword più potenti di Godot Engine. Permette infatti di eseguire del codice direttamente nell’editor. Ci da la possibilità di creare dei piccoli tool personali che eseguono il codice direttamente nell’editor, dandoci la possibilità di eseguire modifiche agli nodi e gestire variabili e cosi via. Attenzione usare @tool può portare a crash dell’engine se il codice presentasse dei bug.

Inseriamo quindi questa linea di codice, basta scrivere @tool prima di tutto lo script.
Perché il codice venga eseguito nell’editor bisogna fare un restart del engine. Dopo il riavvio il “tool” sarà operativo. 

La prossima keyword che useremo sarà :
@onready

Questa keyword viene usata con la dichiarazione delle variabili, in particolar modo ci permetterà di far riferimento ai nostri nodi figli. @onready permette alla variabile di assegnare il valore appena il nodo entra a far parte (inizializzato) della scene tree. Viene eseguita prima della func _ready() .

Creiamo 4 variabili, ciascuna di queste avra un riferimento agli singoli elemento che costituiscono il nostro background e il segmento della vita.

@tool
extends Node2D

@onready var inizio = $LifeParts1
@onready var centro = $LifeParts2
@onready var fine = $LifeParts3
@onready var vita = $"1Bar2"
    
Come noterete ciascuna variabile nel codice di soppra inizia con la keyword @onready, poi il resto della variabile è espressa come normalmente fareste. A ciascuna viariabile poi abbiamo assegnato il valore di ciascuna parte della barra della vita (anche se la parte iniziale del background non sara mai tocata io ho decisso di averla perche in futturo non si sa mai per eventuali ed ulteriori modifiche)

🎯 Alcuni accorgimenti:

- Si usa "$" per potter ricchiamare i child (figli) di un nodo. Potremo cosi, in questo caso chiamarli con nomi piu famigliari semplicemente usando le variabili.

Si puo accedere alle proprieta di ciascun nodo tramite il punto "."
es. $nomeNodeFiglio.proprietaDaModificare --> esempio concreto inizio.position.x = 10

Per poter modificare l'aspetto della barra della vita direttamente dall'editor ci servirano delle variabili che possano essere modificate direttamente sull'inspector.
Per fare cio dobbiamo usare la keyword @export davvanti alle variabili.

Ci servono due variabili una per la vita massima che il giocatore puo avere e una variabbile per la vita del giocatore (questa sarebbe la vita che il giocatore ha durante il gioco)



@tool
extends Node2D

@onready var inizio = $LifeParts1
@onready var centro = $LifeParts2
@onready var fine = $LifeParts3
@onready var vita = $"1Bar2"

@export var vitaMassima = 3
@export var valoreVita = 3
    
Quello che ne risultera è che avremo due variabili disponibili nell'editor , le quali hanno ciascuna assegnato il valore di 3 . Voi potete cambiare questo valore come megli vi agratta e potrette modificarlo anche in gioco

E il momento di programmare il core.

Creiamo una func _process(delta: float) -> void:  pass . Basta scrivere le prime lettere e il suggerimento vi dara l'opzione di crearla.
Questa funzione viene eseguita ad ogni frame del gioco. E' questa funzione che fa accadere la magia.

Dobbiamo scrivere codice per tutti i casi che si possano presentare. Useremo quindi delle semplici if .
🎯 Alcuni accorgimenti:

E' sempre bene dividere il codice che viene eseguito nell'editor da quello che verra eseguito in gioco, questo per evitare dei errori su alcune funzioni.
Per fare cio dobbiamo usare una condizione if Engine.is_editor_hint(): codice da eseguire . Questa condizione verifica se ci troviamo nell'editor o meno, se viene soddisfatta eseguira il codice contenuto dopo la if.
Il codice fino ad adesso dovrebbe risultare cosi 


@tool
extends Node2D

@onready var inizio = $LifeParts1
@onready var centro = $LifeParts2
@onready var fine = $LifeParts3
@onready var vita = $"1Bar2"

@export var vitaMassima = 3
@export var valoreVita = 3

func _process(delta: float) -> void:
	if Engine.is_editor_hint():
    pass
    
È il momento di scrivere un po’ di condizioni. Dobbiamo dare un comportamento alla nostra barra della vita in tutte le possibili condizioni che si possono verificare.

La prima condizione che si puo verificare è che la barra della vita sia di 1. Questa è la vita massima che il giocatore puo avere. Quindi procediamo con creare una if  vita massima == 1: codice .
In questa occasione la parte centrale non deve avere nessuna dimensione , mentre la parte finale deve spostarsi in una posizione tale da far vedere solo la parte terminale e chiudere in questo modo la nostra barra della vita.

Partiamo con la parte centrale, dobbiamo modificare la region_rect .

🎯 Alcuni accorgimenti:

region_rect ha quattro valori ed usa un Rect2(Vector2(0,0),Vector2(0,0))

Rect2 come vedete all’interno accetta 2 valori , due Vector2() .
Rect2 è un Variant , una struttura che all’interno puo ospitare altri tipi di dati.
Rispettivamente il primo Vector2() si riferisce alla posizione, mentre il secondo Vector2() si riferisce alla dimensione.


Come già spiegato prima chiameremo la parte centrale, seguita da un punto “.”, per accedere alle proprietà e poi digiteremo region_rect . Dobbiamo quindi assegnarli un Rect2().
I due Vector2() saranno impostati a 0 perché non vogliamo ne agire sulla posizione ma sopratutto non vigliamo nemmeno cambiare la dimensione. Quello che davvero ci interessa comunque è la dimensione sull’asse delle x.

centrale.region_rect = Rect2(Vector2(0,0), Vector2(0,0)

Dobbiamo cambiare adesso la posizione della parte finale.
Nel mio caso ho scoperto che bisogna devo spostarlo di 3 px sull’ asse delle x.

finale.position.x = 3

Il codice dovrebbe essere cosi:


@tool
extends Node2D

@onready var inizio = $LifeParts1
@onready var centro = $LifeParts2
@onready var fine = $LifeParts3
@onready var vita = $"1Bar2"

@export var vitaMassima = 3
@export var valoreVita = 3

func _process(delta: float) -> void:
	if Engine.is_editor_hint():
		if vitaMassima == 1:
			centro.region_rect = Rect2(Vector2(0,0),Vector2(0,11))
			fine.position.x = 3
    
Il secondo caso che ci si puo presentare è che la vitaMassima sia 2. Procediamo con scrivere un elif vitaMassima == 2: codice .

🎯 Alcuni accorgimenti:

Elif è la forma contrata di else if . 
Si usa quindi per verificare un alternativa alla condizione precedente.

In questo caso la parte centrale sarà la stessa della condizione precedente, quindi potete copiarla ed incollarla. Invece la parte finale deve essere spostata in modo che costituisca la porzione della vita per la seconda tacca di vita.
Nel mio caso bisogna spostare la parte finale di 13 pixel, ovvero la larghezza della mia texture - 1 .

finale.position.x = 13

Il codice risultante dovrebbe essere cosi:

@tool
extends Node2D

@onready var inizio = $LifeParts1
@onready var centro = $LifeParts2
@onready var fine = $LifeParts3
@onready var vita = $"1Bar2"

@export var vitaMassima = 3
@export var valoreVita = 3

func _process(delta: float) -> void:
	if Engine.is_editor_hint():
		if vitaMassima == 1:
			centro.region_rect = Rect2(Vector2(0,0),Vector2(0,11))
			fine.position.x = 3
		elif vitaMassima == 2:
			centro.region_rect = Rect2(Vector2(0,0),Vector2(0,11))
			fine.position.x = 13
    
🎯 Alcuni accorgimenti:

Vi starete chiedendo perché ho tolto ho sottratto 1 alla larghezza della mia texture. 
Se date un occhiata alla parte iniziale e la parte finale della mia barra della vita noterete che la parte iniziale finisce con una linea verticale rossa che sancisce la fine della tacca di vita e nello stesso modo la parte finale della barra della vita inizia con una linea verticale che sancisce l’inizio della tacca della vita. 
Se le due texture venissero posizionate perfettamente una accanto all’altra, si creerebbe la situazione in cui questa linea verticale si ripeta e risulti doppia, sfasando tutta la barra della vita. Dobbiamo fare il modo che se ne veda solo 1.
14x11 px

14x11 px





L'ultimo caso che si puo verificare è che la vitaMassima sia piu grande di 2, è in questo caso che la parte centrale della nostra barra di vita deve reagire.
Scriviamo quindi elif vita massima > 2: codica

Il centro deve allungarsi in modo da creare segmenti della vita, questo allungamento sara dato dalla vitaMassima-2 . Sottraiamo 2 perche questi due segmenti sono gia previsti dal inizio e la fine.

centro.region_rect = Rect2(Vector2(0,0),Vector2(10*(vitaMassima-2),11))


10x11 px
Il secondo Vector2() del Rect2 presenta sulle x :
10 che è il valore della larghezza di una taca di vita, moltiplicato per vitaMassima - 2 
Il valore delle y è uguale a 11 che è l'altezza della texture


Il passo seguente è modificare la posizione di centro. Espandendo la misura della regione, questa espansione aviene in modo uniforme su entrambi i latti. Bisogna quindi fare il modo che la texture si sposti della meta della sua dimensione.

centro.position.x = 13+((10*(vitaMassima-2))/2)

14x11 px
13 rapresenta la dimensione della texture di inizio, il resto della formula è molto simile a quella
 precedente a parte il fatto che la dividiamo per due, come ricordate dobbiamo spostarla della meta in avanti.

La parte finale deve essere posizionata quindi nello stesso modo alla fine di centro.

fine.position.x = 13+(10*(vitaMassima-2))

Il codice risultante fino ad adesso dovrebbe essere cosi:



@tool
extends Node2D

@onready var inizio = $LifeParts1
@onready var centro = $LifeParts2
@onready var fine = $LifeParts3
@onready var vita = $"1Bar2"

@export var vitaMassima = 3
@export var valoreVita = 3


func _process(delta: float) -> void:
	if Engine.is_editor_hint():
		if vitaMassima == 1:
			centro.region_rect = Rect2(Vector2(0,0),Vector2(0,11))
			fine.position.x = 3
		elif vitaMassima == 2:
			centro.region_rect = Rect2(Vector2(0,0),Vector2(0,11))
			fine.position.x = 13
		elif vitaMassima > 2:
			centro.region_rect = Rect2(Vector2(0,0),Vector2(10*(vitaMassima-2),11))
			centro.position.x = 13+((10*(vitaMassima-2))/2)
			fine.position.x = 13+(10*(vitaMassima-2))
    
Arrivati a questo punto ci manca fare l'ultimo passo. Dobbiamo fare il modo che le tache della vita siano visibili e che aumentino e diminuiscano aseconda i valori che ha la variabile valoreVita.

Per prima cosa facciamo il modo che il valoreVita non supperi mai la vitaMassima

valoreVita = min(valoreVita, vitaMassima)
🎯 Alcuni accorgimenti:

La funzione min(v1,v2) riceve due valori e ne restituisce sempre quello piu piccolo.
In modo analogo la funzione max(v1,v2) riceve due valori ma ne restituisce sempre quello poi grande
Procediamo quindi con espandere il region_rect di vita , a questo punto dovreste esserne in grado se avete capito.

vita.region_rect = Rect2(Vector2(0,0),Vector2((valoreVita*10)+1,5))

E anche in questo caso dobbiamo spostare vita della meta della sua dimensione.

vita.position.x = 3.5 + ((valoreVita*10)/2)

Avrette sicuramente notato il valore 3.5, questo è dovvuto aihme agli subpixel. Per questo dovete sempre testare che i vostri elementi siano ben posizionati.

Il codice dovrebbe essere cosi:



@tool
extends Node2D

@onready var inizio = $LifeParts1
@onready var centro = $LifeParts2
@onready var fine = $LifeParts3
@onready var vita = $"1Bar2"

@export var vitaMassima = 3
@export var valoreVita = 3


func _process(delta: float) -> void:
	if Engine.is_editor_hint():
		if vitaMassima == 1:
			centro.region_rect = Rect2(Vector2(0,0),Vector2(0,11))
			fine.position.x = 3
		elif vitaMassima == 2:
			centro.region_rect = Rect2(Vector2(0,0),Vector2(0,11))
			fine.position.x = 13
		elif vitaMassima > 2:
			centro.region_rect = Rect2(Vector2(0,0),Vector2(10*(vitaMassima-2),11))
			centro.position.x = 13+((10*(vitaMassima-2))/2)
			fine.position.x = 13+(10*(vitaMassima-2))
		valoreVita = min(valoreVita,vitaMassima)
		vita.region_rect = Rect2(Vector2(0,0),Vector2((valoreVita*10)+1,5))
		vita.position.x = 3.5 + ((valoreVita*10)/2)
    
E con questo siamo quasi giunti alla fine. 
Vi riccorderete che tutto il codice che abbiamo scritto lo abbiamo fatto con l'idea che questo venise esclusivamente eseguito sull'editor. 
Dobbiamo fare il modo che il codice funzioni pero anche in gioco.

Creiamo una funzione func updateLifeBar(): codice.
Dentro a questa funzione copiamo ed incolliamo tutto il codice che abbiamo creato dentro la condizione if Engine.is_editor_hint(): , mi racomando la sudetta linea non la copiamo.

Per ultimo creiamo una funzione func _ready() -> void: codice.
Qua dentro chiamiamo updateLifeBar() in modo che appena parta il gioco aggiorniamo le dimensioni e la barra della vita.

Il codice finale dovrebbe essere cosi:



@tool
extends Node2D

@onready var inizio = $LifeParts1
@onready var centro = $LifeParts2
@onready var fine = $LifeParts3
@onready var vita = $"1Bar2"

@export var vitaMassima = 3
@export var valoreVita = 3

func _ready() -> void:
	updateLifeBar()


func _process(delta: float) -> void:
	if Engine.is_editor_hint():
		if vitaMassima == 1:
			centro.region_rect = Rect2(Vector2(0,0),Vector2(0,11))
			fine.position.x = 3
		elif vitaMassima == 2:
			centro.region_rect = Rect2(Vector2(0,0),Vector2(0,11))
			fine.position.x = 13
		elif vitaMassima > 2:
			centro.region_rect = Rect2(Vector2(0,0),Vector2(10*(vitaMassima-2),11))
			centro.position.x = 13+((10*(vitaMassima-2))/2)
			fine.position.x = 13+(10*(vitaMassima-2))
		valoreVita = min(valoreVita,vitaMassima)
		vita.region_rect = Rect2(Vector2(0,0),Vector2((valoreVita*10)+1,5))
		vita.position.x = 3.5 + ((valoreVita*10)/2)

func updateLifeBar():
	if vitaMassima == 1:
		centro.region_rect = Rect2(Vector2(0,0),Vector2(0,11))
		fine.position.x = 3
	elif vitaMassima == 2:
		centro.region_rect = Rect2(Vector2(0,0),Vector2(0,11))
		fine.position.x = 13
	elif vitaMassima > 2:
		centro.region_rect = Rect2(Vector2(0,0),Vector2(10*(vitaMassima-2),11))
		centro.position.x = 13+((10*(vitaMassima-2))/2)
		fine.position.x = 13+(10*(vitaMassima-2))
	valoreVita = min(valoreVita,vitaMassima)
	vita.region_rect = Rect2(Vector2(0,0),Vector2((valoreVita*10)+1,5))
	vita.position.x = 3.5 + ((valoreVita*10)/2)

    

Conclusioni

La barra della vita è completa, potete migliorarla con segnali, animazioni ed effetti di shader.
Ricordatevi che ogni volta che cambiate il valore di vitaMassima o valoreVita dovete poi chiamare la funzione updateLifeBar() .


Buy Me a Coffee at ko-fi.com

Commenti