Monday, October 19, 2020

Abusing constexpr to implement gettext's _() macro with zero overhead.

You usually use the C library gettext with a _() macro that returns a localized version of the text passed in parameter.

For example:

puts(_("Hello"));

will result in a runtime search of the string "Hello", and _() eventually returns the localized version of the string. I assume the search is implemented using some sort of binary search, so it's probably extremely fast.
It's fast, but we can get rid of the search altogether using constexpr functions!

The trick is to use a constexpr function that at compile time gets the ID of the given string. Then it's a matter of doing an array lookup in the table of strings for the desired locale. The array lookup is still unavoidable because we don't know at compile time which locale the app will use.

In my case, _() is implemented using I18NStringIndex which looks like this:

constexpr bool I18N_EQ(char const * a, char const * b) {
 return std::string_view(a)==b;
}
constexpr int I18NStringIndex(const char* s) {
 if (I18N_EQ(s, "off")) { return 0; }
 if (I18N_EQ(s, "on")) { return 1; }
 if (I18N_EQ(s, "ok")) { return 2; }
 if (I18N_EQ(s, "Quick pause")) { return 3; }
 if (I18N_EQ(s, "Quick pause %s")) { return 4; }
 if (I18N_EQ(s, "Continue")) { return 5; }
 if (I18N_EQ(s, "Exit")) { return 6; }
 if (I18N_EQ(s, "Pause")) { return 7; }
 if (I18N_EQ(s, "Restart")) { return 8; }
 if (I18N_EQ(s, "Game Over")) { return 9; }
 if (I18N_EQ(s, "Play again")) { return 10; }
 if (I18N_EQ(s, "Cancel")) { return 11; }
 if (I18N_EQ(s, "Game is full")) { return 12; }
 if (I18N_EQ(s, "Local lobby")) { return 13; }
 if (I18N_EQ(s, "Ready")) { return 14; }
 if (I18N_EQ(s, "Waiting for other players...")) { return 15; }
 etc...

It is brutal, but the compiler still manages to transform I18NStringIndex("Game Over") to 9, and without noticeably slowing down the compilation!

It is used together with static arrays of strings, such as this one:

std::array<const char*, 138> kStrings_FR = {
"off",
"on",
"ok",
"Mini-pause",
"Mini-pause %s",
"Continuer",
"Quitter",
"Pause",
"Recommencer",
"Game Over",
"Rejouer",
"Annuler",
"Partie déjà complète",
"Partie locale",
"Je suis prêt !",
"En attente des autres joueurs...",
etc...

I18NStringIndex is generated by going over the code with a regex looking for patterns like _(".*"), and kStrings_LANG is generated using the .po files the translators filled.

One danger of this technique is that if for whatever reason the compiler can't run I18NStringIndex at compile time, then you pay a heavy price at runtime. Fortunately in C++20 you can specify the function to be consteval to make sure it does not happen.

Another downside of this technique is that whenever I18NStringIndex is re-generated, all the files containing _() need to be recompiled.
It's not a problem for me because I re-generate that function only when I'm about to send the .po files to the translators, which does not happen often.

Thursday, September 10, 2020

Exponential = dangerous

Note: This post was initially written in 2013, right after the release of Pacifism. For some reason I never published it. It's still relevant today, so here it is seven years later.

In PP2's pacifism mode, you can blow up enemies with bombs. I wanted to encourage people to take risks and not just blow up all the bombs they see, so I decided to give players an incentive to explode large amounts of enemies at the same time. To do that, I crafted a formula that gave players bonuses that exponentially grew along the number of simultaneous kills:

x=number of enemies killed.  f(x)=the bonus for x enemies killed.

I made sure that the exponential grew slowly: even if a player managed to be twice as good as me and destroy twice as many enemies as me (200), they would only make around 55000 points, which is high but not absurdly so.
Once I was pleased with the feeling of the game, I released it and waited for the scores to come in.
As the high scores started coming in, I got to see the replays of people playing the game mode for the very first time and figuring out how to play. That was really cool.
Quickly though, you could see in the replays the players getting better and better. 

And eventually, some players realized that by using the fastest ship, you could simply circle around the level dodging the bombs for a minute with the enemies never catching up with you. When they did blow a bomb, several hundreds enemies would explode resulting in bonuses of hundred of millions of points.


Eventually what had to happen happened, and people started scoring more than 2 billion points, which is a bad thing when you store your scores in 32 bit signed integers.

The lessons I learned:
  • Use 64 bit integers for scores.
  • Put a cap to exponential scores, or at least be very careful. You never know what the player are going to.
  • Be ready to reset the scores and prevent people with old version of the game to send "bad" scores.
  • Replays are so useful, it's ridiculous. Without them, I would have had no idea what the players were doing, and I probably would have assumed they were cheating and sending fake scores.

Saturday, June 20, 2020

PewPew Live released!

PewPew Live was released today on Android!

Let's go over what's new compared to PewPew 2:

  • LAN Multiplayer

Multiplayer is the main difference with PewPew 2, and the reason PewPew Live exists in the first place. The initial plan was to support online multiplayer, but this proved to be very complex. For now, the game will only support LAN.

  • Support for custom levels

Custom levels are another big new thing. Allowing users to create levels will increase the replayability of the game.
I had to choose where on the spectrum would the creation be: should the players be given building blocks that they can remix (low barrier of entry, low variety of levels), or should the players have to directly write code (high barrier of entry, high variety of levels) like I do when creating levels? I chose the latter because that's what I would have liked as a player, it introduces people to programming and tools that lower the barrier of entry can always be built on top.

  • New game modes
  • Unlock-able ships, trails, bullets
  • Improved graphics

There are also numerous under-the-hood improvements.
The original PewPew was started around 2009, at the beginning of the smartphone era. Many assumptions made back then do not make any sense in today's landscape. On top of this, the original game's feature set grew organically without much planning. I took PewPew Live as an opportunity to fix everything, including:

  • Support for high-refresh-rate screens

PewPew Live supports refresh rates higher than 60 fps because the rendering system can interpolate between game states.

  • Abstraction of the rendering API

PewPew 2 was directly using OpenGL. PewPew Live has an abstraction over the rendering API that improves portability. For example, using directly Vulcan should be possible.
Shader-based rendering
Now that shaders are available on almost all mobile devices, PewPew Live can depend on them for animations.

  • Resizable window support

The UI now dynamically adapts to the size of the window.

  • Deterministic replays

I had retrofitted determinism in PewPew 2, but there were some bugs that I never solved. Now the game is designed from the start to be deterministic. This should make it harder to cheat.

  • An overall better game architecture of the game

It gives me more flexibility to do weird things. E.g. you can run multiple instances of the game simultaneously.

  • Editor

In PewPew 1 I was editing SVGs in Adobe Illustrator and exporting them to PewPew. This time around I invested time to make a tailored editor for graphics and level. This should lead to higher quality models and levels.

  • Better collision detection for walls

In PewPew, a single wall was a quadrilateral with a width. This made editing levels painful, and if the wall was too thin compared to the velocity of an entity, collisions would be missed. This time around, the walls are actual lines and collisions can't be missed.

  • Sound synthesizing

In PewPew sound was made of .wav files. In PewPew Live, the sounds are synthesized at runtime. This allows more flexibility in the sounds and takes less space. It does limit the kind of sound effects the game can have, but the upside is that it makes the sound effects more consistent.

Tuesday, February 18, 2020

Ridiculously cheap depth of field effect for lines

I'm working on PewPew's sequel, for which I've revamped the graphics.
Instead of drawing lines directly using OpenGL, each individual line segment is made up of two triangles whose vertexes are computed with shaders. Getting lines in 3D space to be properly displayed on a 2D screen is not trivial. In PewPew's sequel I use the screen-space projected lines, a technique very well described in the Drawing Lines is Hard post.

The upside of drawing the lines yourself is that you are fully in control, which allows you to implement nice things such as joints, perspective, and even simulate depth of field.

https://en.wikipedia.org/wiki/Depth_of_field

Usually depth of field (DoF) in video games is implemented using a post-processing step that blurs the pixels with an intensity that is a function of the depth of the pixels. When we are rendering lines, we can approximate DoF directly when rendering the lines by having the vertex shader increase the width of lines and reduce their opacity proportionally to the distance they are from the plane of focus.
Left: without DoF
Right: with DoF

On top of that, we can make the lines appear blurry by adding a gradient along the lines in the fragment shader.

Left: DoF with sharp lines
Right: DoF with blurred lines

As long as there are not too many overlapping lines, the whole effect is very cheap in terms of processing power and runs very well on mobile devices.
In addition to that, it also properly handles transparency (unlike standard post-processing based DoF), which in the case of PewPew is very important since almost everything is rendered with additive blending.

Possible improvement

The technique does not work well when one end of a line is in the far plane while the other end is in the near plane. In that case, because the shader interpolates between the two ending of the line, the center will be wide and with a low opacity while it should actually be narrow and crisp.
I reduced the impact of this problem by splitting the geometry.

Unexpected Gotchas in Making a Game Deterministic

When aiming for a fully deterministic program, it is common knowledge that you have to deterministically seed your random number generators,...