ИгрыХабрахабр

Общая игровая логика на клиенте и сервере

На Pixonic DevGAMM Talks выступал еще наш DTO Антон Григорьев. Мы в компании уже говорили, что работаем над новым PvP-шутером и Антон поделился некоторыми нюансами архитектуры этого проекта. Он рассказал, как построить разработку, чтобы изменения в игровой логике клиента появлялись на сервере автоматически (и наоборот), и можно ли не писать код, но при этом минимизировать трафик. Ниже — запись и расшифровка доклада.

Не буду учить, как что-то делать, расскажу о том, как это делали мы. Чтобы вы не наступили на те же грабли и могли использовать наш опыт. Еще полтора года назад мы в компании не умели делать шутеры на мобилках. Вы скажете, как же так, у вас же есть War Robots, 100 млн загрузок, 1,5 млн DAU. Но в этой игре роботы очень медленные, а мы хотели сделать быстрый шутер и архитектура War Robots не позволяла этого.

Тогда мы наняли человека, у которого этот опыт был и сказали: сделай то же самое, что ты уже сто раз делал, только лучше. Мы знали, как и что делать, но у нас не было опыта. Потом сели и начали думать об архитектуре.

Думаю, многие знают, что это такое. Пришли к Entity Component System (ECS). Например, игрок, его пушка, какой-то объект на карте. Все объекты мира представлены сущностями. К примеру, компонент Transform — это положение игрока в пространстве, а компонент Health — это его здоровье. У них есть свойства, которые описываются компонентами. Обычно системы — это метод Execute(), который проходится по компонентам определенного типа и что-то делает с ними, с игровым миром. Есть логика — она отдельна и представлена системами. записывает его в Transform. Например, MoveSystem проходится по всем компонентам Movement, смотрит скорость в этом компоненте, параметр и на основе этого вычисляет новое положение объекта, т.е.

Когда разрабатываешь на ECS, нужно думать и делать по-другому. У такой архитектуры есть свои особенности. Помните этот ромбик с множественным наследованием в С++? Один из плюсов — это композиция вместо множественного наследования. В ECS этого нет. Все его проблемы.

Что это нам дает? Вторая особенность — это разделение логики и данных, про которое я уже говорил. Это всего лишь данные в памяти — мы можем в любое время изменить любое значение. Мы можем пачками хранить состояние мира и его историю, можем сериализовать, можем отправлять эти данные по сети и менять их в real-time. Таким образом очень удобно менять логику игры (или для дебага).

Все системы идут друг за другом, вызываются методом Execute() и, в идеале, должны быть независимыми. Также очень важно следить за порядком вызова систем. Одна система что-то меняет в мире, другая система потом это использует. На практике такого не бывает. Вероятно не сильно, но точно не так, как раньше. И если мы этот порядок нарушим — игра будет идти по-другому.

Наконец, одна из главных и самая важная для нас особенность — мы можем выполнять один и тот же код, как на клиенте, так и на сервере.

Думаю, многие так делали. Дай разработчику возможность, и он найдет 99 способов и причин делать свое решение, а не использовать уже существующие. Рассматривали Entitas, Artemis C#, Ash.net и собственное решение, которое могли бы написать по опыту пришедшего к нам специалиста. Мы на тот момент искали ECS Framework.

Важно то, сколько зеленого и красного в столбцах. Не пытайтесь прочитать, что написано на слайде, это не так важно. Зеленое — значит, что решение поддерживает требования, красное — не поддерживает, желтое — поддерживает, но не совсем.

Как видите, оно круче — мы могли бы поддерживать намного больше требований. В столбце ECS — потенциально наше решение. Выбрали архитектуру, долго работали, делали минимально играбельную версию и… факап. В итоге некоторые из них мы не поддержали (в основном, потому что они не понадобились), а некоторые, без которых мы не смогли работать дальше, пришлось сделать.

Игрока постоянно откатывало, тормоза, сервер зависал в середине матча. Получилась максимально неиграбельная версия. Какие были причины неудач? Играть в это было невозможно.

Но как же так? Причина №1 и самая важная — неопытность. Да, но на самом деле мы дали ему только часть работы. Мы же наняли опытного человека, который должен был сделать все красиво. А в нашей архитектуре (об этом чуть позже) очень важную роль играет клиент. Мы сказали: «Вот тебе гейм-сервер, работай над ним». Нет, он хороший программист, сеньор — просто не было опыта. И именно эту часть мы отдали человеку, у которого не было нужного опыта. он даже не представлял, какие там могут быть грабли. Т.е.

80 Кбайт/кадр. Причина №2 — нереальные аллокации. Если учесть, что у нас 30 кадров в секунду, то за секунду мы получаем 2,5 Мбайт, а за 5-минутный матч уже больше 600 Мбайт. Много это или нет? Garbage collector начинает усиленно пытаться всю эту память освободить (когда мы требуем от него больше и больше), что приводит к спайкам. Короче, много. Причем, как на клиенте, так и на сервере. Учитывая, что мы хотели 30 кадров в секунду, эти спайки нам мешали очень сильно.

Каждый раз практически на каждый кадр. Основная причина аллокаций заключалась в том, что мы постоянно аллоцировали массивы данных. Photon — это сетевая библиотека, с которой мы знакомы и используем в War Robots. Использовали LINQ, лямбда-выражения и Photon. И вроде все хорошо, но она аллоцирует память каждый раз, когда посылает данные или принимает их.

Можно было только уменьшить размер пакета, а он у нас был 5 Кбайт. Если с первыми проблемами мы разобрались (переписали на свои кастомные коллекции, сделали кэширование), то с Photon практически ничего нельзя было сделать, потому что это сторонняя библиотека. Да. Много? Он, примерно, 1,5 Кбайт, а у нас было 5 (это в среднем, было и больше). Есть MTU — это минимальный фактический размер пакета, который посылается по UDP, не разбивая пакет на мелкие части.

с гарантированной доставкой. Соответственно, Photon резал наш пакет на мелкие и отправлял каждый кусок как reliable, т.е. Мы получали еще большую задержку и сеть работала плохо. Каждый раз, когда часть не доходила, он посылал ее еще и еще раз.

А там же рендеринг, симуляция и другие действия — всё это занимает CPU. Все эти аллокации приводили к тому, что мы получали кадр около 100 миллисекунд, когда нужно было 33. нельзя решить было какую-то одну, и все станет хорошо. Все эти проблемы — комплексные, т.е. Нужно было решить их все сразу.

На слайде написано 5, но, мне кажется, их было даже больше. И еще небольшая проблема, которая была во время разработки — большое количество репозиториев. С этим было тяжело работать. Все эти репозитории (для клиента, гейм-сервера, общего кода, настройки и еще чего-то) подключались сабмодулями в два основных репозитория на клиент и гейм-сервер. Думаю, многие пытались научить художника или дизайнера работать с системой контроля версий. Программисты умеют работать с Git, SVN, но есть еще художники, дизайнеры и т.д. В нашем случае психанули даже программисты, и в итоге мы сократили всё до одного репозитория. Это реально тяжело, поэтому если ваш дизайнер умеет это делать — берегите его, он ценный сотрудник.

У нас там лежит папка с сервером и папка с клиентом. Это стало отличным решением проблемы. Сервер состоит из проекта гейм-сервера, генератора кода и вспомогательных инструментов.

Общий код — это структура данных о мире, т.е. Клиент — это Unity-клиент и общий код. Этот код, в основном, создается серверным генератором. Entities, компоненты и симуляция системы. Т.е. Его же использует сервер. это общая часть для клиента и сервера.

Берем TeamCity, натравливаем на наш репозиторий, собираем и деплоим сервер. Лайфках. Обычно есть сервер, клиент и какая-то фича. Каждый раз, когда клиент меняет общую логику, у нас тут же собирается гейм-сервер — теперь для этого не нужен серверный программист. В нашем случае не так — клиент может писать эту фичу и все работает на сервере. Клиент ее пилит у себя, сервер у себя, и когда-то у них это все заработает.

Они не связаны. Матч состоит из общей части (обозначена как ECS) и представления (это юнитивые MonoBehaviour классы, GameObject’ы, модельки, эффекты — все, чем представлен мир).

Как вы понимаете, это MVP (Model-View-Presenter) и любую из этих частей можно заменить, если понадобится. Между ними есть Presenters, который работает с обеими частями. Это сериализация информации о мире, сериализация ввода, отправка на сервер, получение сервером, коннект к серверу и т.д. Есть еще часть, которая работает с сетью (на слайде — Network).

Берем и заменяем эту часть посылкой не реальной, по сети, а виртуальной. Еще лайфках. Он реализует серверную симуляцию — теперь этот объект делает все, что происходило на гейм-сервере. Создаем некий объект внутри клиента и отправляем ему сообщения. Остальных игроков заменяем ботами.

Мы получили игру и возможность ее тестировать без гейм-сервера. Готово. Это значит, что художник, сделав новый эффект, может нажать кнопку Play в редакторе, сразу же на карте попасть в матч и увидеть, как это работает. Что это значит? Или же дебаг для клиентских программистов того, что они написали.

В итоге получили практически реальный матч без гейм-сервера. Но мы пошли дальше и приделали к этому слою эмуляцию пинга задержек сети jitter (это когда пакеты в сети доходят не в том порядке, в котором были отосланы) и другие сетевые штуки. Работает, проверено.

Вернемся к кодогенерации.

Есть свой domain-specific language, который на самом деле простой С# класс. Я уже говорил, что у нас есть кодогенератор в гейм-сервере. Мы помечаем его своими атрибутами. В данном случае класс Health. Он говорит, что Health — это компонент в нашем мире. Например, есть атрибут Component. Их можно написать руками, но он сгенерирует. На основе этого атрибута генератор создаст новый C# класс, в котором будет куча вещей. Есть атрибут типа DontSend, который говорит, что какое-то поле по сети посылать не обязательно — он не нужен серверу или не нужен клиенту. К примеру, метод добавления компонента в Entity, метод поиска компонентов, сериализацию данных и т.д. Что нам это дает? Или же атрибут Мах, сообщающий, что у игрока максимальное значение здоровья — тысяча. Такой кодогенератор позволил нам уменьшить размер пакета с 5 Кбайт до 1. Вместо поля, которое занимает 32 бита (int), мы посылаем 10 бит — в три раза меньше.

мы уложились в MTU. 1 Кбайт < 1,5 — т.е. Практически все ее проблемы у нас ушли. Photon перестал резать и сеть стала намного лучше. Но мы пошли дальше и сделали дельта-компрессию.

Не бывает такого, чтобы весь мир сразу полностью изменился. Это когда вы посылаете один полный стейт, а дальше только его изменения. Мы получили в среднем 300 байт, т.е. Постоянно меняются только какие-то части и эти изменения по размеру намного меньше самого стейта. в 17 раз меньше, чем было изначально.

Игра постоянно растет, появляются новые фичи, а вместе с ними появляются объекты, entity, новые компоненты. Зачем это нужно, если и так в MTU попадали? Если бы мы остановились на 1 Кбайт, то очень скоро вернулись бы к той же самой проблеме. Размер данных растет. Сейчас, переписав на дельта-компрессию, до этого мы дойдем очень не скоро.

Синхронизация. Теперь самая сладкая часть. Для каких-нибудь игр в жанре моба это нормально. Если вы играете в шутеры, то знаете, что такое Input Lag — когда нажимаете на кнопку, а персонаж начинает двигаться через какое-то время, например, полсекунды. Но в шутере вы хотите, чтобы герой тут же стрелял и наносил урон.

Клиент собирает ввод игрока (input) и отправляет его на гейм-сервер (отправка занимает время). Почему происходит Input Lag? Это и есть задержка. Дальше гейм-сервер его обрабатывает (снова время) и отправляет результат назад (опять же, время). Есть штука под названием prediction (предсказание) — клиент не ждет ответа от сервера и сразу начинает пытаться сделать то же самое, что делает гейм-сервер, т.е. Как ее убрать? Берет ввод игрока и начинает симуляцию. симулирует. Поэтому мы запускаем системы симуляцию только на нашем игроке. Мы симулируем только локального клиента, потому что не знаем ввода других игроков — они нам не приходят.

Клиент начинает симуляцию сразу же, как только получил ввод и находится на несколько шагов вперед, относительно гейм-сервера. Во-первых, это позволяет сократить время симуляции. В этот момент гейм-сервер симулирует тик №15 в прошлом. Допустим, на этой картинке он симулирует тик №20. Пока он пошлет 20-й тик на сервер, пока этот ввод дойдет, гейм-сервер уже начнет симулировать 18-й тик или уже 20-й. Клиент видит весь остальной мир, опять же, в прошлом, себя — в будущем. Если 18-й, то он положит его в буфер, дойдет до 20-го, обработает и вернет результат назад.

Обработал, возвращает результат на клиент. Допустим, сейчас он симулирует тик №15. Начинается сравнение с серверным. У клиента есть какой-то просимулированный 15-й тик, 15-й гейм-стейт и игровой мир, который он предсказал. Мы отвечаем только за себя. На самом деле он сравнивает не весь мир, а только своего клиента, потому что за весь остальной мир мы не отвечаем. Дальше продолжаем симулировать 20-й тик, 21-й и так далее. Если игрок совпал — все хорошо, значит мы правильно просимулировали, физика отработала верно и никаких столкновений не возникло.

Пример: так как физика не детерминированная, она посчитала неправильно нашу позицию или что-то произошло. Если клиент/игрок не совпал, значит, мы где-то ошиблись. Тогда клиент берет стейт с гейм-сервера, потому что гейм-сервер его уже подтвердил (он доверяет серверу — если бы не доверял, игроки бы читерили), и ресимулирует все остальные с 15-го по 20-й. Может быть просто баг. Потому что эта ветка времени теперь ошибочная.

параллельные миры. Создаем новую ветку времени, т.е. Когда-то у нас симуляция занимала 5 миллисекунд, но если нам нужно ресимулировать 10 тиков — это уже 50 миллисекунд и мы не попадаем в наши 30 миллисекунд. Ресимулируем эти пять тиков за один тик. Потому что там еще есть рендеринг. Оптимизировали и получили одну миллисекунду — теперь 10 тиков обрабатываются за 10 миллисекунд.

Минус — у нас был факап, а плюс — что программист теперь знает, как делать правильно. Все эти вещи работают на клиенте, а мы отдали это человеку без нужного опыта.

Клиент на левой картинке пытается выследить противника. Такая схема имеет свои особенности. Потому что пинг и клиент опережает сервер на 5 тиков. Он находится в 20-м тикe, противник находится в 15-м тикe. Но на сервере картина другая — когда сервер начнет симулировать 20-й тик, враг может уже сместиться. Клиент стреляет и должен точно попасть и нанести урон, может быть даже хэдшот. По идее, мы не должны попасть. Например, если противник двигался. В зависимости от пинга вероятность попадания тоже менялась: чем хуже пинг — тем хуже попадаете. Но если это так работало, то никто не играл бы в сетевые шутеры из-за постоянных промахов. Поэтому делают по-другому.

Сервер знает, когда это было, откатывает его на 15-й тик и видит левую картинку. Сервер берет и откатывает весь мир в тот тик, в котором видел мир игрок. Всё хорошо. Видит, что игрок должен был попасть, и наносит урон его противнику уже в 20-м тикe. Если противник бежал и забежал за препятствие, то мы хэдшотим уже через стену. Почти. Так оно работает, ничего с этим не поделаешь. Но эта известная проблема, игроки о ней знают и не парятся.

Сейчас на нашем сервере играет примерно 600 игроков одновременно. Итак, мы достигли 30 тиков в секунду, 30 кадров в секунду. около 100 матчей. В матче 6 игроков, т.е. Всю логику клиентщики пишут в редакторе Unity, Rider’e, на С# и оно работает на гейм-сервере. У нас нет серверного программиста, он нам не нужен. Мы сократили размер пакета в 17 раз и уменьшили аллокации памяти в 80 раз — теперь даже меньше килобайта на клиенте и сервере. Почти всегда. 200 — это стандарт для мобильных сетевых игр, в отличие от PC, где все происходит намного быстрее, особенно по локальной сети. Средний пинг был 200-250 мс, сейчас — 150.

Но пока об Open Source речи не идет. Мы планируем выделить написанное в отдельный фреймворк, чтобы использовать его на других проектах. Сейчас у нас 30 тиков в секунду, можем рисовать так, как тикает. И добавим туда интерполяцию. Соответственно, если мы будем рисовать 10 раз в секунду — персонажи будут двигаться рывками. Но есть игры, где хватает 20 тиков в секунду или 10. Мы написали собственную сетевую библиотеку вместо Photon — аллокаций памяти там нет. Поэтому нужна интерполяция.

Например, когда мы отправляем состояние мира клиенту, то вырезаем те данные, которые ему не нужны. Есть еще части, которые можно не писать руками, а генерировать код. На самом деле это можно генерировать, пометив каким-нибудь атрибутом. Пока мы делаем это руками и когда появляется новая фича, а мы забываем вырезать эти данные, то что-то идет не так.

Вопросы из зала

— Для кодогенерации вы что используете? Свое собственное решение?

Мы думали заюзать что-то готовое, но быстрее оказалось просто написать своими руками. — Всё просто — руки. Пошли по этому пути, это хорошо работало тогда и сейчас.

Unity же не поддерживает последнюю версию С#, у него собственный под капотом движок. — Вы отказались от серверного разработчика, но вы же не просто сократили время разработки за счет того, что один и тот же код переиспользуется. NET Core, вы не можете использовать последние фичи, определенные структуры и прочее. Вы не можете использовать . Разве от этого не страдает производительность где-то на треть?

Написали прототип, как это будет выглядеть в коде, как эти структуры будут программисты использовать для того, чтобы писать логику. — Когда начинали все это делать, мы думали, чтобы использовать не классы, а структуры, это должно было намного быстрее работать. Мы остановились на классах и той производительности, которая сейчас есть, нам достаточно. И оно оказалось жутко неудобным.

И как вы симулируете игрока, если снэпшот не пришел в нужный кадр? — Как вы живете сейчас без интерполяции?

Допустим у нас есть 18-й,19-й и 20-й стэйт. — У нас есть интерполяция, только она не визуальная, а на те пакеты, которые приходят по сети. Как раз используем кодогенерацию для того, чтобы не писать код интерполяции. Пришел 18-й, пришел 20-й, а 19-й либо потерялся, либо еще не дошел — вот его мы интерполируем.

— Есть ли еще лайфхаки, чтобы сильнее сжать кватернионы?

Из лайфхаков могу сказать еще такой: так как мы используем UDP, чтобы ввод не потерялся, мы отправляем его пачками: с нулевого по пятый ввод, потом с первого по шестой и так далее. — Я говорил про 2D — там просто угол альфа, а на новый проектах кватернионы есть и там свои проблемы. Это дешевле и доставка лучше.

— Но ведь порядок воспроизведения ввода на сервер играет роль?

Если какой-то ввод вдруг потерялся (хотя это маловероятно, у вас либо очень плохая сеть, либо она совсем отвалилась), то у нас 2 варианта: либо копировать предыдущий ввод, либо брать нулевой ввод. — Да, конечно.

А как это выглядит визуально? — Вы не симулируете на клиенте других игроков. Если пинг больше 1000 в районе секунды, что произойдет? Их не телепортирует? Просто переместит, при приходе следующего потока?

Но у нас сейчас стоит интервал в секунду, если клиент на секунду ушел дальше гейм-сервера, то мы просто разрываем соединение, он пытается переконнектиться. — Сейчас интерполяции нет, соответственно бывает дерганье. Это дешевле, чем ресимулировать все 30 тиков.

— Бывает ли у вас расхождение стейтов, как вы их находите и как боритесь?

Изначально, когда мы сравнивали очень многие вещи (сейчас мы сравниваем только локального игрока), у нас постоянно были мисспредикшены. — Конечно. В какой-то момент мы поняли, что некоторые вещи не нужно симулировать, повырезали их и сейчас все стало намного проще. Это порождает перепредикшены — ресимуляции, а это нагрузка на обработку, процессор. Там есть свои хитрости. А если, например, взять выстрелы, то мы их фактически предиктим только для того, чтобы визуально отобразить.

Как у вас организован этот баланс? — Насколько большая часть игры у вас именно на ECS, насколько жирные системы, насколько загружены сущности и компоненты?

80 на сервере на данный момент, может быть уже больше. — 30 систем на клиенте крутится, мы же только локального игрока симулируем. По сущностям сейчас точно не вспомню.

Когда мы на 20-м кадре что-то предсказали, а нам пришел стейт с сервера, что мы не могли стрелять и у нас есть какой-то пул команд — как вы потом накатываете, как мержите? — Вопрос по расхождению в prediction. Или есть какие-то общие решения? Ощущение, что там для каждого отдельного случая свое специфическое решение.

И на его основе (допустим, 15-го) ресимулируешь 16-й,17-й,18-й и так далее. — Общее решение простое: берешь стейт с сервера и подставляешь его как последний заапрувленный.

— На основе команд своего пула команд?

Но есть некоторые вещи, например, выстрелы. — Да, мы храним историю инпутов клиента и храним историю состояния мира. И так как эта генерация объектов происходит на клиенте — на сервере таких выстрелов может быть больше, от других игроков в том числе. Выстрелы генерируют много сущностей Entity (на каждый выстрел практически), которые мы там отображаем. ID могут расходиться, у нас есть свои хаки на этот счет.

Я так понимаю, в каждом отдельном случае отдельное решение? — Если какой-то выстрел из базуки — мы хотим нарисовать эту ракету, а потом оказывается, что мы не могли стрелять, мы должны ее удалять?

В 3D когда ты кидаешь гранату, там какие есть хаки — ты начинаешь ее кидать, ее не видно еще на экране и только когда она создастся на сервере, пройдет какое-то время. — Да, например, есть граната. У нас top-down, поэтому сложнее — там гранату видно сразу. Но ты ее не будешь видеть, потом она появится, и вроде бы все хорошо. На клиенте ее создаем сразу, туда же запускаем, но когда она приходит с сервера реально, мы интерполируем ее путь. Мы тоже написали со своими хитростями. В итоге все выглядит нормально.

— А если нельзя было запустить эту гранату?

Такое тоже бывает. — Она просто исчезает.

В какой-то момент сервер берет инпут со всех клиентов и симулирует. — Вопрос по поводу момента, когда все клиенты посылают на сервер свой инпут, он собирается в какой-то буфер. Как это резолвится? Но если какой-то клиент отстает, но отстает не на целую секунду, а просто скачек, 500 миллисекунд, задержка, и у сервера в какой-то конкретный момент нет инпута какого-то клиента.

— Сейчас покажу.

он симулирует 20-й тик и берет 20-й инпут, затем кидает его в будущее серверу. Клиент забрасывает инпут в будущее, т.е. Он пытается предугадать: если я сейчас 20-й тик пошлю, сервер в этот момент дойдет до этого тика или нет? Насколько в будущее — это отдельная сложная штука, число варьируется в зависимости от пинга. Соответственно, если он послал чуть дальше — когда-нибудь сервер до него просто дойдет. Есть опять же инпут буфер на сервере, куда эти инпуты складываются. Клиент потом от сервера получит акк, что «я сейчас обработал твой такой-то инпут, не 21-й, а 18-й». Если он послал за этот буфер, то инпут потеряется. Такая гибкая система. Сервер: «ага, значит мне нужно чуть-чуть ужаться».

они могут иногда пропадать, но потом он подгоняет клиента и клиент посылает более свежий инпут? — Т.е.

— Да, клиент пытается адаптироваться в этой ситуации.

— Вы мельком упомянули reliable UDP — у вас какая-то своя реализация?

— Это Photon, в Photon есть reliable UDP, unreliable, c гарантированной и без гарантированной доставкой.

— Гарантированно доставляются все пакеты?

Дойдут они или не дойдут, для сетевой игры не так важно. — У нас не гарантированно, просто посылаются куда-то. Чтобы они не потерялись, мы отправляем их пачками. Например, клиент отправляет инпуты. Если пакет потерян на 100%, то естественно ничего не дойдет, если на 80%, то скорее всего дойдет. Если мы отправляем пачками по пять, то как минимум пять раз он туда будет послан.

— А повторно?

— Нет, повторно не отправляем, Photon это делает сам, если размер пакета больше MTU.

Чем его поддерживать? — Правда ли от кодогенерации польза больше?

Надо нам это или нет. — Когда человек, который его писал, только начал, у нас с ним были дискуссии. Тогда это было неэффективно, а сейчас наоборот, потому что практически ничего не нужно делать, ты просто атрибуты шлепаешь. Как оказалось, действительно надо.

— А пример, где можно выиграть время, где можно управляемость получить?

Если бы у тебя кодогенератора не было, ты бы пошел писать код. — Ты увидел параметр, который тебе не нужно отсылать. Потом, когда-нибудь ты это обнаружишь. Забыл, всё, он посылается. Или же параметр, который можно посылать с меньшим размером. А здесь ты атрибут повесил, всё автоматом работает.

И если я забуду написать код, то и атрибут забуду поставить. — Просто из моей практики написать код запись/чтение из стрима — это раз в месяц где-то пять минут.

Кстати, помимо сериализации (мы генерируем еще и сериализацию), у нас есть некий вьювер на гейм-сервере, который позволяет видеть мир таким, какой он есть на гейм-сервере. — Игра постоянно развивается, соответственно таких компонентов появляется довольно много и постоянно приходится шлепать это дело. Поля можно менять — эта часть, она тоже генерируется. Все объекты, все их поля.

Вы не думали сделать ее детерминированной? — У вас физика не детерминированная, соответственно какие-то проблемы возникают.

Но в начале работы над проектом у нас был выбор по трем пунктам: ECS, физика и сетевая библиотека. — Сделать детерминированной не собираемся, хотя идеи такие были. Сейчас очень рады, что написали ECS сами. Физику и сетевую библиотеку мы взяли готовые, ECS написали сами. Как оказалось, ту физику, которую мы взяли, писал студент (мы тогда посмотрели, вроде бы нормально, а когда начали работать, глубже копать, оказалось не совсем то, что нам нужно). Очень больно, что не написали сетевую библиотеку и физику. Фактически 3D физику для этих вещей, объектов, мы пишем сами. Плюс у нас 2D игра, но бывают вещи, которые нужно делать в 3D — мы их пишем сами. Там будут какие-то расхождения, но они будут минимальными. И детерминизм в такой архитектуре не нужен. Если есть детерминизм, тогда и не нужно посылать с гейм-сервера гейм-стейт, достаточно просто инпутов от всех игроков.

Насколько я понял, позволяет то, что везде язык C#? — В докладе был тезис, что ECS позволяет запускать код и на клиенте, и на сервере.

— В первую очередь — да.

в принципе это к EСS не относится? — Т.е. Т.е. Как я понимаю, ECS — это просто способ написать игровую модель, в итоге она выплевывает стейт мира и не важно, как я это написал, если на выходе получаю то же самое. ECS — это правильный способ писать модель, чтобы в ней не запутаться.

Есть у него и плюсы, и минусы. — Я не скажу, что правильное, это просто один из способов. Потому что те, кто раньше использовали OОП или какие-то другие подходы, им тяжело будет понять, как и что делать. Основной минус — человека надо научить, как правильно это делать, как на нем готовить.

— Получается, вы использовали ECS-подход как парадигму?

Мы оптимизацию не делали, не стали писать на структурах — писали на классах. — Если по-хорошему, ECS это не только парадигма, которую мы использовали, это еще (если писать на структурах) довольно сильная оптимизация — и по памяти, и по многим другим вещам. Мы можем с ними работать, посылать по сети, принимать, сравнивать и т.д. Главное — это подход и то, что у нас есть данные, которые отделены от логики.

Еще доклады с Pixonic DevGAMM Talks

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть