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 компонента и писать там код дважды для общих операций или свойств построения. И затем добавьте их по-разному, вы можете использовать силу наследования, а затем полиморфизма и инкапсуляции.
Я бы предложил использовать что-то среднее. И не злоупотреблять компонентами.
Я настоятельно рекомендую прочитать эту книгу о шаблонах игрового программирования - она бесплатна на веб-сайте.