KingdomRushDove · 最后更新:2026
KingdomRushDove 内置了一套轻量级的模组系统,允许开发者在不修改游戏源码的前提下,通过覆盖资产、 修改模板数据、拦截函数调用等方式对游戏内容进行扩展或修改。
模组系统由以下核心模块组成:
mod_main.lua —
模组管理器,负责发现、加载和初始化所有模组
mod_db.lua —
模组数据库,扫描并维护可用模组列表
mod_hook.lua —
内置系统级钩子(图像、声音、关卡、波次资产覆盖)
hook_utils.lua — 钩子工具库,供模组使用
mod_utils.lua — 通用工具函数库mod_globals.lua — 注入模组可用的全局变量
所有模组均放置在
mods/local/
目录下,每个模组占据一个独立的子目录:
.lua)。 模组 ID
必须全局唯一,且只能包含字母、数字和下划线。
data/ 下的子目录会被特殊处理:data/kui_templates
会自动以最高优先级注册到 KUI 模板路径,其他子目录则加入
require 路径。
_assets/ 目录不会加入
require 路径(由系统忽略)。
每个模组的根目录下必须有一个
config.lua,返回一个配置表:
return {
name = "我的模组", -- 显示名称(可含中文)
entry = "my_mod", -- 模组唯一标识符,只能含字母/数字/下划线
version = "1.0.0",
game_version = {"kr5", "kr3", "kr2", "kr1"}, -- 支持的游戏版本,必须为字符串数组
desc = "模组描述",
url = "https://example.com", -- 发布链接,可为空字符串
by = "作者名", -- 必须与插件商店账户名相同
category = "other", 插件类型,详见下表
enabled = true, -- false 则跳过加载
priority = 0 -- 数值越大优先级越低,不确定填 0
}
| 字段 | 类型 | 说明 |
|---|---|---|
name |
string | 模组显示名称(可含中文) |
entry |
string |
模组唯一标识符,也是模组目录名和入口文件名(不含
.lua)。只能包含字母、数字和下划线。
上传到插件商店时以此命名 zip 文件,zip
内必须能直接找到同名 .lua 文件。
|
version |
string | 模组版本号 |
game_version |
string[] |
兼容的游戏版本,用于在加载前校验。可选值:"kr1"
"kr2" "kr3"
"kr5"
|
desc |
string | 模组描述 |
url |
string | 模组发布页链接 |
by |
string | 作者名。上传到插件商店时,此字段必须与登录账户的用户名完全一致。 |
category |
string | 插件类型。可选项:"gameplay"(玩法), "cosmetic"(美化), "display"(显示), "tower"(防御塔), "hero"(英雄), "enemy"(敌人), "level"(关卡), "other"(其它), |
enabled |
boolean | false 时跳过加载 |
priority |
number |
优先级,数值越小越先初始化(覆盖力越强),默认
0
|
config.lua 和
$entry.lua(即 entry 字段值加
.lua)两个文件。zip
文件本身的名称不限,可以使用中文。上传需要先在插件商店注册账户并登录,且
by 字段必须与登录用户名相同。
模组按
priority
升序排列。数值小的模组最后初始化,因此其钩子会覆盖数值大的模组。
多个模组同时存在时,优先级越小(数值越低)的模组"胜出"。
入口文件名必须与模组目录名相同(如目录为
my_mod/,入口文件为 my_mod.lua)。
文件必须返回一个 table,且该 table 必须包含
init 方法。
local hook_utils = require("hook_utils")
local HOOK = hook_utils.HOOK
local hook = hook_utils:new()
-- 必须实现:模组初始化入口
function hook:init(mod_data)
self.mod_data = mod_data
-- 在此注册钩子、修改模板数据等
HOOK(E, "load", self.E.load)
end
function hook.E.load(load, self)
load(self) -- 先调用原函数
require("my_mod_templates")
end
return hook
mod_data 是由系统传入的模组数据表,常用字段:
| 字段 | 类型 | 说明 |
|---|---|---|
mod_data.name |
string | 模组目录名(即模组 ID) |
mod_data.path |
string | 模组根目录的完整路径 |
mod_data.config |
table | config.lua 返回的配置表 |
mod_data.priority |
number | 模组优先级 |
mod_data.check_paths |
table | 检测到的资产覆盖路径映射表 |
钩子系统允许模组拦截并扩展任意对象上的函数,而无需直接替换它。 多个模组可以对同一函数注册多个钩子,系统会以链式方式依次调用。
local hook_utils = require("hook_utils")
local HOOK = hook_utils.HOOK
-- HOOK(对象, 函数名, 处理器函数 [, 优先级])
HOOK(SomeObj, "some_method", function(original_fn, self, arg1, arg2)
-- 可在调用原函数前执行逻辑
local result = original_fn(self, arg1, arg2) -- 调用原函数(或下一个钩子)
-- 可在调用原函数后执行逻辑
return result
end)
处理器函数的签名:function(next_fn, self_or_first_arg, ...)
next_fn —
调用下一个钩子(或原函数)的函数,必须调用以维持调用链
0,数值越小越先执行
推荐使用
hook_utils:new()
创建实例,将处理器存放在实例的命名空间下,便于管理:
local hook = hook_utils:new()
function hook:init(mod_data)
HOOK(E, "load", self.E.load) -- 钩子处理器定义在 hook.E.load
HOOK(S, "play", self.S.play)
end
-- 处理器:self.E.load(使用嵌套命名空间隔离)
function hook.E.load(load, self)
load(self)
-- 自定义逻辑
end
hook_utils:new()
使用了自动创建子表的元表,访问不存在的键会自动创建空表,
因此可以直接写
hook.E.load = function(...) end 而不会报错。
hook_utils.UNHOOK(SomeObj, "some_method", handler_function)
传入与注册时相同的函数引用即可移除对应钩子。
hook_utils.CALL_ORIGINAL(SomeObj, "some_method", arg1, arg2)
绕过所有钩子,直接调用被钩住前的原始函数。
注册时
priority 越小,越先被执行(越外层包装)。
最后注册且优先级最高的钩子最先被调用,最终才调用原始函数。
模组 priority 越小(数值低),其
init 越晚执行,注册的钩子也越靠外。
系统在加载图像、声音、关卡数据、波次数据时会自动检测模组目录下对应路径是否存在覆盖文件, 若存在则以模组版本替换原始资源。无需手动注册,只需按规定目录放置文件即可。
路径:_assets/images/<atlas_name>.lua
当游戏加载名为
atlas_name 的图集时,若模组在对应路径存在同名
.lua
文件,则图集会被模组版本覆盖。图集文件格式与原始游戏一致。
| 路径 | 作用 |
|---|---|
_assets/sounds/settings.lua |
覆盖声音源分组设置(source_groups)
|
_assets/sounds/sounds.lua |
完全替换声音定义表 |
_assets/sounds/groups.lua |
完全替换声音分组表 |
_assets/sounds/extra.lua |
增量追加声音与分组(推荐,不破坏其他模组) |
_assets/sounds/files/ |
覆盖声音文件(实际音频) |
return {
sounds = {
my_sound = { files = {"my_sound.ogg"} }
},
groups = {
-- append = true 表示追加到现有分组
existing_group = { append = true, files = {"extra.ogg"} },
-- alias 表示复用另一分组
another_group = { alias = "existing_group" },
-- 直接赋值则替换整个分组
new_group = { files = {"new.ogg"} }
}
}
路径:data/levels/<level_name>.lua
覆盖关卡的 data 和
locations 字段,其余字段保持原样。
路径:data/waves/<level_name>.lua
完全替换指定关卡的波次路径数据。
以下函数和变量由
mod_globals.lua 注入,在模组代码中可直接使用:
| 变量 | 说明 |
|---|---|
IS_KR5 |
当前是否运行 kr5 版本 |
IS_LOVE_11 |
是否使用 LÖVE 11+ |
E |
entity_db 实例,管理实体模板 |
UPGR |
upgrades 模块 |
SH |
shader_db 实例 |
V |
vector 工具库(V.v,
V.vv)
|
signal |
信号/事件系统 |
class |
middleclass OOP 库 |
km |
宏工具(klua.macros) |
bit / band / bor / bnot |
位运算 |
copy |
table.deepclone 的别名 |
clone |
table.clone 的别名 |
storage |
all.storage 持久化存储 |
SU |
script_utils 脚本工具 |
U |
utils 通用工具 |
-- 注册新模板(name: 模板名, ref: 派生自哪个已有模板)
RT("my_tower", "base_tower")
-- 获取已有模板的引用(可直接修改其字段)
local t = T("hero_alleria")
t.motion.max_speed = 3.5 * FPS
-- 为模板添加组件
AC("my_tower", "comp_health", "comp_armor")
-- 深拷贝一个组件(避免多个模板共享同一表)
local c = CC("comp_health")
-- 创建实体实例
local e = create_entity("my_tower")
-- 将实体加入插入队列
queue_insert(store, e)
-- 将实体加入移除队列
queue_remove(store, e)
-- 将伤害实体加入伤害队列
queue_damage(store, damage_entity)
-- 帧数转秒数(基于当前 FPS)
local seconds = fts(30) -- 30 帧 → 对应秒数
-- 角度转弧度
local rad = d2r(90) -- 90° → π/2
通过 require("mod_utils") 获取。
批量更新动画数据库。传入一个动画定义表,函数会将其合并/删除到全局动画数据库
A.db 中。
mod_utils.a_db_reset({
-- 修改现有动画(合并)
hero_idle = { fps = 12 },
-- 删除动画
hero_run = { removed = true },
-- 图层展开(layerX 会被展开为 layer1, layer2, ...)
effect_layerX = {
layer_from = 1, layer_to = 3,
layer_prefix = "effect_%d_",
fps = 24, group = "effects"
}
})
对表中某字段乘以
factor。若字段是数组则逐元素乘。is_int
为 true 时向上取整。
-- 将绿兵的血量 * 1.5
mod_utils.apply_factor(T("soldier_forest").health, "max", 1.5, true)
对实体的所有近战攻击(melee.attacks)、远程攻击(ranged.attacks)、
定时攻击(timed_attacks.list)的
k 字段统一乘以 factor。
-- 将所有攻击冷却时间减半
mod_utils.mixed_apply_factor(T("enemy_orc"), "cooldown", 0.5)
本节通过一个完整的示例,带你由浅入深地了解模组开发的全流程。
示例模组名为
stronger_archers,将逐步增强游戏中的弓箭手防御塔单位,
演示模板修改、钩子使用、配置文件分离和新模板注册四个核心能力。
tower_archer_1、arrow_1)仅供演示,
实际开发时请替换为游戏中真实的模板名称。可通过查阅游戏数据文件确认正确的名称。对于dove版,可查阅kr1目录里面的game_templates.lua,
archer_towers.lua,
mage_towers.lua,
engineer_towers.lua,
barrack_towers.lua, enemies.lua,
boss.lua, heroes.lua。
一个合法的模组只需两个文件:config.lua
和与目录同名的入口文件。
先把骨架搭起来,确认模组可以被系统识别和加载。
目录结构:
return {
name = "强化弓箭手",
entry = "stronger_archers"
version = "1.0.0",
game_version = {"kr1"},
desc = "提升弓箭手的攻击力与射程。",
url = "",
by = "你的名字",
category = "tower"
enabled = true,
priority = 0,
}
local hook_utils = require("hook_utils")
local HOOK = hook_utils.HOOK
local hook = hook_utils:new()
function hook:init(mod_data)
self.mod_data = mod_data
-- 目前什么都不做,只验证加载流程正常
end
return hook
将上面两个文件放入
mods/local/stronger_archers/,启动游戏后若没有报错, 则模组骨架已正常工作。
游戏中所有单位、子弹、特效都以"模板"的形式存储在
E(entity_db)中。 使用全局函数
T("模板名") 可以获取并直接修改任意模板的字段。
修改模板的代码必须在游戏数据加载完成后才能运行,否则模板尚不存在。
标准做法是在 init 里注册一个
E.load 钩子, 在钩子内部(调用原始
load 之后)执行修改逻辑。
local hook_utils = require("hook_utils")
local HOOK = hook_utils.HOOK
local hook = hook_utils:new()
function hook:init(mod_data)
self.mod_data = mod_data
HOOK(E, "load", self.E.load)
end
-- E.load 钩子:在原始加载完成后修改模板
function hook.E.load(load, self)
load(self) -- 必须先调用原始加载
local archer = T("tower_archer_1")
if archer then
-- 射程 + 50
archer.attacks.range = archer.attacks.range + 50
end
end
return hook
if archer then ... end 做保护判断是个好习惯。
当模组跨版本运行时,某些模板可能并不存在,保护判断能防止加载时报错。
把所有可调整的数值硬编码在入口文件里不便于维护。 推荐将它们放在独立的 Lua 文件中,使用者只需修改配置文件而无需接触主逻辑。
新增 config_archer.lua:
return {
range_bonus = 50, -- 射程加成
}
local hook_utils = require("hook_utils")
local HOOK = hook_utils.HOOK
local hook = hook_utils:new()
function hook:init(mod_data)
self.mod_data = mod_data
HOOK(E, "load", self.E.load)
end
function hook.E.load(load, self)
load(self)
-- 每次游戏加载时重新 require,确保热重载时配置也刷新
package.loaded.config_archer = nil
local cfg = require("config_archer")
local archer = T("archer")
if archer then
archer.attacks.range = archer.attacks.range + cfg.range_bonus
end
end
return hook
E.load 钩子内
require 其他模块前, 先将其从
package.loaded 中清除(置为
nil),
可以确保每次调用都重新加载最新内容,避免游戏重载时使用到旧的缓存数据。
如果只修改现有模板字段,所有使用该模板的实体都会受到影响。
当你只想让部分实体使用修改后的参数时,可以用
E:register_t("新模板名", "父模板名")(即全局
RT) 注册一个派生模板,再把它分配给指定实体。
下面的例子将弓箭手的普通箭矢替换为一个伤害更高的新子弹模板:
local hook_utils = require("hook_utils")
local HOOK = hook_utils.HOOK
local hook = hook_utils:new()
function hook:init(mod_data)
self.mod_data = mod_data
HOOK(E, "load", self.E.load)
end
function hook.E.load(load, self)
load(self)
package.loaded.config_archer = nil
local cfg = require("config_archer")
-- 注册一个派生自 arrow_archer 的新箭矢模板
local tt = RT("__strongeer_archers__arrow_archer_enhanced", "arrow_1")
tt.bullet.damage_min = math.ceil(tt.bullet.damage_min * cfg.damage_factor)
tt.bullet.damage_max = math.ceil(tt.bullet.damage_max * cfg.damage_factor)
-- 让弓箭手使用新的箭矢模板,并扩大射程
local archer = T("tower_archer_1")
if archer then
archer.attacks.list[1].bullet = "__stronger_archers__arrow_archer_enhanced"
archer.attacks.range = archer.attacks.range + cfg.range_bonus
end
end
return hook
return {
damage_factor = 1.5, -- 箭矢伤害倍率
range_bonus = 50, -- 射程加成(像素)
}
arrow_1
会影响所有使用它的实体。注册派生模板后,只有明确指定了
"__stronger_archers__arrow_archer_enhanced"
的实体才会使用新行为,对其余实体无副作用。
一份说明可以让玩家更好地理解你的插件在做什么事情。于是,我们可以创建 README.md,在里面添加详细的说明。
比如说,对于我们这个简单的示例而言,README.md 的内容可能为:
一个简单的 kr1 模组,提升弓箭手防御塔的攻击力与射程。
将 stronger_archers/ 文件夹放入游戏的
mods/local/ 目录,重启游戏即可生效。
编辑 config_archer.lua 可调整数值:
| 参数 | 默认值 | 说明 |
|---|---|---|
range_bonus |
50 |
射程加成(像素) |
damage_factor |
1.5 |
箭矢伤害倍率 |
你的名字 · v1.0.0
| 步骤 | 做了什么 | 涉及 API |
|---|---|---|
| 第一步 | 创建 config.lua + 空入口文件,验证加载流程 |
hook_utils:new(),
hook:init()
|
| 第二步 |
注册
E.load
钩子,在数据加载后修改模板字段
|
HOOK(E, "load", ...),
T("...")
|
| 第三步 | 将数值提取到 config_archer.lua,在钩子内 require |
package.loaded[k] = nil,
require(...)
|
| 第四步 | 注册派生模板,将其指定给具体实体,避免副作用 | RT("新名", "父名") |
| 第五步 | 编写 README.md,向玩家说明模组用途与参数配置 | — |