Как я могу избежать классов гигантских игроков?

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

47 голосов | спросил user441521 22 FebruaryEurope/MoscowbWed, 22 Feb 2017 00:01:27 +0300000000amWed, 22 Feb 2017 00:01:27 +030017 2017, 00:01:27

4 ответа


66

Обычно вы используете систему компонентов сущностей (система компонентов сущностей представляет собой компонентную архитектуру). Это также упрощает создание других объектов, а также позволяет врагам /NPC иметь те же компоненты, что и игрок.

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

Например, у игрока есть компонент позиции, компонент анимации и компонент ввода, и когда пользователь нажимает пробел, вы хотите, чтобы игрок прыгал.

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

Это помогает разбить код на более мелкие модули многократного использования и может привести к более организованному проекту.

ответил Bálint 22 FebruaryEurope/MoscowbWed, 22 Feb 2017 00:12:50 +0300000000amWed, 22 Feb 2017 00:12:50 +030017 2017, 00:12:50
20

Игры не уникальны в этом; бог-классы повсюду являются антитравами.

Общим решением является разбиение большого класса на дерево меньших классов. Если у игрока есть инвентарь, не делайте часть управления инвентаризацией class Player. Вместо этого создайте class Inventory. Это один элемент для class Player, но внутренний class Inventory может обернуть много кода.

Другой пример: персонаж игрока может иметь отношения с NPC, поэтому вы можете иметь class Relation, ссылаясь как на объект Player, так и на NPC объект, но не принадлежащий ни одному.

ответил MSalters 22 FebruaryEurope/MoscowbWed, 22 Feb 2017 14:27:14 +0300000000pmWed, 22 Feb 2017 14:27:14 +030017 2017, 14:27:14
11

1) Player: State-machine + компонентная архитектура.

Обычные компоненты для Player: HealthSystem, MovementSystem, InventorySystem, ActionSystem. Это все классы, такие как class HealthSystem.

Я не рекомендую использовать Update() там (в обычных случаях нет смысла иметь обновление в системе работоспособности, если вам не нужны какие-либо действия там, в каждом кадре, это редко происходит Один случай, о котором вы также можете подумать, - игрок получает отравление, и вам нужно, чтобы он время от времени терял здоровье - здесь я предлагаю использовать сопрограммы. Другой постоянно восстанавливает здоровье или работает, вы просто принимаете текущее здоровье или силу и вызываете сопрограмму заполните это значение, когда придет время. Перерыв coroutine, когда здоровье наполнилось, или он был поврежден, или он снова начал работать и т. д. ОК, это было немного оффтопным, но я надеюсь, что это было полезно) .

Состояния: 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() of WalkState и вызываем его снова внутри этого метода, но теперь с другим состоянием, потому что мы не хотим терять действие в этом фрейме. И теперь, конечно, мы вызываем _playerMovementSystem.Run();.

Но зачем LootState, когда игрок не может ходить или бежать, пока он не выпустит кнопку? В этом случае, когда мы начали грабежи, например, когда нажата кнопка E, мы вызываем _currentPlayerState.Loot();, мы переключаемся на LootState и теперь вызываем его вызываемый оттуда. Там мы, например, вызываем метод collsion, чтобы получить, если есть что-то, чтобы грабить в диапазоне. И мы вызываем сопрограмму, где у нас есть анимация или где мы ее начинаем, а также проверяем, держит ли игрок кнопку, если не coroutine ломается, если да, мы даем ему добычу в конце сопрограммы. Но что, если игрок нажимает WASD? - _currentPlayerState.Walk(); вызывается, но вот красиво о state-machine, в LootState.Walk() у нас есть пустой метод, который ничего не делает или как я бы сделал в качестве функции - игроки говорят: «Эй, парень, я еще не разграбил это, ты можешь подождать?». Когда он заканчивает грабеж, мы переходим на IDLEState.

Кроме того, вы можете сделать еще один скрипт, который называется class BaseState : IState, который использует все эти методы поведения по умолчанию, но имеет их virtual, чтобы вы могли override их в классе class LootState : BaseState.


Компонентная система отличная, единственное, что меня беспокоит, - это экземпляры, многие из них. И это требует больше памяти и работы для сборщика мусора. Например, если у вас 1000 экземпляров врага. Все они имеют 4 компонента. 4000 объектов вместо 1000. Мб, это не так уж важно (я не запускал тесты производительности), если мы рассмотрим все компоненты, которые есть в единственном игровом объекте.


2) Архитектура, основанная на наследовании. Хотя вы заметите, что мы не можем полностью избавиться от компонентов - на самом деле невозможно, если мы хотим иметь чистый и рабочий код. Кроме того, если мы хотим использовать шаблоны проектирования, которые настоятельно рекомендуется использовать в надлежащих случаях (не злоупотребляйте ими тоже, это называется переопределением).

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

Прежде всего, я собираюсь сказать, что Inventory, Crafting, Movement, Building должны быть компонентами, потому что не несет ответственности за такие методы, как AddItemToInventoryArray() - хотя у игрока может быть метод например PutItemToInventory(), который вызовет предыдущий описанный метод (2 слоя - мы можем добавить некоторые условия в зависимости от разных слоев).

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

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

OrganicBuilding : Building, TechBuilding : Building. Вам не нужно создавать 2 компонента и писать код там дважды для общих операций или свойств здания. И затем добавьте их по-другому, вы можете использовать силу наследования, а затем полиморфизм и инкапсуляцию.


Я бы предложил использовать что-то промежуточное. И не злоупотребляйте компонентами.


Я настоятельно рекомендую прочитать эту книгу о Шаблонах программирования игр - это бесплатно на WEB.

ответил Candid Moon 22 FebruaryEurope/MoscowbWed, 22 Feb 2017 01:49:30 +0300000000amWed, 22 Feb 2017 01:49:30 +030017 2017, 01:49:30
4

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

Во-первых, сама идея класса Player вводит в заблуждение в первую очередь. Многие люди склонны думать о персонаже игрока, персонажах npc и монстрах /врагах как о разных классах, когда на самом деле все из них имеют довольно много общего: все они нарисованы на экране, все они перемещаются, они могут все имеют запасы и т. д.

Этот способ мышления приводит к тому, что персонажи игроков, персонажи не-игроков и монстров /врагов считаются «Entity s», а не рассматриваются по-разному. Естественно, однако, они должны вести себя по-другому - персонаж игрока должен управляться через вход, а npcs - ai.

Решением этого является наличие классов Controller, которые используются для управления Entity s. Таким образом, вся тяжелая логика заканчивается в контроллере, и все данные и общность сохраняются в сущности.

Кроме того, путем подкласса Controller в InputController и AIController, он позволяет игроку эффективно управлять любым Entity в комнате. Этот подход также помогает в многопользовательском режиме, используя класс RemoteController или NetworkController, который работает через команды из сетевого потока.

Это может привести к тому, что большая часть логики будет привязана к одному контроллеру Controller, если вы не будете осторожны. Способ избежать этого состоит в том, чтобы иметь Controller s, которые состоят из другого Controller s или создания функциональности Controller, зависят от различных свойств Controller. Например, AIController будет иметь DecisionTree , а PlayerCharacterController может состоять из различных других Controller s, таких как MovementController, JumpController (содержащий конечный автомат с состояниями OnGround, Ascending и Descending), InventoryUIController. Дополнительным преимуществом этого является то, что новый Controller s может быть добавлен по мере добавления новых функций - если игра начинается без системы инвентаризации и одна добавлена, контроллер для нее может быть привязан позже.

ответил Pharap 22 FebruaryEurope/MoscowbWed, 22 Feb 2017 17:03:46 +0300000000pmWed, 22 Feb 2017 17:03:46 +030017 2017, 17:03:46

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132