Thread Tools Display Modes
01-24-14, 10:47 PM   #1
Jonisaurus
An Aku'mai Servant
 
Jonisaurus's Avatar
Join Date: Aug 2011
Posts: 35
Instance methods and variables don't work - Metatable code faulty?

Hey!
This is my first OOP test with Lua and I'm a beginner so please be easy on me with bad, ugly code.
I'm mainly trying to test functionality in order to understand everything.

For my OOP practice I made a class (prototype) that can display font strings of units and their different values.
Like level, name, HP, power of the specified units.

The problem I have is that none of my methods or variables are being inherited by the instance object.
"targetNameTest" does not inherit from its class "UnitFontString".

My test method call and test instance variable print give me a Lua error and 'nil'.

Thanks for the help

Code:
-- [[ CLASS: Unit Font String ]]

local UnitFontString = {};
UnitFontString.alpha = 0.5;
UnitFontString.frameStrata = "BACKGROUND";
UnitFontString.width = 228;
UnitFontString.height = 64;
UnitFontString.point = {"CENTER", 0, 0};
UnitFontString.font = {"Fonts\\FRIZQT__.TTF", 220, "THICKOUTLINE"};

-- Constructor function
function UnitFontString.new(self, displayType, unitID, event1, event2, event3)
    local o = {};
    setmetatable(o, self);
    self._index = self;
    
    o.frame = CreateFrame("Frame", nil, UIParent);
    o.frame:SetFrameStrata(self.frameStrata);
    o.frame:SetWidth(self.width);
    o.frame:SetHeight(self.height);
    o.frame:SetPoint(self.point[1], self.point[2], self.point[3]);
    o.frame:Hide();
    
    o.fontString = o.frame:CreateFontString();
    o.fontString:SetFont(self.font[1], self.font[2], self.font[3]);
    o.fontString:SetAllPoints(o.frame);
    
    -- register events according to number of event parameters passed
    if event1 and event2 and event3 then
        o.frame:RegisterEvent(event1);
        o.frame:RegisterEvent(event2);
        o.frame:RegisterEvent(event3); 
    elseif event1 and event2 then
        o.frame:RegisterEvent(event1);
        o.frame:RegisterEvent(event2);
    elseif event1 then
        o.frame:RegisterEvent(event1);            
    -- if no event parameter was passed then return nothing for construction
    else
        return;
    end
    
    -- set up event handler function according to displayType and unitID parameters (purpose and unit)
    if unitID and displayType == "name" then
        function o.eventHandler(self, event, ...)
            o.frame:Hide();
            o.fontString:SetText(UnitName(unitID));
            o.frame:Show();
        end
    elseif unitID and displayType == "level" then
        function o.eventHandler(self, event, ...)
            o.frame:Hide();
            o.fontString:SetText(UnitLevel(unitID));
            o.frame:Show();
        end
    elseif unitID and displayType == "hp" then
        function o.eventHandler(self, event, ...)
            o.frame:Hide();
            o.fontString:SetText(UnitHealth(unitID));
            o.frame:Show();
        end
    elseif unitID and displayType == "power" then
        function o.eventHandler(self, event, ...)
            o.frame:Hide();
            o.fontString:SetText(UnitPower(unitID));
            o.frame:Show();
        end
    -- if no displayType or unitID parameter was passed then return nothing
    elseif not unitID or not displayType then
        return;
    end
    
    o.frame:SetScript("OnEvent", o.eventHandler);
    return o;
end


--[[ INSTANCE METHODS ]]

function UnitFontString.setAlpha(self, v)
    self.alpha = v;
    self.frame:SetAlpha(v);
end

function UnitFontString.setFrameStrata(self, v)
    self.frameStrata = v;
    self.frame:SetFrameStrata(v);
end

[...] [SHORTENED FOR FORUM]

-- [[ CLASS declaration END ]]


-- construct test object, print instance variable and call method
targetNameTest = UnitFontString:new("name", "target", "PLAYER_TARGET_CHANGED");
print(targetNameTest.width);
targetNameTest:setAlpha(0.5);

Last edited by Jonisaurus : 01-24-14 at 11:17 PM.
  Reply With Quote
01-24-14, 11:01 PM   #2
Vrul
A Scalebane Royal Guard
 
Vrul's Avatar
AddOn Author - Click to view addons
Join Date: Nov 2007
Posts: 404
You are making it way more complicated than it needs to be, however your error is a simple fix:
Code:
self._index = self
should be:
Code:
self.__index = self
  Reply With Quote
01-24-14, 11:29 PM   #3
Jonisaurus
An Aku'mai Servant
 
Jonisaurus's Avatar
Join Date: Aug 2011
Posts: 35
My god what a stupid mistake.
Thanks a lot for that though!

By complicated what do you mean specifically? A few things in there I wrote simply for syntax practice, but the whole thing is probably way too big, huh?
  Reply With Quote
01-24-14, 11:46 PM   #4
Phanx
Cat.
 
Phanx's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2006
Posts: 5,617
Originally Posted by Jonisaurus View Post
For my OOP practice I made a class (prototype) that can display font strings of units and their different values.
Like level, name, HP, power of the specified units.
I understand you're just practicing, but what you're practicting is way more complicated than it needs to be, and has some issues regardless of efficiency. 220 is not a valid font size; anything larger than 36* will be treated as if it were 36. You don't need to hide and re-show a frame before and after registering events. You should be using RegisterUnitEvent instead of RegisterEvent for unit-specific events. You're limiting your "font strings" to a few pre-defined event handlers, but allowing any arbitrary events to be registered; either allow a custom handler, or just hardcode the right event(s) for the pre-defined handler (eg. if you're handling health, register UNIT_HEALTH and UNIT_MAXHEALTH). There's absolutely no reason to wrap every frame API in an additional function that stores the alpha/position/whatever on keys on the frame, as you can just call frame:GetWhatever() if you really need to know those values later. You should just create a frame and return it so you can use its own API methods directly.

* The maximum font size is specific to the font file you're using, and is closer to 32 for most fonts, but can be much lower for some. It is never higher than 36.

Here's how I would write the same thing, in a more straightforward and more easily extensible way:

Code:
local NewUnitFontString
do
	-- Define events and handlers for specific "display types":
	local displayEvents = {
		health = {
			UNIT_HEALTH = true,
			UNIT_MAXHEALTH = true,
		},
		name = {
			PLAYER_TARGET_CHANGED = false,
			UNIT_NAME_UPDATE = true,
		},
	}
	local displayHandlers = {
		health = function(self, event, unit)
			local hp = UnitHealth(unit or self.unit)
			self:SetText(hp and hp > 0 and hp or nil)
		end,
		name = function(self, event, unit)
			local name = GetUnitName(unit or self.unit, true)
			self:SetText(name)
		end,
	}

	-- Define functions to let the frame be moved:
	local function OnMouseDown(self)
		if IsAltKeyDown() then
			self.isMoving = true
			self:StartMoving()
		end
	end
	local function OnMouseUp(self)
		if self.isMoving then
			self:StopMoving()
			self.isMoving = nil
		end
	end

	function NewUnitFontString(unit, displayType)
		-- Only proceed if this displayType is defined:
		local events, handler = displayEvents[displayType], displayHandlers[displayType]
		if not events or not handler then
			return print("NewUnitFontString:", displayType, "is not a valid display type!")
		end

		-- Make the frame:
		local f = CreateFrame("Frame", nil, UIParent, "SecureHandlerStateTemplate")
		f:SetFrameStrata("BACKGROUND")
		f:SetPoint("CENTER")
		f:SetWidth(228)
		f:SetHeight(64)

		-- Add the font string:
		local text = f:CreateFontString(nil, "OVERLAY")
		text:SetFont("Fonts\\FRIZQT__.TTF", 22, "THICKOUTLINE")
		text:SetAllPoints(true)

		-- Let the frame be dragged around:
		f:EnableMouse(true)
		f:SetMovable(true)
		f:RegisterForDrag("LeftButton")
		f:SetScript("OnMouseDown", OnMouseDown)
		f:SetScript("OnMouseUp", OnMouseUp)
		f:SetScript("OnHide", OnMouseUp) -- otherwise it can get stuck to the cursor if it's being dragged when it gets hidden

		-- Register the events and assign the handler:
		f.unit = unit
		f:SetScript("OnEvent", handler)
		for event, unitSpecific in pairs(events) do
			if unitSpecific then
				f:RegisterUnitEvent(event, unit)
			else
				f:RegisterEvent(event)
			end
		end

		-- Hide the frame when the unit doesn't exist:
		RegisterStateDriver(f, "visibility", "[@"..unit..",exists]show;hide")
		f:SetShown(UnitExists(unit))

		-- Return the frame:
		return f
	end
end
Then you can just ask for and get a normal frame in a normal way, and access and manipulate its properties directly through the normal frame API:

Code:
local targetName = NewUnitFontString("target", "name")
print(targetName:GetWidth())
targetName:SetAlpha(0.5)
(I also made the frames movable when Alt is pressed, and automatically hide themselves when their unit doesn't exist.)
__________________
Retired author of too many addons.
Message me if you're interested in taking over one of my addons.
Don’t message me about addon bugs or programming questions.
  Reply With Quote
01-25-14, 07:52 PM   #5
Jonisaurus
An Aku'mai Servant
 
Jonisaurus's Avatar
Join Date: Aug 2011
Posts: 35
Reading alternative code like that really helps. I have a few questions:

1. The pairs() function. I don't quite understand it.
Earlier you declare 'events' to be the result of the 'displayEvents[displayType]' table access.
Where does 'unitSpecific' come from and what do 'event' and 'unitSpecific' do exactly in 'events'?
Code:
for event, unitSpecific in pairs(events) do
			if unitSpecific then
				f:RegisterUnitEvent(event, unit)
			else
				f:RegisterEvent(event)
			end
		end
2. Why did you set the value of the event keys to a BOOLEAN value? And why is it UNIT_HEALTH and not "UNIT_HEALTH"?
Code:
local displayEvents = {
		health = {
			UNIT_HEALTH = true,
			UNIT_MAXHEALTH = true,
		},
		name = {
			PLAYER_TARGET_CHANGED = false,
			UNIT_NAME_UPDATE = true,
		},
	}
3. What exactly does this expression mean? Why is the existence of 'hp' queried twice?
Code:
SetText(hp and hp > 0 and hp or nil)
4. In order for 'self.unit' to work the 'frame:SetScript("OnEvent", handler)' function passes the 'frame' as first parameter 'self' to the handler function, correct?
How would the handler function ever receive the third parameter 'unit' directly though? When would '(unit or self.unit)' ever resort to using 'unit'?
Code:
health = function(self, event, unit)
			local hp = UnitHealth(unit or self.unit)
			self:SetText(hp and hp > 0 and hp or nil)
		end,
Thanks!
  Reply With Quote
01-25-14, 08:54 PM   #6
Torhal
A Pyroguard Emberseer
 
Torhal's Avatar
AddOn Author - Click to view addons
Join Date: Aug 2008
Posts: 1,196
1: The pairs function returns the key/value pairs from a table. The "unitSpecific" refers to a variable which is holding the value for that particular key/value entry in the loop. It could very well have been named "flufferNutter."

2: The value for the table is set to "true" simply to give it a value to iterate over. When making a table, if the key is a non-space-separated string, the quotation marks and brackets are optional.

3: The expression
Code:
SetText(hp and hp > 0 and hp or nil)
is Lua's way of performing a pseudo ternary operation. Read this for a full explanation - there is a specific section for Lua there as well.

4: I'll let someone else answer this one, as I've not been reading this thread to be familiar with the posted code.
__________________
Whenever someone says "pls" because it's shorter than "please", I say "no" because it's shorter than "yes".

Author of NPCScan and many other AddOns.

Last edited by Torhal : 01-25-14 at 08:57 PM.
  Reply With Quote
01-25-14, 09:06 PM   #7
Xrystal
nUI Maintainer
 
Xrystal's Avatar
Premium Member
AddOn Author - Click to view addons
Join Date: Feb 2006
Posts: 5,892
I found Phanx's explanation interesting myself and have dabbled with that side of things today to see how it all worked as I've never used tables to hold event and handler functions before so handy to know how they can be used. Here follows my understanding of how it all works. I'm sure others will be able to offer better explanations but here goes anyway.


1. When you use the pairs function you are asking it to grab a key,value pair from the table listed.

Lua Code:
  1. local displayEvents = {
  2.         health = {
  3.             UNIT_HEALTH = true,
  4.             UNIT_MAXHEALTH = true,
  5.         },
  6.         name = {
  7.             PLAYER_TARGET_CHANGED = false,
  8.             UNIT_NAME_UPDATE = true,
  9.         },
  10.     }

In this example the keys are health and name and the values are the values after the '=' in otherwords the table of events and whether they are unitSpecific ( true/false). As far as I am aware these could be written as :
displayEvents[health][UNIT_HEALTH] = true
displayEvents["health"]["UNIT_HEALTH"] = true
displayEvents.health["UNIT_HEALTH"] = true
displayEvents.health.unit_health = true

However if health or UNIT_HEALTH had been previously set to other values such as health = 123. Then displayEvents[health] may ( I think ) be translated as displayEvents[123] but I could be wrong.

From what I understand words do not need to be contained in speech marks as it is automatically treated as a string.

edit: Torhal's explanation was a might bit better than mine.

Lua Code:
  1. for event, unitSpecific in pairs(events) do
  2.             if unitSpecific then
  3.                 f:RegisterUnitEvent(event, unit)
  4.             else
  5.                 f:RegisterEvent(event)
  6.             end
  7.         end

In this example we are asking the addon to look at the events list for selected key earlier requested and check to see if the event ( key ) is a unitSpecific event ( value ). Dependant on that result will be whether it registers the unit event or the regular event.

2. Is answered in the above explanation.

3. I am not sure myself why the hp is queried twice but the hp and hp > 0 is just making sure that there is a hp value and if there is a value that it is more than 0.

4. You are correct in that statement. The frame is passed to the event handler and if the event passes the unit to the handler itself it will use that, otherwise it will use the one that was stored when creating the frame.


I hope that helps you understand things a bit more. The wowpedia website has a link to the lua functions wow uses and a link to the lua manual itself so that you can research some more on that subject.
__________________

Last edited by Xrystal : 01-25-14 at 09:12 PM.
  Reply With Quote
01-25-14, 11:35 PM   #8
Choonstertwo
A Chromatic Dragonspawn
 
Choonstertwo's Avatar
AddOn Author - Click to view addons
Join Date: Jan 2011
Posts: 194
Originally Posted by Xrystal View Post
In this example the keys are health and name and the values are the values after the '=' in otherwords the table of events and whether they are unitSpecific ( true/false). As far as I am aware these could be written as :
displayEvents[health][UNIT_HEALTH] = true
displayEvents["health"]["UNIT_HEALTH"] = true
displayEvents.health["UNIT_HEALTH"] = true
displayEvents.health.unit_health = true
Lines 2 and 3 do the same thing as each other, but lines 1 and 4 are both different.

Line 1 is using the values of the variables health and UNIT_HEALTH as keys instead of the strings health and UNIT_HEALTH like line 2 and 3.

Line 4 is using the string unit_health as a key instead of UNIT_HEALTH. Lua is case-sensitive, so these are two completely separate keys. If it was in uppercase, it would be the same as lines 2 and 3.

Originally Posted by Jonisaurus View Post
4. In order for 'self.unit' to work the 'frame:SetScript("OnEvent", handler)' function passes the 'frame' as first parameter 'self' to the handler function, correct?
How would the handler function ever receive the third parameter 'unit' directly though? When would '(unit or self.unit)' ever resort to using 'unit'?
Code:
health = function(self, event, unit)
			local hp = UnitHealth(unit or self.unit)
			self:SetText(hp and hp > 0 and hp or nil)
		end,
Thanks!
The OnEvent handler always receives two arguments: self (the frame that the event fired for) and event (the event that fired). After these two, it receives any arguments passed by the event itself. All of the UNIT_X events have unit (the unitID that the event fired for) as their first argument.

Last edited by Choonstertwo : 01-25-14 at 11:40 PM. Reason: Added response to question 4.
  Reply With Quote
01-26-14, 07:38 AM   #9
Xrystal
nUI Maintainer
 
Xrystal's Avatar
Premium Member
AddOn Author - Click to view addons
Join Date: Feb 2006
Posts: 5,892
Thanks for clarifying Choonster ..

line 4 I did mess up, forgot to put caps on the UNIT_HEALTH part. line 1 I thought was the same as ["health"]["UNIT_HEALTH"] if, and only if, there is no variable set with those names but it does make sense that it wouldn't assume they were text values. I always use the speech marks just to avoid the confusion.
__________________
  Reply With Quote
01-26-14, 09:32 PM   #10
Phanx
Cat.
 
Phanx's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2006
Posts: 5,617
Originally Posted by Xrystal View Post
... I thought was the same as ["health"]["UNIT_HEALTH"] if, and only if, there is no variable set with those names but it does make sense that it wouldn't assume they were text values.
Yeah, if you have not definited UNIT_HEALTH as a variable then looking up myTable[UNIT_HEALTH] will just give you an error about attempting to index a nil value. The only place you don't need quotes around a string value is when defining a key inside a table contructor, in which context strings are the only type of keys you don't need to enclose in square brackets.

Code:
local myTable = {
     -- Now you're inside a table constructor!
     -- You don't need to quote strings used as keys here.
     -- These will all work:
     myKey = "myValue", -- "myKey" is a string, but it doesn't need quotes and brackets here
     ["otherKey"] = "otherValue", -- "otherKey" is also a string
     [12] = "twelve", -- 12 is not a string
     [function(x) return x-1 end] = "okay!", -- yes, you can use functions as keys!
     [UnitHealth] = 47, -- UnitHealth is a (reference to a) function
     [RAID_CLASS_COLORS] = function() print("cat") end, -- and tables too!
     ["RAID_CLASS_COLORS"] = "x", -- but this key is a string, not a table

     -- But these will not work without brackets, because the keys aren't strings:
     14 = "wrong",
     function(x) return x-1 end = "also wrong",
     { sub = "table" } = "and wrong again!",
}
Even if you already have a variable with the same name, you still don't need quotes for string-type keys inside a table constructor; this is perfectly valid:

Code:
local class = UnitClass("player")
local playerInfo = {
     class = class, -- the key (left side) is the string "class", and the value (right side) is the value of the variable class
}
However, your strategy of always quoting strings is a good one for avoiding confusion.
__________________
Retired author of too many addons.
Message me if you're interested in taking over one of my addons.
Don’t message me about addon bugs or programming questions.

Last edited by Phanx : 01-26-14 at 09:35 PM.
  Reply With Quote

WoWInterface » Developer Discussions » Lua/XML Help » Instance methods and variables don't work - Metatable code faulty?

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off