Skip to content

[教程] 使用自定义回调⚓︎

忏悔1.79b更新中加入了一些供回调使用的函数,使用它们可以为mod中的自定义回调添加优先级与兼容性。

基本回调⚓︎

你可以将任意代表回调ID的值与 mod:AddCallback (或者 Isaac.AddCallback, 但我们更推荐使用mod表,即前者)搭配使用。这些值可以是任意类型,包括字符串。

1
2
3
MOD:AddCallback("TEST", function(_, a, b, c)
    print("test callback triggered", a, b, c)
end)

就实际情况而言,我们更推荐使用字符串而非数字作为自定义回调的ID. 虽然原版游戏提供的mod API中回调有对应的数字ID, 但使用字符串可以避免与其他mod所添加的自定义回调产生冲突。

但仅仅输入这串代码不会起到任何作用,因为游戏中(或者mod里)没有任何事件可以触发"test"回调使其执行。为了使这一回调执行,你应该输入:

1
Isaac.RunCallback("TEST", 1, 2, 3)

这条代码会按优先级和添加的先后顺序(首先判断优先级,然后判断先后)执行所有ID为"test"的自定义回调。

注意一点:在这一过程中我们不必“定义”添加的回调。mod可以仅通过使用自定义回调对应的ID的值来将其执行,无需任何前置定义。

返回值⚓︎

通过Isaac.RunCallback执行回调时,若回调返回了任意值,该函数将在首个返回了值的回调处终止执行,并返回该值。 示例如下:

1
2
3
4
5
6
7
MOD:AddCallback("TEST_RETURN", function(_, a, b)
    return a + b
end)
MOD:AddCallback(ModCallbacks.MC_POST_GAME_STARTED, function()
    local ret = Isaac.RunCallback("TEST_RETURN", 1, 2)
    print(ret)
end)

输入这串代码后,控制台和日志文件应当在每次游戏开始时显示"3".

实体类型/参数对应⚓︎

游戏mod API中提供的回调有部分需要特定参数才会执行,我们也可以如此对自定义回调加以限制。任意你设想的情景中所必须的值都可以作为特定参数,如实体类型或实体变种等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
-- 添加自定义回调
MOD:AddCallback("TEST_ENTITY", function(_, entity, a, b, c)
    print("test callback triggered", entity.Type, a, b, c)
end)

-- 为自定义回调添加限制参数
MOD:AddCallback("TEST_ENTITY", function(_, entity, a, b, c)
    print("tears only callback triggered", entity.Variant, a, b, c)
end, EntityType.ENTITY_TEAR)

-- 执行有限制参数的自定义回调

-- 此函数将执行所有自定义回调,无视限制参数
Isaac.RunCallback("TEST_ENTITY", 1, 2, 3)
-- 此函数将不执行“仅眼泪”(限制参数为EntityType.ENTITY_TEAR)的自定义回调
Isaac.RunCallbackWithParam("TEST_ENTITY", EntityType.ENTITY_PLAYER, entity, 1, 2, 3)
-- 此函数将执行“仅眼泪”回调,而非一个ID不同的回调
Isaac.RunCallbackWithParam("TEST_ENTITY", EntityType.ENTITY_TEAR, entity, 4, 5, 6)

正如前文所说,特定参数可以是任意值,不必局限于实体类型。Isaac.RunCallbackWithParam 函数会逐个检查所有回调的限制参数是否为nil(无限制),若不为nil则检查其是否与该函数的第二个变量相对应。

Mod兼容⚓︎

之前讨论过,自定义回调不必先“定义”后使用。创建自定义回调只需要Isaac.RunCallback 执行至少一次且与AddCallback搭配。

这使得mod可以十分轻松地为其他mod提供自己的API: 依赖该mod运行的其他mod无需等待主mod完全加载便可使用其API. 假如有这么一个小地图mod,暂且称其为MinimapLibrary(小地图库)。现在这个mod希望其他依赖其运行的mod能够在小地图变换大小后执行特定代码。 MinimapLibrary中的代码如下:

1
2
-- ... 小地图变换大小时执行的回调
Isaac.RunCallback("MINIMAPLIB_POST_MINIMAP_ENLARGE", nil, currentSize)

依赖其运行的mod(Mod 2)代码如下:

1
2
3
4
MOD2:AddCallback("MINIMAPLIB_POST_MINIMAP_ENLARGE", function(_, currentSize)
    print("Minimap size has changed!", currentSize)
    -- 以currentSize为变量做些事情
end)

可以看出Mod 2在使用MinimapAPI提供的mod方法时,完全不需要检查MinimapAPI是否加载。原因很简单:前置mod已加载则执行,尚未加载则不执行。完全不需要等到前置库/API mod完全加载才执行——前置mod定义了对外提供的mod方法时才需要先加载再使用。

执行模式⚓︎

通常情况下Isaac.RunCallback会在首个返回了值的回调处中断并返回该值,但如果你需要别的方法处理回调的执行与返回模式,那你需要通过Isaac.GetCallbacks手动修改。

如果你想让回调继续执行,并且用最后得出的返回值覆盖其首个参数,可以这么做:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
MOD:AddCallback("TEST_GETCALLBACKS", function(_, arg) 
    return arg + 2
end)
MOD:AddCallback("TEST_GETCALLBACKS", function(_, arg) 
    return arg * 2
end)

local thisArg = 10
local callbacks = Isaac.GetCallbacks("TEST_GETCALLBACKS")
for _, callback in ipairs(callbacks) do
    local ret = callback.Function(callback.Mod, thisArg)
    if ret ~= nil then
        thisArg = ret
    end
end

print(thisArg) -- 显示 24

Isaac.GetCallbacks会返回一张包含所有回调的表,并按回调优先级与添加的先后顺序自动进行排序,所以无需担心循序问题(除非你想改)。表中元素结构如下所示:

1
2
3
4
5
6
{
    Mod = <mod table>,
    Function = function(mod, callback args),
    Priority = integer (default 0),
    Param = entity id / other param (default -1),
}

若该回调尚不存在,向Isaac.GetCallbacks的第二参数中传入true值可以为其分配一张空表。

进阶参数⚓︎

既然通过Isaac.GetCallbacks可以得到该回调对应的表,那自然可以为其分配一张元表。此过程中,游戏不再使用默认方式(使用==的判定方式)进行检验,而是特别使用一个新函数,传递一个不同的参数并进行判断。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
--使用带自定义参数的函数初始化自定义回调
--在Isaac.GetCallbacks中传入true作为其第二个参数,若该回调不存在则为其分配一张空表
--参数可以是任意类型,但在此例中我们将表作为参数
--若任意参数为nil则总是匹配
setmetatable(Isaac.GetCallbacks("TEST_PARAMS_2", true), {
    __matchParams = function(a, b)
        return not a or not b or (type(a) == "table" and type(b) == "table" and a[1] == b[1] and a[2] == b[2])
    end
})

--该回调无参数,每次都会触发
MOD:AddCallback("TEST_PARAMS_2", function()
    print("hi!")
end)

--这些回调有参数,只会在参数与提供给Isaac.RunCallbackWithParam的一致时触发
--使用__matchParams这一元方法检测参数是否匹配
MOD:AddCallback("TEST_PARAMS_2", function()
    print("hello world")
end, {"hello", "world"})

MOD:AddCallback("TEST_PARAMS_2", function()
    print("greetings earth")
end, {"greetings", "earth"})

MOD:AddCallback("TEST_PARAMS_2", function()
    print("howdy globe")
end, {"howdy", "globe"})

--这样做应该只会显示“hi!”,然后显示“hello world”
Isaac.RunCallbackWithParam("TEST_PARAMS_2", {"hello", "world"})

(案例提供: _Kilburn)

唯一回调ID⚓︎

强烈推荐在自定义回调的ID中添加独特前缀,最好与你的mod相关。这样可以避免与其他mod的自定义回调重名,导致冲突。例如,如果mod名叫AchievementLibrary,自定义回调应该取名为ACHLIB_TEST_CALLBACK,或其他类似的命名。

若想让自定义回调即使重名也具有唯一的ID,比方说存在于其他mod中的库mod,且该回调基于其逻辑应当运行不止一次,那就可以使用表引用作为其ID(使用字符串ID会导致其他同名回调也运行不止一次)。示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
MyLibrary.Callbacks.TEST_CALLBACK = {}
MyLibrary.Callbacks.TEST_CALLBACK_2 = {}

MOD:AddCallback(MyLibrary.Callbacks.TEST_CALLBACK, function
() 
    print("TEST 1")
end)
MOD:AddCallback(MyLibrary.Callbacks.TEST_CALLBACK_2, function()
    print("TEST 2")
end)

Isaac.RunCallback(MyLibrary.Callbacks.TEST_CALLBACK)
Isaac.RunCallback(MyLibrary.Callbacks.TEST_CALLBACK_2)

由于每个表引用都是唯一的,因此每张表都会被视作不同的ID,进而避免冲突。这么做有个优点:每个回调都具有唯一的局部定义ID,不会与可能重名的字符串共享全局空间。


Last update: May 13, 2023