В игре почти всегда есть класс игрока. Игрок, как правило, может многое сделать в игре, что для меня означает, что этот класс оказывается огромным с кучей переменных для поддержки каждой функциональности, которую может выполнять игрок. Каждая часть довольно мала, но в итоге я получаю тысячи строк кода, и становится сложно найти то, что вам нужно, и страшно вносить изменения. С чем-то, что в основном является общим контролем для всей игры, как избежать этой проблемы?
architecture
user441521
источник
источник
Ответы:
Обычно вы используете компонентную систему объекта (компонентная система является компонентной архитектурой). Это также облегчает создание других сущностей, а также позволяет врагам / NPC иметь те же компоненты, что и у игрока.
Этот подход идет в противоположном направлении как объектно-ориентированный подход. Все в игре является сущностью. Сущность - это просто случай без встроенной игровой механики. У этого есть список компонентов и способ управлять ими.
Например, у игрока есть компонент позиции, компонент анимации и компонент ввода, а когда пользователь нажимает пробел, вы хотите, чтобы игрок прыгал.
Вы можете достичь этого, предоставив объекту игрока компонент прыжка, который при вызове превращает компонент анимации в анимацию прыжка, и вы заставляете игрока иметь положительную скорость y в компоненте позиции. В компоненте ввода вы слушаете клавишу пробела и вызываете компонент прыжка. (Это всего лишь пример, у вас должен быть компонент контроллера для движения).
Это помогает разбить код на более мелкие, многократно используемые модули и может привести к более организованному проекту.
источник
Игры не уникальны в этом; бог-классы являются анти-паттерном везде.
Распространенным решением является разбиение большого класса на дерево меньших классов. Если у игрока есть инвентарь, не делайте его частью
class Player
. Вместо этого создайтеclass Inventory
. Это один член дляclass Player
, но внутреннеclass Inventory
может обернуть много кода.Другой пример: персонаж игрока может иметь отношения с неигровыми персонажами, поэтому вы можете
class Relation
ссылаться как наPlayer
объект, так и наNPC
объект, но не принадлежать ни одному из них.источник
1) Player: конечный автомат + компонентная архитектура.
Обычные компоненты для Player: HealthSystem, MovementSystem, InventorySystem, ActionSystem. Это все классы, как
class HealthSystem
.Я не рекомендую использовать
Update()
там (в обычных случаях не имеет смысла иметь обновление в системе здравоохранения, если оно не требуется для некоторых действий там каждый кадр, это происходит редко. Один случай, о котором вы также можете подумать - игрок отравлен, и он вам нужен время от времени терять здоровье - здесь я предлагаю использовать сопрограммы. Другой постоянно восстанавливает здоровье или силу бега, вы просто берете текущее здоровье или силу и вызываете сопрограмму, чтобы заполнить до этого уровня, когда придет время. он был поврежден или он снова начал бегать и т. д. Хорошо, это было немного оффтопно, но я надеюсь, что это было полезно) .Состояния: LootState, RunState, WalkState, AttackState, IDLEState.
Каждое государство наследует от
interface IState
.IState
имеет в нашем случае 4 метода только для примера.Loot() Run() Walk() Attack()
Кроме того, у нас есть,
class InputController
где мы проверяем каждый ввод пользователя.Теперь реальный пример:
InputController
мы проверяем, нажимает ли игрок какую-либо из кнопок,WASD or arrows
и затем он также нажимаетShift
. Если он нажал толькоWASD
тогда, мы вызываем,_currentPlayerState.Walk();
когда это происходит, и мы должныcurrentPlayerState
быть равныWalkState
тогда, когда уWalkState.Walk()
нас есть все компоненты, необходимые для этого состояния - в этом случаеMovementSystem
, поэтому мы заставляем игрока двигатьсяpublic void Walk() { _playerMovementSystem.Walk(); }
- вы видите, что у нас здесь? У нас есть второй уровень поведения, и это очень хорошо для поддержки кода и отладки.Теперь ко второму случаю: что если мы нажали
WASD
+Shift
? Но наше предыдущее состояние былоWalkState
. В этом случаеRun()
будет вызванInputController
(не путайте это,Run()
вызывается, потому что у насWASD
+Shift
регистрацияInputController
не из-заWalkState
). Когда мы вызываем_currentPlayerState.Run();
вWalkState
- мы знаем , что мы должны перейти_currentPlayerState
кRunState
и мы делаем этоRun()
изWalkState
и вызвать его снова в этом методе , но теперь с другим государством , потому что мы не хотим потерять действие этого кадра. А теперь конечно позвоним_playerMovementSystem.Run();
.Но зачем
LootState
когда игрок не может идти или бежать, пока он не отпустит кнопку? Хорошо в этом случае, когда мы начали грабить, например, когда кнопкаE
была нажата, мы вызываем,_currentPlayerState.Loot();
мы переключаемся наLootState
и теперь вызываем ее вызываемый оттуда. Там мы, например, вызываем метод collsion, чтобы получить, если есть что-то, что можно добыть в диапазоне. И мы вызываем сопрограмму, где у нас есть анимация или где мы ее запускаем, а также проверяем, удерживает ли игрок кнопку, если не перерывы сопрограммы, если да, мы даем ему лут в конце сопрограммы. Но что, если игрок нажимаетWASD
? -_currentPlayerState.Walk();
называется, но здесь это довольно вещь о государственной машине, вLootState.Walk()
у нас есть пустой метод, который ничего не делает или, как я сделал бы как особенность, - игроки говорят: «Эй, чувак, я еще не разграбил это, можешь подождать?». Когда он заканчивает мародерство, мы меняемся наIDLEState
.Кроме того, вы можете создать еще один вызываемый скрипт, в
class BaseState : IState
котором реализованы все эти методы по умолчанию, но они есть,virtual
чтобы вы могли использоватьoverride
их вclass LootState : BaseState
типах классов.Компонентная система великолепна, единственное, что меня беспокоит - это экземпляры, многие из них. И это требует больше памяти и работы для сборщика мусора. Например, если у вас есть 1000 экземпляров противника. Все они имеют 4 компонента. 4000 объектов вместо 1000. Мб это не так уж и сложно (я не проводил тесты производительности), если мы рассмотрим все компоненты, которые есть в едином игровом объекте.
2) Архитектура, основанная на наследовании. Хотя вы заметите, что мы не можем полностью избавиться от компонентов - это на самом деле невозможно, если мы хотим иметь чистый и работающий код. Кроме того, если мы хотим использовать шаблоны проектирования, которые настоятельно рекомендуется использовать в надлежащих случаях (не злоупотребляйте ими тоже, это называется чрезмерной разработкой).
Представьте, что у нас есть класс Player, у которого есть все свойства, необходимые для выхода из игры. У него есть здоровье, мана или энергия, он может перемещать, бегать и использовать способности, имеет инвентарь, может создавать предметы, грабить предметы, даже может строить некоторые баррикады или башни.
Прежде всего, я хочу сказать, что Инвентарь, Крафт, Движение, Строительство должны быть основаны на компонентах, потому что игрок не обязан иметь такие методы,
AddItemToInventoryArray()
хотя у игрока может быть такой методPutItemToInventory()
, который вызовет ранее описанный метод (2 слоя - мы можем добавить некоторые условия в зависимости от разных слоев).Еще один пример со строительством. Игрок может вызвать что-то подобное
OpenBuildingWindow()
, ноBuilding
позаботится обо всем остальном, и когда пользователь решает построить какое-то конкретное здание, он передает игроку всю необходимую информацию,Build(BuildingInfo someBuildingInfo)
и игрок начинает строить его со всеми необходимыми анимациями.SOLID - ООП принципы. S - единая ответственность: это то, что мы видели в предыдущих примерах. Да, хорошо, но где наследование?
Здесь: здоровье и другие характеристики игрока должны обрабатываться другой сущностью? Думаю, нет. Без здоровья не может быть игрока, если он есть, мы просто не наследуем. Например, у нас есть
IDamagable
,LivingEntity
,IGameActor
,GameActor
.IDamagable
конечно имеетTakeDamage()
.Поэтому здесь я не смог отделить компоненты от наследования, но мы можем смешивать их, как вы видите. Мы также можем создать некоторые базовые классы для системы Building, например, если у нас разные типы и мы не хотим писать больше кода, чем необходимо. Действительно, у нас также могут быть разные типы зданий, и на самом деле нет хорошего способа сделать это на основе компонентов!
OrganicBuilding : Building
,TechBuilding : Building
. Вам не нужно создавать 2 компонента и писать там код дважды для общих операций или свойств построения. И затем добавьте их по-разному, вы можете использовать силу наследования, а затем полиморфизма и инкапсуляции.Я бы предложил использовать что-то среднее. И не злоупотреблять компонентами.
Я настоятельно рекомендую прочитать эту книгу о шаблонах игрового программирования - она бесплатна на веб-сайте.
источник
В этой проблеме нет серебряной пули, но существуют различные подходы, почти все из которых вращаются вокруг принципа «разделения интересов». В других ответах уже обсуждался популярный компонентный подход, но есть и другие подходы, которые можно использовать вместо или вместе с компонентным решением. Я собираюсь обсудить подход сущности-контроллера, так как это одно из моих предпочтительных решений этой проблемы.
Во-первых, сама идея
Player
класса вводит в заблуждение в первую очередь. Многие люди склонны думать об игровом персонаже, персонажах NPC и монстрах / врагах как о разных классах, когда на самом деле все они имеют довольно много общего: все они нарисованы на экране, все они перемещаются, они могут у всех есть запасы и т. д.Этот способ мышления приводит к подходу, при котором персонажи игрока, неигровые персонажи и монстры / враги рассматриваются как «
Entity
а», а не по-разному. Однако, естественно, они должны вести себя по-другому - персонаж игрока должен управляться вводом, а npcs нужно ai.Решением этого является наличие
Controller
классов, которые используются для управленияEntity
s. При этом вся тяжелая логика попадает в контроллер, а все данные и общность сохраняются в объекте.Кроме того, подклассы
Controller
вInputController
иAIController
, это позволяет игроку эффективно контролировать любогоEntity
в комнате. Этот подход также помогает в многопользовательской игре, имея классRemoteController
или,NetworkController
который работает с помощью команд из сетевого потока.Это может привести к тому, что многие логики будут объединены,
Controller
если вы не будете осторожны. Чтобы избежать этого, нужно иметьController
s, состоящие из другихController
s, или сделать так, чтобыController
функциональность зависела от различных свойствController
. Например,AIController
объект будет иметьDecisionTree
прикрепленный к нему элемент , которыйPlayerCharacterController
может состоять из различных других элементов,Controller
таких как aMovementController
, aJumpController
(содержащий конечный автомат с состояниями OnGround, Ascending и Descending), anInventoryUIController
. Дополнительным преимуществом этого является то, что новыеController
s могут быть добавлены по мере добавления новых функций - если игра запускается без системы инвентаря и одна из них добавляется, контроллер для нее может быть добавлен позже.источник