Luanti / Minetest — 给自己造一个开源体素游戏引擎
是什么
Luanti(前身叫 Minetest)是一个完全开源的体素游戏引擎——它让你不需要买 Minecraft 也能造一个自己的方块世界,并且用 Lua 脚本随意扩展玩法。
日常类比:把它想成”乐高底板 + 零件说明书”。Minecraft 是一盒固定玩法的成品乐高;Luanti 则是把那块底板拆开给你,连怎么拼新零件的规则都附上。你不仅能玩别人拼好的,还能自己造零件卖给别人。
技术上,Luanti 用 C++ 写核心渲染引擎(体素世界、光照、物理),用 Lua 写游戏逻辑(物品、配方、地图生成、玩家事件)。两层职责分明:C++ 跑得快,Lua 改起来容易。任何人都可以下载引擎、写几行 Lua,就做出一个能分享给朋友的游戏。
2024 年 10 月,项目正式从 Minetest 改名为 Luanti——名字来自芬兰语 luonti(创造)加上引擎使用的 Lua 语言,象征从”测试用克隆”蜕变为真正的游戏创作平台。
为什么重要
不了解 Luanti,下面这些事很难解释:
- 为什么一个 C++ 游戏引擎能让只会写
print("hello")的人在一下午内做出新游戏——Lua 嵌入式脚本的典范设计就在这里 - 为什么”服务端 mod”模式可以让玩家不装任何客户端插件就加入有 mod 的服务器——Luanti 的 mod 全在服务端跑,客户端零改动
- 为什么体素引擎需要把世界切成小块存储(MapBlock),而不是一张大地图——这涉及无限世界的内存与磁盘权衡
- 为什么开源游戏引擎的社区 mod 生态比游戏本身活得更久——ContentDB 已有数千个 mod,有些比引擎本身年龄还大
核心要点
1. C++ 引擎 + Lua API 的双层架构
引擎底层用 C++ 处理体素渲染、光照、碰撞检测,Lua 层通过注册回调函数响应游戏事件。类比:C++ 是餐厅的厨房设备,Lua 是厨师的菜谱——你不需要懂蒸汽阀门怎么工作,只要写清楚”鸡肉加热 3 分钟”就够了。
关键 API 模式:
-- 注册一个新节点(方块类型)minetest.register_node("mymod:glowstone", { description = "发光石", tiles = {"mymod_glowstone.png"}, light_source = 14, -- 亮度 0-14 groups = {cracky = 3}, -- 可以被镐挖})这几行 Lua 就能让一个新方块出现在游戏里,贴上你提供的材质,并且会发光。
2. 服务端 mod 模式——零客户端修改
Luanti 的 mod 全部在服务端执行。玩家连进来时不需要提前安装任何东西,服务端自动同步必要的资源文件(材质、声音)。类比:餐厅换菜单不需要每位客人把菜单打印一份带回家,他们只要进门坐下就行。
这意味着服务器管理员可以随时给服务器加 mod、改规则,玩家体验到的变化是即时的。
3. 地图以 MapBlock 为单位分块存储
Luanti 把整个世界切成 16×16×16 节点的 MapBlock,按需加载到内存,闲置时写回磁盘。类比:图书馆不把所有书都摆在桌上,哪本有人要才从架子上取下来。这让”理论上无限的世界”(实际是 ±30,000 米)变得可以在内存里跑。
-- 批量读取某区域的节点内容local vm = minetest.get_voxel_manip()local emin, emax = vm:read_from_map(pos1, pos2)-- 修改后写回(比逐节点操作快 100 倍以上)vm:write_to_map()实践案例
案例 1:添加一种新物品和合成配方
最常见的 mod 入门任务:给游戏加一把”超级锄头”。
-- init.lua(mod 入口文件)
-- 注册物品minetest.register_tool("mymod:super_hoe", { description = "超级锄头", inventory_image = "mymod_super_hoe.png", tool_capabilities = { full_punch_interval = 0.5, max_drop_level = 1, groupcaps = { crumbly = {times = {[1]=0.5, [2]=0.3, [3]=0.2}, uses = 200}, }, },})
-- 注册合成配方(3 格铁锭 + 2 格木棍)minetest.register_craft({ output = "mymod:super_hoe", recipe = { {"default:steel_ingot", "default:steel_ingot", "default:steel_ingot"}, {"", "default:stick", ""}, {"", "default:stick", ""}, },})逐行解释:
register_tool:告诉引擎”有个工具叫 super_hoe,图标是这张 PNG,挖土类方块的速度是这些”register_craft:定义玩家在工作台摆出这个形状就能合成出工具crumbly:Luanti 用”group”描述方块属性,crumbly = 松散土壤类,锄头特别快
案例 2:自定义地图生成器——造一个”蘑菇星球”
Luanti 允许 mod 完全接管世界生成逻辑。
-- 注册一个蘑菇地形生成器minetest.register_on_generated(function(minp, maxp, seed) local vm, emin, emax = minetest.get_mapgen_object("voxelmanip") local data = vm:get_data() -- 拿到这个 MapBlock 的节点数组 local area = VoxelArea:new({MinEdge = emin, MaxEdge = emax})
for z = minp.z, maxp.z do for x = minp.x, maxp.x do -- 地面高度用正弦波模拟起伏 local ground_y = math.floor(math.sin(x * 0.1) * 5 + math.cos(z * 0.1) * 5) for y = minp.y, maxp.y do local vi = area:index(x, y, z) if y <= ground_y then data[vi] = minetest.get_content_id("default:dirt") elseif y == ground_y + 1 then data[vi] = minetest.get_content_id("default:dirt_with_grass") end end end end vm:set_data(data) vm:write_to_map(true) -- 写回地图并触发光照更新end)关键点:
register_on_generated:每次引擎生成新 MapBlock 时调用你的函数VoxelArea:把三维坐标映射到一维数组下标,避免每次查询都经过 Lua 调用开销write_to_map(true):写回时重新计算光照,否则地下会一片漆黑
案例 3:多人服务器的玩家进入事件与权限判断
在多人服务器中,你经常需要在玩家登录时做初始化,或根据权限开放功能。
-- 玩家首次加入时,给他一套新手工具包minetest.register_on_newplayer(function(player) local inv = player:get_inventory() inv:add_item("main", "default:pick_wood") -- 木镐 inv:add_item("main", "default:axe_wood") -- 木斧 inv:add_item("main", "default:torch 10") -- 10 个火把 minetest.chat_send_player(player:get_player_name(), "欢迎!你获得了新手礼包。")end)
-- 每次有玩家发言,检查是否有管理员权限minetest.register_on_chat_message(function(name, message) if message:sub(1,1) == "/" then return false end -- 命令交给引擎处理 if minetest.check_player_privs(name, {admin = true}) then minetest.chat_send_all("[管理员] " .. name .. ": " .. message) return true -- 拦截原消息,换格式发出 endend)逐部分解释:
register_on_newplayer:只在玩家第一次连接时触发,与每次登录触发的register_on_joinplayer不同get_inventory/add_item:操作玩家背包,物品格式是"mod名:物品名 数量"check_player_privs:查询玩家是否被赋予某个权限名称,权限可以用minetest.set_player_privs动态修改
踩过的坑
-
误用阻塞 Lua 代码:在 mod 里直接
while true do end或调用os.execute("sleep 1")会冻结整个服务端,因为 Lua 和 C++ 在同一个主线程里跑。正确姿势是用minetest.after(1, callback)延迟执行,或调用内置的 HTTP API(异步)。 -
逐节点修改大面积地图:用
minetest.set_node()一个一个改节点,改 1000 个要触发 1000 次 C++/Lua 边界穿越和磁盘写入。应该用VoxelManip批量读取 → 修改数组 → 整块写回,速度差距可达两个数量级。 -
Minetest / Luanti 双名混淆:2024 年更名后,搜索”Minetest mod API”仍然有效(旧文档),但搜”Luanti”找到的是新版文档。两者 API 绝大多数兼容,但新特性(如新的 GUI 系统 Formspec 2.0)只在新文档里有。建议收藏 Luanti 官方 Wiki。
-
mod 依赖声明漏填:
mod.conf里的depends字段如果没列全依赖,服务器加载顺序可能让你的 mod 在依赖 mod 之前初始化,导致minetest.registered_nodes["default:stone"]返回 nil。每个 mod 文件夹里必须有mod.conf并声明name = mymod和depends = default。
适用 vs 不适用场景
适用:
- 学习游戏引擎架构:双层 C++/Lua 分离是嵌入式脚本语言的经典范本
- 快速原型验证体素玩法概念:一个 mod 的最小骨架只需
mod.conf+init.lua共约 10 行 - 需要完全掌控服务端逻辑的多人游戏(无版权壁垒、可自部署)
- 低性能设备(如 Raspberry Pi、老旧 Android 手机)上的轻量游戏服务器
不适用:
- 需要高精度 3D 物理(刚体旋转、布料、流体模拟)→ 选 bevy 或 godot
- 要做 3D 动作类或射击类游戏(Luanti 没有内置的精细碰撞网格)→ 考虑 panda3d
- 需要图形 API 层直接操控着色器 → Luanti 封装了渲染,不暴露底层 OpenGL/Vulkan 接口
- 纯 2D 游戏 → pixi 或 cocos2d-x 更合适,体素引擎在 2D 场景是杀鸡用牛刀
历史小故事(可跳过)
- 2010 年:芬兰程序员 Perttu Ahola(celeron55)启动实验项目 Minetest-c55,看看能否自己做一个 Minecraft 克隆。最初目标只是”能跑就行”。
- 2012 年前后:Lua mod API 成型,社区开始形成。第一个大型 mod 游戏包”Minetest Game”出现,把引擎和玩法内容分离的思路就此确立。
- 2013-2020 年:ContentDB 上线,成为 mod 和游戏的统一分发平台;多人服务器生态在全球各地自发生长,高峰期同时在线服务器超过 200 个。
- 2024 年 10 月 13 日:经过多年社区讨论,正式更名为 Luanti。旧名里的 “test” 让人误以为这是个实验项目,新名字表明它已是成熟的游戏创作平台。
学到什么
- 脚本语言嵌入的最佳实践:C++ 管性能敏感路径,Lua 管逻辑扩展——这种分工让引擎可以保持高性能同时降低 mod 开发门槛,是 Redis / Nginx / World of Warcraft 同类架构的典范
- 服务端权威模式的优势:所有 mod 逻辑跑在服务端可以防止客户端作弊,同时让玩家加入有 mod 服务器时无需配置,这比客户端 mod(如 Minecraft Forge)的分发摩擦小得多
- 分块存储无限世界:MapBlock 设计告诉我们无限世界的代价是”按需分页”,延迟加载的思路在数据库分区、操作系统虚拟内存、游戏引擎 LOD 里反复出现
- 名字重要性:Minetest → Luanti 的更名说明一个项目名里的”test”会让人低估它的成熟度——好名字是产品的一部分
延伸阅读
- 官方文档:Luanti Developer Wiki — Lua API 完整参考,mod 开发必备
- 入门教程:Luanti Modding Book — rubenwardy 写的开源 mod 开发书,从零到能写完整 mod
- ContentDB:content.luanti.org — 官方 mod 和游戏分发平台,看别人怎么写是最快的学习方式
- bevy —— 同样开源的现代游戏引擎,Rust 写的 ECS 架构,对比 Luanti 的 C++/Lua 双层设计
- panda3d —— 另一款支持 Python 脚本扩展的开源 3D 引擎,与 Luanti Lua 嵌入思路相近
关联
- bevy —— Rust ECS 游戏引擎,与 Luanti 同属开源游戏引擎生态,架构路线完全不同
- panda3d —— 支持 Python 脚本扩展的开源 3D 引擎,同样是”核心 C++ + 脚本层”的双层架构
- cocos2d-x —— 2D 游戏引擎,同样开源且支持 Lua 脚本,侧重移动端 2D 游戏
- pixi —— 2D WebGL 渲染库,与 Luanti 形成对比:前者专注 2D 浏览器渲染,后者专注体素 3D
- lua —— Luanti 的 mod 层使用语言;Lua 轻量可嵌入的特性使它成为游戏引擎脚本首选
反向链接
- 3d-gaussian-splatting —— 3D Gaussian Splatting — 用一堆 3D 模糊光斑重建场景
- bevy —— Bevy — Rust 数据驱动 ECS 游戏引擎
- openrct2 —— OpenRCT2 — 把一款 x86 汇编游戏彻底用 C++ 重写
- panda3d —— Panda3D — Disney/CMU 出品的开源 3D 游戏引擎
- perlin-1985-noise —— Perlin Noise — 让计算机生成的图像不再有”机器味”
- pixi —— PixiJS — 浏览器里画 2D 的高性能 GPU 引擎
- wasmtime —— Wasmtime — Bytecode Alliance 标准 wasm runtime