Farmework With PG.ToLua And PMServer
Introduction
在游戏开发中,Lua由于其简洁规范的语法的特点得以广泛使用,在引进Luajit后,脚本语言的性能问题也得以解决,而ToLua的插件则给出了Lua与C#交互的解决方案。C#实现与Unity3D交互的内核框架,Lua脚本编写业务逻辑,ToLua插件进行两者的交互,这套客户端开发体系逐渐成熟。对于服务器端,则使用与Lua契合度高的C++编写内核框架,Lua实现服务器脚本逻辑。本文主要是对使用ToLua与PMServer实现Demo过程的一点记录。
PMServer Architecture
PMServer是一个游戏开发的服务器端引擎,内核框架封装了网络通信,配置文件与脚本解析,数据库操纵等一些底层接口。而开发人员通过编写规定格式的业务逻辑脚本来实现游戏逻辑。
在组成上,PMServer主要有以下部分:
- mgrapp。负责对数据库的操作,其他app的管理,调试的接口
- loginapp。主要处理登陆相关的逻辑,选择gateapp
- gateapp。在登陆完毕之后连接的网关服务器,之后客户端所有的交互都与gateapp进行。它是客户端和gameapp之间的桥梁
- gameapp。游戏中所有的逻辑和Entity都存在于gameapp中,一般会有多个gameapp。在服务器中各种对象都是以Entity的形式存在的,例如各种管理类PlayerMgr、SpaceMgr都是一个Entity,玩家的Player、Monster等也是Entity。
PMServer Deploy
使用PMServer的第一步就是部署了,老实说,我对服务器还是接触的不较少,部署的时候走了不少弯路,在这里做个流程的总结吧。
服务器的部署大致步骤如下:
- 申请服务器开发权限,并使用XShell连接至服务器
- 下载服务器端逻辑脚本并上传至服务器
- 编辑配置文件(/server/bin/config.ini)
- 设置其中的对外开放的端口与IP(登录的服务器IP相同)
- 注意端口不能和别人冲突(在这里我卡了好久,启动服务器时一直报端口拒绝访问)
- 设置客户端连接逻辑的IP和端口
- ./start.sh
PMServer Business Develop
游戏的服务器端逻辑开发基于Lua脚本,主要流程如下:
- 在defs/entities.xml中定义要实现的entity
- 创建相应的entity.xml,并按格式填写属性与方法
- 在script下实现entity.lua的游戏逻辑
其中,entity代表了游戏中的每个对象,客户端与服务器端的属性同步自动进行。
对于每个entity,在客户端中也要像服务器端这样操作,不同的只是Lua脚本中游戏逻辑的实现。
Login Realize
登录的逻辑实现主要是客户端Login_Panel与服务器端loginapp的交互。实现的思路主要如下:
- 在客户端的登录按钮绑定登录事件
function LoginCtrl:initButton()
print(type(self.view.button))
self.view.button.actionClick = function()
self:initServerState()
self:connectToLoginServer()
end
end
- 在客户端登录实现中发送用户输入的用户名密码
function LoginCtrl:connectToLoginServer()
local model = self.model
if model:isDisconnected("login") then
gamelog.info("connectToLoginServer")
model:setConnecting("login")
ClientUtils.connectServer(model.loginIP, model.loginPort, true)
elseif model:isConnected("login") then
gamelog.info("login isConnected")
if not model:isConnected("gate") then
model:setConnecting("gate")
ClientUtils.callServerReliable("login",{["username"]=self.view.InputField.text, ["password"]=self.view.InputPwd.text},"")
end
end
end
- 在服务器loginapp中实现验证逻辑,对客户端进行digest验证
function cs.proxy.login(clientId, extraInfo, digest)
print(string.format("login: clientId[%s] username[%s] password[%s] digest[%s] ip[%s]",
clientId, tostring(extraInfo.username), tostring(extraInfo.password), digest, pg.global.clientIpMap[clientId]))
local cs_stub = cs.stub(clientId)
if not pg.global.enableLogin then
cs_stub.onServerNotReady()
return
end
if type(extraInfo) == "table" then
local username, secretkey = unpack(extraInfo)
if secretkey == (pg.config.all.bot_login_key or "Cnr9,gMmqq$6Aaxsx=y<cd%#E3jBNQ") and username:starts("bot") then
print("bot login ", username)
login.loginSucc(clientId, username, nil, nil, "")
return
end
end
if extraInfo.password ~= getPwd(extraInfo.username) then
print("password incorrect!")
cs_stub.onPasswordError()
return
else
print("verify correct!")
end
if digest ~= pg.def_digest then
gamelog.info(string.format("def digest mismatch server[%s] client[%s]", pg.def_digest, digest))
end
if gameconfig.getConfig("enableMockLogin") then
if type(extraInfo) == "table" then
local username = extraInfo.username
login.loginSucc(clientId, username, nil, nil, "")
return
end
end
end
- 当验证不通过,如密码错误时,则通过cs.stub调用客户端的回调函数
function LoginCtrl:onPasswordError()
gamelog.error("Password Incorrect")
end
- 验证成功后,则通过loginapp连接到gateapp并请求Token返回给客户端,客户端则根据返回的数据实现下一步逻辑,这里主要是通过客户端框架调用C#中的ResMgr跳转场景
function LoginCtrl:onLoginSucc()
pg.global.resMgr:LoadLevel(nextScene, true)
pg.global.ui:close(UIConst.UI_ID_LOGIN)
end
这样就大致实现了登录的逻辑
Cambat System
在之前的介绍中,已经做过一个单机版本的战斗场景了,这里主要是扩展之前的场景,通过C#调用Lua接口实现服务器与客户端的数据同步
- 首先在场景的onLoaded中创建各个Entity
function CambatScene:onLoaded()
for k,v in ipairs(heroEntity) do
pg.global.entityMgr:CreateEntity(v, Avatar.Avatar)
end
for k,v in ipairs(monsterEntity) do
pg.global.entityMgr:CreateEntity(v, Monster.Monster)
end
for k,v in ipairs(dummyEntity) do
pg.global.entityMgr:CreateEntity(v, Dummy.Dummy)
end
end
- 在客户端对应的entity.lua下实现实体类
- 与动画层面的交互主要通过为每个实体封装了一个pg.global.entity的全局接口,而后通过C#调用Appfacade.instance.Calllua来实现用户操作事件的相应数据逻辑,动画表现则在原先的C#脚本中实现,如用户点击地面移动角色的流程如下:
1. AppFacade.instance.CallLua("pg.global.avatar.move", position); //C#的移动逻辑中调用实体接口
2. Avatar:Move(position) --设置Avator的位置,使其同步到服务器
- 由于服务器可以将entity数据持久化,所以当用户下一次登录时,则可以在C#的start()中通过调用上述的全局接口来获得服务器中的持久化数据(客户端调用serverMethod中定义的方法),从而实现如继续上次位置这样的功能
curPosition = AppFacade.instance.CallLua("pg.global.avatar.getCurPosition", null);
transform.position = curPosition;