🏠 Главная

Unity 2D tutorial для новичков: Point & Click

В этом туториале мы шаг за шагом соберём основу простой игры в жанре point & click на Unity 2D.

Ты познакомишься с базовыми принципами взаимодействия в сцене: клики по объектам, осмотр, подбор предметов и работу с инвентарём.

По ходу туториала разберём:

Создавай новый Unity 2D проект — и поехали.

Импорт ассетов

Для начала скачайте архив с изображениями, которые будут использоваться в проекте:

📦 point-click-images.zip

Распакуйте архив и импортируйте изображения в ваш Unity-проект.

Интерфейс Unity с импортированными спрайтами в папке Assets
Импортированные спрайты в окне Assets проекта Unity

Добавление фона

Перетащите спрайт фона (он уже есть в архиве с ассетами) на сцену Unity. Для удобства переименуйте объект в Background.

Фоновый спрайт для сцены point and click
Спрайт фона, используемый для сцены point and click

Установите параметр Order in Layer в значение -10, чтобы фон отрисовывался позади остальных спрайтов.

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

Фоновый спрайт, размещённый на сцене Unity
Фоновый спрайт на сцене с пониженным Order in Layer

Создание ScriptableObject

ScriptableObject — это удобный контейнер для данных в Unity. Он позволяет хранить информацию отдельно от логики и объектов сцены.

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

Создайте новый скрипт через меню: Create → Scripting → ScriptableObject Script и назовите его ItemData.


using UnityEngine;

// ScriptableObject — контейнер для данных предмета.
// Хранит информацию отдельно от логики и объектов сцены.
[CreateAssetMenu(fileName = "ItemData", menuName = "Scriptable Objects/ItemData")]
public class ItemData : ScriptableObject
{
    // Уникальный идентификатор предмета
    public int id;

    // Название предмета, которое видит игрок
    public string title;

    // Текстовое описание предмета
    [TextArea]
    public string description;

    // Иконка предмета для UI и курсора
    public Sprite icon;
}

Теперь на основе этого класса можно создавать отдельные ассеты с данными предметов прямо в проекте Unity, не привязывая их к конкретным объектам сцены.

Создание ScriptableObject ItemData в Unity
Создание ассета ScriptableObject для хранения данных предмета

Игровая цепочка и данные объектов

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

Цепочка взаимодействий будет выглядеть так: Scissors → Picture → Key → Safe → Code → Door.

Игрок подбирает ножницы, разрезает картину, находит ключ, открывает сейф, получает код и открывает финальную дверь.

Для каждого объекта мы создадим отдельный ScriptableObject ItemData с описанием и иконкой.

Создайте ScriptableObject через меню Assets → Create → Scriptable Objects → ItemData и заполните данные для следующих предметов.

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

ScriptableObject ItemData для предметов в Unity
Созданные ScriptableObject ItemData для всех объектов пазла

UI для вывода подсказок

Создайте UI для отображения текстовых подсказок. Он будет использоваться для вывода описаний предметов при осмотре.

Добавьте на сцену Canvas и внутри него создайте Panel (GameObject → UI (Canvas) → Panel). Панель будет служить фоном для текста.

Выберите объект Canvas и установите параметр UI Scale Mode в значение Scale With Screen Size.

Сделайте двойной клик по Canvas в иерархии, чтобы перейти в режим редактирования UI. Затем выберите Panel и в инспекторе установите привязку Stretch.

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

Установите тёмный цвет фона панели — так текст будет лучше читаем. Также отключите параметр Raycast Target, чтобы UI не мешал обработке кликов и физики на сцене.

Canvas и Panel для UI подсказок в Unity
Canvas и панель для отображения подсказок в Unity

Добавим дочерний UI-элемент для отображения текста. Кликните правой кнопкой мыши по Panel и выберите UI(Canvas) - TextMeshPro. При первом добавлении Unity может предложить импортировать модуль TextMeshPro — подтвердите загрузку.

Выберите созданный текстовый объект в иерархии и установите для него привязку Stretch. Настройте размеры текстового окна относительно панели, оставив небольшие внутренние отступы.

В компоненте TextMeshPro - Text (UI) откройте раздел Extra Settings и отключите параметр Raycast Target, чтобы текст не перехватывал клики.

Настройте отображение текста по своему вкусу. В нашем случае используются следующие параметры:

Настройки TextMeshPro для текста подсказок в Unity
Настройки TextMeshPro для отображения текста подсказок

Скрипт управления выводом текста

Создайте пустой GameObject на сцене и назовите его ItemDescriptionUI.

Создайте новый MonoBehaviour-скрипт с именем ItemDescriptionUI и добавьте его как компонент на созданный объект.

Ниже приведён полный код скрипта. Он отвечает за отображение текстовых подсказок и их автоматическое скрытие.


using TMPro;
using UnityEngine;

// Этот скрипт управляет UI-панелью с текстом описания предметов
public class ItemDescriptionUI : MonoBehaviour
{
    // Статичная ссылка на экземпляр скрипта (Singleton)
    public static ItemDescriptionUI Instance;

    // Панель, которая служит фоном для текста
    [SerializeField] private GameObject panelText;

    // Текстовое поле TextMeshPro для вывода описания
    [SerializeField] private TMP_Text descriptionText;

    // Флаг, показывающий, отображается ли сейчас текст
    private bool isShowing = false;

    // Время (в секундах), через которое окно будет скрыто
    public float timeToDisable = 3f;

    // Текущий таймер скрытия
    private float tmpTimeToDisable;

    private void Awake()
    {
        // Реализация Singleton:
        // если экземпляр уже существует — удаляем дубликат
        if (Instance != null)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;

        // Инициализируем таймер
        tmpTimeToDisable = timeToDisable;

        // Скрываем панель при старте сцены
        if (panelText != null)
            panelText.SetActive(false);
    }

    private void Update()
    {
        // Если текст сейчас не отображается — ничего не делаем
        if (!isShowing) return;

        // Уменьшаем таймер с учётом времени кадра
        tmpTimeToDisable -= Time.deltaTime;

        // Если время вышло — скрываем окно
        if (tmpTimeToDisable <= 0f)
            Hide();
    }

    // Метод для показа текста описания
    public void Show(ItemData item)
    {
        // Если данные не переданы — просто скрываем окно
        if (item == null)
        {
            Hide();
            return;
        }

        // Защита от отсутствующих ссылок в инспекторе
        if (panelText == null || descriptionText == null) return;

        // Активируем отображение
        isShowing = true;

        // Сбрасываем таймер скрытия
        tmpTimeToDisable = timeToDisable;

        // Показываем панель и выводим текст
        panelText.SetActive(true);
        descriptionText.text = item.description;
    }

    // Скрытие панели с текстом
    private void Hide()
    {
        isShowing = false;

        if (panelText != null)
            panelText.SetActive(false);
    }
}
  

Перетащите объект Panel в поле Panel Text, а компонент TextMeshPro (Text) — в поле Description Text в инспекторе.

В этом скрипте используется паттерн Singleton. Он позволяет другим объектам сцены напрямую обращаться к ItemDescriptionUI.Instance, не сохраняя ссылки вручную.

Любой объект может вызвать метод Show(), передав ItemData, и текст будет показан на экране. Если метод вызывается повторно, таймер скрытия обновляется. При отсутствии новых вызовов окно автоматически скрывается через 3 секунды.

Объект ItemDescriptionUI и его настройки в инспекторе Unity
Объект ItemDescriptionUI с настроенными ссылками в инспекторе

Подсказка о предмете

Теперь создадим скрипт для показа описания предмета. Возможность выводить подсказку будет у всех интерактивных объектов в сцене.

Данные предмета мы будем брать из ScriptableObject ItemData, поэтому ссылка на эти данные будет храниться прямо в скрипте объекта.

Для доступа к логике подсказки мы воспользуемся интерфейсом. Это позволит не зависеть от конкретного скрипта или его названия.

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

Сначала создадим интерфейс подсказки. Создайте пустой C#-скрипт и назовите его ICheckable.


using UnityEngine;

// Интерфейс для объектов, которые можно "осмотреть"
// и получить текстовую подсказку
public interface ICheckable
{
    // Метод, который вызывается при осмотре объекта
    void TellAbout();
}
  

Теперь создадим скрипт, который будет использовать этот интерфейс. Создайте новый MonoBehaviour-скрипт и назовите его CheckableItem.


using UnityEngine;

// Этот скрипт добавляет объекту возможность
// показывать описание при осмотре
public class CheckableItem : MonoBehaviour, ICheckable
{
    // Данные предмета (ScriptableObject)
    public ItemData data;

    // Реализация метода интерфейса ICheckable
    public void TellAbout()
    {
        // Если данные не назначены — ничего не делаем
        if (data == null) return;

        // Передаём данные в UI для отображения текста
        ItemDescriptionUI.Instance.Show(data);
    }
}
  

Теперь любой объект, на котором есть компонент CheckableItem, считается осматриваемым. В дальнейшем мы просто проверим наличие интерфейса ICheckable и вызовем метод TellAbout(), не зная ничего о внутренней реализации.

Интерактивные предметы

Теперь добавим игровые предметы в сцену. Начнём с конца цепочки — с двери.

1) Door (Дверь)

Перетащите спрайт двери на сцену. Unity создаст новый GameObject. Переименуйте его в Door.

Чтобы объект можно было кликать и находить через физику, добавьте компонент Box Collider 2D.

Затем добавьте компонент CheckableItem. В поле Data этого скрипта назначьте ScriptableObject Door.

Объект Door на сцене Unity с Box Collider 2D и CheckableItem
Дверь на сцене: коллайдер для кликов и данные через CheckableItem

2) Code (Записка с кодом)

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

Настройте размер относительно двери (в нашем примере: Scale X = 0.5 и Scale Y = 0.5). Переименуйте объект в Code.

Добавьте к объекту компоненты Box Collider 2D и CheckableItem. В поле Data назначьте ScriptableObject Code.

Теперь отключите объект на сцене (галочка рядом с именем объекта в инспекторе). Записка будет спрятана за сейфом и “ждать” момента, когда мы включим её через логику.

Объект Code на сцене Unity с Box Collider 2D и CheckableItem
Записка с кодом: настроенные компоненты и отключение объекта на сцене

3) Safe (Сейф)

Добавим сейф, в котором будет «храниться» записка с кодом. Перетащите спрайт пустого сейфа на сцену и переименуйте объект в Safe.

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

Настройте размеры сейфа (в нашем примере: Scale X = 0.5, Scale Y = 0.5) и установите параметр Order in Layer в значение 5, чтобы сейф отрисовывался поверх записки.

Добавьте к объекту Safe компоненты Box Collider 2D и CheckableItem. В поле Data назначьте ScriptableObject Safe.

Теперь добавьте в сцену спрайт двери сейфа. Назовите объект SafeDoor и установите для него Order in Layer равный 10, чтобы дверь рисовалась поверх корпуса сейфа.

Разместите дверь поверх сейфа, настройте её размеры, а затем сделайте её дочерним объектом сейфа: просто перетащите SafeDoor на Safe в иерархии.

Объект Safe и его компоненты в инспекторе Unity
Сейф на сцене: корпус, дверь и настроенные компоненты

4) Key (Ключ)

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

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

Добавьте к объекту компоненты Box Collider 2D и CheckableItem. В поле Data назначьте ScriptableObject Key.

После этого отключите объект на сцене (галочка рядом с именем в инспекторе). Ключ будет скрыт до тех пор, пока мы не откроем к нему доступ через логику.

Объект Key на сцене Unity с Box Collider 2D и CheckableItem
Ключ на сцене: настроенные компоненты и отключённый объект

5) Picture (Картина)

Перетащите спрайт картины на сцену и переименуйте объект в Picture.

Установите параметр Order in Layer в значение 5, чтобы картина рисовалась поверх ключа. Переместите картину на место, где спрятан ключ (при необходимости можно временно включить объект Key для удобства, а затем снова отключить его).

Добавьте к объекту компоненты Box Collider 2D и CheckableItem. В поле Data назначьте ScriptableObject Picture.

Объект Picture на сцене Unity с Box Collider 2D и CheckableItem
Картина на сцене: отрисовка поверх ключа и подключённые компоненты

6) Scissors (Ножницы)

Перетащите спрайт ножниц на сцену и разместите его в любом удобном месте. Переименуйте объект в Scissors.

Добавьте к объекту компоненты Box Collider 2D и CheckableItem. В поле Data назначьте ScriptableObject Scissors.

Объект Scissors на сцене Unity с Box Collider 2D и CheckableItem
Ножницы на сцене: коллайдер для кликов и данные через CheckableItem

Инвентарь предметов

Теперь создадим инвентарь для хранения предметов. Обратите внимание: мы не будем хранить сами игровые объекты, вместо этого в инвентаре будут лежать контейнеры с данными — ScriptableObject ItemData.

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

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

Создайте новый MonoBehaviour-скрипт и назовите его Inventory.


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

// Инвентарь для хранения данных предметов
public class Inventory : MonoBehaviour
{
    // Статичная ссылка на инвентарь (Singleton)
    public static Inventory Instance;

    // Текущий выбранный предмет
    public ItemData SelectedItem { get; private set; }

    // Список предметов в инвентаре
    public List<ItemData> items = new();

    // Событие, которое вызывается при добавлении предмета
    public event Action<ItemData> OnItemAdded;
    // Событие, которое вызывается при смене выбранного предмета
    public event Action<ItemData> OnSelectionChanged;

    private void Awake()
    {
        // Проверяем, чтобы в сцене был только один инвентарь
        if (Instance != null)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
    }

    // Добавление предмета в инвентарь
    public void Add(ItemData item)
    {
        if (item == null) return;

        items.Add(item);

        // Уведомляем подписчиков о добавлении предмета
        OnItemAdded?.Invoke(item);
    }

    // Выбор предмета в инвентаре
    public void Select(ItemData item)
    {
        SelectedItem = item;
        // Уведомляем подписчиков об изменении выбранного предмета
        OnSelectionChanged?.Invoke(SelectedItem);
    }

    // Сброс выбранного предмета
    public void ClearSelection()
    {
        SelectedItem = null;
        // Уведомляем подписчиков об изменении выбранного предмета
        OnSelectionChanged?.Invoke(SelectedItem);
    }
}
  

Добавьте пустой GameObject на сцену и назовите его Inventory. Затем добавьте к нему этот скрипт как компонент.

Объект Inventory на сцене Unity с компонентом Inventory
Объект Inventory на сцене и его настройки в инспекторе

Отображение инвентаря UI-кнопками

Теперь у нас есть инвентарь, но пока он существует только в коде и не отображается на экране. Для визуализации предметов мы воспользуемся UI-кнопками.

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

При добавлении нового предмета мы будем создавать (spawn) UI-кнопку, которая станет визуальным слотом инвентаря. Сначала подготовим префаб этой кнопки.

Перейдите в режим редактирования UI (двойной клик по Canvas) и добавьте кнопку: UI(Canvas) → Button - TextMeshPro. Назовите её InventorySlot.

Вместе с кнопкой автоматически создаётся дочерний текстовый объект. Он нам не нужен — его можно удалить или просто очистить текст, так как мы будем использовать только компонент Image у кнопки.

Настройте размер кнопки так, чтобы он соответствовал размеру будущей иконки предмета в инвентаре.

Теперь создадим скрипт для обработки нажатий по кнопке. Создайте новый MonoBehaviour-скрипт и назовите его InventorySlot.


using UnityEngine;
using UnityEngine.EventSystems;

// Скрипт для UI-слота инвентаря
public class InventorySlot : MonoBehaviour, IPointerClickHandler
{
    // Данные предмета, связанного с этим слотом
    private ItemData item;

    // Инициализация слота данными предмета
    public void Init(ItemData data)
    {
        item = data;
    }

    // Обработка кликов по UI-кнопке
    public void OnPointerClick(PointerEventData eventData)
    {
        // Левый клик — показать описание предмета
        if (eventData.button == PointerEventData.InputButton.Left)
        {
            ItemDescriptionUI.Instance.Show(item);
        }
        // Правый клик — выбрать или снять выбор предмета
        else if (eventData.button == PointerEventData.InputButton.Right)
        {
            if (Inventory.Instance.SelectedItem == item)
                Inventory.Instance.ClearSelection();
            else
                Inventory.Instance.Select(item);
        }
    }
}
  

В этом скрипте: левый клик по кнопке показывает подсказку, а правый — выбирает предмет или снимает выбор, если он уже выбран.

Добавьте этот скрипт на кнопку InventorySlot, а затем создайте префаб, перетащив кнопку из иерархии в папку Assets. После этого кнопку можно удалить из сцены — нужный нам префаб уже сохранён.

Префаб InventorySlot и его настройки в Unity
Подготовка UI-кнопки InventorySlot и создание префаба

Управление визуалом инвентаря

Чтобы не рассчитывать позиции кнопок вручную, воспользуемся UI-компонентом Horizontal Layout Group. Этот компонент автоматически размещает кнопки в заданной области.

Добавьте к UI новый пустой GameObject и назовите его InventoryBar. В RectTransform включите режим Stretch, настройте позицию и размеры.

Сделайте высоту контейнера чуть больше высоты кнопки, а по длине — так, чтобы в него помещалось как минимум 4 и более слотов инвентаря.

Добавьте компонент Horizontal Layout Group и установите следующие параметры:

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

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

Создайте новый MonoBehaviour-скрипт и назовите его InventoryUI.


using UnityEngine;
using UnityEngine.UI;

// Скрипт, связывающий инвентарь с UI
public class InventoryUI : MonoBehaviour
{
    // Контейнер, в который будут добавляться слоты
    public Transform slotsParent;

    // Префаб кнопки слота инвентаря
    public GameObject slotPrefab;

    private void Start()
    {
        // Подписываемся на событие добавления предмета
        Inventory.Instance.OnItemAdded += AddSlot;
    }

    private void OnDisable()
    {
        // Отписываемся от события при отключении объекта
        if (Inventory.Instance != null)
            Inventory.Instance.OnItemAdded -= AddSlot;
    }

    // Создание нового UI-слота
    private void AddSlot(ItemData item)
    {
        // Создаём кнопку-слот внутри контейнера
        GameObject slot = Instantiate(slotPrefab, slotsParent);

        // Назначаем иконку предмета
        Image icon = slot.GetComponentInChildren<Image>();
        if (icon != null)
            icon.sprite = item.icon;

        // Передаём данные предмета в логику слота
        InventorySlot slotLogic = slot.GetComponent<InventorySlot>();
        slotLogic.Init(item);
    }
}
  

Добавьте этот скрипт на объект InventoryBar. В поле Slots Parent укажите сам InventoryBar, а в поле Slot Prefab перетащите префаб кнопки InventorySlot.

InventoryBar с Horizontal Layout Group и компонентом InventoryUI
InventoryBar с настройками Layout Group и скриптом InventoryUI

Иконка выбранного предмета у курсора

Добавим UI-иконку выбранного предмета, которая будет следовать за курсором. Если предмет не выбран — иконка скрывается.

Сделайте двойной клик по Canvas, чтобы перейти в режим редактирования UI. Затем добавьте к Canvas новый объект: UI (Canvas) → Image. Переименуйте созданный UI-объект Image в IconSelected. Настройте размер изображения под будущую иконку предмета.

Создайте новый MonoBehaviour-скрипт и назовите его SelectedCursorUI. Добавьте этот скрипт на созданный UI-объект Image.


using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;

// UI-иконка выбранного предмета, которая следует за курсором
public class SelectedCursorUI : MonoBehaviour
{
    private Image image;
    private RectTransform rect;

    private void Awake()
    {
        // Берём компоненты UI-объекта
        image = GetComponent<Image>();
        rect = GetComponent<RectTransform>();

        // Иконка не должна перехватывать клики мыши
        image.raycastTarget = false;

        // По умолчанию иконку скрываем
        image.enabled = false;
    }

    private void Start()
    {
        // Подписываемся на событие выбора предмета в инвентаре
        if (Inventory.Instance != null)
            Inventory.Instance.OnSelectionChanged += HandleSelectionChanged;
    }

    private void OnDisable()
    {
        // Отписываемся от события, когда объект отключается
        if (Inventory.Instance != null)
            Inventory.Instance.OnSelectionChanged -= HandleSelectionChanged;
    }

    // Обновляем иконку, когда меняется выбранный предмет
    private void HandleSelectionChanged(ItemData selected)
    {
        if (selected == null)
        {
            image.enabled = false;
            return;
        }

        image.sprite = selected.icon;
        image.enabled = true;
    }

    private void Update()
    {
        // Если иконка скрыта — позицию не обновляем
        if (!image.enabled) return;

        // Двигаем иконку за курсором (небольшое смещение, чтобы было видно)
        rect.position = Mouse.current.position.ReadValue() + new Vector2(24, -24);
    }
}
  

💡 Иконка двигается в Update(), потому что позиция курсора меняется каждый кадр. А сам спрайт и видимость мы обновляем через событие выбора предмета.

UI Image для иконки выбранного предмета и компонент SelectedCursorUI в Unity
UI-иконка выбранного предмета и скрипт SelectedCursorUI

Типы объектов по действию

В нашей сцене будет два типа интерактивных объектов.

Первый тип — предметы, которые можно подобрать в инвентарь и использовать как «ключ» для выполнения действий (например, ножницы или ключ).

Второй тип — объекты, которые остаются в сцене и ждут, пока игрок применит к ним нужный предмет (картина, сейф, дверь).

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

Создадим два интерфейса: интерфейс подбора предметов и интерфейс использования.

Создайте пустой C#-скрипт и назовите его IPickable.


using UnityEngine;

// Интерфейс для объектов, которые можно подобрать
public interface IPickable
{
    // Метод вызывается, когда игрок подбирает объект
    void TryPickUp();
}
  

Теперь создайте ещё один пустой C#-скрипт и назовите его IUsable.


using UnityEngine;

// Интерфейс для объектов, которые можно использовать
// с предметом из инвентаря
public interface IUsable
{
    // otherData — данные предмета, который игрок пытается применить
    void TryUse(ItemData otherData);
}
  

Благодаря такому подходу мы можем легко определять, что именно умеет объект: подбираться, использоваться или делать и то, и другое — просто добавляя нужные интерфейсы.

Поднимаемые предметы

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

Создайте новый MonoBehaviour-скрипт и назовите его PickableItem.


using UnityEngine;

// Скрипт для предметов, которые можно подобрать
public class PickableItem : MonoBehaviour, IPickable
{
    // Метод интерфейса IPickable
    public void TryPickUp()
    {
        // Берём данные предмета из компонента CheckableItem
        CheckableItem checkable = GetComponent<CheckableItem>();
        if (checkable == null) return;

        ItemData currentData = checkable.data;
        if (currentData == null) return;

        // Добавляем предмет в инвентарь и удаляем объект со сцены
        Inventory.Instance.Add(currentData);
        Destroy(gameObject);
    }
}
  

Наличие интерфейса IPickable показывает, что объект можно поднять. Благодаря интерфейсу мы сможем вызывать метод поднятия, не думая о том, в каком именно компоненте он реализован.

Всего в нашем пазле будет 3 поднимаемых предмета: Scissors, Key и Code.

Если вы всё ещё находитесь в режиме редактирования UI, сделайте двойной клик по Main Camera, чтобы вернуться к обычному виду сцены. Затем выберите объект Scissors и добавьте к нему компонент PickableItem.

Точно так же добавьте PickableItem к объектам Key и Code.

Компонент PickableItem добавлен к поднимаемому предмету в Unity
Поднимаемый предмет с компонентом PickableItem

Интерактивные объекты сцены

Дальше у нас осталось три объекта в сцене, на которые мы будем применять использование других предметов. Мы используем общий интерфейс IUsable для применения «ключей», но логика обработки у каждого объекта будет индивидуальной.

Картина

Начнём с картины. Она будет реагировать на ножницы: при использовании ножниц картина «разрезается», её спрайт меняется, коллайдер отключается, и становится доступным спрятанный ключ.

Создайте новый MonoBehaviour-скрипт и назовите его PictureInteraction.


using UnityEngine;

// Логика взаимодействия с картиной
public class PictureInteraction : MonoBehaviour, IUsable
{
    // Данные предмета, который является "ключом" (ножницы)
    public ItemData scissorsData;

    // Спрайт разрезанной картины
    public Sprite brokenPicture;

    // Объект, который нужно активировать (ключ под картиной)
    public GameObject itemToEnable;

    // Флаг, чтобы логика сработала только один раз
    private bool isBroken = false;

    // Метод интерфейса IUsable
    public void TryUse(ItemData otherData)
    {
        // Если картина уже разрезана — ничего не делаем
        if (isBroken) return;

        // Проверяем, что применён нужный предмет
        if (otherData != null && otherData == scissorsData)
        {
            // Меняем спрайт картины
            var sr = GetComponent<SpriteRenderer>();
            if (sr != null)
                sr.sprite = brokenPicture;

            // Отключаем коллайдер, чтобы картина больше не реагировала
            var col = GetComponent<Collider2D>();
            if (col != null)
                col.enabled = false;

            // Включаем спрятанный предмет (ключ)
            if (itemToEnable != null)
                itemToEnable.SetActive(true);

            // Помечаем, что действие выполнено
            isBroken = true;
        }
    }
}
  

Добавьте этот скрипт к объекту Picture.

В инспекторе настройте поля:

Объект Picture с компонентом PictureInteraction в инспекторе Unity
Картина на сцене и настройки скрипта PictureInteraction

Сейф

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

Создайте новый MonoBehaviour-скрипт и назовите его SafeInteractions.


using UnityEngine;

// Логика взаимодействия с сейфом
public class SafeInteractions : MonoBehaviour, IUsable
{
    // Данные предмета, который открывает сейф (ключ)
    public ItemData keyData;

    // Дверца сейфа, которую нужно скрыть
    public GameObject safeDoor;

    // Предмет, который появится после открытия (записка с кодом)
    public GameObject itemToEnable;

    // Флаг, чтобы сейф открывался только один раз
    private bool isOpened = false;

    // Метод интерфейса IUsable
    public void TryUse(ItemData otherData)
    {
        if (isOpened) return;

        // Проверяем, что применён нужный предмет
        if (otherData != null && otherData == keyData)
        {
            // Убираем дверь сейфа
            if (safeDoor != null)
                safeDoor.SetActive(false);

            // Показываем спрятанный предмет (Code)
            if (itemToEnable != null)
                itemToEnable.SetActive(true);

            isOpened = true;
        }
    }
}
  

Добавьте скрипт SafeInteractions на объект Safe. Затем в инспекторе настройте поля:

Объект Safe с компонентом SafeInteractions в инспекторе Unity
Сейф: ссылка на ключ, дверцу и спрятанный объект Code

План финала: завершение квеста и рестарт

Сделаем простой финал: после открытия двери мы запускаем небольшой визуальный эффект с камерой, временно отключаем взаимодействие с объектами и через несколько секунд перезапускаем сцену.

Создайте новый MonoBehaviour-скрипт и назовите его DoorInteraction.


using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

// Финальная логика двери: эффект + рестарт сцены
public class DoorInteraction : MonoBehaviour, IUsable
{
    // Данные предмета, который подходит к двери (Code)
    public ItemData codeData;

    // Камера сцены
    public Camera mainCamera;

    // Время до рестарта сцены
    public float restartDelay = 5f;

    // Скорость вращения камеры
    public float rotateSpeed = 30f;

    private bool isFinished = false;

    public void TryUse(ItemData otherData)
    {
        if (isFinished) return;

        if (otherData != null && otherData == codeData)
        {
            // Отключаем коллайдер двери
            var col = GetComponent<Collider2D>();
            if (col != null)
                col.enabled = false;

            // Запускаем финальный эффект
            isFinished = true;
            StartCoroutine(FinalEffect());
        }
    }

    private IEnumerator FinalEffect()
    {
        float timer = 0f;

        while (timer < restartDelay)
        {
            if (mainCamera != null)
            {
                mainCamera.transform.Rotate(0f, 0f, rotateSpeed * Time.deltaTime);
            }

            timer += Time.deltaTime;
            yield return null;
        }

        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }
}
  

Добавьте скрипт DoorInteraction на объект Door. В поле Code Data назначьте ScriptableObject Code.

💡 Мы перезагружаем сцену с задержкой, чтобы игрок успел увидеть результат и понять, что квест завершён. Такой финал выглядит намного лучше, чем мгновенный рестарт.

Объект Door с компонентом DoorInteraction в инспекторе Unity
Финальный объект Door с настройками скрипта DoorInteraction

Обработка ввода

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

Создайте новый MonoBehaviour-скрипт и назовите его ClickHandler.


using UnityEngine;
using UnityEngine.InputSystem;

// Этот скрипт — центральный обработчик ввода.
// Он висит один раз в сцене и реагирует на клики мыши,
// а сами игровые объекты решают, что с этим кликом делать
// через интерфейсы.
public class ClickHandler : MonoBehaviour
{
    private void Update()
    {
        // Если по какой-то причине мышь недоступна — ничего не делаем
        if (Mouse.current == null) return;

        // ===== ЛЕВЫЙ КЛИК =====
        // Используется для осмотра объектов (подсказки / описание)
        if (Mouse.current.leftButton.wasPressedThisFrame)
        {
            // Получаем позицию мыши в мировых координатах
            Vector2 mouseWorldPos =
                Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());

            // Пускаем луч "в точку" — проверяем, есть ли коллайдер под курсором
            RaycastHit2D hit =
                Physics2D.Raycast(mouseWorldPos, Vector2.zero);

            // Если под курсором есть объект
            // и он поддерживает интерфейс ICheckable —
            // показываем его описание
            if (hit.collider != null &&
                hit.collider.TryGetComponent<ICheckable>(out var checkable))
            {
                checkable.TellAbout();
            }
        }

        // ===== ПРАВЫЙ КЛИК =====
        // Контекстное действие: подбор, выбор или использование предмета
        if (Mouse.current.rightButton.wasPressedThisFrame)
        {
            // Позиция мыши в мировых координатах
            Vector2 mouseWorldPos =
                Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());

            // Проверяем, попали ли мы в объект сцены
            RaycastHit2D hit =
                Physics2D.Raycast(mouseWorldPos, Vector2.zero);

            // Если ни во что не попали — выходим
            if (hit.collider == null) return;

            // 1. Проверяем, можно ли объект подобрать
            // (ножницы, ключ, записка с кодом)
            if (hit.collider.TryGetComponent<IPickable>(out var pickable))
            {
                pickable.TryPickUp();

                // Важно: если предмет подобран,
                // дальше этот клик обрабатывать не нужно
                return;
            }

            // 2. Если объект нельзя подобрать,
            // проверяем, можно ли использовать выбранный предмет
            if (hit.collider.TryGetComponent<IUsable>(out var usable))
            {
                // Использовать можно только если в инвентаре
                // выбран какой-то предмет
                if (Inventory.Instance.SelectedItem != null)
                {
                    usable.TryUse(Inventory.Instance.SelectedItem);
                }
            }
        }
    }
}
  

Теперь добавьте в сцену пустой GameObject и назовите его ClickHandler. Добавьте к нему этот скрипт как компонент.

На этом сцена полностью готова. Запустите игру и попробуйте пройти логическую цепочку от ножниц до финальной двери 🙂

Объект ClickHandler с компонентом обработки ввода в инспекторе Unity
Объект ClickHandler с центральным скриптом обработки кликов