В этом туториале мы шаг за шагом соберём основу простой игры в жанре point & click на Unity 2D.
Ты познакомишься с базовыми принципами взаимодействия в сцене: клики по объектам, осмотр, подбор предметов и работу с инвентарём.
По ходу туториала разберём:
Создавай новый Unity 2D проект — и поехали.
Для начала скачайте архив с изображениями, которые будут использоваться в проекте:
Распакуйте архив и импортируйте изображения в ваш Unity-проект.
Перетащите спрайт фона (он уже есть в архиве с ассетами) на сцену
Unity. Для удобства переименуйте объект в Background.
Установите параметр Order in Layer в значение -10, чтобы фон отрисовывался позади остальных спрайтов.
Растяните изображение так, чтобы оно было больше области камеры. Это предотвратит появление пустых зон по краям экрана.
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, не привязывая их к конкретным объектам сцены.
В этом пазле мы используем простую логическую последовательность, чтобы не перегружать туториал и сосредоточиться на архитектуре.
Цепочка взаимодействий будет выглядеть так: Scissors → Picture → Key → Safe → Code → Door.
Игрок подбирает ножницы, разрезает картину, находит ключ, открывает сейф, получает код и открывает финальную дверь.
Для каждого объекта мы создадим отдельный ScriptableObject ItemData с описанием и иконкой.
Создайте ScriptableObject через меню Assets → Create → Scriptable Objects → ItemData и заполните данные для следующих предметов.
icon.
💡 Для простоты мы используем сам спрайт предмета как иконку. В реальных играх иконки обычно делают отдельными и меньшего размера, но для туториала этого более чем достаточно.
Создайте UI для отображения текстовых подсказок. Он будет использоваться для вывода описаний предметов при осмотре.
Добавьте на сцену Canvas и внутри него создайте Panel (GameObject → UI (Canvas) → Panel). Панель будет служить фоном для текста.
Выберите объект Canvas и установите параметр UI Scale Mode в значение Scale With Screen Size.
Сделайте двойной клик по Canvas в иерархии, чтобы перейти в режим редактирования UI. Затем выберите Panel и в инспекторе установите привязку Stretch.
Настройте размеры и положение панели так, как считаете удобным. В нашем случае панель размещена в левом верхнем углу экрана.
Установите тёмный цвет фона панели — так текст будет лучше читаем. Также отключите параметр Raycast Target, чтобы UI не мешал обработке кликов и физики на сцене.
Добавим дочерний UI-элемент для отображения текста. Кликните правой кнопкой мыши по Panel и выберите UI(Canvas) - TextMeshPro. При первом добавлении Unity может предложить импортировать модуль TextMeshPro — подтвердите загрузку.
Выберите созданный текстовый объект в иерархии и установите для него привязку Stretch. Настройте размеры текстового окна относительно панели, оставив небольшие внутренние отступы.
В компоненте TextMeshPro - Text (UI) откройте раздел Extra Settings и отключите параметр Raycast Target, чтобы текст не перехватывал клики.
Настройте отображение текста по своему вкусу. В нашем случае используются следующие параметры:
Создайте пустой 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 секунды.
Теперь создадим скрипт для показа описания предмета. Возможность выводить подсказку будет у всех интерактивных объектов в сцене.
Данные предмета мы будем брать из 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(), не зная ничего о внутренней
реализации.
Теперь добавим игровые предметы в сцену. Начнём с конца цепочки — с двери.
Перетащите спрайт двери на сцену. Unity создаст новый GameObject. Переименуйте его в Door.
Чтобы объект можно было кликать и находить через физику, добавьте компонент Box Collider 2D.
Затем добавьте компонент CheckableItem. В поле Data этого скрипта назначьте ScriptableObject Door.
Перетащите спрайт записки с кодом на сцену и расположите его так, чтобы позже записка перекрывалась спрайтом сейфа.
Настройте размер относительно двери (в нашем примере: Scale X = 0.5 и Scale Y = 0.5). Переименуйте объект в Code.
Добавьте к объекту компоненты Box Collider 2D и CheckableItem. В поле Data назначьте ScriptableObject Code.
Теперь отключите объект на сцене (галочка рядом с именем объекта в инспекторе). Записка будет спрятана за сейфом и “ждать” момента, когда мы включим её через логику.
Добавим сейф, в котором будет «храниться» записка с кодом. Перетащите спрайт пустого сейфа на сцену и переименуйте объект в 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 в иерархии.
Теперь добавим ключ для открытия сейфа. Он будет спрятан под картиной.
Перетащите спрайт ключа на сцену и разместите его в том месте, где позже будет находиться картина. Переименуйте объект в Key.
Добавьте к объекту компоненты Box Collider 2D и CheckableItem. В поле Data назначьте ScriptableObject Key.
После этого отключите объект на сцене (галочка рядом с именем в инспекторе). Ключ будет скрыт до тех пор, пока мы не откроем к нему доступ через логику.
Перетащите спрайт картины на сцену и переименуйте объект в Picture.
Установите параметр Order in Layer в значение 5, чтобы картина рисовалась поверх ключа. Переместите картину на место, где спрятан ключ (при необходимости можно временно включить объект Key для удобства, а затем снова отключить его).
Добавьте к объекту компоненты Box Collider 2D и CheckableItem. В поле Data назначьте ScriptableObject Picture.
Перетащите спрайт ножниц на сцену и разместите его в любом удобном месте. Переименуйте объект в Scissors.
Добавьте к объекту компоненты Box Collider 2D и CheckableItem. В поле Data назначьте ScriptableObject Scissors.
Теперь создадим инвентарь для хранения предметов. Обратите внимание: мы не будем хранить сами игровые объекты, вместо этого в инвентаре будут лежать контейнеры с данными — 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. Затем добавьте к нему этот скрипт как компонент.
Теперь у нас есть инвентарь, но пока он существует только в коде и не отображается на экране. Для визуализации предметов мы воспользуемся 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. После этого кнопку можно удалить из сцены — нужный нам префаб уже сохранён.
Чтобы не рассчитывать позиции кнопок вручную, воспользуемся 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.
Добавим 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(), потому что позиция
курсора меняется каждый кадр. А сам спрайт и видимость мы обновляем
через событие выбора предмета.
В нашей сцене будет два типа интерактивных объектов.
Первый тип — предметы, которые можно подобрать в инвентарь и использовать как «ключ» для выполнения действий (например, ножницы или ключ).
Второй тип — объекты, которые остаются в сцене и ждут, пока игрок применит к ним нужный предмет (картина, сейф, дверь).
Для определения типа объекта и выполнения его логики мы снова используем интерфейсы. Это позволяет не привязываться к конкретным скриптам и проверять только наличие нужного поведения.
Создадим два интерфейса: интерфейс подбора предметов и интерфейс использования.
Создайте пустой 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.
Дальше у нас осталось три объекта в сцене, на которые мы будем применять использование других предметов. Мы используем общий интерфейс 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.
В инспекторе настройте поля:
Теперь настроим сейф. Он будет открываться ключом: после использования ключа дверца сейфа исчезнет, а спрятанная записка с кодом станет активной.
Создайте новый 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. Затем в инспекторе настройте поля:
Сделаем простой финал: после открытия двери мы запускаем небольшой визуальный эффект с камерой, временно отключаем взаимодействие с объектами и через несколько секунд перезапускаем сцену.
Создайте новый 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.
💡 Мы перезагружаем сцену с задержкой, чтобы игрок успел увидеть результат и понять, что квест завершён. Такой финал выглядит намного лучше, чем мгновенный рестарт.
Последним штрихом оживим всю сцену. Мы создадим скрипт, который будет обрабатывать ввод мыши:
Создайте новый 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. Добавьте к нему этот скрипт как компонент.
На этом сцена полностью готова. Запустите игру и попробуйте пройти логическую цепочку от ножниц до финальной двери 🙂