company_banner

Про создание платформера на Unity. Часть 4.1, злодейская

  • Tutorial
Привет, Хабр!

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



Осторожно, под катом по-прежнему много гифок!



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

Итак, поехали. Я выделил четыре основных типа объектов, которые могут так или иначе помешать герою вашей игры достигнуть цели:

1) Статичные (вращающиеся пилы, «смертельные» блоки итд)
image

2) Ходящие по платформам (грибы и черепахи из Super Mario Bros)


3) Летающие (вороны со второго уровня Ghosts and Goblins)
image

4) Стреляющие (Баужя из своего замка того же Марио)


Еще есть боссы, но они могут объединять в себе все указанные выше типы, поэтому на них останавливаться не будем.

1) Статичные препятствия.
Сделаем вращающуюся пилу. Для реализации такого «врага» нужны буквально пара вещей — спрайт пилы и скрипт, который будет ее вращать. Сказано — сделано.

Перетаскиваем спрайт на сцену:
image

Создаем новый скрипт (это, как обычно, очень просто)
image

И добавляем туда код, выглядящий примерно так:
using UnityEngine;
using System.Collections;

public class rotator : MonoBehaviour {
	public float speed = 0.04f;
	
	void Update () {
		 transform.Rotate (new Vector3 (0f, 0f, speed * Time.deltaTime));
	}
}


В публичной переменной speed задается скорость вращения. Важно отметитить, что положительное значение вращает пилу против часовой стрелки, а отрицательное — по часовой.

Теперь, чтобы взаимодействовать с пилой, добавим на нее какой-нибудь коллайдер (подробно это описано в интереснейших предыдущих частях) и изменим тег объекта на какой-нибудь подходящий в данной ситуации.

Теги — хорошая штука. Мы можем назначить игроку тег player, врагам тег enemy, а стенам и полу — level. После этой нехитрой процедуры проверка того, с чем мы, например, столкнулись, будет происходить гораздо проще. А еще можно найти какой-нибудь один (или все, что есть на сцене) объект с определенным тегом. Делается это примерно так:

GameObject someEnemy = GameObject.FindGameObjectWithTag ("Enemy");


Массив всех объектов с заданным тегом можно получить используя метод FindGameObjectsWithTag — проще некуда.

Вернемся к нашим пилам и создадим новый тег для врагов в игре.
image

В скрипт персонажа добавим следующую проверку
	void OnCollisionEnter2D(Collision2D col){
				if (col.gameObject.tag == "Enemy")
						Application.LoadLevel (Application.loadedLevel);
		}


Как видите, все элементарно: проверяем коллизии, проверяем тег того, с чем столкнулись. Если все плохо, перезагружаем уровень. Или отнимаем жизнь. Или что-нибудь в этом духе. Вы ведь играли в платформеры, правда?

Вот как все это выглядит в итоге:
image

Вот так, просто и быстро, мы создали первого врага — который, конечно, не враг, но вполне себе препятствие. Идем дальше.

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

К сожалению, рисовать я не умею и моего творческого таланта хватило только на такого злодея:

Для его перемещения используем следующий скрипт
using UnityEngine;
using System.Collections;

public class walkingEnemy : MonoBehaviour {
	public float speed = 7f;
	float direction = -1f;
	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
		rigidbody2D.velocity = new Vector2 ( speed * direction, rigidbody2D.velocity.y);
	}

Задаем скорость врага и направление его движения (-1 — влево, 1 — вправо), которое можно менять при столкновении со стенами, к примеру. Дальше просто — устанавливаем горизонтальную скорость, равную произведению значения скорости и направления.

Забавный факт
Забавный факт — если поставить у rigidbody2D галку fixedAngle, то враг будет ползти, а если убрать, то
image

«Но он смотрит вправо, а двигается влево!» — заметит внимательный читатель. Давайте пофиксим это и будем разворачивать спрайт соответственно направлению движения:

using UnityEngine;
using System.Collections;

public class walkingEnemy : MonoBehaviour {
	public float speed = 7f;
	float direction = -1f;
	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
		rigidbody2D.velocity = new Vector2 ( speed * direction, rigidbody2D.velocity.y);
		transform.localScale = new Vector3 (direction, 1, 1);
	}
}


И научим разворачиваться при столкновении со стеной. Для этого сделаем на уровне пару стен с тегом wall и напишем обработку коллизий. Вот такую:

void OnCollisionEnter2D(Collision2D col){
		if (col.gameObject.tag == "Wall")
						direction *= -1f;
	}

Теперь, когда все на месте, итоговый результат будет выглядеть вот так.
image
Останется только добавить врагу тег Enemy для того, чтобы он действительно стал опасен для нашего персонажа.

Подведем промежуточный итог. Мы разобрали как создаются два типа «врагов» в 2D-платформерах: статичные и перемещающиеся по уровню. Как видите, это действительно очень просто и базовая реализация занимает совсем мало времени.

В следующей части я расскажу как создать два остальных вида врагов — летающих и стреляющих.
Stay tuned — будет интересно!

Предыдущие серии:
Часть первая, в которой мы учимся ходить
Часть вторая, в которой мы учимся проигрывать
Часть третья, в которой мы веселимся с физикой

Еще немного полезных ссылок


Изучить курсы виртуальной академии Microsoft по созданию игр и другим технологиям
Введение в программирование игр на Unity
Увлекательное программирование на языке C#

Загрузить Unity
Загрузить бесплатную или пробную Visual Studio
Стать разработчиком универсальных приложений Windows
Попробовать Azure бесплатно на 30 дней!
Microsoft
Microsoft — мировой лидер в области ПО и ИТ-услуг

Комментарии 17

    –2
    В корпоративном блоге Microsoft без ката…
      +2
      Как же без ката? Он на месте.
        –2
        Не было, но надо отдать должное — очень быстро исправились)
      +3
      if (direction == -1)
        transform.localScale = new Vector3 (-1, 1, 1);
      else
        transform.localScale = new Vector3 (1, 1, 1);
      

      к чему лишние проверки?

      transform.localScale = new Vector3 (direction, 1, 1);
      
        +4
        И правда! Спасибо, добавлю в пост!
          0
          A вдруг direction нуль иль абы что. Будет весело вдоль x.
            0
            Как инструкция
            direction *= -1;
            

            может превратить '1' в «нуль иль абы что»?
              +1
              Да легко, Вы же ошиблись при копировании инструкции)
          +5
          Еще один момент. Этот код:
          void Update () {
          	transform.Rotate (new Vector3 (0f, 0f, speed));
          }
          

          Сделает вращение зависимым от FPS.
          Вот так лучше (теперь speed это градусы\секунда, а не градусы\update):
          void Update () {
          	transform.Rotate (new Vector3 (0f, 0f, speed * Time.deltaTime));
          	// или
          	// transform.Rotate (Vector3.forward * speed * Time.deltaTime);
          }
          


          Особенно это важно для физики, а не для анимации. Но там используется фиксированный шаг (Time.fixedDeltaTime), очевидно, что ускорение игрока от фпс зависеть не должно :)
            0
            По-моему, такое замечание было в комментариях к одному из прошлых постов. Вечно забываю про Time.deltaTime! Спасибо большое!
            +1
            Ни в коем случае нельзя вращать или перемещать коллайдеры без твердых тел. Это вызывает их пересоздание и полный пересчет всей физики в кадре. Также, нельзя перемещать твердые тела через их трансформы. Вы можете этого не заметить на одной пиле, но уже пару сотен поставят любое мобильное устройство на колени. Добавьте на пилу kinematic твердое тело, отключите ему гравитацию и исправьте скрипт:
            Единственный допустимый вариант вращения объекта с коллайдером
            public class Saw : MonoBehaviour
            {
                [SerializeField]
                private float speed = 1f;
            
                private float currentAngle = 0;
                private Rigidbody2D cachedRigidbody;
            
                void Awake() {
                    cachedRigidbody = rigidbody2D;
                }
            
                void Update() {
                    currentAngle += speed * Time.deltaTime;
                    currentAngle %= 360;
                    cachedRigidbody.MoveRotation( currentAngle );
                }
            }

            Без профайлера причина будущих тормозов будет совсем неочевидна. Тест на 240 пилах. Слева — ваша реализация. Посередине — пила с твердым телом, но вращение через трансформ. Справа — моя реализация.
            Многократная разница в производительности


            В данном конкретном случае есть еще более быстрая альтернатива — делаете статический объект пилы, с коллайдером, но без спрайта, добавляете ему в детей еще один объект, но со спрайтом и вращаете именно его. Но можно забыть, что тело нельзя двигать, добавить какие-то движущиеся вращающиеся пилы и напороться на ту же ошибку…
              0
              Красивые графики, без комментариев заберу в копилку (если когда-нибудь доберусь до Unity).
              Не подскажете, зачем сериализовать поле speed?
              [SerializeField]
                  private float speed = 1f;
              
                0
                Таким образом оно остается private и сохраняется инкапсуляция, но при этом остается возможность удобного изменения его значения непосредственно в редакторе.
                Грубо говоря, это пометка «показывать в редакторе».
                0
                Кстати, может подскажете:
                Начал делать один прототип, есть 2d планеты и физ. объекты на них и в космосе.
                Физические тела, попадающие в коллайдер-триггер этой планеты, начинают притягиваться и вращаться вокруг центра.
                Проблема возникает с вращением. Вращаю объекты через MovePosition и МоveRotation. (изначально делал добавление тел в transform самой планеты, при попадании в коллайдер, и вращал саму планету. Скорость и угловая скорость при этом, судя по всему, не «поворачивается». Отказался от такого наследования).
                Ощущение, что при вызове MovePosition или МоveRotation сбрасывается либо скорость объекта, либо применяемые силы (стоит раскомментировать эти строки, аddForce перестает адекватно работать).

                (Прошу прощения, что заставляю ванговать, куски исходников смогу отправить только вечером)
                  0
                  Я так понимаю, и планеты, и объекты у вас — твердые тела, а не просто коллайдеры. Твердое тело не может быть ребенком твердого тела и при этом вести себя нормально — родитель будет «доминировать» непредсказуемым образом, поэтому у вас не работало наследование.
                  MovePosition и MoveRotation выполняют, по сути, физическую телепортацию за один кадр, что применимо лишь в очень простых или очень синтетических случаях, вроде вращения пилы или движения уровня вокруг игрока. Т.е. если ваши твердые тела кинематические и следуют упрощенным правилам симуляции. Скорость, сила, трение — более сложные понятия и при телепортации могут ломаться. Если вы планируете использовать MovePosition с MoveRotation и вам хочется иметь силы и скорости — вам придется написать с их использованием свой собственный addForce, addVelocity и.т.п., потому что имеющиеся могут вести себя непредсказуемо криво.
                  Если вам нужно просто начать красиво падать на планету при пролете мимо — добавляйте силу в направлении ее центра в OnTriggerStay2D — это будет лучшим решением. Если вам нужно выйти на орбиту и держаться на ней, при этом используя физический движок… все будет намного сложнее, придется достать школьных учебников по физике и это уже тема для отдельной статьи.
                  Думаю, на неделе напишу статью про свою борьбу с физикой в Unity… в нашей игре физика используется ну очень специфически и движок был явно не готов к таким конструкциям.
                    0
                    Нет, сама планета — не твердая, это некая зона, отображаемая графически:
                    Выглядит это так
                    image

                    На нее повешан триггер, в обработчике OnTriggerStay2D которого я применяю гравитацию, вращение и т.д.
                    И после применения в нем MovePosition и MoveRotation, похоже, применяемые силы и правда ломаются.
                  0
                  Потрясающе компактный и насыщенный полезностью комментарий. Вроде я так и не делал, но свой проект на всякий случай перепроверю :)

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

                Самое читаемое