Guida tecnica dettagliata: game development con Godot, architettura web scalabile e fisica realistica

Unitiva, Main Sponsor del Guerri Napoli Basket, ha trasformato la partita del 16 novembre scorso in un'esperienza di branding innovativa attraverso la tecnologia. L'obiettivo? Creare qualcosa che coinvolgesse tutti, grandi e piccini, mettendoci al contempo alla prova.
Nella nostra azienda abbiamo il privilegio di lavorare con risorse dalle competenze e passioni più disparate. In occasione di questi eventi speciali, amiamo spingerci oltre i confini, dando l'opportunità al nostro team di sperimentare e dare vita a nuove idee. E, proprio da questa filosofia, è nato questo progetto.

Abbiamo sviluppato un minigioco a tema Napoli Basket dal concept immediato: 60 secondi per segnare il maggior numero di canestri possibile. Tutto inizia con un QR code stampato sui biglietti distribuiti all'ingresso del palazzetto, che porta direttamente alla web app.
Dalla home page, due strade: registrarsi con Google per entrare ufficialmente in classifica, oppure dare un'occhiata ai punteggi e studiare la concorrenza prima di lanciarsi nella sfida.
Registrazione completata, giocatore preferito scelto, e si parte. Sessanta secondi di pura adrenalina: ogni canestro incrementa il punteggio, ogni secondo conta. L'obiettivo? Conquistare il primo posto e portare a casa una maglia ufficiale del Napoli Basket. La vera ciliegina sulla torta: i primi tre classificati scendono in campo durante l'intervallo per tirare veri tiri liberi davanti a tutto il pubblico.
La prima fase del progetto è stata la progettazione dell'intera esperienza utente su Figma. Abbiamo mappato ogni passaggio dell'app per fornire al team di sviluppo una roadmap chiara e dettagliata.

Per lo sviluppo vero e proprio, abbiamo scelto Godot, un motore di gioco open source versatile e potente. In Godot, il gioco è stato costruito utilizzando un'architettura modulare basata su scene e nodi: ogni elemento (il menù, la palla, il canestro e così via) è stato sviluppato come scena indipendente, per poi essere importato e assemblato nella scena principale. Per definire al meglio le meccaniche e le tipologie di una scena i nodi sono essenziali perché creano dei collegamenti in un oggetto come se fosse una matrioska così da definire diversi elementi che lo compongono. Questo approccio modulare ha permesso al team di lavorare in parallelo su componenti diverse, ottimizzando i tempi di sviluppo.
Il cuore pulsante del gioco è stata la fisica della palla. Abbiamo dedicato particolare attenzione alla simulazione realistica del lancio: calibrare la traiettoria, gestire le collisioni con il canestro e ottenere rimbalzi credibili è stato un lavoro complesso e meticoloso.
La meccanica del lancio si basa sul riconoscimento dei gesti touch dello schermo. Quando l'utente trascina il dito, il sistema rileva automaticamente l'inizio, il movimento e il rilascio del tocco. A quel punto entrano in gioco i calcoli: la lunghezza del trascinamento determina la forza verticale applicata alla palla (più lungo è il gesto, più alta andrà la palla), mentre la velocità del trascinamento influisce sulla forza orizzontale (un gesto veloce produce un lancio più potente in avanti). Anche l'angolazione conta: se il trascinamento è troppo laterale, il tiro risulterà impreciso, simulando così un errore realistico.
Per mantenere il codice scalabile e modulare, abbiamo suddiviso la logica in diverse funzioni riutilizzabili. Ogni funzione ha un compito specifico: alcune calcolano le metriche del gesto (lunghezza, velocità, direzione), altre determinano le forze da applicare alla palla, altre ancora combinano tutti questi valori in un unico vettore velocità. Infine, abbiamo implementato dei limiti di sicurezza per evitare che velocità eccessive compromettessero il realismo del gioco. Tutto questo lavoro di calibrazione è stato fondamentale per ottenere una parabola di lancio che sembrasse naturale e permettesse ai giocatori di fare canestro con la giusta dose di sfida. Vediamo nel dettaglio come funziona il sistema di lancio.
La prima funzione serve per rilevare quando e come l'utente interagisce con lo schermo. Ogni volta che viene toccato lo schermo, il sistema registra se si tratta dell'inizio di un tocco, di un trascinamento o del rilascio finale:
func _input(event):
if not _ready_to_launch:
return
if event is InputEventScreenTouch:
if event.pressed:
_reset_swipe_variables()
_start_swipe(event.position)
else:
_end_swipe(event.position)
_handle_swipe()
elif event is InputEventScreenDrag:
_swipe_end = event.position
Questa funzione coordina altre funzioni più specifiche per mantenere il codice ordinato e riutilizzabile. In caso di problemi o modifiche future, sarà più semplice intervenire su singole parti senza riscrivere tutto.
Prima di ogni nuovo lancio, è fondamentale azzerare i dati del lancio precedente. Questa funzione assicura che ogni tiro parta da zero, senza che restino tracce delle forze calcolate in precedenza:
func _reset_swipe_variables():
"""Resetta tutte le variabili dello swipe"""
_swipe_start = Vector2.ZERO
_swipe_end = Vector2.ZERO
_swipe_time_start = 0.0
_swipe_time_end = 0.0
Quando il giocatore inizia a trascinare il dito sullo schermo, questa funzione memorizza la posizione di partenza e il momento esatto in cui è iniziato il gesto:
func _start_swipe(position: Vector2):
"""Inizia a tracciare lo swipe"""
_swipe_start = position
_swipe_time_start = Time.get_time_dict_from_system()["second"]
Una volta completato il trascinamento, il sistema calcola tutte le caratteristiche del gesto: quanto è stato lungo, quanto è durato, quanto veloce è stato e in che direzione è andato. Tutti questi valori vengono organizzati in un dizionario per essere facilmente accessibili:
func _calculate_swipe_metrics() -> Dictionary:
"""Calcola e restituisce tutte le metriche dello swipe in un dizionario"""
var swipe_vec = _swipe_end - _swipe_start
var swipe_length = swipe_vec.length()
var swipe_duration = max(min_swipe_duration, _swipe_time_end - _swipe_time_start)
var swipe_speed = swipe_length / swipe_duration
return {
"vector": swipe_vec,
"length": swipe_length,
"duration": swipe_duration,
"speed": swipe_speed,
"direction": swipe_vec.normalized() if swipe_length > 0 else Vector2.ZERO
}
Gestisce il completamento del trascinamento applicando le forze alla palla:
func _handle_swipe():
"""Gestisce il completamento di uno swipe e applica la forza alla palla"""
# Calcola tutte le metriche dello swipe
var metrics = _calculate_swipe_metrics()
# Verifica che lo swipe sia valido
if metrics["length"] < swipe_length_min:
return
# === CALCOLO SEPARATO DELLE FORZE ===
# 1. FORZA VERTICALE (dipende dalla LUNGHEZZA)
var vertical_force = _calculate_vertical_force(metrics["length"])
# 2. FORZA ORIZZONTALE (dipende dalla VELOCITÀ)
var horizontal_force = -_calculate_horizontal_force(metrics["speed"])
# 3. FORZA LATERALE (proporzionale alla orizzontale e direzione X)
var lateral_force = _calculate_lateral_force(horizontal_force, metrics["direction"])
# Combina tutte le forze in un vettore velocità
var velocity_vector = _create_velocity_vector(
vertical_force,
horizontal_force,
lateral_force,
metrics["direction"]
)
# Applica limiti di sicurezza
velocity_vector = _apply_safety_limits(velocity_vector)
# Applica la velocità alla palla
linear_velocity = velocity_vector
_ready_to_launch = false
ball_launched.emit()
Calcola la forza verticale applicata alla palla definita dalla lunghezza del trascinamento del dito sullo schermo:
func _calculate_vertical_force(swipe_length: float) -> float:
"""
Calcola la forza verticale basata sulla LUNGHEZZA dello swipe
- Swipe corto → forza verticale minima
- Swipe lungo → forza verticale massima
"""
if swipe_length < swipe_length_min:
return 0.0
# Normalizza la lunghezza tra 0 e 1
var length_normalized = clamp(
(swipe_length - swipe_length_min) / (swipe_length_max - swipe_length_min),
0.0, 1.0
)
# Interpola linearmente tra min e max
return lerp(vertical_force_min, vertical_force_max, length_normalized)
Entrambi i calcoli, orizzontale e verticale vanno calibrati e coordinati per avere come risultato la parabola utile per riuscire a fare canestro:
func _calculate_horizontal_force(swipe_speed: float) -> float:
""" Calcola la forza orizzontale basata sulla VELOCITÀ dello swipe
- Swipe lento → forza orizzontale minima
- Swipe veloce → forza orizzontale massima
"""
if swipe_speed < swipe_speed_min:
return horizontal_force_min
# Normalizza la velocità tra 0 e 1
var speed_normalized = clamp(
(swipe_speed - swipe_speed_min) / (swipe_speed_max - swipe_speed_min),
0.0, 1.0
)
# Interpola linearmente tra min e max
return lerp(horizontal_force_min, horizontal_force_max, speed_normalized)
func _calculate_lateral_force(horizontal_force: float, swipe_direction: Vector2) -> float:
"""
Calcola la forza laterale basata sulla componente X della direzione
- Mantiene la proporzionalità con la forza orizzontale
- Permette spostamento laterale naturale
"""
return horizontal_force * -abs(swipe_direction.x)
func _create_velocity_vector(vertical_force: float, horizontal_force: float, lateral_force: float, swipe_direction: Vector2) -> Vector3:
"""
Crea il vettore velocità finale combinando tutte le forze
"""
return Vector3(
swipe_direction.x * lateral_force, # Movimento laterale (sinistra/destra)
vertical_force, # Movimento verticale (alto)
-swipe_direction.y * horizontal_force # Movimento in avanti (negativo su Z per avanti)
)
func _apply_safety_limits(velocity_vector: Vector3) -> Vector3:
"""
Applica limiti di sicurezza per evitare velocità eccessive
"""
var current_magnitude = velocity_vector.length()
if current_magnitude > max_total_velocity:
return velocity_vector.normalized() * max_total_velocity
return velocity_vector
Essendo la palla l'elemento centrale dell'intera esperienza, una volta perfezionata la sua fisica, abbiamo costruito attorno ad essa tutte le altre componenti del gioco.
Dal punto di vista grafico, tutti gli assets 3D e le texture sono stati modellati in Blender, garantendo uno stile visivo coerente e accattivante. Per l'interfaccia utente (HUD), che visualizza il timer, i punti, il nome e la foto del giocatore, abbiamo utilizzato Canva, combinando semplicità di design e rapidità di esecuzione.
Il gioco è ospitato all’interno di un tag HTML chiamato iframe, che permette di eseguire applicazioni esterne alla pagina principale: una volta compilato, il progetto Godot viene infatti convertito in HTML5 e JavaScript ed eseguito all’interno di questo contenitore dedicato.
Per gestire la comunicazione tra il gioco e la pagina web, abbiamo utilizzato in Godot un nodo speciale chiamato JavaScriptBridge, che non può essere inserito direttamente in una scena ma viene richiamato via codice. Questo strumento ci ha permesso di eseguire comandi JavaScript all’interno dell’iframe, rendendo possibile sia il recupero dei dati del giocatore all’inizio della partita (nome e personaggio selezionato), sia il reindirizzamento automatico alla pagina della classifica al termine del gioco.
Dietro l'esperienza apparentemente semplice del minigioco si nasconde un'infrastruttura web progettata per essere veloce, stabile e sicura anche con centinaia di utenti connessi contemporaneamente.
Per lo sviluppo del sito abbiamo scelto Next.js, uno dei framework più moderni e performanti dell'ecosistema React. Questo ci ha permesso di realizzare un'interfaccia rapida, fluida e ottimizzata sia per mobile che per desktop. Il design è costruito con Tailwind CSS, un sistema che consente di mantenere coerenza visiva e velocità di sviluppo, adattando l'esperienza a qualsiasi dispositivo.
Una delle sfide più delicate è la comunicazione tra il gioco in Godot e la piattaforma web. Il gioco, ospitato su Cloudflare, dialoga con il sito tramite API: invia i punteggi e riceve i dati necessari per aggiornare la classifica in tempo reale. L'intera architettura segue il pattern "Backend for Frontend", un backend progettato su misura per le esigenze specifiche dell'interfaccia utente.
L'applicazione è distribuita su Vercel, piattaforma ottimizzata per Next.js con una rete CDN globale che garantisce tempi di caricamento minimi anche sotto traffico intenso. Gli asset del gioco (file, immagini, contenuti statici) sono ospitati su Cloudflare R2, assicurando velocità e affidabilità costanti anche nei momenti di picco.
In un sistema con classifiche e premi reali, la sicurezza diventa fondamentale. Abbiamo implementato diverse protezioni:
Questo ha impedito tentativi di cheating e garantito correttezza nei risultati finali.
Questo progetto ha rappresentato molto più di una semplice attività di branding: è stata un'occasione per unire creatività, tecnologia e passione sportiva. Vedere il pubblico divertirsi con qualcosa che abbiamo creato insieme, sperimentando tecnologie nuove e uscendo dalla nostra zona di comfort è stato il vero premio.
Se hai un'idea che ti entusiasma, che sia un gioco, un'app, un'esperienza interattiva o qualcosa che ancora non ha un nome, noi siamo l’azienda giusta che può offrirti supporto.
Ascoltiamo la tua visione, ci sporchiamo le mani con le tecnologie più adatte e puntiamo a risultati che superano le aspettative.
Parliamone davanti a un caffè (o una videocall): raccontaci la tua sfida e scopriamo insieme come affrontarla.