Sto lavorando a un gioco isometrico 2D con multiplayer su scala moderata, circa 20-30 giocatori collegati contemporaneamente a un server persistente. Ho avuto qualche difficoltà ad attuare una buona implementazione della previsione del movimento.
Fisica / Movimento
Il gioco non ha una vera implementazione fisica, ma utilizza i principi di base per implementare il movimento. Anziché eseguire continuamente il polling dell'input, i cambiamenti di stato (ad es. / Eventi del mouse giù / su / sposta) vengono usati per cambiare lo stato dell'entità personaggio che il giocatore controlla. La direzione del giocatore (cioè / nord-est) è combinata con una velocità costante e trasformata in un vero vettore 3D - la velocità dell'entità.
Nel loop di gioco principale, "Update" viene chiamato prima di "Draw". La logica di aggiornamento attiva un "task di aggiornamento della fisica" che tiene traccia di tutte le entità con una velocità diversa da zero e utilizza un'integrazione molto semplice per modificare la posizione delle entità. Ad esempio: entity.Position + = entity.Velocity.Scale (ElapsedTime.Seconds) (dove "Seconds" è un valore in virgola mobile, ma lo stesso approccio funzionerebbe per valori interi in millisecondi).
Il punto chiave è che nessuna interpolazione viene utilizzata per il movimento: il motore fisico rudimentale non ha il concetto di "stato precedente" o "stato attuale", ma solo una posizione e una velocità.
Pacchetti di modifica e aggiornamento dello stato
Quando la velocità dell'entità personaggio controllata dal giocatore cambia, un pacchetto "sposta avatar" viene inviato al server contenente il tipo di azione dell'entità (stare in piedi, camminare, correre), la direzione (nord-est) e la posizione corrente. Questo è diverso da come funzionano i giochi 3D in prima persona. In un gioco 3D la velocità (direzione) può cambiare fotogramma in fotogramma mentre il giocatore si muove. L'invio di ogni cambio di stato trasmetterebbe effettivamente un pacchetto per frame, che sarebbe troppo costoso. Invece, i giochi 3D sembrano ignorare i cambiamenti di stato e inviare pacchetti di "aggiornamento dello stato" a intervalli fissi, ad esempio ogni 80-150 ms.
Poiché gli aggiornamenti di velocità e direzione si verificano molto meno frequentemente nel mio gioco, posso evitare di inviare ogni cambio di stato. Sebbene tutte le simulazioni fisiche si verifichino alla stessa velocità e siano deterministiche, la latenza è ancora un problema. Per questo motivo, invio pacchetti di aggiornamento di posizione di routine (simile a un gioco 3D) ma molto meno frequentemente - in questo momento ogni 250 ms, ma sospetto che con una buona previsione posso facilmente aumentarlo verso i 500 ms. Il problema più grande è che ora mi sono allontanato dalla norma: tutta la documentazione, le guide e i campioni online inviano aggiornamenti di routine e interpolano tra i due stati. Sembra incompatibile con la mia architettura e ho bisogno di elaborare un algoritmo di previsione del movimento più vicino a un'architettura (di base) di "fisica in rete".
Il server quindi riceve il pacchetto e determina la velocità del giocatore dal suo tipo di movimento in base a uno script (il giocatore è in grado di correre? Ottieni la velocità di corsa del giocatore). Una volta che ha la velocità, la combina con la direzione per ottenere un vettore: la velocità dell'entità. Si verifica un rilevamento dei cheat e una convalida di base e l'entità sul lato server viene aggiornata con la velocità, la direzione e la posizione correnti. La limitazione di base viene inoltre eseguita per impedire ai giocatori di inondare il server con richieste di movimento.
Dopo aver aggiornato la propria entità, il server trasmette un pacchetto di "aggiornamento della posizione dell'avatar" a tutti gli altri giocatori nel raggio d'azione. Il pacchetto di aggiornamento della posizione viene utilizzato per aggiornare le simulazioni di fisica lato client (stato mondiale) dei client remoti ed eseguire la previsione e la compensazione del ritardo.
Previsione e compensazione del ritardo
Come accennato in precedenza, i clienti sono autorevoli per la propria posizione. Salvo casi di imbrogli o anomalie, l'avatar del client non verrà mai riposizionato dal server. Non è richiesta alcuna estrapolazione ("sposta ora e correggi dopo") per l'avatar del cliente - ciò che il giocatore vede è corretto. Tuttavia, è necessaria una sorta di estrapolazione o interpolazione per tutte le entità remote in movimento. Una sorta di previsione e / o compensazione del ritardo è chiaramente richiesta nel motore di simulazione / fisica locale del cliente.
I problemi
Ho lottato con vari algoritmi e ho una serie di domande e problemi:
Dovrei estrapolare, interpolare o entrambi? La mia "sensazione viscerale" è che dovrei usare la pura estrapolazione basata sulla velocità. Il cambiamento di stato viene ricevuto dal client, il client calcola una velocità "prevista" che compensa il ritardo e il normale sistema fisico fa il resto. Tuttavia, sembra in contrasto con tutti gli altri codici e articoli di esempio: sembrano tutti memorizzare un certo numero di stati ed eseguire l'interpolazione senza un motore fisico.
Quando arriva un pacchetto, ho provato a interpolare la posizione del pacchetto con la velocità del pacchetto per un periodo di tempo fisso (diciamo 200 ms). Prendo quindi la differenza tra la posizione interpolata e l'attuale posizione di "errore" per calcolare un nuovo vettore e posizionarlo sull'entità invece della velocità che è stata inviata. Tuttavia, il presupposto è che un altro pacchetto arriverà in quell'intervallo di tempo, ed è incredibilmente difficile "indovinare" quando arriverà il pacchetto successivo, specialmente dal momento che non arrivano tutti a intervalli fissi (cioè anche i cambiamenti di stato). Il concetto è fondamentalmente imperfetto o è corretto ma necessita di alcune correzioni / regolazioni?
Cosa succede quando un lettore remoto si ferma? Posso immediatamente fermare l'entità, ma sarà posizionata nel punto "sbagliato" fino a quando non si sposta di nuovo. Se stimo un vettore o provo a interpolare, ho un problema perché non memorizzo lo stato precedente: il motore fisico non ha modo di dire "devi fermarti dopo aver raggiunto la posizione X". Capisce semplicemente una velocità, niente di più complesso. Sono riluttante ad aggiungere le informazioni sullo "stato di movimento dei pacchetti" alle entità o al motore fisico, poiché violano i principi di progettazione di base e diffondono il codice di rete in tutto il resto del motore di gioco.
Cosa dovrebbe accadere quando le entità si scontrano? Esistono tre scenari: il giocatore di controllo si scontra localmente, due entità si scontrano sul server durante un aggiornamento di posizione o un aggiornamento di entità remota si scontra sul client locale. In tutti i casi non sono sicuro di come gestire la collisione - a parte barare, entrambi gli stati sono "corretti" ma in periodi di tempo diversi. Nel caso di un'entità remota non ha senso disegnarlo mentre cammina attraverso un muro, quindi eseguo il rilevamento delle collisioni sul client locale e lo faccio "arrestare". Sulla base del punto 2 sopra, potrei calcolare un "vettore corretto" che tenta continuamente di spostare l'entità "attraverso il muro" che non avrà mai successo - l'avatar remoto è bloccato lì fino a quando l'errore non diventa troppo alto e "scatta" in posizione. Come funzionano i giochi attorno a questo?