Tagebuch: r-process

Ich habe jetzt endgültig die Nase voll gehabt, und mir einen extra Layer für das Rendern in LibGDX gebastelt. Da Performance bei mir nicht so sehr das Problem ist, kann ich es auch ziemlich einfach halten. Beispiel der alten Schreibweise (schon mit einigen Hilfsfunktionen):

override fun drawBackground(batch: Batch, parentAlpha: Float) {
    batch.draw(Assets.dialog, bounds.x, bounds.y, bounds.width, bounds.height)
    batch.useFont(Assets.russelSquare, 0.5F, Color.GOLD) {
        draw(batch, title, bounds.x + bounds.width / 2F - titleWidth / 2F, bounds.y + bounds.height - titleHeight)
    }
    batch.end()
    withLineSR(sr) {
        color = Color.GOLD
        rect(bounds)
        val lineHeight = bounds.y + bounds.height - titleHeight - 40F
        line(bounds.x, lineHeight, bounds.x + bounds.width, lineHeight)
    }
    batch.begin()
}

Das kann nur jemand gut finden, dessen Lieblingsfach im Studium Buchhaltung war. Und hier die neue Version:

override fun render() = arrayOf(
       Picture(Assets.dialog, bounds),
       Text(title, at(bounds.x + w2 - titleWidth / 2F, bounds.y + bounds.height - titleHeight), 0.5F, Color.GOLD),
       Rect(bounds, Color.GOLD),
       Line(at(bounds.x, lineHeight), at(bounds.x + bounds.width, lineHeight), Color.GOLD)
)

Ich hoffe, ihr erkennt eine gewisse Verbesserung. Im Hintergrund arbeitet eine Klasse die ganzen Zeichenobjekte ab, und macht dabei das Batch und den ShapeRenderer automatisch auf und zu u.s.w.

Es hat sich so viel geändert, und es gibt trotzdem so wenig zu zeigen. Die Planeten und so sind jetzt 3D, aber noch deutlich zu hässlich für einen Screenshot.

Ein Video vom Macher von “Return of the Obra Dinn” hat mir ein bisschen Angst gemacht bezüglich der Internationalisierung. Und tatsächlich musste ich z.B. meine Font neu generieren, weil ich keine Umlaute hatte. Ich werde jetzt erst mal alles zweisprachig durchziehen:

Damit hätte ich auch die erste Sache, die ins Options-Menü gehört…

Allerdings muss ich mich andauernd am Riemen reißen, um kein Bikeshedding zu betreiben, und mich auf die zentralen Aspekte zu fokussieren.

Nebenbei bemerkt kam gerade ein Spiel raus, dass in eine recht ähnliche Richtung wie r-process geht, nämlich “Void Bastards” - auch wenn es bei mir keinen eingebauten 3D-Shooter geben wird…

Crafting nimmt langsam Gestalt an. Wenn man jetzt auf ein Resource-Icon klickt, werden alle “Rezepte” herausgesucht, die diesen Stoff als Input benötigen. Die Zahl am Ende zeigt, wie oft das Rezept mit den aktuell vorhandenen Resourcen angewandt werden könnte. Nächster Schritt ist natürlich, ein einzelnes Rezept anzeigen und ausführen zu können.

Edit: Neuer Screenshot. Als ich den ersten hochgeladen habe, habe ich mir gesagt, dass es sicher mit transparenten Hintergrund viel besser aussieht…

Das Crafting funktioniert, natürlich mit ein paar Ecken und Kanten.

Das Fragezeichen steht für ein Blueprint (hier: “Elektrolyse”), was ich noch nicht gezeichnet habe.

Mal sehen, wo ich weitermache…

Warte, warte, du hast das Männchen auch selbst gezeichnet? Was für ein Zeichenprogramm verwendest du? :slight_smile:
(Das Menü sieht immer noch schick aus mit dem Dunkelblau/Dunkelrot-Gradient.)

Wann kommt die erste Demo? :smiley:

Viele der Icons sind einfach Unicode-Emoticons. Das Männchen :walking_man: ist U+1F6B6 - „Pedestrian“. Als Zeichenprogramm reicht Paint NET aus.

Bis zu einem Demo wird es noch lange dauern: Das Spiel wird drei Ansichten (Karte, Sternsystem, Erforschung von Strukturen) haben, und ich habe jetzt gerade mal die Grundlagen einer dieser Ansichten fertig. Im Hintergrund muss auch noch die Spielwelt einigermaßen balanciert generiert werden, und ein Ereignissystem fehlt auch noch.

Wobei ich auch sagen muss, dass ich relativ wenig an r-process arbeite. Meine Projekt-Stoppuhr zeigt aktuell 72h, dazu vielleicht noch 15h sonstige Arbeiten.

So, ich habe alle 3D-Elemente rausgeworfen, und bin damit einige Probleme losgeworden. Die erste Version mit 2D ist nicht toll, aber ausbaufähig. Vor allen an den Schatten muss ich noch arbeiten, aber wenigstens sind sie schonmal richtig gegenüber dem Stern ausgerichtet.

Um weiteres Bikeshedding zu vermeiden, starte ich erst mal mit der Sternkarte.

Ich habe noch das Problem, dass das Spiel unter Windows am Ende crasht, habe noch keine tolle Idee…

Die erste Karte, die man auch als solche bezeichnen kann, wenn auch mit vielen Problemchen:

Wahrscheinlich werde ich eine zweite Kamera benutzen müssen, um Zoom und Verschiebung zu erlauben. Vielleicht geht es aber auch etwas extravaganter, ich dachte an sowas wie einen Fisheye-Effekt

3 „Gefällt mir“

super :sunglasses:

Ich finds klasse. Unter deiner Beschreibung konnte ich mir nichts vorstellen. :+1:

Wirkt nur ein bisschen stark im Video. Aber es muss sich ja vorallem gut anfühlen wenn man es bedient. :slight_smile:

Ehrlich gesagt ist das einfacher zu implementieren als Zoom + Verschiebung, sind nur ein paar Zeilen Code:

    fun distort(v: Vector2, mx: Float, my: Float): Vector2 {
        val dist = v.dst(mx, my)
        if (dist < 10) return v
        val drag = 100 / (1 + sqr(sqr(2 - dist / 100)))
        return Vector2(v.x + drag * (v.x - mx) / dist, v.y + drag * (v.y - my) / dist)
    }

v ist der Ausgangspunkt, mx und my die Mauskoordinaten. Zuerst berechne ich aus dem Abstand zum Mauszeiger den Betrag der Verschiebung (“drag”) und dann verschiebe ich den Punkt um diesen Betrag weg vom Mauszeiger.

Die verwendete Funktion hat diesen Verlauf: https://www.wolframalpha.com/input/?i=y+%3D+1%2F(1+%2B+(x-2)^4)

Ja, man kann jetzt reisen. Die nötigen Resourcen hängen natürlich von der Länge des Sprungs, von der Ausstattung des Schiffs, sowie Anzahl und Spezies der Crew ab. Das braucht sicher noch einiges Finetuning.

Währenddessen füllt sich mein Backlog mit den vielen kleinen Dingen, die ich erst mal in den Skat gedrückt habe.

Der weitere Plan sieht vor:

  • mehr Resourcen, Blueprints und Rezepte
  • realistischer Generator mit Seed
  • Forschungsbaum
  • Ereignisse, inklusive rudimentärer Story-Line
  • Strukturen innerhalb von Systemen (Ruinen, Handelsstationen, Schiffswracks u.s.w)
  • Erkundung dieser Strukturen
  • Stats / History, Hilfe
  • Sound

Fisheye und Zoom mit Mausrad schließen sich ja nicht gegenseitig aus (siehe Beispiel das ich vor 5 Jahren mal gedengelt hatte) ;-). Aber leichter bedienbar ist es sicher ohne, wenn man ohne auskommt…

Die erste Drag-Funktionalität ist eingebaut (Sachen über Bord werfen).

Dann hatte ich ein größeres Refactoring, ausgelöst von der Notwendigkeit, “kaputte” Bereiche im Schiff zu modellieren. Vielleicht ist es für Euch interessant, das alte Anti-Pattern einmal näher zu beleuchten:

Ich hatte ein Set von Positionen namens layout, was - wie der Name schon sagt, die vorhandenen Zellen auf dem Schiff modulierte. Dann hatte ich eine Map resources, die von einer Position auf ein Cargo-Objekt (Resource plus Anzahl) gemappt hat.
Ich war von Anfang an irgendwie mit diesem Design nicht glücklich, aber ich habe es mir als “einfach” schöngeredet. Der Knackpunkt ist natürlich, dass die Information über eine “Zelle” jetzt auf zwei Collections verteilt ist. Es bestand z.B. immer die Gefahr, etwas außerhalb des Layouts in die Resources-Map zu packen. Und natürlich hatten leere Zellen keine expliziten Objekte, sie waren definiert als “im Layout, aber nicht in den Resources”.

Das ist schlecht. Es sollte immer einen “Single Point of Truth” geben, niemals implizites Wissen, dass sich aus der Kombination von irgendwelchen Werten ergibt.

Der “einfache” Fix wäre gewesen, Layout wegzuwerfen, und zwei Pseudo-Resourcen “leere Zelle” und “kaputte leere Zelle” einzuführen. Auch keine gute Idee, sowas zieht immer einen Rattenschwanz von Extra-Tests nach sich.

Ich habe layout weggeworfen eine neue Klasse Cell eingeführt, von der Cargo jetzt eine Unterklasse ist, dazu die abstrakte Klasse NoCargo mit den Unterobjekten EmptyCell und DamagedCell. Natürlich hat Kotlin’s Pattern-Matching das alles etwas schmerzloser gemacht, aber der eigentliche Trick war, die alte Struktur jetzt über Methoden zu simulieren, z.B. eine Methode cargoMap(), die eben nur die Cargos raussortiert - und damit hat sich relativ wenig geändert. An manchen Stellen ist es durch das “kompliziertere” Design sogar einfacher geworden. Meine Testabdeckung ist recht mies, aber Ship war erfreulicherweise recht gut gecovert, deshalb bin ich mir auch recht sicher, dass ich nichts größeres kaputtgemacht habe.

Und die Moral von der Geschicht’: Macht es so einfach wie möglich, aber nicht einfacher. Vermeidet implizites Wissen, das sich aus drei verschiedenen Variablen und der aktuellen Mondphase ableitet - macht es explizit.

Hättest du nicht einfach eine boolean Variable „damaged“ einführen können?

Ich hätte das ganze halt so programmiert als Modelklasse

public class Cell {
     Position position;
     Cargo cargo;
     boolean damaged;
}

ohne jegliche Subtypen.

Ich versuche immer jedes Bit Information von euch studierten Programmierern aufzusaugen, was gutes Programmieren angeht. :smiley: Gerade bei meinem Nachbar-Riesenprojekt (relativ für eine Person) merke ich doch häufig strukturelle und planerische Probleme, die in multiples Refactoring und Hit-and-Run Prototyping ausartet, wie bei dir jetzt.

Erst einmal habe ich Position separat gehalten, weil es in den meisten Fällen eine gute “Adresse” für die Zelle ist. Ich denke, die Map dafür geht schon in Ordnung.

Jetzt weiß ich nicht, wie du dir in deinem Design eine leere Zelle vorstellst, hoffentlich nicht mit cargo == null. Mir fiele nur Optional<Cargo> ein, um das sicher zu machen. Wäre auch eine Lösung, allerdings ein bisschen komisch, da sowohl eine leere Zelle, als auch in einer gefüllten Zelle bestimmte Resourcen (nämlich Geräte oder Crew) beschädigt sein können. damaged bezöge sich also einmal auf Cell und einmal auf Cargo.

In meinem Fall kommt hinzu, dass es nützlich ist Cargo und die leeren Zellen auf einer Ebene zu haben, da man so in Kotlin leicht Fallunterscheidungen treffen kann wie

when (cell) {
    is Cargo -> ...
    is NoCargo -> ...
}

oder

when (cell) {
    is Cargo -> ...
    is EmptyCell -> ...
    is DamagedCell -> ...
}

Meine Hierarchie sieht jetzt insgesamt so aus:

sealed class Cell {
    abstract val amount: Int
    abstract val damaged:Boolean
}

abstract class NoCargo : Cell() {
    override val amount: Int = 0
}

object EmptyCell : NoCargo() {
    override val damaged: Boolean = false
}

object DamagedCell : NoCargo() {
    override val damaged: Boolean = true
}

data class Cargo(val resource: Resource,
                 override val amount: Int, 
                 override val damaged: Boolean = false) : Cell() { }

Ich bin ein Fan der Schrift, die auf den Screenshots zu sehen ist (Russell Square), aber es hat sich herausgestellt, dass sie zwar für “normalen” Gebrauch mit 35€ erschwinglich ist, für mich aber wahrscheinlich 350€ fällig wären.

Ich bin deshalb zu einer komplett freien Schrift gewechselt, die mir auch gut gefällt und den “eckigen” Look beibehält, nämlich Xolonium:

1 „Gefällt mir“

Ich verwende die gleiche Schriftart seit einiger Zeit (Blog Eintrag fehlt noch). :smiley: Gar nicht so leicht, gute kostenlose zu finden. Die Lizenzen sind häufig nicht so eindeutig.

Und doch, ich hätte es mit if(cargo != null) gemacht. Wieder was gelernt. Ich hab aber auch keine große Erfahrung mit Kotlin. Ich wusste auch nicht das instanceof dort zum guten Ton gehört. :slight_smile:

Instanceof gehört vor allem bei Algebraischen Datentypen zum guten Ton, wobei besonders die Eigenschaft der Abgeschlossenheit der Typhierarchie (in Kotlin: sealed) Pattern-Matching attraktiv und sicher macht. Einfach gesagt: Man weiß ganz genau, was alles kommen kann, deshalb ist es nicht „böse“, über die Klasse zu matchen.

In Java sieht die Sache anders aus, zum einen ist die Syntax natürlich nicht schön, zum anderen ist es schwierig, diese Abgeschlossenheit zu erreichen (meines Wissens nach nur darüber, die Subklassen zu finalen inneren statischen Klassen zu machen, und den Konstruktor der Basisklasse privat).

Wenn man sich es aussuchen kann, gibt es für mich sehr wenig Gründe, Java statt Kotlin oder Scala zu verwenden. Alles was mir einfällt ist

  • man möchte eine Bibliothek / Plugin o.ä schreiben
  • man möchte direkt oder indirekt GWT verwenden (was bei LibGDX relevant wäre, wenn ich auch eine Web-Version haben wöllte)