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.
Objects
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 ]]
end
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]]
end
--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:Look(Degrees)
self:Move(Degrees, self.Speed)
end
return BadGuy
When the engine calls think
, the entity will follow the player
Userdata
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()
end
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_t
s 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
Entity:Init()
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.