diff --git a/init.lua b/init.lua index 9abe197..c8e0869 100644 --- a/init.lua +++ b/init.lua @@ -31,6 +31,8 @@ local _batteries = { vec3 = require_relative("vec3"), intersect = require_relative("intersect"), -- + timer = require_relative("timer"), + pubsub = require_relative("pubsub"), unique_mapping = require_relative("unique_mapping"), state_machine = require_relative("state_machine"), async = require_relative("async"), @@ -73,6 +75,9 @@ function _batteries:export() --overwrite assert wholesale (it's compatible) assert = self.assert + --export the whole library to global `batteries` + batteries = self + return self end diff --git a/pubsub.lua b/pubsub.lua new file mode 100644 index 0000000..a877f07 --- /dev/null +++ b/pubsub.lua @@ -0,0 +1,60 @@ +--[[ + dead-simple publish-subscribe message bus +]] + +local path = (...):gsub("pubsub", "") +local class = require(path .. "class") +local pubsub = class() + +--create a new pubsub bus +function pubsub:new() + return self:init({ + subscriptions = {}, + }) +end + +--(internal; notify a callback set of an event) +function pubsub:_notify(callbacks, ...) + if callbacks then + for _, f in callbacks:ipairs() do + f(...) + end + end +end + +--publish an event, with optional arguments +--notifies both the direct subscribers, and those subscribed to "everything" +function pubsub:publish(event, ...) + self:_notify(self.subscriptions[event], ...) + self:_notify(self.subscriptions.everything, event, ...) +end + +--subscribe to an event +--can be a specifically named event, or "everything" to get notified for any event +--for "everything", the callback will recieve the event name as the first argument +function pubsub:subscribe(event, callback) + local callbacks = self.subscriptions[event] + if not callbacks then + callbacks = set() + self.subscriptions[event] = callbacks + end + callbacks:add(callback) +end + +--unsubscribe from an event +function pubsub:unsubscribe(event, callback) + local callbacks = self.subscriptions[event] + if callbacks then + callbacks:remove(callback) + if callbacks:size() == 0 then + self.subscriptions[event] = nil + end + end +end + +--check if there is a subscriber for a given event +function pubsub:has_subcriber(event) + return self.subscriptions[event] ~= nil +end + +return pubsub diff --git a/readme.md b/readme.md index 17fdb98..b4b3ab5 100644 --- a/readme.md +++ b/readme.md @@ -64,6 +64,8 @@ General utility data structures and algorithms to speed you along your way. - `set` - A set type supporting a full suite of set operations with fast membership testing and `ipairs`-style iteration. - `sort` - Provides a stable merge+insertion sorting algorithm that is also, as a bonus, often faster than `table.sort` under luajit. Also exposes `insertion_sort` if needed. Alias `stable_sort`. - `state_machine` - Finite state machine implementation with state transitions and all the rest. Useful for game states, AI, cutscenes... +- `timer` - a "countdown" style timer with progress and completion callbacks. +- `pubsub` - a self-contained publish/subscribe message bus. Immediate mode rather than queued, local rather than networked, but if you were expecting mqtt in 60 lines I don't know what to tell you. Scales pretty well nonetheless. **Geometry:** diff --git a/timer.lua b/timer.lua new file mode 100644 index 0000000..f5c070d --- /dev/null +++ b/timer.lua @@ -0,0 +1,59 @@ +--[[ + basic timer class + + can check for expiry and register a callback to be called on progress and on finish + + if you find yourself using lots of these for pushing stuff into the future, + look into async.lua and see if it might be a better fit! +]] + +local path = (...):gsub("timer", "") +local class = require(path .. "class") +local timer = class() + +--create a timer, with optional callbacks +--callbacks recieve as arguments: +-- the current progress as a number from 0 to 1, so can be used for lerps +-- the timer object, so can be reset if needed +function timer:new(time, on_progress, on_finish) + return self:init({ + time = math.max(time, 1e-6), --negative time not allowed + timer = 0, + on_progress = on_progress, + on_finish = on_finish, + }) +end + +--update this timer, calling the relevant callback if it exists +function timer:update(dt) + if not self:expired() then + self.timer = self.timer + dt + + --get the relevant callback + local cb = self:expired() + and self.on_finish + or self.on_progress + + if cb then + cb(self:progress(), self) + end + end +end + +--check if the timer has expired +function timer:expired() + return self.timer >= self.time +end + +--get the timer's progress from 0 to 1 +function timer:progress() + return math.min(self.timer / self.time, 1) +end + +--reset the timer +--will resume calling the same callbacks, so can be used for intervals +function timer:reset() + self.timer = 0 +end + +return timer