0
Обложка: Создаем гиперказуальную игру в стиле Fire Balls 3D на Unity

Создаем гиперказуальную игру в стиле Fire Balls 3D на Unity

Александр Ватолин
Александр Ватолин
Геймдизайнер и преподаватель в Школе программистов МШП

Меня зовут Ватолин Александр, я геймдизайнер, а для этого почти всегда нужны знания в самых разных областях: от разработки до 3D-моделирования. Готов ими поделиться.

Сейчас расскажу, как создать мобильную игру на основе гиперказуалки Fire Balls 3D.

В чем соль

Мы сделаем работающий прототип с помощью декомпозиции другого проекта. Логичный вопрос, который сразу возникнет: зачем копировать другой проект, а не создавать свой?

И у нас на это 5 причин:

— Пригодится для портфолио;

— Важно уметь декомпозировать фишки из других проектов для удачной переработки идеи. А иногда и просто для того, чтобы понять, что пишет твой сосед по отделу;

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

— Вы отрабатываете для себя новые решения. Сегодня мы познакомимся с DOTween, к примеру.

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

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

Скриншоты из игры Fire Balls 3D

Секрет игры Fire Balls 3D в простоте и увлекательности. Скачайте ее и поиграйте пару минут. Вы поймете, чем она цепляет.

Где будем разрабатывать игру?

Нам нужна мобильная, гиперказуальная, с простой графикой. Unity тут просто идеально подходит.

Устанавливаем любую 19+ версию и готовим несколько модулей, которые пригодятся при разработке:

  • Probuilder. Отличный пакет для постройки прототипов. Но он не поможет в постройке базовых моделей, если вы ничего не знаете о 3D.
  • DOTween. Его реально найти в AssetStore. Он нам понадобится для создания основной анимации. Это очень мощный пакет, который удобен при разработке не слишком сложных игр. Если еще не знаете, что это, советую изучить!
  • Любой шейдер для воды. Один из них вы увидите в конце статьи в материалах. Если есть свои любимчики, можете использовать любой другой.

Вот и все.

Начнем разработку

После создания проекта импортируем данные проекты и не забываем переключиться в File -> Build Settings на платформу Android.

Если у вас нет данной платформы, установите Android SDK для Unity.

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

Тут есть не все нужные папки, но такая структура обеспечивает простую навигацию

Теперь построим платформу, состоящую из куба и цилиндра. Также нам понадобится установить камеру под нужный ракурс и создать Пустышку — она будет управлять башней из создаваемых блоков. Назовем ее Tower.

Вариант постройки платформы, установки камеры и расположения Пустышки

Также создадим вариант кольцацилиндра, что будет выступать в роли части Башни. Создадим из нее префаб, не забыв создать для них папочку.

Пришло время создать первый скрипт, назовем его TowerBuilder и прикрепим его к нашей Пустышке.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TowerBuilder : MonoBehaviour
{
    [SerializeField] private float towerSize;
    [SerializeField] private Transform SpawnPoint;
    [SerializeField] private BlockTower towerBlock;
    private List<BlockTower> towerBlocks;

    public List<BlockTower> Build()
    {
        towerBlocks = new List<BlockTower>();
        Transform currentPoint = SpawnPoint;
        for (int i = 0; i < towerSize; i++)
        {
            BlockTower newBlock = BuildTower(currentPoint);
            towerBlocks.Add(newBlock);
            currentPoint = newBlock.transform;
        }
        return towerBlocks;
    }
    private BlockTower BuildTower(Transform CSpawnPoint)
    {
        return Instantiate(towerBlock,GetSpawnPoint(CSpawnPoint), Quaternion.identity, SpawnPoint);
    }
    private Vector3 GetSpawnPoint(Transform curBlock)
    {
        return new Vector3(SpawnPoint.position.x,
            curBlock.position.y + curBlock.localScale.y*1.5f + towerBlock.transform.localScale.y/2,
            SpawnPoint.position.z);
    }
}

Попробуем разобраться, что это за скрипт. Филды нам нужны для параметров скрипта, а именно:

towerSize — размер нашей башни.

SpawnPoint — местоположение точки появления. Именно здесь будет браться наша Пустышка.

towerBlock — Блок башни, наш цилиндр. Можете заметить, что он типа BlockTower, что не позволит поместить любой gameobject, и вынудит нас создать скрипт BlockTower и прикрепить к префабу блока.

towerBlocks — список, хранящий текущие блоки башни.

Метод Build() выстраивает башню при помощи двух вспомогательных методов:

BuildTower — устанавливает блоки в позиции.

GetSpawnPoint — рассчитывает координаты положения, учитывая сдвиги с каждым новым сдвигом.

Весь метод работает в цикле, таким образом выстраивая полностью башню.

Не забываем добавить к префабу башни MeshCollider(Vertex — on, trigger — on).

В итоге у нас получится башня в соответствии с нашими настройками.

Можно заметить, что мы выставили 50 блоков башни, что и совершилось благодаря TowerBilder (для этого достаточно вызвать Build в Start).

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

Помимо пушки создадим префаб снаряда — простого шарика.Также внутри танка расположим Shot point — место, откуда будут вылетать снаряды.

Скрипт снаряда назовем Bullet и реализация (пока что) у него будет простая:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    [SerializeField] private float speed;
    private Vector3 moveDirection;
    void Start()
    {
        moveDirection = Vector3.left;
    }

    void Update()
    {
        transform.Translate(moveDirection * speed * Time.deltaTime);
    }
}

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

Есть снаряд, являющийся префабом с данным скриптом. Осталось реализовать пушку. Пишем:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Gun : MonoBehaviour
{

    [SerializeField] private Transform shootPoint;
    [SerializeField] private Bullet bulletTemplate;
    [SerializeField] private float delayBetweenShoots;
    private float timeAfterShoot;
    private void Update()
    {
        timeAfterShoot += Time.deltaTime;
        if (Input.GetMouseButtonDown(0))
        {
            if (timeAfterShoot > delayBetweenShoots)
            {
                Shoot();
                timeAfterShoot = 0;
            }
        }
    }
    private void Shoot()
    {
        Instantiate(bulletTemplate, shootPoint.position, Quaternion.identity);
    }
}

Тут все еще проще. У пушки есть параметры под позицию выстрела, снаряда, чем стрелять, а также времени перезарядки.

Цикл проверяет нажатие на экран (либо левый клик мыши), и если время превысило время перезарядки, производит выстрел. После этого происходит обнуление счетчика. За само производство снарядов отвечает метод Shoot().

Что у нас есть на данную минуту:

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

Теперь подготовим систему удаления блоков при столкновении. Будет много кода, так что готовьтесь.

Начнем с метода BlockTower , он лежит внутри пребафа блока башни, помните?

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class BlockTower : MonoBehaviour
{
    public event UnityAction<BlockTower> BulletHit;
    public void Break()
    {
        BulletHit?.Invoke(this);
        Destroy(gameObject);
    }
}

Тут есть просто система самоуничтожения, а также событие, к которому мы вернемся чуть позже. Просто напишите его, чтобы не возвращаться еще раз.

Вернемся к Bullet. Добавим систему проверяющую столкновение с блоком башни. Тут все просто, появится один if в OnTriggerEnter.

Однако у блоков башни должно быть: MeshCollider (Vertex — on, trigger — on).

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    [SerializeField] private float speed;
    private Vector3 moveDirection;

    void Start()
    {
        moveDirection = Vector3.left;
    }

    void Update()
    {
        transform.Translate(moveDirection * speed * Time.deltaTime);
    }

    private void OnTriggerEnter(Collider other)
    {
      if (other.TryGetComponent(out BlockTower block))
            {
                block.Break();
                Destroy(gameObject);
            }
    }
}

Данное условие вызывает уничтожение блока и шарика. Работает это сейчас примерно так, что никуда не годится:

Пришло время реализовать систему опускания башни.

Реализация управляющей системы башни ( прикрепляем к Tower):

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

[RequireComponent(typeof(TowerBuilder))]
public class Tower : MonoBehaviour
{
    private TowerBuilder towerBuilder;
    private List<BlockTower> blocks;
    public event UnityAction<int> SizeUpdated;
    void Start()
    {
        towerBuilder = GetComponent<TowerBuilder>();
         blocks = towerBuilder.Build();

        foreach (var block in blocks)
        {
            block.BulletHit += OnBulletHit;
        }
        SizeUpdated?.Invoke(blocks.Count);
    }

    private void OnBulletHit(BlockTower hitblock)
    {
        hitblock.BulletHit -= OnBulletHit;

        blocks.Remove(hitblock);

        foreach (var block in blocks)
        {
            block.transform.position = new Vector3(block.transform.position.x, block.transform.position.y - block.transform.localScale.y*2, block.transform.position.z);
        }
        SizeUpdated?.Invoke(blocks.Count);
    }

}

Попробуем разобраться, что тут происходит. Для начала вспомним событие BulletHit, которое создается при уничтожении блока башни. Оно является спусковым крючком, которое будет вызывать методы OnbulletHit().

А теперь подробнее:

— В Старте мы инициализируем механизм постройки башни, что более корректно с точки зрении ООП, ну и просто удобнее;

— Далее для каждого созданного блока мы подключаем OnBulletHit для события BulletHit. Многие отметят, что корректно помимо подписки на событие реализовать и отписку, которая и реализована в данном методе;

— Метод OnBulletHit убирает блок из списка блоков, обновляя информацию о башне, сдвигая ее вниз. Важный момент: мы уничтожаем нижний блок, поэтому логично сдвигать всю башню вниз. Однако если мы бы уничтожали не нижний блок, пришлось бы сдвигать только элементы, которые выше данного блока.

— Также тут можно заметить событие SizeUpdated, которое нам понадобится на этапе проектирования интерфейса.

Итак, башня реализована:

Реализация системы башни. К слову вот здесь уровень башни был поставлен ниже для удобства работы

Пришло время вспомнить о препятствиях и Probuilder. Нам понадобится Пустышка, которая станет центром вращения, а также несколько элементов препятствий. Это могут быть хоть классические кубы, но лично я использовал Arch из ProBuilder.

Вариант расположения препятствий

Для частей мы создадим скрипт ObstacleChapter, который оставим в стартовом варианте, на данном этапе он нам не понадобится.

Для Пустышки же создадим скрипт Obstacle, который реализует вращения данной структуры.

Скрипт уже потребует работы с DOTween, мы покажем один из методов, но внутри пакета их множество.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
public class Obstacle : MonoBehaviour
{
    [SerializeField] private float animationDuration;
    private void Start()
    {
        transform.DORotate(new Vector3(0,360,0),animationDuration, RotateMode.FastBeyond360).SetLoops(-1,LoopType.Yoyo);
    }
}

Если внимательно прочитать, то можно понять, что метод позволит вращать объект вокруг своей оси.

Следом у нас идет модификация On trigger Enter из Buller.cs

private void OnTriggerEnter(Collider other)
    {
      if (other.TryGetComponent(out BlockTower block))
            {
                block.Break();
                Destroy(gameObject);
            }
      if (other.TryGetComponent(out ObstacleChapter obstacleChapter))
        {
            moveDirection = Vector3.right + Vector3.up;
            Rigidbody rigidbody = GetComponent<Rigidbody>();
            rigidbody.isKinematic = false;
            rigidbody.AddExplosionForce(bounseForce, transform.position + new Vector3(0,-1,1),bounseDistance);
        }
    }

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

Последняя часть, которая добавит шарма, это простенький Ui. Создадим TMP )text-mesh-pro, установив canvas в мировой режим и расположив все элементы на сцене. Мои настройки вы сможете изучить на скриншоте:

Настройки и итоговый вариант расположения. В конце я отцентрировал текст, что упростило расположение блока

Ну и скрипт для управления Ui:

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class TowerSizeView : MonoBehaviour
{
    [SerializeField] private TMP_Text sizeView;
    [SerializeField] private Tower tower;

    private void OnEnable()
    {
        tower.SizeUpdated += onSizeUpdated;
    }

    private void OnDisable()
    {
        tower.SizeUpdated -= onSizeUpdated;
    }

    private void onSizeUpdated(int size)
    {
        sizeView.text = size.ToString();
    }
}

Реализуем систему подписки на событие SizeUpdated из Tower.cs. По своей сути она просто берет размера списка блоков и выводит его на экран.

Что же у нас получилось:

А теперь попробуем разобраться, что мы сделали:

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

А какие улучшения можно внести ?

  • Графика. Думаю, тут все просто, но все же перечислю: новые модели, цветовая палитра, работа над окружением, разноцветные платформы;
  • Меню входа и выхода. Данный проект не предполагает сложного меню, однако оно явно не помешает;
  • Система уровней. Переход со сцены на сцену, либо перестройка уровня на основе конфига с данными об уровнях. Любая система, которая вам покажется удачной.
  • Наполнение контентом. Какой он тут может быть ? Простой, как и сама игра: другие уровни, несколько уровней препятствий, особые выстрелы пушкой.

Итого: мы успешно реализовали протопип игры, у которого может быть развитие. Буду рад, если поделитесь конечными результатами.

А теперь обещанные материалы:

До встречи!