Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@GWRon
Copy link
Member

@GWRon GWRon commented Nov 1, 2022

No description provided.

@GWRon
Copy link
Member Author

GWRon commented Nov 1, 2022

Ich muss mal schauen, wie wir das umsetzen.
"Klassisch" waere, fuer jeden Befehl ein eigenes Type TMovieDealerDoBuyLicenceAICommand extend TAICommand zu machen.
Auch moeglich waere ein

Type TMovieDealerCommand extends TCommand
  Field action:EMovieDealerCommand

  Method CustomRun()
    Select action
      case EMovieDealerCommand.buyLicence
         ...
    End Select
  End Method
End Type

...
Enum EMovieDealerCommand
  buyLicence
  sellLicence
End Enum

Dann waere das alles nach "Raeumen" und "base" (ueberall erreichbar) gruppiert. Ueber die "enums" wuerde man auch gut erkennen, welche Befehle "erlaubt" sind.

Der erste Commit enthaelt nun erstmal - als Beispiel - eine Umsetzung fuer "SendToChat"

@nittka
Copy link
Contributor

nittka commented Nov 1, 2022

Es müssen ja wahrscheinlich nicht sämtliche KI-Befehle umgeschrieben werden. Ich finde es schon sinnvoll, fachlich ähnliche Kommandos (Parameter sind gleich: eine Werbung, eine Lizenz,...) als gemeinsamen Typ zu behandeln und über action zu steuern, was damit gemacht werden soll.

Ich wäre aber dafür, das Thema erst nach dem nächsten Release ernsthaft anzugehen, denn das wird wohl eine größere Baustelle.

@GWRon
Copy link
Member Author

GWRon commented Nov 1, 2022

Du kannst gerne die richtigen Befehle raussuchen - ich denke vorrangig die "Setter" (doXYZ usw) sollten davon betroffen sein.

der Code ist insofern vorbereitet, als das man auch bequem Dinge wie "onSentToChat" einbinden kann (also dann Aufruf nach "Lua", wenn das Ergebnis eines Funktionsaufrufs vorliegt).

doBuyProgrammeLicence -> onBuyProgrammeLicence

Das waere von Interesse, wenn die KI eben "nicht" auf das Ende eines Funktionsaufrufes warten soll - im Sinne von "dauert eventuell zu lange" oder "Daten werden nur aller X Sekunden berechnet".

@GWRon
Copy link
Member Author

GWRon commented Nov 3, 2022

TODO: jede KI sollte eine eigene Queue bekommen, damit bei Bankrott und Co die Queue spezifisch geleert werden kann.

@GWRon
Copy link
Member Author

GWRon commented Nov 3, 2022

Ich habe ueberlegt, hier gleich die Chance zu nutzen ... und die "TLuaFunctions" vielleicht ein wenig ... aufzuraeumen.

Mein groesseres Ziel waere es ja, dass es "Aktionen" gibt, die von Spieler und auch KI ausgefuehrt werden koennen. Wenn wir also irgendwo die GUI benutzen, ist der Befehl am "Ende", eigentlich der gleiche, wie wenn die KI etwas macht.

Kurz, ich wuerde gern aus dem "TVT.of_doBuyProgrammeLicence(id)" etwas machen, was auch der menschliche Spieler ausfuehrt.

Dazu waere es aber wohl praktisch, wenn wir die Funktionen umlagern. In etwa ein TPlayerActions. Hier koennte man auch etwas "umraeumen und dann sowas daraus machen:

Type TPlayerActions
  Field office:TOfficePlayerActions
  Field movieagency:TMovieAgencyPlayerActions
  Field common:TCommonPlayerActions
...
End Type

Type TPlayerActionsBase
  Field playerID:Int
  ...
End Type

'
Type TOfficePlayerActions extends TPlayerActionsBase
   Method BuyProgrammeLicence:Int(licenceID:Int=-1)
...

In Lua wuerde das dann vielleicht so aufgerufen werden koennen:

TVT.actions.office.BuyProgrammeLicence(licID)

ohne die Unterteilung waere es:

TVT.actions.of_BuyProgrammeLicence(licID)

Natuerlich muesste man sich noch Gedanken machen, wie man das so aufgesplittet bekommt, dass die "actions"-Typen keine grossen Abhaengigkeiten mitschleppen (so dass sie sich bei den jeweiligen GUI-"on click" usw bequem verquicken lassen, ohne dass man alle moeglichen "actions" (und dazu zu kennende "Types") an der Backe hat.

Ein "einfacher" Ansatz waere ... alle "actions" bekommen nur noch Primitive (oder "object") als Parameter/Result. Dann kann ein Interface genutzt werden (oder der von uns bereits angewandte Ansatz mit "TActionsBase" was die Methoden beschreibt ("API"), und die waehrend der Runtime druebergesetzte "TActions" die dann die eigentliche Funktionalitaet umsetzt (also "object" in Typen castet und dann korrekt auf deren Member und Methoden zugreift)
Ein weiterer Vorteil hier waere, dass auch die daraus resultierenden "TCommand" diese Primitive (bspweise "LicenceID:Int") nutzen koennen und sich das prima serialisieren laesst. (selbst die "TCommand" koennten dann statt einem eigens zu schreibendem Callback einen auf "CommandID" basierenden/zugeordneten vorher festgelegten Callback ausfuehren.

So aehnlich machen wir es glaube schon mit den GameModifiers usw - bei denen die run/undo nur durch die individuellen Parameter gesteuert werden.

@nittka
Copy link
Contributor

nittka commented Nov 4, 2022

Ich finde den Ansatz für den Umbau grundsätzlich gut; das Aufräumen sowieso (Eliminieren nicht verwendeter Methoden). Die Unterteilung TVT.actions.office.X befürworte ich (in der Hoffnung, dass die Indirektionszeiten unproblematisch sind, denn insb. bei der Programmplanung wird es in jedem Fall extrem viele Zugriffe geben). Im Lua-Code kann man dann pro Task nämlich noch genauer sehen, ob "unerlaubte" Methoden aufgerufen werden, alles Suchen nach "TVT.actions" müssen dann entweder mit ".raum" oder ".interface/.common" weitergehen (nun ja, außer beim ScriptTask, der ja in mehreren Räume stattfindet). Außerdem kann die Raumaktion dann generisch prüfen, ob man sich im richtigen Raum befindet.

@GWRon
Copy link
Member Author

GWRon commented Nov 4, 2022

Ich denke wir wuerden generell eine "Indirektionsschicht" einbauen. Aehnlich der TGameModifier und anderer Klassen.

Der Grundgedanke ist, dass die TCommand gar keine Callbacks direkt zugeordnet bekommen - sondern nur noch die "ID" eines Callbacks. Diese ID muss eine Konstante sein - die sich im Spiel nicht aendert (bzw bei Savegames).
Irgendwo im Spiel werden also "IDs" mit Callbacks belegt (eine "callbackMap") und beim Ausfuehren der Befehle wird der entsprechende Callback rausgesucht und ausgefuehrt.
Eventuell koennte gar der Callback selbst ein "struct" sein, der sich selbst mit Leben befuellt ("getCallback()") und nur beim Persistieren dann zur "nur ID" wird. Spart "Lookup"-Zeit.

Wenn das soweit umgearbeitet wird - und man das auch fuer den menschlichen Spieler nutzt, dann ist das auch gleich ein riesen Grundstein fuer den Mehrspielermodus, da dann ebenfalls nur (synchronisiert - wie bei Lockstep) die "TCommand" umhergereicht werden muessen.

Mein einfachster Ansatz waere der "TMytypeBase und TMytype extends TMytypeBase"-Ansatz. Ersteres definiert die verfuegbaren Methoden (die durch die Primitiven ohne fiese Abhaengigkeiten ist), letzteres ist dann die aktuelle Implementierung, die dann die Kenntnis ueber Programmplaninterna oder aehnliches haben muss.
Das kann man natuerlich prima auf "office", "boss" usw. runterbrechen und jeweils "vor ort" definieren. So wie das mit den Modifiers/Effects auch geschieht.
Eventuell laesst sich das auch als "Interface" gestalten, keine Ahnung ob das sonderlich Vorteile haette.

@GWRon
Copy link
Member Author

GWRon commented Nov 4, 2022

(in der Hoffnung, dass die Indirektionszeiten unproblematisch sind, denn insb. bei der Programmplanung wird es in jedem Fall extrem viele Zugriffe geben)

Ich habe jetzt mal einen Vergleich "direkter Funktionsaufruf" und "indirekter Funktionsaufruf" gemacht ... wenn der GCC nicht fies optimiert hat, waren das sowieso vernachlaessigbar kleine Zeiten:

'performance:
Local t:Int = Millisecs()
For local i:int = 0 until 100000
	PACB_GoToRoom(1, string(i))
Next
Print "direct took: " + (Millisecs() - t)


t = Millisecs()
For local i:int = 0 until 100000
	playerActions.common.GoToRoom(i)
Next
Print "indirect took: " + (Millisecs() - t)

9 und 14ms.

letzteres geht:

Type TPlayerActions_Common extends TPlayerActionsBase
	Method GoToRoom:Int(roomID:Int)
		If IsActionAllowed(TPlayerActionKeys.COMMON_GOTOTARGET)
		'Befehl anlegen, um den Wunsch "gehe zu Raum" abzuschicken
			Local a:TPlayerAction = New TPlayerAction(TPlayerActionKeys.COMMON_GOTOROOM, playerID, String(roomID))
		'TEST ... in "Echt" auf queue umstellen
			a.Run()
		EndIf
...

Type TPlayerAction
	Method Run:SPlayerActionResult()
		self.status = EPlayerActionStatus.RUNNING

		local callbacks:TPlayerActionCallbacksWrapper = GetActionCallbacks()
...
		If callbacks and callbacks.runCallback
			result = callbacks.runCallback(playerID, payload)
...

Type TPlayerActionCallbacksCollection
	Method GetCallbacks:TPlayerActionCallbacksWrapper(key:Int)
		Return TPlayerActionCallbacksWrapper(actionCallbacks.ValueForKey(key))
...

Sprich ein wenig indirektion und am Ende lookup aus einer intMap (der auch auch 100.000 mal stattfand). Zeiten koennten bei einer groesseren Lookuptabelle ein wenig anwachsen.

Denke aber, die Strafe fuer die Indirektion ist noch im Rahmen.

@nittka
Copy link
Contributor

nittka commented Jan 7, 2023

Ich denke, ein initialer guter Testballon für die Aktionen sind das Gehen zum Ziel und das Verlassen eines Raums. Insb. bei letzterem gab es ja schon Exceptions. Interessant wäre dann die Frage, ob es spürbaren Einfluss auf die Geschwindigkeit hat, da ggf. mehr Update-Zyklen ins Land gehen, bevor eine Aktion abgeschlossen ist.

@nittka
Copy link
Contributor

nittka commented Jan 20, 2023

Würden die Abfragen immer nur in einem Update-Zyklus beantwortet?

  • Zyklus 1: Anfrage KI->Hauptthread
  • Zyklus 2: Antwort Hauptthread->KI

Das würde bedeuten, dass sämtliche Interaktion der KI mit den Hauptdaten wesentlich länger dauern würde. Hohe Vorspulgeschwindigkeiten würden sich nur erreichen lassen, wenn man die Anzahl der Update-Zyklen pro Sekunde auch hochdreht.

Ein vollständiges Umstellen auf solche Queues kann ich mir dann nur vorstellen, wenn es einen separaten Update-Thread (keine GUI) mit einer massiv höheren Zahl von Zyklen gibt.

@GWRon
Copy link
Member Author

GWRon commented Jan 20, 2023 via email

@nittka
Copy link
Contributor

nittka commented Jan 20, 2023

Die Idee für die KI war doch aber, den asynchronen Teil in die AI-API auszulagern. D.h. Lua stellt eine Anfrage an ai.bmx, dort wird ein asynchroner Aufruf abgesetzt und gewartet bis die Antwort an Lua zurückgeschickt wird.

Während eines "Lua"-Ticks wird die KI also geblockt bis die eine Anfrage abgearbeitet wurde. Das wird aber wahrscheinlich nicht die einzige asynchrone Anfrage während des Lua-Ticks sein. Deshalb wäre es sinnvoll, dass der Anfrage-Beantworter mit einer wesentlich höheren Frequenz läuft.

@GWRon
Copy link
Member Author

GWRon commented Jan 20, 2023 via email

@nittka
Copy link
Contributor

nittka commented Jan 20, 2023

Ja, das reicht für normales Spielen und vermutlich auch für kurzzeitiges Vorspulen (Tempo 1800). Beim den KI-Spielen ist bei mir aktuell bei Tempo 3600 Schluss. Bei Tempo 7200 bricht die KI-Performance ein, weil die Wartezeiten am Fahrstuhl verhältnismäßig länger werden (Warten auf einen Update-Zyklus) und auch die teuren Lua-Blitzmax-Anfragen dauern so viel Realzeit, dass wesentlich mehr Spielminuten vergehen. (z.B. Scheduling 50 min statt 25).

(D.h. mindestens für den Entwicklungsmodus muss man das Thema im Hinterkopf behalten.)

@nittka
Copy link
Contributor

nittka commented Jan 20, 2023

Ich muss nochmal korrigieren, ich hatte das falsch in Erinnerug. Die Spielminuten pro Task (insb. Scheduling) gehen bei 7200 zwar schon deutlich in die Höhe, wären meiner Ansicht nach aber noch vertretbar. Bei 14000 bekommt man definitiv kein brauchbares Ergebnis mehr (was die Spielweise der KI angeht).

@GWRon
Copy link
Member Author

GWRon commented Jan 20, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants