Perita’s Blog

← Return to blog index

Which Way is Up?

It was starting to get a bit annoying having to keep whose turn it was in our heads, and most of the time i ended trying to flick on the wrong window. To make matters worse, this week we added rank-based rounds to the gameplay: Once every player has taken her turn, the round is over and the game chooses the order under which players will play the next round based on their distance to the finishing line. Thus, we had even more things to remember, such as the current round and the players’ ranking, and were forced, as it were, to include a heads-up display (HUD).

Something simple, just like this:

A puck on the grass part of a race circuit, with a kart drawn within, a “Your turn” label on top of it, a “Round: 12” label on the top-left corner of the screen, and the players’ ranking on the top-right corner
Screenshot of the current barebones HUD, with round number, ranking, and a label showing whether it is the player’s turn.

Unconcerned, at this point, for the HUD’s aesthetics, we instantiated a new BitmapFont with its default 14 pt Arial font included with libGDX and, as a first attempt, rendered the current round using the same ViewPort that we have setup to render the world, expressed in SI units. The result was a 2 × 1 meters text.

A zoomed-out race circuit with a huge “Round 1” label that covers almost the entirety of the game sceen
“Round 1” label rendered with the viewport that uses meters as units.

Besides the obvious issue with the size, that could be fixed simply enough scaling down the font, this approach presents two additional problems: The text size is affected by the viewport’s zoom, and at every frame we have to compute the label position within the world (i.e., in meters) so that it will be shown at the screen’s top-left corner. This is, of course, doable, but it would be better if we could use pixels to place all elements in the HUD, as this is its “natural” units. The solution is to use separate ViewPort objects to render the world and the HUD.

worldViewPort = new FitViewPort(WIDTH_IN_METERS, HEIGHT_IN_METERS);
hudViewPort = new FitViewPort(WIDTH_IN_PIXELS, HEIGHT_IN_PIXELS);

// …

worldViewPort.apply();
batch.setProjectionMatrix(worldViewPort.getCamera().combined);
batch.begin();
// Render world
batch.end();

hudViewPort.apply();
batch.setProjectionMatrix(hudViewPort.getCamera().combined);
batch.begin();
// Render HUD
batch.end();

Having separate ViewPorts works wonderfully for all our purposes but one: we wanted the “Your Turn” label to follow the player’s puck after she flicks it.

The “Your Turn” label following the puck.

It would seem that we have to place the label in the world at the same position as the puck, that is expressed in meters. And that would mean that we have again the problem with the text size affected by the viewport’s zoom that we mentioned above. We can not just set the zoom to one only to render the text because then its position on the screen would be completely different than that of the puck.

We realized that, instead of placing the text at the puck’s position inside the world, in meters, we can draw it after the world has been rendered to the screen and, thus, “converted” to pixels. For instance, the following figure is a view 3 meters wide and 1.5 meters tall of the world drawn to a screen of 640 × 320 pixels. After rendering, the puck is at pixel coordinates (320, 160) because the camera is centered on it.

Race circuit with with a puck at the center. At the bottom there is a “0” on the left, a “320” in the middle, and “640” on the right; at the right border there is a “0” on top, a “160” in the center, and a “320” at the bottom.
3 × 1.5 meters slice of the world rendered to a 640 × 320 pixels screen.

Since the camera it is not necessarily centered around the puck, we need to convert to puck’s world position to screen coordinates and then from screen coordinates to the coordinates used by HUD’s ViewPort. Fortunately, ViewPort has the project and unproject methods to perform these coordinates conversions. Therefore, the most obvious code is:

Vector2 position = puck.getPosition();
position = worldViewPort.project(position);
position = hudViewPort.unproject(position);
font.draw(batch, "Your Turn", position.x, position.y);

This does not work: the Y axis is drawn inverted.

Y-axis is moving on the opposite direction.

It turns out that libGDX uses various coordinate systems. The two system relevant to this article, mentioned above, are world coordinates and HUD coordinates; the fundamental difference between these two is their units: world uses meters and HUD pixels. However, ViewPort.unproject assumes that the position is in a third coordinate system: touch coordinates. This coordinate system also uses pixels as units, but the most important difference with HUD is that the y-axis points downwards. That is, the point (0, 0) is on the top-left corner when using touch coordinates but on the bottom-left with HUD coordinates. Therefore, we must reverse the Y-axis returned by ViewPort.project.

Vector2 position = puck.getPosition();
position = worldViewPort.project(position);

// Invert Y-axis
position.y = Gdx.graphics.getHeight() - labelPosition.y;

position = hudViewPort.unproject(position);
font.draw(batch, "Your Turn", position.x, position.y);