Garry's Mod Wiki

Object Oriented Lua

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).

--Define our class. We're going to create airport objects. --We are going to preset the defaults for some of the required properties of all new airport objects made. local AirportClass = {} AirportClass.Name = "Airport" AirportClass.Code = "ABC" AirportClass.City = "Cityville" AirportClass.State = "CA" --[[So now we have a basic table which represents a non-existant airport which we will derive our airports objects from. Let's make a constructor so we can start creating airport objects. A basic constructor is as follows:]] function Airport(code) --Code is an optional argument. local newAirport = table.Copy(AirportClass) --table.Copy is a Garry's Mod function. Look for it in the source code should you need to replicate it in a different API. --Override the old default Code property should we have a new code to replace it with: if code then newAirport.Code = code end --Return our new Object. return newAirport end --Now we're ready to create some Airport objects! Let's start by defining a few... local BWI = Airport("BWI") local LAX = Airport("LAX") local ORD = Airport("ORD") --Okay, they exist now. --BUT WAIT! Our new airports are the same as the original BaseAirport class! --This is unacceptable. Let's go ahead and change the specifics of our airports. BWI.Name = "Thurgood Marshall" BWI.City = "Baltimore" BWI.State = "MD" LAX.Name = "Los Angeles International Airport" LAX.City = "Los Angeles" -- LAX.State = "CA" -- The state is "CA" by default, so we don't need to change it here. 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:

  1. Create a table for the class to be defined into.
  2. Run the file(s).
  3. Save the class table somewhere for future use.
  4. Refresh existing objects of that class. (We won't do this in our tutorial.)
  5. 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") --Create a list of all inventory items. local invitems = invitems or {} local allitems = allitems or {} --Create our root baseclass, with all items are based off somewhere down the line. 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 --We set this when we create the object. function invitems.item_baseitem:Init() end function invitems.item_baseitem:Remove() allitems[self.id] = nil self:OnRemove() end function invitems.item_baseitem:OnRemove() --This is the function we can override per-class. end -- These two functions are for the Grid-Based Inventory Tutorial, which this system is compatable with. function invitems.item_baseitem:GetSize() return self.Width, self.Height end function invitems.item_baseitem:GetIcon() return self.Icon end --baseclass.Set is a GMod function. See lua/includes/modules/baseclass.lua baseclass.Set("item_baseitem", invitems.item_baseitem) --Saves a class to our internal list of items, and defines our class's baseclass. function Register(classtbl, name) name = string.lower(name) baseclass.Set( name, classtbl ) classtbl.BaseClass = baseclass.Get(classtbl.Base) invitems[ name ] = classtbl end --Our constructor, which takes an argument to determine the class. function Create(class) --Prevent non-existant classes from being created. if not invitems[class] then error("Tried to create new inventory item from non-existant class: "..class) end local newItem = table.Copy(invitems[class]) --Add our new object to the list of all items currently in the game. local id = table.insert(allitems, newItem) --Give it a unique ID. newItem.UniqueID = id --Call our Init function when we create the new item. newItem:Init() return newItem end --Returns a table of all classes. function GetClasses() return invitems end --Returns the class table of a given class from our saved list. function GetClassTable(classname) return invitems[classname] end --Returns a COPY of the class table, so we don't modify the original. function GetClassTableCopy(classname) return table.Copy(invitems[classname]) end --Returns a list of all current items objects. 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()--Only needed on Garry's Mod. AddCSLuaFile("../includes/modules/invitem.lua") end --Run our module: require("invitem") --Let's run our class files: --Get a list of all files and folders in our classes folder. --file.Find is a Garry's Mod function. See the wiki if you want to remake it. local files,folders = file.Find("items/*", "LUA") --Store our files in lua/items/ --Consider any solo files to be shared, such as "item_dildo.lua" for k,File in pairs(files)do local name = string.sub(File,1,string.find(File,"%.lua")-1) --Create our class table, which the files write to. ITEM = {} --Set our ClassName for future reference. ITEM.ClassName = name if SERVER then AddCSLuaFile("items/"..File) end include("items/"..File) if not ITEM.Base then ITEM.Base = "item_baseitem" end --Register the class table. invitem.Register(name,ITEM) --Delete the class table. ITEM = nil end --Include each file, e.g. init.lua, shared.lua, and cl_init.lua, in their respective domains. for k,folder in pairs(folders)do local name = string.sub(folder,1,string.find(folder,"%.lua")-1) --Create our class table, which the files write to. ITEM = {} --Set our ClassName for future reference. ITEM.ClassName = name --Include all of our files for this item. local dir = "items/"..folder.."/" if SERVER then --file.Exists is a Garry's Mod function. See the wiki if you want to remake it. 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 --Register the class table. invitem.Register(name,ITEM) --Delete the class table. ITEM = nil end --Now that we've included all our class files, let's make them inherit their baseclasses. for classname,classtbl in pairs(invitem.GetClasses())do --table.Inherit replaces nil values from one table with non-nil values from another table. See lua/includes/extensions/table.lua 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 = {} -- This is our metatable. It will represent our "Class" Book.__index = Book --If a key cannot be found in a table, it will look in it's metatable's __index. --This means any function we define for the 'Book' table will be accessible by any object whose metatable is 'Book'

Now, we need a function that will return our object, most often called "new"

Book = {} -- This is our metatable. It will represent our "Class" Book.__index = Book function Book:new( series, seriesNum, title, text ) -- Variables are optional -- This is the table we will return local EmptyBook = { series = series or "", seriesNum = seriesNum or 0, title = title or "", text = text or "", } setmetatable( EmptyBook, Book ) -- Set the metatable of 'EmptyBook' to 'Book' return EmptyBook -- Return the 'EmptyBook' table, whose metatable is 'Book'. This is our object --[[ More often than not, the table will be set inside the function itself, like so: return setmetatable( { series = series or "", seriesNum = seriesNum or 0, title = title or "", text = text or "", }, Book ) ]] end

Now that we've got our base set up, lets add some functions

-- First, a function to print our Book function Book:Print() if self:IsEmpty() then return end -- No point in printing an empty book print( "\n====================================" ) -- Lets make it clear when our print starts if self.seriesNum > 0 then -- Only print the series name if it's part of a series (And thus seriesNum > 0) print( self.series .. " " .. self.seriesNum .. ": " .. self.title .. "\n" ) -- Nice format else print( self.title .. "\n" ) -- Otherwise, we can just print the title end print( self.text ) -- And of course, print the text print( "====================================\n") -- Lets make it clear when our print ends end -- We will now define some more functions, these are just the ones I felt like making 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 -- Remove the last line of text function Book:RemoveLine() if self:IsEmpty() then return end -- Can't remove a line if there's no text local index = string.find( self.text, "\n" ) if not index then self.text = "" return end -- If there's only 1 line, set the text to nothing 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

setmetatable( Book, {__call = Book.new } )

We have now successfully created our new simulated class - Book!

Book = {} -- This is our metatable. It will represent our "Class" 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 -- No point in printing an empty book 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

-- Option #1: Create an empty book then set its data with our defined metafunctions local myBook = Book() myBook:SetSeries( "My First Series" ) myBook:SetSeriesNum( 1 ) -- First book in the series myBook:SetTitle( "My First Book" ) myBook:AddText( "This is my very first book.\nThe End" ) myBook:Print() -- Option #2: Create a new book with all (or some) of its data 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: