Author Topic: Level Programming Guides  (Read 40374 times)

annikk.exe

  • Achiever
  • Ent
  • ****
  • Thank You
  • -Given: 0
  • -Receive: 4
  • Posts: 1,809
« Last Edit: May 30, 2012, 05:58:58 PM by Eufloria Admin »

annikk.exe

  • Achiever
  • Ent
  • ****
  • Thank You
  • -Given: 0
  • -Receive: 4
  • Posts: 1,809
Guide To Making Levels For Absolute Beginners
« Reply #1 on: May 28, 2012, 07:07:47 PM »

Click Here for Original Discussion Thread

Overwhelmed by the thought of trying to code your own levels?
Don't worry, Fluffy is here to halp.
Follow this guide and you'll be making levels in no time!
(If you actually try everything suggested here for yourself, it will take you about 40-60 mins to learn how to make levels.)

To start with we will create a very simple level, and then learn by adjusting values and playing the level each time to see what has changed.




1.
Click Start




2.
Click on "Run".




3.
Type "notepad.exe" into the box as shown, and click OK.




4.
Notepad opens.




5.
Enter the text shown below into Notepad.
If you would prefer to copy & paste the text (recommended) instead of typing it out yourself, click the "Spoiler" button below:

(click to show/hide)




6.
Now click "File" in the top left of Notepad, and choose "Save As".




7.
We need to make sure we save it in the right folder.
The picture below shows the correct location of the "Maps" folder.
Use the drop-down box at the top of the "Save As" window to navigate to the right folder.




8.
Give your level a name, and make sure that it ends with ".lua"
Set the file type to All Files.
Set Encoding to ANSI.

Then click the Save button.  Now it's time to check out our level.


9.
Load up Eufloria, choose "Custom Levels" from the main menu, and your new level should be waiting there.
Click on it to try playing it!




10.
When it loads up, this is what it should look like.




11.
If you send a scout to the asteroid next door, you should see that there are enemy seedlings orbiting it.





If you got this far, congratulations - you are through the biggest barrier, which is getting started.  It will be plain sailing from here!






Now that we've made a simple level, lets experiment with it by changing some values.



12.
To start with, lets change the number of seedlings that the Player starts with from 90 up to 500.  Make the change indicated below:




13.
Now that we've made the change, it is important that we remember to save the file.  Otherwise our changes won't be apparent when we try to play the level.




14.
Now go back to Eufloria and try loading up the level like before.  You should start with loads of seedlings this time!




15.
Next lets try to change the size of the Player's asteroid.  Make the change indicated below, and don't forget to save the level afterwards by clicking "File" and "Save", like in step 13.




16.
Now load the game up, and lo and behold your asteroid has swollen to epic proportions!




17.
Next lets try changing the maximum number of trees allowed on the Player's asteroid.  Make the change indicated below - make sure you change the right asteroid!  Then save the level again.




18.
Load it up in Eufloria and now we can plant up to 20 trees :>





Now try changing some of the other values yourself.  I would suggest to only change one thing at a time - that way if you accidentally break the level, it's easy to know what to undo to fix the problem.



19.
Hopefully by now you should be getting an idea of how this works.
You can kind of tell what each line means... it doesn't take a genius to work out that "SendDistance" is how far the asteroid can send seedlings.

However, one command is not so obvious, and that is the AddAsteroidWithAttribs command, which as you might guess is used to create asteroids and set their basic attributes.  Lets take a moment to look at that line, because it's a good example to teach us about the other commands as well.



Lets deal with the bits in brackets first.

There are 5 values seperated by commas.

The first two values are the X and Y coordinates of the asteroid - it tells the game where on the map the asteroid is located.  These have to be integers (whole numbers).

This map hopefully gives you an idea of what sort of values you can use.






20.
The next three values are the Energy, Strength, and Speed of the asteroid.



You can use any number between 0 and 1.  So you could have 0.5, or you could have 0.55555 or whatever you like really.




21.
Now that we've dealt with the brackets, lets look at the bit at the start:



The command starts with an "a = ".  The "a" is sort of like the name of the asteroid that we are creating.
Then we can use the various functions and property commands to tell the game what the asteroid is like.

This is what a.TreeCap is doing - we refer to the asteroid called "a", and then we tell the game that "a" has a Tree Cap of...whatever we set it to.






22.

You might notice that we use "a" a second time to make the second asteroid:



It doesn't matter if we use it twice in a row like that.  When we use it to make the second asteroid, we are just telling the game to "forget" which asteroid "a" is, because now this asteroid is called "a".  We just overwrite the old value by declaring a new one.

Then we can set the properties for the second asteroid using the same commands like before, such as "a.TreeCap" and "a:AddSeedlings()" and so on.

Note: You can give each asteroid a unique name if you wish, rather than just re-using "a" all the time.  This can actually be useful if you want to refer to them in your scripts later on.
fluffy = AddAsteroidWithAttribs is perfectly acceptable.  :>  fluffy.TreeCap and other commands will work without problems.



"a." (with a dot) is used to set properties, and "a:" (with a colon) is used for functions.  You must be careful not to mix them up, or the level won't load.
A complete list of the different commands you can use for asteroid properties can be found in the LUA Scripting Reference Sticky.




23.
Finally, the complete command looks like this:

NAME = AddAsteroidWithAttribs(Horizontal Position,Vertical Position,Energy,Strength,Speed)




24.
Now to try adding a new asteroid to your level.

You need to make sure you put it within the bounds of the function LevelSetup().  That is, it must appear after the LevelSetup() command, and before the end command, as shown.



Lets make an asteroid by typing in the code for it where the red arrow indicates.

First we should make a comment to remind ourselves later on that this is Asteroid 2 (remember we had asteroid 0, 1, and this will be asteroid 2).
So type in "-- Asteroid 2" like this:






25.
Now lets type the command to add an asteroid, and pick some values for the new asteroid.
I'll use coordinates 2000, -2000 and energy 0.8, strength 0.6, speed 0.9.






26.
To save some time, I'll copy and paste the commands I used on the previous asteroids.






27.
Now I'll change the values around a bit to make the asteroid the way I want it:






28.
Now lets save the level, load it up, and check out the new roid! :D







29.
There are some other nifty commands you can use in LevelSetup.
A really handy one is SetBackdropColour() which lets you change the background colour using 3 comma-seperated RGB values between 0 and 255.
Lets make the background red!






30.
And the fruits of our work so far....







31.
Maybe you noticed by now that the asteroid positions don't seem to be quite right.  Because of their large size the game is squashing them apart.
To prevent this behaviour, add this line to each of your asteroids:






32.
Save the level and try playing it.  Now the asteroids are positioned exactly where we specified them in our coordinates.  :>






33.
We have been studying the section called function LevelSetup(), which deals with the creation of the level and all the initial conditions.

Now it's time to have a look at the function LevelLogic() section.  This section lets you make things happen during play, such as scripted events.






34.
To begin with, lets make a Message Box appear after 5 seconds of game time has passed.
The messagebox could contain a greeting, and instructions for the player.


Add this code:
(click to show/hide)




Load up the level to test it out:





35.
So what happened there?  Lets think about this for a minute before we move on.

a.
Well, first we created a variable called Timer (case sensitive!).

We set it to Game Time + 5, and since it is the first thing the game evaluates once it's running, Timer must have a value of 0:05 seconds.


b.
Then we used something called a While loop to create a delay.

We told the game, while the game time is less than Timer, just keep repeating these commands.


c.
The part which tells it the loop has ended is the coroutine.yield() command.

This command tells the game the loop has ended and the game should go back up and check if the conditions of the While statement are still being met.

We don't have any commands in between our While statement and our coroutine.yield().  So basically the game just does nothing for 5 seconds. :>


d.
Once the 5 seconds are up, the While loop exits at the end statement.



e.
Then next thing that happens is that the game pauses, a Message Box appears, and when the player clicks OK, the game unpauses again.





Look at the commands and read over this part a few times if you need to.  It is helpful if you can conceptualise what is going on here.




36.
The level is a bit easy at the moment.  Lets add a scripted event where a bunch of enemy seedlings spawn on Asteroid 1.
I'll make mine 20 seconds after the Message Box appears, and I guess I'll add about... 400 seedlings.
You might notice I've started adding comments as well, to remind myself what each part does later on.



Did you notice we used the :AddSeedlings command in LevelSetup() too?  Most of the commands used there are also usable here.  In this case, we weren't able to refer to it by a name like "a" because "a" had been replaced with a different asteroid, so instead we just tell the game which asteroid we mean with GetAsteroid(ID).  Hence, GetAsteroid(1):AddSeedlings(400) is the command to add 400 seedlings to asteroid 1.




37.
Lets save the level and see if the the enemies spawn correctly. :>







38.
If you would like to make it even more interesting, you can add the following line after:

GetAsteroid(1):SendSeedlingsToTarget(2,400,GetAsteroid(0))

This sends seedlings from Asteroid 1, belonging to player 2... it sends 400 of them, to Asteroid 0.





39.
At the moment, if the player takes over all the asteroids, the game just continues running.  There is no way to "win".

We will create a winning condition and a losing condition, so that the game can actually be won or lost by the player.

Add this code:
(click to show/hide)



There are three new things here.

Quit(true) means the game ends and the player won.
Quit(false) means the game ends and the player lost.

If and then statements.  These check if something is true, and if so, does something.
So if the variable gamewon is equal to 1, then the player is told they have won and the game ends in victory.
But if the variable gamewon is equal to 2, it skips the first If statement and proceeds to the second If statement... resulting in the player being told they have lost, and the game ending in defeat.

You might also notice that in some places we use a single "=", and in other places we use the double: "==".
A single equals sign "=" is used to set values, whereas the double "==" is used to compare values.




40.
Now that we have created the winning and losing code, we need to have some way to trigger the victory or defeat.
For this example, we will say that if Player 1 gets asteroid 1, the player wins (gamewon = 1).  And if Player 2 gets Asteroid 0, the player loses (gamewon = 2).

The easiest way to check this is to use an entirely new function - seperate entirely from function LevelLogic - this new one is called function OnAsteroidTaken, and we place it below everything else.

Add this code:
(click to show/hide)





41.
function OnAsteroidTaken is triggered every time an asteroid changes owner.
We can put If statements in there to check the asteroid and the owner, and if appropriate, change the gamewon variable to either 1 or 2.

Once we change the gamewon variable to something other than 0, the While loop that we made in our function LevelLogic() for the victory/defeat code ends.  Straight after that loop ends, the gamewon variable is evaluated to see whether the player lost or won.  Go and look again at the code in step 39 and make sure you understand how this works.


Now you should understand the mechanism of our victory/defeat detection system.


Lets give it a try:









42.
Now you know enough to begin making your own levels.
I hope you will read the LUA Scripting Reference as well and see all the crazy things it's possible to change in this game, and let your imagination run wild :>


Happy coding,
-Fluffy

Click Here for Original Discussion Thread
« Last Edit: May 30, 2012, 06:00:42 PM by Eufloria Admin »

annikk.exe

  • Achiever
  • Ent
  • ****
  • Thank You
  • -Given: 0
  • -Receive: 4
  • Posts: 1,809
Troubleshooting Your Designs
« Reply #2 on: May 28, 2012, 07:07:54 PM »
Troubleshooting Your Designs

Click Here for Original Discussion Thread




1.  Introduction
2.  What have you just been working on?
3.  Finding error messages
4.  Analyse what your code is doing
5.  Comment out suspect parts
6.  Use Developer Mode
7.  Write down in words what your code ought to be doing
8.  Use the console to test values in-game
9.  Use a test MessageBox
10. Assign asteroid names to the values of your variables
11. Ask on the forums for help
12. Sleep on it








1. Introduction

So you made it through the beginners guide!  ...or maybe you didn't.  I can't possibly hope to cover every individual bug you might encounter, so instead I will cover the basic tools and techniques used to troubleshoot a map.

Understand that everyone gets bugs.  If you made it through all the examples in the beginners guide without a single bug along the way, you are definitely the exception rather than the rule!
Bugs are something you will constantly have to deal with, so it makes sense to get comfortable with troubleshooting them early on.



So.  You've got a bug.  Lets say for example your map doesn't load at all.  Where do you start?




2. What have you just been working on?

If you've just added 2 new lines of code, and now the level won't load anymore, the chances are high that the lines you just entered caused the problem.  Go and take another look at them.  Is there a typo?

Are you referring to any variables that haven't been assigned a value yet?  Did you use a dot instead of a colon or vice versa?  EG did you write GetAsteroid(0).AddSeedlings(100) instead of GetAsteroid(0):AddSeedlings(100)?  The difference is small but it can be sufficient to prevent the level from loading.

Look for common mistakes and correct any you find.




3. Finding error messages

Suppose your map won't load and you can't spot which part of your code is wrong.



Eufloria will typically give you an error message.  Read what this says!!  It will often give a number, this is the line number where the code causing the error is located so be sure to look for that.
If your text editor does not display line numbers, I suggest you download Notepad++.  It's free and awesome.

Eufloria will sometimes give you specific information about what caused the bug, sometimes even mentioning a specific variable that is causing the problem.  Then again, sometimes it does not give specific information, but is more generalised.

There are more error messages too - not just the popup error messages you see when your level fails to load.  Sometimes your level will load but stop working instantly, so any active scripting won't proceed.  In this case, you will probably be able to tell something is not right, but you have no clues as to what the problem is because the level managed to get past the initial loading, so there is no messagebox with the error message.

In this case, it's a good idea to check the console to look for the error message there.



The console is accessed by pressing the Tilde key.  (`)
The tilde key is the key to the left of "1" on most keyboards.


When you open the console, any recent error messages will be displayed there.  As with the above, it will sometimes be specific, other times general.. sometimes it mentions a line number, other times it does not.

Note: Occasionally an error message gets "stuck" in the console.  To check if there is a stuck error message, just type something into the console and press enter.  You can type anything you like, eg type in "fish" and press enter.  You should see an error message in response, because obviously Eufloria doesn't know any "fish" command...  if there was a stuck error message, you will also see this appear now.




4. Analyse what your code is doing

Try to think about all the possible values your variables might be assigned.  Will they ever run into a situation where a number is divided by zero?  If that case arises, you have a crash on your hands.



Try to spot logic flaws in your code.  If you have a condtional statement like "if x > y then", make sure that this condition is met when it's appropriate.  Think everything through, and take your time.  Avoid changing things randomly and hoping for the best - that almost never works.  Instead, work on your understanding of what is happening in the code, and let that guide your changes.




5. Comment out suspect parts

We covered commenting briefly in the Beginner's Guide.  If you put a double dash (--) at the start of a line, you "comment it out".  This means anything on that line written after the double-dashes will no longer be run and instead is just treated as a comment.
The aim of this troubleshooting approach is to comment out large sections of your code until you've stripped the level back to a state where it will load again.



Then, un-comment your code a few lines at a time until you discover the point where it no longer works.
This helps you to pinpoint which part of the code is causing the issue.




6. Use Developer Mode

Pressing CTRL-D during gameplay toggles Developer Mode on and off.
Once you have activated Developer Mode, you can use the Function keys F1-F12 to activate different useful functions.  F4 and F12 are the most useful ones.



Spend 2 minutes learning what these do, and your level designing will be that much easier going forward.




7. Write down in words what your code ought to be doing

Sometimes the simple act of writing things down helps you to realise where your code and your intentions differ.



Don't underestimate the usefulness of this exercise for both planning and troubleshooting!




8. Use the console to test values in-game

If your level loads but produces unexpected results, this can often be the best investigative tool.
Suppose you are trying to draw a line on the screen, and the line's location is to be based on the position of an asteroid.
Maybe you have an "angle" variable which is calculated, and then used in a subsequent calculation to figure out the coordinates of the line.

At certain points, you know that "angle" ought to be within a certain range.  For example, at the start of the level you expect (from your plans) that "angle" will have a value of about 50.

Test whether it really does have a value of 50!  Load up your level and open the console.
Then type the following: print (angle)
Press enter and the console will show the value of the "angle" variable.



Maybe it turns out to be 12484382834832483248324.12321 - then you KNOW something isn't right!  Time to study the way "angle" is being calculated...




9. Use a test MessageBox

Eufloria stops running your level code if it encounters an error.
It's possible to exploit this behaviour to see "how far through the code does it get before reaching the error?"
If you have a long section of code and you know at a certain point it is failing, try adding the following line at an opportune point:

Code: [Select]
MessageBox("Fish")
Now run the level.  If you see a Fish messagebox, you know that the code ran fine up until the message box....  so you know the code above it must be fine, and the code below must contain the error.  Go move the Fish command further down a bit, and then try again.  Repeat in this manner until you find the line that is causing the problem.






10. Assign asteroid names to the values of your variables

If you have a variable that you want to monitor closely during testing, one neat trick is to place a command like this inside your LevelLogic's While loop:

Code: [Select]
GetAsteroid(0).Name = MyVariable
Where "MyVariable" is the variable you want to monitor.
Now when the game is running, the name of the asteroid will constantly change to reflect the value of MyVariable.  This allows you to see the values fluctuating in real time while the code is running, and can help a great deal in spotting problems.




11. Ask on the forums for help

That's what we're here for!  You could be the greatest coder in the world, sometimes it just takes another pair of eyes to spot the problem.



When asking for help, please ALWAYS post the level file you are working on.  Don't just post the part of the code you think is causing the problem.  Maybe you don't think the other stuff is relevant but people - including me - have assumed that before, and then later discovered some of that "irrelevant" code was actually the root cause of the problem.  Learn from my mistake and just attach the whole .lua file.  :>




12. Sleep on it

Hey, soldier.  Been working on this for a while?

Is it 3am?



4am?

If it's really late and you've been working on this for ages, sometimes the best thing to do is to just call it a night and get some sleep.  When you wake up, your mind will be more focused, and last night's impossible barriers become mere toy puzzles in the face of your well-rested intellect.  Going to sleep is not giving up.. it's a tactical retreat.  :>


Click Here for Original Discussion Thread
« Last Edit: May 28, 2012, 07:54:36 PM by Eufloria Admin »

annikk.exe

  • Achiever
  • Ent
  • ****
  • Thank You
  • -Given: 0
  • -Receive: 4
  • Posts: 1,809
Intermediate Coding Guide
« Reply #3 on: May 28, 2012, 07:08:01 PM »


Click Here for Original Discussion Thread

Hi coders!




Required Knowledge
This 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.

Scope
This 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

i)
Code: [Select]
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.
Code: [Select]
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.


Code: [Select]
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.

Code: [Select]
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:

Code: [Select]
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:

Code: [Select]
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:

Code: [Select]
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:

Code: [Select]
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:
Code: [Select]
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)....
Code: [Select]
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).  :>
Code: [Select]
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 loops



i)
The for loop is an essential tool when it comes to coding.
A typical for loop might look like this:

Code: [Select]
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 end



iii)
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:
Code: [Select]
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:

Code: [Select]
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:
Code: [Select]
GetAsteroid(0).SendDistance = 5000
On the second pass of the loop, it would resolve to this:
Code: [Select]
GetAsteroid(1).SendDistance = 5000
...and so on.



v)
Thus, this:
Code: [Select]
for i = 0,99 do
GetAsteroid(i).SendDistance = 5000
end

...is just a (much) shorter way of writing this:
Code: [Select]
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:

Code: [Select]
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.  Arrays



i)
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:
Code: [Select]
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().


Code: [Select]
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:

Code: [Select]
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:

Code: [Select]
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.

Code: [Select]
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.

Code: [Select]
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.  Click here to download.






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.

Code: [Select]
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.

Code: [Select]
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!

Code: [Select]
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.5
If i = 5, then the calculation would be 1 / 5 = 0.2
If i = 10, then the calculation would be 1 / 10 = 0.1
If i = 100, then the calculation would be 1 / 100 = 0.01



ix)
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:

Code: [Select]
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:

Code: [Select]
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 = 255
If i = 1, then the calculation would be (1 / (1 + 1)) * 255 = 0.5 * 255 = 127
If i = 4, then the calculation would be (1 / (4 + 1)) * 255 = 0.2 * 255 = 51
If i = 9, then the calculation would be (1 / (9 + 1)) * 255 = 0.1 * 255 = 26
If i = 99, then the calculation would be (1 / (99 + 1)) * 255 = 0.01 * 255 = 3

Hopefully 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.random
i)
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.

Code: [Select]
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...

Code: [Select]
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:


Code: [Select]
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:

Code: [Select]
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...

Code: [Select]
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:

Code: [Select]
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.

Code: [Select]
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:

Code: [Select]
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:

Code: [Select]
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:


Code: [Select]
-- 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:

  • 1
  • 2
  • 3
  • 4
  • 5

Values we definitely WOULD NOT get from that command:

  • 0
  • 7
  • 1.5
  • 3.141592653
  • -3


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:

Code: [Select]
-- 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.cos
i)
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:

Code: [Select]
x = math.sin(GetGameTime())
y = math.cos(GetGameTime())

...or to make it slightly clearer, lets assign the game time to a variable first:

Code: [Select]
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".  Click here to download
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:

Code: [Select]
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...

Code: [Select]
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.


Code: [Select]
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

-1

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

Code: [Select]
-- 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:

Code: [Select]
input = (((input + 1) / 2) * 450) + 50


iv)
Now lets fire that line into the rest of our code:


Code: [Select]
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".


Code: [Select]
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.  Click here to download.




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:


Code: [Select]
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().

Code: [Select]
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.


Code: [Select]
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:


Code: [Select]
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:


Code: [Select]
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:

Code: [Select]
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....

Code: [Select]
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.  
Code: [Select]
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!


Click Here for Original Discussion Thread


« Last Edit: May 28, 2012, 09:14:10 PM by Eufloria Admin »

annikk.exe

  • Achiever
  • Ent
  • ****
  • Thank You
  • -Given: 0
  • -Receive: 4
  • Posts: 1,809
Common Coding Tasks
« Reply #4 on: May 28, 2012, 07:08:09 PM »
Click Here for Original Discussion Thread

Figured I'd start a thread where I (and others) give examples of program code that is likely to be used often.

Let me know if you guys have any requests!
Also let me know if you spot a mistake, or you tried some of this code and it didn't work, even in an otherwise empty level file.

0. Contents

1. Send seedlings after a delay
2. Perform an arbitary number of different tasks after different amounts of time
3. Determine whether a number is odd or even
4. Calculate the distance between the centers of two asteroids
5. Calculate the distance between the closest edges of two asteroids
6. Calculate how much send distance is required for Asteroid 1 to be able to send seedlings to Asteroid 2
7. Calculate the X and Y coordinates at which the line formed by points: [Lx1, Ly1] [Lx2, Ly2] intersects with the line formed by [Lx3, Ly3] [Lx4, Ly4]
8. Calculate the X, Y and Z coordinates at which a line, denoted by any 2 points with coordinates [Lx1, Ly1, Lz1] [Lx2, Ly2, Lz2] intersects with a plane, denoted by any 3 coordinates on that plane; [Px1, Py1, Pz1] [Px2, Py2, Pz2] [Px3, Py3, Pz3]
9. Convert a set of 2D LevelDraw Coordinates into a set of 2D ScreenDraw Coordinates
10.  Concatenate one or more variables and/or one or more strings of text into a single MessageBox



1. Send seedlings after a delay
Code: [Select]
function LevelLogic()
-- create a "latch" variable, so that the seedlings are only sent once.
once = false


-- Run this loop continuously
while GameRunning() do

-- check if enough game time has passed yet
if GetGameTime() > 3 and once == false then

-- 3 seconds have passed - Asteroid 0 should now send 50 seedlings belonging to Empire 2 to Asteroid 1.
GetAsteroid(0):SendSeedlingsToTarget(50,2,GetAsteroid(1))

-- flip the latch
once = true

end

coroutine.yield()
end
end



2. Perform an arbitary number of different tasks after different amounts of time
Code: [Select]
function LevelLogic()
-- create an "incident" variable.  This will be used to store a note of which event is due to happen next.
incident = 0

-- create a "next" variable, this will record what the Game Time should be when the next incident must be run
next = 0

-- Run this loop continuously
while GameRunning() do

-- check if it's time for the next event
if GetGameTime() > next then

-- It's time, so run the action function for the current incident
Action(incident)

-- increment the incident counter by 1
incident = incident + 1

end

coroutine.yield()
end
end

function Action(incident)

if incident == 0 then
GetAsteroid(0):AddSeedlings(10)
next = GetGameTime() + 10
elseif incident == 1 then
GetAsteroid(0):AddSeedlings(20)
next = GetGameTime() + 5
elseif incident == 2 then
GetAsteroid(0):PlantDysonTree(1)
next = GetGameTime() + 25
elseif incident == 3 then
GetAsteroid(0):SendSeedlingsToTarget(50,1,GetAsteroid(1))
next = GetGameTime() + 10
elseif incident == 4 then
SetBackdropColour(0,0,80)
next = GetGameTime() + 15
else
-- no further incidents defined
end
end



3. Determine whether an integer is odd or even
Code: [Select]
if integer % 2 == 0 then -- if the integer divided by 2 has a remainder of 0, then
--the integer is even
else
-- the integer is odd
end


4. Calculate the distance between the centers of two asteroids
Code: [Select]
function LevelLogic()

-- first lets assign the values to some variables
roid1X = GetAsteroid(1).position.X
roid1Y = GetAsteroid(1).position.Y
roid2X = GetAsteroid(2).position.X
roid2Y = GetAsteroid(2).position.Y

-- next, calculate the diffence between their X coordinates
dx = roid2X - roid1X

-- and their Y coordinates
dy = roid2Y - roid1Y

-- dx and dy are like 2 sides of a right angled triangle.
-- If you recall pythagoras, the 3rd side (hypotenuse) can be calculated with a^2 + b^2 = c^2
-- So dx^2 + dy^2 = DistanceBetweenAsteroids^2

distance = math.sqrt((dx * dx) + (dy * dy))

end



5. Calculate the distance between the closest edges of two asteroids
Code: [Select]
function LevelLogic()

-- first lets assign the values to some variables
roid1X = GetAsteroid(1).position.X
roid1Y = GetAsteroid(1).position.Y
roid2X = GetAsteroid(2).position.X
roid2Y = GetAsteroid(2).position.Y

-- next, calculate the diffence between their X coordinates
dx = roid2X - roid1X

-- and their Y coordinates
dy = roid2Y - roid1Y

-- dx and dx are like 2 sides of a right angled triangle.
-- If you recall pythagoras, the 3rd side (hypotenuse) can be calculated with a^2 + b^2 = c^2
-- So dx^2 + dy^2 = DistanceBetweenAsteroids^2

dist = math.sqrt((dx * dx) + (dy * dy))

-- dist is the distance between the asteroid centers.  To get the actual distance between their surfaces, we must subtract both radii from dist
distance = dist - GetAsteroid(1).radius - GetAsteroid(2).radius

end


6. Calculate how much send distance is required for Asteroid 1 to be able to send seedlings to Asteroid 2
Code: [Select]
function LevelLogic()

-- first lets assign the values to some variables
roid1X = GetAsteroid(1).position.X
roid1Y = GetAsteroid(1).position.Y
roid2X = GetAsteroid(2).position.X
roid2Y = GetAsteroid(2).position.Y

-- next, calculate the diffence between their X coordinates
dx = roid2X - roid1X

-- and their Y coordinates
dy = roid2Y - roid1Y

-- dx and dx are like 2 sides of a right angled triangle.
-- If you recall pythagoras, the 3rd side (hypotenuse) can be calculated with a^2 + b^2 = c^2
-- So dx^2 + dy^2 = DistanceBetweenAsteroids^2

dist = math.sqrt((dx * dx) + (dy * dy))

-- dist is the distance between the asteroid centers.  To get the minimum required SendDistance, we measure from the center of Asteroid 1 (startpoint) to the edge of Asteroid 2 (destination)
distance = dist - GetAsteroid(2).radius

end


7. Calculate the X and Y coordinates at which the line formed by points: [Lx1, Ly1] [Lx2, Ly2] intersects with the line formed by [Lx3, Ly3] [Lx4, Ly4]
Code: [Select]
-- calculate A, B and C for both lines (standard form)
A1 = Ly2-Ly1
B1 = Lx1-Lx2
C1 = A1*x1+B1*y1
A2 = Ly4-Ly3
B2 = Lx3-Lx4
C2 = A3*x3+B3*y3

-- detect if they will ever intersect..
det = A1*B2 - A2*B1

if det == 0
--Lines are parallel

else
-- intersection coordinates give as:

intersectX = ((B2*C1) - (B1*C2)) / det
intersectY = ((A1*C2) - (A2*C1)) / det

end



8. Calculate the X, Y and Z coordinates at which a line, denoted by 2 points with coordinates [Lx1, Ly1, Lz1] [Lx2, Ly2, Lz2] intersects with a plane, denoted by 3 coordinates on that plane; [Px1, Py1, Pz1] [Px2, Py2, Pz2] [Px3, Py3, Pz3]
Code: [Select]
-- get 2 vectors along the plane
-- PV1x stands for Plane Vector #1, X-coordinate
PV1x = Px2 - Px1
PV1y = Py2 - Py1
PV1z = Pz2 - Pz1

PV2x = Px3 - Px1
PV2y = Py3 - Py1
PV2z = Pz3 - Pz1

-- now find the cross product, which is a line perpendicular to the plane
-- PV1 X PV2 = normal vector!
i = (PV1y * PV2z) - (PV1z * PV2y)
j = (PV1z * PV2x) - (PV1x * PV2z)
k = (PV1x * PV2y) - (PV1y * PV2x)

-- for the line, calculate tx, ty, tz
tx = Lx2 - Lx1
ty = Ly2 - Ly1
tz = Lz2 - Lz1

-- calculate t
num = ((i * (Px1 - Lx1)) + (j * (Py1 - Ly1)) + (k * (Pz1 - Lz1)))
denom = ((i * (Lx2 - Lx1)) + (j * (Ly2 - Ly1)) + (k * (Lz2 - Lz1)))
t = num / denom

-- calculate coordinates of the intersection point
intersectX = (t * tx) + Lx1
intersectY = (t * ty) + Ly1
intersectZ = (t * tz) + Lz1


9. Convert a set of 2D LevelDraw Coordinates into a set of 2D ScreenDraw Coordinates
Code: [Select]
-- where lvlX is the x-coordinate of the point in Leveldraw that must be converted, and lvlY is the y-coordinate.

screenX = (lvlX * GetCameraScale()) - (GetCameraX() * GetCameraScale()) + (GetScreenWidth() - (GetScreenWidth() / 2))
screenY = (lvly * GetCameraScale()) - (GetCameraY() * GetCameraScale()) + (GetScreenHeight() - (GetScreenHeight() / 2))


10.  Concatenate one or more variables and/or one or more strings of text into a single MessageBox
Code: [Select]
c1 = 5
d1 = 0
omg1 = "cats"

MessageBox(c1 .. d1)
-- "05"

MessageBox("Hello " .. omg1)
-- "Hello cats"

MessageBox("You have " .. c1 .. " " .. omg1 .. " and " .. d1 .. " dogs.  Clearly you must prefer " .. omg1 .. ".")
-- "You have 5 cats and 0 dogs.  Clearly you must prefer cats."

Click Here for Original Discussion Thread
« Last Edit: May 28, 2012, 08:28:08 PM by Eufloria Admin »

annikk.exe

  • Achiever
  • Ent
  • ****
  • Thank You
  • -Given: 0
  • -Receive: 4
  • Posts: 1,809
Annikk's Mechanics
« Reply #5 on: May 28, 2012, 07:08:19 PM »
Click Here for Original Discussion Thread





A series of guides intended to help you install all the different engines into your level.

Free to use, copy and modify.




~



Contents

Thanks to CybeRogue for the banner.



~



Read this first.

Hello.



I wrote all of the engines in this thread.  This series of guides assumes some very basic knowledge of map-making - I expect for example that you are aware how to make a While GameRunning() do loop, that you are comfortable with creating your own Asteroids in LevelSetup(), etc.  If you are a newcomer to level design please read the Beginner's Guide first and try all the examples there.  That combined with a couple hours of experimentation will bring you up to speed sufficiently to have a go at using some engines.

Attached to each guide you will find three files:
  • The first file is not a level - it's actually just the complete code for the engine, which you just copy and paste to the bottom of your level file, below absolutely everything else.
  • The second file is a simple playable map, the way I imagine your maps might look before you have any engines in them.  Think of it as a "before"...
  • The third file is the "after" version.  This is the exact same map but with the engine in question added.  This is so that you can compare the two to see the differences.

I have furnished each template with some default values, so installing the engine in your level is little more than a simple copy and paste affair, and the level will then load fine and you'll be able to see it doing something, at least.

However, only the Infected AI is intended to be used "as is".  The other engines all have variables that can and should be tweaked in order to get the particular functionality you are looking for.
It would be disappointing and extremely cheesy to see finished maps being released with these default values, especially when I have taken the trouble to explain the comparitively simple ins and outs of modifying them to your own design.
Therefore, if you use any of my engines other than the Infected AI, please ensure you alter the variables in such a way that it looks sufficiently different from the defaults.

I can't force you to do this.  The code is up here, freely available for all, and ultimately you can do what you want with it and there's nothing I or anyone can do to stop you.  However I wanted to make my personal wishes clear in the hope that they will be respected because - from my own experimentation - I know how much is actually possible with the code contained herein, and it would be a tragic waste if that potential was not exploited by the map-making community.

Furthermore, to my mind, tweaking variables is what is so much fun about these engines.  I encourage you to let your imagination go wild, and if you have any questions regarding any aspect of the engines or a query on how to implement a particular behaviour, please ask about it in the original discussion thread and I will do my best to respond.

Happy coding. :>


-Annikk.



~






Difficulty: Easy
CPU Cost: Medium; intermittent bursts.
Downloads: Before | After | Engine



1.  Description

  • The Infected AI Engine is a scripted alternative to the normal, ingame AI.  It is designed as an "aggressive expansionist" and will use its seedlings very efficiently.
    It is designed to:
    i) target poorly defended asteroids when attacking
    ii) use large swarms effectively by splitting its attacks across multiple different points
    iii) not become traumatised by extensive damage to its empire
    iv) cope with having its empire split up into parts that cannot reach one another.

    If the AI's empire is reduced to a single asteroid, it will still (if subsequently left unchecked) come back to ultimately take over the entire galaxy and win.

  • The Infected AI engine is the easiest of all the engines to implement, because it doesn't require you to change any variables.  It will just automatically learn about your level and take control of the seedlings/trees/etc that you've added for Empires 2 and up.

  • The AI is equally at home on a static map as it is on a map with moving asteroids or maps where the numbers of asteroids in play changes over time.

  • It will automatically take control of any seedlings or asteroids that you assign to Empires 2 and up, and will cause them to efficiently colonise the galaxy around them, defending themselves if attacked, before ultimately aggressively attacking the player and each other.

  • You only need one copy of the engine to run as many different AI empires as you like.

  • The AI will not control Greys (Empire 0).  So you can still use greys as a passive buffer force to guard valuable asteroids.  The AI will generally avoid trying to colonise these asteroids unless it has a sufficient force to kill them all.

  • The AI knows how to use flowers to get Super Seedlings, but it doesn't know how to plant them on Defence Trees to make laser mines.  Perhaps a future update will introduce this functionality.  However, the AI does respond vigourously if the player attacks with a laser mine,  so feel free to allow the player to build mines in your level, should you deem it appropriate.

  • Finally, for the avoidance of any doubt, commands like "GetAI(2).Cowardice = 0.05" and "GetAI(2).MinExpansionForce = 10" do not affect the Infected AI in any way - so remove them if you don't want the extra clutter.  Those commands are for the default in-game AI, which the Infected AI automatically switches off for you when the engine initialises.



2.  Implementation

  • 1.  Download the file attached to this post called "Infected AI Engine.lua".
  • 2.  Open the file in notepad or whatever text editor you use.
  • 3.  Select all of the code and copy it to your clipboard.
  • 4.  Open your own level and scroll down to the very bottom, and paste the code in.  It is imperative that it appears below everything else - including the "end" from your function LevelLogic().  The code you are pasting is two entirely seperate functions so if you inadvertantly put it inside function LevelLogic() then the map won't work.
  • 5.  At the bottom of your function LevelSetup(), insert this command: AIinit()
  • 6.  In your function LevelLogic(), ensure you have a While GameRunning() do loop (or create one if you don't), then insert the following command anywhere in the loop: InfectedAIEngine()

Now the engine is implemented.  Save your level and try to play it.


That's all that is required to implement Infected AI.
The process for the other engines is exactly the same - the only thing that differs is the name of the functions.



3.  Variables

There are no variables to change in Infected AI.  Once you have completed the above steps, the AI will be in the level.  It will take control of any asteroids and seedlings that you create for Empires 2 and up.




~






Difficulty: Medium
CPU Cost: Low/medium - dependent on number of asteroids
Downloads: Before | After | Engine


1.  Description

  • The Gravity/Collision Engine allows you to designate certain asteroids (or all asteroids) as having gravitational properties.  It gives asteroids the ability to move around, bump into each other, and orbit each other in a realistic way.  The fact that it is based on real life physics (loosely - antigravity doesn't really exist as far as I know) makes it all the more interesting to play around with - and some surprising behaviours fall out of the equations naturally...

  • You can use this engine to make asteroids orbit each other in complex patterns.  You can make asteroids incredibly bouncy, or so non-bouncy that they stick together the moment they collide.  You can make asteroids that behave like balloons.  You can create asteroids that have antigravity.  There is a LOT you can do with this - it's extremely fun to sit around tweaking the settings to produce unlikely looking trajectories, and experiment with strange anomalies such as black holes.

  • The engine is quite forgiving for mistakes.  If you forget to specify variables for some of your asteroids, the level will still load - but those asteroids will not move or have any gravity.  Other asteroids will still be able to bounce off them, however.



2.  Implementation

  • 1.  Download the file attached to this post called "Gravity Engine.lua".
  • 2.  Open the file in notepad or whatever text editor you use.
  • 3.  Select all of the code and copy it to your clipboard.
  • 4.  Open your own level and scroll down to the very bottom, and paste the code in.  It is imperative that it appears below everything else - including the "end" from your function LevelLogic().  The code you are pasting is two entirely seperate functions so if you inadvertantly put it inside function LevelLogic() then the map won't work.
  • 5.  At the bottom of your function LevelSetup(), insert this command: GravityInit()
  • 6.  In your function LevelLogic(), ensure you have a While GameRunning() do loop (or create one if you don't), then insert the following command anywhere in the loop: GravityEngine()

Now the engine is implemented, but you will need to set some variables before it will do what you want it to do.



3.  Variables

In this engine there are several "global" variables which you only need to set once for the whole engine, and a handful of variables that you need to specify on a per-asteroid basis.
You can find the variables inside the "function GravityInit()" section in the code that you pasted in.  The variables are all initialised here, so this is where you change all the settings.


Global Variables

  • G - the overall force of gravity.  I'd suggest setting this to 1 to begin with.
  • bounce - how bouncy are asteroids?  A perfectly elastic collision (ie the most bouncy any real life object could possibly be) is 1.  I'd suggest trying 1.1 or 1.2 personally.  :>  It stops asteroids from clumping up together.
  • EmergencyBrakeThreshold, MidBrakeThreshold, LowBrakeThreshold - These are for slowing down asteroids that are moving too fast.  Set it to the speed an asteroid must be travelling to incur a slowing-down effect.
  • EmergencyBrakeFactor, MidBrakeFactor, LowBrakeFactor - When one of the above thresholds is reached, this is the value that the speed is multiplied by.  Setting a value of less than about 0.98 causes the asteroid to slow down like a balloon does in air.

The brakes aren't exactly very true to astrophysics, but it lets you simulate (somewhat crudely) things like the response to an atmosphere such as if you try to throw a balloon.  The balloon slows down very rapidly and doesn't go very far.

Brakes are necessary if you use a bounce factor greater than 1, because that will mean each collision adds energy to the system.  Without brakes, the asteroids will collide and collide, picking up more and more speed, until eventually they fly all over the screen at lightning speed colliding with each other dozens of times a second, and vanish.
This is certainly interesting but hardly makes for a productive level-making environment.  Hence, the brakes.


Per-Asteroid Variables

Just copy and paste these ready-made template examples for each asteroid you need to declare for.

Code: [Select]
-- Asteroid 1
-- This asteroid has gravity and moves around.  It is drifting northwest at game start.
roid = 1
hasgravity[roid] = true
canmove[roid] = true
MomentumX[roid] = -13.1
MomentumY[roid] = -9
density[roid] = 0.3


If you forget to set these attributes for one of your asteroids, some default settings will be used for it instead (the asteroid won't move and won't have gravity).


  • roid - Enter the ID of the asteroid you are setting properties for.
  • hasgravity - True or False.  If you set to true, the asteroid will contribute forces to the gravitational field around it.  If you set false, it won't produce any gravity or feel any gravity from others.
  • canmove - True or False.  Can the asteroid be moved (by gravity and from being struck by other asteroids), or is it static and unmoving?  Using different combinations of "hasgravity" and "canmove" is the one of the main methods of producing different asteroid behaviours.
  • density - how dense is the asteroid?  Asteroids with greater density have more gravity.  An asteroid made of metal (which has very high density, eg 10) would produce more gravity, and also take more force to get it moving, than an asteroid made of gas (which would have a density of about 0.1).
  • MomentumX and MometumY - This is the starting speed/direction for this asteroid.  For example, if you set the X to -3 and the Y to 0, the asteroid will start the game drifting gently west.


You can also change the properties of individual asteroids ingame whilst the simulation is in progress.  To do this, simply open the console and declare a new value for the variable in question.
For example, if I wanted to set the density of Asteroid 1 to 500 instead of 0.3, this is the command I would type into the console:

Code: [Select]
density[1] = 500
Asteroid 1 would then instantly have a gravitational field more than 1500 times stronger than before, and the asteroids would all behave differently as a result of the change.
This looks hilarious, by the way.  It's like someone just pulled a giant plug in space, and everything starts rushing towards it...



~






Difficulty: Advanced
CPU Cost: High - dependent on number of asteroids and stars
Downloads: Before | After | Engine



1.  Description

  • The Parallax Scrolling Engine lets you create stars that float behind the asteroids in your level, giving a faux-3D effect.

  • This engine is the most difficult to understand and configure.  However, in its default form it is cut back a great deal.  You should be able to massively increase the number of stars that are displayed with some tweaking, especially if this is the only engine you are using in your level.

  • This version is designed to display stars, but you can also use it to display other things.  Lots of different visual effects are possible.  The engine is set up to use Sprite Index 0 which in my view is the most star-like, however you can also set it to display other sprite indexes.  Possibly with some interesting results.



2.  Implementation

  • 1.  Download the file attached to this post called "Parallax Engine.lua".
  • 2.  Open the file in notepad or whatever text editor you use.
  • 3.  Select all of the code and copy it to your clipboard.
  • 4.  Open your own level and scroll down to the very bottom, and paste the code in.  It is imperative that it appears below everything else - including the "end" from your function LevelLogic().  The code you are pasting is two entirely seperate functions so if you inadvertantly put it inside function LevelLogic() then the map won't work.
  • 5.  At the bottom of your function LevelSetup(), insert this command: ParallaxInit()
  • 6.  Insert a LevelDraw function in between your LevelSetup() and your LevelLogic() using this command: function LevelDraw()
  • 7.  Ensure that you place an "end" after it, to indicate where the code in that function stops.
  • 8.  Inside your LevelDraw, insert this command: ParallaxEngine()

Now the engine is implemented, and will work immedietely.  However, be sure to change the variables to your own design before releasing it.



3.  Variables

The Parallax Engine has a large number of variables.  Some of the variables are arrays, so you will need to set values for all of the arrays too.  Because some of the arrays can span hundreds of slots, you may wish to combine formulas and For loops in order to set all the values efficiently, without having to manually specify each one.

This engine is not nice and friendly like gravity.  If you forget to set one of the values, the level will probably not load.

The settings for the Parallax Engine all appear in the ParallaxInit() function that you pasted in.
The section marked "User-Changeable Variables" roughly marks out the bounds of which variables are designed to be modified easily.


Single Variables


These are just single values that you can specify to govern global properties of the engine.

  • numberofstars - The total number of stars in your level.  Be careful with setting this too high - stars require a lot of processing power, especially if you have a lot of asteroids in your level.
  • numberoflayers - How many Parallax layers are there?  The stars will be split equally among the layers, so if you have 50 stars and 10 layers, there will be 5 stars in each layer.  You can set some attributes per-star, and other attributes per-layer.
  • widthofstarfield, heightofstarfield - How large an area shall the starfield occupy?  Change this to be optimum for your size of map.  The engine always centres the starfield at coordinates 0,0.
  • starsize - How big should each star be?  200 is good for average sized stars.  Setting this value very high creates some interesting effects, but the higher the value, the greater the CPU cost.
  • stoprenderingbelow - When you zoom out, the engine will start to reduce the alpha of certain layers (exactly which layers is definable in the arrays section below).  When the alpha falls below a certain point, the engine will just stop rendering them completely, saving CPU.  Set that threshold here.  Allowed values 0-1.


Setup Arrays

This is where you specify the attributes for individual stars and layers.
All these arrays are required by the engine and their values must be specified by you.

Red, Green, Blue - How much of each colour in the star?  0,0,0 is black.  1,1,1 is white.

For example, if you have 3 stars:

Code: [Select]
red[0] = 1
green[0] = 0.2
blue[0] = 0.2

red[1] = 0.2
green[1] = 0.2
blue[1] = 1

red[2] = 1
green[2] = 1
blue[2] = 1
   
This code would make the first star red, the second star blue, and the third star white.
The number in square brackets refers to the individual star we are colouring.

If you have hundreds or thousands of stars, this method is not a good approach.  Instead, set the values with a formula.  For example, this formula would colour the stars different shades of grey:

Code: [Select]
for setcol = 0,numberofstars do
red[setcol] = math.sqrt(1 / numberofstars)
green[setcol] = math.sqrt(1 / numberofstars)
blue[setcol] = math.sqrt(1 / numberofstars)
end
   
How to make the right formula?
Well, first think of what sort of values you are going to need.  I know that for grey stars I need the values for red, green and blue to be the same for each star.  So the formula should be the same for each of them (which isn't always the case - it depends entirely on the effect you're trying to achieve).
Next, I know that the values should vary between 0 and 1.  I also know that the one thing that I can rely on changing between one star and another, is its ID number.  So we can use that as the seed of difference for our formula.
"setcol" is going to vary between 1 and however many stars we have.  Say it was 50.  What could we do with the number 50 to get a value between 0 and 1?
One answer is to divide 1 by that 50, and use the result.  But that's only 0.02 - if we have 100 stars then it means most of them will basically be black.
We need to find a way to increase the result we get, without making it stray above 1.
An ideal way to do this is to take the square root.  The square root of 0.02 for the 50th star would be around 0.14, so its r, g and b values would be at 14%.  That's a dark grey, but not black.  The 10th star would be the square root of 0.1, which is about 32% - a gunmetal grey.  So it looks like this formula would work well for creating stars with different shades of grey.

It's up to you to develop your own methods for setting the colours of these arrays.  Lets move onto the next array.



threshold - How zoomed out do we have to be before each layer starts to fade out/disappear?

This is useful for keeping lots of detail when zoomed in, but only displaying a certain amount of stars when zoomed out so that the FPS doesn't suffer.
You need to specify a value for each layer.  Unlike stars, the numbering of layers always starts on 1, NOT 0.

You can specify values manually:

Code: [Select]
threshold[1] = 0.3
threshold[2] = 0.2
threshold[3] = 0.1
threshold[4] = 0.07
threshold[5] = 0.04

Or you can do it with some formula that you invent:

Code: [Select]
for setthresh = 1, numberoflayers do
threshold[setthresh] = 1 / setthresh
end



zdepth - How far away does this layer look?

This should normally be a value greater than 1.  The higher the value, the closer the stars look.
If the value is equal to 1, the stars will look like they are so far away that they don't move with the camera at all.
If the value is less than 1, the stars will move backwards!
If the value is 0, the stars will not be rendered.
If the value is less than 0, the stars will seem to be in the foreground - in front of the asteroids (but they will still disappear when they actually move in front of one)

In the template, the line looks like this:

Code: [Select]
zdepth[numberoflayers - lnumber + 1] = 1 + (5 * (1 / lnumber))
You can replace the bit after the = and write your own formula, or you can replace that line entirely and specify the z-depths for all the layers manually:

Code: [Select]
zdepth[1] = 1.1
zdepth[2] = 1.4
zdepth[3] = 1.8
zdepth[4] = 2.5

...or with any other method of your choosing.




Additional Arrays

As well as the arrays that are required for startup, there are also two others that you may be interested in using.

They are SetStarX and SetStarY.  These are the "given" positions of stars.  The engine generates these automatically at the start of the level but there is no reason why you couldn't refer to it and change it during play using some code in your function LevelLogic().
For example, if I want to make a star (lets say star number 50) move from side to side, I can do it with this:


Code: [Select]
function LevelLogic()
While GameRunning() do
SetStarX[50] = 1000 * math.sin(GetGameTime())

coroutine.yield()
end
end

That would cause star number 50 to drift left and right.
Using this, it's possible to make some - or all - of the stars move.  You could move lots of them at once by employing a for loop.




You can do a similar thing with the other variables and arrays.  You could have stars that flash/twinkle, or pulsate, or anything you like.  You can even have stars that come closer or move further away!  (by changing the zdepth of their layer..)





Click Here for Original Discussion Thread
« Last Edit: May 28, 2012, 10:55:17 PM by Eufloria Admin »

annikk.exe

  • Achiever
  • Ent
  • ****
  • Thank You
  • -Given: 0
  • -Receive: 4
  • Posts: 1,809
Re: Level Programming Guides
« Reply #6 on: May 28, 2012, 07:15:27 PM »
Reserved
« Last Edit: May 28, 2012, 07:29:35 PM by Eufloria Admin »