Tagebuch: Industry City

Hab die letzten Tage hauptsächlich Krank auf der Couch verbracht und deswegen nicht viel machen können. Dafür war ich heute morgen recht kreativ. Zum einen gibt es noch ein paar weitere Views zum anderen glaube (und hoffe ich) so langsam zum Ende zu kommen. Das Big-Picture schaut nun so aus:

Neu ist die Anzeige, was ein Gebäude an Geld kosten wird. Dafür hab ich Tabs eingeführt und für den Shop schaut das z.B. so aus:

Als ich mir den Prototypen vo rkurzem angeschaut hatte, ist mir zudem eine Sache aufgefallen. Ich hab vorgesehen, dass man Gebäuden Namen geben kann, verwende diese aber nirgends. Also blieben mir zwei Möglichkeiten:

  • Diese Möglichkeit entfernen
  • Einen sinnvollen Verwendungszweck dafür finden

Einen Zweck hab ich auch gefunden. Nämlich das man bei den Routen den Namen sehen kann. Vor allem wenn mehrere Fabriken das gleiche Produzieren kann das für die Lastenverteilung sinnvoll sein zu wissen, woher jetzt eine Resource bezogen wird.

Neben kleineren Änderungen hab ich noch eine die sich auf den Fokus bezieht. Die Buttons zum kaufen von Fahrzeugen werden im ersten Schritt keine Funktion haben. Es ist naheliegen, dass diese einen Dialog triggern der den Kauf bestätigen lässt. Um den Scope aber nicht weiter aufzublasen (der UI-Prototyp ist bereits jetzt schon sehr groß) möchte ich das auf später verschieben.

Gefühlt dürfte aber jetzt alle information dar gestellt werden, die auch tatsächlich schon in der Simulation vorhanden ist. Auch sollte wirklich alles sinnvoll und begründet sein was vorhanden ist und es folgt gewissen Richtlinien. So folgen z.B. die Farben von Buttons einer Bedeutung (blau: ändern, grün: übernehmen, rot: vorsicht). Dadurch glaube ich ein recht einheitliches Bild geschaffen zu haben.

So. Ich glaube, dass ich alles habe und hab deswegen mal mein Board basierend auf dem Plan erstellt. Hat etwas gedauert, denn ich hab daraus 5 User-Stories mit 21 Subtasks erstellt.

Für die Umsetzung werde ich auch glaub einfach den Branch vom UI-Prototyp mergen. Letztendlich befindet sich darin ja nicht viel mehr als das UI und so muss ich es nicht nochmal nachbauen - sondern lediglich mit Leben füllen. Außerdem hatte ich mir die Mühe gemacht und viele Komponenten sinnvoll als Prefab hinterlegt.
Dementsprechend sollte das alles recht sauber sein.

So. Den einen Task hätte ich sogar schon erledigt. Und zwar wird nun das Geld korrekt angezeigt. Meine Simulation rennt gerade und die kleine Stadt ist wirtschaftlich erfolgreich wie man hier sehen kann :stuck_out_tongue: :

Gestartet hat das ganze bei 100 und durch den Verkauf von Wasserflaschen sind wir bei 145 :slight_smile:.

Kleine Änderung - aber ist mal wieder schön zu sehen, wie sich da was bewegt.

Es ist immer so ein Glücksspiel, wenn man Zeitbasierten code testen darf. Ich hab nämlich die bestehende Logik für die monatlichen Zahlungen umgebaut von der Schleife:

  1. Warte 10s
  2. Kassiere die Zahlungen

In etwas, was auch 10s wartet, aber pro Sekunde 25 Signale feuert, damit ich darauf basierend die Kalenderanzeige updaten kann.
Für das Warten hatte ich WaitForSeconds von Unity verwendet, weil es angenehm einfach war. Und lesbar.
Hat auch wunderbar funktioniert. Nur leider nicht für den Test. Und basierend auf meinen Beobachtungen liegt das an den beschleunigten Tests (der Test beschleunigt als mal kurzzeitig die Zeit um Faktor 100, sodass 10s nur noch 0.1s dauern).

Meine Vermutung war dann, dass der LifeCycle von Unity3D da einfach nicht mehr mit klar kommt. Denn meine Theorie schaut so aus.

Ich hatte innerhalb einer Schleife WaitForSeconds aufgerufen mit einem Wert von 1/25 (0.04). Dieser Wert wird jetzt nochmal um den Faktor 100 beschleunigt => 0.004. Das sind 4ms. Ich nehme mal an, dass es einfach länger dauert, den LifeCycle zu durchlaufen als 4ms. Sagen wir mal, ein Durchlauf würde 10ms dauern. Dann würde der Code 250ms brauchen. Ich warte aber nur 100ms bevor ich den Status vom Spiel überprüfe.

Ich bin vor kurzem zufällig über PixelArt gestoßen. In einem Discord-Channel hat einer sein Spiel vorgestellt und wie er mit PixelArt begonnen hat. Das ganze war ein YT-Video und seit dem bekomme ich über YT immer mal wieder PixelArt vorgeschlagen.

Das hat mich jetzt durchaus mal gereizt und ich hab jetzt mal ein paar Tools ausprobiert. Und ich find mit Aseprite hab ich tatsächlich eine nette kleine Szene hinbekommen :slight_smile:. Da ich es in der Trial-Version nicht speichern kann, muss halt ein Screenshot her:

Und es war tatsächlich nicht schwer. Viele Kommentare sagen, dass Pixelart auch nicht wirklich schwer zu lernen sein soll. Jetzt überlege ich mir ob ich mir das Tool kaufe und nebenher mir mal die Fähigkeiten aneigne.

Habs mir jetzt einfach mal gegönnt. Zumal ich mir gedacht hab. Denn vllt kann ich die Produkt-Bilder durch Pixel-Art ersetzen (noch nicht sicher, ob ich das möchte). Aber das hier kam raus, als ich mit dem Wasserglas rumgespielt hab:

Sprite-0004

Und das hier ist so nebenbei entstanden. Auf der Couch vorm Fernseher.
Sprite-0003

Tu ich im übrigen sicher nicht. Nachdem ich mir nochmal kurz meine mockups angeschaut hab wurde ganz schnell klar: blöde Idee xD.

So. Die erste Story hab ich abgeschlossen und zugleich auch noch einen Bug gefixt, der nur aufm Handy aufgetreten ist. Der Shop wurde falsch initialisiert und hat deswegen nichts verkauft. Das hat für erst mal für ein paar Schreckminuten gesorgt, weil ich mir nicht erklären konnte, wo jetzt das Problem ist.
Nachdem ich aber die App nochmal als Development-Build gebaut hab und in LogCat reingeschaut hab wurde es klar.

Nun funktioniert alles wie es soll :slight_smile::

Jetbrains zeigt Humor? Hab mich gerade über diesen Hinweis „gefreut“ ^^:

image

Viel vom Hauptmenü fehlt mir auch nicht mehr. Man kann es nun aufrufen + pausiert das Spiel, sobald man den GameScreen verlässt:

Hab heute Abend mal wieder etwas weitergemacht. Man kann nun den Namen der Fabrik in der entsprechenden Ansicht sehen + ändern. Dazu wieder verwende ich einfach den Namen des GameObjects. Netter Nebeneffekt an dem ganzen: Man kann es in der Hierarchie von Unity sehen.

Wie immer gilt: Ein Video sagt mehr als 1000 Worte :slight_smile::

Hab heute mal ein wenig refactored. Meine komplette UI-Logik war innerhalb einer StateMachine, was mittlerweile unübersichtlich wurde.

Danach sind mir einige Tests gebrochen und es lag im wesentlich daran, dass der Test an einem früheren Abschnitt war als die StateMachine. Dementsprechend war Logik nicht bereit weswegen der Test fehl schlug.

Ich dachte bei dem anderen Test wäre es der gleiche Fall, aber ich habs einfach nicht hinbekommen. Nachdem mehr Zeit vergangen ist als man Stolz drauf sein kann, hab ich es mal manuell getestet. Und dann kam die „Hoppla, den Fall hab ich ja garnicht übernommen“-Erkenntnis ^^.

Aber hey, dafür sind ja letztendlich die Tests da. Man macht ein Refactoring, vergisst was und der Test nervt einen :stuck_out_tongue:. So ist richtig.

So. Ich hab jetzt drin, dass man das Produkt einer Fabrik ändern kann. Was am längsten gedauert hat, war der IntegrationsTest dafür. Nicht, weil er kompliziert wäre - nein. Ich hatte nur (nachdem heute morgen) absolut keinen Bock einen zu schreiben. Überaschenderweise hat dieser aber absolut keine Probleme gemacht. Ich hatte den einmal geschrieben und hat direkt auf Anhieb gepasst oO.

Das Video dazu hab ich gleich 2x hochgeladen, einmal auf YT und einmal auf Twitter:


Warum YouTube? Ich hatte auf Twitter erst das falsche Video hoch geladen, dachte Twitter kürzt mein Video auf 30 Sekunden und habs auf YouTube hochgeladen. Da ist mir dann aufgefallen, dass ein anderes Video was direkt neben diesem lag die Laufzeit hatte, die mir auf Twitter angezeigt wurde. Da hab ichs nochmal so versucht.

In Zukunft werde ich vllt auf YT verzichten und direkt Twitter nutzen zum verteilen von den Videos.

Ich denke gerade auch über einen neuen Weg nach, wie ich meine Objekte besser für Tests auffindbar mache.

Bisher hab ich das so gelöst, dass ich „einzigartige“ Klassen habe, die mir Referenzen auf die Objekte geliefert haben. Z.B. sowas hier:

public class UiManager : MonoBehaviour {
    [Header("UIs")]
    [SerializeField] private GameObject gameUi;
    [SerializeField] private MainMenu mainMenu;
    [SerializeField] private FactorySettings factorySettings;

    public GameObject GameUi => gameUi;
    public MainMenu MainMenu => mainMenu;
    public FactorySettings FactorySettings => factorySettings;

}

Das hat aber ein paar Nachtteile.

  1. Sie bieten keinen wirklichen Mehrwert / Keine Funktionalität.
  2. Ich muss für jede neue Komponente den Code anpassen (= aufwendige Pflege)
  3. Muss bereit stehen, bevor der Test bereit steht.
  4. Demotiviert (es fühlt sich einfach verkehrt an)

Darum versuche ich gerade einen anderen Weg, der in einem kleinen Test eigentlich ganz vielversprechend ausschaut. Orientiert hab ich mich dabei an assertJ (Swing). Letztendlich läuft dort alles über die Namen von Komponenten.

Zwar hat jedes GameObject in Unity einen Namen - der ist aber viel zu leicht zu ändern. Und ich würde den auch eher als Information für den Entwickler ansehen. Dementsprechend wird der gerne mal geändert. Von daher gesehen, kann ich mich darauf nicht verlassen.

Darum gehe ich den sichereren Weg und hab mir ein MonoBehavior namens Named erstellt. Das bringt folgende Vorteile:

  1. Der Wert bleibt Konstant und wird mit Logik verknüpft

  2. Ich kann Ihn mir basierend auf dem Namen des Objektes generieren lassen.

    public class Named : MonoBehaviour {
    
     [SerializeField] private string objectName;
    
     public string ObjectName => objectName;
     
    }
    

Und im Editor schaut das ganze dann so aus:
image
Momentan schaut die Logik von Generate Name so aus:

  1. Nimm denn Namen vom GameObject
  2. Packe " button" dahinter, wenn eine Button-Komponente präsent ist (kann man für andere Use-Cases erweitern)

Und mithilfe dieser Query-Klasse komme ich einfach an die Objekte ran:

public static class Query {

    public static T QueryObject<T>(string objectName) {
        return LocateTarget<T>(objectName, Object.FindObjectsOfType<Named>());
    }

    public static T QueryObject<T>(this MonoBehaviour self, string objectName) {
        var namedComponents = self.GetComponentsInChildren<Named>();
        
        return LocateTarget<T>(objectName, namedComponents);
    }

    private static T LocateTarget<T>(string objectName, Named[] namedComponents) {
        foreach (var named in namedComponents) {
            if (named.ObjectName == objectName) {
                return named.GetComponent<T>();
            }
        }

        return default;
    }
}

Ich hab das ganze mal in diesem Test ausprobiert - und es funktioniert wunderbar:

private void StartGame() {
    Query.QueryObject<Button>("continue game button").Click();
    AssertGameUiIsVisible();
}

Bin mit dem neuen System echt mehr als zufrieden. Hab hier mal ein Demo-Video davon gemacht, indem man sieht, wie ein kompletter Test damit aussehen kann:

Das hier geht glaub eher an die Leute, die so Zeug eher gemieden haben wie ich. Für andere wird das, was ich jetzt schreibe, vermutlich offensichtlich sein.

An der Stelle schonmal ein kleines Update. Auch wenn ich noch keinen einzigen Follower hab (wieso solls im Internet anders sein als im richtigen Leben?), so lohnt es sich glaub doch schon durchaus. Denn ich wurde schon 2x von Bots geretweetet die über 3K-Follower haben. Auch sehe ich Likes von immer mal wieder den selben Personen.

Ich bin deswegen zuversichtlich, dass mir Tweeter später einige Spieler liefern kann und wird :slight_smile:.

Bin wieder etwas weiter. Zum einen habe ich mal einen großen IntegrationsTest in 3 kleinere aufgeteilt. Damit dürfte ich wieder etwas Übersicht reinbekommen haben :slight_smile:. Zudem hab ich eine Basisklasse abgeleitet, die mir nützliche Hilfsmethoden anbieten soll.

Desweiteren hab ich jetzt die Route-Buttons fertig:
image

Außerdem hab ich mich um ein Problem kümmern dürfen, bei dem GameObjects nicht korrekt entfernt worden sind. Irgendwie hat immer eines überlebt und mir ist nicht ganz klar warum. Letztendlich hat mir das Internet den Tipp gegeben, dass ich alles nochmal in eine temporäre Liste packen soll. Das hat dann letztendlich auch geholfen:

transform
    .Cast<Transform>()
    .ToList()
    .ForEach(child => DestroyImmediate(child.gameObject));

Wäre sogar fast schon eine Überlegung wert, das ganze in eine Hilfsklasse zu packen.

Ich mache gerade eine kleine techdemo neben dem eigentlichen Projekt:

Video:

Dabei möchte ich vor allem ein wenig mehr mit PlayMaker experimentieren. Denn mein bisheriger Weg gefällt mir nicht. Ich hatte für viele Methoden eine weitere PlayMaker-Action geschrieben, die eben diese Methoden aufrufen. Das sorgt für eine unglaubliche Redundanz und ist auch etwas nervig. Daneben gefällt mir auch nicht, wie meine StateMachines untereinander kommunizieren und Objekte weiterreichen.

Deswegen auch das Experiment, was in etwa versucht die gleichen Probleme nachzustellen, die ich auch in Industry City habe. Danach kann ich dann etwas zuversichtlicher ans Refactoring gehen ^^

So. Ich hab meine TechDemo fertig und bin ganz glücklich damit:

Ziel war es, dass ich auf Aktionsebene nach Möglichkeit nur mit Daten arbeite. Denn meine bisherige Architektur hatte jede Methode eines Monobehaviors verdoppelt. Denn ich hatte die Methode einmal im Monobehavior angeboten und einmal als PlayMaker-Action, damit ich diese ausführen kann.

Darauf möchte ich gerne verzichten, denn das endet in haufenweise Aktionen. Und ich möchte eigentlich lieber existierende Wiederverwenden.

Es gibt ein weiteres Update. Man kann nun das Ziel von Autos ändern. Dabei werden die Ziele abhängig von dem ausgewählten Produkt angezeigt.

Damit sollte es auch sehr sehr einfach gehen, seine Autos richtig zu managen. Ursprünglich wollte ich es ja so machen, dass man sein Ziel auf der Karte auswählen kann. Aber bei z.B. Fabriken kann man ja nicht sehen, was die Produzieren. Und wie schonmal erwähnt: man muss auch das aktuelle Rezept im Kopf behalten.

Deswegen halte ich die aktuelle Umsetzung auch für recht elegant. Es ist nicht sonderlich komplex und sollte zudem intuitiv sein.