Jump to content
Unity Insider Forum

Player Inventar mit Ui und Drag and Drop


Triky313

Recommended Posts

Hallo Leute,

aktuell arbeite ich an einem Inventar.

In meinem Spiel wird es viele verschiedene Items geben.

Ähnlich wie Diablo oder Borderlands mit zufälligen Eigenschaften.

Mein Problem liegt darin, dass ich zwei Listen führe und die eine oft mit der anderen nicht übereinstimmt. Mittlerweile bin ich auf Arrays umgestiegen, da ich da einfacher mit dem Index arbeiten konnte, dachte ich. Leider habe ich da auch noch nicht das erwünschte Ergebnis erzielen können.

Gibt es hier nicht eine bessere oder einfacherer Lösung? Aktuell fällt mir nur nichts elegantes ein, wenn ich ehrlich bin.

 

Hier mal meine aktuellen Skripte:

Inventar:

using UI;
using UnityEngine;

namespace Game.Player
{
    public class Inventory : MonoBehaviour
    {
        [ReadOnly] public UiInventory InventoryUi;
        private Item.Item[] _inventory;
        
        private void Start()
        {
            _inventory = GetComponent<Player>().Stats.Inventory;
            //Array.Resize(ref _player.Stats.Inventory, _player.Stats.InventoryCapacity);
        }
        
        public bool AddItem(Item.Item itemToAdd)
        {
            for (var i = 0; i < _inventory.Length; i++)
            {
                if (_inventory[i] == null)
                {
                    _inventory[i] = itemToAdd;
                    InventoryUi.SetSlot(i, itemToAdd);
                    return true;
                }
            }
            return false;
        }

        public void RemoveItem(Item.Item itemToRemove)
        {
            for (int i = 0; i < _inventory.Length; i++)
            {
                if (_inventory[i] == itemToRemove)
                {
                    _inventory[i] = null;
                    InventoryUi.SetSlot(i, null);
                    return;
                }
            }
        }
      
        public void SetInventoryUi(UiInventory inventoryUi)
        {
            InventoryUi = inventoryUi;

            if (InventoryUi != null)
                InventoryUi.SetInventoryUi(_inventory);
            else
                Debug.LogWarning("Player has no InventoryUi object reference");
        }
        
    }
}

 

Inventar Ui:

using System;
using System.Collections.Generic;
using Game.Item;
using UnityEngine;
using UnityEngine.UI;

namespace UI
{
    public class UiInventory : MonoBehaviour
    {
        public GameObject Ui;
        public List<UiItem> UiItems = new List<UiItem>();
        public GameObject SlotPrefab;
        public Transform SlotPanel;
        public GameObject SelectedItem;
        public GameObject ItemRemoveArea;
        public GameObject Tooltip;
        
        public void SetInventoryUi(Item[] items)
        {
            if(SelectedItem != null)
                SelectedItem.GetComponent<Image>().sprite = null;

            foreach (Transform slot in SlotPanel.transform)
            {
                Destroy(slot.gameObject);
            }

            UiItems.Clear();

            for (var i = 0; i < items.Length; i++)
            {
                var instance = Instantiate(SlotPrefab);
                instance.transform.SetParent(SlotPanel);
                instance.transform.localScale = Vector3.one;
                instance.transform.localPosition = new Vector3(0,0,0);
                if (SelectedItem != null)
                    instance.GetComponentInChildren<UiItem>().SelectedItem = SelectedItem.GetComponent<UiItem>();
                instance.GetComponentInChildren<UiItem>().Tooltip = Tooltip.GetComponent<Tooltip>();
                instance.GetComponentInChildren<UiItem>().Ui = Ui;
                instance.GetComponentInChildren<UiItem>().SlotIndexNumber = i;
                UiItems.Insert(i, instance.GetComponentInChildren<UiItem>());

                if (items[i] != null)
                    UiItems[i].SetItem(items[i]);
            }
        }
        
        public bool SetSlot(int index, Item item)
        {
            try
            {
                UiItems[index].SetItem(item);
                return true;
            }
            catch (Exception ex)
            {
                Debug.LogWarning(ex.Message);
            }
            return false;
        }

    }
}

 

Ui Item:

using Game.Item;
using Game.Player;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace UI
{
    public class UiItem : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler
    {
        // INFOS: https://medium.com/@yonem9
        public GameObject Ui;
        public UiItem SelectedItem;
        public Tooltip Tooltip;
        public bool IsItemDeleter;
        public GameObject ItemObjectPrefab;
        private Image _spriteImage;
        [ReadOnly] public int SlotIndexNumber;
        [ReadOnly] public Item Item;

        private void Awake()
        {
            if (!IsItemDeleter)
                _spriteImage = GetComponent<Image>();
            SetItem(null);
        }
        
        public void SetItem(Item item)
        {
            Item = item;
            if (Item != null)
            {
                _spriteImage.color = Color.white;
                _spriteImage.sprite = Resources.Load<Sprite>("Sprites/Items/" + Item.SpriteName);
            }
            else
            {
                if(!IsItemDeleter)
                    _spriteImage.color = Color.clear;
            }
        }

        public void OnPointerClick(PointerEventData eventData)
        {
            var inventory = Ui.GetComponent<UiController>().MainGameObject.GetComponent<MainGameController>()
                .CurrentPlayerObject.GetComponent<Inventory>();

            if (IsItemDeleter && SelectedItem.Item != null)
            {
                var positionWithNull = Ui.GetComponent<UiController>().MainGameObject.GetComponent<MainGameController>()
                    .GetWorldPositionOnPlane(Input.mousePosition, 0);
                
                if (positionWithNull == null)
                    return;

                var position = (Vector3) positionWithNull;

                inventory.RemoveItem(SelectedItem.Item);
                ItemController.Drop(Ui.GetComponent<UiController>().MainGameObject, ItemObjectPrefab, position, SelectedItem.Item, 2);
                SelectedItem.SetItem(null);
                return;
            }

            // Slot: FULL - Selected: FULL
            if (Item != null && SelectedItem.Item != null)
            {
                var clone = new Item(SelectedItem.Item);
                SelectedItem.SetItem(Item);
                inventory.AddItem(clone);
            }

            // Slot: FULL - Selected: EMPTY
            if (Item != null && SelectedItem.Item == null)
            {
                SelectedItem.SetItem(Item);
                inventory.RemoveItem(Item);
            }

            // Slot: EMPTY - Selected: FULL
            if (Item == null && SelectedItem.Item != null)
            {
                SetItem(SelectedItem.Item);
                inventory.AddItem(SelectedItem.Item, SelectedItem.SlotIndexNumber);
                SelectedItem.SetItem(null);
            }
        }

        public void OnPointerEnter(PointerEventData eventData)
        {
            if (Item != null)
                Tooltip.Show(Item);
        }

        public void OnPointerExit(PointerEventData eventData)
        {
            if(Tooltip != null)
                Tooltip.gameObject.SetActive(false);
        }
    }
}

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Vorsicht , hier schreibt dir eine Laie. Möchte Dir aber trotzdem gerne helfen. Es findet sich bestimmt noch jemanden der mich ergänzt.

Ich würde ein Inventar immer mit Dictionaries und Scriptable Objects lösen. Sagen Dir die beiden Dinge was? Deine Drag und Drop seperat machen. 
Deine Slots kannst du aujedenfall mit Arrays machen. Willst du wirklich Namespace nutzen?
z.B.

private Dictionary<InventoryItem, int> items = new DictionaryItem,int>();
public Image[] ItemSlots

public bool AddItem(InventoryItem ip)

{

// Ist das Item noch nicht im Inventar enthalten?
if  (!items.Containskey(ip))
{

  { 
  	if (items.count < ItemSlots.Length)  //Prüft ob noch Slots verfügbar sind
     items.Add(ip,1);  // Legt das Item neu an
  }
	else
	return false;
  }
   else
    items[ip]++;      // Wenn Item schon in einer deiner Slots, dann zähle Item um 1 hoch. (Wenn deine Items stackable sein sollen)                            
	// Hier deine Slots durchlaufen
	return true;
}                                 
                                  
                                       


    

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Hi, danke für deine Antwort.

Was genau meinst du mit den Dictionary, ich mein das ist ja auch eine Art Sammlung / Liste nur mit definierten key und value.

Scriptable Objects kenne ich tatsächlich und war da damals auf massive Probleme gestoßen. Ich denke jedoch, ich muss meine gesamte Struktur noch einmal überarbeiten und wirklich versuchen Scriptable Objects einzusetzen.

 

Das mit den Namespaces ist so eine Sache. Aktuell habe ich Klassen die gleich heißen, daher ist es unumgänglich. Bislang hatte ich auch noch keine Probleme damit, sind dir welche Bekannt die ich beachten sollte?

Link zu diesem Kommentar
Auf anderen Seiten teilen

Du kannst das auch ohne Scriptable Object. Ich rate es dir sehr, das ist sehr kompakt und übersichtlich. Es genügt eine einzige Zeile um ein Scriptable zu erzeugen.
Die Liste erkennt den gleichen Typ nicht, das kann aber die Dictionary. Das ist das tolle, ich gehe mal davon aus du einige Items stackable machen möchtest. Sonst würde dir echt eine Liste reichen. Im ip (also InventoryItem ip ) ist bereits dein Item mit der Anzahl zusammengefasst. Key und Value , richtig.
 Eigentlich ist nichts gegen die Namespace einzuwenden....ich hasse das nur!  :D Zu Beginner Zeiten raffte ich nicht warum ich da nicht zugreifen konnte. Darum die Ablehnung, und ästhetisch siehts auch nicht aus.

haha...wie große das You Tube Fenster geworden ist! 
 

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ob du ein Dictionary, eine Liste oder etwas anderes nimmst, um deine Objekte zu speichern, hängt zu 100% davon ab, wie dein Inventar funktioniert. Einfach zu sagen "schmeiß ein Dictionary drauf" ist keine gute Idee.

Nehmen wir ein paar Beispiele.

In Breath of the Wild hat man ein klassisches Inventar in dem Sinne, dass man da Slots hat, Dinge kommen in die Slots und man kann umsortieren. Dasselbe Konzept kann man erweitern mit Sachen wie 2D-Inventar, in dem man Sachen geschickt unterbringen muss. Ich denke an Diablo, lehne mich aber mit meiner mangelnden Spielzeit darin lieber nicht aus dem Fenster... darum BotW :). Bei einem solchen Spiel ist eine Liste absolut sinnvoll, denn genau dieses Verhalten bietet sie an: Dinge haben eine bestimmte (wenn auch gerne änderbare) Reihenfolge, und durch Code festzustellen, ob ein bestimmtes Item im Inventar ist, ist vielleicht nebensächlich oder gar unnötig.

In Crashlands dagegen gibt es so ein Inventar nicht. Die Entwickler haben sich während der laufenden Entwicklung entschlossen, das klassische Inventar als Feature rauszunehmen und dem Spieler nicht mehr anzuzeigen, was er hat. Denn alles, was du in Crashlands im Inventar besitzt, sind Items zum Vercraften. Das Crafting-System ist auch nicht wie in Minecraft - du gehst ins Crafting-Menü und sagst was du haben willst; dann sagt das Spiel dir, ob du alles dafür hast oder was du ansonsten noch brauchst. Zu keinem Zeipunkt sucht der Spieler ein Item aus dem Inventar oder platziert etwas an eine bestimmte Stelle. Damit fällt der Bedarf nach einer Reihenfolge der Items weg. Das Spiel musst nur immer wissen, wieviel von jeder Sorte Item der Spieler hat.

Bei klassischen Zelda-Spielen gibt es ja die Werkzeuge, die man nach und nach kriegt. Hier gibt es auch keine Reihenfolge im Inventar, auf die der Spieler oder irgendetwas anderes als die UI Einfluss auf die Reihenfolge hat. Und es gibt nur einen ganz bestimmten Satz Werkzeuge, und jedes davon hat man oder man hat es nicht.

Bei jedem dieser drei Beispiele würde meine Implementation komplett anders aussehen.

Bei Zelda-Items würde ich ganz Stumpf mehrere Bool-Variablen anlegen. Dann kann man ganz gezielt abfragen, ob der Spieler den Greifhaken hat oder eben nicht. Kein Index, keine Reihenfolge, keine Anzahl, einfach ja oder nein bei einer kleinen, begrenzten Menge an Items.

Bei Crashlands ist ein Dictionary die richtige Wahl. Das Spiel kann beliebig viele verschiedene Arten Items haben, und der Spieler kann eine beliebige Teilmenge dieser Items besitzen, und von jedem dieser Items beliebig viele. Das heißt, dass man zu jedem Item, das der Spieler hat, eine Nummer speichern will, und man will diese Nummer möglichst fix und elegant abfragen können. Man fragt nach einer bestimmten Sorte Item und kriegt gesagt wie viele davon der Spieler hat. Das ist die große Stärke der Dictionary-Klasse: Nach einem Schlüssel fragen und den dazugehörigen Wert kriegen. Wie ein Wörterbuch eben.

Für klassiche Inventare wie in BotW allerdings sind Dictionarys erstmal weniger geeignet, denn die Dinger können keine feste Reihenfolge für Items garantieren. Wenn der Spieler alle seine Heiltränke nebeneinander haben will (oder wenn das Spiel das so will!), dann hilft dir ein Dictionary erstmal nicht weiter. Dort ist eine Liste, die die Items in der Reihenfolge speichert, in der man sie auch sieht, sinnvoll.

Jetzt hat man aber manchmal das Problem, dass man mehrere dieser Ansprüche kombiniert. Starbound hat z.B. ein klassisches Inventar in dem der Spieler eigenständig Items sortiert. Das Crafting-System ist allerdings wie bei Crashlands: Man gibt das Produkt an und das Spiel sucht sich von alleine die Items aus dem Inventar, die man dafür verbraucht. In solchen Fällen kann man Sammlungen kombinieren. Anstatt sich zwischen einer Liste und einem Dictionary zu entscheiden, baust du einfach beides in eine Klasse und sorgst dafür, dass Items immer in beide Sammlungen hinzugefügt und aus beiden gelöscht werden. Das geht am besten mit einer eigenen Klasse, anstatt beide Sammlungen "lose" in dein Inventar einzubauen. Wenn du da Hilfe brauchst, sag Bescheid.

Soviel zur Sammlung, nun aber mal zum eigentlichen Problem.

Mit deinen zwei Listen meinst du sicherlich, dass du einmal eine Sammlung für deine Item-Objekte hast und dann eine zweite Liste für deine UI-Elemente, richtig?

Das ist so ein Problem, für das ich keine so hundertprozentige Alternativlösung kenne - es gibt immer kleine Vor- und Nachteile.

Was du z.B. machen kannst: Du baust eine Sammlung beliebiger Art und bündelst dein Item und den dazugehörigen Button in einem Objekt oder einem Struct. Würde initial eher zum Struct tendieren. Der Nachteil hier ist, dass du UI-Elemente in deinem Inventar referenzierst, anstatt dieses abstrakt zu halten und vom UI abzukoppeln. Aber letztenendes hast du immer ein Problem mit klarer Trennung und loser Kopplung, wenn dein UI den Inhalt deines Inventars 1:1 wiederspiegelt... und das muss es nun einmal.

Noch kurz zu ScriptableObjects: Die sind hervorragend und unersetzlich, wenn man in Unity eine saubere Architektur fahren will. Sie sind aber nicht "das Ding das alles besser macht", sondern wollen genau da eingesetzt werden, wo es Sinn ergibt, und sonst eben nicht. Was die Dinger machen ist letztendes genau eines: Daten repräsentieren, die im Editor eingegeben/gesetzt werden, und diese dann im Spiel zugänglich machen. Genau wie z.B. ein GameObject in einer Szene oder ein Prefab, nur eben völlig befreit von irgendwelchen inhärenten Dingen wie der Transform-Komponente oder Szenen-Zugehörigkeit (langes "es sei denn" hier einfügen...). In einem ScriptableObject steht genau das drin, was du reinschreibst, und sonst nix. Deshalb sind Inventarsysteme das beste Beispiel für die Nutzung von ScriptableObjects. Du machst eine Item-Klasse, die von ScriptableObject erbt, erstellst eins davon in deinen Assets, trägst Name, Icon, Wert in Gold und alles andere ein, was ein Item so bei dir an Eigenschaften hat, und dann ziehst du das Ding einfach auf deine Schatztruhe und schon weiß die Kiste, was für Loot sie gibt.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Vielen Dank für die außerordentlich umfangreichen Antworten.

Zum Verständnis, welche Anforderungen ich selber habe:

  • Es ist ein klassisches Inventar
  • Viele Items sind Individuell, wie z.B. bei Diablo
  • Stapelbar ist eine Möglichkeit, diese lässt sich für mich aber ohne Probleme hinzufügen

Das Inventar selbst (im Charakter so zu sagen) ist kein Problem, natürlich kann ich da alles hinzufügen oder entfernen. Nur die Verbindung zum Ui ist für mich problematisch.

Ein Beispiel: Wie schaffe ich es z.B. wenn ich ein Item per Drag and Drop mit einem anderen im Inventar tausche, dass es im Inventar selbst auch passiert? Aktuell treten genau hier immer wieder Probleme auf, da ich beide Inventare separat updaten muss, was zu Fehlern führt, da nicht die gleichen Items.

Wie schon geschrieben, habe ich das Inventar selbst und ein Ui Inventar, dadurch stoße ich jedoch immer wieder auf Schwierigkeiten. Außerdem fühlt es sich nicht sauber programmiert an.

Gibt es vielleicht eine Möglichkeit direkt auf das Inventar ohne Ui Inventar zuzugreifen?

Lädt man vielleicht das ganze Inventar immer neu, sobald eine Änderung vorgenommen wurde? (Wobei das nicht schön klingt)

 

Mein großes Problem ist halt die Verbindung zwischen Charakter Inventar und dem Ui.

 

PS: Scriptable Object werde ich mir auf jeden Fall noch einmal genauer angucken.

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 2 Stunden schrieb Triky313:

Außerdem fühlt es sich nicht sauber programmiert an.

Ja... das ist so das, was ich meinte mit

vor 22 Stunden schrieb Sascha:

Das ist so ein Problem, für das ich keine so hundertprozentige Alternativlösung kenne - es gibt immer kleine Vor- und Nachteile. 

Letztenendes hast du entweder ein UI, in dem dein Inventar ist - das bedeutet, dass du Mechanik in dein UI einbaust (bäh) - oder du hast eben dein Inventar zweimal. Einmal in einer abstrakten, sauber gebauten Form im Hintergrund, und einmal visuell im Vordergrund des UI. Ist auch irgendwie bäh, aber andere Möglichkeiten als die Implementationen dieser beiden Varianten gibt's halt nicht.

vor 2 Stunden schrieb Triky313:

Gibt es vielleicht eine Möglichkeit direkt auf das Inventar ohne Ui Inventar zuzugreifen? 

Naja, wenn du dein Inventar trennst in Logik und UI (und die dadurch entstehende Redundanz in kauf nimmst), dann kannst du direkt mit deinem sauberen, abstrakten Inventar arbeiten. Das UI-Inventar könnte dann per Beobachtermuster auf alle Änderungen reagieren und sich updaten.

vor 2 Stunden schrieb Triky313:

Lädt man vielleicht das ganze Inventar immer neu, sobald eine Änderung vorgenommen wurde? (Wobei das nicht schön klingt) 

Tuts auch nicht, aber wenn du damit meinst dass dein UI jedes Mal neu gebaut wird, wenn sich etwas im Inventar ändert, dann ist das gerade mit Pooling der UI-Elemente nichtmal die schlechteste Variante, würde ich sagen.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Okay, ich habe es schon fast beführtet, dass ich das keine super schöne Sache hin bekomme.

Ich werde einfach mal mit einer Variante anfangen und es testen.

 

Das Ui neu bauen, wenn etwas geändert wurde ist sicherlich nicht das beste, sobald die Items auf 100-200 steigen, könnte das vielleicht etwas länger dauern alles.

Link zu diesem Kommentar
Auf anderen Seiten teilen

So, hab nun mal die Scriptable Object verwendet.

Das Problem hier war, dass man nicht mehr speichern kann.

Ich habe BinaryFormatter() verwendet. Dann aufgrund der Scriptable Object zusätzlich noch JsonUtility.ToJson und JsonUtility.FromJson, was jedoch trotzdem nicht funktioniert.

Offensichtlich kann ich keine Scriptable Object Listen speichern. Verwendet das hier noch wer und speichert diese?

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 1 Stunde schrieb Triky313:

Offensichtlich kann ich keine Scriptable Object Listen speichern.

Jetzt mit dem Unity-Serializer oder zur Laufzeit mit was eigenem?

Denn beides sollte wunderbar gehen. ScriptableObjects haben keine magische Eigenschaft, die verhindern würde, dass man mit ihnen abreiten kann wie mir jeder anderen Objektklasse.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Am 10.4.2019 um 17:14 schrieb Sascha:

Jetzt mit dem Unity-Serializer oder zur Laufzeit mit was eigenem?

Denn beides sollte wunderbar gehen. ScriptableObjects haben keine magische Eigenschaft, die verhindern würde, dass man mit ihnen abreiten kann wie mir jeder anderen Objektklasse.

Okay, dann muss ich da noch mal genauer gucken, da scheint ein Problem zu sein.

Danke.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Archiviert

Dieses Thema ist jetzt archiviert und für weitere Antworten gesperrt.

×
×
  • Neu erstellen...