Platform Hack is a game in development using XNA, for release on XBox 360 Indie Game Channel. This blog will explore various concepts and algorithms used to build the game.
Tuesday, January 25, 2011
For the Love Of...
Great write-up on the appeal of Rogue-likes: http://www.gearfuse.com/unevenly-distributed-how-i-learned-to-stop-worrying-and-love-the/
Tuesday, January 18, 2011
Mini-Maps
For games with complex level design, mini-maps are essential. Especially in games like Super Metroid where the focus of the gameplay is more on backtracking and using your new abilities than pure exploration, the many map is very important tool.
For Platform Hack, I'm planning on two different "mini" maps. One map will be accessible from the pause menu, and will show the entire level, along with special icons for the player, doors, chests, etc. The other map will be a true mini-map, in the upper-right corner of the HUD, showing only a tiny portion of the current level.
Originally I intended to draw a pixel directly on the screen to correspond with each tile in the level, but that turned out to be too slow.
Instead, what we can do it create a Texture2D object on the fly and display it on the screen. If we need to crop (for the small mini-map in the corner), we can calculate coordinates to center on the player, and then specify the sourceRectange parameter of SpriteBatch.Draw.
To build a Texture2D in XNA, we first have to set the size. Then we load in an Color array to specify the individual color of the pixels:
Drawing the full map is pretty straightforward:
If we want to have a cropped version of the mini-map, we can use a function to determine the cropping rectangle, centered on the player position:
Then we can draw with these params in the Draw function:
One important feature of mini-maps is the Fog of War. We don't want to reveal the entire map to the player from the beginning. Instead, we want to hide parts of it and reveal more as the player explores. Uncovering new sections of the map is one of the fun parts of games with complex level design.
To implement Fog of War, we store an extra 2D character array that determines which tiles in the map have been revealed. As shown above in the GetColorArray() function, sections that are not yet revealed (char '?') are drawn Gray.
The Fog of War char[,] array starts out completely filled with "unknown" tiles - '?'. Each time we draw, we make a call to update the Fog Of War, determine what tiles the player can see, and switch those tiles over to '.'.
I'm using an open source algorithm called MRPAS to determine what the player can see from his position. Full source available here: http://www.umbrarumregnum.net/downloads/mrpas
The code itself is somewhat complex, but essentially it draws lines out from the player until it hits an obstacle. One nice feature of the algorithm is you can specify to "light walls", which means the wall tiles themselves will be revealed in the Fog Of War, and therefore show up in the minimap.
I was a little worried that the whole thing would run slow, since we're doing a dozen nested loops on nxn mazes every Draw cycle, as well as running the MRPAS algorithm. However, it runs without hiccup. If performance started to become an issue (potentially on massive maps), you could simply keep track of which tiles the player has occupied. If we've already been to this tile, don't run the Fog of War / MRPAS algorithm. Similarly, only recalculate the Color[] for the actual mini-map tile if the player changes tiles.
Next time I'm going to look into everything related to enemies - path finding, line of sight, rudimentary AI, attacking, etc.
For Platform Hack, I'm planning on two different "mini" maps. One map will be accessible from the pause menu, and will show the entire level, along with special icons for the player, doors, chests, etc. The other map will be a true mini-map, in the upper-right corner of the HUD, showing only a tiny portion of the current level.
Originally I intended to draw a pixel directly on the screen to correspond with each tile in the level, but that turned out to be too slow.
Instead, what we can do it create a Texture2D object on the fly and display it on the screen. If we need to crop (for the small mini-map in the corner), we can calculate coordinates to center on the player, and then specify the sourceRectange parameter of SpriteBatch.Draw.
To build a Texture2D in XNA, we first have to set the size. Then we load in an Color array to specify the individual color of the pixels:
Drawing the full map is pretty straightforward:
spriteBatch.Draw(MapTexture,TopLeftPixel, null, Color.White,0,new Vector2(0,0),this.scale,SpriteEffects.None,0);
If we want to have a cropped version of the mini-map, we can use a function to determine the cropping rectangle, centered on the player position:
Then we can draw with these params in the Draw function:
spriteBatch.Draw(MapTexture, TopLeftPixel, getPlayerCenteredMapRec(this.scale), Color.White, 0, new Vector2(0, 0), this.scale,SpriteEffects.None, 0);
One important feature of mini-maps is the Fog of War. We don't want to reveal the entire map to the player from the beginning. Instead, we want to hide parts of it and reveal more as the player explores. Uncovering new sections of the map is one of the fun parts of games with complex level design.
To implement Fog of War, we store an extra 2D character array that determines which tiles in the map have been revealed. As shown above in the GetColorArray() function, sections that are not yet revealed (char '?') are drawn Gray.
The Fog of War char[,] array starts out completely filled with "unknown" tiles - '?'. Each time we draw, we make a call to update the Fog Of War, determine what tiles the player can see, and switch those tiles over to '.'.
I'm using an open source algorithm called MRPAS to determine what the player can see from his position. Full source available here: http://www.umbrarumregnum.net/downloads/mrpas
The code itself is somewhat complex, but essentially it draws lines out from the player until it hits an obstacle. One nice feature of the algorithm is you can specify to "light walls", which means the wall tiles themselves will be revealed in the Fog Of War, and therefore show up in the minimap.
I was a little worried that the whole thing would run slow, since we're doing a dozen nested loops on nxn mazes every Draw cycle, as well as running the MRPAS algorithm. However, it runs without hiccup. If performance started to become an issue (potentially on massive maps), you could simply keep track of which tiles the player has occupied. If we've already been to this tile, don't run the Fog of War / MRPAS algorithm. Similarly, only recalculate the Color[] for the actual mini-map tile if the player changes tiles.
Next time I'm going to look into everything related to enemies - path finding, line of sight, rudimentary AI, attacking, etc.
Monday, January 10, 2011
Building Mazes
Happy New Year!
From NetHack to Civilization to Diablo to MineCraft, lots of games have used procedural algorithms to generate semi-randomized levels. For a one man shop like myself, its a way to generate lots of gameplay without a ton of gruntwork.
So how to go you about building mazes?
One site that really helped me out was PCG.Wikidog.com - Procedural Content Generation. It contains algorithms for some of the classic roguelikes, including Angband.
A maze itself can be generated with a pretty simple iterative or recursive algorithm. The psuedocode is as follows:
1. Start out with a 2D array of size nxn.
2. Fill each cell in the grid with character '?' (undefined)
3. Carve out a random cell.
a. Define function Carve: make the input cell empty '.'
b. Make the surrounding four cells "maybe" ','.
c. Add these surrounding cells to a coordinate list called Frontier
4. Iterate or Recurse through the Frontier list until it is empty.
a. pick a random cell from Frontier. You can use weighted algorithms to pick near the front or back of the list, which lead to different styles of maze (tight and claustrophic vs long empty passages)
b. Run function Check on the random cell. Define check (Note, this is the most complex part of the algorithm, so you may need to follow along the actual code below)
i. get the EdgeState of the cell - sum up the cells surroundind the current cell. Empty to the north = 1, south = 2, west = 4, east = 8.
ii. If we're not up against the side of the map, set the diagonal spaces adjacent to the input space as empty, and return false. Otherwise return true. This basically makes the maze have corners.
c. If Check was true, Cave out the cell. Otherwise Harden the cell - turn it into a wall '#'
d. Remove this cell from the Frontier list.
5. Once the Frontier list is emtpy, go through the entire map and change "maybe" cells ',' into empty cells '.' . Turn unknown '?' cells into walls '#'
Full Code:
Next time I'm going to look into minimaps, and "fog of war". We don't want to be able to spoil the entire maze in one glance!
From NetHack to Civilization to Diablo to MineCraft, lots of games have used procedural algorithms to generate semi-randomized levels. For a one man shop like myself, its a way to generate lots of gameplay without a ton of gruntwork.
So how to go you about building mazes?
One site that really helped me out was PCG.Wikidog.com - Procedural Content Generation. It contains algorithms for some of the classic roguelikes, including Angband.
A maze itself can be generated with a pretty simple iterative or recursive algorithm. The psuedocode is as follows:
1. Start out with a 2D array of size nxn.
2. Fill each cell in the grid with character '?' (undefined)
3. Carve out a random cell.
a. Define function Carve: make the input cell empty '.'
b. Make the surrounding four cells "maybe" ','.
c. Add these surrounding cells to a coordinate list called Frontier
4. Iterate or Recurse through the Frontier list until it is empty.
a. pick a random cell from Frontier. You can use weighted algorithms to pick near the front or back of the list, which lead to different styles of maze (tight and claustrophic vs long empty passages)
b. Run function Check on the random cell. Define check (Note, this is the most complex part of the algorithm, so you may need to follow along the actual code below)
i. get the EdgeState of the cell - sum up the cells surroundind the current cell. Empty to the north = 1, south = 2, west = 4, east = 8.
ii. If we're not up against the side of the map, set the diagonal spaces adjacent to the input space as empty, and return false. Otherwise return true. This basically makes the maze have corners.
c. If Check was true, Cave out the cell. Otherwise Harden the cell - turn it into a wall '#'
d. Remove this cell from the Frontier list.
5. Once the Frontier list is emtpy, go through the entire map and change "maybe" cells ',' into empty cells '.' . Turn unknown '?' cells into walls '#'
Full Code:
private char[,] maze; //field private ListAnd that's it. You end up with something like this:frontier; private int height; private int width; private void BuildMaze() { maze = new char[height, width]; //traverse the maze and fill in unknown blocks for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { maze[i, j] = '?'; } } //carve out a random spot Carve(r.Next(width-1),r.Next(height-1)); while (frontier.Count > 0) { double pos = Math.Pow(r.NextDouble(), Math.Pow(Math.E, branchrate)); int index = (int)(pos * frontier.Count); Coord c = frontier[index]; if (Check(c.x, c.y)) Carve(c.x, c.y); else Harden(c.x, c.y); frontier.Remove(c); } for (int i = 0; i < height; i++) { for (int j = 0; j 0) { if (maze[y - 1,x ] == '?') { maze[ y - 1,x] = ','; frontier.Add(new Coord(x, y - 1)); } } if (y < height - 1) { if (maze[y + 1,x] == '?') { maze[y+ 1,x ] = ','; frontier.Add(new Coord(x, y + 1)); } } if (x > 0) { if (maze[y,x - 1] == '?') { maze[y,x - 1] = ','; frontier.Add(new Coord(x - 1, y)); } } if (x < width - 1) { if (maze[y,x + 1] == '?') { maze[y,x + 1] = ','; frontier.Add(new Coord(x + 1, y)); } } //shuffle frontier? } private void Harden(int x, int y) { maze[y,x] = '#'; } private bool Check(int x, int y) { int edgestate = 0; if (y > 0) { if (maze[y - 1,x ] == '.') edgestate += 1; } if (y < height - 1) { if (maze[ y + 1,x] == '.') edgestate += 2; } if (x > 0) { if (maze[y,x - 1] == '.') edgestate += 4; } if (x < width - 1) { if (maze[y,x + 1] == '.') edgestate += 8; } if (this.noDiagonals) { if (edgestate == 1) { if (y < height - 1) { if (x > 0) { if (maze[ y + 1,x - 1] == '.') return false; } if (x < width - 1) { if (maze[y + 1,x + 1 ] == '.') return false; } } return true; } else if (edgestate == 2) { if (y > 0) { if (x > 0) { if (maze[y - 1,x - 1 ] == '.') return false; } if (x < width - 1) { if (maze[ y - 1,x + 1] == '.') return false; } } return true; } else if (edgestate == 4) { if (x < width - 1) { if (y > 0) { if (maze[y - 1,x + 1 ] == '.') return false; } if (y < height - 1) { if (maze[ y + 1,x + 1] == '.') return false; } } return true; } else if (edgestate == 8) { if (x > 0) { if (y > 0) { if (maze[y - 1,x - 1 ] == '.') return false; } if (y < height - 1) { if (maze[y + 1,x - 1 ] == '.') return false; } } return true; } return false; } else { if (edgestate == 1 || edgestate == 2 || edgestate == 4 || edgestate == 8) return true; else return false; } }
.#.##.#.#.#....#.#.# ....#.#.#.#.##.#.... ###.#...#...####.### #...###...#..#.#.#.. ..#.#.##.#####.....# ###.#.#........###.. .#..#.##.#####...### .#.##..#.....#.####. ....#.##.#######..#. #.###.#...#...###...The trouble is that the passageways are only a single tile wide or tall, which would make it very hard for our hero to navigate the game world, since he's two tiles tall! So we can scale the maze, simply by take a tile and copying it to 3 tiles to the east, south and south east. This doubles the size of the board. We can continue to double until the maze feels right - for my game it's a factor of 2, which is 4 times the size. You end up with something like this:
....########....####........####....####........########....####....########.... ....########....####........####....####........########....####....########.... ....########....####........####....####........########....####....########.... ....########....####........####....####........########....####....########.... ........................########....########........####....####........####.... ........................########....########........####....####........####.... ........................########....########........####....####........####.... ........................########....########........####....####........####.... ####################....................########............########....####.... ####################....................########............########....####.... ####################....................########............########....####.... ####################....................########............########....####.... ........................################################....########............ ........................################################....########............ ........................################################....########............ ........................################################....########............ ################....########....####....####....####............####....######## ################....########....####....####....####............####....######## ################....########....####....####....####............####....######## ################....########....####....####....####............####....########The world is still pretty boring, so one thing we can do is generate "rooms". These are large areas of empty space that we can later populate with platforms, elevators, traps and enemies. Generating rooms is pretty easy. Prior to building the map, select a few cells and expand them by random dimensions, changing them to type the "maybe" type ',' We don't want to completely empty them out, so the maze algorithm will still work. So now we have something like this. Better, but still not a game world:
....................########............########....####....####....####....#### ########....####................############........................####........ ########....####................############........................####........ ########....####................############........................####........ ########....####................############........................####........ ....####....####....####....########............################............#### ....####....####....####....########............################............#### ....####....####....####....########............################............#### ....####....####....####....########............################............#### ....####################....################................####....####........ ....####################....################................####....####........ ....####################....################................####....####........ ....####################....################................####....####........ ....########............................####.................................... ....########............................####.................................... ....########............................####.................................... ....########............................####.................................... ........########....########....############.................................... ........########....########....############.................................... ........########....########....############.................................... ........########....########....############....................................The next step is to place special characters. These are tiles that will represent platforms, traps, enemies, elevators, items, powerups, etc. Once we have our fully scaled map, we can traverse through and check for certain patterns in which to place our special tile. For example, to place a platform '-', we want to be in a large empty room. So before we place a platform, check to see that the 5 spaces above and below are empty:
........................####............ ........................####............ ........................####............ ........................####.......A.... ....####................####....######## ....####..............-.####....######## ....####................####....######## ....####................####....######## ############............####............ ############........-...####............ ############...-........####............ ############............####............ ......................-.........####.... ................................####..-. ................................####.... ..A......A...-..................####.... ############....-...........########.... ############................########.... ############................########.... ############................########....Lastly, we want to add some challenge and progression to our maps. We can do this with locked doors and keys. The easiest way is to generate lots of smaller maps, place a random key 'K' inside, enclose the entire thing with a wall and a single locked door as the exit 'D'. Connect all the smaller maps together, and we have a maze that requires some thinking to complete!
############################################################ #..................#............####...#............####...# #....K.............#.....K......####...#.....K......####...# #.A........A.......#..A.........####...#..A.........####...# ################...#########...........#########...........# ################.-.#########...........#########...........# ################...#########.-.........#########.-.........# ################...#########...........#########...........# #..................D............####...D............####...# #..................#............####...#............####...# #..................#............####...#............####...# #.A.............A..#...A........####...#...A........####...# ########....################....################....######## ########....################....################....######## ########....################.-..################.-..######## ########....################....################....######## #..................#...................#...................# #..................#...................#...................# #..................#...................#...................# ############################################################The fun thing about maze generation is that the possibilities are endless. You can instantly generate a new world to explore and jump around in.
Next time I'm going to look into minimaps, and "fog of war". We don't want to be able to spoil the entire maze in one glance!
Wednesday, December 29, 2010
Platform Physics: Grapple Hook
One of the best upgrades in Super Metroid is the grapple beam. It gives you the ability the explore much more of the world, and avoid dozens of annoying enemies on the way. It's even useful for shocking to death a boss. So I knew I wanted a swinging grapple hook in Platform Hack.
Not only is the grapple hook fun to use in game, its an interesting challenge to implement in code. There are essentially three main parts:
1. The "rope" needs to shoot out from the player and catch on some surface of the world. This can either be special tiles, or any sort of tile, though in some games this may lead to exploits.
2. Once attached, the player swings in motion similar to a pendulum.
3. When you release, you swing in the direction you were traveling, potentially covering gaps. The player can also re-attach the grapple, and swing horizontally ala Tarzan.
Similar to the jump logic, the first thing I did was create a data structure to hold all the variables we need to keep track of during the grapple swing:
SwingCurveStruct is also a data type that comes in handy - it holds two Curve objects. The Curve class allows you to interpolate over a curve given a set of discrete points. So instead of defining complex movement purely by physics formulas, you can numerically determine a few key points along the curve, plug them into a Curve class and then approximate the curve easily. We use a SwingCurveStruct to hold the points in the swing itself.
GrappleTimer tracks how much time the player has been "attached" to the grapple, so we can evaluate the correct position in the swing.
launchVector defines the vector when the player releases from the grapple hook, and lets the player "fling" up in the air.
FireVector is the direction the player is shooting the grapple hook. Right now the "rope" only goes in an upward 45 degrees for ease of use, however this value could be set by the player.
The RopeLength and fireSpeed define how far and how fast the rope itself can shoot out. These values are set when the grapple struct is first created. Longer and faster ropes make it much easier to attach to surfaces, and vice-versa.
The bulk of the logic for the grapple is done in the DoGrapple function:
The function has three main sections, corresponding to the three parts listed above.
1. If we're shooting the grapple hook (artifactPressed) and in mode 0, then increment the location of the "hook" and check if we've collided with any surfaces.
We use the GrappleCollide function to check:
getLineCollision essentially gets a list of all tiles that are overlapped by a line connecting the player to the hook. We then traverse this list, moving outward from the player, and if we hit an Impassable or Platform tile, we return true, we're connected.
Once we're connected, we change to mode 1 and we have to calculate the swing curve:
First, we have to determine the initial angle the player is attached:
2. Once we are attached, we just need to determine our current position on the swing curve.
3. Once the player lets go (artifactPushed == false), we need to apply the launch velocity. We actually set the launch velocity while we are connected with the following function:
We find the difference in position during the swing, then multiply by 20. The 20 is an arbitrary value, and can be adjusted to set how much the player "flings" after they release the grapple hook. This is another case where if you use realistic physics, the gameplay isn't as fun as using exaggerated calculations.
Last, we apply gravity as normal. The launch vector is saved in Force1Vector, and is applied along with the other vectors.
So that's the basic grapple hook, probably the most difficult "movement tool" being implement in Platform Hack. Next time, it's onto something completely different - procedural maze generation!
Not only is the grapple hook fun to use in game, its an interesting challenge to implement in code. There are essentially three main parts:
1. The "rope" needs to shoot out from the player and catch on some surface of the world. This can either be special tiles, or any sort of tile, though in some games this may lead to exploits.
2. Once attached, the player swings in motion similar to a pendulum.
3. When you release, you swing in the direction you were traveling, potentially covering gaps. The player can also re-attach the grapple, and swing horizontally ala Tarzan.
Similar to the jump logic, the first thing I did was create a data structure to hold all the variables we need to keep track of during the grapple swing:
public struct GrappleStruct { public int mode; //0=shooting, 1=attached public SwingCurveStruct swingCurve; public Vector2 position; //position of "hook" public TimeSpan GrappleTimer; public Vector2 launchVector; public Vector2 FireVector; public float RopeLength; public float fireSpeed; public Vector2 aimVector; } public struct SwingCurveStruct { public Curve YCurve; public Curve XCurve; }
SwingCurveStruct is also a data type that comes in handy - it holds two Curve objects. The Curve class allows you to interpolate over a curve given a set of discrete points. So instead of defining complex movement purely by physics formulas, you can numerically determine a few key points along the curve, plug them into a Curve class and then approximate the curve easily. We use a SwingCurveStruct to hold the points in the swing itself.
GrappleTimer tracks how much time the player has been "attached" to the grapple, so we can evaluate the correct position in the swing.
launchVector defines the vector when the player releases from the grapple hook, and lets the player "fling" up in the air.
FireVector is the direction the player is shooting the grapple hook. Right now the "rope" only goes in an upward 45 degrees for ease of use, however this value could be set by the player.
The RopeLength and fireSpeed define how far and how fast the rope itself can shoot out. These values are set when the grapple struct is first created. Longer and faster ropes make it much easier to attach to surfaces, and vice-versa.
The bulk of the logic for the grapple is done in the DoGrapple function:
public bool DoGrapple(bool artifactPushed, GameTime gameTime, ref Vector2 position, ref Vector2 Force1Vector, ref Vector2 velocity, Level level, SpriteEffects flip) { float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds; bool isSwinging = false; if (artifactPushed) { if (playerGrappleStruct.mode == 0) { if (playerGrappleStruct.FireVector.X == 0 && playerGrappleStruct.FireVector.Y == 0) { if (playerGrappleStruct.aimVector.X == 0 && playerGrappleStruct.aimVector.Y == 0) { if (flip == SpriteEffects.FlipHorizontally) { playerGrappleStruct.FireVector = new Vector2(1f, -1f); } else { playerGrappleStruct.FireVector = new Vector2(-1f, -1f); } } else { playerGrappleStruct.FireVector = new Vector2(playerGrappleStruct.aimVector.X, playerGrappleStruct.aimVector.Y); } } playerGrappleStruct.position += (playerGrappleStruct.FireVector * elapsed * playerGrappleStruct.fireSpeed) + (velocity * elapsed); if (Helper.getDistance(position, playerGrappleStruct.position) > playerGrappleStruct.RopeLength) { playerGrappleStruct.position = position + new Vector2(0, -25); } GrappleCollision(level,position,velocity); isSwinging = false; } if (playerGrappleStruct.mode == 1) { playerGrappleStruct.GrappleTimer += gameTime.ElapsedGameTime; float maxTime = playerGrappleStruct.swingCurve.XCurve.Keys[playerGrappleStruct.swingCurve.XCurve.Keys.Count - 1].Position; float newPositionX = playerGrappleStruct.swingCurve.XCurve.Evaluate((float)playerGrappleStruct.GrappleTimer.TotalSeconds % maxTime); float newPositionY = playerGrappleStruct.swingCurve.YCurve.Evaluate((float)playerGrappleStruct.GrappleTimer.TotalSeconds % maxTime); playerGrappleStruct.launchVector = setSwingVelocity(new Vector2(newPositionX, newPositionY), position, (float)gameTime.ElapsedGameTime.TotalSeconds); playerGrappleStruct.LaunchTimer = TimeSpan.FromSeconds(1); playerGrappleStruct.launchCurve = setLaunchCurveStruct(playerGrappleStruct.launchVector); position.X = newPositionX; position.Y = newPositionY; isSwinging = true; } } else { if (playerGrappleStruct.mode == 1) { playerGrappleStruct.mode = 0; Force1Vector = playerGrappleStruct.launchVector; } //reset the rope; playerGrappleStruct.position = position + new Vector2(0, -25); playerGrappleStruct.FireVector = new Vector2(0f, 0f); isSwinging = false; } return isSwinging; }
The function has three main sections, corresponding to the three parts listed above.
1. If we're shooting the grapple hook (artifactPressed) and in mode 0, then increment the location of the "hook" and check if we've collided with any surfaces.
We use the GrappleCollide function to check:
private void GrappleCollision(Level level, Vector2 position, Vector2 velocity) { Vector2 temp; if (Collision.getLineCollision(position + new Vector2(0, -25), playerGrappleStruct.position, level, out temp)) { if ((position.Y - 25) > temp.Y && Math.Abs(position.X - temp.X) > 50) //make sure we only grappling to points above us { playerGrappleStruct.position = temp; playerGrappleStruct.mode = 1; playerGrappleStruct.swingCurve = setGrappleSwingCurve(playerGrappleStruct.position, position + new Vector2(0, -25), velocity); playerGrappleStruct.GrappleTimer = TimeSpan.FromSeconds(0f); } } }
getLineCollision essentially gets a list of all tiles that are overlapped by a line connecting the player to the hook. We then traverse this list, moving outward from the player, and if we hit an Impassable or Platform tile, we return true, we're connected.
Once we're connected, we change to mode 1 and we have to calculate the swing curve:
/* * given the initial force vector (x, y), and the angle of attachment, calculate the swing curve of the grapple, using pendulum physics * pendulum forumula * * O'' = -g/R sin O * angular acceleration = - gravity / length of rope * sin angle * angle (O=vertical) * */ private SwingCurveStruct setGrappleSwingCurve(Vector2 origin, Vector2 playerPosition, Vector2 initVelocity) { //length of rope float length = (float)Math.Sqrt(Math.Pow((origin.X - playerPosition.X), 2) + Math.Pow((origin.Y - playerPosition.Y), 2)); Vector2 slope = (playerPosition - origin); slope.Normalize(); slope = new Vector2(slope.X, slope.Y * -1); //get start angle float angleStart = (float)(Math.Atan2((double)slope.Y, (double)slope.X)); float initVelocityRadians = getInitVelocity(length, origin, playerPosition, initVelocity); float time = 0f; float angleAcc = 0f; float angleVel = initVelocityRadians; float curAngle = angleStart; SwingCurveStruct retval = new SwingCurveStruct(); retval.XCurve = new Curve(); retval.YCurve = new Curve(); retval = addCurvePosition(retval, time, origin, length, curAngle); //iterate through and set curve keys for (float f = time; f < 20f; f += .1f) { angleAcc = -(GravityAcceleration / length) * (float)Math.Sin(curAngle + (float)Math.PI / 2); angleVel += angleAcc * .1f; curAngle += angleVel * .1f; retval = addCurvePosition(retval, f, origin, length, curAngle); } return retval; }This function uses the formula for a pendulum to create Curve objects that the player will move on while attached. Here's a great site that walked me through pendulum physics: http://www.myphysicslab.com/pendulum1.html
First, we have to determine the initial angle the player is attached:
float angleStart = (float)(Math.Atan2((double)slope.Y, (double)slope.X));Next, we set an initial velocity. This basically gives the player a little more momentum in the swing if they are moving when they attach, and makes "Tarzan" swings a little more powerful.
private float getInitVelocity(float length, Vector2 origin, Vector2 playerPosition, Vector2 initVelocity) { //pythagorean? float velocityDist = (float)Math.Sqrt((initVelocity.X * initVelocity.X) + (initVelocity.Y * initVelocity.Y)); float cosx = Math.Abs((length * length + length * length - velocityDist * velocityDist) / (2 * length * length)); float initVelocityRadians = (float)Math.Acos(cosx); if (float.IsNaN(initVelocityRadians)) initVelocityRadians = 0f; //unit circle goes counter clockwise, so if we are swinging the otherway, then multiple by -1 if (initVelocity.X < 0) initVelocityRadians *= -1; return initVelocityRadians; }Last, we just need to iterate the points of a pendulum into the Curve structs, The pendulum takes the following forumla:
angleAcc = -(GravityAcceleration / length) * (float)Math.Sin(curAngle + (float)Math.PI / 2);Once we have the angular acceleration, we can determine the angular velocity, and thus the current position in radians. From there, we can easily calculate the current position along the circumference of a circle (the "hook" being the origin).
//gets the x,y vector at the end of the pendulum, given origin, length and radians private Vector2 getCurvePosition(Vector2 origin, float length, float radians) { return new Vector2((float)(origin.X + length * Math.Cos(radians)), (float)(origin.Y + length * (float)Math.Sin(radians) * -1)); }Note: the swing algorithm breaks down if the player attaches to a surface at less than the horizontal plane. This is because the swing follows the path of a pendulum, and if the angle is greater than the horizontal, the rope will be "stiff", and the player could even use it as a sort of stilt. If you want full elastic grapple hooks, some different physics logic is needed
2. Once we are attached, we just need to determine our current position on the swing curve.
float newPositionX = playerGrappleStruct.swingCurve.XCurve.Evaluate((float)playerGrappleStruct.GrappleTimer.TotalSeconds % maxTime); float newPositionY = playerGrappleStruct.swingCurve.YCurve.Evaluate((float)playerGrappleStruct.GrappleTimer.TotalSeconds % maxTime);Note: We're also doing a special collision check at this point to make sure the player can't swing through walls. Because we aren't adjusting the player velocity, and instead directly updating the player position, there is the chance that the player could move so fast they could warp through walls (given a long enough rope, or a strong enough GravityAcceleration). So lots of tweaking/testing is needed to find the right balance.
3. Once the player lets go (artifactPushed == false), we need to apply the launch velocity. We actually set the launch velocity while we are connected with the following function:
private Vector2 setSwingVelocity(Vector2 position1, Vector2 position2, float timeDiff) { return (position1 - position2) * (float)(1.0 / timeDiff) * 20; }
We find the difference in position during the swing, then multiply by 20. The 20 is an arbitrary value, and can be adjusted to set how much the player "flings" after they release the grapple hook. This is another case where if you use realistic physics, the gameplay isn't as fun as using exaggerated calculations.
Last, we apply gravity as normal. The launch vector is saved in Force1Vector, and is applied along with the other vectors.
So that's the basic grapple hook, probably the most difficult "movement tool" being implement in Platform Hack. Next time, it's onto something completely different - procedural maze generation!
Tuesday, December 28, 2010
Platform Physics: Jumping
Ever since Super Mario Brothers (and perhaps Donkey Kong before that), the jump has been iconic of Platform games. At first glance, it seems like it would be pretty easy to implement a jump using classic physics - apply an upward force to the player and then have gravity act as a constant downward acceleration. This can be done, however, it leads to pretty punishing gameplay.
Platform games don't follow the laws of physics. Here's a few reasons.
1. We want the player to be able to move horizontally in the air. This lets the player tweak movement and land much more difficult jumps. This breaks the 1st law of Thermodynamics.
2. We want the jump (and the pull of gravity) to be limited by a max velocity. There are a few reasons for this. One, it makes the gameplay a bit more manageable to not be flying all over the place. Second, the algorithms of collision detection start to break down if the player (or any other object) moves faster than the height of a tile in a single update of the game loop.
3. We want the jump to have a constant upward motion, instead of a large initial velocity immediately starting to taper off by gravity. As long as a player presses the jump key (within limits), we want the player to jump upwards.
So let's look at the pieces needed to implement a jump:
For Platform Hack, I wanted jumps to be pretty versatile. Which means there will be double (and even triple) jumps, wall jumps and upgrades to jump power. So I needed a data type that could store and abstract away the details of the jump:
In the struct we have curNumJump and totalNumJump. This is used to keep track of double and triple jumps.
isJumping is a boolean thats mapped to the player jump key. wasJumping is a boolean that specifies whether the player was pressing jump *the previous update*. This is important for making sure the player presses the jump key for each jump instead of just holding it down.
jumpTime is the total amount of time the player can jump. Increasing this number allows the player to press down the jump key longer, and have larger jumps. jumpTimer is the actual timer that counts down as soon as the jump starts.
During each game loop, we must run the update on the player physics:
One thing I've done is kept the various velocities (GravityVector, PlayerVector) seperate, and then combined them before applying to the player movement. However, when I update gravity, it draws from the full velocity. This little tweak helps smooth out the upward curve of the player jump, or if they fall off a ledge. Gravity is also "clamped" at MaxFallSpeed, which acts as a sort of terminal velocity. Clamp is important for reason #2 above.
The Player.X velocity (horizontal movement) is updated by player control - the movement variable is set with the left and right buttons. This allows the player to move horizontally even if they are in the air.
The Player.X veloctiy is set with the DoJump function:
Essentially, this function first checks if we can jump. JumpCheck returns true if the player is on the ground, or the curNumJumps is less than totalNumJumps.
If we're on the ground, or the jump timer is less than the total jump time, then set the Player.Y velocity. We're using a power function to give the player a sort of exponential curve to upward velocity, starting with the most power and decreasing as we reach the top. We also are setting Gravity to 0 to give the jump a bit more "floatiness".
Once the DoJump returns, we have to calculate the player's new position. Velocity (and acceleration values - JumpAcceleration, GravityAcceleration) is stored in terms of seconds (500 pixels / second). Each update function only takes a fraction of a second, so we need to multiple the velocity by elapsed, which will give the appropriate amount to move the player.
Lastly, we round the player position to the nearest round number, so the sprite is positioned cleanly on the pixels. This is essential for player collision - otherwise the player can get stuck in floors and walls due to a fraction of a pixel overlapping.
The *feel* of the jump is essential to how the game plays. Stronger gravity and less precision of player movement can make the game much more difficult. Conversely, if gravity is too weak and the player is too floaty, the game can feel slow, frustrating or boring. Tweaking the GravityAcceleration, JumpAcceleration and Clamp values can drastically change the feel of the jump.
Next: I'll go into the details of the infamous grappling hook!
Note on Code samples: I'm more interested exploring algorithms and general concepts on this blog rather than complete code samples. The codebase I'm working with is a bit messy to upload for each example. If you want to follow along, start with the Platformer Starter Kit for XNA and go from there.
Platform games don't follow the laws of physics. Here's a few reasons.
1. We want the player to be able to move horizontally in the air. This lets the player tweak movement and land much more difficult jumps. This breaks the 1st law of Thermodynamics.
2. We want the jump (and the pull of gravity) to be limited by a max velocity. There are a few reasons for this. One, it makes the gameplay a bit more manageable to not be flying all over the place. Second, the algorithms of collision detection start to break down if the player (or any other object) moves faster than the height of a tile in a single update of the game loop.
3. We want the jump to have a constant upward motion, instead of a large initial velocity immediately starting to taper off by gravity. As long as a player presses the jump key (within limits), we want the player to jump upwards.
So let's look at the pieces needed to implement a jump:
For Platform Hack, I wanted jumps to be pretty versatile. Which means there will be double (and even triple) jumps, wall jumps and upgrades to jump power. So I needed a data type that could store and abstract away the details of the jump:
public struct JumpStruct
{
public int curNumJump;
public int totalNumJump;
public bool isJumping;
public bool wasJumping;
public float jumpTime;
public TimeSpan jumpTimer;
}
In the struct we have curNumJump and totalNumJump. This is used to keep track of double and triple jumps.
isJumping is a boolean thats mapped to the player jump key. wasJumping is a boolean that specifies whether the player was pressing jump *the previous update*. This is important for making sure the player presses the jump key for each jump instead of just holding it down.
jumpTime is the total amount of time the player can jump. Increasing this number allows the player to press down the jump key longer, and have larger jumps. jumpTimer is the actual timer that counts down as soon as the jump starts.
During each game loop, we must run the update on the player physics:
GravityVector.Y = MathHelper.Clamp(velocity.Y + GravityAcceleration * elapsed, -MaxFallSpeed, MaxFallSpeed);
PlayerVector.Y = DoJump(PlayerVector.Y, gameTime);
PlayerVector.X += MathHelper.Clamp(movement * MoveAcceleration * elapsed, -MaxMoveSpeed, MaxMoveSpeed);
velocity = GravityVector + PlayerVector + Force1Vector + Force2Vector;
velocity.X = MathHelper.Clamp(velocity.X, -MaxMoveSpeed, MaxMoveSpeed);
velocity.Y = MathHelper.Clamp(velocity.Y, -MaxMoveSpeed, MaxMoveSpeed);
Position += velocity * elapsed;
Position = new Vector2((float)Math.Round(Position.X), (float)Math.Round(Position.Y));
One thing I've done is kept the various velocities (GravityVector, PlayerVector) seperate, and then combined them before applying to the player movement. However, when I update gravity, it draws from the full velocity. This little tweak helps smooth out the upward curve of the player jump, or if they fall off a ledge. Gravity is also "clamped" at MaxFallSpeed, which acts as a sort of terminal velocity. Clamp is important for reason #2 above.
The Player.X velocity (horizontal movement) is updated by player control - the movement variable is set with the left and right buttons. This allows the player to move horizontally even if they are in the air.
The Player.X veloctiy is set with the DoJump function:
private float DoJump(float VelocityY, GameTime gametime)
{
VelocityY = 0;
if (JumpCheck(gametime, IsOnGround))
{
if ((IsOnGround) || playerJumpStruct.jumpTimer > TimeSpan.Zero)
{
if (playerJumpStruct.jumpTimer.TotalSeconds == playerJumpStruct.jumpTime)
{
level.thisPlatformerGame.gameAudio.playSound("PlayerJump");
}
}
// if we are in the ascent of the jump
if (TimeSpan.Zero < playerJumpStruct.jumpTimer && playerJumpStruct.jumpTimer <= TimeSpan.FromSeconds(playerJumpStruct.jumpTime))
{
GravityVector.Y = 0;
// fully override the vertical velocity with a power curve that gives players more control over the top of the jump
VelocityY = JumpLaunchVelocity * (1.0f - (float)Math.Pow((playerJumpStruct.jumpTime-playerJumpStruct.jumpTimer.TotalSeconds) / playerJumpStruct.jumpTime, JumpControlPower));
}
}
return VelocityY;
}
Essentially, this function first checks if we can jump. JumpCheck returns true if the player is on the ground, or the curNumJumps is less than totalNumJumps.
If we're on the ground, or the jump timer is less than the total jump time, then set the Player.Y velocity. We're using a power function to give the player a sort of exponential curve to upward velocity, starting with the most power and decreasing as we reach the top. We also are setting Gravity to 0 to give the jump a bit more "floatiness".
Once the DoJump returns, we have to calculate the player's new position. Velocity (and acceleration values - JumpAcceleration, GravityAcceleration) is stored in terms of seconds (500 pixels / second). Each update function only takes a fraction of a second, so we need to multiple the velocity by elapsed, which will give the appropriate amount to move the player.
Lastly, we round the player position to the nearest round number, so the sprite is positioned cleanly on the pixels. This is essential for player collision - otherwise the player can get stuck in floors and walls due to a fraction of a pixel overlapping.
The *feel* of the jump is essential to how the game plays. Stronger gravity and less precision of player movement can make the game much more difficult. Conversely, if gravity is too weak and the player is too floaty, the game can feel slow, frustrating or boring. Tweaking the GravityAcceleration, JumpAcceleration and Clamp values can drastically change the feel of the jump.
Next: I'll go into the details of the infamous grappling hook!
Note on Code samples: I'm more interested exploring algorithms and general concepts on this blog rather than complete code samples. The codebase I'm working with is a bit messy to upload for each example. If you want to follow along, start with the Platformer Starter Kit for XNA and go from there.
Thursday, December 23, 2010
Where I'm At Now
Right now I have a pretty good idea of what I want Platform Hack to be in regards to gameplay. The original inspiration was actually Super Metroid, one of the best platformers of all time.

Super Metroid has all the standard action-platformer elements of jumping and attacking enemies, but what really set the game apart were the RPG elements, weapons and boss fights. As you make your way through the maze-world, you unlock upgrades to health and weapons. There are also tools you acquire that allow you to traverse the world more easily, including the the grapple beam and the space jump. The boss fights were also extremely polished, with animations to match, finding that great balance between challenge and cakewalk.
I'd like Platform Hack to borrow a lot of those elements, including the "movement" upgrades like the double jump and the grapple hook. Because the world in Platform Hack will be procedurally generated, there's the possibility of having super high vertical walls and cliffs, which could potentially post a problem for the standard jump. Of course, I'll have to tweak those maze-generation algorithms to limit the cliffs on the easier levels. But one of the goals of the game will be to acquire these "movement tools" in addition to more powerful weapons.
As far as development, I see the project in three stages:
1. Core Engine Development. This includes algorithms for collision detection, physics, player control, HUD, enemy movement and attacks, and boss scripting.
2. Game World Development. This is the world generation using procedural maze algorithms, different "features" like elevators, springs, doors, traps, as well as the more specific enemy and boss instances.
3. Asset Creation. This is where the game finally gets it's look - includes drawing and importing all the player, enemy and boss sprites, particle effects, tile and background images, HUD font and icons, sounds and music. Hopefully I have some help on board for this stage. But it's still a ways off.
Right now I'm still deep in stage 1, trying to work out the tweaks in collision detection for various movement tools, including the Grapple Hook and Jump Jets. But that's for another post -

Super Metroid has all the standard action-platformer elements of jumping and attacking enemies, but what really set the game apart were the RPG elements, weapons and boss fights. As you make your way through the maze-world, you unlock upgrades to health and weapons. There are also tools you acquire that allow you to traverse the world more easily, including the the grapple beam and the space jump. The boss fights were also extremely polished, with animations to match, finding that great balance between challenge and cakewalk.
I'd like Platform Hack to borrow a lot of those elements, including the "movement" upgrades like the double jump and the grapple hook. Because the world in Platform Hack will be procedurally generated, there's the possibility of having super high vertical walls and cliffs, which could potentially post a problem for the standard jump. Of course, I'll have to tweak those maze-generation algorithms to limit the cliffs on the easier levels. But one of the goals of the game will be to acquire these "movement tools" in addition to more powerful weapons.
As far as development, I see the project in three stages:
1. Core Engine Development. This includes algorithms for collision detection, physics, player control, HUD, enemy movement and attacks, and boss scripting.
2. Game World Development. This is the world generation using procedural maze algorithms, different "features" like elevators, springs, doors, traps, as well as the more specific enemy and boss instances.
3. Asset Creation. This is where the game finally gets it's look - includes drawing and importing all the player, enemy and boss sprites, particle effects, tile and background images, HUD font and icons, sounds and music. Hopefully I have some help on board for this stage. But it's still a ways off.
Right now I'm still deep in stage 1, trying to work out the tweaks in collision detection for various movement tools, including the Grapple Hook and Jump Jets. But that's for another post -
Fire it up
So I've started this blog to track progress and explore ideas for the game I'm building: Platform Hack.
Platform Hack is a combination platform-jumper / rogue-like (NetHack)
The game is being built in XNA 4.0 using a heavily modified Platformer starter kit. I'll also be using some open source maze generation algorithms that come from Angband and other rougelikes.
The "engine" was already used for a simple game I released called Zombie Bash, which was basically an experiment to figure out the collision detection and intricacies of drawing / displaying assets in XNA.
There's still a lot to go, and the next few blogs will go over the goals I have for the game, as well as some challenges of building it -
Platform Hack is a combination platform-jumper / rogue-like (NetHack)
The game is being built in XNA 4.0 using a heavily modified Platformer starter kit. I'll also be using some open source maze generation algorithms that come from Angband and other rougelikes.
The "engine" was already used for a simple game I released called Zombie Bash, which was basically an experiment to figure out the collision detection and intricacies of drawing / displaying assets in XNA.
There's still a lot to go, and the next few blogs will go over the goals I have for the game, as well as some challenges of building it -
Subscribe to:
Posts (Atom)