4

I'm working on a game in Lua, and so far I have everything working with everything in one document. However, to better organize everything, I've decided to expand it into modules, and while I figure I could probably get it working more or less the same, I figure now might be an opportunity to make things a little more clear and elegant.

One example is enemies and enemy movement. I have an array called enemyTable, and here is the code in Update that moves each enemy:

    for i, bat in ipairs(enemyTable) do
        if bat.velocity < 1.1 * player.maxSpeed * pxPerMeter then
            bat.velocity = bat.velocity + 1.1 * player.maxSpeed * pxPerMeter * globalDelta / 10
        end

        tempX,tempY = math.normalize(player.x - bat.x,player.y - bat.y)

        bat.vectorX = (1 - .2) * bat.vectorX + (.2) * tempX
        bat.vectorY = (1 - .2) * bat.vectorY + (.2) * tempY

        bat.x = bat.x + (bat.velocity*bat.vectorX - player.velocity.x) * globalDelta
        bat.y = bat.y + bat.velocity * bat.vectorY * globalDelta

        if bat.x < 0 then
            table.remove(enemyTable,i)
        elseif bat.x > windowWidth then
            table.remove(enemyTable,i)
        end     
    end

This code does everything I want it to, but now I want to move it into a module called enemy.lua. My original plan was to create a function enemy.Move() inside enemy.lua that would do this exact thing, then return the updated enemyTable. Then the code inside main.lua would be something like:

enemyTable = enemy.Move(enemyTable)

What I'd prefer is something like:

enemyTable.Move()

...but I'm not sure if there's any way to do that in Lua? Does anyone have any suggestions for how to accomplish this?

Adam
  • 155
  • 1
  • 1
  • 11
  • 1
    Your table traversal is losing enemies. You cannot use `table.remove` on a table while traversing it with `ipairs`. Every time you do you lose/miss an element in the original table from your loop. Try it: `t = {1,2,3,4,5,6,7,8,9,10}; for i, v in ipairs(t) do print(v) if v % 2 == 0 then table.remove(t, i) end end` (or just see it [here](https://eval.in/582230)). – Etan Reisner Jun 02 '16 at 21:36
  • Ooh, good point, thanks! – Adam Jun 03 '16 at 12:01
  • So, wait, I'm trying to solve this and I'm realizing it's not exactly a trivial solution. My first thought was to create a "trash" table that stored any elements I wanted to delete, then traverse the original table removing the trash elements. But this leads to the same problem: values get shifted down every time you delete something. Is there some clever way around this that I'm missing? – Adam Jun 03 '16 at 13:00
  • 2
    You can delete while traversing as long as you do it in reverse (and therefore don't use `ipairs`). `for i = #enemyTable, 1, -1 do ... table.remove(enemyTable, i) ... end` is safe since you only ever shift elements you've *already* dealt with. The alternative is to not edit the table in-place but to copy elements you want to save to a new table as you go and then return that. The choices have trade-offs in space and cost and so, in part, depend on your table sizes, etc. – Etan Reisner Jun 03 '16 at 13:43

2 Answers2

4

Sounds like you just want the metatable of enemyTable to be the enemy module table. Lua 5.1 reference manual entry for metatables

Something like this.

enemy.lua

local enemy = {}

function enemy:move()
    for _, bat in ipairs(self) do
        ....
    end
end

return enemy

main.lua

local enemy = require("enemy")

enemyTable = setmetatable({}, {__index = enemy})

table.insert(enemyTable, enemy.new())
table.insert(enemyTable, enemy.new())
table.insert(enemyTable, enemy.new())

enemyTable:move()
Etan Reisner
  • 77,877
  • 8
  • 106
  • 148
  • 1
    I think this is the answer I'm looking for. I think I might have the move() function affect individual enemies and have the loop in main, but that's a small difference. Thanks! – Adam Jun 03 '16 at 00:20
  • 1
    @Adam That's the model I'd choose to but I didn't want to rewrite more of your answer then necessary to get the point across. – Etan Reisner Jun 03 '16 at 01:22
1

Of course you can do it. For what I can see, your Move function processes the table you pass it as an argument and returns another table, in a functional programming fashion, leaving the first table immutate. You just have to set your Move function so that it knows it has to operate on your enemy table instead of creating a new table. So in your module write:

local enemy = {}

-- insert whatever enemy you want in the enemy table

function enemy.Move()
    for _, bat in ipairs(enemy) do
        --operate on each value of the enemy table
    end
    --note that this function doesn't have to return anything: not a new enemy table, at least
end

return enemy

And in your love.load function you can call

enemyTable = require "enemymodule"

Then you just have to call enemyTable.Move()

user6245072
  • 2,051
  • 21
  • 34
  • So wait, how would I add enemies to enemy table after setting it equal to require "enemymodule"? Right now I use table.insert(enemyTable,newEnemy)... would that still work? – Adam Jun 02 '16 at 19:36
  • Why is this function not using `self` and the `:` sugar? Also why is this using the module table as both the functions and the actual enemy storage table? – Etan Reisner Jun 02 '16 at 21:31
  • @Adam yes, it would work, the `enemyTable` in main is exactly the same as the returned `enemy` from the module. – user6245072 Jun 03 '16 at 05:24
  • @Etan Reisner isn't storing upvalues faster than asking to evaluate `enemy` each time and passing it as an argument? The `Move` function doesn't need to be stored in different tables (so that the `self` syntactic sugar would be helpful), it's just being used for `enemy`. – user6245072 Jun 03 '16 at 05:27
  • If the time spent calling the function dominates the runtime here (or even takes up a noticeable amount of time given the other work the loop needs to do) then I would be intensely surprised. But the main thing is that this is impossible to extend (and move should most likely be a function on an enemy and not on the global list to begin with) and that this confuses the enemy storage table and the enemy module of functions (which is going to bite someone at some point). – Etan Reisner Jun 03 '16 at 11:19
  • @Etan Reisner it _should_ be a function on an enemy and not on the enemy table. But that's not what the OP asked for: he wanted a function to operate on its enemy table. Also there's no confusion in returning an object with its own methods stored inside from a module, I could say it's even more straight forward. – user6245072 Jun 03 '16 at 12:37
  • Yes, I didn't make that edit in my answer either (though I did then mention it in my comment once the OP did). And no, having an enemies functions in the metatable of the enemy table itself is one thing, having the functions in the actual enemy table is another and having the functions and the *list* of enemies in the same table is yet another. (If you wanted to do that latter I'd keep an `currentEnemies` table inside the `enemy` module/table for that, but I still wouldn't recommend it.) – Etan Reisner Jun 03 '16 at 13:46
  • @Etan Reisner you're right! Those are totally different things, and having 3 tables just for a list of enemys with an appended function _is_ confusing. Anyway, we did all we could do to help the OP, he'll choose whatever method he likes the more. – user6245072 Jun 03 '16 at 13:51