-
Notifications
You must be signed in to change notification settings - Fork 23
Home
Welcome to the soluna wiki! google translate this page into English
soluna 是一个基于 sokol 的 2D 游戏引擎。它以 Lua 为编程语言,整合了 ltask 作为多线程框架。sokol + lua 是其名字的由来。
soluna 目前支持 Windows/macOS/Linux/Web ,可以使用 luamake 构建各个平台的版本。在 Windows 平台,也可以使用 GNU Make 构建:默认使用 mingw 环境,可以用 make CC=cl 切换为 msvc 环境。
在构建完毕后,引擎所有相关代码和资源都会打包到一个执行文件中,没有额外的数据文件依赖。你也可以直接下载预编译版本。
执行引擎执行文件可以运行游戏。
游戏由若干 lua 代码文件和相关的资产文件(如数据、图片等)构成。可以是一个本地目录,也可以是一个 zip 包。引擎运行时,默认检查当前目录下是否有 main.zip ,若有则将其作为需要启动的游戏;若没有则将当前目录作为游戏所在地。也可以从命令行传入 zip 包文件名改变需要启动的游戏,或传入一个游戏环境文件指定游戏。
游戏环境指游戏运行的若干配置,其默认值在 src/data/settingdefault.dl 被打包到引擎执行文件中。可以在游戏包中放入一个环境文件 main.game 覆盖这些默认配置。或在命令行中指定游戏环境文件名。
也可以在命令行中通过 key=value 指定需要改写的配置项。
入口文件名是游戏启动时第一个被加载运行的 lua 代码文件。默认为 main.lua ,即引擎默认会在游戏包中查找名为 main.lua 的文件运行它。如果引擎在运行时找不到这个 entry (入口)文件,则会报告 Can't load entry main.lua 并退出。
如果命令行指定 soluna test/window.game 启动,则会以 test/window.game 为环境启动游戏(并将当前目录设定为 test)。因为在 test/window.game 中指定了 entry:window.lua ,所以 test/window.lua 就成为了游戏入口。
也可以通过命令行指定 soluna entry=test/window.lua 也可以以它为入口文件启动游戏。这里的命令行参数 entry=test/window.lua 指将 entry 的默认值 main.lua 修改为 test/window.lua
soluna.js 是一个 ES module,默认导出 createApp 工厂函数;页面侧需要用 import 或动态 import() 加载它,再显式传入 canvas、arguments、preRun 和 locateFile 等参数。
一个典型流程如下:
- 使用 soluna (Windows/macOS/Linux) 进行游戏开发。
- 将游戏源码 (lua) 打包成
main.zip。 - 在仓库根目录构建 wasm runtime:
luamake -compiler emcc
- 部署页面时,需要提供
soluna.js、soluna.wasm、main.zip。 - soluna wasm 使用了 pthread,因此页面必须启用跨源隔离(COOP/COEP)。具体要求可见 https://emscripten.org/docs/porting/pthreads.html#pthreads-support 。如果静态托管平台不方便直接配置响应头,也可以像示例站点一样使用
coi-serviceworker.min.js来补上隔离环境。
示例站点源码位于 website/ 目录,职责分为两部分:
-
luamake负责构建 wasm runtime,本体产物位于bin/emcc/<mode>/,包括soluna.js、soluna.wasm,以及可选的soluna.wasm.map。 -
website/scripts/prepare-runtime.mjs在 Astro 构建前把这些运行时文件复制到website/public/runtime/,同时打包asset/生成asset.zip。 - GitHub Pages workflow 会先执行
.github/actions/soluna,再把这些路径通过环境变量传给 Astro build,最后部署website/dist/。
如果你只是想运行站点,最直接的入口是:
luamake -compiler emcc
cd website
pnpm install
pnpm run dev更多细节可以参考仓库里的 website/README.zh-CN.md。
下面是一个最小示例。它会以 ES module 的方式加载 soluna.js,再把 main.zip 写入 memfs 后启动 soluna。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Soluna Game</title>
<style>
body { margin: 0; background: #000; }
canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
<canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>
<script type="module">
import createApp from './soluna.js';
async function fetchBytes(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status} while fetching ${url}`);
}
return new Uint8Array(await response.arrayBuffer());
}
async function main() {
if (!window.crossOriginIsolated) {
throw new Error('Cross-origin isolation is required for pthread-enabled Soluna wasm.');
}
const canvas = document.getElementById('canvas');
const mainZip = await fetchBytes('./main.zip');
await createApp({
canvas,
arguments: ['zipfile=/data/main.zip'],
locateFile(path) {
return new URL(path, import.meta.url).toString();
},
preRun: [
function (module) {
module.FS_createPath('/', 'data', true, true);
module.FS.writeFile('/data/main.zip', mainZip, { canOwn: true });
},
],
print(text) {
console.log(text);
},
printErr(text) {
console.error(text);
},
onAbort(reason) {
console.error('Program aborted:', reason);
},
});
}
main().catch((error) => {
console.error(error);
});
</script>
</body>
</html>extlua 是 soluna 用来加载外部 C 模块的一套机制。通过此机制,可以为 soluna 扩展常规的 Lua C 模块,也可以进一步扩展渲染材质。
典型配置如下:
extlua_entry : extlua_init
extlua_preload : sample
这表示启动时会按 package.cpath 查找名为 sample 的外部模块,并以 extlua_init 作为入口函数加载它。extlua_preload 可以是单个模块名,也可以是模块名列表。
外部模块不应该直接链接 Lua 和 sokol 的源码,而要通过一组 API 由 soluna 注入实现:
- luaapi_init(L):注入 soluna 使用的 Lua API 实现;
- sokolapi_init(L):注入 soluna 使用的 sokol API 实现;
- solunaapi_init(L):注入 soluna 扩展 API 实现。
这样做的目的,是避免外部模块和宿主之间出现版本错配。例如外部模块自己链接了一份不同版本的 Lua,或使用了和 soluna 版本不一致的 sokol/soluna ABI, 都会出现问题。通过宿主注入 API 表,外部模块实际调用的是当前 soluna runtime 提供的实现。
常规 C 模块通常只需要 Lua API,因此只需要:
luaapi_init(L);
只有在编写外部材质、需要访问 sokol 或 soluna 渲染相关 API 时,才需要额外调用:
sokolapi_init(L);
solunaapi_init(L);
外部模块的入口函数需要返回一个 Lua C 模块注册表。表的 key 是之后可用于 require 的 Lua 模块名,value 是对应的 luaopen_* 函数。
常规 C 模块的入口通常类似这样:
#include <lua.h>
#include <lauxlib.h>
LUA_API void luaapi_init(lua_State *L);
#if defined(_WIN32)
#define EXTLUA_EXPORT __declspec(dllexport)
#else
#define EXTLUA_EXPORT __attribute__((visibility("default")))
#endif
static int
lhello(lua_State *L) {
lua_pushstring(L, "Hello World From Sample");
return 1;
}
static int
luaopen_foobar(lua_State *L) {
luaL_Reg libs[] = {
{ "hello", lhello },
{ NULL, NULL },
};
luaL_newlib(L, libs);
return 1;
}
EXTLUA_EXPORT int
extlua_init(lua_State *L) {
luaapi_init(L);
luaL_Reg libs[] = {
{ "ext.foobar", luaopen_foobar },
{ NULL, NULL },
};
luaL_newlib(L, libs);
return 1;
}业务代码中即可这样使用:
local foobar = require "ext.foobar"
print(foobar.hello())编译该模块时,需要把 extlua/extlua.c 一起编译进去。
外部材质除了普通 Lua C 模块能力外,还需要访问 sokol 和 soluna 的渲染扩展 API,因此入口函数通常需要初始化三组 API:
#include <lua.h>
#include <lauxlib.h>
#include "sokol/sokol_gfx.h"
#include "solunaapi.h"
LUA_API void luaapi_init(lua_State *L);
void sokolapi_init(lua_State *L);
void solunaapi_init(lua_State *L);
EXTLUA_EXPORT int
extlua_init(lua_State *L) {
luaapi_init(L);
sokolapi_init(L);
solunaapi_init(L);
luaL_Reg libs[] = {
{ "ext.material.matxx", luaopen_ext_material_xx },
{ NULL, NULL },
};
luaL_newlib(L, libs);
return 1;
}编译外部材质模块时,需要把以下文件编译进去:
extlua/extlua.c
extlua/sokolapi.c
extlua/solunaapi.c
其中:
- extlua/extlua.c 提供 Lua API 桥接;
- extlua/sokolapi.c 提供 sokol API 桥接;
- extlua/solunaapi.c 提供 soluna 扩展 API 桥接;
- extlua/solunaapi.h 是外部材质使用 soluna 渲染扩展 API 时需要包含的头文件。
外部材质还需要在游戏配置中声明使用的外部材质和注册逻辑:
extlua_entry : extlua_init
extlua_preload : sample
extlua_material : matxx
extlua_material_path : extlua/material/?.lua
其中:
- extlua_preload 负责加载包含材质 C 接口的外部模块;
- extlua_material 指定要加载的外部材质名;
- extlua_material_path 指定包含外部材质注册逻辑的脚本路径。
注册逻辑将会在 soluna 启动时由 render service 加载和执行。编写注册脚本通常需要 require 对应的外部材质,创建 pipeline、buffer、binding 等渲染资源,并返回一个材质表和提供以下方法:
local render = require "soluna.render"
local mat = require "ext.material.matxx"
local ctx = ...
function material.submit(ptr, n)
end
function material.draw(ptr, n, tex)
end
function material.reset()
end
return material完整示例可以参考:
- test/extlua.game
- test/extlua.lua
- test/extlua/material/perspective_quad.lua
- extlua/extlua_sample.c
- clibs/sample/make.lua