Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 

Repository files navigation

signal.lua

this is a simple and slightly opinionated but powerful lua library (and one of many) that implements the observer pattern. vaguely influenced by godot signals. use this to send messages from an object without having to care who listens.

core functions

  • signal.register(emitter, signal_id) - register a new signal
  • signal.connect(emitter, signal_id, listener, connection_id[, callback][, oneshot]) - connect a listener to a signal
  • signal.emit(emitter, signal_id, ...) - emit a signal with optional parameters
  • signal.disconnect(emitter, signal_id, listener, connection_id) - disconnect a specific listener
  • signal.cleanup(object) - remove all signals and connections for an object
  • signal.deregister(emitter, signal_id) - remove a signal and all its connections

example usage

frequently, things in video games keep stored a number of "healths" so that they don't immediately die when they are born. sometimes you want a visual representation of how many healths a video game thing has on the screen, so you can know if it is close to dying. but it would be annoying if the thing had to remember to show its own healths meter. really it shouldnt care about that.

there is a better way

imagine it like this:

  • the player is a radio deejay on KPLYR 107.3 FM
  • the player does not know jack about the healthbar, he's just slinging tunes
  • the healthbar is a big fan of this radio station and tunes in every day and even buys all the exclusive tee shirts to wear

could you imagine if the radio deejay had to drive to your house and play a song for you in person in order for you to hear it? that would be annoying. this makes video games more like radio stations

basic usage

local signal = require "signal"

-- create a player with health
local player = {
    health = 100,
}

-- register the health_changed signal
signal.register(player, "health_changed")

-- add a method to handle damage
function player:take_damage(amount)
    self.health = self.health - amount
    signal.emit(self, "health_changed", self.health)
end

-- create a ui element to display health
local health_display = {}

function health_display:update(current_health)
    local health_bar = string.rep("", current_health / 10)
    print(string.format("health: %d/100 [%s]", current_health, health_bar))
end

-- connect the health display to player's health changes
signal.connect(player, "health_changed", health_display, "update")

-- now when we damage the player, the display updates automatically
player:take_damage(30)  -- outputs: health: 70/100 [███████]
player:take_damage(20)  -- outputs: health: 50/100 [█████]

other features

cleaning up dead objects

when objects are destroyed, you need to clean up their signals or else they will stay in memory forever. this removes all connections to and from the object:

player:take_damage(30)
if player.health <= 0 then
    signal.cleanup(player)
    player = nil
end

custom callbacks

instead of using method names, you can provide callback functions directly:

-- create a sound effect handler
local audio = {}

signal.connect(player, "health_changed", audio, "play_hurt_sound",
    function(health)
        if health < 20 then
            print("playing critical health warning sound!")
        elseif health < 50 then
            print("playing hurt sound!")
        end
    end
)

one-shot signals

create signals that automatically disconnect after first use:

local achievement_manager = {}

-- this will only trigger once when health drops below 50%
signal.connect(player, "health_changed", achievement_manager, "low_health_achievement",
    function(health)
        if health <= 50 then
            print("achievement unlocked: take a whole load of damage")
        end
    end,
    true  -- oneshot parameter
)

restrictions

  • emitters and listeners must be tables
  • signal and connection ids must be strings or numbers
  • each signal must have a unique connection id to its listener. for example:
    • ✅ you can connect signal "door_opened" from object door to object lights with id "turn_on", and also connect the same signal "door_opened" to object security with id "turn_on"
    • ❌ you cannot connect signal "door_opened" from object door to object lights with id "turn_on" twice

additional notes

  • you have to be diligent about cleaning up signals when objects are destroyed. whenever you destroy an object, you should call signal.cleanup(obj) unless you want weird things to happen.
  • most of the important api calls generate a bit of garbage but it probably won't matter unless you are doing something really insane.
  • i dont know if this is thread safe.

About

observer pattern for lua

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages