Programming Games

Class 11 - Platforms

Play homework

VVVVVV

VVVVVV by Terry Cavanaugh

Topics

Description

This week’s class is where we continue to build out our basic platformer, adding mechanics, physics, sprites and animations.

Class notes

Baseline 2d platformer

In this tutorial, we will be creating a “Baseline 2D Platformer”. In this context, “baseline” means the basic functions. Therefore, in a baseline 2d platformer, you can see or experience the following:

platform = {}

function love.load()
        -- This is the height and the width of the platform.
	platform.width = love.graphics.getWidth()    -- This makes the platform as wide as the whole game window.
	platform.height = love.graphics.getHeight()  -- This makes the platform as tall as the whole game window.
        
        -- This is the coordinates where the platform will be rendered.
	platform.x = 0                               -- This starts drawing the platform at the left edge of the game window.
	platform.y = platform.height / 2             -- This starts drawing the platform at the very middle of the game window
end

function love.update(dt)

end

function love.draw()
	love.graphics.setColor(1, 1, 1)        -- This sets the platform color to white.

        -- The platform will now be drawn as a white rectangle while taking in the variables we declared above.
	love.graphics.rectangle('fill', platform.x, platform.y, platform.width, platform.height)
end

Nice! Now we have a simple white platform!

Adding a player

Next will be the player character. For tutorial purposes, the player character will be a 32x32 purple block. This time, instead of using the love.graphics.rectangle, we will use love.graphics.draw. The reason for this is because the purple block would be an external file. This way, you can learn how to draw an image “internally” as well as how to draw an “external” image. You can get the purple block here.

Note: Just as a quick reference, [love.graphics.rectangle](https://www.love2d.org/wiki/love.graphics.rectangle) lets you draw a rectangle in the game window. We can consider this as a form of "internal-file rendering" (not the exact or proper term) since we are declaring its (possible) parameters in the game's code. On the other hand, [love.graphics.draw](https://www.love2d.org/wiki/love.graphics.draw) lets you draw an image in the window - as long as the file type is supported. We can consider this as a form of "external-file rendering" (again, not the exact or proper term) since the image we are rendering is an external file being called by the game's code.
...
player = {}  -- Add this below the platform variable

function love.load()
	...
        -- Add this below the platform variables.

        -- This is the coordinates where the player character will be rendered.
	player.x = love.graphics.getWidth() / 2   -- This sets the player at the middle of the screen based on the width of the game window. 
	player.y = love.graphics.getHeight() / 2  -- This sets the player at the middle of the screen based on the height of the game window. 

        -- This calls the file named "purple.png" and puts it in the variable called player.img.
	player.img = love.graphics.newImage('purple.png')
end

function love.update(dt)

end

function love.draw()
	...
        -- Add this below the love.graphics.rectangle line.
        
        -- This draws the player.
	love.graphics.draw(player.img, player.x, player.y, 0, 1, 1, 0, 32)
end

All right! Now the player exists!

Player movement

We have the platform, we have the player! Time to make this guy move. Remember that in 2d platformers, the player must be able to move left and right! We can’t just have the player move forward to the right forever. To do this, we need to declare the speed of the player’s movement as well as assign keyboard inputs so that when the player presses a certain button, the character will move left or right depending on what is coded.

function love.load()
	...
	player.speed = 200    -- This is the player's speed. This value can be change based on your liking.
end

function love.update(dt)
        -- This is how to assign keyboard inputs.
        
	if love.keyboard.isDown('right') then                    -- When the player presses and holds down the "D" button:
		player.x = player.x + (player.speed * dt)    -- The player moves to the right.
	elseif love.keyboard.isDown('left') then                -- When the player presses and holds down the "A" button:
		player.x = player.x - (player.speed * dt)    -- The player moves to the left.
	end
end

If you run the code, the player can now move left and right! HOWEVER! There is a problem. If you haven’t noticed by now, if you keep on moving the character to the right (or to the left), it will go pass the game window and be out of the player’s vision. For tutorial purposes, we don’t want that. So inside the love.update() function:

function love.update(dt)
	if love.keyboard.isDown('right') then
		-- This makes sure that the character doesn't go pass the game window's right edge.
		if player.x < (love.graphics.getWidth() - player.img:getWidth()) then
			player.x = player.x + (player.speed * dt)
		end
	elseif love.keyboard.isDown('left') then
		-- This makes sure that the character doesn't go pass the game window's left edge.
		if player.x > 0 then 
			player.x = player.x - (player.speed * dt)
		end
	end
end

Now the player can’t get pass the screen. This way the player character will always be within the constraints of the game window.

Jumping

All that’s left now is jumping. You can’t have a 2d platformer without jumping. Without it, player’s can’t really get over obstacles like pits or walls. It’s time for some basic physics. For something to jump and fall, an object (in this case, the player’s character) needs to have a Y-Axis Velocity, a Jump Height, and Gravity. For tutorial purposes, we will not be taking into consideration an object’s mass however it is worth mentioning that giving mass to an object can change the way it’s physics works. First, let’s declare the three things we’ve mentioned earlier. I will also be adding a variable called “ground”. This is to indicate where the ground is. Think of it as the place where the feet should touch and land after jumping.

function love.load()
	...
        -- Add this below the player.img
	player.ground = player.y     -- This makes the character land on the plaform.

	player.y_velocity = 0        -- Whenever the character hasn't jumped yet, the Y-Axis velocity is always at 0.

	player.jump_height = -300    -- Whenever the character jumps, he can reach this height.
	player.gravity = -500        -- Whenever the character falls, he will descend at this rate.
end

Note: Feel free to change the values of player.jump_height and player.gravity to your liking. Just remember that for the most part, unless you want the game’s physics to not work the usual way, player.gravity should be greater than player.jump_height

After declaring the variables, we will now proceed to making the character jump. To do this, we need to assign a key that will make the character jump.

...
function love.update(dt)
	...
        -- Add below the right key assignment. 

        -- This is in charge of player jumping.
	if love.keyboard.isDown('space') then                     -- Whenever the player presses or holds down the Spacebar:

                -- The game checks if the player is on the ground. Remember that when the player is on the ground, Y-Axis Velocity = 0.
		if player.y_velocity == 0 then
			player.y_velocity = player.jump_height    -- The player's Y-Axis Velocity is set to it's Jump Height.
		end
	end
end

We aren’t done yet. If you try to jump, the character won’t jump yet. This is because we haven’t added the physics of the jump yet.

...
function love.update(dt)
	...
        -- Add below the jump key assignment.

        -- This is in charge of the jump physics.
        if player.y_velocity ~= 0 then                                      -- The game checks if player has "jumped" and left the ground.
		player.y = player.y + player.y_velocity * dt                -- This makes the character ascend/jump.
		player.y_velocity = player.y_velocity - player.gravity * dt -- This applies the gravity to the character.
	end
        
        -- This is in charge of collision, making sure that the character lands on the ground.
        if player.y > player.ground then    -- The game checks if the player has jumped.
		player.y_velocity = 0       -- The Y-Axis Velocity is set back to 0 meaning the character is on the ground again.
    		player.y = player.ground    -- The Y-Axis Velocity is set back to 0 meaning the character is on the ground again.
	end
end

And finally, the character can now jump!

Congratulations! You have created a Baseline 2D Platformer! Now for reference, the whole code:

Namespace and binding objects

In Pico-8, we use a global namespace. All variables and functions are by default global.

In Love2d, our special library classes and functions are denoted with the love class name.

We can bind methods such that we create an easier to type shortcut. We do this in our global variables section, and then proceed to use them.

gfx = love.graphics

--now we can use it
function love.draw()
  gfx.print("test",20,20)
end

Transformations and “camera”

Now we’ll add a camera that follows the player. We can use this as a base that will allow us to build more complex levels that are larger than a single screen.

We use the concept of a push/pop matrix. When we “push” we are storing the current coordinate system to a memory stack, and when we “pop” we are restoring that coordinate system. They are always used together. This allows you to treat the drawing instructions between as a single placed down “layer” applied to the game screen.

  love.graphics.push()
  
  windowWidth = love.graphics.getWidth()
  windowHeight = love.graphics.getHeight()
  love.graphics.translate(-player.x+(windowWidth/2), -player.y+(windowHeight/2))
  
  -- draw scene here
  love.graphics.setColor(1, 1, 1)
  love.graphics.rectangle('fill', platform.x, platform.y, platform.width, platform.height)

  love.graphics.draw(player.img, player.x, player.y, 0, 1, 1, 0, 32)

  love.graphics.pop()
  -- draw gui here
  love.graphics.print("Score: 0",20,20)

The “score” we are printing on the screen doesn’t move. This is because it is not affected by the push/pop matrix since the code is outside of it by being after the pop() command.

Data structures

Now that we have the basics of our “game engine” set up we can concentrate on our gameplay. In other words, we have our “mechanics” in place, but we need to now craft compelling levels. To do so, we need to be able to save the placement of our platforms.

We can create a table to hold our platform data.

function love.load()
  platforms = {
      {x = 100, y = 500, width = 200, height = 20},
      {x = 400, y = 400, width = 200, height = 20},
      {x = 700, y = 300, width = 200, height = 20},
  }
end

We can draw all of our platforms by looping through them and drawing them to the screen.

function love.draw()
    for _, platform in ipairs(platforms) do
        love.graphics.rectangle("fill", platform.x, platform.y, platform.width, platform.height)
    end
end

We can also create a table to hold all “collectibles.”

local collectibles = {
    {x = 150, y = 450, width = 20, height = 20, collected = false},
    {x = 450, y = 350, width = 20, height = 20, collected = false},
}

And just like our platforms, loop through them to draw them in the love.draw() method.

    for _, collectible in ipairs(collectibles) do
        if not collectible.collected then
            love.graphics.rectangle("fill", collectible.x, collectible.y, collectible.width, collectible.height)
        end
    end

Lastly we need to add in overlap/collision detection to pick up any collectibles that the player “collects” through collision. This is the same collision detection we implemented previously in Pico-8. This code gets added to the love.update()

We are checking to see if the player overlaps with each collectible object. If it does, we change that individual object’s collected attribute to true to mark that it has been collected. As with our previous overlap function, it is easier to check that the player is NOT to the left or to the right or above or below the object. If it is NOT, for all of these, then it MUST BE overlapping.

for _, collectible in ipairs(collectibles) do
        if not collectible.collected and 
           player.x < collectible.x + collectible.width and 
           player.x + player.width > collectible.x and 
           player.y < collectible.y + collectible.height and 
           player.y + player.height > collectible.y then
            collectible.collected = true
        end
    end

Obstacles

We can add in obstacles as a challenge in the level.

We will add in spikes.

local spikes = {
    {x = 300, y = 500, width = 50, height = 10},
}

Next we draw those spikes in our our draw, just the same as our platforms and collectibles.

 for _, spike in ipairs(spikes) do
        love.graphics.rectangle("fill", spike.x, spike.y, spike.width, spike.height)
    end

Finally, we add collision detection between the player and the spikes.

   for _, spike in ipairs(spikes) do
        if player.x < spike.x + spike.width and 
           player.x + player.width > spike.x and 
           player.y < spike.y + spike.height and 
           player.y + player.height > spike.y then
            -- Reset player position or handle game over
            player.x = 50
            player.y = 450
        end
    end

Code homework

For homework this week you will complete a DRAFT version of your platformer game with one complete level.

A complete draft includes:

Optional:

The final version of your platformer will be due November 20.

Credits