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().
class LivinEntity : IDamagable {
private float _health; // For fields that are the same between Instances I would use Flyweight Pattern.
public void TakeDamage() {
....
}
}
class GameActor : LivingEntity, IGameActor {
// Here goes state machine and other attached components needed.
}
class Player : GameActor {
// Inventory, Building, Crafting.... components.
}
Поэтому здесь я не смог отделить компоненты от наследования, но мы можем смешивать их, как вы видите. Мы также можем создать некоторые базовые классы для системы Building, например, если у нас разные типы и мы не хотим писать больше кода, чем необходимо. Действительно, у нас также могут быть разные типы зданий, и на самом деле нет хорошего способа сделать это на основе компонентов!
OrganicBuilding : Building, TechBuilding : Building. Вам не нужно создавать 2 компонента и писать там код дважды для общих операций или свойств построения. И затем добавьте их по-разному, вы можете использовать силу наследования, а затем полиморфизма и инкапсуляции.
Я бы предложил использовать что-то среднее. И не злоупотреблять компонентами.
Я настоятельно рекомендую прочитать эту книгу о шаблонах игрового программирования - она бесплатна на веб-сайте.