The Sharpest One #03 | Рефаторинг "статов"

Всем привет! Страшно извиняемся за 2.5 месяца молчания. Всё это время мы работали над сложными вещами, про которые не написать пока их не закончишь. Да и ГД заболел (доделать дизайн документы и тех. задания не успел). Плюс поступления и вступительные в университеты тоже не улучшили нашу продуктивность в проекте.

Этот девлог написал наш программист Евгений. Он будет о коде (далее от его лица).

Расскажу о рефакторинге "статов" и "атаки", а также о том, что я выучил по пути

Что такое "статы"?

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

Звучит конечно легко, и так оно и есть, однако у меня по пути возникли довольно банальные трудности и глупые ошибки, которых можно было избежать если бы мы с Ильёй регулярно созванивались и делали совместный ревью кода (он у нас ещё и программирует, но для визуала; основной код пишу я).

До рефакторинга

Если пишете девлоги, то сохраняйте всё или пишите его параллельно. Я вот сделал рефакториг, а как всё было до этого в подробностях не помню. Пришлось лезть в историю коммитов в гите. Хорошо, что я могу легко достать прошлые версии. Вот был бы художником, то можно сказать, что потеряна история.

Так вот, в древние времена за всё отвечал один класс: "AbstractBaseStats". В нём были расписаны три основных стата: здоровье, мана и стамина, вместе со всем, что было необходимо для взаимодействия с этими статами: события, вызывающиеся при их изменении, методами OnOverflow и OnExpire, вызывающиеся при переполнении стата или его истощения. Предполагалось, что у каждого существа и объекта (или общего скрипта какой-то группы существ и объектов) будет собственный скрипт наследующийся от этого скрипта, и они перезапишут методы OnOverflow и OnExpire под свои нужны + может добавят что-нибудь своё. Если скажем бандиту нужны только здоровье и стамина, то мана будет стоять по нулям. Вроде бы всё хорошо, но что не так? Многое.

1. Все статы расписаны отдельно и каждый метод для взаимодействия с ними тоже. Список некоторых публичных полей и методов этого скрипта:

The Sharpest One #03 | Рефаторинг "статов"

Ну и вы понимаете куда это идет. Захотелось изменить логику восстановления стата, а измени-ка сначала в одном методе, а потом измени остальные два тоже. DRY-принцип (Don’t Repeat Yourself) нарушен!

2. А что если мне не нужны все три стата? Персонаж не будет пользоваться стаминой, но я все равно могу ее менять и даже использовать для чего бы то ни было. Звучит странно, не удобно и наверняка наплодит багов в будущем.

3. Дополнительный функционал для статов? Задача статов отслеживать состояние персонажей (существ), они не должны делать ничего больше.

4. Этот пункт немного связано со вторым, но я все-таки распишу это отдельно. Допустим я хочу в катсцене споить игрока каким-нибудь зельем, которое ударит по его мане. Все что мне нужно это метод для понижения маны. Я кидаю ссылку на скрипт "BaseStatsPlayer", и что я вижу? Десятки других полей и методов, которое мне подсказывает IDE, и которыми я пользоваться не собираюсь. Можно сказать, что там все просто, и ты легко найдешь свой метод, только остальное не трогай. Но ведь такой сценарий не будет единичным, будет много мест, когда нам понадобится лишь один метод одного из статов, и мы будем каждый раз кидать ссылку на все состояние персонажа? Фи и фу.

Статы после рефакторинга

Самое важное изменение — я создал абстрактный класс "Stat". Теперь в нем хранится вся логика для взаимодействия со статом: методы для его повышения, понижения, регенерация и ресет — весь набор + экшены которые уведомят нас о каждом шорохе в этом скрипте + виртуальные методы "Overflow" и "Expire" если кто нибудь захочет крутануть сальто при переполнении стамины (также я убрал с них приставку On, она только для экшенов)

Этот класс абстрактный и не уточняет что это за стат. Если хочешь что-то конкретное на своего, например, огра, то наследуйся от него, уточни какой стат ты сейчас описываешь и желательно перепиши "Overflow" и "Expire". Вот так, например, выглядит стат здоровья для стреляющего дерева из сцены для тестов:

The Sharpest One #03 | Рефаторинг "статов"

Интерфейс "ITakeDamage" необходим для боевой системы. Об этом чуть дальше (метод "TakeDamage" вылазит из него). Ну и это не настоящий скрипт из проекта, его оригинал наследуется не от "AbstractHit" и "ITakeDamage" а только от "Health", но я решил избавиться от еще одного звена в иерархии для наглядности.

Ну и для стамины и маны нечто похожее. Как вы видите фикс простой – генерализация. Если что-то можно обобщить, то лучше это сделать и тогда твой код будет универсальным, простым и модульным. А все новые программисты, которые придут на проект не будут поливать тебя неприятными словами — прекрасно, я считаю.

Коротко об "атаке"

Раньше вся логика атаки проходила через метод "OnTriggerEnter", GameObject со скриптом "Hit" и "BoxCollider"-ом установленным на триггер. Сталкиваясь с каким-то обьектом, "Hit" проверял с начала был ли это другой хит, и, если да, то выключал его, если поле "DisableAfterHit" равнялось true и потом делал такую же проверку на себе. Если это не хит то он пытался достать из этого обьекта "AbstractBaseStats" и если удавалось то успех, уменьшаем хп скрипта и вырубаемся.

Проблемы снова очевидны. Если мы бьем в какую-то точку, где стоят два противника, то врежем только одному. И, плюс, такой подход часто вызывал недопонимания — атака была, я попал по обьекту, но он ее игнорил и гулял себе дальше. Также были более забавные случаи, например, когда я нажимал на блок, а игрок начинал ехать влево (коллайдер блока и игрока входили друг в друга).

Сейчас же, в классе "AbstractHit" есть метод "Attack" и не реализованный "TakeDamage" от интерфейса "ITakeDamage". Этот скрипт тоже абстрактный чтобы разные виды атаки от него наследовались, уточняли как они будут реагировать на другие хиты (через метод "TakeDamage") и при необходимости вносили свои поля или переписывали виртуальный метод атаки.

Метод "Attack" вызывается инпут контроллером персонажа, когда тот пытается совершить атаку. Включается коллайдер на обьекте со скриптом и начинается магия. Очень полезными штуками при определении коллизий являются "ContactFilter", с его помощью можно искать коллизии только на каком-то определенном слое.

The Sharpest One #03 | Рефаторинг "статов"

Таким образом, мы сначала чекаем коллизии на слое для остальных хитов, если там кто-нибудь есть, то вызываем "TakeDamage" на каждом из них, потом на себе и можно return отсюда. Если же этот "return" не сработал (то есть других хитов мы не нашли), то в контакт фильтре меняем слой на "healthLayer", очищаем наш список с коллайдерами и проводим те же операции но теперь ищем скрипт "Health", и разумеется вызываем "TakeDamage" на всех что найдем.

Итог

Выводы: создание абстракций — круто, коммуникация — золото. Надеемся, что было интересно читать этот девлог.

Хотим напомнить, что у нас есть группа в ВК и там иногда выходят посты и проходят арт-стримы.

На этом всё. Всем удачи, чем бы вы не занимались, и будьте здоровы!

1515
19 комментариев

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

4
Ответить

Есть предыдущие девлоги. Ссылки прикрепим, как только автор поста вернется (пардон, забыли, не подумали).
Что касается скринов: по этой части пока постить в общем то нечего, над еще артом работаем. А этот девлог посвящен чисто коду.

3
Ответить

Комментарий недоступен

4
Ответить

Комментарий недоступен

2
Ответить

Спасибо! ^_^

1
Ответить

Комментарий недоступен

Ответить

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

Ответить