When people think of concept art, they usually think of the super slick artworks that AAA games release as part of their marketing. However, making such perfected art takes a lot of time and is very inefficient when the goal is to explore many concepts. Today I would like to show the concept art that our artists made for the map AI Station 404 in Awesomenauts, as it is an excellent example of how rough real concept art can be.
click for high resolution
A couple of months ago I was at a presentation at the Control Conference where concept artist James Daly showed their concept art for one of the Transformers games. Every image looked mindblowingly awesome (have for some examples a look at this, this and this), but in a sense this is also weird: it seemed like concept artists only make superbly detailed and shaded paintings.
Presentations like that give the impression that that is how concept art works, but in practice making such high quality art is highly inefficient. The goal of concept art is not to make pretty pictures, but to design things for the game, to explore. If every image needs to be so detailed, then there is much less room for exploration than if an artist instead creates many simpler, quicker sketches and tries out many things that way. I suppose Daly and his team also made tons of quicker sketches and designs, but I don't remember seeing any of those in his presentation.
Of course such slick concept art does serve several purposes: it can be used in marketing and to make people enthusiastic. I imagine that any art that needs to be approved by a gigantic IP holder like Hasbro (for Transformers) has to be extra slick just to convince Hasbro to approve the designs. However, in practice most concept art is not intended for marketing and can thus be much simpler. Another reason why concept art might be so detailed is to help 3D artists, especially if the artist lacks imagination or is outsourced off-site.
The concept art that the artists at Ronimo make is usually not intended for outsiders, and only for exploration and internal communication. That means that it is often rough, but at the same time communicates the core ideas really well. The result is series of pictures that explore ideas in all kinds of directions. Such series are often really awesome to see, especially for the great variation in colours and shapes they play with. Good artists can create magic with just a few rough lines. An excellent example is this set of designs that was made for the level AI Station 404 in Awesomenauts:
click for high resolution
These images were drawn halfway development of Awesomenauts by Tristan Kok, who did an internship at Ronimo at the time. Below are all of them individually and at full size. When you look closely, you can see that only as much has been drawn as is needed to communicate the idea and colour scheme of the map. The right half of the map is often even left out, and objects are drawn very roughly.
1. click for high resolution
2. click for high resolution
3. click for high resolution
4. click for high resolution
5. click for high resolution
6. click for high resolution
7. click for high resolution
8. click for high resolution
9. click for high resolution
10. click for high resolution
11. click for high resolution
I think the final choice our art team made was to combine the shapes from number 8 and the colour scheme from number 10. Some of the brownish ones also look pretty cool in my opinion (like numbers 3 and 5), but would not have added enough variation because their colour schemes are similar to the then already existing Sorona map.
Which of these designs do you like best?
Another thing you can notice here is that the level geometry is still visible in many cases. These images were drawn on top of a screenshot of the level as our game designers had made it at the time. At Ronimo most ideas start with a gameplay idea and the art is only made after we have played it and concluded that it makes for fun and interesting gameplay. A while ago I wrote a blogpost about how our artists usually jump in when the gameplay is already set: Extreme Transformation Of Nauts During Development.
The art is based on the gameplay, so what better way to draw it than on top of an actual screenshot of that gameplay? Beginning concept artists often forget this link between art and game: they draw objects from their most awesome angle, instead of from the angles under which the objects are actually seen in the game. When designing a car for a racing game, the most important thing is what it looks like from the back since that is how the player will be viewing it 99% of the time. Yet many beginning concept artists focus only on the front, since that is where a car looks most awesome.
After some more concepting the level went into production. Art for it was gradually implemented into the playable level. Some shots didn't work immediately, so our lead artist Gijs grabbed a bunch of screenshots of the art that was there already and started drawing on top of them. Again: rough, drawing only as much as is needed to communicate the idea and style. A smaller studio like Ronimo does not have the limitless resources that AAA studios seem to have, so we need to work efficiently. Spending days on making awesome concept art while something simpler would suffice is not efficient.
click for high resolution
click for high resolution
click for high resolution
Gijs also drew a concept for the central machine in the level, as you can see below. In the bottom right this image even still shows the selection boxes of the objects that happened to be selected in our editor when the underlying screenshot was captured. I could personally never work like that, since I am way too much of a perfectionist. However, working so roughly is highly efficient and very fast, so this is much better than spending more time on perfecting an image while it already contains everything that is needed to communicate the point. (Another reason I could not work like this is that I am a programmer and am horrible at drawing...)
click for high resolution
I hope these images make it clear that concept art does not have to be super slick and detailed to work. The key thing is realise what the goal of the concept art is. If it is marketing or persuading people, then an image might have to be as awesome as possible. But if the goal is actual concepting, then it is much more important to explore many directions than to make slick images. As soon as the image communicates what it was intended for, there is no real need to add more detail.
Also, I personally think that despite the roughness, these images look incredibly cool, so I hope you enjoy viewing them as much as I do!
Saturday, 25 January 2014
Sunday, 19 January 2014
How chance in games is rarely just a roll of the dice
Games contain many forms of chance. From simply selecting a random character or level to play, to random pick-ups and even completely procedural worlds: chance, luck and randomisation play a huge role. While developing Awesomenauts we have experienced this first hand, especially in the various versions of Leon's crits, which has been changed several times in patches during the past 1.5 years. Last week I mostly talked about the psychology of crits, and today I am going to look at chance in games in general.
When a programmer is told to implement a random for something, he will often simply use std::rand() or something similar and that's it. However, in practice it turns out that both players and designers rarely really want a truly random chance.
A good example of this can be found in level selection in Awesomenauts: in a practice match there are four levels to chose from, and there is a 'random' button to select random levels. With a true random, the random button might result in playing the same level several times in a row. There is a small chance that a player gets the same level 4 times in a row (1.5625%, to be exact), and even 10 times the same level in a row is possible. When this happens, both players and designers start to voice complaints like “the random is not random, it always selects the same map”. Of course, in a truly random dice roll, the chances of throwing a number are completely independent of what was thrown before that, so even a million times the same value in a row can happen. The chance of that happening is just really small.
So what every game programmer needs to realise, is that when a designer asks for random, he rarely asks for a complete random. The random usually needs some extra logic to keep unwanted situations from happening. In Awesomenauts, the game remembers which map you last played and does not select it again for the next match (although if you join matches, you might still join several matches in a row with the same map). Similarly the music player remembers the last four songs it played and does not select those again when it chooses the next song to play. And I imagine that in a random dungeon generator it is likely not desirable to put instances of the same room next to each other.
An important part of random is human perception. Humans are exceptionally good at recognising patterns. In fact, humans are so good at this that they often recognise patterns where there are none. Noteworthy situations are remembered, while other situations are not. Astrology is for a large part based on this. If only one out of ten predictions turns out to be correct, many people will tend to remember that one correct prediction and not the nine false ones, making it possible for charlatans to pass for real predictors of the future.
Something similar goes for the perception of random in games. We have had numerous reports from players that the map they dislike most is selected much more often by the random map selection. However, every player who reports this reports it for a different map, and our random is just fine. What really happens there, is that people remember every time they get the map they don't want, and don't really remember what other maps they played recently.
Human psychology is therefore an important part in designing random. A great example of this can be found in one of my favourite game series: Civilization. Sid Meier did a hilarious presentation on how they modified their random in Civilization Revolution. One of the things they did is that if a unit has 90% chance to win, this is treated as 100%, because it turned out that it always felt unreasonable to lose when the win chance was 90%. Another trick they did is that if a number of 50% battles in a row are all lost, then at some point the next one is automatically won, because it simply feels unfair to lose so often in a row.
Of course, with a mechanic like that it is important to either keep players from knowing it works like that, or to keep a bit of random in there, because otherwise knowing how this works will result in exploitable tactics. If there is one thing we learned during development of Awesomenauts and Swords & Soldiers, it is that if something can be abused, it always will!
Inspired by the Civilization article, we started playing with Leon's crit chance. The original implementation we had for this was simply a clean chance. However, this sometimes resulted in having two or three crits in a row, which is incredibly powerful. Dying from this felt unfair, because it did so much damage that there was little opportunity to get away from it.
To solve this we modified the crit chance to never happen twice in a row, and also to never fail ten times in a row. This way the crits are still random, but the situations that feel unfair don't happen anymore.
This worked fine and had the desired result, but what we did not realise is that it massively changes the actual crit chance! With a 15% crit chance, the chance of it missing 10 times in a row is a surprisingly high 19.7%. Adding a guaranteed extra crit so often increases the crit chance significantly! So in that particular patch we accidentally buffed Leon a lot, making him pretty overpowered.
We did not realise we had done this until players started counting. They quickly noticed that Leon had become a lot stronger, but we simply didn't believe them. To convince us that something was wrong, a couple of players performed several hundred attacks in a row and counted how many crits they got. Even then we still didn't believe them at first, but once we understood what was happening here, we modified the crit chances to be the same as before again, including the extra rules for consecutive hits.
Image by Awesomenauts player 'Offline' (I think).
This brings me to the end of this blogpost. As the various topics I have discussed today have shown, there is much more to chance, luck and random in games than just using pure random functions. Whenever random is involved, the designer and programmer should always consider what they want exactly, since in practice it turns out that a true random is quite rarely wanted.
When a programmer is told to implement a random for something, he will often simply use std::rand() or something similar and that's it. However, in practice it turns out that both players and designers rarely really want a truly random chance.
A good example of this can be found in level selection in Awesomenauts: in a practice match there are four levels to chose from, and there is a 'random' button to select random levels. With a true random, the random button might result in playing the same level several times in a row. There is a small chance that a player gets the same level 4 times in a row (1.5625%, to be exact), and even 10 times the same level in a row is possible. When this happens, both players and designers start to voice complaints like “the random is not random, it always selects the same map”. Of course, in a truly random dice roll, the chances of throwing a number are completely independent of what was thrown before that, so even a million times the same value in a row can happen. The chance of that happening is just really small.
So what every game programmer needs to realise, is that when a designer asks for random, he rarely asks for a complete random. The random usually needs some extra logic to keep unwanted situations from happening. In Awesomenauts, the game remembers which map you last played and does not select it again for the next match (although if you join matches, you might still join several matches in a row with the same map). Similarly the music player remembers the last four songs it played and does not select those again when it chooses the next song to play. And I imagine that in a random dungeon generator it is likely not desirable to put instances of the same room next to each other.
An important part of random is human perception. Humans are exceptionally good at recognising patterns. In fact, humans are so good at this that they often recognise patterns where there are none. Noteworthy situations are remembered, while other situations are not. Astrology is for a large part based on this. If only one out of ten predictions turns out to be correct, many people will tend to remember that one correct prediction and not the nine false ones, making it possible for charlatans to pass for real predictors of the future.
Something similar goes for the perception of random in games. We have had numerous reports from players that the map they dislike most is selected much more often by the random map selection. However, every player who reports this reports it for a different map, and our random is just fine. What really happens there, is that people remember every time they get the map they don't want, and don't really remember what other maps they played recently.
Human psychology is therefore an important part in designing random. A great example of this can be found in one of my favourite game series: Civilization. Sid Meier did a hilarious presentation on how they modified their random in Civilization Revolution. One of the things they did is that if a unit has 90% chance to win, this is treated as 100%, because it turned out that it always felt unreasonable to lose when the win chance was 90%. Another trick they did is that if a number of 50% battles in a row are all lost, then at some point the next one is automatically won, because it simply feels unfair to lose so often in a row.
Of course, with a mechanic like that it is important to either keep players from knowing it works like that, or to keep a bit of random in there, because otherwise knowing how this works will result in exploitable tactics. If there is one thing we learned during development of Awesomenauts and Swords & Soldiers, it is that if something can be abused, it always will!
Inspired by the Civilization article, we started playing with Leon's crit chance. The original implementation we had for this was simply a clean chance. However, this sometimes resulted in having two or three crits in a row, which is incredibly powerful. Dying from this felt unfair, because it did so much damage that there was little opportunity to get away from it.
To solve this we modified the crit chance to never happen twice in a row, and also to never fail ten times in a row. This way the crits are still random, but the situations that feel unfair don't happen anymore.
This worked fine and had the desired result, but what we did not realise is that it massively changes the actual crit chance! With a 15% crit chance, the chance of it missing 10 times in a row is a surprisingly high 19.7%. Adding a guaranteed extra crit so often increases the crit chance significantly! So in that particular patch we accidentally buffed Leon a lot, making him pretty overpowered.
We did not realise we had done this until players started counting. They quickly noticed that Leon had become a lot stronger, but we simply didn't believe them. To convince us that something was wrong, a couple of players performed several hundred attacks in a row and counted how many crits they got. Even then we still didn't believe them at first, but once we understood what was happening here, we modified the crit chances to be the same as before again, including the extra rules for consecutive hits.
Image by Awesomenauts player 'Offline' (I think).
This brings me to the end of this blogpost. As the various topics I have discussed today have shown, there is much more to chance, luck and random in games than just using pure random functions. Whenever random is involved, the designer and programmer should always consider what they want exactly, since in practice it turns out that a true random is quite rarely wanted.
Friday, 10 January 2014
The surprisingly many subtleties of designing crits
Crits (or "critical hits") seem like a pretty straightforward topic: there is a certain chance that you deal more damage, and that's about it. It is easy enough to calculate average damage and balance based on that, and beyond that it seems like just a choice whether your game has weapons with crits or not. However, in practice it turns out that there are a lot of subtleties to crits that are not immediately apparent. In Awesomenauts we have had various implementations of crits and each time more subtleties surfaced. So today I would like to talk about their many aspects.
Crits can be applied to any weapon or attack and their core element is chance: there is a small chance that a weapon does extra damage. Usually this chance is low, below 30%. Instead of damage, the crit might also add other effects, like a stun or knockback. Crits are mostly seen in RPGs, where they are a nice way of spicing up the weapon variation. A sword that does 10 damage feels quite different from a sword that does 7 damage and has a 25% chance of doing 19 damage. The average damage output of these two swords is the same, but they feel and play very differently. Crits also add extra variation to levelling and upgrading: one can upgrade not only the base damage, but also the crit damage and the crit chance.
Crits are often a fun and interesting mechanic. While a normal sword always has the same result and is thus entirely predictable, a sword with a crit chance makes the player curious for every next hit. “Will it crit?! I hope it will crit!”
Adding chance to a game adds unpredictability. Having a lot of unpredictability greatly changes the feel of a game. A great example of this is Mario Kart. In Mario Kart when you grab a pick-up you usually get a random item and the effectiveness of the items varies massively. Picking up the right item at the right time often makes the difference between winning or losing the game. This is great for beginning players: even if they are playing against much better opponents, there is still a chance that the beginner might win because she has some lucky pick-ups. This makes a world of difference in comparison to purely skill-based games, in which the beginner will basically always lose until she becomes as good as her opponents.
Luck also has a soothing effect. If you lose because your opponent was better, then the player can do little but blame her own lack of skill. This can be frustrating. On the other hand, if luck is involved, the player can always blame that, which is much easier to accept. Most losing players need scapegoats to be able to cope with their own failure. Luck is a great scapegoat, just like the referee's mistakes are in soccer.
Knowing that players need a scapegoat actually helps in understanding player feedback. Any competitive online game has players complaining about balance, and as a developer it is important to realise that part of that is completely unrelated to the actual balance, and is just players needing a scapegoat to be able to accept losing that match. (By which I am of course not suggesting that balance in Awesomenauts is perfect. I am merely saying that a part of balance complaints are caused by players needing a scapegoat instead of actual problems in the balance.)
The above text may make it sound like crits only have benefits, but this is absolutely not the case. The big problem with crits and luck in general is that they take away part of the competitive nature of a game. If a player invests hundreds of hours into becoming highly skilled at a game, then losing a match because the opponent got lucky just plain sucks. Competitive players generally hate luck with a passion. This is the reason why in the end, we removed Leon's crits and turned them into a completely predictable consecutive-hit system.
Bluntly summarising today's blogpost, one could claim that crits and luck make a game more casual, while complete predictability make it more hardcore and competitive.
The title of this blogpost said I was going to talk about the many subtleties of crits, but it turns out that there are so many that I cannot cramp it all into one single blogpost. Therefore I will come back to this topic next week and talk about how luck in games is rarely just a roll of the dice, and how modifying crit chances in the wrong way can be painfully destructive to balance.
Edit: Here's a link to that next blogpost: How chance in games is rarely just a roll of the dice
Crits can be applied to any weapon or attack and their core element is chance: there is a small chance that a weapon does extra damage. Usually this chance is low, below 30%. Instead of damage, the crit might also add other effects, like a stun or knockback. Crits are mostly seen in RPGs, where they are a nice way of spicing up the weapon variation. A sword that does 10 damage feels quite different from a sword that does 7 damage and has a 25% chance of doing 19 damage. The average damage output of these two swords is the same, but they feel and play very differently. Crits also add extra variation to levelling and upgrading: one can upgrade not only the base damage, but also the crit damage and the crit chance.
Crits are often a fun and interesting mechanic. While a normal sword always has the same result and is thus entirely predictable, a sword with a crit chance makes the player curious for every next hit. “Will it crit?! I hope it will crit!”
Adding chance to a game adds unpredictability. Having a lot of unpredictability greatly changes the feel of a game. A great example of this is Mario Kart. In Mario Kart when you grab a pick-up you usually get a random item and the effectiveness of the items varies massively. Picking up the right item at the right time often makes the difference between winning or losing the game. This is great for beginning players: even if they are playing against much better opponents, there is still a chance that the beginner might win because she has some lucky pick-ups. This makes a world of difference in comparison to purely skill-based games, in which the beginner will basically always lose until she becomes as good as her opponents.
Luck also has a soothing effect. If you lose because your opponent was better, then the player can do little but blame her own lack of skill. This can be frustrating. On the other hand, if luck is involved, the player can always blame that, which is much easier to accept. Most losing players need scapegoats to be able to cope with their own failure. Luck is a great scapegoat, just like the referee's mistakes are in soccer.
Knowing that players need a scapegoat actually helps in understanding player feedback. Any competitive online game has players complaining about balance, and as a developer it is important to realise that part of that is completely unrelated to the actual balance, and is just players needing a scapegoat to be able to accept losing that match. (By which I am of course not suggesting that balance in Awesomenauts is perfect. I am merely saying that a part of balance complaints are caused by players needing a scapegoat instead of actual problems in the balance.)
The above text may make it sound like crits only have benefits, but this is absolutely not the case. The big problem with crits and luck in general is that they take away part of the competitive nature of a game. If a player invests hundreds of hours into becoming highly skilled at a game, then losing a match because the opponent got lucky just plain sucks. Competitive players generally hate luck with a passion. This is the reason why in the end, we removed Leon's crits and turned them into a completely predictable consecutive-hit system.
Bluntly summarising today's blogpost, one could claim that crits and luck make a game more casual, while complete predictability make it more hardcore and competitive.
The title of this blogpost said I was going to talk about the many subtleties of crits, but it turns out that there are so many that I cannot cramp it all into one single blogpost. Therefore I will come back to this topic next week and talk about how luck in games is rarely just a roll of the dice, and how modifying crit chances in the wrong way can be painfully destructive to balance.
Edit: Here's a link to that next blogpost: How chance in games is rarely just a roll of the dice
Saturday, 4 January 2014
Bitcrunching numbers for bandwidth optimisation
An important part of making a complex multiplayer game like Awesomenauts is optimising bandwidth usage. Without optimisations, just sending position updates for one character to one other player already costs 240 bytes per second (30 updates per second at 4 bytes each for both X and Y). And this is just a fraction of what we need to send: Awesomenauts can have up to 35 characters running around the game world, and we need to synchronise much more than just their positions. As you can see, bandwidth usage quickly spirals out of control, so we need to be smarter than just naively sending all the data all the time.
In the many months we spent on bandwidth optimisation, we employed lots of different techniques. The one I would like to talk about today can massively decrease bandwidth by crunching numbers on the level of individual bits. This technique not only worked great for the multiplayer implementation of Awesomenauts, but I am now also using this extensively for reducing the size of our replays.
Let's have a look at the core idea here. Storing one unsigned int normally costs 32 bits. In this we can store a value in the range of 0 to over 4 billion. However, we rarely need a range as big as that. For example, the amount of Solar you collect during an Awesomenauts match rarely goes over 5,000. In a really long match we might go past that, so let's bet on the safe side and say we want to store Solar values up to 10,000. This could be stored in 14 bits, since 2^14 = 16,384. That is less than half of the normal 32 bits used for a value like this!
Using too many bits is not a problem in memory since modern computers have plenty of that. But when sending the Solar over the network, we would like to use only those 14 bits we actually need. However, normally we can only handle data on a byte level, not on a bit level, so the best we can easily do is to store this in two bytes (16 bits).
What we need here to solve this is a bitstream, something that we can use to group together a lot of values on a bit-level. This way we can just grab a block of memory and push in all the values we want to send over the network. The idea is quite straightforward, so let's jump right in and have a look at how one could use that:
The details of actually implementing the bitstreams itself is too much for this blogpost, so I will leave that as a fun exercise for the reader. Hint: you can use bitwise operators like |, ‹‹, &.
When looking at this code there are a number of observations to make. First is that I am actually using only 31 bits while storing them in 4 bytes. In other words: I am wasting 1 bit here! This is something that almost always happens when using bits: at some point they need to turn into bytes, since that is what network packets are measured in, and we usually waste a couple of bits there. This loss can be decreased by using larger bitstreams with many more values in them.
A much more important realisation here is how easy it is to break this code. If I read back the solar with the wrong number of bits, then not only will the solar be complete garbage, but also all values after it. The same happens if I accidentally read back values in the wrong order, or forget to read one. The compiler will not warn you of any of these things and debugging on a bit-level is difficult, so this produces extremely stubborn bugs! Another easy mistake is to use too few bytes for the data, resulting in buffer overflows and thus memory corruption, or to use too many bytes, resulting in wasting bandwidth. In short: use bitstreams only when you really need them and be extremely careful around them!
So far we have only stored the easy kinds of data: bools and unsigned ints translate very easily to bits. However, many things in a game are floats. When using an unsigned int, taking just its first 10 bits results in a valid number, just with a smaller range. But with a float taking those same 10 bits results in nonsense. How can we send floats with arbitrary numbers of bits?
The trick here is rather simple. A float has a number of properties that we usually don't need. Floats have very high precision near 0, but when storing a position, we want the same precision everywhere. Floats also have an extremely large range, but for that same position we already know the size of the world and we don't need to be able to store values outside that range.
Knowing these things we can store positions (and most other floats) in a different way. Levels in Awesomenauts happen to all be in the range -10.0 to 30.0. Knowing this we can just store the x-position as an unsigned int with an arbitrary number of bits. If we want to store a position in 8 bits, then the lowest value (0) means position -10.0, and the highest value (255) means position 30.0. Values in between are just interpolated. This is a simple and very effective trick. Depending on how much precision we need, we can use more or less bits.
When synching over the network we actually don't need all that much precision, since the receiver is not using the position for a physics simulation. He mostly just uses it for visualisation and checking whether a bullet hits. So in Awesomenauts we use 13 bits for synching the x-position and another 13 bits for the y-position, which is a lot less than the normal 32 bits each would need as a float!
The same idea can be applied to many other things that are also floats and for which we know the range. Some things even need extremely little precision. For example, the health of other players is only visualised in their healthbar, so we only need to synch as many bits of that as are needed to make the healthbar look correct. Since healthbars are usually in the order of 100 pixels wide, we can store health in 7 bits, even if the actual range of the health might be 0.0 to 2000.0. With 7 bits our for that range would be only 15.625, but more isn't needed because the difference is not visible in the healthbar anyway. The simulation is not influenced at all by this, since the owner of the character still stores the full precision float for the health.
Using tricks like these we managed to get the bandwidth usage of Awesomenauts down to around 16 kilobyte per second. This is the total upload needed at the player who manages bots and turrets. The five other players in a match don't manage those bots and turrets, so they use far less bandwidth still. For comparison: our very first network implementation in the Awesomenauts prototype we had four years ago used around 50x (!) more bandwidth. Bit-crunching numbers as explained in this blogpost is just one of the many tricks we used to decrease bandwidth, but it was definitely one of the strongest!
In the many months we spent on bandwidth optimisation, we employed lots of different techniques. The one I would like to talk about today can massively decrease bandwidth by crunching numbers on the level of individual bits. This technique not only worked great for the multiplayer implementation of Awesomenauts, but I am now also using this extensively for reducing the size of our replays.
Let's have a look at the core idea here. Storing one unsigned int normally costs 32 bits. In this we can store a value in the range of 0 to over 4 billion. However, we rarely need a range as big as that. For example, the amount of Solar you collect during an Awesomenauts match rarely goes over 5,000. In a really long match we might go past that, so let's bet on the safe side and say we want to store Solar values up to 10,000. This could be stored in 14 bits, since 2^14 = 16,384. That is less than half of the normal 32 bits used for a value like this!
Using too many bits is not a problem in memory since modern computers have plenty of that. But when sending the Solar over the network, we would like to use only those 14 bits we actually need. However, normally we can only handle data on a byte level, not on a bit level, so the best we can easily do is to store this in two bytes (16 bits).
What we need here to solve this is a bitstream, something that we can use to group together a lot of values on a bit-level. This way we can just grab a block of memory and push in all the values we want to send over the network. The idea is quite straightforward, so let's jump right in and have a look at how one could use that:
//Class for pushing bits into a block of memory class BitInStream { public: BitInStream(char* data); void addUnsignedInt(unsigned int value, unsigned int numBits); void addBool(bool value); }; //Class for reading bits from a block of memory class BitOutStream { public: BitOutStream(const char* data); unsigned int getUnsignedInt(unsigned int numBits) const; bool getBool() const; }; //Example of using these void main() { //The bits are stored in this char data[4]; //Example of writing some values BitInStream bitInStream(data); bitInStream.addUnsignedInt(solar, 14); bitInStream.addUnsignedInt(itemsBought, 6); bitInStream.addBool(isAlive); bitInStream.addUnsignedInt(damageDone, 10); //Read back values BitOutStream bitOutStream(data); solar = bitOutStream.getUnsignedInt(14); itemsBought = bitOutStream.getUnsignedInt(6); isAlive = bitOutStream.getBool(); damageDone = bitOutStream.getUnsignedInt(10); } |
The details of actually implementing the bitstreams itself is too much for this blogpost, so I will leave that as a fun exercise for the reader. Hint: you can use bitwise operators like |, ‹‹, &.
When looking at this code there are a number of observations to make. First is that I am actually using only 31 bits while storing them in 4 bytes. In other words: I am wasting 1 bit here! This is something that almost always happens when using bits: at some point they need to turn into bytes, since that is what network packets are measured in, and we usually waste a couple of bits there. This loss can be decreased by using larger bitstreams with many more values in them.
A much more important realisation here is how easy it is to break this code. If I read back the solar with the wrong number of bits, then not only will the solar be complete garbage, but also all values after it. The same happens if I accidentally read back values in the wrong order, or forget to read one. The compiler will not warn you of any of these things and debugging on a bit-level is difficult, so this produces extremely stubborn bugs! Another easy mistake is to use too few bytes for the data, resulting in buffer overflows and thus memory corruption, or to use too many bytes, resulting in wasting bandwidth. In short: use bitstreams only when you really need them and be extremely careful around them!
So far we have only stored the easy kinds of data: bools and unsigned ints translate very easily to bits. However, many things in a game are floats. When using an unsigned int, taking just its first 10 bits results in a valid number, just with a smaller range. But with a float taking those same 10 bits results in nonsense. How can we send floats with arbitrary numbers of bits?
The trick here is rather simple. A float has a number of properties that we usually don't need. Floats have very high precision near 0, but when storing a position, we want the same precision everywhere. Floats also have an extremely large range, but for that same position we already know the size of the world and we don't need to be able to store values outside that range.
Knowing these things we can store positions (and most other floats) in a different way. Levels in Awesomenauts happen to all be in the range -10.0 to 30.0. Knowing this we can just store the x-position as an unsigned int with an arbitrary number of bits. If we want to store a position in 8 bits, then the lowest value (0) means position -10.0, and the highest value (255) means position 30.0. Values in between are just interpolated. This is a simple and very effective trick. Depending on how much precision we need, we can use more or less bits.
When synching over the network we actually don't need all that much precision, since the receiver is not using the position for a physics simulation. He mostly just uses it for visualisation and checking whether a bullet hits. So in Awesomenauts we use 13 bits for synching the x-position and another 13 bits for the y-position, which is a lot less than the normal 32 bits each would need as a float!
The same idea can be applied to many other things that are also floats and for which we know the range. Some things even need extremely little precision. For example, the health of other players is only visualised in their healthbar, so we only need to synch as many bits of that as are needed to make the healthbar look correct. Since healthbars are usually in the order of 100 pixels wide, we can store health in 7 bits, even if the actual range of the health might be 0.0 to 2000.0. With 7 bits our for that range would be only 15.625, but more isn't needed because the difference is not visible in the healthbar anyway. The simulation is not influenced at all by this, since the owner of the character still stores the full precision float for the health.
Using tricks like these we managed to get the bandwidth usage of Awesomenauts down to around 16 kilobyte per second. This is the total upload needed at the player who manages bots and turrets. The five other players in a match don't manage those bots and turrets, so they use far less bandwidth still. For comparison: our very first network implementation in the Awesomenauts prototype we had four years ago used around 50x (!) more bandwidth. Bit-crunching numbers as explained in this blogpost is just one of the many tricks we used to decrease bandwidth, but it was definitely one of the strongest!
Subscribe to:
Posts (Atom)