Изучаю Unity. Конспект.

Конспект как конспект. Для себя, самое важное.

Откуда брать ресурсы?

unity AssetStore — официальный магаз unity
sketchfab.com
mixamo.com — анимации
flaticon.com — иконки
freesound.org — звук
freesfx.co.uk — звук

3D Физика

Rigidbody

— встроенный компонент, который реализует физическую симуляцию и имитацию силы тяжести.

Настройки rigidbody.

Параметры rigidbody:

  1. Mass — масса объекта в кг.
  2. Drag — сопротивление воздуха. Определяет насколько плотный воздух вокруг объекта.
  3. Angular drag — сопротивление вращению. Сколько силы надо приложить, чтобы объект начал крутиться.
  4. Automatic center of mass — точка равновесия объекта определяется движком.
  5. Automatic tensor — если галку убрать то:
    1. Inertia tensor
    2. Inertia tensor rotation — насколько трудно будет повернуть объект по оси.
      (Эти настройки появились в unity 2022 года. До этого надо было писать в коде.)
  6. Use gravity — определяет применяется ли гравитация. Если выключить — объект не будет падать, но продолжит взаимодействовать с другими объектами. (после удара продолжит невесомый полёт, если Drag = 0)
  7. Is kinematic — явл ли тело кинематическим. Если да — то на тело перестают влиять любые физ взаимодействия, но др объеты могут с ним сталкиваться. (Деревья в GTA)

Точность вычисления физики на объекте:

  1. Interpolate — нахождение точки между 2 точками. Extrapolate — продолжение точки на основе предыдущих.
  2. Collision detection — отслеживание столкновений:
    1. descrete — самый простой и ненадёжный способ. На высоких скоростях объекты могут проходить сквозь друг друга.
      Остальные более точные. Они отслеживают и просчитывают все возможные взаимодействия.
    2. continuous
    3. continuous dynamic
    4. continuous speculative
  3. Constraints — позволяет блокировать перемещение / вращение под влиянием физики по указанным осям.

Коллайдеры

— компоненты, которые отвечают за определение физических границ объекта.

Настройки box collider.

Параметры базовых collider:

  1. Edit collider — позволяет редактировать коллайдер прямо на сцене. Эта опция доступна только для 3х базовых коллайдеров.
  2. Is trigger — преврацает коллайдер в зону, кот. отслеживает попадание др объектов с коллайдерами. (Чекпойнты в гонках, зона заданий в RPG, водоёмы в кот. можно плавать)
  3. Provides contacts — когда он активен в систему физики записываются все события о его взаимодействии, когда выкл — записываются события опред. разрабом.
  4. Material — определяет силу трения и упругость..
  5. Center — позиция относительно объекта
  6. Radius — радиус сферы
    У box collider вместо радиуса будут размеры по трём осям.

Terrain collider — создаётся вместе с terrain.

Настройки terrain collider.

Параметры:
Enable tree colliders — позволяет вкл и откл коллайдеры для деревьев.

Mesh collider — используется когда надо создать collider по форме 3d модели.

2D физика

Rigidbody 2D

Параметры rigidbody 2D:

  1. Body type:
    1. Dynamic — тело реагирует на физические силы (гравитация, столкновения..)
    2. Kinematic — тело стационарно, его взаимодействие можно менять через код.
    3. Static — не реагирует ни на что.
  2. Simulated — если убрать — физика перестаёт работать. Столкновения не будут рассчитываться, даже если есть коллайдер.
  3. Use auto mass — масса тела автоматически определяется из размера его коллайдера 2D.
  4. Mass — задать массу вручную. Телам с большей массой требуется больше силы для перемещения. И они будут иметь больший эффект при столкновении с другими объектами.
  5. Linear drag — сопротивление движению.
  6. Angular drag — сопротивление вращению.
  7. Gravity scale — сила воздействия гравитации на объект.
  8. Constraints — позволяет блокировать перемещение / вращение под влиянием физики по указанным осям.

Эффекторы

— компоненты, позволяющие создать различные физические эффекты (притяжение, отталкивание, симуляцию плавающего объекта)

Surface effector 2D — симуляция конвейера.
(Требует, чтобы в Tilemap collider стояла галка Used by effector.)

Параметры surface effector 2D:

  1. Use collider mask
  2. Collider mask
  3. Force
    1. Speed — скорость движения конвейера.
    2. Speed Variation — с корости прибавляется рандомное от 0 до этого значения.
    3. Force Scale — сила прилагаемое к объекту.
  4. Options
    1. Use contact force — сила будет приложена к точке контакта, а не к центру коллайдера. (Если убрать Constraints для вращения — объект будет кувыркаться)
       

Buoyancy effector 2D — эффектор плавучести.
(Требует, чтобы в Tilemap collider стояла галка Used by effector и Is trigger.)

Параметры buoyancy effector 2D:

  1. Density — плотность воды
  2. Surface level — уровень поверхности
  3. Flow — течение
    1. Flow angle — угол течения
    2. Flow magnitude — больше нуля — уносит вправо, меньше — влево
    3. Flow variation — прибавляется рандомное от 0 до этого значения.

Area effector 2d — позволяет задать область, в которой к объектам применяется сила под определённым углом.

Point effector 2d — притягивает / отталкивает заданную исходную точку.

Параметры point effector 2d:
Force magnitude — с минусом объект притягивается, с + притягивается.

Platform effector — одностороннее проникновение сквозь коллайдер. (напр. снизу персонаж может пройти, а сверху нет.)

Анимация объекта и анимационные кривые

Для работы используются 2 окна: Animation и Animator.

Animator для общей структуры
Animation для конкретной анимации (window — animation — animation)

В окне иерархии выбрать компонент, в окне animation нажать create..

Введение в скриптинг

Применение C# в Unity

В играх никакие процессы не происходят сами по себе. В главе по созданию уровня мы уже это заметили: запустив игру мы увидели что на нашем красивом уровне не происходило абсолютно ничего. А вот в главе по физике мы заметили, что добавив компоненты rigidbody и коллайдеры на объекты мы придали им жизнь. Таким образом, компоненты — это то, что придаёт игре жизнь. Они описывают логику всех процессов в ней. Все компоненты, с которыми мы ранее работали, присутствуют в unity по умолчанию, но почти всегда их функционала не хватает для воплощения наших идей, откуда появляется необходимость создавать собственные компоненты.

У всех компонентов есть общая структура, чтобы её понять создадим новый скрипт и новую папку для него с названием scripts. Далее мы будем использовать такие термины как скрипт, класс и компонент. Важно понимать различия между ними. Скрипт — файл в проекте, который содержит в себе один класс. Компонент — это экземпляр этого класса, который находится на объекте.

Все компоненты, которые мы напишем сами, ничем фундаментально не отличаются от встроенных в unity. можем заметить что скрипт получил название NewBehaviourScript — это название по умолчанию для скриптов

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

Для переименования скрипта в Visual Studio применяется комбинация Ctrl + R + R.

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

public class ConsoleLogger : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Скрипт наследуется от MonoBehaviour — базовый класс для компонентов unity. В нём реализован функционал позволяющий добавлять его экземпляры как компоненты объектов, а также MonoBehaviour даёт возможность использовать встроенные методы.

Для использования методов Unity у нас уже импортировано пространство имён Unity Engine.

По умолчанию в скрипте присутствуют два встроенных метода, которые Unity сам вызывает для всех экземпляров MonoBehaviour. У методов нет модификатора доступа, поэтому надо ставить private.

Метод Start() можно сравнить с методом Main. Start() выполняется до первого кадра на сцене, но для каждого компонента он называется отдельно.

Метод Update() вызывается один раз за кадр — это основной цикл изменения игры, после каждого вызова Update() камера отрисовывает новый кадр (здесь важно понимать, что если на одном объекте у нас висит несколько компонентов, то методы Start() не обязательно будут вызываться в том порядке в котором указаны компоненты в инспекторе, его определяет сам движок).

Чтобы вывести сообщение в консоль:

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

public class ConsoleLogger : MonoBehaviour
{
    // Start is called before the first frame update
    private void Start()
    {
        Debug.Log("This is start.");
    }

    // Update is called once per frame
    private void Update()
    {
        
    }
}

Теперь скрипт можно перекинуть на персонажа и при запуске в консоль выведется сообщение. Если добавить Debug.Log() в Update(), сообщение будет выводиться много раз.

Координатная система

С точки зрения движка, движение — процесс изменения координат объекта. Сами координаты описывают смещение объектов вдоль координатных осей. Появляется вопрос: где у этих осей находятся центр и куда они смотрят? Если у текущего объекта нет родительского объекта, то в пространстве она расположена относительно глобальных осей. Их центр находится в нулевой точке сцены, а их направление строго фиксировано. Однако, объекты, которые являются дочерними от других, зависят от положения своих родительских объектов. В таком случае принято говорить о локальных координатах, то есть, о координатной системе, центр которой — это расположение родительского объекта, а направление осей совпадает с ориентацией родительского объекта в пространстве. В этой ситуации положение объекта описано в двух координатных системах одновременно: глобальной и локальной. Числовые представления координат в них разные, но они описывают одно и то же положение в пространстве.

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

Каждый экземпляр MonoBehaviour имеет ссылку на свой transform в свойстве transform. Позиция объекта доступна через свойства position и имеет тип Vector 3.

Vector 3 — это структура которая описывает трёхмерный вектор, у неё есть свойства x, y, z для компонентов вектора.

Почему позиция описана вектором? Этот вектор описывает куда нужно сдвинуться от нулевой точки чтобы прийти к текущему объекту. Свойство position возвращает вектор, который описывает положение объекта в глобальных координатах. Для получения позиции объекта в локальных координатах существует свойство local position.

Попробуем каждый кадр сдвигать объект на один юнит относительно оси X: transform.position.z -= 0.05f;

И тут будет ошибка:

Ошибка transform.

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

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

public class Mover : MonoBehaviour
{
    private void Update()
    {
        var nextPosition = transform.position;
        nextPosition.z -= 0.05f;
        transform.position = nextPosition;
    }
}

Если двигать объект в локальных координатах, код будет такой:

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

public class Mover : MonoBehaviour
{
    private void Update()
    {
        var nextPosition = transform.localPosition;
        nextPosition.z += 0.05f;
        transform.localPosition = nextPosition;
    }
}

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

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

public class Mover : MonoBehaviour
{
    [SerializeField] private Vector3 _movementDirection;

    private void Update()
    {
        transform.position += _movementDirection;
    }
}
[SerializeField] — указывет Unity, что нужно отобразить (приватное) поле в инспекторе.

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

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

public class Mover : MonoBehaviour
{
    [SerializeField] private Vector3 _movementDirection;

    private void Update()
    {
        transform.Translate(_movementDirection, Space.World);
    }
}

В качестве второго аргумента он принимает перечисление Space. Значения Space.Self означает движение в локальных координатах, а значение Space.World — движение в глобальных координатах. По умолчанию он двигает объект в глобальных координатах.

Иногда требуется один объект двигать в сторону другого.

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

public class PlayerFollower : MonoBehaviour
{
    [SerializeField] private Transform _player;  //ссылка на компонент Transform игрока
    [SerializeField] private float _speed;  //скорость, для следования за игроком

    private void Update()
    {
        var direction = (_player.position - transform.position).normalized;
        transform.Translate(direction * _speed);
    }
}

Каждый кадр потребуется рассчитывать вектор от текущего объекта к игроку. Сделать это можно вычитая позицию текущего объекта из позиции игрока: _player.position — transform.position. Однако, длина этого вектора зависит от расстояния до игрока, поэтому чем дальше объект от игрока тем быстрее он будет двигаться. Чтобы этого избежать можно сократить длину вектора до 1, сохранив его направление. Это называется нормализация вектора: (_player.position — transform.position).normalized. Осталось сдвинуть объект на этот вектор, но мы хотим учитывать скорость движения, поэтому: direction * _speed.

Для удобства Vector3 содержит статические свойства, которые возвращают единичные векторы глобальных осей, напр.: Vector3.up. Они доступны только для чтения. Подобные свойства существуют и для локальных осей, они есть в transform каждого объекта, например: transform.forward. В отличие от своих глобальных аналогов эти свойства доступны для записи.

Чтобы попробовать это, создадим объект-наблюдатель, чтобы он следил за преследователем, поворачиваясь в его сторону:

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

public class Observer : MonoBehaviour
{
    [SerializeField] private Transform _follower;

    void Update()
    {
        var direction =(_follower.position - transform.position).normalized;
        transform.forward = direction;
    }
}

Для этого каждый кадр надо рассчитывать направление в сторону преследователя ([SerializeField] private Transform _follower;) и поворачивать локальную ось в это направление (var direction =(_follower.position — transform.position).normalized;).

Можно сократить реализацию используя компонент LookAt:

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

public class Observer : MonoBehaviour
{
    [SerializeField] private Transform _follower;

    void Update()
    {
        transform.LookAt(_follower);
    }
}

Повороты

Если попытаемся изменить поле rotation с помощью Vector3 то возникает ошибка:

Ошибка rotation

Дело в том, что rotation имеет тип данных Quaternion. Кватернион — это гиперкомплексные числа и тип данных. В первую очередь, это гиперкомплексное число, которое моделируется типом данных и позволяет через него описывать поворот объекта. Метод Quaternion.Euler используется для преобразования углов Эйлера в кватернион.

Углы Эйлера — это способ описать вращение объектов в трёхмерном пространстве к которому мы привыкли. Это те значения, с которыми мы работаем в инспекторе, когда меняем повороты transform, но изменить поворот можно только в виде кватерниона, что делать неудобно. Для решения этой проблемы существует метод Euler, принимающий углы Эйлера и возвращающий кватернион. Кватернионы часто применяются при создании анимаций и плавных переходов между различными состояниями объектов.

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

Таким образом, мы используем кватернионы, чтобы описывать корректно повороты. Чтобы применять кватернионы последовательно нужно умножать один на второй (порядок умножения при этом важен). Чтобы повернуть объект динамически можно использовать метод Quaternion.Euler:

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

public class Spin : MonoBehaviour
{
    private void Update()
    {
        transform.rotation *= Quaternion.Euler(0,10,0);
    }
}

Существует и более простое решение — метод Rotate. Он принимает привычные нам углы Эйлера и поворачивает на них:

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

public class Spin : MonoBehaviour
{
    private void Update()
    {
        transform.Rotate(0, 0, 1);
    }
}

Существует метод для вращение объекта вокруг заданной оси — RotateAround. Он принимает центр вращения и ось вокруг которой объект будет вращаться:

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

public class Spin : MonoBehaviour
{
    private void Update()
    {
        transform.RotateAround(transform.position, -transform.right, 100f * Time.deltaTime);  //позиция центра вращения, ось вращения, скорость вращения
    }
}

Практика с поворотами

Модель солнечной системы..

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

public class SolarSystem : MonoBehaviour
{
    [SerializeField] private float _speed;
    [SerializeField] private Transform _rotationCenter;
    [SerializeField] private Vector3 _offset;
    [SerializeField] private float _rotateSpeed;

    private void Start()
    {
        _offset = transform.position - _rotationCenter.position;
    }

    private void Update()
    {
        transform.position = _rotationCenter.position + _offset;
        transform.RotateAround(_rotationCenter.position, Vector3.up, _speed * Time.deltaTime);
        transform.Rotate(Vector3.up * _rotateSpeed *  Time.deltaTime);
        _offset = transform.position - _rotationCenter.position;
    }
}

Жизненный цикл MonoBehaviour

Блок-схема MonoBehaviour

Методы, вызывающиеся в редакторе:

Reset (сброс) — вызывается для инициализации свойств компонента, когда он только присоединяется к объекту.

Первая загрузка сцены — эти методы вызываются при запуске цены один раз для каждого объекта на сцене:

Awake — этот метод всегда вызывается до любых методов Start и также после того как экземпляр префаба был создан на сцене. Важно: если GameObject не активен на момент старта Awake не будет вызван, пока не GameObject не будет активирован или другой компонент не вызовет Awake.

OnEnable — вызывается только если объект активен. Этот метод вызывается сразу после включения объекта. Это происходит при создании экземпляра MonoBehaviour, например: при загрузке уровня. Когда объект добавляется в сцену, изначально, его компоненты проходят через все этапы инициализации, включая вызов метода Awake и OnEnable до того как будут вызванные методы Start и Update. Это происходит при загрузке сцены или создании нового объекта в редакторе unity. Однако, когда объект создаётся во время игрового процесса его компоненты могут быть добавлены на лету и могут пропустить стадию инициализации, который происходит при добавлении объектов сцены изначально. Но эти методы всё равно будут вызваны, просто после создания.

Перед первым обновлением кадра:

Start — вызывается до обновления первого кадра (first frame) только если компонент включен. Для объектов, добавленных на сцену, метод Start будет называться во всех компонентах до метода Update. Естественно, это не может быть обеспечено при создании объекта непосредственно во время игры.

Между кадрами:

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

Порядок обновления:

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

Update вызывается раз за кадр. Это главный метод для вызова кадров, в отличии от FixedUpdate или LateUpdate.

FixedUpdate — случается, что вызывается чаще Update. FixedUpdate может быть вызван несколько раз за кадр, если FPS снизок и вовсе не вызван между кадрами если FPS высок. Все физические вычисления и обновления происходят в FixedUpdate. При применении расчётов передвижения внутри FixedUpdate не нужно умножать значения на Time.deltaTime, потому что FixedUpdate вызывается в соответствии с надёжным таймером, не зависящим от частоты кадра.

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

Рендеринг:

OnPreCull — вызывается до того? как камера отсечёт cцену. Отсечение определяет какие объекты будут видны в камере.

OnGUI — вызывается несколько раз за кадр и отвечает за элементы интерфейса. Но это старая система обновления, сейчас она почти нет пользуется.

OnDrawGizmos — используется для отрисовки Gizmo в окне Scene в целях визуализации. Используется в редакторе.

Разрушение:

OnDestroy — вызывается после всех обновлений кадров, в последнем кадре объекта, пока он ещё существует. Объект может быть уничтожен при помощи Destroy или при закрытии сцены.

OnDisable — вызывается когда объект отключается или становится неактивным.

Альтернативные способы

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

MoveTowards — возвращает промежуточную координату между координатой А и координатой Б. Аргументом принимает текущее положение объекта, целевое положение, шаг на который сдвинется объект в сторону целевого.

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

public class LinearFollower : MonoBehaviour
{
    [SerializeField] private Transform _player;
    [SerializeField] private float _speed;

    private void Update()
    {
        transform.position = Vector3.MoveTowards(transform.position, _player.position, _speed * Time.deltaTime)
    }
}

Lerp — осуществляет линейную интерполяцию между 2мя точками. Его третий параметр — не величина шага, а относительное положение между координатами (где 1 — финальная точка, 0 — начальная точка, 0,5 — посередине). Объект резко ускоряется к цели, но никогда не достигнет той же точки.

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

public class LinearFollower : MonoBehaviour
{
    [SerializeField] private Transform _player;
    [SerializeField] private float _speed;

    private void Update()
    {
        transform.position = Vector3.Lerp(transform.position, _player.position, _speed);
    }
}

Практика с Transform

Игрок будет следовать по замкнутому пути и его будут преследовать противники.

Код для игрока:

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

public class Leader : MonoBehaviour
{
    [SerializeField] private Transform[] _waypoints;
    [SerializeField] private float _speed;

    private int _currentWaypoint = 0;

    void Update()
    {
        if (transform.position == _waypoints[_currentWaypoint].position)
        {
            _currentWaypoint = (_currentWaypoint + 1 ) % _waypoints.Length;
        }

        transform.position = Vector3.MoveTowards(transform.position, _waypoints[_currentWaypoint].position, _speed * Time.deltaTime);
    }
}

Абстрактный преследователь:

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

public abstract class Follower : MonoBehaviour
{
    [SerializeField] private Transform _leader;
    [SerializeField] private float _speed;

    protected Vector3 Target => _leader.position;
    protected float Speed => _speed;

    private void Update()
    {
        Move();
    }

    protected abstract void Move();
}

Преследователь:

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

public class AdaptiveFollower : Follower
{
    protected override void Move()
    {
        transform.position = Vector3.Lerp(transform.position, Target, Speed);
    }
}

Защитник (следует за игроком и смотрит на преследователя):

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

public class Companion : Follower
{
    [SerializeField] private Transform _target;
    [SerializeField] private float _lengthRay;
    [SerializeField] private Vector3 _offset;

    void Start()
    {
        _offset = transform.position - Target;
    }

    protected override void Move()
    {
        transform.position = Target + _offset;
        transform.RotateAround(Target, Vector3.up, Speed);
        transform.LookAt(_target.position);
        _offset = transform.position - Target;
        Debug.DrawRay(transform.position, transform.forward * _lengthRay, Color.red);
    }
}

Ввод пользователя

Основные методы класса Input:

Input.GetKeyDown — для отслеживания нажатия клавиш.

Input.GetKey — для удержания клавиши

Input.GetKeyUp — для отпускания клавиши.

Эти методы позволяют реагировать на определённые действия пользователя, напр.: запускать анимацию прыжка при нажатии пробела. В реализации компонента это будет выглядеть как условие: если нажата клавиша W то мы двигаем игрока вперёд. С клавишами ASD точно так же.

Во многих играх предусмотрены возможности управлять игроком одновременно как на WASD, так и стрелками. В таком случае мы можем применить оператор ИЛИ, если нажата W или up arrow. Использование условных операторов if для каждой клавиши может привести к неудобству и избыточному коду при управлении более чем одной клавишей одновременно. Для облегчения управления и сокращения кода можно воспользоваться методами:

Input.GetAxis возвращает значения от -1 до 1 со сглаживанием.

Input.GetAxisRaw возвращает значения от -1 до 1 без зглаживания. (Только 3 цифры: -1, 0, 1)

Они позволяют получать значение осей ввода, напр.: вертикальных (ось Y) и горизонтальных (ось X), которые можно использовать для движения объекта.

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

public class ConsoleLogger : MonoBehaviour
{
    private void Start()
    {
        Debug.Log("This is start. Privet, Albina!");
    }

    private void Update()
    {
        float horizantal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        Debug.LogFormat("Horizontal:{0} Vertical:{1}", horizantal, vertical);
    }
}

Механика открытия сундука

Не работает.

Создаём цилиндр и ставим на него скрипт PlayerMovement:

using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    private const string Horizontal = nameof(Horizontal);
    private const string Vertical = nameof(Vertical);

    [SerializeField] private float _rotateSpeed;
    [SerializeField] private float _moveSpeed;

    private void Update()
    {
        Rotate();
        Move();
    }

    private void Rotate()
    {
        float rotation = Input.GetAxis(Horizontal);

        transform.Rotate(rotation * _rotateSpeed * Time.deltaTime * Vector3.up);
    }

    private void Move()
    {
        float direction = Input.GetAxis(Vertical);
        float distance = direction * _moveSpeed * Time.deltaTime;

        transform.Translate(distance * Vector3.forward);
    }
}

Создаём скрипты для сундука.

Chest:

using UnityEngine;

public class Chest : MonoBehaviour
{
    private readonly int OpenTrigger = Animator.StringToHash("Open");

    [SerializeField] private Animator _amimator;

    public void Open()
    {
        _amimator.SetTrigger(OpenTrigger);
    }
}

ChestOpenTrigger:

using UnityEngine;

public class ChestOpenTrigger : MonoBehaviour
{
    [SerializeField] private Chest _chest;

    private bool _isOpened = false;
    private bool _hasOpener;

    private void OnTriggerEnter(Collider other)
    {
        if (other.GetComponent<ChestOpener>())
            _hasOpener = true;
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.GetComponent<ChestOpener>())
            _hasOpener = false;
    }

    private void Update()
    {
        if (_isOpened)
            return;

        if (_hasOpener && Input.GetKeyDown(KeyCode.E))
        {
            _chest.Open();
            _isOpened = true;
        }
    }
}

ChestOpener:

using UnityEngine;

[RequireComponent(typeof(Rigidbody))]

public class ChestOpener : MonoBehaviour { }

Далее вставляем на сцену модель сундука, состоящий из 2х частей. Ставим на него скрипт Chest.

Создаём пустой элемент с именем OpenTrigger, ставим на него скрипт ChestOpenTrigger и Box Collider с галкой Is Trigger. Ставим коллайдер перед сундуком.

Создаём анимацию (Open) открытия крышки сундука в папку Assets — Animations. В инспекторе с этой анимации убираем галку Loop Time.

Для сундука в Animator создаём пустое состояние Idle, делаем его Default State. От него тянем стрелку на анимацию Open. Создаём trigger Open. В инспекторе убираем галку Has Exit Time, ставим триггер в Conditions.

Механика взрыва бочек

При клике по бочке она будет взрываться, раскидывая другие бочки..

Barrel:

using System.Collections.Generic;
using UnityEngine;

public class Barrel : MonoBehaviour
{
    [SerializeField] private float _explosionRadius; //радиус взрыва
    [SerializeField] private float _explosionForce; //сила взрывной волны
    [SerializeField] private ParticleSystem _effect; //эффект взрыва

    private void OnMouseUpAsButton()
    {
        Explode();
        Instantiate(_effect, transform.position, transform.rotation);
        Destroy(gameObject);
    }

    private void Explode()
    {
        foreach (Rigidbody explodableObjects in GetExplodableObjects())
        {
            explodableObjects.AddExplosionForce(_explosionForce, transform.position, _explosionRadius);
        }
    }

    private List<Rigidbody> GetExplodableObjects()
    {
        Collider[] hits = Physics.OverlapSphere(transform.position, _explosionRadius);

        List<Rigidbody> barrels = new();

        foreach (Collider hit in hits)
        {
            if (hit.attachedRigidbody != null)
            {
                barrels.Add(hit.attachedRigidbody);
            }
        }

        return barrels;
    }
}

Поля, необходимые для работы: радиус взрыва, сила взрывной волны и эффект взрыва.

Метод GetExplodableObjects() будет возвращать List<Rigidbody> объектов, которые должны быть подвержены взрыву. Методом OverlapSphere получим и запишем в массив все объекты которые попадают в радиус взрыва. ()

List<Rigidbody> barrels заполняется через foreach, в котором для каждого объекта hit попытаемся получить и сохранить его Rigidbody.

В методе Explode() перебираются все объекты, найденные в GetExplodableObjects(). К ним применяется сила методом AddExplosionForce().

Explode() должен вызываться при клике мышкой. Для этого используется встроенный метод OnMouseUpAsButton().

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

Механика строительства как в Minecraft

Передвижение игрока. Movement.cs будет хранить в себе название осей ввода для класса Input и скорость передвижения _speed:

using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    private readonly string Horizontal = "Horizontal";
    private readonly string Vertical = "Vertical";

    [SerializeField] private float _speed;

    private void Update()
    {
        Vector3 direction = new Vector3(Input.GetAxis(Horizontal), 0f, Input.GetAxis(Vertical));

        transform.Translate(_speed * Time.deltaTime *  direction);
    }
}

В методе Update() определяется направление движения на основе значений ввода. И двигается объект при помощи transform.Translate.

Поворот игрока. Looking.cs будет хранить скорость поворота и ссылки на Transform камеры и игрока:

using UnityEngine;

public class Looking : MonoBehaviour
{
    private readonly string MouseX = "Mouse X";
    private readonly string MouseY = "Mouse Y";

    [SerializeField] private float _speed;
    [SerializeField] private Transform _camera;
    [SerializeField] private Transform _body;

    private void Update()
    {
        _camera.Rotate(_speed * -Input.GetAxis(MouseY) * Time.deltaTime * Vector3.right);
        _body.Rotate(_speed * Input.GetAxis(MouseX) * Time.deltaTime * Vector3.up);
    }
}

В методе Update() поворачивается камера по оси X и тело игрока по Y на основе движений мыши.

Теперь надо добавить Movement.cs и Looking.cs на игрока, сделать камеру дочерним объектом к игроку и заполнить все поля.

Реализация строительства. Builder.cs будет хранить в себе _checkDistance (максимальную дистанцию, на которую можно стоить), _raycastPoint (Transform точки, от которой будет исходить луч рейкаста), класс-пустышка Block (которая будет спавниться), RaycastHit (которая будет хранить инфу о результате работы метода Raycast()):

using UnityEngine;

public class Builder : MonoBehaviour
{
    [SerializeField] private float _checkDistance;
    [SerializeField] private Transform _raycastPoint;
    [SerializeField] private Block _blockPrefab;
    [SerializeField] private BuildPreview _buildPreview;

    private RaycastHit _hitinfo;

    private Vector3 BuildPosition => _hitinfo.transform.position + _hitinfo.normal;

    private void Update()
    {
        if (_hitinfo.transform == null)
            return;

        if (_hitinfo.transform.GetComponent<Block>() == null)
            return;

        if (Input.GetMouseButtonDown(0))
            Build();
    }

    private void FixedUpdate()
    {
        if (Physics.Raycast(_raycastPoint.position, _raycastPoint.forward, out _hitinfo, _checkDistance))
        {
            if (_buildPreview.IsActive == false)
            {
                _buildPreview.Enable();
            }

            _buildPreview.SetPosition(BuildPosition);
        }
    }

    private void Build()
    {
        Vector3 position = BuildPosition;

        Instantiate(_blockPrefab, position, Quaternion.identity);
    }
}

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

Как искать позицию: Есть нормаль, которая перпендикулярна поверхности. И эта нормаль — вектор единичной длины. Если взять точку попадания на краницы объекта и прибавить к ней эту нормаль (_hitinfo.transform.position + _hitinfo.normal), то получится точка, которая находится рядом с этим объектом и которая будет являться центром следующего куба (Это не будет работать, если размер кубов больше 1).

В методе В методе Build() реализуется спавн блока. Build() нужно где-то вызывать, т.к. реализуется всё на основе ввода, вызывать нужно в Update(). Здесь проверяется попадает ли куда-нибудь луч и является ли этот объект блоком. Если эти условия выполнены, то при нажатии лкм вызывается метод Build().

FixedUpdate() будет управлять превью (см. ниже) и использовать Raycast. Сначала проверяется попадает ли куда-ниб Raycast. Если условие срабатывает — надо включить превью и ставить в предполагаемое место строительства блока. Если Raycast никуда не попал, надо выключить превью.

BuildPreview.cs будет содержать публичное свойство IsActive, которое будет показывать включён ли сейчас блок. Также реализуются методы включения (Enable()) и выключения блока (Disable()), в которых используется метод SetActive().

using UnityEngine;

public class BuildPreview : MonoBehaviour
{
    public bool IsActive => gameObject.activeSelf;

    public void Enable()
    {
        gameObject.SetActive(true);
    }

    public void Disable()
    {
        gameObject.SetActive(false);
    }

    public void SetPosition(Vector3 position)
    {
        transform.position = position;
    }
}

Метод SetPosition() изменяет позицию блока и принимает Vector3 position.

Теперь надо добавить Builder.cs и BuildPreview.cs на игрока.

В папке Prefabs надо создать префаб блока и BuildPreview (блок с прозрачной текстурой и выкл коллайдером).

Бросание гранат

Добавить комментарий