PICO-8



Nerdy Pong Cartridge



This is a classic Pong game, kept as clean and simple as possible.

It uses only code and a few Sound FX. It does not use any Sprites or Map. So everything is drawn on the screen using only code!


This will be made into a video tutorial in the future. So if you have suggestions for improvements, or what other games would make great Bite-Size game tutorials, then send us a message either on Facebook, Youtube, or Email. theNerdyTeachers@gmail.com





Challenges!

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

1. Make your own Sound FX.

2. Change the ball from a square to a circle. (Hint)

2. Add some background music.

3. Use the Main Menu tutorials to add a Menu and other scenes.

Nerdy Pong!


Video Coming Soon!



Explanation of Code!


  • 0. What do I Need to Know?

      Before you start making this game, you should know some of these basics of coding.

      1. Variables

      2. If Statements

      3. Tables






      This game uses tables to hold almost all of the variables. So it is important to either already be comfortable reading and writing code with tables, or at least be prepared to focus on learning that here.

      You will see a table for the player, computer, and the ball. These each hold similar information about each of those game objects, such as the position (x,y), the size (w,h), and the color (c).






  • 1. Variables and Set Up the Game

      What is a variable?
      player_points=0
      com_points=0
      scored=""
      

      These are the first variables created for this game. By being set here, outside of any function, they will not be reset when the game resets. So the points will be remembered while playing multiple games.


      player_points = a number for the player's starting score.

      com_points = a number for the computer's starting score.

      scored = a word ("string") for who scored the last point. (Used for playing different sounds.)

      What is a string?




      PICO-8 runs _init() ("initialize") as the first function, before even _update() or _draw().

      Even though _init() is optional, in these bite-size games, we often use it to set up the game variables and create a good point to reset the game each time we want to play a new round.

      function _init()
          --variables
      
          player={
              x=10,
              y=50,
              c=12,
              w=2,
              h=10,
              speed=1
          }
      
          com={
              x=117,
              y=50,
              c=8,
              w=2,
              h=10,
              speed=0.75
          }
      
          ball={
              x=63,
              y=63,
              c=7,
              w=2,
              dx=0.5,
              dy=0.5,
              speed=1,
              speedup=0.05
          }
      
          ...
      end
      

      There are 3 tables created here:

      What is a table?

      player holds all of the data specific to the player's paddle on the left.

      com ("computer") holds all of the data specific to the computer's paddle on the right.

      ball holds all of the data specific to the game ball (square).



      Inside each of those tables are similar keys that will be the variables used often in the code.

      x = the position on the X axis (the number of pixels from the left).

      y = the position on the Y axis (the number of pixels from the top).

      c = ("color") a number from 0 to 15 that represents one of the PICO-8 color options.

      w = ("width") a number of pixels wide for a paddle or ball.

      h = ("height") a number of pixels tall for a paddle.

      speed = a number for how many pixels a paddle or ball should move each frame.

      speedup = a number for how much the ball should speed up by when it hits a paddle.

      dx = ("delta/change in X") a number for the X axis momentum on the ball to move left and right.

      dy = ("delta/change in Y") a number for the Y axis momentum on the ball to move up and down.


      Most of those are self explanatory, but DX and DY are often harder to understand if you are using them for the first time. They are well explained in the Platformer Tutorials (#3 and #5).

      In this game, they are used for keeping track of the current direction and speed of the ball.

      If DX is positive, then the ball is moving right.

      If DX is negative, then the ball is moving left.

      If DY is positive, then the ball is moving down.

      If DY is negative, then the ball is moving up.



      The DX and DY are not only positive or negative, but also can be different amounts, which means how fast the ball is moving.

      If DX is close to zero, then the ball moves slowly left or right.

      If DX is far from zero, then the ball moves quickly left or right.

      If DY is close to zero, then the ball moves slowly up or down.

      If DY is far from zero, then the ball moves quickly up or down.



      When the DX and DY work together at different amounts, it creates a ball that can bounce at different angles, making the ball movement more interesting.






      function _init()
          --variables
      
          ...
      
          --sound
          if scored=="player" then
              sfx(3)
          elseif scored=="com" then
              sfx(4)
          else
              sfx(5)
          end
      end
      

      This code is also in the _init() function but checks variables that were created outside of it.

      The code reads as follows: If the scored variable is the word "player", then play sound effect number 3 (happy sound). But if it is the word "com", then play sound effect number 4 (sad sound). And if neither of those, then play sound effect number 5 (basic intro sound)


      So this code checks for who scored the last point, and plays the appropriate sound whenever the game resets.






  • 2. Draw the Game

      function _draw()
          cls()
      
          --court
          ...
      
          --ball
          ...
      
          --player
          ...
      
          --computer
          ...
      
          --scores
          ...
      end
      

      This _draw() function will be run after _init() and handles everything that is seen in the game screen.

      cls() = clear the screen

      It is common to clear the screen before drawing the updated game objects every frame. You can best understand why this is important to do by simply deleting it from the code, and running the game. You should see the paddles and ball leaving a trail behind them, because where they were is not erased, so we quickly lose track of where they are currently.

      The following sections are the code that should go inside of the _draw() function.






      --court
      rect(0,10,127,127,6)
      line(63,10,63,127,6)
      

      Here we will draw the outline and center line of the pong "court".

      rect() = "rectangle" draws the outline of a rectangle.

      How to Draw a Rectangle?

      line() = draws a line.

      How to Draw a Line?

      If you are wondering what those numbers mean, you should be able to figure it out by clicking on the links above to the guide page. And try experimenting with those numbers and see how the rectangle and line changes.



      Basically, we set the court's outline to start from the top left corner at coordinate (0,10) which is a little lower from the top, to make room for the points. Then we set the court's bottom left corner at coordinate (127,127) which is the bottom left corner of the game screen. And finally we set the whole rectangle to be color #6 which is a light gray.

      For the center line, we set a line that starts at coordinate (63,10) which is middle screen and at the top edge of the rectangle. Then we set the line to end at coordinate (63,127) which is middle screen and at the lower edge of the rectangle.






      --ball
      rectfill(
          ball.x,
          ball.y,
          ball.x+ball.w,
          ball.y+ball.w,
          ball.c
      )
      

      Here is the code used to draw the ball as a square.

      rectfill() = "filled rectangle" draws the outline of a rectangle and fills it with the same color.

      How to Draw a Filled Rectangle?

      This code creates a square "ball" using the variables that we set up in the ball table already.

      Anytime you see ball.something, that means we are retrieving the data from the ball table.



      So we start the ball's rectangle at coordinate (x,y) from the ball table. Then we end it at coordinate (x+w,y+w) from the ball table. So if the width of the ball is set to 2, then the rectangle will end 2 pixels to the right and 2 pixels down from wherever the ball X and Y is.

      Finally, we set the ball's color to the color number set as c in the ball table.






      --player
      rectfill(
          player.x,
          player.y,
          player.x+player.w,
          player.y+player.h,
          player.c
      )
      
      --computer
      rectfill(
          com.x,
          com.y,
          com.x+com.w,
          com.y+com.h,
          com.c
      )
      

      Here is the code used to draw the player's paddle and the computer's paddle as tall rectangles.

      rectfill() = "filled rectangle" draws the outline of a rectangle and fills it with the same color.

      How to Draw a Filled Rectangle?

      We'll explain how the player's paddle uses the variables that we set up in the player table.

      Just like the ball, anytime you see player.something, that means we are retrieving the data from the player table.



      So we start the player's rectangle at coordinate (x,y) from the player table. Then we end it at coordinate (x+w,y+h) from the player table. Since the ball is a square, then we only need a width and use the same width for the ball's height. But for the paddles, they should have a thin width and a tall height.

      Finally, we set the paddle color to be the color number of c in the player table.

      The exact same is done for the computer's paddle.






      --scores
      print(player_points,20,2,player.c)
      print(com_points,100,2,com.c)
      

      The last game objects to draw on the screen are the scores.

      They first print the numbers that are set in the variables player_points and com_points. And at the next two numbers' coordinates. So the player's score will be at (20,2) while the computer's score will be at (100,2).

      Finally, the last numbers given to the print() functions are the color to print the scores in. To make it simple, the scores match the player and computer's paddle colors.






  • 3. Update the Game

      This is the _update() function of the game. But you may notice that we are actually using the faster _update60 to get smoother movement.

      function _update60()
          --player controls
          ...
      
          --computer controls
          ...
      
          --collide with com
          ...
      
          --collide with player
          ...
      
          --collide with court
          ...
      
          --score
          ...
      
          --ball movement
          ...
      end
      

      This opens the _update60() function and shows the different sections that go inside. We will go over each section of code below:






      --player controls
      if btn(⬆️)
      and player.y>10 then
          player.y-=player.speed
      end
      if btn(⬇️)
      and player.y+player.h<127 then
          player.y+=player.speed
      end
      

      This part handles the player controls. The player only uses two buttons in this simplified game, up and down.

      So we first check if the player presses up or down.

      Then we also check if the player's Y position is more than 10 or less than 127. We do this to limit the player's paddle movement to inside the "court" which starts at 10 and ends at 127 vertically. The player's Y position is the top pixel, so we add the player's height when checking the bottom edge.

      If those checks fail, then the player does not move, even when pressing a button. This will feel like the player hit a wall.


      If those checks pass, then we allow the player's paddle to move by either adding or subtracting the player's speed to the player's Y position. Subtract to move upwards. Add to move downwards.






      --computer controls
      mid_com = com.y+(com.h/2)
      if ball.dx>0 then
          if mid_com > ball.y
          and com.y>10 then
              com.y-=com.speed
          end
          if mid_com < ball.y
          and com.y+com.h<127 then
              com.y+=com.speed
          end
      else
          if mid_com > 73 then
              com.y-=com.speed
          end
          if mid_com < 53 then
              com.y+=com.speed
          end
      end
      

      This part handles the computer's controls. The computer has a simple AI built into it here.

      First we check if the ball.dx is more than zero, which would mean that the ball is moving towards the right because it is a positive number.

      We check for this because the computer will do something different depending on whether the ball is moving toward or away from the computer's paddle.


      If the ball is coming toward the computer, then we want the computer's paddle to move either up or down, depending where the ball is. It does not predict where the ball will be.

      mid_com is a new variable created to hold the position middle of the computer's paddle which is found by doing this math: com.y + (com.h/2). That takes the computer's Y position (at the top) and adds half of the com's height.

      We are able to compare the middle of the computer's paddle to the position of the ball to know whether the computer needs to move up or down.

      So if the mid_com is more than the ball's Y (ball.y) then that means the computer is lower on the screen than the ball. So if that is true, then the computer should move up by subtracting from the computer's Y position. And opposite if the ball is higher.

      We also limit the computer's paddle to the "court" by also checking if the computer's Y position is more than 10 and less than 127, just like the player.


      Now going back to the first check: (if ball.dx>0)

      If that is false, then the code jumps down the the part inside else.


      Instead of following the ball, the computer acts a little more human-like and waits for the ball to start coming back. But it doesn't just stop moving. We made it a little smarter than that.

      So it checks if the middle of the computer's paddle (mid_com) is more than 73 or less than 53, which means that the computer is either far to the top or bottom of the court. And if either of those are true, then the computer will move up or down to return to a more middle court position to make it easier to get to the ball when it starts coming back.






      --collide with com
      if ball.dx>0
      and ball.x>=com.x
      and ball.x<=com.x+com.w
      and ball.y>=com.y
      and ball.y<=com.y+com.h
      then
          ball.dx=-(ball.dx+ball.speedup)
          sfx(0)
      end
      

      This code checks if the ball collides with the computer's paddle.

      There are quite a few checks happening here that all must be true for the ball to be known to collide. Basically, we first check if the ball is moving towards the computer with ball.dx>0.

      Then we check if the ball's X and Y are inside the computer paddle's rectangle.

      ball.x>=com.x checks if the ball is to the right of the computer paddle's left side.

      ball.x<=com.x+com.w checks if the ball is to the left of the computer paddle's right side.

      ball.y>=com.y checks if the ball is below the computer paddle's top side.

      ball.y<=com.y+com.h checks if the ball is above of the computer paddle's bottom side.


      Together, if all of these checks are true, then that means the ball is inside the computer's paddle.

      You could make the collision better by predicting where the ball will be on the next frame, and by comparing the right side of the ball, but we'll leave that as a challenge for you if you want.

      Anyway, if the ball does collide, then first we reverse the ball's direction from moving right to moving left.

      To do that, all you need to do is take the ball's DX and flip it from a positive to a negative number. ball.dx+=-ball.dx is all that is needed to flip a number from positive to negative, or negative to positive.

      But we also want to speed up the ball every time it hits a paddle. So we add the ball's speedup variable to the ball's dx before flipping positive to negative. We do that with this math: -(ball.dx+ball.speedup).

      Finally, we also want to play a sound effect (#0) when the ball hits the paddle. So we use the sfx() function to play it.






      --collide with player
      if ball.dx<0
      and ball.x>=player.x
      and ball.x<=player.x+player.w
      and ball.y>=player.y
      and ball.y<=player.y+player.h
      then
          ball.dx=-(ball.dx-ball.speedup)
          sfx(1)
      end
      

      This is basically the exact same as checking collision with the computer above, only using the opposide direction, and the player's variables.

      The other difference is that if it collides, then we play sound effect #1 instead of #0.






      --collide with court
      if ball.y>=127
      or ball.y<=10 then
          ball.dy=-ball.dy
          sfx(2)
      end
      

      Here we will check if the ball collides with the bottom and top of the court.

      The top of the court is at 10, and the bottom of the court is at 127, so we simply check if the ball's Y position is above the top or below the bottom.

      If so, then we just flip the ball's DY to be the opposite negative or positive. Similar to flipping the DX when it hits the paddle, we do ball.dy=-ball.dy and we don't speed the ball up, so it's as simple as that.

      Lastly, sfx(2) plays a different sound than hitting either of the paddles, sound effect #2.






      --score
      if ball.x>127 then
          player_points+=1
          scored="player"
          _init() --reset game
      end
      if ball.x<0 then
          com_points+=1
          scored="com"
          _init() --reset game
      end
      

      This code detects if the ball scores a point, gives the point to the correct side, and resets the game.

      if ball.x>127 then checks if the ball has moved beyond the right side of the screen, meaning the player scored a point.

      if ball.x<0 then checks if the ball has moved beyond the left side of the screen, meaning the computer scored a point.


      player_points+=1 adds 1 to the player's score.

      com_points+=1 adds 1 to the computer's score.


      scored="player" or scored="com" remembers who scored the last point when the game resets. This is used to play a happy or sad sound effect at the start of the next round.


      _init() calls the whole _init function to run again and reset all the game variables except for the score.






      --ball movement
      ball.x+=ball.dx
      ball.y+=ball.dy
      

      The last thing we do in this game is apply the momentum (DX and DY) to the ball's position.

      We simply add the ball's DX to the ball's X position, and add the ball's DY to the ball's Y position.


      That is is for this classic bite-size game! Try it out yourself by either following the video, downloading the game cartridge, or copying the full code below. For an extra challenge or some suggestions of how to improve this game yourself, check out the "Challenges" section on the left and near the top of this page.






  • Full Code!

      
      player_points=0
      com_points=0
      scored=""
      
      function _init()
          --variables
          player={
              x=10,
              y=50,
              c=12,
              w=2,
              h=10,
              speed=1
          }
          com={
              x=117,
              y=50,
              c=8,
              w=2,
              h=10,
              speed=0.75
          }
          ball={
              x=63,
              y=63,
              c=7,
              w=2,
              dx=0.5,
              dy=0.5,
              speed=1,
              speedup=0.05
          }
          --sound
          if scored=="player" then
              sfx(3)
          elseif scored=="com" then
              sfx(4)
          else
              sfx(5)
          end
      end
      
      function _draw()
          cls()
      
          --court
          rect(0,10,127,127,6)
          line(63,10,63,127,6)
      
          --ball
          rectfill(
              ball.x,
              ball.y,
              ball.x+ball.w,
              ball.y+ball.w,
              ball.c
          )
      
          --player
          rectfill(
              player.x,
              player.y,
              player.x+player.w,
              player.y+player.h,
              player.c
          )
      
          --computer
          rectfill(
              com.x,
              com.y,
              com.x+com.w,
              com.y+com.h,
              com.c
          )
      
          --scores
          print(player_points,20,2,player.c)
          print(com_points,100,2,com.c)
      end
      
      function _update60()
          --player controls
          if btn(⬆️)
          and player.y>10 then
              player.y-=player.speed
          end
          if btn(⬇️)
          and player.y+player.h<127 then
              player.y+=player.speed
          end
      
          --computer controls
          mid_com = com.y+(com.h/2)
          if ball.dx>0 then
              if mid_com > ball.y
              and com.y>10 then
                  com.y-=com.speed
              end
              if mid_com < ball.y
              and com.y+com.h<127 then
                  com.y+=com.speed
              end
          else
              if mid_com > 73 then
                  com.y-=com.speed
              end
              if mid_com < 53 then
                  com.y+=com.speed
              end
          end
      
          --collide with com
          if ball.dx>0
          and ball.x>=com.x
          and ball.x<=com.x+com.w
          and ball.y>=com.y
          and ball.y<=com.y+com.h
          then
              ball.dx=-(ball.dx+ball.speedup)
              sfx(0)
          end
      
          --collide with player
          if ball.dx<0
          and ball.x>=player.x
          and ball.x<=player.x+player.w
          and ball.y>=player.y
          and ball.y<=player.y+player.h
          then
              ball.dx=-(ball.dx-ball.speedup)
              sfx(1)
          end
      
          --collide with court
          if ball.y>=127
          or ball.y<=10 then
              ball.dy=-ball.dy
              sfx(2)
          end
      
          --score
          if ball.x>127 then
              player_points+=1
              scored="player"
              _init() --reset game
          end
          if ball.x<0 then
              com_points+=1
              scored="com"
              _init() --reset game
          end
      
          --ball movement
          ball.x+=ball.dx
          ball.y+=ball.dy
      end
      


  • Play the Game!


font