Object Oriented

I chose Lua for this project because it's really easy to work with and there's lots of precedent for it in the game dev industry.

However, I wanted to make each script as modular as possible. Initially I put everything is a global space and kind of copied the DOM tree model from Javascript. When an entity was instantiated from a map, its corresponding script would be run once. A global table called “Entities” would be create and each entity had a global entry on that with associated callbacks and hooks. I began to dislike this model because it felt like everything was global. I decided to use a more object oriented approach, which also allowed me to store some light userdata on each instantiation of an entity object for faster interaction between C and Lua.


I based what I did off of Lua's guide on Object Oriented Programming. In abstract, Lua's system is very similar to pre-ES6 Javascript closures. If you want a function in Lua to be publicly accessible outside of the module, then simply add that function to the Object you're exporting, similar to Javascripts this.FunctionName = function() { /* ... */ } Lua's is simply function Object:FunctionName() --[[ ... ]] end

Inheritance works similarly as well; just call Parent:New:

local Child = Parent:New()

function Child:OverriddenParentFunction ()
    --[[ etc. this will override the function of the same name in the parent ]]

So I created a base Object class that has a few Lua-to-C calls that get information about the entity and the world around it, such as GetX(), GetY(), Move, GetPlayerOrigin, etc. The two entry points for this class are Init and Think. Upon creation, Init is called once per entity, and each frame Think is called. The entire entity is a module, so it's important to return your class at the end of the file. A full example would be

local Object = require('Object')
local BadGuy = Object:New() -- Inherit Object's methods

function BadGuy:Init()
    self:SetModel('models/chars/badguy.md2') -- Set model
    self.Speed = 2 --[[Custom field for this class; can be
                       referenced outside the class as well]]

--Follow the player, slowly
function BadGuy:Think()
    local PlayerCoordinates = GetPlayerOrigin() -- Global API function
    local X = self:GetX() - PlayerCoordinates.X
    local Y = self:GetY() - PlayerCoordinates.Y

    --Calculate the angle that the player is in in relation to the entity
    local Direction = math.atan2(Y, X)
    --Quake does things in degrees
    local Degrees = Direction / math.pi * 180
    self:Move(Degrees, self.Speed)

return BadGuy

When the engine calls think, the entity will follow the player


To ensure that each Lua entity can call its C edict_t* quickly, the Object Lua entity type has a table inside of it called Entity. That Entity has all the Object-specific API functions on it and a userdata field called __userdata_entity. Each API call from Entity is defined in C, and the structure of each one basically goes like this:

1.) Lua Makes call to C, passes self 2.) C function grabs the Entity.__userdata_entity field from self and casts to edict_t* 3.) C grabs whatever data is needed from the edict_t (or makes whatever modifications) and returns it to Lua.

This is abstracted by the Object functions:

function Object GetX()
    return self.Entity:GetX()

So that every instantiation or child of Object can call self:GetX() and all other API methods specific to entities.

This part is quick because of the userdata pointer. Initial lookup, however, may be a bottleneck in the future.

Calling the Lua object

The Think function is called each frame. When Id-tech 2 (Quake 2's engine) “thinks”, it goes down a linked list of entities and reacts accordingly based on their edict_t.className. I've programmed it so that edict_ts with the className lua_entity call the lua_think.

When a lua_entity is created in C, the effect Lua call is basically

local Entity = {}
Entity = require('EntityName'):New(Entity) -- Pass empty table in for "self"
Entities[EntityId] = Entity

Where EntityId is a set or generated ID given to every entity on the edict_t

so in each think loop, the engine basically does Entities[EntityId]:Think(), so there's some string comparison overhead. I don't think that will matter much but I can always change it to a sequential ID generated upon creation for each Entity for faster lookup.