View Single Post
Old 05-16-2016, 02:32 PM   #45
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: Calgary, AB, Canada
Posts: 6,019
Default

8. Tables, classes, and methods

Note: This lesson refers to the LS GUI library provided in part 6. If you haven't downloaded those files yet, see this post: 6. Introducing the LS GUI library

Let's take a closer look at how everything in LS GUI is set up. I'm not going to have much code in this lesson, since it's all in the library file, so you may want to have both this post and the library open at the same time. For those with at least Windows 7 (I think), you can drag a window all the way to left or right edge of the screen and it will be resized to take up only that side - it's really handy.

Tables

(For those familiar with other programming languages, tables are Lua's version of arrays. Sort of. They have slightly different rules, and are often much more flexible.)

Tables, as the name suggests, are an easy way to store multiple things in one variable, perform operations on all of them, sort them, etc, while keeping things really tidy. Here's a good article covering how tables are created, how to fill them, and how to work with them: lua-users.org Table Tutorial

As you'll see, everything in LS GUI relies on tables. Seriously, the entire file is one big table - the first line of code is this:
Code:
local GUI = {}
and the last line is this:
Code:
return GUI
Everything we do in between - the color presets, the miscellaneous functions, the Main loop, and call of the GUI elements - is stored in that table:
Code:
local function do_stuff()
end
GUI.do_stuff = do_stuff
This creates a perfect copy of do_stuff within the table. By placing the entire library in a table and returning it to the parent script we accomplish two important things:

1. The parent script can use LS GUI's functions and access its variables.
2. Nothing in LS GUI will affect the parent script or be affected by it, so there are no questions of variable scope or having to write the parent script in a specific way to have everything work properly.

If we think of the parent script as a lady sitting at a desk, LS GUI is a coworker under her supervision. She says "draw me a button" and LS GUI just makes it happen, no fuss.

For comparison, if we remove the table and just place everything in the library at "top level"...
Code:
GUI.fonts --> fonts
GUI.mouse --> mouse
GUI.Main --> Main
GUI.Lbl --> Lbl
...those are common enough names for variables and functions that we'd probably interfere the parent script, so the user would to be very careful not to duplicate any of them. Even worse, anything the library does to mouse would happen to the parent script's mouse as well, so you're not only making the user do things your way but you're potentially screwing with their data at the same time.

In this case, LS GUI is more like one of those coworkers that's always using your stuff, never puts it back in the right place, spills coffee on an important report and then says "wow, you really screwed up this time" before walking away happily back to his own desk. Nobody likes that guy.

Classes

Declaring variables all the time gets really, really old, especially when all you want is five or six of basically the same thing. The same goes for writing functions in many languages, where working with normal variables might force you to write five of the same function to work with each of those five things.

If only there was a way to simplify things... oh wait, there is. It's known as "object-oriented programming" - rather than working simply with variables, we create objects that contain multiple variables. For instance, a person would contain variables like their name, height, weight, favorite shitty reality TV show, whether or not they've paid for their Reaper license, etc.

By creating what's known as a "class", we can build a prototype of one thing and then produce copies of it on-demand, each with their own name, rank, and serial number. As a result, rather than five functions doing the same thing, you just write it once and provide it the name of the object you'd like it to work on.

All of this fancypants stuff happens in the various :new() functions. Here's a simple one, for the Lbl element:

Code:
local Lbl = {}
Just like everything else in LS GUI, we'll start by making ourselves a table.

Code:
function Lbl:new(x, y, caption, shadow)
We'll call this function every time we want to create a new Lbl, giving it the values provided just like a rich parent giving their kid a Ferrari when they turn 16. Calling it Lbl:new rather than, say, new_label tells Lua to store the function inside Lbl{}.

The function's name uses that : to do another neat Lua trick. As I said above, functions written for use with objects from a class are always going to want the object's name. You could include the name as a parameter in all your functions if you wanted to:

Code:
function Lbl.new(lbl_name, x, y, caption, shadow)
...but the : serves as a shortcut to do the same thing. Lbl:new creates a function in Lbl{} that will automatically check the object's ID and store it in a variable called self. If you scroll down to some of the other functions in LS GUI, you'll see that self shows up all over the place. It's just a timesaver, really, like tapping your credit card on the machine rather than swiping it, typing in your PIN, waiting, etc.

The function then creates its own temporary table, label{} and stores the given parameters in it, along with a couple of other values that we'll need down the road.

It's worth noting what one line is doing:
Code:
label.shadow = shadow or 0
This is an easy way to see if shadow was specified, and setting it to 0 if it wasn't. I'm not sure exactly how it works, but it does, so you'll see it here and there in the library. It's a nice time-saver.

The magic that turns our wooden doll of a function into a real live boy is at the end:
Code:
   setmetatable(label, self)
   self.__index = self 
   return label
Again, my understanding of this bit is a little shaky, but here's a rough idea of what it's doing:

1. setmetatable tells label{}, to use self as its blueprint. Remember that self is standing in for Lbl{}.

2. Normally, if you look a value up in a table and there's nothing there Lua will return nil. __index tells the table to, instead, try looking in its metatable to see if there's an appropriate value there.

3. By having the function return label{}, which includes the metatable and __index stuff, we can create a variable, direct it to the :new function, and that variable will become a "child" of the "parent" object, inheriting all of its properties and functions:

Code:
my_new_label = Lbl:new(50, 50, "I'm a label", 0)
Because my_new_label is only a copy we can do things to it without affecting the parent, but if it needs help it can still phone up Mommy to ask "hey, so how do I do my own laundry?"

Methods

Moving on in the library file, you can see Lbl has the following functions:

Code:
:new()
:draw()
:onmousedown()
:onmouseup()
:ondoubleclick()
:ondrag()
:ontype()
Functions like these that are meant to work with a class of objects are known as "methods" of that class, and as their names suggest they're a list of things our sweet lil' Lbl has been taught how to do. Again, they're all stored in Lbl{}, so all of our child objects know how to use them, and because they all have that : in their names they'll have a self parameter that can be used to get information about the object that called them. i.e.
Code:
my_new_label:print_your_name()
would theoretically spit out
Code:
"Hi guys, I'm my_new_label. Does anyone here like Pokemon? I LOVE Pokemon. Guys? Hello?"
Lastly, we copy Lbl{} into GUI{}:
Code:
GUI.Lbl = Lbl
After all of that, my_new_label now exists with the values we specified in its blueprint, and has been sent off into the world with a little instruction booklet covering what to do if someone wants to draw it, or click on it, or type at it.

*sigh*....they grow up so fast.

Last edited by Lokasenna; 05-16-2016 at 02:40 PM.
Lokasenna is offline   Reply With Quote