Awesome
Luvent: A Simple Event Library for Lua
Luvent is a library for Lua, written entirely in Lua, which helps support event-driven programming. Luvent lets you create events, which are objects with any number of associated functions. Whenever you trigger an event the library will execute all functions attached to that event. You may trigger an event multiple times and can provide different arguments to that event’s functions each time.
There are great alternatives to Luvent listed below. However, most of those libraries do not have an explicit license. I created Luvent to use in a commercial game, built on the LÖVE framework, and so I could not use any library that did not explicitly grant legal permission to use the code in that situation. Luvent may not be that different from existing libraries in terms of its functionality; but it is and will always be free for developers to use in any program whether it is commercial or not.
Requirements
Luvent requires one of the following Lua implementations:
These are the versions we use to test Luvent. It should work with later versions of each, and possibly older versions as well.
Optional
The following programs are not necessary in order to use Luvent but you will need them to run the unit tests, generate API documentation, and so on.
Installation
All you need to do is place src/Luvent.lua
in a directory that is
part of package.path
so that Lua can find and load it. Since the
entire library is that one file you can also simply copy
src/Luvent.lua
into the directory alongside the rest of your code
and require()
it from there. If you have Busted then you can run
make tests
first to ensure that Luvent behaves as intended.
Documentation
Running the command make docs
will populate the docs/
directory
with HTML documents that describe Luvent’s API.
Terminology
-
Event: An object which you can
trigger(...)
in order to invoke all actions associated with the event. -
Action: Any of the following qualify as actions that you can associate with events:
- A function.
- A coroutine.
- A table that supports the
__call()
metamethod.
-
Action ID: An object that represents an action. The method
addAction()
will return an action ID that you can save to later use with methods such asdisableAction()
. Instead of the ID you can also use the action itself, e.g. if the action is a function then any method that asks for an ‘action or ID’ will accept that function itself or the ID thataddAction()
returned when given the function. Action IDs are always true in a boolean context. If two IDs are equal then they represent the same action. Those are the only two properties that the API provides; if you rely on anything else about action IDs (e.g. their type) then your code may suddenly break in the future.
Basic Example
You create new events with the newEvent()
function. Note well that
you must never rely on the properties of the event objects.
Anything that is not a method is not part of the API and may change at
any time.
Once you have an event object you can begin to add ‘actions’ to it via
its addAction()
method. To invoke those actions you ‘trigger’ the
event by calling its trigger()
method. Every action associated with
the event will receive any parameters you give to trigger()
.
Below is a lengthy example that demonstrates the basics of creating and triggering events, and adding and removing actions.
-- In this example you will pretend you are implementing a module in a
-- game that creates and manages enemies. To simplfy the example you
-- use Enrique García Cota's terrific MiddleClass library in order
-- to make the class and objects for enemies.
--
-- https://github.com/kikito/middleclass
--
local class = require "middleclass"
local Luvent = require "Luvent"
local Enemy = class("Enemy")
-- This hash contains a reference to all living enemies.
Enemy.static.LIVING = {}
function Enemy:initialize(family, maxHP)
self.family = family
self.maxHP = maxHP
self.HP = maxHP
table.insert(Enemy.LIVING, self)
end
-- This is the event you trigger any time an enemy dies.
Enemy.static.onDie = Luvent.newEvent()
-- This method applies damage to an enemy and will trigger its 'onDie'
-- event if the enemy's hit points reach zero or less.
function Enemy:damage(damage)
self.HP = self.HP - damage
if self.HP <= 0 then
Enemy.onDie:trigger(self)
end
end
-- Now you can start associating actions with the 'onDie' event. First
-- you start by removing the enemy from the table of living enemies.
Enemy.onDie:addAction(
function (enemy)
for index,living_enemy in ipairs(Enemy.LIVING) do
if enemy == living_enemy then
table.remove(Enemy.LIVING, index)
return
end
end
end)
-- For debugging you want to see on the console when an enemy dies, so
-- you add that as a separate action. This time you save the return
-- value of addAction() so that later you can use that to remove the
-- action when you want to stop printing debugging output.
local debugAction = Enemy.onDie:addAction(
function (enemy)
print(string.format("Enemy %s died", enemy.family))
end)
-- Now you make some enemies and kill them to demonstrate how the
-- trigger() method used in Enemy:damage() invokes the actions.
local bee = Enemy:new("Bee", 10)
local ladybug = Enemy:new("Ladybug", 1)
-- This will print "2"
print(#Enemy.LIVING)
-- This kills the enemy so the program invokes the two actions above,
-- meaning it will print "Enemy Ladbug died" to the console and will
-- remove it from Enemy.LIVING.
ladybug:damage(100)
print(#Enemy.LIVING) -- Prints "1"
-- Now you turn off the debugging output by removing that action. As a
-- result you will see no output after killing the bee.
Enemy.onDie:removeAction(debugAction)
bee:damage(50)
print(#Enemy.LIVING) -- Prints "0"
Note: Luvent discards all return values from action functions or anything that coroutines yield.
Getting Information About Actions
Luvent provides two methods for gathering information about the relationship between an event an actions.
-
The method
getActionCount()
tells you the number of actions associated with an event. This is not necessarily the number of actions thattrigger()
will invoke. The method only tells you the number of unique actions associated with the event via itsaddAction()
method. Luvent allows you to temporarily disable actions and to delay their execution, which meanstrigger()
will not call those actions even though they are still associated with the event. That is why you cannot rely ongetActionCount()
to tell you the exact number of actions an event will run. -
The method
hasAction(action_or_id)
accepts an action or an action ID (i.e. the return value ofaddAction()
) and returns a boolean indicating whether or not the action is part of the event. However, ifhasAction()
returns true that is not a guarantee that the event will call that action, for the same reasons that affectgetActionCount()
.
Enabling and Disabling Actions
In the example above you removed an action. Calling the
getActionCount()
of an event will tell us how many actions it has.
However, this is not necessarily the number of actions it will invoke
if you trigger the event.
When you add an action Luvent enables it by default. You can disable actions though. For example:
local debugAction = Enemy.onDie:addAction(
function (enemy)
print(string.format("Enemy %s died", enemy.family))
end)
-- ...Later in the code...
Enemy.onDie:disableAction(debugAction)
The difference between this method and removeAction()
is that this
method only turns-off the action temporarily. Later you could call
enableAction()
to turn the action back on. When you use
removeAction()
, however, it is like deleting the action from the
event.
You can also think of the methods in pairs.
-
removeAction()
is the opposite ofaddAction()
. -
disableAction()
is the opposite ofenableAction()
.
The first pair of methods affect the return value of
getActionCount()
and hasAction()
. The second pair does not.
Action Intervals
Events may have actions with intervals, i.e. guaranteed delays in the amount of time that must pass before the action will run again. Luvent allows us to define intervals in terms of seconds. For example:
-- In this example you have a game where the AI has an 'onThink' event.
-- You want the AI to do many things on that event, but some of them
-- may be expensive in terms of performance. So there may be actions
-- which you only want to run every so many seconds.
local function someSlowFunction(ai)
-- You do something with the AI here that can take a while and so
-- you do not want to always run this action.
end
AI.onThink:addAction(someSlowFunction)
-- Now you can tell the event to execute the action every ten seconds.
AI.onThink:setActionInterval(someSlowFunction, 10)
-- No matter how often you trigger the event, someSlowFunction() will
-- only run once per ten seconds.
while true do
AI.onThink:trigger()
end
Actions have no interval by default. You can use the method
removeActionInterval()
to take away any interval from an action,
even if that action has no interval in the first place.
Prioritizing Actions
By default Luvent makes no guarantees about the order in which an event will execute actions. But you can create such guarantees by assigning numeric priorities to actions. In that case Luvent will invoke actions based on the order of the priority, from highest to lowest, for example:
-- Let's say you are writing an AI for a board game. You have an
-- 'onMove' event which triggers a variety of actions. The functions
-- have stub implementations for the sake of brevity.
AI.onMove = Luvent.newEvent()
local function makeMove(player, board) end
local function analyzeCurrentPosition(player, board) end
local function searchPatternDatabase(player, board) end
local function estimateScore(player, board) end
AI.onMove:addAction(makeMove)
AI.onMove:addAction(analyzeCurrentPosition)
AI.onMove:addAction(searchPatternDatabase)
AI.onMove:addAction(estimateScore)
-- At this point you have given no action any explicit priority. So if
-- you trigger the event now then there is no guarantee about the order
-- in which Luvent will call each action. You cannot even rely on the
-- event to call the actions in the order you added them.
AI.onMove:setActionPriority(makeMove, 2)
AI.onMove:setActionPriority(analyzeCurrentPosition, 4)
AI.onMove:setActionPriority(searchPatternDatabase, 3)
AI.onMove:setActionPriority(estimateScore, 1)
After you set the priorities at the end then when you call
AI.onMove:trigger()
it will invoke the actions in this order:
analyzeCurrentPosition()
searchPatternDatabase()
makeMove()
estimateScore()
You can use removeActionPriority()
on any of these actions to place
them back at the bottom of the list, which is Luvent’s default
behavior. Any action without an explicit priority will run last. If
more than one action has the same priority then there is no guarantee
about the order in which Luvent will call those actions.
Action Limits
There are situations where you may want to limit the amount of times an event will invoke a specific action. You can control this by setting the ‘limit’ for the action. For example:
-- In this example you are working with an 'onDeath' event for players
-- in a game. The first time the player dies you want to save his or
-- her score. But the player can continue after that and you do not
-- want to record scores after the first continue. And so you want
-- the action for saving the score to run only once.
Game.onDeath = Luvent.newEvent()
local function saveScore(player) end
local function promptForContinue(player) end
Game.onDeath:addAction(saveScore)
Game.onDeath:addAction(promptForContinue)
-- This tells Luvent the limit for the action, i.e. the number of
-- times to invoke the action before automatically removing it. This
-- specific example causes the event to call saveScore() only once and
-- then it will remove the action from 'onDeath'.
Game.onDeath:setActionLimit(saveScore, 1)
-- And for sanity this makes sure to save the score first by giving it
-- a higher priority than promptForContinue(). The number ten here is
-- an arbitrary choice; it just needs to be a number greater than zero
-- since there is no explicit priority for the other action.
Game.onDeath:setActionPriority(saveScore, 10)
The first call to Game.onDeath:trigger()
will invoke saveScore()
and then promptForContinue()
. All future trigger()
calls will
only invoke the second action. Once an action reaches its limit then
Luvent effectively calls removeAction()
, meaning you would have to
manually re-add the action before the event would use it again.
Getters
Luvent gives you three getters for obtaining information about actions:
getActionTriggerLimit(action_or_id)
getActionInterval(action_or_id)
getActionPriority(action_or_id)
This methods complement the setters above and help you gain insight into the behavior of an action, e.g. how many times it will be triggered before being disabled.
Looping Over Actions
The API provides two methods for looping through all of the actions
associated with an event. The first is allActions()
. It returns an
iterator to use in a for
loop, e.g.
-- Continuing the previous example, this loop temporarily disables all
-- actions attached to the event.
for action in Game.onDie:allActions() do
Game.onDie:disableAction(action)
end
If you only need to call one function or method for each action, as in
the above example, then you can use forEachAction()
. It accepts one
argument, a function which is called once for each action. That
function receives two arguments per invocation:
-
The Event object containing the actions.
-
The ID of the current action.
This requirement makes it possible to call a method for each action by
passing that method to forEachAction()
. For example, you can
rewrite the previous loop like so:
Game.onDie:forEachAction(Luvent.disableAction)
It is an error to add or remove actions during iteration. You
cannot call addAction()
, removeAction()
, or removeAllActions()
during a loop using allActions()
or via forEachAction()
.
Complete List of the Public API
You create events with the function Luvent.newEvent()
. The function
returns an object with the following methods:
trigger(...)
addAction(action)
removeAction(action_or_id)
removeAllActions()
getActionCount()
hasAction(action_or_id)
isActionEnabled(action_or_id)
enableAction(action_or_id)
disableAction(action_or_id)
setActionPriority(action_or_id, integer)
removeActionPriority(action_or_id)
setActionTriggerLimit(action_or_id, integer)
removeActionTriggerLimit(action_or_id)
setActionInterval(action_or_id, integer)
removeActionInterval(action_or_id)
allActions()
forEachAction(callable)
getActionTriggerLimit(action_or_id)
getActionInterval(action_or_id)
getActionPriority(action_or_id)
Acknowledgments and Alternatives
EventLib by Elijah Frederickson is the major inspiration for the design and implementation of Luvent. The Luvent API also owes a debt of ideas and names to Node.js by Ryan Dahl et al. The following is a list of alternatives to Luvent for the sake of comparison, as some may be better suited for some developers or projects:
- Custom Event Support by Ben Beltran
- Emitter by Chris Friesen
- Lua-Event by Alex Szpakowski
- Lua-Events by syntruth
- Lua-Events by Wouter Scherphof
- events.lua by José Miguel Molina
License
Copyright 2013–2015 Eric James Michael Ritz