"Cacti VS Llamas" - BCC #98

Kommentare anzeigen Worklog abonnieren
Gehe zu Seite 1, 2  Weiter

Worklogs "Cacti VS Llamas" - BCC #98

TMap

Sonntag, 14. April 2019 von DivineDominion
Today I Learned, dass die "Compare" Methode dafür verantwortlich ist, festzustellen, ob ein Key in einer TMap scchon vorhanden ist oder nicht.

Für eine 2D-Position, die ich als Key nutze, um Figuren zu platzieren, habe ich jetzt also folgendes eingebaut:

BlitzMax: [AUSKLAPPEN]
Type CTTokenPosition
Field column%, row%

Method New(column%, row%)
Assert column >= 0 Else "column negative"
Assert row >= 0 Else "row negative"
Self.column = column
Self.row = row
End Method

Method Compare:Int(other:Object)
Local otherPosition:CTTokenPosition = CTTokenPosition(other)
If Not otherPosition Then Return Super.Compare(other)

If Self.column < otherPosition.column
Return -1
Else If Self.column > otherPosition.column
Return 1
Else ' ==
If Self.row = otherPosition.row Then Return 0
If Self.row < otherPosition.row Then Return -1
If Self.row > otherPosition.row Then Return 1
End If
End Method

Method IsEqual:Int(other:CTTokenPosition)
Return (Compare(other) = 0)
End Method
End Type


Damit werden Positionen zuerst nach ihrer X-Komponente (Spalte) sortiert, dann nach Y (Reihe).

Cactus Party Picker

Donnerstag, 11. April 2019 von DivineDominion
Nächster großer Meilenstein: Game Scenes laufen. Damit wechseln sich verschiedene, in sich abgeschlossene Szenen oder Bildschirme ab.

Beispiel Asteroids-Clone: Hauptmenü, Credits, Scores, und eigentliches Spiel.

In meinem Fall: man sucht sich aus der Kaktusarmee eine Kampftruppe aus, und die wird dann auf dem Schlachtfeld angezeigt.

user posted image

Größte Schwierigkeit hierbei war, neben der BCC Bugs, das Interface für den "Party Picker". Aufbauend auf den Dingen, die ich bisher implementiert hatte, sind die zwei Spalten im Party Picker unabhängige Menüs. Wenn man im linken Menü etwas auswählt, wird es ins rechte eingefügt, und umgedreht.

Der Aufbau davon ist folgendermaßen:

- SplitListView enthält zwei Menu Objekte
- aktiviert man die SplitListView, aktiviert diese eines der Menüs, das dann Tastendruck verarbeitet
- Die beiden Menüs schlucken Tasten-Events nicht, sondern geben sie an die SplitListView weiter
- SplitListView reagiert nur auf horizontale Bewegung und aktiviert das je passende Menü (links oder rechts)

Indem ich jeweils ein Menü aktiviere und eins deaktiviere, kann ich mit einer SplitListView zwei Menüs unmabhängig voneinander kontrollieren.
Source Code: https://github.com/DivineDomin...ew.bmx#L74


Die SplitListView registriert sich als Callback (delegate) für die Menüs. Wenn man in einem Menü etwas wählt, gibt die SplitListView das ihrem eigenen Callback weiter: was wurde gewählt, und auf welcher Seite.
Source Code: https://github.com/DivineDomin...w.bmx#L141

Der Callback, der die Auswahl eigentlich erst verarbeitet, ist ein anderes Objekt. In diesem Falle eine PartyPickerView. Diese besteht aus der geteilten Liste, dem zentrierten Label mit dem Status, wieviele Charaktere ausgewählt wurden, und den Bestuatigungsknöpfen ganz unten. Im Grunde malt sie bloß alles an den richtigen Platz und leitet Events ihrer Subviews weiter an ihren eigenen Callback.

So kümmert sich jedes GUI Element um seine Sachen und informiert sein Parent-Element bei Bedarf.

Code: [AUSKLAPPEN]

+----------------+     show           +---------------------+
|                +------------------->+                     +------------------------+
|  CTPickParty   |                    |   CTPartyPickerView |                        |
|                +<-------------------+                     +--+                     |
+----------------+    didSelectParty  +-----+---------------+  |                     |
                                            |                  |                     | proceed/cancel
                                      lists |                  |                     |
                                            |           status |                     |
                            +---------------v--+         +---v-v-------+     +-------v----------+
                            |                  |         |             |     |                  |
                            |  CTSplitListView |         |   CTLabel   |     |  CTDialog        |
                            |                  |         |             |     |                  |
                            +--+------------+--+         +-------------+     +-------+----------+
                               |            |                                        |
                          left |            |right                                   | is a horizontal menu
                    +----------v-+       +--v--------+                       +-------v----------+
                    |            |       |           |                       |                  |
                    |  CTMenu    |       | CTMenu    |                       |   CTMenu         |
                    |            |       |           |                       |                  |
                    +------------+       +-----------+                       +------------------+


Die horizontalen Buttons ganz unten sind auch ein Menu Objekt, nur horizontal gemalt.

Das use case/Service Objekt, das die Views erst erstellt und das eigentlich interessante Event abbekommt, ist PickParty und der Code ist hier:
https://github.com/DivineDomin...kParty.bmx

Dieses Objekt informiert die PickPartyGameScene über die Auswahl, und diese Szene wechselt zum Kampf. Dann geht's los.

BCC kaputt #2

Mittwoch, 10. April 2019 von DivineDominion
Ich konnte den Bug reproduzieren. Mal schauen, was das Problem ist. Hoffentlich fixt Brucey das bald Smile

https://github.com/bmx-ng/bcc/issues/428

BCC kaputt? Zirkuläre Methodenfolge ohne Aufrufe

Dienstag, 9. April 2019 von DivineDominion
Irgendwie hab ich es geschafft, dass drei Methodenaufrufe immer wieder auftreten, bis ich einen stack overflow hab.

user posted image

Das komische daran ist, dass sich CTMenu#ConfirmSelection, CTSplitListView#Draw und CTView#Draw gar nicht gegenseitig aufrufen.

Bin mit dem Debugger durch gesteppt und zum Schluß gekommen, dass da irgendetwas ganz und gar nicht stimmt. Das sieht aus wie eine typische race condition -- nur arbeite ich nicht mit mehreren Threads. Und da sich die Methoden gegenseitig nicht aufrufen, gibt es auch keinen offensichtlichen Zirkel. Ich glaube darum eher, dass irgendein internes pointer-Offset im generieren C-Code nicht passt.

Ich habe langsam eher das Gefühl, dass der BlitzMax-NG Compiler überhaupt noch gar nicht mit dem klar kommt, was er anbietet.

BlitzMax IDE / Tooling

Montag, 8. April 2019 von DivineDominion
Was mich richtig nervt sind die macOS Tools, mit denen ich arbeite Smile

TextMate ist ein wundervoller Editor. Aber das BlitzMax-Bundle musste ich anpassen, damit es überhaupt kompiliert. Und dann kompiliert es einmal alles en bloc, und der Editor ist so lange eingefroren. Wenn ich das Spiel starte, dann sehe ich keine DebugLog/Print Ausgaben. Und wenn es abstürzt, muss ich den Prozess manuell killen und sehe keine Fehlermeldung. Zum Debuggen muss ich also immer die MaxIDE öffnen. Die hat wiederum eigene Probleme ...

Das führt mir vor Augen, wie gut ich es im Alltag sonst eigentlich habe. Eine vernünftige IDE ist Gold wert.

Überlege, zum Editieren später in Emacs zu wechseln. Da kann ich einfach Shell-Prozesse starten und hoffentlich auch Fehler korrekt angezeigt bekommen.

Zirkuluare Imports bei GUI-Widgets umgehen

Montag, 8. April 2019 von DivineDominion
BlitzMax ist streng mit zirkulären `Import`s. So kann ich leider keine Window-Klasse schreiben, die ein View-Objekt hat, und wiederum dem View-Objekt eine Methode hinzufügen, mit der man auf das umschließende Window käme.

Wenn alles in derselben Datei wäre, würds gehen.

So verhält sich BlitzMax halt anders als Swift und Objective-C.

Die Funktion kann ich über den WindowManager immer noch implementieren, aber verglichen mit der Idee, die ich hatte, ist der Aufruf nicht so schön:

BlitzMax: [AUSKLAPPEN]

' Idee
Local Window = Self.View.GetWindow()

' Realität
Local Window = WindowManager.GetInstance().WindowWithView(Self.View)

Decoupling

Dienstag, 2. April 2019 von DivineDominion
BlitzMax hat noch keine "weak references". Dadurch erzeugt man schnell Zyklen, die dazu führen, das Objekte nicht mehr freigegeben werden, wenn sie eigentlich niemand verwendet -- weil sie sich gegenseitig (oder selbst) referenzieren.

Das bmax wikibook hat ein Beispiel:

BlitzMax: [AUSKLAPPEN]

Type TCyclic
Field cycle:TCyclic=Self ' << Zyklische Referenz

Method Remove()
cycle=Null ' << Manuelle Auflösung des Zyklus
End Method
End Type

For k=1 To 10
Local cyclic:TCyclic=New TCyclic
GCCollect
Print GCMemAlloced()
cyclic.Remove ' << ohne diese Zeile sammeln sich Objekte "für immer" an
Next


Mit "weak references" wäre es anders.

Wenn A -> B und B -> A verweist, dann hat man einen Zyklus, weil sich die Objekte durch die Referenzen am Leben halten.

Wenn A -> B verweist und B -> A "weak" ist, dann hat die Verbindung B -> A keine lebenserhaltende Kraft. Es gibt dadurch keinen Zyklus. Sobald alle anderen Referenzen auf A verschwinden, wird A vom Garbage Collector eingesammelt, und sein Field "B" auch.

Das ist eine superpraktische Einrichtung zum Beispiel in der macOS und iOS App Entwicklung.

Denkt man an MVC, dann besitzt der Controller die View, aber die View hat nur eine "weak reference" zum Controller in seiner Funktion als Event Handler. Das heißt, dass wenn man den Controller löscht, die View mit verschwindet, weil es keine starke Abhängigkeit in die andere Richtung gibt.

Das hätte ich auch gerne. Ein Objekt, z.B. eine Spielfigur, die von einem anderen Objekt ferngesteuert wird, aber auch Ereignisse an einen Event Handler weiter gibt. Jetzt muss ich halt aufpassen wie die Hölle, dass ich keine zyklischen Verbindungen baue.

Googlet man nach "loose coupling blitzmax", dann ist der Standardratschlag etwa: referenziere Objekte nicht direkt, sondern über eine ID und eine globale Kollektion.

BlitzMax: [AUSKLAPPEN]

' Lookup via map:
Global characters:TMap = New TMap()

Local playerID:Int = 12345
characters.Insert(playerID, New Player)

Local enemyID:Int = 12345
characters.Insert(enemyID, New Enemy)

' ...

Local controller:TPlayerController = New TPlayerController(playerID)

Type TPlayerController
' ...
Method MoveUp()
Local player:Player = characters.ValueForKey(playerID)
player.y :- 1
End Method
End Type


Ds kann man machen. Aber das kann man nicht für alles machen. Für eine Liste aller Spielfiguren und aller Animationen geht das noch gut. Aber wenn man andere Objekte als Services dazwischen schalten will, und für jede neue Klasse eine neue TMap bastelt, dann wird das schnell hässlich.

Eine andere Möglichkeit wäre, einen Mediator dazwischen zu schalten und den zu entfernen.

- Player wird erstellt
- PlayerController referenziert Player direkt (zum kontrollieren)
- PlayerController erstellt PlayerEventHandler (Mediator) und übergibt ihn an Player
- Player referenziert PlayerEventHandler
- PlayerEventHandler referenziert PlayerController
- PlayerController löscht seinen PlayerEventHandler bei Bedarf und entfernt ihn von Player

Der Kreislauf wäre: Controller -> Player -> EventHandler -> Controller

Indem man den EventHandler herausnimmt, wird der Kreis durchbrochen.

Dasselbe kann man auch ohne Mediator machen, indem PlayerController <-> Player auf sich gegenseitig verweisen, und bei Bedarf der PlayerController sich beim Player abmeldet, um die Referenz aufzulösen.

All das ist etwas fehleranfälliger, als mir lieb ist Smile

Interaktion mit dem User Interface

Montag, 1. April 2019 von DivineDominion
Es hat sich jetzt ausgezahlt, dass ich eine "responder chain" eingebaut habe. Bei dieser verarbeitet nur das letzte Element den Tastendruck, aber theoretisch können Events die gesamte Kette nach oben aufsteigen.

Gelohnt hat es sich, weil ich jetzt schon zwei voneinander völlig unabhängige Fenster ins Spiel eingebracht habe.

1. Das Schlachtfeld. Dort sind Spielfiguren platziert. Mit den Pfeiltasten wählt man eine Figur aus.
2. Das Aktionsmenü. Das erscheint nur, wenn man eine Spielfigur auswählt und mit Enter/Leertaste bestätigt.

Die "responder chain" hilft dabei, Schlachtfeld und Menü isoliert zu entwickeln und auf Events zu reagieren. Wenn das Menü offen ist, schluckt es alle Events. Wenn das Menü geschlossen wird, verschwindet es von der "responder chain". Dadurch bekommt das Schlachtfeld wieder die Tastendrücke weitergeleitet, weil es die neue Spitze ist.


Video Demo davon: https://imgur.com/kQVmPJY
user posted image


Erster Bug

Montag, 1. April 2019 von DivineDominion
Bug im BCC entdeckt.

Abstrakte Basis-Typen können `New` implementieren, um Standardwerte für ihre Fields zu setzen. Aber wenn man die abstrakten Typen in eine andere Datei packt und importiert, wird das nicht mehr ausgeführt.

https://github.com/bmx-ng/bcc/issues/417

Keyboard Input

Sonntag, 31. März 2019 von DivineDominion
Seit 2001 schreibe ich imperative Game Loops und fange dort KeyDown events ab, die ich dann an Spielfiguren weiterreiche.

2019 ist Schluss damit.

Nachdem ich die letzten 6 Jahre fast ausschließlich macOS und iOS Apps entwickelt habe, habe ich mich an die Architektur dort sehr gewöhnt. Man kann mit BLitzMax noch nicht alles davon nachbauen, aber BlitzMax NG ist schon auf einem passenden Weg. In diesem Spiel will ich ein Pattern von macOS/iOS aufgreifen und habe folgendes nachgebaut:

- Views, die zeichnen,
- Controls, (oder Gadgets), die von Views erben und Benutzereingaben verarbeiten,
- Respondern, die auf rohen Tastendruck reagieren.

Im Spiel gibt es maximal einen Responder, der im Fokus ist, aka "firstResponder". Wenn man ein verschachteltes Menü hat, dann stelle man sich einen Stapel vor. Das Menü oben auf dem Stapel empfängt Benutzereingaben. Die anderen Elemente darunter werden gezeichnet, verarbeiten aber keinen Tastendruck. Wenn das oberste Element verschwindet, weil das Menü sich schließt, ist das danach oberste Menü der neue firstResponder.

Das ist das kleinste Stück, das ich rausbrechen kann. Ich habe Aktions-Auswahlen jetzt so implementiert:


Code: [AUSKLAPPEN]

SuperStrict

Global firstResponder:CTResponder = Null

Interface CTResponder
    Method KeyUp(key:Int)
    Method MakeFirstResponder()
End Interface

AddHook EmitEventHook, HandleKeyboardInput

Function HandleKeyboardInput:Object(id:Int, data:Object, context:Object)
    Local event:TEvent = TEvent(data)

    Select event.ID
    Case EVENT_KEYUP
        If firstResponder
            firstResponder.KeyUp(event.Data)
        End If
    End Select

    Return data
End Function


Damit wird ein Tastatur-Event an den firstResponder weitergeleitet, wenn es denn einen gibt. Das Interface CTResponder wird dann einmal von der Basisklasse für alle Controls implementiert und dort der Tastendruck verarbeitet. Weil ich aber nicht immer die KEY_UP, KEY_LEFT, usw behandeln will, mache ich daraus einmalig einen "Dispatcher". Die Control-Basisklasse empfängt die KeyUp-Nachricht und macht daraus neue Nachrichten, wie MoveUp, MoveDown, usw.:

Code: [AUSKLAPPEN]

SuperStrict

Import "CTResponder.bmx"

Type CTControl Implements CTResponder
    '#Region CTResponder
    Method MakeFirstResponder()
        firstResponder = Self
    End Method

    Rem
    bbdoc: Default implementation calls #InterpretKey.
    EndRem
    Method KeyUp(key:Int)
        Self.InterpretKey(key)
    End Method
    '#End Region

    '#Region Control hooks
    Rem
    bbdoc: Interprets key and calls one of `CTControl`'s action methods: #MoveUp, #MoveDown.
    EndRem
    Method InterpretKey(key:Int)
        Select key
        Case KEY_UP
            MoveUp()

        Case KEY_DOWN
            MoveDown()
        End Select
    End Method

    Method MoveUp(); EndMethod
    Method MoveDown(); EndMethod
    '#End Region
End Type


Mit dieser Basisklasse kann ich jetzt das eigentliche Menü bauen und muss nicht mehr in der Ebene von Tastatur-Events denken, sondern habe eine Abstraktionsebene darüber geschaffen:

Code: [AUSKLAPPEN]

SuperStrict

Import "CTControl.bmx"

Type CTMenu Extends CTControl
    Field selectedIndex:Int = 0
    Field menuItems:String[] = ["First", "Second", "Third"]

    ' These are overrides for by CTControl's methods:
    Method MoveUp()
        If selectedIndex > 0
            selectedIndex :- 1
        End If
        Print menuItems[selectedIndex]
    End Method

    Method MoveDown()
        If selectedIndex < menuItems.length - 1
            selectedIndex :+ 1
        End If
        Print menuItems[selectedIndex]
    End Method
End Type


Das ist schon alles, was es braucht, um einmal zentral alle Tastatureingaben zu erhalten und dann ans Menü weiterzuleiten. Wenn es Pfeiltasten empfängt, versucht es, die Auswahl zu verändern und zeigt das Ergebnis in der Konsole.

Code: [AUSKLAPPEN]

SuperStrict

Import "CTMenu.bmx"

Local menu:CTMenu = New CTMenu
menu.MakeFirstResponder()

Repeat; Until KeyDown(Key_Escape) Or AppTerminate()

Gehe zu Seite 1, 2  Weiter