Skip to content

HelloWorld

云风 edited this page May 7, 2024 · 5 revisions

游戏世界

Ant 引擎采用的是 ECS 结构。在 ecs 构架下,一切数据都按类别组织在不同的组件 (Component) 里,每个对象 (Entity) 由不同类型的组件构成,这些都由系统 (System) 驱动。所有这一切,存在于一个叫做世界 (World) 的大容器中。

ECS 这一篇文章中我们提到,引擎中指的 ecs 和 luaecs 这个库是两个不同的东西。后者其实只实现了 ecs 所用的数据模型,并没有 system 行为。luaecs 中也有一个叫做 world 的概念,但它指的是 luaecs 中的数据的一个容器。而 Ant 给 luaecs 加上了 system 后的这个 ecs 框架,目前主要用于图形渲染。如果开发者也想用 ecs 结构解决一些问题,可以选择用 luaecs 构建一个 luaecs 的 world 独立管理一些数据,和引擎的渲染数据隔离。本文之后所提到的 world 均指 Ant 引擎在 luaecs 之上封装的这个包含有数据 (Component) 和行为 (System) 的容器。

Ant 的框架会维护唯一的 world 。它在启动时创建,(目前)不可以创建第二个。

它是由 ant.ecs 这个 Package 里的 new_world() 创建的:

local ecs   = import_package "ant.ecs"
local world = ecs.new_world(config)

需要一些初始化配置信息,它包括了初始的功能列表。

游戏主服务

Ant 在 ltask 中的游戏主服务的这个 Lua 虚拟机中创建默认的 world 。游戏主服务为 "ant.window|world" ,你可以在 pkg/ant.window/service/window.lua 找到它的实现。

在开发游戏时,所有的行为都是由这个 world 中的 system 驱动的。所以,开发者需要做的是编写一个 ECS 的 feature ,给这个 feature 增加一些 system ,在一开始把这个自定义 feature 传给游戏主服务去启动 world 。同时别漏掉了游戏需要的那些引擎提供的其它 feature 。

我们来看一个最简单的游戏实例,在引擎的 test/simple 目录可以找到它。

test/simple/main.lua 是它的启动脚本:

package.path = "/engine/?.lua"
require "bootstrap"
import_package "ant.window".start {
    feature = {
        "ant.test.simple",
        "ant.render",
        "ant.animation",
        "ant.camera|camera_controller",
        "ant.shadow_bounding|scene_bounding",
        "ant.imgui",
        "ant.pipeline",
        "ant.sky|sky",
    },
}

第一行设置了 require 的搜索路径,bootstrap 这个库放在 /engine 目录下。注: require 被修改过,和官方 Lua 带的不同,它可以搜索 VFS 中的路径,所以这里 "/engine/?.lua" 是一个 vfs 路径,而并非本地路径。

第二行调用了 require 加载 bootstrp 这个库,这个库完成引擎的自举过程,并增加一些基本功能,比如 import_package() 。然后就可以使用 Ant 自带的 Package 了。

从第三行开始,调用了 ant.window 包中的 start 函数。它创建了一个系统窗口,并启动了游戏主服务。

start 的参数中 feature 列表,会传递给游戏主服务的启动流程,用来初始化 world 。此处的第一行 "ant.test.simple" 是这个游戏示例的自定义行为,它的代码位于 /test/simple/pkg/ant.test.simpleVFS 会将 /test/simple/pkg 挂接在游戏运行时的 /pkg 路径上,和引擎提供的 /pkg 合并在一起。

因为此处的 "ant.test.simple" 指的是 ECS 中的一个 feature ,所以最终引擎查阅的是 /pkg/ant.test.simple/package.ecs 这个文件。这个文件描述了对应的 feature 是什么。

在这个实例中,还引用了一些引擎的基本功能:

  • ant.render 图形渲染器。除非你只是想测试一些命令行功能,否则一定会需要它。
  • ant.animation 动画
  • ant.camera|camera_controller 预定义的摄像机控制器。一些简单的摄像机交互控制行为,用鼠标或触摸屏控制摄像机。复杂的游戏可能不会用上它,这个用于简单演示。
  • ant.shadow_bounding|scene_bounding 用于阴影的包围盒处理。
  • ant.imgui 用于调试的界面。这个是一个基于 imgui 这个库的界面系统,我们在写测试或工具时偏好使用它。但不推荐用于游戏界面。游戏界面有另外的 RmlUI 可以使用。
  • ant.pipeline 引擎定义的 pipeline ,用来驱动整个 ecs 的行为。这是一个必选特性。
  • ant.sky|sky 天空盒。

注:VFS 的全部功能是在第二行 require "bootstrap" 之后才正式生效。所以,如果你想让后续的行为有更多的动态性。比如,如果你希望后续的代码可以使用 vfs 的 mod 机制动态覆盖。那么,应该把第三行以后的代码写在 "ant.test.simple" 这个包的内部。

扩展游戏主服务

游戏程序运行在 ltask 的多个服务上,游戏主服务只是其中的一个。对于简单的游戏应用来说,我们只需要通过 ecs 的 feature 注入到这个主服务的 world 中就够了。但如果你自己编写了一些额外的自定义服务处理图形渲染之外的业务,你可能会需要在游戏主服务上扩展一些 RPC 调用的入口,供其它服务使用。

local ltask = require "ltask"
local S = ltask.dispatch()

function S.ping(d)
  return d
end

这样,就可以定义一个叫做 ping 的自定义方法。在其它服务中,可以调用它。

local ltask = require "ltask"
local ServiceWorld = ltask.queryservice "ant.window|world"
assert(ltask.call(ServiceWorld, "ping", "pong") == "pong")

重启 world

如果你的游戏足够复杂,必然涉及多个游戏场景和场景切换。如果将 world 视为场景的容器,那么切换场景就需要一个新的 world 。

固然,world 本身被设计成可以在同一个虚拟机内有多个实例。但 Ant 的其它功能在实现时大多假定只有一个 world ,所以目前,尚不支持同时存在两个 world 。ant.window 这个包提供了 reboot 方法,它会销毁前一个 world ,并创建一个新的 world 顶替它。reboot 和 start 一样,也需要传入一个 feature 列表。如果你的新场景和老场景有所不同,那么这个 新的 feature 列表中应该包含新场景所需的对应 feature 。

如果你有一些状态需要从前一个 world 继承到后一个 world ,那么这些数据应该存放在 world 之外的地方。可以考虑以下方案:

方案一, requireimport_package 引入的模块都是和 ecs 无关的。这些模块中的数据不会因为 world 销毁而消失。只有 ecs.require 引入的模块才会和 world 关联。

方案二,实现一个独立服务。该服务拥有完全独立的 Lua 虚拟机甚至执行线程,不会受 world 更替的影响。可以把需要继承的数据放在独立服务里,world 重启后,再通过 rpc 调用重建那些对应的状态。

事实上,渲染底层和资源管理模块都在独立服务中,它们是由 ant.window 的 start 方法启动起来的。所以在 reboot 时,这些服务不受影响。引擎没有重置渲染底层的状态,前一个 world 所用到的渲染资源,也还存在于引擎的 Cache 中。

注:游戏界面 也工作在独立服务中,它也不受 world 重启的影响。它和调试用的 imgui 不同,游戏界面模块并非通过 feature 引入。

更多细节

从运行 Ant 引擎的执行文件到把引擎加载完毕运行游戏,如果想了解这个过程的更多实现细节可以阅读 Ant 主程序