Как создать первую игру в Unity: собираем монеты, добавляем таймер и делаем сборку

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

Сделаем 2D-игру с видом сверху: игрок двигается по комнате и должен собрать три монеты за 30 секунд. После последней монеты появляется победа, после окончания времени — поражение. Клавиша R запускает новую попытку. В конце получим отдельное приложение для компьютера.

Пример рассчитан на Unity 6 и клавиатуру. Используется пакет Input System. Графика создаётся из стандартных 2D-фигур; сторонние ассеты не нужны.

ТОП-5 курсов обучения Unity:

Что вы получите и какие основы освоите

Мы будем работать с объектами сцены, компонентами, физикой 2D, скриптами и сборкой. Для старта достаточно уметь находить окна Hierarchy, Project и Inspector и копировать код в файл. Если Editor ещё не открывается, сначала выполните установку Unity.

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

Шаг 1. Создайте 2D-проект и сохраните сцену

  1. В Unity Hub создайте проект CoinCollector на установленной версии Unity 6. Выберите базовый 2D-шаблон с Built-in Render Pipeline.
  2. В окне Project создайте папки Assets/Scenes и Assets/Scripts.
  3. Сохраните текущую сцену как Assets/Scenes/Main.unity.
  4. Откройте File → Build Profiles, найдите Scene List и добавьте сохранённую сцену через Add Open Scenes. Оставьте Main включённой первой сценой.

Список сцен нужен не только для финальной сборки. Наш перезапуск загружает текущую сцену по её индексу, поэтому Main должна быть в списке уже при тестировании. Unity описывает этот механизм в руководстве по сценам сборки.

Шаг 2. Подключите Input System

Откройте Package Manager через меню Window. Найдите Input System в Unity Registry и установите совместимую с вашим Editor версию, если пакет ещё не установлен. Согласитесь включить новую систему ввода и перезапустить Editor, если Unity предложит это.

Проверьте Edit → Project Settings → Player → Other Settings → Active Input Handling. Для примера требуется Input System Package (New) или Both. Изменение параметра требует перезапуска редактора. Этот порядок приведён в документации Input System.

Мы читаем Keyboard напрямую, чтобы первый пример не требовал настройки Input Actions. Для расширяемого управления и переназначения клавиш стоит позже перейти к действиям ввода. Подробности — в статье о вводе и управлении в Unity.

Шаг 3. Подготовьте камеру и комнату

Выберите Main Camera. Поставьте позицию (0, 0, -10), проекцию Orthographic, Size 5 и тёмный цвет фона. В окне Game выберите соотношение сторон 16:9. Все игровые фигуры разместим на Z = 0.

Создайте квадрат через GameObject → 2D Object → Sprites → Square. Названия подменю могут немного отличаться между версиями. Назовите объект WallTop, задайте серый цвет в Sprite Renderer и добавьте Box Collider 2D. Rigidbody 2D стенам не нужен: они неподвижны.

Сделайте ещё три стены дублированием и выставьте параметры:

  • Объект: WallTop. Position: X, Y, Z: 0; 3,5; 0. Scale: X, Y, Z: 10,25; 0,25; 1.
  • Объект: WallBottom. Position: X, Y, Z: 0; −3,5; 0. Scale: X, Y, Z: 10,25; 0,25; 1.
  • Объект: WallLeft. Position: X, Y, Z: −5; 0; 0. Scale: X, Y, Z: 0,25; 7,25; 1.
  • Объект: WallRight. Position: X, Y, Z: 5; 0; 0. Scale: X, Y, Z: 0,25; 7,25; 1.

У каждого Box Collider 2D оставьте Is Trigger выключенным. Стены должны останавливать игрока. Если в Inspector числа вводятся с точкой, используйте 3.5 вместо 3,5; это вопрос локального отображения чисел, а не другое значение.

Шаг 4. Создайте игрока и монеты

Игрок

Создайте ещё один Square, назовите его Player, поставьте в (0, 0, 0), задайте Scale (0.6, 0.6, 1) и заметный цвет. Добавьте Box Collider 2D и Rigidbody 2D.

Для Rigidbody 2D установите Body Type → Dynamic, Gravity Scale → 0, Linear Damping → 0. В Constraints включите Freeze Rotation Z. Для более ровного отображения движения можно включить Interpolate. Игрок не должен падать или вращаться от столкновения со стеной.

Монеты

Создайте Circle через то же меню 2D-фигур. Назовите его Coin1, задайте жёлтый цвет, Scale (0.45, 0.45, 1) и добавьте Circle Collider 2D с включённым Is Trigger. Монете не нужен собственный Rigidbody 2D: он есть у игрока.

Дублируйте монету ещё два раза. Разместите Coin1 в (−3, 2, 0), Coin2 в (3, 1, 0), Coin3 в (1, −2, 0). Проверьте, что они находятся внутри комнаты и не накладываются на стены.

Объект игровой сессии

Создайте пустой объект через GameObject → Create Empty и назовите его GameSession. Он будет хранить правила текущей попытки. На этом этапе основные объекты готовы: камера, четыре стены, игрок, три монеты и GameSession.

Шаг 5. Добавьте три скрипта

В Assets/Scripts создайте три C#-файла: GameSession.cs, PlayerController2D.cs, CoinPickup.cs. В Unity 6 создание скрипта может находиться в Create → Scripting → MonoBehaviour Script. Имена файлов и классов ниже должны совпадать.

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

GameSession.cs: счёт, время и новая попытка

using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.SceneManagement; public class GameSession : MonoBehaviour { [SerializeField, Min(1)] private int goal = 3; [SerializeField, Min(1f)] private float timeLimit = 30f; private int collected; private float remaining; public bool Won => collected >= goal; public bool Finished => Won || remaining <= 0f; private void Awake() { remaining = timeLimit; } private void Update() { if (Finished) { if (Keyboard.current != null && Keyboard.current.rKey.wasPressedThisFrame) { SceneManager.LoadScene( SceneManager.GetActiveScene().buildIndex); } return; } remaining = Mathf.Max(0f, remaining - Time.deltaTime); } public bool TryCollect() { if (Finished) return false; collected++; return true; } private void OnGUI() { GUIStyle style = new GUIStyle(GUI.skin.label); style.fontSize = 22; style.normal.textColor = Color.white; GUI.Box(new Rect(12, 12, 380, Finished ? 138 : 78), ""); GUI.Label(new Rect(24, 20, 350, 30), quot;Монеты: {collected}/{goal}", style); GUI.Label(new Rect(24, 50, 350, 30), quot;Время: {Mathf.CeilToInt(remaining)}", style); if (Finished) { GUI.Label(new Rect(24, 82, 350, 30), Won ? "Победа!" : "Время вышло", style); GUI.Label(new Rect(24, 112, 350, 30), "Нажмите R для новой попытки", style); } } }

GameSession уменьшает оставшееся время на Time.deltaTime — прошедшее время кадра. Поэтому таймер не зависит напрямую от количества кадров. Когда собраны все монеты или время закончилось, Finished становится true. С этого момента новые монеты не засчитываются.

OnGUI здесь выводит небольшой текстовый экран без дополнительных объектов интерфейса. Перезапуск читается через Input System, а не через кнопку OnGUI. Для оформленного игрового интерфейса позже можно заменить этот вывод на Canvas с TextMeshPro или UI Toolkit; сами правила сессии менять не потребуется. Начать можно со статьи об интерфейсе игры в Unity.

PlayerController2D.cs: управление и движение

using UnityEngine; using UnityEngine.InputSystem; [RequireComponent(typeof(Rigidbody2D))] public class PlayerController2D : MonoBehaviour { [SerializeField] private GameSession game; [SerializeField, Min(0f)] private float speed = 5f; private Rigidbody2D body; private Vector2 movement; private void Awake() { body = GetComponent<Rigidbody2D>(); } private void Update() { movement = Vector2.zero; if (game == null || game.Finished) return; Keyboard keyboard = Keyboard.current; if (keyboard == null) return; float x = ((keyboard.dKey.isPressed || keyboard.rightArrowKey.isPressed) ? 1f : 0f) - ((keyboard.aKey.isPressed || keyboard.leftArrowKey.isPressed) ? 1f : 0f); float y = ((keyboard.wKey.isPressed || keyboard.upArrowKey.isPressed) ? 1f : 0f) - ((keyboard.sKey.isPressed || keyboard.downArrowKey.isPressed) ? 1f : 0f); movement = Vector2.ClampMagnitude(new Vector2(x, y), 1f); } private void FixedUpdate() { body.linearVelocity = game != null && !game.Finished ? movement * speed : Vector2.zero; } }

Клавиши читаются в Update, а скорость Rigidbody 2D задаётся в FixedUpdate — шаге физики. ClampMagnitude ограничивает длину вектора, чтобы движение по диагонали не было быстрее движения вдоль одной оси.

Мы задаём скорость в мировых единицах в секунду. Умножать её здесь на Time.deltaTime не нужно: интеграцию движения выполняет физика. В Unity 6 используется свойство Rigidbody2D.linearVelocity. В уроках для более старых версий вы можете встретить velocity.

CoinPickup.cs: подбор монеты

using UnityEngine; public class CoinPickup : MonoBehaviour { [SerializeField] private GameSession game; private bool collected; private void OnTriggerEnter2D(Collider2D other) { if (collected || game == null) return; if (!other.TryGetComponent<PlayerController2D>(out _)) return; if (!game.TryCollect()) return; collected = true; Destroy(gameObject); } }

Монета реагирует только на объект, у которого есть PlayerController2D. Когда сессия принимает подбор, монета исчезает. Флаг collected защищает от повторного начисления до фактического удаления объекта.

Для события OnTriggerEnter2D нужны компоненты физики 2D и Rigidbody 2D хотя бы у одного участника. В нашей сцене это игрок. Обычные Collider и Rigidbody для 3D не заменяют их в этом примере. Условия события описаны в справочнике Unity.

Шаг 6. Подключите скрипты и ссылки в Inspector

  1. Добавьте GameSession.cs на одноимённый пустой объект. Оставьте Goal = 3 и Time Limit = 30.
  2. Добавьте PlayerController2D.cs на Player.
  3. В поле Game у PlayerController2D перетащите объект GameSession из Hierarchy.
  4. Добавьте CoinPickup.cs на каждую монету. У каждой заполните поле Game тем же объектом GameSession.
  5. Сохраните сцену.

В поля Game нужно передавать объект сцены с компонентом GameSession. Сам C#-файл из Project для этого не подходит. Значение None означает, что ссылка пока не назначена. В примере игрок с пустой ссылкой не двигается, а монета не засчитывается.

Проверьте, что Collider 2D и PlayerController2D находятся на одном объекте Player: код монеты ищет компонент на объекте, который вошёл в триггер. Если позже перенесёте коллайдер на дочерний объект, способ поиска понадобится изменить.

Проверьте игру по отдельным действиям

Нажмите Play и щёлкните внутри окна Game, чтобы оно получило фокус. Двигайтесь WASD или стрелками. Затем последовательно проверьте:

  1. Игрок движется во всех направлениях и останавливается у стен.
  2. При касании монеты она исчезает, а счёт увеличивается ровно на один.
  3. После третьей монеты появляется победа и игрок останавливается.
  4. Нажатие R возвращает игрока и все монеты на старт, счёт и время сбрасываются.
  5. В отдельной попытке дождитесь окончания времени: должно появиться поражение, а оставшиеся монеты больше не должны засчитываться.

После теста выйдите из Play перед редактированием сцены. Обычные изменения объектов, сделанные во время игры в Editor, при остановке обычно откатываются. Для постоянной настройки меняйте сцену вне Play и сохраняйте её.

Если пример не работает

Player стоит на месте

Проверьте поле Game, установку Input System, Active Input Handling и фокус окна Game. Затем убедитесь, что Rigidbody 2D имеет тип Dynamic, а Freeze Position X/Y выключены. Откройте Console: красная ошибка компиляции может препятствовать запуску скриптов.

Игрок падает или вращается

Для вида сверху Gravity Scale должен быть 0. Вращение по Z блокируется в Constraints. Если Rigidbody 2D не найден, проверьте, на какой объект добавлен контроллер.

Монета видна, но не подбирается

У неё должны быть Circle Collider 2D, Is Trigger и CoinPickup с заполненным Game. У игрока — Box Collider 2D, Rigidbody 2D и PlayerController2D на том же объекте. Оставьте объекты на стандартном слое; если меняли Physics 2D, проверьте разрешённые взаимодействия слоёв.

После R появляется ошибка загрузки сцены

Проверьте, что Main сохранена и включена в активный Scene List сборки. Если сцена туда не добавлена, её buildIndex не подходит для использованного перезапуска.

Unity не знает linearVelocity

Проверьте, что используете Unity 6, как указано в начале. Для старых веток API отличается; смена названия одного свойства не подтверждает совместимость всей инструкции со старой версией.

Шаг 7. Соберите отдельное приложение

  1. Остановите Play и сохраните сцену.
  2. Откройте File → Build Profiles. Выберите платформу своего компьютера и сделайте нужный профиль активным, если интерфейс предлагает этот шаг.
  3. Если Unity сообщает об отсутствии поддержки платформы, установите соответствующий модуль для этой версии Editor через Hub.
  4. Проверьте, что Main включена первой сценой в списке активной сборки.
  5. Выберите Build And Run и отдельную выходную папку, например Builds/CoinCollector. Не сохраняйте результат внутрь Assets.
  6. В запущенном приложении повторите проверку движения, победы, поражения и R.

На Windows передавайте всю папку сборки, которую создала Unity, включая сопутствующие файлы. Один .exe без данных игры может не запуститься на другом компьютере. Подробный процесс и платформенные различия разобраны в статье о сборке проекта Unity.

Что улучшить после рабочего результата

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

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

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

Документация к примеру

11