So, ich hab gerade ein wenig Zeit über, da kann ich ebensogut noch ein paar Worte zum intelligenten DragDrop verlieren - ich denke nach meinem letzten Post hat wohl kaum jemand wirklich eine Ahnung davon, was es eigentlich
tut. :D
Also, worum gehts dabei eigentlich? Das Grundproblem ist eigentlich recht banal: Der Dualitor Workflow basierte von Anfang an zu einem gewissen Teil auf DragDrop, beispielsweise um Verknüpfungen zwischen Ressourcen (Materialien, Texturen, Sounds, etc.) und Komponenten von GameObjects (SpriteRenderer, SoundEmitter, etc.) herzustellen. Ich packe mir im Project View einfach die entsprechende Ressource mit dem Cursor und ziehe sie im Object Inspector auf den jeweiligen Slot der Komponente. Soweit kein Problem.
Ein anderes Anwendungsgebiet von DragDrop ist das Erstellen und instanziieren von Prefabs: Wenn ich als Nutzer ein Prefab aus einem GameObject machen will, greife ich mir das GameObject im Scene View und ziehe es in den Projekt View. GameObjects haben bei den Projektressourcen nichts zu suchen, das weiß der Project View und erstellt deswegen eine Prefab-Ressource, wo das gezogene GameObject dann reingepackt wird.
Will ich aus diesem Prefab jetzt ein GameObject instanziieren, packe ich mir das Prefab im Project View und ziehe es runter in den Scene View oder wahlweise auch den großen Hauptbereich mit der Levelansicht (Camera View). Dort wiederum haben Ressourcen nichts zu suchen, also wird das Prefab kurzerhand ausgepackt und eine Instanz des GameObjects erstellt.
So weit so gut. Auch ohne intelligentes DragDrop war das schonmal ein relativ intuitives Verhalten. Leider funktionierten nicht alle Dinge im Editor so einfach. Nehmen wir an, man wollte (ohne intelligentes DragDrop) ein Sprite-Objekt erstellen, das eine Grafik verwendet, die irgendwo auf der Festplatte herumliegt. Folgendes müsste man dafür tun:
1. DragDrop der Bilddatei in den Project View. Die Grafik wird importiert und eine neue Pixmap-Ressource wird angelegt.
2. Jetzt brauchen wir eine Textur, welche die Grafik verwendet. Rechtsklick auf die Pixmap-Ressource und "Create Texture" im Kontextmenü wählen.
3. Objekte werden aber nicht mit Texturen, sondern mit Materialien gerendert. Also Rechtsklick auf die Texture und "Create Material" im Menü wählen.
4. Jetzt brauchen wir noch das Objekt. Rechtsklick auf eine leere Stelle im Scene view und "Create / GameObject" wählen.
5. Das Objekt benötigt nun Komponenten um ein Sprite darstellen zu können, zunächst einmal eine Position. Rechtsklick auf das neue Objekt und "Create / Components / Transform" wählen.
6. Jetzt noch die Renderer Komponente: "Create / Components / SpriteRenderer".
7. Zu guter letzt müssen wir dem Renderer noch das Material mitgeben. Also die Material-Ressource packen und in den entsprechenden Slot im PropertyGrid ziehen, wenn das Objekt ausgewählt ist.
Wenn sich an dieser Stelle jemand denkt "Wtf, so viel Aufwand für ein einfaches Sprite?!", den kann ich beruhigen: In aller Regel passierte es nicht sehr oft dass man wirklich den kompletten Strang an Aktionen ausführen musste. Oft gab es ja bereits ein passendes Material oder das Objekt existierte schon, etc.
Trotzdem gab es da natürlich Verbesserungsbedarf und hier kommt intelligentes DragDrop ins Spiel. Spulen wir mal zurück auf Anfang und schauen uns an wie die gesamte Aktion jetzt mit intelligentem DragDrop aussieht:
1. Grafik importieren. Im Prinzip noch genauso wie vorher, da hat sich nix geändert. Siehe Punkt 1 oben.
2. Jetzt noch die neue Pixmap Ressource packen und in Scene- oder Cam View absetzen.
3. Tadaa!
Diese simple Aktion veranlasst den Editor dazu, automatisch Textur und Material zu erstellen, konfigurieren und abzuspeichern, ein neues GameObject zu erstellen sowie Transform- und SpriteRenderer Komponente hinzuzufügen. Vollautomatisch und superschnell :D
Aber kommen wir langsam mal zum intelligenten Teil: Was ist da intern eigentlich gerade passiert? Diese Aktion hardcoded festzulegen wäre doch ein übler Schnitzer im flexiblen Plugin-Design von Engine und Editor. Dem Editor selbst sollte es völlig egal sein, was es für Komponenten und Ressourcen gibt und was man damit tun kann, all dieses "Wissen" kommt erst über entsprechende Plugins hinzu. Doch selbst wenn man diese Aktion hardcoded in ein Plugin packt, wirklich schön oder flexibel ist das nicht.
Also auf zur Lösung des Problems:
Eine DragDrop-Aktion enthält grundsätzlich erstmal Daten eines bestimmten Typs. Das mögliche Ziel einer DragDrop-Aktion (also beispielsweise Scene- oder Cam View) interessiert dabei gar nicht, was für Daten das genau sind, sondern nur ob man diese in eine Form bringen kann, die das jeweilige Steuerelement verwalten kann. Die Cam View arbeitet z.B. mit GameObjects, da man diese dort drinnen herum schieben kann.
Grundsätzlich könnte man der Cam View nun also beibringen wie es andere Datentypen (z.B. Pixmaps) in GameObjects konvertiert, aber was ist dann mit der Scene View? Und allen anderen Steuerelementen? Damit man dasselbe Verhalten nicht allen Steuerelementen einzeln beibringen muss, ist es ratsam, dieses zu zentralisieren. Ein erster Ansatz wäre also, dass Cam View, Scene View und Co beim Feststellen einer DragDrop-Aktion die Rohdaten extrahieren, diese an die "Zentrale" weiterleiten und darum bitten, sie in GameObjects zu konvertieren.
Dieses Konzept abstrahierend kam ich auf folgendes: In der Zentrale wird eine Liste von DataConverter-Objekte verwaltet. Jedes dieser Objekte ist in der Lage, einen bestimmten Datentyp in einen anderen Datentyp zu konvertieren. Editor-Plugins können eigene DataConverter definieren und ebenfalls in der Zentrale registrieren.
Ich kann nun der "Zentrale" beliebige Objektdaten rüberschicken und Objekte eines bestimmten Typs zurückverlangen. Was die Zentrale nun tut ist herauszufinden auf welche Weise sich die registrierten DataConverters am effizientesten kombinieren lassen um die Anfrage zu erfüllen. Man kann sich das im Prinzip wie eine Pathfinding-Operation vorstellen.
Was also bei der oben vorgestellten DragDrop-Aktion intern passiert ist folgendes:
1. DragDrop erreicht die Cam View. Selbige verlangt GameObjects als Daten und startet "über die Zentrale" eine Konvertierung mit dem Zieltyp "GameObject"
2. Dort wird das Datenpaket geöffnet und festgestellt: Hm, Mist. Keine GameObjects drin. Fragen wir mal rum, ob sich ein Konverter mit Zieltyp "GameObject" findet
3. GameObjectFromPrefab und GameObjectFromComponents bieten sich an. Die Anfrage wird nacheinander an beide weitergeleitet.
4. GameObjectFromPrefab lässt sie nach einigem Pathfinding zurückgehen da der Konverter feststellt dass ihm die zum Arbeiten nötigen Daten fehlen. Es werden keine Prefabs gefunden und es lassen sich auch keine aus den verfügbaren Daten ableiten.
5. Bleibt noch GameObjectFromComponents. Ich überspringe jetzt aber mal die Pathfinding-Details. Die vollständige Konvertierungskette ist:
(Nicht alle existierenden DataConverter angezeigt)
Es werden also im Rahmen der Aktion automatisch eine Textur aus der Pixmap erstellt, dann ein Material aus der Textur, dann eine SpriteRenderer-Komponente die das Material nutzt und anschließend ein GameObject welches erst alle "required Components" des SpriteRenderers hinzufügt (Transform) und dann den Renderer selbst. Und da sind wir.
Natürlich kann man jetzt sagen "Oh man, so ein Aufwand für so ne Kleinigkeit", aber dieses System hier hat tatsächlich ein paar Vorteile, die anders nur schwer erreichbar wären. Es reagiert zum Beispiel sehr flexibel wenn Nutzer eigene Komponententypen erstellen - einfach einen eigenen Konverter schreiben (Wenige Codezeilen in der Regel) und in der Zentrale registrieren. Und sofort ist der gesamte Editor in der Lage, DragDrop-, Clipboard- und Konvertierungsaktionen mit den Nutzerkomponenten durchzuführen.
Ein anderer Vorteil entsteht durch die Arbeitsaufteilung der DataConverter: Eine Konvertierung wird nicht als atomare Aktion begriffen, sondern als Teamwork. Dementsprechend können auch viele DataConverter an einem gemeinsamen Datensatz arbeiten und sich gegenseitig ergänzen. Es ist beispielsweise ohne weiteres Möglich, im Project View ein paar Sounds und eine Textur zu wählen und den ganzen Packen komplett in die Cam View zu ziehen. Das Resultat ist ein GameObject, das neben der SpriteRenderer-Komponente (aus der Textur) auch eine SoundEmitter-Komponente enthält, welche die beiden Sounds als Sound Sources hinzugefügt bekam. Das ist nur dadurch möglich, dass DataConverter "in Teamarbeit" vorgehen.
Natürlich gibt es auch DataConverter, die das explizit verhindern. Packt man beispielsweise ein Prefab gemeinsam mit einem Sound, wird das Prefab instanziiert und der Sound ignoriert. Prefabs haben vorrang, da hier angenommen wird dass diese grundsätzlich erstmal in ihrer unbearbeiteten Reinform instanziiert werden sollen. Das ist nirgendwo hardgecodet, sondern steht im DataConverter GameObjectFromPrefab - welcher in der Zentrale bei Bedarf ohne Weiteres überschrieben werden kann.
Okay, jetzt hab ich eine ganze Menge Theorie von mir gegeben. Hier zum Abschluss noch ein paar kommentierte Codefragmente:
Das hier ist mal so ein DataConverter von Innen, und zwar TextureFromPixmap.
Code: Alles auswählen
public class TextureFromPixmap : DataConverter
{
public override bool CanConvertFrom(ConvertOperation convert)
{
return
convert.AllowedOperations.HasFlag(ConvertOperation.Operation.Convert) &&
convert.CanPerform<Pixmap>();
}
public override bool Convert(ConvertOperation convert)
{
bool finishConvertOp = false;
List<ContentRef<Pixmap>> dropdata = new List<ContentRef<Pixmap>>();
var matSelectionQuery = convert.Perform<Pixmap>();
if (matSelectionQuery != null) dropdata.AddRange(matSelectionQuery.Ref());
// Generate objects
foreach (ContentRef<Pixmap> pixRef in dropdata)
{
if (convert.IsObjectHandled(pixRef.Res)) continue;
if (!pixRef.IsAvailable) continue;
Pixmap pix = pixRef.Res;
// Find Material matching Texture
ContentRef<Texture> texRef = ContentRef<Texture>.Null;
if (pixRef.IsDefaultContent)
{
var defaultContent = ContentProvider.GetAllDefaultContent();
texRef = defaultContent.Where(r => r.Is<Texture>() && (r.Res as Texture).BasePixmap == pix).FirstOrDefault().As<Texture>();
}
else
{
string texPath = pix.FullName + Texture.FileExt;
texRef = ContentProvider.RequestContent<Texture>(texPath);
if (!texRef.IsAvailable && convert.AllowedOperations.HasFlag(ConvertOperation.Operation.CreateRes))
{
// Auto-Generate Texture
texRef = Texture.CreateFromPixmap(pix);
}
}
if (!texRef.IsAvailable) continue;
convert.AddResult(texRef.Res);
finishConvertOp = true;
convert.MarkObjectHandled(pixRef.Res);
}
return finishConvertOp;
}
}
Und so wird er "in der Zentrale" registriert:
Code: Alles auswählen
CorePluginHelper.RegisterDataConverter<Texture>(new DataConverters.TextureFromPixmap());
(Die Zentrale hat noch einige andere Aufgaben - man kann dort z.B. auch Editor-Metadaten für Core-Klassen hinterlegen, z.B. zu nutzende Icons, etc.)
Hier eine zum Verständnis gekürzte und editierte Version dessen was die Cam View tut, um an ihre Daten zu kommen:
Code: Alles auswählen
private void LocalGLControl_DragDrop(object sender, DragEventArgs e)
{
DataObject data = e.Data as DataObject;
ConvertOperation convert = new ConvertOperation(data, ConvertOperation.Operation.All);
if (convert.CanPerform<GameObject>())
{
var dragObjQuery = convert.Perform<GameObject>();
List<GameObject> dragObj = dragObjQuery.ToList();
// .. snip ..
}
}
Mithilfe der ConvertOperation-Klasse sind es im Prinzip nur ein paar Zeilen, um jeden beliebigen Objekttyp anzufordern. Sie kapselt auch die ganzen Zugriffe auf die "Zentrale".
So. Hoffe, das war so halbwegs informativ :)