Skip to content

Package

云风 edited this page Jan 18, 2024 · 3 revisions

包系统

Lua 从 5.0 开始提供了一个简单的包系统。Lua 5.1 中,这个包系统变得比较复杂,到了 Lua 5.2 又反思了这些不必要的复杂性,然后减去了一些特性,一直到现在都维持着那个简单的系统。

我赞同保持包系统的简单性,但 Ant 不得不面对一些 Lua 原生包机制无法解决的问题。所以,我们不得不额外开发了一套包系统,并同时尽可能的保留 Lua 原生机制。

Ant 的包(Package)是功能和数据的封装单位。一个基于 Lua 编写的的模块是一个包,一组多媒体资产也是一个包。

包自己是一个环境,在环境内部,数据和代码之间可以相互引用。我们不支持类似文件系统那样用 .. 引用上一级目录,一切内部引用都是从这个环境的根向下展开的。同时,我们也可以使用外部引用,即引用其它包内的数据。可以这样看:当用一个字符串引用某组数据时,分为包名和包内路径两部分,它们用 | 分割,前面是包的名字,后面是包内路径。如果不写 | ,那么就默认为当前包。

包的名字是一个字符串,通常我们会用 . 分割。但它并不表示文件系统上的多级目录。一般我们会用带 . 的字符串来做包在文件系统中的目录名。所有的包都在 VFS 的 /pkg 路径下。例如 foo.bar 这个包在 /pkg/foo.bar 路径下,而不是 /pkg/foo/bar 。如果我们要引用 foo.bar 包下的 something ,就会写为 foo.bar|something 。如果它指一段 Lua 代码编写的子模块,通常源文件指的是 /pkg/foo.bar/something.lua 。

代码包

如果一个包是用 Lua 代码实现的模块,它应该使用 import_package(pkgname) 引入它。看起来和 Lua 原生机制中的 require 非常相像,但参数是包的名字组成的字符串。import_package 会去对应的包路径下找到其根目录下的 main.lua 这个文件,和 require 一样的方式运行它:即每个 Lua 虚拟机中只会加载一次,并缓存运行结果,通常是一个 table 。这个运行结果会返回。

在包内,你可以像普通 Lua 代码那样使用原生的 require 。只不过,require 会被限制在当前包的环境内。它的路径搜索基于 VFS 中对应包路径。例如,在 foo.bar 这个包的 main.lua 中,如果你写下

local m = require "modname"

require 会去查找 /pkg/foo.bar/modname.lua 。但你可以在不同的包下使用相同的模块名,require 找到的会是不同的源文件。

require 不支持引用外部包里的子模块,工作环境不能超出当前包。

ECS 模块

Ant 基于 ECS 结构设计。ECS 的数据被 world 隔离。同一个 Lua 虚拟机中可以同时存在多个 ECS 的 world 。通常,我们希望这些不同的 world 间引用的数据都是相互独立的。如果需要这种独立性,那么在 ECS 框架下使用 require 就是不合适的。因为同一个 Lua 虚拟机中,require 的结果是被缓存的,多次 require 同一个模块名,会得到同一个 table ,状态是共享的。

我们应该使用 ecs.require() 让它们在不同 world 间隔离开。ECS 专属模块和普通的代码包不同,它没有根文件,任何一个 ecs 模块都必须是某个包内部的一个子模块。

ECS 框架下的子模块的特征是,.lua 文件的第一行一定是

local ecs = ...

这表示是一个 ECS 子模块,会由 ecs.require() 传入 context 给 ecs 变量,赋予它独立环境。这个模块内也可以使用 ECS 框架的专有 API 。

当我们写 ecs.require "module" 其实查找的是当前包内的 module.lua 这个源文件,它的第一行一定是 local ecs = ... 。和原生 require 不同,ecs.require 还可以引用外部包中的 ECS 模块,我们可以写 ecs.require "foo.bar|module" ,它会去查找 /pkg/foo.bar/module.lua 这个文件。

ECS 特性

特性是 Ant 引擎的 ECS 架构中的一个概念。当使用 import_feature(name) 时,将引入定义在 name 这个包内的主特性。import_feature() 会解析 /pkg/name/package.ecs 这个特性定义文件。

ltask 服务

Ant 引擎使用 ltask 来使用处理器的多核,并将功能模块分离到不同的服务中。这些服务各自处于独立的 Lua 虚拟机,被 ltask 调度到不同的线程运行。服务间通过消息通讯,而不能直接共享状态。简单来说,一个服务中用 Lua 定义的全局变量,另一个服务的 Lua 代码是无法访问的。

服务也是用 Lua 实现,它的源代码也是放在包系统内。引擎规定,所有服务的主文件必须放在某个包的 service 目录下。例如,引擎内置了一个 webserver 用于游戏在运行时窥探自身的内部状态,开发者可以通过浏览器连上这个 webserver 查看运行时的各种数据。

这个 webserver 被实现为一个服务,这个服务放在 ant.webserver 这个包里面。那么,该服务的源代码就处于 vfs 中的 /pkg/ant.webserver/service/webserver.lua

启动这个服务可以用:

ltask.uniqueservice "ant.webserver|webserver"

ant.webserver|webserver 描述了这是一个位于 ant.webserver 包中名为 webserver 的服务。


一个包中可以同时包含有代码包、ECS 模块或特性、ltask 服务的实现。也可以不包含以上任何东西,纯粹是一组数据。通常,多媒体资产就放在这种纯数据包里面。对于 Ant 引擎来说,几乎全部代码和数据都是由一个个的包构成的。除了少许例外:

  • 第一个启动脚本。
  • /engine 放置了一些自举用的代码。
  • 通常会把内置 webserver 用到的静态文件放在 /web 下。
  • VFS 会把资产编译结果放在 /res 下,然后通过软连接被某些包引用。
  • 其它开发者不愿意用包管理的数据。是否使用包机制是一个可选项,我们推荐它,但不强制使用。
Clone this wiki locally