VIDEO:
Roguelike-Diary 04: Flying Monsters, Ranged Combat, Effect-Actors
Uuuuuund, Update!
Die Fernkampf-Mechaniken sind implementiert! War ganz schön kniffelig, hat aber Spaß gemacht, das auszuknobeln ...
Wie funktionierts?
Zunächst checke ich, ob eine freie Schußbahn existiert, das mache ich mit dem Bresenham-Algorithmus, allerdings füttere ich ihn nicht mit Line-of-Sight-Daten, sondern mit den Informationen aus der collisionMap(). Außerdem habe ich, für schnellere Abfragen, ein interactionMap()-Array initialisiert, in dem gespeichert bleibt, mit welchem sichtbaren Felder man interagieren kann (Türen, Monster, etc.). Dieses wird nach jedem Schritt, genau wie die bresenMap() und die collisionMap(), aktualisiert.
Wird jetzt auf ein weiter entferntes Monster ein Rechtsklick ausgeführt UND die interactionMap(x,y) gibt ihr Okay (Ziel wird nicht durch andere Actors() oder Mauern geblockt), ist die Voraussetzung für erfolgreichen Fernkampf bereits erfüllt und man könnte direkt zur Schadensverteilung übergehen; ohne Schußanimation wär's aber nur halb so schön:
Dabei hilft mir die Geradengleichung: ich stelle die Gleichung auf mit den absoluten Koordinaten (nicht den Tile-Koordinaten!) von der Mitte des Startfeldes zur Mitte des Zielfeldes. Anschließend überprüfe ich die x/y-Achsen des umschließenden Rechteckes und entscheide so, ob ich nach x oder y auflöse (so umgehe ich die gefürchtete Division durch Null, außerdem erhalte ich auch bei sehr steilen Geraden eine durchgängige Linie).
In die fertige Gleichung (Zweipunkteform) setze ich x, bzw. y Koordinaten mit einem Vorschub von 10 Pixeln ein, und speichere die neuen Punkte als Elemente in einer Linked-List (kleinere Werte ergeben eine langsamere Schuß-Animation). Dann überprüfe, ob sich das Startfeld an erster Stelle der LinkedList befindet; falls nicht, initialisiere ich einen Vorschub-Modifikator mit -1. Und dann gehe ich an den Anfang (bzw. Ende) der Liste und arbeite diese nach oben (bzw. unten) ab.
Außerdem gibts Fliegende Monster - das Prinzip ist hier sehr einfach: jeder Actor() hat einen bestimmten Kollisionswert: der Spieler, normale Monster, Bäume, Schatztruhen, etc. haben 1, fliegende Monster 2, und geschlossene Türen und Mauern 4. Bei jedem Schritt wird die collisionMap() aktualisiert, und zwar wird der jeweilige Kollisionsfaktor vom alten Feld subtrahiert, und aufs neue Feld aufaddiert. Und da jeder Actor() sich nur auf Felder bewegen darf, welche einen geringeren Kollisionwert haben als sie selbst, können so manche Monster über andere Monster und Objekte hinwegfliegen, aber nicht durch geschlossene Türen. Und weil 1+2 = 3, können maximal 2 Actors() auf ein und demselben Feld verweilen.
Noch eine Anmerkung zur weiter oben beschriebenen "drawPriority": jeder neu erstelle Actor() wird in die LinkedList nach "drawPriority" sortiert eingetragen - fliegende Monster haben eine höhere drawPriority als der Spieler oder Bäume. Die Zeichenroutine klappert die Actorliste von unten nach oben ab, während bei Interaktionen von oben nach unten gesucht wird. Deswegen wird man, wenn man z.B. auf ein Monster in einer offenen Tür klickt, immer zuerst das Monster angreifen! Die Tür kann erst geschlossen werden, wenn sich kein anderer Actor() mehr auf demselben Feld befindet.
Last but not least, Effect-Actors:
Das sind die "Blutspritzer" bei Treffern. Könnten aber genauso gut Teleporter-Animation sein, ein Schutzschild oder Vergiftungseffekt. Auch diese "Effekte" sind Actors, die genauso wie Monster und Objekte über die actorFactory() erstellt werden. Ein Effect-Actor hat keine Kollision und blockiert keine Sichtlinie. Er hat x und y Koordinaten wie jeder andere Actor (und zusätlich frames, timer, frameTimer und killLimits). Und, jetzt wird's spannend: es ist möglich, Effect-Actors an andere Actors() zu koppeln! Das ergibt Sinn bei eben jenen Blutzspritzern, die ge
timed sind und nach Ablauf ihrer Lebenszeit wieder gelöscht werden - deswegen müssen sie in der Lage sein, ihren
Parent-Actors zu folgen, wenn sich diese bewegen bevor der
animKillTimer auf Null gezählt hat - auch das wird automatisch in der actorFactory() erledigt:
Es gibt einen globalen actorCounter, der am Anfang immer auf Null steht und den Index des allerersten erstellten Actors festlegt. Wenn jetzt die actorFactory() aufgerufen wird, hat man die Option, einen index-Parameter mitzugeben - für eigenständige Actors ist das immer -1, damit wird der actorFactory() mitgeteilt, daß sie den aktuellen Index vergeben, und anschließend um 1 nach oben zählen soll. Für gekoppelte Effect-Actors() wird stattdessen der Index des
Parent-Actors mitgegeben! Und das ist schon das ganze Geheimnis:
In der Routine, in der Monsterbewegungen reguliert werden, gibt es zwei lokale Arrays (newX() und newY()), die bei Bewegug eines Actors mit den jeweiligen neuen Koordinaten aktualisiert werden. So kann ich am Ende, nachdem sich alle Actors einmal bewegt haben, einmal die Actor()-Liste, gefiltert nach actor()\type = #animationActor abklappern und schauen, wo ein newX() Wert mit der jeweiligen Index-Nummer korreliert, und so die x/y-Koordinaten des Effect-Actors anpassen (der Blutspritzer zieht quasi hinterher).
So, das war's erstmal.
Uff.