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!