In diesem Tutorial erstellen wir Schritt für Schritt die Grundlage eines einfachen Point-&-Click-Spiels in Unity 2D.
Du lernst die grundlegenden Prinzipien der Interaktion in der Szene kennen: das Anklicken von Objekten, das Untersuchen, das Aufnehmen von Gegenständen und die Arbeit mit einem Inventar.
Im Verlauf des Tutorials behandeln wir:
Erstelle ein neues Unity-2D-Projekt — und los geht’s.
Lade zunächst das Archiv mit den Bildern herunter, die im Projekt verwendet werden:
Entpacke das Archiv und importiere die Bilder in dein Unity-Projekt.
Ziehe den Hintergrund-Sprite (er ist bereits im Asset-Archiv
enthalten) in die Unity-Szene. Benenne das Objekt zur besseren
Übersicht in
Background um.
Setze den Wert Order in Layer auf -10, damit der Hintergrund hinter allen anderen Sprites gerendert wird.
Skaliere das Bild so, dass es größer als der Kamerabereich ist. Dadurch werden leere Bereiche an den Bildschirmrändern vermieden.
ScriptableObject ist ein praktischer Datencontainer in Unity. Er ermöglicht es, Informationen getrennt von Logik und Szenenobjekten zu speichern.
In unserem Fall wird das ScriptableObject zur Beschreibung von Items verwendet: Name, Icon und Beschreibungstext. Solche Daten lassen sich leicht bearbeiten, wiederverwenden und erweitern, ohne den Code zu ändern.
Erstelle ein neues Skript über das Menü: Create → Scripting → ScriptableObject Script und nenne es ItemData.
using UnityEngine;
// ScriptableObject — ein Container für Item-Daten.
// Speichert Informationen getrennt von Logik und Szenenobjekten.
[CreateAssetMenu(fileName = "ItemData", menuName = "Scriptable Objects/ItemData")]
public class ItemData : ScriptableObject
{
// Eindeutige Kennung des Items
public int id;
// Name des Items, der dem Spieler angezeigt wird
public string title;
// Textbeschreibung des Items
[TextArea]
public string description;
// Item-Icon für UI und Cursor
public Sprite icon;
}
Auf Basis dieser Klasse kannst du nun direkt im Unity-Projekt einzelne Assets mit Item-Daten erstellen, ohne sie an konkrete Szenenobjekte zu binden.
In diesem Puzzle verwenden wir eine einfache logische Abfolge, um das Tutorial nicht zu überladen und den Fokus auf die Architektur zu legen.
Die Interaktionskette sieht wie folgt aus: Scissors → Picture → Key → Safe → Code → Door.
Der Spieler nimmt die Schere auf, schneidet das Bild auf, findet einen Schlüssel, öffnet den Safe, erhält einen Code und öffnet schließlich die Tür.
Für jedes Objekt erstellen wir ein eigenes ScriptableObject ItemData mit Beschreibung und Icon.
Erstelle ein ScriptableObject über das Menü Assets → Create → Scriptable Objects → ItemData und fülle die Daten für die folgenden Items aus.
icon.
💡 Der Einfachheit halber verwenden wir den Item-Sprite selbst als Icon. In echten Spielen sind Icons meist separat und kleiner, für dieses Tutorial ist dieser Ansatz jedoch vollkommen ausreichend.
Erstelle ein UI zur Anzeige von Texthinweisen. Es wird verwendet, um Item-Beschreibungen beim Untersuchen von Objekten anzuzeigen.
Füge der Szene ein Canvas hinzu und erstelle darin ein Panel (GameObject → UI (Canvas) → Panel). Das Panel dient als Hintergrund für den Text.
Wähle das Canvas-Objekt aus und setze den Parameter UI Scale Mode auf Scale With Screen Size.
Doppelklicke im Hierarchy-Fenster auf das Canvas, um in den UI-Bearbeitungsmodus zu wechseln. Wähle anschließend das Panel aus und setze im Inspector die Anker-Voreinstellung auf Stretch.
Passe Größe und Position des Panels nach Bedarf an. In unserem Fall ist das Panel in der oberen linken Ecke des Bildschirms platziert.
Wähle eine dunkle Hintergrundfarbe für das Panel, damit der Text besser lesbar ist. Deaktiviere außerdem die Option Raycast Target, damit das UI keine Klicks oder Physik-Interaktionen in der Szene blockiert.
Füge nun ein untergeordnetes UI-Element zur Anzeige des Textes hinzu. Klicke mit der rechten Maustaste auf das Panel und wähle UI (Canvas) → TextMeshPro. Beim ersten Hinzufügen kann Unity vorschlagen, das TextMeshPro-Paket zu importieren — bestätige den Import.
Wähle das erstellte Textobjekt in der Hierarchie aus und setze dessen Anker-Voreinstellung auf Stretch. Passe die Größe des Textbereichs relativ zum Panel an und lasse dabei kleine Innenabstände.
Öffne im Component TextMeshPro - Text (UI) den Bereich Extra Settings und deaktiviere Raycast Target, damit der Text keine Mausklicks abfängt.
Passe die Textdarstellung nach deinem Geschmack an. In unserem Beispiel werden folgende Einstellungen verwendet:
Erstelle ein leeres GameObject in der Szene und nenne es ItemDescriptionUI.
Erstelle ein neues MonoBehaviour-Skript mit dem Namen ItemDescriptionUI und füge es dem erstellten Objekt als Komponente hinzu.
Unten siehst du den vollständigen Skriptcode. Er ist dafür zuständig, Texthinweise anzuzeigen und sie nach kurzer Zeit automatisch wieder auszublenden.
using TMPro;
using UnityEngine;
// Dieses Skript steuert das UI-Panel zur Anzeige von Item-Beschreibungen
public class ItemDescriptionUI : MonoBehaviour
{
// Statische Referenz auf die Skript-Instanz (Singleton)
public static ItemDescriptionUI Instance;
// Panel, das als Hintergrund für den Text dient
[SerializeField] private GameObject panelText;
// TextMeshPro-Textfeld zur Anzeige der Beschreibung
[SerializeField] private TMP_Text descriptionText;
// Gibt an, ob der Text aktuell angezeigt wird
private bool isShowing = false;
// Zeit (in Sekunden), nach der das Panel ausgeblendet wird
public float timeToDisable = 3f;
// Aktueller Ausblend-Timer
private float tmpTimeToDisable;
private void Awake()
{
// Singleton-Implementierung:
// Existiert bereits eine Instanz, wird das Duplikat zerstört
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
// Initialisieren des Timers
tmpTimeToDisable = timeToDisable;
// Panel beim Start der Szene ausblenden
if (panelText != null)
panelText.SetActive(false);
}
private void Update()
{
// Wenn aktuell kein Text angezeigt wird, nichts tun
if (!isShowing) return;
// Timer unter Berücksichtigung der Frame-Zeit verringern
tmpTimeToDisable -= Time.deltaTime;
// Wenn die Zeit abgelaufen ist, Panel ausblenden
if (tmpTimeToDisable <= 0f)
Hide();
}
// Methode zum Anzeigen der Item-Beschreibung
public void Show(ItemData item)
{
// Wenn keine Daten übergeben wurden, Panel ausblenden
if (item == null)
{
Hide();
return;
}
// Sicherheitsprüfung für fehlende Referenzen im Inspector
if (panelText == null || descriptionText == null) return;
// Anzeige aktivieren
isShowing = true;
// Ausblend-Timer zurücksetzen
tmpTimeToDisable = timeToDisable;
// Panel anzeigen und Text setzen
panelText.SetActive(true);
descriptionText.text = item.description;
}
// Blendet das Text-Panel aus
private void Hide()
{
isShowing = false;
if (panelText != null)
panelText.SetActive(false);
}
}
Ziehe das Objekt Panel in das Feld Panel Text und die TextMeshPro (Text)-Komponente in das Feld Description Text im Inspector.
Dieses Skript verwendet das Singleton-Pattern.
Dadurch können andere Objekte in der Szene direkt auf
ItemDescriptionUI.Instance zugreifen, ohne Referenzen
manuell speichern zu müssen.
Jedes Objekt kann die Methode Show() aufrufen und
ItemData übergeben, um Text auf dem Bildschirm
anzuzeigen. Wird die Methode erneut aufgerufen, wird der
Ausblend-Timer zurückgesetzt. Erfolgen keine weiteren Aufrufe, wird
das Panel nach 3 Sekunden automatisch ausgeblendet.
Nun erstellen wir ein Skript zur Anzeige von Item-Beschreibungen. Die Möglichkeit, einen Hinweis anzuzeigen, steht allen interaktiven Objekten in der Szene zur Verfügung.
Die Item-Daten stammen aus dem ScriptableObject ItemData, daher wird eine Referenz auf diese Daten direkt im Skript des Objekts gespeichert.
Um auf die Logik der Hinweise zuzugreifen, verwenden wir ein Interface. Dadurch vermeiden wir Abhängigkeiten von einem bestimmten Skript oder Klassennamen.
Der größte Vorteil von Interfaces besteht darin, dass wir nicht wissen müssen, welche konkrete Komponente das Verhalten implementiert oder wie es intern umgesetzt ist. Wir prüfen lediglich, ob das Interface vorhanden ist, und rufen die benötigte Methode auf.
Zuerst erstellen wir das Hinweis-Interface. Lege ein leeres C#-Skript an und nenne es ICheckable.
using UnityEngine;
// Interface für Objekte, die "untersucht" werden können
// und einen Texthinweis liefern
public interface ICheckable
{
// Methode, die beim Untersuchen eines Objekts aufgerufen wird
void TellAbout();
}
Erstelle nun ein Skript, das dieses Interface verwendet. Lege ein neues MonoBehaviour-Skript an und nenne es CheckableItem.
using UnityEngine;
// Dieses Skript verleiht einem Objekt die Fähigkeit,
// beim Untersuchen eine Beschreibung anzuzeigen
public class CheckableItem : MonoBehaviour, ICheckable
{
// Item-Daten (ScriptableObject)
public ItemData data;
// Implementierung des ICheckable-Interfaces
public void TellAbout()
{
// Wenn keine Daten zugewiesen sind, nichts tun
if (data == null) return;
// Übergibt die Daten an das UI zur Anzeige des Textes
ItemDescriptionUI.Instance.Show(data);
}
}
Jedes Objekt, das die Komponente
CheckableItem besitzt, gilt nun als untersuchbar.
Später prüfen wir lediglich, ob das Interface
ICheckable vorhanden ist, und rufen
TellAbout() auf, ohne etwas über die interne
Implementierung wissen zu müssen.
Nun fügen wir die Spielobjekte zur Szene hinzu. Wir beginnen am Ende der Kette — mit der Tür.
Ziehe den Tür-Sprite in die Szene. Unity erstellt automatisch ein neues GameObject. Benenne es in Door um.
Damit das Objekt anklickbar ist und von der Physik erkannt wird, füge eine Box Collider 2D-Komponente hinzu.
Füge anschließend die Komponente CheckableItem hinzu. Weise im Feld Data dieses Skripts das ScriptableObject Door zu.
Ziehe den Sprite der Code-Notiz in die Szene und platziere ihn so, dass er später vom Safe-Sprite überdeckt wird.
Passe die Größe relativ zur Tür an (in unserem Beispiel: Scale X = 0.5 und Scale Y = 0.5). Benenne das Objekt in Code um.
Füge die Komponenten Box Collider 2D und CheckableItem hinzu. Weise im Feld Data das ScriptableObject Code zu.
Deaktiviere nun das Objekt in der Szene (Checkbox neben dem Objektnamen im Inspector). Die Notiz wird hinter dem Safe verborgen und „wartet“, bis wir sie per Logik aktivieren.
Nun fügen wir den Safe hinzu, der die Code-Notiz „enthält“. Ziehe den Sprite des leeren Safes in die Szene und benenne das Objekt in Safe um.
Verschiebe den Safe an die Position der Code-Notiz. Zur besseren Ausrichtung kannst du das Objekt Code kurzzeitig aktivieren und anschließend wieder deaktivieren.
Passe die Größe des Safes an (in unserem Beispiel: Scale X = 0.5, Scale Y = 0.5) und setze Order in Layer auf 5, damit der Safe über der Notiz gerendert wird.
Füge dem Objekt Safe die Komponenten Box Collider 2D und CheckableItem hinzu. Weise im Feld Data das ScriptableObject Safe zu.
Füge nun den Sprite der Safe-Tür zur Szene hinzu. Benenne das Objekt SafeDoor und setze Order in Layer auf 10, damit die Tür über dem Safe-Körper gerendert wird.
Platziere die Tür über dem Safe, passe ihre Größe an und mache sie anschließend zu einem Kindobjekt des Safes, indem du SafeDoor in der Hierarchie auf Safe ziehst.
Nun fügen wir den Schlüssel zum Öffnen des Safes hinzu. Er wird hinter dem Bild verborgen sein.
Ziehe den Schlüssel-Sprite in die Szene und platziere ihn an der Stelle, an der sich später das Bild befindet. Benenne das Objekt in Key um.
Füge die Komponenten Box Collider 2D und CheckableItem hinzu. Weise im Feld Data das ScriptableObject Key zu.
Deaktiviere anschließend das Objekt in der Szene (Checkbox neben dem Namen im Inspector). Der Schlüssel bleibt verborgen, bis wir ihn durch Logik freigeben.
Ziehe den Bild-Sprite in die Szene und benenne das Objekt in Picture um.
Setze den Wert Order in Layer auf 5, damit das Bild über dem Schlüssel gerendert wird. Verschiebe das Bild an die Position, an der der Schlüssel verborgen ist (bei Bedarf kannst du das Objekt Key kurzzeitig aktivieren und danach wieder deaktivieren).
Füge die Komponenten Box Collider 2D und CheckableItem hinzu. Weise im Feld Data das ScriptableObject Picture zu.
Ziehe den Scheren-Sprite in die Szene und platziere ihn an einer beliebigen, gut erreichbaren Stelle. Benenne das Objekt in Scissors um.
Füge die Komponenten Box Collider 2D und CheckableItem hinzu. Weise im Feld Data das ScriptableObject Scissors zu.
Nun erstellen wir ein Inventar zur Speicherung von Items. Beachte dabei, dass wir nicht die eigentlichen GameObjects speichern. Stattdessen enthält das Inventar Datencontainer — ScriptableObject ItemData.
Zur Speicherung der Items verwenden wir eine dynamische Liste. Das Inventar selbst wird statisch umgesetzt, sodass es direkt aus jedem Skript heraus erreichbar ist.
Zusätzlich nutzen wir Events. Ein Event ermöglicht es, andere Skripte darüber zu informieren, dass etwas passiert ist (zum Beispiel, dass ein Item dem Inventar hinzugefügt wurde). Abonnierte Objekte können darauf jeweils auf ihre eigene Weise reagieren.
Erstelle ein neues MonoBehaviour-Skript und nenne es Inventory.
using System;
using System.Collections.Generic;
using UnityEngine;
// Inventar zur Speicherung von Item-Daten
public class Inventory : MonoBehaviour
{
// Statische Referenz auf das Inventar (Singleton)
public static Inventory Instance;
// Aktuell ausgewähltes Item
public ItemData SelectedItem { get; private set; }
// Liste der Items im Inventar
public List<ItemData> items = new();
// Event, das beim Hinzufügen eines Items ausgelöst wird
public event Action<ItemData> OnItemAdded;
// Event, das bei Änderung des ausgewählten Items ausgelöst wird
public event Action<ItemData> OnSelectionChanged;
private void Awake()
{
// Sicherstellen, dass nur ein Inventar in der Szene existiert
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
}
// Fügt ein Item dem Inventar hinzu
public void Add(ItemData item)
{
if (item == null) return;
items.Add(item);
// Abonnenten über das neue Item informieren
OnItemAdded?.Invoke(item);
}
// Wählt ein Item im Inventar aus
public void Select(ItemData item)
{
SelectedItem = item;
// Abonnenten über die Änderung der Auswahl informieren
OnSelectionChanged?.Invoke(SelectedItem);
}
// Setzt die aktuelle Item-Auswahl zurück
public void ClearSelection()
{
SelectedItem = null;
// Abonnenten über die Änderung der Auswahl informieren
OnSelectionChanged?.Invoke(SelectedItem);
}
}
Füge der Szene ein leeres GameObject hinzu und nenne es Inventory. Füge diesem Objekt anschließend das Skript als Komponente hinzu.
Jetzt haben wir ein Inventar, aber bisher existiert es nur im Code und wird noch nicht auf dem Bildschirm angezeigt. Zur Visualisierung der Items verwenden wir UI-Buttons.
Dieser Ansatz ermöglicht es uns nicht nur, Inventar-Slots darzustellen, sondern auch Klicks auf diese zu verarbeiten. Jedes Item im Inventar wird durch einen eigenen Button repräsentiert.
Beim Hinzufügen eines neuen Items erzeugen wir einen UI-Button, der als visueller Inventar-Slot dient. Zunächst bereiten wir dafür ein Prefab vor.
Wechsle in den UI-Bearbeitungsmodus (Doppelklick auf das Canvas) und füge einen Button hinzu: UI (Canvas) → Button - TextMeshPro. Nenne ihn InventorySlot.
Zusammen mit dem Button wird automatisch ein untergeordnetes Textobjekt erstellt. Dieses benötigen wir nicht — du kannst es löschen oder den Text einfach leeren, da wir ausschließlich die Image-Komponente des Buttons verwenden.
Passe die Größe des Buttons so an, dass sie der Größe des Item-Icons im Inventar entspricht.
Erstelle nun ein Skript zur Verarbeitung der Button-Klicks. Lege ein neues MonoBehaviour-Skript an und nenne es InventorySlot.
using UnityEngine;
using UnityEngine.EventSystems;
// Skript für einen Inventar-UI-Slot
public class InventorySlot : MonoBehaviour, IPointerClickHandler
{
// Item-Daten, die diesem Slot zugeordnet sind
private ItemData item;
// Initialisierung des Slots mit Item-Daten
public void Init(ItemData data)
{
item = data;
}
// Verarbeitung von Klicks auf den UI-Button
public void OnPointerClick(PointerEventData eventData)
{
// Linksklick — Item-Beschreibung anzeigen
if (eventData.button == PointerEventData.InputButton.Left)
{
ItemDescriptionUI.Instance.Show(item);
}
// Rechtsklick — Item auswählen oder Auswahl aufheben
else if (eventData.button == PointerEventData.InputButton.Right)
{
if (Inventory.Instance.SelectedItem == item)
Inventory.Instance.ClearSelection();
else
Inventory.Instance.Select(item);
}
}
}
In diesem Skript zeigt ein Linksklick auf den Button den Item-Hinweis an, während ein Rechtsklick das Item auswählt oder die Auswahl aufhebt, falls es bereits ausgewählt ist.
Füge dieses Skript dem Button InventorySlot hinzu und erstelle anschließend ein Prefab, indem du den Button aus der Hierarchie in den Ordner Assets ziehst. Danach kann der Button aus der Szene entfernt werden — das benötigte Prefab ist nun gespeichert.
Um die Buttons nicht manuell positionieren zu müssen, verwenden wir die UI-Komponente Horizontal Layout Group. Diese Komponente ordnet Buttons automatisch innerhalb eines definierten Bereichs an.
Füge dem UI ein neues leeres GameObject hinzu und nenne es InventoryBar. Aktiviere im RectTransform den Modus Stretch und passe Position sowie Größe an.
Setze die Höhe des Containers etwas größer als die Höhe eines Buttons und wähle die Breite so, dass mindestens 4 oder mehr Inventar-Slots hineinpassen.
Füge die Komponente Horizontal Layout Group hinzu und setze die folgenden Parameter:
Die Buttons werden nun automatisch innerhalb dieses Bereichs gruppiert, ohne dass Positionen manuell angepasst werden müssen.
Als Nächstes fügen wir ein Skript hinzu, das die Inventar-Logik aus dem Code mit der visuellen Darstellung verbindet.
Erstelle ein neues MonoBehaviour-Skript und nenne es InventoryUI.
using UnityEngine;
using UnityEngine.UI;
// Skript, das das Inventar mit dem UI verbindet
public class InventoryUI : MonoBehaviour
{
// Container, in dem die Inventar-Slots hinzugefügt werden
public Transform slotsParent;
// Prefab des Inventar-Slot-Buttons
public GameObject slotPrefab;
private void Start()
{
// Abonnieren des Events beim Hinzufügen eines Items
Inventory.Instance.OnItemAdded += AddSlot;
}
private void OnDisable()
{
// Abmelden vom Event, wenn das Objekt deaktiviert wird
if (Inventory.Instance != null)
Inventory.Instance.OnItemAdded -= AddSlot;
}
// Erstellt einen neuen UI-Slot
private void AddSlot(ItemData item)
{
// Slot-Button innerhalb des Containers instanziieren
GameObject slot = Instantiate(slotPrefab, slotsParent);
// Item-Icon zuweisen
Image icon = slot.GetComponentInChildren<Image>();
if (icon != null)
icon.sprite = item.icon;
// Item-Daten an die Slot-Logik übergeben
InventorySlot slotLogic = slot.GetComponent<InventorySlot>();
slotLogic.Init(item);
}
}
Füge dieses Skript dem Objekt InventoryBar hinzu. Weise im Feld Slots Parent die InventoryBar selbst zu und ziehe im Feld Slot Prefab das Prefab des InventorySlot-Buttons hinein.
Fügen wir nun ein UI-Icon für das ausgewählte Item hinzu, das dem Mauszeiger folgt. Ist kein Item ausgewählt, wird das Icon ausgeblendet.
Doppelklicke auf das Canvas, um in den UI-Bearbeitungsmodus zu wechseln. Füge anschließend dem Canvas ein neues Objekt hinzu: UI (Canvas) → Image. Benenne das erstellte UI-Objekt Image in IconSelected um und passe die Bildgröße an das spätere Item-Icon an.
Erstelle ein neues MonoBehaviour-Skript mit dem Namen SelectedCursorUI und füge dieses Skript dem erstellten UI-Objekt Image hinzu.
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
// UI-Icon des ausgewählten Items, das dem Cursor folgt
public class SelectedCursorUI : MonoBehaviour
{
private Image image;
private RectTransform rect;
private void Awake()
{
// Komponenten des UI-Objekts holen
image = GetComponent<Image>();
rect = GetComponent<RectTransform>();
// Das Icon soll keine Mausklicks blockieren
image.raycastTarget = false;
// Icon standardmäßig ausblenden
image.enabled = false;
}
private void Start()
{
// Abonnieren des Events bei Änderung der Item-Auswahl
if (Inventory.Instance != null)
Inventory.Instance.OnSelectionChanged += HandleSelectionChanged;
}
private void OnDisable()
{
// Vom Event abmelden, wenn das Objekt deaktiviert wird
if (Inventory.Instance != null)
Inventory.Instance.OnSelectionChanged -= HandleSelectionChanged;
}
// Aktualisiert das Icon, wenn sich das ausgewählte Item ändert
private void HandleSelectionChanged(ItemData selected)
{
if (selected == null)
{
image.enabled = false;
return;
}
image.sprite = selected.icon;
image.enabled = true;
}
private void Update()
{
// Position nur aktualisieren, wenn das Icon sichtbar ist
if (!image.enabled) return;
// Icon dem Cursor folgen lassen (kleiner Versatz für bessere Sichtbarkeit)
rect.position = Mouse.current.position.ReadValue() + new Vector2(24, -24);
}
}
💡 Die Position des Icons wird in Update() aktualisiert,
da sich die Cursor-Position jedes Frame ändert. Das Sprite selbst
sowie die Sichtbarkeit werden hingegen über das Event zur Item-Auswahl
gesteuert.
In unserer Szene gibt es zwei Arten von interaktiven Objekten.
Der erste Typ umfasst Items, die in das Inventar aufgenommen und als „Schlüssel“ für Aktionen verwendet werden können (zum Beispiel Schere oder Schlüssel).
Der zweite Typ umfasst Objekte, die in der Szene verbleiben und darauf warten, dass der Spieler das passende Item auf sie anwendet (Bild, Safe, Tür).
Um den Typ eines Objekts zu bestimmen und seine Logik auszuführen, verwenden wir erneut Interfaces. Dadurch vermeiden wir eine Bindung an konkrete Skripte und prüfen stattdessen nur auf das benötigte Verhalten.
Wir erstellen zwei Interfaces: eines zum Aufnehmen von Items und eines zum Verwenden von Items.
Erstelle ein leeres C#-Skript und nenne es IPickable.
using UnityEngine;
// Interface für Objekte, die aufgenommen werden können
public interface IPickable
{
// Methode wird aufgerufen, wenn der Spieler das Objekt aufnimmt
void TryPickUp();
}
Erstelle nun ein weiteres leeres C#-Skript und nenne es IUsable.
using UnityEngine;
// Interface für Objekte, die verwendet werden können
// mit einem Item aus dem Inventar
public interface IUsable
{
// otherData — Daten des Items, das der Spieler anwenden möchte
void TryUse(ItemData otherData);
}
Mit diesem Ansatz können wir leicht bestimmen, was ein Objekt kann: aufgenommen werden, verwendet werden oder sogar beides — einfach durch das Hinzufügen der entsprechenden Interfaces.
Zunächst definieren wir die Items, die aufgenommen und beim Verwenden dem Inventar hinzugefügt werden können. Dafür haben wir bereits das Interface IPickable erstellt.
Erstelle ein neues MonoBehaviour-Skript und nenne es PickableItem.
using UnityEngine;
// Skript für Items, die aufgenommen werden können
public class PickableItem : MonoBehaviour, IPickable
{
// Methode des IPickable-Interfaces
public void TryPickUp()
{
// Item-Daten aus der CheckableItem-Komponente holen
CheckableItem checkable = GetComponent<CheckableItem>();
if (checkable == null) return;
ItemData currentData = checkable.data;
if (currentData == null) return;
// Item dem Inventar hinzufügen und Objekt aus der Szene entfernen
Inventory.Instance.Add(currentData);
Destroy(gameObject);
}
}
Das Vorhandensein des IPickable-Interfaces zeigt an, dass ein Objekt aufgenommen werden kann. Dank des Interfaces können wir die Aufnahmelogik aufrufen, ohne wissen zu müssen, in welcher konkreten Komponente sie implementiert ist.
Insgesamt gibt es in unserem Puzzle drei aufnehmbare Items: Scissors, Key und Code.
Falls du dich noch im UI-Bearbeitungsmodus befindest, doppelklicke auf die Main Camera, um zur normalen Szenenansicht zurückzukehren. Wähle anschließend das Objekt Scissors aus und füge ihm die Komponente PickableItem hinzu.
Füge die Komponente PickableItem auf die gleiche Weise auch den Objekten Key und Code hinzu.
An diesem Punkt bleiben in der Szene drei Objekte übrig, auf die wir andere Items anwenden werden. Wir verwenden das gemeinsame IUsable-Interface, um „Schlüssel“ anzuwenden, wobei jedes Objekt seine Logik auf eigene Weise verarbeitet.
Beginnen wir mit dem Bild. Es reagiert auf die Schere: Wird die Schere verwendet, wird das Bild „zerschnitten“, sein Sprite wird geändert, der Collider deaktiviert und der versteckte Schlüssel wird zugänglich.
Erstelle ein neues MonoBehaviour-Skript und nenne es PictureInteraction.
using UnityEngine;
// Logik für die Interaktion mit dem Bild
public class PictureInteraction : MonoBehaviour, IUsable
{
// Daten des Items, das als „Schlüssel“ dient (Schere)
public ItemData scissorsData;
// Sprite des zerschnittenen Bildes
public Sprite brokenPicture;
// Objekt, das aktiviert werden soll (der Schlüssel hinter dem Bild)
public GameObject itemToEnable;
// Flag, damit die Logik nur einmal ausgeführt wird
private bool isBroken = false;
// Methode des IUsable-Interfaces
public void TryUse(ItemData otherData)
{
// Wenn das Bild bereits zerschnitten ist, nichts tun
if (isBroken) return;
// Prüfen, ob das richtige Item verwendet wurde
if (otherData != null && otherData == scissorsData)
{
// Sprite des Bildes ändern
var sr = GetComponent<SpriteRenderer>();
if (sr != null)
sr.sprite = brokenPicture;
// Collider deaktivieren, damit das Bild nicht mehr reagiert
var col = GetComponent<Collider2D>();
if (col != null)
col.enabled = false;
// Verstecktes Item (Key) aktivieren
if (itemToEnable != null)
itemToEnable.SetActive(true);
// Aktion als abgeschlossen markieren
isBroken = true;
}
}
}
Füge dieses Skript dem Objekt Picture hinzu.
Konfiguriere im Inspector die folgenden Felder:
Jetzt konfigurieren wir den Safe. Er wird mit einem Schlüssel geöffnet: Nach der Verwendung des Schlüssels verschwindet die Safetür und der versteckte Zettel mit dem Code wird aktiviert.
Erstelle ein neues MonoBehaviour-Skript und nenne es SafeInteractions.
using UnityEngine;
// Logik für die Interaktion mit dem Safe
public class SafeInteractions : MonoBehaviour, IUsable
{
// Daten des Gegenstands, der den Safe öffnet (Schlüssel)
public ItemData keyData;
// Safetür, die ausgeblendet werden soll
public GameObject safeDoor;
// Gegenstand, der nach dem Öffnen erscheint (Code-Zettel)
public GameObject itemToEnable;
// Flag, damit der Safe nur einmal geöffnet werden kann
private bool isOpened = false;
// Methode des IUsable-Interfaces
public void TryUse(ItemData otherData)
{
if (isOpened) return;
// Prüfen, ob der richtige Gegenstand verwendet wurde
if (otherData != null && otherData == keyData)
{
// Safetür ausblenden
if (safeDoor != null)
safeDoor.SetActive(false);
// Versteckten Gegenstand anzeigen (Code)
if (itemToEnable != null)
itemToEnable.SetActive(true);
isOpened = true;
}
}
}
Füge das Skript SafeInteractions dem Objekt Safe hinzu. Konfiguriere anschließend im Inspector die folgenden Felder:
Erstellen wir ein einfaches Finale: Nachdem die Tür geöffnet wurde, lösen wir einen kleinen visuellen Effekt mit der Kamera aus, deaktivieren vorübergehend die Interaktion mit Objekten und starten die Szene nach einigen Sekunden neu.
Erstelle ein neues MonoBehaviour-Skript und nenne es DoorInteraction.
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
// Finale Türlogik: Effekt + Szenen-Neustart
public class DoorInteraction : MonoBehaviour, IUsable
{
// Daten des Gegenstands, der zur Tür passt (Code)
public ItemData codeData;
// Kamera der Szene
public Camera mainCamera;
// Verzögerung vor dem Neustart der Szene
public float restartDelay = 5f;
// Rotationsgeschwindigkeit der Kamera
public float rotateSpeed = 30f;
private bool isFinished = false;
public void TryUse(ItemData otherData)
{
if (isFinished) return;
if (otherData != null && otherData == codeData)
{
// Kollider der Tür deaktivieren
var col = GetComponent<Collider2D>();
if (col != null)
col.enabled = false;
// Finalen Effekt starten
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
);
}
}
Füge das Skript DoorInteraction dem Objekt Door hinzu. Weise im Feld Code Data das ScriptableObject Code zu.
💡 Wir starten die Szene mit einer Verzögerung neu, damit der Spieler Zeit hat, das Ergebnis zu sehen und zu verstehen, dass das Quest abgeschlossen ist. Das wirkt deutlich besser als ein sofortiger Neustart.
Als letzten Feinschliff erwecken wir die gesamte Szene zum Leben. Dazu erstellen wir ein Skript, das die Mauseingaben verarbeitet:
Erstelle ein neues MonoBehaviour-Skript und nenne es ClickHandler.
using UnityEngine;
using UnityEngine.InputSystem;
// Dieses Skript ist der zentrale Eingabe-Handler.
// Es existiert einmal in der Szene und reagiert auf Mausklicks,
// während die Spielobjekte selbst über Interfaces entscheiden,
// wie sie auf diese Klicks reagieren.
public class ClickHandler : MonoBehaviour
{
private void Update()
{
// Falls die Maus aus irgendeinem Grund nicht verfügbar ist,
// machen wir nichts
if (Mouse.current == null) return;
// ===== LINKSKLICK =====
// Wird zum Untersuchen von Objekten verwendet
// (Hinweise / Beschreibungen)
if (Mouse.current.leftButton.wasPressedThisFrame)
{
// Mausposition in Weltkoordinaten
Vector2 mouseWorldPos =
Camera.main.ScreenToWorldPoint(
Mouse.current.position.ReadValue()
);
// Raycast "in den Punkt", um einen Collider zu prüfen
RaycastHit2D hit =
Physics2D.Raycast(mouseWorldPos, Vector2.zero);
// Wenn sich ein Objekt unter dem Cursor befindet
// und es das ICheckable-Interface unterstützt —
// zeigen wir dessen Beschreibung an
if (hit.collider != null &&
hit.collider.TryGetComponent<ICheckable>(
out var checkable))
{
checkable.TellAbout();
}
}
// ===== RECHTSKLICK =====
// Kontextaktion: aufnehmen, auswählen oder benutzen
if (Mouse.current.rightButton.wasPressedThisFrame)
{
// Mausposition in Weltkoordinaten
Vector2 mouseWorldPos =
Camera.main.ScreenToWorldPoint(
Mouse.current.position.ReadValue()
);
// Prüfen, ob wir ein Objekt in der Szene getroffen haben
RaycastHit2D hit =
Physics2D.Raycast(mouseWorldPos, Vector2.zero);
// Wenn nichts getroffen wurde — abbrechen
if (hit.collider == null) return;
// 1. Prüfen, ob das Objekt aufgenommen werden kann
// (Schere, Schlüssel, Code-Notiz)
if (hit.collider.TryGetComponent<IPickable>(
out var pickable))
{
pickable.TryPickUp();
// Wichtig: Wenn der Gegenstand aufgenommen wurde,
// wird dieser Klick nicht weiter verarbeitet
return;
}
// 2. Falls das Objekt nicht aufgenommen werden kann,
// prüfen wir, ob der ausgewählte Inventargegenstand
// verwendet werden kann
if (hit.collider.TryGetComponent<IUsable>(
out var usable))
{
// Ein Gegenstand kann nur verwendet werden,
// wenn im Inventar einer ausgewählt ist
if (Inventory.Instance.SelectedItem != null)
{
usable.TryUse(
Inventory.Instance.SelectedItem
);
}
}
}
}
}
Füge nun ein leeres GameObject zur Szene hinzu und nenne es ClickHandler. Ergänze dieses Objekt um das Skript als Komponente.
Damit ist die Szene vollständig abgeschlossen. Starte das Spiel und versuche, die komplette logische Kette zu durchlaufen — von der Schere bis zur finalen Tür 🙂