Lua 5.3 中每个脚本的独特环境
A unique environment per script in Lua 5.3
我希望能够有一大块 Lua 代码(一个 "script")可以在游戏中的敌人类型之间共享,但是脚本的每个实例都有一个唯一的执行环境。为了说明我的问题,这是我第一次尝试脚本的样子:
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
但这失败了,因为我将在所有敌人之间共享全局 time_since_last_shoot 变量。然后我尝试了这个:
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
然后我为每个实体创建一个唯一的 table,然后在调用 spawn 和 tick 函数时将其作为第一个参数传递。然后以某种方式将 table 映射回 运行 时的 id。哪个可行,但我有几个问题。
首先,它容易出错。脚本仍然可能意外创建全局状态,这可能导致稍后在同一脚本甚至其他脚本中出现难以调试的问题。
其次,由于更新和 tick 函数本身是全局的,因此当我创建试图使用相同界面的第二种类型的敌人时,我仍然会 运行 遇到问题。我想我可以通过某种命名约定来解决这个问题,但肯定有更好的方法来处理这个问题。
我确实找到了 this 问题,它似乎在问同样的事情,但接受的答案没有具体说明,并且指的是 [=40] 中不存在的 lua_setfenv 函数=] 5.3。好像被_ENV 代替了,可惜我对Lua 不够熟悉,无法完全理解and/or 翻译这个概念。
[edit] 根据@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
在 C++ 中(使用 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);`
这按我的预期工作,我喜欢这个界面,但我不确定它是否遵循了比我更熟悉 Lua 的人最不意外的路径。也就是说,我使用隐含的自我参数和我prototype/instance之间的区别。我的想法是正确的还是我做了什么奇怪的事情?
_ENV 在 5.3 中的工作方式是全局变量是 "syntactic" 糖,用于从 _ENV 变量读取字段。例如,一个程序
local x = 10
y = 20
print(x + y)
相当于
local x = 10
_ENV.y = 20
_ENV.print(x + _ENV.y)
默认情况下,_ENV 是一个 "global table",其工作方式与您期望的全局变量的行为一样。但是,如果您创建一个名为 _ENV 的局部变量(或函数参数),那么在该变量的范围内,任何未绑定的变量都将指向这个新环境,而不是指向通常的全局范围。例如,以下程序打印 10:
local _ENV = {
x = 10,
print=print
}
-- the following line is equivalent to
-- _ENV.print(_ENV.x)
print(x)
在您的程序中,使用此技术的一种方法是为您的环境函数添加一个额外参数:
tick = function(_ENV, entity, dt)
-- ...
end
那么,函数内的任何全局变量实际上只是访问 _ENV 参数中的字段,而不是实际的全局变量。
也就是说,我不确定 _ENV 是解决您问题的最佳工具。对于您的第一个问题,即意外创建全局变量,一个更简单的解决方案是在您分配给未声明的全局变量时使用 linter 来警告您。至于第二个问题,您可以将更新和滴答功能放在 table 中,而不是让它们成为全局的。
对于您的第一个问题(意外创建全局变量),您可以依赖像 luacheck or a module that prevents you from creating globals like strict.lua from Penlight.
这样的 linter
然后,为什么不把事情本地化呢?我的意思是 time_since_last_shoot
和 tick
。这利用了闭包,闭包是 Lua 最有用的特性之一。如果你想要不同的报价函数,每个函数都有自己的变量,你可以这样做:
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()
当然,你也可以使用环境,但这里我认为局部变量和闭包是更好的解决问题的方法。
我希望能够有一大块 Lua 代码(一个 "script")可以在游戏中的敌人类型之间共享,但是脚本的每个实例都有一个唯一的执行环境。为了说明我的问题,这是我第一次尝试脚本的样子:
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
但这失败了,因为我将在所有敌人之间共享全局 time_since_last_shoot 变量。然后我尝试了这个:
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
然后我为每个实体创建一个唯一的 table,然后在调用 spawn 和 tick 函数时将其作为第一个参数传递。然后以某种方式将 table 映射回 运行 时的 id。哪个可行,但我有几个问题。
首先,它容易出错。脚本仍然可能意外创建全局状态,这可能导致稍后在同一脚本甚至其他脚本中出现难以调试的问题。
其次,由于更新和 tick 函数本身是全局的,因此当我创建试图使用相同界面的第二种类型的敌人时,我仍然会 运行 遇到问题。我想我可以通过某种命名约定来解决这个问题,但肯定有更好的方法来处理这个问题。
我确实找到了 this 问题,它似乎在问同样的事情,但接受的答案没有具体说明,并且指的是 [=40] 中不存在的 lua_setfenv 函数=] 5.3。好像被_ENV 代替了,可惜我对Lua 不够熟悉,无法完全理解and/or 翻译这个概念。
[edit] 根据@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
在 C++ 中(使用 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);`
这按我的预期工作,我喜欢这个界面,但我不确定它是否遵循了比我更熟悉 Lua 的人最不意外的路径。也就是说,我使用隐含的自我参数和我prototype/instance之间的区别。我的想法是正确的还是我做了什么奇怪的事情?
_ENV 在 5.3 中的工作方式是全局变量是 "syntactic" 糖,用于从 _ENV 变量读取字段。例如,一个程序
local x = 10
y = 20
print(x + y)
相当于
local x = 10
_ENV.y = 20
_ENV.print(x + _ENV.y)
默认情况下,_ENV 是一个 "global table",其工作方式与您期望的全局变量的行为一样。但是,如果您创建一个名为 _ENV 的局部变量(或函数参数),那么在该变量的范围内,任何未绑定的变量都将指向这个新环境,而不是指向通常的全局范围。例如,以下程序打印 10:
local _ENV = {
x = 10,
print=print
}
-- the following line is equivalent to
-- _ENV.print(_ENV.x)
print(x)
在您的程序中,使用此技术的一种方法是为您的环境函数添加一个额外参数:
tick = function(_ENV, entity, dt)
-- ...
end
那么,函数内的任何全局变量实际上只是访问 _ENV 参数中的字段,而不是实际的全局变量。
也就是说,我不确定 _ENV 是解决您问题的最佳工具。对于您的第一个问题,即意外创建全局变量,一个更简单的解决方案是在您分配给未声明的全局变量时使用 linter 来警告您。至于第二个问题,您可以将更新和滴答功能放在 table 中,而不是让它们成为全局的。
对于您的第一个问题(意外创建全局变量),您可以依赖像 luacheck or a module that prevents you from creating globals like strict.lua from Penlight.
这样的 linter然后,为什么不把事情本地化呢?我的意思是 time_since_last_shoot
和 tick
。这利用了闭包,闭包是 Lua 最有用的特性之一。如果你想要不同的报价函数,每个函数都有自己的变量,你可以这样做:
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()
当然,你也可以使用环境,但这里我认为局部变量和闭包是更好的解决问题的方法。