Добро пожаловать во вторую часть нашего туториала по Unity 2D платформеру. Если в первой части ты учился управлять персонажем, то теперь — создашь полноценный игровой уровень, по которому приятно пройтись.
Вместо сложных врагов и сцен загрузки, мы сосредоточимся на игровом окружении: добавим монеты, платформы, ямы, стену, к которой прилипает крыса, и даже слегка ворчливого гриба. А ещё — кое-что неожиданное, что будет сопровождать игрока до самого конца.
Ты научишься размещать объекты, использовать коллайдеры и Rigidbody, а также настроишь UI-счётчик и простую логику для врагов и перезапуска уровня. Когда доберёшься до выхода — поймёшь, что главное было совсем не в нём.
Открывай Unity, запускай сцену — и начни не потому, что надо, а потому что где-то внутри уровня уже что-то ждёт, чтобы ты это создал.
Начнём с добавления вертикальной стены в сцену. Используй тот же
спрайт largeGround
, что и для пола — просто поверни его и
поставь вертикально. Unity автоматически создаст новый объект.
Переименуй его в Wall (стена).
В Inspector для новой стены:
0
.Теперь нажми Play и попробуй: беги к стене, прыгни на неё и удерживай клавишу движения. Крыса прилипнет к стене как крошечный супергерой. Это не магия — это трение.
Такое поведение происходит потому, что у стандартных коллайдеров Unity есть встроенное трение. Чтобы исправить это, мы создадим Physics Material 2D с нулевым трением.
В папке Assets:
noFriction
.0
.
Теперь выбери объект Rat. В его компоненте
Capsule Collider 2D установи
Material на noFriction
.
Снова запусти сцену — крыса больше не должна застревать на стенах и платформах.
noFriction
назначен крысе.
Давайте дадим нашей крысе что-то, что можно собирать — монетки!
Импортируй изображение coin
в проект:
Скачать изображение
(мы используем монетку из этого спрайтшита). Затем перетащи её в сцену
— Unity создаст новый объект.
Выдели монетку в Hierarchy, затем в Inspector:
0
.-10
(чтобы
она отображалась позади крысы).
Создай новый тег с именем Coin
и назначь его объекту
монеты.
Переименуй монетку в CoinPrefab, затем перетащи её в папку Assets, чтобы создать префаб.
Coin
.
Теперь создадим скрипт для сбора монет. В папке Assets:
CoinController
.
Вставь следующий код в скрипт:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CoinController : MonoBehaviour {
// Звук при сборе монеты
public AudioClip CoinSound;
// Счётчик монет
public int coin;
void OnTriggerEnter2D (Collider2D other) {
// Если объект имеет тег Coin
if (other.tag == "Coin") {
// Увеличить счётчик
coin = coin + 1;
// Проиграть звук сбора
AudioSource.PlayClipAtPoint(CoinSound, transform.position);
// Удалить монету
Destroy(other.gameObject);
}
}
}
Прикрепи скрипт CoinController
к объекту
Rat. Скачать звук монеты можно здесь:
Скачать CoinSound.wav. Импортируй CoinSound
в проект и назначь его в поле
Coin Sound
скрипта в Inspector.
💡 Подсказка: мы используем PlayClipAtPoint
, поэтому звук
воспроизводится в мировом пространстве. Если он звучит слишком тихо,
скорее всего, Audio Listener остаётся на камере.
Чтобы это исправить, можно либо приблизить камеру к персонажу, либо
переместить Audio Listener с камеры на объект
Rat.
Покажем, сколько монет собрано — с помощью UI-текста.
В Hierarchy выбери:
Canvas
,
Text(TMP)
и EventSystem
.
Выбери объект Canvas. В Inspector:
Scale With Screen Size
.
Expand
.
Scale With Screen Size
и Expand
для
адаптивного UI.
Дважды кликни по Canvas в Hierarchy, чтобы отцентрировать UI. Появится белый прямоугольник — это область экрана.
Выбери элемент Text (TMP) и внеси следующие изменения:
CoinCounterText
.0
.
Теперь обновим скрипт CoinController
, чтобы связать его с
текстом.
using UnityEngine;
using TMPro;
public class CoinController : MonoBehaviour
{
// Звук при сборе монеты
public AudioClip CoinSound;
// Счётчик монет
public int coin;
// Ссылка на текст TextMeshPro
public TMP_Text CoinCounterText;
void OnTriggerEnter2D(Collider2D other)
{
// Проверка на тег Coin
if (other.tag == "Coin")
{
coin = coin + 1;
// Воспроизведение звука
AudioSource.PlayClipAtPoint(CoinSound, transform.position);
// Обновление текста
CoinCounterText.text = coin.ToString();
// Удаление монеты
Destroy(other.gameObject);
}
}
}
Выдели Rat в Hierarchy. В компоненте
CoinController перетащи объект
CoinCounterText
в поле
Coin Counter Text.
Нажми Play — при сборе монет счётчик будет обновляться в реальном времени.
Добавим простое испытание: яму, в которую может упасть игрок.
Перемести одну из платформ, чтобы создать разрыв в полу — это будет зона падения.
Теперь щёлкни правой кнопкой в Hierarchy и выбери:
Create Empty. Переименуй новый объект в
DeadEnd
.
В Inspector сделай следующее:
0
.
Теперь создай новый MonoBehaviour
скрипт и назови его
DeadEndController
. Вставь в него следующий код:
using UnityEngine;
using UnityEngine.SceneManagement; // подключаем сцену для перезапуска
// Скрипт вешается на объект-яму, чтобы перезапустить уровень при падении
public class DeadEndController : MonoBehaviour
{
// Метод вызывается, когда кто-то входит в триггер-область
void OnTriggerEnter2D(Collider2D other)
{
// Проверяем, тег ли у объекта "Player"
if (other.tag == "Player")
{
// Перезапускаем текущую сцену
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
}
Выдели объект DeadEnd и прикрепи к нему скрипт
DeadEndController
. Затем создай префаб: перетащи
DeadEnd
из Hierarchy в папку
Assets.
Когда крыса попадает в область триггера, сцена перезагружается — это простой способ сбросить уровень без дополнительной логики.
Пора добавить подвижное препятствие: врага-гриба, который ходит туда-сюда. Если крыса прыгнет ему на голову — он исчезает. Если коснётся сбоку — уровень перезапускается.
Помести префаб Ground
, а под ним — DeadEnd
.
Затем перетащи спрайт гриба из ассетов в сцену, на последнюю
платформу. Переименуй новый объект в Enemy.
В Inspector для Enemy:
0
.Interpolate
.
Теперь создай новый скрипт и назови его
MushroomController
. Вставь в него следующий код:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class MushroomController : MonoBehaviour
{
// Максимальное расстояние, на которое гриб может отходить от стартовой позиции (в Unity-единицах)
public float distanceToRun = 2f;
// Скорость движения гриба
public float speed = 2f;
// Направлен ли гриб сейчас влево?
public bool isLookingLeft = true;
// Начальная позиция гриба
Vector3 startPos;
// Ссылка на Rigidbody2D компонента гриба
Rigidbody2D rb;
void Start()
{
rb = GetComponent<Rigidbody2D>();
startPos = transform.position;
// Упрощаем вычисление дистанции: сравниваем квадрат расстояния
distanceToRun = distanceToRun * distanceToRun;
}
void FixedUpdate()
{
// Текущее смещение от старта
Vector2 dist = transform.position - startPos;
if (isLookingLeft)
{
// Если гриб ушёл слишком влево — разворачиваем
if (transform.position.x < startPos.x && dist.sqrMagnitude > distanceToRun)
{
TurnTheMushroom(); // меняем направление
rb.linearVelocity = new Vector2(speed, rb.linearVelocity.y);
}
else
{
rb.linearVelocity = new Vector2(-speed, rb.linearVelocity.y);
}
}
else
{
// Если гриб ушёл слишком вправо — разворачиваем
if (transform.position.x > startPos.x && dist.sqrMagnitude > distanceToRun)
{
TurnTheMushroom(); // меняем направление
rb.linearVelocity = new Vector2(-speed, rb.linearVelocity.y);
}
else
{
rb.linearVelocity = new Vector2(speed, rb.linearVelocity.y);
}
}
}
// Разворачивает гриб и зеркалит его спрайт по оси X
void TurnTheMushroom()
{
isLookingLeft = !isLookingLeft;
transform.localScale = new Vector3(
transform.localScale.x * -1,
transform.localScale.y,
transform.localScale.z
);
}
// Если игрок врезается в гриб сбоку — перезапускаем уровень
void OnCollisionEnter2D(Collision2D other)
{
if (other.collider.gameObject.tag == "Player")
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
// Если игрок прыгает на голову грибу (через триггер) — уничтожаем гриб
void OnTriggerEnter2D(Collider2D other)
{
if (other.tag == "Player")
{
Destroy(gameObject);
}
}
}
Прикрепи скрипт MushroomController
к объекту
Enemy. Отрегулируй параметры Speed и
Distance To Run в Inspector (например, оба по
2
).
Наконец, перетащи Enemy в папку Assets, чтобы создать повторно используемый префаб.
Добавим вертикальную движущуюся платформу, на которой крыса сможет ехать вверх и вниз.
Перетащи спрайт largeGround
в сцену и переименуй новый
объект в Platform.
В Inspector для объекта Platform:
0
.Ground
.Kinematic
.
Interpolate
.
Теперь добавим скрипт, который будет двигать платформу вверх и вниз между двумя точками.
Создай новый MonoBehaviour
скрипт с именем
PlatformMoving
и вставь в него этот код:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlatformMoving : MonoBehaviour
{
// Скорость движения платформы
public float speed;
// Верхняя точка, к которой движется платформа
public Transform endPoint;
// Стартовая позиция
Vector3 startPoint;
// Ссылка на Rigidbody2D
Rigidbody2D rb;
// Текущая скорость
float currentSpeed;
void Start()
{
// Сохраняем начальную позицию
startPoint = transform.position;
// Получаем Rigidbody2D
rb = GetComponent<Rigidbody2D>();
// Задаём начальное направление движения
currentSpeed = speed;
}
void FixedUpdate()
{
// Если прошли endPoint — меняем направление
if (transform.position.y > endPoint.position.y)
currentSpeed = -speed;
// Если вернулись к началу — снова меняем направление
if (transform.position.y < startPoint.y)
currentSpeed = speed;
// Применяем скорость к Rigidbody
rb.linearVelocity = new Vector3(0, currentSpeed, 0);
}
}
В Hierarchy создай пустой GameObject и назови его EndPoint. (Можно включить иконку, чтобы он был виден.) Расположи его над платформой.
Снова выбери Platform. Прикрепи скрипт
PlatformMoving
. Установи Speed в
1
и перетащи объект EndPoint в
соответствующее поле.
Запусти сцену и посмотри, как платформа движется вверх и вниз между двумя точками.
Добавь стену справа от платформы и назначь ей Box Collider 2D. Затем помести префаб DeadEnd под платформу и подгони его размеры.
Добавим коробку, которая будет взаимодействовать с крысой и другими объектами с помощью физики.
Перетащи спрайт imageGround
в сцену и переименуй новый
объект в Box.
В Inspector для объекта Box:
Ground
.0
.Scale X = 0.2
, Y = 1.5
).
Interpolate
.
Теперь ты можешь толкать коробку в сцене — попробуй прокатиться на ней на платформе или использовать как ступеньку!
Добавим выход из уровня. Он пока не загружает новую сцену, но работает как финишный триггер — если ты сможешь до него добраться.
Сначала добавь ещё один префаб Ground там, где
движущаяся платформа будет останавливаться — в верхней точке. Затем
поставь стену в конце уровня, используя спрайт
largeGround
, и добавь к ней
Box Collider 2D.
Перетащи спрайт Exit
из папки Assets в
сцену — он уже добавлен в проект. Размести его рядом с верхней стеной,
на такой высоте, чтобы крыса
не могла допрыгнуть до него сама. Чтобы добраться до выхода,
игроку понадобится Box в качестве подставки.
В Inspector объекта Exit:
0
.DeadEndController
(мы используем его для
перезапуска сцены).
Теперь, когда крыса касается выхода — сцена перезапускается. Но только если тебе удалось туда забраться.
Чтобы добавить ощущение глубины, сделаем так, чтобы фон немного двигался при движении игрока. Это называется эффект параллакса — он имитирует глубину, прокручивая дальние слои медленнее.
Скачать изображение фона можно здесь: Скачать BackGround.png
Импортируй изображение в проект и перетащи его в сцену. В Inspector:
X: 6
,
Y: 6
(при необходимости подгони).
-100
, чтобы
фон отрисовывался позади всего остального.
Создай новый скрипт на MonoBehaviour
с именем
Parallax
и вставь в него следующий код:
using UnityEngine;
public class Parallax : MonoBehaviour
{
// Игрок, за которым мы будем следить
private GameObject player;
// Позиция игрока в предыдущем кадре (чтобы знать, куда он двинулся)
private Vector3 lastPosition;
// Насколько быстро фон будет двигаться (меньше — медленнее, создавая эффект глубины)
public float speed = 60f;
void Start()
{
// Находим игрока по тегу "Player"
player = GameObject.FindWithTag("Player");
// Запоминаем стартовую позицию игрока
if (player != null)
{
lastPosition = player.transform.position;
}
}
void Update()
{
// Если игрок не найден — ничего не делаем
if (player == null) return;
// Разница между новой и старой позицией игрока
Vector3 offset = player.transform.position - lastPosition;
// Сдвигаем фон пропорционально этому смещению, но медленнее — для эффекта параллакса
transform.Translate(offset * speed * Time.deltaTime);
// Обновляем сохранённую позицию на текущую
lastPosition = player.transform.position;
}
}
Прикрепи скрипт к объекту фона. Установи значение
Speed — 60
подойдёт для начала, но можно
поэкспериментировать.
Это тот шаг, к которому всё тихо вело. Не монетки, не гриб, даже не выход. Именно он — настоящая причина.
В ассетах есть улыбающееся облако. Оно не атакует, не даёт очков. Но оно — единственное, что остаётся с тобой. Всегда.
Перетащи спрайт облака в сцену и назови его Cloud.
Установи Z Position в 0
и
Order in Layer в -50
, чтобы оно было
позади крысы.
Размести его где-нибудь выше и сзади крысы — например,
(x - 2, y + 3)
. Не нужно точности. Главное — чтобы оно
было.
Создай новый MonoBehaviour
скрипт с именем
CloudFollower
и прикрепи его к объекту Cloud. Вставь этот
код:
using UnityEngine;
public class CloudFollower : MonoBehaviour
{
// Смещение облака относительно игрока (влево и вверх)
public Vector3 offset = new Vector3(-2f, 3f, 0f);
// Скорость, с которой облако догоняет игрока
public float followSpeed = 1.5f;
// Ссылка на объект, за которым следует облако (игрок)
private Transform target;
void Start()
{
// Ищем объект с тегом "Player"
GameObject playerObj = GameObject.FindWithTag("Player");
if (playerObj != null)
{
target = playerObj.transform; // Сохраняем ссылку на трансформ игрока
}
}
void Update()
{
// Если цель не найдена — ничего не делаем
if (target == null) return;
// Целевая позиция: позиция игрока + смещение
Vector3 desiredPos = target.position + offset;
// Плавно перемещаем облако к целевой позиции
transform.position = Vector3.Lerp(
transform.position, // текущая позиция
desiredPos, // желаемая позиция
followSpeed * Time.deltaTime // интерполяция по времени
);
}
}
Теперь облако будет следовать за крысой — не идеально, не точно, а с мягким отставанием. Оно не помогает. Оно не говорит. Оно просто остаётся.
Если ты замедлишься или упадёшь — оно подождёт. Если перезапустишь уровень — оно вернётся. Ты не один в этом уровне. По-настоящему — нет.
Теперь можно добавить монетки и грибы. Скинуть ящик под платформу. И — возможно — остаться в этом уровне чуть дольше, чем планировалось.
Всё это кажется простым. И в этом сила. Не пытайтесь сразу создать лучшую игру в мире. Сначала разберитесь в основах: движение, столкновения, взаимодействия.
А потом — уже сами создадите свою игру. Ту, которая будет по-настоящему вашей.