1.7 万物皆Actor
合理分割功能是分布式模型的一大难点,我们需要寻找一种模式,它既能符合游戏逻辑的表达,又能让计算机高效执行。
游戏业界苦苦追寻着更适合当代游戏的服务端模型,蓦然回首,几十年前就被提出的Actor并发模型就在灯火阑珊处。如图1-25所示,在Actor模型中,每个Actor相互隔离,只通过消息通信,具有天然的并发性。
图1-25 Actor模型的示意图
想要借用Actor模型的理念,首先要了解什么是Actor模型。
1.7.1 灵感来自Erlang
Actor模型由来已久。早在1973年,Carl Hewitt提出了Actor并发计算的理论模型;1991年爱立信推出的编程语言Erlang将Actor模型融入语言里,并应用在通信领域里。2009年前后,珠三角的一些游戏公司(四三九九、菲音、明朝网络)开始大规模地将Erlang语言应用于游戏领域。2012年前后,云风(吴云洋的网名)开源了C语言Actor模型框架Skynet,并称之为游戏服务端引擎,且将其应用在不少商业游戏上。
1.7.2 对世界的抽象
Actor模型的理念——万物皆Actor,它是更进一步的面向对象,即把世间万物都当作Actor对象。Actor可以代表一个角色、一只动物,也可以代表整个游戏场景,图1-26展示的是用Actor模型抽象的一个游戏世界,方括号代表Actor的类型,id代表Actor的标识,中间文本代表名称。
图1-26 万物皆Actor
说明:Skynet中将Actor对象称为服务,Erlang中将其称为进程(不同于操作系统进程),为统一术语,在解释Actor模型时,使用“Actor”一词;在Skynet的语义下,使用“服务”一词。
在图1-27中,每个Actor都会包含自身状态(HP、Coin),以及一个信箱(消息队列),Actor通过给其他Actor“寄信”来实现通信。至于收到信件后的反应,取决于收信的Actor。
图1-27 Actor示意图
整个Actor系统可类比为“邮局”,它负责信件的传送。如图1-28所示,Actor1001给1002发送信件,请求“查询登录玩家数量”,寄出的“信件”经由邮局,投递到Actor1002的信箱。由于各个Actor相互独立,计算机很容易让它们并行工作。
图1-28 Actor消息传递示意图
如果理论解释不易理解,不妨直接看代码。代码1-10用Lua定义了Role类型的Actor对象。代码中的local Role=function()...end用定义方法的方式定义了一个类,这是Lua的特殊语法,只需记住这段代码定义了Role类,它包含id、coin、hp这三个属性和dispatch方法即可。dispatch是处理消息的方法,参数source代表消息发送方,参数msg代表消息内容。如果收到“work”,角色会努力工作赚钱,并督促发送方努力工作(发送“work”给对方);如果收到“eat”,角色会吃美食恢复健康。
代码1-10 定义一个Actor对象(Lua语言)
local Role = function() local M = { id = -1, --Actor标识 coin = 100, hp = 200, } function M:dispatch(source, msg) if msg == "work" then self.coin = self.coin + 10 print(self.id.." work, coin:"..self.coin) send(self.id, source, "work") elseif msg == "eat" then self.hp = self.hp + 5 print(self.id.." eat, hp:"..self.hp) else --更多消息处理 end end return M end
Actor系统会提供newactor(创建Actor对象)、send(向Actor发送消息)之类的方法,如代码1-11中,创建了4个Role类型的Actor,它们的id是从101到104,这里让101给102发送“work”,103给104发送“eat”。
代码1-11 创建Actor对象并发送消息(Lua语言)
newactor(101, Role()) newactor(102, Role()) newactor(103, Role()) newactor(104, Role()) send(101, 102, "work") send(103, 104, "eat")
图1-29是代码1-11中4个Role对象的运行示意图,其中101和102这两个Actor在相互督促工作,努力赚钱;103让104吃好喝好。图1-30展示了代码1-11的运行结果,尽管是101先向102发送“work”后,103再向104发送“eat”,但各个Actor是并行执行的,因此消息处理的顺序不确定。
图1-29 代码1-11运行的示意图
图1-30 代码1-11的运行结果
1.7.3 为何适用
为什么说Actor模型适用于游戏开发呢?
回顾1.4.3节的多进程程序,从某种程度上说,Actor模型和传统的多进程服务端结构有很多相似之处。不同的是,一个操作系统进程会占用很多的系统资源,按照1.5.3节的分析,进程不仅会占用较多的内存,操作系统在切换进程(线程)时也会占用较多的CPU时间,一台物理机只能运行几百个进程,这会限制游戏的业务分割。
举个例子,假设要开发一款斗地主游戏(如图1-31所示),每局游戏由3名玩家参与。那么,一种处理方式是开启多个进程,每个进程处理多张桌子的逻辑(如图1-32所示)。如果每个进程可以处理10张桌子,单台物理机开启100个进程就可以支撑3000名玩家。但是,进程A和进程B究竟是什么东西呢?是桌子集合?它其实是计算机系统的概念,因为在现实世界找不到对应的事物,所以不容易理解。还有一种方法,即让每个进程只处理一张桌子的逻辑,然而由于进程会占用很多的系统资源,因此这样做会导致单台物理机只能支持几百名玩家。
图1-31 《斗地主》游戏示意图
图1-32 传统的多进程模型
Erlang、Skynet通过内部处理,让每个Actor都是轻量级的,可让每张桌子独立分开(如图1-33所示),让游戏逻辑更符合现实世界场景。同时,Actor与多进程模型同样具备天然分布式属性,在图1-33中,不同的桌子可以运行在不同的物理机上。
图1-33 Actor模型提供了更多的灵活性
对游戏服务端而言,Actor并发模型给游戏业务的分割提供了灵活性。
再回顾1.2.3节的简单服务端程序,如果说Node.js提供了“单线事件模型”的运行环境,那么Skynet提供了Actor模型的运行环境(如图1-34所示)。
图1-34 类比Node.js和Skynet
现在,你已经初步了解游戏服务端开发所需的关键技术,以及一些需要注意的事项。本书分为“学以致用”“入木三分”“各个击破”三大篇章,第一篇“学以致用”就是要让读者能够以最快的速度做出成品。接下来我们使用Skynet引擎,先把游戏做出来!