Тармактык кечиктирүү компенсация алгоритми менен мобилдик аткыч үчүн баллистикалык эсептөөнүн механикасын кантип жасадык

Тармактык кечиктирүү компенсация алгоритми менен мобилдик аткыч үчүн баллистикалык эсептөөнүн механикасын кантип жасадык

Салам, мен Никита Брижак, Pixonic компаниясынан сервер иштеп чыгуучумун. Бүгүн мен мобилдик мультиплеердин артта калуусунун ордун толтуруу жөнүндө айткым келет.

Сервердин артта калуусунун ордун толтуруу жөнүндө көптөгөн макалалар жазылган, анын ичинде орус тилинде. Бул таң калыштуу эмес, анткени бул технология 90-жылдардын аягынан бери көп оюнчу FPS түзүүдө активдүү колдонулуп келет. Мисалы, сиз биринчилерден болуп колдонгон QuakeWorld режимин эстей аласыз.

Биз аны мобилдик көп оюнчу аткыч Dino Squadда да колдонобуз.

Бул макалада менин максатым - буга чейин жазылгандарды миң жолу кайталоо эмес, технология стекибизди жана негизги оюн ойноо өзгөчөлүктөрүбүздү эске алуу менен биздин оюнда артта калуу компенсациясын кантип ишке ашырганыбызды айтып берүү.

Биздин кортекс жана технология жөнүндө бир нече сөз.

Dino Squad бул тармактык мобилдик PvP аткычы. Оюнчулар ар кандай курал-жарактар ​​менен жабдылган динозаврларды көзөмөлдөп, 6v6 командаларында бири-бири менен күрөшүшөт.

Кардар да, сервер да Биримдикке негизделген. Архитектура аткычтар үчүн абдан классикалык: сервер авторитардык, ал эми кардарлардын божомолу кардарларда иштейт. Оюндун симуляциясы ички ECS аркылуу жазылган жана серверде да, кардарда да колдонулат.

Эгерде сиз лаг компенсация жөнүндө биринчи жолу угуп жатсаңыз, анда бул маселеге кыскача экскурсия.

Көп оюнчу FPS оюндарында дал келүү адатта алыскы серверде симуляцияланат. Оюнчулар серверге киргизүүнү (басылган баскычтар жөнүндө маалыматты) жөнөтүшөт жана жооп катары сервер алынган маалыматтарды эске алуу менен аларга жаңыланган оюн абалын жөнөтөт. Бул өз ара аракеттенүү схемасы менен алдыга баскычын басуу менен оюнчунун каарманы экранда жылган учурдун ортосундагы кечигүү ар дайым пингден көбүрөөк болот.

Жергиликтүү тармактарда бул кечигүү (эл арасында киргизүү лаги деп аталат) байкалбай калышы мүмкүн, ал эми интернет аркылуу ойноп жатканда каарманды башкарууда "музда тайып кетүү" сезимин жаратат. Бул көйгөй мобилдик тармактар ​​үчүн эки эсе актуалдуу, мында оюнчунун пинги 200 мс болгон учур дагы эле эң сонун байланыш болуп эсептелет. Көбүнчө пинг 350, 500 же 1000 мс болушу мүмкүн. Андан кийин киргизүү лагы менен тез аткычты ойноо дээрлик мүмкүн эмес болуп калат.

Бул маселени чечүү кардар тарабынан симуляциялык болжолдоо болуп саналат. Бул жерде кардар өзү серверден жооп күтпөстөн, оюнчунун мүнөзүнө киргизүүнү колдонот. Ал эми жооп келгенде, ал жөн гана натыйжаларды салыштырып, оппоненттердин позицияларын жаңыртат. Бул учурда баскычты басуу менен натыйжаны экранда көрсөтүүнүн ортосундагы кечигүү минималдуу.

Бул жерде бир нюансты түшүнүү маанилүү: кардар ар дайым өзүнүн акыркы киргизүүсүнө жараша өзүнө тартат, ал эми душмандар - серверден алынган маалыматтардан мурунку абалга ылайык, тармак кечигүү менен. Башкача айтканда, душманга ок атканда оюнчу аны өзүнө карата өткөндө көрөт. кардар алдын ала тууралуу көбүрөөк Жогоруда жазган.

Ошентип, кардар болжолдоо бир маселени чечет, бирок экинчисин жаратат: эгер оюнчу душман мурда болгон чекитте, серверде ошол эле чекитте атканда, душман ал жерде болбой калышы мүмкүн. Сервердин артта калуусунун ордун толтуруу бул көйгөйдү чечүүгө аракет кылат. Курал атылганда, сервер атуу учурунда оюнчу жергиликтүү көргөн оюн абалын калыбына келтирет жана ал чындап эле душманды ура алабы же жокпу, текшерет. Эгер жооп "ооба" болсо, душман ошол учурда серверде жок болсо дагы, сокку эсептелет.

Бул билим менен куралданган биз Dino Squadда сервердик лаг компенсациясын ишке ашыра баштадык. Биринчиден, биз кардар көргөндү серверде кантип калыбына келтирүүнү түшүнүшүбүз керек эле? Анан так эмнени калыбына келтирүү керек? Биздин оюнда курал-жарак жана жөндөмдүүлүктөрдүн соккулары raycasts жана катмарлар аркылуу эсептелет, башкача айтканда, душмандын физикалык коллайдерлери менен өз ара аракеттенүү аркылуу. Демек, биз серверде оюнчу жергиликтүү "көргөн" бул коллайдерлердин позициясын кайра чыгарышыбыз керек болчу. Ал убакта биз Unity 2018.x версиясын колдонуп жатканбыз. Ал жерде физикалык API статикалык, физикалык дүйнө бир нускада бар. Анын абалын сактап, анан кутудан калыбына келтирүүгө эч кандай жол жок. Анда эмне кылуу керек?

Чечим үстүртөн эле, анын бардык элементтери башка көйгөйлөрдү чечүү үчүн биз тарабынан колдонулган:

  1. Ар бир кардар үчүн, ал баскычтарды басканда кайсы убакта оппоненттерин көргөнүн билишибиз керек. Биз буга чейин бул маалыматты киргизүү пакетине жазганбыз жана аны кардар божомолун тууралоо үчүн колдондук.
  2. Биз оюн мамлекеттеринин тарыхын сактай билишибиз керек. Анда биз каршылаштарыбыздын (демек, алардын коллидерлеринин) позицияларын кармайбыз. Бизде буга чейин серверде мамлекеттик тарых бар болчу, аны куруу үчүн колдонгон дельталар. Убакытты билип, тарыхта туура мамлекетти оңой эле таба алмакпыз.
  3. Азыр бизде тарыхтан алынган оюн абалы бар, биз оюнчу маалыматтарын физикалык дүйнөнүн абалы менен синхрондоштурууга жөндөмдүү болушубуз керек. Бар коллайдерлер - жылдыруу, жок болгондор - түзүү, керексиздер - жок кылуу. Бул логика да буга чейин жазылган жана бир нече ECS системаларынан турган. Биз аны бир Unity процессинде бир нече оюн бөлмөлөрүн өткөрүү үчүн колдондук. Жана физикалык дүйнө ар бир процессте бир болгондуктан, ал бөлмөлөрдүн ортосунда кайра колдонулушу керек болчу. Модельдештирүүнүн ар бир белгисинин алдында биз физикалык дүйнөнүн абалын "калыбына келтиребиз" жана аны учурдагы бөлмөнүн маалыматтары менен кайра инициализациялап, акылдуу топтоо тутуму аркылуу Unity оюн объекттерин мүмкүн болушунча кайра колдонууга аракет кылабыз. Болгону, мурунку оюндун абалы үчүн ошол эле логиканы колдонуу гана калды.

Бул элементтердин баарын чогултуу менен биз физикалык дүйнөнүн абалын керектүү учурга кайтара ала турган “убакыт машинасына” ээ болдук. Код жөнөкөй болуп чыкты:

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;
     }
}

Болгону, бул машинаны кантип колдонсо болорун, атуулардын жана жөндөмдөрдүн ордун толтуруу гана калды.

Эң жөнөкөй учурда, механика бир гана хитска негизделгенде, баары түшүнүктүү болуп көрүнөт: оюнчу атуудан мурун, ал физикалык дүйнөнү каалаган абалга кайтарып, рейкаст жасап, соккуну же сагынууну санап, жана дүйнөнү баштапкы абалына кайтаруу.

Бирок Дино отрядында мындай механизаторлор ото аз! Оюндагы курал-жарактардын көбү снаряддарды түзөт - бир нече симуляциялык кенелерге (айрым учурларда ондогон кенелерге) учкан узак мөөнөттүү октор. Алар менен эмне кылуу керек, алар качан учушу керек?

В байыркы макала Half-Life тармагынын стек жөнүндө, Valve жигиттери ушул эле суроону беришти жана алардын жообу мындай болду: снаряддын артта калуусунун ордун толтуруу көйгөйлүү жана андан качуу жакшы.

Бизде мындай вариант болгон эмес: снаряддын негизиндеги куралдар оюндун дизайнынын негизги өзгөчөлүгү болгон. Ошондуктан биз бир нерсе ойлоп табышыбыз керек болчу. Бир нече акыл чабуулунан кийин биз иштегендей көрүнгөн эки вариантты түздүк:

1. Снарядды аны жараткан оюнчунун убактысына байлайбыз. Сервер симуляциясынын ар бир белгиси, ар бир оюнчунун ар бир огу үчүн биз физикалык дүйнөнү кардар абалына кайтарып, керектүү эсептөөлөрдү жасайбыз. Мындай ыкма серверде бөлүштүрүлгөн жүктөмдү жана снаряддардын болжолдуу учуу убактысын түзүүгө мүмкүндүк берди. Алдын ала билүү биз үчүн өзгөчө маанилүү болду, анткени бизде бардык снаряддар, анын ичинде душмандын снаряддары кардарда алдын ала айтылган.

Тармактык кечиктирүү компенсация алгоритми менен мобилдик аткыч үчүн баллистикалык эсептөөнүн механикасын кантип жасадык
Сүрөттө, кене 30 оюнчу күтүү менен ракета атып жатат: ал душман кайсы тарапка чуркап жатканын көрүп, ракетанын болжолдуу ылдамдыгын билет. Жергиликтүү ал 33-кенеде бутага тийгенин көрөт. Кечиккен компенсациянын аркасында ал серверде да пайда болот

2. Биз бардыгын биринчи варианттагыдай кылып жасайбыз, бирок ок симуляциясынын бир белгисин эсептеп, биз токтобойбуз, бирок ошол эле сервердик белгинин ичинде анын учушун симуляциялоону улантабыз, ар бир жолу анын убактысын серверге жакындатат. бирден белги коюу жана коллайдердин позицияларын жаңылоо. Муну эки нерсенин бири болгонго чейин жасайбыз:

  • Октун мөөнөтү бүттү. Бул эсептөөлөр бүттү дегенди билдирет, биз мисс же хит санай алабыз. Жана бул ок атылган кенедей! Биз үчүн бул плюс да, минус да болду. Плюс - анткени атуу оюнчусу үчүн бул соккунун ортосундагы кечигүүнү жана душмандын ден соолугунун төмөндөшүн бир топ кыскартты. Жаман жагы, каршылаштар оюнчуга ок атканда да ушундай эле эффект байкалган: душман, сыягы, жай ракета гана атты, ал эми зыяны буга чейин эле саналып калган.
  • Ок сервердик убакытка жетти. Бул учурда, анын симуляциясы кийинки сервердик белгиде эч кандай лаг компенсациясы жок уланат. Жай снаряддар үчүн бул теориялык жактан биринчи вариантка салыштырмалуу физикалык артка кайтуулардын санын азайтышы мүмкүн. Ошол эле учурда симуляцияга бирдей эмес жүктөм көбөйдү: сервер же иштебей турган, же бир сервер белгисинде бир нече ок үчүн ондогон симуляциялык кенелерди эсептеп жаткан.

Тармактык кечиктирүү компенсация алгоритми менен мобилдик аткыч үчүн баллистикалык эсептөөнүн механикасын кантип жасадык
Мурунку сүрөттө эле сценарий, бирок экинчи схема боюнча эсептелген. Ракета атуу болгон белгиде сервердик убакытты "кутуп" алды жана соккуну кийинки кенедей эле санаса болот. 31-кечеде, бул учурда артта калган компенсация колдонулбайт

Биздин ишке ашырууда бул эки ыкма бир-эки код саптары менен айырмаланып турду, ошондуктан экөөнү тең түздүк жана көп убакыт бою алар параллелдүү болгон. Куралдын механикасына жана октун ылдамдыгына жараша ар бир динозавр үчүн тигил же бул вариантты тандап алдык. Бул жерде бурулуш учур механиктердин оюнунда пайда болгон: «Эгер сен баланча убакта душманды ушунча жолу чапсаң, баланча бонус ал» деген сыяктуу. Оюнчу душманга тийген учур маанилүү роль ойногон ар кандай механик экинчи ыкма менен иштөөдөн баш тартты. Ошентип, биз биринчи вариантка өттүк жана ал азыр бардык курал-жарактарга жана оюндагы бардык активдүү жөндөмдүүлүктөргө тиешелүү.

Өзүнчө, бул аткаруу маселесин көтөрүүгө арзырлык. Эгер мунун баары жайлатат деп ойлосоңуз, мен жооп берем: ошондой. Биримдик коллайдерлерди жылдырууда жана аларды күйгүзүүдө жана өчүрүүдө абдан жай. Дино отрядында, "эң начар" учурда, согушта бир эле учурда бир нече жүздөгөн снаряддар болушу мүмкүн. Ар бир снарядды өзүнчө эсептөө үчүн коллайдерлерди жылдыруу – бул кол жеткис люкс. Ошондуктан, физикалык "артка кайтуулардын" санын азайтуу биз үчүн абдан зарыл болгон. Бул үчүн биз ECSде өзүнчө компонент түздүк, анда биз оюнчунун убактысын жаздырабыз. Биз аны артта калгандардын ордун толтурууну талап кылган бардык субъекттерге коштук (снаряддар, жөндөмдөр ж.б.). Мындай объекттерди иштетүүнү баштоодон мурун, биз аларды ушул убакытка чейин кластерлейбиз жана аларды чогуу иштетип, физикалык дүйнөнү ар бир кластер үчүн бир жолу артка жылдырабыз.

Бул этапта бизде жалпы иштеген система бар. Анын коду бир аз жөнөкөйлөштүрүлгөн түрдө:

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);
               }
          }
     }
}

Болгону деталдарды конфигурациялоо гана калды:

1. Кыймылдын максималдуу аралыгын убагында канча чектөө керектигин түшүнүү.

Биз үчүн оюнду мобилдик тармактардын начар шартында мүмкүн болушунча жеткиликтүү кылуу маанилүү болчу, ошондуктан биз окуяны 30 кене (20 Гц ылдамдыгы менен) чектедик. Бул оюнчуларга өтө жогорку пингде да атаандаштарына сокку урууга мүмкүнчүлүк берет.

2. Кайсы объектилерди убагында жылдырса болот, кайсынысы жылдырылбастыгын аныктаңыз.

Биз, албетте, атаандаштарыбызды жылдырып жатабыз. Бирок, мисалы, орнотулган энергетикалык калканчтар андай эмес. Биз көбүнчө онлайн аткычтарда жасалгандай, коргонуу жөндөмдүүлүгүнө артыкчылык берүү туура деп чечтик. Эгерде оюнчу буга чейин эле калкан орноткон болсо, анда өткөн лаг-компенсацияланган октор ал аркылуу учпоого тийиш.

3. Динозаврлардын жөндөмдүүлүктөрүн компенсациялоо керекпи же жокпу, чечиңиз: тиштеп, куйрук чабуу ж.б. Биз эмне керек экенин чечтик жана аларды ок менен бирдей эрежелер боюнча иштетебиз.

4. Артыкчылыктын ордун толтуруу аткарылып жаткан оюнчунун коллайдерлери менен эмне кылуу керектигин аныктаңыз. Жакшы жагынан алганда, алардын позициясы өткөнгө өтпөшү керек: оюнчу өзүн азыр серверде турган убакта көрүшү керек. Бирок, биз атуу оюнчусунун коллайдерлерин да артка кайтарабыз жана мунун бир нече себептери бар.

Биринчиден, бул кластерлерди жакшыртат: биз жакын пинги бар бардык оюнчулар үчүн бирдей физикалык абалды колдоно алабыз.

Экинчиден, бардык рейкасттарда жана кайталанууларда биз ар дайым жөндөмдүүлүктөргө же снаряддарга ээ болгон оюнчунун коллайдерлерин чыгарабыз. Dino Squad оюнунда оюнчулар атуу стандарттары боюнча стандарттуу эмес геометрияга ээ болгон динозаврларды көзөмөлдөшөт. Оюнчу адаттан тыш бурч менен аткан күндө да жана октун траекториясы оюнчунун динозавр коллайдери аркылуу өтүп кетсе да, ок ага көңүл бурбай калат.

Үчүнчүдөн, биз динозаврдын куралынын позицияларын же ECS маалыматтарын колдонуу менен жөндөмдүүлүктүн колдонулуш чекитинин ордун толтуруу башталганга чейин эсептейбиз.

Натыйжада, лаг-компенсацияланган оюнчунун коллайдерлеринин реалдуу абалы биз үчүн маанилүү эмес, ошондуктан биз жемиштүү жана ошол эле учурда жөнөкөй жолду басып өттүк.

Тармактын кечигүү убактысын жөн эле алып салуу мүмкүн эмес, аны бир гана маскировкалоого болот. Маскөө кылуунун башка ыкмалары сыяктуу эле, сервердин артта калуусунун компенсациясынын да өз пайдасы бар. Атылган оюнчунун эсебинен атып жаткан оюнчунун оюн тажрыйбасын жакшыртат. Dino Squad үчүн, бирок, бул жерде тандоо ачык эле.

Албетте, мунун баары сервердик коддун татаалдыгы менен төлөнүшү керек болчу - программисттер жана оюн дизайнерлери үчүн. Эгерде мурда симуляция системалардын жөнөкөй ырааттуу чакырылышы болсо, анда артта калуу компенсациясы менен анда уяланган циклдер жана бутактар ​​пайда болгон. Аны менен иштөөгө ыңгайлуу болушу үчүн да көп күч жумшадык.

2019-жылдагы версиясында (жана балким бир аз мурдараак), Unity көз карандысыз физикалык көрүнүштөрдү толук колдоону кошту. Биз аларды серверге жаңыртуудан кийин дароо киргиздик, анткени биз бардык бөлмөлөр үчүн жалпы физикалык дүйнөдөн тез арылууну кааладык.

Биз ар бир оюн бөлмөсүнө өзүнүн физикалык көрүнүшүн бердик жана ошону менен симуляцияны эсептөөдөн мурун коңшу бөлмөнүн маалыматтарынан көрүнүштү “тазалоо” зарылдыгын жок кылдык. Биринчиден, ал өндүрүмдүүлүктүн бир кыйла жогорулашын берди. Экинчиден, жаңы оюн элементтерин кошууда программист сахнаны тазалоо кодунда ката кетирсе, пайда болгон мүчүлүштүктөрдүн бүтүндөй классынан арылууга мүмкүндүк берди. Мындай каталарды оңдоо кыйын болгон жана алар көп учурда бир бөлмөнүн көрүнүшүндөгү физикалык объектилердин абалынын экинчи бөлмөгө "агып кетишине" алып келген.

Мындан тышкары, биз физикалык көрүнүштөр физикалык дүйнөнүн тарыхын сактоо үчүн колдонулушу мүмкүнбү деген изилдөө жүргүздүк. Башкача айтканда, шарттуу түрдө ар бир бөлмөгө бир көрүнүштү эмес, 30 көрүнүштү бөлүп, алардан окуяны сактоо үчүн циклдик буферди жасаңыз. Жалпысынан алганда, вариант иштеп чыкты, бирок биз аны ишке ашырган жокпуз: ал өндүрүмдүүлүктүн акылга сыйбаган өсүшүн көрсөткөн жок, тескерисинче, кооптуу өзгөрүүлөрдү талап кылды. Мынчалык көп көрүнүштөр менен узак убакыт иштегенде сервер өзүн кандай алып барарын алдын ала айтуу кыйын болчу. Ошондуктан биз эрежени кармандык: "аны талкалап жок болсо, аны чечүү эмес,«.

Source: www.habr.com

Комментарий кошуу