V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Game Engines
Unreal Engine
MyCryENGINE
etwxr9
V2EX  ›  游戏开发

写 mod 时遇到个 lua 的问题,写了一千多字问题描述,把思路理死了。。

  •  
  •   etwxr9 · 169 天前 · 1764 次点击
    这是一个创建于 169 天前的主题,其中的信息可能已经有所发展或是发生改变。

    试图用小黄鸭法理顺思路,但是最终结果是真的没搞懂。

    我在写一个 mc 的服务端插件,它用的是 java 语言。但是为了能够让插件使用者能够自定义一些游戏物品功能逻辑,我加入了 lua 作为脚本语言。

    比如可以写一个盾牌物品的脚本 shield.lua

        function shield.onPlayerDamage(e)
            e.SetDamage(0)
        end
    

    意思是当玩家受伤时,将伤害设置为 0

    这个 onPlayerDamage 是游戏中的事件,可以在 java 中订阅。
    在插件初始化的时候,会读取配置目录下所有的物品 lua,并且将每个这样的 lua 函数存入一个管理类中,在插件的相应事件触发时,管理类就会调用 lua 中的同名函数(同时也要在 java 中判断玩家是否具有此盾牌物品,再执行)

    到此为止还是没问题的,但是我希望这些物品能存局部变量,比如

        function shield.onPlayerDamage(e)
            self.a = self.a - 1
            if (self.a <= 0) then
                e.SetDamage(0)
            end
        end
    

    这样可以实现计数功能,比如只能抵挡 3 次伤害。
    但是问题就在于,如果有多个玩家同时拥有此盾牌,这些玩家就会共享变量 a,从而产生冲突。

    接下来我试着在一个新盾牌生成时,插件可以以 shield.lua 为原型,New 一个 luaTable,复制 shield.lua 中的所有内容( lua 的 OOP 方式),以此达成每个盾牌实例拥有独立的局部变量,这种情况下,处理每个盾牌的逻辑的并非 shield 这个表,而是一个以 shield 为原型的新表(假定它叫 shield_A )。
    但是这样一来,事件管理类在调用 lua 中事件函数时,就无法调用这个 shield_A,而还是调用的是最初加载的 shield.lua 的函数,从而就无法操作到特定的变量 a

    接下来我试着每当 new 一个 luaTable,就重新在该 luaTable 中查找事件函数,并加入事件管理类
    但是这样一来,事件触发时就必须判断每个 luaTable 对应的是哪个玩家,比如玩家 A 就调用 shield_A,玩家 B 就调用 shield_B 。不然一个玩家受伤,所有盾牌的变量都变了。这就难以做到。
    而且在该盾牌消失之后,我又得把对应的函数从事件管理类中移除,这又难以做到。

    再者,我又遇到另一个问题。我希望这个 luaAPI 具有一些全局函数可供调用,比如 Game.HealPlayer(hp)
    这个 Game 内含一些常用的全局函数。问题比较类似,也是面向对象的。

    Game 不能只有一个,该插件有一个“游戏房间”的概念,每个玩家通过创建游戏房间进行游玩,每个房间游戏中只有一个玩家。
    那么,我希望 Game 是可以处理房间内逻辑和数据的,比如当前房间的难度、计时器等等,这就要求在创建游戏房间时,也得创建一个对应的 Game 实例。我们假设创建了一个游戏房间 A,并且带有一个 Game_A 的 Game.lua 的复制。

    假设我们不仅有 shield.lua ,还有回血药水 potion.lua ,那么 potion.lua 中就要调用 Game.HealPlayer(hp)
    该 potion 自然也是 potion.lua 的一个复制(比如叫做 potion_A ),而它调用的 Game_A 也应该是 Game.lua 的一个复制,并且两者同属于一个游戏房间。那么在 potion.lua 中就不能直接写 Game.HealPlayer(hp),那样调用的就是 Game.lua 原型而不是 Game_A 。

    然而因为游戏房间都是动态创建的,写 potion.lua 中的 API 调用时就没法写出对应的表名。

    所以我卡在这里了,我是看到很多游戏都在用 lua 做 modding 脚本语言我才试着学一下,但是看了几个游戏的 mod 教程,只看到他们就是能做到方便的 lua 逻辑配置,但就是看不出来他们是怎么处理这些问题的。

    5 条回复    2021-07-10 21:26:19 +08:00
    eason1874
        1
    eason1874  
       169 天前
    没做过 mc mod,做过一些网页游戏,原理应该差不多。

    目测你的第一个问题是把玩家实例属性存到了插件实例上面,然后顺着掉坑里研究起了插件多实例。onPlayerDamage(e) 里的 self 应该是指插件实例吧?如果是这样,问题就是我说的,解决方法就是把计数的变量 a 存到 player 上。看起来传入的 e 就是指 player,怎么存你可以看文档去确定。

    第二个问题其实类似。每个房间创建一个实例,玩家进入的时候把玩家 ID 记录到当前房间,治疗玩家的时候,先定位当前房间的玩家,再对玩家实例进行操作,就不会冲突。
    GeruzoniAnsasu
        2
    GeruzoniAnsasu  
       169 天前
    我写过一点 factorio 的 mod,应该可以一定程度相互借鉴启发:
    https://wiki.factorio.com/Tutorial:Modding_tutorial/Gangsir

    你要明白 lua 脚本是不直接控制游戏中实体的实例化的,所以一定有什么接口来获取你要的实体,然后拿获得的那个实体再进行你的逻辑

    lua 和 js 有点类似,是基于 prototype 的 (伪) OO,但实际上你写的脚本都是 class,并不直接管理实际对象的生命周期,除非真的没有其它办法要自己实现,比如定义一个全局表,然后在事件回调里获取 player 对象去更新全局表,其它 model 的逻辑里去这个全局表里查来替代不存在的 getXxxAttributes,否则都应该先考虑框架提供的 api 能不能实现需求


    话说你给的例子逻辑其实有点奇怪的,game.healplayer 看起来并不能关联到哪个 player,感觉是一个给当前玩家操控的角色加血的 api,而不是“给某个 character 增加体力”

    在 factorio 的那个 tutorio 里,如果要给它的示例 fire armor 加上跟你所述的类似功能,首先得找到这个 item 的实例,好然后我翻了一下文档,这个实例应该由 LuaItemStack 来表示,然后参考它获取当前 player 是否穿着 armor 的代码,翻到 [LuaInventory.find_empty_stack]( https://lua-api.factorio.com/latest/LuaInventory.html#LuaInventory.find_empty_stack) 有了 LuaItemStack 的实例,它有一个方法叫 [set_tag]( https://lua-api.factorio.com/latest/LuaItemStack.html#LuaItemStack.set_tag) ,想必通过这个方法就能把值存到玩家身上的物品实例中去了,这个逻辑写在 on_damage 这个全局回调里,全程都不会用自己实现(比如全局 /临时变量)的存储



    ----

    重新看了一遍问题描述,似乎在写的是 lua 的宿主? 那只需要提供好回调和 api 就好了,lua 里需要持久化的值直接写到 kv 数据库里
    etwxr9
        3
    etwxr9  
    OP
       169 天前
    @GeruzoniAnsasu
    @eason1874

    感谢回答
    我大致有些新的思路了,看来确实有必要在 lua 中操作和传递一些实例的。

    我之前做过 starbound 的 mod,例如一个回血 buff 的 lua 文件,它直接调用 animator.setParticleEmitterActive("healing", true),这个 animator 不需要传入玩家实例参数,就能直接把效果加到具有该 buff 的玩家身上。starbound 的 luaAPI 中全局函数都是这样,写起来非常简洁,我就蛮好奇底层是怎么实现的。

    而且可能有误解的地方,就是我现在写的是一个 mc 插件,这个插件是用 java 写的,官方提供的是 java 的框架。
    而我是想给里面插入 lua 配置的功能,所以相当于我在从零开始搞这个底层为 java 的 luaAPI (用的是 luaj )

    总之我回头去写一下测试测试再说。
    Baleine
        4
    Baleine  
       168 天前
    正好也在用 luaj 在 Minecraft 里做类似的事情。

    这边的解决方案是给每一个玩家一个对应的数据实例,并在调用 LuaValue::call 之前将这个实例作为 lua 脚本的变量传递进去。

    类似于:
    Globals globals = JsePlatform.standardGlobals();
    LuaValue luaPlayer = CoerceJavaToLua.coerce(dataInstance);
    globals.set("data", luaPlayer);

    其中 dataInstance 是对应的数据实例,"data"则是变量名。

    在 lua 中可以直接调用实例中的成员方法,所以其实 API 也可以用类似的操作传递进去。
    etwxr9
        5
    etwxr9  
    OP
       144 天前
    @Baleine
    没理解错的话,我最后也是这样做的。
    我给每个游戏副本流程单独创建了一个 Global 全局环境,用它加载一遍所有 lua 脚本,一个副本一个全局,互不干扰,这样就不需要额外创建 lua 表的实例了。
    而至于那个盾牌的问题,最后我还是用物品上的数据储存解决了问题。
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1812 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 00:14 · PVG 08:14 · LAX 16:14 · JFK 19:14
    ♥ Do have faith in what you're doing.