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

46

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

user441521
источник
26
Несколько файлов или один файл, код должен идти куда-то. Игры сложны. Чтобы найти то, что вам нужно, напишите хорошие названия методов и описательные комментарии. Не бойтесь вносить изменения - просто тестируйте. И
Крис МакФарланд
7
Я понимаю, что это должно куда-то идти, но дизайн кода имеет значение в гибкости и обслуживании. Наличие класса или группы кода, состоящей из тысяч строк, меня тоже не поразило.
user441521
17
@ChrisMcFarland не предлагать для резервного копирования, предложить версию кода XD.
GameDeveloper
1
@ChrisMcFarland Я согласен с GameDeveloper. Наличие контроля версий, такого как Git, svn, TFS, ... значительно упрощает разработку благодаря возможности гораздо проще отменять большие изменения и возможности легко восстанавливаться после таких вещей, как случайное удаление проекта, аппаратный сбой или повреждение файла.
Nzall
3
@TylerH: я категорически не согласен. Резервные копии не позволяют объединять многие исследовательские изменения, а также не привязывают столько полезных метаданных к наборам изменений, а также не допускают нормальных рабочих процессов для нескольких разработчиков. Вы можете использовать контроль версий, как очень мощную систему резервного копирования на определенный момент времени, но в ней не хватает большого потенциала этих инструментов.
Фоши

Ответы:

67

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

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

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

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

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

Балинт
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
MichaelHouse
8
Хотя я понимаю движущиеся комментарии, которые необходимо переместить, не перемещайте те, которые ставят под сомнение точность ответа. Это должно быть очевидно, нет?
много
20

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

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

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

MSalters
источник
Да, я просто искал идеи о том, как это сделать. Что такое мышление, так это то, что в нем много мелких функциональных возможностей, поэтому при программировании для меня неестественно ломать эти маленькие функциональные возможности. Однако становится очевидным, что все эти маленькие функциональные возможности начинают увеличивать класс игрока.
user441521
1
Люди обычно говорят, что что-то является классом бога или объектом бога, когда оно содержит и управляет каждым другим классом / объектом в игре.
Балинт
11

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


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


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

Candid Moon _Max_
источник
Я покопаюсь позже сегодня вечером, но, к вашему сведению, я не использую единство, поэтому мне придется скорректировать кое-что, что хорошо.
user441521
Ох, жаль, что я думал, что это был тег Unity, мой плохой. Единственное, что является MonoBehavior - это просто базовый класс для каждого экземпляра на сцене в редакторе Unity. Что касается Physics.OverlapSphere () - это метод, который создает сферический коллайдер во время кадра и проверяет, к чему он прикасается. Сопрограммы похожи на поддельные обновления, их звонки могут быть уменьшены до меньших сумм, чем fps на ПК игроков - это хорошо для производительности. Start () - просто метод, вызываемый один раз при создании экземпляра. Все остальное должно применяться везде. Следующая часть я не буду использовать ничего с Unity. Sry. Надеюсь, это прояснило что-то.
Candid Moon _Max_
Я использовал Unity раньше, поэтому я понимаю идею. Я использую Lua, у которого также есть сопрограммы, так что все должно переводиться довольно хорошо.
user441521
Этот ответ кажется слишком специфичным для Unity, учитывая отсутствие тега Unity. Если бы вы сделали его более общим и сделали пример единства больше примером, это был бы гораздо лучший ответ.
Pharap
@CandidMoon Да, так лучше.
Pharap
4

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

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

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

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

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

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

Pharap
источник
Мне нравится идея этого, но кажется, что он перенес весь код в класс контроллера, оставив меня с той же проблемой.
user441521
@ user441521 Только что понял, что я собираюсь добавить дополнительный абзац, но потерял его, когда мой браузер вышел из строя. Я добавлю это сейчас. В принципе, у вас могут быть разные контроллеры, которые могут объединять их в агрегатные контроллеры, поэтому каждый контроллер обрабатывает разные вещи. например, AggregateController.Controllers = {JumpController (привязки клавиш), MoveController (привязки клавиш), InventoryUIController (привязки клавиш, uisystem)}
Pharap