Система оружия через компоненты в 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.
Для создания оружия нам нужны два класса: BP_BaseWeapon и BP_Weapon_Action.
BP_BaseWeapon
BP_BaseWeapon — это Actor, для всех видов оружия он будет основой и базовым классом. В BP_BaseWeapon мы добавим CustomAction на каждое действие из E_WeaponActionType. Эти действия будут интерфейсом взаимодействия игрока с оружием. Также BP_BaseWeapon будет содержать в себе коллекцию объектов типа BP_Weapon_Action.
BP_Weapon_Action
BP_Weapon_Action — это ActorComponent, потомки этого класса будут реализовывать поведение оружия: выстрел, перезарядку, включение/отключение снайперского прицела и др. Каждый BP_Weapon_Action содержит в себе ссылку на оружие, а также тип действия, которое выполняет конкретный BP_Weapon_Action. Еще BP_Weapon_Action содержит набор public функций для работы с ним из BP_BaseWeapon (StartUseAction, StopUseAction, IsCanUseAction), а также несколько protected функций и свойств.
BP_BaseWeapon и BP_Weapon_Action пометим как абстрактные классы, чтобы случайно их не инстанцировать. Но для их потомков так делать не будем.
А теперь как будет выглядеть автомат с подствольным гранатометом и прицелом. Это будет потомок класса 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, и наше оружие выстрелит.
Плюс такого подхода в том, что если для нового вида оружия требуется новое поведение, мы не переписываем BaseGun или какой-то другой класс-родитель и не override-им логику. Мы создаем потомка BP_BaseWeapon, а затем просто добавляем и настраиваем компоненты, которые реализуют поведение нового типа оружия. Получается, мы собираем новые классы из готовых блоков. Если какого-то поведения не оказалось, то мы просто дописываем новый компонент на основе BP_Weapon_Action.
Применение описанного похода не ограничивается только созданием разных типов оружия. Его можно распространить и на другие игровые подсистемы:
- Gamemode для разных игровых режимов;
- игровые способности персонажей;
- создание разнообразных мобов;
- создание разнообразных интерактивных игровых объектов и др.