Tiles³: Basic Example

Posted by gered,

Screenshot

It's been a while since I've released some game code, so today I'm going to start small and release a really basic example application. This is probably not that exciting really, but it showcases a very, very basic entity system implementation built upon libGDX and my own libraries gdx-toolbox (which includes the base entity and event system used in this example app) and gdx-tilemap3d.

You can find the Git repository here.

I've basically copied a subset of the entity system implementation in use in my other game project, stripping out a bunch of extra stuff that isn't totally necessary for a simple example demo (such as the combat system, particles, NPC handling and more).

I'm calling this basic entity system implementation on top of gdx-toolbox and gdx-tilemap3d the "Tiles³ Framework" just because I feel I need some kind of name for it ("my game framework" just doesn't sound that great). Though the use of the word "framework" should not be confused as meaning something totally generic that can be used for a lot of different things with no or minimal changes. That is not the goal with this! Rather I expect that each project using this code will need to customize it quite a bit to suit the particular needs of that application. This is really just a template to get started with.

I'd like to build on this over the next couple weeks and put up another example application that showcases a really simple game. Nothing even remotely fancy, but something that will be more interesting then a walk-around demo.

Following that, I'd like to finally get started on the map editor I wrote about briefly in a post over a year ago and still haven't begun yet. I've written a couple unfinished toy projects using this code over the last year and each time I have to design a new map it's the most tedious thing ever, so such an editor would be extremely helpful to me at least!

Using Java annotations with Clojure and deftype, definterface, etc. is a bit under-documented in my opinion. There's an example from Rich Hickey, but even so, some more examples in the official documentation would be quite nice. Especially so when using these functions combined with your own custom macros, say if you want to generate classes/interfaces at runtime and pass in dynamic values for the annotation's properties.

It just so happens I spent the bulk of the afternoon today when it was gorgeous outside sitting inside at my computer fiddling about with reify and macros and applying Java annotations to the generated anonymous class methods. I spent a fair bit of time on Google first but didn't find anyone who was doing quite the same thing ... there was some stuff on gen-class, but that wasn't really what I needed. Perhaps my Google-fu is weak.

Anyway, lets say that we are working with some random Java library and some method we want to use in it expects to be passed an instance of some class with a calculate method accepting two numbers. Yup, totally.

(definterface ICalculator
  (calculate [a b]))

(def adder
  (reify ICalculator
    (calculate [_ a b]
      (+ a b))))

(.calculate adder 1 2)
=> 3

Nothing special. And it works great too!

But what if said Java library would not run our calculate method unless it was decorated with some Java annotation? And that annotation has a property that needs to be set to different values based on the method of calculation! (Why, you ask? Because! Don't ask silly questions!)

The horror!

@Retention(RUNTIME)
@Target(METHOD)
public @interface AmazingAnnotation {
	String foobar();
}

Now applying it to reify is very similar to the example shown for deftype in the Gist linked above:

(def adder
  (reify ICalculator
    (^{AmazingAnnotation {:foobar "important value"}}
     calculate [_ a b]
      (+ a b))))

We can verify that the annotation exists on the method:

(.getAnnotation
  (->> adder
       (.getClass)
       (.getDeclaredMethods)
       (filter #(= "calculate" (.getName %)))
       (first))
  AmazingAnnotation)
=> #<$Proxy6 @testapp.AmazingAnnotation(foobar=important value)>

Great! But we don't want to hard-code the :foobar value, we need that to be specified at runtime from some other source.

(defn make-adder [foobar]
  (reify ICalculator
    (^{AmazingAnnotation {:foobar foobar}}
      calculate [_ a b]
      (+ a b))))
=> CompilerException java.lang.UnsupportedOperationException: Can't eval locals ... 

Hrm, well... I didn't know that it wasn't possible to do that, but now I do. So I guess we need a macro! And this for me was the fun part, because you have to use something like with-meta, vary-meta, etc. because you want to apply the annotation metadata to the form emitted by the macro, not the forms in the macro itself!

(defmacro make-adder [foobar]
  `(reify ICalculator
     (~(with-meta
         'calculate
         `{AmazingAnnotation {:foobar ~foobar}})
      [_ a# b#]
      (+ a# b#))))

And to verify that it does work:

(.getAnnotation
  (->> (make-adder "hooray!")
       (.getClass)
       (.getDeclaredMethods)
       (filter #(= "calculate" (.getName %)))
       (first))
  AmazingAnnotation)
=> #<$Proxy6 @testapp.AmazingAnnotation(foobar=hooray!)>

In the end, I think what messed me up for the longest time was getting the right combination of quoted/unquoted forms in the macro and wrapping the exact right form in the call to with-meta. Hope this helps someone else out there.

Game Project WIP Update

Posted by gered,

Screenshot 1

Screenshot 2

Since I've been posting about stuff I've been working on recently (oh hey, that was why I started a blog way back in the first place!) I figured I'd write something about this project. I don't have a final title for this game yet, but it's essentially a re-do of the "Monster Defense" game I mentioned briefly in this post, except the direction I'm going with the gameplay is more fleshed out.

So what is this game about? Probably the closest explanation would be that it's a kind of tower defense meets twin-stick shooter game. You directly control a character which you move around with one directional control (ASWD keys / joystick), and aim/shoot with the other direction control (JKIL keys / second joystick). You have a set of stationary crystals on each level that must be defended against from waves of bad guys that enemy crystals will spawn as the wave progresses. Your crystals also double as automated defenses (they will shoot nearby bad guys), while the enemy crystals will also shoot you if you get too close, but if you can destroy these crystals, bad guys will stop spawning from them. You win by either outlasting all of the waves of bad guys, or by destroying all of the enemy crystals. You lose by either getting killed, or losing all of your own crystals to the bad guys.

I've got a bunch of plans for extra things to add in the form of power-ups the player can collect from bad guys, random special buffs you can collect to gain temporary special powers, different unique and varied weapon types and a bunch of different types of enemies and behaviours.

As it stands right now, basic gameplay is all implemented. You can pick a level from a level selection list and start it, the waves will progress, enemies will spawn as they should and seek out the player crystals and try to destroy them or the player if he gets in the way. Levels end either in success or defeat. All of that works just fine. What is missing at the moment is any semblance of balance.

I can easily see at this point that balancing this game will be one of the hardest things to do. Of course by balance, I'm talking about tweaking numbers. Things like the strength and defensive power of the player, the good and bad guy crystals, the bad guys themselves. Various other attributes and behaviours of enemies (how fast / slow they move, what is their attack range / speed, what weapon(s) do they possess ...). The exact composition of the lists of bad guys to spawn in each wave, how to space out the bad guys, how many waves per level, etc. Lots of questions to answer, and lots of play testing will need to be done. It seems like the hard parts of development are behind me at this point in this project when I think of all that kind of stuff.

Under the hood, this game is being developed in Java using libgdx. I also make use of my own libraries, gdx-toolbox and gdx-tilemap3d. I've posted a bit about gdx-tilemap3d before. In the past month I've made some good overall changes to it with respect to asset loading. Now I integrated with libgdx's AssetManager system, something that should have been done from the beginning.

Artwork is pretty much all placeholder stuff at the moment. The level models (floors, walls, pillars, etc.) are from the Low Poly 3D Pixel Dungeon Set by Bitgem. The "8-bit" styled character sprites and most of the particle / item sprites are from the Lo-fi Fantasy Sprite Set by Oryx with the animations taken somewhere out of this big forum topic on the TIGForums. Lava and water textures temporarily borrowed from Minecraft due to not having any sufficient placeholder art for those things handy, but that will be remedied very soon.

At present, I am only targeting Windows/Mac/Linux. This game does run on Android due to libgdx's excellent cross-platform support (probably would even run on iOS with RoboVM since libgdx officially supports it, but I have not tried), but being a twin-stick shooter type of game, I'm pretty firmly convinced that touchscreen controls would make it entirely "un-fun" and I don't see how I could really change the control scheme around for mobile specifically and keep the same fast-action feel for this game that I want for it.

More to come!

Ever created some silly border-line pointless libraries just for fun? Well, I have two more to add to that particular pile.

clj-figlet

This library allows you to load FIGlet fonts (for which there is a large database of them available) and render any string of text. So, you can do stuff like:

(use 'clj-figlet.core)

(println 
  (render-to-string 
    (load-flf "/Users/gered/standard.flf")
    "Hello, world!"))

And get output like:

  _   _          _   _                                           _       _   _ 
 | | | |   ___  | | | |   ___         __      __   ___    _ __  | |   __| | | |
 | |_| |  / _ \ | | | |  / _ \        \ \ /\ / /  / _ \  | '__| | |  / _` | | |
 |  _  | |  __/ | | | | | (_) |  _     \ V  V /  | (_) | | |    | | | (_| | |_|
 |_| |_|  \___| |_| |_|  \___/  ( )     \_/\_/    \___/  |_|    |_|  \__,_| (_)
                                |/        

And that's pretty much all there is to it!

Right now clj-figlet only supports the "Full Size" style of rendering FIGlets. The other "smushing" style will be added in an upcoming release.

clj-image2ascii

This library was inspired by Claskii. The author also has a great post detailing it's development here.

So, why would you want to use this library? Well, what a silly question. I mean, who among us has not ever wanted to change this:

into this:

.................................................................................................... .................................................................................................... .................................................................................................... .................................................................................................... .................................................................................................... .................................................................................................... .................................................................................................... .................................................................................................... .................................................................................................... .................................................................................................... ........................................$###########################:...$##:........................ ................................A#############################################...................... ...........................*################$.............................=####..................... ........................###########............#####+.....###................###.................... .....................#######=:*@####@A######@.................##..............###................... ...................#####.........................................##............###.................. .................####....+##A................A###,.......####......#+...........###................. ................###,....%.....:#############%.................##.....#...........###................ ..............####...............................................##...##..........##................ .............####..........................................A##@....#%...#.........###............... ............@##%..........##=.....###*.............#............##...#...:=........##............... ............A#A.........#.........................#................#..#....#.......###.............. ............##.........#.........................#..................#...#...........##.............. ............##........A.............#............#.......................#..........##%............. ............##........,..........................@........................$..........##............. ...........###..........................................#########....................##A............ ...........##,.......................................###############..................##............ ..........###.............#####=...................#####..###########.................###........... .........###............###########...............###.....############.................###.......... ........###............###....#######............###.......#######...##..#........#########......... .......###...#####A##..###############..........###....################....##....#####A..###,....... ......###,.A#:...........#################......###..######+......#####.................#..##A...... ......###.#A.....................:#######........#######.....#...............#######......#.###..... .....###.#...#......................###...........###........###...........###########.......##..... .....###.A.#....$#..................@#.......................,###........######.....###.......##.... .....##.#..+..########..............@#........................#####=..#######...#.....##......###... .....##.#....###########..@#........@#..........................##########$....##......##...:..##... .....##..............#######........##.........................................##......##...#..##... .....##...........#..%#####=......+###........................................###@......##..#..##@.. .....##...........##.............####.............###...,..................*#######.....##..#..+##.. .....##.+.........##............###..............######..................#####@.####....##..#..%##.. ......###..#,....###...........###..................,##.$######........#####....######..##..,..###.. ......###....#...###.........#####...................##.............A#####......##.A##.##......##... ......A###......####........#.#####.........#######..##...........######%......###.....##......##... .......##..##...#####.....#......###........#######.###........########.......*##......#...#..###... .......###...#..##.###............###...............##=.....#######*.##......####..........#..##.... ........##=....########............###.##................#######.....##.....#####.........#..###.... ........###....#####.####...........####@............#######%.......*##..#######........#...###..... .........##....#####.#######..........##.........########...........###########............###...... .........##....####..##.########%.........+###########%##..........#######..###..........####....... .........%##...####..##...#######################......###.......#######...###...........###........ ..........##...####..##...##..,##########$..##..........##...$#########....##...........###......... ..........##...####.###...##.......##.......##..........##@#########.##...##A..........###.......... ..........##...########..###.......##.......##..........##########..##A..###...........##........... ..........##...##########.##.......##.......##......############.....##.###...........###........... ..........##...##############################################........#####............##............ ..........##...############################################..........%####............##............ ..........##...:#######################################..##..........####............##............. ..........##....####################################.....##.........####.............##............. ..........##....##.############################*.........###......####@.............##.............. ..........##....######.####################=..#...........##.....####..............###.............. ..........##.....##.##.###..+##....###........#,..........###..#####..............###............... ..........#@.....##A##..##...##.....##.......,##...........##+####...............###................ .........##......*####..###..###....##.......+##...........######...............###................. .........##.......####...##...##.....##.......##........#######......#=...##...###.................. .........##........########...##*....##.......##.....########.....$#....##...A###................... .........##..........#############,..##.....+#############......##....##%...####.................... .........##.............###############################.......##....##%...####...................... .........##...................=################%............##....##....A####....................... .........##...............................................##....##....=####......................... .........##.........#,.........@@@@@@@$::A###...........##...##@....@####........................... .........##..........##..............................##...A##.....#####............................. ........,##....#.......###.......................###...###......#####............................... ........=##.....#.........,#####%==%###+............##........#####................................. ........=##......##............................,##..........#####................................... ........,##........##A....................###.............####+..................................... .........##...........=#############=...................####,....................................... .........##...........................................####.......................................... ..........##........................................####=........................................... ..........###................................#########$............................................. ...........###............................A#########................................................ ............####.......................##########................................................... .............:#####...............#########......................................................... ...............########################............................................................. ..................################.................................................................. .................................................................................................... .................................................................................................... .................................................................................................... .................................................................................................... .................................................................................................... ....................................................................................................

Seems like a real no-brainer to me.

clj-image2ascii can also convert animated GIFs to a set of ASCII frames with frame delay information so you can set up your own animation of ASCII images.

Performance

Working on this library was a nice exercise in writing performant Clojure code for me. I've been working with Clojure now for about a year on mostly just CRUD web apps where writing super performant code is not that important. After fiddling around with the original Claskii code, I decided to benchmark it (using Criterium mostly) and found that it was quite slow. Which isn't all that surprising if you take a look at the code.

For one, there are no type hints anywhere, so Clojure will end up doing quite a bit of reflection lookups at runtime. From what I've seen in other bits of code before working on this project, adding type hints -- even just by itself -- can make quite a significant difference in performance. However, we can still do better.

For example, Claskii is doing this for each pixel it reads from the source image it is converting:

(get-properties
  (Color. (.getRGB img x y))
  .getRed .getGreen .getBlue)

get-properties is a macro the author had which just returns a vector of the values from each of the property getters listed. Essentially this is creating a new Color object for each pixel (obtained via BufferedImage.getRGB which returns a 32bit packed ARGB integer). Obviously creating a new object for each pixel is not performant.

Later on, to convert the pixel to an ASCII character with HTML representation including color information, it does this:

(html [:span {:style (format "color: rgb(%s,%s,%s);" red green blue)} output])

Which is using hiccup to generate an HTML string from Clojure data structures, and of course that call to format which internally uses String.format which performs memory allocations and is somewhat slow to be calling in an inner loop.

There are a few more examples, but you get the idea. My intention is not to pick apart Claskii. It does work, and it works quite well. It could just be faster, and I figured what the heck, lets give it a shot.

Eventually I ended up with the following set of functions:

(defn add-pixel [^Integer argb ^StringBuilder sb color?]
  (let [r          (bit-shift-right (bit-and 0x00ff0000 argb) 16)
        g          (bit-shift-right (bit-and 0x0000ff00 argb) 8)
        b          (bit-and 0x000000ff argb)
        peak       (int
                     (Math/sqrt
                       (+ (* r r 0.241)
                          (* g g 0.691)
                          (* b b 0.068))))
        char-index (if (zero? peak)
                     (dec num-ascii-chars)
                     (dec (int (* num-ascii-chars (/ peak 255)))))
        pixel-char (nth ascii-chars (if (pos? char-index) char-index 0))]
    (if color?
      (doto sb
        (.append "<span style=\"color: rgb(")
        (.append r)
        (.append ",")
        (.append g)
        (.append ",")
        (.append b)
        (.append ");\">")
        (.append pixel-char)
        (.append "</span>"))
      pixel-char)))

(defn pixels->ascii [^BufferedImage image color?]
  (let [width        (.getWidth image)
        height       (.getHeight image)
        sb           (StringBuilder. (+ (* width height 47) (* height 4)))
        ^ints pixels (.getRGB image 0 0 width height nil 0 width)]
    (dotimes [y height]
      (dotimes [x width]
        (add-pixel (aget pixels (+ x (* y width))) sb color?))
      (.append sb (if color? "<br>" \newline)))
    (.toString sb)))

I arrived at this after doing quite a bit of reading into writing fast Clojure code. It's probably not the fastest or best code, but it is still very fast. Key factors that make this performant:

  • Type hints. (set! *warn-on-reflection* true) is your friend!
  • Using a StringBuilder, and initializing it with a capacity that will be large enough to hold the entire string we will be building up.
  • Accessing the raw pixels in BufferedImage via its getRGB method which returns the pixels as an int[].
  • Bit shifts to decompose the packed 32-bit ARGB integer used for each pixel in the image.

Using Criterium for benchmarking on my computer, I found this had a mean execution time of about 86ms when converting a 300x300 24-bit color PNG with color information being included in the output ASCII string. Very good, considering my first unoptimized version of Claskii's algorithm ran with a mean execution time of over 800ms!

But the question left in my mind was, if I wrote this in pure Java, would it be faster? So, I went ahead and wrote a Java version:

public class ImageToAscii {
	static final char[] asciiChars = {'#', 'A', '@', '%', '$', '+', '=', '*', ':', ',', '.', ' '};
	static final int spanLength = "<span style=\"color:rgb(255,255,255);\">X</span>".length();
	static final int lineTerminatorLength = "<br>".length();

	public static String convert(BufferedImage image, boolean useColor) {
		int width = image.getWidth();
		int height = image.getHeight();

		int maxLength = (useColor ?
		                 (width * height * spanLength) + (height * lineTerminatorLength) :
		                 (width * height) + height);

		StringBuilder sb = new StringBuilder(maxLength);

		int[] pixels = image.getRGB(0, 0, width, height, null, 0, width);
		for (int y = 0; y < height; ++y) {
			for (int x = 0; x < width; ++x) {
				int argb = pixels[(y * width) + x];
				int r = (0x00ff0000 & argb) >> 16;
				int g = (0x0000ff00 & argb) >> 8;
				int b = (0x000000ff & argb);
				int brightness = (int)Math.sqrt((r * r * 0.241f) +
				                                (g * g * 0.691f) +
				                                (b * b * 0.068f));
				int charIndex;
				if (brightness == 0.0f)
					charIndex = asciiChars.length - 1;
				else
					charIndex = (int)((brightness / 255.0f) * asciiChars.length) - 1;

				char pixelChar = asciiChars[charIndex > 0 ? charIndex : 0];

				if (useColor) {
					sb.append("<span style=\"color:rgb(");
					sb.append(r);
					sb.append(',');
					sb.append(g);
					sb.append(',');
					sb.append(b);
					sb.append(");\">");
					sb.append(pixelChar);
					sb.append("</span>");
				} else
					sb.append(pixelChar);
			}
			if (useColor)
				sb.append("<br>");
			else
				sb.append('\n');
		}

		return sb.toString();
	}
}

The same benchmark (with Criterium, using a Clojure wrapper function that called this Java version) showed me that yes, indeed Java was still faster for raw performance: mean execution time of 16ms.

I'm honestly not sure how much faster I could get the pure Clojure version to run, but I suspect any possible speed improvements (if there are any) would end up making the code really un-idiomatic and harder to read. I don't find the Java version hard to read at all, so I'm happy arriving at the conclusion that for raw performance, it does make sense to at least consider using bits of Java in a predominantly Clojure project, especially given that interop between Java and Clojure is extremely easy.

clj-metasearch

Posted by gered,

Happy to say that a little utility library of mine, clj-metasearch was released earlier this week.

This library allows you to search through all Clojure namespaces in the current classpath for vars which have metadata matching a predicate. This has a number of potential uses, such as automatically finding functions, or other types of vars that have metadata marking them as some kind of "plugin" for the application. You could do something like:

(find-vars :myapp-plugin)

Which would scan the currently loaded namespaces only for any vars containing metadata with a :myapp-plugin key which had a "truthy" value. find-vars returns a sequence of maps:

({:ns myapp.plugins, :var (var myapp.plugins/awesome-plugin)}
 {:ns myapp.plugins, :var (var myapp.plugins/foobar-plugin)})

You can use var-get on the :var value to get the actual value found. I'll likely be changing this return value format somewhat and possibly introducing a kind of "transform" function as an additional optional argument to find-vars that can be used to transform these results as they are found so the return value is exactly in the format you need.

If you wanted to scan all namespaces, including ones that have yet to be loaded:

(find-vars
  :myapp-plugin
  :require-all-namespaces? true)

The extra parameter will cause each namespace to be loaded via require before it is checked. This can potentially cause problems or warnings to be output. For example, if namespaces are loaded that redefine vars, or other namespaces that can't be loaded in the current JVM version (clojure.core.reducers is an example of this, it can't be loaded on Java 6 without some extra library also being present).

When using the :require-all-namespaces? parameter it's usually best to supply an extra predicate to limit the number of Clojure namespaces that are checked (if this is at all possible for your use case):

(find-vars
  :myapp-plugin
  :require-all-namespaces? true
  :namespace-pred #(.contains (str %) "myapp-plugin"))

Example: Automatic Compojure Route Discovery

So, I originally put this library together because I wanted to be able to automatically find Compojure routes and have them get added to the Ring handler routes vector without me needing to explicitly list them all out. This is admittedly quite a minor thing to need to do, but it has always seemed silly to me that adding routes to a Ring handler is a manual process in your typical Clojure web app.

Right now I just use a couple macros and a single function for this automatic Compojure route discovery:

(ns yourwebapp.route-utils
  (:require [clj-metasearch.core :refer [find-vars]]
            [compojure.core :refer [defroutes]]
            [noir.util.route :refer [def-restricted-routes]]))

(defmacro register-routes [name & routes]
  `(defroutes
     ~(with-meta name {:compojure-routes? true})
     ~@routes))

(defmacro register-restricted-routes [name & routes]
  `(def-restricted-routes
     ~(with-meta name {:compojure-routes? true})
     ~@routes))

(defn find-routes
  "finds all routes created with the above two macros. namespace-filter is a string or a collection of
   strings used to limit the namespaces searched for available routes. more-routes is any additional
   routes you want to manually append to the list of found routes."
  [namespace-filter & more-routes]
  (let [routes (find-vars
                 :compojure-routes?
                 :require-all-namespaces? true
                 :namespace-pred (if (coll? namespace-filter)
                                   (fn [namespace]
                                     (some #(.startsWith (str namespace) %) namespace-filter))
                                   (fn [namespace]
                                     (.startsWith (str namespace) namespace-filter))))]
    (as-> routes x
          (map :var x)
          (map var-get x)
          (concat x more-routes)
          (vec x))))

The register-routes andregister-restricted-routes macros are drop-in replacements for Compojure's defroutes and lib-noir's def-restricted-routes. Example use of find-routes:

(find-routes "yourwebapp.routes." app-routes)

Would find all the Compojure routes defined with the register-routes and/or register-restricted-routes macros and also append the routes in app-routes to this list. You would then pass the returned list off to your Ring handler.