Let's get physical!!! ;)

Got yah!! :rofl2: This is actually going to be about my homebrew game engine that needs a massive re-factoring and redesign before I will be losing my mind!!!! I will try to clear my thoughts and combine what I have been reading the past weeks about "how to write a platform game" and best practices in this blog post. It might get long, boring, confusing, sarcastic, funny or very technical this time but I really need to get it out of my head....sorry! :D

So physics!!! We all know about gravity right?? The force keeping us and everything around us on the floor instead of floating around the rooms in your house and stuff? No idea actually how this work in the real world though :lol:...but how is this black magic sorcery trick done in classic platform games? Simple, it's all about math ofcourse! :P The number one thing in school (among many) I was terrible at but luckily I only need to tell my code on "how" to do the calculations and I do not have to do it all from my head...thankfully!!! Yeah, this was about the best of my efforts to bring in some humor into this stuff. The rest is going to be pretty much no fun at all.....:D

When you as the player control the protagonist of the game you assume the floors, walls and other obstacles to be solid. And when you jump into the air you naturally come down again to the first solid object below your feet...right? As a casual gamer you would be right this is how things "just work" when you play any type of game. Now here is a shocking secret about video games....every single thing on your screen is actually NOT solid at all!!! At least not without a lot of work to figure out where in the game world you are. Is there a floor below you? Did you just walk into a wall? Did an enemy attack you? Or did you just collected an item? And that needs to happen every...single....frame!! Now ofcourse I could just adopt a physics library but what is the fun in that ^_^

I have explained before how I have extended my level browser demo into a monstrosity of spaghetti code that tries to pass for "collision detection" into the graphics rendering code. The number one thing that I have read just about everywhere in all the tutorials is...do not mix drawing, collision detection, and sound code!! And that is just what I have done in the worst possible way and now it's grown into a monster I can no longer maintain. I mean...I probably could maintain it...but I don't want to continue doing it the wrong way!! So I have been trying to figure out the best way to rewrite smaller parts and why my code is so bad to begin with...and then I noticed the problem! Nearly all of my drawing code is based on how the levels are stored in the data files! Partly because I thought it was ingenious how they manged to save disk space and have such a logical structuring....at least for the time...which was 1995 or something like that. While my biggest issue in the game engine is not with the tilemap I just really want to explain how it works..at least a little! :D

The BatteryCheck level "binnen.j2l" is 355x123 tiles in size for the main three layers and it has a few smaller ones for the background and parallax layers (which do not work yet in my previews). Every tile is referenced by a two byte integer that also specifies the orientation of the tile which means we need more than 87,330 bytes to store each layer, meaning 261,990 bytes for just the three main layers and there are five more! To save space the Jazz2 file formats make extensive use of zlib compression already but they have used an even more cleaver trick to reduce the amount of storage required. By creating a dictionary of unique combinations of four tiles (called a "tile group" in the documentation I found) only 2 bytes are required to refer to the dictionary entry instead of 8 bytes required for the individual tiles we save space! Or they did I guess. With this trick only about 22kb are required per layer resulting in just 66kb for the three main layers. Pretty neat right....if you understand what I am talking about that is :lol: (read all about it here).

So after I had finally figured this stuff out I tried drawing the tiles using this dictionary lookup directly as it's used in the data files and never really looked back! So you might be wondering how the solid objects like the floor's and walls are defined in the level layers. Surprise!...They're not!!!:huh: It's actually the tileset with the graphics that has a second imagemask for each tile! Actually they used another trick to save memory by reusing the same tilemasks but that is another story I guess! This allows pixel perfect collision detection to be implemented into the game but the only way I could do it would be far to slow, so I cheated a little in my v0.2 preview by just having a "solid" tile or not and checking if the tile below the player is solid or not. I have written a fast pixel perfect solid detection function but again...I cheated by defining the solid parts of a tile not with actual pixels...but with math! It's difficult to explain well but I noticed how the solid parts were mostly rectangular and on the edges so the code below is how I implemented it:
Code:
          switch(_map_) {
            case 0x01:  if(rx<=5)                  {solid = true;}  break;
            case 0x02:  if(ry>=25)                 {solid = true;}  break;
            case 0x03:  if(rx<=5 &&ry>=24)         {solid = true;}  break;
            case 0x04:  if( (rx<=5) || (ry>=25) )  {solid = true;}  break;
            //////////////////////////////////////////////////////////////
            case 0x06:  if(rx<=4 &&ry>=24)         {solid = true;}  break;
            case 0x07:  if(rx<=5)                  {solid = true;}  break;
            case 0x08:  if( (rx<=5) || (ry>=25) )  {solid = true;}  break;
            case 0x09:  if(rx<=5)                  {solid = true;}  break;
            case 0x0A:  if( (rx==0) || (ry>=25) )  {solid = true;}  break;
            case 0x0B:  if(ry>=25)                 {solid = true;}  break;
            case 0x0C:  if(rx<=5)                  {solid = true;}  break;
            case 0x0D:  if( (rx<=6) || (ry<=10) )  {solid = true;}  break;
            case 0x0E:  if(ry<=9)                  {solid = true;}  break;
            case 0x0F:  if(rx<=3 &&ry>=25)         {solid = true;}  break;
            case 0x10:  if(ry<=10)                 {solid = true;}  break;
            case 0x11:  if( (rx>=21) || (ry>=26) ) {solid = true;}  break;
            case 0x12:  if(rx>=21)                 {solid = true;}  break;
            case 0x13:  if( (rx>=21) || (ry<=19) ) {solid = true;}  break;
            case 0x14:  if(ry<=18)                 {solid = true;}  break;
           
            //case 0x15:  if(ry>=25)                 {solid = true;}  break;
            //case 0x16:  if(ry>=25)                 {solid = true;}  break;
            case 0x17:  if(ry>=25 && ry<=26)       {solid = true;}  break;
           
            case 0x18:  if(ry>=25)                 {solid = true;}  break;
            case 0x19:  if(ry>=25)                 {solid = true;}  break;
            case 0xFF:  solid = true;  break;
              default:  solid = false; break;
          }
During loading of the tileset the maskaddress assigned to a tile is translated into the magic numbers in each of these case statements.
Code:
          //Primitive collision map generation
          colmap = 0x00;
          switch(J2T.tileset->MaskAddress[idx]) {
            case 0x000000: colmap = 0x00 ; break;
            case 0x000080: colmap = 0x01 ; break;
            case 0x000100: colmap = 0x02 ; break;
            case 0x000180: colmap = 0x03 ; break;
            case 0x000200: colmap = 0x04 ; break;
            case 0x000280: colmap = 0xFF ; break;
            case 0x000300: colmap = 0x06 ; break;
            case 0x000380: colmap = 0x07 ; break;
            case 0x000400: colmap = 0x08 ; break;
            case 0x000480: colmap = 0x09 ; break;
            case 0x000500: colmap = 0x0A ; break;
            case 0x000580: colmap = 0x0B ; break;
            case 0x000600: colmap = 0x0C ; break;
            case 0x000680: colmap = 0x0D ; break;
            case 0x000700: colmap = 0x0E ; break;
            case 0x000780: colmap = 0x0F ; break;
            case 0x000800: colmap = 0x10 ; break;
            case 0x000880: colmap = 0x11 ; break;
            case 0x000900: colmap = 0x12 ; break;
            case 0x000980: colmap = 0x13 ; break;
            case 0x000a00: colmap = 0x14 ; break;
            case 0x000a80: colmap = 0x15 ; break; //-----------------
            case 0x000b00: colmap = 0x16 ; break; //-----------------
            case 0x000b80: colmap = 0x17 ; break; //-----------------
            case 0x000c00: colmap = 0x18 ; break;
            case 0x000c80: colmap = 0x19 ; break; //-----------------
        }
Using these replacement numbers and fixed calculations instead of the actual masks makes my whole collision detection code very static and tied to this single tileset! Meaning it would be impossible..or a lot of work...to add a second tileset. And eliminate the possibility of ever using a third or fourth tileset without another set of translation codes again! This is not acceptable for the long term and I need to change that really soon if I ever want to support other tilesets than just the one I have now! And this is only to make the basic wall and floor appear solid...this does not even take into account how the objects like: Batteries, Rafts, Belts, etc are handled and checked for solid parts! The simple explanation would be that objects also have a mask image to indicate what is solid or not....and I don't make use of that at all!! The only reason for not using those is that my current collision detection is using the bad coding example above...and it gets even worse!! :shy::cry::sad:

While drawing layer 3 and 4 an array called the "solid map" is build which is just a little more than the visible area on screen. So to make use off this array it needs a lot of calculations to get the positions right and compensate for the camera offset, and this is part of the reason why at the edges of the map the collision detection get's confused and even worse than it is normally. This already bad method to detect collisions got even worse when I included the objects into the same "solid_map" array using a different ID ofcourse than the floors or walls I can kind of detect batteries, extra life's and all other interactive things that got solid in my latest release. Remember the bug causing allowing you to collect a single battery multiple times at certain places? Errors in the calculations in the offsets of the player's position, the start of the solid map and a few other coordinates translating back into the "Event map" which I still need to explain is why that is possible!

The objects you can collect and interact with are called events and are loaded into the event map. Each event is four bytes long and could be considered an extra layer the same size as layer 3 and 4 (355x123) where each entry points to a single tile position and the total size is 174,660 bytes for this level. Not every tile has an event ofcourse and for those all four bytes have a value of 0x00 but it still wastes unnecessary space. To see how much RAM is wasted I have counted the events and there are only 459 in total. I have not decided how to implement a solution for this yet but if I would just store the X and Y in two 16 bit integers and a copy of the 4 bytes from the event map I would need just 8 bytes per event. Unless I have miscalculated (8*459) I would only need 3,672 bytes to store every single object in the event map!! That is almost 98% less memory required!!!!! If I would add a width and height for the object into the new structure I might need an additional two bytes...so 10 bytes...that's easy: 4590 bytes!! Still 97% less memory than the full event map!!! Ofcourse most of the current platforms have more than enough RAM to not even care about memory usage that much, but my future plans of also supporting more limited systems will benefit if I can pull this optimization off at the same time of fixing the collision detection.

Did I say already this was going to be long, boring, and confusing? If you're still reading along here...wow! Congratulations on your patience...and thank you for your interest. Please bare with me...it's almost over. Really! :lol:

Looking back on my novel above here I think the short version would be that my whole game engine is based far to much on the Jazz2 level file format. And nothing from the tutorials and best practices I have read used the method I have, and combining rendering code with game logic is considered a bad thing. In theory and thinking way out of the box...game logic could run on one machine while the rendering happens on another....which is useful in case anyone would be interested in some online multiplayer action at some point.:lol:

What I have been trying to design the last few days is a new set of classes that will be much more structured and optimized to be more flexible. My game engine should only provide the very most basic event / object loading and the game using it should implement derived classes that provide the behavior for an object. The most simple event would be the start position of the player when the level loads...but it's not really an event actually. It should just set some coordinates in the world class. The battery would be a better example since that has an animation sequence and when collected by the player it needs to play a sound and remove itself from the map. A recharge gate has animations on different sprite layers and fills up the health bar. It also triggers the players animation and all of this should not be part of the game engine library....which it is right now. I need to move that code out of the game engine and into the specific "batterycheck" part of it. Nothing the player of my game(s) would notice but it makes using the engine for "Jazz2" and "SuperTux" a lot easier in the future. :D

This blog got far longer than I expected but I hope anyone finds it interesting to read how I did things and why. I have not gone that much into detail as I planned originally about how I had implemented the physics of my engine. Guess that will have to wait for some other time ^_^

Thank you for your time. :D
  • Like
Reactions: 4 people

Comments

This is interesting and not a bit boring. If many people nowadays have the tl;dr problem, this is not your fault. I love anything about clever ways of saving disk space (as well as RAM) and did not even know such tricks were used in Jazz2 (which I considered to be a more modern program as it uses quite some space on HDD compared to games on cartridge based consoles of that time).

Thank you for the “novel” and keep up the detailed explanations.
 
  • Like
Reactions: 2 people
Mate, I have zero coding knowledge but I've had a good read. I want to congratulate you on your progress. As with any hobby, half the fun is in the battle of getting things right! Best of luck for the future.
 
  • Like
Reactions: 3 people
Thanks, I am not such a quick writer so this entire post took me about 3-4 hours to write.:shy: It was mostly to clear my mind and allow me to read back later...but I am glad you found it interesting to read and learned something from it!:D

Compared to most games from that time Jazz2 is indeed quite large! But not many games had that much levels, tilesets, music, animations, soundeffects and even video's back than either. ^_^ I have to admit though I have not played Jazz2 all that much and only bought it on GOG when I started this project.:D

Maybe for my next blog post I will go into more detail s on how I am implementing the new and improved objects/events. Or just write smaller ones more often :rofl2:
 
  • Like
Reactions: 1 person

Blog entry information

Author
Archerite
Views
322
Comments
6
Last update

More entries in Personal Blogs

More entries from Archerite