【问题标题】:A unique environment per script in Lua 5.3Lua 5.3 中每个脚本的独特环境
【发布时间】:2017-01-11 20:20:27
【问题描述】:

我希望能够拥有一段 Lua 代码(“脚本”),可以在游戏中的敌人类型之间共享,但脚本的每个实例都有一个独特的执行环境。为了说明我的问题,这是我第一次尝试脚本的外观:

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

然后为每个实体创建一个唯一的表,然后在调用 spawn 和 tick 函数时将其作为第一个参数传递。然后在运行时以某种方式将该表映射回一个 id。这可以工作,但我有几个担忧。

首先,它容易出错。脚本仍可能意外创建全局状态,这可能导致稍后在同一脚本甚至其他脚本中难以调试问题。

其次,由于 update 和 tick 函数本身是全局的,所以当我创建第二种类型的敌人并尝试使用相同的界面时,我仍然会遇到问题。我想我可以通过某种命名约定来解决这个问题,但肯定有更好的方法来处理它。

我确实找到了this 问题,它似乎在问同样的事情,但接受的答案是对细节的轻描淡写,并且指的是 Lua 5.3 中不存在的 lua_setfenv 函数。似乎它已被 _ENV 取代,不幸的是我对 Lua 不够熟悉,无法完全理解和/或翻译这个概念。

[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 的人最不意外的路径。即,我使用隐式 self 参数和我的原型/实例之间的区别。我有正确的想法还是做了一些奇怪的事情?

【问题讨论】:

    标签: lua game-engine


    【解决方案1】:

    对于您的第一个问题(意外创建全局变量),您可以依赖像 luacheck 这样的 linter 或阻止您创建像 strict.lua from Penlight 这样的全局变量的模块。

    然后,为什么不把事情本地化呢?我的意思是time_since_last_shoottick。这利用了闭包,这是 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()
    

    当然,你也可以使用环境来解决这个问题,但这里我认为局部变量和闭包是解决问题的更好方法。

    【讨论】:

      【解决方案2】:

      _ENV 在 5.3 中的工作方式是全局变量是用于从 _ENV 变量中读取字段的“语法”糖。例如,一个程序

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

      等价于

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

      默认情况下,_ENV 是一个“全局表”,其工作方式与您期望全局变量的行为方式相同。但是,如果您创建一个名为 _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 在您分配给未声明的全局变量时警告您。至于第二个问题,您可以将 update 和 tick 函数放在一个表中,而不是让它们是全局的。

      【讨论】:

      • 谢谢@hugomg。我根据您的最后建议提出了另一个建议的解决方案并编辑了我的问题。它看起来像你预期的那样吗?
      • Lua 在很大程度上是一种“有不止一种方法可以做到”的语言,所以只要做你认为对你的用例更好的事情。如果你对“自我”感到好奇,Lua 没有内置的面向对象支持,所以明确的自我是要走的路(尽管你可能想使用: 语法糖只是为了让它看起来更好一点)。也许你应该看看《Lua 编程》中的面向对象章节。
      • 太棒了!我确实阅读了那一章,但我仍然不清楚 Lua 认为实例与原型的区别,因此我不确定我为自己提供的内容是否符合人们的期望。例如,如果有人要在baddie.lua 的某个地方执行baddie.foo = 3,然后尝试在“tick”中使用self.foo,那将是零。但我相信我可以通过使用 __index 元方法来解决这个问题?但是,如果你觉得它通常看起来不错,那么我很满意。
      • Lua 中的原型是一种优化形式。 Lua 中的对象只是一个表,实例变量和方法只是该表中的字段。如果您可以有许多属于同一个“类”的对象,那么它们的实例变量将不同,但它们的方法都是相同的。不是在每个对象中单独存储对方法的引用,而是有一个存储方法的原型,然后每个对象只需要使用它的元表来指向这个原型。最终结果是 obj.themethod 和以前一样是一个函数,但您使用的内存更少。
      • 我喜欢将存储在 __index 表中的方法视为静态方法:相同的函数在所有实例之间共享,并且无法直接访问调用它的对象,除非您传递该对象作为它的一个论点,: 只是语法糖。但是,如果您想要“普通”方法,一个用于类的每个实例并直接访问对象及其私有属性的方法,您必须在“init”方法中声明它们并将私有变量存储为它们的上值。这意味着您不会“使用更少的内存”,您只是被激励使用静态方法。
      猜你喜欢
      • 1970-01-01
      • 2013-04-06
      • 2010-09-25
      • 2019-01-09
      • 2014-08-06
      • 1970-01-01
      • 2011-05-27
      • 2020-10-26
      • 1970-01-01
      相关资源
      最近更新 更多