Posted on May 13, 2018 by Abhirath Mahipal
This is a part of a series of notes. You can find notes to other lectures here Please feel free to bring to my attention mistakes, other interesting resources and feedback via the comments section. I’m all ears and will do my best to incorporate them into the blog post.
16 x 16tiles.
Some columns in the level there are no tiles (chasms the player can fall into), some columns are a bit higher (player has to jump), some have power ups and so on. All these variations in pattern put together create a level.
2D array of numbers. Each number normally depicts the kind of tile that should appear on the corresponding spot of the level. They can have a host of other properties like allow to pass things, trigger an animation and so on. You could do this by associating properties (using Tables in Lua) against each tile. The sky is the limit.
Colour in the background and just render a few tiles. That’s all.
Question by Colton:- What would be the first step to implement this?
Student Answer:- Loop over tiles and render them.
Colton goes on to explain that we’ll create a table to store tiles. We just won’t store numbers. We will store each tile as a mini table that will have an id as well as some properties (sort of like metadata).
mapHeightare used to to fill a table with appropriate number of entries for this demo.
TILE_SIZEis a constant set to 16. It’ll be 16 throughout the lecture.
Line 74 - 85 => Iterate over and use the above generated table to render. Sky tiles have no quad. If it’s a ground tile render the appropriate quad.
Final outcome:- Screen with tiles and a random sky colour.
Types of scrolling in games.
love.graphics.translate() Moves the coordinate system. Everything drawn to the screen after that gets rendered respective to the new coordinate system. So we shift everything and move the world. This gives the effect of the world moving as the player moves. We pass in X & Y based on the player position.
Demo => Key presses now move the tiles. If you move too much you will reach a point where there are no tiles. Normally such information is hidden from the user (recall that games are illusions).
Line 95 => Translate by negative camera speed. If the player moves to the left, we want everything to move to the right to give the illusion of the player moving. Y coordinate is set to 0 because we don’t want the map to move vertically. Math.floor is used as decimal values might cause blurring (called Blur Artifacting). Any time you condense (texture rendered on a small virtual window) or magnifying values while using a virtual screen there are chances of weird blurs appearing.
Line 80 - 87 => Take care of updating camera scroll based on right and left key inputs.
We translate before we draw. Everything drawn after the translate gets effected by it.
Love 2D is a low level game engine which gives more control and is good for learning. It has no camera objects and the likes. Higher level game engines like Phaser etc have camera objects which encapsulate this translate behvaiour.
We again generate quads from the sprite sheet and just draw the first quad. It’s just static and does nothing. The other quads you notice in the image files are going to be used for animating the character.
Now instead of camera scroll, have a variable called
characterSpeed. In this demo the character moves but the world (i.e tiles) are stationary.
3 things yet to be done.
The camera is fixed on the player. World moves relative to the player.
This demo is just to show how the world changes with respect to the player so does not account the case where the player reaches the edge. See the actual code in
mario/src for that.
character2/main.lua Lines 93 - 103 =>
function love.update(dt) -- update camera scroll based on user input if love.keyboard.isDown('left') then characterX = characterX - CHARACTER_MOVE_SPEED * dt elseif love.keyboard.isDown('right') then characterX = characterX + CHARACTER_MOVE_SPEED * dt end -- set the camera's left edge to half the screen to the left of the player's center cameraScroll = characterX - (VIRTUAL_WIDTH / 2) + (CHARACTER_WIDTH / 2) end
In the line
cameraScroll = characterX - (VIRTUAL_WIDTH / 2) + (CHARACTER_WIDTH / 2) character width is added so that the world’s coordinates line up perfectly in the center. Also notice that we check for keypresses and keep updating
In this example we just translate the world by the X axis and leave Y unchanged. Say mario could jump extremely high or we were to implement some sort of vertical scrolling game, we would have to translate on the Y axis as well (using the same approach as given above).
If the player moves loop through some image to create the illusion of animation. Animations are nothing but a series of images shown in succession.
Created a class called
The class also includes logic for animating single and multiple frames. Idle animation for instance has a single frame. So by writing it this way the same class can be used for all animations.
What about moving left animation?
We just flip the drawing when calling
love.graphics.draw(). If you recall it takes in optional arguments to flip it (rather scale, rotate i.e translate it). So we have to keep track whether he’s walking left or right and flip it on the X axis if he’s walking towards the left.
Animation.lua => Notice that while initialising the class the current frame it set to 1 (line 17). Also on line 22 we check if there are more than one frames. If there’s only one frame it’s kept constant throughout otherwise on the completion of the specified timer we keep changing the frame.
Notice that we have a function
function Animation:getCurrentFrame() that returns the frame. The main render function basically calls this function every frame to get which quad to render. So every few hundred milliseconds it changes internally and therefore returns the next quad and the main render function shows that quad.
The animation class does not actually render the quad on screen. It just keeps track of the ongoing frame in the animation. Any render function can use this object to query the current frame and render it.
main.lua Lines 51 - 58 => The idle animation object is defined here. An interval is passed into it for the sake of consistency even though it doesn’t make a difference. He’s also defined
movingAnimation after that. Notice that the sprites 10 and 11 are the sprites which show the character moving (the walking away frames).
Lines 60 and 67 =>
currentAnimation keeps track of the animation currently activated.
direction keeps track which direction the player is walking in so that we can flip conditionally flip the sprite while rendering.
Line 150 =>
love.graphics.draw() gets the current frame from the animation object by calling
getCurrentFrame(). The animation object keeps updating the frame as time progresses and the render function gets the current frame to show from the animation object.
Lines 150 - 160 => All these lines are just one function call. When a sprite is flipped, it is done so with respect to it’s origin. Default origin is the top left corner. So by flipping the character it will drift a bit from the center. So while calling
love.graphics.draw() the last argument we pass, we set the origin to be the center of the sprite. Normally the sprite is recentered when we flip sprites and want consistency.
If you’re wondering how does the current animation switch?
Lines 115 - 125 => Continuously checks for keypresses. If right or left key is pressed, change
movingAnimation else let it remain
Question:- How to jump?
On closer observation it becomes apparent that we need two states. One while going up and the other to indicate that we are coming down.
Why do we need two states?
While jumping we destroy blocks, when falling down we kill enemies. The inverse doesn’t hold true. We need a way to differentiate.
The two states share the same animation but have different behaviour.
Main.lua 120 =>
if key == 'space' and characterDY == 0 then we ensure that DY is zero so that the player only jumps when he’s on ground. If he’s mid air, DY isn’t going to be zero and allowing him to jump in such a state will allow a player to jump infinitely in mid air.
Main.lua 132 - 135 => We haven’t reached collision detection yet so we quickly hack it up. If the character reaches below the ground set his dy to zero so that he stops moving.
Colton found the easiest way to think and reason about generating tiles for this game was to go column by column. First you start with no tiles (just the sky denoted by a background colour). Then you go column by column and ask do I want ground here, if yes fill it will tiles from the bottom edge till ground level. Do you want a raised pillar here, if yes add a few more tiles post ground level. If you don’t want ground in that column (i.e a chasm or a ditch) don’t fill any tiles at all in that column. All this can be done with boolean flags and
Try thinking of ways to generate it row by row (horizontal strips across the screen) and it will dawn upon you that it’s much harder. Column by column helps to think of it in individual units (this column has a chasm, this has normal ground and so on) but row by row you are forced to deal with tiles that aren’t a part of the same structure.
You could easily extend this idea. Say you want a pillar of varying width you could introduce a width variable and fill up the next few columns with a pillar and decrement the width variable by one on every iteration.
Placing objects (grass, enemies, powerups etc) can be dealt with after you are done with the iteration that places all tiles. You can conditionally place objects on certain types of tiles (should not place grass on ditches and those sort of things). You can also keep track of where you’ve placed obstructions and place the next obstruction at a reasonable distance from the last.
He then goes ahead to show how a varying sprite sheet with a simple algorithm like the one described above can be used to generate rich levels. Assortment of sprites for the ground tiles, topper (thin layer near the top most tile added for visual appeal) tiles, backgrounds, obstacles make it look vivid.
The sprite sheet is well organised into similar chunks. This organisation provides easy programmatic access (splitting the sprite sheet into quads and accessing different variations of the same element. Most importantly the same can be done for all of them).
Topper with ground. How it’s done?
He’s just taking the topper and the tile set (ground tiles) from different parts of the sprite sheet. We don’t fix a place for toppers so as to keep it general enough to work with pillars and chasms. There’s a flag which indicates whether a tile has a topper or not. If it’s set to true if it’s the topmost tile of that column.
Splitting tile set and toppers?
Have a look at
tiles.png for a better idea of what we’re about to do.
level0/utils.lua => We need to write a four way nested loop to get the tiles and toppers. The first two levels of nesting is to actually get one of the 30 (3 X 10) different segments of tile sets. After that’s done, we’ll have to iterate again and get tiles across the X and Y dimensions again. So we’ll need a two level nested loop for that. Have a look at
GenerateTileSet(). It takes in the actual quads, the X and Y of the large segment (3 columns by 10 rows) and the size of each segment as well. Basically taking a segment of the sprite sheet and extracting the desired group of related tiles from it.
This is only possible as the sprite sheet is structured and laid out in an organised fashion. Programmatic access would not have been possible if they were randomly spread out.
How to put pillars?
Just put up tiles above ground level in that column.
You could have a flag spawn pillar which controls whether to have a pillar or not. You could also pass in the width or something and draw the same height multiple times and set the flag back to false.
main.lua Line 227 - 236 => Fill the entire map with the sky tile. So the level is basically empty at this point.
Once done we don’t have to worry about inserting or managing the table. We can just replace the stuff we want.
main.lua Line 239 - 259 => Go column by column to generate the level map. Randomly set a flag to generate a pillar or not. If the flag is true fill in positions 4, 5 & 6 with tiles (these are 3 positions above ground.) Also place a topper on the top most tile.
main.lua line 253 to 259 => Fills tiles to create the ground in that column. If a pillar exists don’t put a topper else place a topper as usual.
You can also create mario like steps instead of straight pillars in a similar manner. Keep reference to the previous height and thn iterate (each iteration reduce height). You could also keep track of width and keep increasing height till half way up and reduce height from there on.
Because of the code’s random nature it’s possible that two pillars spawn next to each other. Pseudo random generators can also surprise the person who wrote the code.
How to generate chasms?
You just skip a column. So at the start itself you randomly decide if you want a chasm in the block or not (in the code there’s an arbitrary set to
math.random(7) for 1 in a 7 chance, which in general is a bad practice).
I think (I’m not sure myself) the reason being you have no control over where the chasm ends up (like the very first step where the player stands) and it probably is a very raw approach, we could do better by having a few hand spun rules like at so many intervals, only after a certain point in the level.
Lua has no continue statements, so we use a goto statement and avoid rendering ground in the column. Using
::continue:: is a community accepted standard work around for the same.
Audience Question => Level generation do we do it all at once?
In mario we do it all at once. It’s pretty small and can easily fit in memory. Also it’s much easier to teach like that.
Incremental approach can be taken for infinite runners. You split the game into parts. As the user approaches the right end, generate the new level, obstacle etc and append it to the level. You can discard the level or obstacle that the user has already crossed (in infinite runners you normally can only move in one direction).
Audience Question => Do we render the entire level at once?
Yes . Since it’s a small game rendering the entire map 60 times a second isn’t a problem. Large game render only a certain section. You would write your code to ensure it renders only areas near the camera or the user.
Related resource from a previous lecture Games are Illusions.
We have a fixed 2D tile system. We can get to the tile from the x & y coordinates. We can use that to your advantage. We just have to check that particular tile. If you recall with AABB collision detection you have to iterate over all the tiles to check if it’s colliding with the player. In this case we know where the player is and we just can check that tile.
You can find the relevant function in mario/TileMap.lua. Also I’d recommend seeing slide#25 for more details.
Tile collisions while jumping up => Just check both the top edges of our character (which tile the head hits). Checking only one side will miss a collision if half his body is under a tile and the other half is not blocked by anything (and we happen to check the side which is not under anything). We just need the tile our player will hit. We get the index of the tile and check if it’s solid (property set via a flag in the table).
Notice how the top portions of the bounding box of the player touches the tiles (it therefore will trigger a collision even though the player’s head is not touching the box). Reduce one pixel from each side which checking for collisions so that it can go through gaps that are exactly of the same size without triggering a collision.
To get the tile index => 6th from top, 5th from top and so on , we just have to divide the player’s Y coordinate by a tile height and add one to it. The same things can be done for the X coordinate as well. We can then index into the tilemap table.
Bottom Collisions => We do something very similar for the bottom. This time we check for collisions with the character’s bottom edges. The coordinates of the bottom edges are (x, y + height) and (x + width, y + height). The same is done for checking right and left collisions.
Remember that top, right, bottom and left collisions are checked in different states. We need to check top collision while jumping for instance. We needn’t check for top collisions while walking towards the right for instance.
This is good to repeat again. You check for collisions at different points on the character based on the different actions he performs. Say he is jumping you check for collisions on the top left and right edges. Since the gaps are wide enough to let our player pass between them we only check the top edges while jumping. Assume that our player is 8 tiles tall, in that case we’d have to check for collisions across his entire body even while walking.
Game objects need a different treatment and are a little more complex. Have a table of some objects like snails and we have to iterate over them one by one to check for collisions. It’s because they have their own X & Y coordinates and unlike the tiles they aren’t fixed at a place.
Player Falling State
mario/src/states/entity/PlayerFallingState.lua Lines 62 - 78 => Iterate over every game object. If the game object is solid change dy (rate of change of height) to zero and move the player just above the object. If the object is consumable (gem for instance) remove the consumable object without doing anything to the player.
Depending on the object do different stuff. If it’s an enemy it dies or kills you, if power up show the relevant effect and so on.
Check resources for the link on Entity Component Systems.
How would you implement ladders?
Check for a collision with a ladder. If it’s true and the up arrow key is pressed then enter climb state. If reached the end enter walking state again.
You can generate a lot of objects based on functionality. For instance:-
isLethalagainst the object and on collision it should kill the player.
collidable = false. Bushes therefore don’t trigger anything when collided against.
solid = true. The user cannot pass or move through them.
The above set properties are tested for and certain actions can then be triggered depending on these properties. Open
mario/src/Player.lua and have a look at the methods
Player:checkObjectCollisions() to see how they are used.
Audience Question => Wouldn’t it be better to have a helper class that takes care of creating objects?
Such a pattern is called a Factory Pattern (or at least is very similar in spirit).
At this scale it isn’t necessary. So he just created one class called
GameObject that abstracts away creation of all objects. He also felt the need to reduce the number of files to simplify the code and increase comprehension. If Colton were to design a large game he would probably create a class for each kind of object like a bush class, a gem class and so on. In a large game there’d be many kinds of gems, bushes and so on. The class would take optional parameters to create different variations of bushes and so on.
GameObject class is abstracted enough to allow for custom event handlers that can be tested for conditions. We can thus define custom behaviour for them. For instance a gem is consumable but a bush isn’t. So on collision we check if the flag
consumable is true and then we call a custom function.
He again briefly hints at Entity Component Systems. It’s a pretty neat way of modelling stuff. You just add components like consumable to an object such as a gem.
Check resources and check out the link on Factory Patterns.