Introducing Luvent: Events in Lua

Today I created and published a new project: Luvent. As the GitHub project page says, it is a library that implements support for events in Lua. I use the word ‘events’ to refer to the concept as it appears in places like the Node.js standard library. In this post I want to talk about the purpose of Luvent, particularly as it relates to the game I’m creating.

Use-Case Overview

Our shmup has all kinds of entities on screen: enemies, bullets, the player, and so on. We want to describe the consequences of certain interactions between those entities. For example, whenever a bullet touches the player or an enemy we want to calculate any damage, see if it kills the affected entity, remove the bullet from the screen (usually), and so on.

One way we can approach this is to define these actions in the context of an event. The event for this example would represent bullet contact. Web programmers will recognize this concept from JavaScript where we create event handlers to respond to events such as when a user clicks on a button. In JavaScript we call that the onClick event. The purpose of Luvent is to let us create similar events for our game. An event for handling bullet contact might look like this:

local Luvent = require "Luvent"
local onCollision = Luvent.new("onCollision")

The first line loads Luvent while the second creates the actual event. Notice that I use names similar to events found in JavaScript web applications and similar environments. Now that I have an event representing bullet collision I can begin to add functionality, code that the game should execute every time it ‘triggers’ the event. Here is one example:

onCollision:addAction(function (bullet, enemy)

    -- If 'enemy' is not actually an enemy then we do nothing.
    if getmetatable(enemy) ~= Enemy then return end

    enemy.health = enemy.health - bullet.damage

    if enemy.health <= 0 then
        enemy:die()
    end

    bullet:remove()

end)

This associates an action with the event, i.e. a function to execute whenever the game triggers the event. What we call ‘actions’ other event systems call ‘listeners’, ‘handlers’, ‘callbacks’, et cetera. The behavior of our action is hopefully obvious.

Let’s add another action. This one handles the situation where a bullet collides with a player. We want to handle this differently from the action above since the player may be invincible, and if not he may have a measure of defense we multiply to the bullet damage in order to lessen its impact.

onCollision:addAction(function (bullet, player)

    -- Like before, make sure we actually have a player.
    if getmetatable(player) ~= Player then return end

    if player.invincible == false then
        player.health = player.health - (bullet.damage * player.defense)

        if player.health <= 0 then
            player:die()
        end
    end

    bullet:remove()

end)

We make use of the event by triggering it when appropriate. Luvent executes every action associated with an event when we trigger it. For example, let’s say we have this piece of logic for all bullets:

-- Here the 'actors' table is an array of all enemies on screen along
-- with the player.
for _,actor in ipairs(actors) do
    if bullet:collidesWith(actor.hitbox) then
        onCollision:trigger(bullet, actor)
    end
end

See how we call onCollision:trigger() and pass it two arguments? Luvent takes those arguments and invokes all of the actions we previous defined, giving those arguments to those actions. That demonstrates the over-arching process of Luvent:

  1. We create an object to represent each event.

  2. We associate actions to events, functions we want to run every time we trigger the event.

  3. We trigger events wherever appropriate, thereby executing those actions, passing along any useful date to those actions.

This is not all that Luvent does—or not all that I have planned anyways—but that provides a brief overview of its general structure and how we can use it in our game.

Tip of the Iceberg

Jeff and I have long planned to release various components from our game to the public so that they may benefit other game developers. Luvent is one example of that. In the near future I will be talking more about the design of Luvent as I improve it and we begin using it more throughout our game, along with some other components we will release. And if you look at Luvent on GitHub you will see the unit tests using Busted. I have some glowing praise for Busted that I will be posting in the coming days.

Advertisements

7 thoughts on “Introducing Luvent: Events in Lua

  1. Busted, eh? I have never used Busted for the following reason: it seems to have meaningless filler constructs in order to mimic natural language. For example, what is the point of “are” in assert.are.unique? I think that mimicking natural language is fine to an extent, but adding superfluous constructs, even optional ones, is a mistake. But I guess this is just a matter of taste.

    1. > I think that mimicking natural language is fine to an extent, but adding superfluous constructs, even optional ones, is a mistake.

      Personally I like the way Busted attempts to mimic natural language, as I feel like that increases readability. But I also agree with you that it is mostly a matter of taste.

      That said, writing off all of the constructs as ‘meaningless’ and a ‘mistake’ seems like an indefensibly harsh criticsm. I am unsure that anyone could demonstrate that Busted’s superfluous use of ‘are’, ‘has’, ‘is’, etc., is an objective design flaw. I’ll admit that one problem I can see with it is that, even though the constructs are optional, they are permanently part of the API; if the Busted team ever decided, “You know this was a bad idea,” they cannot remove those constructs without breaking backwards compatibility. I believe one could argue it is a design flaw that Busted cannot remove things like ‘are’ without breaking existing scripts. But saying that all of the constructs are a ‘mistake’ is unreasonable, in my opinion.

      1. LuaUnit looks okay, but I would take an even more minimalistic approach. I haven’t done a lot of unit testing in lua. However, last summer I was working on a lua project which had unit tests. The unit testing infrastructure that I used consisted of the assert function, a has_errors function that I wrote, and lua’s built-in operators (not, ==, ~=, etc.). Tests were described using comments. I was running all of my tests under an error trapping debugger. I would post a sample, but I don’t have the code with me at the moment.

        Of course, I did not have spies, stubs, mocks, or asynchronous calls, which all seem like useful things to have.

      2. That sounds interesting and useful, particularly a testing infrastructure designed in mind to fall back into a debugger as needed. If you come across the code I’d definitely like to see it, if you wouldn’t mind sharing.

        Like you, I also have not done much unit testing in Lua. So far I have tried out LuaUnit, Busted, and Telescope, along with something I hacked together myself that was poor quality. Like I said in the post, I’m really loving Busted. But on my next Lua project I want to try out a different unit testing library/framework to keep sampling what’s out there.

        The spies/stubs/mocks of Busted have been very useful thus far. I’ve yet to try out its support for testing asynchronous code, but I’ll be doing with Luvent, so I intend to write about how well or poorly Busted works for that.

  2. Very nice framework. Thanks.
    I’m trying to migrate robotlegs (https://github.com/JesterXL/Robotlegs-for-Corona) for corona into Luvent. How would you go about this. Cheers.

    Context = {}

    function Context:new()
    luabinding:luaPrint(“RobotLegs:Context:new”)
    local context = {}
    context.commands = {}
    context.mediators = {}
    context.mediatorInstances = {}

    function context:init()
        luabinding:luaPrint("Context::init")
        onCollision = Luvent.new("onRobotlegsViewCreated")
        Runtime:addEventListener("onRobotlegsViewCreated", self)
        Runtime:addEventListener("onRobotlegsViewDestroyed", self)
    end
    
    function context:onRobotlegsViewCreated(event)
        luabinding:luaPrint("Context::onRobotlegsViewCreated")
        local view = event.target
        if view == nil then
            error("ERROR: Robotlegs Context received a create event, but no view instance in the event object.")
        end
        self:createMediator(view)
    end
    

    …….

    1. Thanks!

      As for your question, I am not sure what the best approach would be. Maybe to bind functions or tables with Corona’s addEventListener() which pass off the data to Luvent. Something like:

      context.onCollision = Luvent.newEvent()
      
      function context:init()
          self.onCollision:addAction(…) -- Add the actual event logic here
          Runtime:addEventListener(&quot;onRobotlegsViewCreated&quot;, self)
          Runtime:addEventListener(&quot;onRobotlegsViewDestroyed&quot;, self)
      end
      
      function context:onRobotlegsViewCreated(event)
          luabinding:luaPrint(&quot;Context::onRobotlegsViewCreated&quot;)
          local view = event.target
          if view == nil then
              error(&quot;ERROR: Robotlegs Context received a create event, but no view instance in the event object.&quot;)
          end
      
          -- Call whatever actions are setup for this event.
          self.onCollision:trigger(event)
      
          self:createMediator(view)
      end
      

      However, since Corona provides its own event system I think it would be better to use only that unless there is some specific functionality in Luvent you want which Corona’s system does not provide. To use them together is (almost certainly) going to require writing dummy event handlers for Corona which do nothing but immediately hand-off event data to Luvent. So personally I would avoid that tedious extra step unless you really need to mix them together.

      I have not used Corona too much, but I knew it had its own event library. Now this has me thinking if I should tweak Luvent in a way to make it better work together with Corona, and by extension things like Robotlegs. So thanks again for the comment.

Add Your Thoughts

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s