Wednesday, January 4, 2012

Super Sprint Intricate Design Details (Under The Hood)

Hello, Happy 2012 everyone...now to get to something I put off for too long.  These are some intricate details on the Super Sprint game which you can download HERE.  I'll try to explain everything the best I can.

Track Design:

There are seven tracks in Version 1.0.  This is one of them--Assets/Art/track6.png.  There are two layers of track detail--top level and bottom level.  The pavement/beach is the bottom level.  The palm trees/umbrellas are the top level which is rendered over the bottom level.  The top level has the liberty to be transparent/translucent as you can see here:


 The final product turns out to be something like this:


Track Entities:

Well, this makes sense.  How about bridges and stuff?  How about game logic in general?  Each track has a system of lines/checkpoints in place on a 1024x768 grid.  When a car crosses one of these, something happens.  NOTE: These things are direction-sensitive--you must cross it going a certain direction (north, south, east, west) for it to count.  If you travel through it in the reverse direction, the opposite effect happens.  Here's an example of Track 7:


Blue: Finish Line.  Everyone knows what this does--cross the finish line traveling west to increment the lap counter.  Cross it going east to decrement the counter (you don't want to do this).  This also dictates where the cars initially line up and where the waving flag appears.

Green: Checkpoint.  There's one per track and it helps to enforce crossroads.  You MUST cross this line driving north like normal.  When you do, it sets a car's checkpoint Boolean to True.  Cross it going south to set it to False, therefore forcing you to go through the whole loop.  If a car crosses the finish line going west and its checkpoint boolean is set to True, then increment the lap like normal.  Otherwise, you didn't complete the lap and don't get any credit.  Haha, so don't take shortcuts.

Yellow: Bridge.  Each car has another Boolean that dictates whether or not the car is on the top level.  Drive through the lines to enter/exit the top-most state.  If the car's top level Boolean is true, the car is rendered on top of the bridge.  Otherwise, it is rendered below the bridge.

To create this instance of depth (stuff on top of each other), game objects are rendered in a certain order--the bottom-most being rendered first:  This is the order from top to bottom:

1. HUD interface stuff, I love my millisecond counter.
2. Tornado.
3. Car smoke (when they bounce off walls).
4. Cars on top-level.*
5. Top-level track BG.
6. Cars on bottom level.*
7. Wrenches, oil slicks, water.
8. Bottom-level track BG.

* The order in which the cars are rendered amongst each other is determined by their speed...if two cars are on the same level, the faster car will be rendered above the slower one.

Purple: AI Waypoint.  Basically, the direction that the car is being guided.  I'll talk about this later.

Not included are the water, oil, and wrench coordinates.  There's eight possible wrench coordinates and eight possible water/oil coordinates.  Basically, when the game loads a new track, the water and oil positions are defined to one of these eight coordinates.  The wrench position is also redefined whenever one is called during the second, fourth, and fifth (8-player only) laps of the race.

All of these entities are defined in Assets\Scripts\track#.txt, with the # being the current track number.  Obviously, I'm able to edit this outside of the compiler though it needs some heavy debugging to make sure everything's in the right place.

There's one catch though--at the moment, the lines/checkpoints MUST BE perfectly horizontal or vertical.  What the game does is check if the car's current and previous positions intersect one of the lines and reacts accordingly.  It's easier for me and the program to check a car crossing a horizontal or vertical line (eliminating the need for fancy math) than it is a diagonal one.

Bump Maps:

Car collision was rather hard to figure out but it's user-friendly once you figure out how to set up your track png's.


This is the track stripped down to its four elements.  Black (0,0,0) is a wall on both top and bottom levels.  Pink (255,0,255) is a wall for top level only (bridge fence).  Blue (0,0,255) is a wall for bottom level only (tunnel wall).  And every other color is pavement that the car can drive on so no problems there.  I have an additional program that reads in .bmp files like this one and converts it to the collision map you see in the game.  These binary files are Assets/Scripts/track#_1024_768_collision.dat and basically tells the game where the car can and cannot drive.  This is just an example of the text inside:

? 1¿ 0? 1¿ 0? 1¿ 0+ 1ç 0 1ç 0 1ç 0 1ç 0 1ÿ 0ÿ 1ÿ 0ÿ 1ÿ 0ÿ 1ÿ 0÷ 1 0ï 1 0ï 1 0ï 1 0ë 1 0ç 1 0ç 1 0ç 1 0ß 1' 0× 1' 0× 1' 0× 1' 0Ó 1/ 0Ï 1/ 0Ï 1/ 0Ï 1/ 0Ë 17 0Ç 17 0Ç 17 0Ç 17 0à 1? 0¿ 1? 0¿ 1? 0¿ 1? 0» 1G 0· 1G 0· 1G 0· 1G 0³ 1O 0¯ 1O 0¯ 1O 0¯ 1O 0« 1W 0§ 1W 0§ 1W 0§ 1W 0§ 1W 0§ 1W 0§ 1W 0§ 1W 0£ 1_ 0Ÿ 1_ 0Ÿ 1_ 0Ÿ 1_ 0› 1g 0— 1g 0— 1g 0— 1g 0— 1g 0— 1g 0— 1g 0— 1g 0“ 1o 0 1o 0 1o 0 1o 0 1o 0 1o 0 1o 0

Makes sense right?  In a nutshell, it's creating a large 1024x768 array that points out which pixels are driveable (the road) and which ones aren't (the walls).  Rather than store a bunch of ASCII characters in a text file (about 900 kb large, geez), this additional program I wrote compresses similar values by how many instances of a given value in a row.  So, going from left-to-right, top-to-bottom, if there's 500 black tiles, write down "500 black" instead of "black black black black black...." 500 times.  Then "200 white" or "50 pink" or whatever.  Because this is a binary file, it stores values like ç ÿ instead of decimal numbers because that's how programmers roll...we use HEXADECIMAL non-ASCII characters.

Now, how does the car use one of these bitmaps?  First, the Super Sprint program loads these .dat file arrays into each track and sets the roads, walls, and stuff.  Then for each frame, the car reads NINE points around the car, like this (the ninth being the dead-center of the car itself).  Think a "GetPixel" function but instead of reading the bitmaps, it reads the data from the .dat file:


What happens is if one of these dots touches a "wall" then it tells the game that a collision has occurred and the car bounces back accordingly.  How it bounces depends on which dots come in contact with the wall (the "collision state") and which direction the car was previously facing.


Here is a car "sprite sheet."  States 0 thru 15 are all possible directions the car can be driving.  16 and 17 are reserved for crashes only.  The collision state is basically the direction the car has to be facing to be perpendicular with the wall. For instance, if the car hits a wall to its east, the collision state is 4.

When a car collides with a wall, it gets the difference between the previous state and the collision state and changes the car's state from there.  For instance, if a car driving east crashes head-on with the wall (state 4), then the car's state doesn't change--he just keeps facing the wall.  However, if he skims the wall to the east (state 6), then the wall will push the car off as expected (car's state is now 8).
 
 
I cannot be arsed into explaining this further since it's very confusing, even for me.  In other words, the smaller the angle between the car direction and the wall it collides with, the more the car is pushed away from the wall.  This on top of stuff like lower the car's speed, make a smoke cloud appear, back the car up a few previous frames so it doesn't get stuck in a wall, etc.
 
Oh, and car explosions?  These occur during a collision when the car is spinning, the center-most dot is ticked off, or if 4 or more dots are ticked off.  Get the collision state (i.e. 8 if the car hits a wall to its south), then when it respawns, flip the car in the opposite direction from the collision state.  Car crashes on a wall to the south (state 8)?  Change its state facing north (state 0) and it's off on its merry way--very convenient!

Like I said before, each pixel on the map can be of one of four states (road, wall, bridge wall, tunnel wall).  It's entirely possible to create new states, like sloped roads or doors that open & close from the arcade version.  Any state that can say, "Hey, so-and-so happens at this location," etc.

Drifting:

Something as rudimentary as getting the car to drift was a pain in the ass for me.  I couldn't figure out the best method for some time.  Then it struck me like lighting--here's how I did it:

Depending on the car's state (0 thru 15, remember?) the car is pushed in one of 16 directions (multiples of 22.5 degrees).  Except when a car is in a spinout (it moves forward at same velocity regardless of situation), the car's current state dictates which direction to push the car.  But this only works if you're trying to move the car in a straight line with no drift whatsoever.

To add drift, record the car's state approximately 250 ms ago, the so-called "previous state".  There's a "drift meter" that determines how much the car drifts, ranging from 0 to 75% of total handling.  Assume it's a variable called X.  When you're turning, it goes up.  When you're going straight, the meter goes down.  When it's time to increment the car's position, move the car X * previous state + (1 - X)  * current state.  In other words, hold the steering wheel down for longer periods of time to make the car drift more.  Example: If you're turning left, the car is going to be pushed slightly to the right against your will as the car overcomes momentum from its previous states.  That's drifting in action.

The best way to limit drift is to either slow down (the drift variable X increments at a lower rate at slower speeds) or tap the steering wheel rather than hold it down.  Or better yet, increase your car's grip level via upgrades.  This grip level determines the rate at which the drift variable X increments--a grip level of 6 pretty much eliminates all drift whatosever.  Also it's worth mentioning a higher grip level reduces spinout time and speed loss through water puddles : )

Getting The AI To Move:
 
The AI was very fun to mess with in this game.  When I first got it working, I cried and proclaimed, "It's like watching your first-born learn to walk."
 
Anyway, the Version 1.0 AI is rather simple, I suppose.  There's a trail of coordinates leading through the whole track, kind of like a bunch of rails in a roller coaster.  Take a look at this example, every "dot" resembles a node:
 
 
Each one of these nodes are stored in Assets\Scripts\track#.txt.  The car's goal is to go through each node from start to finish.  It uses the same car physics as human players so it is suspect to sliding and wreckage.  The difference is the direction of the next node tells it which direction to face.  See here:
 

Say the car is driving upwards, but the node is to his upper-right.  So the game tells the car to turn right so that it's facing directly at the next node.  An analogy would be a fish that bites a hook and is reeled in--it's drug by it's mouth towards the fisherman in the boat.  Once the car comes within a certain distance of the next node, its then given the next node to drive to.  So basically, the car is on rails but to randomize things a bit, the node locations are slightly randomized so the car doesn't follow the same exact line like in previous Super Sprint games.  This is steering in a nutshell.  Kind of like the "collision state" mentioned previously, the goal is to figure out the AI's desired direction so that its facing directly at the next node.
 
Right now, the AI is completely unaware of obstacles, including other cars, but it's entirely possible for me to add this later in the future.  Not like it's totally necessary though unless I want a damn good-looking screensaver : )
 
One more thing, and this is a biggie.  This is Bresenham's Line Algorithm.  Basically, if you've ever used Paint and tried to draw a line from point A to B, you see it's pixelated in a few areas to try to get as perfect as a line as possible since we're dealing with pixels with whole number coordinates instead of decimals.
 
A problem I had with AI pathfinding is that sometimes, they would get stuck trying to headbutt the wall thinking they can make it to the next point when they really can't.  Let's use Bresenham's Line Algorithm to solve the problem.  Basically, if there is a wall between the AI's position and its node, then SET THE CAR TO THE PREVIOUS NODE.  Repeat this multiple times a frame if necessary.  In other words, make the AI only drive to nodes that they have a clear view of.  It's actually quite foolproof should the AI accidentally set off a node but get sucked backwards due to a tornado, oil slick, or car collision.  The AI also seldom skips the intersections this way too since it basically guarantees it hits every node on the track.  Basically, it tells him to "go back" until he's back in the race like normal.
 

Cars Collide With Each Other:
 
When two cars come within a certain proximity of each other, they will bump each other.  If one car is faster than the other, the faster car will then slow down the slower car and change his state.  Kind of like wall collisions, the greater the angle between the two cars, the further the slower car is "bumped off."
 
Car vs. car collisions also take a car's durability stat into consideration.  A car's durability tacks on mass and therefore, a slightly slower car with a higher durability stat may knock a faster-yet-lighter car completely out.
 
AI Difficulty:
 
When starting the game, an AI's top speed and acceleration are capped at 50%.
Each race in which all human players "win" (don't lose to an AI car), this percentage increases by 5%.  This cannot go above 100%
Each race in which a human player "loses" (AI car finishes before them), this percentage decreases by 10%.  This cannot go below 50%.
Every three races, the AI gets one random upgrade.  Also, if an AI car gets two or more wrenches (by accident, I suppose), then it gets yet another random upgrade.
That's about how the AI works.  I fared 322 points before I Game Overed in 8-Player Arcade.  See if you can try to beat it.

Time-Based Movement:

Let me define it like this: You're playing two different copies of the same game.  One runs at a smooth 60 FPS, the other fluctuates between 15 to 30 FPS.  However, if you compare the two copies side-by-side, the cars should be traveling at the same speed--the only difference is that one updates slower than the other.  This is what time-based movement does--it basically gets the time interval between updates (usually 15-20 milliseconds) and changes physics accordingly so the game handles seamlessly across different platforms.

This is contrary to frame-based movement in which every frame is shown on the computer but it can lead to inconsistent performance.  A good example would be an emulator, particularly Model3 like Daytona USA 2 or Scud Race.  The game is supposed to run at 60 FPS but if it doesn't, it slows the game down enough to ensure that every frame is shown even though it makes the game very choppy.

I'm hoping that Super Sprint handles the same on every computer, even older ones.  In theory, the game should run at 60 FPS.  The game runs some complicated math each frame but nonetheless is nowhere near as complicated as full-fledged games today.

Should Super Sprint lag, however, the maximum possible frame should be 25 milliseconds.  Should a frame take longer than 25 ms, the game will still assume it's 25 ms long.  This of course means the game will lag and the time counter will not align perfectly with a real-life timer but that's the breaks.  The game records previous car states and positions each 25 ms and should the game run slower than that, then the physics become all messed up--cars warp into walls, slide around too much, etc.  So it's part-frame, part-time-based gameplay. 

If your computer can't run the game faster than 40 FPS (25 ms per frame), then the game wouldn't be fun to play anyway, even if I tweaked it.  I still don't have a large sample of computers to play-test the game on but for now, the game seems to be running well so that is fine at the moment.  This should be an easy fix if I need to make the frames longer to accomodate 30 FPS at the very least.

Car Sprites:

HA, some of you may be curious so here's where I got the car sprites from: Famicom F1 Grand Prix.



I was just typing in "car sprites" on Google, not knowing whether or not to make my own sprites, and these little cars popped up.  So I liberated them from that mediocre Nintendo game cause I just rule like that.

How To Put New Tracks In The Game:
 
Alright, so you want to help make the game better?  Here's what I want you to do:
 
Make TWO 1024 x 768 .png files of the track--one of the bottom level and one of the top level.  When creating the stage, use something like Photoshop and save the two as separate layers.  Also, if you're making shadows, they can be between 0 to 100% transparent like in the palm tree example above.  It's HIGHLY recommended that you don't get too fancy with curves and whatnot--the game still needs a completely horizontal or vertical finish line so keep that in mind while working on it.
 
Then I'll need a third layer--the 1024 x 768 .bmp bump map (the black-pink-blue-grey example above).  I could possibly make this on my own but if you can strip away just the road like in my example and just mark the off-limits area black, then that would be great.
 
I'll then take the three .png's you sent me, compress them into their own files (both track layers need to be compressed to a 1024x512 .png), create the finish line, checkpoints, bridge lines, oil/water/wrench locations, and AI waypoints and get them into the game.  The only problem with adding new tracks is that it may increase the game's initial loading time.  Otherwise, I welcome new content.  Hope this post made sense.  I'm done.

5 comments:

  1. I gotta admit after playing this, the first thing I did was go through the image files mostly out of curiosity. Seeing those transparent PNG images brought me back to the days as a Stepmania skin creator (which nowadays I often forget about). Good times... But I never imagined a simple image could be used to determine collision like that. That's awesome.

    ReplyDelete
  2. Yeah, one of the things I planned since the beginning was to use that GetPixel collision. My teacher suggested lines but I thought that would've been lousy. I already used pixel detection in my previous game 30 Seconds or Less (look at my deviantArt) so I thought I could do better here. Turns out it worked to perfection which is really convenient :)

    ReplyDelete
  3. been playing it bit more
    lately... very nice..

    i've tried doing some
    simple games using yoyo's
    gamemaker.. but haven't done
    any racing games.. mostly
    maze/pac-man and puzzle games.

    keep it up.. hope there's
    some new levels made.

    later
    -1
    neg1sell@ hotmail.com

    ReplyDelete
  4. Lots of fun !
    Could you please make a simple screensaver of a track or 2 with AI cars racing ?

    ReplyDelete
  5. Maybe, I don't know how to make screensavers but someday I might. It's a cool game to leave on. You can just turn on the game and do nothing, just like a real arcade game.

    ReplyDelete