Intro to Game Programming in TIC 80 - nesbox/TIC-80 GitHub Wiki

In this tutorial I will cover the basics of game programming using tic-80. It won't teach any programming, and assumes you already know some lua. 

You can play the completed game here: https://tic80.com/play?cart=247

Game programming follows a basic structure:.

VARIABLES 
FUNCTIONS 
INITIALIZE 
GAME LOOP 
    CHECK FOR USER INPUT 
    UPDATE GAME DATA 
    DRAW GAME GRAPHICS 
    CHECK FOR GAME OVER 

This is a loose template, but the majority of games follow something along these lines. Let's go over each section.

VARIABLES

This is where you'll declare the majority of the game variables. These will contain game data such as character stats and inventories, or global variables like the game speed or size of the game board. Not every variable will be declared here, but the important ones should be.

(EXAMPLE)

    --VARIABLES
    GAMESPEED=100
    Time=0
    Name="Levant"
    Capturelevel=50
    Inventory={
              weapon="dagger",
              armor ="leather vest"
              item  ="mugwort"}
    player={
           x=15,
           y=8, }

Here you can see a global variable GAMESPEED typed in all caps. These will never be altered during the game.  We have the time, name and capture level. These may change as the game progresses.  And finally we have a dictionary to log inventory, and another to log players coordinates. These will be updated as the game character moves.

FUNCTIONS

This is where we declare the functions we will use during the game.  They can be blocks of code we will be using repeatedly (such as calculating a players coordinates)   or blocks of code that work together to do some specific task (such as drawings each bad guy in a level). And all functions help to make your code easier to read and debug. 

(EXAMPLE)

    --FUNCTIONS
    function moveplayer() --update players coordinates according to input
     if btn(3) then --if the player is pressing "right"
      player.x = player.X + playerVelocity
     elseif btn(2) then --if the player is pressing "left"
     player.x = player.x - playerVelocity
     end
    end

    function isOutOfBounds(n)      --check if "n" is out of game bounds
     If (n.x > 10 or n.x < 0) then --board is 10x10
      return true
     elseif (n.y > 10 or n.y < 0) then
      return true
     else
      return false

Here is two examples of functions. moveplayer() is a function that will only be called once each game loop, but packing all these commands under the descriptive name "moveplayer" makes everything easier to read.  As for isOutOfBounds(), it will likely be used multiple times to check if anything has left the game board. And packing all these commands into a function saves you from typing all that code each time you need to check that.

INITIALIZE 

This is a common section in game programming. It is simply a section where the game is set up (or initialized) for the first time, or for restarting a game. Most languages/game engines will use the function init() which will be called automatically whenever the game run, and contains all the code you want run at the start of a game. However, tic-80 does not do this. Instead we have to manually call init() before starting the game loop.    Remember, you don't have to use init() at all. It just makes starting/resetting a game simpler.

(EXAMPLE)

    Function init() --define our init() function
     score=0 --set score to zero
     player={
            x=0,
            y=0,} --set player at (0,0)
     lives=3 --set players lives to three

Now we can simply call init() before the game loop starts, and whenever we want to reset the game.

GAME LOOP

This is where the game code actually gets executed. In tic-80, the game loop is defined as "function TIC()" and it will be run automatically when the game starts. This is where the game listens for user input, modifies the game data, and updates the graphics. A typical game loop might look like this:

(EXAMPLE)

    --VARIABLES
    Important variables here

    --FUNCTIONS
    Useful functions here
    Init() function defined here

    Init() --game initialized
    function TIC() --start of game loop
     If not player.dead then
      moveplayer()
      moveenemies()
      If gotCoin(player) then
       score = score + 500
      end
      if collision(player,enemy)
       if player.invincible == true then
        respawn(enemy)
        score = score + 1000
       else
        player.lives = player.lives - 1
        if player.lives == 0 then
         player.dead = true
        else
         respawn(player)
       end
      end
     elseif player.dead == true then
      print("game over")
      print("score: ".. score)
       init()
     end
     drawGraphics(). --update the game graphics
     clock = clock+1 --update game clock
    end

This is a rough idea, and some stuff has been simplified for easier reading. You can see where taking the time to write all of the functions beforehand made coding the actual game loop a piece of cake! 

Now let's look at coding a simple game in tic-80

    -- title:  Coin Grab
    -- author: Bear Thorne
    -- desc:   Grab the coin before the cpu does
    -- script: lua

    --VARIABLES

    --we will be defining the directions
    --in the game for moving the enemy.
    DIRECTIONS={
               {x= 0,y=-1}, --up
               {x= 0,y= 1}, --down
               {x=-1,y= 0}, --left
               {x= 1,y= 0}} --right

    --we will be skipping frames to reduce
    --the game speed. GAMESPEED just says
    --how many frames to skip.
    GAMESPEED = 8

    --tic-80 references its 16 color pallete
    --by number, so we will be assigning each
    --colors number to a variable with the
    --appropriate name. this will make using
    --colors in our code easier.
          BLACK=0
       DARK_RED=1
      DARK_BLUE=2
      DARK_GRAY=3
          BROWN=4
     DARK_GREEN=5
            RED=6
     LIGHT_GRAY=7
     LIGHT_BLUE=8
         ORANGE=9
      BLUE_GREY=10
    LIGHT_GREEN=11
          PEACH=12
           CYAN=13
         YELLOW=14
          WHITE=15

    --this will be the coordinates we use
    --to place the player when respawning
    PLAYER_RESPAWN_POINT={x=2,y=2}

    --this is the coordinates for the
    --enemys spawn point
    ENEMY_RESPAWN_POINT={x=28,y=15}

    --to save some typing, i usually define
    --the init() function in the variables
    --section. now i can declare the variables
    --and initialize them at the same time.
    --so here goes...
    function init()
     gameover=false
     player={
          id="player",
           x=2,
           y=2,
       lives=3,
       score=0}

      enemy={
         id="enemy",
           x=28,
           y=15,
       score=0}

      coin={
         x=0,
         y=0,}

    clock=0

    spawncoin()
    --we will be defining spawncoin() in a
    --minute, but we will have to perform
    --this whenever the game starts.
    end

    --FUNCTIONS

    --we will be passing players and coins
    --or players and enemies to this function
    --and asking wether they are occupying
    --the same map coordinates. that's how
    --we can check for collision.
    function collision(body1,body2)

     if (body1.x == body2.x and body1.y == body2.y) then
     return true --true means they have collided

     else
      return false--false means they haven't collided
     end
    end

    --we need to randomly spawn a coin on
    --the map, but not where the player or
    --enemy are. this function will do that
    function spawncoin()

    --the coins x coordinates will be
    --randomly chosen from 0-29, which is
    --the width of the game board.
     coin.x=math.random(0,29)

    --and the coins y coordinates will be
    --randomly chosen from 0-16, which is
    --the height of the game board.
    coin.y=math.random(0,16)

     --now we must make sure the coin isn't
     --colliding with the player or enemy.
     --we will call collision(). yeah, you
     --can call a function from inside
     --another function...inception, right?!
     if collision(coin,player) or collision(coin,enemy) then
      spawncoin()

     --this means it call itself and starts
     --over. respawning a coin in another spot
     --until it finds a free space.
     end
    end

    --now let's make a function to respawn
    --a player or enemy based on the
    --argument passed to it.
    function respawn(body)

    --if body is the player then...
     if body.id=="player" then

    --respawn player at PLAYER_RESPAWN_POINT
    body.x=PLAYER_RESPAWN_POINT.x
    body.y=PLAYER_RESPAWN_POINT.y

    --if body is the enemy then...
    elseif body.id=="enemy" then

    --respawn enemy at ENEMY_RESPAWN_POINT
    body.x=ENEMY_RESPAWN_POINT.x
    body.y=ENEMY_RESPAWN_POINT.y
     end
    end

    --now we need to move the enemy
    function moveenemy()

    --pick a random direction from DIRECTIONS
     random=math.random(1,4)
     dir=DIRECTIONS[random]
    --check if the enemy will move out of the screen
    if enemy.x+dir.x < 29 and enemy.x+dir.x > 0 and
    enemy.y+dir.y < 16 and enemy.y+dir.y > 0 then

     --move enemy in the approved direction
     enemy.x=enemy.x+dir.x
     enemy.y=enemy.y+dir.y

    --if the move isn't valid then...
    else
     --call moveenemy() again.
     moveenemy()
    end
    end

    --now we will make a function that will analyze input,
    --check if the move is valid, and then move the player
    --accordingly.
    function moveplayer()
    --if the player presses 'up' and they
    --aren't at the top already then...
    if btn(0) and player.y>0 then

    --move player up by 1
     player.y=player.y-1

    --if the player presses 'down' and
    --they aren't at the bottom already.
    elseif btn(1) and player.y<16 then

     --move player down by 1
     player.y=player.y+1

    --if the player presses 'left' and
    --they aren't at the far left.
    elseif btn(2) and player.x>0 then

    --move player left by 1
    player.x=player.x-1

    --if the player presses 'right' and
    --they aren't at the far right.
    elseif btn(3) and player.x<29 then

    --move player right by 1
    player.x=player.x+1
    end
    end

    --now we will write a function for drawing
    --the game graphics.
    function draw()

     --first we need to clear the screen of
     --all previous graphics and fill it
     --with light grey
     cls(BLUE_GREY)

    --place a blue 8x8 square at the players
    --current position.
    rect(player.x*8,player.y*8,8,8,LIGHT_BLUE)

    --place a red 8x8 square at the enemys
    --current position
    rect(enemy.x*8,enemy.y*8,8,8,RED)

     --place a yellow 3x5 circle at the coins
     --current positio
     circ(coin.x*8+4,coin.y*8+4,3,YELLOW)

     --last we want to draw the scores of
     --the player and enemy. plus the lives
     --of the player remaining.
     print("P="..player.score,3,3,LIGHT_BLUE)
     print("E="..enemy.score,220,3,RED)
     print("Lives="..player.lives,3,9,LIGHT_BLUE)

    --you'll notice that we're multiplying
    --the coordinates by 8. this is just
    --because we are displaying a 16x29
    --board on a 128x232 display. so if we
    --multiply the coordinates by 8 we get
    --the appropriate pixel coordinates.
    --if you're confused then try changing
    --it from 8 to something smaller.
    end

    --now let's put everything together in
    --the game loop!!!

    --initialize the game first
    init()
    function TIC()

    --this is where we use frame skipping
    --to bring the game speed down to a
    --reasonable level.
    --this works by dividing clock by
    --GAMESPEED and only executing the game
    --loop if the remainder is zero.
    if clock%GAMESPEED==0 then

    --this is the main part of the game loop

    --if a game over hasn't happened.
    if not gameover then

     --move the player
     moveplayer()

     --move the enemy
     moveenemy()

     --check for player/coin collision
     if collision(player,coin) then

     --add 1 to the player score
     player.score=player.score+1

     --spawn another coin
     spawncoin()

    --respawn player at spawn point
    respawn(player)

    --check for enemy/coin collision
    elseif collision(enemy,coin) then

    --add 1 to the enemys score
    enemy.score=enemy.score+1

    --spawn another coin
    spawncoin()

    --respawn enemy at spawn point
    respawn(enemy)

    --check for enemy/player collision
    elseif collision(player,enemy) then

    --respawn player and enemy
    respawn(player)
    respawn(enemy)

    --subtract a life from player
    player.lives=player.lives-1

    --check for game over
    if player.lives==0 then
     gameover=true
    end
    end

    --else if a game over has happened
    elseif gameover then

    --reset the game
    init()

    end

    --now that all the data is updated we
    --will call the draw() function to
    --update the game graphics.
    draw()

    end

    --add 1 to clock before finishing the
    --game loop
    clock=clock+1
    end

you'll see that this game is incredibly simple, and not really that fun to play. but the idea is just to understand how the code works for game programming.

everything is about using variables to store game data, use functions to alter and update the data, and finally a way to draw that data to the screen for the player to see.

Feel free to use any portion of this code in your own projects. If you're feeling generous, leave a credit for me in the top of your file!

If you've got questions or requests for another tutorial... You can email me at [email protected]