Как сделано население в SimCity 4 и Cities: SkyLine - это 100к объектов?

1,00
р.
Все видели в SimCity и Cities:Skyline и подобных им, что в городе можно клацнуть по пешеходу / машине и вам высветится интерфейс, в котором есть: Имя персонажа, место жительства, место работы и куда он направляется. Насколько я понимаю, эти жители, просто объекты одного, а может и нескольких классов, и создать такое же не тяжело. А когда города под 100к человек??.. Так вот вопрос к опытным программистам: мне кажется это очень "тяжелая" для ресурсов ноша. 100к + объектов? и нормальные системные требования? быть не может.. В чем подвох? Или лучше сказать моя недальновидность.

Ответ
То, что в городе 100к объектов, не значит, что создаются объекты для них всех. 100к объектов, которые, скажем, весят по 200 байт - это 20 мегабайт. Вместится. Проблемы, правда, будут с GC, скорей всего. Но, думаю, можно настройки generations подкрутить так, чтоб не было больших фризов при сборке.
По существу же, сомнительно, что все 100к объектов всегда в памяти. Я как минимум несколько способов вижу оптимизации:
Разбиение на зоны/локации. Грузить объекты только в рамках этой зоны. Вспомните старые игры, при переходах между локациями было окно загрузки. Если мне память не изменяет, то в той же WoW'ке в своё время при переходах между локациями оно было. Это сейчас уже они оптимизировали, что не приходится ждать и создаётся иллюзия бесшовного мира. Использовать пулы объектов. Чтоб не грузить лишний раз GC, да и просто, чтоб не тратить ресурсы на создание объектов, используются пулы. Скажем, изначально создать пул из 100 объектов-пустышек. По мере надобности заполнять их свойства нужными данными (когда человек/здание/машина в зоне видимости). Когда объект выходит из зоны видимости, очищать объект и запихивать назад в пул. Потом этот объект из пула уже возьмёт другой персонаж игрового мира. Я такие пулы использовал для объектов, которые часто создаются: пули, заклинания и т.п. Я могу привести код из текущего нашего проекта. Я там использую org.apache.commons.pool2 для создания пула подключений к базе данных: Например, метод, который грузит мета данные для сервиса из базы: public void loadMetaInfo(){ LOG.debug("load meta info") DB db = null try{ db = mDB.getResource() // получаем подключение из пула String reqId = getRequestId() db.send(new CommandRequest(Command.INFO.toString(), reqId), this) mRequests.put(reqId, new CacheItem("load info")) } catch(Exception ex){ LOG.error("loadMetaInfo: db_problem") } finally{ if(db != null) db.close() // после окончания работы возвращаем объект в пул, // чтоб им могли воспользоваться другие клиенты } }
Как заметили в комментариях, для оптимизация работы с памятью можно предотвращать создание экземпляров классов, имеющих общую сущность, используя паттерн Приспособленец (Flyweight pattern). Почитать про него (да и про многие другие паттерны) можно на этом сайте. По сути, это похоже на п.2. Типичный пример использования этого шаблона - Buffer pools.
Паттерн Приспособленец (Flyweight)
Это структурный шаблон проектирования, позволяющий использовать разделяемые объекты сразу в нескольких контекстах. Данный паттерн используется преимущественно для оптимизации работы с памятью.
Скажем, если мы хотим написать редактор текстовый. Для простоты положим, что используются только буквы русского алфавита. Создавать объект на каждую букву - накладно очень. Можно определить словарь, состоящий из 33 объектов, которые мы будет переиспользовать. В итоге, к примеру, для текста из 330 букв мы уменьшили число создаваемых объектов в 10 раз. Если речь о книге, то там речь уже на тысячи идёт.
Ключевым моментом является разделение состояния на внутренне и внешнее. Внутреннее состояние от контекста не зависит. В примере с символами внутреннее состояние описывается кодом символа из таблицы кодировки. Так как внутреннее состояние не зависит от контекста, то оно может быть разделяемым и поэтому выносится в разделяемые объекты.
Внешнее состояние зависит от контекста и является изменчивым. В применении к символам внешнее состояние может представлять положение символа на странице (строка + колонка). То есть код символа может быть использован многими символами, тогда как положение на странице будет для каждого символа индивидуально.
Применительно к вашему вопросу. У каждого объекта внутренним состоянием может задаваться размер объекта, его материал, какие-то физические величины, параметры коллайдера. Внешнее же состояние: имя персонажа, место жительства, место работы и куда он направляется.
Можно показать принцип работы UML диаграммой с Wiki:
Применительно к вашему вопросу накидаю по-быстрому примерную реализацию на Java. Проверить код сейчас нет возможности, но, как мне кажется, главное - уловить суть.
Скажем, у нас персонажи двух типов: толстые и худые. Тогда будет что-то такое.
Базовый класс, определяющий объект-персонаж:
public abstract class Character{
protected String mTitle
protected String getTitle() { return mTitle }
protected void setTitle(String mTitle) { this.mTitle = mTitle }
protected int getWidth() { return mWidth }
protected void setWidth(int mWidth) { this.mWidth = mWidth }
protected int getHeight() { return wHeight }
protected void setHeight(int wHeight) { this.wHeight = wHeight }
protected int mWidth
protected int wHeight
public abstract void printCharacter() }
Две его реализации:
public class FatCharacter extends Character {
public FatCharacter(){ setTitle("Я толстяк") setWidth(20) setHeight(40) }
@Override public void printCharacter() { System.out.println("Title: " + getTitle() + ", width:" + getWidth() + ", height: " + getHeight()) }
}
public class ThinCharacter extends Character {
public ThinCharacter(){ setTitle("Я худой") setWidth(10) setHeight(40) }
@Override public void printCharacter() { System.out.println("Title: " + getTitle() + ", width:" + getWidth() + ", height: " + getHeight()) }
}
Фабрика, собственно, которая отвечает за создание объектов:
public class FlyweightFactory {
public static enum Code{ Thin, Fat}
private Map mCharacters = new EnumMap(Code.class)
public Character getCharacter(Code characterCode){ Character character = mCharacters.get(characterCode)
// если такого объекта ещё нету, то создаём if (character == null){ switch (characterCode){ case Fat : character = new FatCharacter() break case Thin : character = new ThinCharacter() break }
// добавляем в пул mCharacters.put(characterCode, character) } return character }
}
Использовать так:
Character character = factory.getCharacter(Code.Fat) character.printCharacter()