Hoe we de mechanismen van ballistische berekeningen voor een mobiele shooter hebben verbeterd met een algoritme voor netwerklatentiecompensatie

Hoe we de mechanismen van ballistische berekeningen voor een mobiele shooter hebben verbeterd met een algoritme voor netwerklatentiecompensatie

Hallo, ik ben Nikita Brizhak, een serverontwikkelaar van Pixonic. Vandaag wil ik het hebben over het compenseren van vertragingen in mobiele multiplayer.

Er zijn veel artikelen geschreven over compensatie voor serververtraging, ook in het Russisch. Dit is niet verrassend, aangezien deze technologie sinds eind jaren 90 actief wordt gebruikt bij het creëren van multiplayer-FPS. U kunt zich bijvoorbeeld de QuakeWorld-mod herinneren, die een van de eersten was die deze gebruikte.

We gebruiken het ook in onze mobiele multiplayer-shooter Dino Squad.

In dit artikel is het niet mijn doel om te herhalen wat al duizend keer is geschreven, maar om te vertellen hoe we vertragingscompensatie in onze game hebben geïmplementeerd, rekening houdend met onze technologiestapel en de belangrijkste gameplay-functies.

Een paar woorden over onze cortex en technologie.

Dino Squad is een mobiele netwerk-PvP-shooter. Spelers besturen dinosaurussen die zijn uitgerust met een verscheidenheid aan wapens en vechten met elkaar in 6v6-teams.

Zowel de client als de server zijn gebaseerd op Unity. De architectuur is vrij klassiek voor shooters: de server is autoritair en clientvoorspelling werkt op de clients. De spelsimulatie is geschreven met behulp van het eigen ECS en wordt zowel op de server als op de client gebruikt.

Als dit de eerste keer is dat u over vertragingscompensatie hoort, vindt u hier een korte uitleg over dit onderwerp.

In multiplayer FPS-games wordt de wedstrijd meestal gesimuleerd op een externe server. Spelers sturen hun invoer (informatie over de ingedrukte toetsen) naar de server, en als reactie stuurt de server hen een bijgewerkte spelstatus, rekening houdend met de ontvangen gegevens. Met dit interactieschema zal de vertraging tussen het indrukken van de vooruittoets en het moment waarop het personage van de speler op het scherm beweegt altijd groter zijn dan de ping.

Hoewel deze vertraging (in de volksmond inputlag genoemd) op lokale netwerken misschien onmerkbaar is, creëert het bij het spelen via internet het gevoel van “glijden op ijs” bij het besturen van een personage. Dit probleem is dubbel relevant voor mobiele netwerken, waar het geval waarin de ping van een speler 200 ms is, nog steeds als een uitstekende verbinding wordt beschouwd. Vaak kan de ping 350, 500 of 1000 ms zijn. Dan wordt het bijna onmogelijk om een ​​snelle shooter met input lag te spelen.

De oplossing voor dit probleem is simulatievoorspelling aan de clientzijde. Hier past de client zelf de invoer toe op het personage van de speler, zonder te wachten op een reactie van de server. En wanneer het antwoord wordt ontvangen, worden eenvoudigweg de resultaten vergeleken en de posities van de tegenstanders bijgewerkt. De vertraging tussen het indrukken van een toets en het weergeven van het resultaat op het scherm is in dit geval minimaal.

Het is belangrijk om de nuance hier te begrijpen: de client trekt zichzelf altijd op basis van zijn laatste invoer, en vijanden - met netwerkvertraging, volgens de vorige status uit de gegevens van de server. Dat wil zeggen, wanneer de speler op een vijand schiet, ziet hij hem in het verleden ten opzichte van zichzelf. Meer over klantvoorspelling we schreven eerder.

Klantvoorspelling lost dus één probleem op, maar creëert een ander probleem: als een speler schiet op het punt waar de vijand zich in het verleden bevond, op de server wanneer hij op hetzelfde punt schiet, is de vijand mogelijk niet langer op die plaats. Er wordt geprobeerd dit probleem op te lossen met serververtragingscompensatie. Wanneer een wapen wordt afgevuurd, herstelt de server de spelstatus die de speler lokaal zag op het moment van het schot, en controleert of hij de vijand echt had kunnen raken. Als het antwoord ‘ja’ is, wordt de treffer geteld, zelfs als de vijand zich op dat moment niet meer op de server bevindt.

Gewapend met deze kennis zijn we begonnen met het implementeren van serververtragingscompensatie in Dino Squad. Allereerst moesten we begrijpen hoe we op de server konden herstellen wat de klant zag? En wat moet er precies gerestaureerd worden? In ons spel worden treffers van wapens en vaardigheden berekend door middel van raycasts en overlays, dat wil zeggen door interacties met de fysieke botsers van de vijand. Daarom moesten we de positie van deze botsers, die de speler lokaal ‘zag’, op de server reproduceren. Op dat moment gebruikten we Unity versie 2018.x. De natuurkunde-API daar is statisch, de fysieke wereld bestaat in één exemplaar. Er is geen manier om de staat ervan op te slaan en vervolgens vanuit de doos te herstellen. Dus wat te doen?

De oplossing lag aan de oppervlakte; alle elementen ervan waren al door ons gebruikt om andere problemen op te lossen:

  1. Van elke cliënt moeten we weten op welk tijdstip hij tegenstanders zag toen hij op de toetsen drukte. Deze informatie hebben wij al in het invoerpakket geschreven en gebruikt om de klantvoorspelling aan te passen.
  2. We moeten de geschiedenis van spelstatussen kunnen opslaan. Daarin zullen we de posities van onze tegenstanders (en dus hun botsers) behouden. We hadden al een statusgeschiedenis op de server, we hebben deze gebruikt om te bouwen delta's. Als we het juiste moment kennen, kunnen we gemakkelijk de juiste toestand in de geschiedenis vinden.
  3. Nu we de spelstatus uit de geschiedenis in handen hebben, moeten we spelersgegevens kunnen synchroniseren met de staat van de fysieke wereld. Bestaande botsers - bewegen, ontbrekende - creëren, onnodige - vernietigen. Deze logica was ook al geschreven en bestond uit verschillende ECS-systemen. We gebruikten het om meerdere gamerooms in één Unity-proces te houden. En omdat de fysieke wereld één per proces is, moest deze tussen kamers worden hergebruikt. Vóór elke tik van de simulatie hebben we de toestand van de fysieke wereld "gereset" en opnieuw geïnitialiseerd met gegevens voor de huidige kamer, in een poging Unity-spelobjecten zoveel mogelijk te hergebruiken via een slim poolingsysteem. Het enige dat overbleef was het aanroepen van dezelfde logica voor de spelstatus uit het verleden.

Door al deze elementen samen te voegen, kregen we een ‘tijdmachine’ die de toestand van de fysieke wereld naar het juiste moment kon terugdraaien. De code bleek eenvoudig:

public class TimeMachine : ITimeMachine
{
     //История игровых состояний
     private readonly IGameStateHistory _history;

     //Текущее игровое состояние на сервере
     private readonly ExecutableSystem[] _systems;

     //Набор систем, расставляющих коллайдеры в физическом мире 
     //по данным из игрового состояния
     private readonly GameState _presentState;

     public TimeMachine(IGameStateHistory history, GameState presentState, ExecutableSystem[] timeInitSystems)
     {
         _history = history; 
         _presentState = presentState;
         _systems = timeInitSystems;  
     }

     public GameState TravelToTime(int tick)
     {
         var pastState = tick == _presentState.Time ? _presentState : _history.Get(tick);
         foreach (var system in _systems)
         {
             system.Execute(pastState);
         }
         return pastState;
     }
}

Het enige dat nog overbleef, was uitzoeken hoe je deze machine kon gebruiken om gemakkelijk schoten en vaardigheden te compenseren.

In het eenvoudigste geval, wanneer de mechanismen gebaseerd zijn op een enkele trefferscan, lijkt alles duidelijk: voordat de speler schiet, moet hij de fysieke wereld terugdraaien naar de gewenste staat, een raycast uitvoeren, de treffer of misser tellen, en breng de wereld terug naar de oorspronkelijke staat.

Maar er zijn maar heel weinig van dergelijke mechanismen in Dino Squad! De meeste wapens in het spel creëren projectielen: kogels met een lange levensduur die meerdere simulatietikken vliegen (in sommige gevallen tientallen tikken). Wat moet je ermee doen, hoe laat moeten ze vliegen?

В oud artikel over de Half-Life-netwerkstack stelden de jongens van Valve dezelfde vraag, en hun antwoord was dit: projectielvertragingscompensatie is problematisch, en het is beter om dit te vermijden.

We hadden deze optie niet: op projectielen gebaseerde wapens waren een belangrijk kenmerk van het spelontwerp. Wij moesten dus iets verzinnen. Na wat brainstormen formuleerden we twee opties die leken te werken:

1. We koppelen het projectiel aan de tijd van de speler die het heeft gemaakt. Met elke tik van de serversimulatie, voor elke kogel van elke speler, draaien we de fysieke wereld terug naar de clientstatus en voeren we de nodige berekeningen uit. Deze aanpak maakte het mogelijk om een ​​verdeelde belasting op de server en een voorspelbare vliegtijd van projectielen te hebben. Voorspelbaarheid was voor ons vooral belangrijk, omdat we alle projectielen, inclusief vijandelijke projectielen, op de klant hebben voorspeld.

Hoe we de mechanismen van ballistische berekeningen voor een mobiele shooter hebben verbeterd met een algoritme voor netwerklatentiecompensatie
Op de foto vuurt de speler bij tik 30 anticiperend een raket af: hij ziet in welke richting de vijand rent en kent bij benadering de snelheid van de raket. Lokaal ziet hij dat hij bij de 33e tik het doel raakt. Dankzij vertragingscompensatie verschijnt deze ook op de server

2. We doen alles hetzelfde als in de eerste optie, maar nadat we één tik van de kogelsimulatie hebben geteld, stoppen we niet, maar blijven we de vlucht ervan binnen dezelfde servertik simuleren, waarbij we elke keer de tijd dichter bij de server brengen één voor één aanvinken en botsposities bijwerken. We doen dit totdat een van de volgende twee dingen gebeurt:

  • De kogel is verlopen. Dit betekent dat de berekeningen voorbij zijn, we kunnen een misser of een treffer tellen. En dit is op hetzelfde moment waarop het schot werd afgevuurd! Voor ons was dit zowel een pluspunt als een minpunt. Een pluspunt - omdat dit voor de schietende speler de vertraging tussen de treffer en de afname van de gezondheid van de vijand aanzienlijk verminderde. Het nadeel is dat hetzelfde effect werd waargenomen wanneer tegenstanders op de speler schoten: de vijand leek alleen een langzame raket af te vuren en de schade was al geteld.
  • De kogel heeft de servertijd bereikt. In dit geval zal de simulatie doorgaan bij de volgende servertik zonder enige vertragingscompensatie. Voor langzame projectielen zou dit theoretisch het aantal fysische terugdraaiingen kunnen verminderen in vergelijking met de eerste optie. Tegelijkertijd nam de ongelijkmatige belasting van de simulatie toe: de server was inactief, of in één servertik berekende hij een tiental simulatietikken voor verschillende kogels.

Hoe we de mechanismen van ballistische berekeningen voor een mobiele shooter hebben verbeterd met een algoritme voor netwerklatentiecompensatie
Hetzelfde scenario als in de vorige afbeelding, maar berekend volgens het tweede schema. De raket heeft de tijd van de server 'ingehaald' op hetzelfde moment dat het schot plaatsvond, en de treffer kan al bij de volgende tik worden geteld. Bij de 31e tik wordt in dit geval geen vertragingscompensatie meer toegepast

Bij onze implementatie verschilden deze twee benaderingen in slechts een paar regels code, dus hebben we beide gemaakt, en lange tijd bestonden ze naast elkaar. Afhankelijk van de werking van het wapen en de snelheid van de kogel, kozen we voor elke dinosaurus een of andere optie. Het keerpunt hier was het verschijnen in het spel van mechanica als "als je de vijand zo vaak raakt in die en die tijd, krijg dan die en die bonus." Elke monteur waarbij het tijdstip waarop de speler de vijand raakte een belangrijke rol speelde, weigerde met de tweede benadering te werken. We zijn dus voor de eerste optie gegaan, en deze geldt nu voor alle wapens en alle actieve vaardigheden in het spel.

Afzonderlijk is het de moeite waard om de kwestie van de prestaties aan de orde te stellen. Als je dacht dat dit alles de zaken zou vertragen, antwoord ik: dat is zo. Unity is vrij traag in het verplaatsen van botsers en het in- en uitschakelen ervan. In Dino Squad kunnen er in het "worst case" scenario honderden projectielen tegelijkertijd in de strijd aanwezig zijn. Het verplaatsen van botsers om elk projectiel afzonderlijk te tellen is een onbetaalbare luxe. Daarom was het absoluut noodzakelijk dat we het aantal natuurkundige “rollbacks” tot een minimum beperkten. Om dit te doen hebben we in ECS een aparte component gemaakt waarin we de tijd van de speler registreren. We hebben het toegevoegd aan alle entiteiten die vertragingscompensatie vereisen (projectielen, vaardigheden, enz.). Voordat we dergelijke entiteiten gaan verwerken, clusteren we ze tegen die tijd en verwerken we ze samen, waarbij we de fysieke wereld voor elk cluster één keer terugdraaien.

In dit stadium hebben we een algemeen werkend systeem. De code in een enigszins vereenvoudigde vorm:

public sealed class LagCompensationSystemGroup : ExecutableSystem
{
     //Машина времени
     private readonly ITimeMachine _timeMachine;

     //Набор систем лагкомпенсации
     private readonly LagCompensationSystem[] _systems;
     
     //Наша реализация кластеризатора
     private readonly TimeTravelMap _travelMap = new TimeTravelMap();

    public LagCompensationSystemGroup(ITimeMachine timeMachine, 
        LagCompensationSystem[] lagCompensationSystems)
     {
         _timeMachine = timeMachine;
         _systems = lagCompensationSystems;
     }

     public override void Execute(GameState gs)
     {
         //На вход кластеризатор принимает текущее игровое состояние,
         //а на выход выдает набор «корзин». В каждой корзине лежат энтити,
         //которым для лагкомпенсации нужно одно и то же время из истории.
         var buckets = _travelMap.RefillBuckets(gs);

         for (int bucketIndex = 0; bucketIndex < buckets.Count; bucketIndex++)
         {
             ProcessBucket(gs, buckets[bucketIndex]);
         }

         //В конце лагкомпенсации мы восстанавливаем физический мир 
         //в исходное состояние
         _timeMachine.TravelToTime(gs.Time);
     }

     private void ProcessBucket(GameState presentState, TimeTravelMap.Bucket bucket)
     {
         //Откатываем время один раз для каждой корзины
         var pastState = _timeMachine.TravelToTime(bucket.Time);

         foreach (var system in _systems)
         {
               system.PastState = pastState;
               system.PresentState = presentState;

               foreach (var entity in bucket)
               {
                   system.Execute(entity);
               }
          }
     }
}

Het enige dat overbleef was het configureren van de details:

1. Begrijp hoeveel u de maximale bewegingsafstand in de tijd moet beperken.

Het was belangrijk voor ons om de game zo toegankelijk mogelijk te maken in omstandigheden met slechte mobiele netwerken, dus beperkten we het verhaal met een marge van 30 ticks (met een tick rate van 20 Hz). Hierdoor kunnen spelers tegenstanders zelfs met zeer hoge pings raken.

2. Bepaal welke objecten in de tijd kunnen worden verplaatst en welke niet.

Wij verplaatsen uiteraard onze tegenstanders. Maar installeerbare energieschilden zijn dat bijvoorbeeld niet. We besloten dat het beter was om prioriteit te geven aan het verdedigende vermogen, zoals vaak wordt gedaan in online shooters. Als de speler al een schild in het heden heeft geplaatst, mogen vertragingsgecompenseerde kogels uit het verleden er niet doorheen vliegen.

3. Bepaal of het nodig is om de capaciteiten van de dinosaurussen te compenseren: bijten, staartaanvallen, enz. We hebben besloten wat er nodig was en verwerken ze volgens dezelfde regels als kogels.

4. Bepaal wat er moet gebeuren met de botsingen van de speler voor wie vertragingscompensatie wordt uitgevoerd. Op een goede manier mag hun positie niet naar het verleden verschuiven: de speler moet zichzelf zien in dezelfde tijd waarin hij zich nu op de server bevindt. We draaien echter ook de botsers van de schietende speler terug, en daar zijn verschillende redenen voor.

Ten eerste verbetert het de clustering: we kunnen dezelfde fysieke status gebruiken voor alle spelers met close-pings.

Ten tweede sluiten we bij alle raycasts en overlappingen altijd de botsers uit van de speler die de vaardigheden of projectielen bezit. In Dino Squad besturen spelers dinosaurussen, die volgens schietstandaarden een nogal afwijkende geometrie hebben. Zelfs als de speler vanuit een ongebruikelijke hoek schiet en de baan van de kogel door de dinosaurusbotser van de speler gaat, zal de kogel dit negeren.

Ten derde berekenen we de posities van het wapen van de dinosaurus of het toepassingspunt van de vaardigheid met behulp van gegevens van de ECS, zelfs vóór het begin van de vertragingscompensatie.

Als gevolg hiervan is de werkelijke positie van de botsers van de speler met vertragingscompensatie voor ons onbelangrijk, dus hebben we een productiever en tegelijkertijd eenvoudiger pad gevolgd.

Netwerklatentie kan niet zomaar worden verwijderd, maar alleen worden gemaskeerd. Net als elke andere vermommingsmethode heeft compensatie voor serververtraging zijn nadelen. Het verbetert de spelervaring van de speler die schiet, ten koste van de speler waarop wordt geschoten. Voor Dino Squad lag de keuze hier echter voor de hand.

Dit alles moest natuurlijk ook betaald worden door de toegenomen complexiteit van de servercode als geheel - zowel voor programmeurs als game-ontwerpers. Als de simulatie eerder een eenvoudige opeenvolgende oproep van systemen was, dan verschenen er met vertragingscompensatie geneste lussen en vertakkingen in. We hebben ook veel moeite gedaan om het gemakkelijk te maken om mee te werken.

In de 2019-versie (en misschien iets eerder) heeft Unity volledige ondersteuning toegevoegd voor onafhankelijke fysieke scènes. We hebben ze vrijwel onmiddellijk na de update op de server geïmplementeerd, omdat we snel wilden afrekenen met de fysieke wereld die alle kamers gemeen hebben.

We gaven elke speelkamer zijn eigen fysieke scène en elimineerden zo de noodzaak om de scène te ‘wissen’ uit de gegevens van de aangrenzende kamer voordat de simulatie werd berekend. Ten eerste zorgde het voor een aanzienlijke toename van de productiviteit. Ten tweede maakte het het mogelijk om een ​​hele reeks bugs te verwijderen die ontstonden als de programmeur een fout maakte in de scène-opruimcode bij het toevoegen van nieuwe spelelementen. Dergelijke fouten waren moeilijk te debuggen en hadden vaak tot gevolg dat de toestand van fysieke objecten in de scène van de ene kamer 'overvloeide' naar een andere kamer.

Daarnaast hebben we onderzoek gedaan naar de vraag of fysieke scènes gebruikt kunnen worden om de geschiedenis van de fysieke wereld vast te leggen. Dat wil zeggen: wijs voorwaardelijk niet één scène aan elke kamer toe, maar 30 scènes, en maak er een cyclische buffer van, waarin u het verhaal kunt opslaan. Over het algemeen bleek de optie te werken, maar we hebben hem niet geïmplementeerd: er was geen sprake van een gekke productiviteitsstijging, maar er waren nogal risicovolle veranderingen voor nodig. Het was moeilijk te voorspellen hoe de server zich zou gedragen als hij lange tijd met zoveel scènes zou werken. Daarom volgden wij de regel: “Als het niet kapot is, repareer het dan niet.

Bron: www.habr.com

Voeg een reactie