Garry's Mod Wiki

Revision Difference

Object_Oriented_Lua#510451

<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.⤶ ⤶ ⤶ 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.⤶ ⤶ -- 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 &gt; 0 then -- Only print the series name if it's part of a series (And thus seriesNum &gt; 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 &gt; 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**:⤶ ⤶ ⤶ ⤶ ⤶ ⤶