"Cacti VS Llamas" - BCC #98

Kommentare anzeigen Worklog abonnieren
Gehe zu Seite Zurück  1, 2, 3  Weiter

Worklogs "Cacti VS Llamas" - BCC #98

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()

Kampfmechanik #1

Samstag, 30. März 2019 von DivineDominion
Wie wird der Kampf berechnet? Wann trifft man? Wieviel Schaden wird pro Angriff gemacht? Wieviel Lebenspunkte sollte man so haben?

Unverzichtbares Tool zur Visualisierung von Würfelwerten und ihrer Wahrscheinlichkeit: https://anydice.com/

Code: [AUSKLAPPEN]
output 3d6
output 5d20


Bei 3W6 hat man um die 10, bei 5W20 um die 50 als Mitte. Das nebeneinander zu legen finde ich praktisch, um zum Beispiel abzuschätzen, wie ich Schaden verbuche.

- Wenn ich mit 5W20 oft Werte zwischen 30 und 70 würfle, dann sollte der Gegner nicht nur 20 Lebenspunkte haben, sondern eher ein paar Hundert.
- Wenn ich mit 3W6 würfle, hab ich in 90% der Fälle mindestens eine 7, in 50% der Fälle mindestens eine 11, in 10% der Fälle mindestens eine 14. Im Schnitt mache ich 10.5 Punkte Schaden; wenn der Gegner 50 Lebenspunkte hat, hält er im Schnitt 5 Attacken durch.

user posted image

Vorausgesetzt, er wehrt sich nicht, weicht nicht aus, und hat keine andere Art von Verteidigungsboni oder ich Angriffsmali.

Witzige Idee von einem Kumpel: Sonderaktionen bei einem 2er Pasch.

Mit 3W6 ist die Wahrscheinlichkeit leicht zu berechnen, dass mindestens 2 Würfen dieselbe Augenzahl zeigen, indem man den umgekehrten Fall ausrechnet: jeder Würfen zeigt eine andere Zahl. Der erste Würfel kann zeigen, was er will (1), der zweite alles außer die Zahl vom ersten (5/6 Seiten erlaubt), der dritte alles außer die Zahl des ersten oder zweiten Würfels (4/6 Seiten erlaubt).

Die Formel:

Code: [AUSKLAPPEN]
1 - (1 * 5/6 * 4/6) = 0.44  also 44%


Das heißt, in fast der Hälfte aller Angriffswürfe mache ich mit 3W6 mindestens 11 Punkte schaden und habe einen Pasch.

Wenn man mehrere Päsche sammelt, kann man diese Pasch-Punkte für Spezialaktionen ausgeben. Das könnte eine lustige Mechanik sein.

Concept Art #1

Samstag, 30. März 2019 von DivineDominion

Gehe zu Seite Zurück  1, 2, 3  Weiter