Actors, Baby!
*********** VIDEO ***********
Roguelike Diary 02: Actors
*******************************
Es gibt keine xPlayer,yPlayer-Koordinaten mehr, stattdessen ist JEDES Objekt ein Actor, inklusive dem Spieler (Actor vom Typ #monsterActor).
Ich mache keinen Unterschied mehr zwischen Türen, Schatztruhen und Monstern! Das hat den Vorteil, daß ich so recht einfach ein
Agent System basteln kann, welches ein stabiles Fundament für
Emergent Gameplay legt: jeder Actor kennt seine DisplaySprite()-Parameter (actor()\type, actor()\subtype[0], actor()\subtype[1], usw), x/y-Koordinaten, ob er Kollision hat, ob er Sichtlinie blockt, ob er sich bewegen kann, ob er eine KI hat, usw. Oder anders ausgedrückt: Wenn eine Tür Lebenspunkte hat, kann man sie auch mit Gewalt aufbrechen.
Dazu braucht es einen *player-Pointer, mit dem ich gezielt die Parameter des Spieler-Actors abrufen kann.
Ich arbeite mit 4 verschiedenen Arrays:
• worldMap(#xMax,#yMax)
• collisionMap(#xMax,#yMax)
• binaryMap(#xMax,#yMax)
• bresenMap(#xMax,#yMax)
Die worldMap() beinhaltet die Level-Architektur (0 = Mauer, 1 = freies Feld) und wird u.a. in der Zeichenroutine benutzt um den jeweiligen Ausschnitt rund um die gegenwärtige KameraCenter-Position zu zeichnen; Felder außerhalb des Viewports werden erst gar nicht angesteuert.
Die collisionMap() beinhaltet Informationen über Kollisionen: 0 = keine Kollision, 1 = Kollision. Das ist notwendig, da es Actors mit wechselnden
States gibts, z.B. offene und geschlossene Türen. Jedesmal wenn sich ein Actor bewegt (Spieler/Monster), wird das aktuelle Feld in der collisionMap() um 1 herabgesetzt, und das Zielfeld kriegt +1. Das ergibt Sinn, wenn ich z.B. fliegende Monster haben möchte, die sich auf ein Feld bewegen dürfen, wo sich bereits ein Actor mit Kollision befindet. Jetzt hat dieses Feld in der collisonMap() +2; und wenn das Monster weiterfliegt, wird das Startfeld um 1 herabgesetzt und verliert dabei seine Kollision nicht (2-1 = 1)! Actors, mit denen man gar nicht interagiert, z.B. Bäume oder Säulen, haben Kollision -1, d.h. ich kann so gezielt das rote Interaktions-Rechteck ausblenden, wenn die Maus auf ein solches Feld bewegt wird.
Die binaryMap() ist von Relevanz, wenn bei LoS-Berechnungen und Pfadsuche der gegenwärtige
World State in 0en und 1en übersetzt wird: mit CopyArray() wird zunächst der Inhalt der worldMap() kopiert, und anschließend mit ForEach actor() alle Actor-Informationen von Relevanz (Kollision bzw. Sichtlinie blockend) zur binaryMap() hinzugefügt. Es gibt zwei Prozeduren, die eine binaryMap() auslesen und verarbeiten können: getPath(...) und getBresenham(...).
Die bresenMap() enthält Informationen über Sichtlinie und LoS-blockende Felder, und wird nach jedem Schritt aktualisiert.
Außerdem habe ich eine sogenannte actorFactory() - das ist eine Prozedur, in der Actors erstellt werden. Warum ist notwendig?
Ich muß beim Zeichnen von Actors auf die "Zeichentiefe" achten - z.B. muß ein Monster, welches in einer Tür steht, immer sichtbar bleiben, d.h. die Tür muß immer zuerst gezeichnet werden. Und um dies möglichst elegant und effizient zu lösen, dafür habe ich die actorFactory(): Jeder Actor hat einen Parameter drawPriority. Actors mit drawPriority = 0 werden immer zuerst gezeichnet. Und damit ich nicht in meiner linkedList hin und herspringen muß, was verdammt viel Zeit bräuchte, werden die Actors gleich bei Erstellung nach dem Parameter drawPriority sortiert; ich muß also beim Zeichnen die actorList() nur einmal von vorne nach hinten abklappern, und führe trotzdem alle Zeichenoperationen in korrekter Reihenfolge durch:
Code: Alles auswählen
Procedure actorFactory(type,subtype0,drawPriority,visible,collision,blockLoS,x,y)
Define a
Dim subtype(#actorSubtypeMax)
subtype(0) = subtype0
LastElement(actor())
If ListSize(actor()) <> #Null And actor()\drawPriority > drawPriority
;funktioniert, weil <>#Null-Abfrage zuerst abgearbeitet wird (kein "Invalid Memory Access" bei leerer Liste)
ForEach actor()
If actor()\drawPriority >= drawPriority
InsertElement(actor())
Break
EndIf
Next
Else
AddElement(actor())
EndIf
actor()\type = type
For a = 0 To #actorSubtypeMax
actor()\subtype[a] = subtype(a)
Next
actor()\drawPriority = drawPriority
actor()\visible = visible
actor()\collision = collision
actor()\blockLoS = blockLoS
actor()\x = x
actor()\y = y
locatePlayer()
EndProcedure
Anschließend muß ich den *player-Pointer zurück auf den Spieler-Actor setzen, da nach addElement() die Speicheradresse nicht mehr gültig ist:
Code: Alles auswählen
Procedure locatePlayer()
ForEach actor()
If actor()\type = #monsterActor And actor()\subtype[0] = 0
*player = @actor()
ProcedureReturn
EndIf
Next
EndProcedure
Und ich muß in der Lage sein, erstellte Elemente wieder zu löschen, ohne daß meine Actor-Library "kaputt" geht ...
Dazu rufe ich killActor(@actor()) auf - so kann ich gewährleisten, daß im Anschluß der *player-Pointer wieder hergestellt wird:
Code: Alles auswählen
Procedure killActor(*deletePointer)
ChangeCurrentElement(actor(),*deletePointer)
DeleteElement(actor())
locatePlayer()
EndProcedure