Tagebuch: Industry City

Ok. Die Shop-Settings gingen jetzt echt flott von der Hand. Letztendlich sind diese aber auch nichts anderes als eine abgespeckte Variante von den Factory-Settings:

Heute hab ich nen Lauf. Hab nun auch (hoffentlich) den letzen View fertig. Im Gesamten schaut es jetzt so aus:

Wie Ihr seht gibt es unten zwei neue Einträge mit „Route Settings“ die nahezu identisch sind. Der einzige Unterschied: Beim Shop wird alles gelistet - bei der Factory sollen die angezeigten Elementen nach dem zu produzierenden Item gefiltert werden.

Gefreut hab ich mich vor allem darüber, dass ich den Button wiederverwenden konnte, den ich heute mittag aufwendig designed habe.

Als nächstes werde ich mir mal ein paar GamePlay-Momente im Kopf überlegen und schauen, ob das mit dem angepeilten UI möglich wäre. Und ob es noch Schwachstellen gibt, die ich ausmerzen könnte.

ThinMatrix bastelt auch an einem City-Builder-Spiel, vielleicht kannst du dir ja Inspiration holen:

1 Like

Hehe, ich weiß. Verfolge dieses Projekt schon seit dem ersten Video :stuck_out_tongue:. Aber bisher hat er hauptsächlich Probleme gelöst, die mit Unity3D geschenkt werden ^^. Trotzdem ist es irgendwie interessant, seinem Fortschritt zuzuschauen und werde mir auch die Folgevideos angucken.

Ich merke gerade, wie wichtig ein BigPicture ist. Warum? Erklär ich später. Erstmal Context:

Wie bereits erwähnt möchte ich in dem Spiel durchaus Werbung unterbringen. Dabei aber (wie schon oft erwähnt) so, dass es vom Spieler selbst getriggert wird. Ich dachte da an ein Konzept: 1x Werbung gucken gibt einen Kristall (oder wie auch immer ich die Extra-Währung nennen werde). Dementsprechend hätte der Spieler die Chance sich durch einige male Werbung gucken im W-Lan diese Währung anzusammeln um offline trotzdem alle Möglichkeiten zu haben.

Soweit so gut. Jetzt muss ich diese Währung natürlich auch irgendwo anzeigen. Kurzzeitig hab ich überlegt, ob ich es unter dem Gold-Betrag anzeigen lasse. Aber dann hab ich mal auf meine anderen Screens geschaut und gemerkt, dass das nicht gerade gut wäre. Denn dann würde die Anzeige im Titel von den Fullscreen-Dialogen drin hängen. Und hier wären wir bei meiner Einleitung. Durch das BigPicture kenne ich nun meine Restriktionen.

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: