Организация данных при реализации "тумана войны"
От: johny5 Новая Зеландия
Дата: 22.06.07 02:31
Оценка: 5 (1)
Пишу сюда, потому что проблема и решение C++ specific. Если не прав, переместите куда либо ещё (только не в игровой форум плз!!).

Краткая информация. Пишу космическую онлайн стратегию. Клиент-Серверная модель.

Игра начинается так: собираются несколько игроков, генерят себе игровую партию, играют. Игровая партия длиться в среднем 1,2 месяца. Потом выявляется победитель и начинается новая партия. С точки зрения софта, игровая партия это набор данных обо всей игровой вселенной.
Обсчёт игровой партии это продвижение модели на 1 квант времени вперёд.
В игровой партии есть флоты.
Флот состоит из списка кораблей.
Корабль ссылается на инфу о корабельном дизайне.
Корабельный дизайн в свою очередь ссылается на корпус и список корабельных компонентов.


Хочу обсудить проблемы, которые возникают при попытке реализовать концепцию "туман войны" и прочих факторов, которые ведут к разному представлению игровой партии на клиенте и сервере.


Существующие проблемы модели
1. Поддержка передачи вычислимой инфы без передачи аргументов для вычислений
2. Явное указание аттрибутов, которые видны игроку в данный момент
3. Поддержка "истории" аттрибутов для конкретного игрока
4. Поддержка хранения различных аттрибутов в данных о флоте, которые специфичны только серверу или только клиенту и предназначены для сериализации на винт но не предназначены для передачи по сети.


  1. В глобальной игровой партии количество кораблей во флоте можно легко получить просто спросив ships.size().

    Теперь вступает в роль туман войны.
    Игрок в игровом мире видит не всё, но было бы всё просто если бы видимость делилась на вижу/не вижу. Она делится на более плавные градиенты, в частности, возможно такое, что игрок будет видить вражеский флот и количество его кораблей но не видеть из каких кораблей он состоит.
    Это приводит к тому, что в ships я не могу передать ничего, потому что в контейнере лежат конкретные описания кораблей, а просто заполнять всё это мусором лишь бы только ради размера ну как то будет странно. Кроме того, для некоторых случаев, например коэффициент невидимости флота будет явно виден вражьим игроком, хотя в модели он задаётся длинным путём через ships -> дизайн_корабля -> корабельные_модули -> модули_прозрачности да ещё и по отношении к общей массе флота. В последнем случае всё это "эмулировать" будет очень тяжело.

  2. Серверу нужно как то указывать, какие аттрибуты из переданного класса игрок видит в данный момент а какие нет. Логикой выбора "видимых аттрибутов" по идее занимается сервер.
    Но я не могу выкинуть из класса аттрибуты, которые на самом деле клиент не "видит", т.е. не имеет информации о них. Идея пустоты заполнять '-1'-ами ломается при попытке заполнить ими какойнть вектор с классами, причём когда пустой вектор это тоже валидное состояние. Использовать boost::optional для каждого мембера это имхо кощунство, кроме того это не лечит от проблемы №3

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

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




Для решения вышеобозначенных проблем, я пробовал 2 выхода из этой ситуации

  1. Разбил параметры флота на первичные и вычислимые, вычислимые я стараюсь пересчитывать каждый раз, когда что то меняется в первичных данных флота либо в окружении флота, т.е. в данных партии. Естественно я делаю всё это ручками. И естественно, при передаче по сети я всё время имею проблемы "не забыть пересчитать" (из-за сети я не могу сделать callback привязки, потому что в момент передачи сам флот оказывается как бы "отвязан" от партии и поэтому всё равно всё приходится делать ручками".
    И первичные и вычислимые являются сериализуемыми(!). Естественно вылазит гемор с постоянной проверкой целостности модели после чтения с винта, передачи по сети (защита от юных хакеров), потому что для своего флота игрок будет видеть как ships.size() так и ship_num и они должны быть всегда равными.


  2. Я сделал в модели понятие EnemyFleet. Из за этого у меня класс GameParty разбился на 2 класса, ServerGameParty и ClientGameParty. Из за этого же, во многих местах клиенского кода у меня повылазило большое дублирование кода, например выдача информации о флоте, когда изменяется лишь способ получения с дополнительными проверками аттрибута "видимости", т.е. что на самом деле из полученной информации игроку сейчас видно.
    Кроме того, усложнился код проекта вообще, многие классы, которые могли работать с GameParty, теперь вынуждены поддерживать оба класса.
    Сервер получил код конвертации данных из ServerGameParty в ClientGameParty и обратно, в общем телега начала еле проворачиваться.


Для 3й и 4й проблемы я сделал над-структуры, которые ссылаются на данные партии.



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


Какие решения в данном случае могли бы быть, что вы посоветуете? Как эти проблемы решаются в реальных игровых проектах?

Спасибо.



22.06.07 12:15: Перенесено модератором из 'C/C++. Прикладные вопросы' — Хитрик Денис
Re: Организация данных при реализации "тумана войны"
От: Аноним  
Дата: 22.06.07 06:03
Оценка:
На вскидку попробывать реализовать что-то вроде Doc\View
Re: Организация данных при реализации "тумана войны"
От: dr.Chaos Россия Украшения HandMade
Дата: 22.06.07 07:52
Оценка:
Здравствуйте, johny5, Вы писали:



    J>
  1. В глобальной игровой партии количество кораблей во флоте можно легко получить просто спросив ships.size().

    J>Теперь вступает в роль туман войны.

    J>Игрок в игровом мире видит не всё, но было бы всё просто если бы видимость делилась на вижу/не вижу. Она делится на более плавные градиенты, в частности, возможно такое, что игрок будет видить вражеский флот и количество его кораблей но не видеть из каких кораблей он состоит.
    J>Это приводит к тому, что в ships я не могу передать ничего, потому что в контейнере лежат конкретные описания кораблей, а просто заполнять всё это мусором лишь бы только ради размера ну как то будет странно. Кроме того, для некоторых случаев, например коэффициент невидимости флота будет явно виден вражьим игроком, хотя в модели он задаётся длинным путём через ships -> дизайн_корабля -> корабельные_модули -> модули_прозрачности да ещё и по отношении к общей массе флота. В последнем случае всё это "эмулировать" будет очень тяжело.

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

    J>
  2. Серверу нужно как то указывать, какие аттрибуты из переданного класса игрок видит в данный момент а какие нет. Логикой выбора "видимых аттрибутов" по идее занимается сервер.
    Тут такое дело, если у клиента есть все корабли, то это можно сделать проблемой клиента, т.к. это проблема отображения модели. Если же это делать на сервере, то передавать надо только набор видимых кораблей -> меньше трафик. Сам рассчет зависит от расположения объектов, вычислить контур видимости лучше все-таки на клиенте, ИМХО, а потом уже раздать уровни на месте или передать серверу.

    J>
  3. Т.к. набор информации о флоте может значительно варьироваться, для клиента достаточно важно сохранять старые значения, т.к. старое лучше чем вабще ничего. Но для этого нужно будет помечать каждый аттрибут, насколько он стар и есть ли данные у аттрибута вообще.

    Ну тут такое дело, что изменяется объект целиком, соответственно можно хранить его предыдущее состояние в мен самом глянь паттерн Memeto(хранитель). Т.е. сделать чтобы сохраняли свое состояние более высокоуровневые сущности.

    J>
  4. Во время обсчёта партии возникает необходимость сохранять временные данные в модели, которые для клиента совершенно не важны. Например количество выделенных уже ресурсов на выполнение какой то команды флота, очень нужная штука для "откатов" если команда будет вдруг отменена. Но это количество ресурсов игроку не нужны. максимум — ему нужно будет передавать процент выполнения команды.
    J>Такие данные также необходимо сериализовать на винт, потому что при перезапуске сервера, всё должно работать в точности так же как было до останова.

    Глянь паттерн Command(команда), ИМХО то что нужно.
Побеждающий других — силен,
Побеждающий себя — Могущественен.
Лао Цзы
Re: Организация данных при реализации "тумана войны"
От: ALSK  
Дата: 22.06.07 13:47
Оценка: 1 (1)
Здравствуйте, johny5, Вы писали:

J>4. Поддержка хранения различных аттрибутов в данных о флоте, которые специфичны только серверу...



J>b) Я сделал в модели понятие EnemyFleet. Из за этого у меня класс GameParty разбился на 2 класса, ServerGameParty и ClientGameParty. Из за этого же, во многих местах клиенского кода у меня повылазило большое дублирование кода, например выдача информации о флоте, когда изменяется лишь способ получения с дополнительными проверками аттрибута "видимости", т.е. что на самом деле из полученной информации игроку сейчас видно.

J>Кроме того, усложнился код проекта вообще, многие классы, которые могли работать с GameParty, теперь вынуждены поддерживать оба класса.
J>Сервер получил код конвертации данных из ServerGameParty в ClientGameParty и обратно, в общем телега начала еле проворачиваться.

Если я правильно понял, то почему бы не сделать базовый абстрактный EnemyFleet и две разновидности от него ServerEnemyFleet и ClientEnemyFleet. Тогда GameParty можно параметризовать флотом, передавая экземпляр флота в GameParty снаружи, либо пусть GameParty запрашивает флот у фабрики. Возможно всю специфику можно локализовать в этих флотских классах. Какие-то функции можно сделать пустыми. Доступ к аттрибутам сделать через get/set методы, и если аттрибут отсутствует, то эти методы будут ничего не делать.
Re: Организация данных при реализации "тумана войны"
От: Константин Л. Франция  
Дата: 24.06.07 10:23
Оценка: 1 (1)
Здравствуйте, johny5, Вы писали:

[]

J>
  • В глобальной игровой партии количество кораблей во флоте можно легко получить просто спросив ships.size().

    J>Теперь вступает в роль туман войны.

    J>Игрок в игровом мире видит не всё, но было бы всё просто если бы видимость делилась на вижу/не вижу. Она делится на более плавные градиенты, в частности, возможно такое, что игрок будет видить вражеский флот и количество его кораблей но не видеть из каких кораблей он состоит.
    J>Это приводит к тому, что в ships я не могу передать ничего, потому что в контейнере лежат конкретные описания кораблей, а просто заполнять всё это мусором лишь бы только ради размера ну как то будет странно. Кроме того, для некоторых случаев, например коэффициент невидимости флота будет явно виден вражьим игроком, хотя в модели он задаётся длинным путём через ships -> дизайн_корабля -> корабельные_модули -> модули_прозрачности да ещё и по отношении к общей массе флота. В последнем случае всё это "эмулировать" будет очень тяжело.

    ну заведи классы-коллекции, и внутри Ships::size() созвращай фейковые значения

    J>
  • Серверу нужно как то указывать, какие аттрибуты из переданного класса игрок видит в данный момент а какие нет. Логикой выбора "видимых аттрибутов" по идее занимается сервер.
    J>Но я не могу выкинуть из класса аттрибуты, которые на самом деле клиент не "видит", т.е. не имеет информации о них. Идея пустоты заполнять '-1'-ами ломается при попытке заполнить ими какойнть вектор с классами, причём когда пустой вектор это тоже валидное состояние. Использовать boost::optional для каждого мембера это имхо кощунство, кроме того это не лечит от проблемы №3

    предлагаю следать следующее. Все свойства класть в список, который можно фильтровать перед посылкой клиенту. Каждое свойство, в свою очередь, должно содержать список предыдущих значений:

    
    class Property
    {
    public:
    
       typedef std::list<std::string> PropContainer;
    
    private:
    
       PropContainer History_;
    
    public:
    
       template <class Iter>
       Property( Iter const& begin, Iter const& end )
                : History_(begin, end)
       {}
    
       Property( std::string const& value )
                : History_(value, 1) //так вроде можно у list'а
       {}
    
       std::string Value() const { return History_.front(); } //подразумевается, что History_ полюбому не пусто
    
       PropContainer const& History() const { return History_; } 
    };
    
    class ObjectwithProperties
    {
        typedef std::map<std::string, Property> PropContainer;
        PropContainer Props_;
    
    public:
    
        Property GetProperty( std::string name ) const
        {
             PropContainer filtered = Filter(Props_); //тут оставляем только видимые свойства
             PropContainer::const_iterator it = filtered.find(name);
             if( filtered.end() != it ) return it->second;
             return Property();
        }
    
    protected:
    
        ~ObjectwithProperties(){}
    };



    где-то так,

    J>
  • Т.к. набор информации о флоте может значительно варьироваться, для клиента достаточно важно сохранять старые значения, т.к. старое лучше чем вабще ничего. Но для этого нужно будет помечать каждый аттрибут, насколько он стар и есть ли данные у аттрибута вообще.

    J>
  • Во время обсчёта партии возникает необходимость сохранять временные данные в модели, которые для клиента совершенно не важны. Например количество выделенных уже ресурсов на выполнение какой то команды флота, очень нужная штука для "откатов" если команда будет вдруг отменена. Но это количество ресурсов игроку не нужны. максимум — ему нужно будет передавать процент выполнения команды.
    J>Такие данные также необходимо сериализовать на винт, потому что при перезапуске сервера, всё должно работать в точности так же как было до останова.
    J>[/list]

    ну про паттерны тебе уже сказали
  • Re: Организация данных при реализации "тумана войны"
    От: wildwind Россия  
    Дата: 24.06.07 19:57
    Оценка: +1
    Здравствуйте, johny5, Вы писали:

    Не буду бросаться громкими названиями паттернов, приведу только простые соображения. По-моему, решение можно построить на двух принципах.
    1. Клиент видит и знает только то, что отдает сервер. И все, что когда-то увидел, запоминает.
    2. Сервер отдает клиенту только то, что тот может видеть. Что может, а что нет, определяется на основании множества факторов; определяется при обсчете мира.

    При таком подходе сразу решается вопрос с "юными хакерами", так как ломать клиента или сетевой протокол бессмысленно, больше данных чем видно в игре, там все равно нет. Ломать можно только сервер.
    Сервер перед отправкой данных каждому клиенту, конечно, проверяет, что изменилось (для этого клиента). Тут можно реализовать какую-нибудь модель разграничения доступа. С помощью ACL или атрибутов, как в *nix. Например, каждый объект (корабль, флот и т.д.) имеет атрибуты доступа или видимости: для друзей, для врагов, для всех. Сервер же должен реализовать имперсонацию, то есть работу с данными в контексте клиента. То есть: сервер проверяет, что флот игрока N находится в пределах видимости игрока M (для которого сейчас готовится порция данных). Дальше сервер запрашивает характеристики флота, состав и т.д. Но делает это от имени игрока M. В каждый метод GetXXX() передается контекст M. Методы проверяют права доступа и возвращают нужные значения или ошибку. Если ошибка, то атрибуты в объектах для M помечаются как "неизвестные". Тут IMHO boost::optional или аналог не кощунство, а как раз то что нужно.
    Re: Организация данных при реализации "тумана войны"
    От: johny5 Новая Зеландия
    Дата: 25.06.07 00:08
    Оценка:
    В общем я прочитал всё что здесь написано, по вашим описаниям выглядит действительно проще
    Я понял свою ошибку — я залип на статической типизации, которая C++ нам предлагает. Перейдя на виртуальные иерархии я во многом упрощу себе жизнь.

    В общем есть над чем подумать, спасибо!
    Re[2]: Организация данных при реализации "тумана войны"
    От: WolfHound  
    Дата: 25.06.07 04:54
    Оценка: 1 (1) +1
    Здравствуйте, johny5, Вы писали:

    J>В общем есть над чем подумать, спасибо!

    Я тебе предлагаю подумать в другом направлении.
    А именно метапрограммирование.
    Только не пытайся знаниматься им на С++ это себе дороже.
    Создай DSL хоть на базе XML'ного файлика и по нему генери код.
    Либо можно взять болие мощьный язык. Сделать DSL не нем и генерить код на С++.

    Хотя лично я на С++ такие вещи делать бы не стал. Ибо пользы от С++ в данной задаче нет, а гемороя...

    И еще. ИМХО в таких играх нет данных которые можно хранить на клиенте. Исключение только локальные настройки типа разрешение экрана.
    Это нужно чтобы игроку небыло мучительно больно если у него полетит винт. Или он захочет зайти с другого компа.
    И на сервере нужно озаботиться репликацией в реальном времени ну или хотябы раз в несколько (5 — 10) минут.
    Если игра пошоговая то после каждого шага.
    ... << RSDN@Home 1.2.0 alpha rev. 673>>
    Пусть это будет просто:
    просто, как только можно,
    но не проще.
    (C) А. Эйнштейн
    Re[3]: Организация данных при реализации "тумана войны"
    От: johny5 Новая Зеландия
    Дата: 25.06.07 07:15
    Оценка:
    WH>Создай DSL хоть на базе XML'ного файлика и по нему генери код.
    WH>Хотя лично я на С++ такие вещи делать бы не стал. Ибо пользы от С++ в данной задаче нет, а гемороя...

    Я от C++ давно решил уже отказываться — надоел он мне. Но до DSL я пока ещё не дорос.
    К сожалению проект уже глубоко погружен в C++ код, тут два пути, дописать или всё бросить. Что то внедрять революционное на данном этапе может оказаться дороже.


    WH>И еще. ИМХО в таких играх нет данных которые можно хранить на клиенте. Исключение только локальные настройки типа разрешение экрана.

    WH>Это нужно чтобы игроку небыло мучительно больно если у него полетит винт. Или он захочет зайти с другого компа.

    Да, это так и есть, на сервере будет храниться всё что игрок будет делать у себя в клиенте, переименовывать чужие объекты, давать приватные комментарии и даже, возможно, layout панелей для каждого разрешения.

    Т.е. сейчас уже сервак хранит глобальный образ партии и кучу локальных образов на каждого игрока и ворочает всем этим.
    Обсчёт соответственно в 3 этапа


    А локальная копия локального образа в основном нужна для оптимизации траффика клиент <-> сервер. Объём данных всей партии много раз больше чем пакет обновления изменений за 5-10 секунд (игра — медленный реалтайм).
    Re[4]: Организация данных при реализации "тумана войны"
    От: WolfHound  
    Дата: 25.06.07 08:01
    Оценка: 1 (1) +1 :)
    Здравствуйте, johny5, Вы писали:

    J>Я от C++ давно решил уже отказываться —

    Это разумно.

    J>надоел он мне.

    А это нет. Должны быть болие весомые причина чем надоел/не надоел.
    И они есть. Но их нужно понимать.

    J>Но до DSL я пока ещё не дорос.

    Я бы на твоем месте всетки дорос до ДСЛ. Хотябы XML'ного.
    Ибо судя по твоему описанию структура у тебя далека от завершения и будет переделываться еще не один раз.
    Так вот переделывать генератор на много проще и быстрее чем переделывать кучу кода.
    Факт медицинский.

    J>К сожалению проект уже глубоко погружен в C++ код, тут два пути, дописать или всё бросить. Что то внедрять революционное на данном этапе может оказаться дороже.

    Ну переводить все на другой язык это конечно слишком.
    А вот сделать генерацию структур данных и алгоритмов с которыми ты судя по всему еще и близко не определился очень даже стоит.
    Ибо большое колличество тупого кода генерировать сильно дешевле чем писать.

    Болие того ты сможешь попробовать кучу вариантов.
    Ибо в случае с генератором тебе нужно будет внести изменение в одно место, а в случае с рукопашным кодом придется менять кучу мест с перспективой что-то забыть или где то опечаться.
    А добавление новых объектов в случае с генератором вобще тривиально.

    Сам генератор вполне можно писать на чемто по умней чем С++.
    Я бы взял немерле. (Надо посмотреть что там VladD2 со стринг-темплейт навоял. Возможно это наиболие простой способ.)
    Ты можешь взять то что тебе больше нравится.

    J>Обсчёт глобальной партии

    Это структура данных номер раз.
    По ней должны происходить все расчеты.
    Тут все должно быть доступно.
    Эта структура должна жить только на сервере.

    J>Построение клиентских образов по "туману войны"

    Структура данных номер 2.
    Тут должны быть опциональные параметры + история.
    На основе той информации что ты дал я бы ограничился последним извесным значением + время когда оно было получено.
    Тут никаких расчетов быть не должно. Кроме возможно экстраполяции при рендере для обеспечения плавности движения.
    Клиент работает с этой и только с этой структурой.

    Соответственно нужен код конвертации первой структуры во вторую с учетом тумана войны.
    Писать его руками задолбаешься.

    J>Генерация исходящих сетевых diff сообщений по сравнению 2х клиентских образов

    Код генерации и наложения diff'а тоже проще всего сгенерить.
    Ибо он туп и его "написание" легко автоматизировать.

    Также тебе будет нужна сераилизаци/десериализация обоих структур для сохранения на винт.
    И это тоже очень лего сгенерировать и очень муторно сделать както иначе.

    И вобще делать руками то что может сделать программа глупо.
    Ну может быть за исключением одноразовой и очень не большой по объему работы. У тебя судя по всему далеко не этот случай...
    ... << RSDN@Home 1.2.0 alpha rev. 673>>
    Пусть это будет просто:
    просто, как только можно,
    но не проще.
    (C) А. Эйнштейн
     
    Подождите ...
    Wait...
    Пока на собственное сообщение не было ответов, его можно удалить.