Jump to content
Unity Insider Forum
  • Einträge
    9
  • Kommentare
    52
  • Aufrufe
    34.826

ScriptableObjects - Assets für jeden Zweck (oder: Wie man ein Inventarsystem richtig baut)


Sascha

5.392 Aufrufe

Es ist nicht das erste Mal gewesen, dass ich meinte, an die Grenzen von Unity gestoßen zu sein - und mich irrte.
Dieses Mal war es der zweite oder dritte Ansatz gewesen, ein komplett sauberes Inventarsystem zu erstellen.
Unity ist ja sonst immer sehr flexibel und bietet alles, was der Programmierer braucht, für jeden Zweck.
Aber für diese Situation schien die Engine keine vernünftige Lösung bereit zu stellen:[list=1]
[*]Das Inventar soll in mehreren Scenes verfügbar sein
[*]Ich will jede Scene starten und testen können
[*]Ich will nicht mit Item-IDs arbeiten, da sie unübersichtlich und unsicher sind
[*]Ich will die Items per Drag&Drop angeben können
[*]Items sollen ein Bild, einen Namen und evtl noch Eigenschaften haben
[/list]
Wer sich schon einmal an einem Inventarsystem versucht hat (und alles folgende noch nicht kennt), wird auch das Gefühl gehabt haben, dass Unity für diesen Fall keine wirklich schöne Lösung parat hält.

Was man bräuchte, wären Datenstrukturen, die Scene-übergreifend verfügbar sind (Punkt 1).
Wegen Punkt 2 kommt [url="http://unity3d.com/support/documentation/ScriptReference/Object.DontDestroyOnLoad.html"]DontDestroyOnLoad()[/url] dafür nicht in Frage, weil ich zum Testen nicht in jedem Level ersteinmal ein Inventory-Objekt platzieren will, und sollte dieses am Player drankleben, dann will ich nicht jedes Mal das Inventory neu mit Items bestücken.
Das allein schon, weil ich die auch alle wieder löschen muss, wenn ich das Spiel von vorne starte, da wegen DontDestroyOnLoad() bei jedem Scenewechsel ein neues Inventar auftaucht.
Ein Singelton wäre die nächste Möglichkeit:
[Inventory.js]
[code]private static var existing : Inventory;

function Awake()
{
if(existing)
{
Destroy(this);
}
else
{
existing = this;
DontDestroyOnLoad(this);
}
}[/code]
...davon gäbe es immer nur eins.
Aber wer will denn schon diesen hässlichen GameObject-Müll in seinen Scenes haben, besonders, wenn er, wie ich an diesem Punkt, ein sauberes Konzept haben will?

Punkt 3 und 4 sorgen dafür, dass ich nicht einfach ints hin- und herschieben kann, will ich ja auch nicht.
Welcher Designer will schon ständig darauf achten müssen, welche Nummer jetzt wo liegt... und wenn sich dann mal wo die Nummer ändert... gar nicht erst drüber nachdenken.
[b]Ich brauche also irgendein Objekt oder Asset, das jeweils ein Item repräsentiert.[/b]

Und Punkt 5 besagt, dass dieses auch noch verschiedene Felder haben soll, also mind. eine Texture2D, ein paar Strings und wer weiß, was noch alles.

[b]Zusammengefasst:[/b] Ich will ein Asset haben, dass ich selbst programmieren kann.

[size=4][u]Prefabs benutzen?[/u][/size]
Was hat man denn alles an Assets, die Scripts haben können?
Naja, Prefabs.
Prefabs sind prima, aber nicht dafür, denn sie sind Schablonen zum Klonen und können verändert werden.
Ein Prefab-Item könnte zwar als Referenz hin- und hergeschoben werden, aber auch Instanziiert und dann verändert, und das geht völlig am Zweck vorbei, dann hat man zwei Instanzen des gleichen Items, die unterschiedlich sind...
Das kann man vielleicht sogar wollen, aber das ist dann wieder eine ganz andere Geschichte.
Ausserdem hat ein Prefab, welches ja ein GameObject ist, immer eine Transform-Komponente, wobei wir wieder bei überflüssigem Datenmüll wären.

Keine Prefabs also.

[size=4][u]Und wie soll das jetzt gehen?[/u][/size]
Eine Datei wie eine xml, die ich mit einem Skript auslese, kam für mich nicht in Frage, da so etwas im Editor schlecht zu benutzen ist und den Workflow ausbremst.
Um also herauszufinden, was ich noch alles als Variablentyp in meinem Inventory-Script verwenden könnte, ab in die Scripting-Reference.
Eine Variable (und damit alles, was ich im Inspektor als Eigenschaft deklarieren kann) ist - wer objektorientiertes Programmieren richtig gelernt hat - immer vom Typ Object oder einem Subtyp.
Also schaue ich mir die Subtypen von Object an, die Unity so anbietet, und stoße auf [b]ScriptableObject[/b]s.

ScriptableObject erbt direkt von Object und ist damit schon mal kein MonoBehaviour, also nichts, das man als Skriptkomponente einem GameObject geben kann.
Der andere wichtige Subtyp von Object in Unity ist [i]Component[/i], dessen Subtypen allesamt genau das Gegenteil, also GameObjects zuweisbar, sind.

Was ist nun aber der Sinn davon?
Die Scripting Reference sagt (übersetzt):
[quote]Eine Klasse, von der man erben kann, um Objekte zu kreiren, die keinen GameObjects zugewiesen werden sollen.
Das ist am nützlichsten für Assets, die Daten speichern sollen.[/quote]
Selbstgemachte Assets also - Hurra!

[size=4][u]Aber wie benutzt man sie?[/u][/size]
Gute Frage. Die Scripting Reference hilft hierbei nicht weiter.
Aber wer schon hier und da mal Assets über ein Skript bearbeitet hat, kennt bereits die [url="http://unity3d.com/support/documentation/ScriptReference/AssetDatabase.html"]AssetDatabase[/url]-Klasse, genauer:
Die Funktion [url="http://unity3d.com/support/documentation/ScriptReference/AssetDatabase.CreateAsset.html"]CreateAsset()[/url].
Erster Parameter: Ein Objekt, das als Asset-Datei (.asset) in dem Asset-Ordner gespeichert werden soll,
zweiter Paramater: Der Pfad relativ zum Projektordner.

Also einfach ein ScriptableObject als Parameter übergeben und fertig?
überraschende Antwort: Ja.

Aber als erstes erstellen wir uns unser [b]InventoryItem.cs[/b]:
[code]using UnityEngine;
using System.Collections;

public class InventoryItem : ScriptableObject //nicht MonoBehaviour
{
public Texture2D image;
public string itemName; //Nicht "name", das gibt ärger wegen Object.name
public int value; //der Preis des Items
}[/code]
Wie man sieht: Wir erben nicht von MonoBehaviour, sondern von ScriptableObject.
Man kann das Skript deshalb nicht mehr als Komponente an ein GameObject heften.
Ich benutze hier bewusst C#, da man in JavaScript immer automatisch von MonoBehaviour erbt, also kein ScriptableObject damit scripten kann (ausser als embedded Class, und das muss ja nicht sein...).

[size=4][u]Und wann CreateAsset() aufrufen?[/u][/size]
Da man keinen Eintrag "Create Asset" im Kontextmenü hat, und auch das Erstellen einer .asset-Datei außerhalb von Unity nicht funktioniert (ScriptableObject nicht nachträglich zuweisbar), müssen wir CreateAsset irgendwo unterbringen.

Hierbei hilft uns [url="http://unity3d.com/support/documentation/ScriptReference/MenuItem.html"][MenuItem][/url] (JS: @MenuItem).

Mit [MenuItem] erstellen wir uns jetzt eine statische Funktion in einem neuen Script, welches im einem Editor-Ordner stecken muss:
[code]using UnityEditor; //WICHTIG!
using UnityEngine;
using System.Collections;

public class CreateInventoryItem
{
[MenuItem("Inventory/Create Inventory Item")]
static void CreateAsset()
{

}
}
[/code]
[font=Courier New]using UnityEditor;[/font] ist standardmäßig nicht in einem neuen C#-Skript enthalten, also unbedingt einfügen, denn ohne funktioniert [MenuItem] nicht.
Wegen genau dieser Anweisung muss das Skript auch in einem Ordner namens "Editor" stecken: In einen Build kann man dieses Skript nicht mehr exportieren, daher würde das Skript überall anders eine Fehlermeldung werfen.

Jetzt füllen wir der Reihe nach CreateAsset() mit Anweisungen:[list]
[*]Ein neues InventoryItem erzeugen:[code]ScriptableObject asset = ScriptableObject.CreateInstance(typeof(InventoryItem));[/code]
...warum nicht einfach über "new", sei später erklärt.
[*]Das Asset erzeugen:[code]AssetDatabase.CreateAsset(asset, "Assets/NewInventoryItem.asset");[/code]
...das .asset ist wichtig, genau wie der Ordner Assets.
[*]Und damit das Ganze richtig schön wird, Fokus-Schmankerl, die das Asset im Project View markieren:[code]EditorUtility.FocusProjectWindow();
Selection.activeObject = asset;[/code]
[/list]
Das fertige Script sieht dann so aus:
[code]using UnityEditor;
using UnityEngine;
using System.Collections;

public class CreateInventoryItem
{
[MenuItem("Inventory/Create Inventory Item")]
static void CreateAsset()
{
ScriptableObject asset = ScriptableObject.CreateInstance(typeof(InventoryItem));
AssetDatabase.CreateAsset(asset, "Assets/NewInventoryItem.asset");
EditorUtility.FocusProjectWindow();
Selection.activeObject = asset;
}
}[/code]

Speichern, schließen, fertig!
Jetzt einmal oben im Editor auf Inventory klicken (wenn es nicht da ist, einfach oben irgendetwas anklicken, dann kommt's), neues Item erstellen, umbenennen und Werte zuweisen.
Das kann man jetzt beliebig oft und einfach machen.

[size=4][u]Fertig![/u][/size]

Erstellt man jetzt irgendwo ein Feld[code]Inventory item;[/code]...erhält man ein Skript, das weiß, welches Item der Spieler bekommt, wenn man es aufsammelt.
[spoiler][code]InventoryItem item;

void OnTriggerEnter(Collider other)
{
if( ... ) //Wenn Spieler, oder: Wenn Inventar hat
{
// Inventar erhält item
Destroy(gameObject);
}
}[/code][/spoiler]
Ein Inventar hat dafür ein Array:[code]InventoryItem[] items;[/code]...vermutlich aber auch ein dynamisches ;)

Jetzt haben wir ein System erstellt, mit dem man durch einfache Klicks, Drag&Drop und Texteingabe beliebig tolle Items erstellen kann, und das, ohne dass man beim Benutzen auch nur einen Moment lang eine Zeile Code anschauen muss.
Völlig intuitiv also, ganz so, wie es in Unity üblich ist, sodass sich der Designer drüber freut :)

Bei den Schnipseln habe ich, wie man sieht, weiterhin C# benutzt. Wer mal versucht, eine mit C# gemachte Klasse in JS zu benutzen, weiss, warum.

[size=4][u]Nachtrag zu ScriptableObject.CreateInstance()[/u][/size]
Ach ja, da war ja noch etwas: Warum nicht einfach [font=Courier New]new InventoryItem()[/font] benutzen?
Zuerst einmal: Das geht auch.
Unity mag es nur nicht und gibt dabei eine Warnmeldung aus, man solle doch CreateInstance() benutzen. Das ist dann etwas nervig.

In der Praxis macht "new" aber nur einen Unterschied: Ein über CreateInstance() erzeugtes Objekt verhält sich genau wie ein MonoBehaviour als Komponente.
Schaut man sich das Asset, das ein mit CreateInstance() erzeugtes Objekt beinhaltet, an, sieht man, dass man die Klasse tauschen kann, also ein anderes ScriptableObject-Derivat einfügen kann,
genau wie man bei MonoBehaviour-Komponenten im Inspektor das Script tauschen kann.
Das ist vermutlich eine gute Sache, wenn das Script irgendwie mal abhanden kommt.

Benutzt man dagegen "new", fehlt diese Einstellung und das Asset ist an die ScriptableObject-Klasse gebunden.
Das klingt zwar ganz nett eigentlich, warum sollte man tauschen können?
Aber die Warnmeldung drängt schon ein wenig dazu, CreateInstance() zu bentzen, wer weiß schon, welcher Sonderfall ansonsten die Assets corrupted oder solche Scherze...
Von daher: Jeder für sich.

[size=4][u]Schluss für heute[/u][/size]
So, jetzt habt ihr was über ScriptableObjects, MenuItem und über objektorientierte Programmierung gelernt. Happy Coding!

[size=4][u]Noch ein kleiner Nachtrag[/u][/size]
Wenn man es mit MenuItem übertreibt, sieht der Unity Editor plötzlich reichlich unübersichtlich aus.
Den MenuItem-Eintrag sollte man daher vielleicht ändern auf
[code][MenuItem("Assets/Create/Inventory Item")][/code]
Viel besser!

18 Kommentare


Recommended Comments

Ich werds wohl noch ein paar mal durchlesen müssen bis ichs ganz kapiert habe aber toll dass mal wieder ein neuer Blog-Eintrag kommt ;)
Außerdem kapier ich das nicht ganz:
Du schreibst, dass du ein neues InventoryItem erzeugst [code]ScriptableObject asset = ScriptableObject.CreateInstance(typeof(InventoryItem));
[/code]
aber beim "fertigen Code" steht dann plötzlich ImageInfo da? o.O
Link zu diesem Kommentar
[quote name='Bowserkoopa' date='03. August 2011 - 22:02 Uhr']aber beim "fertigen Code" steht dann plötzlich ImageInfo da? o.O[/quote]
Haha, Copypasta :D
Ist korrigiert, danke.
Link zu diesem Kommentar
Das ScriptableObject darf nicht in Editor/ liegen, sonst hast Du es im Spiel nicht mitkompiliert!
Ansonsten ja, nur die zwei Scripts und irgendwelche, die das ScriptableObject benutzen.
Link zu diesem Kommentar
wenn ich oben in der leiste, dann das inventoryitem erstellen will, dann krieg ich 2 fehler: [color=#ff0000]Instance of InventoryItem couldn't be created. The the script class needs to derive from ScriptableObject.
UnityEngine.ScriptableObject:CreateInstance(Type)
CreateIventoryItem:CreateAsset() (at Assets/Editor/Editor.cs:10)[/color]

[color=#000000]und: [/color]

[color=#FF0000]NullReferenceException
CreateIventoryItem.CreateAsset () (at Assets/Editor/Editor.cs:11)[/color]

ich kriege aber auch schon vorher 3 fehler.....hoffe auf hilfe
Link zu diesem Kommentar
Sodele, da ich mit c# warm gewrden bin, nutze ich deine ScriptableObject jetzt gerne! Ich hab die nicht nur für Inventaritems sondern auch für Monster genutzt.
Sehr schöne Sache! :)
Bitte korrigier noch den einen Fehler hier:
[CODE]public class IventoryItem : ScriptableObject //nicht MonoBehaviour[/CODE]

Mach mal ein N ins Inventory rein. Das war nämlich damals mein Problem, als ich von c# noch nix wusste. ;)
Link zu diesem Kommentar
ich buddel die leiche mal aus... andererseits verliert das thema wohl nicht an aktualität! schon witzig wie man über jahre nette features ignorieren kann;) ...da gehen gerade wieder türen auf, die mich ganze projekte umschmeißen lassen. man lernt eben nie aus..egal wie lange man dabei ist.
Link zu diesem Kommentar
Voll übersehen den Kommentar, sorry :/
Bei Fragen zu den Dingern gerne im Forum fragen!

Jedenfalls erzeugst du sie mit dem beschriebenen Editor-Code in den Assets - und da bleiben sie auch.
Link zu diesem Kommentar
Gast
Kommentar schreiben...

×   Du hast formatierten Text eingefügt.   Formatierung jetzt entfernen

  Only 75 emoji are allowed.

×   Dein Link wurde automatisch eingebettet.   Einbetten rückgängig machen und als Link darstellen

×   Dein vorheriger Inhalt wurde wiederhergestellt.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Lädt...
×
×
  • Neu erstellen...