Object Oriented Lua

From Garry's Mod
Jump to: navigation, search

Contents

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

--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:

book class output.png

Personal tools
Navigation