Factory.create - error if factory taken from table (SOLVED)

hey there.

Got a little problem. I got this code-snip:

if message_id == hash("spawn") then
	local class = message.class
	local toX = message.mapPosX
	local toY = message.mapPosY
		
	local classes = {
		[hash("tank")] = "#tank_factory",
		[hash("collector")] = "#collector_factory",
		[hash("buggy")] = "#buggy_factory",
		[hash("emp")] = "#emp_factory",
		[hash("constructor")] = "#constructor_factory",
		[hash("rocketlauncher")] = "#rocketlauncher_factory"
	}
	
	local vPos = vmath.vector3(mapPosX * TILESIZE, mapPosY * TILESIZE, 0)
	local newGO	
	local factory = classes[class]
	
	newGO = factory.create(factory, vPos) 
	msg.post(newGO, "define", {class = class}) 

ERROR: attempt to call field ‘create’ (a nil value)

if I simply remove/comment the line

	local factory = classes[class] 

and use

local factory = "#tank_factory"

it works fine.
BUT, If I print the factory-variable it shows me the right value.

@britzl any ideas ? You also use this kind of factory-variables in your ONEROOM example for the enemies, f.e.

What you’ve run into here is what’s called variable shadowing.

What it means in your case is that the first line of code will declare a local variable named factory and assign it the value of classes[class] which in this case happens to be a string.

On the next line you intend to call the create function on the engine defined Lua module factory, but since you have declared a local variable named factory on the line above it’s actually that local variable that will be used. And the local variable has been assigned a string and there’s obviously no create function to call.

This will work:

local factory_url = classes[class]

newGO = factory.create(factory_url, vPos)

Ahhh…lol know what you mean. (i quess with my bad english).

its like:

never give a variable the name of a class/method :smiley:

Never had problems with shadowing. :slight_smile:

hm…but… :smiley:

I’ve just declared ONE variable called factory, so it cant be shadowing - or?
Just for my understanding. Sure its easy to rename a variable, but in the code above there is only one time a declaration for factory.

Will check that tomorrow. Too late now.

These simple problems costs tons of time GRRR

Umm, yes, you declare a local variable named factory but the Defold engine itself declares a factory module with the create function on it. This means that in the scope of your code the Defold provided factory module will be shadowed by your local variable.

Remember that factory, go, msg, print and all other modules and functions are just variables. It’s like if you do:

print = 10
print("Hello")
1 Like

Thanks @sicher.

Thats what I meant… never name a variable/constants etc like a class/method/module etc :slight_smile:

Its me again :smiley:

There is another problem with the length/size of the table…

Got this source:

function spawnUnit(self, class, mapPosX, mapPosY)
--[[	local classes = {
		[hash("tank")] = "#tank_factory",
		[hash("collector")] = "#collector_factory",
		[hash("buggy")] = "#buggy_factory",
		[hash("emp")] = "#emp_factory",
		[hash("constructor")] = "#constructor_factory",
		[hash("rocketlauncher")] = "#rocketlauncher_factory"
	}
	]]
	
	local classes = {
		["tank"] = "#tank_factory",
		["collector"] = "#collector_factory",
		["buggy"] = "#buggy_factory",
		["emp"] = "#emp_factory",
		["constructor"] = "#constructor_factory",
		["rocketlauncher"] = "#rocketlauncher_factory"
	}
	
	-- GRID-KORREKTUR
	local wPosX = mapPosX * TILESIZE + TILESIZE / 2
	local wPosY = mapPosY * TILESIZE + TILESIZE / 2	
	local vPos = vmath.vector3(wPosX , wPosY, 0)
	
	if class == nil then
		local randomclass = math.random(#classes)
		local factory_url = classes[randomclass]
		local class = classes[factory_url]	
	end
	
	local factory_url = classes[class] -- factory aus table holen	
	
	if factory_url then
		newGO = factory.create(factory_url, vPos) -- GO instanzieren
		msg.post(newGO, "define", {class = hash(class)}) -- GO sagen welche Unit es ist
		table.insert(units, {class, mapPosX, mapPosY}) -- in units table eintragen
			
		print("spawning " .. factory_url .. " to worldPos " .. wPosX .. "x" .. wPosY .. " MapPos: " .. mapPosX .. ":" .. mapPosY)		
	end
end

The Line

local randomclass = math.random(#classes)

returns an Error. random is nil. The size of classes are 0.
If I do a print(#classes) the result is null. Why?

The interesting thing is, the other code (if the variable class is given) works fine and spawns a unit.
The return of class[hash(“tank”)] returns correcty a “#tank_factory”. But the size, if read by #classes or table.getn(classes) are 0 (zero/null).

Why?

You can read about Lua’s # operator here: http://www.defold.com/manuals/lua/#_operators

1 Like

The length operator (#) only works on the part of a table where the keys are sequentially indexed from 1 up to some value, without any gaps in the numerical sequence. Remember that all Lua tables are associative arrays (key-value pairs), but for convenience that functionality can be hidden away when creating a table that functions as a classic array. An example:

local t = { "foo", "bar", "defold", "britzl" }
print(t[3]) -- defold
print(#t) -- 4

-- equivalent to the above, and it is what happens "behind the scenes" when the table above is created
-- every value gets an associated numeric key, but still lets the table work as a traditional array with numeric index
local t = { [1]  = "foo", [2] = "bar", [3] = "defold", [4] = "britzl" }
print(t[3]) -- defold
print(#t) -- 4

Now if we create a hole in our numerical sequence we break the “contract” we have with the Lua length operator and we get this result instead:

local t = { [1]  = "foo", [2] = "bar", [3] = "defold", [4] = "britzl" }
t[3] = nil
print(#t) -- 2, there is now a hole in my sequence and the # operator will work up until the first nil value

In your case you do not have any numeric keys at all. Your Lua table functions not as an array but rather as a map or dictionary with all keys being non-numeric. The length operator will correctly report a length of 0. I’m assuming that the classes table is static and it will never change? You could duplicate the data in the table and have it both as key value pairs for easy access of the factory url when the class is known and at the same time have an array part that can be used when randomly picking a class. Something like this:

local classes = {}

-- add class both in the "array" part of the table for # operator to work
-- and add class to the "dictionary" part of the table
function add_class(class, url)
	classes[#classes + 1] = url
	classes[class] = url
end

add_class("tank", "#tank_factory")
add_class("collector", "#collector_factory")
add_class("buggy", "#buggy_factory")
add_class("emp", "#emp_factory")
add_class("constructor", "#constructor_factory")
add_class("rocketlauncher", "#rocketlauncher_factory")

print(#classes)	-- 6
print(classes[4]) -- "#emp_factory"
print(classes.collector) -- "#collector_factory"

Or like this:

local classes = {
	{ class = "tank", factory = "#tank_factory" },
	{ class = "collector", factory = "#collector_factory" },
	{ class = "buggy", factory = "#buggy_factory" },
	{ class = "emp", factory = "#emp_factory" },
	{ class = "constructor", factory = "#constructor_factory" },
	{ class = "rocketlauncher", factory = "#rocketlauncher_factory" }
}

-- convert the original data structure to an "array" part with the factory url
-- and a "dictionary" part keyed on class type
for i,v in ipairs(classes) do
	classes[v.class] = v.factory
	classes[i] = v.factory
end

print(#classes)	-- 6
print(classes[4]) -- "#emp_factory"
print(classes.collector) -- "#collector_factory"
3 Likes

Thanks @sicher. Didnt read that manual.

very thanks @britzl. Amazing examples!

I took the variant of that table (using hash etc) from somewhere. dont know actually.

I’ll prefer your last example. For my understanding the best solution. Thanks!

you do great support!

2 Likes

oh my …

After deleting my post, I’ve tried a bit. I am/was a little confused about the way brizl you “organized” the table after creating it using the FOR-loop. Didnt understand its meaning or sense.

I did it my way of “parsing” the table in a for-loop.

That code seems to work now. May not be very pro or very elegant, but “better for my tiny understanding”

function spawnUnit(self, class, mapPosX, mapPosY)
	
	local classes = {
		{ class = "tank", factory = "#tank_factory" },
		{ class = "collector", factory = "#collector_factory" },
		{ class = "buggy", factory = "#buggy_factory" },
		{ class = "emp", factory = "#emp_factory" },
		{ class = "constructor", factory = "#constructor_factory" },
		{ class = "rocketlauncher", factory = "#rocketlauncher_factory" }
	}

	-- GRID-CALCULATION
	local wPosX = mapPosX * TILESIZE + TILESIZE / 2
	local wPosY = mapPosY * TILESIZE + TILESIZE / 2	
	local vPos = vmath.vector3(wPosX , wPosY, 0)
	local factory_url = nil
	
	if class == nil then
		local randomclass = math.random(#classes)
		factory_url = classes[randomclass].factory
		local class = classes[randomclass].class	
		print("Random unit: " .. class ..", factory_url " .. factory_url)
	else
		print("Manual unit: " .. class)
		for i,v in ipairs(classes) do
			if classes[i].class == class then
				factory_url = classes[i].factory
				break
			end
		end
		print("Manual unit: " .. class .. ", factory_url " .. factory_url)
	end

	print("spawning " .. factory_url .. " to worldPos " .. wPosX .. "x" .. wPosY .. " MapPos: " .. mapPosX .. ":" .. mapPosY)		

	newGO = factory.create(factory_url, vPos) -- GO instanciate
	msg.post(newGO, "define", {class = hash(class)}) -- define GO the unit-class
	table.insert(units, {class, mapPosX, mapPosY}) -- store new unit in table
		
	-- DEBUG
	pprint(units)
	label.set_text("gamecam#right_bottom", "Units: " .. #units)		
end
2 Likes