WoW:Object-oriented programming

From AddOn Studio
Revision as of 17:38, 13 November 2007 by WoWWiki>Ukadrezel
Jump to navigation Jump to search


Object Oriented Programming (OOP) is a modern programming paradigm centered on the behavior of objects. While there are many ways to slice a problem, the OOP way involves defining one or more objects. Each object has a type (its class). It is the job of the programmer to define the classes of objects needed to solve their problem. For a more complete description of OOP, check out the wikipedia article.

What about Lua?

Lua is not inherently object oriented. However, because Lua is so flexible, its easy to achieve object oriented behavior. We can accomplish encapsulation in Lua by making our variables members of a table. This almost guarantees that other addon writers will not accidentally redefine our variables to mean other things. Any member of a table can be accessed using dot notation. In the following code snippets, we define a table that represents a Wow character. The following are equivalent:

// 1
Character = {}
Character.name = "Murp"
Character.faction = "Horde"
function Character.cheerFaction() 
  SendChatMessage("For the "..Character.faction)
end
// 2
Character = {
["name"] = "Murp",
["faction"] = "Horde",
["cheerFaction"] = function() 
  SendChatMessage("For the "..Character.faction)
end,
}

At this point, Character contains 3 variables: name, faction and cheerFaction. The Wow client runs all addon code in a shared execution environment. That environment will contain one object called Character. Thats great and all, but what we really wanted was to be able to create lots of objects that are Characters (maybe one for each toon in your guild or each toon in your raid). And they can't all be named Murp.

Constructors

In OOP, all objects know how to instantiate (or create an instance of) themselves. Whenever an instance of a class is needed, that class's constructor is called. In Lua, we'll do the same.

You can name your constructor whatever you want, but for sanity's sake, most people call it new.

Character = {};
function Character:new()
  local self = {};         -- Create a blank table
  self.name = "Unknown";   -- Make a name variable in the class
  self.faction = "Unknown";-- Make a faction variable in the class
  return self;             -- Return the instance
end

We can now create multiple instances of the class:

  local player1 = Character:new(); -- Create a Character
  local player2 = Character:new(); -- Create another
  player1.name = UnitName("player");
  player2.name = UnitName("target");

All member variables should be initialized in the constructor. We want each instance to have its own name and other attributes, so we create a name and give it a starting value every time we construct a new Character.

Member functions

Unlike member variables, we do not need more than one copy of each member function. For this reason, we don't need to initialize member functions in the constructor.

If we define the functions outside the constructor, only one copy is ever made (in some cases, this can make your addon much more efficient).

Character = {};
function Character:new()
  local self = {};         -- Create a blank table
  self.name = "Unknown";   -- Make a name variable in the class
  self.faction = "Unknown";-- Make a faction variable in the class
  return self;             -- Return the instance
end
function Character:cheerFaction() 
  SendChatMessage("For the "..self.faction)  -- Using the : operator instead of . is shorthand for passing self as the first parameter
end

At this point, we can now create new Character objects, but when we try to call the member function, it doesn't work as we'd hope.

  local player1 = Character:new(); -- Create a Character
  player1.name = UnitName("player");
  player1.faction = UnitFactionGroup("player");
  player1:cheerFaction()           -- player1.cheerFaction was never defined.  we would like lua to find Character.cheerFaction() in its place

Metatables

We'll accomplish this with metatables. According to the Lua documentation, "Every value in Lua may have a metatable. This metatable is an ordinary Lua table that defines the behavior of the original value under certain special operations. You can change several aspects of the behavior of operations over a value by setting specific fields in its metatable." [1].

In particular, we want to control how the table accesses variables that do not exist. We specify that the instance should do a lookup in the table "Character" whenever the variable (or function) can't be found in self. This way, when it can't find player1.cheerFaction(), it searches for Character.cheerFaction() instead, which does exist.

All metatables have a field called "__index" (note that is two underscores, not one). The __index field specifies an alternate search location--a fallback for when normal lookups fail. Here's the constructor code with this change:

Character = {};
Character.__index = Character;    -- Set the __index parameter to reference Character
function Character:new()
  local self = {};                -- Create a blank table
  setmetatable(self, Character);  -- Set the metatable so we used Character's __index
  self.name = "Unknown";          -- Make a name variable in the class
  self.faction = "Unknown";       -- Make a faction variable in the class
  return self;                    -- Return the instance
end
function Character:cheerFaction() 
  SendChatMessage("For the "..self.faction)  
end

Now, every instance of Character can access cheerFaction() and we can get away with only one copy of the function. Even more usefully, if another addon later hooks Character.cheerFaction(), all instances will use the new hooked function.

Full Example

This is the fully written class we have created through the above methods. It has a constructor, creates the required fields, and gives each instance two functions: isAlliance and isHorde, which returns true if the character's race is set to an Alliance or Horde race, respectively. It also has a function load(), which loads data given a Unit ID via the Blizzard API.

  Character = {};
  Character.__index = Character;
  function Character:new()
     local self = {};
     setmetatable(self, Character);
     self.name = "Unknown";
     self.race = "Unknown";
     self.class = "Unknown";
     self.faction = "Unknown";
     self.level = 0;
     return self;
  end
  function Character:load(uid)
     if ~UnitExists(uid) then
        return false;
     end
     self.name = UnitName(uid);
     self.race = UnitRace(uid);
     self.class = UnitClass(uid);
     self.level = UnitLevel(uid);
     self.faction = UnitFactionGroup("player");
     return true;
  end
  function Character:isAlliance()
     if ( self.race == "Human"        or
          self.race == "Night Elf"    or
          self.race == "Dwarf"        or
          self.race == "Gnome"        or
          self.race == "Draenei"
        )
     then return true;
     else return false;
     end
  end
  function Character:isHorde()
     if (self.race == "Unknown") then
        return false;
     else
        return ~self:isAlliance();
     end
  end
  function Character:cheerFaction() 
     SendChatMessage("For the "..self.faction)  
  end

And we can quickly use this class to get information about our 40 raid members, and store them in a table:

  local units = {};
  for x = 1, 40 do
     units[x] = Character:new();
     units[x]:load("raid"..x);
  end

See Also