Tagebuch: Industry City

So. Bin noch immer dran an den „aufräumarbeiten“. Denn durch Playmaker konnte ich auch ein weiteres Script durch eine Aktion ausmerzen (den sogenannten BuildButton). Dieser hatte ursprünglich 2 Aufgaben:

  • Sende das richtige Signal
  • De-/aktiviere dich basierend auf dem Signal.

Die zweite Aufgabe wurde ja generell etwas vereinfacht. Da ich nun ein komplett neues Menü im Build-Mode anzeige, müssen alle Buttons verschwinden. Von daher hab ich die Animation eine Ebene höher auf das Panel gelegt (was über PlayMaker gesteuert wird + getestet ist).

Somit blieb nur noch das Signal übrig. Das kann aber auch PlayMaker übernehmen. Und letztendlich tut es das auch. Da ich jetzt für jedes Signal eine Aktion hab, hab ich funktional die Buttons alle fertig - sodass auch der hässliche Button am oberen Ende verschwinden konnte. Das Ergebnis schaut dann bisher so aus:

Damit hätte ich diesen (etwas sehr viel größeren als gewohnt Task) auch fast fertig. Zumindest von den Anforderungen her. Was mir da fehlt ist letztendlich nur, dass ich verbiete das Gebäude übereinander platziert werden können.

Technisch gesehen hab ich noch etwas mehr zu tun. Da ich mit PlayMaker noch keine Erfahrungen hab, musste ich viel ausprobieren. Deswegen hab ich in dem Bereich natürlich kein TDD betrieben. Somit fehlen mir noch tests für:

  • Alle meine Aktionen
  • Die StateMachine die im Video zu sehen ist

Seit meinem letzten post vor 2h hab ich gefrühstückt und mir mal angeschaut, wie ich meine Actions testen kann. Was zuerst ausgesehen hat, als ob es total einfach wäre - stellte sich als komplexe Aufgabe heraus.

Denn die „netten“ Leute von PlayMaker haben wohl so überhaupt keinen Bock darauf, dass man deren Zeug weg mocken könnte und haben alles dicht gemacht. Also hab ich einen weiteren Layer einbauen müssen, welche meine Listener-actions nun nutzen. Bevor jetzt ein Event-Call an PlayMaker geht, geht dieser call erstmal durch meinen neuen Layer - welchen ich mocken kann. Denn mir war es einfach nicht möglich im Test abzufangen, ob meine Action jetzt einen neuen State triggern wird oder nicht -.-.

Als nächstes durfte ich mit dem Feld rumkämpfen, welches den Layer hält. Ist es public, dann wird es im Editor angezeigt - aber der Editor hat keinen support für den Typ also fliegt es mir um die Ohren. Wer das jetzt nicht nachvollziehen kann - nicht wichtig. Es war einfach nur nervig wie Sau, dass ich per Try-Error herausfinden musste, wie ich jetzt meinen Layer halten kann.

Nachdem ich das hatte, konnte ich ENDLICH einen Test grün bekommen.

Zufrieden damit war ich aber noch immer nicht. Denn ich wollte nicht für jeden Signal-Listener einen eigenen Test aufsetzen. Da sich diese kaum unterscheiden würden hab ich mich für eine parameterisierte Variante entschieden.

Mein Test schaut dann so aus:

using FakeItEasy;
using Game.DI;
using Game.FSM;
using Game.FSM.SignalListener;
using HutongGames.PlayMaker;
using NUnit.Framework;
using Zenject;

namespace Tests.UnitTests.Game.FSM.SignalListener {
    public class SignalListenerTest : ZenjectUnitTestFixture {

        private IFsmUtil util;
        private FsmEvent fsmEvent;

        public override void Setup() {
            base.Setup();

            SignalInstaller.Install(Container);
            
            util = A.Fake<IFsmUtil>();
            fsmEvent = new FsmEvent("Generic FSM Event");
        }

        [TestCaseSource(typeof(SignalListenerTestCases), nameof(SignalListenerTestCases.Signals))]
        public void CheckSignals(SignalAction action, object signal) {
            SetupAction(action);

            action.OnEnter();
            
            FireSignal(signal);

            A.CallTo(
                () => util.Event(A<FsmEvent>.That.Matches(
                    receivedEvent => receivedEvent == fsmEvent
                ))
            ).MustHaveHappenedOnceExactly();
        }

        private void FireSignal(object signal) {
            Container.Resolve<SignalBus>().Fire(signal);
        }

        private void SetupAction(SignalAction action) {
            action.targetEvent = fsmEvent;
            action.SetFsmUtil(util);
            action.Init(Container);
            action.Init(new FsmState(new Fsm()));
        }
    }
}

Und meine Datenquelle für den Test so:

using System.Collections;
using Core.Buildings;
using Game.FSM.SignalListener;
using Game.Signals;
using NUnit.Framework;

namespace Tests.UnitTests.Game.FSM.SignalListener {
    public static class SignalListenerTestCases {

        public static IEnumerable Signals() {
            yield return new TestCaseData(new ExitBuildModeAction(), new ExitBuildModeSignal());
            yield return new TestCaseData(new EnterBuildModeAction(), new EnterBuildModeSignal(BuildingType.Factory));
        }
        
    }
}

Sollte ich einen neuen Listener haben, dann brauch ich also in Zukunft nur eine Zeile Code hinzuzufügen um diesen zu testen - anstatt einen kompletten test aufzusetzen :slight_smile:.

So. Habe eben meine Tests für die Sender von Signalen fertig geschrieben. Die funktionieren ähnlich wie die Listener. Nur konnte ich meine Tests etwas verbessern.

Wie Ihr in meinem Codebeispiel vom letzten mal sehen könnt, hatte ich meine Generics im Test aufgelöst und mit object gearbeitet. Per Zufall hab ich gerade rausgefunden, dass ich auch Generics für den Test an sich nutzen kann. Somit schaut mein Test jetzt so aus:

using FakeItEasy;
using Game.DI;
using Game.FSM;
using Game.FSM.SignalListener;
using HutongGames.PlayMaker;
using NUnit.Framework;
using Zenject;

namespace Tests.UnitTests.Game.FSM.SignalListener {
    public class SignalListenerTest : ZenjectUnitTestFixture {

        private IFsmUtil util;
        private FsmEvent fsmEvent;

        public override void Setup() {
            base.Setup();

            SignalInstaller.Install(Container);
            
            util = A.Fake<IFsmUtil>();
            fsmEvent = new FsmEvent("Generic FSM Event");
        }

        [TestCaseSource(typeof(SignalListenerTestCases), nameof(SignalListenerTestCases.Signals))]
        public void CheckSignals<T>(SignalAction<T> action, T signal) {
            SetupAction(action);

            action.OnEnter();
            
            FireSignal(signal);

            A.CallTo(
                () => util.Event(A<FsmEvent>.That.Matches(
                    receivedEvent => receivedEvent == fsmEvent
                ))
            ).MustHaveHappenedOnceExactly();
        }

        private void FireSignal(object signal) {
            Container.Resolve<SignalBus>().Fire(signal);
        }

        private void SetupAction<T>(SignalAction<T> action) {
            action.targetEvent = fsmEvent;
            action.InitForTest(Container, util);
        }
    }
}

So langsam fühl ich mich wohl mit PlayMaker und testen. Dürfte auch jetzt wieder alles voll abgedeckt haben :slight_smile:.

Was gut ist, denn damit hab ich meine technische Schuld abgearbeitet, die ich mit PlayMaker eingeführt hab.

Das bedeutet auch, dass ich als nächstes den letzten Punkt für die Story angehen kann. Nämlich verhindern das Gebäude übereinander platziert werden können.

So. Hab die Story praktisch fertig. Man kann Gebäude nicht über andere bauen und somit ist das Baumenü eigentlich fertig. Eigentlich.

Letztendlich ist es natürlich doch relativ komplex. Deswegen läuft gerade die erste refactoring Runde. Und eine zweite - größere - ist auch schon geplant.

Der Punkt ist der. Würde ich jetzt mit dem Projekt starten, dann würde ich mehr auf PlayMaker setzen. Dementsprechend möchte ich sehen, wieviel Aufwand es ist die bisherige Logik in PlayMaker-Actions umzuschreiben und diese zu nutzen. Dann wäre das System auch einheitlich.

Zum Schluss natürlich noch ein Video vom aktuellen Stand der Dinge :slight_smile::

Shit. Meine Umsetzung passt doch noch nicht. Ich nutze Raycast zum prüfen ob ein Gebäude an einer Stelle ist. Dummerweise hab ich false-positives weiter hinten.

Muss also eine andere Lösung finden. Vielleicht arbeite ich mit einer Registry oder einfachen Map. Immerhin sind die Koordinaten alle absolute Werte und wenn ich ein Level speichern möchte, brauche ich die Informationen eh.

Von daher gesehen ist es eigentlich gar nicht so schlecht, dass der Bug aufgetaucht ist.

Wie sieht es aus, in dem letzten Video hast du am Ende ein Gebäude gebaut ohne Straßenanbindung. Willst du das zulassen (bzw verbinden sich die Gebäude dann zu einem wenn es der gleiche Typ ist) und man wird dann irgendwie benachrichtigt dass da eine Straße fehlt, oder soll der Bau direkt verhindert werden?

Bisher schaut es so aus, dass ich den Nutzer möglichst die Freiheit lassen möchte wie und wo er baut. Denn manchmal macht es Sinn vom Ziel aus zu bauen (kann gerade kein konkretes Beispiel nennen - aber ich meine das in Spielen wie Anno & Co durchaus schon so gemacht zu haben).

Interessant wird es dann ja erst, wenn man Warentransporte über die Straßen jagt. Denn dann muss eine machbare Route bestehen. Dementsprechend wird es hier eine Überprüfung/Warnung geben müssen, ob das Streckennetz korrekt ist.

Vermutlich wird das im ersten Schritt auch sehr simpel gehalten in dem ich bei den Autos eine Prüfung einbaue ähnlich wie beim Tower-Defender damals: bleibt eins stecken, dann gibt es eine Meldung.

So. Die letzten Tage bin ich leider zu nicht wirklich was gekommen - dafür hab ich aber heute morgen schon was machen können. Nämlich meinen Bug zu fixen.

Ich spiele gerade die neue Version auf mein Handy. Was heißt: ich hab es schon fertig umgesetzt :slight_smile:. Und ich würde auch mal sagen, dass das jetzt definitiv die bessere Variante ist. Denn ich komme jetzt mit einem Raycast weniger aus und ich hab im Speicher die Informationen über meine Gebäude und Resourcen einfach greifbar.

Dennoch möchte ich versuchen, das ganze auf PlayMaker umzuschreiben. Ich muss mal schauen, wie ich das dann jetzt am besten mache.

Momentan glaube ich, werde ich den Weg gehen: das was ich jetzt habe, sehe ich als Prototypen an der schon gut was kann. Da ich jetzt schon das Bedürfnis nach einem Refactoring hab, werde ich glaub am besten einfach ein frisches Projekt aufsetzen. Viele Teile vom Prototyp kann ich ja durchaus übernehmen. Und ich kann viel selektiver die Sachen ins neue Projekt ziehen.

So. Ich hab jetzt einen neuen Branch von Master erstellt, welcher ziemlich jungfreulich war :slight_smile:. Dort hab ich auch schon ordentlich aufgeräumt.

Zudem hab ich einiges vom Prototypen rüber gezogen. Was ich jetzt z.B. behalten hab sind:

Low-Level Layer
Das ist meine Abstraktionsschicht zu Unity. Da Unity selber viele statische Methoden anbietet die man nicht mocken kann (wie z.B. Input.GetMouse(0)) - brauche ich einen Layer dazwischen. Ein anderes Beispiel wären Raycasts.
Besonderheit hier: man kann diese Layer nicht testen.

Projekt Einstellungen
Ich hab natürlich schon viele Einstellungen vorgenommen die ich auch weiterhin behalten möchte. Diese hab ich übernommen

Prefabs
Es gibt einige Prefabs die ich erstellt habe (Gebäude, Straßen, Kamera-Arm, …) die ich auch weiterhin verwenden möchte

Libs / Externes Zeug
Mein ganzes Zeug von Extern möchte ich natürlich auch weiter verwenden. Also konnte ich das rüber ziehen

3D Modelle
Ohne Frage, die möchte ich nicht nochmal neu erstellen

Code den ich heute geschrieben hab (zum Großteil)
Den Code den ich heute entwickelt hab, könnte man hauptsächlich als Backend vom Spiel ansehen. Deswegen kann ich den + dessen Tests übernehmen

Anderer Code
Generell konnte ich recht viel Code übernehmen. Ob ich ihn behalte ist noch eine andere Sache ^^

Shader
Meinen Shader hab ich natürlich auch übernommen

Also wie Ihr seht: ich fange definitiv NICHT bei 0 an. Zumal ich ja schon einige Probleme gelöst und im Prototyp dokumentiert hab.

So. Ich hab mal mit dem UI angefangen und meine StateMachine dafür entwickelt:

Überraschenderweise war es auch sehr gut zu testen:

[UnityTest]
[TestCaseSource(typeof(BuildMenuTestData), nameof(BuildMenuTestData.Buttons))]
public IEnumerator Build(string buttonName) {
    yield return WaitForGameStart();
    
    AssertRootState();
    
    ClickBuildButton(BuildMenu.Prop<Button>(buttonName));

    A.CallTo(() => mapService.PlacePreview(A<GameObject>._)).MustHaveHappenedOnceExactly();
 
    ClickConfirmButton(ConfirmMenu.Accept);

    A.CallTo(() => mapService.SubmitPreview()).MustHaveHappenedOnceExactly();
    
    AssertRootState();
}

[UnityTest]
[TestCaseSource(typeof(BuildMenuTestData), nameof(BuildMenuTestData.Buttons))]
public IEnumerator Cancel(string buttonName) {
    yield return WaitForGameStart();
    
    AssertRootState();
    
    ClickBuildButton(BuildMenu.Prop<Button>(buttonName));
    
    ClickConfirmButton(ConfirmMenu.Cancel);

    A.CallTo(() => mapService.CancelPreview()).MustHaveHappenedOnceExactly();
    
    AssertRootState();
}

Damit hab ich alles abgedeckt und auf das nötigste reduziert. Es werden Animationen getriggert und es wird folgendes Interface verwendet:

namespace Game.Map {
    public interface IMapService {
        void PlacePreview(GameObject prefab);
        void SubmitPreview();
        void CancelPreview();
    }
}

Damit wird auch meine komplette Logik sehr viel einfacher. So brauche ich z.B. den Lookup-Service vom Prototypen nicht mehr (welcher mir die unterschiedlichen Prefabs bereit gestellt hatte). Denn das sollte ich alles in der StateMachine lösen können.

Ich muss sagen, ich war heute echt fleißig. Ich hab ziemlich viel umgesetzt bekommen und bin fast da, wo ich beim Prototypen aufgehört habe.

Interessant finde ich vor allem, mit wie viel weniger Code ich bisher ausgekommen bin. Die StateMachine ist schon echt cool. Vor allem weil ich das Gefühl hab, dass es mir hilft mich besser zu fokusieren.

Zumal ich auch das Gefühl hab, dass meine Tests etwas einfacher und präziser geworden sind.

Ich hätte nicht gedacht, dass das ein VisualScripting-Tool so einen Unterschied machen würde.

Und schon direkt wieder froh, dass ich alles so gut getestet habe.

Hab nämlich mal meinen Code etwas umstrukturiert (hauptsächlich Klassen umbenannt und von A nach B geschoben). Leider verliert der PlayMaker dann die Referenz zum Script und somit funktioniert da nichts mehr.

Betroffen war jeder meine selbst geschriebenen Aktionen.

Gut war, dass diese mit minimalem Konfigurationsaufwand auskommen und wirklich sehr selbsterklärend sind (wie ich jetzt ja zwangsläufig prüfen konnte).

Aber wenn einmal alles durcheinander geworfen ist, ist man doch froh wenn man es nochmal durchtesten lassen kann. Und 77 Grüne Tests geben mir nun eine gewisse Sicherheit, dass ich nichts kaputt gemacht hab :slight_smile:.

So. Mehr oder weniger dürfte ich jetzt tatsächlich das an Logik haben, was ich auch vorher hatte. Das einzige was fehlt ist ein Update auf das NavMesh. Aber ansonsten passt alles :slight_smile:.

Und meine komplexeste StateMachine ist nach wie vor das UI. Oben hatte ich diese ja schonmal gepostet, mittlerweile schaut das ganze so aus:

Von der Architektur her gefällt es mir soweit auch sehr gut. Die StateMachine an sich kennt kaum Daten. Das ist alles abstrahiert in darunter liegende Services. Sie ruft tatsächlich fast nur einzelne Methoden auf und geht dann weiter.
Gerade da lag auch anfangs meine Sorge. Wie schiffe ich elegant Werte von A → B (was ich mit den Signalen ja durchaus gemacht habe). Erstaunlicherweise hat sich alles einfach so ergeben, dass ich bei den StateMachines selber kaum was wissen brauch.
Dementsprechend einfach fallen auch die Tests aus. Ich kann die Interfaces mocken, so dass die einen bestimmten Weg ergeben und prüfe dann, ob eine Service-Methode mit bestimmten Parametern aufgerufen wird. Ist das der Fall, dann passt die komplette Machine.

Ein weitere Vorteil von dem ganzen: Man sieht visuell was passiert! Ich hatte einmal einen Logikfehler drin bzgl. true/false. Auch mein Test war dementsprechend falsch geschrieben. Als ich es ausgeführt hab, hab ich dann in der visuellen Präsentation gesehen, welcher Weg genommen wurde und da ist mir aufgefallen, dass einmal eine falsche Abbiegung drin war. Kurz nachgedacht und dann wurde es klar.
Ohne die visuelle Hilfe hätte ich das erst viel später gemerkt. Nämlich dann, wenn ich die „Auswertung“ für das ganze geschrieben hätte.

So. Ich würde mal behaupten: den Stand der Dinge hab ich nun erreicht:

Es fahren zwar (noch) keine Autos. Aber das war auch nie Teil der Story. Hatte das damals nur schon drin bevor ich die Story angefangen hab (also in der PoC-Phase).

Das blaue, dass ihr auf dem ganz linken Panel sehen könnt ist übrigens die Info, wo sich die Autos später bewegen können :slight_smile:.

Was auch auffallen könnte: Es gibt keine Resourcen auf der Karte. Die gab es das letzte mal auch nur deswegen, weil ich meine Entwicklung nicht mit einer leeren Map begonnen hab.

Es wird aber eine Kleinigkeit sein, dieses bereit zu stellen (muss sie ja nur platzieren). Hatte mir kurzzeitig überlegt, ob ich das dem Spieler überlassen sollte, wo die platziert werden können. Aber ich denke, für die MVP version gebe ich das auf jeden Fall noch vor (genauso wie den Shop).

Ich könnte kotzen. Weil Zenject-Tests am besten mit Resourcen arbeiten, hab ich alle Prefabs in einen Resourcen-Ordner gepackt. Kam mir komisch vor - aber dachte mir es wäre egal. Mit in die Apk müssen die später ja so oder so.

Nach aber z.B. einem Build bekommen diese Resourcen neue IDs. Folge: Komplette Logik bricht. Also musste ich das umbauen. Hab ich.

Jetzt will ich das ganze auf dem Handy testen. Läuft nicht - meine Raycasts funktionieren nicht. Nochmal im Unity-Editor ausprobiert. Da funktionierts. Mit Prototyp verglichen. Ich finde keine differenzen.

Errors fliegen im Log-cat anscheinend auch keine. Bin hier schier am verzweifeln!

Jetzt wo ich dachte ich wäre fertig - kommt gerade viel auf mich zu.

  1. Das oben genannte
  2. Ich war eigentlich noch in der Prototypen-phase. Ich dürfte noch gar nicht eine so konkrete Implementierung haben
  3. Ich hab einen Bottom-Top-Approach gewählt anstatt einen Top-Bottom

Auch wenns weh tun wird. Ich glaub ich geh zurück auf Master und beginne nochmal RICHTIG mit einem Prototypen. Meine Logik behalte ich in dem Branch. Aber ich würde anders starten.

Erstmal eine Referenzstadt aufbauen. Und da dann die ganzen MVP-Simulationselemente rein. Danach erst sollte ich Sachen einbauen, damit der Spieler nach und nach Einfluss nehmen kann.

Also, ich hab gestern nochmal mit der Prototypenphase begonnnen und mich nochmal auf das besonnen, was ich haben möchten.

Und auch wenn ich TDD sehr gerne mag - für den Prototypen werde ich glaub besser drauf verzichten. Erstmal möchte ich die Basisprobleme lösen und dann kümmere ich mich drum. Ich hoffe, dass ich viel vom Prototypen wiederverwenden kann, denn ich weiß ja etwa wie mein Code auszusehen hat, damit dieser nachträglich testbar sein wird. Es bremst einfach doch zu sehr aus und ich glaube schnelles Refaktoring ist mir am Anfang lieber.

Jetzt ist erstmal wichtig, dass ich mich auf den Kern des Spiels fokusiere - und das ist NICHT das platzieren von Objekten in der Welt.

Und den Kern von seinem Spiel auszumachen ist gar nicht mal so einfach, wie man meinen könnte. Ich dachte: meine Fabriken - Produktion. Für die Produktion brauche ich Waren, also hab ich mich erstmal um die Wegfindung der Autos gekümmert, damit diese Waren bringen können.

Bam falsch. Das Kern-Ding ist: Produktion -> Verkauf. Also wären meine Kerngebäude: Fabrik und Shop. Den Shop kann man erstmal aus der Rechnung rausnehmen (#Fabrikverkauf). Dementsprechend produziert die Fabrik alle X Einheiten Y Geld.

Das hab ich auch gestern Umgesetzt. Die Fabrik gibt alle 5 Sekunden 5 Geld (ist zufällig der gleiche Wert). Hinzu kommt eine Datenhaltung im Hintergrund die ich verwenden kann um ein Label mit Geldwert anzuzeigen.

Ich würde mal behaupten: DAS ist mein Kern. Der Spieler prodziert also Geld. Und VON HIER startet auch mein iterativer Prozess. Der im wesentlichen immer das selbe Problem löst: MIT WAS kann ich das ganze sinnvoll erweitern um meinen Kern interessanter zu machen?

In einem Video was ich mal zum Thema Spieleentwicklung gesehen hab, haben die das mit einer Zwiebel verglichen. Der Kern de Spiels ist das innere der Zwiebel und alles was ich entwickle muss sich um den Kern legen - es muss also alles einen Mehrwert dazu bieten.

So und hier mal ein Update zu meinem Prototypen. Nachdem ich den Fabrikverkauf hatte, wollte ich den Shopverkauf. Der ist auch schon mehr oder weniger da:

Im Video seht Ihr rechts die Fabrik-Komponente. Dort verändere ich den Wert von „Resources“. Die Fabrik startet automatisch mit der Produktion, wenn der Wert > 0 ist. Für jede Resource werden 2 Produkte erstellt.

Das Auto prüft das Ziel. Hat die Fabrik Produkte zum Abholen, dann fährt es los. Das Auto hat eine variable Frachtgröße (hier: 1) und nimmt das Produkt auf (was den Wert in der Fabrik reduziert). Danach wird zum Shop zurück gefahren.
Dort angekommen wird der Verkauf simuliert. Dabei wird einfach der Zähler gelöscht und ein entsprechender Betrag auf das Konto gebucht.

Der nächste Schritt wäre dann, dass die Fabrik selber Autos los schickt um Resourcen zu farmen.

Mein Prototyp ist gewachsen und ich hab gefühlt einen Meilenstein sehr einfach erreicht. Denn es findet tatsächlich eine wirkliche Simulation statt! Und die schaut wie folgt aus:

Das Auto rechts von der Fabrik (blaues Gebäude) fährt zum Stein. Dort lädt es sich voll und bringt die Resourcen zur Fabrik. Dort angekommen wird entladen.

Sobald die Fabrik Resourcen hat, fängt diese an zu arbeiten. Aus einer Resource werden derzeit 2 Produkte.

Jetzt kommt der Shop und sein Auto ins Spiel. Dessen Auto hat als Ziel die Fabrik. Soblad die Fabrik ein Produkt im Lager hat, erkennt dass das Auto und fährt los. Das Auto lädt sich voll und fährt zurück (derzeit so eingestellt, dass es auch auf weitere Produkte warten würde, wenn es noch Platz hätte). Dann bringt es die Produkte zum Shop wo alle Sekunde 1 Produkt verkauft wird (worauf sich der Geldbetrag erhöht).

Und so schaut das ganze in „Action“ aus:

Ich finde es witzig wie die Autos sich noch im fahren drehen. Absicht? Interpolation der Animation?