Roguelike-Diary (WIP ...)

Spiele, Demos, Grafikzeug und anderes unterhaltendes.
Benutzeravatar
mk-soft
Beiträge: 3695
Registriert: 24.11.2004 13:12
Wohnort: Germany

Re: Roguelike-Diary (WIP ...)

Beitrag von mk-soft »

Code: Alles auswählen

Procedure locatePlayer()
   ForEach actor()
      If actor()\type = #monsterActor And actor()\subtype[0] = 0
         *player = @actor()
         ProcedureReturn
      EndIf
   Next
EndProcedure
Achtung! ProcedureReturn ohne Angabe gibt das Register RAX (x64) oder EAX (x86) zurück. Dies ist für ASM-Programmierung gedacht.

Wenn die Variable *player nicht als Global gedacht ist, müsste die Procedure so lauten...

Code: Alles auswählen

Procedure locatePlayer()
   Protected *player
   ForEach actor()
      If actor()\type = #monsterActor And actor()\subtype[0] = 0
         *player = @actor()
         ProcedureReturn *player
      EndIf
   Next
 EndProcedure
 
 ; oder
 
 Procedure locatePlayer()
   Protected *player
   ForEach actor()
      If actor()\type = #monsterActor And actor()\subtype[0] = 0
         *player = @actor()
         Break
      EndIf
   Next
   ProcedureReturn *player 
EndProcedure
Alles ist möglich, fragt sich nur wie...
Projekte ThreadToGUI / EventDesigner V3 / OOP-BaseClass-Modul
Downloads auf MyWebspace / OneDrive
Benutzeravatar
diceman
Beiträge: 347
Registriert: 06.07.2017 12:24
Kontaktdaten:

Re: Roguelike-Diary (WIP ...)

Beitrag von diceman »

mk-soft hat geschrieben: Achtung! ProcedureReturn ohne Angabe gibt das Register RAX (x64) oder EAX (x86) zurück. Dies ist für ASM-Programmierung gedacht.
Whoopsie! :o
Das wußte ich nicht! Habe zwar keine Ahnung was das bedeutet, aber es ist definitiv kein Feature, welches ich für mein Roguelike benötige; also bei vorzeitigem ProcedureReturn immer eine Null zurückgeben, richtig? Daran kann ich mich gewöhnen ... scheint sich ja auszuzahlen, das ich hin und wieder Code-Snippets poste - ihr seid super! :allright:
Der locatePlayer()-Murks ist übrigens wieder draußen, der war tatsächlich unnötig.
Und den *player-Pointer halte ich gerne global, weil ich den eigentlich ständig brauche.

Habe das Handling meiner collisionMap() und bresenMap() noch optimiert - die binaryMap() gibt es nicht mehr! Anstatt vor jeder Pfadsuche die collisionMap() zu updaten, ist diese jetzt permanent gültig! Es reicht, einmal nach Erstellung des Dungeons und Actor-Creation ein Update zu fahren, und dann brauche ich nur noch spezifisch für jeden sich bewegenden Actor einmal auf die entsprechenden Felder des collision()-Array zugreifen und diese zu aktualisieren. Ebenso werden jetzt in der actorFactory() und in der killActor()-Prozedur die entsprechenden Felder im Array sofort aktualisiert.
Ebenso bei Line-of-Sight-Berechnungen: ich habe zwei Arrays, eine bresenRef(), welche permanent mit aktualisierten Daten über LoS-blockierende actor()-Elemente gefüttert wird, aber nicht mehr komplett geupdated werden muß, und eine temporäre bresenMap(), welche nach jedem Schritt neudimensioniert wird und dann in der LoS-Routine anhand der bresenRef()-Daten die aktuelle Line-of-Sight-Matrix liefert.

Morgen gibt's ein Update mit Monstern!
Now these points of data make a beautiful line,
And we're out of Beta, we're releasing on time.
Benutzeravatar
Sicro
Beiträge: 955
Registriert: 11.08.2005 19:08
Kontaktdaten:

Re: Roguelike-Diary (WIP ...)

Beitrag von Sicro »

diceman hat geschrieben:
mk-soft hat geschrieben:Achtung! ProcedureReturn ohne Angabe gibt das Register RAX (x64) oder EAX (x86) zurück. Dies ist für ASM-Programmierung gedacht.
...
also bei vorzeitigem ProcedureReturn immer eine Null zurückgeben, richtig?
Wenn du den Rückgabewert von der Prozedure "locatePlayer" nirgends verwendest, ist es egal, wenn du bei "ProcedureReturn" keinen Wert angibst.
Vereinfacht dargestellt möchte dich mk-soft auf das hinweisen:

Code: Alles auswählen

Procedure Beispiel()
  ;a$ = "test" ; Entferne das Semikolon am Anfang und die Procedure liefert ein anderes Ergebnis
  ;b$ = "bla"  ; Wenn du hier das Semikolon ebenfalls entfernst, ändert sich das Ergebnis wieder
  ProcedureReturn
EndProcedure

Debug Beispiel()
Wie du siehst, liefert die Procedure mit ProcedureReturn ohne Wert keinen Null-Wert.
Bild
Warum OpenSource eine Lizenz haben sollte :: PB-CodeArchiv-Rebirth :: Pleasant-Dark (Syntax-Farbschema) :: RegEx-Engine (kompiliert RegExes zu NFA/DFA)
Manjaro Xfce x64 (Hauptsystem) :: Windows 10 Home (VirtualBox) :: Neueste PureBasic-Version
Benutzeravatar
diceman
Beiträge: 347
Registriert: 06.07.2017 12:24
Kontaktdaten:

Re: Roguelike-Diary (WIP ...)

Beitrag von diceman »

VIDEO:
Roguelike-Diary 03: Monsters, World-Interaction, Alpha-Map-Test

:)
Rapid Prototyping ... Ich habe mir zur Regel gemacht, daß ich fürs erste keine externen Grafiken verwende, und mich nur auf die Purebasic eigenen 2D-Zeichenroutinen verlasse. Damit will ich verhindern, daß ich mich in irgendeiner Art und Weise verklüngele und Zeit verschwende, welcher besser investiert wäre mit der Fertigstellung eines Minimum Viable Products.
Been there, done that.
So geschehen mit meinem Blitzbasic-Projekt, wo ich gut ein halbes Jahr in die Routinen eines prozeduralen Dungeon-Generators gesteckt habe ... gut, von der Arbeit werde ich jetzt im Nachhinein profitieren, wenn ich denn mal soweit bin, daß ich "gute" Level-Generierung haben möchte; der Code ist nicht kaputt oder verloren, den kann ich mit etwas Aufwand problemlos nach PureBasic übersetzen.
Aber das ist momentan einfach nicht notwendig. Ich habe eine quick-and-dirty Generierungs-Routine geschrieben, die weder auf lösbare Level noch interessante Architektur achtet, aber es ist absolut ausreichend. So kann ich mich ganz auf die Entwicklung der Core-Mechaniken konzentrieren; und dann erst kommt Theme, Gameplay & Content.

Eine ganz interessante Kopfnuss waren die Türen ... nicht vom Programmierungsaufwand her, man hätte das in 5 Minuten hardcoden können, aber ich wollte eine maximal elegante, modulare Lösung. Habe mich letztendlich für folgendes entschieden: Offene Türen und geschlossene Türen sind zwei verschiedene actor()-Elemente; geschlossene Türen haben Kollision und blocken Sichtlinie, Offene Türen haben weder noch. Wenn ich jetzt mit der rechten Maustaste (Interaktion) auf eine Tür klicke, wird ein Pointer auf den actor() gelegt, der Status der Tür wird ausgelesen (geschlossen/offen?), anschließend wird an derselben Stelle ein neues actor()-Element vom gegenteiligen Typ in der actorFactory() produziert (welche gleichzeitig alle Parameter bezüglich Kollisions-Map und Sichtlinien-Parameter regelt). Pointer zurück auf ursprüngliches actor()-Element, und freigeben zum löschen in der killActor()-Prozedur (auch hier muß ich mich nicht mehr ums updaten des World-Status kümmern, das passiert alles automatisch). Und nach jeder erfolgreichen Aktion (Schritt/Interaktion) wird die Sichtlinie aktualisiert.

Die Interaktion mit Monstern ging überraschend problemlos über die Bühne; ich brauchte lediglich einen neuen Pointer für den Target-actor(), und ein paar Prozeduren (performMeleeCombat(), moveToAnimation(), damageActor() ), welche den Ablauf maximal abstrahiert regeln, so daß theoretisch auch Monster gegen Monster kämpfen können, so sie denn mal eine KI haben und den Respekt vor der collisionMap() verlieren.

Außerdem neu (nicht im Video zu sehen) ist Auflösungs-unabhängige Darstellung: die Größe des Viewports wird jetzt automatisch, abhängig von der eingestellten Fenstergröße, berechnet. Die maximale Sichtweite bleibt natürlich konstant, die wurde anhand der niedrigstmöglichen Auflösung (800x600) festgelegt, und bleibt auch Benchmark fürs Balancing. Höhere Auflösungen bieten lediglich den Vorteil, daß man weniger scrollen muß um alles zu sehen.

Next in line: rudimentäre Monster-KI und Monster-Interaktion mit der Welt.
Zuletzt geändert von diceman am 11.03.2018 16:28, insgesamt 2-mal geändert.
Now these points of data make a beautiful line,
And we're out of Beta, we're releasing on time.
Benutzeravatar
Bisonte
Beiträge: 2427
Registriert: 01.04.2007 20:18

Re: Roguelike-Diary (WIP ...)

Beitrag von Bisonte »

Klingt ja schon beinahe wie : RogueLike GameCreator ;)
PureBasic 6.04 LTS (Windows x86/x64) | Windows10 Pro x64 | Asus TUF X570 Gaming Plus | R9 5900X | 64GB RAM | GeForce RTX 3080 TI iChill X4 | HAF XF Evo | build by vannicom​​
Benutzeravatar
Mijikai
Beiträge: 754
Registriert: 25.09.2016 01:42

Re: Roguelike-Diary (WIP ...)

Beitrag von Mijikai »

Sieht gut aus :)

Gibt es Pläne für
- eine Minimap?
- eine Zentralisierung des Spielers?
Benutzeravatar
diceman
Beiträge: 347
Registriert: 06.07.2017 12:24
Kontaktdaten:

Re: Roguelike-Diary (WIP ...)

Beitrag von diceman »

Den Spieler zentriert man entweder durch Drücken der SPACE-Taste oder wenn der Mauszeiger länger als 0.5 Sekunden auf dem Spieler-actor() ruht. Das hat den schönen, intuitiven Nebeneffekt, daß wenn du also ein Feld anvisiert hast und da ankommst, die Map automatisch zentrierst, wenn du einen winzigen Augenschlag Geduld hast. Das ist aber ein Quality-of-Life-Feature, bei dem es sich anbietet, es "optional" zu gestalten, also daß man es dem jeweiligen Anbieter überlässt, den gewünschten Intervall für automatische Zentrierung selbst im Optionsmenü festzulegen. :-)
Permanente Zentrierung möchte ich aus dem einfachen Grund nicht, weil ich mit einer von der Spielerposition losgelösten "Kamera" eine größere Sichtweite simulieren kann. Wenn du den Mauszeiger an den Spielfeldrand bewegst, "scrollt" (bzw. springt) die Map in die entsprechende Richtung. Wenn du das Spiel in einer höheren Auflösung startest, hast du zwar keine größere Sichtweite, aber einen weiteren Viewport, mußt also weniger scrollen. :-) Im oben geposteten Video habe ich die Sichtweite absichtlich etwas reduziert, damit man den getesteten Alpha-Map-Effekt ausreichend sehen kann; war mein erstes Mal, daß ich mich mit sowas beschäftigt habe, in Blitzbasic hatte ich diese Möglichkeit nicht, zumindest nicht in der reinen 2D-Programmierung (selber 3D-Applikationen zu schreiben hat mich nie interessiert).
Minimap im klassischen Sinne wohl nicht, aber ich plane ein Map-Item, welches man auf jeder Ebene finden kann, welches einerseits die umliegende, von der Sichtlinie ausgeschossene, bereits entdeckte Architektur als Schattenumriss zeigt, und auf dem man den Dungeon-Umriss mit evtl. eingezeichneten Points of Interest angucken kann. Das ist atmosphärischer, als einfach nur auf den oberen rechten Bildschirm zu starren und blinkenden Punkten hinterher zu laufen. :wink:
Ich würde sehr gerne ein Spiel im Lovecraft-Universum basteln, aber davon bin ich noch weit entfernt; ich habe zwar jede Menge Ideen, will mich aber ungern auf Details festlegen, bevor ich nicht die "Core-Engine" mit den wesentlichen Roguelike-Mechaniken fertiggestellt habe. Das war mein großer Fehler, den ich bei meinem Blitzbasic-Projekt gemacht habe - da habe ich mich total verzettelt und alle möglichen Features gebastelt, die sich cool anfühlten, und als ich dann große Milestones implementieren wollte, wie Nahkampf, etc. hatte ich all diese schlecht optimierten Nebenroutinen, "Features" und Content-Generatoren, welche mir häufig den Blick aufs große Ganze versperrt haben, und das Implementieren von neuen Core-Features unnötig verkompliziert haben.
Now these points of data make a beautiful line,
And we're out of Beta, we're releasing on time.
Benutzeravatar
diceman
Beiträge: 347
Registriert: 06.07.2017 12:24
Kontaktdaten:

Re: Roguelike-Diary (WIP ...)

Beitrag von diceman »

VIDEO:
Roguelike-Diary 04: Flying Monsters, Ranged Combat, Effect-Actors

Uuuuuund, Update! 8)
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 getimed 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. :coderselixir:
Now these points of data make a beautiful line,
And we're out of Beta, we're releasing on time.
Benutzeravatar
RSBasic
Admin
Beiträge: 8022
Registriert: 05.10.2006 18:55
Wohnort: Gernsbach
Kontaktdaten:

Re: Roguelike-Diary (WIP ...)

Beitrag von RSBasic »

+1 :allright:
Aus privaten Gründen habe ich leider nicht mehr so viel Zeit wie früher. Bitte habt Verständnis dafür.
Bild
Bild
Antworten