神刀安全网

Making an RPG in Clojure (2010)

What do you get when you combine old-school Final Fantasy-style RPGs with Clojure? Fun times for all. Well, for me at least.

I’m working on a sort of RPG engine in Clojure so I can make my own RPG. Click the thumbnail for a very preliminary video demo (6.5 MB) showing the engine in action.

Making an RPG in Clojure (2010)

All I do in the video is walk around, and eventually start adding random NPCs to the map to test collision detection. Not all that exciting, but I’m proud nonetheless.

To forestall questions, yes I’ll eventually post the source code, but no, not yet. It barely works. Just a proof of concept so far.

Right now I can walk around a map while NPCs also randomly walk around the map, not much more. So there isn’t much to talk about. But not bad for 4 days and 600 lines of code (one tenth of which is ASCII art… more on that later). Keep in mind that I have no idea what I’m doing.

Collision detection works so people don’t walk through walls or each other, and after endless tweaking I got all the animations to be very smooth, even when I add a few dozen NPCs to the map (as I do in the video). The video is a bit jerky but it looks better in person. All of the admittedly poor artwork is also created by myself, thanks to the GIMP and some hastily-read tutorials on pixel art .

It all runs on plain old Swing in Clojure. Here’s some of what went right and what went wrong so far.

Background

I’ve played a lot of RPGs, but I’ve never programmed a game more complex than Pong. I knew what double-buffering is, and that’s as far as my knowledge of game programming went when I started.

My first idea was to use a plain old Swing JFrame. I’d make a bunch of sprites saved as PNG files, read them all in and draw them on the JFrame, as many times per second as I could manage. Then there’s some global state to keep track of where everything is. Simple enough.

(PS I have no idea what I’m doing.)

Failures

My first version used Clojure agents (i.e. threads) for everything. The game logic was a thread, the renderer ran in a thread, every NPC was its own thread. The world itself was a single ref that all of these agents banged on. So the NPCs would tell the ref "I want to move down one square", another thread might say "Brian just pushed ‘left’ on the keyboard, so start scrolling the map". The world-ref would kindly oblige while preventing two NPCs from standing on each other or letting the PC walk through the walls.

Clojure is awesome in letting you do this in a completely safe and coordinated way. Everything worked well. But even for the crappy 2D graphics I’m using, all those threads caused way too much lag. I could get a good framerate if everyone was standing still, but if I was walking while the NPCs were walking, I’d get lots of lag. It was even worse on a slower computer.

My best guess is that the reason for the lag was the constant restarting of canceled transactions due to multiple threads trying to edit the world ref 50+ times per second. My sucky 2-core CPU couldn’t keep up. It isn’t surprising that this failed, in hindsight.

OpenGL?

Next, I decided to try out OpenGL. There are multiple options for OpenGL in Java. One is JOGL and another is lwjgl .

I program in Linux, and my video card is ancient. I can barely get OpenGL to work in the best of times. Installing JOGL was a slight chore (Gentoo doesn’t even include it in its repo). JOGL is not just a JAR you throw onto CLASSPATH, you need some native extensions, obviously. I got it running somehow, but I wouldn’t want to explain to someone else how I did it.

I did get jwjgl to work too, eventually, which was nice. There is a really nice Java 2D game framework called Slick which uses lwjgl. Some good games were created using this, for example Stickvania , which recently hit Reddit recently. I got Slick up and running in short order.

Unfortunately Slick doesn’t play nicely with a Clojure REPL. I could build and start a game, but once the game is stopped, it never runs properly again without restarting the REPL. Slick caches images to try to be speedy, and it seems like the cache is either corrupted or destroyed when you close down your game, for one thing. This is not conducive to Clojure REPL-style development, and I didn’t want to spend a lot of time fixing it.

Another issue is that I’d really like this game to be cross-platform and available to non-hackers, and the thought of trying to tell the average gamer how to install JOGL or lwjgl was daunting, given the bullcrap I had to go through. Not sure if I can just throw JOGL into a JAR and distribute it, maybe I can, but I didn’t want to bother reading about it. Swing on the other hand runs everywhere with no effort.

My main problem is that I know even less about OpenGL programming than I do about Swing, and don’t have a month to learn. Back to the drawing board.

Success

It turns out I don’t need OpenGL anyways. All I need is program more intelligently. (Did I mention I have no idea what I’m doing?) Instead of dozens of threads, my current (working) version has one thread. It updates the game logic, then renders the game, then waits 10 milliseconds or so, then repeats this (forever).

With the single-threaded version, the logic is actually more complex than the multi-threaded version. Agents and refs were really nice and braindead-easy to work with. But such is life.

Once I learned about keeping track of things in realtime by counting milliseconds instead of counting frames or using timeouts to do logic/render updates, things worked better. (Did I mention I have no idea what I’m doing?) The game loop looks like this now:

(defn game-loop [#^Canvas canvas]   (loop [last-time (get-time)]     (let [curr-time (get-time)           delta (- curr-time last-time)]       (do-logic delta)       (do-render canvas)       (when @RUNNING         (Thread/sleep 10)         (recur curr-time))))) 

The code to actually start the game, to give you an idea:

(defn start-world [world]   (let [#^JFrame frame (doto (JFrame. "Game")                          (.addWindowListener (proxy [WindowAdapter] []                                                (windowClosing [e] (stop)))))         #^JPanel panel (doto (.getContentPane frame)                          (.setPreferredSize (Dimension. REAL-WIDTH REAL-HEIGHT))                          (.setLayout nil))         #^Canvas canvas (Canvas.)]     (doto canvas       (.setBounds 0 0 REAL-WIDTH REAL-HEIGHT)       (.setIgnoreRepaint true)       (.addKeyListener (proxy [KeyAdapter] []                          (keyPressed [e] (handle-keypress e))))       (.addMouseListener (proxy [MouseAdapter] []                            (mouseClicked [e] (handle-mouse e))))       )     (.add panel canvas)     (doto frame       (.pack)       (.setResizable false)       (.setVisible true))     (.createBufferStrategy canvas 2)     (dosync (ref-set RUNNING true)             (ref-set PAINTER (agent canvas))             (ref-set WORLD world))     (send-off @PAINTER game-loop))) 

That’s about it for the Swing side of things, aside from scribbling on the Canvas in the render function.

The agent I wrap around the Canvas controls the thread that runs the game loop. The agent helpfully keeps track of any exceptions that happen during the loop, and I can view those exceptions via agent-error , which is handy for debugging.

Note how little code it is to set up a keyboard event handler, thanks to proxy :

    (doto canvas       ...       (.addKeyListener (proxy [KeyAdapter] []                          (keyPressed [e] (handle-keypress e)))) 

A couple lines of Clojure for what would be a lot of senseless boilerplate in Java. Notice how you the keyboard handler calls a normal Clojure function handle-keypress . Clojure / Java interop really is seamless.

Maps

My maps are made using ASCII art. (Did I mention I have no id-… never mind.) Here’s the code for the test map, for example. Map here is a deftype (available in bleeding-edge Clojure), which takes a map of tiles, a "pad" tile, the map, and then a mask showing walls / tiles where the player shouldn’t be allowed to walk. My Map type acts like a Clojure hash-map most of the time, but it also lets me name the fields that all maps should share, and has a proper "type", among other things.

(deftype Map [tileset pad tiles walls]   clojure.lang.IPersistentMap)  (def test-map (cache-map                (Map {/  (tile "ground")                      // (tile "ground-shadow-botright")                      /< (tile "ground-shadow-left")                      /d (tile "dirt")                      /| (tile "wall_vertical")                      /- (tile "wall_horizontal")                      /1 (tile "wall_topleft")                      /2 (tile "wall_topright")                      /3 (tile "wall_bottomleft")                      /4 (tile "wall_bottomright")                      /u (tile "below-wall")                      /v (tile "below-wall-shadow")                      /w (tile "wood-floor")                      /c (tile "cobble")                      /b (tile "bush")}                      (tile "ground")                      ["                 1----2                            "                      "                 |vuuu|                            "                      "1----------------4<bbb3--------------------------2 "                      "|vuuuuuuuuuuuuuuuu/   uuuuuuuuuuuuuuuuuuuuuuuuuuu|<"                      "|<1------------2               cccccccc d        |<"                      "|<|vuuuuuuuuuuu|<       d      c      c          |<"                      "|<|<           |< cccccccccccccc    d ccccccc d  |<"                      "|<3------ -----4< c                         ccccc|<"                      "|<uuuuuuu/uuuuuu/ c  1-------2    1------------2 |<"                      "|<1---2 bcb       c  |vuuuuuu|<   |vuuuuuuuuuuu|<|<"                      "|<|vuu|< c  d     c  |< 1-- -4<   |<           |<|<"                      "|<3- -4<bcb       c  |< |vucuu/   3------ -----4<|<"                      "|<uu/uu/ c        c  3--4< c      uuuuuuu/uuuuuu/|<"                      "|<  c  dbcb       c  uuuu/ cccc         bcb      |<"                      "|<  cccccccccccccccccccccccc  cccccccc   c   d   |<"                      "|<  d        d                     d ccccc       |<"                      "|<                    1--------------------------4<"                      "3---------------------4uuuuuuuuuuuuuuuuuuuuuuuuuuu/"                      "uuuuuuuuuuuuuuuuuuuuuuu/                           "]                      ["                 xxxxxx                            "                      "                 x    x                            "                      "xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx "                      "x                                                x "                      "x xxxxxxxxxxxxxx                                 x "                      "x x            x                                 x "                      "x x            x                                 x "                      "x xxxxxxx xxxxxx                                 x "                      "x                    xxxxxxxxx    xxxxxxxxxxxxxx x "                      "x xxxxx x x          x       x    x            x x "                      "x x   x              x  xxx xx    x            x x "                      "x xx xx x x          x  x         xxxxxxx xxxxxx x "                      "x                    xxxx                        x "                      "x       x x                             x x      x "                      "x                                                x "                      "x                                                x "                      "x                     xxxxxxxxxxxxxxxxxxxxxxxxxxxx "                      "xxxxxxxxxxxxxxxxxxxxxxx                            "]                     ))) 

This lets me make all of my tiles be simple PNG files with sane names. I could dork around with sprite sheets, but why bother? Emacs or Vim column-editing and overwrite modes make it easy enough to make a map this way. Swing thankfully handles transparency for me if I use PNGs, so it’s a no-brainer to make multiple map layers later, which I’ll need later. The code for reading in PNGs is brain-dead simple thanks to Java’s ImageIO:

(defn tile [name]   (ImageIO/read (File. (str "img/" name ".png")))) 

The cache-map function (below) iterates over the ASCII art (via seq s on the Strings), using each character as a key into the map of real images, and builds a BufferedImage out of it. This is cached, since the renderer needs to re-draw the background every frame. In this function I’m also drawing a "padding" layer to sit under the background, so that I see endless fields of grass when I walk close to the edge of the map, instead of garbled, smeared-out graphical artifacts.

(defn cache-map [amap]   (let [height (count (:tiles amap))         width (count (first (:tiles amap)))          #^BufferedImage img (BufferedImage. (tile-to-real width) (tile-to-real height) BufferedImage/TYPE_INT_ARGB)         #^Graphics2D g (.createGraphics img)          #^BufferedImage pad (BufferedImage. (tile-to-real width) (tile-to-real height) BufferedImage/TYPE_INT_ARGB)         #^Graphics2D padg (.createGraphics pad)]     (doseq [[y row] (cseq/indexed (:tiles amap))             [x tile] (cseq/indexed row)]       (.drawImage padg (:pad amap) (tile-to-real x) (tile-to-real y) REAL-TILE-SIZE REAL-TILE-SIZE nil)       (if-let [#^Image img ((:tileset amap) tile)]         (.drawImage g img (tile-to-real x) (tile-to-real y) REAL-TILE-SIZE REAL-TILE-SIZE nil)         (throw (Exception. (str "Missing tile " tile ".")))))     (assoc amap       :map-image img       :pad-image pad       :max-map-x width       :max-map-y height))) 

This code is nasty, mostly due to converting between tile-based coordinates and pixel-based coordinates. This code needs to be cleaned up a bit. But that’s about the most complex code you’ll find in my program so far.

Who needs mutable state?

Clojure is a mostly functional language, in the sense of strongly discouraging unnecessary use of mutable state, and this program is no different. I’m sometimes amazed how far I can get before I need mutable state at all. The vast majority of my functions take a world value (a plain old hash-map) as an argument, and return a new world value after making changes to it. The current state of the world is whatever value is currently in the global WORLD ref.

The render loop grabs a snapshot of the world from the ref on each iteration, and then draws it. Thanks to Clojure refs, the snapshot of the world is guaranteed to be consistent (e.g. no NPC objects in the middle of mutating themselves) and persistent (the world value sticks around as long as the renderer needs it, even if the WORLD ref is changing in another thread). Once it’s been drawn, the renderer throws the world snapshot away and it’s garbage-collected later.

This all happens around 50-100 times per second in my game, and there’s no noticeable lag. So that’s a good thing.

Conclusion

That’s about it. I’ll post the full source code once it doesn’t suck as much.

In my opinion, exploring a new area of study is some of the best fun a person can have. It’s a flood of information and a surprise every 10 seconds.

Making a game has been like that. There’s a huge wealth of knowledge about this kind of programming that I never knew existed. Everything I read on this topic is new to me and fascinating. The saddest(?) part of this whole thing is that I’m going to have more fun programming this game than I usually have playing games I buy in the store.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Making an RPG in Clojure (2010)

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址