JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Variables, Tables, Objects, Classes

May 27, 2025 • [designluaobjectstestingtutorial]


This morning I plan to show, in small steps, how and why we move toward objects and classes in SLua and other languages that support the object notion. Wish me luck.

Overview

All that we have available in LSL is variables and lists. We do have a few variable types that are in fact what I’d call objects: the vector is one example. A vector in LSL behaves a bit like a number, in the sense that you can add one vector to another with +:

vector where = <100, 100, 100>;
vector move = <10, 20, 30>;
vector where_now = where + move;
// where_now is <110, 120, 130>

But of course a vector really is a lot like a number, being made up of three numbers, which are each processed separately as we perform operations on vectors. And while adding two vectors is just adding the individual coordinates, other operations, such as multiplying a vector times a rotation, are quite different from ordinary arithmetic.

Still, a vector is a perfectly good example of an object: a bit of computer stuff that contains one or more other things. In the case of the vector, it contains the x, y, and z values of the vector. And an object typically “understands” how to perform certain operations, which can be specified by operators like +, or by functions that we call methods of the object. LSL does not have methods on its objects. As we’ll see, SLua does.

In what follows, we’ll “solve” the same problem using variables, arrays, tables, objects, and classes, working up through the chain.

What is the problem? Good question.

The Problem

Suppose we are writing an SL object that follows a path. We’ll not concern ourselves with the details of motion, just looking at a simple way of describing a path, and a simple way of accessing its information.

Our user wants to express a path as a series of moves, a count of the number of steps, and a vector indicating the direction and amount of motion, like this:

count = 5, direction = <0.25, 0, 0>
count = 10, direction = <0.1, 0.1, 0>
...

Our object moves from time to time and keeps a sort of cursor indicating where it is in the series. We may have done 3 of the 5 steps in the first item, then we do the 4th, then the 5th, then the first step of the second item.

There is, of course, a glitch: when the object gets to the end of the list, if it has not returned to where it started from, we want it to return there.

Our Test

We’ll write a test, and then a function that produces the sequence of positions we need. We’re assuming for this version that all the variables are at the top level of our script, as they might be in LSL.

Here is my test. I used integer steps for convenience:

function Tests:test_stepper()
   base_pos = vector(0,0,0)
   current_pos = base_pos
   local s1 = {count=3, direction=vector(1,0,0)}
   local s2 = {count=2, direction=vector(0,1,0)}
   steps = {s1, s2}
   step_index=1
   move_index=1
   local results = {
      vector(1,0,0), vector(2,0,0),vector(3,0,0),
      vector(3,1,0), vector(3,2,0),
      vector(0,0,0)
   }
   for i, v in ipairs(results) do
      next = next_position()
      self:assert_equals(next, v, `index {i}`)
   end
   -- should repeat
   for i, v in ipairs(results) do
      next = next_position()
      self:assert_equals(next, v, `index {i}`)
   end
end

Top-Level Variables

And here is the code that passes the test, using variables that are open to the entire script.

local steps
local base_pos
local current_pos
local step_index
local move_index

local function next_position()
   if step_index <= #steps then
      current_pos += steps[step_index].direction
      move_index += 1
      if move_index > steps[step_index].count then
         move_index = 1
         step_index += 1
      end
      return current_pos
   else
      step_index = 1
      move_index = 1
      current_pos = base_pos
      return base_pos
   end
end

Assessment

Coded as we usually would in LSL, or as we might, by habit, do in SLua, this function requires five variables at the top level. If we were going to move two such prims, we would additionally need some kind of separate arrangement, perhaps a strided list, which we would use to prime these five variables with the data for the prim we wanted to move.

(There are other possibilities, of course, but suffice it to say that if we have very many things to do that are like this one, the code tends to get littered with global variables in LSL and, in SLua, variables visible to the whole script.)

Using a Table

We can improve the situation quite a bit by using a table to contain all the information for a given mover, like this:

local step_info = {}

local function next_position()
   if step_info.step_index <= #step_info.steps then
      step_info.current_pos += step_info.steps[step_info.step_index].direction
      step_info.move_index += 1
      if step_info.move_index > step_info.steps[step_info.step_index].count then
         step_info.move_index = 1
         step_info.step_index += 1
      end
      return step_info.current_pos
   else
      step_info.step_index = 1
      step_info.move_index = 1
      step_info.current_pos = step_info.base_pos
      return step_info.current_pos
   end
end

We did have to update the test to do this, and the update was a bit tricky. It looks like this now:

function Tests:test_stepper()
   step_info.base_pos = vector(0,0,0)
   step_info.current_pos = step_info.base_pos
   local s1 = {count=3, direction=vector(1,0,0)}
   local s2 = {count=2, direction=vector(0,1,0)}
   step_info.steps = {s1, s2}
   step_info.step_index=1
   step_info.move_index=1
   local results = {
      vector(1,0,0), vector(2,0,0),vector(3,0,0),
      vector(3,1,0), vector(3,2,0),
      vector(0,0,0)
   }
   for i, v in ipairs(results) do
      next = next_position()
      self:assert_equals(next, v, `index {i}`)
   end
   -- should repeat
   for i, v in ipairs(results) do
      next = next_position()
      self:assert_equals(next, v, `index {i}`)
   end
end

It’s tricky because SLua will be perfectly happy to let you say, which I did:

   step_info.base_pos = vector(0,0,0)
   step_info.current_pos = base_pos

That compiles just fine, and we get a run-time error because base_pos is nil because SLua doesn’t find the variable anywhere in the program.

Assessment

The code in the function is pretty nasty, but I think a lot of the issue is the use of the name step_info. If we pass the table into the function we can make it look better:

local function next_position(data)
   if data.step_index <= #data.steps then
      data.current_pos += data.steps[data.step_index].direction
      data.move_index += 1
      if data.move_index > data.steps[data.step_index].count then
         data.move_index = 1
         data.step_index += 1
      end
      return data.current_pos
   else
      data.step_index = 1
      data.move_index = 1
      data.current_pos = data.base_pos
      return data.current_pos
   end
end

That’s pretty good, and even better, if now we have multiple things to move we can give each one its own table and they can all be used independently.

We could do something similar in LSL, passing a list or something to the function, but it would be really messy with all the llList2ThisOrThat calls. We probably wouldn’t go that way.

So part of our assessment so far surely is “SLua tables can make code more convenient than LSL”. Faint praise, perhaps, but praise.

Object: a Table with functions.

This section represents something one should probably know how to do, but I myself would likely never do it. Instead, I would do what’s in a following section, namely create a class, even if I only ever wanted one instance. We’ll discuss that below. We can make a small improvement to this code by making our next_position function into an element of the data table.

Warning
What we’re about to do can lead to trouble, so if your initial reaction is that this isn’t the best way, you’re right. We’ll see the better way below.

We can put the function next_position right into the step_info table, like this:

local step_info = {}

step_info.next_position = function(data)
   if data.step_index <= #data.steps then
      data.current_pos += data.steps[data.step_index].direction
      data.move_index += 1
      if data.move_index > data.steps[data.step_index].count then
         data.move_index = 1
         data.step_index += 1
      end
      return data.current_pos
   else
      data.step_index = 1
      data.move_index = 1
      data.current_pos = data.base_pos
      return data.current_pos
   end
end

And we need to change our test lines that call the next_position function, to this:

next = step_info:next_position()

There are two aspects to the above that need to be pointed out. First, the next_position function needs a parameter pointing to the data table, which happens to be the table that also contains the next_position function.

Second, for our test to find the function, it needs to refer to the step_info table to find it. We could have said:

next = step_info.next_position(step_info)

But the colon notation actually means “and, by the way, pass what’s in front of me to the function as an argument”.

Assessment

Methods

Functions inside objects, like our next_position function, are commonly called methods.

What one might like about this is that now the main name space of the program is cluttered only by the single table variable step_info. All the other names, next_position, current_pos, and so on, are hiding inside the table, so we need not be quite so careful as we code other aspects of the script.

One concern, alluded to above, is “Yabbut what if the table already has a key named ‘next_position’, this won’t work!” And yes, that is correct.

The more nearly correct thing to do, if we really wanted to build a single-instance object, would be to put any functions into the object’s metatable and set the object up to use that metatable.

I prefer not to think about that, and much prefer not to do it manually. It is not attractive code and it is easy to get wrong. So I’m including what we just did in the “Object a Table with Functions” section as a thing to understand but generally not to do.

OK, you might do it to learn. I would suggest that our final step, Object via class() is the way to go.

But first, the initialization of our table in the test is rather messy. Let’s provide a handy init method.

Object with an init

Let’s begin by noticing that the setup in our test is rather messy:

function Tests:test_stepper()
   step_info.base_pos = vector(0,0,0)
   step_info.current_pos = step_info.base_pos
   local s1 = {count=3, direction=vector(1,0,0)}
   local s2 = {count=2, direction=vector(0,1,0)}
   step_info.steps = {s1, s2}
   step_info.step_index=1
   step_info.move_index=1

Our test really does want to define the table with the two steps in it. It probably wants to define the starting base location, which we set for testing purposes to vector(0,0,0). It doesn’t want to know about the current_pos, and it certainly has no business knowing how to set up the step_index and move_index items.

So let’s provide another method in our table, to do the initialization, given a table of steps and a base location:

step_info.init = function(data, base, steps)
   data.base_pos = base
   data.current_pos = base
   data.steps = steps
   data.move_index=1
   data.step_index = 1
end

And we use init like this:

function Tests:test_stepper()
   local s1 = {count=3, direction=vector(1,0,0)}
   local s2 = {count=2, direction=vector(0,1,0)}
   step_info:init(vector(0,0,0), {s1, s2})
   local results = {
      vector(1,0,0), vector(2,0,0),vector(3,0,0),
      vector(3,1,0), vector(3,2,0),
      vector(0,0,0)
   }
   for i, v in ipairs(results) do
     ...

Assessment

Adding the init method is convenient and allows the table step_info to manage its own internal affairs, while allowing its users to concern themselves only with what they care about, the base location and the steps to be followed.

I think I’d argue that this arrangement of our step_info table is easier to use, but rather obscure and tricky to set up. Which leads us to my preferred way.

Object via class()

My SLua framework includes a function named class(), and for our present situation we would use it like this:

local Stepper = class()

function Stepper:init(base, steps)
   self.base_pos = base
   self.current_pos = base
   self.steps = steps
   self.move_index=1
   self.step_index = 1
end

function Stepper:next_position()
   if self.step_index <= #self.steps then
      self.current_pos += self.steps[self.step_index].direction
      self.move_index += 1
      if self.move_index > self.steps[self.step_index].count then
         self.move_index = 1
         self.step_index += 1
      end
      return self.current_pos
   else
      self.step_index = 1
      self.move_index = 1
      self.current_pos = self.base_pos
      return self.current_pos
   end
end

This is almost exactly like what we had before, except that we define Stepper as a class, and use self where we had data before. And, notably, we define the functions using Stepper:, which means that they are automatically defined by SLua to have the variable self set up to be the table object used in the calls to the function. So in our test, our only change is this:

function Tests:test_stepper()
   local s1 = {count=3, direction=vector(1,0,0)}
   local s2 = {count=2, direction=vector(0,1,0)}
   step_info = Stepper(vector(0,0,0), {s1, s2})

Notice that we put the parameters for the init directly after th class name, as if we sere calling a function named Stepper that calls Stepper:init. (And that is exactly what is going on behind the curtain, by the way.)

This says “make step_info an instance of the class Stepper, with base at <0,0,0> and these steps”.

In the references to next_position we make no change from last time. They are still:

next = step_info:next_position()

Assessment

With the previous setup, the object with an init and next_position function, we had everything encapsulated in step_info. If we wanted to move two things, we would have had to set up a second table like the step_info table, including adding those functions to it.

By simply saying Stepper=class(), we define Stepper to be a thing such that when we say Stepper(vector(0,0,0),{s1,s2}), we get back a table that behaves just like step_info, but we can have as many as we wish.

Furthermore, the way we define our classes will always be just the same:

  1. Give the class a name. I prefer capitalizing the first letter, but you do you. Stepper=class().
  2. Define an init like Stepper:init to take whatever initial values are needed and store them into selfmembers.
  3. For every function like next_position needed in your object, define a function like Stepper:next_position().
  4. Create instances with my_stepper=Stepper(base, tab).
  5. Enjoy the glow.

My Personal Coding Approach

Naturally, I think that what I do is really good and that everyone should do as I do. I also know that what I really do is try to do the things that work well for me, and that they work well, not just because they are pretty good ideas—none of which I have originated by the way—but also because I have practiced them a lot.

So I invite you to practice the things that seem to work for you, and maybe to spice up your work by trying a few things that seem like maybe good ideas, so that you can advance in new directions. If you want to.

Simple Tests

I try to do everything with simple tests. That’s why my starting Lua framework includes the Tests object. With today’s work, four or five different ways of doing the same thing, I used my single test, adjusting it to the new scheme only slightly, and watched it first fail—because I do make mistakes—and then succeed, at which point I wrote up what we had.

Writing small tests, seeing them fail, then seeing them work, gives me more confidence in what I’m doing. I have them set up to run every time I save the script, and I save the script as soon as I think they’ll all run, or when I want to see how they’ll fail.

Classes and Objects

I much prefer to work with small classes and small objects, with clearly-defined responsibilities and carefully tested methods. Just as small tests help me make continuous progress, small classes and methods capture that progress in little packages that are easier to understand, use, and, when necessary, modify.

Our little Stepper class is an example. In an typical LSL script, there would be data items somewhere in the script, possibly at least grouped together textually, representing each time we needed a stepper. And there would be functions, perhaps grouped together, for doing the work. But there is no other grouping in LSL, and we resort often to comment blocks, alphabetization, or just searching around.

Our stepper class is compact, its members and methods are all readily identifiable, as they are all inside Stepper, and, best of all, it has an associated test so that if we set out to change or improve it, we can be confident that we’ve done what we wanted.

Summary

Small is beautiful to me. Small tests, small objects, defined via classes These ideas can’t always apply but when I manage to do it, I get results that I can feel proud of, and I get things working sooner than when I don’t work that way.

Safe paths!