Хорошо, поэтому я понимаю, что этот вопрос довольно старый, но я все же думаю, что это стоит ответить, потому что, к сожалению, г-н Ле Блан, и я говорю это со всем уважением, ошибочно ошибался. Фактически, предложение «После того, как вы сделали свой дизайн, задайте себе следующий вопрос: как я могу организовать все данные, которые я создал в одном огромном блобе?» до сих пор противоположно тому, что DOD пытается сделать, что это почти пародия, хотя остальная часть ответа более важна. Несмотря на неуважение, г-н Ле Блан, очевидно, очень знающий и полезный член этого сообщества.
Итак, что такое DOD? Очевидно, речь идет о производительности, но дело не только в этом. Это также хорошо продуманный код, который можно читать, легко понять и даже повторно использовать. Теперь объектно-ориентированный дизайн - это проектирование кода и данных для встраивания в инкапсулированные виртуальные «объекты». Каждый объект представляет собой отдельный объект с переменными для свойств, которые могут иметь объекты, и методы для принятия мер на себя или на другие объекты в мире. Преимущество дизайна OO заключается в том, что легко мысленно моделировать ваш код на объекты, потому что весь (реальный) мир вокруг нас, похоже, работает одинаково. Объекты со свойствами, которые могут взаимодействовать друг с другом.
Теперь проблема в том, что процессор на вашем компьютере работает совершенно по-другому. Он работает лучше всего, когда вы позволяете ему делать то же самое снова и снова. Почему это? Из-за маленькой вещи, называемой кешем. Доступ к ОЗУ на современном компьютере может занять 100 или 200 циклов процессора (и процессор должен ждать все это время!), Что слишком долго. Таким образом, эта небольшая часть памяти на процессоре, к которой можно получить доступ очень быстро, кэш-память. Проблема в том, что это всего лишь несколько наименований МБ. Поэтому каждый раз, когда вам нужны данные, которые не были в кеше, вам все равно нужно пройти долгий путь в ОЗУ. Дело не только в данных, но и в коде. Попытка выполнить функцию, которая не находится в кеше команд, вызовет срыв, пока код загрузится из ОЗУ.
Назад к программированию на OO. Объекты большие, но для большинства функций требуется лишь небольшая часть этих данных, поэтому мы теряем кеш, загружая ненужные данные. Методы вызывают другие методы, которые вызывают другие методы, избивая кеш команд. Тем не менее, мы часто делаем много одного и того же материала снова и снова. Например, возьмем пулю из игры. В наивной реализации каждая пуля может быть отдельным объектом. Может быть класс менеджера пули. Он вызывает функцию обновления первого пули. Он обновляет 3D-положение, используя направление/скорость. Это вызывает много других данных из объекта, который будет загружен в кеш. Затем мы вызываем класс World Manager для проверки на столкновение с другими объектами. Это загружает много других вещей в кеш, возможно, это даже приводит к тому, что код из исходного класса диспетчера маркеров удаляется из кэша команд. Теперь мы возвращаемся к обновлению пули, не было столкновения, поэтому мы возвращаемся к менеджеру пули. Возможно, потребуется снова загрузить код. Далее, обновление пули # 2. Это загружает много данных в кеш, вызывает мир ... и т. Д. Итак, в этой гипотетической ситуации у нас есть 2 киоска для загрузки кода и, скажем, 2 киоска для загрузки данных. Это по меньшей мере 400 циклов, потраченных впустую, за 1 пулю, и мы не взяли пули, которые поразили что-то еще во внимание. Теперь процессор работает на частоте 3 ГГц, поэтому мы не будем замечать одну пулю, но что, если у нас будет 100 пуль? Или еще больше?
Так вот, где есть много истории. Да, есть случаи, когда вы попали только на объект, ваши классы менеджера, доступ к файлам и т. Д. Но чаще всего существует множество подобных случаев. Наивный или даже не наивный объектно-ориентированный дизайн приведет к множеству проблем. Поэтому введите ориентированный на данные дизайн. Ключ DOD состоит в том, чтобы моделировать код вокруг ваших данных, а не наоборот, как с OO-дизайном. Это начинается на первых этапах проектирования. Вы не сначала разрабатываете свой OO-код, а затем оптимизируете его. Вы начинаете с перечисления и изучения ваших данных и размышления о том, как вы хотите его модифицировать (я на мгновение доберусь до практического примера).Как только вы узнаете, как ваш код будет модифицировать данные, вы можете выложить их таким образом, чтобы сделать его максимально эффективным для его обработки. Теперь вы можете подумать, что это может привести только к ужасному супу кода и данных во всем мире, но это только тот случай, если вы плохо его проектируете (плохой дизайн так же просто с программированием OO). Если вы хорошо его разработали, код и данные могут быть аккуратно разработаны вокруг определенной функциональности, что приводит к очень читабельному и даже очень многократно используемому коду.
Итак, вернемся к нашим пулям. Вместо создания класса для каждой пули мы сохраняем только менеджер пули. Каждая пуля имеет положение и скорость. Позиция каждого пуля должна быть обновлена. Каждая пуля должна иметь проверку на столкновение, и все пули, которые попали что-то, должны предпринять соответствующие действия. Поэтому, просто взглянув на это описание, я могу лучше всего разработать всю эту систему. Давайте поместим позиции всех пуль в массив/вектор. Положим скорость всех пуль в массив/вектор. Теперь давайте начнем с повторения этих двух массивов и обновления каждого значения позиции с соответствующей скоростью. Теперь все данные, загружаемые в кеш данных, являются данными, которые мы собираемся использовать. Мы даже можем добавить интеллектуальную команду предварительной загрузки, чтобы предварительно предварительно загрузить некоторые данные массива, чтобы данные были в кеше, когда мы добираемся до него. Затем, проверка столкновения. Я не буду вдаваться в подробности здесь, но вы можете себе представить, как может помочь обновление всех пуль друг за другом. Также обратите внимание, что если есть столкновение, мы не будем называть новую функцию или ничего делать. Мы просто держим вектор со всеми пулями, которые имели столкновение, и когда проверка столкновения выполняется, мы можем обновить все эти друг за другом. Посмотрите, как мы просто перешли от большого объема доступа к памяти почти до нуля, поместив наши данные по-другому? Вы также заметили, что наш код и данные, даже если они не разработаны в OO-стиле, все еще легко понять и легко повторно использовать?
Итак, чтобы вернуться к «там, где есть много». При разработке кода OO вы думаете об одном объекте, прототипе/классе. Пуля имеет скорость, пуля имеет положение, пуля будет перемещать каждый кадр по ее скорости, пуля может что-то ударить и т. Д. Когда вы об этом подумаете, вы подумаете о классе со скоростью, положением и функция обновления, которая перемещает маркер и проверяет наличие столкновения. Однако, когда у вас есть несколько объектов, вам нужно подумать обо всех них. Пули имеют позиции, скорость. У некоторых пуль может быть столкновение. Вы видите, как мы больше не думаем об отдельном объекте? Мы думаем обо всех них и разрабатываем код намного по-другому.
Надеюсь, это поможет ответить на вашу вторую часть вопроса. Речь идет не о том, нужно ли обновлять каждого врага или нет, а о самом эффективном способе их обновления. И при проектировании только ваши враги, использующие DOD, могут не получить большую производительность, разработка всей игры вокруг этих принципов (только там, где это применимо!) Может привести к большому увеличению производительности!
Итак, на первую часть вопроса, это другие примеры DOD. Извините, но у меня там не так много. Есть один действительно хороший пример, хотя, я натолкнулся на это некоторое время назад, серию по ориентированному на данные дизайну дерева поведения от Bjoern Knafla: http://bjoernknafla.com/data-oriented-behavior-tree-overview Вы, вероятно, захотите начать с первого в серии из 4, ссылки находятся в сама статья. Надеюсь, это все еще помогает, несмотря на старый вопрос. Или, может быть, какой-то другой пользователь SO сталкивается с этим вопросом и может использовать этот ответ.
Ссылка кажется сломанной, но я полагаю, что это тот, который он отправил на [altdevblogaday] (http://www.altdevblogaday.com/2011/07/09/data-oriented-behavior-tree-overview/). Спасибо за очень хороший ответ! – falstro
Отличное объяснение. Есть одна вещь, которую я не получил. Имея вектор для каждого свойства (положение, движение, ...), как они связаны? Представьте, что пуля сталкивается, как я могу узнать, какой элемент мне нужно удалить из всех векторов? – danijar
Они могут быть связаны идентификатором. Поэтому вместо указателя на объект bullet у вас есть идентификатор, который класс BulletManager может перевести в правый индекс. Итак, допустим, вы хотите уничтожить/удалить пулю с идентификатором # 9001, вы вызываете функцию удаления BulletManager с этим идентификатором, BulletManager просматривает # 9001 в своем индексе и находит, что он указывает на индекс массива 42. Затем он может во всех своих данных массивы (в нашем случае положение и скорость) заменяют данные по индексу 42 данными последнего индекса, обновляют таблицу идентификаторов, чтобы отразить, что идентификатор, указанный в последнем индексе, теперь указывает на индекс 42 и № 9001 ушел – Mart