Система оружия через компоненты в Unreal Engine 4

Здравствуйте, в этой статье я хочу поделиться с читателями своим взглядом на подход к разработке на Unreal Engine 4 и использовании такого полезного класса как Actor Component.

Я заметил, что в разных туториалах к Unreal Engine 4 часто используют глубокую и сложную иерархию наследования классов. Хотя сам движок Unreal Engine 4 подталкивает использовать компонентный подход на базе Actor Component.

Если читатель не знаком с тем, что такое Actor, Actor Component и система визуального скриптинга Blueprints, то сначала я рекомендую ознакомиться со следующими материалами:

Unreal Engine 4/Gameplay Programming/Actors или материал на русском языке Введение в разработку C++ в UE4 Часть 2 (раздел Классы геймплея: Object, Actor и Component)
Туториал по Unreal Engine. Часть 2: Blueprints

Проблема


В качестве примера рассмотрим такую вещь как оружие в шутерах. Обычной практикой является создание Actor класса BaseGun и реализация в этом классе логики стрельбы оружия, разброса при стрельбе, создание эффекта стрельбы, инстанцирование Projectile (пуля) и др. Такой класс, как правило, описывает большую часть функционала разных видов оружия: пистолет, винтовка, автомат. Позже новые типы оружия реализуются наследованием от BaseGun.

А что делать, если в игру нужно добавить дробовик или снайперскую винтовку?

Часто в таком случае создают классы-потомки для новых видов оружия. Для снайперской винтовки класс-потомок будет реализовывать дополнительную логику включения/отключения снайперского прицела и уменьшения разброса при стрельбе через прицел. В случае дробовика нужно будет переписать (override) логику для стрельбы. Ведь дробовик не стреляет пулей — он стреляет дробью.

Получается, что при добавлении в игру нового вида оружия разработчик будет выполнять следующий набор действий:

  • создание класса потомка от одного из уже существующих видов оружия или от BaseGun;
  • модифицирование (override) или расширение логики родительского класса.


Вот здесь и появляются проблемы. Чем глубже иерархия классов, тем больше времени требуется программисту чтобы разобраться/вспомнить, как устроена логика выполнения кода. А еще при таком подходе придется постоянно вносить изменения в родительские классы (вплоть до BaseGun). Причина в том, что на момент проектирования базовых классов разработчик почти наверняка не сможет просчитать, какие виды оружия окажутся в финальной версии игры.

Существует еще один способ. Добавить логику для дробовика и снайперской винтовки в класс BaseGun. Но со временем мы получим жирный класс (God object), который делает «слишком много» и нарушает принцип «разделяй и властвуй». Делать такие классы — это порочная стратегия разработки.

Решение


А если перенести логику оружия в компоненты? Сначала определимся со списком действий, которые игрок может делать с оружием. Все эти действия добавляются в Enum E_WeaponActionType.

hyz4wwyytqsk2rlkwsgm4_11u-4.png

Для создания оружия нам нужны два класса: BP_BaseWeapon и BP_Weapon_Action.

BP_BaseWeapon


BP_BaseWeapon — это Actor, для всех видов оружия он будет основой и базовым классом. В BP_BaseWeapon мы добавим CustomAction на каждое действие из E_WeaponActionType. Эти действия будут интерфейсом взаимодействия игрока с оружием. Также BP_BaseWeapon будет содержать в себе коллекцию объектов типа BP_Weapon_Action.

jkv7juqzkmbeu5408dt_swx2a1q.png

BP_Weapon_Action


BP_Weapon_Action — это ActorComponent, потомки этого класса будут реализовывать поведение оружия: выстрел, перезарядку, включение/отключение снайперского прицела и др. Каждый BP_Weapon_Action содержит в себе ссылку на оружие, а также тип действия, которое выполняет конкретный BP_Weapon_Action. Еще BP_Weapon_Action содержит набор public функций для работы с ним из BP_BaseWeapon (StartUseAction, StopUseAction, IsCanUseAction), а также несколько protected функций и свойств.

lgavx5p_8jvsvxkm202qumyd1wa.png

BP_BaseWeapon и BP_Weapon_Action пометим как абстрактные классы, чтобы случайно их не инстанцировать. Но для их потомков так делать не будем.

ijixizhok3wm6yk8-2hv3zxwp5c.png

А теперь как будет выглядеть автомат с подствольным гранатометом и прицелом. Это будет потомок класса BP_BaseWeapon, в который нужно поместить три компонента:

  • BP_Fire_Action — реализует поведение стрельбы патронами, имеет тип Main из E_WeaponActionType;
  • BP_Scope_Action — реализует включение/отключение отображение снайперского прицела, имеет тип Secondary из E_WeaponActionType;
  • BP_Scope_Action — реализует стрельбу из подствольного гранатомета, имеет тип Special из E_WeaponActionType.


При вызове события в BP_BaseWeapon будет произведен поиск соответствующего поведения, и у него будет вызван StartUseAction. Например, при вызове UseWeaponAction_Main будет найден BP_Fire_Action, и наше оружие выстрелит.

rsbhk3ybjp6pnj6nm0y2w7gj-ki.png

Плюс такого подхода в том, что если для нового вида оружия требуется новое поведение, мы не переписываем BaseGun или какой-то другой класс-родитель и не override-им логику. Мы создаем потомка BP_BaseWeapon, а затем просто добавляем и настраиваем компоненты, которые реализуют поведение нового типа оружия. Получается, мы собираем новые классы из готовых блоков. Если какого-то поведения не оказалось, то мы просто дописываем новый компонент на основе BP_Weapon_Action.

Применение описанного похода не ограничивается только созданием разных типов оружия. Его можно распространить и на другие игровые подсистемы:

  • Gamemode для разных игровых режимов;
  • игровые способности персонажей;
  • создание разнообразных мобов;
  • создание разнообразных интерактивных игровых объектов и др.

© Habrahabr.ru