Friday 15 November 2013

Planning for Merpg-2D - library

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

  1. This implementation is somewhat broken with the real code-base...
  2. 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.
  3. fun ∈ {width, height}

No comments:

Post a Comment