Как лучше всего структурировать / управлять сотнями «внутриигровых» персонажей

10

Я делаю простую игру RTS, которая содержит сотни символов, таких как Crusader Kings 2, в Unity. Для их хранения проще всего было бы использовать объекты сценариев, но это не очень хорошее решение, так как вы не можете создавать новые во время выполнения.

Поэтому я создал класс C # с именем «Character», который содержит все данные. Все работает нормально, но, поскольку игра имитирует, она постоянно создает новых персонажей и убивает некоторых персонажей (так как внутриигровые события происходят). Поскольку игра непрерывно симулирует, она создает тысячи персонажей. Я добавил простую проверку, чтобы убедиться, что персонаж является «живым» при обработке его функции. Так что это помогает производительности, но я не могу удалить «Персонаж», если он / она мертв, потому что мне нужна его / ее информация при создании генеалогического древа.

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

Пол П
источник
9
Одна вещь, которую я делал в прошлом, когда мне нужно было подмножество данных персонажа после его смерти, это создание объекта-надгробия для этого персонажа. Надгробный камень может нести информацию, которую я должен найти позже, но он может быть меньше и повторяться реже, потому что он не требует постоянного моделирования, как живой персонаж.
DMGregory
2
Игра похожа на CK2, или это только часть, в которой много персонажей? Я понял, что вся игра похожа на CK2. В этом случае многие ответы здесь не являются неправильными и содержат хорошее ноу-хау, но они упускают суть вопроса. Не поможет то, что вы назвали CK2 стратегической игрой в реальном времени, когда это действительно грандиозная стратегическая игра . Это может показаться придирчивым, но это очень важно для проблем, с которыми вы сталкиваетесь.
Рафаэль Шмитц
1
Например, когда вы упоминаете «1000 символов», люди думают о тысячах 3D-моделей или спрайтов на экране одновременно - так, в Unity - 1000 символов GameObjects. В CK2 максимальное количество персонажей, которые я видел одновременно, было, когда я смотрел на свой корт и видел там 10-15 человек (хотя я играл не очень далеко). Точно так же, армия с 3000 солдат - только одна GameObject, отображающая число «3000».
Рафаэль Шмитц
1
@ R.Schmitz Да, я должен был прояснить эту часть: у каждого персонажа нет привязанного к нему игрового объекта. При необходимости, как перемещение персонажа из одной точки в другую. Создается отдельная сущность, которая содержит всю информацию о персонаже с логикой Ai.
Пол

Ответы:

24

Есть три вещи, которые вы должны рассмотреть:

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

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

  3. Рассмотрим Data-Oriented-Design. Вместо того, чтобы иметь 1000 символьных объектов и вызывать одну и ту же функцию для каждого, имейте массив данных для 1000 символов и имейте один цикл функции по 1000 символов, обновляя каждый по очереди. Этот вид оптимизации может значительно улучшить производительность.

Джек Эйдли
источник
3
Entity / Component / System хорошо работает для этого. Создайте каждую «систему» ​​для того, что вам нужно, пусть она хранит тысячи или десятки тысяч символов (их компонентов) и предоставляет «идентификатор персонажа» системе. Это позволяет вам хранить разные модели данных отдельно и поменьше, а также удалять удаленные символы из систем, в которых они не нужны. (Вы также можете полностью выгрузить систему, если вы не используете ее в данный момент.)
Der Kommissar
1
Сокращая количество символов обновления, которые выводятся за пределы экрана, вы можете значительно увеличить время обработки. Разве вы не имеете в виду снижение?
Tejas Kale
1
@TejasKale: Да, исправлено.
Джек Эйдли,
2
Тысяча персонажей - это не много, но когда каждый из них постоянно проверяет, могут ли они кастрировать остальных, это начинает оказывать значительное влияние на общую производительность ...
curiousdannii
1
Всегда лучше на самом деле проверить, но это, как правило, безопасное рабочее предположение, что римляне захотят кастрировать друг друга;)
curiousdannii
11

В этой ситуации я бы предложил использовать Composition :

Принцип, согласно которому классы должны достигать полиморфного поведения и повторного использования кода посредством их композиции (путем включения экземпляров других классов, которые реализуют желаемую функциональность)


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

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


Решение состоит в том, чтобы разделить отдельные части вашего Characterна подклассы, которым Characterпринадлежит экземпляр. Например:

  • CharacterInfo может быть простой структурой данных с именем, датой рождения, датой смерти и фракцией,

  • Equipmentможет иметь все предметы, которые есть у вашего персонажа, или их текущие активы. Это может также иметь логику, которая управляет ими в функциях.

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

После того, как вы разделили персонажа, вам больше не нужно проверять флаг «Жив / Мертв» в цикле обновления.

Вместо этого, вы бы просто сделать , AliveCharacterObjectчто имеет CharacterController, CharacterEquipmentи CharacterInfoприложенные сценарии. Чтобы «убить» персонажа, вы просто удаляете те части, которые больше не актуальны (такие как CharacterController), - теперь это не будет тратить память или время на обработку.

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


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

Bilkokuya
источник
8

Когда у вас есть большой объем данных для обработки, и не каждая точка данных представлена ​​реальным игровым объектом, то обычно неплохо отказаться от классов, специфичных для Unity, и просто использовать простые старые объекты C #. Таким образом вы минимизируете накладные расходы. Таким образом, вы, кажется, находитесь на правильном пути здесь.

Хранение всех символов, живых или мертвых, в одном списке ( или массиве ) может быть полезным, поскольку индекс в этом списке может служить идентификатором канонического символа. Доступ к позиции в списке по индексу - очень быстрая операция. Но может быть полезно сохранить отдельный список идентификаторов всех живых символов, потому что вам, вероятно, придется повторять их гораздо чаще, чем мертвые символы.

По мере того, как ваша реализация игровой механики прогрессирует, вы также можете посмотреть, какие другие виды поиска вы выполняете чаще всего. Как «все живые персонажи в определенном месте» или «все живые или мертвые предки определенного персонажа». Возможно, было бы полезно создать еще несколько вторичных структур данных, оптимизированных для таких запросов. Просто помните, что каждый из них должен быть в курсе. Это требует дополнительного программирования и станет источником дополнительных ошибок. Так что делайте это только в том случае, если вы ожидаете заметного увеличения производительности.

CKII « удаляет » символы из своей базы данных, когда считает, что они не важны для экономии ресурсов. Если ваша куча мертвых персонажей потребляет слишком много ресурсов в длительной игре, то вы можете сделать что-то подобное (я не хочу называть это «сборкой мусора». Может быть, «уважительный инкрементор»?).

Если у вас есть игровой объект для каждого персонажа в игре, то вам может пригодиться новая система Unity ECS и Jobs . Он оптимизирован для обработки большого количества очень похожих игровых объектов. Но это вынуждает вашу программную архитектуру создавать очень жесткие шаблоны.

Кстати, мне очень нравится CKII и то, как он имитирует мир с тысячами уникальных персонажей, контролируемых ИИ, поэтому я с нетерпением жду возможности сыграть ваш жанр.

Philipp
источник
Здравствуйте, спасибо за ответ. Все расчеты производятся одним диспетчером GameObject. Я назначаю игровые объекты индивидуальным актерам только тогда, когда это необходимо (например, отображение движений Army of Character из одной позиции в другую).
Пол
1
Like "all living characters in a specific location" or "all living or dead ancestors of a specific character". It might be beneficial to create some more secondary data-structures optimized for these kinds of queries.По моему опыту с моддингом CK2, это близко к тому, как CK2 обрабатывает данные. CK2, кажется, использует индексы, которые по сути являются базовыми индексами базы данных, что позволяет быстрее находить символы для конкретной ситуации. Вместо того, чтобы иметь список символов, он, похоже, имеет внутреннюю базу данных символов со всеми вытекающими отсюда недостатками и преимуществами.
Морфилдур
1

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

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

Майкл Джонсон
источник
это РТС. Мы должны предположить, что в любой момент времени на экране появляется значительное количество единиц.
Том
В RTS мир должен продолжаться, пока игрок не смотрит. Большое обновление заняло бы столько же времени, но было бы полным взрывом при перемещении камеры.
PStag
1

Уточнение вопроса

Я делаю простую игру RTS, которая содержит сотни персонажей, таких как Crusader Kings 2 в Unity.

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

CharacterКлассы без функциональности

Поэтому я создал класс C # с именем «Character», который содержит все данные.

Хорошо, потому что персонаж в вашей игре - это просто данные. То, что вы видите на экране, это просто представление этих данных. Эти Characterклассы являются сердцем игры, и поэтому им грозит опасность стать « объектами бога ». Поэтому я бы посоветовал крайние меры против этого: убрать все функциональные возможности из этих классов. Метод, GetFullName()который объединяет имя и фамилию, хорошо, но нет кода, который на самом деле «что-то делает». Поместите этот код в специальные классы, которые выполняют одно действие; например, класс Birtherс методом Character CreateCharacter(Character father, Character mother)окажется намного чище, чем наличие этой функциональности в Characterклассе.

Не храните данные в коде

Для их хранения проще всего было бы использовать объекты сценариев

Нет. Храните их в формате JSON, используя JsonUtility от Unity. С этими Characterклассами без функциональности это должно быть тривиально. Это будет работать как для начальной настройки игры, так и для сохранения ее в savegames. Однако это все еще скучная вещь, поэтому я просто дал самый простой вариант в вашей ситуации. Вы также можете использовать XML, YAML или любой другой формат, если он еще может быть прочитан людьми, когда он хранится в текстовом файле. CK2 делает то же самое, на самом деле большинство игр делают. Это также отличная настройка, позволяющая людям изменять вашу игру, но об этом стоит подумать гораздо позже.

Думай абстрактно

Я добавил простую проверку, чтобы убедиться, что персонаж «Жив» при обработке [...], но я не могу удалить «Персонаж», если он мертв, потому что мне нужна его информация при создании семейного древа.

Этот легче сказать, чем сделать, потому что он часто сталкивается с естественным мышлением. Вы думаете "естественным" образом, о "характере". Однако, с точки зрения вашей игры, кажется, что есть как минимум 2 разных типа данных, которые «являются персонажем»: я назову это ActingCharacterи FamilyTreeEntry. Мертвый персонаж FamilyTreeEntryне нуждается в обновлении и, вероятно, требует намного меньше данных, чем активный ActingCharacter.

Рафаэль Шмитц
источник
0

Я собираюсь говорить с небольшим опытом, переходя от жесткого ОО-дизайна к дизайну Entity-Component-System (ECS).

Некоторое время назад я был таким же, как вы , у меня было множество разных типов вещей, которые имели схожие свойства, и я строил различные объекты и пытался использовать наследование для их решения. Очень умный человек сказал мне , не делайте этого, а вместо этого используйте Entity-Component-System.

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

  1. Сущность : это вещь , игрок, животное, NPC, что угодно . Это вещь, которая нуждается в компонентах, прикрепленных к ней.
  2. Компонент : это атрибут или свойство , например «Имя», «Возраст» или «Родители», в вашем случае.
  3. Система : это логика компонента или поведения . Как правило, вы строите одну систему для каждого компонента, но это не всегда возможно. Кроме того, иногда системы должны влиять на другие системы.

Итак, вот где я хотел бы пойти с этим:

Прежде всего, создайте IDдля своих персонажей. Ан int, Guidчто угодно. Это «Сущность».

Во-вторых, начните думать о различном поведении, которое у вас происходит. Такие вещи, как «Семейное древо» - это поведение. Вместо того, чтобы моделировать это как атрибуты объекта, создайте систему, которая содержит всю эту информацию . Затем система может решить, что с ней делать.

Точно так же мы хотим создать систему для "Живой или мертвый персонаж?" Это одна из самых важных систем в вашем дизайне, потому что она влияет на все остальные. Некоторые системы могут удалять «мертвые» символы (такие как система «спрайт»), другие системы могут внутренне переупорядочивать вещи, чтобы лучше поддерживать новый статус.

Например, вы создадите систему «Sprite», «Drawing» или «Rendering». Эта система будет отвечать за определение того, с каким спрайтом должен отображаться персонаж и как его отображать. Затем, когда персонаж умирает, удалите их.

Кроме того, система «ИИ», которая может сказать персонажу, что делать, куда идти и т. Д. Это должно взаимодействовать со многими другими системами и принимать решения на их основе. Опять же, мертвые персонажи могут быть удалены из этой системы, так как они больше ничего не делают.

Ваша система «Имя» и «Семейное древо», вероятно, должны сохранять персонажа (живого или мертвого) в памяти. Эта система должна вызывать эту информацию, независимо от состояния персонажа. (Джим все еще Джим, даже после того, как мы его похороним.)

Это также дает вам преимущество изменения, когда система реагирует более эффективно: система имеет свой собственный таймер. Некоторые системы должны срабатывать быстро, некоторые нет. Здесь мы начинаем понимать, что делает игру эффективной. Нам не нужно пересчитывать погоду каждую миллисекунду, мы можем делать это каждые 5 или около того.

Это также дает вам больше креативных рычагов: вы можете создать систему «Pathfinder», которая будет обрабатывать расчет пути от A-к-B, и может обновлять по мере необходимости, позволяя системе Movement сказать «куда мне нужно идти дальше? Теперь мы можем полностью разделить эти проблемы и более эффективно рассуждать о них. Движению не нужно искать путь, оно просто должно привести вас туда.

Вы захотите выставить некоторые части системы снаружи. В вашей Pathfinderсистеме вы, вероятно, захотите Vector2 NextPosition(int entity). Таким образом, вы можете хранить эти элементы в строго контролируемых массивах или списках. Вы можете использовать меньшие structтипы, которые помогут вам хранить компоненты в меньших, непрерывных блоках памяти, что может значительно ускорить обновление системы . (Особенно, если внешние воздействия на систему минимальны, теперь ей нужно заботиться только о ее внутреннем состоянии, например Name.)

Но, и я не могу подчеркнуть это достаточно, теперь это Entityпросто ID, включая плитки, объекты и т. Д. Если сущность не принадлежит системе, то система не будет отслеживать ее. Это означает , что мы можем создать «дерево» объектов, хранить их в Spriteи Movementсистемах (деревья не будут двигаться, но у них есть «Позиция» компонент), и держать их из других систем. Нам больше не нужен специальный список для деревьев, так как рендеринг дерева ничем не отличается от символа, кроме бумажного сглаживания. (То, что Spriteсистема может контролировать, или Paperdollсистема может контролировать.) Теперь наша версия NextPositionможет быть слегка переписана: Vector2? NextPosition(int entity)и она может вернуть nullпозицию для объектов, которые ей не нужны. Мы также применяем это к нашему NameSystem.GetName(int entity), оно возвращается nullдля деревьев и камней.


Я нарисую это к концу, но идея здесь , чтобы дать вам представление о ECS, и как вы можете действительно использовать это , чтобы дать вам лучший дизайн на вашей игре. Вы можете повысить производительность, отделить несвязанные элементы и поддерживать порядок в более организованном режиме. (Это также хорошо сочетается с функциональными языками / настройками, такими как F # и LINQ, которые я настоятельно рекомендую проверить F #, если вы этого еще не сделали, он очень хорошо сочетается с C #, когда вы используете их вместе).

Der Kommissar
источник
Здравствуйте, спасибо за такой подробный ответ. Я использую только один GameObject Manager, который содержит ссылку на всех других игровых персонажей.
Пол
Разработка в Unity вращается вокруг сущностей под названием, GameObjectкоторые мало что делают, но имеют список Componentклассов, выполняющих реальную работу. Был сдвиг парадигмы относительно ECSS окольных десять лет назад , как положить актерский код в отдельных классах системы чиста. Unity недавно также внедрила такую ​​систему, но их GameObjectсистема всегда была и остается ECS. ОП уже использует ECS.
Рафаэль Шмитц
-1

Поскольку вы делаете это в Unity, самый простой подход заключается в следующем:

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

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

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

Том
источник