Как сделано население в 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()