Defold Event - Cross-Context Defold Event System

logo

Event

Event - is a single file Lua module for the Defold game engine. It provides a simple and efficient way to manage events and callbacks in your game.

Features

  • Event Management: Create, subscribe, unsubscribe, and trigger events.
  • Cross-Context: You can subscribe to events from different scripts.
  • Callback Management: Attach callbacks to events with optional data.
  • Logging: Set a logger to log event activities.
  • Memory Allocations Tracker: Detects if an event callback causes a huge memory allocations.

Setup

See the Defold-Event repository on Github for the Setup, Documentation, API and Use Cases

Example

You can create a global event module that allows events to be triggered from anywhere within your game (from USE_CASES.md).

-- global_events.lua
-- Create events in the lua module
local event = require("event.event")

local M = {}

M.on_game_start = event.create()
M.on_game_over = event.create()

return M
-- Subscribe to the events from any place
local global_events = require("global_events")

local function on_game_start(self, param1, param2)
    -- Animate GUI elements somehow
end

local function on_game_over(self)
    -- Animate GUI elements somehow
end

function init(self)
    -- The second arg - callback context is optional
    -- Is will be passed as an first argument to the callback
    -- Here we pass the *self* to have an access in the callbacks
    global_events.on_game_start:subscribe(on_game_start, self)
    global_events.on_game_over:subscribe(on_game_over, self)
end

function final(self)
    -- Unsubscribe is mandatory if subscribed instance is destroyed
    global_events.on_game_start:unsubscribe(on_game_start, self)
    global_events.on_game_over:unsubscribe(on_game_over, self)
end
-- Trigger the events from any place
local global_events = require("global_events")

function init(self)
    timer.delay(1, false, function()
         global_events.on_game_start:trigger("param1", "param2")
    end)
end
11 Likes

Cool, i also use have lib for event, that was bases on eve event.

1)Why you use variables in trigger instead of …

function M:trigger(a, b, c, d, e, f, g, h, i, j)
function Event:trigger(...)

2)In my evene lib i use flag to save context or not. For some cases script context is not necessary

3)You use lua_script_instance but your implementation has bug. isInstaceValid not worked in Set_impl

You need to dmScript::SetInstance before check it with dmScript::IsInstanceValid
You

static int Set_impl(lua_State* L)
{
    DM_LUA_STACK_CHECK(L, -1);
    if (!dmScript::IsInstanceValid(L))
    {
        dmLogError("Instance is not valid")
        return DM_LUA_ERROR("Instance is not valid");
    }
    dmScript::SetInstance(L);
    return 0;
}

My

static int Set_impl(lua_State* L){
    DM_LUA_STACK_CHECK(L, 0);
    dmScript::GetInstance(L);//current
    lua_pushvalue(L,-2);//move new on top. stack. new->current->new
    dmScript::SetInstance(L);//set new  stack. new->current
    if (!dmScript::IsInstanceValid(L)){
        dmScript::SetInstance(L);//set current
        DM_LUA_ERROR("Instance is not valid");
    }else{
        lua_pop(L,1);
    }
    return 0;
}

4)You add lua_script_instance in you project. I think you need to use custom name for it?
What happened if i add in same project. Error or some of lua_script_instance ovveride another?
https://github.com/DanEngelbrecht/LuaScriptInstance
https://github.com/Insality/defold-event

This is how i make events)

local CLASS = require "libs.middleclass"
local LOG = require "libs.log"

local Event = CLASS.class("EventClass")

function Event:initialize(name)
	self.name = assert(name)
	self.callbacks = {}
end
function Event:subscribe(save_context, callback)
	assert(type(callback) == "function")

	if self:is_subscribed(callback) then
		LOG:e("Event:" .. self.name .. " is already subscribed", 3)
		return
	end

	table.insert(self.callbacks, {
		callback = callback,
		script_context = save_context and lua_script_instance.Get()
	})

	return function() self:unsubscribe(callback) end
end

function Event:unsubscribe(callback)
	assert(type(callback) == "function")

	for index = 1, #self.callbacks do
		local cb = self.callbacks[index]
		if cb.callback == callback then
			table.remove(self.callbacks, index)
			return true
		end
	end

	return false
end

function Event:is_subscribed(callback)
	for index = 1, #self.callbacks do
		local cb = self.callbacks[index]
		if cb.callback == callback then return true end
	end

	return false
end


--- Trigger the event
-- @function event.trigger
-- @tparam args args The args for event trigger
function Event:trigger(...)
	local current_script_context = lua_script_instance.Get()

	for index = 1, #self.callbacks do
		local callback = self.callbacks[index]

		if callback.script_context and current_script_context ~= callback.script_context then
			lua_script_instance.Set(callback.script_context)
		end

		local ok, error = pcall(callback.callback, self, ...)
		if not ok then LOG.e(error) end

		if callback.script_context and current_script_context ~= callback.script_context then
			lua_script_instance.Set(current_script_context)
		end
	end
end

return Event

1 Like

A question. Does it still face the issue of nodes using in wrong GUI scene or can’t use go in gui script…?

1 Like

Thanks for the feedback! @d954mas

There is a case, when vargs works incorrect. But don’t remember why exactly, probably in the chain calls with vargs. I hit this issue in the Druid before. From this time I keep this over “…” in my libraries

Sounds good! It’s true that often the Event library should not use context switching at all. Nice part to be improved in future.

Thanks for this, I’ll take a look!

If there is another copy of lua script instance - it should override and use only one. But I have a notice in README, that if your already using it, you should remove it from dependency. This is exactly the same library with links to it

Since there is a lot of discussion about is it correct or not to use the context changing. I decide to keep the only one place where it used without explicit using this library. Wdyt about this approach?

Do you mean Eva? :smiley: or different one?

2 Likes

Yes Eva)

1 Like

The event is remember their context when it’s subscribed. So if you subscribe in the one GUI scene and call trigger this event from other scene (GO/GUI), that works correct.

3 Likes

Context changing is true defold way​:wink:
First is was used by @britzl in flow.

1 Like

This example definitely looks more like a hidden way than a true way :slight_smile:

Anyway it’s kind of dangerous or advanced thing that should be used carefully. But I like the approach to have events in data and have the ability to subscribe/unsubscribe the GUI logic over it

3 Likes

Somehows I see it doesn’t look like as its name. If it’s an event, it should have an event name. Here in my opinion, it should be called Callback :sweat_smile:

Well, the callback_instance.subscribe(callback) sound a little weird :smiley:

For named events you can use a module approach, like ~

local M = {
    object_destroyed = event.create(),
    analytics_sent = event.create(),
    another_cool_event = event.create()
}

And use it like global event bus

local events = require("my.events")

events.object_destroyed:subscribe(...)
events.analytics_sent:trigger(...)
1 Like

There are few releases for Defold Event module

V2

Changelog

Now you can use require("event.events") to subscribe and trigger events globally by event name

local events = require("event.events")
events:subscribe("event_id", callback)
events:trigger("event_id", "param1", "param2")
- Add global events module
- The `event:subscribe` and `event:unsubscribe` now return boolean value of success

V3

Changelog

- Event Trigger now returns value of last executed callback
- Add `events.is_empty(name)` function
- Add tests for Event and Global Events modules

V4

Changelog

For context validation fix thanks to @d954mas for pointing out the mistake.

- Rename `lua_script_instance` to `event_context_manager` to escape conflicts with `lua_script_instance` library
- Fix validate context in `event_context_manager.set`
- Refactor `event_context_manager`
- Add tests for changing context and memory allocations
- Add `event.set_memory_threshold` function. Works only in debug builds.
7 Likes

There are another few releases for Defold Event module. Check releases here - GitHub - Insality/defold-event: Cross-Context Defold Event System · GitHub

V5

- The `event:trigger(...)` can be called as `event(...)` via `__call` metamethod
- Add default pprint logger. Remove or replace it with `event.set_logger()`
- Add tests for context changing

V6

- Optimize memory allocations per event instance
- Localize functions in the event module for better performance

V7

- Optimize memory allocations per event instance
- Default logger now empty except for errors
7 Likes

There are another few releases for Defold Event module :upside_down_face:

Check releases here - https://github.com/Insality/defold-event

V8

- Optimize memory allocations per subscription (~35% less)

V9

- Better error tracebacks in case of error in subscription callback
- Update annotations

V10

- The `event:unsubscribe` now removes all subscriptions with the same function if `callback_context` is not provided
- You can use events instead callbacks in `event:subscribe` and `event:unsubscribe`. The subscribed event will be triggered by the parent event trigger.
- Update docs, Use Cases and API reference
5 Likes

The Defold-Event v11 is released

Hello! The update for event module is published. Introducing the defer module, which is basically the event queue with various handling mechanisms.

I using this defer module to make a some communications between GO and GUI scripts in init step. Example: get a atlas info in GUI script (it requires a GO call go.get call).

The event.use_xpcall is new flag in game.project. Due the xpcall is works different between HTML and desktop, I need to make a “wrapper” to keep consistency. This wrapper allows to see a detailed tracebacks in case of errors in events, but required a bit more memory to trigger each event. I usually turn on this option in development.

Changes:

  • Introduced the defer module. The Defer module provides a queuing mechanism for events. Unlike regular events which are immediately processed, deferred events are stored in a queue until they are explicitly handled by a subscriber. This is useful for events that need to persist until they can be properly handled.
  • Add event.use_xpcall game.project option to get detailed tracebacks in case of an error in the event callback.
  • Moved detailed API documentation to separate files
  • Remove annotations files. Now all annotations directly in the code.

Check Repo: GitHub - Insality/defold-event: Cross-Context Defold Event System · GitHub
Tagged Release: https://github.com/Insality/defold-event/archive/refs/tags/11.zip

8 Likes

Hello! There are a bunch of new releases for Defold Event library.

What is new?

Queues

Queues let you store events and process them later in FIFO order. Unlike regular events that run callbacks immediately, queue events stay in the queue until a handler returns a non-nil value (to mark the event as handled).

Use instance-based queues with queue.create() or global queues via the queues module and a string id. Push from anywhere. When you subscribe a handler, it is called for each pending and future events in the queue. You can also process events manually with process() or process_next() without using subscribers. So you can use queues in a different way.

I’m using queues to store logic events to process them later in UI one by one if required. Or to communicate between GO and GUI context at init step sometimes.

Queues API

Example — UI events queue processing:

local queues = require("event.queues")

local function process_next_event(self)
	queues.process_next("quest_events", function(self, data)
		if data.type == "quest_started" then
			animate_quest_start(data)
		elseif data.type == "quest_completed" then
			animate_quest_complete(data)
		end

		return true
	end, self)
end

Example — Cross-context communication:

-- GUI context
local queues = require("event.queues")

function M:init(self)
	queues.push("get_atlas_path", {
		texture_name = gui.get_texture(self.node),
		sender = msg.url(),
	}, self._on_get_atlas_path, self)
end

function M:_on_get_atlas_path(atlas_path)
	local atlas_data = resource.get_atlas(atlas_path)
	local tex_info = resource.get_texture_info(atlas_data.texture)
end
-- GO script
---@param request druid.get_atlas_path_request
---@return string?
local function get_atlas_path(self, request)
	if not is_my_url(request.sender) then
		return nil
	end

	return go.get(request.sender, "textures", { key = request.texture_name })
end

function init(self)
	queues.subscribe("get_atlas_path", get_atlas_path, self)
end

function final(self)
	queues.unsubscribe("get_atlas_path", get_atlas_path, self)
end

Promises

The Promise module wraps asynchronous work and lets you chain with :next(), :catch(), and :finally(). Create a promise with promise.create(executor) or use promise.resolved(value) / promise.rejected(reason). Use promise.all() to wait for multiple promises and promise.race() for the first to finish. You can resolve or reject manually with :resolve() and :reject(), and build pipelines with :append() and :tail().

Promises API

Example — wrap function to promise:

local promise = require("event.promise")

---@param url string
---@return promise
local function load_data(url)
	local p = promise.create()

	http.request(url, "GET", function(response)
		if response.status == 200 then
			p:resolve(response.body)
		else
			p:reject(response.status)
		end
	end)

	return p
end

Example — build promise pipeline:

local promise = require("event.promise")

function M:load_next_level()
	return promise.resolved()
		:next(self.unload_level, nil, self)
		:next(self.wait_level_unload, nil, self)
		:next(levels.get_next_level, nil, self)
		:next(self.load_level, nil, self)
		:next(self.animate_level_appear, nil, self)
end

Event as callback

You can subscribe an event to another event: when the parent triggers, the child event is triggered too. Handy for forwarding or composing events. You can pass a callback context when subscribing an event, e.g. extra data that will be passed as the first argument when the child event runs (e.g. event_2:subscribe(event_1, "extra data")).

Example — forward one event to another:

local event = require("event.event")

local on_click = event.create(function(self) print("clicked!") end)
local on_ui_action = event.create()
on_ui_action:subscribe(on_click)  -- any trigger of on_ui_action also triggers on_click
on_ui_action:trigger()

Example — event with context:

local event_1 = event.create(function(self, x, y) print(self, x, y) end)
local event_2 = event.create()
event_2:subscribe(event_1, "my_context")  -- event_1 runs with "my_context" as self
event_2:trigger(10, 20)  -- prints "my_context", 10, 20

Subscribe Once

Subscribe a handler so it runs only once: after the first trigger (or first handled queue event), it is automatically unsubscribed. Available on events and queues: event:subscribe_once(), events.subscribe_once(), queue:subscribe_once(), and queues.subscribe_once(). Perfect for one-shot reactions like “when loaded”, “first click”, or “handle next item only”.

Example — event, one-time:

local event = require("event.event")
local on_loaded = event.create()

on_loaded:subscribe_once(function(self) print("Loaded once!") end, self)
on_loaded:trigger()  -- runs and unsubscribes
on_loaded:trigger()  -- no callback

Example — queue, handle one item then stop:

local queue = require("event.queue")
local task_queue = queue.create()

task_queue:subscribe_once(function(self, task)
	return run_task(task)  -- return non-nil to handle and auto-unsubscribe
end, self)
task_queue:push(task_a)
task_queue:push(task_b)  -- only first is handled by this subscriber

Event Modes

You can set how the event module runs callbacks and handles errors with event.set_mode(mode).

  • pcall (default): continue on error, not full tracebacks.
  • xpcall: continue on error, same idea but with full tracebacks (more memory).
  • none: stop on error, rethrow the error with full traceback.

Example:

local event = require("event.event")

-- Set inside game.project file or in your code:
event.set_mode("pcall")   -- safe default: log errors, continue
event.set_mode("xpcall")  -- full tracebacks, still continue
event.set_mode("none")    -- errors rethrown with full traceback

Full changes for 12-15 versions

V12

  • MIGRATION: Replace require("event.defer") with require("event.queues") in case of using defer module
  • BREAKING CHANGE: Refactored queue system to be instance-based like event system. queue.lua now creates queue instances with queue.create() instead of global system
  • Added queues.lua for global queues operations (renamed from old defer.lua functionality)
  • Added Promise module on top of event module
  • Fixed queue event processing order from LIFO to FIFO (events now processed in correct queue order)
  • Added event.set_mode function to set the event processing mode (pcall, xpcall and none)
  • The none mode to disable context changing in event callback and using pcall by default

V13

  • Added queue:process_next function to process exactly one event in the queue with a specific handler (subscribers will not be called)
  • Make promise:resolve and promise:reject public functions
  • Added promise:append function to append a task to the current promise without reassigning it.
  • Added promise:tail and promise:reset functions to manage the promise tail

V14

  • Enable cross-context for none event mode
  • In none mode, when an error occurs in a callback, the Lua error is rethrown with full traceback

V15

  • subscribe_once: Added event:subscribe_once, events.subscribe_once, queue:subscribe_once, and queues.subscribe_once to subscribe a handler for a single invocation. The handler is automatically unsubscribed after the first call.
  • Unsubscribe during trigger: Calling unsubscribe from inside a callback no longer breaks the current trigger iteration. Removals are applied after the trigger finishes, so all callbacks in the current trigger still run.
  • Fix when callback_context for subscription can’t be false
  • Event as callback: Support for callback_context when subscribing an event to another event (e.g. event_2:subscribe(event_1, "any additional data")).
  • Unsubscribe event from event: Fixed unsubscribing one event from another so the correct subscription is found and removed.
  • Various edge cases fixes and improvements.
  • Documentation and API pages updates.
11 Likes

Wow! Promises? Queues? This is over the top! :heart_eyes:

4 Likes

This is wonderful! Keep the features coming! :slight_smile:

Love this library, one of the firsts extensions I add.

4 Likes

Thank you, this lib is MANDATORY for any real project!
I think I will use Queues for my input manager system! Queue all input event then process them in order in a post-input message, just before the update loop!

2 Likes