Привет, это будущие разработчики игр и любопытные люди, которые хотят начать свое путешествие с Unity и C#.

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

Мы сосредоточимся на основах C#, таких как:

  1. Переменные
  2. Операторы if
  3. Логические значения
  4. Мы будем создавать методы
  5. Коллайдеры
  6. Твердые тела
  7. Теги и другие мелкие, но очень важные части основы.

Я полагаю, что как любопытный человек или больше как разработчик у вас уже есть необходимые инструменты, такие как программное обеспечение Unity, на вашем компьютере и IDE, в моем случае это Visual Studio Code. Итак, без лишних слов давайте откроем проект Unity. Убедитесь, что вы выбрали формат 2D для игры.

Здесь у нас есть стартовое окно. Во-первых, мы попробуем создать Sprite, чтобы попробовать использовать программное обеспечение. Нам нужно встать на окно SimpleScene и правой кнопкой мыши выбрать 2D объекты -> спрайты -> Капсула, как это дано на скриншоте ниже.

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

Далее мы погружаемся в сам скрипт. Нам нужно встать на окно ассетов и правым кликом мышки выбрать создать -> C#

и документ появится в окне ресурсов. Давайте назовем документ Driver, чтобы собрать в нем весь код, связанный с драйвером.

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

Настало время узнать, что такое метод в C# и зачем он нам нужен. По общему определению метод представляет собой блок кода (AKA Functions), который запускается только при вызове. В нашем случае метод помогает нам сделать игру динамичной.

Есть два способа:

  1. Методы, которые уже доступны в Unity (так называемые встроенные методы/функции)
  2. Наши собственные методы (которые мы можем сделать в зависимости от персонала, который мы хотим исполнить. Мы узнаем об этом больше в ближайшее время).

Как и все другие функции в любом языке программирования, методы в C# также должны иметь имя и тело, часть, которая сообщает, что она должна делать. Итак, мы пишем код, даем ему имя, а затем вызываем его для выполнения.

Потренируемся на нем. Когда мы дважды щелкнем документ драйвера, который мы создали минуту назад, мы переместимся в код vs (в вашем случае в вашу IDE), и внутри он будет выглядеть так:

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

Затем у нас есть публичный класс. Класс — это способ группировать вещи вместе. Затем у нас есть методы Start() и Update(), это так называемые функции обратного вызова, в которые мы не будем углубляться, но главное, что нужно знать о них, это то, что в нашем случае движок Unity будет знать, что выполнять, когда игра запускается и что обновлять, когда игра будет продолжаться. Так сказать, у нас есть две основные части программы начало и прогресс.

Когда мы откроем Unity и в верхней части в центре мы нажмем кнопку воспроизведения, он запустит код, написанный в методе Start(), и будет обновлять код в методе Update() каждый кадр, когда игра включена. .

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

Бытует мнение, что переменные — это контейнеры для хранения значений данных, но приведу пример из моей любимой книги «Красноречивый Javascript».

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

В C# существуют разные типы переменных (определяемых разными ключевыми словами), например:

  • int — сохраняет целые числа (целые числа) без десятичных знаков, например 123 или -123.
  • double — хранит числа с плавающей запятой с десятичными знаками, например 19,99 или -19,99.
  • float — представляет числа с дробной частью, содержащие один или несколько десятичных знаков.
  • char — хранит отдельные символы, такие как «a» или «B». Значения Char заключены в одинарные кавычки
  • string — сохраняет текст, например «Hello World». Строковые значения заключены в двойные кавычки
  • bool - хранит значения с двумя состояниями: true или false

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

public class Driver : MonoBehaviour
{
    float steerSpeed = 1f;
    float moveSpeed = 0.01f;

    void Start()
    {

    }

    void Update()
    {
        transform.Rotate(0, 0, steerSpeed);
        transform.Translate(0, moveSpeed, 0);
  
    }
}

В приведенном выше коде мы объявляем переменные steerSpeed и moveSpeed ​​ и назначаем их свойству 1f. Затем мы применяем эти переменные к встроенным методам в функции Update. Таким образом, в основном мы говорим, что водитель будет вращаться на 1f и двигаться со скоростью 0,01f. Если какая-то часть кода кажется или кажется трудной для понимания, не паникуйте, я вас понял, мы рассмотрим все необходимые детали по пути.

Теперь пришло время использовать [SerializeField]. это автоматический процесс преобразования структур данных или состояний объектов в формат, который Unity может сохранить и восстановить позже. Другими словами, мы присваиваем его переменной, и переменная видна в Unity под скриптом драйвера.

public class Driver : MonoBehaviour
{
    [SerializeField] float steerSpeed = 1f;
    [SerializeField] float moveSpeed = 0.01f;

}

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

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

Давайте напишем еще одну переменную в нашем коде

void Update()
    {
        float steerAmount = Input.GetAxis("Horizontal");

        transform.Rotate(0, 0, steerSpeed);
        transform.Translate(0, moveSpeed, 0);
  
    }

Здесь мы создали переменную с именем steerAmount и присвоили ей значение Input.GetAxis("Horizontal"), где Input — это метод, описанный выше, и метод GetAxis, который получит конкретную ось мы будем передавать в качестве аргумента внутри, в нашем случае это ось X, поэтому мы пишем в ней "Горизонтальная". Учтите, что слова должны быть написаны правильно, иначе код не сработает. Нашу недавно объявленную переменную мы должны присвоить функции поворота.

void Update()
    {
        transform.Rotate(0, 0, steerAmount);
    }

Но вот хитрость, если мы просто присвоим, он будет работать в обратном направлении, и, конечно, мы этого не хотим, поэтому, чтобы пропустить основной урок математики и объяснение, мы добавим минус перед steerAmount в функции поворота, чтобы драйвер работал правильно. .

void Update()
    {
        float steerAmount = Input.GetAxis("Horizontal");

        transform.Rotate(0, 0, -steerAmount);
    }

Все работает отлично, но теперь мы должны контролировать скорость движения водителя, поэтому для этого метода Input мы умножаем на steerSpeed. Та же переменная steerSpeed, которая объявлена ​​в начале нашего кода.

void Update()
    {
        float steerAmount = Input.GetAxis("Horizontal") * steerSpeed;

        transform.Rotate(0, 0, -steerAmount);
    }

Теперь все работает как надо. Но у нас нет входных данных от вертикальной оси, не так ли? Так давайте тоже напишем.

void Update()
    {
        float steerAmount = Input.GetAxis("Horizontal") * steerSpeed;
        float moveAmount = Input.GetAxis("Vertical") * moveSpeed;

        transform.Rotate(0, 0, -steerAmount);
        transform.Translate(0, moveAmount, 0);
    }

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

Давайте узнаем основы о коллайдерах и твердых телах. Во-первых, давайте проверим знания, которые мы уже получили, добавив еще один спрайт в Unity. Если у вас есть проблемы с запоминанием, прокрутите вверх, и там есть все подробности о том, как это сделать. Так что придайте спрайту любую форму, которая вам нравится, и попытайтесь пройти через него с помощью вашего элемента драйвера. Вы заметили, что ничего не изменилось? это связано с тем, что вновь созданный элемент не имеет границ или ограничений, которые могли бы помешать водителю пройти через него. Для игровых целей нам нужно, чтобы объекты были жесткими и применяли детали к ним обоим.

Во-первых, это добавление коллайдеров. Нажмите на драйвер, затем справа нажмите кнопку Добавить компонент и выполните поиск Capsule Collider 2D, так как наш драйвер имеет форму капсулы. Этот коллайдер означает, что вещи не могут пройти через этот объект, и это очень важно. Мы делаем то же самое для другого объекта, в моем случае он круглый, вы можете выбрать коллайдер формы вашего объекта. Еще одна вещь, которую нам нужно применить к драйверу Rigid Body 2D, и она добавляется так же, как и коллайдер, но вместо этого мы ищем твердое тело 2D. Благодаря этому наш драйвер получит дополнительные функции, такие как масштаб гравитации, и когда мы нажмем кнопку воспроизведения, объект будет двигаться без нашего участия. поэтому нам нужно превратить его в ноль.

Идите вперед и измените шкалу гравитации на 0. После этого, когда мы начнем играть, наши объекты будут отскакивать друг от друга. Это именно то, что мы хотели.

Пойдем дальше и объединим уже полученные знания с некоторыми дополнительными. Теперь мы хотим написать код, который сообщит нам, когда мы столкнемся с другими вещами и многим другим. Для этого нам нужно добавить еще один скрипт. Итак, вперед, создайте новый скрипт под названием Collision. Откройте его и удалите все, что находится внутри открытого класса Collision: MonoBehaviour.

Мы будем использовать встроенный метод Unity, похожий на запуск и обновление, который называется OnCollisionEnter2D. Он объявит так:

puclic class Collision: MonoBehaviour 
{
  private void OnCollisionEnter2D(Collision2D other){}

}

Мы можем удалить слово private из объявления, что пока нас не касается. Давайте проверим, работает ли это. напишите Debug.Log с сообщением в нем.

puclic class Collision: MonoBehaviour 
{
  void OnCollisionEnter2D(Collision2D other){
}
      Debug.Log("We bumped into something");
}

Чтобы проверить это, нам нужно еще два шага. Один — зайти в Unity и добавить в компонент скрипт коллизии, как мы уже умеем, а второй — открыть консоль в Unity.

Когда мы натыкаемся на что-то, здесь появляется сообщение.

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

Идите вперед и сделайте еще один спрайт, я уверен, что вы уже знаете, как это сделать так хорошо. Затем добавьте к нему коллайдер, в моем случае это блочный коллайдер, потому что форма прямоугольная, и когда вы применяете коллайдер, перейдите и щелкните свойство коллайдера, чтобы увидеть свойства, которые он имеет в нем. Здесь вы увидите значение Is Trigger, отметьте его, чтобы иметь возможность использовать этот элемент в качестве триггера в будущем.

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

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

Я уверен, что вы сделали это здорово!

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

puclic class Collision: MonoBehaviour 
{
  void OnTriggerEnter2D(Collision2D other){
}
      Debug.Log("We passed through the trigger");
}

С помощью этого кода сообщение будет напечатано в окне консоли Unity.

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

Теперь пришло время добавить некоторые ресурсы в наш проект. Существует несколько ресурсов для загрузки бесплатных ресурсов. например:

  1. https://unityassets4free.com/ — только в образовательных целях;
  2. https://opengameart.org/ — все необходимые ресурсы для небольших 2D-игр.

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

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

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

Теперь у нас есть набор, основной элемент и нам нужна камера, чтобы перейти к действию!
Для этого мы научимся следить за камерой в действии. Сначала создайте еще один скрипт с именем FollowCamera. В нем нам понадобится только метод Update(). В Unity мы применим этот скрипт к основной камере, она находится в списке сцен, где находятся остальные элементы. Мы хотим, чтобы положение камеры было равно тому, за чем следует следовать.

puclic class FollowCamera: MonoBehaviour 
{

  [SerializeField] GameObject thingToFollow; 

  void Update()
  {
    transform.position = thingToFollow.transform.position;
  }
}

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

puclic class FollowCamera: MonoBehaviour 
{

  [SerializeField] GameObject thingToFollow; 

  void Update()
  {
    transform.position = thingToFollow.transform.position + Vector3(0, 0, -10);;
  }
}

Теперь сцена и камера уменьшены и реагируют на движение.

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

Давайте продолжим. Нам нужно несколько спрайтов в качестве пакетов и один для пользователя, которому мы доставляем наши товары, и, что более важно, нам нужно узнать об операторах на C#.
В этом проекте мы будем использовать операторы if. Операторы if позволяют нам проверить, верно что-то или нет, а затем сделать что-то в зависимости от результата.

Но прежде чем использовать операторы if, нам нужно проверить теги, чтобы иметь возможность связать элементы из единства с определенным кодом на C#. Другими словами, с помощью тега мы легко проверяем в коде, принадлежит ли что-то к определенной категории вещей. Здесь, в окне инспектора, мы нажимаем на тег, затем добавляем тег, как показано на рисунке ниже, и добавляем имя тега. Мы называем это «Пакет».

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

Что касается кода, давайте напишем логику.

puclic class Collision: MonoBehaviour 
{
  void OnTriggerEnter2D(Collision2D other){
}
      if(other.tag == "package"){
          Debug.Log("this is the package");
      }
}

Итак, с учетом сказанного, когда автомобиль будет передавать объект пакета, он отобразит сообщение в консоли. Теперь вам пора потренироваться. Идем дальше и создаем спрайт с названием «клиент» и назначаем ему тег «клиент», чтобы использовать его в коде.

Отличная работа! теперь используйте его в коде.

puclic class Collision: MonoBehaviour 
{
  void OnTriggerEnter2D(Collision2D other){
}
      if(other.tag == "package"){
          Debug.Log("this is the package");
      }

      if(other.tag == "Customer"){
          Debug.Log("this is the Customer");
      }
}
  • Быстрая заметка: не забудьте добавить коллизию на спрайт клиента.

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

puclic class Collision: MonoBehaviour 
{
  bool hasPackage; 
}

Итак, здесь мы объявили логическое значение hasPackage, которое по умолчанию имеет значение false, и мы хотим присвоить его нашему пакету, поэтому мы меняем его на true внутри оператора if пакета.

puclic class Collision: MonoBehaviour 
{
  bool hasPackage; 

  void OnTriggerEnter2D(Collision2D other){
  }
        if(other.tag == "package"){
            Debug.Log("this is the package");
            hasPackage = true;
        }
}

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

{
  bool hasPackage; 

  void OnTriggerEnter2D(Collision2D other){
  }
        if(other.tag == "package"){
            Debug.Log("this is the package");
            hasPackage = true;
        }
        if(other.tag == "Customer" && hasPackage){
             Debug.Log("this is the Customer");
             hasPackage = false;
        }
}

Обратите особое внимание на атрибут внутри второго оператора if, прежде чем мы изменим пакет на false, сначала мы проверим, является ли он истинным в атрибуте. Здесь if(other.tag == «Customer» && hasPackage) && — это обозначение «и», как в любом языке программирования.

Теперь, когда мы берем пакет, мы больше не хотим, чтобы коробка была видна, не так ли? И для этого воспользуемся методом уничтожения объектов. Звучит весело 🤩

У нас есть функция Destroy() для удаления объектов со сцены. Эта функция является функцией обратного вызова, и нам нужно передать два аргумента: во-первых, мы говорим, какой игровой объект уничтожить, а во-вторых, когда его уничтожить. Давайте углубимся в код.

{
  [SerializeField] float destroyDelay = 0.5f;
  bool hasPackage; 

  void OnTriggerEnter2D(Collision2D other){
  }
        if(other.tag == "package"){
            Debug.Log("this is the package");
            hasPackage = true;
            Destroy(other.gameObject, destroyDelay);
        }
        if(other.tag == "Customer" && hasPackage){
             Debug.Log("this is the Customer");
             hasPackage = false;
        }
}

Чтобы быть более профессиональными, мы сначала пишем переменную с именем destroyDelay и присваиваем ей значение 0,5f в качестве времени уничтожения объекта, а в операторах пакета мы пишем функцию Destroy, как это показано в коде. при этом наша машина 🚘 может ездить по сцене, собирать посылки и доставлять их.

Почти готово! нам просто нужно увеличить скорость и сбросить ее, когда машина во что-то врежется. Для этого нам нужно объявить переменные скорости.

public class Driver : MonoBehaviour
{
    [SerializeField] float steerSpeed = 1f;
    [SerializeField] float moveSpeed = 20f;
    [SerializeField] float slowSpeed = 15f;
    [SerializeField] float boostSpeed = 30f;


    void Update()
      {
          float steerAmount = Input.GetAxis("Horizontal") * steerSpeed;
          float moveAmount = Input.GetAxis("Vertical") * moveSpeed;
  
          transform.Rotate(0, 0, -steerAmount);
          transform.Translate(0, moveAmount, 0);
      }
  
}

Здесь мы объявляем несколько переменных. Давайте теперь используем его в операторах if, чтобы завершить логику игры.

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

Теперь давайте закончим наш код.

public class Driver : MonoBehaviour
{
    [SerializeField] float steerSpeed = 1f;
    [SerializeField] float moveSpeed = 20f;
    [SerializeField] float slowSpeed = 15f;
    [SerializeField] float boostSpeed = 30f;

    void Update()
      {
          float steerAmount = Input.GetAxis("Horizontal") * steerSpeed;
          float moveAmount = Input.GetAxis("Vertical") * moveSpeed;
  
          transform.Rotate(0, 0, -steerAmount);
          transform.Translate(0, moveAmount, 0);
      }

    void OnCollisionEnter2D(Collider2D other){
          moveSpeed = slowSpeed;
    }

    void OnTriggerEnter2D(Collider2D other){
          if(other.tag == "Boost"){
              moveSpeed = boostSpeed;
          
          }
    
    }
  
}

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

На этом мы официально закончили создание нашей игры! Поздравляем 🎉 мы отлично поработали!

Подводя итог, можно сказать, что:

  • Мы узнали об основах Unity, таких как создание спрайтов.
  • Меняйте и манипулируйте ими
  • Как создавать скрипты C#
  • Как объявить и использовать разные типы данных
  • подключить их к системе Unity и
  • напишите код, который будет работать в среде единства.

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

Исходный код: https://github.com/schincharauli/Delivery-Driver-Unity

Ссылка на игру: https://salometchintcharauli.itch.io/delivery-driver

  • note В качестве справки используется «Complete C# Unity Game Developer 2D» из курсов Udemy.