Как управлять состоянием игры перед лицом EDT? - PullRequest
8 голосов
/ 11 июня 2009

Я занимаюсь разработкой клона стратегии в реальном времени на платформе Java, и у меня есть несколько принципиальных вопросов о том, где разместить и как управлять состоянием игры. В качестве рендеринга игра использует Swing / Java2D. На текущем этапе разработки симуляция и ИИ отсутствуют, и только пользователь может изменить состояние игры (например, построить / снести здание, добавить или удалить производственные линии, собрать флот и оборудование). Следовательно, манипулирование состоянием игры может выполняться в потоке диспетчеризации событий без какого-либо просмотра рендеринга. Состояние игры также используется для отображения различной агрегированной информации пользователю.

Однако, поскольку мне нужно ввести симуляцию (например, прогресс строительства, изменения численности населения, перемещения флота, производственный процесс и т. Д.), Изменение состояния игры в таймере и EDT наверняка замедлит рендеринг.

Допустим, операция симуляции / AI выполняется каждые 500 мс, и я использую SwingWorker для вычислений длиной около 250 мс. Как я могу убедиться в том, что между симуляцией и возможным взаимодействием с пользователем нет расы относительно показаний состояния игры?

Я знаю, что результат моделирования (который представляет собой небольшой объем данных) может быть эффективно перемещен обратно в EDT с помощью вызова SwingUtilities.invokeLater ().

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

Существует ли относительно правильный подход для устранения этого состояния гонки чтения? Возможно, вы выполняете полное / частичное клонирование игрового состояния на каждом таймере таймера или меняете жизненное пространство игрового состояния с EDT на какой-то другой поток?

Обновление: (из комментариев, которые я дал) В игре участвуют 13 игроков, контролируемых искусственным интеллектом, 1 человек-игрок и около 10000 игровых объектов (планет, зданий, оборудования, исследований и т. Д.). Например, игровой объект имеет следующие атрибуты:

World (Planets, Players, Fleets, ...)
Planet (location, owner, population, type, 
    map, buildings, taxation, allocation, ...)
Building (location, enabled, energy, worker, health, ...)

В сценарии пользователь строит новое здание на этой планете. Это выполняется в EDT, поскольку необходимо изменить карту и коллекцию зданий. Параллельно с этим каждые 500 мс запускается симуляция, чтобы вычислить распределение энергии зданиям на всех игровых планетах, которое должно пересекать коллекцию зданий для сбора статистики. Если распределение рассчитано, оно передается в EDT, и энергетическое поле каждого здания назначается.

Этим свойством обладают только взаимодействия человека с игроком, потому что результаты вычислений ИИ в любом случае применяются к структурам в EDT.

Как правило, 75% атрибутов объекта являются статическими и используются только для рендеринга. Остальное можно изменить либо с помощью взаимодействия с пользователем, либо с помощью симуляции / решения AI. Также гарантируется, что новый шаг симуляции / AI не будет запущен, пока предыдущий не записывает все изменения.

Мои цели:

  • Избегайте задержки взаимодействия с пользователем, например, пользователь размещает здание на планете и только через 0,5 с получает визуальный отклик
  • Избегайте блокировки EDT с помощью вычислений, ожидания блокировки и т. Д.
  • Избегайте проблем параллелизма с обходом и модификацией коллекции, изменениями атрибутов

Параметры:

  • Мелкозернистая блокировка объектов
  • Неизменные коллекции
  • Летучие поля
  • Частичный снимок

Все это имеет свои преимущества, недостатки и причины для модели и игры.

Обновление 2: Я говорю о этой игре. Мой клон здесь . Снимки экрана могут помочь представить взаимодействия визуализации и модели данных.

Обновление 3:

Я постараюсь дать небольшой пример кода, чтобы прояснить мою проблему, как это видно из комментариев:

List<GameObject> largeListOfGameObjects = ...
List<Building> preFilteredListOfBuildings = ...
// In EDT
public void onAddBuildingClicked() {
    Building b = new Building(100 /* kW */);
    largeListOfGameObjects.add(b);
    preFilteredListOfBuildings.add(b);
}
// In EDT
public void paint(Graphics g) {
    int y = 0;
    for (Building b : preFilteredListOfBuildings) {
        g.drawString(Integer.toString(b.powerAssigned), 0, y);
        y += 20;
    }
}
// In EDT
public void assignPowerTo(Building b, int amount) {
    b.powerAssigned = amount;
}
// In simulation thread
public void distributePower() {
    int sum = 0;
    for (Building b : preFilteredListOfBuildings) {
        sum += b.powerRequired;
    }
    final int alloc = sum / (preFilteredListOfBuildings.size() + 1);
    for (final Building b : preFilteredListOfBuildings) {
        SwingUtilities.invokeLater(=> assignPowerTo(b, alloc));            
    }
}

Таким образом, перекрытие происходит между onAddBuildingClicked () и distribtePower (). Теперь представьте случай, когда у вас есть 50 таких перекрытий между различными частями игровой модели.

Ответы [ 8 ]

3 голосов
/ 30 июня 2009

Звучит так, как если бы клиент-серверный подход выиграл:

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

ИИ также является клиентом - это эквивалент бота.

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

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

Обратите внимание, что на самом деле это не обязательно должно быть реализовано в виде TCP / IP через Интернет, просто это помогает думать об этом в таких терминах.

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

0 голосов
/ 21 июня 2009

Как насчет реализации архитектуры каналов и фильтров. Трубы соединяют фильтры вместе и помещают запросы в очередь, если фильтр недостаточно быстр. Обработка происходит внутри фильтров. Первый фильтр - это механизм ИИ, а механизм рендеринга реализован с помощью набора последующих фильтров.

На каждом такте таймера новое динамическое состояние мира вычисляется на основе всех входов (время также является входом) и копия , вставленная в первый канал.

В простейшем случае ваш движок рендеринга реализован как единый фильтр. Он просто берет снимки состояния из входного канала и отображает их вместе со статическим состоянием. В живой игре движок рендеринга может захотеть пропустить состояния, если в конвейере их больше одного, а если вы проводите тест или выводите видео, которое вы хотите рендерить каждый.

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

Эта архитектура обеспечивает хороший параллелизм без большой синхронизации.

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

0 голосов
/ 27 июня 2009

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

0 голосов
/ 19 июня 2009

Я думаю, вам не нужно, чтобы World сохранял какие-либо данные или вносил изменения в какие-либо объекты сам по себе, его следует использовать только для сохранения ссылки на объект, а когда этот объект необходимо изменить, попросите игрока внести изменение это напрямую. В этом случае единственное, что вам нужно сделать, - это синхронизировать каждый объект в игровом мире, чтобы, когда игрок вносит изменения, другой игрок не мог этого сделать. Вот пример того, о чем я думаю:

Игроку А нужно знать о Планете, поэтому он просит Мир об этой Планете (как это зависит от вашей реализации). World возвращает ссылку на объект Planet, запрошенный игроком A. Игрок А решает внести изменения, поэтому он и делает. Допустим, это добавляет здание. Метод добавления здания на планету синхронизирован, поэтому только один игрок может сделать это одновременно. Здание будет следить за собственным временем строительства (если таковое имеется), поэтому метод добавления здания Планеты будет освобожден почти сразу. Таким образом, несколько игроков могут запрашивать информацию об одной и той же планете в одно и то же время, не влияя друг на друга, и игроки могут добавлять здания почти одновременно, без особого запаздывания. Если два игрока ищут место для размещения здания (если это является частью вашей игры), то проверка соответствия места будет запросом, а не изменением.

Извините, если это не ответит на ваш вопрос, я не уверен, правильно ли я понял.

0 голосов
/ 14 июня 2009

Можно ли постепенно обновлять игровое состояние и при этом иметь согласованную модель? Например, пересчитать для подмножества объектов планеты / игрока / флота между рендерами / обновлениями пользователя.

Если это так, вы можете запустить инкрементные обновления в EDT, которые вычисляют только небольшую часть состояния, прежде чем EDT сможет обрабатывать пользовательские вводы и выполнять рендеринг.

После каждого инкрементного обновления в EDT вам нужно будет помнить, сколько модели еще предстоит обновить, и запланировать новый SwingWorker на EDT, чтобы продолжить эту обработку после любых ожидающих пользовательских вводов и рендеринга.

Это должно позволить вам избежать копирования или блокировки игровой модели, сохраняя при этом взаимодействие с пользователем.

0 голосов
/ 11 июня 2009

Если быстрое изменение игрового состояния (когда вы знаете, на что его менять), вы можете обращаться с игровым состоянием, как с другими моделями Swing, и изменять или просматривать состояние только в EDT. Если изменение игрового состояния происходит не быстро, то вы можете либо синхронизировать изменение состояния и сделать это в режиме свингера / таймера (но не EDT), либо вы можете сделать это в отдельном потоке, который вы обрабатываете аналогично EDT (в этот момент вы рассмотрим использование BlockingQueue для обработки запросов на изменение). Последнее более полезно, если пользовательский интерфейс никогда не должен извлекать информацию из состояния игры, а вместо этого имеет изменения рендеринга, отправленные через слушателей или наблюдателей.

0 голосов
/ 11 июня 2009

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

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

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

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

0 голосов
/ 11 июня 2009

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

Создайте API, например SwingUtilities.invokeLater () и / или SwingUtilities.invokeAndWait (), для вашей очереди изменения состояния для обработки запросов на изменение состояния.

Как это отражается в графическом интерфейсе, я думаю, зависит от поведения, которое вы ищете. не может снять деньги, потому что текущее состояние составляет 0 долларов США, или вспомнить пользователю, что учетная запись была пуста, когда обрабатывался запрос на снятие денег. (вероятно, не с этой терминологией ;-))

...