Source code for Quantum, an RTS with massive unit counts

Download Quantum

Many moons ago (7 years ago), i wrote a little RTS inspired by Dyson/Eufloria. I actually contacted the Eufloria guys before doing so and offered them to write a multiplayer mode for them. They politely declined and allowed me to write a clone with MP support. So that’s what i did.

After 2 months of work, Quantum was complete. We played it a lot at work. It got around 20k downloads from around the web after making my rounds on a few websites (TIG, JGO, etc.). Eventually, interest faded away and i forgot about it.

Fast forward to today. I was thinking about writing an RTS again. I remembered Quantum and started searching for traces of it on the web. I found a video and shortly after that a fork of the original source.

I forked the fork and started reviving it. A few hours later, it’s ready for you to shame my code from 7 years ago 😀 Here are features that might interest you:

  • Multiplayer via a latency-adaptive lock-step simulation, with thousands of units, inspired by the old 1500 Archers article
  • Scripting via BeanShell, a very old but super lightweight scripting language for the JVM. Here’s the complete AI.
  • A map editor

You should NOT take the code structure as a big inspiration! Code style, architecture and especially the graphics part (mixing fixed function and programmable pipeline) are NOT good. However, there’s a lack of fully open-source games out there that are more than a simple 2D plattformer, so i thought i’d share.

If someone opens a server, let me know :)

Font metrics changes

For bitmap fonts, padding can be added when the bitmap font is created to give more space around the glyphs for effects like a glow or drop shadow. BitmapFont has so far ignored padding, as reported in #3074. This means the font metrics change depending on the padding used, which makes it difficult to draw text where you want it. This PR has now been fixed, so the metrics are the same no matter how much padding is used around the glyphs.

Font metrics

Unfortunately this means that if you had been using a font that used padding, all of your carefully placed text may change size or position. You can go and fix this up everywhere, but for existing apps it is likely easier to just use this code to go back to the old behavior:

/** Adjusts the font data metrics to not take padding into account. */
static private void includePaddingInMetrics (BitmapFontData fontData) {
	float padY = fontData.padTop + fontData.padBottom;
	fontData.xHeight += padY;
	fontData.capHeight += padY;
	fontData.ascent -= padY;
	fontData.descent -= fontData.padBottom;
	fontData.padTop = 0;
	fontData.padLeft = 0;
	fontData.padBottom = 0;
	fontData.padRight = 0;

BitmapFont refactoring

My friends! Let’s talk about font rendering. So far we’ve been using a relatively naive way of rendering in libgdx: for each character in the string, find the glyph, draw the glyph. This works fine for English and others, but not all. Some languages use different glyphs for the same character depending on where it appears in the word or sentence. As soon as we don’t have a one-to-one relationship between characters and glyphs, we can’t do proper rendering.

The solution for this is to transform the list of characters into a list of glyphs, then render the glyphs. The BitmapFontData, BitmapFont, and BitmapFontCache classes have been refactored to support doing that. Here’s how it works:

  1. GlyphLayout, a new class, is given a string of characters. It first breaks the strings into “runs”. A run is a string of characters all the same color and without newlines. The reason to break the text into runs is that each run can be “shaped” individually, which means to convert the characters into glyphs for display. When breaking the string into runs, GlyphLayout parses color markup tags (eg “thisIsWhite[red]thisIsRed”) and stores the color for each run.
  2. Next, the runs are shaped using a new BitmapFontData method, getGlyphs. This takes the characters for the run and generates both a list of glyphs and a list of positions for each glyph. The default implementation just looks up the glyph for the character as has always been done. A more complex implementation could use HarfBuzz or another lib to do the shaping.
  3. Next, the shaped runs are laid out. If wrapping is enabled and a run’s glyphs exceed the wrap width, then glyphs are split for line wrapping using another new BitmapFontData method, getWrapIndex. The run is ended, the remaining glyphs are moved to a new run on the next line, and the layout process repeats. Because BitmapFontData controls line wrapping, it can be customized for a particular langauge.
  4. At this point GlyphLayout is done. It stores the Array<GlyphRun> and provides a width and height which is the bounds of all the runs. Previously getting text bounds required all the work of laying out the glyphs, which then had to be repeated for drawing. With GlyphLayout, the work is only done once. Each GlyphRun has Array<Glyph> and a FloatArray for the positions.
  5. Next comes BitmapFontCache. This class now has an Array<GlyphLayout> and its job is to store the vertex data for rendering the glyphs in those GlyphLayouts.

Introducing GlyphLayout and the new way of rendering glyphs required some relatively small API changes. Sorry! Since breaking the API was unavoidable, we also made a few other breaking changes. It’s better to fix up a few API changes in one go than to have to do it every time you update libgdx.

Here are the changes that may affect you:

  • The BitmapFontCache setText, setMultilineText, setWrappedText, and similar addXxx methods are replaced by setText and addText. The same happened to the BitmapFont draw methods, which delegates to BitmapFontCache. There are fewer methods now and they have more features. These methods all return GlyphLayout instead of TextBounds.
  • BitmapFont.TextBounds and getBounds are done. Instead, give the string to GlyphLayout and get the bounds using its width and height fields. You can then draw the text by passing the same GlyphLayout to BitmapFont, which means the glyphs don’t have to be laid out twice like they used to.
  • BitmapFont.HAlignment is gone. Align is used instead.
  • Align has been moved to the utils package.
  • markupEnabled and setScale have been moved to BitmapFontData. These are really BitmapFontData settings, not per BitmapFont settings.
  • BitmapFont computeGlyphAdvancesAndPositions and computeVisibleGlyphs are gone. GlyphLayout can be used instead, which provides each glyph position.
  • BitmapFont has an Array<TextureRegion> rather than a TextureRegion[]. This makes it easier to add regions as needed.

Here’s a quick guide for moving to the new API:

// Old:
font.draw(batch, "meow", x, y);
font.drawMultiline(batch, "meow\nmeow", x, y);
font.drawWrapped(batch, "meow meow meow meow meow meow", x, y, width, HAlignment.RIGHT);

TextBounds bounds = font.getBounds("meow");
font.draw(batch, "meow", x + bounds.width / 3, y + bounds.height / 3);

// New:
font.draw(batch, "meow", x, y);
font.draw(batch, "meow\nmeow", x, y);
font.draw(batch, "meow meow meow meow meow meow", x, y, width, Align.right, true);

GlyphLayout layout = new GlyphLayout(); // Obviously stick this in a field to avoid allocation each frame.
font.draw(batch, layout, x + layout.width / 3, y + layout.height / 3);

Sorry for the trouble if you end up needing to change your code slightly, but this enables us to have proper font rendering in the future. HarfBuzz and FreeType can allow us to render text in any language, which is a big deal!

But wait, there’s more! Many languages don’t need glyph shaping – they can use a one-to-one mapping of characters to glyphs – but they have a different problem: they have way too many glyphs than we can hope to fit on a texture. There are only a few solutions for this, you need to render on the fly.

Rendering entire sentences to the atlas instead of individual glyphs is one approach, but this isn’t great for text input or text that changes a lot. Rendering glyphs on the fly, as they are encountered, is probably the best we can do. If the glyph atlas becomes filled, we can either add atlas pages (bad for draw calls) or empty the atlas texture and start over (fails if the screen contains more glyphs than can fit in a single page).

Rendering glyphs on the fly means using FreeType. We already have FreeTypeFontGenerator to generate a BitmapFont using FreeType, but now you can tell it to generate an “incremental” font. When the font is queried for a glyph that hasn’t been rendered yet, it renders it to the glyph atlas.

The red boxes at the top are the glyph atlas pages (purposefully made tiny for testing) and the text below is rendered using those glyphs. We can implement better packing (eg, a skyline variant) in the future, for now it uses PixmapPacker as it used to.

It will be interesting to see if this helps libgdx have better adoption in China, where currently cocos2d-x is widely used. Interestingly, cocos2d-x appears to use a “label” approach to font rendering where an entire sentence is rendered to the backing texture rather than individual glyphs.

If you’re interested in the changes for these features, you can check out PRs #2955 and #2973.