Events and handlers
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).
Note: This feature is for ZScript only. |
General information
An event handler is a class that inherits either StaticEventHandler or EventHandler.
The main difference between these is that regular EventHandlers only exist inside a level and is saved to the savegames, while a StaticEventHandler will be created on engine startup and persist between any level changes (including savegame loading).
For most of the level scripting you should use an EventHandler; StaticEventHandler is only needed if you need to do something advanced, like storing 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: in map definition and in gameinfo definition.
Handling events
Each possible event is done as a virtual method in the StaticEventHandler/EventHandler classes which can be overridden.
Note that you don't need to call the original methods because the default implementation is empty and does nothing.
Method | Data available (in the provided event) | Description |
---|---|---|
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). |
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; KeyChar is the ASCII value for the key, while KeyString is a single-character string that contains the character provided for convenience, as ZScript doesn't provide a char type. 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. MouseX and MouseY are delta values (offsets from the last mouse position). These are internally used for player aiming. KeyScan is the internal ASCII value of the pressed key while KeyChar is the real ASCII value of the pressed key. KeyString is a single-character string that contains KeyScan provided for convenience. Converting KeyChar to a string is not guaranteed to be the same as KeyString. Note that, while a bit counter-intuitive (for people unfamiliar with the console bind system), mouse buttons are considered keys for this event. 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. |
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 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) |
|
(New from 4.10.0) 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);
if (sb)
sb.KilledMonster();
}
}
}
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 || !(players [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"))
return;
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.
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 'EventHandler.' 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.