How To: Custom NPC behaviour

From PGE Wiki
Jump to navigation Jump to search

This tutorial will cover examples of custom NPC behaviour. If you haven't already, please read the tutorial on NPCs and Loops as this information will be fundamental for this. In addition, mem functions will be used quite a bit for custom NPC AI, as SMBX's AI can be accessed through various parts of the memory.

Recap of the NPC Class

Recall that in the previous tutorial, we covered the NPC class which contains many useful values for overriding/modifying NPCs. In addition, we covered that NPCs are contained in a table (similar to an array) and that this table should only be accessed/updated through either

1. onTick() function override.

2. onLoopSection#() function override, where # is the section you're working in minus 1 (section 1 is section 0).

Utilizing this knowledge, we can override/create custom NPC behaviour.

Red Birdo Example

This example is one of my favourites, as it's simple enough for beginners. You can download the files used for this here.

Let's take a moment to think about how we can approach this. Red Birdo shoots either an egg or a fireball, it's completely random. For the longest time, this was impossible to do in SMBX. But with a little Lua magic, we can accomplish this easily.

So, what will we need to do?

1. Generate a random number

2. Check the value, if the value is something determine whether or not to shoot a fireball instead of an egg.

Well, how can we do this? Recall back to the NPC Class. It has a value called id (NOTE: Prior to, this value is read only. An alternative will be provided in addition to the id changing way). This value is what makes this whole process work: it will change the NPC to whatever you set the id to. Imagine the possibilities there! The basic steps would look like this:

1. Get a table of Birdo, and her eggs in the level. We need both because we'll be changing the eggs based on our RNG.

2. Check and make sure both exist/there's more than 0 in the level

3. Generate a random number.

4. If the random number checks out to what we want it to be, change the egg's id to our custom fireball (provided in example download).

The code looks a little something like this:

local hasGenerated = false;
local ran;

function onLoop()
  tableOfBirdo = NPC.get(39, -1);
  tableOfBirdoEggs = NPC.get(40, -1);

  if(tableOfBirdo[1] ~= nil) then
    if(tonumber(tableOfBirdo[1]:mem(0xF0, FIELD_DFLOAT)) == 1) then
      if(hasGenerated ~= true) then
        ran = math.random(0, 2); --Note: math.random(0, 2) generates a random number between 0 and 2.
        hasGenerated = true;
      if(ran == 2) then
        if(table.getn(tableOfBirdoEggs) > 0) then
          tableOfBirdoEggs[table.getn(tableOfBirdoEggs)].id = 282;
          playSFX(42); --big fireball
    if(tonumber(tableOfBirdo[1]:mem(0xF8, FIELD_DFLOAT)) == 280) then
      hasGenerated = false; --just reset everything

(The Lua Math library reference, which contains documentation to math.random in addition to many other math functions)

It's a lot to take in, but I'll take you through what happens. The topmost variables: ran is our random number generator instance. hasGenerated is a safety net to make sure we don't keep generating random numbers. This is one disadvantage of having to run everything in a loop.

We get our table of birdos and birdo eggs. If the first Birdo is not null (empty, non-existent, dead inside), then we do the magic. In this example, we use the NPC:mem() function to access SMBX's memory. The value we access is one of SMBX's NPC AI timers. You can find a list of current AI timers/what they do for each NPC here. When 0xF0 is 1, Birdo is shooting! This is our chance to check and make sure eggs exist. Which we do, in addition to generating our random number. If the random number is 2, then we change the latest (hence why we access with table.getn(), this will get the max value which is always the latest addition to the list) Birdo egg to a fireball, and play the fireball sound effect. Finally, we check 0xF8 which means that Birdo is resetting and we need to watch again for another egg.

Some improvements YOU can make.

1. Larger range for random numbers, maybe generating a few randoms first instead of just using the first value you get.

2. Use a for loop to iterate through all Red Birdos in the level. Though, using this method, you can have multiple Birdos and only the first Birdo will have this AI.

Note for Pre

Unfortunately, the id property is read-only in pre versions which means this access can only be read. However, this value can still be changed via NPC:mem(). So, instead of using tableOfBirdoEggs[table.getn(tableOfBirdoEggs)].id = 282, we use tableOfBirdoEggs[table.getn(tableOfBirdoEggs)]:mem(E2, FIELD_WORD, 282);. Functionally, they do the same thing. Setting via the id property is a convienence factor.