3

I would like to be able to have a chunk of Lua code (a "script") that could be shared among enemy types in a game but where each instance of a script gets a unique execution environment. To illustrate my problem, this is my first attempt at what a script might look like:

time_since_last_shoot = 0

tick = function(entity_id, dt)
  time_since_last_shoot = time_since_last_shoot + dt
  if time_since_last_shoot > 10 then
    enemy = find_closest_enemy(entity_id)
    shoot(entity_id, enemy)
    time_since_last_shoot = 0
  end
end

But that fails since I'd be sharing the global time_since_last_shoot variable among all my enemies. So then I tried this:

spawn = function(entity)
  entity.time_since_last_shoot = 0;
end

tick = function(entity, dt)
  entity.time_since_last_shoot = entity.time_since_last_shoot + dt
    if entity.time_since_last_shoot > 10 then
      enemy = find_closest_enemy(entity)
      shoot(entity, enemy)
      entity.time_since_last_shoot = 0
    end
end

And then for each entity I create a unique table and then pass that as the first argument when I call the spawn and tick functions. And then somehow map that table back to an id at runtime. Which could work, but I have a couple concerns.

First, it's error prone. A script could still accidentally create global state that could lead to difficult to debug problems later in the same script or even others.

And second, since the update and tick functions are themselves global, I'll still run into issues when I go to create a second type of enemy which tries to use the same interface. I suppose I could solve that with some kind of naming convention but surely there's a better way to handle that.

I did find this question which seems to be asking the same thing, but the accepted answer is light on specifics and refers to a lua_setfenv function that isn't present in Lua 5.3. It seems that it was replaced by _ENV, unfortunately I'm not familiar enough with Lua to fully understand and/or translate the concept.

[edit] A third attempt based on the suggestion of @hugomg:

-- baddie.lua
baddie.spawn = function(self)
    self.time_since_last_shoot = 0
end

baddie.tick = function(self, dt)
    entity.time_since_last_shoot = entity.time_since_last_shoot + dt
    if entity.time_since_last_shoot > 10 then
      enemy = find_closest_enemy(entity)
      shoot(entity, enemy)
      entity.time_since_last_shoot = 0
    end
end

And in C++ (using sol2):

// In game startup
sol::state lua;
sol::table global_entities = lua.create_named_table("global_entities");

// For each type of entity
sol::table baddie_prototype = lua.create_named_table("baddie_prototype");
lua.script_file("baddie.lua")
std::function<void(table, float)> tick = baddie_prototype.get<sol::function>("tick");

// When spawning a new instance of the enemy type
sol::table baddie_instance = all_entities.create("baddie_instance");
baddie_instance["entity_handle"] = new_unique_handle();

// During update
tick(baddie_instance, 0.1f);`

This works how I expected and I like the interface but I'm not sure if it follows the path of least surprise for someone who might be more familiar with Lua than I. Namely, my use of the implicit self parameter and my distinction between prototype/instance. Do I have the right idea or have I done something weird?

Community
  • 1
  • 1
Chad Layton
  • 127
  • 6

2 Answers2

5

For your first issue (accidentally creating globals), you can rely on a linter like luacheck or a module that prevents you from creating globals like strict.lua from Penlight.

And then, why not just make things local? I mean both time_since_last_shoot and tick. This leverages closures, one of the most useful features of Lua. If you want different tick functions, each with its own variables, you can do something like this:

local function new_tick()
    local time_since_last_shoot = 0
    return function(entity_id, dt)
        time_since_last_shoot = time_since_last_shoot + dt
        if time_since_last_shoot > 10 then
            local enemy = find_closest_enemy(entity_id)
            shoot(entity_id, enemy)
            time_since_last_shoot = 0
      end
    end
end

local tick_1 = new_tick()
local tick_2 = new_tick()

Of course, you could also use the environment for this, but here I think local variables and closure are a better solution to the problem.

catwell
  • 6,770
  • 1
  • 23
  • 21
3

The way _ENV works in 5.3 is that global variable are "syntactic" sugar for reading fields from the _ENV variable. For example, a program that does

local x = 10
y = 20
print(x + y)

is equivalent to

local x = 10
_ENV.y = 20
_ENV.print(x + _ENV.y)

By default, _ENV is a "global table" that works like you would expect global variables to behave. However, if you create a local variable (or function argument) named _ENV then in that variable's scope any unbound variables will point to this new environment instead of point to the usual global scope. For example, the following program prints 10:

local _ENV = {
    x = 10,
    print=print
}
-- the following line is equivalent to 
-- _ENV.print(_ENV.x)
print(x)

In your program, one way to use this technique would be to add an extra parameter to your functions for the environment:

tick = function(_ENV, entity, dt)
    -- ...
end

then, any global variables inside the function will actually just be accessing fields in the _ENV parameter instead of actually being global.


That said, I'm not sure _ENV is the best tool to solve your problem. For your first problem, of accidentally creating globals, a simpler solution would be to use a linter to warn you if you assign to an undeclared global variable. As for the second problem, you could just put the update and tick functions in a table instead of having them be global.

hugomg
  • 68,213
  • 24
  • 160
  • 246
  • Thank you @hugomg. I came up with another proposed solution based on your last suggestion and edited my question. Does it look like something you might expect? – Chad Layton Sep 04 '16 at 15:42
  • Lua is very much a "there is more than one way to do it" kind of language so just do what you think is better for your use case. If you are curious about the "self", Lua has no built-in object orientation support so explicit self is the way to go (although you might want to use the `:` syntactic sugar just to make it look a bit nicer). Maybe you should check out the object orientation chapter in Programming in Lua. – hugomg Sep 04 '16 at 15:58
  • Great! I did read over that chapter but I'm still a bit unclear on what Lua considers an instance vs a prototype and so I wasn't sure what I was providing for self would be what someone would expect. For example, if someone were to do `baddie.foo = 3` somewhere in `baddie.lua` and then tried to use `self.foo` inside `tick' that would be nil. But I believe I could fix that by using the __index metamethod? But if it generally looks okay to you then I'm happy with it. – Chad Layton Sep 04 '16 at 16:22
  • Prototypes in Lua are a form of optimization. An object in Lua is just a table and instance variables and methods are just fields in this table. If you can many objects that belong to the same "class" then their instance variables will be different but their methods will all be the same. Instead of separately storing a reference to the methods in each object, you have a single prototype that stores the methods and then every object just has to use its metatable to point to this prototype. The end result is that `obj.themethod` is a function just as before but you use less memory. – hugomg Sep 04 '16 at 18:55
  • I like to think to methods stored in the __index table as static methods: the same function is shared between all the instances and has no direct access to the object it's called from, except if you pass that object as an argument to it, for which `:` is just syntactic sugar. But if you want "normal" methods, one for each instance of the class and with direct access to the object and its private attributes, you must declare them in the "init" method and store private variables as upvalues to them. This means you don't "use less memory", you are just incentivated to use static methods. – user6245072 Sep 04 '16 at 20:15
  • @user6245072: That is wrong, at least according to the typical definition of static method. Regular methods go in the prototype (the __index field of the metatable) and static methods areprivate variables. top-level functions in the module you are defining. Declaring methods inside the "init" actually is something you only need to do if you really want your instance variables to be private. – hugomg Sep 04 '16 at 20:32
  • Static methods (and variables) belong to the class, they're not objects' private variables. At least, that's what I learned from C++. – user6245072 Sep 04 '16 at 21:47
  • oops that aws a typo :) I meant to say "static methods are top-level functions in the module you are defining." – hugomg Sep 05 '16 at 00:35