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.
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.
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")
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:
We create an object to represent each event.
We associate actions to events, functions we want to run every time we trigger the event.
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.