Unser Freund, das Inventarsystem
Jeder kennt es... Fast jedes Spiel braucht es... Das gute alte Inventarsystem. :) Dazu gehört nicht nur das Umsortieren von Items, sondern auch das Ausrüsten von gefundenen Waffen und deren korrekte Zuordnung. Zudem sollte alles via Drag and Drop veränderbar sein.Führt man sich nun ein Inventarsystem vor Augen kann man es (wenn man es für sich selbst betrachtet) mE. auf folgende Basisfunktionen beschränken:
- Umsortieren von Items per Drag and Drop
- Ausrüsten von Items, anhand eines vorher definierten Modelles
- Anzeigen einer Itembeschreibung durch Hovern
- Dynamische Anpassung der Itemslots, bei Größenveränderung
Ein Inventarsystem mit Unity UI?
Der Unity Assetstore bietet bereits eine große Menge an Inventarsystemen, welche mit allen erdenklichen Funktionen ausgestattet sind (Crafting, mehrere Taschen, dynamische Größenanpassung, etc...). Allen voran sei hier der InventoryMaster (InventoryMaster-AssetstoreLink) erwähnt, welcher aber mit Problemen in Unity5 zu kämpfen hat. Hier sind einige Änderungen notwendig, um dieses Asset ohne Probleme einsetzen zu können.Für alle Interessierten habe ich mal die Basics für ein solches Inventarsystem in einem Blogbeitrag zusammengefasst.
Legen wir los :)
... und erstellen ein neues Unity Projekt. Ein normales 3D Projekt ohne zusatzl. Assets genügt. Danach kümmern uns erstmal um eine gute Ordnerstruktur. Diese umfasst in diesem konkreten Fall 4 Ordner:- Images
- Prefabs
- Scenes
- Scripts
In der Hierarchy erstellen wir ein neues Canvas Objekt (Rechtsklick - UI - Canvas) und nennen es MainCanvas. Unity fügt uns hier automatisch ein EventSystem Objekt ein, welches wir hier nicht weiter beachten werden.
Zuerst stellen wir uns die Frage, wie unser Inventarsystem aussehen soll. Ich habe mich für 3 Panele entschieden. Blau sind alle unsere Items, Grün ist der Ausrüstungsteil und Weiß beinhaltet die Beschreibung der Items.
Für diesen Zweck erstellen wir 3 Panels als Child Objekte des MainCanvas (Rechtsklick - UI - Panel) und nennen sie InventoryParentPanel, EquipmentParentPanel und DescriptionPanel. Durch geschickte Einstellungen von Anchor teilen wir die UI in 3 unterschiedliche Bereiche auf. Der nachfolgende Screenshot zeigt wie so etwas aussehen könnte.
InventarPanel
Im Inventarpanel werden sich später alle unsere Items befinden. Damit wir unser Inventarpanel besser einteilen können, wurde im vorherigen Schritt bereits das InventoryParentPanel erstellt. Als Child Objekte werden nun ein Text (Rechtsklick - UI - Text), welcher später als Überschrift dient und ein weiteres Panel (Rechtsklick - UI - Panel) erstellt. Die Namen dieser Objekte sind nicht so wichtig. Bei mir heißen sie InventoryPanelLabel und InventoryPanel. Die Einstellungen sind im nächsten Screenshot ersichtlich.
Wichtig für ein einfaches Handling der Items ist, dass dem InventoryPanel die GridLayoutGroup hinzugefügt wird. Diese Komponente kümmert sich darum, dass alle direkten Kindobjekte in Form eines Grids organisiert werden. Je nach Einstellungen kann hier die Anordnung und das Aussehen des Inventares verändert werden.
Nun wird es Zeit, die ersten Items in unser Inventar aufzunehmen. Dazu fügen wir dem InventoryPanel 2 Image Child Objekte hinzu (Rechtsklick - UI - Image) und nennen sie ItemSlot und ItemSlot (1). ItemSlot erhält nun ein Image Child Objekt welches wir einfach nur Item nennen.
Um alles optisch etwas aufzuhübschen habe ich Grafik erstellt, welche den Rahmen der einzelnen Slots darstellt. Für die Items habe ich mir aus dem Unity Assetstore Inventar Icons heruntergeladen (RpgInventory-AssetstoreLink). Den ImageSlots wird nun als Source Image das Rahmen Bild eingestellt. Dem Item wird bspw. der Apfel eingestellt und eine CanvasGroup Komponente hinzugefügt. Diese wird später wichtig, um Drag&Drop korrekt umsetzen zu können. Der Screenshot zeigt, wie unsere Umsetzung bis jetzt aussieht:
Bevor wir nun direkt weitermachen,
überlegen wir uns, wann ein Item in einem Slot abgelegt werden kann und
wann ich erkenne, ob dieser Slot bereits belegt ist. An den beiden bereits erstellten ItemSlots der Hierarchy und der Vorschau kann man diese Bedingungen erkennen. Hat ein ItemSlot keine Child Objekte (Item), ist dieser leer. Ist ein Child Objekt (Item) vorhanden, kann hier kein Item abgelegt werden.
Nach dieser Erkenntnis ist es jetzt soweit und wir kommen an unsere erste selbst geschriebene Komponente. Man könnte Drag&Drop auch mit sog. EventTriggern im Inspector steuern, jedoch ist eine Lösung mittels C# mM. wesentlich eleganter, einfacher und verständlicher. Also legen wir im Ordner Scripts ein neues C# Skript and und nennen es DropItemHandler.
Durch einen Doppelklick öffnen wir Visual Studio (oder wahlweise MonoDevelop) und implementieren folgenden Code:
using UnityEngine;
using UnityEngine.EventSystems;
public class DropItemHandler : MonoBehaviour, IDropHandler {
public virtual void OnDrop(PointerEventData eventData) {
// check if there is already an item at the slot
if (transform.childCount == 0) {
// set new item position
eventData.pointerDrag.transform.SetParent(this.transform);
eventData.pointerDrag.transform.localPosition = Vector3.zero;
Debug.Log(eventData.pointerDrag.name + " dropped at " + this.gameObject.name);
}
}
}
Der DropItemHandler wird jedem ItemSlot hinzugefügt. Somit kann für jeden Slot überprüft werden, ob bereits ein Item vorhanden ist oder nicht. Als nächstes kümmern wir uns um das Item, welches bewegt werden muss. Für diesen Zweck brauchen wir ein weiteres Skript, den DragItemHandler, welcher wieder im Scripts Ordner angelegt wird. Zusätzlich wollen wir im EquipmentPanel später auch verschiedene Ausrüstungsgegenstände unterscheiden können, weshalb wir im Scripts Ordner einen neuen Unterordner Enums anlegen, welcher ein Datenmodel (ItemCategory) beinhaltet. Ich habe mich in diesem Bsp. für 6 verschiedene Kategorien entschieden, wobei diese je nach Anwendungszweck variieren können. Das Flags Attribut sorgt dafür, dass Einstellungen im Inspektor, bei einer Aktualisierung nicht verloren gehen. Würde ich zB. dem Enum einen Eintrag hinzufügen und das Flags Attribut wäre nicht vorhanden, würden alle Komponenten welche dieses Enum im Inspektor verwenden den ersten Eintrag None anzeigen.
using System;
public class ItemCategoryModel {
[Flags]
public enum ItemCategory {
None = 0,
Head = 10,
Body = 20,
Hand = 30,
Hip = 50,
Feet = 60,
Special = 70
}
}
Zuvor müssen wir uns aber noch mit einer kleinen Nebensache beschäftigen. Unity rendert die UI Elemente so, wie sie in der Hierarchy angeordnet sind. D.h. die Reihenfolge der Elemente entscheidet, ob UI Elemente oberhalb oder unterhalb von anderen UI Elementen liegen. Bricht man diese Tatsache auf unsere bereits bestehenden ItemSlots herab, so wird zuerst der erste ItemSlot gerendert, danach das Item und zuletzt der zweite ItemSlot. Möchte man nun den Apfel auf den zweiten Itemslot legen, wird während des Drag Vorganges der Rahmen des zweiten ItemSlots über dem Apfel gerendert. Um diesen Effekt zu vermeiden, müssen wir dafür sorgen, dass unser Apfel während der Bewegung immer zuletzt gerendert wird (also sich ganz unten in der Hierarchy befindet).
Aus diesem Grund legen wir uns einen Platzhalter an. Dieses, von mir als DragPanel bezeichnete, Objekt ist das letzte Child Objekt des MainCanvas.
Implementieren wir als erstes den DragItemHandler:
using UnityEngine;
using UnityEngine.EventSystems;
public class DragItemHandler : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
[SerializeField]
private Transform dragPanel;
private Transform _startParent;
private CanvasGroup _canvasGroup;
void Awake() {
_canvasGroup = GetComponent<CanvasGroup>();
}
public void OnBeginDrag(PointerEventData eventData) {
// save initial position in hierarchy
_startParent = transform.parent;
// nessecary to activate OnDrop after drag ended
_canvasGroup.blocksRaycasts = false;
// set item as last silbling, to draw it always on top of UI
transform.SetParent(dragPanel);
transform.SetAsLastSibling();
}
public void OnDrag(PointerEventData eventData) {
this.transform.position = Input.mousePosition;
}
public void OnEndDrag(PointerEventData eventData) {
// reactivate to make item dragable again
_canvasGroup.blocksRaycasts = true;
// check if item was dropped
if (transform.parent == dragPanel) {
transform.SetParent(_startParent);
transform.localPosition = Vector3.zero;
}
}
}
Diese Komponente wird nun für jedes Item hinzugefügt und konfiguriert. Sie kümmert sich darum das Objekt bewegbar zu machen, der Position des Mauszeigers zu folgen und bei Bedarf (sollte das Objekt nicht korrekt positioniert werden) es wieder in die Ausgangsposition zurückzusetzen.
Wichtig ist, dass die Referenz auf DragPanel hinzugefügt wird, ansonsten wird das Item während des Drag Vorganges nicht korrekt angezeigt. Im Play-Mode kann Drag&Drop nun das erste Mal ausprobiert werden. :) Der nachfolgende Screenshot zeigt wie eine mögliche Realisierung aussehen kann.
Das sieht doch schon ganz gut aus. :) Natürlich geben wir uns damit noch nicht zufrieden. Immerhin fehlt noch einiges. Bevor wir uns aber neuen Parts zuwenden kümmern wir uns darum, dass Informationen welche zu dem jeweiligen Item gehören im DescriptionPanel angezeigt werden, sobald sich der Mauszeiger über einem Item befindet.
Für eine gute Darstellung modifizieren wir erstmal das DescriptionPanel und fügen 3 Texte (Rechtsklick - UI - Text) hinzu, welche wir DescriptionHeadText, DescriptionItemText und DescriptionStatsText nennen.
Je nach Anwendung und Geschmack können diese 3 Texte nun im DescriptionPanel platziert und mit Testwerten versehen werden. Ich habe das folgendermaßen umgesetzt:
Nun brauchen wir noch eine Komponente, welche sich um die Informationen des Items und um die Anzeige im DescriptionPanel kümmert. Dazu erstellen wir im Scripts Ordner ein neues Skript, den HoverItemHandler. Der Code dieser Komponente sieht folgendermaßen aus:
using UnityEngine;
using UnityEngine.EventSystems;
public class HoverItemHandler : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler {
public ItemCategoryModel.ItemCategory ItemCategory {
get { return _itemCategory; }
}
[SerializeField]
private string _itemName;
[SerializeField]
private ItemCategoryModel.ItemCategory _itemCategory;
[SerializeField]
[TextArea]
private string _itemStatistics;
[SerializeField]
[TextArea]
private string _itemDescription;
public void OnPointerEnter(PointerEventData eventData) {
DescriptionHandler.DescriptionHead.text =
string.Format("{0} - {1}", _itemName, _itemCategory.ToString());
DescriptionHandler.DescriptionItemStats.text = _itemStatistics;
DescriptionHandler.DescriptionItemText.text = _itemDescription;
}
public void OnPointerExit(PointerEventData eventData) {
DescriptionHandler.DescriptionHead.text = "";
DescriptionHandler.DescriptionItemStats.text = "";
DescriptionHandler.DescriptionItemText.text = "";
}
}
Zusätzlich benötigen wir noch einen DesciptionHandler, welcher sich darum kümmert, dass die Texte auch angezeigt werden. Dieser DescriptionHandler kann auf verschiedene Arten implementiert werden. Ich habe der Einfachheit halber ein Monobehaviour mit statischen Variablen und einem Singleton ausgewählt (eine mögl. Alternative wäre zB. Dependency Injection) und wie folgt implementiert:
using UnityEngine;
using UnityEngine.UI;
public class DescriptionHandler : MonoBehaviour {
public static Text DescriptionHead;
public static Text DescriptionItemText;
public static Text DescriptionItemStats;
private static DescriptionHandler _instance;
void Awake() {
_instance = GameObject.FindObjectOfType<DescriptionHandler>();
if (_instance == null) {
GameObject container = new GameObject("DescriptionPanel");
_instance = container.AddComponent<DescriptionHandler>();
}
// this is just for testing the item description.
// there are a lot of better ways implementing this.
DescriptionHead = _instance.transform.GetChild(0).GetComponent<Text>();
DescriptionItemText = _instance.transform.GetChild(1).GetComponent<Text>();
DescriptionItemStats = _instance.transform.GetChild(2).GetComponent<Text>();
// reset values
DescriptionHandler.DescriptionHead.text = "";
DescriptionHandler.DescriptionItemStats.text = "";
DescriptionHandler.DescriptionItemText.text = "";
}
}
Der DescriptionHandler wird nun dem DescriptionPanel hinzugefügt, der HoverItemHandler jedem Item. Wie nun die Iteminformationen nun gespeichert und geladen werden, bleibt jedem selbst überlassen. Ich habe bei 3 Items die Informationen einfach per Hand im Inspektor eingetragen. Jetzt ist ein guter Zeitpunkt um aus dem ItemSlot und dem Item Prefabs zu generieren und diese im Prefab Ordner abzulegen. Dadurch kann ich später weitere Slots und Items einfach per Code erzeugen. Am folgenden Bsp. sieht man, wie so etwas mit mehreren Items und angezeigter Beschreibung aussehen kann.
Nun haben wir Items, Beschreibungen und die Möglichkeit Items per Drag and Drop zu verschieben. Fehlt nur noch das EquipmentPanel, um Items ausrüsten zu können. Dazu fügen wir dem EquipmentParentPanel einen Text (Rechtsklick - UI - Text) und ein weiteres Panel, das EquipmentPanel, (Rechtsklick - UI - Panel) als Child Objekte hinzu. Ferner erhält das EquipmentPanel ein Hintergrundbild (Rechtsklick - UI - Image) um die Position und Verwendung der einzelnen Ausrüstungsgegenstände zu verdeutlichen.
Nun können wir, ähnlich wie beim InventoryPanel ItemSlots hinzufügen. Die Unterschiede zu den ItemSlots zuvor sind die Position, welche an das Hintergrundbild angepasst wurde, die Farbe (in diesem Fall grün, zur besseren Unterscheidung) und eine Kontrolle welche Items wo abgelegt werden können. Warum sollte man einen Helm bspw. am Fuß tragen? ;) Für die Kontrolle wird der bereits vorhandene DropItemHandler durch den DropEquipmentItemHandler ersetzt.
using UnityEngine;
using UnityEngine.EventSystems;
public class DropEquipmentItemHandler : DropItemHandler {
[SerializeField]
private ItemCategoryModel.ItemCategory _itemCategory;
public override void OnDrop(PointerEventData eventData) {
// check if there is already an item at the slot and if the category is correct
if ((transform.childCount == 0) &&
(eventData.pointerDrag.GetComponent<HoverItemHandler >().ItemCategory == _itemCategory)) {
// set new item position
eventData.pointerDrag.transform.SetParent(this.transform);
eventData.pointerDrag.transform.localPosition = Vector3.zero;
Debug.Log(eventData.pointerDrag.name + " dropped at " + this.gameObject.name);
}
}
}
Der DropEquipmentItemHandler wird nun jedem EquipmentItemSlot hinzugefügt. Wird das Prefab für ItemSlot verwendet, muss die Komponente DropItemHandler natürlich vorher entfernt werden. Für jeden EquipmentItemSlot muss jetzt noch eingestellt werden, welche Items von welcher Equipment Kategorie abgelegt werden können. Bei sieht es folgendermaßen aus:
Somit ist unser Inventarsystem fertig und wenn wir nun alles korrekt eingestellt, Informationen für Items hinterlegt und die UI Elemente fertig poliert haben, könnten die Items wie folgend verwaltet werden :)
Somit ist unser Inventarsystem fertig und wenn wir nun alles korrekt eingestellt, Informationen für Items hinterlegt und die UI Elemente fertig poliert haben, könnten die Items wie folgend verwaltet werden :)









Keine Kommentare:
Kommentar veröffentlichen