Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A unique environment per script in Lua 5.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?

like image 288
Chad Layton Avatar asked Sep 24 '26 09:09

Chad Layton


2 Answers

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.

like image 175
catwell Avatar answered Sep 26 '26 02:09

catwell


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.

like image 22
hugomg Avatar answered Sep 26 '26 01:09

hugomg



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!