Ориентированный на данные дизайн на практике?

Был еще один вопрос о том, что такое ориентированный на данные дизайн, и там статью, на которую часто ссылаются (и я читал ее как 5 или 6 раз). Я понимаю общую концепцию этого, особенно когда речь идет, например, о 3D-моделях, где вы хотите сохранить все вершины вместе, а не загрязнять ваши лица нормалями и т.д.

Однако мне трудно представить, как ориентированный на данные дизайн может работать ни на что, кроме самых тривиальных случаев (3d-модели, частицы, BSP-деревья и т.д.). Есть ли хорошие примеры, которые действительно охватывают ориентированный на данные дизайн и показывают, как это может работать на практике? Я могу пахать через большие кодовые базы, если это необходимо.

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

Любые указатели будут очень благодарны!

Ответы

Ответ 1

Итак, что же такое DOD? Очевидно, это о производительности, но это не только это. Это также о хорошо разработанном коде, который читабелен, прост для понимания и даже для повторного использования. Теперь объектно-ориентированное проектирование - это все о разработке кода и данных для встраивания в инкапсулированные виртуальные "объекты". Каждый объект представляет собой отдельную сущность с переменными для свойств, которые объект может иметь, и методы для действий над собой или другими объектами в мире. Преимущество ОО-дизайна в том, что он легко мысленно моделирует ваш код в объекты, потому что весь (реальный) мир вокруг нас, кажется, работает одинаково. Объекты со свойствами, которые могут взаимодействовать друг с другом.

Теперь проблема в том, что процессор на вашем компьютере работает совершенно по-другому. Это работает лучше всего, когда вы позволяете ему делать то же самое снова и снова. Почему это? Из-за мелочи под названием кеш. Доступ к ОЗУ на современном компьютере может занять 100 или 200 циклов ЦП (а ЦП должен ждать все это время!), Что слишком долго. Таким образом, на процессоре есть небольшая часть памяти, к которой можно быстро получить доступ, кэш-память. Проблема в том, что это всего лишь несколько мегабайт. Таким образом, каждый раз, когда вам нужны данные, которых нет в кеше, вам все равно нужно пройти долгий путь к оперативной памяти. Это касается не только данных, но и кода. Попытка выполнить функцию, которая не находится в кэше инструкций, приведет к остановке, пока код загружается из ОЗУ.

Вернуться к ОО программированию. Объекты большие, но большинству функций требуется лишь небольшая часть этих данных, поэтому мы тратим кэш-память, загружая ненужные данные. Методы вызывают другие методы, которые вызывают другие методы, перебивая ваш кеш инструкций. Тем не менее, мы часто делаем одно и то же снова и снова. Давайте возьмем пулю из игры, например. В наивной реализации каждая пуля может быть отдельным объектом. Там может быть класс диспетчера пуль. Вызывает первую функцию обновления пули. Он обновляет 3D-положение, используя направление/скорость. Это приводит к загрузке большого количества других данных из объекта в кэш. Затем мы вызываем класс World Manager для проверки на коллизия с другими объектами. Это загружает множество других вещей в кеш, возможно, даже вызывает сброс кода из исходного класса диспетчера маркеров из кеша команд. Теперь мы возвращаемся к обновлению маркеров, столкновений не было, поэтому мы возвращаемся к диспетчеру маркеров. Возможно, потребуется снова загрузить некоторый код. Далее, обновление № 2. Это загружает много данных в кэш, вызывает мир... и т.д. Итак, в этой гипетической ситуации у нас есть 2 киоска для загрузки кода и, скажем, 2 киоска для загрузки данных. Это по меньшей мере 400 циклов потрачено впустую, за одну пулю, и мы не приняли во внимание пули, которые попали в что-то еще. Теперь процессор работает на частоте 3+ ГГц, поэтому мы не заметим ни одной пули, но что, если у нас будет 100 пуль? Или даже больше?

Так что это там, где есть одна история. Да, есть некоторые случаи, когда у вас есть только объект, классы вашего менеджера, доступ к файлам и т.д. Но чаще встречается много подобных случаев. Наивный или даже не наивный объектно-ориентированный дизайн приведет к множеству проблем. Так что вводите ориентированный на данные дизайн. Ключом DOD является моделирование вашего кода вокруг ваших данных, а не наоборот, как с ОО-дизайном. Это начинается на первых этапах проектирования. Вы сначала не разрабатываете свой ОО-код, а затем оптимизируете его. Вы начинаете с того, что перечисляете и анализируете свои данные и продумываете, как вы хотите их изменить (я сейчас же приведу практический пример). Как только вы узнаете, как ваш код будет изменять данные, вы можете расположить их так, чтобы сделать их обработку максимально эффективной. Теперь вы можете подумать, что это может привести только к ужасному супу кода и данных повсюду, но это только в том случае, если вы плохо его проектируете (плохой дизайн так же прост с OO-программированием). Если вы спроектируете это хорошо, код и данные могут быть аккуратно спроектированы с учетом конкретной функциональности, что приведет к очень читабельному и даже очень многократному использованию кода.

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

Итак, вернемся к тому, "где там много". При разработке ОО-кода вы думаете об одном объекте, прототипе/классе. У пули есть скорость, у пули есть позиция, пуля будет перемещаться в каждом кадре на ее скорость, пуля может попасть во что-то и т.д. Когда вы думаете об этом, вы будете думать о классе со скоростью, положением и функция обновления, которая перемещает пулю и проверяет наличие столкновений. Однако, когда у вас есть несколько объектов, вам нужно подумать обо всех них. Пули имеют позиции, скорость. У некоторых пуль может быть коллизия. Вы видите, как мы больше не думаем об отдельном объекте? Мы думаем обо всех из них и разрабатываем код совсем по-другому.

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

Итак, первая часть вопроса, это другие примеры DOD. Извините, но у меня там не так много. Тем не менее, есть один действительно хороший пример, с которым я столкнулся некоторое время назад, серия работ Бьорна Кнафла по ориентированному на данные проектированию дерева поведения: http://bjoernknafla.com/data-oriented-behavior-tree-overview Вы, вероятно, хотите начать с первого в серии 4, ссылки в самой статье. надеюсь, что это все еще помогает, несмотря на старый вопрос. Или, может быть, какой-то другой пользователь SO сталкивался с этим вопросом и использовал этот ответ.

Ответ 2

Я прочитал вопрос, с которым вы связались, и статью.

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

Я в значительной степени в той же лодке, что и вы.

То, как я понимаю статью Ноэля, заключается в том, что вы разрабатываете свою игру в типичном объектно-ориентированном виде. У вас есть классы и методы, которые работают над классами.

После того, как вы сделали свой дизайн, задайте себе следующий вопрос:

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

Подумайте об этом с точки зрения написания всего вашего проекта как одного функционального метода с большим количеством подчиненных методов. Это напоминает мне о масштабных 500-тысячных программах Cobol моей юности.

Теперь вы, вероятно, не будете писать всю игру как один огромный функциональный метод. Действительно, в этой статье Ноэль говорит о части рендеринга игры. Подумайте об этом как о игровом движке (один огромный функциональный метод) и о коде для управления движком игры (код ООП).

То, что меня особенно интересует, - это мантра, "там, где ее много", и я не могу, по-видимому, связаться с остальными здесь. Да, всегда есть не один враг, но вам все равно нужно обновлять каждого врага индивидуально, потому что они не двигаются так же, как они?

Вы думаете об объектах. Подумайте о функциональности.

Каждое обновление врага представляет собой итерацию цикла.

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