Introduction
This tutorial discusses the methods used by Garry's Mod to define new objects classes from files. Though this tutorial focuses on application in GMod, the code and concepts can be used elsewhere.
What is an Object?
An object in programming is a collection of data which is organized and structured in a specific manner and is derived from a class. Examples of classes are Entities, Weapons (which are just fancy entities), and Panels. Things like Vectors and ConVars are also classes but they aren't created from files, which is the purpose of this tutorial.
Most classes have a baseclass, from which they inherit their properties. As more classes are created, a baseclass tree begins to form. An example of this is the HL2 "weapon_pistol", which is based off the "weapon" class, which is based off the "anim" class, which is based off the "entity" class. The weapon has all the properties of an entity, such as position and model, and it has its own special properties, such as clip size and damage.
This is an example of an inheritance hierarchy from a hypothetical system comprised of Weapons and Agents.
Objects are created using a constructor. A constructor basically makes a copy of the class and returns it as a new object. Examples of constructors in Garry's Mod are ents.Create, vgui.Create, and Vector.
Objects in Lua
Method 1: module
In Lua, there are two types of objects: those defined in C++ (called userdata) and those defined within Lua (special tables). In this case, we're going to stick with Lua defined objects.
Let's create a new class (we'll get to creating classes in separate files later).
local AirportClass
= {}
AirportClass.Name
= "Airport"
AirportClass.Code
= "ABC"
AirportClass.City
= "Cityville"
AirportClass.State
= "CA"
function Airport(code)
local newAirport
= table.
Copy(AirportClass)
if code
then
newAirport.Code
= code
end
return newAirport
end
local BWI
= Airport(
"BWI")
local LAX
= Airport(
"LAX")
local ORD
= Airport(
"ORD")
BWI.Name
= "Thurgood Marshall"
BWI.City
= "Baltimore"
BWI.State
= "MD"
LAX.Name
= "Los Angeles International Airport"
LAX.City
= "Los Angeles"
ORD.City
= "Chicago"
ORD.State
= "IL"
ORD.Name
= "O'Hare Airport"
That wasn't so hard right? Define a class, copy the class with a constructor, and modify it as needed. Let's move on to file-based classes.
In Garry's Mod, there are a few systems which use folders to define each class, such as SWEPs, effects, and SENTs. In essence, these systems follow these steps:
Find all classes to be defined. For each class folder/file:
- Create a table for the class to be defined into.
- Run the file(s).
- Save the class table somewhere for future use.
- Refresh existing objects of that class. (We won't do this in our tutorial.)
- Delete the class table. (But not the one we saved elsewhere. We use that later.)
So let's make inventory items with this system. The following goes in "lua/includes/modules/invitem.lua":
local string
= string
local table
= table
local error
= error
local Material
= Material
local baseclass
= baseclass
module(
"invitem")
local invitems
= invitems
or {}
local allitems
= allitems
or {}
invitems.item_baseitem
= {}
invitems.item_baseitem.Icon
= Material(
"vgui/items/baseweapon.png")
invitems.item_baseitem.Name
= "Base Item"
invitems.item_baseitem.Width
= 1
invitems.item_baseitem.Height
= 1
invitems.item_baseitem.Weight
= 1
invitems.item_baseitem.Owner
= NULL
invitems.item_baseitem.BaseClass
= {}
invitems.item_baseitem.UniqueID
= -1
function invitems.item_baseitem:
Init()
end
function invitems.item_baseitem:
Remove()
allitems
[self.id
] = nil
self:
OnRemove()
end
function invitems.item_baseitem:
OnRemove()
end
function invitems.item_baseitem:
GetSize()
return self.Width, self.Height
end
function invitems.item_baseitem:
GetIcon()
return self.Icon
end
baseclass.
Set(
"item_baseitem", invitems.item_baseitem)
function Register(classtbl, name)
name
= string.
lower(name)
baseclass.
Set( name, classtbl )
classtbl.BaseClass
= baseclass.
Get(classtbl.Base)
invitems
[ name
] = classtbl
end
function Create(class)
if not invitems
[class
] then error(
"Tried to create new inventory item from non-existant class: "..class)
end
local newItem
= table.
Copy(invitems
[class
])
local id
= table.
insert(allitems, newItem)
newItem.UniqueID
= id
newItem:
Init()
return newItem
end
function GetClasses()
return invitems
end
function GetClassTable(classname)
return invitems
[classname
]
end
function GetClassTableCopy(classname)
return table.
Copy(invitems
[classname
])
end
function GetAll()
return allitems
end
So now we have a module which registers our item classes, lets us create new item objects, allows us to view a given class on demand, and keeps track of all current item objects within the game. We also have a baseclass to base our new classes on.
In a new file, lua/autorun/inventory.lua, let's run all our files:
if SERVER
then
AddCSLuaFile()
AddCSLuaFile(
"../includes/modules/invitem.lua")
end
require(
"invitem")
local files,folders
= file.
Find(
"items/*",
"LUA")
for k,File
in pairs(files)
do
local name
= string.
sub(
File,1,string.
find(File,
"%.lua")
-1)
ITEM
= {}
ITEM.ClassName
= name
if SERVER
then
AddCSLuaFile(
"items/"..File)
end
include(
"items/"..File)
if not ITEM.Base
then ITEM.Base
= "item_baseitem" end
invitem.
Register(name,ITEM)
ITEM
= nil
end
for k,folder
in pairs(folders)
do
local name
= string.
sub(
folder,1,string.
find(folder,
"%.lua")
-1)
ITEM
= {}
ITEM.ClassName
= name
local dir
= "items/"..folder
.."/"
if SERVER
then
if file.
Exists(dir
.."shared.lua",
"LUA")
then
AddCSLuaFile(dir
.."shared.lua")
end
if file.
Exists(dir
.."cl_init.lua",
"LUA")
then
AddCSLuaFile(dir
.."cl_init.lua")
end
if file.
Exists(dir
.."init.lua",
"LUA")
then
include(dir
.."init.lua")
end
end
if file.
Exists(dir
.."shared.lua",
"LUA")
then
include(dir
.."shared.lua")
end
if CLIENT
then
if file.
Exists(dir
.."cl_init.lua",
"LUA")
then
include(dir
.."cl_init.lua")
end
end
if not ITEM.Base
then ITEM.Base
= "item_baseitem" end
invitem.
Register(name,ITEM)
ITEM
= nil
end
for classname,classtbl
in pairs(
invitem.
GetClasses())
do
table.
Inherit(classtbl,classtbl.BaseClass)
end
With that, your items should be automatically included, registered, and inherited.
You can download a working example of this system here. The code is untested but should work.
--[[User:Bobblehead|Bobblehead]] 00:01, 13 December 2014 (UTC)
Method 2: Metatables
Metatables are a comfortable tool for simulating object oriented programming because of the way they behave.
Objects with the same metatable will have access to the same metamethods and custom functions.
Lets begin by creating our metatable
Book = {}
Book.__index = Book
Now, we need a function that will return our object, most often called "new"
Book
= {}
Book.__index
= Book
function Book:
new( series, seriesNum, title, text )
local EmptyBook
= {
series
= series
or "",
seriesNum
= seriesNum
or 0,
title
= title
or "",
text
= text
or "",
}
setmetatable( EmptyBook, Book )
return EmptyBook
end
Now that we've got our base set up, lets add some functions
function Book:
Print()
if self:
IsEmpty()
then return end
print(
"\n====================================" )
if self.seriesNum
> 0 then
print( self.series
.. " " .. self.seriesNum
.. ": " .. self.title
.. "\n" )
else
print( self.title
.. "\n" )
end
print( self.text )
print(
"====================================\n")
end
function Book:
IsEmpty()
return #self.text
== 0
end
function Book:
SetSeries( series )
self.series
= series
end
function Book:
GetSeries()
return self.series
end
function Book:
SetSeriesNum( num )
self.seriesNum
= num
end
function Book:
GetSeriesNum()
return self.seriesNum
end
function Book:
SetTitle( title )
self.title
= title
end
function Book:
GetTitle()
return self.title
end
function Book:
AddText( text )
self.text
= self.text
.. text
end
function Book:
AddLine()
self.text
= self.text
.. "\n"
end
function Book:
RemoveLine()
if self:
IsEmpty()
then return end
local index
= string.
find( self.text,
"\n" )
if not index
then self.text
= "" return end
local lastIndex
= index
while index
do
lastIndex
= index
index
= string.
find( self.text,
"\n", index
+1 )
end
self.text
= string.
sub( self.text,
1, lastIndex
-1 )
end
Lastly, we'll make a Book() function which is identical to Book:new() using some metamagic
We have now successfully created our new simulated class - Book!
Book
= {}
Book.__index
= Book
function Book:
new( series, seriesNum, title, text )
local EmptyBook
= {
series
= series
or "",
seriesNum
= seriesNum
or 0,
title
= title
or "",
text
= text
or "",
}
setmetatable( EmptyBook, Book )
return EmptyBook
end
function Book:
Print()
if self:
IsEmpty()
then return end
print(
"\n====================================" )
if self.seriesNum
> 0 then
print( self.series
.. " " .. self.seriesNum
.. ": " .. self.title
.. "\n" )
else
print( self.title
.. "\n" )
end
print( self.text )
print(
"====================================\n")
end
function Book:
IsEmpty()
return #self.text
== 0
end
function Book:
SetSeries( series )
self.series
= series
end
function Book:
GetSeries()
return self.series
end
function Book:
SetSeriesNum( num )
self.seriesNum
= num
end
function Book:
GetSeriesNum()
return self.seriesNum
end
function Book:
SetTitle( title )
self.title
= title
end
function Book:
GetTitle()
return self.title
end
function Book:
AddText( text )
self.text
= self.text
.. text
end
function Book:
AddLine()
self.text
= self.text
.. "\n"
end
function Book:
RemoveLine()
if self:
IsEmpty()
then return end
local index
= string.
find( self.text,
"\n" )
if not index
then self.text
= "" return end
local lastIndex
= index
while index
do
lastIndex
= index
index
= string.
find( self.text,
"\n", index
+1 )
end
self.text
= string.
sub( self.text,
1, lastIndex
-1 )
end
setmetatable( Book,
{__call
= Book.new
} )
Here is an example code that utilizes the Book class
local myBook
= Book()
myBook:
SetSeries(
"My First Series" )
myBook:
SetSeriesNum(
1 )
myBook:
SetTitle(
"My First Book" )
myBook:
AddText(
"This is my very first book.\nThe End" )
myBook:
Print()
local myBook2
= Book(
"My First Series",
2,
"My Second Book",
"This is my second book.\nIt's a bit longer than the first one" )
myBook2:
Print()
myBook:
RemoveLine()
myBook:
AddLine()
myBook:
AddText(
"I changed it a bit" )
myBook:
AddLine()
myBook:
AddText(
"The End" )
myBook:
SetSeries(
"" )
myBook:
SetSeriesNum(
0 )
myBook:
Print()
Output: