Hook Examples

Every code block in this page can be plugged into a Lua file. Have fun! Play around with each one of them! Try to change variables around.

MobjThinker

local function SillyCrawlaJump(mo)
    local force = 20*FRACUNIT
    local jumpheight = 16*FRACUNIT

    if (leveltime % (5*TICRATE) == 0) then
        mo.momz = jumpheight
        for i = 1,8 do
            local angle = ANGLE_45 * (i-1)
            local xoffset = P_ReturnThrustX(mo, angle, force)
            local yoffset = P_ReturnThrustY(mo, angle, force)
            P_SpawnMobj(mo.x + xoffset, mo.y + yoffset, mo.z, MT_DUST)
        end
    end
end

addHook("MobjThinker", SillyCrawlaJump, MT_BLUECRAWLA)
addHook("MobjThinker", SillyCrawlaJump, MT_REDCRAWLA)

MobjThinker is run once per frame of every mobj that matches the type given in the addHook function. If no type is given, it will apply to every mobj that does not have MF_NOTHINK.

This codeblock makes Blue Crawlas and Red Crawlas jump every 5 seconds. “TICRATE” is a constant, always equal to 35 in SRB2, 35 tics equaling one second of real time. We don’t write 35 directly, because the value itself can always change in the future, and if it does, us writing “35” will be obsolete, while TICRATE itself will change to suit our needs.

When they jump, they will also create 8 dust particles around them. That is what the for i = 1,8 block is for. It loops the block inside 8 times. Each loop, it chooses an angle (0, 45, 90, 135, 180, 225, 270, 315), gets the X and Y values for a certain distance from that angle (20 FRACUNITs, as decided at the start of the function), and creates a dust object at that position.

These functions might seem a little overbearing. Try to think of it as a circle as seen from above. It has a radius of 20 FRACUNITs. The Crawla is the center. You drag 8 lines in 45 degree increments, and put a dust at the end of each one. P_ReturnThrustX/Y let you get the horizontal and vertical values of those drawn lines. Don’t worry if it’s still complex, as you do more trigonometry it will eventually settle into place.

PlayerThink

local function PlayerRocket(p)
    if (p.mo.skin ~= "sonic") then return end

    --Initialise variables if we haven't already
    --More modular to do it in a PlayerSpawn hook instead
    --But that sadly breaks when considering changing skins in MP

    if not (p.rocketcharge) then
        p.rocketcharge = 0
        p.rocketlaunched = false
    end

    --Do nothing if we're grounded, and allow us to rocket again
    if (P_IsObjectOnGround(p.mo)) then
        p.rocketlaunched = false
        p.rocketcharge = 0
    else
        --If we're midair and already launched, do not take inputs
        if not (p.rocketlaunched) then
            --Create charge by holding Spin
            if (p.cmd.buttons & BT_SPIN) then
                p.rocketcharge = $ + 1
            else
                --Release if we have enough charge
                if (p.rocketcharge >= TICRATE / 4) then
                    p.rocketlaunched = true
                    p.mo.momz = P_MobjFlip(p.mo) * p.rocketcharge * FRACUNIT
                end
            end
        end
    end
end

addHook("PlayerThink", PlayerRocket)

Similar to MobjThinker, PlayerThink runs once per frame for each player. This code still runs if the player is spectating or dead, so make sure to check for the proper conditions, such as p.valid and p.mo.valid (spectators do not possess a p.mo, according to the wiki for example).

This codeblock adds a functionality to the player that allows them to rocket up into the air by holding the Spin button while airborne. It requires them to play as Sonic, although this restriction can easily be replaced or removed. Once the player has held the button for at least a quarter of a second (TICRATE / 4), and they release it, they will be sent up into the air depending on the amount of charge they held. The needed variables are created at the start of the code block only if they haven’t been initialised already, so that the remainder of the code can function properly. The launching line also checks for the current gravity of the player.

”Being Midair” and “holding spin” combined sounds a lot like it would fit the “JumpSpinSpecial” hook. And indeed, it would be optimal to delegate the starting “trigger” to that hook if you really want to focus on Separation of Concerns. However, that call only triggers once pressed, NOT held, so the actual checks for charge amount and launching should still be done in a continuous loop.

ThinkFrame

local globalcounter = 0

local function GeneralThinker()
    --iterate over all players
    for p in players.iterate do
        --if a player has more than 20 rings, decrease ring count by one
        --and increase the punishment counter
        if p.rings > 20 then
            p.rings = $ - 1
            globalcounter = $ + 1
            print(globalcounter)
        end
    end

    --if 50 excess rings are collected, kill everyone
    if (globalcounter > 50) then
        globalcounter = 0
        for p in players.iterate do
            P_DamageMobj(p.mo, nil, nil, 1, DMG_INSTAKILL)
        end
    end
end

addHook("ThinkFrame", GeneralThinker)

-- as globalcounter is not tied to any ingame object, it needs to be synchronised for new joiners manually
local function SynchCounter(net)
    globalcounter = net(globalcounter)
end

addHook("NetVars", SynchCounter)

Runs once before every other thinker is run. It takes no arguments, and is therefore more useful for situations where you need to run a global counter, or need to iterate over all instances of a type of object (players, sectors, skins, etc.). Used to be the de-facto way to get players to do something when Lua was first released, as PlayerThink had not been added yet.

The example code provides a scenario with a global counter, and a check if any player is above 20 rings. If so, their ring count is decreased by 1 every frame, and the counter is increased by 1 every frame. If the counter exceeds 50, every player is killed. The code also shows a brief example of the NetVars hook. Without going into too much detail, this hook allows variables that are not tied to any mobj, such as the global counter we defined, to be synchronised properly when a player first joins a netgame.

MobjDamage

Ran when a mobj takes damage. If used on a mobj with more than 1 health, you can activate your own invulnerability code, or do something much sillier. Since most enemies die in one hit, this will be frequently used on either bosses or the player. Keep in mind that this works like MobjThinker, you have to define a type at the end of the addHook function. If you want it to work for the player, you’d add MT_PLAYER.

local function CrawlaDenial(mo, inf, src, dmg, dtype)
    --Blue Crawla just really really hates Fang
    if (inf.type == MT_PLAYER and inf.skin == "fang") then
        P_DamageMobj(inf, mo, mo, 1, DMG_NUKE)
        return true
    elseif (src.type == MT_PLAYER and src.skin == "fang") then
        P_DamageMobj(src, mo, mo, 1, DMG_NUKE)
        return true
    end

    print("Not Fang? Guess I'll die.")

    return false
end

addHook("MobjDamage", CrawlaDenial, MT_BLUECRAWLA)

This code overrides the Blue Crawla’s damage behaviour, where it reflects all damage done by a player with a Fang skin back at them. The code can return early in the if-condition. In this hook, return true means “Override the default behaviour”, which means the Crawla will not be damaged. return false means “use the default behaviour,” where the Crawla will take damage as normal. Keep in mind that return false DOES NOT invalidate the code that runs before it, so that print statement will still execute if the Crawla dies.

MobjLineCollide

Ran when an object tries to move over a linedef. This one’s a bit complex in usage, as changing the return behaviour of this hook makes the game “force” you to either get blocked by the line, or ignore it entirely. If you wanted to do something like… say, a wall jump, you would not mess with the return behaviour at all. You would simply look at the sector that the line is attached to, check if its height definitions mean that it is a “wall” to the mobj, and set the state to the wall jump state if that is the case. Keep in mind that this works like MobjThinker, you have to define a type at the end of the addHook function. If you want it to work for the player, you’d add MT_PLAYER.

Code example coming soon!

AbilitySpecial

Executes whenever you try to press the jump button midair. It is the bread and butter of all character abilities. When you return true from this function, it will replace whatever existing ability you gave your character, allowing you to have completely custom abilities.

local function PlayerAbility(p)
    if (p.pflags & PF_THOKKED) then
        return
    end

    p.pflags = $ | (PF_THOKKED | PF_SPINNING)
    p.mo.state = S_PLAY_ROLL


    P_SpawnThokMobj(p)
    P_InstaThrust(p.mo, p.mo.angle, -60*FRACUNIT)
    S_StartSound(p.mo, sfx_thok)

    P_StartQuake(10*FRACUNIT, TICRATE/6)

    return true
end

addHook("AbilitySpecial", PlayerAbility)

A very simple example that replaces your character’s ability with a Thok…that goes backwards. It also shakes the screen a bit. Notice how the PF_THOKKED flag is added manually by the code. The PF_THOKKED flag, which is more of a general purpose flag to signify that a character has used their ability, prevents you from using your ability midair multiple times. If you return true from AbilitySpecial, this flag does not get set on its own, so you have to add it yourself.

JumpSpecial

Unlike AbilitySpecial, this hook is run as soon as you try to press the jump button. If you override default behaviour by making this function return true, you won’t be able to jump normally at all. Additionally, it will run even if you aren’t currently in a state where you can jump. It simply checks if the button is pressed, with the only exceptions being “in a hurt state” and “completed the level”.

Code example coming soon!

HUD

Also known as hud.add, the HUD hook runs every time something has to be drawn onto the HUD, separate from the sprites you see in the game. There are various ways you can use this hook, depending on the last argument you give it. If you write "game" as the last argument, it will render for every frame you can see the default Score/Time/Rings HUD. If you write "titlecard", it will render for a specified amount of time after the title card of the level is first displayed. You can decide on the amount of time it displays by yourself! There are more options as well.

Code example coming soon!