Events and handlers
Note: This feature is for ZScript only. |
Event handlers are a plugin-like system available since GZDoom 2.4 that allows your ZScript code to receive certain world, client and system events.
There can be multiple event handlers and in most cases all of them will receive the event (note: exceptions exist).
General information
An event handler is a class that inherits either StaticEventHandler or EventHandler. The differences between them are as follows:
Type | Initialized | Destroyed | Serializable? |
---|---|---|---|
StaticEventHandler | On GZDoom startup | When closing GZDoom | No |
EventHandler | At the beginning of every map | At the end of every map | Yes |
For most of the level scripting you should use an EventHandler; StaticEventHandler is only needed if you need to store some non-playsim data locally.
Setting up
In order to make the event handler work, you need to declare it in MAPINFO. For this, two variants are supported: EventHandlers
in map definition and EventHandlers
or AddEventhandlers
in gameinfo definition.
GameInfo
{
AddEventHandlers = "MyEventHandler"
}
This will add an event handler MyEventHandler
To the game. Multiple event handlers can be added with either a comma, or new AddEventHandlers
definitions.
Note, this is a GameInfo section of the MAPINFO lump; not to be confused with the GAMEINFO lump, which is unrelated.
Warning: It is not necessary to add multiple separate event handlers to override the same types of events; it is also not recommended, because the more handlers you have, the more they will impact performance. |
If you want multiple different things to happen within the same event with different conditions, simply add that conditions into that event. For example:
override void WorldThingDied(WorldEvent e)
{
if (e.thing.GetClass() == 'Zombieman')
{
// this will happen if the killed actor was a Zombieman
}
else if (e.thing.GetClass() == 'DoomImp')
{
// if the actor was a DoomImp, this will happen instead
}
else if (e.thing.GetClass() == 'ChaingunGuy')
{
// if the actor was a ChaingunGuy, this will happen instead
}
// and if none of the conditions above were met, simply nothing will happen
}
Functions
All event handler methods are defined as static and clearscope.
- native Find(class<StaticEventHandler> type)
- Retrieves a pointer to the specified event handler.
- Use
StaticEventHandler.Find
to find static event handlers, andEventHandler.Find
for non-static ones.
EventHandler only
- static native void SendNetworkEvent(String name, int arg1 = 0, int arg2 = 0, int arg3 = 0)
- Sends a network event, which can be intercepted in the
NetworkProcess
virtual override. Allows communication from UI scope to play scope. (See Networking.) - Only integer arguments are supported. To pass a string, pass it as part of the event's name and use Split to retrieve it. (See Networking for an example.)
- static native void SendInterfaceEvent(int playerNum, string name, int arg1 = 0, int arg2 = 0, int arg3 = 0)
- Sends an interface event, which can be intercepted in the
InterfaceProcess
virtual override. Allows communication from play scope to UI scope. (See Communicating with the UI.)
Handling events
Each possible event is done as a virtual method in the StaticEventHandler/EventHandler classes which can be overridden.
Note: Calling Super.EventName() in these overrides is not necessary, because by default these functions are empty and don't do anything. Authors simply need to add overrides and put their custom code into them. |
Method | Data available (in the provided event) | Description |
---|---|---|
void OnEngineInitialize () | — | Called right after the engine starts up (After messages like CPU information print). This can only be used on StaticEventHandlers. |
void OnRegister () | — | Called when the engine registers your handler (adds it to the list). Event handler order setup can be performed here. |
void OnUnregister () | — | Called when the engine removes your handler from the list. |
void WorldLoaded (WorldEvent e) |
|
IsSaveGame only works for StaticEventHandlers. By using this field you can detect that the current level was loaded from a saved game. IsReopen will be true when the player returns to the same map in a hub, similar to REOPEN ACS script type. |
void WorldUnloaded (WorldEvent e) |
|
IsSaveGame only works for StaticEventHandlers. By using this field you can detect that the current level is unloaded to load a saved game. NextMap is the name of the map to be entered next, if any. |
void WorldThingSpawned (WorldEvent e) |
WorldThingDied only:
|
These events are received just after the specified Thing was spawned or revived/raised/resurrected (including the player's use of the resurrect cheat), and just before it dies or gets destroyed. Internally, WorldThingSpawned is called just after PostBeginPlay. |
void WorldThingDamaged (WorldEvent e) |
|
The arguments for this event are the same as the DamageMobj arguments. DamageAngle can be different from direct angle to Inflictor if portals are involved — beware. |
void WorldThingGround (WorldEvent e) |
|
Called when Thing actor's corpse is crushed into gibs. CrushedState is the crush state the actor had entered. |
void WorldLineDamaged (WorldEvent e) |
WorldLineDamaged only:
WorldSectorDamaged only:
|
These events are called before dealing damage to a line or sector, which allows the events to manipulate the damage through Damage (raw damage) and NewDamage (damage after modification). |
bool WorldHitscanPreFired (WorldEvent e) (New from 4.14.0) |
|
This event is called whenever a hitscan attack function is called by any actor in the game. This method is non-void: returning true will block the attack from happening.The data received by the function can then be used to do something else, such as spawning projectiles in place of the blocked hitscans. Note: this is only called for "bullet" hitscans, not for railguns. |
void WorldHitscanFired (WorldEvent e) (New from 4.14.0) |
|
This event is called after a hitscan attack has been performed by any actor in the game. It can be used to perform some additional actions after the attack has happened. Note: this is only called for "bullet" hitscans, not for railguns. |
bool WorldRailgunPreFired (WorldEvent e) (New from 4.14.0) |
|
This event is triggered whenever a railgun attack function is called by any actor in the game. This method is non-void: returning true will block the attack from happening.Note: this is only called for railgun attacks, not for regular "bullet" hitscans. |
void WorldRailgunFired (WorldEvent e) (New from 4.14.0) |
|
This event is called after a railgun attack has been performed by any actor in the game. It can be used to perform some additional actions after the attack has happened. Note: this is only called for railgun attacks, not for regular "bullet" hitscans. |
void WorldLightning (WorldEvent e) | — | Same as LIGHTNING ACS script type. |
void WorldTick () | — | Calls at the beginning of each tick, 35 times per second. |
void WorldLinePreActivated (WorldEvent e) |
|
This event is called upon the activation of a line, right before the line's special is executed.
|
void WorldLineActivated (WorldEvent e) |
|
This event is called upon the activation of a line, after the line's special has been successfully executed.
|
void PlayerEntered (PlayerEvent e) |
|
These are generally the same as their ACS counterparts. PlayerEntered is called when players connect. PlayerSpawned is called when the player spawns in the level, much like an ENTER script. PlayerRespawned calls when players respawn (or use the resurrect cheat). PlayerDied calls when players die (along with WorldThingDied) PlayerDisconnected calls at the same point as DISCONNECT scripts (this is generally useless with P2P networking). PlayerNumber is the player that was affected. You can receive the actual player object from the global players array like this: PlayerInfo player = players[e.PlayerNumber]; IsReturn will be true if this player returns to this level in a hub. |
void RenderOverlay (RenderEvent e) |
|
These events can be used to display something on the screen. Elements drawn by these events are drawn underneath the console and menus. The difference between the two events, is that elements drawn by RenderOverlay are drawn over the HUD, while elements drawn by RenderUnderlay are drawn underneath it. Note that it works locally and in ui context, which means you can't modify actors and have to make sure what player you are drawing it for (using consoleplayer global variable). |
bool UiProcess (UiEvent e) |
|
By using this event you can receive UI input in the event handler. UI input is different from game input in that you can receive absolute screen mouse position instead of mouse deltas, and keyboard events are a bit more suitable for text input. This event will only be received if you set your EventHandler in UI mode, e.g. by doing this: self.IsUiProcessor = true; Additionally, mouse events will only be received if you also set RequireMouse to true: self.RequireMouse = true;
Note: this is one of the few non-void methods in the event system. By returning true here you will block any processing of this event by the other event handlers if their Order is lower than the current EventHandler. |
bool InputProcess (InputEvent e) |
|
This event provides direct interface to the commonly used player input. You don't need any special steps in order to use it. For detailed information, see the dedicated InputProces page.
Note: this is one of the few non-void methods in the event system. By returning |
void UiTick () | — | This is the same as WorldTick, except it also runs outside of the level (only matters for StaticEventHandlers) and runs in the ui context. |
void PostUiTick () | — | Similar to UiTick except it's ran after all game operations on the given tick. |
void ConsoleProcess (ConsoleEvent e) |
|
This event is called when the player uses the "event" console command. It runs in the ui context. For example, when the player runs this command: event testevent 1 2 3 The event handler will receive Name as "testevent" and Args[0]...Args[2] as {1,2,3}. |
void NetworkProcess (ConsoleEvent e) |
|
This event is called either when the player uses the "netevent" console command, or when EventHandler.SendNetworkEvent is used. To distinguish between these two cases, you can use IsManual. This field will be true if the event was produced manually through the console. |
void InterfaceProcess (ConsoleEvent e) |
|
This event is called either when the player uses the "interfaceevent" console command, or when EventHandler.SendInterfaceEvent is used. To distinguish between these two cases, you can use IsManual. This field will be true if the event was produced manually through the console. |
void CheckReplacement (ReplaceEvent e) |
|
This event is called when performing actor class replacements.
Replacing actor classes through this method has precedence over both the skill method and the replaces-keyword method in DECORATE and ZScript (regardless of the value of isFinal). |
void CheckReplacee (ReplacedEvent e) |
|
This is called by functions such as A_BossDeath or any other replacee checkers. When using CheckReplacement instead of the 'replaces' keyword directly for actors, those functions check if there is a replacement for monsters such as the Arachnotron with the Doom 2 MAP07 specials. By indicating the replacee is an Arachnotron for example, this will ensure that all the monsters who call those functions will not trigger the special until all replacees of Arachnotron are dead.
|
void NewGame () | — | This event is called upon starting a new game. It is also called upon entering a titlemap, as well as upon being reborn after death without a saved game. |
Event handler order
Event handler order defines the order in which user interaction-related events are received by the handlers.
It also defines the order in which RenderOverlay events are received.
For input events, the higher order receives the event first, while for render events the higher order receives the event last (thus drawing on top of everything).
Events that are reverse ordered:
- PlayerDisconnected
- RenderOverlay
- WorldThingDestroyed
- WorldUnloaded
You can set the order only in OnRegister callback, like this:
override void OnRegister()
{
SetOrder(666);
}
The value is arbitrary and it only matters in relation to other event handler order. Default order is 0.
Communicating with the UI
Sometimes sending information from the playsim to the UI is needed, but this can be difficult to set up since the playsim has no access to the UI whatsoever. EventHandler.SendInterfaceEvent
cam be used to pass select information over similar to a network event. It's ran instantly across all machines so the player number is needed to know which one should execute it. If you want an interface event to trigger on everyone's machine, passing consoleplayer as the first argument will do this. Passing net_arbitrator can be used to always send information over to the host of a game when playing online for potential admin handling.
class SpecialHUDEvents : EventHandler
{
override void WorldThingDied(WorldEvent e)
{
// Pass information to the status bar that a player killed a monster
if (e.thing && e.thing.bIsMonster && e.thing.target && e.thing.target.player)
{
EventHandler.SendInterfaceEvent(e.thing.target.PlayerNumber(), "KilledMonster");
}
}
override void InterfaceProcess(ConsoleEvent e)
{
if (!e.isManual && e.name ~== "KilledMonster")
{
let sb = CustomStatusBar(statusBar); //example name; CustomStatusBar is meant to be a name of your custom BaseStatusBar-based class
if (sb)
{
sb.KilledMonster(); //example name; meant to be a custom function in your custom HUD
}
}
}
}
Networking
Since GZDoom 2.4, scripts that are allowed to directly interact with the user and scripts that are actively involved in the world processing are separated. World scripts are called "play scope" and user interaction scripts are called "ui scope". Altering the world from ui context is not possible; for details see object scopes and versions.
In order to perform some world action when the user clicks in a menu or presses some key in an InputEvent handler, you need to use the combination of EventHandler.SendNetworkEvent and an event handler that has an override of NetworkProcess.
In your ui, you execute it as follows:
EventHandler.SendNetworkEvent("myevent", 666, 23, -1);
Then any event handlers that handle NetworkProcess will receive an event with Name equal to "myevent" and Args equal to {666,23,-1}.
Passing strings to network events
Any event handler can handle any name, so if you need to send a string you can simply check for Name starting with a value, for example (space does not work for this when typing directly into the console - you need an alias or SendNetworkEvent, or put the thing in quotes):
EventHandler.SendNetworkEvent("giveitem:BFG9000");
where the event handler in question will find the strings on either side of the colon and deal with them accordingly:
class ItemGiveEventHandler : EventHandler
{
override void NetworkProcess (ConsoleEvent e)
{
if (e.IsManual || e.Player < 0 || !PlayerInGame[e.Player] || !(players[e.Player].mo))
return;
let player = players [e.Player].mo;
Array<string> command;
e.Name.Split (command, ":");
if(command.Size() == 2 && (command [0] ~== "giveitem"))
{
player.GiveInventory (command [1], 1);
}
}
}
Without the IsManual check, this event handler as written would let any player type "netevent giveitem:BFG9000" ingame to get a BFG and the engine would not recognize it as a cheat.
Passing objects through networks
As of GZDoom 4.12, objects can now be passed around networks for serialization and fetching specific entities using the following functions.
native clearscope static Object GetNetworkEntity(uint id);
native play void EnableNetworking(bool enable);
native clearscope uint GetNetworkID() const;
To do so, EnableNetworking(true);
must be called first and can only be done so from the `play` side. This will enable GetNetworkID()
to return a valid unsigned int that can be plugged into GetNetworkEntity(id)
which retrieves the object.
NOTES:
- Actors always have networking enabled, and cannot be disabled.
- IDs cannot be reserved or overridden. They're first-come-first serve, and whenever a networked object disables networking (such as being destroyed), the freed ID always has higher priority for immediate reassignment.
- IDs should not be stored or used as substitutes for pointers outside of networking, as they can change at any given moment by becoming invalid or pointing to another object entirely, due to the aforementioned recycling.
Network Commands and Buffers
As of GZDoom 4.12, a new API for sending custom network events has been added. This is sent via NetworkBuffers and received thru NetworkCommands. This API allows for much more extensive handling of network messages by allowing the ability to send any basic data type over the network (ints, doubles, and strings) and with a variable amount of them as opposed to the string and three ints of network events. These also can't be sent through the console making them exclusive to its usage within ZScript, offering more basic safety out of the box.
EventHandler.SendNetworkCommand is the simplest way to send a message. This takes a Name for the command followed by an unlimited amount of data pairs. The first argument of the pair describes what type the data is e.g. NET_INT, NET_DOUBLE, etc., and the next value is the actual data being sent.
EventHandler.SendNetworkCommand('MyCmd', NET_DOUBLE, 5.2342, NET_INT, 7, NET_STRING, "Hello world!");
This can even be sent with nothing but a name if you have no data to send, making it much more efficient than using network events.
Processing of these commands is done within a handler's NetworkCommandProcess function. This takes a NetworkCommand that contains the client id who sent it, the command name, and the network message itself. This is safe in that it contains only the message sent by this command and won't error out if reaching the end of the internal buffer. A set of Read* functions allow the buffer to be parsed. If we follow the network command from the above example, we would read it as:
class MyEventHandler : EventHandler
{
override void NetworkCommandProcess(NetworkCommand cmd)
{
if (cmd.Command == 'MyCmd')
{
double myDouble = cmd.ReadDouble();
int myInt = cmd.ReadInt();
string myString = cmd.ReadString();
// Prints "5.2342, 7, Hello world!"
Console.Printf("%.4f, %d, %s", myDouble, myInt, myString);
}
}
}
A more advanced method of sending data is a NetworkBuffer. This allows data to be built up about a network message if the specifics of it aren't entirely known before sending it. This has similar functions to NetworkCommands except they use Add* instead to specify what type of data should be written to the buffer e.g. AddDouble, AddString, etc. Note: this doesn't write directly to the actual network buffer but is merely a wrapper for sending a full command to it. EventHandler.SendNetworkBuffer is similar to SendNetworkCommand except instead of a variable list of arguments it takes a NetworkBuffer object.
NetworkBuffer myBuffer = new("NetworkBuffer");
myBuffer.AddDouble(5.2342);
myBuffer.AddInt(7);
myBuffer.AddString("Hello world!");
EventHandler.SendNetworkBuffer('MyCmd', myBuffer);
Data can be sent in smaller chunks e.g. AddInt8/NET_INT8 can be used to send a number over as an 8-bit byte instead of a full 32-bit value. This can be useful for saving on network traffic but should only be used if you absolutely know the value range of something being sent (e.g. anything outside that range is considered invalid). Otherwise if you don't know the full extent of the value it's best to use full-sized data types to prevent any data loss. This can also be used for custom quantizing of values if needed.
Serializing structs/objects
Given the above functionality, it's possible to use this to create custom serializing functions for large data types/objects. In particular this is useful with objects because without a custom network id set up, there's no way to handle sending an object over the network without sending over its data and reconstructing it on the other side (these things are tracked as memory references which naturally are going to be different on every client's machine). Serialization functions are a form of unified function that handles both writing and reading data to keep it isolated to one location, preventing possible mistakes. There are multiple ways to set this up, but below is a very basic implementation on objects:
class MyObject play
{
int iData;
bool bData;
Array<double> fData;
void Serialize(NetworkCommand cmd = null, NetworkBuffer buffer = null)
{
if (cmd) // Reading.
{
iData = cmd.ReadInt();
bData = cmd.ReadInt8();
cmd.ReadDoubleArray(fData);
}
else if (buffer) // Writing.
{
buffer.AddInt(iData);
buffer.AddInt8(bData);
buffer.AddDoubleArray(fData);
}
}
}
// Sending the object.
NetworkBuffer buffer = new("NetworkBuffer");
myObj.Serialize(buffer: buffer);
EventHandler.SendNetworkBuffer('MyObj', buffer);
// Reading the object back.
if (cmd.Command == 'MyObj')
{
MyObject myObj = new("MyObject");
myObj.Serialize(cmd);
}
Destruction
Event handlers inherit from Object, and can be destroyed by calling the Destroy() function. This is useful for one-off events, since the list of functions being called (see above) is large and can potentially slow down the game.
There are a few circumstances to be aware of:
- Make sure the handler is absolutely unneeded before destroying. Unlike other objects, event handlers require special initialization by the engine and cannot be recreated with 'new' keyword.
- Regular event handlers are recreated after each map is loaded and are automatically destroyed when leaving a hub cluster or non-hub map, but static handlers are only created when the game starts up.
- Avoid calling Destroy() in subfunctions. Processing code can still execute and cause unpredictable behavior or crash the game. An ideal way to destroy a handler is as follows:
private bool DestroyMe;
override void WorldTick()
{
if (DestroyMe) // If we no longer need this event handler, get rid of it.
{
if (!bDESTROYED) // Safety check, a flag found in Object itself.
Destroy();
return;
}
}
Examples
Getting an event handler. This ensures that event handlers are properly found, and are the correct type. Put this inside the event handler, and replace 'MyEventHandler' with the name of the handler that's given this function.
// If put outside an event handler, you would add 'MyEventHandler.' behind Find.
static clearscope MyEventHandler Fetch()
{
return MyEventHandler(Find("MyEventHandler"));
}
Using CheckReplacement, this example replaces all monsters with archviles.
override void CheckReplacement (ReplaceEvent e)
{
if (GetDefaultByType(e.Replacee).bIsMonster)
{
e.Replacement = 'Archvile';
}
}
This event handler prints the class name of any Actor that spawns
version "2.4" // Make sure to use version 2.4 or later
class NamePrinter : EventHandler
{
override void WorldThingSpawned(WorldEvent e)
{
if (e.thing) // Check that the Actor is valid
console.printf("%s", e.thing.GetClassName());
}
}
In the MAPINFO:
GameInfo { AddEventHandlers = "NamePrinter" }
See also the Hello World page, which implements a basic "Hello World!" script using eventhandlers.