How To: NPCs and loops

This tutorial is about the basics of working with NPCs and interacting with NPC objects and their properties during runtime.

By ID/Section
The NPC Class contains various ways to modify NPCs through interaction with SMBX's NPC objects. Often when dealing with NPCs, we don't target a specific NPC, but a group of NPCs of a certain ID.

To do so, we use NPC.get( or ids,  or  sections).

This function takes two arguments: (1) The ID or a table of IDs of the NPCs we want to target (these are the same numerical IDs SMBX assigns them). Leaving this blank will have the function return NPCs of all IDs (However, to specify a section, this argument must be filled in.) (2) The section number or a table of section numbers of the sections we want to look for NPCs in. This argument is optional and if not included, the function will return NPCs of the given ID(s) from all sections. Often, this will be player.section (the ID of the section the player is in). Remember that sections start at 0!

This function returns a table of all of the objects of NPCs that fit the criteria.

For example:

If we wanted to get the first SMB3 goomba (ID 1) in the level (remembering that unlike a lot of other programming languages Lua tables have indices by default starting at 1):

Or even just:

Finding NPCs by Position
Another useful function to be aware of is NPC.getIntersecting( x1, y1,  x2,  y2). If we're looking for NPCs in a specific location, we can target them based on whether or not they collide with a box formed by the arguments of NPC.getIntersecting. x1 and y1 are the x and y-coordinates of the top left corner of the box, while x2 and y2 are the x and y-coordinates of the bottom right corner, as shown below.

https://i.imgur.com/ZyFIb1V.png

Like NPC.get, NPC.getIntersecting returns a table of all NPCs that are colliding with the box.

Interacting with NPCs
We can see that NPCs all have a lot of different properties that they inherit from the NPC Class (see: Instance functions). All NPCs have a bunch of functions (methods) and fields that we can use. Some fields are read-only (marked "ro") which means we can read them but we cannot set values to them.

If we wanted to set a value to make our first goomba stop moving:

Each of our NPC objects is also essentially a table. When we access the value of dontMove from an NPC, we are looking at the index "dontMove" of the NPC and its corresponding value.

Therefore, the following is another equally valid way to write the code above:

We use the former when dealing with objects as it is quicker to write, instead of the lengthier means of writing the same thing in the second example.

We can also call a method on our first goomba to change it into a coin:

Multiple NPCs
When we want to interact with multiple NPCs on the same tick, we use a for loop to iterate over a table of NPCs objects. The code inside the for loop will run with each element (which has a key, value pair representing its index in the table and its value) from the table. We use the ipairs function as our iterator when dealing with any of LunaLua's get and getIntersecting functions across all classes.

The general structure of this type of for loop is:

In the for loop our variables "key" and "value" are automatically localized to the scope of the loop. That means that outside of the loop code, these variables don't exist unless they are defined otherwise. We can name these two variables whatever we choose. Often they will be seen as "k" and "v" to shorten it, "_" and "v" usually when the index of the element in the table doesn't matter to the code, or some other names that best suit the situation.

A more specific example to an NPC.get call:

This code loops over the table returned by NPC.get(1) for every element in the table which has its own key, value pair. We can call these variables whatever we like. In this case, the key is called "index" and the value is "goomba". It runs the code inside the for loop for each element in the array. index = 1, goomba = NPC.get(1)[1] --> run loop code index = 2, goomba = NPC.get(1)[2] --> run loop code ... index = #NPC.get(1), goomba = NPC.get(1)[#NPC.get(1)] --> run loop code

Remember that the hash operator (#) tells us the length of the table. If the length of the table is #NPC.get(1) then the last element in the table will have an index of #NPC.get(1).

Just like when we changed the value of "dontMove" for our first goomba earlier, we can also change the properties of NPC objects in our for loops, in exactly the same way! In our current for loop, the variable holding our NPC object is "goomba". If we want to make all goombas not move by setting each of their "dontMove" fields to true:

The code iterates over all goombas in the table returned by NPC.get(1): index = 1, goomba = NPC.get(1)[1] --> goomba.dontMove = true; index = 2, goomba = NPC.get(1)[2] --> goomba.dontMove = true; ... index = #NPC.get(1), goomba = NPC.get(1)[#NPC.get(1)] --> goomba.dontMove = true;

It runs the loop code on each of the objects it iterates over, one at a time.

Again, we can also call methods on the NPCs we loop over, such as the very useful "kill" function.

This would kill all of our goombas. It's good to be aware that there are different harm types that we can use when calling the "harm" and "kill" methods on NPCs. You can find them here.

Gotchas!
There are a few things to be aware of when working with NPCs: (1) As mentioned earlier, fields that are marked "ro" (read-only) cannot be modified and will often throw an error. Some of these can be bypassed by setting the fields in memory offsets (as well be discussed in the next tutorial). (2) Not all fields, even ones that aren't read-only, will have an effect on all NPCs. NPCs that are walkers (such as goombas) will often not respond to changes of their "speedX" values.

Test Yourself
Based on what you've learned in this tutorial, try to give either of these tasks a try. If you can do them, you're good to go. If not, you might want to go back and do a bit of re-reading. Sample answers are included as well, but try not to look unless you've figured it out or are really stuck. Also take a look at the NPC Class which has the fields you'll need (under "Instance functions").

(1) If there are more than 3 spinies (ID 48) in the player's section, turn them all into coins. (2) BGO.get (see: BGO Class) can be used like NPC.get to get a table of BGOs with a certain ID (BGO.get( or ids)). Given that water bubble BGOs have an ID of 173, make all NPCs in a level act as if they are underwater when intersecting with a water bubble. (Hint: The "underwater" field for NPCs is reset each tick so you don't have to worry about setting it back to false when an NPC is no longer in contact with a bubble.)

If you want to download a level to test your code to these challenges: Test Level

Solutions
Remember that the comments in the code are just comments! You don't need them in your code; they're just there to explain. Your variable names also need not match the ones used in the example solutions, but they should make sense to someone reading your code. Make sure that your indentation is also proper as it'll help a great deal.

(1) https://www.dropbox.com/s/o8ihioi16uzhlbr/lunadll.lua?dl=0 (2) https://www.dropbox.com/s/vypm0qxcp49mhv0/lunadll.lua?dl=0