8 ноября 2023 8.11.23 4 1201

Рогалик по файтингу 2.0 или как я пытался скопировать «Blazblue: Entropy Effect»

+9

Bonjour. Прошло добрых четыре месяца с последней моей попытки вкатиться в геймдев. Напомню, что я планировал наклепать пару проектов для портфолио и устроиться в студию. Попытка оказалась провальной. Но я не отчаялся и сегодня готов представить миру свою новую поделку. Кальку игры "Blazblue: Entropy Effect".

Обложка оригинальной игры
Обложка оригинальной игры

На поприще рогаликов я вступаю достаточно редко, а потому "Entropy Effect" произвела на меня сильное впечатление. Я с большим удовольствием провёл несколько вечеров, раскидывая пачки врагов зрелищными комбо и наслаждаясь прекрасным арт-стайлом. И, заряженный прекрасными эмоциями, я решил попробовать скопировать этот рогалик по файтингу.

Рисовать я не умею от слова совсем, а потому решил "позаимствовать" спрайты из уже существующей игры. " Dengeki Bunko: Fighting Climax".

Обложка
Обложка

"Fighting Climax" – файтинг с персонажами визуальных новел, изданных "Dengeki Bunko". И из всего множества представленных в игре франшиз я знал только "SAO", а потому и взял персонажей именно оттуда.

Традиционно, я провёл небольшое исследование игры и выяснил, какие элементы хочу скопировать:

  • Навигация
  • Соединение атак в связки комбо
  • Элементы рогалика
  • Поведение противников
  • Интерфейс
  • Марафет

На этом этапе заканчивается душное вступление и начинается что-то гораздо более весёлое - разработка.

Перемещение

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

Напомню, работаю я в "Unity", в котором при самой стандартной реализации прыжка, прикладывание направленной вверх силы к объекту, этот самый прыжок получается… "лунным".

Лунный прыжок

Объект взмывает в воздух и опускается на землю с равномерной скоростью, что выглядит и ощущается странно. И подавляющее большинство туториалов предлагает решать эту проблему путём увеличения либо массы, либо гравитации. И если первый способ для отдельных и очевидно тяжёлых объектов я ещё могу понять, то второй путь "улучшения" прыжка вызывает у меня неприязнь. Изменение гравитации для отдельных объектов кажется мне контринтуитивным и неправильным. Есть не иллюзорный шанс изменить влияние силы притяжения для объекта "А" и забыть сделать то же самое с объектом "Б" и после этого удивляться: "А почему это физика действует на них по-разному?" В общем, для себя я решил, что буду улучшать прыжок путём влияния на ускорение объекта.

private async UniTask UpdateAsync() 
{
    while (_updateTokenSource.IsCancellationRequested == false) 
    {
       if (_rigidbody.velocity.y < 0)
          _rigidbody.velocity -= _gravityVector * Constants.FallMultiplier * Time.fixedDeltaTime;
       else if (_rigidbody.velocity.y > 0)
          _rigidbody.velocity *= Constants.LinearVelocitySlowdownSpeed;

       await UniTask.WaitForFixedUpdate();
    }
}

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

Балдёжный прыжок

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

Комбо-система

Базовые атаки

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

  • Отказаться от комбостроения и иметь лишь 3 обычных удара и 1 скилл
  • Хранить последовательность нажатых игроком кнопок и "подставлять" её в заготовки атак. Если последовательность игрока совпадает с последовательностью в заготовке, то происходит атака
  • Хранить последнюю нажатую игроком кнопку действия и в момент завершения атаки проверять, можно ли продолжить эту атаку в выбранное игроком действие?

Потупив над этой задачкой несколько вечеров, я пришёл к решению, объединяющему все три моих идеи.

Примерно так выглядит граф атак. Кружочки - вершины, линии - рёбра
Примерно так выглядит граф атак. Кружочки - вершины, линии - рёбра

Все возможные комбинации атак персонажа представляют из себя небольшой граф. Для удобства предлагаю называть действия игрока вершинами, а "перетекания" из одного действия в другое – рёбрами. Очень сложные термины, но с ними будет проще объяснить, как это всё вообще работает.

Изначально этот граф находится в "нулевом" положении. Игрок ещё не выбрал действие и юнит стоит на земле. В таком состоянии графу доступны "базовые" вершины, которые запускают комбо. Например, первая атака из серии из трёх обычных ударов. Потом игрок решает нажать на левую кнопку мыши и выполнить обычную атаку. Граф пробегается по всем базовым рёбрам и проверяет условия перехода в вершины этих рёбер. В нашем случае переход случится в вершину, условиями которой являются "юнит находится на земле", "нажата кнопка обычной атаки", "юнит стоит на месте/движется только по горизонтали". Если нужная вершина найдена, граф переходит в неё, а юнит выполняет обычную атаку. Во время анимации атаки игрок может ещё раз нажать на кнопку действия. Это действие отправится в буфер. Если в конце анимации атаки в буфере есть действие, то пробежка по рёбрам повториться. Единственной разницей с "нулевым" положением графа будет количество рёбер. Из вершины будет идти значительно меньше связей, чем из "нуля". Если же буфер действия пуст, то граф возвращается в "нулевое" положение.

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

Помимо обычных ударов и скиллов в комбо можно включать и юнитов-саппортов.

Саппорт

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

Мне хотелось, чтобы саппорт появлялся на небольшом расстоянии от игрока, подбегал поближе, наносил удар и убегал в закат. В теории звучит просто, но на практике… Никому же не хочется, чтобы саппорт появлялся в воздухе при вызове на платформе, или же наоборот в пропасти во время её перепрыгивания? Вот и я так подумал и решил написать небольшой алгоритм, вычисляющий координаты спавна юнита. За шесть ужасных часов я родил такой алгоритм.

public class LinearNavigationStrategy : ILegacyUnitNavigationStrategy
    {
        private readonly LayerMask _floorMask;
        private readonly Vector2 _heroGroundedPosition;
        
        public LinearNavigationStrategy(LayerMask floorMask, Vector2 heroGroundedPosition)
        {
            _floorMask = floorMask;
            _heroGroundedPosition = heroGroundedPosition;
        }
        
        public void CalculatePositions(Vector2 unitPosition, float directionMultiplier, out Vector2 startPosition, out Vector2 endPosition)
        {
            float offset = 0.1f;
            
            TryGetFloorPositionWithOffset(unitPosition, Mathf.Infinity, offset, out Vector2 origin);
            
            unitPosition = new Vector2(unitPosition.x, _heroGroundedPosition.y);
            
            RaycastHit2D wallHit = Physics2D.Raycast(origin, Vector2.right * directionMultiplier, Constants.DefaultLegacyUnitSpawnDistance, _floorMask);
            
            float heightDelta = unitPosition.y - origin.y + offset;
            float horizontalPosition = GetPositionData(unitPosition, directionMultiplier, wallHit, out float positionY, out float delta);
            
            Vector2 position = new Vector2(unitPosition.x, positionY);
            Vector2 lastPosition = position;
            float step = 0.05f;
            int stepsCount = (int)(delta / step);
            
            for (int i = 0; i < stepsCount; i++)
            {
                if (TryGetFloorPosition(position, heightDelta, out Vector2 point))
                {
                    position = new Vector2(point.x, position.y);
                    lastPosition = position;
                }
                else
                {
                    TryGetFloorPosition(lastPosition, Mathf.Infinity, out startPosition);
                    TryGetFloorPosition(unitPosition, Mathf.Infinity, out endPosition);
                    startPosition = new Vector2(startPosition.x, unitPosition.y);
                    
                    return;
                }

                position = new Vector2(position.x + step * directionMultiplier, position.y);
            }

            startPosition = new Vector2(horizontalPosition, unitPosition.y);
            TryGetFloorPosition(unitPosition, Mathf.Infinity, out endPosition);
        }
        
        private float GetPositionData(Vector2 unitPosition, float directionMultiplier, RaycastHit2D wallHit, out float positionY, out float delta)
        {
            float horizontalPosition;
            
            if (wallHit)
            {
                horizontalPosition = wallHit.point.x;
                positionY = wallHit.point.y;
                delta = Mathf.Abs(horizontalPosition - unitPosition.x);
            }
            else
            {
                horizontalPosition = unitPosition.x + Constants.DefaultLegacyUnitSpawnDistance * directionMultiplier;
                positionY = _heroGroundedPosition.y;
                delta = Constants.DefaultLegacyUnitSpawnDistance;
            }

            return horizontalPosition;
        }

        private bool TryGetFloorPosition(Vector2 origin, float distance, out Vector2 point)
        {
            RaycastHit2D hit = Physics2D.Raycast(origin, Vector2.down, distance, _floorMask);
            
            if (hit)
            {
                point = hit.point;
                return true;
            }

            point = Vector2.zero;
            return false;
        }
        
        private bool TryGetFloorPositionWithOffset(Vector2 origin, float distance, float offset, out Vector2 point)
        {
            if (TryGetFloorPosition(origin, distance, out point))
            {
                point = new Vector2(point.x, point.y + offset);

                return true;
            }
            
            return false;
        }
    }

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

Заставить всю эту мешанину из английских букв работать было довольно сложно, но когда она наконец соизволила раздуплиться, и игрок получил возможность призывать себе на помощь дополнительный урон… Ммммм, моей радости не было конца. Однако во время тестирования я заметил один неприятный баг. Иногда саппорт всё-таки появлялся в воздухе.

Магия

Я планирую стать программистом "ААА" уровня, а потому забил на этот баг и перешёл к следующему элементу игры.

Рогалик

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

Всего в прототип добавлено пять видов помещений:

Battle: необходимо одолеть всех противников
Battle: необходимо одолеть всех противников
Extensive Battle: необходимо выживать на протяжении n-ого количества времени
Extensive Battle: необходимо выживать на протяжении n-ого количества времени
Trade: типичное пристанище капитализма
Trade: типичное пристанище капитализма
Storage: местная гача с очками &quot;XP&quot;, деньгами и хилками
Storage: местная гача с очками "XP", деньгами и хилками
Selection: здесь у игрока появляется иллюзия выбора следующей комнаты
Selection: здесь у игрока появляется иллюзия выбора следующей комнаты

Больше всего интереса при работе над комнатами вызвала их компоновка. Изначально я планировал выделить по комнате на сцену, но быстро понял, что идея не самая хорошая. Потребовалось бы часто переключаться между сценами, что не совсем удобно, да и добавило бы слишком много никому не нужного микроконтроля. Тогда я решил объединять все нужные комнаты и врагов в "Stage". И уже под один этот "Stage" выделять сцену.

У меня в компьютере стоит довольно старенький HDD, который доживает свои последние дни. И когда мне требуется что-то сделать с файлами, банально даже зайти в проводник, компьютер подвисает секунд на десять. При чём тут "Honkai"? В одном из недавних обновлений в него добавили режим "Swarm Disaster". Тоже своего рода рогалик, в котором нужно пройти три этажа, состоящих из множества комнат. И во время одного из прохождений этого режима я заметил, что мой комп подвисает как раз-таки при загрузке этажей. Переход же между комнатами происходит плавно. Вероятнее всего, под капотом китайская дрочильня работает именно так: все комнаты(а может быть ассеты) подгружаются во время загрузки этажа, а игрока телепортирует между ними(ну или просто выключается старая комната и включается новая).

Когда комнаты были полностью оборудованы и готовы к приёму игрока я решил, что пора бы добавить и награды за их прохождение. Основной, и единственной, наградой выступает опыт. Когда набирается 100 очков "XP" взгляду игрока открывается прекрасный вид на карточки с модификаторами.

Карточки с модификаторами(сломался их бэкграунд)
Карточки с модификаторами(сломался их бэкграунд)

В кодовой базе своей модификаторы работают очень запутанно и мудрёно. Когда игрок набирает необходимое количество опыта, в "Resolver"… как его по-русски назвать, "мозг"? В мозг хранилища модификаторов приходит сообщение о данном событии. И тогда между мозгом и хранилищем происходит примерно следующий диалог:

  • М: Ну чё, базовые модификаторы есть? Ну там "Заморозка" или "Кольцо огня"?
  • Х: Я в прошлый раз отдал всё что было. Остались только улучшения.
  • М: Ясно, ясно. Только улучшения значит… Гугл, отруби ему оперативку.
public class ModifiersResolver
    {
        private readonly ModifiersDataBase _modifiersDataBase;
        private readonly ModifiersHandler _modifiersHandler;

        private readonly Array _modifierTypes;

        public ModifiersResolver(ModifiersDataBase modifiersDataBase, ModifiersHandler modifiersHandler)
        {
            _modifiersDataBase = modifiersDataBase;
            _modifiersHandler = modifiersHandler;
            
            _modifierTypes = Enum.GetValues(typeof(ModifierType));
        }

        public void Clear() => _modifiersHandler.Clear();

        public void AddModifierToHandler(Modifier modifier) => _modifiersHandler.AddModifier(modifier);

        public void ReturnTemplatesToSet(IReadOnlyList<Modifier> modifiers)
        {
            foreach (var modifier in modifiers)
            {
                ModifierTemplate template = modifier.DefaultData;
                ModifierTemplatesSet set = _modifiersDataBase.GetTemplatesSetByType(template.Type);
                
                set.ReturnTemplate(template);
            }
        }
        
        public bool HasFreeModifier()
        {
            int freeSets = (from ModifierType type in _modifierTypes select _modifiersDataBase.GetTemplatesSetByType(type)).Count(set => set.HasFreeTemplate());
        
            return freeSets > 0;
        }
        
        public bool TryGetRandomModifier(out Modifier modifier)
        {
            int index = UnityEngine.Random.Range(0, _modifierTypes.Length);
            ModifierType type = (ModifierType)_modifierTypes.GetValue(index);
            
            modifier = null;

            return TryDefineModifier(type, out modifier);
        }
        
        private bool TryDefineModifier(ModifierType type, out Modifier modifier)
        {
            ModifierTemplatesSet set = _modifiersDataBase.GetTemplatesSetByType(type);
            ModifierTemplate template;

            modifier = null;

            if (_modifiersHandler.HasModifierOfType(type) && set.TryGetRandomNonBaseTemplate(out template))
            {
                modifier = SelectModifier(type, template);

                return true;
            }

            if (_modifiersHandler.HasModifierOfType(type) == false && set.TryGetBaseTemplate(out template))
            {
                modifier = SelectModifier(type, template);

                return true;
            }

            return false;
        }

        private Modifier SelectModifier(ModifierType type, ModifierTemplate template)
        {
            _modifiersHandler.TryGetModifierByType(type, out Modifier modifier);
            
            switch (type)
            {
                case ModifierType.RingOfFire:
                    return new RingOfFireModifier(template, modifier);

                case ModifierType.Freeze:
                    return new FreezeModifier(template, modifier);
                
                default:
                    throw new ArgumentOutOfRangeException(nameof(type), type, null);
            }
        }
    }

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

Выбранный же бонус отправляется в цепкие лапы игрока и оборачивает соответствующее модификатору действие. Вышеупомянутая "Заморозка" срабатывает при попадании по противникам обычным ударом, "Кольцо огня" же запускается раз в n секунд.

Объяснение вышло проще, чем я предполагал.

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

Поведение противников

Наконец пошёл экшен

В прошлом блоге я топил за ИИ противников на основе "Дерева поведения". Я всё ещё считаю применение деревьев довольно мощным инструментом в области создания и настройки поведения юнитов и потому в моём прототипе недруги контролируются "Машиной состояний". Поясню.

В этой игре от противников не требуется какое-то нестандартное и "умное" поведение. Цикл их действий довольно прост: поглазеть на игрока → совершить действие → поглазеть на игрока → … Такого интеллекта Чарли Гордона можно добиться и "Машиной состояний", а "Древо поведения" было бы излишне. Но, хочу отметить, действия противников даже при такой "жёсткой" структуре оказались модульными и широко настраиваемыми.

Настройки действия юнита
Настройки действия юнита

Враги могут атаковать вблизи, запускать снаряды, заряжать мощные атаки, телепортироваться и лечиться. Также можно закидывать действия в очередь и создавать комплексные и сложные атаки. Ещё можно отметить, что настройке подвергается и "область действий" противников. Например, белые… существа атакуют игрока молнией в любой точке карты, а ниндзя ждут, пока цель не окажется выше или на одном с ними уровне.

Ниндзя на платформе не атакует до тех пор, пока не увидит игрока

В целом, я горд этой системой и думаю, что ей можно было бы пользоваться в полноценной игре. ИИ и его настройки получились гибкими и расширяемыми. Новые варианты атак и "областей действия" добавляются и настраиваются вообще без каких бы то ни было проблем. Не знаю, что ещё можно сказать про интеллект врагов, а потому перейду к той части проекта, которая вызвала у меня больше всего… эмоций.

Интерфейс

Интерфейс, интерфейс, интерфейс. Мостик между игроком и данными оригинальным игры показался мне стильным и визуально притягательным. В последний раз такое удовольствие от интерфейса я получал только от " Persona 5" и трейлеров " Persona 3: Reload". Ну ладно, любоваться чем-то это одно, а вот попытаться перерисовать…

Передо мной стояла задача сделать интерфейс для:

  • Игрока
  • Карт модификаторов
  • Комнат
  • Главного меню и экрана выбора персонажа
  • "Активных" данных(HP-бары врагов и нанесённый урон)

В работе над UI игрока и карт модификаторов не было ничего интересного. Рисуешь формы, закидываешь в "Unity" и с помощью кода и великого и могучего выводишь данные на монитор. Довольно типично. Зато с интерфейсом комнат и главного меню я повеселился знатно. И главным генератором хохм послужила анимация.

Весь UI в "Unity" располагается на штуке под названием "Canvas".

&quot;Canvas&quot;
"Canvas"

И эта зараза любит полностью перерисовываться при любом чихе. Поэтому краеугольным камнем создания любого интерфейса становиться оптимизация.

"Unity" для анимирования различных объектов предлагает использовать встроенный компонент "Animator".

Аниматор юнита. Можно настроить переходы между ними, но я переключал анимации напрямую.
Аниматор юнита. Можно настроить переходы между ними, но я переключал анимации напрямую.

Довольно удобная и наглядная штука, когда дело касается юнитов и прочих анимируемых объектов. Однако с интерфейсом ситуация обратная. В сути своей аниматор является "Машиной состояний" и каждый кадр проверяет условия перехода из одного состояния в другое. И в ситуации с канвасом это вызывает серьёзные проблемы, так как аниматор каждый кадр вынуждает интерфейс перерисовывать себя. Даже если визуал элемента интерфейса сейчас никак не меняется. Одним из популярнейших способов "производительного" анимирования UI являются "Tween-анимации". Такие анимации создаются путём изменения базовых параметров(вроде положения, размера и цвета) элемента интерфейса из кода. Именно таким образом и созданы UI анимации в моём прототипе.

Тут я вам поведаю о двух разных подходах к решению одной и той же задачи. Аккомпанировать в рассказе мне будут экран выбора персонажа и рулетка из гача-комнаты.

В чём собственно суть? И рулетка, и селектор персонажа представляют из себя набор ячеек, двигающихся по горизонтали.

Селектор

Главной сложностью здесь является "визуально-бесконечное" прокручивание ряда ячеек. Как же добиться такого эффекта? Начнём с селектора.

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

Допустим, у нас есть 10 ячеек(ну или клеток). Ячейка номер "5" является центральной. Это точка, на которой сфокусирован игрок. Клетки "1", "2" и "9", "10" находятся за пределами экрана(оставляем дополнительные ячейки за экраном, чтобы не было пустых пространств при перелистывании "активной ячейки"). При движении вправо центральной становиться ячейка "6", а остальные клетки съезжают влево(думаю очевидно почему). В это же время ячейка "1" телепортируется на бывшее место клетки "10". Таким образом создаётся иллюзия бесконечности ячеек.

И в реализованном виде эта идея выглядит вот так.

Отображение гачи же работает иначе.

Рулетка

Рулетка должна двигаться только в одну сторону на большой скорости. Можно было бы реализовать такую же систему, как и с селектором персонажа, однако, как по мне, это слишком геморройно и напряжно для компьютера(сугубо из-за скорости вращения рулетки). Поэтому я решил зайти со слегка иной стороны и двигать не ячейки, а саму рулетку.

На старте вращения, рулетка запоминает своё центральное(изначальное) положение и правую и левую границы. Когда рулетка достигает положения своего правого борта, вся махина из ячеек телепортируется на координаты своего левого бока. И тут важную роль играет как раз таки скорость вращения этой самой рулетки. При маленькой скорости игроку будет очевидно, что её обманывают. Однако на высоких скоростях рулетка меняет своё положение настолько быстро, что глазу кажется, будто она действительно вращается.

Ах да, возвращение в исходную точку. Когда время вращения подходит к концу, рулетка, вне зависимости от текущего положения, телепортируется к своему изначальному "центру" и одновременно плавно и агрессивно останавливается.

Ну и выглядит это так.

Скорость 0,25

Остальные анимации были довольно неизобретательны в реализации, а потому я просто прикреплю их в виде гифок.

Комната трейда

Чуть больше комнаты трейда

Карты модификаторов

Главное меню

В целом этими анимациями, да и интерфейсом в целом, я очень сильно доволен. Особенно сильно мне зашла рулетка. Мой маленький "Magnum Opus". Но на этом беседы о UI не заканчиваются. Остались ещё HP-бары врагов и циферки урона, вылетающие из них.

Мне не хотелось лишний раз дёргать именно UI, а потому я решил реализовать последние две интерфейсные штуки каким-нибудь необычным способом. И мне это удалось.

Для отображения здоровья врагов я написал небольшой шейдер, представляющий из себя заполняемую полоску. Ничего необычного.

Шейдер в разрезе
Шейдер в разрезе

А вот с циферками урона всё гораздо интереснее.

На просторах обожаемого в местных кругах сайта из трёх букв я нашёл интересный пост, в котором разработчики игры " The Unliving" рассказывали о том, как использовали систему частиц для вывода на экран различных сообщений. Я прочёл этот пост и мои глаза загорелись, а руки начали копировать.

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

И полоска здоровья, и циферки урона

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

Марафет

Скажу прямо – визуальные эффекты убили меня. Я сидел и рисовал их в Фотошопе. Мышкой. Изначально я хотел найти все нужные мне эффекты на просторах интернета, однако все красивые вещи стоят денег. Пришлось как-то выкручиваться.

Для некоторых вещей было достаточно нарисовать текстуру для материала, а весь сок добавлять уже инструментами "Unity".

Больше всего мне нравится эффект удара по врагу(четвёртый слева)

Что-то срисовывалось покадрово.

А тут я кайфую от верхнего-левого

А часть моей мазни выглядела ну слишком уж плохо.

Нуууууу... я это сделал

Но не смотря на все проблемы, проект приобрёл немного сочности и жизни. И я этому несказанно рад.

Модные пацанчики добавляют ещё и миллиарды эффектов пост-процессинга, но до этого этапа я так и не добрался. "Почему?" – спросите вы. А я вам отвечу.

Итоги

Деньги и время. Они кончились. Сейчас все мои силы будут уходить на выживание, а потому продолжать работу над этим прототипом я не могу. В целом, я успел закончить его процентов на 70%.

Что осталось за бортом:

  • Диалоги. В этом проекте я хотел рассказать небольшую историю и даже общими мазками набросал диалоговую систему. В итоговом прототипе её нет.
  • Аудио составляющая. В прототип можно спокойно играть без колонок и наушников, так как звуков в нём нет. Вообще.
  • Механика "усталости". В оригинальной игре она называлась "Энтропия". Во время прохождения игрок бы накапливал усталость, которая влияла бы на геймплей. Банально понижала бы статы или создавала иллюзий врагов, которые бы не наносили урона, но сбивали игрока с толка
  • Разнообразие в персонажах, комнатах и модификаторах. Сейчас все комнаты представляют из себя кишку и ничем друг от друга не отличаются. Для тестов были созданы модификаторы "Кольцо огня" и "Заморозка". Если первый работает, то второй так и остался заглушкой.
  • Визуал. Хотелось разнообразить частицы и увеличить их количество. Так же хотелось бы поиграться с пост-процессингом.
  • Полировка. Доведение до ума всяких мелких вещей. Навроде дистанции атак, времени между действиями врагов и прочего. В проекте по-прежнему присутствует множество багов. Иногда частицы появляются в самых неожиданных местах, саппорт может бегать по воздуху, UI сбоит. Наверняка есть что-то ещё.
  • Веселье. Последний и, наверное самый важный, пункт. Сейчас в проект играть банально скучно. Врагов можно запинывать в углу, модификаторы ни на что не влияют(потому что их нет), комнаты одинаковы и прочее.

Несмотря на то, что прототип закончить мне не удалось, я доволен тем, что в итоге вышло. Это мой самый крупный и амбициозный проект. На работу над ним у меня ушло около двух месяцев. В этом небольшом эссе я постарался поведать вам о всех моих успехах и неудачах. За скобками остались многие моменты внутреннего устройства кода и занятные трудности разработки. Что-то было бы неинтересно читать, а что-то я банально забыл. Ну ладно, хватит о грустном. Перейдём к позитиву.

Чему я научился за это небольшое путешествие:

  • Архитектура кода. Я наступил на множество граблей и теперь буду знать, с какими трудностями столкнусь при работе над более крупными и серьёзными проектами. Считаю, что мой код стал лучше. Он не самый аккуратный и читабельный, но потихонечку строчка за строчкой я повышаю свой навык.
  • Работа с UI. Альфа и омега этого прототипа. В моих прошлых поделках интерфейса либо не было вообще, либо он был штучным и сугубо функциональным. В этот раз UI я не только программировал, но и рисовал, что помогло лучше понять, с какими трудностями сталкиваются UI/UX дизайнеры. Это действительно невероятно трудная и кропотливая работа.
  • Художественные навыки. Тут я не сильно прокачался, зато понял на какие примерно кадры лучше разбивать покадровые анимации и как проще всего их рисовать(срисовывать).
  • Литьё воды. Ну и конечно же, как можно забыть про этот замечательный блог? Потихоньку учусь писать интересные тексты и расширяю свой литературный запас.

И это, наверное, всё, что я хотел сказать. Прикреплю ещё ссылку на GIT проекта. Можете посмотреть код. Ну или собрать и запустить проект, если есть такое желание. Всем счастья, удачи, денег и здоровья. Ещё увидимся!

Ссылка: https://github.com/Gryisic/RoguelikeFighting/tree/main

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

Дамы и господа! Представляю вашему вниманию геймплей моего последнего проекта под названием "Fighting Climax: Cringe Effect".

 

Геймплей


Лучшие комментарии

Спасибо за приятные слова) Я вкладывал в этот блог всю свою душу и любовь к видеоиграм и их созданию. Рад, что людям заходит.

Привет. Один этот пост был лучше чем 80% дневников разработки, которые тут публикуются.

Он не самый аккуратный и читабельный, но потихонечку строчка за строчкой я повышаю свой навык.

Я видел хуже. Намного. От людей, у которых в должности было слово «senior». Не опускай руки, и у тебя всё получится.

Очень хорошо оформленный текст. Последовательно, с расстановкой, с примерами внятно написанного кода. Было интересно, спасибо за чтиво!

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

Читай также