How To: LunaLua basics

From Moondust Wiki
Jump to navigation Jump to search

So, you want to learn LunaLua? Great! You've come to the right place! Here, I'll go through some of the bare basics of Lua, as well as some of the things specific to LunaLua.

Setting Everything Up

LunaLua is now a core of SMBX2, so download the latest version of SMBX2 first.

Next, you just need to create your Lua file. Create a folder for your level, just like you would for custom graphics or NPCs, and create a new file (notepad works well for this). You need to save this file as luna.lua. With that done, you can start writing Lua code!

Notice Note: For SMBX2 Beta3 and lower, or for old LunaLua toolchains you should use lunadll.lua (Level-specific) and lunaworld.lua (Episode-wide) script names.


It's recommended that you download SciTE (Windows and Linux) or Microsoft's Visual Studio Code (Cross-platform) to write your code. Both are free code editors and come with syntax highlighting for a bunch of languages, including Lua, which makes things a lot easier.

Where to Start

So, you want to get into writing Lua, but you have no idea where to start. Well, some of the beginner tutorials on here will hopefully set you on the right track. Here, we'll just give a brief overview of Lua and some of the very basics. So if you want to get started with Lua, buckle up and keep reading! We'll start with some basic Lua syntax.

Lua

Lua is quite a simple programming language. It works by defining functions, which you can then call from other parts of the code. You can also define variables, which allow you to carry data around.

Functions and Variables

A simple piece of Lua code looks like this:

myVar = 0

function MyFunction()
    myVar = myVar + 1
end

This defines a variable called myVar (and initially sets it to 0). It also defines a function called MyFunction, which adds one to myVar.

Now all that's left is to call this function from somewhere in the code. LunaLua provides a system of Events, which are a series of function names that will be automatically called by LunaLua if you define them. The most important of these are onStart and onTick. The onStart event is automatically called when a level starts, while the onTick event is called every tick SMBX logic runs. We can put semicolons in front of lines where we define variables (but not functions or events), change variables, and call functions (as we will see below); this isn't necessary so don't get too worried about it, but it is good practice.

So let's call our MyFunction() function every frame, and let myVar continuously count up:

myVar = 0

function MyFunction()
    myVar = myVar + 1
end

function onTick()
    MyFunction()
end

Calling a function acts as if you take the body of the function, and replace the call with it. The code written above is exactly the same as this program:

myVar = 0

function onTick()
    myVar = myVar + 1
end

Interacting with SMBX

Now, if you run SMBX, you'll find that nothing happens. This is because we haven't told Lua to do anything with that variable. It's doing what we told it, counting up, but we aren't using that variable in SMBX, so we can't see the results. I'm going to add a simple statement using the printText function found in LunaLua, which allows us to write text to the screen.

myVar = 0

function MyFunction()
    myVar = myVar + 1
end

function onTick()
    MyFunction()
    Text.print(tostring(myVar), 0, 0)
end

Now, if you run SMBX, you should see a counter rapidly increasing in the top-left corner of the screen. I'll just divert for a moment to talk about this new function, Text.print().

Alongside the Events LunaLua provides, it also provides functions, which are pre-defined functions you can call. These allow you to interact with SMBX. The Text.print() function, as I mentioned, draws text to the screen. This needs to know the text you want to draw, the font to use (font 3 is usually the best option, and is default if left blank), and the coordinates to draw it at (0,0 means that the top left of the text will be at the top left of the screen). There is also another function called on just myVar here, which is the tostring function. This is built into the Lua language, rather than the LunaLua SMBX framework. You don't need to worry about it too much, it just makes sure that myVar is formatted in a way that the Text.print() function expects. (In this case, though, myVar is a number, and the Text.print function can already handle numbers, so it's not exactly necessary here.)

Arguments to Functions

Functions can also have something called arguments. These are simply pieces of data you give to a function that allow it to operate differently. Our MyFunction function currently just adds one to the variable myVar, but let's expand it so that it can add any number.

function MyFunction(adder)
    myVar = myVar + adder
end

We've now added an argument to this function, which allows it to be used in a lot more situations. We have to make one more change to our program in order to make this work, though. Now that we have an argument, when we call this function, we need to pass that argument to it. We've defined a temporary variable called adder, which is our argument. This variable only exists inside MyFunction, so you can't use it anywhere else. When we call this function, then, we need to tell it what we want adder to be. We do this by inserting a value between the brackets. A pair of brackets after a function name means we want to call that function, with the arguments inside the brackets. If we have no arguments, as with our first example, then we just put two brackets, but if we want to have arguments, we need to put something there. Here is the adapted program.

myVar = 0

function MyFunction(adder)
    myVar = myVar + adder
end

function onTick()
    MyFunction(1)
    Text.print(tostring(myVar), 0, 0)
end

This should work exactly the same as the previous example. The benefit of this is that we can change what this function does just by changing the argument. If we use this code, for example:

myVar = 0

function MyFunction(adder)
    myVar = myVar + adder
end

function onTick()
    MyFunction(10)
    Text.print(tostring(myVar), 0, 0)
end

You should see that the counter is now increasing in multiples of 10, and we only changed the argument to MyFunction.

We can add as many arguments as we like to a function, separated by commas. Let's make MyFunction a little more complex, by making it multiply by one number, then add a second.

myVar = 0

function MyFunction(adder, multiplier)
    myVar = (myVar*multiplier) + adder
end

function onTick()
    MyFunction(1,2)
    Text.print(tostring(myVar), 0, 0)
end

This will simply multiply myVar by 2, and add 1, each frame. We can change these values at will by changing the values we pass to the function when we call it.

Conditions

The key to any code is something called conditionals. These allow us to control our code based on what else is going on. The usual structure is:

if condition then
   --do action 1
else
   --do action 1
end

Conditions rely on boolean logic, which simply means that we can check if something is true or false. For example, 1 equals 1 is true, but 1 equals 2 is false. The code sample above means "if the condition is true, then we will do action 1, but if the condition is false, then we will do action 2".

If we don't want to have action 2 at all, then we can just leave out the else clause.

if condition then
   --do action 1
end

There are a few things you can use for conditions, and I'll summarise the most simple ones now.


a < b true if number a is smaller than number b, false if number a is greater than or equal to number b.
a > b true if number a is greater than number b, false if number a is smaller than or equal to number b.
a == b true if number a is equal to b, false if number a is not equal to number b. (NOTE: Double-equals is not the same as single equals, which is used to assign variables.)
a ~= b true if number a is not equal to b, false if number a is equal to number b.
a <= b true if number a is smaller than or equal to number b, false if number a is greater than number b.
a >= b true if number a is greater than or equal to number b, false if number a is smaller than number b.
not a true if boolean a is false, false if boolean a is true.
a and b true if boolean a is true and boolean b is true, false if either of boolean a or boolean b is false, or both are false.
a or b true if either of boolean a or boolean b is true, or both are true, false if boolean a is false and boolean b is false.

These boolean expressions can be combined in multiple ways, for example:

if (a > b) and (b > c) then

is perfectly fine. So, now that we've looked at this a bit, let's try it out in some actual Lua code, by once again expanding our MyFunction function.

You may have noticed that the most recent version of MyFunction gets very big, very quickly. Let's say we want it to stop counting up once it hits a certain number. We can use a conditional for that.

myVar = 0

function MyFunction(adder, multiplier, limit)
    if myVar < limit then
        myVar = myVar*multiplier + adder
    end
end

function onTick()
    MyFunction(1, 1, 10000)
    Text.print(tostring(myVar), 0, 0)
end

We've added a limit argument, which stops the counter if the variable goes too high. I've also reduced the multiplier down to 1 (which effectively gives us just adding 1 each frame again), so that it's a little clearer to see what's happening. You should see the number in the top left corner count up, and then stop at 10000. This is exactly what we've told it to do, using conditionals.

We can do better than this, though. We can make that number count up, and then back down again, and continue infinitely. I mentioned that boolean values are used in conditionals, but you can also store them as variables. We're going to do that now:

myVar = 0
countingUp = true

function MyFunction(adder, multiplier, limit)
    if countingUp then
        if myVar < limit then
            myVar = myVar*multiplier + adder
        else
            countingUp = false
        end
    else
        if myVar > 0 then
            myVar = (myVar - adder)/multiplier
        else
            countingUp = true
        end
    end
end

function onTick()
    MyFunction(1,1, 10000)
    Text.print(tostring(myVar), 0, 0)
end

This looks like we've added a lot more code, but it's not all that complex if you look through it (and, in fact, this kind of structure is quite common). We have created a boolean variable, called countingUp, which we set to true when we want to count up, and false when we want to count down. Simple. Now we just need to look at how we're using it.

First of all, we'll split the function into two sections, one for counting up, and one for counting down. We can use the if then else style of conditional to do this:

if countingUp then
    --count up the value
else
    --count down the value
end

This nicely separates the code into the two sections we want. Now, in each of these sections, we need to do two things. First, we need to count up or down the variable, and second, we need to change the countingUp variable when we're done. Once again, we need two sections, so we can split this using an if then else. Since we already have the first part for counting up, let's look at that:


if myVar < limit then
    myVar = myVar*multiplier + adder
else
    countingUp = false
end

So, while our variable is below the limit, we do our multiply and add operation, but if it's not below the limit (which means we've finished counting up), we set countingUp to false. This means that next time we look at this function (on the next frame), we'll look at the other section, because we're counting down now.

The counting down section is very similar:

if myVar > 0 then
    myVar = (myVar - adder)/multiplier
else
    countingUp = true
end

There are only a few differences. First, we need to change our condition. Now that we're counting down, we want to stop at 0, not at our limit, so we use that instead of our limit argument (though we could create a lower limit argument just as easily). The next change is that we change our adding logic. Before, we were multiplying by a number, and then adding another number. Here, I'm just doing that operation in reverse, by first subtracting adder, and then dividing by multiplier. Lastly, we have to set countingUp to true when we're done, so we start counting up again.

If you run this code now, you should see the counter going up and down between 0 and 10000.

Variable Scope

Lua is a bit tricky with variables. When you make a variable like this:

myVar = 0

You're making what's called a global variable. This means that, wherever you are in the code (even in other Lua files!) this variable can be accessed and changed. Not only is this bad because someone else could change the variable without you knowing, but it also means that you might accidentally overwrite someone else's variable without knowing it! Oops!

So, instead, Lua provides a keyword local. If you put this before a variable or function definition, you can restrict the scope of the variable. This means you can control where the variable can be accessed from. If we make the variables from the previous examples into local variables, then we can access them from our code file, but not anywhere else, which is good!


local myVar = 0
local countingUp = true

local function MyFunction(adder, multiplier, limit)
    if countingUp then
        if myVar < limit then
            myVar = myVar*multiplier + adder
        else
            countingUp = false
        end
    else
        if myVar > 0 then
            myVar = (myVar - adder)/multiplier
        else
            countingUp = true
        end
    end
end

function onTick()
    MyFunction(1,1, 10000)
    Text.print(tostring(myVar), 0, 0)
end

The local keyword has other uses too, though. It doesn't just restrict code to the file, but it restricts it to wherever you define it. For example, we could add a variable to the onLoop function:

function onTick()
    MyFunction(1,1, 10000)
    local varText = tostring(myVar)
    Text.print(varText, 0, 0)
end

This means that varText can only be used inside onLoop, and only on lines after it has been defined. These, for example, are both wrong, because we are trying to use varText outside it's scope:

function onStart()
    local varText = tostring(myVar)
end

function onTick()
    MyFunction(1, 1, 10000)
    Text.print(varText, 0, 0)
end
function onTick()
    MyFunction(1, 1, 10000)
    Text.print(varText, 0, 0)
    local varText = tostring(myVar)
end

It's generally best to keep variables as local as possible, to avoid them cluttering the code.

Tables

Lua has these things called tables. These are, in the simplest form, groups of things. You can use these to make lists of variables, and can even include functions. You can define a table like this:

local myTable = {}

This makes an empty table, but you can also define one with some elements in it:

local myTable = {10, 15, 26, 31}

This makes a table with 4 items in it. You can then get these items back out again like this:

myTable[1]

This will get the first element of the table, which is 10 (a note for programmers - Lua tables are 1-indexed). You can treat this just like a variable, so you can assign to it as well as read it. You can also add new values to a table by assigning to an index not in the table already, for example:

myTable[5] = 55

Will add 55 to the end of the table, making:

{10, 15, 26, 31, 55}

One of the key features of Lua tables is the idea of keys. You don't have to store data in them by their index, but can define a key instead (in fact, if you define a table without keys, Lua assigns keys for you, which are the indices into the table). Let's assign some keys to myTable:

local myTable = {a = 10, b = 15, c = 26, d = 31}

Now, instead of accessing the table via the table indices, you can access it via the keys.

myTable["a"]

Will get you the value 10. You can also assign to new keys as we would assign new elements of the table, for example:

myTable["e"] = 55

Will add 55 to the table, with the key e, allowing us to access that number using:

myTable["e"]

The values in a table don't have to be numbers, either. They can be strings, booleans, other tables, or even functions.

The last thing to mention is that if you define named keys (such as we have above), you can also access them like this:

myTable.e

Which makes some code easier to read and manage.

This covers the basics of using Lua. More detailed Lua tutorials with some advanced extensions can be found on the Lua website and Lua Users Wiki. We'll now go on to discussing how to use some of this with SMBX.

LunaLua and SMBX

Interfacing with SMBX is the key to writing any program using LunaLua. All the Lua code you can write is useless for SMBX unless it does something to the game, so we'll look into that a little bit now. We've already looked at the Text.print() function, which draws text to the screen, but that's only useful in some circumstances, and there's a lot more than Lua can do.

Structures

LunaLua comes with a few structures that allow access to SMBX data. These are little bundles of data that we can use in Lua, and affect the game world in SMBX. The most common of these are Player, NPC, Block, and Layer. As you might have guessed, these allow us to access the data for the player, NPCs, blocks, and layers.

Let's look at the Player structure first. The first thing we need to do is get a reference to the player. This gets our Player structure, and lets us apply it to the player (we can also do this for player 2, separately). Now, for the Player structure, this is easy, because there's a value built into LunaLua called player that lets us access the player directly.

Once we have our reference, we can start accessing data. The data we can access is listed in the Player class page, and I've used the speedX field here, which allows us to get or set the horizontal speed of the player. This is a simple piece of code that prevents the player from moving left or right.

function onTick()
    player.speedX = 0
end

And you can do similar things to any of the fields listed on the Player class page. They can be treated just like variables, and the function on that page can be treated just like functions you've defined. With that in mind, let's look at one of the more useful player functions: mem. This is a function that lets us do just about whatever we want. It directly accessing the object in SMBX, and changes some values in there. It isn't particularly easy to use, though, but I'll talk through the basics of it nonetheless.

The mem function requires a memory offset and a type. These let it read the right data from the structure, and in the right way. We can also add a third argument to write data into it. An example of the mem function is a simple character filter:

function onStart()
    player:mem(0xF0, FIELD_WORD, 1)
end

This forces the player to be Mario when the level starts. There is more information on this in the simple filters tutorial. It's also worth taking a look at the memory map, which lists all the memory offsets we know about, what types you should use, and what they do.

Now that we've discussed the player a little, we'll move onto NPCs. Once nice thing is that most of this is the same. To do anything with NPCs, you must first get an NPC reference, then you can access the fields and functions found in the NPC class page. There is even a mem function for NPCs (though the memory offsets mean different things).

There is one difference though. For the player, we could just use the player field to get a reference (since there is only ever a fixed number of players, usually one). The number of NPCs in a level changes depending on the level, though, so we can't just use a field to get NPC references.

We can, however, use one of two functions: NPC.get() and NPC.get(id, section). These give us tables of NPCs, either all NPCs in the level, or filtered by NPC ID and section (though you can use -1 as an argument to remove either of those filters). We can then use a loop to run some code on all of the NPCs in the table. This will be covered in more detail in the NPCs and loops tutorial.

So, lets say we want to look at all the goombas in the level(NPC ID 1), and make them follow the player. We'll start by creating our loop. Note that the second argument used for the NPC.get() function here accesses the player field to find which section the player is in, to avoid looping over goombas in sections the player's not in, which is often unnecessary and not the best of coding practices.

function onTick()
    for k, v in ipairs(NPC.get(1,player.section)) do
        --code goes here
    end
end

So now we have a loop. Inside this loop, we have two temporary variables called k and v. These cannot be used outside the loop. The k variable holds the key into the table we're looping over, while the v variable holds the value in that table. We're usually more interested in v, because that's where the NPC data is. These two variables can be named anything you want, and in this case we're not interested in k, so we can "throw it away" by writing _,v instead of k,v This will run the code inside the loop once for every element in the table (using NPC.get(1,player.section) means that we're running it once for every goomba NPC in the player's section). Now, we can write some code as if we were managing one NPC, and it will be applied to all of them!

Let's use our lessons from conditionals to check whether the player is to the left or to the right of the NPC, and then set the NPC's speed accordingly:

function onTick()
    for _, v in ipairs(NPC.get(1, player.section)) do
        if v.x > player.x then
            v.speedX = -1
        else
            v.speedX = 1
        end
    end
end

You should find now that each goomba (ID 1) that you place in the level will walk towards the player, rather than in a straight line! You can run just about any code you like in these loops, and they're very useful for creating NPC behaviour.

The Layer class allows you to access SMBX layers, and set a few of the properties of them using the fields and functions on the Layer class page. The only thing you need to be particularly aware of is, once again, getting a reference to the layer. For this, you use the Layer.get() function:

local myLayer = Layer.get("LayerName")

You simply create a layer in SMBX, and then use Layer.get() to get a reference to that layer based on its name. You can then use the relevant fields and functions.

More Events

We've discussed on the onStart and onTick events already, which are the most important, but there are a couple of other useful ones you can use. Let's look first at onLoadSection#. This allows you to run code only when you enter a certain section, which can be very useful.

function onLoadSection0()
    player.character = CHARACTER_MARIO
end

This will force the player to be Mario, but only when they enter section 1. If you start the level in section 2, and then go through a door to section 1, the player will switch to Mario (but won't switch back again unless you tell them to).

It's worth noting that in LunaLua, section numbers are between 0 and 20, rather than between 1 and 21. This means that onLoadSection0 refers to section 1, onLoadSection1 refers to section 2, etc.

Another useful player key field is player.keys. This lets you run code when you press a specific key. Let's say we want to change the player character when we press down. We can do this using this event like this:

function onTick()
    if player.keys.down == KEYS_PRESSED then
        player.character = CHARACTER_MARIO
    end
end

This will force the player character to Mario when they press the down key. An interesting thing to note is that this event requires an argument. We discussed function arguments earlier in this tutorial, and mentioned that they are essentially like temporary variables. In this case, the sub-field keyname of player.keys will contain the key that was pressed, when LunaLua calls this function (which it will do automatically).

We also make use of some constants here, which allow us to refer to keys by name, rather than using numbers.

That's more or less it for this basic tutorial. There is more that can be learned, but this should be enough for most things you'll want to do. Be sure to look at the other tutorials for more in-depth discussion on how to do some basic things.

Autocode to LunaLua

If you have experience with old Autocode or you have scripts which are written in Autocode, you might want to check out: How To: Autocode to LunaLua

Further Reading

LunaLua constants

LunaLua events

LunaLua global functions

One Lua Tutorial

Another Lua Tutorial

Lua Tutorial in Chinese