Jump to content

Lua table gotcha

AggrorJorn

1,010 views

I recently was introduced to a bug in my game. I had 20 AI units and only 19 of them were actively doing something. Number 20 was just standing there. The problem eventually lied in using '#enemies' to get the amount of enemies.

Here is what happened:

A lua table index by default starts on index 1. This in contrary to many other languages where it starts at 0. However, you can assign a value to index '0' if you want. Since I use C# on a daily basis, I am more comfortable using the 0 index as a start. As a result this is my (simplified) code:

for i = 0, enemyCount-1, do
	enemies[i] = new Enemy()                          
end

In the AI script I loop over the enemies like this:

for i = 0, #enemies-1, do
	enemies[i]:DoStuff()                          
end

This is really basic lua scripting with one tiny gotcha: The '#' is used to get the amount of consecutive keyed items in the list. This I knew. What I did not know, is that there is also the requirement that this order starts at index 1 (or at least not on index 0). It simply ignores the 0 index!

Here is a full script to try

local enemies = {}
local enemyCount = 4
for i = 0, enemyCount-1, 1 do
   enemies[i] = "I am enemy " .. i
   System:Print(enemies[i])                  
end

System:Print("#enemiesCount: " .. #enemies) 

for i = 0, #enemies-1 do
   System:Print(enemies[i])                  
end 

Output:

I am enemy 0
I am enemy 1
I am enemy 2
I am enemy 3

#enemiesCount: 3

I am enemy 0
I am enemy 1
I am enemy 2

Problem
So what was happening? I did get the amount of enemies back, except for the one enemy that was located on index 0. I quickly noticed the lower count of enemies, but since enemy number 20 wasn't doing anything I was also looking in the wrong place. It was actually enemy number 1 that was the culprit, even though it's AI was being executed.

Solution
It can be solved in numerous simple ways, but I guess best practice is to just stick to Lua's standard and not assign anything to 0. This can really prevent some time being wasted on absolutely silly issues like this.
 

  • Like 1


8 Comments


Recommended Comments

You probably know this already, but another way is to use pairs() to iterate through all pairs in the table, or ipairs() to iterate through them in numerical order.

Share this comment


Link to comment
13 minutes ago, Josh said:

or ipairs() to iterate through them in numerical order.

You have the same issue there when using ipair, the 0 index is ignored. Try it out Lua online.

output

create enemies: 4
I am enemy 0
I am enemy 1
I am enemy 2
I am enemy 3

#enemiesCount: 3    

I am enemy 0
I am enemy 1
I am enemy 2

Using IPAIR
1_I am enemy 1
2_I am enemy 2
3_I am enemy 3

Using PAIR
1_I am enemy 1
2_I am enemy 2
3_I am enemy 3
0_I am enemy 0


 

Share this comment


Link to comment

I usually avoid indices at all. Instead, I insert items with Table.Insert() and iterate through them with pairs() or ipairs():

--Create a new table
mytable={}

--Insert some items into the table
table.insert(mytable,"Thing 1")
table.insert(mytable,"Thing 2")
table.insert(mytable,"Thing 3")

 

  • Like 1

Share this comment


Link to comment
2 minutes ago, Josh said:

I usually avoid indices at all. Instead, I insert items with Table.Insert() and iterate through them with pairs() or ipairs():

I guess for general tables that is a better approach. However when using a multidimensional array, I prefer indices.  I do find the Table.insert function a bit weird. I wish Lua did it more like below. It helps when using intelisense.

myTable:Insert("Thing 1") 

 

  • Like 1

Share this comment


Link to comment

I think it would actually work if you made mytable.insert equal to the function table.insert. 😁

Share this comment


Link to comment

Yep, it works:

local enemies = {}
local enemyCount = 4

enemies.insert = table.insert

print("create enemies: " .. enemyCount )
for i = 1, enemyCount do
   enemies:insert("I am enemy " .. i)                 
end

print("\nUsing IPAIR")
for key,value in ipairs(enemies) do
  print(key .. "_" .. value)                  
end

print("\nUsing PAIR")
for key,value in pairs(enemies) do
  if type(value)=="string" then
    print(key .. "_" .. value)                  
  end
end

 

  • Like 1

Share this comment


Link to comment
13 hours ago, Josh said:

I think it would actually work if you made mytable.insert equal to the function table.insert. 😁

That flexibility is the strong part of Lua (arguably also a weakness). I am sure you could even adept the default table behavior to do this automatically for all tables. It is a pity Lua doesn't do this by default.

Share this comment


Link to comment

Well, notice above I had to add a type check because now one of the field of the table is a function. So you would probably not want this by default.

You could probably use a metamethod to hide the function from the pairs() iterator.

Share this comment


Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...