1
Fork 0
mirror of https://github.com/Steffo99/nanogolf.git synced 2024-11-25 01:24:18 +00:00
This commit is contained in:
Steffo 2024-03-22 06:20:18 +01:00
parent 9e81f4ff84
commit b479498729
Signed by: steffo
GPG key ID: 5ADA3868646C3FC0
18 changed files with 387 additions and 1 deletions

View file

@ -161,6 +161,8 @@ I canali di trasferimento attualmente disponibili sono:
- `unreliable_ordered`, che aggiunge un contatore ad ogni pacchetto inviato tramite UDP, facendo in modo che i peer possano ignorare i pacchetti arrivati nell'ordine sbagliato; - `unreliable_ordered`, che aggiunge un contatore ad ogni pacchetto inviato tramite UDP, facendo in modo che i peer possano ignorare i pacchetti arrivati nell'ordine sbagliato;
- `reliable`, che implementa un protocollo di acknowledgement su UDP molto simile a quello utilizzato da TCP, a costo di una latenza significativamente maggiore. - `reliable`, che implementa un protocollo di acknowledgement su UDP molto simile a quello utilizzato da TCP, a costo di una latenza significativamente maggiore.
> Dove non specificato, in questa relazione si assume che una RPC descritta usi sempre il canale `reliable`.
## Nodi di sincronizzazione ## Nodi di sincronizzazione
Godot mette a disposizione dei `Node` speciali per assistere con la sincronizzazione dello stato del gioco tra tutti i peer. Godot mette a disposizione dei `Node` speciali per assistere con la sincronizzazione dello stato del gioco tra tutti i peer.

View file

@ -16,7 +16,7 @@ Il `Node` `Main` è alla radice di ogni istanza del gioco, e si occupa di inizi
## [`Game`](../scenes/game.gd) ## [`Game`](../scenes/game.gd)
Il `Node` `Game` viene collocato sempre come figlio di `Main`, e rappresenta lo stato di gioco di un peer, che esso sia un server o un client. Il `Node` `Game` viene collocato sempre come figlio di `Main`, e rappresenta lo stato di gioco di un peer connesso ad una specifica stanza (anche detta "lobby" in inglese), che esso sia un server o un client.
Il server ha sempre autorità su di esso, e: Il server ha sempre autorità su di esso, e:
@ -26,24 +26,136 @@ Il server ha sempre autorità su di esso, e:
- quando il giocatore host richiede attraverso l'interfaccia grafica di iniziare la partita, usa il `PhaseTracker` per cambiare ovunque la fase della partita da `LOBBY` a `PLAYING`, e richiede al `LevelManager` di istanziare il primo livello; - quando il giocatore host richiede attraverso l'interfaccia grafica di iniziare la partita, usa il `PhaseTracker` per cambiare ovunque la fase della partita da `LOBBY` a `PLAYING`, e richiede al `LevelManager` di istanziare il primo livello;
- quando il `LevelManager` segnala che la `LevelPlaylist` selezionata è esaurita, usa il `PhaseTracker` per aggiornare la fase della partita da `PLAYING` a `ENDED`. - quando il `LevelManager` segnala che la `LevelPlaylist` selezionata è esaurita, usa il `PhaseTracker` per aggiornare la fase della partita da `PLAYING` a `ENDED`.
![](img/game.png)
## [`PeerNodeDirectory`](../scenes/peernode_directory.gd) ## [`PeerNodeDirectory`](../scenes/peernode_directory.gd)
Il `Node` `PeerNodeDirectory`, essendo contenuto nella scena `game.tscn`, viene istanziato ricorsivamente in tutte le istanze di `Game`.
Come menzionato in precedenza, ha la responsabilità di mantenere aggiornato l'indice dei peer connessi, e a tale scopo rende disponibili al server le remote procedure call `rpc_create_peernode(peer_id)` e `rpc_destroy_peernode(peer_id)` in modo da permettere al server (e solo al server) di informare tutti i client dello stato attuale dei client connessi.
![](img/peernodedirectory.png)
## [`PeerNode`](../scenes/peernode.gd) ## [`PeerNode`](../scenes/peernode.gd)
Il `Node` `PeerNode` rappresenta un singolo peer connesso, di cui ne è la `multiplayer_authority`, e ha il suo identificatore impostato come proprietà `name`.
Ha un singolo metodo, che può essere chiamato come RPC solamente dal peer che controlla il nodo stesso: `rpc_identify`, che comunica al server il nome con cui il peer appena connesso si identifica agli altri.
Quando il server riceve un'identificazione di un peer, il relativo `PeerNode` emette il segnale `identified`, che viene propagato in alto fino a `Game`, il quale lo gestisce come descritto sopra.
## [`PlayerNodeDirectory`](../scenes/playernode_directory.gd) ## [`PlayerNodeDirectory`](../scenes/playernode_directory.gd)
Il `Node` `PlayerNodeDirectory` è molto simile a `PeerNodeDirectory`, ma invece che dei peer, tiene traccia dei giocatori effettivi nella partita, indicizzati per nome del giocatore rappresentato.
Dato che un giocatore, una volta entrato in una lobby, deve mantenere il proprio punteggio fino a quando la partita non è terminata, anche se non l'ha terminata perchè l'ha abbandonata a metà, non è incluso un modo per rimuovere un giocatore dall'indice, ma soltanto uno per aggiungerne uno nuovo.
Questo metodo è `rpc_possess_playernode(player_name, peer_id)`, chiamabile solo dal server, che controlla se un giocatore con il dato nome è già indicizzato, creando il relativo `PlayerNode` in caso negativo, e in entrambi i casi (ri)assegnando l'autorità su di esso al peer con l'identificatore specificato.
In aggiunta, è disponibile `rpc_push_reported_scores()`, chiamabile anch'essa solo dal server, che fa in modo che tutti i `PlayerNode` figli spostino in una lista il numero di colpi impiegati per entrare nella buca attuale (`reported_score`, descritto in seguito), resettando poi il contatore in preparazione alla buca successiva.
![](img/playernodedirectory.png)
## [`PlayerNode`](../scenes/playernode.gd) ## [`PlayerNode`](../scenes/playernode.gd)
Il `Node` `PlayerNode` rappresenta un singolo giocatore nella partita, e la sua `multiplayer_authority` può variare in base ai peer attualmente connessi alla partita: essa prende il valore dell'ultimo peer che si è identificato con il relativo nome, se esso è ancora connesso, altrimenti l'autorità è assegnata al server fino a quando un nuovo peer non si riconnetterà con quel nome.
Esso include tante proprietà e metodi, in quanto è ciò che permette ai dati di uno specifico giocatore di persistere tra un livello e l'altro.
Tra questi, sono degni di nota:
- `sanitize_player_name`, una funzione statica che sostituisce con un underscore (`_`) tutti i caratteri non validi all'interno per il nome di un nodo (`/`, `*`, ` `) dal nome del giocatore;
- `player_name`, proprietà che assieme al nome del nodo stesso riflette il nome del giocatore (necessaria per via di un dettaglio implementativo di come Godot gestisce le stringhe nei nomi);
- `rpc_set_name`, remote procedure call chiamabile solo dall'autorità che modifica il nome del giocatore in questione
- il segnale `name_changed`, emesso quando il nome viene cambiato;
- `rpc_query_name`, remote procedure call chiamabile da qualsiasi peer che richiede all'autorità di ripetere `rpc_set_name` con il nome attuale, in modo da comunicarlo anche ad un peer che si è connesso successivamente alla prima chiamata;
- `player_color`, proprietà che rappresenta il colore del giocatore;
- `rpc_set_color`, authority RPC che modifica il colore;
- il segnale `color_changed`, emesso quando il colore viene cambiato;
- `rpc_query_color`, che fa ripetere `rpc_set_color` all'autorità con il colore attuale;
- `possess`, funzione di utilità che modifica la `multiplayer_authority` del nodo al valore dato;
- il segnale `possessed`, emesso quando la `multiplayer_authority` è modificata attraverso `possess`;
- `hole_scores`, lista di `int` che rappresentano i colpi richiesti dal giocatore per entrare in buca nelle buche precedenti;
- `rpc_set_scores`, remote procedure call chiamabile solo dall'autorità che modifica la lista dei punteggi;
- `rpc_query_scores`, che fa ripetere `rpc_set_scores` all'autorità con i punteggi attuali;
- il segnale `scores_changed`, emesso quando i punteggi vengono cambiati;
- `reported_score`, proprietà che assume il valore di `-1` mentre il giocatore sta affrontando la buca, e ottiene un valore corrispondente al numero di colpi impiegati per terminarla una volta finita;
- `rpc_report_score`, remote procedure call chiamabile solo dall'autorità che imposta un reported score;
- il segnale `score_reported`, emesso quando viene impostato un `reported_score` tramite `rpc_report_score`.
## [`PhaseTracker`](../scenes/phase_tracker.gd) ## [`PhaseTracker`](../scenes/phase_tracker.gd)
Il `Node` `PhaseTracker` si occupa di tenere traccia della fase in cui si trova la partita in ogni dato momento.
Le fasi possibili, rappresentate dall'enumerazione `Phase` e immagazzinate nella proprietà `phase`, sono:
- `LOBBY`, il momento pre-partita in cui viene visualizzato l'elenco dei giocatori connessi ed è data all'host la possibilità di iniziare;
- `PLAYING`, il momento in cui la partita sta venendo disputata;
- `ENDED`, il momento in cui la partita è terminata, e stanno venendo mostrati a schermo i risultati.
La sua autorità è sempre posseduta dal server, e come `PlayerNode`, include una coppia di RPC per modificare la fase attuale, `rpc_set_phase` e `rpc_query_phase`, più un segnale `phase_changed` emesso quando la fase viene effettivamente cambiata.
![](img/phasetracker.png)
## [`LevelPlaylist`](../scenes/level_playlist.gd) ## [`LevelPlaylist`](../scenes/level_playlist.gd)
Il `Node` `LevelPlaylist` contiene una lista `levels`, che in riferimenti a scene rappresentanti i livelli precostruiti che il server invierà ai client.
L'indice del livello attuale viene salvato nella proprietà `idx`, mentre una funzione `advance` restituisce il prossimo livello dalla lista emettendo simultaneamente il segnale `advanced(level)`.
Sui client, `idx` rimane sempre fisso a `-1`, in quanto essi non caricano il livello da una lista predefinita, ma lo costruiscono dinamicamente in base ai dati che il server invia loro.
![](img/levelplaylist.png)
## [`LevelManager`](../scenes/level_manager.gd) ## [`LevelManager`](../scenes/level_manager.gd)
Il `Node` `LevelManager` è gestito dal server, e si occupa di inizializzare i livelli sia su client, sia su server.
Fa uso di tre RPC chiamabili solo dal server:
- `rpc_wipe_level`, che decostruisce il livello attuale, se presente, e ne inizializza uno nuovo, preso dalla `LevelPlaylist`;
- `rpc_destroy_level`, che distrugge il livello attuale e non fa nient'altro, usato per procedere in sicurezza alla schermata dei risultati;
- `rpc_level_completed`, che emette il segnale `level_completed` su tutti i client.
Sono inclusi numerosi segnali che corrispondono alle varie fasi del ciclo di vita di un livello, come `level_destroying`, `level_destroyed`, `level_determined`, `level_initialized`, `level_built` e `level_completed`.
Un segnale specifico è relativo al ciclo di vita dell'intera playlist: quando i livelli disponibili sono esauriti, viene emesso il segnale `playlist_complete`, che viene poi raccolto da `Game` per avanzare alla fase `ENDED`.
![](img/levelmanager.png)
## [`GolfLevel`](../scenes/golf_level.gd) ## [`GolfLevel`](../scenes/golf_level.gd)
Il **`Node2D`** `GolfLevel` è ancora una volta gestito dal server, e contiene gli elementi "effettivi" della buca che i giocatori andranno ad affrontare.
Si può comportare in due diversi modi in base al contesto in cui viene usato:
- gli può essere specificata nella proprietà `target` una scena contenente un altro `GolfLevel` da `LevelManager`; essa verrà istanziata senza essere aggiunta al mondo di gioco, poi da quella scena verranno lette varie proprietà dai nodi `FollowCamera`, `GolfTee`, `GolfHole` e `TileMap` presenti in essa e attraverso RPC saranno replicate su tutti i peer;
- gli può essere specificato `null` nella proprietà `target`, rimanendo così in attesa di ricevere RPC dal server per costruire il livello secondo istruzioni.
![](img/golflevel.png)
### Replicazione remota di `GolfLevel`
La replicazione remota di un livello segue i seguenti passi:
1. il server legge la `global_position`, `global_rotation` e `global_scale` della `TileMap` target, poi chiama `rpc_build_tilemap` usandoli come argomenti, istanziando nel mondo di gioco una griglia vuota nella stessa configurazione di quella target;
2. il server itera su tutti i tile della `TileMap` target, e per ciascuno chiama `rpc_build_tilemap_cell` passandogli le caratteristiche del tile da copiare, istanziando così nel mondo di gioco i "muretti" della buca;
3. il server guarda la posizione del `GolfTee` target, e usa `rpc_build_tee` per ricrearlo lì, connettendo inoltre il suo segnale `everyone_entered_hole` a una sua funzione interna che fa arrivare `level_completed` a `LevelManager` dopo qualche secondo;
4. il server itera su tutti i `PlayerNode` presenti nella `PlayerNodeDirectory` del `Game` attuale, e per ognuno di essi chiama `rpc_build_ball` con le relative caratteristiche, che includono la posizione, il numero attuale di colpi effettuati, e se la pallina è entrata in buca;
5. il server guarda la posizione e la dimensione della `GolfHole` target, e chiama `rpc_build_hole` passandoglieli come parametri,
6. il server guarda la posizione di `FollowCamera`, e controlla il parametro `camera_follows_payer` del target, per poi istanziare una nuova `FollowCamera` nel mondo di gioco e, se `camera_follows_player` è attivato, la configura per seguire la `GolfBall` controllata dal giocatore locale.
## [`GolfTee`](../scenes/golf_tee.gd) ## [`GolfTee`](../scenes/golf_tee.gd)
Il `Node2D` `GolfTee` è sempre gestito dal server, e funziona sia da directory delle "palline" che i giocatori controllano nel corso del livello, sia come loro istanziatore.
Non contiene remote procedure call, perchè la `GolfLevel` ha la responsabilità di richiedere ai peer la creazione di una nuova entità del livello, ma contiene un metodo `spawn` di cui `rpc_build_ball` fa uso.
Connette i segnali `name_changed`, `color_changed`, `possessed` emessi dal `PlayerNode` alle `GolfBall` `spawn`ate per rimanere sincronizzata con lo stato della stanza: ad esempio, se il giocatore dovesse disconnettersi, la relativa `GolfBall` verrebbe nascosta dal mondo di gioco, ma comunque mantenuta, in modo che qualora il giocatore si riconnetterà, continuerà a disputare la buca dalla posizione in cui si è disconnesso.
Inoltre, ogni volta che una `GolfBall` da esso istanziata entra nella buca, `GolfTee` chiama `rpc_report_score` sul relativo `PlayerNode`, e poi controlla se tutte le palline dei giocatori connessi (e quindi la cui `multiplayer_authority` del relativo `PlayerNode` è diversa da quella del server) sono entrate in buca, emettendo esso stesso il segnale `everyone_entered_hole`, che `GolfLevel` usa per determinare quando avanzare di livello.
![](img/golftee.png)
## [`GolfBall`](../scenes/golf_ball.gd) ## [`GolfBall`](../scenes/golf_ball.gd)
## [`PuttController`](../scenes/putt_controller.gd) ## [`PuttController`](../scenes/putt_controller.gd)

BIN
docs/img/game.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

34
docs/img/game.png.import Normal file
View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://by4gofram0cbi"
path="res://.godot/imported/game.png-fcdac8ac86fa7d0c58685bcddac3a78a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/img/game.png"
dest_files=["res://.godot/imported/game.png-fcdac8ac86fa7d0c58685bcddac3a78a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
docs/img/golflevel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dvuju4q6alwyp"
path="res://.godot/imported/golflevel.png-d27e4983f59b7723a043af42b28f8be5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/img/golflevel.png"
dest_files=["res://.godot/imported/golflevel.png-d27e4983f59b7723a043af42b28f8be5.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
docs/img/golftee.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://chxkkxrdxhdy3"
path="res://.godot/imported/golftee.png-252e0b2f30e9a9c0c38ea9fb7be1663b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/img/golftee.png"
dest_files=["res://.godot/imported/golftee.png-252e0b2f30e9a9c0c38ea9fb7be1663b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
docs/img/levelmanager.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cpx3bq8v74ms0"
path="res://.godot/imported/levelmanager.png-1d5d66e9ab6d9122e97efa1bce2c6829.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/img/levelmanager.png"
dest_files=["res://.godot/imported/levelmanager.png-1d5d66e9ab6d9122e97efa1bce2c6829.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
docs/img/levelplaylist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cexns0yby3b03"
path="res://.godot/imported/levelplaylist.png-81c22442bdcead8e3a74dd48b9e2cf77.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/img/levelplaylist.png"
dest_files=["res://.godot/imported/levelplaylist.png-81c22442bdcead8e3a74dd48b9e2cf77.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bp8dcwk8spldq"
path="res://.godot/imported/peernodedirectory.png-919e1251618461314bba777535202563.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/img/peernodedirectory.png"
dest_files=["res://.godot/imported/peernodedirectory.png-919e1251618461314bba777535202563.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
docs/img/phasetracker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b0b237n4m3se1"
path="res://.godot/imported/phasetracker.png-008c365a66e2dc23bda5107f3070a7b6.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/img/phasetracker.png"
dest_files=["res://.godot/imported/phasetracker.png-008c365a66e2dc23bda5107f3070a7b6.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b63iya8v3xypu"
path="res://.godot/imported/playernodedirectory.png-e5070e4b69e8c7c8f32804b2b8f671ab.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/img/playernodedirectory.png"
dest_files=["res://.godot/imported/playernodedirectory.png-e5070e4b69e8c7c8f32804b2b8f671ab.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1