🏠 Home

Unity 2D Tutorial for Beginners: Point & Click

In this tutorial, we will step by step build the foundation of a simple point & click game in Unity 2D.

You will learn the core principles of scene interaction: clicking on objects, inspecting them, picking up items, and working with an inventory.

Throughout the tutorial, we will cover:

Create a new Unity 2D project — and let’s get started.

Importing Assets

First, download the archive with images that will be used in the project:

📦 point-click-images.zip

Unpack the archive and import the images into your Unity project.

Unity interface with imported sprites in the Assets folder
Imported sprites in the Unity project Assets window

Adding the Background

Drag the background sprite (it is already included in the asset archive) into the Unity scene. For convenience, rename the object to Background.

Background sprite for a point and click scene
Background sprite used for the point and click scene

Set the Order in Layer value to -10 so the background is rendered behind all other sprites.

Scale the image so that it is larger than the camera view. This prevents empty areas from appearing at the edges of the screen.

Background sprite placed in the Unity scene
Background sprite in the scene with a lowered Order in Layer

Creating a ScriptableObject

ScriptableObject is a convenient data container in Unity. It allows you to store information separately from logic and scene objects.

In our case, the ScriptableObject will be used to describe items: their names, icons, and description text. Such data is easy to edit, reuse, and extend without changing the code.

Create a new script via the menu: Create → Scripting → ScriptableObject Script and name it ItemData.


using UnityEngine;

// ScriptableObject — a container for item data.
// Stores information separately from logic and scene objects.
[CreateAssetMenu(fileName = "ItemData", menuName = "Scriptable Objects/ItemData")]
public class ItemData : ScriptableObject
{
    // Unique item identifier
    public int id;

    // Item name visible to the player
    public string title;

    // Text description of the item
    [TextArea]
    public string description;

    // Item icon for UI and cursor
    public Sprite icon;
}

You can now create separate assets based on this class directly in your Unity project, without tying them to specific scene objects.

Creating a ScriptableObject ItemData asset in Unity
Creating a ScriptableObject asset for storing item data

Gameplay Chain and Object Data

In this puzzle, we use a simple logical sequence to avoid overloading the tutorial and to stay focused on architecture.

The interaction chain looks like this: Scissors → Picture → Key → Safe → Code → Door.

The player picks up the scissors, cuts the picture, finds a key, opens the safe, obtains a code, and finally opens the door.

For each object, we will create a separate ScriptableObject ItemData with a description and an icon.

Create a ScriptableObject via the menu Assets → Create → Scriptable Objects → ItemData and fill in the data for the following items.

💡 For simplicity, we use the item sprite itself as the icon. In real games, icons are usually separate and smaller, but for this tutorial this approach is more than sufficient.

ScriptableObject ItemData assets for puzzle objects in Unity
Created ScriptableObject ItemData assets for all puzzle objects

UI for Displaying Hints

Create a UI for displaying text hints. It will be used to show item descriptions when inspecting objects.

Add a Canvas to the scene and create a Panel inside it (GameObject → UI (Canvas) → Panel). The panel will serve as the background for the text.

Select the Canvas object and set the UI Scale Mode parameter to Scale With Screen Size.

Double-click the Canvas in the hierarchy to enter UI editing mode. Then select the Panel and set its anchor preset to Stretch in the Inspector.

Adjust the size and position of the panel as you see fit. In our case, the panel is placed in the upper-left corner of the screen.

Set a dark background color for the panel so the text is easier to read. Also disable the Raycast Target option to ensure the UI does not interfere with scene clicks and physics.

Canvas and Panel for UI hints in Unity
Canvas and panel used to display hints in Unity

Now add a child UI element for displaying text. Right-click on the Panel and choose UI (Canvas) → TextMeshPro. When adding it for the first time, Unity may prompt you to import the TextMeshPro package — confirm the import.

Select the created text object in the hierarchy and set its anchor preset to Stretch. Adjust the text area size relative to the panel, leaving small inner margins.

In the TextMeshPro - Text (UI) component, open Extra Settings and disable Raycast Target so the text does not capture mouse clicks.

Configure the text appearance to your liking. In our case, the following settings are used:

TextMeshPro settings for hint text in Unity
TextMeshPro settings for displaying hint text

Text Display Controller Script

Create an empty GameObject in the scene and name it ItemDescriptionUI.

Create a new MonoBehaviour script named ItemDescriptionUI and add it as a component to the created object.

Below is the complete script code. It is responsible for displaying text hints and automatically hiding them after a short delay.


using TMPro;
using UnityEngine;

// This script controls the UI panel that displays item descriptions
public class ItemDescriptionUI : MonoBehaviour
{
    // Static reference to the script instance (Singleton)
    public static ItemDescriptionUI Instance;

    // Panel that serves as the background for the text
    [SerializeField] private GameObject panelText;

    // TextMeshPro text field used to display the description
    [SerializeField] private TMP_Text descriptionText;

    // Indicates whether the text is currently visible
    private bool isShowing = false;

    // Time (in seconds) after which the panel will be hidden
    public float timeToDisable = 3f;

    // Current hide timer
    private float tmpTimeToDisable;

    private void Awake()
    {
        // Singleton implementation:
        // if an instance already exists, destroy the duplicate
        if (Instance != null)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;

        // Initialize the timer
        tmpTimeToDisable = timeToDisable;

        // Hide the panel when the scene starts
        if (panelText != null)
            panelText.SetActive(false);
    }

    private void Update()
    {
        // If the text is not currently shown, do nothing
        if (!isShowing) return;

        // Decrease the timer based on frame time
        tmpTimeToDisable -= Time.deltaTime;

        // If time is up, hide the panel
        if (tmpTimeToDisable <= 0f)
            Hide();
    }

    // Method for showing the item description text
    public void Show(ItemData item)
    {
        // If no data is provided, simply hide the panel
        if (item == null)
        {
            Hide();
            return;
        }

        // Safety check for missing Inspector references
        if (panelText == null || descriptionText == null) return;

        // Enable display
        isShowing = true;

        // Reset the hide timer
        tmpTimeToDisable = timeToDisable;

        // Show the panel and update the text
        panelText.SetActive(true);
        descriptionText.text = item.description;
    }

    // Hides the text panel
    private void Hide()
    {
        isShowing = false;

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

Drag the Panel object into the Panel Text field, and the TextMeshPro (Text) component into the Description Text field in the Inspector.

This script uses the Singleton pattern. It allows other scene objects to access ItemDescriptionUI.Instance directly, without manually storing references.

Any object can call the Show() method and pass ItemData to display text on the screen. If the method is called again, the hide timer is reset. If no new calls occur, the panel is automatically hidden after 3 seconds.

ItemDescriptionUI object and its Inspector settings in Unity
ItemDescriptionUI object with configured references in the Inspector

Item Hint

Now we will create a script for displaying item descriptions. The ability to show a hint will be available to all interactive objects in the scene.

Item data will be taken from ScriptableObject ItemData, so a reference to this data will be stored directly in the object’s script.

To access the hint logic, we will use an interface. This allows us to avoid dependencies on a specific script or class name.

The main advantage of interfaces is that we do not need to know which specific component implements the behavior or how it is implemented internally. We simply check for the interface and call the required method.

First, let’s create the hint interface. Create an empty C# script and name it ICheckable.


using UnityEngine;

// Interface for objects that can be "inspected"
// and provide a text hint
public interface ICheckable
{
    // Method called when the object is inspected
    void TellAbout();
}
  

Now create a script that uses this interface. Create a new MonoBehaviour script and name it CheckableItem.


using UnityEngine;

// This script adds the ability to an object
// to show a description when inspected
public class CheckableItem : MonoBehaviour, ICheckable
{
    // Item data (ScriptableObject)
    public ItemData data;

    // Implementation of the ICheckable interface
    public void TellAbout()
    {
        // If no data is assigned, do nothing
        if (data == null) return;

        // Pass the data to the UI to display the text
        ItemDescriptionUI.Instance.Show(data);
    }
}
  

Any object that has the CheckableItem component is now considered inspectable. Later, we will simply check for the presence of the ICheckable interface and call TellAbout(), without knowing anything about the internal implementation.

Interactive Objects

Now let’s add the game objects to the scene. We will start from the end of the chain — the door.

1) Door

Drag the door sprite into the scene. Unity will create a new GameObject. Rename it to Door.

To make the object clickable and detectable by physics, add a Box Collider 2D component.

Then add the CheckableItem component. In the Data field of this script, assign the Door ScriptableObject.

Door object in the Unity scene with Box Collider 2D and CheckableItem
Door in the scene: collider for clicks and data via CheckableItem

2) Code (Note with a Code)

Drag the code note sprite into the scene and place it so that it will later be covered by the safe sprite.

Adjust its size relative to the door (in our example: Scale X = 0.5 and Scale Y = 0.5). Rename the object to Code.

Add the Box Collider 2D and CheckableItem components. In the Data field, assign the Code ScriptableObject.

Now disable the object in the scene (the checkbox next to the object name in the Inspector). The note will be hidden behind the safe and will “wait” until we enable it through logic.

Code object in the Unity scene with Box Collider 2D and CheckableItem
Code note: configured components and disabled object in the scene

3) Safe

Let’s add the safe that will “store” the note with the code. Drag the empty safe sprite into the scene and rename the object to Safe.

Move the safe to the position of the code note. For convenience, you can temporarily enable the Code object to align positions more easily, then disable it again.

Adjust the safe size (in our example: Scale X = 0.5, Scale Y = 0.5) and set Order in Layer to 5 so the safe is rendered on top of the note.

Add the Box Collider 2D and CheckableItem components to the Safe object. In the Data field, assign the Safe ScriptableObject.

Now add the safe door sprite to the scene. Name the object SafeDoor and set its Order in Layer to 10 so the door is rendered above the safe body.

Place the door over the safe, adjust its size, and then make it a child of the safe by simply dragging SafeDoor onto Safe in the hierarchy.

Safe object and its components in the Unity Inspector
Safe in the scene: body, door, and configured components

4) Key

Now let’s add the key for opening the safe. It will be hidden behind the picture.

Drag the key sprite into the scene and place it where the picture will later be located. Rename the object to Key.

Add the Box Collider 2D and CheckableItem components. In the Data field, assign the Key ScriptableObject.

After that, disable the object in the scene (the checkbox next to its name in the Inspector). The key will remain hidden until we unlock access to it through logic.

Key object in the Unity scene with Box Collider 2D and CheckableItem
Key in the scene: configured components and disabled object

5) Picture

Drag the picture sprite into the scene and rename the object to Picture.

Set the Order in Layer value to 5 so the picture is rendered above the key. Move the picture to the position where the key is hidden (if necessary, temporarily enable the Key object for convenience, then disable it again).

Add the Box Collider 2D and CheckableItem components. In the Data field, assign the Picture ScriptableObject.

Picture object in the Unity scene with Box Collider 2D and CheckableItem
Picture in the scene: rendered above the key and configured components

6) Scissors

Drag the scissors sprite into the scene and place it anywhere convenient. Rename the object to Scissors.

Add the Box Collider 2D and CheckableItem components. In the Data field, assign the Scissors ScriptableObject.

Scissors object in the Unity scene with Box Collider 2D and CheckableItem
Scissors in the scene: collider for clicks and data via CheckableItem

Item Inventory

Now let’s create an inventory for storing items. Note that we will not store the actual game objects themselves. Instead, the inventory will contain data containers — ScriptableObject ItemData.

To store items, we will use a dynamic list. The inventory itself will be made static so it can be accessed directly from any script.

We will also use events. An event allows us to notify other scripts that something has happened (for example, an item was added to the inventory), and subscribed objects can react in their own way.

Create a new MonoBehaviour script and name it Inventory.


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

// Inventory for storing item data
public class Inventory : MonoBehaviour
{
    // Static reference to the inventory (Singleton)
    public static Inventory Instance;

    // Currently selected item
    public ItemData SelectedItem { get; private set; }

    // List of items in the inventory
    public List<ItemData> items = new();

    // Event triggered when an item is added
    public event Action<ItemData> OnItemAdded;
    // Event triggered when the selected item changes
    public event Action<ItemData> OnSelectionChanged;

    private void Awake()
    {
        // Ensure there is only one inventory in the scene
        if (Instance != null)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
    }

    // Adds an item to the inventory
    public void Add(ItemData item)
    {
        if (item == null) return;

        items.Add(item);

        // Notify subscribers that an item was added
        OnItemAdded?.Invoke(item);
    }

    // Selects an item in the inventory
    public void Select(ItemData item)
    {
        SelectedItem = item;
        // Notify subscribers about the selection change
        OnSelectionChanged?.Invoke(SelectedItem);
    }

    // Clears the currently selected item
    public void ClearSelection()
    {
        SelectedItem = null;
        // Notify subscribers about the selection change
        OnSelectionChanged?.Invoke(SelectedItem);
    }
}
  

Add an empty GameObject to the scene and name it Inventory. Then add this script to it as a component.

Inventory object in the Unity scene with the Inventory component
Inventory object in the scene and its Inspector settings

Displaying the Inventory with UI Buttons

Now we have an inventory, but so far it exists only in code and is not displayed on the screen. To visualize items, we will use UI buttons.

This approach allows us not only to display inventory slots, but also to handle clicks on them. Each item in the inventory will be represented by a separate button.

When a new item is added, we will spawn a UI button that becomes a visual inventory slot. First, let’s prepare a prefab for this button.

Switch to UI editing mode (double-click the Canvas) and add a button: UI (Canvas) → Button - TextMeshPro. Name it InventorySlot.

A child text object is automatically created along with the button. We do not need it — you can delete it or simply clear the text, since we will only use the Image component of the button.

Adjust the button size so that it matches the size of the item icon in the inventory.

Now let’s create a script to handle button clicks. Create a new MonoBehaviour script and name it InventorySlot.


using UnityEngine;
using UnityEngine.EventSystems;

// Script for an inventory UI slot
public class InventorySlot : MonoBehaviour, IPointerClickHandler
{
    // Item data associated with this slot
    private ItemData item;

    // Initialize the slot with item data
    public void Init(ItemData data)
    {
        item = data;
    }

    // Handle clicks on the UI button
    public void OnPointerClick(PointerEventData eventData)
    {
        // Left click — show the item description
        if (eventData.button == PointerEventData.InputButton.Left)
        {
            ItemDescriptionUI.Instance.Show(item);
        }
        // Right click — select or deselect the item
        else if (eventData.button == PointerEventData.InputButton.Right)
        {
            if (Inventory.Instance.SelectedItem == item)
                Inventory.Instance.ClearSelection();
            else
                Inventory.Instance.Select(item);
        }
    }
}
  

In this script, a left click on the button shows the item hint, while a right click selects the item or clears the selection if it is already selected.

Add this script to the InventorySlot button, then create a prefab by dragging the button from the hierarchy into the Assets folder. After that, the button can be removed from the scene — the required prefab is now saved.

InventorySlot prefab and its settings in Unity
Preparing the InventorySlot UI button and creating the prefab

Managing the Inventory Visuals

To avoid positioning buttons manually, we will use the Horizontal Layout Group UI component. This component automatically arranges buttons within a defined area.

Add a new empty GameObject to the UI and name it InventoryBar. In the RectTransform, enable Stretch mode and adjust its position and size.

Set the container height slightly larger than the button height, and make it wide enough to fit at least 4 or more inventory slots.

Add the Horizontal Layout Group component and set the following parameters:

The buttons will now be automatically grouped inside this area, without any manual position adjustments.

Next, we will add a script that connects the inventory logic from code to its visual representation.

Create a new MonoBehaviour script and name it InventoryUI.


using UnityEngine;
using UnityEngine.UI;

// Script that connects the inventory with the UI
public class InventoryUI : MonoBehaviour
{
    // Container where inventory slots will be added
    public Transform slotsParent;

    // Prefab of the inventory slot button
    public GameObject slotPrefab;

    private void Start()
    {
        // Subscribe to the item-added event
        Inventory.Instance.OnItemAdded += AddSlot;
    }

    private void OnDisable()
    {
        // Unsubscribe from the event when the object is disabled
        if (Inventory.Instance != null)
            Inventory.Instance.OnItemAdded -= AddSlot;
    }

    // Creates a new UI slot
    private void AddSlot(ItemData item)
    {
        // Instantiate the slot button inside the container
        GameObject slot = Instantiate(slotPrefab, slotsParent);

        // Assign the item icon
        Image icon = slot.GetComponentInChildren<Image>();
        if (icon != null)
            icon.sprite = item.icon;

        // Pass the item data to the slot logic
        InventorySlot slotLogic = slot.GetComponent<InventorySlot>();
        slotLogic.Init(item);
    }
}
  

Add this script to the InventoryBar object. In the Slots Parent field, assign the InventoryBar itself, and in the Slot Prefab field, drag the InventorySlot button prefab.

InventoryBar with Horizontal Layout Group and InventoryUI component
InventoryBar with Layout Group settings and the InventoryUI script

Selected Item Icon Following the Cursor

Let’s add a UI icon for the selected item that follows the mouse cursor. If no item is selected, the icon is hidden.

Double-click the Canvas to enter UI editing mode. Then add a new object to the Canvas: UI (Canvas) → Image. Rename the created UI object Image to IconSelected. Adjust the image size to match the future item icon.

Create a new MonoBehaviour script named SelectedCursorUI. Add this script to the created Image UI object.


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

// UI icon of the selected item that follows the cursor
public class SelectedCursorUI : MonoBehaviour
{
    private Image image;
    private RectTransform rect;

    private void Awake()
    {
        // Get UI object components
        image = GetComponent<Image>();
        rect = GetComponent<RectTransform>();

        // The icon should not block mouse clicks
        image.raycastTarget = false;

        // Hide the icon by default
        image.enabled = false;
    }

    private void Start()
    {
        // Subscribe to the inventory selection change event
        if (Inventory.Instance != null)
            Inventory.Instance.OnSelectionChanged += HandleSelectionChanged;
    }

    private void OnDisable()
    {
        // Unsubscribe from the event when the object is disabled
        if (Inventory.Instance != null)
            Inventory.Instance.OnSelectionChanged -= HandleSelectionChanged;
    }

    // Update the icon when the selected item changes
    private void HandleSelectionChanged(ItemData selected)
    {
        if (selected == null)
        {
            image.enabled = false;
            return;
        }

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

    private void Update()
    {
        // Do not update the position if the icon is hidden
        if (!image.enabled) return;

        // Move the icon with the cursor (slight offset for visibility)
        rect.position = Mouse.current.position.ReadValue() + new Vector2(24, -24);
    }
}
  

💡 The icon position is updated in Update() because the cursor position changes every frame. The sprite and visibility, however, are updated via the inventory selection event.

UI Image for the selected item icon and the SelectedCursorUI component in Unity
Selected item UI icon and the SelectedCursorUI script

Object Types by Action

In our scene, there will be two types of interactive objects.

The first type consists of items that can be picked up into the inventory and used as a “key” to perform actions (for example, scissors or a key).

The second type consists of objects that remain in the scene and wait for the player to apply the correct item to them (picture, safe, door).

To determine an object’s type and execute its logic, we once again use interfaces. This allows us to avoid binding to specific scripts and instead check only for the required behavior.

We will create two interfaces: one for picking up items and one for using items.

Create an empty C# script and name it IPickable.


using UnityEngine;

// Interface for objects that can be picked up
public interface IPickable
{
    // Method called when the player picks up the object
    void TryPickUp();
}
  

Now create another empty C# script and name it IUsable.


using UnityEngine;

// Interface for objects that can be used
// with an item from the inventory
public interface IUsable
{
    // otherData — the data of the item the player is trying to use
    void TryUse(ItemData otherData);
}
  

With this approach, we can easily determine what an object can do: be picked up, be used, or even do both — simply by adding the appropriate interfaces.

Pickable Items

First, let’s define the items that can be picked up and added to the inventory when used. For this purpose, we already created the IPickable interface.

Create a new MonoBehaviour script and name it PickableItem.


using UnityEngine;

// Script for items that can be picked up
public class PickableItem : MonoBehaviour, IPickable
{
    // IPickable interface method
    public void TryPickUp()
    {
        // Get item data from the CheckableItem component
        CheckableItem checkable = GetComponent<CheckableItem>();
        if (checkable == null) return;

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

        // Add the item to the inventory and remove it from the scene
        Inventory.Instance.Add(currentData);
        Destroy(gameObject);
    }
}
  

The presence of the IPickable interface indicates that the object can be picked up. Thanks to the interface, we can call the pickup method without worrying about which specific component implements it.

In total, there will be 3 pickable items in our puzzle: Scissors, Key, and Code.

If you are still in UI editing mode, double-click the Main Camera to return to the normal scene view. Then select the Scissors object and add the PickableItem component to it.

Add the PickableItem component to the Key and Code objects in the same way.

PickableItem component added to a pickable item in Unity
Pickable item with the PickableItem component

Interactive Scene Objects

At this point, three objects remain in the scene to which we will apply the use of other items. We use the common IUsable interface to apply “keys”, but each object will handle the logic in its own way.

Picture

Let’s start with the picture. It will react to the scissors: when the scissors are used, the picture is “cut”, its sprite changes, its collider is disabled, and the hidden key becomes available.

Create a new MonoBehaviour script and name it PictureInteraction.


using UnityEngine;

// Logic for interacting with the picture
public class PictureInteraction : MonoBehaviour, IUsable
{
    // Data of the item that acts as the "key" (scissors)
    public ItemData scissorsData;

    // Sprite of the cut picture
    public Sprite brokenPicture;

    // Object to activate (the key hidden behind the picture)
    public GameObject itemToEnable;

    // Flag to ensure the logic runs only once
    private bool isBroken = false;

    // IUsable interface method
    public void TryUse(ItemData otherData)
    {
        // If the picture is already cut, do nothing
        if (isBroken) return;

        // Check that the correct item is used
        if (otherData != null && otherData == scissorsData)
        {
            // Change the picture sprite
            var sr = GetComponent<SpriteRenderer>();
            if (sr != null)
                sr.sprite = brokenPicture;

            // Disable the collider so the picture no longer reacts
            var col = GetComponent<Collider2D>();
            if (col != null)
                col.enabled = false;

            // Enable the hidden item (key)
            if (itemToEnable != null)
                itemToEnable.SetActive(true);

            // Mark the action as completed
            isBroken = true;
        }
    }
}
  

Add this script to the Picture object.

Configure the following fields in the Inspector:

Picture object with the PictureInteraction component in the Unity Inspector
Picture in the scene and PictureInteraction script settings

Safe

Now let’s configure the safe. It will be opened with a key: after using the key, the safe door disappears and the hidden note with the code becomes active.

Create a new MonoBehaviour script and name it SafeInteractions.


using UnityEngine;

// Logic for interacting with the safe
public class SafeInteractions : MonoBehaviour, IUsable
{
    // Data of the item that opens the safe (key)
    public ItemData keyData;

    // Safe door that needs to be hidden
    public GameObject safeDoor;

    // Item that appears after opening (code note)
    public GameObject itemToEnable;

    // Flag to ensure the safe opens only once
    private bool isOpened = false;

    // IUsable interface method
    public void TryUse(ItemData otherData)
    {
        if (isOpened) return;

        // Check that the correct item is used
        if (otherData != null && otherData == keyData)
        {
            // Hide the safe door
            if (safeDoor != null)
                safeDoor.SetActive(false);

            // Show the hidden item (Code)
            if (itemToEnable != null)
                itemToEnable.SetActive(true);

            isOpened = true;
        }
    }
}
  

Add the SafeInteractions script to the Safe object. Then configure the following fields in the Inspector:

Safe object with the SafeInteractions component in the Unity Inspector
Safe: references to the key, the door, and the hidden Code object

Final Plan: Quest Completion and Restart

Let’s create a simple ending: after opening the door, we trigger a small visual effect using the camera, temporarily disable interaction with objects, and restart the scene after a few seconds.

Create a new MonoBehaviour script and name it DoorInteraction.


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

// Final door logic: effect + scene restart
public class DoorInteraction : MonoBehaviour, IUsable
{
    // Data of the item that fits the door (Code)
    public ItemData codeData;

    // Scene camera
    public Camera mainCamera;

    // Delay before restarting the scene
    public float restartDelay = 5f;

    // Camera rotation speed
    public float rotateSpeed = 30f;

    private bool isFinished = false;

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

        if (otherData != null && otherData == codeData)
        {
            // Disable the door collider
            var col = GetComponent<Collider2D>();
            if (col != null)
                col.enabled = false;

            // Start the final effect
            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
        );
    }
}
  

Add the DoorInteraction script to the Door object. In the Code Data field, assign the Code ScriptableObject.

💡 We restart the scene with a delay so the player has time to see the result and understand that the quest is complete. This feels much better than an instant restart.

Door object with the DoorInteraction component in the Unity Inspector
Final Door object with the DoorInteraction script settings

Input Handling

As the final touch, let’s bring the entire scene to life. We will create a script that handles mouse input:

Create a new MonoBehaviour script and name it ClickHandler.


using UnityEngine;
using UnityEngine.InputSystem;

// This script is the central input handler.
// It exists once in the scene and reacts to mouse clicks,
// while the game objects themselves decide what to do
// with those clicks via interfaces.
public class ClickHandler : MonoBehaviour
{
    private void Update()
    {
        // If the mouse is not available for some reason, do nothing
        if (Mouse.current == null) return;

        // ===== LEFT CLICK =====
        // Used to inspect objects (hints / descriptions)
        if (Mouse.current.leftButton.wasPressedThisFrame)
        {
            // Get mouse position in world coordinates
            Vector2 mouseWorldPos =
                Camera.main.ScreenToWorldPoint(
                    Mouse.current.position.ReadValue()
                );

            // Cast a ray "into the point" to check for a collider
            RaycastHit2D hit =
                Physics2D.Raycast(mouseWorldPos, Vector2.zero);

            // If there is an object under the cursor
            // and it supports the ICheckable interface —
            // show its description
            if (hit.collider != null &&
                hit.collider.TryGetComponent<ICheckable>(
                    out var checkable))
            {
                checkable.TellAbout();
            }
        }

        // ===== RIGHT CLICK =====
        // Context action: pick up, select, or use an item
        if (Mouse.current.rightButton.wasPressedThisFrame)
        {
            // Mouse position in world coordinates
            Vector2 mouseWorldPos =
                Camera.main.ScreenToWorldPoint(
                    Mouse.current.position.ReadValue()
                );

            // Check if we hit a scene object
            RaycastHit2D hit =
                Physics2D.Raycast(mouseWorldPos, Vector2.zero);

            // If nothing was hit, exit
            if (hit.collider == null) return;

            // 1. Check if the object can be picked up
            // (scissors, key, code note)
            if (hit.collider.TryGetComponent<IPickable>(
                    out var pickable))
            {
                pickable.TryPickUp();

                // Important: if the item was picked up,
                // no further processing is needed for this click
                return;
            }

            // 2. If the object cannot be picked up,
            // check if we can use the selected inventory item
            if (hit.collider.TryGetComponent<IUsable>(
                    out var usable))
            {
                // An item can only be used if one is selected
                if (Inventory.Instance.SelectedItem != null)
                {
                    usable.TryUse(
                        Inventory.Instance.SelectedItem
                    );
                }
            }
        }
    }
}
  

Now add an empty GameObject to the scene and name it ClickHandler. Add this script to it as a component.

The scene is now fully complete. Run the game and try to go through the entire logical chain — from the scissors to the final door 🙂

ClickHandler object with the input handling component in the Unity Inspector
ClickHandler object with the central click handling script