PICO-8



Platformer Setup



Follow along with the videos to create a platformer game!


The video moves quickly so pause whenever you need.

The video doesn't explain everything, but this page does!


Make sure you understand why this code works or you won't be able to make your own games later!

After you made this platformer, change it up! Change the sprites, fiddle with the numbers, and even finish the game to see what you can create!





Table of Contents

Video
1Draw Player Sprites
2Draw Map Tiles
3Advanced Player Variables
4Map Collision Function
5Advanced Player Movement
6Game Level and Camera
7Improving Map Collision
8Improving Player Movement





Challenges!

After following the lesson, take on some of these challenges to test your knowledge and skills!


1. Draw your own player character, instead of this ninja.


2. Draw your own map tiles, and add more.


3. Design a complex map level.


4. Apply your own movement and physics settings.

How to make a Platformer Game!



Game Sprites!


  • Player Sprites

    Standing Idle

    Running

    Jumping, Falling, Sliding

  • Map Sprites

    Grass Tiles

    Dirt Tiles

    Bamboo Tiles

What is a sprite?

You can copy these sprites or make your own! Make sure you draw each of these sprites in the exact same place, because the code will look for their sprite number. But once you understand the code, you can draw as many sprites as you want, where ever you want!



Explanation of Code!


  • 1. Set up the Variables

      What is a variable?
      The Player Variables
      function _init()
          --add your variables here
      end
      

      By setting up your variables inside of the _init() function, you can easily reset your game by calling _init(), and the variables here have access to any custom functions you write. That might come in handy later so it's a good practice.


      player={
              sp=1,
              x=59,
              y=59,
              w=8,
              h=8,
              flp=false,
              dx=0,
              dy=0,
              max_dx=2,
              max_dy=3,
              acc=0.5,
              boost=4,
              anim=0,
              running=false,
              jumping=false,
              falling=false,
              sliding=false,
              landed=false
          }
      

      sp = "sprite" and holds the sprite number.

      x = holds the horizontal position of the player

      y = holds the vertical position of the player

      w = "width" and is how wide the player sprite is in number of pixels

      h = "height" and is how tall the player sprite is in number of pixels

      flp = "flip" and is true or false if the player faces left or right

      dx = "delta X" and is how much to change the player X position to move left or right

      dy = "delta Y" and is how much to change the player Y position to move up or down

      max_dx = "maximum delta X" and is the limit of how much the player can move left or right

      max_dy = "maximum delta Y" and is the limit of how much the player can move up or down

      acc = "acceleration" and is how much to add to the DX when running (to build up speed)

      boost = how much to add to the DY when jumping (the jump power)

      anim = "animation" and is the time decimal for when the last animation change occurred

      running, etc = movement status and they are true or false to remember what the player is currently doing.


      Tip 1: Set your game variables inside of the _init() function so that you can easily call _init() from anywhere in your game and it will reset those variables to restart your game. Also, PICO-8 will run _init() immediately after all the code is evaluated. So if you need to call a function to establish variables or create data tables, then it can be done here.


      Tip 2: Organize your variables in groups of game objects (such as players, enemies, bullets, items, etc.) by creating a table to hold the variables as keys and values.

      What are tables, keys, and values?






      The Physics Variables
      gravity=0.3
      friction=0.85
      

      gravity is how much we add to the dy to increase the falling speed.

      friction is how much we shrink the dx to slow horizontal movement down.

  • 2. Code Editor Tabs

      -->8
      --name of tab here

      -->8 = next tab

      This code does not appear in the PICO-8 code editor, but you will see it if you open .p8 files in other editors. And you will see it in the "Full Code" section below.

      Tabs separate the code nicely inside the PICO-8 editor. And you can name each tab by writing a comment directly after this, or on the first line of the new tab inside PICO-8.

  • 3. Map Collision Function

    • 3-A. Receiving Variables

        function collide_map(obj,aim,flag)
        
        end

        () = "parentheses" identify this code as a function. They can be empty for functions that run the exact same way every time. Or they can have a list of variables that will hold the values of whatever is passed to them.

        Passing variables to a function must be done in the same order that they are listed inside of the parentheses here.

        Only the values are passed, not the variable names. So a custom function can have its own variable names of these values.


        obj = "object" a common abbreviation for a game object's table in PICO-8, and also a common abbreviation for similar tables of information in other programming languages.


        aim = (shorter word for "direction") a string that will tell the function which direction the game object is moving, (left, right, up, or down) so that the function will know which way in front of the object to check for a map tile.


        flag = "flag number" which is a number from 0 to 7 that can be turned on for organizing different types of sprites. Flags can be used to identify sprites in any way that you want.

        In this code, we will use flag 0 to identify map tiles that can stop falling objects. And flag 1 will be used to identify map tiles that can stop jumping objects. This way, we can separate map tiles like platforms that allow jumping up through, but also can be stood on without falling down through.

    • 3-B. Local Variables

        local x=obj.x    local y=obj.y
        local w=obj.w    local h=obj.h

        local = a variable that is limited to a small region of code. It is the opposite of "global" which is a variable that can be 'seen' anywhere in the code.

        This set of local variables simply takes the values out of the game object's table (named obj by this function) and saves them to x, y, h, and w for easier writing of the function's code. Without these, then the rest of the function would have to refer to the table name everytime it wants to use one of the object's values.






        local x1=0    local y1=0
        local x2=0    local y2=0

        These local variables are only temporarily set to 0 and are created for holding the position information of a "behind-the-scenes" rectangle in front of the object, for where to check for map tiles.

        This game might only use 8x8 objects, but this function is written so that it can be used with any size objects.

    • 3-C. Hitbox position

        if aim=="left" then
           x1=x-1  y1=y
           x2=x    y2=y+h-1
        
        elseif aim=="right" then
           x1=x+w    y1=y
           x2=x+w+1  y2=y+h-1
        
        elseif aim=="up" then
           x1=x+1    y1=y-1
           x2=x+w-1  y2=y
        
        elseif aim=="down" then
           x1=x      y1=y+h
           x2=x+w    y2=y+h
        end
        

        The "hitbox" is the area where collision is checked. In the video, it is referred to as an "invisible rectangle", which is a true description of hitboxes because they are normally rectangular and not visible in the game.


        The above code will check which direction aim is set to, and then it sets the (x,y) coordinates of where the corners of the hitbox should be.






        This shows what the x1, y1, x2, y2 represent as the hitbox corner positions. And the hitbox should move based on the direction of the object. So if left, then the hitbox will be to the left. If right, then the hitbox will be adjusted to the right. If up, then the hitbox will be above. And if down, then the hitbox will be below.

        Using only the object's position (x,y) coordinates (knowing it is the top left corner of the sprite), and the object's size (w, h) width and height, we can adjust the hitbox to the appropriate location.

        We suggest you draw these out on a graph paper if you want to better understand how we figure them out. It's great practice for geometry, and this type of logic and math will be used a lot in game development.

    • 3-D. Convert Pixels to Tiles

        --pixels to tiles
        x1/=8    y1/=8
        x2/=8    y2/=8
        

        We will need to know the position of each corner of the hitbox in map tiles, which is 1 tile for every 8 pixels. So this simply divides our coordinates by 8 to find the map location (tiles) instead of the screen location (pixels).

    • 3-E. Check Collision

        if fget(mget(x1,y1), flag)
        or fget(mget(x1,y2), flag)
        or fget(mget(x2,y1), flag)
        or fget(mget(x2,y2), flag) then
            return true
        else
            return false
        end
        

        This last part of the map collision function does the actual checking if any one of the hitbox's corners is on a map tile, then return true, and if none are, then return false.


        fget() is a built-in PICO-8 function that takes a sprite number and a flag, then compares the flags on that sprite, with the flag being checked.

        The flag that will be looked for, is given to this whole collide_map() function as the last variable named flag. So the only thing we don't know, is which sprite number to check. So instead of putting a variable of a sprite number, we use another function that returns a sprite number.








        mget() is another built-in PICO-8 function that takes map coordinates (x,y), and finds the sprite number of the sprite that is in that location on the map.



        So mget() will take the map coordinates that we prepared, and give the sprite number of that location to fget(), which then compares that sprite's flags with the flag we want to find. And it will return true if the sprite has the flag, or false if it does not.

        Then the collide_map function returns true if any of those come out to be true, or false, if all four corners turn out to be false.

  • 4. Player Update Function

      function player_update()
      
      end

      player_update = a custom function that holds the player movement code. It should only be run once from inside the game _update() function. And it is its own function so that our code stays organized.


      () = "parentheses" identify this code as a function. They can be empty for functions that run the exact same way every time.






    • 4-A. Gravity and Friction

          --physics
            player.dy+=gravity
            player.dx*=friction
        

        gravity = the amount that DY will be increased by each update, used to constantly pull the player down the screen.


        friction = the amount that DX will be decreased by each update, used to constantly slow the player's horizontal movement (left or right).

    • 4-B. Controls (Run, Slide, Jump)

          --controls
        if btn(L) then
            player.dx-=player.acc
            player.running=true
            player.flp=true
        end
            if btn(R) then
            player.dx+=player.acc
            player.running=true
            player.flp=false
        end

        btn = "button" a built-in PICO-8 function that detects if a button is pressed and held down.


        L = "left" *uppercase L*
        PICO-8 converts uppercase letters to glyphs (tiny images) and so if you are editing inside of the PICO-8 code editor, then an uppercase L will appear as an arrow facing left. And this is understood as the correct button number (0).


        player.dx-=player.acc = "DX decreases by acceleration"
        The player table holds dx and acc (acceleration). This code takes the acceleration amount and subtracts it from the DX (how much X changes). So this does not immediately move the player to the left, but it slowly builds up momentum for moving the player left.


        R = "right" *uppercase R*
        Just like "L" above if you are editing inside of the PICO-8 code editor, then an uppercase R will appear as an arrow facing right. And this is understood as the correct button number (1).


        player.dx+=player.acc = "DX increases by acceleration"
        Just like acceleration is subtracted if moving left, it is added to the DX if moving right. And this will build up the DX momentum for moving the player right.


        player.running=true = a change in the player movement status.
        It is used to know what sprite to show in the animation code. And it can be used elsewhere for simple checks in your game where something should only happen if the player is running.


        player.flp=[true/false] = a flip in the player sprite.
        It is used when drawing the player sprite. If it is false, which is the default setting, then the player sprite will remain as you drew it. If it is true, then the player sprite will be flipped horizontally and facing the opposite direction.
        So if you drew your sprite facing right, then this will flip it to draw it facing left.






          --slide
        if player.running
        and not btn(L)
        and not btn(R)
        and not player.falling
        and not player.jumping then
            player.running=false
            player.sliding=true
        end
        

        This code simply changes the player movement status from running to sliding.

        It will only happen if the player is running, and the running buttons (left or right) are not being pressed, and the player is not falling or jumping because that is the only time a slide should happen.


        NOTE: not player.falling and not player.jumping can actually be simplified to:

        player.landed






          --jump
        if btnp(X)
        and player.landed then
            player.dy-=player.boost
            player.landed=false
        end
        

        This code checks if button X is pressed once and if the player is standing on a ground or platform.

        Then it will make the player jump by suddenly decreasing the dy by the BOOST amount. And since the player will leave the ground, turn landed off.

    • 4-C. Collision Check (Up and Down)

          --check collision up and down
        if player.dy>0 then
            player.falling=true
            player.landed=false
            player.jumping=false
        
            player.dy=limit_speed(player.dy,player.max_dy)
        
            ...
        

        if player.dy>0 then = check if the dy is a positive number.
        A positive dy means that the player will be moving down the screen. So with advanced movement, if you ever want to know if the player is falling, this is the check for that.

        player.falling=true = sets the falling value in our player table to on (true).
        Remember that this movement status is used in animation, and can be helpful anywhere in our game where something should only happen if the player is falling.

        player.landed=false = sets the landed value in our player table to off (false).
        Since the player is falling then they must not be standing on a ground or platform.

        player.jumping=false = sets the jumping value in our player table to off (false).
        They player may have been jumping, and the gravity just increased the dy from a negative number to a positive. But also, if the player is now falling, they must not be jumping.

        player.dy=limit_speed(player.dy,player.max_dy) = uses our custom function to limit the dy if it goes past max_dy. See the later section on how this limit_speed() function works.






          --check collision up and down (continued)
            ...
            if collide_map(player,"down",0) then
                player.landed=true
                player.falling=false
                player.dy=0
                player.y-=((player.y+player.h+1)%8)-1
            end
        

        if collide_map(player,"down",0) then = uses our map collision function from above to check if there is a map tile under the player ("down") that the player can stand on (flag 0).

        player.landed=true player.falling=false = change the movement status from falling to landed.

        player.dy=0 = The video says "kill momentum" for this line of code which means to suddenly set the dy to 0, so that the player movement is stopped right away. That should only happen if the player hits a solid object like a wall or ground tile.

        player.y-=(player.y+player.h)%8 = Reset the player position to just above a ground tile.
        This is explained in detail in the next section.






          --check collision up and down (continued)
        --
        elseif player.dy<0 then
            player.jumping=true
            if collide_map(player,"up",1) then
                player.dy=0
            end
        end
        

        elseif player.dy>0 then = check if the dy is a negative number.
        A negative dy means that the player will be moving up the screen. So with advanced movement, if you ever want to know if the player is jumping, this is the check for that.

        player.jumping=true = sets the jumping value in our player table to on (true).
        Remember that this movement status is used in animation, and can be helpful anywhere in our game where something should only happen if the player is jumping. So you don't have to check DY everytime.

        if collide_map(player,"up",1) then = uses our map collision function to check above the player ("up") for a map tile that cannot be jumped through (flag 1).

        player.dy=0 = "kills momentum" of the player movement upwards by suddenly setting the DY to 0.
        And since gravity is always going to pull the player down, it will add to the DY to make the player start falling until the player hits a ground tile.

    • 4-D. Ground Correction

        player.y-=((player.y+player.h+1)%8)-1

        player.y-= = the player position on the Y-axis, from the top of the screen.

        (player.y+player.h+1) = takes the player position and adds the player height, to find the screen position under the player sprite. Then adds 1 more pixel to be 2 pixels under the player.


        % = "modulo" or "modulus" finds the remainder when dividing.
        So 10 % 5 = 0 because 5 evenly divides into 10, so there is no remainder.
        But 11 % 5 = 1 because 1 is the remainder when dividing 11 by 5.


        %8 = divide something by 8, and find the remainder.


        When you put all of this together, we are checking if the number on the Y-axis of two pixels under the player, can be evenly divided by 8. And so, then %8 will be zero, and the player position will not change. But if the player is a little lower than a map tile (stuck in the ground), then %8 will find how many extra pixels the player is away from standing on the ground, and we add that many pixels to the player Y position immediately, so that the player never looks stuck.


        Another way to explain it would be, find how far off the player position is from being lined up with the map grid (made of 8x8 pixel tiles), then subtract that number of pixels from the player position to move the player up to be aligned with the map grid. And since the player is 8x8 pixels, the bottom of the player will be just above the top of the next tile on the grid, which would be a ground tile thanks to the collision function.


        For example, in the above image:
        A. player's Y + player's height + 1 pixel = 19
        B. 19 divided by 8 gives remainder 3
        C. 3 minus 1 equals 2
        (That tells us that the player is 2 pixels under a map grid line, the tops of map tiles)
        D. Subtract the 2 from the player Y to make player (an 8x8 sprite) lined up on the map grid (8x8 tiles)


        Try removing this line of code, and jump around in your game to see how you can sometimes get stuck too low in a ground tile.

  • 5. Player Animate Function

      function player_animate()
      ...
      end

      player_animate = a custom function that holds the player animation code. It should only be run once from inside the game _update() function. And it is its own function so that our code stays organized.


      () = "parentheses" identify this code as a function. They can be empty for functions that run the exact same way every time.






      ...
        if player.jumping then
          player.sp=7
        elseif player.falling then
          player.sp=8
        elseif player.sliding then
          player.sp=9
      ...

      These are pretty straight forward. When we only have one sprite pose for a certain action, then we simply check if that action is set to true, and set the player sprite to the sprite number of that pose.






      ...
        elseif player.running then
          if time()-player.anim>.1 then
            player.anim=time()
            player.sp+=1
            if player.sp>6 then
              player.sp=3
            end
          end
      ...

      For the running animation we are using four different sprites that need to cycle through if player.running is true. So this uses a simple animation code that cycles to the next sprite pose in the line, restarts the cycle at the right point, and also controls the animation timing.


      .1 = the animation wait time. So you can increase this number to lengthen the delay between the animation poses. That will slow the animation down. Decrease this number to shorten the delay, and so speed the animation up.






      ...
        else --player idle
          if time()-player.anim>.3 then
            player.anim=time()
            player.sp+=1
            if player.sp>2 then
              player.sp=1
            end
          end
        end
      ...

      For the idle (not moving) animation we are using two different sprites that need to cycle through if all the other movement statuses are false. So this uses a simple animation code that cycles to the next sprite pose in the line, restarts the cycle at the right point, and also controls the animation timing.


      .3 = the animation wait time. So you can increase this number to lengthen the delay between the animation poses. That will slow the animation down. Decrease this number to shorten the delay, and so speed the animation up.

  • 6. Camera and Map Limits

      Camera and Map Variables
      --simple camera
        cam_x=0
      
        --map limits
        map_start=0
        map_end=1024

      cam_x = camera X position
      Used for moving the camera left and right. In our example game, we are ignoring the Y axis (up and down), but you can also make a cam_y if you want to make levels taller than one game screen.


      map_start=0 = the pixel from the left of the map editor where the map level begins.
      If you create multiple levels then you only need to change this variable to position the camera at the beginning of that level.


      map_end=1024 = the pixel from the left of the map editor where the map level ends.
      In the example game, we only create one long level so the end of it is at pixel 1024.
      If you create multiple levels then you only need to change this variable to position the camera at the beginning of that level.






      Camera Update
      
      function _update()
      
        ...
      
        --simple camera
        cam_x=player.x-64+(player.w/2)
        if cam_x<map_start then
           cam_x=map_start
        end
        if cam_x>map_end-128 then
           cam_x=map_end-128
        end
        camera(cam_x,0)
      end

      cam_x=player.x-64+(player.w/2) = set the camera X position to always center the player.
      We subtract 64 because that is half the screen width, and the camera X is the left side of the screen. So the left side of the camera should be half a game screen behind the player, putting the player almost in the middle.
      Then we add half the player width, to really center the camera on the player because the player X is the left side of the player, but we want the middle of the player to be in the middle of the screen.




      You could put this code inside of its own function and name it camera_update(), just like we did with the player.


      We named this "simple" because there is a more advanced camera that is not locked to the player, but instead allows the player some room to move around in the center of the screen without adjusting the camera until the player moves far enough away.

  • 7. Player Limit to Map

      function player_update()
      
        ...
      
        --limit player to map
        if player.x<map_start then
          player.x=map_start
        end
        if player.x>map_end-player.w then
          player.x=map_end-player.w
        end
      end

      if player.x<map_start then = check if the player moves left, past the start of the map.


      player.x=map_start = set the player back to the map start
      This will reset the player position before the game finishes the update, so even if the player tries to move off to the left, the player will look like it is hitting a wall.


      if player.x>map_end-player.w then = check if the player moves right, past the end of the map.



      player.x=map_end-player.w = set the player back before the map end
      This will reset the player position before the game finishes the update, so even if the player tries to move off to the right, the player will look like it is hitting a wall.

      We must also subtract the player width because player X is the left side of the player, so to keep them on the game screen, we need them to be left of the map end. How much left? Well however wide they are.



  • Full Code!

      --variables
      
      function _init()
        player={
          sp=1,
          x=59,
          y=59,
          w=8,
          h=8,
          flp=false,
          dx=0,
          dy=0,
          max_dx=2,
          max_dy=3,
          acc=0.5,
          boost=4,
          anim=0,
          running=false,
          jumping=false,
          falling=false,
          sliding=false,
          landed=false
        }
      
        gravity=0.3
        friction=0.85
      
        --simple camera
        cam_x=0
      
        --map limits
        map_start=0
        map_end=1024
      end
      
      -->8
      --update and draw
      
      function _update()
        player_update()
        player_animate()
      
        --simple camera
        cam_x=player.x-64+(player.w/2)
        if cam_x<map_start then
           cam_x=map_start
        end
        if cam_x>map_end-128 then
           cam_x=map_end-128
        end
        camera(cam_x,0)
      end
      
      function _draw()
        cls()
        map(0,0)
        spr(player.sp,player.x,player.y,1,1,player.flp)
      end
      
      -->8
      --collisions
      
      function collide_map(obj,aim,flag)
       --obj = table needs x,y,w,h
       --aim = left,right,up,down
      
       local x=obj.x  local y=obj.y
       local w=obj.w  local h=obj.h
      
       local x1=0	 local y1=0
       local x2=0  local y2=0
      
       if aim=="left" then
         x1=x-1  y1=y
         x2=x    y2=y+h-1
      
       elseif aim=="right" then
         x1=x+w-1    y1=y
         x2=x+w  y2=y+h-1
      
       elseif aim=="up" then
         x1=x+2    y1=y-1
         x2=x+w-3  y2=y
      
       elseif aim=="down" then
         x1=x+2      y1=y+h
         x2=x+w-3    y2=y+h
       end
      
       --pixels to tiles
       x1/=8    y1/=8
       x2/=8    y2/=8
      
       if fget(mget(x1,y1), flag)
       or fget(mget(x1,y2), flag)
       or fget(mget(x2,y1), flag)
       or fget(mget(x2,y2), flag) then
         return true
       else
         return false
       end
      
      end
      
      -->8
      --player
      
      function player_update()
        --physics
        player.dy+=gravity
        player.dx*=friction
      
        --controls
        if btn(L) then
          player.dx-=player.acc
          player.running=true
          player.flp=true
        end
        if btn(R) then
          player.dx+=player.acc
          player.running=true
          player.flp=false
        end
      
        --slide
        if player.running
        and not btn(L)
        and not btn(R)
        and not player.falling
        and not player.jumping then
          player.running=false
          player.sliding=true
        end
      
        --jump
        if btnp(X)
        and player.landed then
          player.dy-=player.boost
          player.landed=false
        end
      
        --check collision up and down
        if player.dy>0 then
          player.falling=true
          player.landed=false
          player.jumping=false
      
          player.dy=limit_speed(player.dy,player.max_dy)
      
          if collide_map(player,"down",0) then
            player.landed=true
            player.falling=false
            player.dy=0
            player.y-=((player.y+player.h+1)%8)-1
          end
        elseif player.dy<0 then
          player.jumping=true
          if collide_map(player,"up",1) then
            player.dy=0
          end
        end
      
        --check collision left and right
        if player.dx<0 then
      
          player.dx=limit_speed(player.dx,player.max_dx)
      
          if collide_map(player,"left",1) then
            player.dx=0
          end
        elseif player.dx>0 then
      
          player.dx=limit_speed(player.dx,player.max_dx)
      
          if collide_map(player,"right",1) then
            player.dx=0
          end
        end
      
        --stop sliding
        if player.sliding then
          if abs(player.dx)<.2
          or player.running then
            player.dx=0
            player.sliding=false
          end
        end
      
        player.x+=player.dx
        player.y+=player.dy
      
        --limit player to map
        if player.x<map_start then
          player.x=map_start
        end
        if player.x>map_end-player.w then
          player.x=map_end-player.w
        end
      end
      
      function player_animate()
        if player.jumping then
          player.sp=7
        elseif player.falling then
          player.sp=8
        elseif player.sliding then
          player.sp=9
        elseif player.running then
          if time()-player.anim>.1 then
            player.anim=time()
            player.sp+=1
            if player.sp>6 then
              player.sp=3
            end
          end
        else --player idle
          if time()-player.anim>.3 then
            player.anim=time()
            player.sp+=1
            if player.sp>2 then
              player.sp=1
            end
          end
        end
      end
      
      function limit_speed(num,maximum)
        return mid(-maximum,num,maximum)
      end
      

font