Hi coders!
Required KnowledgeThis guide is intended for people who have covered the
beginner's guide, done a little basic map-making of their own to get used to things, and now are ready to move on and learn about the structural commands and techniques needed for more advanced scripting.
ScopeThis guide looks almost exclusively at
function LevelLogic() - it is assumed that a suitable
LevelSetup() already exists with appropriate asteroids added, etc. It will cover the syntax of various important structural commands, various commands from the maths library, and usage examples.
Could be heavy going..!Yea, I know you're eying the size of the scrollbar nervously. This IS a long document. However, I have split this guide into numbered chunks, each of which should take no longer than 20 minutes to read and thoroughly understand. Please, give section 1 a read through, and hopefully you will see that it's not all that difficult. :> I promise to regale you with my distinctive brand of humour along the way. Or something.
Contents:- 1. while GameRunning() do
- 2. for loops
- 3. Arrays
- 4. math.random
- 5. math.sin, math.cos
- 6. Build your own functions!
1. While GameRunning() doi)function LevelLogic()
while GameRunning() do
-- later on we'll put some commands here!
coroutine.yield()
end
end
The commands above create a loop that is run continuously, at a rate of 60 times per second, until the game ends.
This specific type of While loop is useful for all sorts of scripting, and I recommend using it as a base for any advanced scripting level. It's the only
while loop you will need in your code - in fact I'd suggest you should avoid using other While loops altogether.
Any code you place after this loop's
end will never run.
function LevelLogic()
while GameRunning() do
-- later on we'll put some commands here!
coroutine.yield()
end
-- CODE PLACED HERE WILL NEVER RUN
end
That is because the condition of the while loop is "while the game is running". The game is always running! :> The game is only considered to have ended when the player quits, or a
Quit() command is triggered, causing the player to return to the menu.
In the beginner's guide, we looked at how you can use the
OnAsteroidTaken(id,owner) function to trigger a Victory or Loss.
Now we'll look at another method of doing this using a
while GameRunning() do loop, which will work much better alongside our other scripts.
ii)
Suppose we want the player to win if they've taken over the whole galaxy, and lose if they've lost all their asteroids.
First lets make a
While GameRunning() do loop and populate it with comments to remind ourselves what each bit of code needs to do, and where it needs to be inserted.
function LevelLogic()
while GameRunning() do
-- if player 1 has conquered whole galaxy then
-- Quit(true)
-- if player 1 has lost all asteroids then
-- Quit(false)
coroutine.yield()
end
end
iii)So how to tell if the player has conquered the whole galaxy?

One way would be to check if the number of asteroids the player owns is equal to the number of asteroids in the level.
If it is, that means the player must own every asteroid - fulfilling the victory condition.
if GetEmpire(1):GetNumOwnedAsteroids() == 20 then
Pause()
MessageBox("You have won")
WaitDialog()
Unpause()
Quit(true)
end
This would work fine if the number of asteroids in the level is indeed 20.
But what if you decide later on to go back and add more asteroids? If there are 25 asteroids in the level but the
If statement is only looking for 20, then the player will win when there are 5 asteroids still to conquer.
So although this would work, it's not ideal because it means every time we add or remove asteroids from the level we also have to change the number of asteroids the victory condition is checking for.
So what other method could we use that avoids this problem?
iv)Well, maybe we could check how many asteroids are owned by the other empires.
If this map only has empire 2 in it, then maybe we could check it like this:
if GetEmpire(2):GetNumOwnedAsteroids() == 0 then
Pause()
MessageBox("You have won")
WaitDialog()
Unpause()
Quit(true)
end
Then when Empire 2 has no asteroids left, player 1 would win. Doing it like this would mean subsequent changes to the number of asteroids in the level would not break the win condition. :>
But there's a potential problem with this too. Suppose there are 20 asteroids and the player conquers 5 of them, then goes straight on to conquer all 5 of Empire 2's asteroids. That leaves 10 asteroids uncolonised, ie they belong to the greys (empire 0).
The player would win at this point even though they've only conquered half the galaxy - because Empire 2 has 0 asteroids left.

This behaviour isn't bad, but the behaviour we were actually looking for is "if the player has conquered
the whole galaxy"...
v)If we really want the player to have to take over the whole galaxy to score a win, we would need to make a modification to our
If statement to include Empire 0:
if GetEmpire(2):GetNumOwnedAsteroids() == 0 and GetEmpire(0):GetNumOwnedAsteroids() == 0 then
Pause()
MessageBox("You have won")
WaitDialog()
Unpause()
Quit(true)
end
That's better. :> We're saying If Empire 2 doesn't have any asteroids left, and Empire 0 doesn't have any asteroids left, then the player wins.
Now this will mean the player
must conquer every asteroid in the level in order to win.
vi)Ok but what if you have Empire 3, too? What then? The statement in its current form doesn't check for Empire 3 at all, so if there is an Empire 3 in the level then the player might win even when Empire 3 still owns some asteroids!
We could make another modification to our
If statement to also take Empire 3 into account:
if GetEmpire(2):GetNumOwnedAsteroids() == 0 and GetEmpire(3):GetNumOwnedAsteroids() == 0 and GetEmpire(0):GetNumOwnedAsteroids() == 0 then
Pause()
MessageBox("You have won")
WaitDialog()
Unpause()
Quit(true)
end
So there's our victory condition.
vii)What about the loss condition?

Well, in our comments we said that the player should lose "if the player has lost all asteroids".
So we can represent that like this:
if GetEmpire(1):GetNumOwnedAsteroids() == 0 then
Pause()
MessageBox("You have lost")
WaitDialog()
Unpause()
Quit(false)
end
Looking at this code, it seems it should work no matter how many asteroids or empires we add or remove. :>
The loss condition is a lot simpler to make than the win condition..
viii)Lets return to the template that we wrote out at the start:
function LevelLogic()
while GameRunning() do
-- if player 1 has conquered whole galaxy then
-- Quit(true)
-- if player 2 has lost all asteroids then
-- Quit(false)
coroutine.yield()
end
end
ix)...and populate it with the Win Condition we made in
step vi)....
function LevelLogic()
while GameRunning() do
-- win condition
if GetEmpire(2):GetNumOwnedAsteroids() == 0 and GetEmpire(3):GetNumOwnedAsteroids() == 0 and GetEmpire(0):GetNumOwnedAsteroids() == 0 then
Pause()
MessageBox("You have won")
WaitDialog()
Unpause()
Quit(true)
end
-- if player 2 has lost all asteroids then
-- Quit(false)
coroutine.yield()
end
end
x)... and the Lose Condition from
step vii). :>
function LevelLogic()
while GameRunning() do
-- win condition
if GetEmpire(2):GetNumOwnedAsteroids() == 0 and GetEmpire(3):GetNumOwnedAsteroids() == 0 and GetEmpire(0):GetNumOwnedAsteroids() == 0 then
Pause()
MessageBox("You have won")
WaitDialog()
Unpause()
Quit(true)
end
-- lose condition
if GetEmpire(1):GetNumOwnedAsteroids() == 0 then
Pause()
MessageBox("You have lost")
WaitDialog()
Unpause()
Quit(false)
end
coroutine.yield()
end
end
Now we've made a working Win/Lose condition using a
while GameRunning() do loop. :> This will set us up for scripting some advanced behaviours.
This data tunnel is what advanced coding actually looks like...2. For loopsi)The
for loop is an essential tool when it comes to coding.
A typical
for loop might look like this:
for i = 0,9 do
-- some code goes here
end
As you can see, it too needs an "end" to close it off.
What are the numbers about though? And what's that "i =" doing there?
ii)This type of loop counts from the first number (0) to the second number (9), and runs the code within the loop for each number counted.
The example above would start with the counter called "i" on "0", and then run all the code inside the loop. Then, it would increment the counter by one, and run through all the code again. Then it increments "i" again, runs the code again, and so on.
After the code has been run for the 10th time (ie "i" = 9) then the loop will stop and exit at the
endiii)The first and most obvious use of a
for loop is to run a piece of code multiple times. For example, the code below would produce 3 message boxes:
for i = 0,2 do
MessageBox("Hallo Fluffy")
end
iv)The second thing that
for loops are useful for, is to change values for lots of different entities that are
numbered. For example, asteroids. :>
Asteroids are numbered according to their ID. Suppose we had 100 asteroids and wanted to change the Send Distance of all of them at once. Here is how we could do it:
for i = 0,99 do
GetAsteroid(i).SendDistance = 5000
end
What will happen is that each time the loop runs through, "i" is increased by 1, and then when we refer to "GetAsteroid(i)" it's simply getting whichever asteroid ID currently corresponds to the value of "i".
So on the first run through of the loop, the command effectively reduces to this:
GetAsteroid(0).SendDistance = 5000
On the second pass of the loop, it would resolve to this:
GetAsteroid(1).SendDistance = 5000
...and so on.
v)Thus, this:
for i = 0,99 do
GetAsteroid(i).SendDistance = 5000
end
...is just a (much) shorter way of writing this:
GetAsteroid(0).SendDistance = 5000
GetAsteroid(1).SendDistance = 5000
GetAsteroid(2).SendDistance = 5000
GetAsteroid(3).SendDistance = 5000
GetAsteroid(4).SendDistance = 5000
GetAsteroid(5).SendDistance = 5000
GetAsteroid(6).SendDistance = 5000
GetAsteroid(7).SendDistance = 5000
GetAsteroid(8).SendDistance = 5000
GetAsteroid(9).SendDistance = 5000
GetAsteroid(10).SendDistance = 5000
GetAsteroid(11).SendDistance = 5000
GetAsteroid(12).SendDistance = 5000
GetAsteroid(13).SendDistance = 5000
GetAsteroid(14).SendDistance = 5000
GetAsteroid(15).SendDistance = 5000
GetAsteroid(16).SendDistance = 5000
GetAsteroid(17).SendDistance = 5000
GetAsteroid(18).SendDistance = 5000
GetAsteroid(19).SendDistance = 5000
GetAsteroid(20).SendDistance = 5000
GetAsteroid(21).SendDistance = 5000
GetAsteroid(22).SendDistance = 5000
GetAsteroid(23).SendDistance = 5000
GetAsteroid(24).SendDistance = 5000
GetAsteroid(25).SendDistance = 5000
GetAsteroid(26).SendDistance = 5000
GetAsteroid(27).SendDistance = 5000
GetAsteroid(28).SendDistance = 5000
GetAsteroid(29).SendDistance = 5000
GetAsteroid(30).SendDistance = 5000
GetAsteroid(31).SendDistance = 5000
GetAsteroid(32).SendDistance = 5000
GetAsteroid(33).SendDistance = 5000
GetAsteroid(34).SendDistance = 5000
GetAsteroid(35).SendDistance = 5000
GetAsteroid(36).SendDistance = 5000
GetAsteroid(37).SendDistance = 5000
GetAsteroid(38).SendDistance = 5000
GetAsteroid(39).SendDistance = 5000
GetAsteroid(40).SendDistance = 5000
GetAsteroid(41).SendDistance = 5000
GetAsteroid(42).SendDistance = 5000
GetAsteroid(43).SendDistance = 5000
GetAsteroid(44).SendDistance = 5000
-- ok i can't be bothered writing them all out.
-- hopefully you get the idea...
-- ...
GetAsteroid(97).SendDistance = 5000
GetAsteroid(98).SendDistance = 5000
GetAsteroid(99).SendDistance = 5000
vi)Finally, just so you're aware, you don't
have to use "i" for the counter. You can use whatever name you want. For example:
for fluffy = 0,99 do
GetAsteroid(fluffy):AddSeedlings(fluffy)
end
That would put 0 seedlings on asteroid 0, 1 seedling on asteroid 1, 2 seedlings on asteroid 2, etc.
It doesn't matter that we called the counter "fluffy" instead of "i". It will still work just fine. :>
This explosion is HUGE....3. Arraysi)Arrays are special variables that have lots of different "slots" for storing values, rather than just being one big slot like the variables we've seen so far.
Another way to think about arrays is to imagine them as numbered lists.
ii)Lets look at some array syntax.
Before you can use an array, you must first declare it like this:
fluffy = {}
Note the unusual shaped brackets. This creates an empty array called
fluffy.
The usual place to put this command is anywhere in
function LevelSetup(), but for now, to keep everything together, I'll put mine at the very top of
function LevelLogic().
function LevelLogic()
-- declare the array
fluffy = {}
while GameRunning() do
-- some commands go here later
coroutine.yield()
end
end
iii)Now that we've initialised the
fluffy array, we can start assigning values to the slots in it. But how to refer to the different slots in
fluffy?
The answer is with some square brackets tagged on the end like this:
fluffy[0]That would refer to slot 0 of
fluffy.
From now on when we refer to the array, we always refer to the slot number that we are interested in as well.
By convention, we usually start with slot 0.

So we can use commands like these:
fluffy[0] = 2
fluffy[1] = 4
fluffy[2] = 7
fluffy[3] = 9
fluffy[4] = 5
iv)Then, when we want to know about the value in a slot, we can test it using the same square brackets as above. For example:
if fluffy[0] == 2 then
MessageBox("w00t!")
end
So what practical use is this?
Well, as I discovered, it turns out there are all sorts of reasons why you might want to store things in an array like this.
To illustrate why, and also to show you a good example of using an array to do useful work, lets implement an example advanced behaviour using some arrays.
What we will do is create a mechanic whereby the player's lasermine can fly from one asteroid to another, and each asteroid they fly to changes the background to a different colour.
So when the game starts, the player's lasermine is orbiting the first asteroid, and the background is black. When it flies to a nearby second asteroid, the background turns red. When it flies to the next asteroid, the background turns blue, and so on.
We will store the colours in three arrays named
red,
green and
blue because those are nice logical names that we can plug into our
SetBackDropColour command later on.
v)In this example lets assume there are 5 asteroids in the level, numbered 0, 1, 2, 3 and 4.
At the top of our
function LevelLogic(), before the start of the
while loop, I'll initialise the arrays and assign some values to them.
function LevelLogic()
-- initialise arrays
red = {}
green = {}
blue = {}
-- assign values for the different slots in the arrays
red[0] = 0
green[0] = 0
blue[0] = 0
red[1] = 255
green[1] = 0
blue[1] = 0
red[2] = 0
green[2] = 255
blue[2] = 0
red[3] = 0
green[3] = 0
blue[3] = 255
red[4] = 127
green[4] = 0
blue[4] = 127
while GameRunning() do
-- later on we'll put some commands here!
coroutine.yield()
end
end
vi)Now we'll just need to make some code in our
while loop to check if the player has a mine at any of the asteroids.
Here we can combine the
for loops we learned about earlier with the arrays we've just initialised.
function LevelLogic()
-- initialise arrays
red = {}
green = {}
blue = {}
-- assign values for the different slots in the arrays
red[0] = 0
green[0] = 0
blue[0] = 0
red[1] = 255
green[1] = 0
blue[1] = 0
red[2] = 0
green[2] = 255
blue[2] = 0
red[3] = 0
green[3] = 0
blue[3] = 255
red[4] = 127
green[4] = 0
blue[4] = 127
while GameRunning() do
-- Check all the asteroids in turn
for i = 0,4 do
-- if an asteroid has a player mine on it...
if GetAsteroid(i):GetNumMines(1) > 0 then
-- set the backdrop colour to the values for slot i in the red, green and blue arrays
SetBackdropColour(red[i], green[i], blue[i])
end
end
coroutine.yield()
end
end
And that is how we can use arrays to do useful things for us.You can download a playable level file containing this example script and check out the functionality for yourself. It's attached to the bottom of this post, named "
array example.lua".
vii)Before we finish up with arrays, lets look at how you might set the values for hundreds of slots in an array using a formula placed inside a
for loop.
Imagine the example above, but instead of 5 colour-changing asteroids, we have 500 colour-changing asteroids!
Clearly setting these manually would take a very long time.
Therefore it becomes necessary to think up some way to set the colours for all 500 at once.
Lets take the
red array as our example, and figure out some way to generate different values for all 500 slots.
for i = 0,500 do
red[i] = ?
end
Any ideas? Well, the very most simple thing we could do is to make the slot [
i] equal to the value
i itself.
for i = 0,500 do
red[i] = i
end
Then slot 0 would contain the value 0, slot 230 would contain the value 230, and so on.
viii)That's a bit boring though - and besides, Asteroid 500 would have the corresponding red value of 500, and that's more than the maximum 255 that
SetBackdropColour is expecting.
Maybe we can use some maths to make a more interesting series of values!
for i = 0,500 do
red[i] = 1 / i
end
That should be pretty cool. Imagine what this would work out as, for all the different values of
i.
If
i = 2, then the calculation would be 1 / 2 =
0.5If
i = 5, then the calculation would be 1 / 5 =
0.2If
i = 10, then the calculation would be 1 / 10 =
0.1If
i = 100, then the calculation would be 1 / 100 =
0.01ix)There's a problem with this though.
What do you think happens when i = 0?
If
i = 0, then the calculation would be 1 / 0 = ........
Dividing by zero makes Eufloria crash. It's because if you divide something by zero, the answer is always infinity.

To combat this problem, we can make a change:
for i = 0,500 do
red[i] = 1 / (i + 1)
end
Why do you think we need "(i + 1)" and not simply "
i" on its own?
We add 1 to i. That way on the first cycle it works out as 1 / 1, which is just a nice harmless 1 instead of an infinity. :>
The curly brackets just mean that part of the calculation will be worked out first, as a seperate chunk - before the result is used in the rest of the calculation.
x)This calculation will then give us an array called
red that has all its slots filled with values between 0 and 1.
This is ideal mathematically, because it means we can just multiply in the range that we need. In this case, SetBackdropColour expects values between 0 and 255.
So we just multiply each value by 255:
for i = 0,500 do
red[i] = 1 / (i + 1)
red[i] = red[i] * 255
end
Then, using some similar example calculations as we used above...
If
i = 0, then the calculation would be (1 / (0 + 1)) * 255 = 1 * 255 =
255If
i = 1, then the calculation would be (1 / (1 + 1)) * 255 = 0.5 * 255 =
127If
i = 4, then the calculation would be (1 / (4 + 1)) * 255 = 0.2 * 255 =
51If
i = 9, then the calculation would be (1 / (9 + 1)) * 255 = 0.1 * 255 =
26If
i = 99, then the calculation would be (1 / (99 + 1)) * 255 = 0.01 * 255 =
3Hopefully you should be able to see from this selection of examples that this gives us a nice smooth gradiant of different values. The asteroids with the lowest ID numbers will be reddest. :>
Multiple arrays can be combined to represent a matrix. We'll cover matrix usage in a future Advanced Guide.It's up to you to find your own formulas to assign values for your arrays. The maths sections below will help you with this.
4. math.randomi)The command
math.random(a,b) generates a random number between
a and
b.

So for example, the following command would mean that the variable "
MyRandomNumber" is assigned a value between 1 and 6.
MyRandomNumber = math.random(1,6)
This is the equivalent of rolling a six-sided dice. :>
ii)So what use is this?
Well, it's useful for all sorts of things. One good example is randomising the stats of your asteroids. This can make the level different every time it is played, adding variety.
Suppose we have an asteroid like this one...
a = AddAsteroidWithAttribs(1500,1200, 0.5,0.5,0.5)
a.owner = 1
a.TreeCap = 3
a.radius = 200
a.SendDistance = 2000
a.Moveable = false
a:AddSeedlings(20)
We could make some of the stats random, like this:
a = AddAsteroidWithAttribs(1500,1200, 0.5,0.5,0.5)
a.owner = 1
a.TreeCap = math.random(2,5)
a.radius = math.random(100,450)
a.SendDistance = 2000
a.Moveable = false
a:AddSeedlings(math.random(10,50))
Now the asteroid will have a different treecap, radius and number of seedlings each time we play. :>
iii)How else could we improve this code?
Well, we could randomise the send distance, too.
That would be like this:
a.SendDistance = math.random(1500,2500)
But the problem with that approach is that we lose the relationship between an asteroid's radius, and its Send Distance. It's visually pleasing to have asteroids with send distances proportional to their size.

So maybe we could do something else instead, that preserves the relationship between size and send distance...
a.radius = math.random(100,450)
a.SendDistance = a.radius * 10
This means that if the asteroid spawns with a radius of 100, its send distance would be 1000; if it spawns with radius 250, its send distance would be 2500, and so on.
This technique allows us to model the send distance after the asteroid size, even when we don't know in advance exactly what size the asteroid will be.
iv)What else could we do to make it even more random?
Well, we could try adjusting the X and Y coordinates of the asteroid.
So our
a = AddAsteroidWithAttribs(1500,1200, 0.5,0.5,0.5) becomes this:
a = AddAsteroidWithAttribs(math.random(-1500,1500),math.random(-1200,1200), 0.5,0.5,0.5)
Negative numbers like those in the line above work fine in math.random. :>
But, there is a problem with randomising asteroid positions, and that is that there's a chance asteroids may spawn on top of each other!
This clearly will not do, so to fix it we can change the
Moveable command from
false to
true.
a.Moveable = true
Then, if asteroids spawn on top of each other, the game will move them apart for us. :>
v)Lets take a look at what we've got so far:
a = AddAsteroidWithAttribs(math.random(-1500,1500),math.random(-1200,1200), 0.5,0.5,0.5)
a.owner = 1
a.TreeCap = math.random(2,5)
a.radius = math.random(100,450)
a.SendDistance = a.radius * 10
a.Moveable = true
a:AddSeedlings(math.random(10,50))
That's pretty damn random!

We might get a tiny asteorid with treecap 5 and 50 seedlings to the northeast, or we might get a large asteroid with treecap 2 and 10 seedlings to the south... And so forth.
There's one more thing I want to show you while we're looking at math.random, and that is the question of how to randomise asteroid attributes effectively.
vi)Consider this command:
a = AddAsteroidWithAttribs(math.random(-1500,1500),math.random(-1200,1200), math.random(0,1),math.random(0,1),math.random(0,1))
Ok so it's a long one, but basically if you study that, you should get that the asteroid attributes should be between 0 and 1.
Well, that's correct. But there's a problem that you would quickly discover if you were to use this type of command on one of your asteroids. See if you can figure out what it is. :> You'll be doing well if you manage, because I haven't explicitly mentioned it yet...
In the mean time, lets reorganise that line so it's much easier to read:
-- generate X and Y
x = math.random(-1500,1500)
y = math.random(-1200,1200)
-- generate stats
energy = math.random(0,1)
strength = math.random(0,1)
speed = math.random(0,1)
-- add the asteroid
a = AddAsteroidWithAttribs(x,y, energy,strength,speed)
That's better. Did you spot what the problem could be?
vii)The problem we will face is that the asteroid's attributes will be set to either total maximum (1) or nothing whatsoever (0).
There's no in between!
To have nice stats for our asteroids, we actually wanted values
between 0 and 1 -
not 0 or 1 themselves!More generally, we can say that the
math.random(a,b) command generates an
integer (a whole number) between A and B.
So suppose we used
math.random(1,5)Values we might get from that command:
Values we definitely WOULD NOT get from that command:
Hopefully this is crystal clear to you now. Assuming so, lets press on and solve the asteroid attributes problem!
viii)If we really need random values like 0.3, we are going to have to find a way to make them out of integers.
Here's one way:
-- generate stats
energy = math.random(0,10) / 10
strength = math.random(0,10) / 10
speed = math.random(0,10) / 10
So lets follow through what happens here.
Take
energy for our example. First, a random number between 0 and 10 is generated; lets say we generate a
6.
Then, that 6 is divided by 10. What's 6 divided by 10? It's 0.6!
So then the asteroid would have 60% energy.
ix)The problem with that approach, once again, is that simply randomising the asteroid's attributes sometimes results in tiny asteroids with awesome stats, and huge asteroids with rubbish stats.
This breaks the relationship we talked about earlier between the size of an asteroid, and its quality.
What do we really need, then?
Lets think about this. It's important to put the solution into words (and perhaps diagrams) before we try to code it.
We need asteroids to have their stats correspond with their size. But we don't simply want flat values for energy, strength and speed - we want the
proportions of each stat to be random.
So for example, we want an average sized asteroid to have stats like 0.3,0.7,0.5 - NOT 0.5,0.5,0.5.
I know a good solution to this problem, but instead of me telling you what it is yet again, I'm going to leave this problem open-ended and see if you guys can work it out. :> If you've read this far down the guide you already know everything you need to know in terms of commands needed to code it.
5. math.sin, math.cosi)Maybe you remember these dreaded symbols from school. If you're anything like me, the "sin" and "cos" buttons on my calculator taunted me back in maths class with their mystery. Nobody ever really bothered to explain to me what they actually
are. I mean, what are they? Sure, you put numbers into them and a result magically comes out, but
how?
Incidentally, I had forgotten just about everything I used to know about their usage when I started teaching myself to code in lua. I had to figure this out on my own from researching it on the internets. So even if you've never heard these terms before in your life, don't worry. I'll help you to understand them. :>
The first half of this section will focus on helping you to conceptualise what sine and cosine are.
The second half will focus on using the calculations in your scripts to do useful (and often spectacular!) work.
ii)The commands might be used inside of a
while GameRunning() do loop, as follows:
x = math.sin(GetGameTime())
y = math.cos(GetGameTime())
...or to make it slightly clearer, lets assign the game time to a variable first:
time = GetGameTime()
x = math.sin(time)
y = math.cos(time)
We feed a value
into the
math.sin or
math.cos (in this case,
time).
Then we record the result to a variable, in this case
x and
y respectively.
What sort of values will we get for
x and
y as the Game Time proceeds?
iii)It will
always be a value between -1 and 1. So it might be 0.33351, or it might be -0.778391, or it might even be 0. It all depends on what value is fed into it.
If
sin is plotted on a graph, it produces a wavy line:

This wavy line repeats itself for as long as the value fed in (eg GetGameTime()) increases.
The graph for
cos is almost exactly the same. The only difference is that it starts with the line in a different position. Don't worry about this for now, it will all become clear (hopefully!)
iv)Lets proceed to conceptualising how this "value between -1 and 1" is calculated.
Imagine you are a bird hovering above a field.
In the field below you is a circular train track.
A train drives in circles round the track.

The
distance the train has travelled so far is what we will feed into our sin and cos to produce the values between -1 and 1.
math.sin(DistanceTravelled) is equal to wherever on the
vertical axis the train would be after travelling that far.
math.cos(DistanceTravelled) is equal to wherever on the
horizontal axis the train would be after travelling that far.
So for example, if math.cos(DistanceTravelled) = -1, the train must be on the west side of the track.
Lets look at another diagram to try to visualise this better:
v)That's the whole deal.
Sin and
cos are simply representations of each axis when imagining travelling round and round in a circle.
So if the train had gone round the circle exactly once, the output would be the same as if it had gone round the circle exactly twice, or exactly 3 times.
Please download and run the level file attached to this post called "
sincos example.lua"
Play the level in Eufloria and you can see another example of what
sin and
cos do. Check the names of each asteroid to see what they represent.
vi)The commands
math.sin() and
math.cos() both expect a number inside their brackets, as we've seen.
This input is technically thought to be radians, rather than train tracks or seconds of game time, but in reality you can use any unit of measurement you want and it will be considered as the radians-equivalent. Time is a very common thing to use as the input for
sin and
cos, because it continually counts up at a steady pace, making it ideal for achieving regular pulsing motions.
Now lets look at some useful things we can do with this pulsing effect.
vii)First of all lets build our usual wrapper for this to go into:
function LevelLogic()
while GameRunning() do
-- some code goes here later!
coroutine.yield()
end
end
Now we'll record the game time to a variable called
time...
function LevelLogic()
while GameRunning() do
-- record the time
time = GetGameTime()
coroutine.yield()
end
end
Now lets create a variable called.... oh... I don't know, I guess we'll go with something generic like
input.
function LevelLogic()
while GameRunning() do
-- record the time
time = GetGameTime()
-- calculate a value for "input" by taking the math.sin of time
input = math.sin(time)
coroutine.yield()
end
end
A good start. Now the variable called
input will pulse between -1 and 1 as the game progresses.
1
-1In this graph, the X-axis represents game time. The Y-axis represents the value we'll get for
input.
viii)Now that we've got our pulsing variable, lets figure out something that it could control.
How about the radius of an asteroid?
We could make an asteroid grow bigger, then smaller... then bigger, then smaller...
For that, values between -1 and 1 are no good. We need values between, say, 50 and 500!
Time to do some maths to change
input to our desired range of values.
-- input is a value between -1 and 1
input = input + 1
-- now input is a value between 0 and 2
input = input / 2
-- now input is a value between 0 and 1
input = input * 450
-- now input is a value between 0 and 450
input = input + 50
-- now input is a value between 50 and 500!
I did it in some slow steps there to make it easy to follow, but to make things a bit neater, I could just bundle all of that into one line like this:
input = (((input + 1) / 2) * 450) + 50
iv)Now lets fire that line into the rest of our code:
function LevelLogic()
while GameRunning() do
-- record the time
time = GetGameTime()
-- calculate a value for "input" by taking the math.sin of time
input = math.sin(time)
-- now input is between -1 and 1
input = (((input + 1) / 2) * 450) + 50
-- now input is between 50 and 500
coroutine.yield()
end
end
Finally, lets change the radius of Asteroid 0 using our "input".
function LevelLogic()
while GameRunning() do
-- record the time
time = GetGameTime()
-- calculate a value for "input" by taking the math.sin of time
input = math.sin(time)
-- now input is between -1 and 1
input = (((input + 1) / 2) * 450) + 50
-- now input is between 50 and 500
GetAsteroid(0).radius = input
coroutine.yield()
end
end
And there we have it. Download the level file "
pulse example.lua" and play it in Eufloria to see this code in action.
6. Build your own functions!i)This is it. This is the part where you realise you are actually inside the matrix.

Well, I guess we'll start at the beginning. What is a function?
A function is a container for code.
You put code inside the container, and then you "call" the function from elsewhere.
This very general description likely makes little sense to you for now. Don't worry! Lets look at some examples.
ii)For our first example, lets imagine it's the start of the game and you want to bring up a MessageBox to greet the player.
Your
function LevelLogic() might look like this:
function LevelLogic()
Pause()
MessageBox("Welcome to happyfuntimeland. Is nice here.")
WaitDialog()
Unpause()
end
Four lines of code just to bring up a message on the screen!
It's not a problem for now really, but when you have a
LevelLogic() that is several hundred lines long, it can start to become a nightmare to keep track of where everything is.
Lets put those 4 commands into a function.
We place this text
below everything else, including the "
end" of your
LevelLogic().
function GreetPlayer()
Pause()
MessageBox("Welcome to happyfuntimeland. Is nice here.")
WaitDialog()
Unpause()
end
iii)Now that we've moved that code into its own function, we will need to "call" it.
function LevelLogic()
GreetPlayer()
end
function GreetPlayer()
Pause()
MessageBox("Welcome to happyfuntimeland. Is nice here.")
WaitDialog()
Unpause()
end
Ok so what happens is that when the level loads, LevelLogic() will run the command "GreetPlayer()". This will then run the commands in the function with the corresponding name. It's practically as if the compiler copies and pastes the code inside our new function in place of anywhere we put our "GreetPlayer()" command.
iv)What about other occasions when we need a Message Box?
If we know that we'll always pause the game when these messages appear, then the only thing that changes each time is the content of the message.
We could have lots of different functions, with names like "GreetPlayer()", "PlayerLoseMessage()", "PlayerWinMessage()", and so on. But that would mean writing out loads of functions and cluttering up our code again, when really we only need one function to do this.
How can we display different messages with just the one function, you ask?
Well, what we do is that we
pass a variable into the function.
From now on when we use our "GreetPlayer()" command, the command will expect a variable or value of some kind in the brackets.
Like this:
function LevelLogic()
DisplayMessage("Welcome to happyfuntimeland. Is nice here.")
end
function DisplayMessage(message)
Pause()
MessageBox(message)
WaitDialog()
Unpause()
end
v)So lets walk through what happens there.
First, the level loads and
LevelLogic() starts running. Straight away, it runs the DisplayMessage command which activates the DisplayMessage function. It also passes a value into the function, in this case the value is "Welcome to happyfuntimeland. Is nice here."
The function receives this value, and stores it in a variable called "message".
Then, the game is paused, and a MessageBox is displayed, containing the contents of the variable called "message".
When the MessageBox is clicked, it will Unpause the game.
vi)If you followed all that, you should be able to figure out what this code would do:
function LevelLogic()
DisplayMessage("Welcome to happyfuntimeland. Is nice here.")
DisplayMessage("Chase rabbits down holes!!")
DisplayMessage("It's fun and the rabbits enjoy it.")
end
function DisplayMessage(message)
Pause()
MessageBox(message)
WaitDialog()
Unpause()
end
It would display 3 message boxes one after the other, all with different messages!
This is already just 9 lines of code. Doing it the "normal" way would be 12 lines. Hopefully you can see how, if you have a lot of message boxes, this would save you a lot of space!
viii)You can also call functions from within functions.
For example, consider this code:
function LevelSetup()
energy = {}
strength = {}
speed = {}
radius = {}
senddist = {}
x = {}
y = {}
for i = 0,math.random(5,15) do
energy[i] = math.random(1,10) / 10
strength[i] = math.random(1,10) / 10
speed[i] = math.random(1,10) / 10
radius[i] = math.random (100,500)
senddist[i] = radius[i] * 10
x[i] = math.random(-5000,5000)
y[i] = math.radom(-5000,5000)
a = AddAsteroidWithAttribs(x[i],y[i],energy[i],strength[i],speed[i])
a.radius = radius[i]
a.SendDistance = senddist[i]
a.Moveable = true
end
end
We could split it up like this....
function LevelSetup()
InitArrays()
for i = 0,math.random(5,15) do
RandomiseAttributes(i)
PlaceTheRoids(i)
end
end
function InitArrays()
energy = {}
strength = {}
speed = {}
radius = {}
senddist = {}
x = {}
y = {}
end
function RandomiseAttributes(RoidID)
energy[RoidID] = math.random(1,10) / 10
strength[RoidID] = math.random(1,10) / 10
speed[RoidID] = math.random(1,10) / 10
radius[RoidID] = math.random (100,500)
senddist[RoidID] = radius[i] * 10
x[RoidID] = math.random(-5000,5000)
y[RoidID] = math.radom(-5000,5000)
end
function PlaceTheRoids(RoidID)
a = AddAsteroidWithAttribs(x[RoidID],y[RoidID],energy[RoidID],strength[RoidID],speed[RoidID])
a.radius = radius[RoidID]
a.SendDistance = senddist[RoidID]
a.Moveable = true
end
... and then like this.
function LevelSetup()
GenerateLevel()
end
function GenerateLevel()
InitArrays()
for i = 0,math.random(5,15) do
RandomiseAttributes(i)
PlaceTheRoids(i)
end
end
function InitArrays()
energy = {}
strength = {}
speed = {}
radius = {}
senddist = {}
x = {}
y = {}
end
function RandomiseAttributes(RoidID)
energy[RoidID] = math.random(1,10) / 10
strength[RoidID] = math.random(1,10) / 10
speed[RoidID] = math.random(1,10) / 10
radius[RoidID] = math.random (100,500)
senddist[RoidID] = radius[i] * 10
x[RoidID] = math.random(-5000,5000)
y[RoidID] = math.radom(-5000,5000)
end
function PlaceTheRoids(RoidID)
a = AddAsteroidWithAttribs(x[RoidID],y[RoidID],energy[RoidID],strength[RoidID],speed[RoidID])
a.radius = radius[RoidID]
a.SendDistance = senddist[RoidID]
a.Moveable = true
end
Note how some functions are called from within another function. You can "nest" functions as deep as you like in this way.
viii)Have you realised what's going on yet?
You really are somewhere down the rabbit hole, Alice... because
function LevelSetup() and
function LevelLogic() are themselves just functions which are being called by the Eufloria engine.

You've been inside The Matrix all along!