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?

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😉
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 - https://github.com/Insality/defold-event

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