Revision Difference
Object_Oriented_Lua#518607
<cat>Dev.Lua</cat>
# 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.
<image src="class-diagram-methods.png"/>
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 <page>ents.Create</page>, <page>vgui.Create</page>, and <page>Global.Vector</page>.
# 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).
```
--<page>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.</page>
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")
--<page>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.</page>
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.
1. Run the file(s).
1. Save the class table somewhere for future use.
1. Refresh existing objects of that class. (We won't do this in our tutorial.)
1. 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](https://www.dropbox.com/s/cn1w46q6oxmp05u/inventory.zip?dl=1). 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
--<page>
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'
</page>
```
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)
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
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**:
<image src="book_class_output.png"/>