Game Design

MZR leadergrid programming tricks

Posted on

IMG_1156
The MZR leadergrid.

In MZR (now on App Store: http://bit.ly/MZR_Game) there is an alternative approach to the concept of leaderboards. As the height of the player’s tower is the score we decided that it might be a good idea to have everyone else’s tower around. See your friends and their scores at a glance. As they are all represented as square columns on a grid I call this the MZR leadergrid.

User can browse around, zoom in or out and see the global picture – scores aggregated by country as well as their friends.

In this post I’ll talk about the leaderbaord implementation. In general it’s a collection of programming tricks and shortcuts and I wanted to describe some of those.

Most of the techniques are aimed at providing visual complexity while reducing the number of draw calls. High number of draw calls is possibly the main thing that would hurt your performance  – any opportunity to reduce that number without impact on quality should be considered.

The implementation of the MZR leadergrid score representation is split in two parts.

  • The local grid where every tower is an accurate representation of a real active player.
  • The global grid where a representation of the global leadergrid is rendered based on aggregated data from our servers.

Local grid

Local in this case refers to the locality of the other users’ towers to the centre player tower.  There is a grid of 9×9 towers around the main one.

This local grid, contains a few types of entries:

  • friend entries – these are the towers of friends logged in Game Center or Facebook. Every friend gets their avatar picture on top of their tower, as well as their score
  • active player entries – there are towers of players who have played the game recently. The idea is to get a selection players who are currently playing the game.
  • global grid entries – these are entries that could not be filled by friends or active players and are sampled in a similar way as the global grid one (described below)
output_J5uFZq
Local leadergrid – 9×9 towers of friends and active players. Animation showing each separate render call: towers, frames, friend avatars and scores.

In a way this local bit is the “friends” section of a normal leaderboard but extended so it can always fill 9×9 matrix.

This local grid is the immediate vicinity of the player playing field and as such visible during gameplay. I also wanted to indicate on the nearby columns (towers) how the current player score compares to them.

The local grid is accurate. Every column (friends and active players) is an actual player with their score as reported by our servers. Every time a player submits a high-score to the servers, it is stored in our database and then reported to the player’s friends when they play.

Rendering of the local grid

All of the columns in the local grid are a single rendering mesh. That allows efficient draw call submission – 1 call is better than 9×9 calls. The writing and avatars on top are additional draw calls as they are translucently blended on top. That also allows me to dynamically alter/grow columns as players upload higher scores. Each column is always in the same place in the vertex array so I can calculate what part of the vertex array I’m changing when the column grows.

A similar approach is taken with player scores. A single draw call renders all players score. They use the same font and can change dynamically. That’s possible because I always leave space in the vertex array for 4 digits. By using degenerate (zero area) triangles I can display scores that with less than 4 digits while maintaining vertex count for the full 4 digits.

Finally the avatars. The avatars can change at any time as player logs in to Game Center or Facebook and gets lists of friends… and their avatar pictures are downloaded from the respective network. Furthermore, to optimise the draw call for those avatar images they need to be in the same texture – not separate textures. Again a single draw call is a lot better than 81 (9×9) ones.

To achieve that I pack all images into a dynamic texture atlas. I use a render-to-texture technique to pack each avatar picture into a separate place on the same texture. Then I just need to update the display avatar mesh with the right texture coordinates of where that avatar image finds itself in the final atlas texture. A bonus of this atlas rendering is that I can apply a custom shader effect to the avatar images achieving the specific MZR look they have.

Finally I have the hight indicators rendered on the closest friend columns indicating how high the current player run has reached. At the same time if a local grid column is overtaken, it would flash briefly by changing the colours of the appropriate vertices in the grid vertex array.

Global grid

The global grid is everything outside the 9×9 local grid. It repeats infinitely the space so the user can scroll around in any direction when they browse. By repeats infinitely, I mean that the camera is telported back once it reaches certain limit – wrapping back to the opposite side of the area – that gives impression of endless scrolling.

The global grid is not a 1-to-1 representation of all the players in the world. It is a statistical representation. This is done for couple of reasons:

  1. to reduce the data transfer between the app and our servers
  2. to show the player a bigger picture about the scores of the world.

    table_country_pic
    Scores are aggregated by country on the server. This table shows the top 10 countries (by max score) at the time of writing of this article. (score = average score, score_count = number of players submitted score form that country, score_max = the top score form that country)

To achieve that I do a certain aggregation on the server with respect to players scores. At regular intervals a routine runs that calculates stats for every country in the world. The stats calculated are number of players from that country, the average score for that country and the maximum score.

The game client downloads the country score stats from the server. The global leadergrid is then rendered using that information. You can look at this from a data compression point of view: it’s a loss compression method where the data aggregation is the compression and the visualisation is the decompression part.

In terms of visualisation each country looks like a small mountain of columns. The more people play in that country the larger space it occupies. The top score for that country is the highest point of the mountain. The average score of the country dictates how “sharp” the mountain is. If the average score is low (a lot lower than the max one) then we get a mountain that’s fairly pointy – naturally the people with high scores are not many. If the average score is high, then we get a meatier mountain as there are more people with relatively high scores.

Using average and max scores to control the curve of the country statistical representation.
Using average and max scores to control the curve of the country statistical representation.

I achieve that by using a power function to approximate the curve slope. I take the average score (av) and the max score (max) for a given country and calculate the ratio as av/(max*0.5). That way score average that is half the max score for that country would mean a power of 1. Say we have average score of 50 and max score of 100 – give the formula we get 50/(100*0.5)=1. We then use the function f(x) = 1-pow(x, av/(max*0.5)) to get the height at various point of x where x is the distance of the sample point form the centre of the country region.

Note: the mathematical ground behind this was if we imagine these mountains as cones (or a swept/revolved curve that makes a kind of a strange cone) then the volume of that cone would be the total added scores of all players playing. That can be found by calculating player scores x average score. We know the height of the cone (max score) and the radius of the cone (number of player playing). Having the radius and the height of the  cone the unknown would be the curve which swept would dictate the volume of the “cone”. The connection between the curve and the cone volume would be an integral of that curve over a circle.

Solving this  however goes beyond my mathematical skills and seemed extremely OTT when I had a sensible approximation already.

Global grid country field

IMG_1157
Global country leadergird. Canada has the lead. Notice the relatively pointy score mountain due to relatively low average score of 213 (see the table above) for CA compared to the top score of 735.

Once the country scores have been downloaded from the server. They are inserted into a CountryField object that places them on a plane and uses a relaxation algorithm to make sure they are spaced evenly according to their respective radius. After that I can query the CountryField at any point in the world and it will return a height for that point based on what countries overlap and influence that point. The relaxation is seeded randomly so that every time a user gets the countries from the servers the global grid looks different.

When sampling at a certain point I consider all countries that the sampling point overlaps with. Then I apply a calculation described above and take the highest sample position. Layered on that is a deterministic noise function so that the score mountains don’t look uniform.

The rendering routine then uses this CountryField object to sample the heights for each of the global grid columns.

Adaptive subdivision 

Rendering all global grid towers in one go proved slow – too much geometry to update at the same time. I opted out for an adaptive subdivision technique where if columns are close to the camera then they are rendered if they are further away they get converted into a bigger column that’s the average of their height (it’s actually biased above the average as that proved better looking). This is done on 3×3 columns basis.  As camera moves around some of these combined columns split up dynamically and other coalesce. This combined with some non-linear interpolation gives the unique MZR look when user is browsing around.

This adaptive technique also means that geometry rendered is kept under control.It also allows the adaptive algorithm to work on only fraction of the geometry to interpolate and update the vertex array. The entire global grid is a single mesh and there for a single draw call.

Country names

On top, like clouds, are rendered the country initials (alpha-2 country code) and the country top score. This is again a single mesh so I can render that in a single draw call.

 

Learnings

MZR has unique look when it comes to leaderboards. In some ways however it is a bit “style before function”. That’s ok. MZR has always been about trying something new, be willing to go where traditionally games have done stuff another way.

With that in mind here’s what I think could be better:

  • No easy way to compare. In a traditional leaderboard user gets a list of entries with them inserted in the middle. They immediately know who is ahead of them and behind them. This information is more difficult to read in MZR. User gets a bigger picture view but details remain a bit hazy – the details are still there but not so easy to compare.
  • No easy way to compare country scores. User has to browse around  Its’ been fun to watch different countries take the lead (as of writing of this post Canada has the lead) but it would have been better to have a clear indication of who is in front.
  • In retrospect just using alpha-2 country codes make for a more alienating experience. Using something like full names or flags would have work better.

 

That’s it. If you are interested in reading more about any of the areas I describe, do let me know and I can write that up in more detail in a separate post.

See you next time.

MZR

Posted on

MZR_851x315_CoverImage

The name votes are in and by far the popular choice was MZR. It helps I was leaning towards that too.

Thanks to everyone who voted online or shared their opinion offline – it’s been really great to hear everyone’s thoughts.

I know this isn’t a major departure form the original working title “Mazer” but I guess that was a popular one too – shame it’s already taken on the App Store.

So, the game is going to be called MZR.

The logo was created by Steven Whitfield. I personally think it’s amazing – we tried couple of concepts but this one was there from the start – you can’t improve on perfection – thank you Steven!

There are a few things in the game that have changed visually. There is a new font, some new colour schemes… but most of the work recently has been towards getting this game finished so you can get your hands on it.

There is going to be more info here soon. I have accumulated some cool things to write about recently but it’s a tough choice between writing this blog and actually developing the game. I guess there is only so much spare time a “spare time indie” can find 🙂

Thank you.

 

Mazer: Core Game

Posted on

After that previous Mazer post about breadth-first graph traversal, or as I put it: wavefront propagation, I got couple of people asking “how do you make a game out of that?”. So, I’d like to talk about how that works and the ideas I tried before I got there.

I wanted to make an action puzzle game. One that has short gameplay loops where the player achieves something every few seconds. I also wanted to make it as procedural as possible so I can avoid having to hand-craft levels and play-test them to verify they are achievable. In a way the game would need to generate a “level” and setup it up in a way that:

  • it’s playable – it doesn’t result in an impossible scenario where there is no winning move
  • challenging – challenges the player in-line with where they are on the difficulty curve
  • fair – not having a win-win strategy that the user can apply without solving the puzzle
  • it has a short game loop – user is presented a new puzzle every few seconds

 

Take 1: The One Tap Mechanic

My first idea was to make a game where the player would asses the playing field – the maze – then apply a single tap/click to make their move. They would watch the game unfold and get a win/lose result.

After thinking this through for a while I settled on a reverse “estimated time of arrival” mechanic. This involves:

  1. Game generates a maze
  2. Game selects 3 locations in the maze as exits. Each exit is numbered – 1 to 3.
  3. Player has to tap on location in the maze that would be the start point.
  4. A wave propagates through the maze from the start point and the exits need to be reached in order – that’s success.
  5. If the wave reaches an exit before it’s order has come that’s a fail.

I liked that idea. It required player to “solve the maze” with regards to the 3 exits and estimate the starting location. It required them to think before they acted. To make this a challenge I decided to add time pressure. Given infinite time anyone would be able to trace the mazes and the make the perfect move… if time is ticking down there is incentive to act quickly.

To satisfy my “it’s playable” condition I decided to get the game to select an start point and solve the maze using Dijkstra’s algorithm then record in what order were the random exits reached. That would guarantee that there is always a valid solution. In a way the game would play the maze behind the scenes and do a few calculations before presenting it as puzzle the user. This carried on to be a recurring theme in the final game.

Core loop FAIL screen.
Core loop FAIL screen.

At this point I had written zero lines of code for this game. It was all in my head and my paper notebook. It was still a Zombie Bus.

I set out to prototype. I wrote a maze generator. It was a simple maze generator that didn’t create any loops in the maze. Between any two points in the maze there was exactly one direct route and there were no isolated parts – no unreachable parts of the maze.

It was at this stage I realised this mechanic would have a win-win strategy. All the user had to do was tap/click very close to the first exit and given the “no-loops” generated maze the exits would usually be reached in the right order. I could still make this work by guaranteeing there would be loops in the maze and that exits are generated in a such a way that maze loop presented alternative routes between the exits and the start point… but that seemed like desperately holding onto idea that wouldn’t be able to provide generic game puzzles players can enjoy without considerable post-processing of those mazes.

Take 2: Tap The Exits In Order Mechanic

I decided to move on to and come up with different approach. I knew liked the idea of mazes, estimated time of arrival and self verifying maze puzzles. It wasn’t a big departure to switch the roles of the game and the player. The game would be generating the start point and the exits but now the user would be assigning the order. User would have to tap on the exits in the oder they would be reached from the the start point.

This also made the mechanic more presentable to the user. They now had to solve the maze 3 times (incrementally if they can) judging which exit would be reached first, second and so on. It was a lot more straightforward than the reverse-engineer of start point the first mechanic above.

Level up/Success screen.
Level up/Success screen.

This did mean that I would lose some of the speed in terms of game loop repetition. Players would have to tap multiple times now. It also opens the can of worms of user input. When is the player solution final? I didn’t want to a “confirm”/”GO” button in there so I decided to have the last exit seals the solution mechanic. However, if player had made a mistake prior to that last input they’d have a chance to change their mind so I had to implement a toggle on/off for exits to accommodate that – adding the fact that the player is assigning order it’s not the most clean of input mechanic.

An interesting scenario started cropping up. Often exits would be at the same distance from the start point – presenting player with a hard to judge but always correct puzzle for those two exits. I did have to tweak the generation and setup algorithms to run the solution in the background and record the distances at all proposed exits and chose ones which different in travel distance. 

Time Rewards

Another area of experimentation was (and still is) time reward per solved maze. Do I:

  • give the player 1 second for every maze solved? (+1 seconds for every maze)
  • give them 1 second for every exit they successfully reach? (+3 seconds for 3 exits)
  • give them 1,2,3 seconds for respective exits? (+6 seconds for 3 exits)

The last one turned out to be very generous and was quickly dismissed. I’m currently running the middle one but I am finding that game can go on to last for a bit too long for my liking. Plus the user gets more time through the bonus levels…

 

Bonus Levels

Yes, there is more to this game. Even though what I just described is the core game loop – really it’s that simple. There are bonus levels where other maze algorithms are involved and pickups are collected, there is the scoring mechanic… and much more.

Stay tuned and I’ll see you next time.