Hi, nerds.
I've been spamming qmapper's github's issues with tickets about what and how to do. This has decreased the need to document stuff on this blog. At least on the small stuff, where a title and a few sentences are enough for me to deduce what I'm supposed to do. Tickets like #27 are my favourite, the need is obvious and well communicated by the title and the labels. and it's almost already implemented in the ticket description. Then there are tickets like #28 where there are obvious shortcomings in the app, but the fix requires some explorative programming and implementing the first thing that seems to make sense. And then there is #38, where, again, the shortcomings are obvious and I have a couple of ideas about how to fix those, but I have no clue if any of those do make sense. Thus, before I shall begin explorative programming to get me those clues, let's do some explorative writing first
A little background: the app keeps as much of its state as possible inside an immutable hashmap. This hashmap shall be called the document, as its contents are what is prin1'd to disk when saving the document editor is editing :D Language you'd modify document with is common lisp, on top of which I have written a small clojure-tastic data structure manipulation library (assoc(-in)?, update(-in)? and dissoc(in)). Fset, the library that implements the immutable data structures I'm using, provides (sadly non-polymorphic versions of) map, filter and reduce. The only part which is supposed to be mutable is the set-doc! - function, which setfs its single parameter as the new document and runs document-hooks (which in turn try to keep the moronic mutable c++ side of qmapper in sync).
Writing editor routines is simple enough, most of the code does functional things on the old value of the document and set-doc!s it. Here's an example from the function qmapper's editor runs when user presses Ctrl+V:
(add-key-lambda "C-V" (lambda () (if-let (selection (root-selected-coordinates *document*)) (let* ((left (fset:lookup selection 0)) (top (fset:lookup selection 1)) (tiles *clipboard*) (real-tiles (lookup tiles "tiles")) (width (lookup tiles "width")) (height (lookup tiles "height"))) (set-doc (reduce (lambda (doc coord-pair) (let* ((x (+ left (car coord-pair))) (y (+ top (cadr coord-pair)))) (if-let (tile (-> (remove-if-not (lambda (tile) (let ((relative-coords (lookup tile "relative-coordinate"))) (equalp relative-coords (list (- x left) (- y top))))) real-tiles) car (lookup "tile"))) (progn (format t "in C-V tile is ~a ~%" tile) (set-tile-at-chosen-map doc x y tile)) (progn (format t "in C-V tile is ~a :(~%" tile) (format t "real-tiles are ~a~%" real-tiles) doc)))) (loop for x in (mapcar #'dec (range width)) append (loop for y in (mapcar #'dec (range height)) collect (list x y))) :initial-value *document*)) (format t "resulting doc calculated~%")) (format t "There's no selection!~%"))))
It generates x,y pairs for all the valid indexes to the real-tiles - 2d list. For each pair, it finds a relevant tile from the real-tiles (which is from *clipboard*). Then this tile is put to the chosen map at selection + x,y. set-tile-at-chosen-map returns the new document, and in case the tile application is trying to insert isn't found, it must still return something that walks and quacks like a document, namely, the old value. When all the x,y pairs are gone through, reduce returns the final value to the set-doc and everyone's happy.
This, I think, is a nice way to script editor routines. API makes sense, and is somewhat verbose for a lisp, but not painstakingly much. Let's try doing the same in the engine side!
The game scripting api is a bit less thought out. Back in the day I decided event-based patterns for reacting to IO are stupid and I want to be able to poll kbd and mouse in arbitrary places in the scripts, and wrote an infinite-loop-in-new-thread - routine and a function that returns the keyboard state from a hashmap of booleans that's updated by Qt. Back when the dinosaurs used to toam the earth and I had libguile as the lisp in my lisp-game-engine I used to have a function for polling mouse coordinates too, but it didn't survive #25.
I'm not confident on the no-events-only-polling decision anymore. I kind of like the add-key-lambda - thing, which binds a lambda to an emacs-style keychord. I might write similar system to work in the engine side. But until that distant moment in the future arrives, we're going to poll keyboard in a loop. Here's an example code which should result in a sprite being moved according to arrow keys. It doesn't really work at the moment, because of #38.
; -*- mode: lisp; -*- (defpackage :the-game.main (:use :common-lisp :cl-arrows :qmapper.map :qmapper.root :qmapper.std)) (in-package :the-game.main) (defvar *obj-id* (fset:first (map-sprites (get-prop (root-maps *document*) (root-chosenmap *document*))))) (defun inc-a-lot (n) (+ 40 n)) (defun dec-a-lot (n) (- n 40)) (setf qmapper.std::*loops-running* t) (defparameter *result* (qloop (lambda () (format t "Looping~%") (cond ((key-down? "DOWN") (set-doc (update-prop-in *document* (list "SPRITES" *obj-id* "Y") #'inc-a-lot))) ((key-down? "UP") (set-doc (update-prop-in *document* (list "SPRITES" *obj-id* "Y") #'dec-a-lot))) ((key-down? "RIGHT") (set-doc (update-prop-in *document* (list "SPRITES" *obj-id* "X") #'inc-a-lot))) ((key-down? "LEFT") (set-doc (update-prop-in *document* (list "SPRITES" *obj-id* "X") #'dec-a-lot)))))))
*obj-id* is the first of currently-chosen-map's sprite ids. The following two functions do what it says on the tin. qloop is a bit stupid routine in that a loop can't be easily stopped, and the only way to stop one of them is to kill all of them by setfing *loops-running* to nil. Then we got to the actual loop.
key-down?'s domain is defined here.
When key-down? has granted the script passage to the correct branch, there we'll declare the path of the property we're updating and a function that does the actual job, returning the updated document to save-doc, which commits the value to memory.
It's still not too verbose, but I'd love if the act of moving a sprite wasn't this complex. Ideally it should just be a (move object-id length), with object's angle implicit behind the id, or (move object-id angle length) at most. set-doc should be automatically called... somewhere, preferably as far away from the function that transforms the actual document as possible.
There are couple of ways I see this happening. We could borrow the basic principle (ie. not the api, please $deity not the api) from redux, or considering this a lisp image we're running inside, maybe re-frame. My first idea was to have a message queue where the scripter pushed "events" like the following:
#{| ("state-change-fn" #'obj-mover)
("doc-path" (list "SPRITES" *obj-id*))
("rest-params" (list 10)) |}
Then the actual obj-mover would get whatever is at doc-path as the inital parameter and contents of the rest-params field mapped to the other parameters. It is expected to return the first parameter with transformations it promises to transform. These events are trivial to handle in a dispatcher that could look like this:
;; expect this to be run in another thread and a c-like while loop macro to just exist (defun dispatcher () (while true (if-let (event (pop *message-queue*)) (let ((fn (get-prop event "state-change-fn")) (doc-path (get-prop event "doc-path")) (rest-params (get-prop event "rest-params"))) (set-doc (update-prop-in *document* doc-path (lambda (obj) (apply fn obj rest-params))))))))
Another idea would be to carbon-copy the way redux works. There would be actions, blobs like the previous hashmaps that have a string id identifying what they're supposed to do, with the rest of the params that are needed. Afterwards the user would hand-craft reducers that don't usually do anything more complicated than the previous message-queue-poller. They take the then-current-doc, action, and return a doc modified as the action expects. A system inside the framework then does a (set-doc (reduce #'master-ultimate-reducer actions :initial-value *document*)). Pros of this approach:
- time travelling is trivial if we don't prune applied actions ever
- there... might? be some other pros I can't come up with now
Cons are a lot easier to list:
- verbose as hell
- complex as hell
- to do a thing, you have to hack at least on two places (introduce an action here, write the actual transformation function there, wire those together in a reducer)
- I dunno I don't really like redux
So, let's not clone the reducers and actions - pattern redux uses. I like the message queue approach better. I'd like to try to pepper it with a re-frame like twist though. What if the state-change-fn's didn't return raw transformed documents, but instead hashmaps with new docs under a key like "document" and declarative metadata about what the engine is supposed to do next. Demo of this is visible in this in blog's source code
re-frame's reg-event-fx macro introduces these special functions that are made for transforming the document (or in re-frame's lingo: the (app-)?db, which is shall continue to call the document even though murja's source calls it the db). There are uses of reg-event-db macro too, but let's avoid those as irrelevant. New document is returned under the :db key, rest is control data. New effects, as the framework calls them, are trivial to introduce in way that you can wrap almost anything under this api. The wrapper gets the value returned under the control-data-key as parameter, let's the effect developer call... well, anything, and one they're done, they can (dispatch) the control back to re-frame's event-effect-system.
murja's http effects are implemented this way
The pros of this system in a web-project's frontend implemented in a js-alike lisp are obvious: callback hell is abstracted away and... it looks nice. In re-frame's case it makes state transformations more explicit (than is probably neccessary, but I don't care).
How would this kind of a system make programming a game inside a c++/common lisp frankenstein's hybrid make a developer's life easier?
I don't know. It will be a nicer-feeling way to develop game scripts, I sincerely hope, and there might be some value in making the state transformations explicit? In fact, if we have marked all the state transitions inside the macro that defines them, we might do be able to do interesting stuff inside the editor. Maybe Age of Empires 2 - style triggers with arbitrarily defined conditions and effects.
Yeah, if I ever need to sell this design to anybody and I need a better reason for this design than "me likes", that'll be it
Summary
Updating a game's state (a document, if you will) is hard. Let's write a re-frame-like effect-wrapper with which the developer introduces lambdas that get the old state as the single parameter, and returns both the new state and metadata that tells the framework what effects to fire next. Then we'll write a message-queue and a dispatcher similar to the previously mentioned one (this blog platform doesn't really support linking inside a post...), with the extra handling for the effect metadata. Then we'll document this system either in doc-strings or here or github or somewhere
UPDATE
I implemented this. I documented the usage in github