Let me start by announcing stuff: I have moved my coding-projects and myself to a beautiful maritime city of Kotka (in August :P Better to announce late than never, 'eh?), and plan to graduate as a software engineer in a near future. To fight the oppressive blog-silence I shall publish a few words about MERPG
MERPG-context
A few days ago I released a second version of the Clojure-based MEMAPPER and had an idea to return to work with the engine (which, by the way, resides in the same google code - repository). Yeah, as if it would be that easy: I told a few months ago in this very blog that I had written an experimental engine on top of the Quil - 2D-graphics library. In tuesday I opened that experimentation again, only to find some ugly experimentation, most of which was removed immediately. Then I made sure I still knew how the library worked... and now I hope I wouldn't have :)
I don't have the willpower to deconstruct why I don't like Quil anymore. It just didn't flex to a mental model I had, and it has had some troubles with the REPL-based development. For example, the load-image - function returns nil always when called in REPL (something to do with single-threadedness - policy of most of the GUI toolkits?), and using it had some other nil-troubles. But then, I promised to not deconstruct this.
What I have willpower to do is design a graphics API I can live with, and wrap either Quil or Swing inside it.
How to make 2D-drawings?
Are there any requirements for this API? Oh yes. With Quil drawing to not-screen surfaces required hacking with the Processing - classes underneath (I think). The Quil API didn't provide (last summer, when I last looked) any way to achieve this. So, first requirement: we need to have two versions of all the functions. Other version takes the target surface as a parameter, other uses the default surface. Or... no, this seems rather nice space to apply dynamics scope. If you want to draw stuff into the default surface, you just call (draw-stuff x y & params), but if you want to draw into other surfaces, for example into an image, you bind *default-surface* into that surface and call the previous function.
(binding [*default-surface* in-memory-surface] (draw-stuff 300 400 32 11 12))
Binding - forms aren't beautiful in the not-library code. Luckily hiding them is easy. With a little macro-magic we can make the previous code look like this:
(draw-to-surface in-memory-surface (draw-stuff 300 400 32 11 12))
What are the stuff we should be able to draw with this library? Strings, primitives and images - for now. All of these could be abstracted behind a single Draw-protocol, so that one could write a rendering-procedure for an immutable map1
(let [tileset (image "/Users/Feuer/Dropbox/memapper/tileset.png") W 10 H 10 tileW 50 map-data (merpg.State/get-current-map) map-surface (image (* W tileW) (* H tileW))] (draw-to-surface map-surface (dotimes [layer (layer-count map-data)] (dotimes [x (map-width layer)] ;;map-x - fns return count of tiles, not of pixels (dotimes [y (map-height layer)] (let [tile (tile-at map-data layer x y)] (draw (subimage tileset (:x tile) (:y tile) tileW tileW) (* tileW x) (* tileW y))))))))
Or... a HUD-screen? 2
(let [hud-data {:health 100 :max-health 130 :character-name "Varsieizan" :character-face-img "I'm an image, and I don't break if this string really is changed to an image \o/"} screen-width (width)] ;Width checks the width of the *default-surface*, but has overloads that take the surface as parameter for cases when draw-to-surface-macro would look stupid (draw (str (:health hud-data) "/" (:max-health hud-data)) 10 0) ;Draw should provide dimensionless overloads for when they are easily deduced (let [to-render (str "Playing: " (:character-name hud-data)) text-width (width to-render) text-x (-> (/ screen-width 2) (- (/ text-width 2)))] (draw to-render text-x 0)) (draw (:character-face-img hud-data) (- screen-width (width (:character-face-img hud-data))) 0))
The beauty of the Clojure's dispatching system is this: draw-function can be written with Java's dispatching system (in other words, type of the second parameter (which represents the same entity as java's this-pointer) determines what function will be called), and with Clojure's extensions (namely extend-type) one can trick the type system to think that String - class has the draw-method. And when I've written the draw-method for for example the BufferedImage - class, and I've changed the :character-face-img - field to an actual image, the previous code still works.
What else?
In two previous code snippets I presented the (width) and (height) - functions, which are supposed to return (fun *default-surface*)3 if called without params, and the dimension of their parameter if such is provided. Then there is the image-function, which loads images if provided a string, and creates them if provided with 2 number params. Of course under all these small abstractions lies an important one too: the drawing queue which, I think, should be created per-frame. Sadly this'll mean either a one-frame policy or some peculiar way to handle in what frame a media should reside. Or maybe raw images reside outside the queues, and I'll write a distinction between these images (which have to be drawn separately in the game loop) and Objects (who live in the frame's drawing queue, have a location and support metadata).
So, functions to implement:
- (image [path])
- (image [w h])
- (object [path/img x y angle])
- (set-/get-angle [object])
- (get-x/-y [object])
- (set-position [object x y])
- (move [object length])
- (visible? [obj])
- (set-visible [obj bool])
And what else? I'd say we need more graphics primitives than simple strings. We need also some kind of color-management. Maybe same sort of a per-frame *drawing-color*, and a with-color - macro to edit it? The primitives could be implemented as records, so that one could do this:
(draw-to-surface img (with-color "#FF0000" ;; With color delegates these values to... something in the java.awt - ns (draw (Circle. :r 40) 40 40)) (with-color (java.awt.Color/BLUE) (draw "Hello-world" 40 50)) (with-color {:r 244 :g 50 :b 177 :a 44} (draw (Rect. :w 60 :h 10) 100 20)))
Footnotes
- This implementation is somewhat broken with the real code-base...
- In real life these would be functions, but the types of these values in these demos are somewhat important, and they wouldn't be visible in a defn-form.
- fun ∈ {width, height}