A step by step introduction to TIC 80, Part 1 The Default Cart - nesbox/TIC-80 GitHub Wiki
Download TIC-80 here. For additional help, the TIC-80 wiki start page is a good place to start from!
When you boot TIC-80 up, it opens in the command line view. Think of TIC as a tiny computer inside your computer - this is its user interface.
TIC-80 games are stored as .tic files, commonly referred to as cartridges or carts. Let's open the default cart by typing new lua
and pressing enter to execute the command.
new cart has been created
is TIC's response.
Now the default cart is loaded! It's not empty, and we're going to need to type another command to actually see what the cart actually contains. Let's execute the command run
, which, well, runs the loaded cart. Alternatively, you can just press CTRL+R.
There's not much: a "HELLO WORLD!
" text and an animated sprite which we can move with the arrow keys.
Exit the game by pressing the ESC, selecting CLOSE GAME on the pause menu with the arrow keys, and pressing ENTER.
At this point it would be nice to see what made the cart act like it just did. So, we're going to take a look at the source code!
Press Esc to reveal the code editor. (We can also use the shortcut button on the top left.)
-- title: game title
-- author: game developer
-- desc: short description
-- script: lua
t=0
x=96
y=24
function TIC()
if btn(0) then y=y-1 end
if btn(1) then y=y+1 end
if btn(2) then x=x-1 end
if btn(3) then x=x+1 end
cls(13)
spr(1+t%60//30*2,x,y,14,3,0,0,2,2)
print("HELLO WORLD!",84,84)
t=t+1
end
The code is just 21 lines, so let's go through it step by step here.
The default cart is written in Lua, which is a programming language a bit like Python. This page is a great introduction to Lua!
In Lua, two dashes (--) mark the beginning of a one-line comment, marked grey. Normally, comments are disregarded when executing the program. HOWEVER, the first four lines of the default cart are special comments, in which you can write down info about your game. The fourth line tells TIC which programming language the cart is written with, let's stick with Lua for now.
-- title: Fantas.tic
-- author: Borb
-- desc: An action-adventure game
-- script: lua
Then, we declare some variables and give them values. It's easy in lua:
t=0
x=96
y=24
The purpose of these three fellows (or at least x and y) might become a bit clearer by changing their values. Let's change x and y to zeroes with
x=0
y=0
To see what this accomplishes, we can run the cart again.
The smiling sprite is now in the top-left corner! The top-left corner is indeed the origin, and (unlike in maths and physics) the positive values of y
go down from the origin. The bottom-right corner has the coordinates x=240, y=136
. The resolution of TIC-80 is just that, 240 by 136 pixels.
Let's continue onwards! Now we get to the most important function, the main loop of the game, TIC()
.
function TIC()
...
end
TIC()
is called 60 times in a second. Here, we declare it: every time it's called, TIC-80 executes stuff inside. Inside it the game does its magic: we change values of some variables, draw all kinds of stuff on screen, etc.
All functions are declared the same way - starting with function functionname()
and ending with end
. For example, a sum function would be
function sum(a,b)
local sum = a + b
return sum
end
We could then call it with, say, sum(5,2)
and get a satisfying result of 7
.
Unlike Python, Lua doesn't necessitate the use of indentation, but using it produces clearer code.
But what did we have inside TIC()
, again?
First, there were these four lines:
if btn(0) then y=y-1 end
if btn(1) then y=y+1 end
if btn(2) then x=x-1 end
if btn(3) then x=x+1 end
A basic if statement in lua is of the form
if something then
dostuff()
end
We could extend it with elseif
and else
:
if something then
doStuff()
elseif something_else then
doOtherStuff()
else
doCompletelyAnotherStuff()
end
But let's not get ahead of ourselves! Inside those four lines, we change the values of y and x only if a certain button is down, and the function btn(i) tests this.
Buttons are indexed with numbers in TIC-80:
0 means up, 1 means down, 2 means left and 3 means right. Which number denotes which button is documented in the key map page, and the function btn()
has its own wiki page describing how it works, like all the functions available in TIC-80 do.
After the button pressing lines we have this function call:
cls(13)
It isn't inside any if statements, so it's called 60 times per second! What the function does is that it clears the screen. In other words it paints the screen with a solid colour. The colours, like buttons before, are indexed with numbers - we have 16 colours to use. The 13 which was seen here, corresponds to a light blue colour.
We could now take a slight detour to the sprite editor by pressing F2.
The colour palette is located in the bottom left - and if we count from 0, light blue is number 13. We could fiddle with the pen tool and draw stuff (see our moving sprite on the right side!), but let's go back to code editor by pressing the button on the top-left corner, or F1.
So it's possible to change the background colour by changing the argument of the cls()
function to some other number from 0 to 15. We could also remove the function call altogether to get some interesting effects...
After cls()
we have a rather nasty-looking statement.
spr(1+t%60//30_2,x,y,14,3,0,0,2,2)
Let's... not dive into it yet. It's easier to explain first the last two lines:
print("HELLO WORLD!",84,84)
This is the print function, it just draws the white text HELLO WORLD
to coordinates 84, 84.
And then we have
t=t+1
TIC()
gets called 60 times per second, and every time t is increased by 1. So in one second, it gets increased by 60! It tells the passage of time, and its units are frames, not seconds. It will provide itself useful later!
An important note: to be able to use the expression above, we have to have an initial value for t
, and at the beginning, we declared it to be 0
. In Python we could use a shorthand t+=1
, but on Lua this isn't unfortunately possible!
But now the TIC()
function has reached its end, so it's time to get back to this:
spr(1+t%60//30*2,x,y,14,3,0,0,2,2)
The function is spr()
, which draws a sprite from the sprite memory to the screen, right where we want it. Let's first go with a simpler example to understand the basics: change the line to
spr(1,x,y)
Now we get...
So, it still draws... something? At least the sprite is still drawn in the coordinates x
and y
, which change as we press the directional buttons.
Let's take a look at the sprite function description. It says
spr id x y [colorkey=-1] [scale=1] [flip=0] [rotate=0] [w=1 h=1]
This essentially means that the function is called like this:
spr( id, x, y, colorkey, scale, flip, rotate, w, h )
Mind that the arguments in [brackets]
are optional, having their default values written out in description. That's why we can just call the function with
spr(1, x, y)
, like we just did! It's the same as spr(1, x, y, -1, 1, 0, 0, 1, 1).
NOTE: Mind that the order of arguments can't change, so if you want to use the "rotate" argument, you have to include colorkey, scale and flip, too!
The id
argument tells which sprite to load from memory. You can see all the sprites saved to the sprite memory in sprite editor. It contains two sections (FG and BG, or foreground and background), each containing 256 8x8 sprites. BG contains sprites with ids 0-255 and FG 256-511. You can change between the slots by pressing TAB.
The only difference between FG and BG memory slots is this: we can use the sprites from the BG memory to draw the map in the map editor - more on that later! But for the spr(), we can call any sprite from BG or FG slots: just call the sprite with a number from 0 to 511.
But let's not get ahead of ourselves, let's go back to spr()
!
The function description tells that if we want to draw multiple sprites with just one spr() call, we adjust the width and height arguments! If we change them both to 2 and call the function as
spr(1, x, y, -1, 1, 0, 0, 2, 2)
Now we see the whole 16x16 character! We can make it bigger by changing the scale back to 3:
spr(1, x, y, -1, 3, 0, 0, 2, 2)
And then we'd like to get rid of the yellow background. There isn't a transparent colour in TIC-80, so we have to deliberately choose which colour will be transparent with colorkey
. By default all colours are drawn (it's -1), so to make yellow transparent, we change it to
spr(1, x, y, 14, 3, 0, 0, 2, 2)
Now everything looks like before - almost. The original sprite was blinking - or jumping between two animation frames. We can get the other frame of the animation by calling spr like this:
spr(3, x, y, 14, 3, 0, 0, 2, 2)
The original sprite call used a clever trick to make the value jump between 1 and 3 every half seconds (or 30 frames). We can test it by printing its value in the print() function:
print(1+t%60//30*2,84,84)
Let's break the value down:
1+t%60//30*2
% is the modulo operator, it tells us the remainder when dividing a number with another:
For example 150/60=2.something
, or in other words 2, remains 30. Thus,
150%60=30
And as we might remember from earlier, t
tells how many frames have passed since the beginning of the game. By using the modulo operator we limit the values:
t%60
returns values 0,1,2,...58,59,0,1,2...
Similarly, t%2
returns values 0,1,0,1,0,1...
The modulo expression can be utilised to only return values 1
and 3
with
1+t%2*2
But numbers we get from this statement change way too fast to make for a readable animation. To make it slower we use the floor division operator //
, which isn't part of regular Lua but is added to TIC-80. It's basically a counterpart for the modulo operator, giving the result of division operation without the remainder, or in other terms without any decimals that we would get from regular division:
150/60 = 2.5
150//60 = 2
So with floor division we always get integers as a result! It's indeed useful, as e.g. the sprite ids are always integers.
So. t%60
gives always numbers from 0
to 59
, which are then operated with //30
. Let's denote t%60
with n
. If number n
is under 30
, n//30=0
. If n
is 30 or over, but under 60,
n//30=1
. Thus,
t%60//30
jumps between 0
and 1
every 30 frames! To get the desired values 1
and 3
, we multiply this whole thing by 2 and add 1:
1+t%60//30*2
And that's it! The animation works just like before. And not only that - we've gone through the whole default cart.
Hopefully you now understand better how things work in TIC-80. But that was (mostly) only about the code
editor - in the next lesson we'll get acquainted with the other ones!
See also Part 2 of this tutorial.