Jump to content
Unity Insider Forum

Soda - ScriptableObject Dependency Architecture


Sascha
 Share

Recommended Posts

Moin zusammen!

Nach etwa einem Jahr Arbeit (mit Pausen... :)) kann ich mein neues Asset Store-Paket vorstellen!

CardImage_420x280.png

Es handelt sich dabei um eine stark weiterentwickelte Implementation der Konzepte, die in diesem Talk vorgestellt werden:

Um es kurz zu fassen: Damit könnt ihr extrem saubere Architekturen bauen, doch der Kniff ist: Ihr arbeitet hier mit Unity und nicht dagegen an.

Das Paket ersetzt keine von Unitys Funktionalitäten, es gibt kein "das kannst du jetzt so nicht mehr benutzen". Es wird ausschließlich erweitert, was schon da ist. Ich sag mal so... ich arbeite nicht mehr ohne, seit ich das Ding für meine eigenen Projekte gebaut habe.

Falls ihr euch jetzt fragt, wie das geht... ich könnte erstmal eine Menge schreiben, aber schaut euch besser den Talk an, der ist hervorragend.

Hier geht's zum Paket: https://assetstore.unity.com/packages/tools/integration/soda-scriptableobject-dependency-architecture-137653

cca78b7f-0004-477a-9a20-8c3646239507.web

Bei Fragen gerne fragen!

  • Like 4
Link to comment
Share on other sites

Wow, ein wirklich nutzliches Asset hast du entwickelt. Vor allem, dass man Events als Scriptable Objects erstellen kann. Hatte schon viel Stress gehabt mit ganzen Objekt Verbindungen, so dass ich mit der Zeit einfach die Übersicht verlor :D Ich denke mit dem Asset würde mir das nicht passieren. Ich habe bei mir auch einiges nach Ereignise umgebaut. allerdings wäre es natürlich schöner, wenn man nach Ereignis Scriptable klickt und sofort sieht, was ausgelöst wird. 

Ich frage mich aber wie es performancemäßig aussieht. Wann sollte man mit Ereignis auflösen und wann einfache Objektverbinding ausreichen würde. Und zu dem Asset frage ich mich ob man mit dem einen Ereignis weitere Ereignise auslösen kann?

Link to comment
Share on other sites

Danke schonmal für's Feedback!

vor 9 Stunden schrieb TheOnlyOne:

Ich frage mich aber wie es performancemäßig aussieht. Wann sollte man mit Ereignis auflösen und wann einfache Objektverbinding ausreichen würde.

Performance kosten die Dinger nichts besonderes. Es geht deshalb bei dieser Entscheidung viel mehr um saubere Architektur. Wenn du z.B. einen Knopf in deine Szene baust, der eine Tür öffnet, dann willst du da kein Event dazwischenknallen. Es ist in deinem Interesse, dass es offensichtlich ist, welche Tür der Knopf öffnet. Events sind eher für die Fälle, wo Auslöser und Beobachter (also die, die auf das Event reagieren) nicht wirklich etwas miteinander zu tun haben. Ein Beispiel dafür wäre ein Game Over-Event, wo der Auslöser (z.B. die sterbende Spielfigur oder der abgelaufene Timer) nichts mit den Beobachtern (also dem Game Over-Bildschirm oder dem Highscore-System) zu tun haben. Letztenendes löst das eine das andere aus, aber diese Verbindung geht keinen von beiden etwas an.

So nebenbei: Soda hat eine ScriptableObject-Klasse, mit der du ein GameObject samt component cache referenzieren kannst, um dann damit von irgendwo anders aus zu arbeiten. Das wäre neben direkten Drag and Drop innerhalb der Szene, Events und globalen ScriptableObject-Variablen ein weiterer Weg. Wann welches davon sinnvoll zu verwenden ist, ist gar nicht so leicht, aber deshalb habe ich die detaillierte Anleitung geschrieben, die dem Paket beiliegt :)

vor 9 Stunden schrieb TheOnlyOne:

Und zu dem Asset frage ich mich ob man mit dem einen Ereignis weitere Ereignise auslösen kann?

Jap. GameEvents haben ein UnityEvent, in das du alles mögliche aus den Assets ziehen kannst. Du kannst auch ein GameObject in die Szene packen, das auf ein Event mit dem Auslösen eines anderen Events reagiert, wenn diese Kette nicht permanent, sondern nur in bestimmten Situationen vorhanden sein soll.

Link to comment
Share on other sites

vor 21 Stunden schrieb Sascha:

Danke schonmal für's Feedback!

 Events sind eher für die Fälle, wo Auslöser und Beobachter (also die, die auf das Event reagieren) nicht wirklich etwas miteinander zu tun haben.

Also wenn ich zum Beispiel Fische im Wasser habe, die einfach zur Dekoration da sind und zudem noch spielerisch Objekte verfolgen welche sich im Wasser bewegen, somit dem Spieler auch, falls er im Wasser ist, haben sonst nichts mehr mit dem Spieler zutun. Und jetzt möchte ich eine weitere Funktion für Fische hinzufügen, sobald der Spieler hochspringt aus dem Wasser, sollten die Fische sich erschreckend von dem Spieler wegfliehen.

Und jetzt die Frage: wie würdest du es auf meiner Stelle lösen, für Sprung ein Event anlegen(auch wenn ich sonst nichts mehr plane irgendwas vom Sprung abhängig zu machen, was nicht zum Spieler gehört), oder sagen: "ok, da es nur eine kleine Funktionalität ist, verbinde ich die Fische mit dem Spieler Script und rufe fischewegflehen Funktion aus dem Spieler Sprung Funktion aus und gut ist es"?

Link to comment
Share on other sites

Am 26.7.2019 um 20:18 schrieb TheOnlyOne:

Also wenn ich zum Beispiel Fische im Wasser habe, die einfach zur Dekoration da sind und zudem noch spielerisch Objekte verfolgen welche sich im Wasser bewegen, somit dem Spieler auch, falls er im Wasser ist, haben sonst nichts mehr mit dem Spieler zutun. Und jetzt möchte ich eine weitere Funktion für Fische hinzufügen, sobald der Spieler hochspringt aus dem Wasser, sollten die Fische sich erschreckend von dem Spieler wegfliehen.

Und jetzt die Frage: wie würdest du es auf meiner Stelle lösen, für Sprung ein Event anlegen(auch wenn ich sonst nichts mehr plane irgendwas vom Sprung abhängig zu machen, was nicht zum Spieler gehört), oder sagen: "ok, da es nur eine kleine Funktionalität ist, verbinde ich die Fische mit dem Spieler Script und rufe fischewegflehen Funktion aus dem Spieler Sprung Funktion aus und gut ist es"?

Moin... sorry für die späte Antwort, hab deinen Post irgendwie übersehen... Dafür würde ich ein Event anlegen. Eben weil das Springen der Fische kein Teil der Definition der Spielfigur ist. Die Figur funktioniert bestimmt auch einwandfrei, ohne dass da irgendwo definiert ist, was ein Fisch ist. Und auch wenn es keine Fische in der Szene gibt. Und was das Springen der Fische angeht: Was das auslöst, ist auch kein Teil der Definition eines Fisches. Das Springen könnte ja auch genausogut von etwas anderem ausgelöst werden.

Anders wäre es, wenn du da mehr Semantik im Fisch-Sprung hättest. Wenn nur Fische in einem bestimmten Radius um den Spieler springen sollen, oder wenn rote Fische auf den einen Spieler und blaue Fische auf den anderen reagieren sollen... da kann man sich das nochmal überlegen, das anders anzugehen.

Am 29.7.2019 um 08:12 schrieb devandart:

Ist die Dokumentation auch öffentlich zugänglich, so dass man sich erstmal einen Überblick verschaffen kann?

Ich hab's gerade einfach mal hochgeladen: http://13pixels.de/pages/assetstore/soda/documentation/annotated.html

  • Like 1
Link to comment
Share on other sites

  • 1 month later...

Glückwunsch für den neuen AssetStore Packet. 

Ich persönlich bin nicht so der Fan davon (bezogen auf das Video) und ich meine  das gab es frei zu downloaden. Aber wenn ich mal sowas brauche mache ich passend zum Objekt. Beispiel passend zu meinem Projekt, Health, Shield.. (siehe unten) alles in einem ScriptableObject und das benutze ich das als Kommunikations (Ja Events gibt es da auch). 

Ich benutze sehr selten mittlerweile Singletons. Nur bei Komplexen ein System Sachen die einmal da sind (Beispiel mein InputReader Klasse) oder Kleinigkeiten wie (public static) Player.localPlayer.

Bei komplizierteren Sachen stell ich mir das aber harter vor.
Ich frage mich zum Beispiel, wie man das verwenden könnte, wenn man jetzt kleinere Minibosse hat, die ab und zu da sind dann im HUD einen Healthbar anzeigen möchte. Muss ich für jeden SO Asset erstellen? Gut es könnten ja alle, den einen Asset teilen, aber was wenn mal zwei Minibosse gleichzeitig gibt und ich will beide Healthbars anzeigen lassen.. Dann lässt sich das nicht so leicht teilen.

Ein Beispiel wie ich zum Beispiel mein Health  und Shield UI update.

[CreateAssetMenu]
public class DamageEventSO : ScriptableObject
{
    public EventValue<float> Health;
    public EventValue<float> Shield;
}

public struct EventValue<T>
{
    public UnityAction<T> OnValueChanged;

    T value;
    public T Value
    {
        get => value;
        set
        {
            this.value = value;
            OnValueChanged?.Invoke((T)(object)this.value);
        }
    }
}

So wie man sieht, ist das hier generic und brauche da nicht floats, ints oder sonst ähnliches.

Link to comment
Share on other sites

Am 17.9.2019 um 15:18 schrieb MaZy:

das gab es frei zu downloaden

Gibt verschiedene Pakete. Die kostenlosen fand ich alle auf die eine oder andere Weise doof, darum hab ich mein eigenes geschrieben, mit dem ich sehr zufrieden bin.

Am 17.9.2019 um 15:18 schrieb MaZy:

So wie man sieht, ist das hier generic und brauche da nicht floats, ints oder sonst ähnliches.

Benutzt du Odin oder so? Denn vanilla würde man da ja nichts serialisiert kriegen oder im Editor manipulieren können.

Am 17.9.2019 um 15:18 schrieb MaZy:

Ich frage mich zum Beispiel, wie man das verwenden könnte, wenn man jetzt kleinere Minibosse hat, die ab und zu da sind dann im HUD einen Healthbar anzeigen möchte. Muss ich für jeden SO Asset erstellen? Gut es könnten ja alle, den einen Asset teilen, aber was wenn mal zwei Minibosse gleichzeitig gibt und ich will beide Healthbars anzeigen lassen.. Dann lässt sich das nicht so leicht teilen.

Sobald du keine einzelnen Objekte mehr hast, sondern beliebig viele und da entsprechend skalieren willst, kannst du z.B. RuntimeSets benutzen, wo sich jeder relevante Miniboss einträgt. Dein Health Bar-Bereich im UI muss dann natürlich (so oder so) die Anzahl der sichtbaren Health Bars regulieren. Da du in so einem Fall sowieso in der Regel nicht einfach nur mehrere gleiche Health Bars anzeigen willst, sondern irgendwie noch darstellen willst, welche Bar zu welchem Boss gehört, hast du damit dann direkt Zugriff auf beliebige weitere Eigenschaften der Zielobjekte.

Am 17.9.2019 um 15:18 schrieb MaZy:

Ich benutze sehr selten mittlerweile Singletons.

Wenn es um GameObjects geht, benutze ich gar keine mehr. Ausnahmslos.

Link to comment
Share on other sites

vor 8 Stunden schrieb Sascha:

Benutzt du Odin oder so? Denn vanilla würde man da ja nichts serialisiert kriegen oder im Editor manipulieren können.

Nein kein Odin. Also mein Beispiel zeigt auch keine Werte an. War auch für mich nicht wichtig. Könnte es eventuell mit Custom Inspector anzeigen lassen, aber hab sowas für ScriptableObjects nie gemacht.

Link to comment
Share on other sites

  • 1 month later...
  • 6 months later...
  • 7 months later...

Mit 1.3.0 ist soeben das bisher größte Update live gegangen!

grafik.pngNervt's euch auch, wenn eure ScriptableObjects nur in einer einzelnen Szene relevant sind, sie aber trotzdem zuhauf in den Assets rumliegen? Dann hilft euch das Scenebound-System! Damit könnt ihr Soda- aber auch beliebige andere ScriptableObjects in Szenen erstellen statt in den Assets.

Die könnt ihr dann ganz normal benutzen, indem ihr sie aus dieser Liste irgendwoanders hinzieht. Damit könnt ihr GameEvents erstellen für das Auslösen einen spezifischen Alarms oder ein GlobalInt, das die zu findenden Ziegen zählt, die es nur in einer Szene gibt. Ihr könnt aber auch eure eigenen ScriptableObjects mit einem Attribut markieren, und schon können auch diese Objekte an Szenen gebunden werden. Da ich gerade auch an einem System baue, mit dem man Graphen (Loot-Graphen, Sound-Graphen, aber auch Logik-Graphen!) als ScriptableObjects erstellen kann, habe ich da schon krasse Synergien mit gefunden :D

Darüber hinaus gibt's noch jede Menge kleinere Dinge, hier sind die Patch Notes:

- Added SceneBound system
- Made GameEventBase.debug a serialized field which allows it to be used to debug builds
- Improved editor support for scoped variables defined in base classes
- Improved RuntimeSet iteration options
- Improved robustness of editor classes
- Moved MenuItems from "Window" to "Tools"
- Overhauled manual
- Made various small improvements

 

  • Like 1
Link to comment
Share on other sites

  • 2 weeks later...

Achso, hab mich schon gewundert :D

Das neuste Update (inzwischen 1.4.1) enthält das ModuleSettings-System.

Die Erklärung dazu ist viel komplexer als das System selbst... Ich mache ja kein Geheimnis daraus, dass ich kein Freund von "...Manager"-Komponenten bin. Wenn du mich fragst, soll man eher versuchen, dass Objekte sich selbst (ansonsten untereinander) managen, anstatt dafür eine zusätzliche Instanz zu haben. Im Zweifelsfall auch mit statischen Feldern und Methoden in der eigenen Klasse. Geht aber auch nicht immer, manchmal muss einfach ein Service Provider her. Aber auch der sollte nicht Leute dazu zwingen, ihn in eine Szene einzufügen oder sowas. Grundregel bei mir ist: Wenn du eine neue Szene erstellst, sollte es keine Checkliste von Dingen geben, die du erfüllen musst, damit dein Code funktioniert. Du willst eine Spielfigur haben? Zieh dein Prefab in die Szene. Du willst, dass Input funktioniert? Dann solltest du dafür nichts extra tun müssen.

Da gibt's halt zwei Probleme:

  1. MonoBehaviour-Events. Du willst in vielen Services auf z.B. Start, Update oder OnApplicationFocus reagieren können. Dafür empfehle ich einen kurzen Code mit [RuntimeInitializeOnLoadMethod]-Attribut, der ein GameObject erstellt, HideFlags drauf, DontDestroyOnLoad drauf, Worker-Komponente drauf, fertig. Die Komponente ruft dann Methoden in der statischen Service-Klasse auf.
  2. Serialisierte Felder. Viele Services wollen Referenzen auf AudioClips, Sprites usw., vor allem aber auf ScriptableObjects oder Prefabs haben.

Jetzt kann man halt sowas machen wie eine Initialisierungs-Szene, in der alle wichtigen Systeme drin sind, DontDestroyOnLoad drauf, vielleicht noch ein bisschen Editor-Code, damit man den Play Mode in einer beliebigen Szene starten kann und die Init-Szene dann additiv geladen wird... geht schon. Da hat man dann einen Haufen Komponenten drin, die einen auf Pseudo-Singleton machen. Aber eine andere Regel von mir ist: Wenn du einen Klebezettel an etwas im Editor pappen musst, auf dem steht, dass man da bitte die Finger von lassen soll, sonst könnte was kaputt gehen... dann ist der Code nicht besonders gut. Ich will halt einfach den Editor für Game/Level/UI-Designer haben. Und ich will möglichst wenig Restriktionen für sie haben. Wenn ich in einem Team einen neuen Designer habe, dann soll so wenig wie möglich im Editor rumfliegen, worüber der Mensch Bescheid wissen muss. Also bleibt der technische Kram schön im Code und der Design-Kram schön im Editor.

Und damit kommt das neue ModuleSettings-System ins Spiel. Du baust eine statische Service-Klasse und dazu eine Settings-Klasse:

using UnityEngine;
using ThirteenPixels.Soda.ModuleSettings;

internal class InputSystemSettings : ModuleSettings
{
  protected override string title => "My Input System"; // This is optional.
  
  public Sprite gamepadIcon;
}

Mehr musst du nicht machen, dann kümmert sich das System um eine Instanz, die in den ProjectSettings liegt statt in den Assets. Sie taucht dann im Project Settings-Fenster auf:

grafik.png

Dort kannst du alles mögliche zuweisen. In deiner statischen Service-Klasse musst du die Settings dann nur noch so laden:

ModuleSettings.Get<InputSystemSettings>();

und schon hast du die Daten zur Laufzeit!

Bei mir im Projekt kann ich halt einfach eine Szene anfangen und abgesehen von so Unity-Dingen (wie das EventSystem-GameObject...) gibt's da exakt nichts einzurichten. Ich tu das rein, was ich in der Szene vom Design-Standpunkt aus haben will und lasse alles weg, was ich nicht brauche. Verpflichtendes Setup, damit die Systeme funktionieren? Gibt's nicht. Entsprechend einfach ist halt auch Testing. Kurz mal nen Spieler und zwei Boxen in die Szene, schauen ob das mit dem Springen klappt, fertig.

  • Thanks 1
Link to comment
Share on other sites

Hallo

 

Also bei mir hat alles wunderbat geklappt. Dazu kam gleich noch der Erkenntnisgewinn, dass es ja Dinge gibt, die man über alle alle Szenen hinweg braucht. Hatte ich noch gar nicht verinnerlicht. Ein Logsystem ist genau so etwas. Das hatte ich bisher immer an einem GameManager hängen und da ich deine Abneigung kenne, habe ich mich immer schlecht dabei gefühlt. Des Weiteren hätte ich diesen in jeder neuen Szene neu erstellen müssen. So fällt das nun weg.

 

Christoph

  • Like 1
Link to comment
Share on other sites

Hallo

 

Da es vielleicht auch andere interessiert, frage ich hier mal öffentlich und nicht per pm. Nach etwas probieren, stelle ich mir die Frage, wozu ich eine statische Klasse brauche, um auf meine Daten zu zugreifen? Warum greife ich nicht direkt per 

ModuleSettings.Get<InputSystemSettings>();

auf die benötigten Daten zu? Jetzt habe ich folgendes:

public static class GameService
    {
        public static Log GetLog()
        {
            return ModuleSettings.Get<Log>();
        }
    }

und greife dann per:

GameService.GetLog().Write("Debug","Enter WalkState");

darauf zu. Warum der Umweg über die statische Klasse? Habe ich etwas komplett falsch verstanden?

 

Danke

Christoph

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

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

Loading...
 Share

×
×
  • Create New...