Как быстро написать игру для Android на Unity

unity

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

Чтобы продемонстрировать, насколько легко написать что-то подобное, сегодня мы напишем свой Flappy Bird с помощью Unity всего за 10 минут.

Игровой персонаж

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

Загрузите свой спрайт птицы в сцену. Не забудьте включить фантазию!

Затем отрегулируйте размер спрайта как вам нравится, перетягивая его за угол в нужном направлении. Спрайт должен быть виден в окне иерархии (hierarchy) слева. В нём видны все объекты в сцене, и на данный момент их должно быть всего два: камера и птица.

Перетащите камеру на птицу и отпустите. Камера должна оказаться под птицей, это будет значить, что камера теперь «ребёнок» птицы. Теперь позиция камеры будет фиксироваться относительно птицы. Если птица двигается вперёд, то камера делает то же самое.

unity

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

Теперь нажмите на Add Component. Выберите Physics2D > Rigidbody2D — это готовый набор инструкций для применения гравитации к нашему персонажу. Нажмите на Constraints в этой панели и затем выберите freeze rotation Z. Это позволит предотвратить вращение птицы вместе с камерой по кругу.

Таким же образом добавьте Polygon Collider, который говорит Unity, где находятся границы персонажа. Нажмите Play и увидите, как спрайт вместе с камерой бесконечно падает вниз.

unity

Пока всё идёт хорошо!

Теперь пора заняться полётами персонажа, благо это будет несложно.

Сначала нужно создать C#-скрипт. Создайте для него папку (кликните правой кнопкой мыши где-нибудь в assets и создайте папку «Scripts»), сделайте клик правой кнопкой мыши и выберите Create > C# Script.

Назовём его «Character». Кликните по нему дважды, чтобы открыть его в вашей IDE, будь то MonoDevelop или Visual Studio. Затем добавьте следующий код:

public class Character : MonoBehaviour {

    public Rigidbody2D rb;
    public float moveSpeed;
    public float flapHeight;

    // Это нужно для инициализации
    void Start () {
        rb = GetComponent();
    }
      

    // Update вызывается один раз на кадр
    void Update () {
        rb.velocity = new Vector2(moveSpeed, rb.velocity.y);
        if (Input.GetMouseButtonDown(0))
        {
            rb.velocity = new Vector2(rb.velocity.x, flapHeight);
        }

        if (transform.position.y > 18 || transform.position.y < -19)
        {
            Death();
        }

    }

    public void Death()
    {
        rb.velocity = Vector3.zero;
        transform.position = new Vector2(0, 0);
    }

}

Этот код делает две вещи. Он заставляет персонажа двигаться вперёд со скоростью, которую мы определим в инспекторе, и создаёт ощущение полёта птицы. Метод Update() вызывается повторно на протяжении игры, поэтому всё, что вы сюда поместите, будет выполняться непрерывно. В данном случае мы добавляем немного скорости нашему объекту. Переменная rb является скриптом RigidBody2D, который мы применили к нашему объекту ранее, поэтому когда мы пишем rb.velocity, мы обращаемся к скорости объекта.

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

Переменная moveSpeed будет отвечать за скорость движения, а переменная flapHeight — за увеличение высоты полёта птицы после каждого нажатия. Поскольку эти переменные объявлены как public, мы сможем изменить их вне скрипта.

Метод Death() тоже объявлен как public, что значит, что другие объекты и скрипты смогут его вызвать. Этот метод просто возвращает положение персонажа в начало. Также он будет использоваться каждый раз, когда персонаж будет залетать слишком высоко или низко. Скоро вы поймёте, почему он объявлен именно как public. Строка rb.velocity = Vector3.zero; нужна, чтобы убрать импульс — мы же не хотим, чтобы после каждой смерти персонаж падал всё быстрее и быстрее?

Теперь можно выходить из IDE и добавлять скрипт как компонент к персонажу. Для этого нужно выбрать нашу птицу и нажать Add Component > Scripts > Character. Теперь мы можем определять moveSpeed и flapHeight в инспекторе (для этого и нужны public-переменные). Присвоим переменным значения 3 и 5 соответственно.

И ещё: в инспекторе нужно добавить тег к персонажу. Для этого кликните там, где написано Tag: Untagged и затем выберите Player в выпадающем списке.

Препятствия

Теперь добавим препятствия: трубы. Кто-то в трубах находит грибы, а кто-то — свою смерть.

Перетащите спрайт трубы в сцену в место, где должно быть первое препятствие, и назовите его pipe_up.
Теперь создадим новый скрипт под названием Pipe:

public class Pipe : MonoBehaviour {

    private Character character;

    // Это нужно для инициализации
    void Start () {
        character = FindObjectOfType();
    }

      
    // Update вызывается один раз на кадр
    void Update () {
        if (character.transform.position.x - transform.position.x > 30)
        {

        }
    }

    void OnCollisionEnter2D(Collision2D other)
    {
        if (other.gameObject.tag == "Player")
        {
            character.Death();  
        }
    }

}

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

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

Итак, у нас есть одна труба, которая время от времени будет исчезать и появляться на другом конце экрана. Врежешься в неё — умрёшь.

Перевёрнутые трубы

Сейчас у нас есть только один спрайт трубы. Давайте добавим ещё один. Для этого кликните правой кнопкой мыши в окне иерархии, нажмите New 2D Object > Sprite и затем выберите спрайт, который хотите использовать. Ещё проще будет просто перетащить файл в сцену ещё раз.

Назовите этот спрайт pipe_down. В инспекторе в части Sprite Renderer выберите опцию Flip Y, чтобы перевернуть трубу вверх ногами. Добавьте такое же RigidBody2D.

unity

Теперь напишем новый C#-скрипт под названием PipeD. В нём будет похожий код:

public class PipeD : MonoBehaviour {

    private Character character;

    //Это нужно для инициализации
    void Start()
    {
        character = FindObjectOfType();
    }

    // Update вызывается один раз на кадр
    void Update()
    {
        if (character.transform.position.x - transform.position.x > 30)
        {
        }

    }

    void OnCollisionEnter2D(Collision2D other)
    {
        if (other.gameObject.tag == "Player")
        {
            character.Death();
        }

    }

}

Префабы

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

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

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

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

unity

Для этого создайте новую папку «Prefabs». Затем перетащите pipe_up и pipe_down из окна иерархии в папку.

Каждый раз, когда вы перетаскиваете объект из этой папки в сцену, у него будут те же свойства, поэтому вам не нужно будет постоянно добавлять компоненты. Более того, если вы измените размер компонента в папке, это повлияет на все трубы в игре, и вам не придётся изменять каждую из них отдельно.

Как вы понимаете, это сильно сэкономит наши ресурсы. Также это означает, что мы можем взаимодействовать с объектами из кода. Мы можем создавать экземпляры наших труб.

Сначала добавьте этот код в условное выражение в методе Update()  скрипта Pipe, которое мы оставили пустым:

void Update () {
        if (character.transform.position.x - transform.position.x > 30)
        {

            float xRan = Random.Range(0, 10);
            float yRan = Random.Range(-5, 5);

            Instantiate(gameObject, new Vector2(character.transform.position.x + 15 + xRan, -10 + yRan), transform.rotation);
            Destroy(gameObject);

        }

    }

Это нужно для создания экземпляра нашего gameObject. В результате получается новая идентичная копия. В Unity всегда, когда вы используете слово gameObject, оно ссылается на объект, к которому скрипт в данный момент привязан — в нашем случае к трубе.

Мы генерируем заново наши трубы в случайных вариациях, чтобы было повеселее.

unity

Но вместо того, чтобы проделывать всё то же в скрипте PipeD , мы генерируем оба объекта в одном месте. Таким образом мы можем легко устанавливать положение второй трубы относительно первой. Также это значит, что нам нужно меньше кода для PipeD.

Создайте public gameObject с именем pipeDown. Затем обновите код следующим образом:

if (character.transform.position.x - transform.position.x > 30)
        {

            float xRan = Random.Range(0, 10);
            float yRan = Random.Range(-5, 5);
            float gapRan = Random.Range(0, 3);

            Instantiate(gameObject, new Vector2(character.transform.position.x + 15 + xRan, -11 + yRan), transform.rotation);
            Instantiate(pipeDown, new Vector2(character.transform.position.x + 15 + xRan, 12 + gapRan + yRan), transform.rotation);
            Destroy(gameObject);

        }

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

Возвращаемся обратно в Unity и перетаскиваем префаб pipe_down из папки с префабами (это важно!) в место, где написано  «Pipe Down» (заметьте, как наш camel case заменяется пробелом) на спрайт трубы pipe up. Если вы помните, мы определили Pipe Down как public gameObject, что даёт нам возможность определять, чем является этот объект откуда угодно – в данном случае через инспектор. Выбирая префаб для этого объекта, мы убеждаемся, что при создании экземпляра трубы он будет включать в себя все атрибуты и скрипт, которые мы добавили ранее. Мы не просто создаём спрайт, но пересоздаём объект с коллайдером, который может убить персонажа.

Всё, что мы добавим в то же место в скрипте PipeD —  просто Destroy(gameObject),  чтобы труба саморазрушалась при выходе за левую границу экрана.

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

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

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

Бесконечный полёт

Теперь создадим public-переменные pipe_up и pipe_down в скрипте Character. Это даст вам возможность ссылаться на созданные объекты, перетаскивая префабы на объект персонажа, прямо как когда мы добавили pipe_down в скрипт Pipe.

Нам нужно добавить эти переменные:

public GameObject pipe_up;
public GameObject pipe_down;

Затем мы напишем такой метод:

 public void BuildLevel()
    {
        Instantiate(pipe_down, new Vector3(14, 12), transform.rotation);
        Instantiate(pipe_up, new Vector3(14, -11), transform.rotation);

        Instantiate(pipe_down, new Vector3(26, 14), transform.rotation);
        Instantiate(pipe_up, new Vector3(26, -10), transform.rotation);

        Instantiate(pipe_down, new Vector3(38, 10), transform.rotation);
        Instantiate(pipe_up, new Vector3(38, -14), transform.rotation);

        Instantiate(pipe_down, new Vector3(50, 16), transform.rotation);
        Instantiate(pipe_up, new Vector3(50, -8), transform.rotation);

        Instantiate(pipe_down, new Vector3(61, 11), transform.rotation);
        Instantiate(pipe_up, new Vector3(61, -13), transform.rotation);

    }

Мы будем вызывать его один раз в методе Update() и один раз в методе Death().

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

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

В заключение

Вот мы и сделали целую игру! Добавьте счётчик очков, попробуйте сделать его более оригинальным и увеличивайте сложность игры по мере продвижения. Также не будет лишним сделать меню. Ещё хорошей идеей будет уничтожать трубы на экране после смерти персонажа. Как только вы с этим закончите — считайте, что готовая к выпуску в Play Store игра у вас в кармане! Когда-то похожая игра сделала другого разработчика очень богатым, и это доказывает, что вам не нужно быть гением-программистом или иметь за своей спиной богатого издателя, чтобы достичь успеха. Вам просто нужна хорошая идея и десять минут!

Хотите писать приложения под Android, но не знаете, с чего начать? Тогда ознакомьтесь с нашей большой подборкой ресурсов для изучения Android-разработки.

Перевод статьи «Flappy Bird Unity tutorial for Android – Full game in 10 minutes!»

Ещё интересное для вас:
Тест: какой язык программирования вам стоит выбрать для изучения?
Тест: как хорошо вы разбираетесь в Data Science?
Соревнования и бесплатная онлайн-школа для программистов

Вакансии в тему:

Wheely
IT Project Manager
IT Project Manager
Wheely, Москва, 160 000 ₽
«Банк Точка»
Android developer
Android developer
Банк Точка, Екатеринбург, 130 000 ₽