Pivity

Lua Tips and Tricks (and Snippets)

Last read Never
#1

Heyo! Nice to see that there has been a new section added to the forum. I thought I'd kick off this subsection with a little Tips and Tricks thread so we can all spread our knowledge with our love for Lua. I saw someone (Maybe Luna?) do something like this for PHP a long while ago and I actually learned a handful of useful things, so thought "what the heck", I might as well contribute too in some way.

I encourage anyone and everyone to spit their mad Lua skillz in this thread. I'm always up for learning new methods, so why not share them? :P

Table Indexing

This is a pretty basic thing, but if you're learning Lua this may turn out to be really useful for you to know early. If your table is structured in a specific way, you can often check data through the index to save those valuable server resources. It is easier to explain in an example.

Example:

Bad method:

local coolJobs = {TEAM_CITIZEN, TEAM_CP, TEAM_MAYOR} -- Set the jobs that are considered cool
if table.HasValue(coolJobs, ply:Team()) then -- Check if the users team is in the table
  print("This man is so cool!") -- Oh hey, you're cool!
end

Good method:

local coolJobs = {}
coolJobs[TEAM_CITIZEN] = true -- Set the teams as the table key
coolJobs[TEAM_CP] = true
coolJobs[TEAM_MAYOR] = true

if coolJobs[ply:Team()] then -- Index the users job against the table
  print("This man is so cool!") -- Oh hey, you're cool!
end


Conditions when setting vars/args

There is probably some fancy name for this but I'm not aware of what it is, so if there is one please let me know :P

I only just recently started using this in my day to day coding, but it's something I should have been using for a long while. You can use advanced conditions when you're setting variables. For example, you may know about the or operator. Well you can take that a step further as shown in the examples

Example:

Bad method:

local plyName = "Unknown"

if IsValid(ply) then
  plyName = ply:Name()
end

Good method:

local plyName = (IsValid(ply) and ply:Name()) or "Unknown"


Prevent deep if embedding

Something that I did all the time when I was learning is find myself in deep if statements. With functions I'd stack if statements instead of listing them. It can be much easier to read code when it's neatly laid out and not heavily embedded.

Example:

Bad method:

function hasClout(ply)
  if IsValid(ply) then -- Is real entity
    if ply:IsPlayer() then -- Is a player
      if ply:Team() == TEAM_CLOUT then -- Is on that clout job
        if ply:Name() == "Owain" then -- Has got a cool name (Owain > Owen)
          print("This guy has clout!") -- You're a clout god
        end
      end
    end
  end
end

Good method:

function hasClout(ply)
  -- Not a real entity
  if not IsValid(ply) then return end
  -- Not a player
  if not ply:IsPlayer() then return end
  -- Not on that clout job
  if not (ply:Team() == TEAM_CLOUT) then return end
  -- Not got a cool name
  if not (ply:Name() == "Owain") then return end
  -- A true clout king
  print("This guy has clout!")
end


DistToSqr over Distance

When checking distances, in most cases you can use :DistToSqr. It is much faster and a better optimization method than using :Distance. It can be easy habit to just use :Distance, but you should be using :DistToSqr as much as possible. Keep in mind that the value it returns is much higher than :Distance, so it will take a little fiddling to find a nice value that works well for you. But it should be used in most cases over :Distance.


That seems about all I can think of off the top of my head. Again, I encourage you to share your best practice methods here too. People can't learn if you're not willing to teach them! Please also don't be a cunt in the replies, gotta spread the love!

Winner x 15Agree x 1Friendly x 1Informative x 1
#2

Circles Functions

Here is my attempt at circle functions, arc functions and segment functions. I'm not very sure how good these functions are in terms of efficiency compared to what's already out there but I'll post this anyway. If for whatever reason my functions are inefficient compared to perhaps bobbleheads functions please let me know why and post their functions (with permission) for people to use over mine.


function draw.drawCircle(x, y, r, step, cache)
    local positions = {}


    for i = 0, 360, step do
        table.insert(positions, {
            x = x + math.cos(math.rad(i)) * r,
            y = y + math.sin(math.rad(i)) * r
        })
    end


    return (cache and positions) or surface.DrawPoly(positions)
end

This is a bog standard circle, nothing too different from the one on the wiki but this has the ability to be cached to avoid doing calculations every frame when possible although that would only work for static elements. For dynamic elements that need to be recalculated every second, cache should be set to false and the function should be called within the drawing function.





function draw.drawArc(x, y, r, startAng, endAng, step ,cache)
    local positions = {}


    positions[1] = {
        x = x,
        y = y
    }


    for i = startAng - 90, endAng - 90, step do
        table.insert(positions, {
            x = x + math.cos(math.rad(i)) * r,
            y = y + math.sin(math.rad(i)) * r
        })
    end


    return (cache and positions) or surface.DrawPoly(positions)
end

This is a simple rendition of an arc, there aren't many checks inside the function so it might be error prone if you're using it incorrectly. Same thing with the cache as before. Side note, startAng always needs to be smaller than endAng.




function draw.drawSubSection(x, y, r, r2, startAng, endAng, step, cache)
    local positions = {}
    local inner = {}
    local outer = {}


    r2 = r+r2
    startAng = startAng or 0
    endAng = endAng or 0


    for i = startAng - 90, endAng - 90, step do
        table.insert(inner, {
            x = math.ceil(x + math.cos(math.rad(i)) * r2),
            y = math.ceil(y + math.sin(math.rad(i)) * r2)
        })
    end


    for i = startAng - 90, endAng - 90, step do
        table.insert(outer, {
            x = math.ceil(x + math.cos(math.rad(i)) * r),
            y = math.ceil(y + math.sin(math.rad(i)) * r)
        })
    end


    for i = 1, #inner * 2 do
        local outPoints = outer[math.floor(i / 2) + 1]
        local inPoints = inner[math.floor((i + 1) / 2) + 1]
        local otherPoints


        if i % 2 == 0 then
            otherPoints = outer[math.floor((i + 1) / 2)]
        else
            otherPoints = inner[math.floor((i + 1) / 2)]
        end


        table.insert(positions, {outPoints, otherPoints, inPoints})
    end


    if cache then
        return positions
    else
        for k,v in pairs(positions) do 
            surface.DrawPoly(v)
        end
    end
end

This is what I call a sub-section, not sure what they're really called. Same thing with cache as before.




Here's a little example of how to draw things after caching them as it isn't really immediately obvious.

I 100% recommending using the cache feature as much as you can as doing all these calculations every frame can injure FPS.




local circle = draw.drawCircle(ScrW()/2,ScrH()/2,300,3,true)

local arc1 = draw.drawArc(ScrW()/2,ScrH()/2,300,90,180,3,true)
local arc2 = draw.drawArc(ScrW()/2,ScrH()/2,300,270,360,3,true)


local subsection1 = draw.drawSubSection(ScrW()/2, ScrH()/2, 350, 25, 0, 90, 3, true)
local subsection2 = draw.drawSubSection(ScrW()/2, ScrH()/2, 350, 25, 180, 270, 3, true)
local subsection3 = draw.drawSubSection(ScrW()/2, ScrH()/2, 350, 25, 90,180, 3, true)
local subsection4 = draw.drawSubSection(ScrW()/2, ScrH()/2, 350, 25, 270, 360, 3, true)


hook.Add("HUDPaint","drawing_functions",function()
    draw.NoTexture()
    surface.SetDrawColor(200,100,100,230)
    surface.DrawPoly(circle)
    
    for k,v in pairs(subsection3) do 
        surface.DrawPoly(v)
    end


    for k,v in pairs(subsection4) do 
        surface.DrawPoly(v)
    end


    surface.SetDrawColor(30,30,30,230)
    draw.NoTexture()
    surface.DrawPoly(arc1)
    surface.DrawPoly(arc2)


    for k,v in pairs(subsection1) do 
        surface.DrawPoly(v)
    end


    for k,v in pairs(subsection2) do 
        surface.DrawPoly(v)
    end
end)



Winner x 6Informative x 5Agree x 2Creative x 2
#3

Optimising Materials and Colors in HUD/rendering hooks

Example of badness:

function SWEP:DrawHUD(w,h)
surface.SetDrawColor(Color(255,0,0))
surface.DrawRect(0,0,w,h)

surface.SetMaterial(Material("xd.vtf"))
surface.DrawTexturedRect(0,0,w,h)
end

Example of goodness:

local col_red = Color(255,0,0)
local xd_mat  = Material("xd.vtf")

function SWEP:DrawHUD(w,h)
surface.SetDrawColor(col_red)
surface.DrawRect(0,0,w,h)

surface.SetMaterial(xd_mat)
surface.DrawTexturedRect(0,0,w,h)
end

Yeah but why

Because DrawHUD is called every time the game renders a frame; therefore, in the badness example you are creating a new Color object and a new Material object every frame, which fills up memory, slows down framerate, and hurts the garbage collector's performance.

Winner x 9Agree x 2Funny x 2
#4

If you ever find yourself looping through ply.GetAll() or ents.GetAll() when trying to find an entity of a certain class or with a certain attribute, try keeping them in your own table by putting them into your table on spawn/initialise to reduce the amount of checks that have to be made. For example lets say I want to check whether or not a player is near all entities with the class 'health_station' instead of doing

local isNextToHealthStation = false
for k,v in pairs(ents.GetAll()) do
if v:GetClass() == "health_station" and LocalPlayer():DistToSqr(v:GetPos()) < 25000 then
isNextToHealthStation = true
break
end
end

which loops through all the entities on the map (ents.FindByClass() does the same thing)


you should add the entity to your own table when it is created, for example

function ENT:Initialize()
bens_health_stations.stations[self] = true -- Add the station too our table
-- We use self as the key so we don't have to use a loop to remove it
end

function ENT:OnRemove()
bens_health_stations.stations[self] = nil -- Remove the station from our table, it no longer exists.
end

and then to loop through it you do the same thing but with our new table

local isNextToHealthStation = false
for k,v in pairs(bens_health_stations.stations) do
if k:GetClass() == "health_station" and LocalPlayer():DistToSqr(k:GetPos()) < 25000 then
isNextToHealthStation = true
break
end
end


Yeah but why

A simple flatgrass world with nothing in it can still have around 90 items in the ents.GetAll() table, now if you have a server with 64 players all building with a small prop limit of 20 and there are three health stations around the map, that would be up too 1390 items that have to be checked per player meaning there would be 88,960 checks done as a worst-case scenario assuming there's nothing else on the map. Looping through the 1390 items is very expensive and inefficient, especially if you're doing it every frame or every tick. However if you use the other method and store your items in their own table, you'd only have to check through three items per player meaning you've gone from 88,960 checks to a measly 192 checks which can be a lot better for both clients and servers performance.

Winner x 9
#5
Conditions when setting vars/args
There is probably some fancy name for this but I'm not aware of what it is, so if there is one please let me know :P


This is called a ternary operator, and there's not a native one in Lua. Using it with a && b || c "fakes" it because of how the operator coalescing works, but it can behave a bit finicky when b is false and a is true - it will give you c. To solve this, GLua provides you a bit of a lengthier Either(a, b, c) function that works properly without the aforementioned issue. You can also just inline an if statement in an immediately-invoking function expression like so, but you generally shouldn't do this unless you have a reason for it: (function() if(a) then return b else return c end end)()

Informative x 3Agree x 1Winner x 1
#6

Stickied the thread.

Will probably also use it for the new knowledge base category Script Submissions.

Winner x 7Friendly x 1
#7

DONT USE PAIRS ON SEQUENTIAL TABLES!!!!!!! @Ben

Dumb x 7
#8

Use re-usable components!

Recently I've noticed a lot of people still use default Derma panels and at best override their paint. To fix this problem you can either use a ui library like Pixel Ui and Nexus Library or you can create your own components and use them.


This is a panel I made as an example, the comments should explain everything.

local PANEL = {}


// This can be used to change the default values of the panel

AccessorFunc(PANEL, "Bool", "Bool", FORCE_BOOL)

AccessorFunc(PANEL, "String", "String", FORCE_STRING)


function PANEL:Init()

    // This is called when the panel is created

    // use this to create base elements and variables


    self:SetBool(false)

    self:SetString("This is the accessor function from above")


    print(self:GetBool(), self:GetString())


    self.examplePanel = self:Add("DPanel")

    self.examplePanel:Dock(TOP)

    self.examplePanel:DockMargin(5, 5, 5, 5)

    self.examplePanel.Paint = function(pnl, w, h)

        draw.RoundedBox(6, 0, 0, w, h, Color(40, 42, 49))

    end

   

end


function PANEL:Think()

    // This is called every frame

end


function PANEL:PerformLayout(w, h)

    // This is called when the panel is resized


    self.examplePanel:SetTall(50)

end


function PANEL:Paint(w, h)

    // This overrides the default paint function (only of the main panel)

    draw.RoundedBox(6, 0, 0, w, h, Color(24, 25, 29))

end


vgui.Register("ExamplePanel", PANEL, "DPanel") // ExamplePanel is the name of the panel, PANEL is the table we declared above and DPanel is the base panel we are inheriting from


// Example usage

// Note you can use this element in any other panel as much times as you want

concommand.Add("open_example_panel", function()

    local frame = vgui.Create("DFrame")

    frame:SetTitle("Example Frame")

    frame:SetSize(300, 200)

    frame:Center()

    frame:MakePopup()


    local examplePanel = vgui.Create("ExamplePanel", frame)

    examplePanel:Dock(FILL)


    print(examplePanel:GetBool(), examplePanel:GetString())

    examplePanel:SetBool(true)

    examplePanel:SetString("Showing that the accessor functions can be accesed and changed here")

    print(examplePanel:GetBool(), examplePanel:GetString())

end)


How it looks when opened:



I hope this was helpful, but if you still don't understand something here is a wonderful video made by xenin on the topic:

https://youtu.be/aChXeLsLtfQ?list=PLxIHL2TEXiCeFgCo2ormzzd9cw5xWF_Gq

Dumb x 1
Users viewing this thread
Nobody