Seite 9 von 13

Re: Roguelike-Diary (WIP ...)

Verfasst: 29.04.2018 01:10
von diceman
Roguelike Diary 18: Interactive Message Elements and Tagging Actors

Kleines Update:

• Meine Message-Routine kann jetzt ein kleines Auswahlmenü für Optionen liefern.
• Ebenfalls ist es möglich, Actors zu "taggen": also wenn ich den Mauszeiger auf einem Actor (bevorzugt einem Monster) liegen habe und TAB drücke, kann ich ein Info-Panel zum Actor aufrufen.
• Der positive Nebeneffekt: ich kann das System auch prima zum Debuggen verwenden: dazu habe ich es so eingerichtet, daß ein einmal getaggter Actor getaggt bleibt, solange bis man den Mauszeiger auf einen neuen Actor bewegt und wieder TAB drückt ... wenn ich zwischenzeitlich TAB drücke, ohne eine einen Actor angewählt zu haben, kann ich die Stats-Ansicht vom ausgewählten Actor updaten, und so "verfolgen", auch wenn er sich außerhalb des Sichtradius befindet.

Re: Roguelike-Diary (WIP ...)

Verfasst: 29.04.2018 14:58
von diceman
Ich habe soeben Monstern die Fähigkeit gegeben, sich zusammenzurotten.
Bzw. kann ich dies als Verhaltensmuster für eine Monsterklasse festlegen, und das hat dann diverse Auswirkungen, z.B. versuchen jetzt Monster aktiv einen in eine Falle zu locken! :o
Bin einer fliehenden Fledermaus hinterher gelaufen, die um eine Ecke bog, hinter der ein ganzer Schwarm ausgeruhter Fledermäuse nur darauf wartete, sich auf mich zu stürzen. :o

Wie funktionierts?

• Man initialisiere eine neue Dijkstra-Map (gatherDijkstra())
• Man setze jedes Monster mit Fähigkeit sich zusammenzurotten auf eine offene Liste, dann pickt man sich zufällig 55% aller Elemente der Liste heraus, und setzt die jeweiligen Koordinaten auf der gatherDijkstra() auf 1. Dies sind die bevorzugten Sammelpunkte für Fledermäuse (je höher man den Prozentwert ansetzt, umso kleiner und sporadischer sind die Grüppchen).
• Monster, wenn sie eine Bewegung machen, überprüfen immer alle 8 umliegenden Tiles ob es gültig ist, dann setzen sie diese Tiles auf eine offene Liste mit dem entsprechenden Dijkstra-Value, abhängig ob sie sich dem Spieler nähern, ihn suchen oder vor ihm fliehen. Von dieser offenen Liste werden alle Tiles gelöscht, welche <> lowest Dijkstra-Value. Von den verbleibenden Tiles pickt sich das Monster ein zufälliges heraus. Soweit, so funktioniert es.
• Wenn man jetzt, wenn eine Fledermaus ihre offene Liste an Bewegungs-Tiles zusammenstellt, für jedes dieser Tiles den entsprechenden Wert von der gatherDijkstra()-Map (multipliziert mit einem Faktor, wie wichtig ihnen Gesellschaft ist), aufaddiert, hat man auf einmal, dank simpler Mathematik, ein Monster, welches zu jedem Zeitpunkt in der Lage ist, abzuwägen, welcher Weg momentan der attraktivste ist (irgendwas zwischen 25-35% hat sich gut bewährt). 8)

"Ich bin verwundet, ich muß fliehen."
"In der Nähe befinden sich ein paar Artgenossen."
"Lohnt es sich dorthin zu gehen, oder kann mir der Spieler vorher den Weg abschneiden?"

• Und das tolle ist: die gatherDijkstra() muß man nur updaten, sobald sich ein Objekt mit #pathCollision ändert, eine Fledermaus stirbt, oder eine weitere Fledermaus "geboren" wird. Ansonsten bleibt sie als statische Referenz für Entscheidungsfindung von Fledermäusen relativ konstant, und stiehlt keinerlei Performance. 8)

Re: Roguelike-Diary (WIP ...)

Verfasst: 29.04.2018 18:24
von diceman
Und hier ein paar Türen und zugehörige Schlüssel (die erste Tür in der Reihe ist unverschlossen).
Ich habe mir Mühe gegeben, daß man sie nicht nur farblich, sondern auch optisch unterscheiden kann:

Bild
Bild

Re: Roguelike-Diary (WIP ...)

Verfasst: 30.04.2018 11:37
von diceman
Ich arbeite jetzt mit einem "Faction-System":
Das heißt, jedes Monster wird bei Erstellung einer Gruppe zugeteilt. So kann ich leicht filtern, ob ein Monster ein gültiges Ziel für einen Angriff ist.
Auch Pets sind mit diesem System möglich (Monster, die dem Spieler folgen und in seiner Agenda dienen).
5 Factions habe ich initialisiert:

#nullFaction
Wird vom Debugger als Fehler klassifiziert; das Programm bricht ab und gibt den Index des Monsters ohne Faction zurück

#playerFaction
Hierzu gehören Spieler und Monster, die auf seiner Seite kämpfen

#monsterFaction
Die Antagonisten-Fraktion; SpielerFraktion und MonsterFraktion sind per Default verfeindet

#npcFaction
Wenn ich hierfür isEnemy(*requester.ACTOR,*compare.ACTOR) aufrufe, wird die Prozedur IMMER ein #False zurückgeben.

#rogueFaction
Wird jeweils von #playerFaction UND #monsterFaction als Feind eingestuft und umgekehrt. Damit ist es mir später möglich, Zustände wie Verwirrung zu simulieren (Confusion-Spell), wenn ein Monster seine Artgenossen angreift.


Außerdem kleiner KI-Tweak:

Bild

Ein Monster, welches in die Sackgasse gedrängt wurde (das heißt ein Feind befindet sich auf einem angrenzenden Feld, und es hat sich in dieser Runde weder bewegt noch angegriffen, weil der KI-Baum eine Movement-Aktion vorgegeben hat), wird jetzt IMMER einen Nahkampfangriff ausführen.

Außerdem geraten Fernkampf-Monster in Panik, wenn sie sich zulange in direkter Nachbarschaft zu einem feindlichen Fraktion befinden, und keinen Abstand gewinnen konnten; ein panicCounter zählt nach oben, und wird erst wieder zurückgesetzt, wenn isAdjacent() ein #False zurückgibt. Sobald der panicCounter > 3 erreicht, wird im KI-Baum die Frage, ob Nahkampf oder nicht, immer mit einem #True überschrieben. Ein Monster in Panik wird also IMMER einen Nahkampfangriff ausführen. Damit fange ich einen edge case auf, daß Fernkampf-Monster, welche nur 2 Tiles Bewegungsspielraum haben und sich beide in direkter Nachbarschaft zum Spieler befanden, immer hin und her gezogen sind anstatt anzugreifen, im verzweifelten Versuch, Abstand zu gewinnen.

Re: Roguelike-Diary (WIP ...)

Verfasst: 03.05.2018 14:33
von diceman
Worklog:

• Man kann jetzt zwischen Dungeon-Ebenen "reisen". Das heißt, wenn man den Ausgang gefunden hat, wird ein neuer Dungeon erstellt, bzw. falls die Zielkoordinate des Exit-Teleporters ein #True zurückgibt, wird stattdessen die existierende Dungeon-Ebene geladen.
• Ich arbeite ausschließlich mit lokalen Daten: Beim "reisen" wird die aktuelle Ebene (floorData) auf Festplatte gespeichert, und nur die Spieler-relevanten Daten werden in die nächste Ebene übernommen. Das hat den Vorteil, daß meine Listen schön klein bleiben, und ich muß mir keine Sorgen über Zugriffsgeschwindigkeiten machen.
• Ein paar Fallstricke bringt das System mit sich, z.B. muß man sich genau Gedanken machen, welche Daten relevant sind, welche überschrieben werden, wie ich gewährleiste, daß wirklich alles gelöscht, bzw. geladen wird, in welcher Reihenfolge Variablen initialisiert werden, Fragen wie: muß diese Variable/Liste, etc. wirklich gespeichert werden, oder kann ich deren Zustand anhand bestehender Spieldaten wiederherstellen?
• Zu dem Zweck habe ich mir jetzt eine Prozedur namens "Game Manager" gebastelt, die alle diese Dinge organisiert. Wenn immer geladen, gespeichert, ein neues Spiel gestartet, in anderen Worten, die GameLoop verlassen wird, wird ein sogennanter "Job" kreiert, welcher vom gameManager() an die zuständige Instanz weitergeleitet wird.

Folgende Jobs existieren derzeit:

Code: Alles auswählen

Global jobDescription
Enumeration 0
	#nullJob
	#newGameJob
	#loadJob
	#exploreJob
	#travelJob
	#saveJob
	#quitJob
EndEnumeration
#nullJob
Der Debugger schlägt Alarm.

#newGameJob
Das aktuelle Spielobjekt schreddern und ein neues Spiel starten.

#loadJob
Das aktuelle Spielobjekt schreddern und einen Spielstand laden (coreData UND floorData).

#exploreJob
Eine neue Ebene wird betreten; der Dungeon existiert noch nicht und muß erstellt werden.
Aktuelle floorData muß gespeichert werden

#travelJob
Eine neue Ebene wird betreten; die Ebene existiert bereits und wird geladen.
Aktuelle floorData muß gespeichert werden.

#saveJob
Spielstand speichern (coreData UND floorData)

#quitJob
Duh. :doh:

Weitere denkbare Jobs für die Zukunft:

#restartCampaignJob
Für den Fall, daß ich mich für ein Modell nach DIABLO-Vorbild entscheide, bei dem man nach Spielende einfach von vorne weiterspielen kann, ohne daß Level-Progression und Inventar gelöscht werden.

#deleteSavesJob
Was wäre das für ein Roguelike ohne Permadeath? :twisted:





Mein Creator-Tool ist jetzt in der Lage, bessere und interessantere Loops zu kreieren. Bislang funktioniert das ausschließlich über ein brute force Verfahren, das zufällig gesucht hat, wo man 2 Parts vom Dungeon durch Graben eines Ganges verknüpfen kann und dies solange wiederholt hat, bis eine geseedete Obergrenze an Loops erstellt wurde (anschließend wurde zerstörte Architektur gefixt).
Ich habe das Verfahren dahingehend getweaked, daß vorm Graben selektiert wird, ob beide Parts des Dungeons einen unterschiedlichen Index haben, außerdem wird vor dem Graben ein A*-Search von Punkt a zu b durchgeführt und für die Aktion ein "Evaluierungs-Score" vergeben: je weiter der reguläre Weg und umso kürzer der zu grabende Loop, umso höher der Score:

Bild





Abschließend einen Überblick über in die Tools, welche mir zur Verfügung stehen, wenn es ans Erzeugen von Zufallszahlen geht:

Code: Alles auswählen

Procedure weightedRoll(n1,n2,n3,n4,n5,n6,n7,n8,n9,n10)
  Define nMax
  Define nDice
  Define nAdd
  Dim chance(10)
  Define a
  
  chance(1) = n1
  chance(2) = n2
  chance(3) = n3
  chance(4) = n4
  chance(5) = n5
  chance(6) = n6
  chance(7) = n7
  chance(8) = n8
  chance(9) = n9
  chance(10) = n10
  
  For a = 1 To 10
    nMax = nMax + chance(a)
  Next
  nDice = Random(nMax,1)
  
  For a = 1 To 10
    nAdd = nAdd + chance(a)
    If nAdd >= nDice
      ProcedureReturn a
    EndIf
  Next
EndProcedure
weightedNumber(0,4,1,0,0,0,0,0,0,5) gibt mir entweder eine 2,3 oder 10 zurück mit entsprechenden Wichtungsfaktoren:
2 = 40% Chance
3 = 10% Chance
10 = 50% Chance

Code: Alles auswählen

Procedure gaussRoll(minValue,maxValue,ceilWeight)
	Define newMax
	Define add		
	Define sum
	Define dice
	Define a
	Define n
	
	newMax = (maxValue-minValue)+1
	sum = (newMax+1)*(newMax*0.5)
	dice = Random(sum,1)
	For a = 0 To newMax
		add = add+a
		If add >= dice
			If ceilWeight
				ProcedureReturn (a + minValue)-1
			Else
				If Not ceilWeight 
					ProcedureReturn ((newMax-(a-1))+minValue)-1
				EndIf
			EndIf
		EndIf
	Next
EndProcedure
Simuliert einen "Kleinen Gauss-Würfel":
gaussRoll(1,5,#True) erzeugt einen 15 seitigen Würfel, von dem 5 Seiten eine 5 haben, 4 eine 4 und so weiter bis zur 1, von der nur eine Fläche existiert. Mit diesem Würfel wird einmal gewürfelt und die Zahl zurückgegeben. Dies ist in beide Richtungen möglich:
gaussRoll(2,3,#False) erzeugt mit 66% Wahrscheinlichkeit eine 2, und mit 33% Wahrscheinlichkeit eine 3.

Code: Alles auswählen

Procedure bellCurveRoll(min,max)
	Define value = max-min
	Define modValue = Mod(value,2)
	Define dice
	
	value/2
	dice = Random(value)+Random(value+modValue)+min
	
	ProcedureReturn dice
EndProcedure
Teilt jede beliebige Differenz auf zwei möglichst gleichflächige Würfel auf und gibt mir eine zufällige Zahl gemäß der Gauß'schen Normalverteilung wieder.

Code: Alles auswählen

Procedure wellCurveRoll(min,max)
	Define lowMedian, highMedian	
	Define absLow, absHigh
	Define minusDice = 0
	Define plusDice = 1
	Dim dice(1)

	
	lowMedian = Round((max+min)/2,#PB_Round_Down)
	highMedian = Round((max+min)/2,#PB_Round_Up)
	
	dice(minusDice) = Random(lowMedian,min)
	dice(plusDice) = Random(max,highMedian)
	absLow = Abs(dice(minusDice)-lowMedian)
	absHigh = Abs(dice(plusDice)-highMedian)
	
	If absLow = absHigh
		ProcedureReturn dice(Random(plusDice,minusDice))
	EndIf
	If absLow > absHigh
		ProcedureReturn dice(minusDice)
	EndIf
	If absHigh > absLow
		ProcedureReturn dice(plusDice)
	EndIf
EndProcedure
Das gleiche, nur invertiert: Die Funktion erzeugt bevorzugt "extreme" Werte mit Wichtung zu beiden Enden der Kurve.

Re: Roguelike-Diary (WIP ...)

Verfasst: 21.05.2018 17:19
von diceman
Viel Neues zu zeigen gibt es momentan nicht, aber an dieser Stelle trotzdem ein kleiner Lagebericht ... nicht daß ihr denkt, ich hätte das Handtuch geworfen! 8)


Ich stecke immer noch in meinen Inventory-Funktionen fest ...
Das ist eine ziemlich umfassende und komplexe Angelegenheit, und ich habe mir die Aufgabe in klitzekleine Bröckchen aufgespaltet, so daß ich jeden Tag wenigstens etwas von der Liste streichen kann. Die Idee ist, daß alle slotInterfaces (Inventory, equipmentSlots, craftingSlots, containerSlots für multiple Items auf ein und demselben Tiles) alle auf dieselbe Art und Weise funktionieren. Und die drawFunction filtert nur, welche Art von slotInterface gerade aktiv ist (auch mehr als eines), und greift auf die entsprechenden temporären Pointer zu, die jedesmal aufs Neue erstellt werden, wenn man ein slotInterface öffnet. Dabei kann jedes slotInterface auch multiple Tabs haben, zwischen denen man hin und herschalten kann. Für das playerInventory ist die Anzahl der verfügbaren Slots eine "softe" Konstante (kann im Laufe des Spiels bis zu einem festgelegten Maximalwert gesteigert werden), aber für containerSlots wird dieser Wert immer on-the-fly erstellt, da theoretisch unendlich viele Items auf ein und demselben Tile abgelegt werden können. Die Anzahl der notwendigen Tabs wird auch on-the-fly berechnet (hierfür gibt es einen maximal Wert an Slots, welche auf einem Tab dargestellt werden können).
Equipment-Slots sind eine Konstante.

Die Anzeige funktioniert schon. Das heißt, wenn ich einen Container öffnet, wird mir sein Inhalt angezeigt, ebenso wenn ich das Inventory öffne.
Die nächsten Schritte:

• Das Erkennen von selektierten Slots und dem jeweiligen Inhalt.
• Items aus Container per Klick "aufheben" (ins playerInventory transferieren)
• Items aus dem Inventory droppen
• Items bei geöffnetem Inventory zwischen Slots hin und herbewegen
• Items benutzen/kombinieren/ausrüsten
• Auf den ersten unvermeidlichen Item-Dupe-Bug warten, und unschädlich machen

Bild

Re: Roguelike-Diary (WIP ...)

Verfasst: 24.05.2018 20:10
von diceman
*** Video ***
Roguelike Diary 19: Containers and Interactive Inventory


Ich war fleißig in der vergangenen Woche, ich nähere mich mit großen Schritten der Vollendung meines letzten großen "Engine"-Meilensteins ...
Am schwierigsten war es hier, einen Startpunkt zu finden. Nach zwei Tagen Prokrastination habe ich einfach an irgendeiner Ecke angefangen, ins Blaue zu hardcoden, dann stellte sich nach und nach der Flow ein, und als die ersten Schritte getan waren, fiel es zunehmend leichter, einen Plan zu erstellen, welche Features nötig sind, und wie diese am effizientesten zu implementieren sind, und wie ich meinen Code nach und nach modularer und zunehmend abstrakter strukturieren konnte. Tatsächlich nutzen sowohl die Container, als auch das Inventory die exakt gleichen Prozeduren (mit ein paar Special Rules on top); . Und ich weiß nicht, wie es euch geht, aber für mich hat sich Klassische Musik beim Coden extrem bewährt ... ein bißchen Mozart und Beethoven im Hintergrund, und ich kann buchstäblich meine Synapsen klicken hören. :wink:

Der Großteil von meinem Inventory-System steht, es ist Interaktion möglich, Inventory-Tabs, und ebenso sind Container implementiert; Container verhalten sich auf der Basis-Ebene wie Items, aber man kann sie nicht aufheben; sie dienen lediglich als Schnittstelle zwischen dem Spieler und der Welt, wenn dieser mit einem Tile interagiert, auf dem mehr als ein Item liegt.

• Container werden automatisch erstellt und auch wieder gelöscht.
• Jedesmal wenn ein Item aufgehoben oder fallengelassen wird, checkt die entsprechende Routine, ob ein Container erstellt werden muß oder obsolet geworden ist.
• Wenn ein Container erstellt wird, werden alle anderen Items auf dem Feld unsichtbar und verlieren ihr Interaktions-Flag.
• Interaktion ist fortan nur über den Container möglich; dieser kann entweder eine Grafik haben, oder auch unsichtbar sein (für's Verwalten von Schatztruhen z.B.).
• Öffnet man einen Container, wird ein dynamisches floorInventory erstellt, dessen Größe abhängig von der Menge an Items ist, welche durch den Container "gemanaged" werden.
• Wenn noch höchstens ein Item auf dem Feld verbleibt, wird der Container gelöscht, und das verbliebene Item bekommt seine Interaktions-Parameter zurück.
diceman hat geschrieben:• Auf den ersten unvermeidlichen Item-Dupe-Bug warten, und unschädlich machen
Ja, und auch dieser Fall ist eingetreten ... 8)
Zu einem Zeitpunkt wurden nach einen Klick aufs Inventory diverse Pointer auf Items umgeleitet, die irgendwo im Dungeon verstreut lagen, und man so über den Container aufheben konnte, ohne das ensprechende targetActors entfernt wurden.
Die Ursache habe ich zum Glück recht schnell ausfindig machen und beseitigen können.

Bild

Re: Roguelike-Diary (WIP ...)

Verfasst: 26.05.2018 12:42
von Mijikai
Das sieht schon sehr professionell aus :allright:
Bitte halte uns weiter auf dem Laufenden
die Beiträge wirken motivierend :)

Re: Roguelike-Diary (WIP ...)

Verfasst: 28.05.2018 09:55
von RSBasic
Mijikai hat geschrieben:die Beiträge wirken motivierend :)
+1, das stimmt. Ich hätte langsam auch wieder Bock auf LiS2.

Re: Roguelike-Diary (WIP ...)

Verfasst: 03.06.2018 21:01
von diceman
******** VIDEO ********
Roguelike Diary 20: The Road to MVP


Ich hatte diese Woche einen irren Spaß daran, nach und nach Platzhalter-Grafiken durch ungleich ansehnlichere Sprites zu ersetzen.
Desweiteren habe ich an meinen Draw Logic Routinen geschraubt und habe so (bei den Säulen) einen kleinen Pseudo-3D-Effekt gezaubert (jedes primäre Sprite kann jetzt childSprites "spawnen", welche mit einem Alpha-Kanal relativ zum parentActor positioniert werden und keinerlei Kollision oder Interaktion besitzen).
Ich habe vor einigen Wochen mit dem Gedanken gespielt, mich an einer isometrischen Engine zu versuchen, bin davon aber wieder abgekommen, einfach weil es die Darstellung der Welt um ein vielfaches verkomplizieren würde, und so viel Spaß ich auch Designen von Sprites habe, diese Extra-Zeit stecke ich lieber in Gameplay, Content und Features (was auch eher eine Stärke von mir ist); ein paar Pixelart-Techniken habe ich mir selbst beigebracht, ich bin kein großer Künstler, aber für meine Zwecke reicht es.

Bild