clock

Modding The Binding Of Isaac

In this article, we're going to take a look at The Binding of Isaac: Afterbirth+ modding API and go over the basic steps to create a simple item mod for the game.

Folder Setup

The first step to making a new mod is to navigate to your mods directory and create a new folder.

// Windows
C:\Users\YourName\My Games\Binding of Isaac Afterbirth+ Mods\

// OSX
~/Library/Application Support/Binding of Isaac Afterbirth+ Mods/

This new folder requires a few files and a specific folder structure to actually work correctly. In our case, we're going to be creating an item - so our mod will require a folder named content a folder named resources.

Inside of our resources directory we need to add a few more folders. When done, our directory structure should look like so:

- my-mod/
    - content/
    - resources/
        - gfx/
        - items/
        - collectibles/

The reason that we have to structure the resources folders in this way is because they directly map to the folder structure that the game already uses. We're essentially mimicking this structure so that the game knows where to find our files and pull them into the game.

Creating The Items File

Next, inside of our content directory, we will need to create a new file named items.xml. This file will contain the basic information for the item that we are creating in addition to a reference to the graphic that we will be using for it.

This file has to be located inside of the content directory to work properly. Unlike the resources directory, this breaks the pattern of mimicking the same location the the game already uses.

<items gfxroot="gfx/items/" version="1">
    <passive
      cache="damage shotspeed range luck flying"
      name="My Item"
      description="My item description"
      soulhearts="6"
      gfx="collectibles_my_item.png"/>
</items>

For the <items /> element, we just need to provide the root directory to find the graphics files that we will be using and the version of the API.

Inside, we added a new <passive /> element which signifies that we are creating a passive item (for reference, the other options are <active /> and <trinket />). On this element, we provide all of the necessary properties that that game will use.

First we provide it a name and description. These will be used directly inside of the game when the player picks up the item. Next, we have a couple function properties cache and soulhearts.

The cache property values are used to trigger stat changes for the player. You will provide a corresponding cache value for every stat that you wish to modify.

The soulhearts property values reflect how many soul hearts we would like to give to the player when they pick up this item. It is important to note that this numeric value corresponds to the smallest heart unit: the half-heart. So, in this case, we wish to give the player 3 full soulhearts so we must double the number assigned to this property.

Adding Your Item Graphics

Once you have successfully added your items.xml file, we can now add in an image file for our item. Making the image from scratch is well beyond the scope of this tutorial, but all you need is to have an image that is 32px-by-32px and place it into the resources/gfx/items/collectibles/ directory that we created earlier. Just make sure that the image filename is exactly what you set as the value in the gfx property of the item in the items.xml file we created above.

Adding Logic

Our item is going to do basic stat updates for the player, similar to the various items already in the game that give increase all of your stats.

To do this, we need to add in some logic to tell the game which stats we would like to increase and by how much.

In the root directory of our project, create a new main.lua file. Inside of the file, we'll first register our new mod and set a reference to the game.

local MyMod = RegisterMod("My Mod", 1)
local game = Game()

This registers our new mod by passing in a string of "My Mod" and 1, which sets the name of our mod and the API version we're using. A reference to our mod is then stored in the MyMod local variable that we can now use within our code.

As an additional convenience, we've also created a game variable to reference the game class.

Next, let's make some references to the item that we are creating and attach them to our mod. This will make it easier reference them throughout our code.

-- Initialize Item Constants
MyMod.COLLECTIBLE_MY_ITEM = Isaac.GetItemIdByName("My Item")

Next, we will begin setting up some various callbacks. These callbacks are what the game uses to trigger logic around events that happen in the game. We will be using these callbacks to execute custom logic for our item.

Since this item is going to update our stats, let's make a function called onCacheEval and then assign it to the MC_CACHE_EVALUATE callback. This will trigger the the onCacheEval function anytime a cache update occurs.

-- Handle Cache Updates (eg. Stat Changes)
function MyMod:onCacheEval(player, flag)
  -- My Item Functionality
  if player:HasCollectible(MyMod.COLLECTIBLE_MY_ITEM) then
    if flag == CacheFlag.CACHE_DAMAGE then
      player.Damage = player.Damage + 1;

      -- Prevent Delay From Going Too Low
      -- (Best Practice Is 5 As Lowest Value)
      if player.MaxFireDelay >= 5 + 2 then
        player.MaxFireDelay = player.MaxFireDelay - 2;
      elseif player.MaxFireDelay >= 5 then
        player.MaxFireDelay = 5;
      end
    end

    if flag == CacheFlag.CACHE_SPEED then
      player.MoveSpeed = player.MoveSpeed + 0.25;
    end

    if flag == CacheFlag.CACHE_RANGE then
      player.TearHeight = player.TearHeight - 1;
      --player.TearFallingSpeed = player.TearFallingSpeed - 1;
      --player.TearColor = TearFlags.TEAR_FEAR;
    end

    if flag == CacheFlag.CACHE_TEARFLAG then
      player.TearFlags = player.TearFlags + TearFlags.TEAR_BOUNCE;
    end

    if flag == CacheFlag.CACHE_LUCK then
      player.Luck = player.Luck + 1;
    end

    if flag == CacheFlag.CACHE_SHOTSPEED then
      player.ShotSpeed = player.ShotSpeed + 0.15;
    end
  end
end
MyMod:AddCallback(ModCallbacks.MC_EVALUATE_CACHE, MyMod.onCacheEval)

The majority of this code should read fairly straightforward. We are simply writing if statements to check whether or not cache flags are set. These flags are the string values that we assigned to the cache property of our item in the items.xml file we created before.

The cache values are preset keywords defined by the game itself. So, these have to be exactly what the game defines to work properly.

The oddball in this set of code is where we are updating the fire delay. For some strange reason, this value must be updated within the code that checks the damage cache flag. Unfortunately, I don't know a lot of detail behind this issue, but as soon as I find out I will update this to reflect the finer details.

Debug Placement

So, to easily test our item, let's setup a quick way to immediately spawn our custom item into the game as soon as we begin a new run.

Let's add in a debug variable that will hold a boolean value. This is what we will use to easily trigger our item in game for testing purposes.

At the beginning of our main.lua file, let's add in a isDebugging property after our mod and game references.

local TheOfficeMod = RegisterMod("The Office", 1)
local game = Game()
local isDebugging = false // Added

With that added, let's add in a new callback that will run on every game update.

-- Update Callback (Runs Every Frame)
function MyMod:onUpdate(player)
  -- Place Item On Ground (For Testing)
  -- Change isDebugging Variable To True To Enable
  if game:GetFrameCount() == 1 and isDebugging  then
    Isaac.DebugString("! Initializing Debug Items !")
    -- Spawn My Item
    Isaac.Spawn(
      EntityType.ENTITY_PICKUP,
      PickupVariant.PICKUP_COLLECTIBLE,
      MyMod.COLLECTIBLE_MY_ITEM,
      Vector(320, 300),
      Vector(0, 0),
      nil
    )
  end
end
MyMod:AddCallback(ModCallbacks.MC_POST_PEFFECT_UPDATE, MyMod.onUpdate)

The overall structure of this code should be familiar to you having completed the cache callback code above. This creates a new onUpdate function and then assigns it to the MC_POST_PEFFECT_UPDATE callback which is executed every time the game updates (every frame).

For more information on the various mod callbacks that the Afterbirth+ API gives us, check out this article on modding callbacks.

Now, we don't want the item to be added every single frame so we add in a bit of logic to only add the item when both the game frame count is 1 (aka the beginning of the run) and the isDebugging property is set to true.

This will only spawn the item as the game is beginning and gives us a mechanism to easily toggle it's availability for when we are debugging.

Testing Your Item

With the code we have written above we can now open the game and begin testing our item. If all goes well, the game will book up normally and you will be able to access the "Mods" menu item in the options and see your mod listed.

If the title of your mod is a red color, then it means that there is an error in your code. You will have to close the game and begin exploring the log files for the cause. We will be covering that next!

Go ahead and start a new run. When you spawn into the first room, your item should now be spawned in the middle of the room on item altar. You can pick it up and double check your item title and description and also ensure that the stats are properly being updated.

Debugging

So, what happens if things go wrong? The easiest way to debug any problems is to take a look at the log file that the game generates.

// Windows
C:\Users\YourName\My Games\Binding of Isaac Afterbirth+\log.txt

// OSX
~/Library/Application Support/Binding of Isaac Afterbirth+/log.txt

I will personally open this via the command line by running tail -f log.txt which will keep displaying any updates to the log file so I don't have to keep reopening the file to see what went wrong.

Of course, this is just a simple text file, so you can open it up in a text editor if that works better for you!

Once you have identified the issue, jump back into your code and patch things up. Once you have a solution in place, you have a couple options.

You could just reboot the game, but that can be quite time consuming. So, the developers have given us another option via the in-game console that will allow us to reload our mod while the game is running.

To access the console, type ` while in the game. This will bring up a command line where you can run various commands. The command that we are interested in is luamod.

To reload the mod, type luamod followed by the directory name of your mod. In this case, it will be:

luamod my-mod

After running this command, it will provide you with a success or error message. If successful, press Enter again to close the console and quickly jump back into the game.


I hope you've enjoyed this article and it has been insightful. I'm really quite pleased with the modding API after the initial learning curve. It definitely has some confusing turns and shortcomings but overall, it's a relatively easy way to get started and create your own mods for the game.