Parker Engine 中文文档

Lua 的 this 代理(LuaProxy)

this 是什么

每个 Lua 脚本在创建时都会拿到一个全局 this

// FunkinLua 构造函数(source/psych/script/FunkinLua.hx:220)
setProxy('this', getScriptState());

getScriptState() 返回的是:

场景 this 指向
歌曲内运行的脚本 PlayState.instance
LuaSState 启动的菜单脚本 当前菜单状态(FunkinLua.currentMenuState,回退到 menuOwner / FlxG.state

这是 Psych 0.7 遗留下来的 this,Parker Engine 把它从「属性快照」改成了实时代理

为什么要用代理

引擎把 Haxe 值交给 Lua 走的是 llua.Convert.toLua()。它的行为是「把实例的字段转成一张一次性的表」,而且只递归一层——表里再遇到对象字段就变成 nil。所以老实现里:

-- 旧行为(现在是错的写法带来的观感)
this.camGame:flash(0xffffffff, 1)   -- camGame 是 nil,直接报错

LuaProxy 换了个思路:表里只放一个数字 id(字段名 __pklua_ref),真正的读写都通过元表回到 Haxe 侧的活对象上。于是

this.camGame:flash(0xffffffff, 1)        -- 真的闪白 camGame
this.camGame.zoom = 1.2                  -- 真的改了相机缩放
this.boyfriend:playAnim('idle')          -- 真的播放 BF 的 idle
this.boyfriend.animation.curAnim.name    -- 真的读得到当前动画名
local hp = this.health                   -- 读任意 PlayState 字段

都能落到真正的引擎对象上。

注意

this.boyfriend:playAnim('idle') 用的是 Character.playAnim(anim, forced, reverse, startFrame),而不是 spr.animation.play——所以角色偏移、sing 时长等逻辑都会正常生效。

语法限制:this:camGame:flash(...) 是语法错误

这是 Lua / Luau 语言本身的限制,引擎无法兼容。

a:b(...) 这种冒号语法必须是方法调用a 只能是「对象」,b 只能是方法名。而下面这种写法:

this:camGame:flash(0xffffffff, 1)   -- ✗ 解析错误(parse error)

在解析阶段就会被拒绝:解析器读到 this:camGame 后会期待一个函数调用,紧接着又出现 :flash(...),语法不通。这和 this 是不是代理表无关——换成任何普通表也一样报错。

正确写法只有两种:

this.camGame:flash(0xffffffff, 1)   -- ✓ 点号取字段,冒号调方法
this.camGame.flash(this.camGame, 0xffffffff, 1)  -- ✓ 等价显式写法(一般不需要)

代理的读写规则

  • 读字段__indexLuaProxy.luaIndex()Reflect.getProperty()
  • nil / 数字 / 字符串 / 布尔 → 原样返回值。
  • 函数 → 交回一个闭包,调用时走 __pklua_invoke()
  • 其他对象 / 数组 / Map / Class → 再包一层代理表。
  • 读失败(Haxe 抛异常)会在屏幕上红字报告 LuaProxy: could not read "xxx" ...,然后把 nil 返回给脚本。
  • 写字段__newindexLuaProxy.luaSetProp()
  • Map 走 set(),数组按数字下标赋值(t[1] 对应 Haxe 的 [0],越界会 push)。
  • 其他对象走 Reflect.setProperty()
  • 引擎对象上没有这个字段时写操作返回 false,于是 Lua 侧的 __newindexrawset 把它留在代理表里。好处是脚本自己的记账字段(spr.myFlag = true)不会报错;代价是拼错字段名也不会报错,值只是留在 Lua 那张表里,永远到不了引擎。写失败被刻意静默(源码注释:「this is not a failure worth a screen full of red text on every spr.customFlag = true」)。
  • 调用方法obj.method(args)obj:method(args) 都可用。Haxe 的成员函数(HX_DEFINE_DYNAMIC_FUNC*)本身已经绑定了对象,所以引擎侧不传 this;Lua 侧的闭包还会检测「第一个参数是不是指向同一个 id 的代理表」,是就先丢掉——这就是冒号写法能工作的原因。
  • Class:new(...) / Class.new(...):如果代理指向的是一个(不是实例),读 new 会得到一个构造用的函数,调用它会 Type.createInstance() 并返回新实例的代理。读类上的其他字段时,引擎只查静态字段Type.getClassFields()),因为 hxcpp 下随意读类字段会白白 new 一个实例出来。
  • 数组:只有 index > 0 才会被当作数组下标(t[1] → Haxe arr[0]);#t__len)只对数组有意义,其他对象返回 0tostring(t)__tostring)返回 Std.string(实际值)
  • 同一个 Haxe 对象总是同一张代理表refIds 按对象身份缓存,__pklua_wrapped 又用弱表缓存,所以同一个 Lua 状态里 this.boyfriend == this.boyfriend 为真。this.boyfriend 和其它来源的同一个对象不保证相等(不同脚本的 Lua 状态、或另一条注入路径),比较对象请比较名字/标签而不是表本身。
  • 代理可以作为参数传回引擎LuaProxy.unwrap() 会把带 __pklua_ref 的表还原成真正的 Haxe 对象(数组会逐项还原)。所以 setProperty('x.y', this.boyfriend) 这类写法是可行的。

生命周期与范围

  • 只有 this 是代理。 引擎里 FunkinLua.set() 绑定的其它全局(curStepcurBeatbpmscorehealthdefaultPlayerStrumX0 等等)都是基本类型快照,不是代理。更重要的是:在本分支里 camGame / camHUD / boyfriend / dad / gf / notes / strums 这些对象只通过 setOnHScripts()(HScript)和 setOnScripts()(LScript + Python)注入PlayState.setOnScripts() 的实现里只列了这两个),Lua 脚本并没有这些全局名。所以在 Lua 里:

lua camGame.zoom = 1.2 -- ✗ camGame 是 nil this.camGame.zoom = 1.2 -- ✓ 走代理 setProperty('camGame.zoom', 1.2) -- ✓ 走名字查找

也就是说,Lua 侧访问活对象有两条路:this.<字段> 代理,或者名字查找——getProperty / setProperty / playAnim / getObjectOrder / screenCenter 这类函数接受的对象路径,最终都会回落到 Reflect.getProperty(PlayState.instance, 名字),所以直接传 'camGame''boyfriend''dad''gf' 是有效的。 - id 只在脚本存活期间有意义FunkinLua.stop()(脚本被停止、场景销毁、或 close() 后回调返回)会调用 proxy.dispose(),清空 refs / refIds。此后再用一张留着没放的代理表,读会得到 nil、写只会 rawset 到那张表上。 - 桥装不上时不会让脚本崩install() 里解析或执行 Lua 预置代码失败时,会在屏幕上红字报 LuaProxy: could not ...,代理表保持惰性(读写都失败),脚本其余功能照常。 - 占用了一批全局名:桥会向脚本的全局表写入 __pklua_proxy__pklua_wrap__pklua_decode__pklua_wrapped__pklua_index__pklua_invoke__pklua_setprop__pklua_len__pklua_tostring自己的脚本不要用这些名字,否则桥会被破坏。 - 同样的机制也用在 .lscriptscript/FunkinLScript.hx 用它推送值,所以 .lscriptthis 本来就是「真状态」。这次改动只是让 .lua.lscript 行为一致。

常见写法对照

目的 写法
闪白主相机 this.camGame:flash(0xffffffff, 1)
改相机缩放 this.camGame.zoom = 1.2
让 BF 播放 idle this.boyfriend:playAnim('idle')
读当前动画名 debugPrint(tostring(this.boyfriend.animation.curAnim.name))
读血量 local hp = this.health
改血量 this.health = 1.5
让对手跳舞 this.dad:dance()
读第 3 个 strum 的 x local x = this.strumLineNotes.members[3].x
调 PlayState 的方法 this:startCountdown()this.startCountdown(this)
访问菜单态的成员 this.members(菜单脚本里 this 是那个状态本身)

反例(不要写)

this:camGame:flash(0xffffffff, 1)   -- ✗ 语法错误
boyfriend:playAnim('idle')          -- ✗ Lua 里没有 boyfriend 这个全局
camGame.zoom = 1.2                  -- ✗ Lua 里没有 camGame 这个全局
this.boyfreind.x = 0                -- ✗ 拼错字段名不会报错,值只会留在 Lua 表里