Thoughts on game development with LibGDX and JRuby
As part of my ongoing, never-ending plan to Finally Write Another Roguelike, I've been dabbling with LibGDX, a game development library for Java. And having gotten to the point where it kind of does a few game-like things, I'm writing this to document my progress and in the hope that someone else may find this information useful.
LibGDX is a nice library but has the immediate major downside of requiring me to use Java without getting paid for it. I worked around that by using JRuby. (As you know Bob, JRuby is a version of Ruby that runs on the JVM and interoperates nicely with existing Java code.)
My first project was a class-by-class rewrite of Zombie Bird, a Flappy Bird clone written as a GDX tutorial. The code for that is here. (Feel free to grab it to use as a starting point.)
I then went on to port an existing Roguelike attempt in Ruby and ncurses to JRuby and GDX. This project is ongoing but I've managed to get it to do basic things (walk around, manage inventory) using GDX.
And during this process, I have managed to Learn Things. I will now impart my hard-won Knowledge to you.
Using Maven to Handle Java Dependencies
GDX has a GUI-based configuration thingy that will fetch the latest GDX jars and create an empty Gradle(?) project for you. Which is all well and good if you're using Java, but I'm not. Some Googling revealed that other people who've tried this just copied the jars somewhere into JRuby's classpath but I've gotten spoiled by the Maven repository network so I wanted something more automated.
JRuby has no problem accessing Java code in an external jar file, and GDX is accessible by Maven, so the solution was pretty straightforward:
- Create a trivial GDX program.
- Use Maven and the
maven-assembly-plugin
to build it into a big jar that includes all dependencies. - Add the jar to my JRuby $CLASSPATH variable and
require
it.
(Why Maven and not one of the better other build
tools? Because Maven is the one I know. You could do this with a
better build tool or you could just clone my code from github and be
done with it.)
My trivial GDX program was the default minimal program generated by one of the scaffolds (I don't remember which). One nice thing about it is that I can run it as a standalone program:
java -jar mvn_lib/target/bigjar-CURRENT-jar-with-dependencies.jar
It doesn't do much beyond display a moving image, but it's enough to prove to me that I successfully found all of the pieces GDX needs to run. This came in handy the times my program wouldn't work for some stupid reason because I could confirm that at least the library was correctly installed.
So now when I start on a fresh checkout, I just need to do a
cd mvn_lib; mvn clean package
and everything is there.
Upgrading GDX is simple too: just change the version in
mvn_lib/pom.xml
and rerun the build command.
Doing a GUI
GDX has a set of GUI widgets built on top of its 2D scene graph. As a GUI library, its, uh, pretty good for a game library.
Since I'm writing a turn-based game, having decent UI code is a lot more important to me than graphic performance and low-level control, and I started to regret using GDX for this project after a while. I persevered though and was eventually able to get a decent UI up and running. (Presumably, I'll need to do animations and sound effects at some point in the future, at which time I'll be thankful I stayed with GDX and didn't go to Swing or something.)
In any case, my game has a pretty simple screen layout: a map, a set of stats and a text window for game messages, arranged top to bottom:
(I'm using little pictures of characters because I'm oldschool. And a terrible artist.)
This was all pretty simple. The map is a subclass of
com.badlogic.gdx.scenes.scene2d.ui.Widget
and was pretty easy to
code in Ruby. (Note: remember to super()
in initialize
, lest you
get a really obscure crash.)
The others were just provided widgets (Table
and TextArea
respectively), each configured by a corresponding Ruby class. It
would have been relatively simple to make them into subclasses, but I
did them first and wasn't clear how well subclassing GDX components
would work.
Getting the layout to work correctly was kind of painful and required
a lot of trial-and-error intermixed with careful reading of the
related Wiki pages. Turning on debug mode (by calling
setDebug(true)
on the table) helped a lot--this makes the table
outline its cells with coloured lines and gives you a much better idea
of how it works.
It also helped to be able to edit and run without needing to rebuild the project each time I made a small change, so that's one for Ruby1.
(This is not intended as a slam on the GDX UI code; this stuff is complicated. I have the same kinds of problems with Tcl/Tk and I've been using it on and off for decades.)
Don't Use Dialog
The one thing that ended up being a huge pain was the inventory window:
I initially wrote it as a subclass of
com.badlogic.gdx.scenes.scene2d.ui.Dialog
, which provides some nice
functionality. Unfortunately, keystrokes sent to the inventory dialog
would leak back to the main game. For example, pressing 'U' to
stop equipping an item would then also cause the player to move up and
to the right, which is what 'U' means during normal movement.
It turns out that pressing a key emits three events: KeyDown
,
KeyPressed
and KeyUp
, in that order. The Dialog
was listening
for KeyDown
while the map listened for KeyPressed
, so using a
keyboard shortcut on the inventory window would close it on the
KeyDown
, returning control to the main game which would then receive
the KeyPressed
.
I wasn't able to find a way to make Widget
deal with this correctly
and ultimately ended up rewriting the inventory window as a subclass
of Window
, Dialog
's parent.
Function Overloading
One major pain in using JRuby to call Java code is dealing with overloaded functions. The workaround is ugly, so it's fortunate that this comes up pretty rarely.
Here's the problem:
Suppose a Java class defines two functions with the same name and number of arguments but different types:
class Foo {
int bar(int x) { ... }
int bar(String x) { ... }
}
When calling them from Java, like this:
Foo x = new Foo();
...
x.bar(42); // calls int version
x.bar("forty-two"); // calls string version
the Java compiler knows exactly which function to actually call because it knows the type of the argument. This is not the case in JRuby:
Foo.new.bar("thingy") # may or may not work
Because Ruby is dynamically typed, JRuby can't tell which bar()
to
call, so it guesses and issues a warning.
Fortunately, JRuby provides the java_send
method, which lets you
call a method by name and type signature:
Foo.new.javaSend :bar, [java.lang.String], "thingy"
I've only needed to do this once in my project; most overloaded methods also have different numbers of arguments.
Implied Setters
Ruby (mostly) follows the
Uniform Access Principle,
while Java does not. As a result, the convention is for Java classes
to implement getter and setter methods of the form setXXX()
and
getXXX()
.
However, JRuby automagically converts between the two. That is, this:
x.text = "some text"
puts x.text
works the same way as this:
x.setText("some text")
puts x.getText()
in a Java class that implements only setText()
and getText()
.
I initially stuck to the Java form for Java classes because I didn't want to obscure the underlying implementation. However, the Ruby form is so much nicer that I eventually gave in. I'm not sure if that's a good idea or not.
Unsolved Stuff
Dynamic Constructors
Similar to method overloads above, JRuby has trouble with overloaded constructors. And like the method above, it provides a workaround:
construct = JavaClass.java_class.constructor(Java::int, Java::int, Java::int)
object = construct.new_instance(0xa4, 0x00, 0x00)
When I tried it, I did indeed get an object that claimed to be of the
expected class, but I couldn't call the methods. It looked to me like
it was an instance of the class but deep down, JRuby believed it was
an instance of java.lang.Object
.
It ended up that I didn't need that class at all, so I never investigated further, but I wasn't able to find out if this was a JRuby bug or something else I didn't understand.
Worst case, I can write a helper function in Java and include it in my part of the big JAR.
Building a Release
I haven't yet come up with a satisfactory way of creating a release suitable for end-users.
Warbler looks promising. I've played with it a little, but haven't yet gotten it to do what I want. Perhaps with more fiddling, I can get it to make a releasable build for me.
The other issue is that JRuby needs you to include the source code in the jar file. This won't be a problem if you're just going to release it as open-source software, but if you're trying to sell the game or even are just trying to prevent spoilers, you'll need to do something else.
One possible solution is to encrypt the Ruby sources and keep them as
resources, then write a small Java loader that reads them in, decrypts
them and hands them off to the JRuby API. Alternately, you could
write a script to compile
the Ruby sources into a Java array literal
which, once again a loader routine decodes and hands off to the JRuby
API. Either way, the result is a more-or-less ordinary Java program.
Overall, It's Not Bad
In general, I'm finding the JRuby+GDX combination to work pretty well. There's nothing insurmountable and the problem areas, where they exist, are pretty rare. Ruby itself is a much more powerful and expressive language than Java, and is a lot more fun to use.
-
That being said, it takes about 6 seconds for JRuby to go from invocation to displaying that first window, so it's not lightning-fast either. There's a bunch of time that static typing and an IDE would have saved me too. ↩
# Posted 2016-03-05 21:55:13 UTC; last changed 2019-06-25 18:04:38 UTC