-
Notifications
You must be signed in to change notification settings - Fork 0
Level_01
Syntax of variable declaration is variable_name = value
or variable_name = other_variable
Value can be a number, boolean, string of characters or Object.
If other variable is given as a value, the value of that variable is copied.
Variables can be declared anywhere. Preceding the variable name with keyword local creates a local variable that is only accessible until the next end. Variables that are not local, are global variables and are accessible everywhere. It is a good practice to use local variables whenever possible.
The example code declares different variables and writes their values to the TIC-80 command line by using the trace() function. This function is useful for testing your code and seeing the results of operations. At the very end there is call to exit() which will change from graphical mode back to the command line. Since we are not drawing any graphics yet you need to remember to place this in the end or press ESC to see the output of trace().
Lines starting with -- are comments and are ignored when the code is run.
-- Numbers
integer=20
negative=-5
fraction=1.99
function TIC()
trace(integer)
-- Operator .. concatenates values to text
trace("Value of negative is: " .. negative)
-- Numbers support the basic math operations + - * and /
trace(2+2)
trace(1-3)
trace(5*2)
trace(10/3)
local product=integer*negative
trace("Product is " .. product)
-- For integer division use //
trace(10//3)
-- For getting the remainder of integer division, use % aka modulo
trace(10%3)
exit()
end
Write your own program that translates days to seconds and seconds to days. Use variables to store the multipliers and dividers and present the calculation using variables. Don't account for leap yeas :)
function TIC()
local input_days=1
local input_seconds=5000
local out_seconds = 0
local out_days = 0
-- Your code here
trace("Days to seconds: " .. input_days .. " days is " .. out_seconds .. " seconds")
trace("Seconds to days: " .. input_seconds .. " seconds is " .. out_days .. " days")
exit()
end
The previous examples have had function TIC() in them, but what is it? This is a special function that your code must always have. It is what the TIC-80 calls or executes each frame. That means that whatever code is inside the TIC() function happens 60 times every second. Usually the first thing to do in TIC() is to call cls() to clear the screen before drawing anything else. The code inside the function TIC() is any line that is between lines function TIC()
and end
.
On the contrary, whatever code is outside function TIC() happens only once, before the function TIC() is called for the first time.
If in the future exercises you find that either nothing happens or too much happens, it is usually because of mistake made regarding this function. Here is a short troubleshooting list for that kind of situation:
- Did you call cls() before drawing anything?
- Did you place the function call outside or inside function TIC()?
- Did you assign to a variable outside or inside function TIC()?
- Did the things you draw move, and if they move, did they instantly fly outside the screen area?
The example code below demonstrates what it means when things happen 60 times per second. It prints the amount of frames and moves a rectangle one pixel every frame. The other rectangle changes colour every frame.
frame=0
function TIC()
cls(13)
local x=frame%240
local c=frame%16
print(frame, 1,1,4)
rect(x,20,10,10,5)
rect(0,30,10,10,c)
frame=frame+1
end
- What happens if the line
frame=0
is inside function TIC()? - What happens if you move the print() line or either of rect() lines outside function TIC()?
- What happens if you remove the line
cls(13)
?
TIC-80 comes with a selection of functions that can be used to draw things, read input, measure time etc. Functions require zero or more parameters to specify what the function should do. Some parameters are optional, meaning that if they are not provided, a default is used. Documentation of the functions is found in the TIC-80 website. The documentation gives the function name and the list of parameters. Optional parameters and their default values are surrounded with brackets.
For example the documentation for screen clearing function cls [color=0]
means that the cls() function takes one optional parameter. If the parameter is not given, it gets the default value 0.
This code first clears the screen to dark blue and then draws different shapes over it. To see what color is what, press F2 to change to sprite editor and mouse over the colors in the palette. The screen size of TIC-80 is 240 by 136 pixels.
The example code calls different drawing functions. Change the numbers around and read the documentation for each one to become familiar with them.
function TIC()
cls(9)
rect(70,30,60,40,4)
elli(180,30,16,40,2)
circ(100,100,10,6)
tri(4,130, 80,80, 20,40, 11)
end
Using the drawing functions, draw your favourite flag. See the gallery at Wikipedia for inspiration
All of the random functions are perfectly predictable in a sense that they will do the same thing every time. presuming the parameters given are the same every time. No matter how many times you call circ(100,00,10,6), the circle will be drawn in the same position and will be of same size. In all other ways too, the computer is predictable and that is great. But for games it is important to have the element of chance and randomness. Without a way to randomize things, the only kind of games possible would be deterministic games, such as chess. Dice games and card games, would be impossible (or someone would have to write very long lists of numbers (and someone else would try to memorize those numbers to get an advantage)).
Luckily, Lua offers a way to get a random number (or at least, a random enough). Calling a function math.random()
will return a fractional number between 0 and almost 1, meaning that the largest number possible is as close to one as possible. But calling it with a single argument returns an integer between 1 and that number. Calling with two arguments works similarly, but the returned number is between the given ones.
Here is a summary:
Call | Smallest possible | Largest Possible | Examples |
---|---|---|---|
math.random( ) | 0.0 | 0.9999999... | 0.14, 0.562, 0.94 |
math.random(N) | 1 | N | (7)-> 1, 5, 7, 3 |
math.random(B,N | B | N | (3,12)-> 5, 3, 10, 7, 12 |
Uses of random numbers in games are many but here are some useful ones:
- Randomizing the starting state, positions, resources, map etc.
- Randomizing the outcome of action, dice roll, the next position to appear in, size of explosion etc.
- Randomizing between similar things: multiple sounds or effects for the same thing.
- Making decisions for the AI.
To test the randomness righ now, you can leave out the call to cls() inside the TIC() so that the things drawn the previous frame are not erased. And then on each run of TIC() draw a new rectangle to random position. This also demonstrates how often the TIC() is called.
SCR_W=240
SCR_H=136
cls(0)
function TIC()
local w=math.random(2,SCR_W/2)
local h=math.random(2,SCR_H/2)
local x=math.random(0,SCR_W-w)
local y=math.random(0,SCR_H-h)
local color=math.random(1,15)
rect(x,y,w,h,color)
end
- Use a different drawing function to fill the screen randomly
- Use rect() function to randomize a new rect that fits to the previous one so that the screen is filled with a kind of twisting corridor.
One of the main purpose of functions is to enable reusing code and building abstractions. The functions we used in drawing the basic shapes are also reusing code. All of them put pixels on the screen, but do it in a different way. It would be difficult and error prone to put individual pixels on the screen with the [pix()(https://github.com/nesbox/TIC-80/wiki/pix) function every time we wanted to draw a circle. Having the circ() function enables us to draw circles in any place, in any color and size. In turn, we can use the drawing functions provided by TIC-80 to compose our own functions for drawing more complex shapes as easily.
The following code creates two custom functions which draw the symbols Diamond and Heart that are used in playing cards. The functions work similarly to the functions provided by TIC-80 in that they also take x, y, size and color as parameters. Note that the functions are placed outside the function TIC(). This way they are on the same level or in the same scope and we can be sure that we can call them inside the TIC() function.
function diamond(x,y,size,color)
local left=x
local center=x+size/2
local right=x+size
local top=y
local middle=y+size/2
local bottom=y+size
tri(left,middle, center,top, right,middle, color)
tri(left,middle, center,bottom, right,middle, color)
end
function heart(x,y,size,color)
local left=x
local center=x+size/2
local right=x+size
local top=y
local bottom=y+size
local radius=size/4
--left circle center x and y
local lccx=left+radius
local lccy=top+radius
-- same for right
local rccx=right-radius
local rccy=top+radius
circ(lccx,lccy,radius,color)
circ(rccx,rccy,radius,color)
tri(left,lccy, right,rccy, center,bottom, color)
end
function TIC()
cls(12)
diamond(40,40,10,2)
diamond(80,40,12,6)
diamond(120,40,22,8)
heart(40,100,10,2)
heart(80,100,12,6)
heart(120,100,22,8)
end
Create the functions for drawing the Club and Spade. If you want to make them really fancy, use the background color to carve out from other shapes. You can also try to create the symbols for Bamboos and Circles from Mahjong.
The keyword if allows to choose what code to execute based on a condition. A condition is expressed as a truth value, also called a Boolean. A boolean value is either true or false. Boolean values are created by comparison operators. If the value is false, the code between if and end is not executed.
The example declares 4 variables and demonstrates the if and comparison operators. The last parameter to trace() is the color of the output.
function TIC()
local a=3
local b=5
local c=3
local d=4
trace("a: "..a, 6)
trace("b: "..b, 6)
trace("c: "..c, 6)
trace("d: "..d, 6)
-- boolean variables
local a_to_b=a<b
trace("a < b: ", 13)
trace(a_to_b, 13)
local b_to_a=b<a
trace("b < a: ", 13)
trace(b_to_a, 13)
if a<b then
trace("a is less than b", 5)
end
if a>b then
trace("a is greater than b", 5)
end
if b>a then
trace("b is greater than a", 5)
end
if a~=b then
trace("a is not equal to b", 3)
end
if a==c then
trace("a is equal to c", 3)
end
if a>=c then
trace("a is equal or greater than c", 10)
end
if a<=d then
trace("a is equal or less than d", 10)
end
exit()
end
Write a program that tells whether a given temperature is warm or cold. You can decide the limits yourself. For example with temperature value of -16 the program could print "Temperature of -16 feels Cold".
cold="Cold"
neutral="Neutral"
warm="Warm"
function TIC()
local temp=20
local result=""
-- your code here assigns one of the temperature text variables to result based on comparisons
trace("Temperature of: " .. temp .. " feels: " .. result)
exit()
end
If condition is dependent on multiple comparisons, the comparisons can be combined together using boolean operators. There operators take 1 or 2 boolean values and return one. The operators are and, or, not.
The rules are:
Logical operations | |||
---|---|---|---|
a | operator | b | result |
true | and | true | true |
true | and | false | false |
false | and | false | false |
true | or | true | true |
true | or | false | true |
false | or | false | false |
- | not | false | true |
- | not | true | false |
Use the logical operators between if and else to combine multiple comparisons. You can chain together more than 2 comparisons and use parenthesis too.
function TIC()
local cold=-10
local nice=0
local warm=10
local temperature=8
if temperature > cold and temperature < nice then
trace("Not cold enough")
end
if temperature < warm and temperature > nice then
trace("Not warm enough")
end
if temperature < cold or temperature > warm then
trace("Temperature outside limits")
end
if temperature==nice or temperature==cold or temperature==warm then
trace("Exact temperature")
end
exit()
end
In games, the boolean operators are often used to calculate state of the game objects. There is a hierarchy of logic usually in the form of:
- Collision tests and comparing numbers to other numbers and limits
- Setting truth values based on those comparisons
- Combining truth values using boolean operators
- Setting or changing the values of variables based on the result of combining truth values
- Repeat.
An example of this could be a situation where the code calculates if the player character is getting wet. In the game the character can be:
- Inside or outside
- Weather can be sunny or rainy
- The character can have an umbrella. From these variables we can find out if character is getting wet or not. If they are outside check if it is rainy. If it is rainy, check if they have the umbrella. If they don't have the umbrella, they are getting wet. Otherwise, or else they are not getting wet. You can use the keyword else to denote what happens if the opposite of the given condition is true.
function TIC()
local getting_wet=false
local outside=true
local rainy=true
local umbrella=false
if outside then
if rainy then
if not umbrella then
getting_wet=true
else
getting_wet=false
end
end
end
if getting_wet then
trace("Player is getting wet")
else
trace("Player is safe and dry")
end
exit()
end
Writing tests like this takes a lot of space and it is easy to make a mistake. This is where boolean operators are useful. The previous check can be compressed down to:
local getting_wet = outside and rainy and not umbrella
This way it is easy to see in what condition the player is getting wet. If any of the three conditions is not true, then player is dry. Note that the not operator is applied before the and operators.
Add one more variable describing what the player is doing inside. They can be on the sofa or in the shower. Now it is possible to be getting wet in the inside as well. Add that variable to the example check and try to compress everything down to one or two lines of boolean logic. Decide for yourself if having an umbrella in the shower is allowed ☔
How could you use boolean logic to notice impossible situations? What should happen in those cases?
If is very useful combined with functions. Continuing from earlier card example, you can use if inside a custom function to draw different playing cards with just one one function card(), that takes position, card number and symbol as parameters. Since playing cards differ only on what symbol they have, it is handy to have one function to draw any kind of card.
To draw text and numbers on the screen, use the print() function, which is similar to trace(), but also needs to know the position of the text.
The example code creates a card function that draws a white background with a black border, and then the symbol and number in the middle. The heart and diamond are drawn in red, club and spade in black. Note the new keyword elseif that combines else and if. It is useful in situations like this where only one of the symbols should be drawn.
-- constants for symbols
HEART=0
DIAMOND=1
CLUB=2
SPADE=3
function card(x,y,number,symbol)
local width=20
local height=32
-- White card with black border
rect(x,y,width,height,12)
rectb(x,y,width,height,0)
-- Symbol and number vertically in the middle. Calculate the upper left position for them
local size=8
local center=x+width/2-size/2
local top=y+size
-- Color is black unless symbol is heart or diamond
local color=0
if symbol==HEART or symbol==DIAMOND then
color=2
end
if symbol==HEART then
heart(center,top,size,color)
elseif symbol==DIAMOND then
diamond(center,top,size,color)
elseif symbol==CLUB then
club(center,top,size,color)
elseif symbol==SPADE then
spade(center,top,size,color)
end
-- Number goes under symbol.
local bottom=y+height-size
print(number,center,bottom,color)
end
- Call the card() function in TIC() to draw cards on the screen.
- Draw your favourite poker hand.
- What happens when card number 10 is drawn? How to center the number?
- Change the function so that number 1 is shown as A and numbers 11 to 13 are J, Q and K.
Instead of drawing complex images with a combination of drawing functions, you can use sprites. Press F2 to open the sprite editor. The default cartridge has two robot sprites. A sprite is a 8x8 pixels square, but multiple sprites can be drawn together as composite sprites. To draw sprites use the spr() function.
Note that when drawing sprites, all the pixels in the 8x8 rectangle are drawn. When drawing the symbols either draw the sprites on a white background or you can select one color of the 16 color palette to be treated as transparent when drawing a sprite.
This code draws the default robot sprite, first just one part and then the whole robot as a composite sprite. The size of the composite sprite are the last two parameters to the spr() function.
function TIC()
cls(13)
-- Single sprite
spr(1,40,40)
-- 2x2 Composite sprite
local w=2
local h=2
spr(1,40,60,-1,1,0,0,w,h)
end
- Draw the card symbols in the sprite editor and change the symbol constants to match the indices. Change your card() function so that instead of having an if-elseif-end chain with the shape functions, just call the spr() with the given sprite index.
- Create a function to draw composite sprites more easily. The function signature should be
function cmp_spr(sprite_index, x, y, width, height)
This far the programs have not been interactive: they do the same thing every time. Now we are adding input listening so that player can press some buttons. The TIC-80 emulates 8 button game pads with directional buttons and A, B, X, Y. In this example we are using the directional buttons of player one to change the number and symbol on the card.
The basic idea is that reading the input changes variables and those variables are then used in drawing the screen. Here the variables are the card number and symbol. In the example code, pressing up and down changes the number on the card.
Here also it is useful to employ '''elseif''' instead of having multiple if sections for each buttons. This is because we want to allow player to only press one button at a time. Pressing up and down simultaneously would be meaningless in this situation.
The function btnp() returns true when the button given as a parameter is released. The btn() version returns true if the button is down at the moment and is useful in movement.
To keep track of the input, you need to call this function every frame. You can place the function call directly between if and then, it is equivalent to having
local up_pressed = btnp(UP)
if up_pressed then
number=number+1
end
Add the code below to your program that has the card() function.
-- Global variables changed by input
number=1
symbol=HEART
-- Constants for buttons
UP=0
DOWN=1
LEFT=2
RIGHT=3
-- inside function TIC()
if btnp(UP) then
number=number+1
elseif btnp(DOWN) then
number=number-1
end
cls(7)
card(10,10,number,symbol)
end
- Use if checks to prevent player from choosing numbers other than 1 to 13
- Listen for the left and right buttons to change the symbols. NOTE! Attempting to draw sprite index -1 will crash the TIC-80
- Similarly prevent selecting symbols that are not available. Alternatively you can make both number and symbol selection loop around so that pressing up on number 13 goes back to 1 and vice versa.
TIC-80 also emulates a mouse. The mouse() function is different from functions we have used before, since it returns multiple variables. In the example code a simple cross cursor is drawn on the mouse position and the color changes when the left mouse button is down.
function TIC()
local x,y,left,middle,right,scrollx,scrolly=mouse()
cls()
local color=12
if left then
color=2
end
line(x-4,y,x+4,y,color)
line(x,y-4,x,y+4,color)
end
- Draw a nicer mouse cursor by using the drawing functions or by using a sprite. Make the cursor change when user is holding down the left mouse button.
- Stop the mouse cursor from leaving the screen area.
A simple collision test is to test if a point is inside a rectangle. The example code shows a program that gets the mouse position and sets variables for a rectangle. If the mouse position is inside the rectangle, the rectangle color changes before the rectangle is drawn.
function TIC()
cls()
--Rectangle
local rx=10
local ry=20
local rw=60
local rh=40
local rc=3
--Mouse variables
local x,y,left,middle,right,scrollx,scrolly=mouse()
--Check if cursor is inside rectangle
local inside=false
if x>rx and x<rx+rw and
y>ry and y<ry+rh then
inside=true
end
if inside then
rc=5
end
rectb(rx,ry,rw,rh,rc)
line(x-4,y,x+4,y,12)
line(x,y-4,x,y+4,12)
end
- Check what happens when the cursor is just on the border of the rectangle. Does it work correctly? How to fix it?
- Make a check to see if the cursor is outside the rectangle.
- Make a check to see if the cursor is exactly on the border of the rectangle.
Thus far, the programs have had very little state. Meaning that there are only one or two states the program can be in. In the previous example the rectangle was different depending on the state of the mouse and also the card was drawn differently depending on the variables.
In this example we look at different kinds of buttons and the state information needed to implement them. The example code demonstrates:
- A buzzer button. This kind of button is either pressed down or released. When pressed down, it makes a sound. Like a door buzzer.
- A toggle button. Every time a toggle is clicked it changes state. A computer's power button is a toggle button. It turns the computer on and off. Also a ball point pen works like a toggle.
- A switch button. This kind of button has two positions, on and off. A light switch is a switch button. When the button is in ON position, setting it to ON again does nothing.
The thing we are interested in is when the something changes. To detect any kind of change, we need to know how the thing was before, how it is now and compare the two. For example if a cheeseburger used to cost 2 euro, but now it costs 5, it is clear that the price has changed. By comparing the old and new information, we can deduct how the information has changed.
Here is a table showing all the possible states of the toggle button and the conclusion.
Old state | New state | Change? |
---|---|---|
Up | Up | No change |
Up | Down | Button was pressed! |
Down | Down | No change |
Down | Up | Button was released! |
From this we can see that when Old state differs from the new state, a change has taken place. And then depending on the states, we can know whethet the button was pressed down or released. In the case of toggle button, the only change we are interested in is when button is released.
Study how each of the buttons work. Notice how the computer goes on and off only after you release the button.
buzzer_down=false
toggle_prev=false
toggle_on=false
switch_on=false
UP=0
DOWN=1
LEFT=2
RIGHT=3
function onoff(state)
if state then
return "ON"
else
return "OFF"
end
end
function TIC()
buzzer_down=btn(UP)
-- Read current state
local toggle_now = btn(DOWN)
-- Check that and the previous state
if toggle_now==false and toggle_prev==true then
-- Set ON or OFF
if toggle_on==true then
toggle_on=false
else
toggle_on=true
end
end
-- Replace old state with new state
toggle_prev=toggle_now
if btn(LEFT) and switch_on==true then
switch_on=false
end
if btn(RIGHT) and switch_on==false then
switch_on=true
end
cls(13)
if buzzer_down then
print("BZZZZ!",10,10)
end
print("Computer is "..onoff(toggle_on),10,20)
print("Light is " .. onoff(switch_on),10,30)
end
- There is no ready made function to know when user has clicked a mouse button. Add a new toggle button that reacts to left button on the mouse.
- Implement a three-way switch similar to automatic transmission selector in cars. It has 3 positions: Reverse, Park and Drive
- Implement a toggle that can only be operated while a secondary button is held down. Think of a real world example.
- Are there other kinds of buttons in the real world? How would you implement them?
The mouse works quite differently from buttons. It is a good idea to create custom functions that work similarly so that it is easy to use the mouse to control the game.
The functions would be:
function mbtn(button)
function mbtnp(button)
Where the button is either left, middle or right. And they would work similarly to the btn() and btpn() functions that TIC-80 provides. To facilitate this, we need to read the mouse input at the start of each frame and keep track of the state of all the buttons.
The example code shows a short program that uses these functions and other state of the mouse to register mouse buttons and draw a selection area. All the code is not given, but left as an exercise.
-- Names for mouse buttons
MOUSE_LEFT=0
MOUSE_MIDDLE=1
MOUSE_RIGHT=2
-- Mouse state
-- Store what buttons are down
mouse_left_down=false
mouse_middle_down=false
mouse_right_down=false
-- Store if button was clicked
mouse_left_clicked=false
mouse_right_clicked=false
--[[
Read the mouse state
and check if any of the buttons
was clicked and store it
Then update state
]]--
function update_mouse()
mouse_x,mouse_y,mouse_left,mouse_middle,mouse_right,mouse_scroll_x,mouse_scroll_y=mouse()
--[[
If mouse button is no longer down,
but it WAS down last time, then it
must have been released:
record a click
]]--
if mouse_left==false -- no longer down
and mouse_left_down then -- was down
mouse_left_clicked=true
else
mouse_left_clicked=false
end
-- and so on for middle and right buttons
-- Store current state
mouse_left_down=mouse_left
end
-- Return true if given button is down
function mbtn(button)
if button==MOUSE_LEFT then
return mouse_left_donw
end
-- add code for middle and right
end
-- Return true if given button was
-- clicked
function mbtnp(button)
if button==MOUSE_LEFT then
return mouse_left_clicked
end
-- add checks for middle and right
end
mouse_clicks=0
kb_clicks=0
function TIC()
--[[
Call read_mouse at the start of
frame so that the other functions
work correctly
--]]
update_mouse()
cls()
if btnp(0) then
kb_clicks=kb_clicks+1
end
print("UP button clicked "..kb_clicks.." times!",10,10,5)
if mbtnp(MOUSE_LEFT) then
mouse_clicks=mouse_clicks+1
end
print("Mouse left clicked "..mouse_clicks.." times!",10,20,4)
end
Develop the code further so that you can also use the middle and right buttons of the mouse.