PICO-8



Fishy Cartridge



This is a single player game similar to the flash game "Fishy!", where you become a tiny fish trying to eat smaller fish while avoiding larger fish.

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 colors of the fish. (Hint)

3. Add some background music.

4. Add animations to each fish.

5. Make the fish swim in different patterns.

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

Fishy


[ Video Coming Soon! ]



Explanation of Code!


  • 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

      4. Custom Functions

      This game uses tables to hold many 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, and the enemies. These each hold similar information about each of those game objects, such as the position (x,y), the sprite number, and the size.






  • 1. Variables and Set Up the Game

      What is a variable?
      --init
      function _init()
          --player table
          ...
      
          --game settings
          ...
      end
      

      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.

      Inside of the _init() function, we will create most of the variables of the game, and what numbers things should start at in the beginning of each game when we reset everything.






      --player table
      player={
          x = 60,
          y = 60,
          w = 0,
          h = 0,
          size = 2,
          sx = 0,
          sy = 0,
          dx = 0,
          dy = 0,
          speed = 0.08,
          flp = false
      }
      player = set_sprite(player)
      

      The first variable we put inside _init() is the player table that will hold all of the player's variables as keys.

      What is a table and keys?

      We can imagine this player table like this:



      player = a table that holds all of the data specific to the player's cell.

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

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

      w = ("width") the inner circle color of the player's cell.

      h = ("height") the outer circle color of the player's cell.

      size = a number to compare the player size with the enemy size and set the correct fish sprite.

      sx = a number used for the sprite X position on the sprite sheet

      sy = a number used for the sprite Y position on the sprite sheet

      dx = ("delta X") or ("change in X") a number for the X axis momentum on the player's fish to move left and right.

      dy = ("delta Y") or ("change in Y") a number for the Y axis momentum on the player's fish to move up and down.

      speed = how much we will add to the player's momentum every time a button is pressed.

      flp = ("flip") a boolean (true or false) variable that will know whether to flip the sprite left or right.


      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 player.

      DX : if positive, then the fish is moving right. If negative, then the fish is moving left.

      DY : if positive, then the fish is moving down. If negative, then the fish is moving up.



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

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

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

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

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



      When the DX and DY work together at different amounts, it creates a fish that can swim around in any direction, making the player movement more interesting.






      Immediately at the end of the player table, we call a function to run called set_sprite() and pass the whole player table to that function.

      We will explain and write the function later but just know that it will calculate which fish sprite to set the player to and set the w, h, sx, and sy variables inside the player table. If you want to know now why we need to use those variables to draw a sprite of any size in our PICO-8 Guide.

      How to draw a sprite of any size?




      The next section of the _init() function are the variables that control the game settings.

      --game settings
      enemies = {}
      max_enemies = 15
      max_enemy_size = 10
      max_enemy_speed = 1
      win_size = 10
      weeds = {
          {x1=4,y1=120,x2=2,y2=101},
          {x1=6,y1=123,x2=8,y2=103},
          {x1=110,y1=125,x2=109,y2=102},
          {x1=121,y1=122,x2=120,y2=104},
          {x1=123,y1=122,x2=125,y2=102},
      }
      

      enemies = {} is an empty table (for now) that will hld all of the enemy fish data.

      max_enemies is the total number of enemies that will be spawned at any time. Increase this to make the game harder.

      max_enemy_size is the total number of fish that we drew and we consider the smallest #1 and the largest #10.

      max_enemy_speed is the fastest speed we want the enemy fish to swim. Increase this to make the game harder.

      win_size is the size of the player fish when the win banner will be displayed.

      weeds = { ... } is a table of tables that hold the positions of where to draw the seaweed.


      We will explain how we draw the seaweed using this weeds table down in the "Draw" section. But if you want to know why we use x1, y1, x2, y2, then check out how to draw a line in our PICO-8 Guide and what information we need to have to do that.

      How to draw a Line?




  • 2. Update the Game

      This is the _update() function of the game. It is where we will do all of our logic to change things based on what is happening in the game.

      What is _update?
      --update
      function _update()
          --player controls
          ...
      
          --player movement
          ...
      
          --screen edges
          ...
      
          --enemy update
          ...
      
          --win
          ...
      end
      

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






      --player controls
      if btn(⬅️) then player.dx -= player.speed player.flp=true end
      if btn(➡️) then player.dx += player.speed player.flp=false end
      if btn(⬆️) then player.dy -= player.speed end
      if btn(⬇️) then player.dy += player.speed end
      

      The ⬅️ is made in PICO-8 by pressing Shift+L.

      The ➡️ is made in PICO-8 by pressing Shift+R.

      The ⬆️ is made in PICO-8 by pressing Shift+U.

      The ⬇️ is made in PICO-8 by pressing Shift+D.


      This part handles the player controls. The player only uses four buttons in this game for moving in 4 directions.

      We first check if the player presses left. If so, then we subtract the player speed from the player's current DX amount. And set the flip variable to true, meaning the fish sprite will flip to face left.

      We also set the flip variable back to false if the player presses direction right.

      Remember that DX is the player's momentum moving left or right. So if the DX is positive, that means the player is moving to the right, and so we subtract to slow it down. If the DX is already negative then subtracting is also good because it will speed up the movement to the left.

      Then we check if each direction button is pressed and either add or subtract from the DX or DY. Compare this code with our explanation of how DX and DY work when we created the player table above to fully understand each step, and why we should be adding or subtracting based on the direction the player presses.






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

      This part applies the player's momentum to the player's position. So after we adjust the player's momentum based on the player's button presses, we add the new momentum to the player's position.

      We can simply add the DX and DY, even if they are negative because adding a negative number will subtract. And that is exactly what we want it to do. Really understanding how negative numbers work, helps us simplify a lot of math in our code. So it is very useful!






      --screen edges
      if player.x > 127 then player.x = 1 end
      if player.x < 0 then player.x = 126 end
      if player.y+player.h > 120 then
          player.y = 120-player.h
          player.dy = 0
      end
      if player.y < 0 then
          player.y = 0
          player.dy = 0
      end
      

      This code is used to check if the player runs off the edge of one side of the game screen and resets the player position to the opposite side of the screen. This creates the look and feel of a player running on a space that wraps around, instead of being walled in.

      The PICO-8 game screen is 128 pixels wide by 128 pixels tall. But remember that the first pixel in the top left corner is 0, not 1. So that means the game screen starts at 0 and ends at 127, for both width and height.

      The second part of this check is for the top and bottom edges of the playable area. Since we will draw some sand and rocks at the bottom, then the bottom edge will be around 120 instead of 127. And to compare the bottom edge with the bottom of the fish, we have to take the player.y and add the player.h which changes depending on the fish size.

      We also want to kill the vertical momentum (DY) when the player hits the top or bottom edges. So we just reset DY to zero.






      --enemy update
      create_enemies()
      for enemy in all(enemies) do
          --movement
          enemy.x += enemy.dx
      
          --delete enemies
          if enemy.x > 200
          or enemy.x < -70 then
              del(enemies,enemy)
          end
      
          --collide with player
          if collide_obj(player,enemy) then
              --compare size
              if flr(player.size) > enemy.size then
                  --add to player based on size
                  player.size += flr((enemy.size/2)+.5) / (player.size*2)
      
                  --set sprite based on size
                  player = set_sprite(player)
      
                  sfx(0)
                  del(enemies,enemy)
              else
                  sfx(1)
                  _init()
              end
          end
      end
      

      In the "enemy update" section we will write the code for creating, deleting, moving, and checking collision of the enemy fish.

      We are going to save one complicated part of this game for later because we will build a custom function named create_enemies(). For now, let's just add it here because this is where we want that function's code to run.

      It will simply add fish in random places and random sizes and speeds inside of two spawn zones either to the left or right. And we want it to run every time in case the player eats some or some enemy fish move too far outside the game screen and get deleted.


      After creating all the enemies in the create_enemies() function, we will update each enemy fish.

      To focus on each enemy inside of our enemies table, we will need to cycle through ("iterate") each entry of the table. We do that with a for entry in all(table) do loop. In this case, each entry is a single enemy fish, and the table name is enemies. So we write it like this:

      for enemy in all(enemies) do ... end

      After the do we can now use the variable we named enemy to refer to the currently selected entry of the table, and it will repeat over each and every entry in the table until the last.

      Funny thing is, each entry of the enemies table, will be ANOTHER table! But don't worry! It sounds more complicated than it actually is. And this should help you understand what we are about to do.






      --movement
      enemy.x += enemy.dx
      

      The first thing we do to each enemy is the same thing we do to the player's movement: add the DX to the X.

      Each enemy will have its own momentum (DX) variable and this is just applying the momentum to the enemy position.






      --delete enemies
      if enemy.x > 200
      or enemy.x < -70 then
          del(enemies,enemy)
      end
      

      The next thing we want to do for each enemy is to detect if the enemy moves too far outside of the game screen. The enemy fish will never turn around, so they would move away from the game screen forever. So we should delete them from the enemies table.







      --collide with player
      if collide_obj(player,enemy) then
          --compare size
          if flr(player.size) > enemy.size then
              --add to player based on size
              player.size += flr((enemy.size/2)+.5) / (player.size*2)
      
              --set sprite based on size
              player = set_sprite(player)
      
              sfx(0)
              del(enemies,enemy)
          else
              sfx(1)
              _init()
          end
      end
      

      Here's another complicated part of the code that we are going to prepare here, but write as its own function later. It will be called collide_obj().

      This will check collision of any enemy with the player, and we might as well do that here when we are already looping through each enemy in the enemies table.


      if collide_obj(...) then will check if our future function named collide_obj will come out as true or false. If true, then we know the player collided. If false, then the player did not collide and we don't have to do anything.

      We will need to give our collision function some information to calculate if the enemy and player have hit or not. That is why we pass the two tables inside the collide_obj's parentheses.

      The collide_obj() function will need to know the player's position (X and Y), the player's size (W and H), and the same for the currently selected enemy's position and size.


      Now let's talk about what will happen if the player and an enemy do collide! We will write what should happen next, after the then of this collision check.


      The first thing we should know is if the player is larger or smaller than the enemy.

      if flr(player.size) > enemy.size checks if the player's size (rounded down by flr) is larger than the enemy's size.

      If that is true, then we add to the player.size. Now there is a bit of math here to figure out how much to add the player's size depending on the enemy size.

      First we cut the enemy.size in half, then round it down using flr(), then add .5. Let's say the enemy fish's size is 1. So half of 1 is .5, round it down to 0 and that's not good so that's why we add a .5 at the end to be our minimum.

      After we do those enemy size calculations, then we will divide it by double the player's size.

      We know that the player's size is more than the enemy size, so by dividing the smaller number by the larger number, we know we will get a decimal between 0 and 1. We want that because we want to add somewhere between 0 and 1 to our player's size so they need to eat more than a few fish at each stage before they grow the next level with is the next whole number.

      If we simply add 1 to the player's size, then eating one fish will make the player jump up to the next size level. And if we don't think about enemy size, then the player will have to eat for example 4 of any size fish to reach the next level.

      That doesn't make much sense when the player gets to size 6 and only eats 4 tiny size 1 fish, right? So instead, this math makes it so that the size of the eaten fish becomes a smaller decimal, the smaller it is compared to the player's size.

      We encourage you to take the math, and plug in some enemy size and player size numbers, and see what each one calculates out to be. You may even find a way to improve the math here to make the game's difficulty progression even better.



      Let's continue...

      After adding to the player's size, we reset the player's fish sprite in case the player grew enough to become a larger fish.

      And we play a sound effect (#0) which will be a happy sound.

      And we delete that enemy, using del(table,entry) function, because it has been eaten.

      Here is how we created the eating sound effect #0:


      Next, something very different should happen if the player is NOT larger than the enemy. So we can add an else to the check because we know the player did collide, but is not larger.

      If this happens then the player should be eaten and the player loses the game. So we play sound effect (#1) which will be a sad sound. And simply restart the game by calling the _init() to run again and reset all the game variables.

      Here is how we created the sad sound effect #1:

      --win
      if player.size > win_size then
          if btn(4) or btn(5) then _init() end --reset
      end
      

      The last part of the _update() function is what to listen for if the player wins.

      So if the player size is more than the game setting variable win_size then we want a way for the player to reset the game on their own. So we can check if player presses the X or O buttons (Z or X on the keyboard) and they are numbered 4 and 5. And if so, then call _init() to reset the game variables and restart the game.






  • 3. Draw the Game

      --draw
      function _draw()
          cls(12)
          --sand
          ...
          --seaweed
          ...
          --rocks
          ...
      
          --player
          ...
      
          --enemies
          ...
      
          --player size
          ...
      
          --win
          ...
      end
      

      This _draw() function will handle everything that is seen on 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 all the cells leaving a trail behind them, because where they were is not being erased, so the game screen quickly gets filled with color.

      You can include a color number inside of the cls() parentheses to set the color of the cleared screen to that color. We do this with the number 12 which is the light blue and will be our water background.

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






      --sand
      rectfill(0,120,127,127,15)
      --seaweed
      for weed in all(weeds) do
          line(weed.x1,weed.y1,weed.x2,weed.y2,3)
          line(weed.x1+1,weed.y1+1,weed.x2+1,weed.y2+1,11)
      end
      --rocks
      circfill(8,120,5,13)
      circfill(5,123,3,5)
      circfill(100,122,4,13)
      circfill(122,118,6,6)
      circfill(116,120,3,5)
      

      Here we will draw the rest of the background: sand, rocks, and seaweed.

      rectfill() will draw our sandy bottom as a rectangle filled with color #15.

      The seaweed is next so that it is in front of the sand but behind everything after it, such as the rocks and later the fish.

      Remember at the start we prepared our seaweed in a table named weeds, and also filled that table with smaller tables to hold the coordinates of each line of seaweed.

      How to draw a Line?

      So let's draw a line for each of the entries in the weeds table. We will use the for entry in all(table) do loop to repeat over each entry.

      Inside of this loop, we start by drawing a line at the x1, y1, x2, y2, coordinates of each inner table we called weed at the start of this loop. This first line will be the color dark green which is #3.

      Then we will draw a second line to give the seaweed some thickness and add 1 to each coordinate to set it one pixel lower and to the right. We will also draw this second line as a light green which is color #11.



      The last background objects we draw are the rocks as circles of different colors.

      circfill() = "circle filled" draws a circle filled in with color.

      How to Draw a circle?




      --player
      sspr(player.sx, player.sy, player.w, player.h, player.x, player.y, player.w, player.h, player.flp)
      

      That is one looooong line of code because we have to dig out each variable from the player table and rewrite the name of the table every time. So let's simplify the code just for us to read it easier here by removing the table name, and just remember that each variable we pass inside the parentheses are coming from the player table. Here we go removing each "player.":

      --player
      sspr(sx, sy, w, h, x, y, w, h, flp)
      

      Now compare that with our PICO-8 guide on how to draw a sprite of any size using the SSPR() function:

      How to use SSPR()?

      Each variable in the player table is named the exact same as the variables needed for this sprite drawing function. That's just to make things easier, but of course variables can be named anything you want.


      Here's how it works:


      We start by drawing different size fish all lined up in our sprite sheet.

      sx is the number of pixels from the left of the game screen.

      sy is the number of pixels from the top of the game screen.

      w is the number of pixels to the right of sx that we want to use.

      h is the number of pixels down from the sy that we want to use.



      Now that we can grab the exact sized sprite out of the sprite sheet, we set it on the game screen with the next variables in the SSPR() function, which is the x and y coordinates.

      x is the number of pixels from the left of the sprite sheet.

      y is the number of pixels from the top of the sprite sheet.


      w is the number of pixels to the right of x that we want to draw.

      h is the number of pixels down from the y that we want to draw.

      The next two variables are w and h, but we already used those right? Well this function allows you to set a different draw size than its sprite sheet size. So you could take a sprite out of the sprite sheet and draw it at double size or even shrink it using these second two width and height arguments (passed variables).

      But we want to draw it the same size that it appears on the sprite sheet, so we simply use the same variables for width and height as we did with the sprite sheet size.


      flp is a boolean (true/false) variable that flips the sprite on the X axis (left/right).

      This flip variable will tell the drawing function whether to leave the sprite as it was drawn in the sprite sheet, or to flip it to face the opposite direction. So since we drew the fish facing right, then when flp is set to false, then the fish remains drawn facing right. But when flp is set to true, then the fish is flipped to face left.

      That's why we set this variable true or false depending on if the player presses buttons left or right.






      Next, is how we draw the enemies:

      --enemies
      for enemy in all(enemies) do
          pal(9,enemy.c)
          sspr(enemy.sx, enemy.sy, enemy.w, enemy.h, enemy.x, enemy.y, enemy.w, enemy.h, enemy.flp)
      end
      pal()
      

      Since the enemies are each organized in tables and all of them are organized further into one large table named enemies, we can cycle through the whole enemies table the same way we did in _update(), using the for entry in all(table) do loop.

      for enemy in all(enemies) will loop through each entry of the enemies table and set the entry as a local variable that we named enemy right there after the "for". That means inside of this table loop, we can access each inner table properly as "enemy", and access the keys or variables of each enemy's table by using enemy.key.


      Inside the loop, the first thing we do is access the palette function, pal().

      This function lets us swap colors around. So even if we drew all of our fish orange in the sprite sheet, we can swap orange for another color before drawing to the game screen. This is how our enemies are going to become drawn a variety of colors.

      pal(9,enemy.c) takes color #9, which is orange, and swaps it with the random color in the enemy table. Don't worry, we didn't get to setting that up yet and we will do that when we build the create_enemies() function. For now, just know that we will set a random color for each fish and save it to the enemy table as c for color.

      After that, it is as simple as using the same SSPR() function as we did with the player to draw the enemy fish onto the game screen. Except now they won't be orange and instead all of the orange will be some other color.


      We should reset the palette after drawing each fish or later things we draw may come out strange colors!

      To do that we just call pal() again but leave the parentheses empty and it will reset all colors to the original numbers.






      Next, we draw the player size indicator on the screen.

      --player size
      rectfill(2,3,22,10,0)
      rectfill(2,4,2+(player.size-flr(player.size))*20,9,8)
      

      We are going to build a progress bar to show how close the player is getting to reaching the next fish size.

      First we draw a rectangle in the upper left corner of the game screen that will be black (color #0). Notice that the X starts at 2, just away from the edge of the screen, and ends at 22. The Y starts at 3 and ends at 10, but the height doesn't really matter.

      The next rectangle we will draw right on top of that black one will be red (color #8). Notice that its Y starts one more than the first rectangle, and ends one pixel less so that a 1 pixel frame will remain as the red rectangle grows.

      The red rectangle's X starts at 2, and then we do some fancy math to figure out how to calculate the player's size percentage and display that as a progress bar as the player's size changes.


      Let's take out that part of this code and just look at the math:

      2+(player.size-flr(player.size))*20


      We start with 2 because that is the minimum we want the X to be. If it is less than that, then the rectangle will be drawn to the left.

      Then we add a number based on the player's size. Inside the first parentheses there, we take the player's size and subtract the rounded down player's size.

      So if the player's size is 2.4, then we take 2.4 and round it down to 2, then subtract that: 2.4 - 2 = .4

      That's great because now we are able to ignore the player's current fish size, and focus on how close they are to the next size up by only having the decimal! In that example, the player is .4 or 40% on the way. So we want to draw the red rectangle 40% of the way on the black rectangle.


      The final math we do is multiply the player's progress (the decimal we just found) by the number of pixels of the black rectangle's width.

      So since our black rectangle is 20 pixels wide, then we multiply by 20, and that will end the red rectangle at the correct player progress width of the black rectangle! Pretty cool!






      --win
      if player.size > win_size then
          rectfill(0,55,127,75,10)
          print("congratulations!!!",28,56,1)
          print("you became",43,63,1)
          print("the biggest fish!",20,70,1)
      end
      

      The last thing to draw would be a message if the player manages to win the game by growing larger than the win_size in the game settings.

      if player.size > win_size then will check if the player size is greater than the win setting.

      rectfill(0,55,127,75,dgray) will draw a yellow (color #10) banner in the middle of the screen.

      print("congratulations!!!",28,56,1) will write the word "congratulations!!!" in dark blue (color #1) over the rectangle.

      And two more print functions to write "you became the biggest fish" on two separate lines.






  • 4. Object Collision

      --collision
      function collide_obj(obj, other)
          if  other.x+other.w > obj.x
          and other.y+other.h > obj.y
          and other.x < obj.x+obj.w
          and other.y < obj.y+obj.h
          then
              return true
          end
      end
      

      This is an object collision function that we use to detect if the enemy fish collide with the player. This function is very easy to use in any sprite to sprite collision.

      You just need tables to hold your game objects information and it must have x, y, w, h. Lucky for us, that is how we set up the player and enemy tables.


      function collide_obj(obj, other) creates the custom function with the name "collide_obj" (obj meaning object) and then inside the parentheses, we prepare the function for what variables it should expect to receive and what those variables will be named locally, inside of this function.

      You could add a comment to the function that obj and other must be tables with X, Y, W, and H keys.


      This collision function has only one complicated check, and returns true if all of the requirements are met.

      if other.x+other.w > obj.x begins the check by comparing the right side of "other" with the left side of "object".

      and other.y+other.h > obj.y also compares the bottom side of "other" with the top side of "object".

      and other.x < obj.x+obj.w then also compares the left side of "other" with the right side of "object".

      and other.y < obj.y+obj.h finally also compares the top side of "other" with the bottom side of "object".


      Together, these four checks are asking if the first object is inside of the second object. This may help you visualize it:


      So using this image, you can see which sides are being compared and if all checks are true, then we return true to say that the two objects have collided.

      We don't actually have to return false if the check fails because if the function returns nothing, it will be interpreted as false.


      You could improve this function by using another table within each object's table and set each object to have a specific hitbox size that matches the true sprite size better than the entire sprite's rectangle that we are drawing. But this one is good enough for our purpose and we drew the fish to be as rectangular as possible to fill in most of the collision area.

      To test out what we are talking about here, try drawing your fish with a small pointy nose and big tail. Now when you play the game, it will probably tell you that you collided with that fish, but it did not look like you touched it becuase the pixels of the sprite did not fill the full sprite size which is used as the collision rectangle.






  • 5. Create Enemies Function

      --enemies
      function create_enemies()
          if #enemies < max_enemies then
              --local variables
              ...
      
              --random start position
              ...
      
              --make enemy table
              ...
      
              --set sprite based on size
              ...
      
              --add it to enemies table
              ...
          end
      end
      
      

      First we make another custom function named create_enemies(). We leave the parentheses empty because this function doesn't need to be given anything and it will run the same way every time.

      Then we only need to create enemies when the number of enemies is less than the game settings variable: max_enemies.

      So we check if #enemies < max_enemies

      The # sign in front of our enemies table is an easy way to get the total count of entries in the table. So that code reads as "if the total number of entries in the enemies table is less than the maximum enemies number..." then we want to create more enemies!

      But if the number of enemies reaches the maximum, then we just skip all the rest of the code inside this function and don't create any more enemies.






      --local variables
      local x = 0	 local y = 0
      local dx = 0
      local size = flr(rnd((max_enemy_size+player.size)/2))+1
      local flp = false
      local c = flr(rnd(7))+1
      

      Next, inside of the first check, we need to set some local variables that we will use only inside of this function.

      x and y will be the enemy fish's position.

      dx will be the enemy fish's speed and direction of movement.

      size will be the enemy fish's size (1-10 assigned to each fish size).

      flp will be whether or not to flip the enemy fish's sprite to face left or right.

      c will be the enemy fish's color.


      Let's look at the math for a random size.

      flr(rnd((max_enemy_size+player.size)/2))+1

      flr() is used to round down.

      rnd() is used to get a random number between 0 and the number inside the parentheses.

      +1 is at the end because we don't want a circle with a radius of 0, it should be at least 1.

      (max_enemy_size+player.size) is used to create a maximum size for the random generator based on the player's size. This is so that as the player gets bigger, so do the enemies. But we also divide that very large number by 2 /2 to bring the max enemy size back to a number not too much bigger than the player's current size.


      Lastly, lets look at the math for a random color.

      flr(rnd(7))+1

      flr() is used again to make sure we end with a whole number and not a decimal.

      rnd(7) is inside of that to get a random number between 0 and 7. If we stop here, then the enemy fish would be any color between black (#0) and white (#7).

      But we don't want a fish to be black because that is the default transparent color, so outside of all parentheses, we finish by adding 1. That means we will end with a number between 1 and 8.

      We stop there because we don't want to allow for number 9, which is orange, because that is the color we drew all the sprites as, and we will use that as the player color. The player should be the only orange fish so that it is easy to see which fish they are.






      --random start position
      place = flr(rnd(2))
      if place == 0 then
          --left
          x = flr(rnd(16)-64)
          y = flr(rnd(115))
          dx = rnd(max_enemy_speed)+.25
          flp = false
      elseif place == 1 then
          --right
          x = flr(rnd(48)+128)
          y = flr(rnd(115))
          dx = -rnd(max_enemy_speed)-.25
          flp = true
      end
      

      The player will start in the middle of the game screen, and enemies will spawn from the sides and move across the screen left or right.

      We need the enemies to spawn in one of two places: left or right

      So let's first create a random number between 0 and 2 to represent each side.

      place = flr(rnd(2)) will find a random number between 0 and 2 and then round down so it will be one of these numbers: 0 or 1)


      Then we check if place == 0 or 1 using an elseif check.

      It's basically like flipping a coin to choose which side of the screen the enemy will start at!


      Inside of each of those checks we will set the X, Y, DX, and DY with random numbers but controlled slightly to make sure the enemies are moving in a direction across the screen, not away from it.

      Let's look closer at just the first set.

      ***Keep in mind, this is for an enemy that will start on the left side of the game screen and move right.***


      x = flr(rnd(16)-64

      This will set the X position to be a rounded down random number between 0 and 16 but minus 64.

      In the end, that means X will be a number between -64 and -48.

      Where will that be on the game screen? Well if the left side of the game screen is where X = 0, and this will be a negative number, then that means the enemy fish will start to the left of the visible screen. That is a good thing because as it moves towards the screen, it will appear a little at a time instead of suddenly.

      So for both sides of the game screen, we want to start the enemy fish off of the screen.


      y = flr(rnd(115))

      The Y for a left side enemy is easy because it can start anywhere from the top of the screen to the sand. And we know that the top of the screen is pixel 0 and the bottom of the screen is around pixel 115.

      So we just need to get a random number between 0 and 115 and round it down.


      dx = rnd(max_enemy_speed)+.25

      Since this enemy starts on the left, the DX (momentum left or right) is simple. We want the enemy to move toward the right. So any positive number between 0 and the maximum enemy speed will work. However, the speed may be too close to 0, and so the fish will move way too slow.

      So we do +.25 to the random number to make .25 become the minimum speed.






      --make enemy table
      local enemy = {
          sx = 0,
          sy = 0,
          x = x,
          y = y,
          w = 0,
          h = 0,
          c = c,
          dx = dx,
          size = size,
          flp = flp
      }
      

      After we have set up all of the enemy variables, we can neatly store them inside of a table. We create a table named "enemy" with local enemy = { ... }.

      Inside of the curly braces { } we list the local variables in the order of key = value.

      What are keys and values in a table?

      So inside this table, there will be a key named X and it will hold the value of whatever the local variable named x became from our random number generating earlier. And so on for each of the enemy data, all the way to its flip setting.






      Now some of those enemy variables stored in the table are only set to 0, but we need to set them to some numbers based on which size fish they turned out to be.


      --set sprite based on size
      enemy = set_sprite(enemy)
      
      --add it to enemies table
      add(enemies,enemy)
      

      As the comment title of this section describes, we will be setting the sprite of the enemy fish based on its size.

      enemy = set_sprite(enemy) will save the enemy table to whatever is returned after we give the enemy table to the function named set_sprite(). We will go into detail about that custom function in the next major section of this tutorial. Here, we are just calling it to run, but we haven't actually built the function yet.


      Lastly, we want to add each enemy table to the larger enemies table to keep them all organized together.






  • 6. Set Sprite Size Function

      --sprites
      function set_sprite(obj)
          if flr(obj.size) <= 1 then
              obj.sx = 0 obj.sy = 0 obj.w = 4  obj.h = 3
          elseif flr(obj.size) == 2 then
              obj.sx = 5 obj.sy = 0 obj.w = 4  obj.h = 4
          elseif flr(obj.size) == 3 then
              obj.sx = 9 obj.sy = 0 obj.w = 6  obj.h = 5
          elseif flr(obj.size) == 4 then
              obj.sx = 15 obj.sy = 0 obj.w = 9   obj.h = 7
          elseif flr(obj.size) == 5 then
              obj.sx = 24 obj.sy = 0 obj.w = 14  obj.h = 9
          elseif flr(obj.size) == 6 then
              obj.sx = 38 obj.sy = 0 obj.w = 14  obj.h = 10
          elseif flr(obj.size) == 7 then
              obj.sx = 52 obj.sy = 0 obj.w = 16  obj.h = 12
          elseif flr(obj.size) == 8 then
              obj.sx = 68 obj.sy = 0 obj.w = 15  obj.h = 15
          elseif flr(obj.size) == 9 then
              obj.sx = 83 obj.sy = 0 obj.w = 19  obj.h = 16
          elseif flr(obj.size) == 10 then
              obj.sx = 102 obj.sy = 0 obj.w = 26  obj.h = 17
          else
              obj.sx = 102 obj.sy = 0 obj.w = 26  obj.h = 17
          end
      
          return obj
      end
      

      function set_sprite(obj) creates the function, names it "set_sprite" and expects to receive 1 variable that will be locally named "obj".

      Inside of this function is just one long IF statement with many ELSEIFs.

      The whole point of this function is to take the size of a fish (player or enemy) and determine which fish that should be in the sprite sheet. Then set that player or enemy fish's sprite variables to the correct numbers.


      if flr(obj.size) <= 1 then

      This is the opening of the check. It first checks if the rounded down fish size is less than or equal to 1.

      Remember that the player's size will almost always be a decimal so we round it down here to ignore the decimal and get the correct fish size. Then we compare it to the smallest fish size: 1


      If that is not true, then the function moves to the next elseif and comapres with 2, 3, 4, etc. all the way up to size 10 which is the largest fish that we drew. But you could keep drawing more, and simply add more elseif checks and raise the max_enemy_size variable.

      The final else check is for any fish larger than 10. We didn't draw any larger fish so we will just set them as size 10 fish.


      How do we set the sprite sizes?


      Once we know the size number of the fish, then we have to look at the sprite sheet.


      This shows the numbers that we have to find and set them to the correct variables: sx, sy, w, and h.

      To find the SX, we count the number of pixels from the left of the sprite sheet.

      To find the SY, we count the number of pixels from the top of the sprite sheet.

      To find the W, we count the number of pixels to the right of X.

      To find the H, we count the number of pixels down from Y.


      To make it easier, hover your mouse on the pixel in the sprite sheet and look at the lower left corner. It will tell you what pixel number the X and Y positions are of your mouse cursor.






  • Full Code!

      --init
      function _init()
          --player table
          player={
              x = 60,
              y = 60,
              w = 0,
              h = 0,
              size = 2,
              sx = 0,
              sy = 0,
              dx = 0,
              dy = 0,
              speed = 0.08,
              flp = false
          }
          player = set_sprite(player)
      
          --game settings
          enemies = {}
          max_enemies = 15
          max_enemy_size = 10
          max_enemy_speed = 1
          win_size = 10
          weeds = {
              {x1=4,y1=120,x2=2,y2=101},
              {x1=6,y1=123,x2=8,y2=103},
              {x1=110,y1=125,x2=109,y2=102},
              {x1=121,y1=122,x2=120,y2=104},
              {x1=123,y1=122,x2=125,y2=102},
          }
      end
      
      -->8
      --update
      function _update()
          --player controls
          if btn(⬅️) then player.dx -= player.speed player.flp=true end
          if btn(➡️) then player.dx += player.speed player.flp=false end
          if btn(⬆️) then player.dy -= player.speed end
          if btn(⬇️) then player.dy += player.speed end
      
          --player movement
          player.x += player.dx
          player.y += player.dy
      
          --screen edges
          if player.x > 127 then player.x = 1 end
          if player.x < 0 then player.x = 126 end
          if player.y+player.h > 120 then
              player.y = 120-player.h
              player.dy = 0
          end
          if player.y < 0 then
              player.y = 0
              player.dy = 0
          end
      
          --enemy update
          create_enemies()
          for enemy in all(enemies) do
              --movement
              enemy.x += enemy.dx
      
              --delete enemies
              if enemy.x > 200
              or enemy.x < -70 then
                  del(enemies,enemy)
              end
      
              --collide with player
              if collide_obj(player,enemy) then
                  --compare size
                  if flr(player.size) > enemy.size then
                      --add to player based on size
                      player.size += flr((enemy.size/2)+.5) / (player.size*2)
      
                      --set sprite based on size
                      player = set_sprite(player)
      
                      sfx(0)
                      del(enemies,enemy)
                  else
                      sfx(1)
                      _init()
                  end
              end
          end
      
          --win
          if player.size > win_size then
              if btn(4) or btn(5) then _init() end --reset
          end
      end
      
      -->8
      --draw
      function _draw()
          cls(12)
          --sand
          rectfill(0,120,127,127,15)
          --seaweed
          for weed in all(weeds) do
              line(weed.x1,weed.y1,weed.x2,weed.y2,3)
              line(weed.x1+1,weed.y1+1,weed.x2+1,weed.y2+1,11)
          end
          --rocks
          circfill(8,120,5,13)
          circfill(5,123,3,5)
          circfill(100,122,4,13)
          circfill(122,118,6,6)
          circfill(116,120,3,5)
      
          --player
          sspr(player.sx, player.sy, player.w, player.h, player.x, player.y, player.w, player.h, player.flp)
      
          --enemies
          for enemy in all(enemies) do
              pal(9,enemy.c)
              sspr(enemy.sx, enemy.sy, enemy.w, enemy.h, enemy.x, enemy.y, enemy.w, enemy.h, enemy.flp)
          end
          pal()
      
          --player size
          rectfill(2,3,22,10,0)
          rectfill(2,4,2+(player.size-flr(player.size))*20,9,8)
      
          --win
          if player.size > win_size then
              rectfill(0,55,127,75,10)
              print("congratulations!!!",28,56,1)
              print("you became",43,63,1)
              print("the biggest fish!",20,70,1)
          end
      end
      
      -->8
      --collision
      
      function collide_obj(obj, other)
          if  other.x+other.w > obj.x
          and other.y+other.h > obj.y
          and other.x < obj.x+obj.w
          and other.y < obj.y+obj.h
          then
              return true
          end
      end
      
      -->8
      --enemies
      
      function create_enemies()
          if #enemies < max_enemies then
              --local variables
              local x = 0	 local y = 0
              local dx = 0
              local size = flr(rnd((max_enemy_size+player.size)/2))+1
              local flp = false
              local c = flr(rnd(7))+1
      
              --random start position
              place = flr(rnd(2))
              if place == 0 then
                  --left
                  x = flr(rnd(16)-64)
                  y = flr(rnd(115))
                  dx = rnd(max_enemy_speed)+.25
                  flp = false
              elseif place == 1 then
                  --right
                  x = flr(rnd(48)+128)
                  y = flr(rnd(115))
                  dx = -rnd(max_enemy_speed)-.25
                  flp = true
              end
      
              --make enemy table
              local enemy = {
                  sx = 0,
                  sy = 0,
                  x = x,
                  y = y,
                  w = 0,
                  h = 0,
                  c = c,
                  dx = dx,
                  size = size,
                  flp = flp
              }
      
              --set sprite based on size
              enemy = set_sprite(enemy)
      
              --add it to enemies table
              add(enemies,enemy)
          end
      end
      
      
      -->8
      --sprites
      
      function set_sprite(obj)
          if flr(obj.size) <= 1 then
              obj.sx = 0 obj.sy = 0
              obj.w = 4  obj.h = 3
          elseif flr(obj.size) == 2 then
              obj.sx = 5 obj.sy = 0
              obj.w = 4  obj.h = 4
          elseif flr(obj.size) == 3 then
              obj.sx = 9 obj.sy = 0
              obj.w = 6  obj.h = 5
          elseif flr(obj.size) == 4 then
              obj.sx = 15 obj.sy = 0
              obj.w = 9   obj.h = 7
          elseif flr(obj.size) == 5 then
              obj.sx = 24 obj.sy = 0
              obj.w = 14  obj.h = 9
          elseif flr(obj.size) == 6 then
              obj.sx = 38 obj.sy = 0
              obj.w = 14  obj.h = 10
          elseif flr(obj.size) == 7 then
              obj.sx = 52 obj.sy = 0
              obj.w = 16  obj.h = 12
          elseif flr(obj.size) == 8 then
              obj.sx = 68 obj.sy = 0
              obj.w = 15  obj.h = 15
          elseif flr(obj.size) == 9 then
              obj.sx = 83 obj.sy = 0
              obj.w = 19  obj.h = 16
          elseif flr(obj.size) == 10 then
              obj.sx = 102 obj.sy = 0
              obj.w = 26  obj.h = 17
          else
              obj.sx = 102 obj.sy = 0
              obj.w = 26  obj.h = 17
          end
      
          return obj
      end
      


  • Play the Game!


font