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

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

2

Спасибо! ^_^

1

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

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