本教程基于libuv,需要自行下载和搭建libuv开发环境,参考我之前的文章:C/C++服务器基础(网络、协议、数据库)
本教程基于之前搭建过的C++高效游戏服务器的单服架构,拓展成为多服。
分布式结构
用户访问的服务器可以分为两种,一种是web服务器,一种是网关服务器,并通过网关服务器来访问其他的服务器。其他服务器主要有用户服务器、系统服务器、逻辑服务器。用户服务器、系统服务器的很多功能都是通用的。
分布式基础结构
1: 负责验证和转发客户端过来的消息,给服务器;
2: 负责把服务器发给客户端的消息转给客户端;
3: 网关服务器优点:
(1) 带来系统健壮性
网关服务器这种经过锤百炼的进程不容易挂掉,而且后面的业务进程经常改变,很容易出问题。
当业务进程挂掉时,玩家仍然和网关连接着,所以业务进程可以重启而且玩家却不会感知到;
(2)独立网关服务器可以分散业务进程的压力,让服务器可以容纳更多人。
(3)负载均衡和广播消息的负载均衡
(4)等等;
服务器配置表设计
1: 新建Stype.lua文件,编写游戏里面的服务类型;
2: 新建Ctype.lua 文件,来编写游戏中的命令值,可以来自protobuf enum
3: 新建 game_config 文件来配置游戏中的网关与远程的服务器;
4: 编写网关服务结构
5: 编写Auth用户中新服务器结构;
6: 编写独立启动脚本
服务号类型stype.lua
local Stype = {
Auth = 1,
System = 2,
Logic = 3,
}
return Stype
命令号类型Cmd.lua
local Cmd = {
eLoginReq = 1,
eLoginRes = 2,
eExitReq = 3,
eExitRes = 4,
eSendMsgReq = 5,
eSendMsgRes = 6,
eOnUserLogin = 7,
eOnUserExit = 8,
eOnSendMsg = 9,
}
return Cmd
服务器配置game_config.lua
local Stype = require("Stype")
local remote_servers = {}
-- 注册我们的服务所部署的IP地址和端口
remote_servers[Stype.Auth] = {
stype = Stype.Auth,
ip = "127.0.0.1",
port = 8000,
desic = "Auth server",
}
--[[
remote_servers[Stype.System] = {
stype = Stype.System,
ip = "127.0.0.1",
port = 8001,
}
]]
local game_config = {
gateway_tcp_ip = "127.0.0.1",
gateway_tcp_port = 6080,
gateway_ws_ip = "127.0.0.1",
gateway_ws_port = 6081,
servers = remote_servers,
}
return game_config
网关服务器设计
基础框架main.lua
--初始化日志模块
Logger.init("logger/gateway/", "gateway", true)
--end
-- 初始化协议模块
local proto_type = {
PROTO_JSON = 0,
PROTO_BUF = 1,
}
ProtoMan.init(proto_type.PROTO_BUF)
-- 如果是protobuf协议,还要注册一下映射表
if ProtoMan.proto_type() == proto_type.PROTO_BUF then
local cmd_name_map = require("cmd_name_map")
if cmd_name_map then
ProtoMan.register_protobuf_cmd_map(cmd_name_map)
end
end
--end
local game_config = require("game_config")
-- 开启网关端口监听
Netbus.tcp_listen(game_config.gateway_tcp_port)
print("Tcp server listen At "..game_config.gateway_tcp_port)
Netbus.ws_listen(game_config.gateway_ws_port)
print("Ws server listen At "..game_config.gateway_ws_port)
--Netbus.udp_listen(8002)
--end
local servers = game_config.servers
local gw_service = require("gateway/gw_service")
--在各个服务器注册gateway的服务
for k, v in pairs(servers) do
local ret = Service.register(v.stype, gw_service)
if ret then
print("register gw_servce:[" .. v.stype.. "] success!!!")
else
print("register gw_servce:[" .. v.stype.. "] failed!!!")
end
end
gw_service.lua
-- {stype, ctype, utag, body}
function on_gw_recv_cmd(s, msg)
end
function on_gw_session_disconnect(s)
end
local gw_service = {
on_session_recv_cmd = on_gw_recv_cmd,
on_session_disconnect = on_gw_session_disconnect,
}
return gw_service
验证服务器设计
基础框架main.lua
--初始化日志模块
Logger.init("logger/auth_server/", "auth", true)
--end
-- 初始化协议模块
local proto_type = {
PROTO_JSON = 0,
PROTO_BUF = 1,
}
ProtoMan.init(proto_type.PROTO_BUF)
-- 如果是protobuf协议,还要注册一下映射表
if ProtoMan.proto_type() == proto_type.PROTO_BUF then
local cmd_name_map = require("cmd_name_map")
if cmd_name_map then
ProtoMan.register_protobuf_cmd_map(cmd_name_map)
end
end
--end
local game_config = require("game_config")
local servers = game_config.servers
local Stype = require("Stype")
-- 开启网关端口监听
Netbus.tcp_listen(servers[Stype.Auth].port)
print("Auth Server Start at ".. servers[Stype.Auth].port)
--Netbus.udp_listen(8002)
--end
local auth_service = require("auth_server/auth_service")
local ret = Service.register(Stype.Auth, auth_service)
if ret then
print("register Auth service:[" .. Stype.Auth.. "] success!!!")
else
print("register Auth service:[" .. Stype.Auth.. "] failed!!!")
end
auth_service.lua
-- {stype, ctype, utag, body}
function on_auth_recv_cmd(s, msg)
end
function on_auth_session_disconnect(s)
end
local auth_service = {
on_session_recv_cmd = on_auth_recv_cmd,
on_session_disconnect = on_auth_session_disconnect,
}
return auth_service
启动方式
C语言入口代码回顾:
int main(int argc, char** argv) {
netbus::instance()->init();
lua_wrapper::init();
if (argc != 3) { // 测试
std::string search_path = "../../apps/lua/scripts/";
lua_wrapper::add_search_path(search_path);
std::string lua_file = search_path + "gateway/main.lua";
lua_wrapper::do_file(lua_file);
// end
}
else {
std::string search_path = argv[1];
if (*(search_path.end() - 1) != '/') {
search_path += "/";
}
lua_wrapper::add_search_path(search_path);
std::string lua_file = search_path + argv[2];
lua_wrapper::do_file(lua_file);
}
netbus::instance()->run();
lua_wrapper::exit();
system("pause");
return 0;
}
因此用控制台打开时,我们可以自由设置调用的脚本。在项目生成之后,可以在Debug的目录下找到exe文件,使用.bat文件来执行这个exe文件并赋予对应的参数,这样就能灵活启动多个服务器。shell脚本如下:
gameserver.exe ../../apps/lua/scripts/ gateway/main.lua
在windows双击运行就能启动服务器,可以同时复制几份这个.bat文件,基于不同的参数,启动不同的服务器
网关服务器
数据转发与连接管理
原理:网关一开始会主动链接各个服务器,并读取服务器的配置表,把service注册到对应的stype上。
根据用户传过来的stype,就能确定把消息转发到什么服务器上。用户传递数据的时候也会打上utag,方便根据utag找session。当其他服务器给用户发消息时,发过来的session也会带有用户的utag,这样网关服务器就能通过utag找到对应的用户session了。
步骤:
1: service添加接口,注册转发服务, 和普通服务的区别是不用解码命令内容;
2: 编写gw_service模块, 为每个session生成ukey来保存session,如果是登陆了后就用uid;
3: 判断session是否为客户端发过来的数据,如果是转发给服务器, 打上utag–>数据包里面;
4: 判断session是否为服务器转发过来数据,如果是根据utag来找session,发给客户端 ;
为了方便网关转发,给c语言底层加上消息的直接不解码传输的功能
框架调整
proto_man新增内容
struct raw_cmd {
int stype;
int ctype;
unsigned int utag;
unsigned char* raw_data;
int raw_len;
};
class proto_man {
public:
static bool decode_raw_cmd(unsigned char* cmd, int cmd_len, struct raw_cmd* raw);
};
bool
proto_man::decode_raw_cmd(unsigned char* cmd, int cmd_len, struct raw_cmd* raw) {
if (cmd_len < CMD_HEADER) {
return false;
}
raw->stype = cmd[0] | (cmd[1] << 8);
raw->ctype = cmd[2] | (cmd[3] << 8);
raw->utag = cmd[4] | (cmd[5] << 8) | (cmd[6] << 16) | (cmd[7] << 24);
raw->raw_data = cmd;
raw->raw_len = cmd_len;
return true;
}
netbus.cc修改
static void
on_recv_client_cmd(session* s, unsigned char* body, int len) {
struct raw_cmd raw;
if (proto_man::decode_raw_cmd(body, len, &raw)) {
if (!service_man::on_recv_raw_cmd((session*)s, &raw)) {
s->close();
}
}
}
proto_man_export_to_lua.cc修改
bool
proto_man::decode_raw_cmd(unsigned char* cmd, int cmd_len, struct raw_cmd* raw) {
if (cmd_len < CMD_HEADER) {
return false;
}
raw->stype = cmd[0] | (cmd[1] << 8);
raw->ctype = cmd[2] | (cmd[3] << 8);
raw->utag = cmd[4] | (cmd[5] << 8) | (cmd[6] << 16) | (cmd[7] << 24);
raw->raw_cmd = cmd;
raw->raw_len = cmd_len;
return true;
}
service_man.cc修改
bool
service_man::on_recv_raw_cmd(session* s, struct raw_cmd* raw) {
if (g_service_set[raw->stype] == NULL) {
return false;
}
bool ret = false;
// 直接传递
if (g_service_set[raw->stype]->using_raw_cmd) {
return g_service_set[raw->stype]->on_session_recv_raw_cmd(s, raw);
}
// 进一步解码
struct cmd_msg* msg = NULL;
if (proto_man::decode_cmd_msg(raw->raw_data, raw->raw_len, &msg)) {
ret = g_service_set[raw->stype]->on_session_recv_cmd(s, msg);
proto_man::cmd_msg_free(msg);
}
return ret;
}
session_uv与udp_session 新增
void
uv_session::send_raw_cmd(struct raw_cmd* raw) {
this->send_data(raw->raw_data, raw->raw_len);
}
lua导出
Service服务注册raw数据传输
static int
lua_register_raw_service(lua_State* tolua_S) {
int stype = (int)tolua_tonumber(tolua_S, 1, 0);
bool ret = false;
// table
if (!lua_istable(tolua_S, 2)) {
goto lua_failed;
}
unsigned int lua_recv_raw_handler;
unsigned int lua_disconnect_handler;
lua_getfield(tolua_S, 2, "on_session_recv_raw_cmd");
lua_getfield(tolua_S, 2, "on_session_disconnect");
// stack 3 on_session_recv_cmd , 4on_session_disconnect
lua_recv_raw_handler = save_service_function(tolua_S, 3, 0);
lua_disconnect_handler = save_service_function(tolua_S, 4, 0);
if (lua_recv_raw_handler == 0 || lua_disconnect_handler == 0) {
goto lua_failed;
}
// register service
lua_service* s = new lua_service();
s->using_raw_cmd = true;
s->lua_disconnect_handler = lua_disconnect_handler;
s->lua_recv_cmd_handler = 0;
s->lua_recv_raw_handler = lua_recv_raw_handler;
ret = service_man::register_service(stype, s);
// end
lua_failed:
lua_pushboolean(tolua_S, ret ? 1 : 0);
return 1;
}
tolua_function(tolua_S, "register_with_raw", lua_register_raw_service);
给raw_cmd解码数据和设置utag功能
在lua端新增三个个功能函数,根据raw数据导出对应的header,body,给数据设置对应的utag
proto_man.cc
// RawCmd.read_header(raw_cmd)
static int
lua_raw_read_header(lua_State* tolua_S) {
int argc = lua_gettop(tolua_S);
if (argc != 1) {
goto lua_failed;
}
struct raw_cmd* raw = (struct raw_cmd*) tolua_touserdata(tolua_S, 1, NULL);
if (raw == NULL) {
goto lua_failed;
}
lua_pushinteger(tolua_S, raw->stype);
lua_pushinteger(tolua_S, raw->ctype);
lua_pushinteger(tolua_S, raw->utag);
return 3;
lua_failed:
return 0;
}
static int
lua_raw_read_body(lua_State* tolua_S) {
int argc = lua_gettop(tolua_S);
if (argc != 1) {
goto lua_failed;
}
struct raw_cmd* raw = (struct raw_cmd*) tolua_touserdata(tolua_S, 1, NULL);
if (raw == NULL) {
goto lua_failed;
}
struct cmd_msg* msg;
if (proto_man::decode_cmd_msg(raw->raw_data, raw->raw_len, &msg)) {
if (msg->body == NULL) {
lua_pushnil(tolua_S);
}
else if (proto_man::proto_type() == PROTO_JSON) {
lua_pushstring(tolua_S, (const char*)msg->body);
}
else {
push_proto_message_tolua((Message*)msg->body);
}
proto_man::cmd_msg_free(msg);
}
return 1;
lua_failed:
return 0;
}
static int
lua_raw_set_utag(lua_State* tolua_S) {
int argc = lua_gettop(tolua_S);
if (argc != 2) {
goto lua_failed;
}
struct raw_cmd* raw = (struct raw_cmd*) tolua_touserdata(tolua_S, 1, NULL);
if (raw == NULL) {
goto lua_failed;
}
unsigned int utag = (unsigned int)luaL_checkinteger(tolua_S, 2);
raw->utag = utag;
// 修改我们的body内存;
unsigned char* utag_ptr = raw->raw_data + 4;
utag_ptr[0] = (utag & 0x000000FF);
utag_ptr[1] = ((utag & 0x0000FF00) >> 8);
utag_ptr[2] = ((utag & 0x00FF0000) >> 16);
utag_ptr[3] = ((utag & 0xFF000000) >> 24);
return 0;
lua_failed:
return 0;
}
int
register_raw_cmd_export(lua_State* tolua_S) {
lua_getglobal(tolua_S, "_G");
if (lua_istable(tolua_S, -1)) {
tolua_open(tolua_S);
tolua_module(tolua_S, "RawCmd", 0);
tolua_beginmodule(tolua_S, "RawCmd");
tolua_function(tolua_S, "read_header", lua_raw_read_header);
tolua_function(tolua_S, "read_body", lua_raw_read_body);
tolua_function(tolua_S, "set_utag", lua_raw_set_utag);
tolua_endmodule(tolua_S);
}
lua_pop(tolua_S, 1);
return 0;
}
gateway注册服务与连接管理(重要)
main.lua
local servers = game_config.servers
local gw_service = require("gateway/gw_service")
for k, v in pairs(servers) do
local ret = Service.register_with_raw(v.stype, gw_service)
if ret then
print("register gw_servce:[" .. v.stype.. "] success!!!")
else
print("register gw_servce:[" .. v.stype.. "] failed!!!")
end
end
gw_service.lua
连接管理
local game_config = require("game_config")
-- stype --> session的一个映射
local server_session_man = {}
-- 当前正在做连接的服务器
local do_connecting = {}
function connect_to_server(stype, ip, port)
Netbus.tcp_connect(ip, port, function(err, session)
do_connecting[stype] = false
if err ~= 0 then
Logger.error("connect error to server ["..game_config.servers[stype].desic.."]"..ip..":"..port)
return
end
server_session_man[stype] = session
print("connect success to server ["..game_config.servers[stype].desic.."]"..ip..":"..port)
end)
end
function check_server_connect()
for k, v in pairs(game_config.servers) do
if server_session_man[v.stype] == nil and
do_connecting[v.stype] == false then
do_connecting[v.stype] = true
print("connecting to server ["..v.desic.."]"..v.ip..":"..v.port)
connect_to_server(v.stype, v.ip, v.port)
end
end
end
function gw_service_init()
for k, v in pairs(game_config.servers) do
server_session_man[v.stype] = nil
do_connecting[v.stype] = false
end
-- 启动一个定时器
Scheduler.schedule(check_server_connect, 1000, -1, 5000)
-- end
end
-- {stype, ctype, utag, body}
function on_gw_recv_raw_cmd(s, raw_cmd)
end
function on_gw_session_disconnect(s)
-- 连接到服务器的session断线了
if Session.asclient(s) then
for k, v in pairs(server_session_man) do
if v == s then
print("gateway disconnect ["..game_config.servers[k].desic.."]")
server_session_man[k] = nil
return
end
end
return
end
-- 连接到网关的客户端断线了
end
gw_service_init()
local gw_service = {
on_session_recv_raw_cmd = on_gw_recv_raw_cmd,
on_session_disconnect = on_gw_session_disconnect,
}
return gw_service
流程梳理
网关注册所有的服务,其他服务器注册自己的服务(lua端设置)->与其他服务器保持连接->用户发送数据->网关收到数据->netbus处理数据->proto_man数据解包得到服务号,命令号—>service_man把数据传给对应的网关service(lua端设置)->网关service调用接收函数(lua端设置)->设置utag->转发给别的服务器
utag管理
1: 服务器收到网关的数据包要知道是哪个用户,网关收到服务器数据包要知道转发给哪个用户
网关打上utag,转给服务器, 服务器打上utag转给网关;
网关做好utag–> 客户端session的映射;
登陆之前网关生成utag,放入到一个表, utag–>客户端 session的映射表;
登陆以后网关使用uid 放到一个uid–>客户端session的映射表;
代码变更:
1: Session导出send_raw_cmd
2: 给Session加一个 uid的字段;
3: 给Session到处get/set uid;
4: utag用来标记没有登陆前找到客户端session的钥匙;
5: uid 用来标记登陆后的用户;
session部分函数的lua导出
static int
lua_send_raw_cmd(lua_State* tolua_S) {
session* s = (session*)tolua_touserdata(tolua_S, 1, NULL);
if (s == NULL) {
goto lua_failed;
}
struct raw_cmd* cmd = (struct raw_cmd*) tolua_touserdata(tolua_S, 2, NULL);
if (cmd == NULL) {
goto lua_failed;
}
s->send_raw_cmd(cmd);
lua_failed:
return 0;
}
static int
lua_set_uid(lua_State* tolua_S) {
session* s = (session*)tolua_touserdata(tolua_S, 1, NULL);
if (s == NULL) {
goto lua_failed;
}
unsigned int uid = lua_tointeger(tolua_S, 2);
s->uid = uid;
lua_failed:
return 0;
}
static int
lua_get_uid(lua_State* tolua_S) {
session* s = (session*)tolua_touserdata(tolua_S, 1, NULL);
if (s == NULL) {
goto lua_failed;
}
lua_pushinteger(tolua_S, s->uid);
return 1;
lua_failed:
return 0;
}
tolua_function(tolua_S, "send_raw_cmd", lua_send_raw_cmd);
tolua_function(tolua_S, "set_uid", lua_set_uid);
tolua_function(tolua_S, "get_uid", lua_get_uid);
网关服务器service代码
注意,这里修改了service断开连接函数的底层和导出的lua函数,使得函数增加了stype的参数,识别是哪个服务断开了链接
local game_config = require("game_config")
-- stype --> session的一个映射
local server_session_man = {}
-- 当前正在做连接的服务器
local do_connecting = {}
-- 临时的ukey 来找client session
local g_ukey = 1
local client_sessions_ukey = {}
-- uid来找client session,登录后才有
local client_sessions_uid = {}
local Stype = require("Stype")
local Cmd = require("Cmd")
local Respones = require("Respones")
function connect_to_server(stype, ip, port)
Netbus.tcp_connect(ip, port, function(err, session)
do_connecting[stype] = false
if err ~= 0 then
Logger.error("connect error to server ["..game_config.servers[stype].desic.."]"..ip..":"..port)
return
end
server_session_man[stype] = session
print("connect success to server ["..game_config.servers[stype].desic.."]"..ip..":"..port)
end)
end
function check_server_connect()
for k, v in pairs(game_config.servers) do
if server_session_man[v.stype] == nil and
do_connecting[v.stype] == false then
do_connecting[v.stype] = true
print("connecting to server ["..v.desic.."]"..v.ip..":"..v.port)
connect_to_server(v.stype, v.ip, v.port)
end
end
end
--初始化函数,启动所有服务器,并持续检查服务器连接情况
function gw_service_init()
for k, v in pairs(game_config.servers) do
server_session_man[v.stype] = nil
do_connecting[v.stype] = false
end
-- 启动一个定时器
Scheduler.schedule(check_server_connect, 1000, -1, 5000)
end
--判断是否登录应答
function is_login_return_cmd(ctype)
if ctype == Cmd.eGuestLoginRes then
return true
end
return false
end
--数据发送给客户端
function send_to_client(server_session, raw_cmd)
local stype, ctype, utag = RawCmd.read_header(raw_cmd)
local client_session = nil
--登录应答
if is_login_return_cmd(ctype) then
client_session = client_sessions_ukey[utag]
client_sessions_ukey[utag] = nil
if client_session == nil then
return
end
local body = RawCmd.read_body(raw_cmd)
--登录不成功
if body.status ~= Respones.OK then
RawCmd.set_utag(raw_cmd, 0)
Session.send_raw_cmd(client_session, raw_cmd)
return
end
local uid = body.uinfo.uid
-- 判断一下,是否有已经登陆的session,把别的登录用户踢出
if client_sessions_uid[uid] and client_sessions_uid[uid] ~= client_session then
local relogin_cmd = {Stype.Auth, Cmd.eRelogin, 0, nil}
Session.send_msg(client_sessions_uid[uid], relogin_cmd)
Session.close(client_sessions_uid[uid])
-- client_sessions_uid[uid] = nil
end
client_sessions_uid[uid] = client_session
Session.set_uid(client_session, uid)
--登录成功,返回成功应答
body.uinfo.uid = 0;
local login_res = {stype, ctype, 0, body}
Session.send_msg(client_session, login_res)
return
end
--非登录应答,普通传递数据
client_session = client_sessions_uid[utag]
RawCmd.set_utag(raw_cmd, 0)
if client_session then
Session.send_raw_cmd(client_session, raw_cmd)
end
end
function is_login_request_cmd(ctype)
if ctype == Cmd.eGuestLoginReq then
return true
end
return false
end
-- s 来自于客户端,发送给其他服务器
function send_to_server(client_session, raw_cmd)
local stype, ctype, utag = RawCmd.read_header(raw_cmd)
print(stype, ctype, utag)
local server_session = server_session_man[stype]
if server_session == nil then --可以回一个命令给客户端,系统错误
return
end
--登录请求
if is_login_request_cmd(ctype) then
--生成g_ukey作为utag
utag = Session.get_utag(client_session)
if utag == 0 then
utag = g_ukey
g_ukey = g_ukey + 1
Session.set_utag(client_session, utag)
end
client_sessions_ukey[utag] = client_session
else --非登录数据传输,检查是否处于登录状态
local uid = Session.get_uid(client_session)
utag = uid
if utag == 0 then --改操作要先登陆
return
end
-- client_sessions_uid[uid] = client_session;
end
-- 打上utag然后转发给我们的服务器
RawCmd.set_utag(raw_cmd, utag)
Session.send_raw_cmd(server_session, raw_cmd)
end
-- {stype, ctype, utag, body}
function on_gw_recv_raw_cmd(s, raw_cmd)
if Session.asclient(s) == 0 then --转发给服务器
send_to_server(s, raw_cmd)
else
send_to_client(s, raw_cmd)
end
end
function on_gw_session_disconnect(s, stype)
-- 连接到服务器的session断线了
if Session.asclient(s) == 1 then
for k, v in pairs(server_session_man) do
if v == s then
print("gateway disconnect ["..game_config.servers[k].desic.."]")
server_session_man[k] = nil
return
end
end
return
end
-- 连接到网关的客户端断线了
-- 把客户端从临时映射表里面删除
local utag = Session.get_utag(s)
if client_sessions_ukey[utag] ~= nil and client_sessions_ukey[utag] == s then
client_sessions_ukey[utag] = nil -- 保证utag --> value 删除
Session.set_utag(s, 0)
end
-- end
-- 把客户端从uid映射表里移除
local uid = Session.get_uid(s)
if client_sessions_uid[uid] ~= nil and client_sessions_uid[uid] == s then
client_sessions_uid[uid] = nil
end
-- end
local server_session = server_session_man[stype]
if server_session == nil then
return
end
-- 客户端uid用户掉线了,我要把这个事件告诉和网关所连接的stype类服务器
if uid ~= 0 then
local user_lost = {stype, Cmd.eUserLostConn, uid, nil}
Session.send_msg(server_session, user_lost)
end
end
gw_service_init()
local gw_service = {
on_session_recv_raw_cmd = on_gw_recv_raw_cmd,
on_session_disconnect = on_gw_session_disconnect,
}
return gw_service
其他服务器service收发数据
local Stype = require("Stype")
local Cmd = require("Cmd")
-- {stype, ctype, utag, body}
function on_auth_recv_cmd(s, msg)
print(msg[1], msg[2], msg[3])
local res_msg = {Stype.Auth, Cmd.eLoginRes, msg[3], { status = 200 }}
Session.send_msg(s, res_msg)
end
local auth_service = {
on_session_recv_cmd = on_auth_recv_cmd,
on_session_disconnect = on_auth_session_disconnect,
}
Auto用户服务器
这个服务器管理这用户中心数据库,是所有游戏服务器中共用的部分
数据库设计
1: 用户中心服务器的数据库需要独立部署,主要用于让平台通用的账号系统能玩不同的游戏;
2: 用户中心服务器的数据库是独立的,存放玩家的基本信息;
3: 数据库的设计: utf8, auth_center数据库, 数据表为uinfo;
uinfo数据表的设计:
uid: 用户唯一的id号 INT数据类型
unick: 用户的昵称; VARCHAR
usex: 用户性别; 0男1女
uface: 用户图像; 系统默认的用户头像, 整数
uname: 用户名字 VARCHAR
upwd: 用户密码; VARCHAR
phone: 用户电话; VARCHAR
email: 用户邮件 VARCHAR
address: 用户城市; VARCHAR
is_guest: 是否为游客账号 1/0
guest_key: 游客账号的key; 游客的唯一的key;
uvip: 用户vip等级, VIP等级
vip_end_time: 结束时间撮; VIP结束的时间;
status: 用户状态,0为正常,其他看需求定
1: game_config里面添加数据库
auth_center: {
host = “127.0.0.1”,
port = 3306,
db_name = “auth_center”,
uname = “root”,
upwd = “123456”,
},
2:编写auth_mysql.lua代码, 加载的时候连接数据库
local game_config = require(“game_config”)
local auth_db = game_config.auth_center
local mysql_conn = nil
编写connect_mysql函数
Mysql.connect(IP, port, db_name, uname, upwd, callback);
数据库连接代码
mysql_auth_center.lua
local game_config = require("game_config")
local mysql_conn = nil
local function is_connected()
if not mysql_conn then
return false
end
return true
end
function mysql_connect_to_auth_center()
local auth_conf = game_config.auth_mysql
Mysql.connect(auth_conf.host, auth_conf.port,
auth_conf.db_name, auth_conf.uname,
auth_conf.upwd, function(err, conn)
if err then
Logger.error(err)
Scheduler.once(mysql_connect_to_auth_center, 5000)
return
end
Logger.debug("connect to auth center db success!!!!")
mysql_conn = conn
end)
end
mysql_connect_to_auth_center()
游客的注册与登录
1: auth服务器处理游客登陆命令;
2: 新建一个auth登陆的命令表,然后将每个命令的处理函数配置好,接入服务;
3: 新建一个lua文件, guest.lua专门用来处理游客登陆,基本后续每个命令一个文件;
4: guest_login逻辑:
step1: 根据guest_key来查询数据库,如果没有,插入一条游客信息;
step2: 如果查询到了,比对下,这个账号是否已经升级,如果已经升级,拒绝游客模式登陆;
step3: 返回用户的信息到客户端;
5: 添加一个中心数据库表,导出外部需要调用的函数;
get_guest_uinfo
“select uid, unick, usex, uface, uvip, status, is_guest from uinfo where guest_key = "%s" limit 1”;
insert_guest_user
“insert into uinfo(guest_key
, unick
, uface
, usex
, is_guest
)values("%s", "%s", %d, %d, 1)”;
7: 添加Respones.lua表示Respones 返回状态描述;
客户端程序
public class user_login : Singletom<user_login> {
void on_guest_login_return(cmd_msg msg) {
GuestLoginRes res = proto_man.protobuf_deserialize<GuestLoginRes>(msg.body);
if (res == null){
return;
}
if (res.status != Respones.OK) {
Debug.Log("Guest Login status: " + res.status);
return;
}
UserCenterInfo uinfo = res.uinfo;
Debug.Log(uinfo.unick + " " + uinfo.usex);
}
// 登录的服务器回调函数
void on_auth_server_return(cmd_msg msg) {
switch (msg.ctype) {
case (int)Cmd.eGuestLoginRes:
this.on_guest_login_return(msg);
break;
}
}
// 初始化函数,注册服务监听
public void init() {
network.Instance.add_service_listener((int)Stype.Auth, this.on_auth_server_return);
}
// 发送登录信息
public void guest_login() {
string g_key = utils.rand_str(32);
g_key = "8JvrDstUNDuTNnnCKFEw4pKFs27z9xSr";
Debug.Log(g_key);
GuestLoginReq req = new GuestLoginReq();
req.guest_key = g_key;
network.Instance.send_protobuf_cmd((int)Stype.Auth, (int)Cmd.eGuestLoginReq, req);
}
}
服务器程序
数据库逻辑文件
mysql_auth_center.lua
包括登录数据库,提供查询数据,创建数据的功能
local game_config = require("game_config")
local mysql_conn = nil
--数据库连接
function mysql_connect_to_auth_center()
local auth_conf = game_config.auth_mysql
Mysql.connect(auth_conf.host, auth_conf.port,
auth_conf.db_name, auth_conf.uname,
auth_conf.upwd, function(err, conn)
if err then
Logger.error(err)
Scheduler.once(mysql_connect_to_auth_center, 5000)
return
end
Logger.debug("connect to auth center db success!!!!")
mysql_conn = conn
end)
end
mysql_connect_to_auth_center()
--查询用户信息
function get_guest_uinfo(g_key, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "select uid, unick, usex, uface, uvip, status, is_guest from uinfo where guest_key = \"%s\" limit 1"
local sql_cmd = string.format(sql, g_key)
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
-- 没有这条记录
if ret == nil or #ret <= 0 then
if ret_handler ~= nil then
ret_handler(nil, nil)
end
return
end
-- end
local result = ret[1]
local uinfo = {}
uinfo.uid = tonumber(result[1])
uinfo.unick = result[2]
uinfo.usex = tonumber(result[3])
uinfo.uface = tonumber(result[4])
uinfo.uvip = tonumber(result[5])
uinfo.status = tonumber(result[6])
uinfo.is_guest = tonumber(result[7])
ret_handler(nil, uinfo)
end)
end
--创建用户数据
function insert_guest_user(g_key, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local unick = "gst" .. math.random(100000, 999999)
local uface = math.random(1, 9)
local usex = math.random(0, 1)
local sql = "insert into uinfo(`guest_key`, `unick`, `uface`, `usex`, `is_guest`)values(\"%s\", \"%s\", %d, %d, 1)"
local sql_cmd = string.format(sql, g_key, unick, uface, usex)
Mysql.query(mysql_conn, sql_cmd, function (err, ret)
if err then
ret_handler(err, nil)
return
else
ret_handler(nil, nil)
end
end)
end
local mysql_auth_center = {
get_guest_uinfo = get_guest_uinfo,
insert_guest_user = insert_guest_user,
}
return mysql_auth_center
登录状态
local Respones = {
OK = 1,
SystemErr = -100,
UserIsFreeze = -101,
UserIsNotGuest = -102,
}
return Respones
游客登录逻辑
local mysql_center = require("database/mysql_auth_center")
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
-- {stype, ctype, utag, body}
function login(s, msg)
local g_key = msg[4].guest_key
local utag = msg[3];
print(msg[1], msg[2], msg[3], msg[4].guest_key)
mysql_center.get_guest_uinfo(g_key, function (err, uinfo)
if err then -- 告诉客户端系统错误信息;
local msg = {Stype.Auth, Cmd.eGuestLoginRes, utag, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
if uinfo == nil then -- 没有查到对应的 g_key的信息
mysql_center.insert_guest_user(g_key, function(err, ret)
if err then -- 告诉客户端系统错误信息;
local msg = {Stype.Auth, Cmd.eGuestLoginRes, utag, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
login(s, msg)
end)
return
end
-- 找到了我们gkey所对应的游客数据;
if uinfo.status ~= 0 then --账号被查封
local msg = {Stype.Auth, Cmd.eGuestLoginRes, utag, {
status = Respones.UserIsFreeze,
}}
Session.send_msg(s, msg)
return
end
if uinfo.is_guest ~= 1 then --账号已经不是游客账号了
local msg = {Stype.Auth, Cmd.eGuestLoginRes, utag, {
status = Respones.UserIsNotGuest,
}}
Session.send_msg(s, msg)
return
end
-- end
print(uinfo.uid, uinfo.unick) -- 登陆成功返回给客户端;
local msg = { Stype.Auth, Cmd.eGuestLoginRes, utag, {
status = Respones.OK,
uinfo = {
unick = uinfo.unick,
uface = uinfo.uface,
usex = uinfo.usex,
uvip = uinfo.uvip,
uid = uinfo.uid,
}
}}
Session.send_msg(s, msg)
end)
end
local guest = {
login = login
}
return guest
auto服务器入口
auth_service.lua
该文件的功能提供了命令的注册
local Stype = require("Stype")
local Cmd = require("Cmd")
local guest = require("auth_server/guest")
local auth_service_handlers = {}
--这里把登录的函数注册到eGuestLoginReq的处理中
auth_service_handlers[Cmd.eGuestLoginReq] = guest.login
-- {stype, ctype, utag, body}
function on_auth_recv_cmd(s, msg)
if auth_service_handlers[msg[2]] then
auth_service_handlers[msg[2]](s, msg)
end
end
function on_auth_session_disconnect(s, stype)
end
local auth_service = {
on_session_recv_cmd = on_auth_recv_cmd,
on_session_disconnect = on_auth_session_disconnect,
}
return auth_service
用户信息修改
协议
enum Cmd {
INVALID_CMD = 0;
eGuestLoginReq = 1;
eGuestLoginRes = 2;
eRelogin = 3;
eUserLostConn = 4;
eEditProfileReq = 5;
eEditProfileRes = 6;
}
message EditProfileReq {
required string unick = 1;
required int32 uface = 2;
required int32 usex = 3;
}
message EditProfileRes {
required int32 status = 1;
}
客户端
private EditProfileReq temp_req = null;
void on_edit_profile_return(cmd_msg msg) {
EditProfileRes res = proto_man.protobuf_deserialize<EditProfileRes>(msg.body);
if (res == null) {
return;
}
if (res.status != Respones.OK) {
Debug.Log("edit profle status: " + res.status);
return;
}
ugame.Instance.save_edit_profile(this.temp_req.unick, this.temp_req.uface, this.temp_req.usex);
this.temp_req = null;
event_manager.Instance.dispatch_event("sync_uinfo", null);
}
// 入口函数
public void edit_profile(string unick, int uface, int usex) {
if (unick.Length <= 0) {
return;
}
if (uface <= 0 || uface > 9) {
return;
}
if (usex != 0 && usex != 1) {
return;
}
// 提交我们修改资料的请求;
EditProfileReq req = new EditProfileReq();
req.unick = unick;
req.uface = uface;
req.usex = usex;
this.temp_req = req;
network.Instance.send_protobuf_cmd((int)Stype.Auth, (int)Cmd.eEditProfileReq, req);
// end
}
服务器
edit_profile.lua
local mysql_center = require("database/mysql_auth_center")
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
-- {stype, ctype, utag, body}
function do_edit_profile(s, req)
local uid = req[3]
local edit_profile_req = req[4]
print(uid, edit_profile_req.unick, edit_profile_req.uface, edit_profile_req.usex)
--检查数据合法性
if (string.len(edit_profile_req.unick) <= 0) or
(edit_profile_req.uface <= 0 or edit_profile_req.uface > 9) or
(edit_profile_req.usex ~= 0 and edit_profile_req.usex ~= 1) then -- 参数错误
local msg = {Stype.Auth, Cmd.eEditProfileRes, uid, {
status = Respones.InvalidParams,
}}
Session.send_msg(s, msg)
return
end
-- 更新用户中心数据库
local ret_handler = function(err, res)
local ret_status = Respones.OK
if err ~= nil then
ret_status = Respones.SystemErr
end
--
local msg = {Stype.Auth, Cmd.eEditProfileRes, uid, {
status = ret_status,
}}
Session.send_msg(s, msg)
end
mysql_center.edit_profile(uid, edit_profile_req.unick, edit_profile_req.uface, edit_profile_req.usex, ret_handler)
-- end
end
local edit_profile = {
do_edit_profile = do_edit_profile,
}
return edit_profile
mysql_auth_center.lua
function edit_profile(uid, unick, uface, usex, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "update uinfo set unick = \"%s\", usex = %d, uface = %d where uid = %d"
local sql_cmd = string.format(sql, unick, usex, uface, uid);
Mysql.query(mysql_conn, sql_cmd, function (err, ret)
if err then
ret_handler(err, nil)
return
else
ret_handler(nil, nil)
end
end)
end
local mysql_auth_center = {
get_guest_uinfo = get_guest_uinfo,
insert_guest_user = insert_guest_user,
edit_profile = edit_profile,
}
return mysql_auth_center
auth服务器注册对应的响应处理函数
local edit_profile = require("auth_server/edit_profile")
auth_service_handlers[Cmd.eEditProfileReq] = edit_profile.do_edit_profile
用户中心redis
redis作为内存数据库,比mysql快得多,适合需要频繁查询的操作
1: 哈希表–> key, 表{字段, 值}
HMSET key name “blake” age “1”
HGETALL key
HDEL key 字段 删除一个或多个字段
HEXISTS key 字段
HGET key 字段
HKEYS key 返回所有的字段filed
HMGET key filed
2: Hash表结果多用于存储数据, 存入在redis里面的都是字符串
使用规则:
1: 用户中心redis的Hash表:
key: moba_auth_center_user_uid_xxxxxx;
value: unick, uface, usex, uvip, is_guest
2: 编写代码: redis_center.lua
connect_to_center(host, port, db_index): 连接到redis中心数据库
set_uinfo_inredis(uid, uinfo): 设置用户信息到中心redis;
get_uinfo_inredis(uid, callback): 获取用户信息到中心redis;
edit_profile(uid, unick, uface, usex)跟新用户信息到redis;
3:key, –> value
moba_auth_center_user_uid_8791
uinfo : {
unick: string,
uface: 图像ID,
usex: 性别,
uvip: VIP等级
is_guest: 是否为游客
}
redis功能性文件
local game_config = require("game_config")
local redis_conn = nil
local function is_connected()
if not mysql_conn then
return false
end
return true
end
function redis_connect_to_center()
local host = game_config.center_redis.host
local port = game_config.center_redis.port
local db_index = game_config.center_redis.db_index
Redis.connect(host, port, function (err, conn)
if err ~= nil then
Logger.error(err)
Scheduler.once(redis_connect_to_center, 5000)
return
end
redis_conn = conn
Logger.debug("connect to redis center db success!!!!")
Redis.query(redis_conn, "select " .. db_index, function (err, ret)
end)
end)
end
redis_connect_to_center()
function set_uinfo_inredis(uid, uinfo)
if redis_conn == nil then
Logger.error("redis center disconnected")
return
end
local redis_cmd = "hmset moba_auth_center_user_uid_" .. uid ..
" unick " .. uinfo.unick ..
" usex " .. uinfo.usex ..
" uface " .. uinfo.uface ..
" uvip " .. uinfo.uvip ..
" is_guest " .. uinfo.is_guest
Redis.query(redis_conn, redis_cmd, function (err, ret)
if err then
return
end
Logger.debug("success")
end)
end
-- ret_handler(err, uinfo)
function get_uinfo_inredis(uid, ret_handler)
local redis_cmd = "hgetall moba_auth_center_user_uid_" .. uid
Redis.query(redis_conn, redis_cmd, function (err, ret)
if err then
if ret_handler then
ret_handler(err, nil)
end
return
end
local uinfo = {}
uinfo.uid = uid
uinfo.unick = ret[2]
uinfo.uface = tonumber(ret[4])
uinfo.uvip = tonumber(ret[6])
uinfo.usex = tonumber(ret[8])
uinfo.is_guest = tonumber(ret[10])
ret_handler(nil, uinfo)
end)
end
function edit_profile(uid, unick, uface, usex)
get_uinfo_inredis(uid, function (err, uinfo)
if err then
Logger.error("get uinfo inredis error")
return
end
uinfo.unick = unick
uinfo.usex = usex
uinfo.uface = uface
set_uinfo_inredis(uid, uinfo)
end)
end
local redis_center = {
set_uinfo_inredis = set_uinfo_inredis,
get_uinfo_inredis = get_uinfo_inredis,
edit_profile = edit_profile,
is_connected = is_connected,
}
return redis_center
1: 配置中心redis数据库配置文件
2:登陆成功后,将用心信息存入redis;
3:修改用户资料后,就修改的信息存入redis
游客账号升级
协议
enum Cmd {
INVALID_CMD = 0;
eGuestLoginReq = 1;
eGuestLoginRes = 2;
eRelogin = 3;
eUserLostConn = 4;
eEditProfileReq = 5;
eEditProfileRes = 6;
eAccountUpgradeReq = 7;
eAccountUpgradeRes = 8;
}
message AccountUpgradeReq {
required string uname = 1;
required string upwd_md5 = 2;
}
message AccountUpgradeRes {
required int32 status = 1;
}
客户端
void on_guest_account_upgrade_return(cmd_msg msg) {
AccountUpgradeRes res = proto_man.protobuf_deserialize<AccountUpgradeRes>(msg.body);
if (res.status == Respones.OK) {
ugame.Instance.is_guest = false;
}
event_manager.Instance.dispatch_event("upgrade_account_return", res.status);
}
public void do_account_upgrade(string uname, string upwd_md5) {
AccountUpgradeReq req = new AccountUpgradeReq();
req.uname = uname;
req.upwd_md5 = upwd_md5;
network.Instance.send_protobuf_cmd((int)Stype.Auth, (int)Cmd.eAccountUpgradeReq, req);
}
void on_auth_server_return(cmd_msg msg) {
switch (msg.ctype) {
case (int)Cmd.eGuestLoginRes:
this.on_guest_login_return(msg);
break;
case (int)Cmd.eEditProfileRes:
this.on_edit_profile_return(msg);
break;
case (int)Cmd.eAccountUpgradeRes:
this.on_guest_account_upgrade_return(msg);
break;
}
}
public void init() {
network.Instance.add_service_listener((int)Stype.Auth, this.on_auth_server_return);
}
服务器
数据库逻辑文件新增
local game_config = require("game_config")
local mysql_conn = nil
function mysql_connect_to_auth_center()
local auth_conf = game_config.auth_mysql
Mysql.connect(auth_conf.host, auth_conf.port,
auth_conf.db_name, auth_conf.uname,
auth_conf.upwd, function(err, conn)
if err then
Logger.error(err)
Scheduler.once(mysql_connect_to_auth_center, 5000)
return
end
Logger.debug("connect to auth center db success!!!!")
mysql_conn = conn
end)
end
mysql_connect_to_auth_center()
function do_guest_account_upgrade(uid, uname, upwd_md5, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "update uinfo set uname = \"%s\", upwd = \"%s\", is_guest = 0 where uid = %d"
local sql_cmd = string.format(sql, uname, upwd_md5, uid)
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
if ret_handler then
ret_handler(nil, nil)
end
end)
end
function check_uname_exist(uname, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "select uid from uinfo where uname = \"%s\""
local sql_cmd = string.format(sql, uname)
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
if ret == nil or #ret <= 0 then
if ret_handler ~= nil then
ret_handler(nil, nil)
end
return
end
if ret_handler then
ret_handler(nil, ret)
end
end)
end
function get_uinfo_by_uid(uid, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "select uid, unick, usex, uface, uvip, status, is_guest from uinfo where uid = %d limit 1"
local sql_cmd = string.format(sql, uid)
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
-- 没有这条记录
if ret == nil or #ret <= 0 then
if ret_handler ~= nil then
ret_handler(nil, nil)
end
return
end
-- end
local result = ret[1]
local uinfo = {}
uinfo.uid = tonumber(result[1])
uinfo.unick = result[2]
uinfo.usex = tonumber(result[3])
uinfo.uface = tonumber(result[4])
uinfo.uvip = tonumber(result[5])
uinfo.status = tonumber(result[6])
uinfo.is_guest = tonumber(result[7])
ret_handler(nil, uinfo)
end)
end
function get_guest_uinfo(g_key, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "select uid, unick, usex, uface, uvip, status, is_guest from uinfo where guest_key = \"%s\" limit 1"
local sql_cmd = string.format(sql, g_key)
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
-- 没有这条记录
if ret == nil or #ret <= 0 then
if ret_handler ~= nil then
ret_handler(nil, nil)
end
return
end
-- end
local result = ret[1]
local uinfo = {}
uinfo.uid = tonumber(result[1])
uinfo.unick = result[2]
uinfo.usex = tonumber(result[3])
uinfo.uface = tonumber(result[4])
uinfo.uvip = tonumber(result[5])
uinfo.status = tonumber(result[6])
uinfo.is_guest = tonumber(result[7])
ret_handler(nil, uinfo)
end)
end
function insert_guest_user(g_key, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local unick = "gst" .. math.random(100000, 999999)
local uface = math.random(1, 9)
local usex = math.random(0, 1)
local sql = "insert into uinfo(`guest_key`, `unick`, `uface`, `usex`, `is_guest`)values(\"%s\", \"%s\", %d, %d, 1)"
local sql_cmd = string.format(sql, g_key, unick, uface, usex)
Mysql.query(mysql_conn, sql_cmd, function (err, ret)
if err then
ret_handler(err, nil)
return
else
ret_handler(nil, nil)
end
end)
end
function edit_profile(uid, unick, uface, usex, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "update uinfo set unick = \"%s\", usex = %d, uface = %d where uid = %d"
local sql_cmd = string.format(sql, unick, usex, uface, uid);
Mysql.query(mysql_conn, sql_cmd, function (err, ret)
if err then
ret_handler(err, nil)
return
else
ret_handler(nil, nil)
end
end)
end
local mysql_auth_center = {
get_guest_uinfo = get_guest_uinfo,
insert_guest_user = insert_guest_user,
edit_profile = edit_profile,
check_uname_exist = check_uname_exist,
do_guest_account_upgrade = do_guest_account_upgrade,
get_uinfo_by_uid = get_uinfo_by_uid,
}
return mysql_auth_center
更新逻辑文件
account_upgrade.lua
local mysql_center = req uire("database/mysql_auth_center")
local redis_center = require("database/redis_center")
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
--真正完成数据库的操作,并回复升级完成的信息给客户端
function _do_account_upgrade(s, req, uid, uname, upwd_md5)
mysql_center.do_guest_account_upgrade(uid, uname, upwd_md5, function (err, ret)
if err then
local msg = {Stype.Auth, Cmd.eAccountUpgradeRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
local msg = {Stype.Auth, Cmd.eAccountUpgradeRes, uid, {
status = Respones.OK,
}}
Session.send_msg(s, msg)
end)
end
function _check_is_guest(s, req, uid, uname, upwd_md5)
mysql_center.get_uinfo_by_uid(uid, function (err, uinfo)
if err then
local msg = {Stype.Auth, Cmd.eAccountUpgradeRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
if uinfo.is_guest ~= 1 then -- 不是游客账号了
local msg = {Stype.Auth, Cmd.eAccountUpgradeRes, uid, {
status = Respones.UserIsNotGuest,
}}
Session.send_msg(s, msg)
return
end
--真正的更新
_do_account_upgrade(s, req, uid, uname, upwd_md5)
end)
end
function do_upgrade(s, req)
local uid = req[3]
local account_upgrade_req = req[4]
local uname = account_upgrade_req.uname
local upwd_md5 = account_upgrade_req.upwd_md5
--检查md5密码的合法性
if string.len(uname) <= 0 or string.len(upwd_md5) ~= 32 then
local msg = {Stype.Auth, Cmd.eAccountUpgradeRes, uid, {
status = Respones.InvalidParams,
}}
Session.send_msg(s, msg)
return
end
--检查名字是否存在
mysql_center.check_uname_exist(uname, function (err, ret)
if err then
local msg = {Stype.Auth, Cmd.eAccountUpgradeRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
if ret then -- uname已经被占用了
local msg = {Stype.Auth, Cmd.eAccountUpgradeRes, uid, {
status = Respones.UnameIsExist,
}}
Session.send_msg(s, msg)
return
end
--检查是否游客
_check_is_guest(s, req, uid, uname, upwd_md5)
end)
end
local account_upgrade = {
do_upgrade = do_upgrade,
}
return account_upgrade
auth服务器注册对应的响应处理函数
local account_upgrade = require("auth_server/account_upgrade");
auth_service_handlers[Cmd.eAccountUpgradeReq] = account_upgrade.do_upgrade
正式账号登录
协议
客户端
public void uname_login(string uname, string upwd) {
string upwd_md5 = utils.md5(upwd);
Debug.Log(uname + " " + upwd_md5);
UnameLoginReq req = new UnameLoginReq();
req.uname = uname;
req.upwd = upwd_md5;
network.Instance.send_protobuf_cmd((int)Stype.Auth, (int)Cmd.eUnameLoginReq, req);
}
void on_uname_login_return(cmd_msg msg) {
UnameLoginRes res = proto_man.protobuf_deserialize<UnameLoginRes>(msg.body);
if (res == null) {
return;
}
if (res.status != Respones.OK) {
Debug.Log("Uname Login status: " + res.status);
return;
}
UserCenterInfo uinfo = res.uinfo;
ugame.Instance.save_uinfo(uinfo, false);
event_manager.Instance.dispatch_event("login_success", null);
event_manager.Instance.dispatch_event("sync_uinfo", null);
}
服务器
数据库逻辑文件新增
function get_uinfo_by_uname_upwd(uname, upwd, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "select uid, unick, usex, uface, uvip, status, is_guest from uinfo where uname = \"%s\" and upwd = \"%s\" limit 1"
local sql_cmd = string.format(sql, uname, upwd)
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
-- 没有这条记录
if ret == nil or #ret <= 0 then
if ret_handler ~= nil then
ret_handler(nil, nil)
end
return
end
-- end
local result = ret[1]
local uinfo = {}
uinfo.uid = tonumber(result[1])
uinfo.unick = result[2]
uinfo.usex = tonumber(result[3])
uinfo.uface = tonumber(result[4])
uinfo.uvip = tonumber(result[5])
uinfo.status = tonumber(result[6])
uinfo.is_guest = tonumber(result[7])
ret_handler(nil, uinfo)
end)
end
local mysql_auth_center = {
get_uinfo_by_uname_upwd = get_uinfo_by_uname_upwd,
}
正式账号登录逻辑文件
uname_login.lua
local mysql_center = require("database/mysql_auth_center")
local redis_center = require("database/redis_center")
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
-- {stype, ctype, utag, body}
function login(s, req)
local utag = req[3]
local uname_login_req = req[4]
-- 检查参数
if string.len(uname_login_req.uname) <= 0 or
string.len(uname_login_req.upwd) ~= 32 then
local msg = {Stype.Auth, Cmd.eUnameLoginRes, utag, {
status = Respones.InvalidParams,
}}
Session.send_msg(s, msg)
return
end
-- 检查用户名和密码是否正确
mysql_center.get_uinfo_by_uname_upwd(uname_login_req.uname, uname_login_req.upwd, function (err, uinfo)
if err then -- 告诉客户端系统错误信息;
local msg = {Stype.Auth, Cmd.eUnameLoginRes, utag, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
if uinfo == nil then -- 没有查到对应的 用户,返回不存在用户,或密码错误
local msg = {Stype.Auth, Cmd.eUnameLoginRes, utag, {
status = Respones.UnameOrUpwdError,
}}
Session.send_msg(s, msg)
return
end
if uinfo.status ~= 0 then --账号被查封
local msg = {Stype.Auth, Cmd.eUnameLoginRes, utag, {
status = Respones.UserIsFreeze,
}}
Session.send_msg(s, msg)
return
end
-- end
redis_center.set_uinfo_inredis(uinfo.uid, uinfo)
local msg = { Stype.Auth, Cmd.eUnameLoginRes, utag, {
status = Respones.OK,
uinfo = {
unick = uinfo.unick,
uface = uinfo.uface,
usex = uinfo.usex,
uvip = uinfo.uvip,
uid = uinfo.uid,
}
}}
Session.send_msg(s, msg)
end)
--end
end
local uname_login = {
login = login,
}
return uname_login
用户账号注销
协议
enum Cmd {
eLoginOutReq = 11;
eLoginOutRes = 12;
}
message LoginOutRes {
required int32 status = 1;
}
客户端
public void user_login_out() {
network.Instance.send_protobuf_cmd((int)Stype.Auth, (int)Cmd.eLoginOutReq, null);
}
void on_user_loginout_return(cmd_msg msg) {
LoginOutRes res = proto_man.protobuf_deserialize<LoginOutRes>(msg.body);
if (res == null) {
return;
}
if (res.status != Respones.OK) {
Debug.Log("Guest Login status: " + res.status);
return;
}
// 注销成功了
ugame.Instance.user_login_out();
// end
event_manager.Instance.dispatch_event("login_out", null);
}
服务器
注销逻辑文件
local mysql_center = require("database/mysql_auth_center")
local redis_center = require("database/redis_center")
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
function do_login_out(s, req)
local uid = req[3];
Logger.debug("user ".. uid .. " login out!")
--
-- end
local msg = {Stype.Auth, Cmd.eLoginOutRes, uid, {
status = Respones.OK,
}}
Session.send_msg(s, msg)
end
local login_out = {
do_login_out = do_login_out
}
return login_out
更改网关服务
function send_to_client(server_session, raw_cmd)
local stype, ctype, utag = RawCmd.read_header(raw_cmd)
local client_session = nil
if is_login_return_cmd(ctype) then
client_session = client_sessions_ukey[utag]
client_sessions_ukey[utag] = nil
if client_session == nil then
return
end
local body = RawCmd.read_body(raw_cmd)
if body.status ~= Respones.OK then
RawCmd.set_utag(raw_cmd, 0)
Session.send_raw_cmd(client_session, raw_cmd)
return
end
local uid = body.uinfo.uid
if client_sessions_uid[uid] and client_sessions_uid[uid] ~= client_session then
local relogin_cmd = {Stype.Auth, Cmd.eRelogin, 0, nil}
Session.send_msg(client_sessions_uid[uid], relogin_cmd)
Session.close(client_sessions_uid[uid])
end
client_sessions_uid[uid] = client_session
Session.set_uid(client_session, uid)
body.uinfo.uid = 0;
local login_res = {stype, ctype, 0, body}
Session.send_msg(client_session, login_res)
return
end
client_session = client_sessions_uid[utag]
if client_session then
RawCmd.set_utag(raw_cmd, 0)
Session.send_raw_cmd(client_session, raw_cmd)
if ctype == Cmd.eLoginOutRes then -- 这里处理注销,将对应的session和uid去掉即可
Session.set_uid(client_session, 0)
client_sessions_uid[utag] = nil
end
end
end
系统服务器
mysql数据库设计
1: 用户游戏数据库moba_game表ugame设计(根据具体得需求来设计)
id: 某游戏中玩家数据唯一id号;
uid: 这个数据所对应得玩家;
uchip: 玩家在游戏中的金币或其他
uchip2: 玩家在游戏中的金币或其他
uchip3: 玩家在游戏中的金币或其他;
uvip: 每个游戏有每个游戏自己的付费等级VIP,和用户信息的等级是不一样的;
uvip_endtime: 游戏付费等级vip到期的时间戳;
uexp: 玩家的经验;
udata1/updata2/udata3/udata4, 玩家在游戏中的道具等数目;
ustatus: 玩家在此游戏中的状态,是否封住这个游戏中的账号权限, 0,表示正常;
2: 将游戏数据库的表接口导出到.sql文件里面,并放到sql文件下;
数据库基础逻辑代码
local game_config = require("game_config")
local moba_game_config = require("moba_game_config")
local mysql_conn = nil
local function is_connected()
if not mysql_conn then
return false
end
return true
end
--连接服务器
function mysql_connect_to_moba_game()
local conf = game_config.game_mysql
Mysql.connect(conf.host, conf.port,
conf.db_name, conf.uname,
conf.upwd, function(err, conn)
if err then
Logger.error(err)
Scheduler.once(mysql_connect_to_moba_game, 5000)
return
end
Logger.debug("connect to moba game db success!!!!")
mysql_conn = conn
end)
end
mysql_connect_to_moba_game()
--查询用户信息
function get_ugame_info(uid, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "select uchip, uchip2, uchip3, uvip, uvip_endtime, udata1, udata2, udata3, uexp, ustatus from ugame where uid = %d limit 1"
local sql_cmd = string.format(sql, uid)
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
-- 没有这条记录
if ret == nil or #ret <= 0 then
if ret_handler ~= nil then
ret_handler(nil, nil)
end
return
end
-- end
local result = ret[1]
local uinfo = {}
uinfo.uchip = tonumber(result[1])
uinfo.uchip2 = tonumber(result[2])
uinfo.uchip3 = tonumber(result[3])
uinfo.uvip = tonumber(result[4])
uinfo.uvip_endtime = tonumber(result[5])
uinfo.udata1 = tonumber(result[6])
uinfo.udata2 = tonumber(result[7])
uinfo.udata3 = tonumber(result[8])
uinfo.uexp = tonumber(result[9])
uinfo.ustatus = tonumber(result[10])
ret_handler(nil, uinfo)
end)
end
--新增用户数据信息
function insert_ugame_info(uid, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "insert into ugame(`uid`, `uchip`, `uvip`, `uexp`)values(%d, %d, %d, %d)"
local sql_cmd = string.format(sql, uid,
moba_game_config.ugame.uchip,
moba_game_config.ugame.uvip,
moba_game_config.ugame.uexp)
Mysql.query(mysql_conn, sql_cmd, function (err, ret)
if err then
ret_handler(err, nil)
return
else
ret_handler(nil, nil)
end
end)
end
local mysql_game = {
get_ugame_info = get_ugame_info,
insert_ugame_info = insert_ugame_info,
is_connected = is_connected,
}
return mysql_game
游戏redis数据库逻辑
local game_config = require("game_config")
local redis_conn = nil
function redis_connect_to_game()
local host = game_config.game_redis.host
local port = game_config.game_redis.port
local db_index = game_config.game_redis.db_index
Redis.connect(host, port, function (err, conn)
if err ~= nil then
Logger.error(err)
Scheduler.once(redis_connect_to_game, 5000)
return
end
redis_conn = conn
Logger.debug("connect to redis game db success!!!!")
Redis.query(redis_conn, "select " .. db_index, function (err, ret)
end)
end)
end
redis_connect_to_game()
function set_ugame_info_inredis(uid, ugame_info)
if redis_conn == nil then
Logger.error("redis center disconnected")
return
end
local redis_cmd = "hmset moba_ugame_user_uid_" .. uid ..
" uchip " .. ugame_info.uchip ..
" uexp " .. ugame_info.uexp ..
" uvip " .. ugame_info.uvip
Redis.query(redis_conn, redis_cmd, function (err, ret)
if err then
return
end
end)
end
-- ret_handler(err, uinfo)
function get_ugame_info_inredis(uid, ret_handler)
local redis_cmd = "hgetall moba_ugame_user_uid_" .. uid
Redis.query(redis_conn, redis_cmd, function (err, ret)
if err then
if ret_handler then
ret_handler(err, nil)
end
return
end
local ugame_info = {}
ugame_info.uid = uid
ugame_info.uchip = tonumber(ret[2])
ugame_info.uexp = tonumber(ret[4])
ugame_info.uvip = tonumber(ret[6])
ret_handler(nil, ugame_info)
end)
end
function add_chip_inredis(uid, add_chip)
get_ugame_info_inredis(uid, function (err, ugame_info)
if err then
Logger.error("get ugame_info inredis error")
return
end
ugame_info.uchip = ugame_info.uchip + add_chip
set_ugame_info_inredis(uid, ugame_info)
end)
end
local redis_game = {
set_ugame_info_inredis = set_ugame_info_inredis,
get_ugame_info_inredis = get_ugame_info_inredis,
add_chip_inredis = add_chip_inredis,
}
return redis_game
基础代码
main.lua
--初始化日志模块
Logger.init("logger/system_server/", "system", true)
local proto_type = {
PROTO_JSON = 0,
PROTO_BUF = 1,
}
ProtoMan.init(proto_type.PROTO_BUF)
-- 如果是protobuf协议,还要注册一下映射表
if ProtoMan.proto_type() == proto_type.PROTO_BUF then
local cmd_name_map = require("cmd_name_map")
if cmd_name_map then
ProtoMan.register_protobuf_cmd_map(cmd_name_map)
end
end
--end
local game_config = require("game_config")
local servers = game_config.servers
local Stype = require("Stype")
-- 开启网关端口监听
Netbus.tcp_listen(servers[Stype.System].port)
print("System Server Start at ".. servers[Stype.System].port)
--Netbus.udp_listen(8002)
--end
local system_service = require("system_server/system_service")
local ret = Service.register(Stype.System, system_service)
if ret then
print("register System service:[" .. Stype.System.. "] success!!!")
else
print("register System service:[" .. Stype.System.. "] failed!!!")
end
system_service.lua
local Stype = require("Stype")
local Cmd = require("Cmd")
local system_service_handlers = {}
-- {stype, ctype, utag, body}
function on_system_recv_cmd(s, msg)
print(msg[1], msg[2], msg[3])
if system_service_handlers[msg[2]] then
system_service_handlers[msg[2]](s, msg)
end
end
function on_system_session_disconnect(s, stype)
end
local system_service = {
on_session_recv_cmd = on_system_recv_cmd,
on_session_disconnect = on_system_session_disconnect,
}
return system_service
初始数据配置文件
local moba_game_config = {
ugame = {
uchip = 2000,
uvip = 0,
uexp = 0,
-- ...
},
}
return moba_game_config
用户游戏数据的注册和获取
协议
enum Cmd {
eGetUgameInfoReq = 13;
eGetUgameInfoRes = 14;
}
message UserGameInfo {
required int32 uchip = 1;
required int32 uexp = 2;
required int32 uvip = 3;
required int32 uchip2 = 4;
required int32 uchip3 = 5;
required int32 udata1 = 6;
required int32 udata2 = 7;
required int32 udata3 = 8;
}
message GetUgameInfoRes {
required int32 status = 1;
optional UserGameInfo uinfo = 2;
}
客户端
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using gprotocol;
public class system_service_proxy : Singletom<system_service_proxy>
{
void on_get_ugame_info_return(cmd_msg msg) {
GetUgameInfoRes res = proto_man.protobuf_deserialize<GetUgameInfoRes>(msg.body);
if (res == null) {
return;
}
if (res.status != Respones.OK) {
Debug.Log("get ugame info status: " + res.status);
return;
}
UserGameInfo uinfo = res.uinfo;
ugame.Instance.save_ugame_info(uinfo);
event_manager.Instance.dispatch_event("get_ugame_info_success", null);
event_manager.Instance.dispatch_event("sync_ugame_info", null);
}
// 获取响应入口
void on_system_server_return(cmd_msg msg)
{
switch (msg.ctype) {
case (int)Cmd.eGetUgameInfoRes:
this.on_get_ugame_info_return(msg);
break;
}
}
public void init() {
network.Instance.add_service_listener((int)Stype.System, this.on_system_server_return);
}
// 发送请求入口
public void load_user_ugame_info() {
network.Instance.send_protobuf_cmd((int)Stype.System, (int)Cmd.eGetUgameInfoReq, null);
}
}
服务端
获取用户数据逻辑文件
ugame.lua
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
local mysql_game = require("database/mysql_game")
-- {stype, ctype, utag, body}
function get_ugame_info(s, req)
local uid = req[3];
mysql_game.get_ugame_info(uid, function (err, uinfo)
if err then -- 告诉客户端系统错误信息;
local msg = {Stype.System, Cmd.eGetUgameInfoRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
if uinfo == nil then -- 没有查到对应的 g_key的信息
mysql_game.insert_ugame_info(uid, function(err, ret)
if err then -- 告诉客户端系统错误信息;
local msg = {Stype.System, Cmd.eGetUgameInfoRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
get_ugame_info(s, req)
end)
return
end
-- 读取到了
-- 找到了我们gkey所对应的游客数据;
if uinfo.ustatus ~= 0 then --账号被查封
local msg = {Stype.System, Cmd.eGetUgameInfoRes, uid, {
status = Respones.UserIsFreeze,
}}
Session.send_msg(s, msg)
return
end
-- end
-- 返回给客户端
local msg = { Stype.System, Cmd.eGetUgameInfoRes, uid, {
status = Respones.OK,
uinfo = {
uchip = uinfo.uchip,
uexp = uinfo.uexp,
uvip = uinfo.uvip,
uchip2 = uinfo.uchip2,
uchip3 = uinfo.uchip3,
udata1 = uinfo.udata1,
udata2 = uinfo.udata2,
udata3 = uinfo.udata3,
}
}}
Session.send_msg(s, msg)
--end
end)
end
local ugame = {
get_ugame_info = get_ugame_info,
}
return ugame
在服务文件中注册
local ugame = require("system_server/ugame")
system_service_handlers[Cmd.eGetUgameInfoReq] = ugame.get_ugame_info
每日登陆奖励
时间戳lua导出
utils_export_to_lua.cc
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "../utils/timestamp.h"
#include "lua_wrapper.h"
#ifdef __cplusplus
extern "C" {
#endif
#include "tolua++.h"
#ifdef __cplusplus
}
#endif
#include "tolua_fix.h"
#include "utils_export_to_lua.h"
#include "../utils/small_alloc.h"
#define my_malloc small_alloc
#define my_free small_free
static int
lua_timestamp(lua_State* tolua_S) {
unsigned long ts = timestamp();
lua_pushinteger(tolua_S, ts);
return 1;
}
static int
lua_timestamp_today(lua_State* tolua_S) {
unsigned long ts = timestamp_today();
lua_pushinteger(tolua_S, ts);
return 1;
}
static int
lua_timestamp_yesterday(lua_State* tolua_S) {
unsigned long ts = timestamp_yesterday();
lua_pushinteger(tolua_S, ts);
return 1;
}
int
register_utils_export(lua_State* tolua_S) {
lua_getglobal(tolua_S, "_G");
if (lua_istable(tolua_S, -1)) {
tolua_open(tolua_S);
tolua_module(tolua_S, "Utils", 0);
tolua_beginmodule(tolua_S, "Utils");
tolua_function(tolua_S, "timestamp", lua_timestamp);
tolua_function(tolua_S, "timestamp_today", lua_timestamp_today);
tolua_function(tolua_S, "timestamp_yesterday", lua_timestamp_yesterday);
tolua_endmodule(tolua_S);
}
lua_pop(tolua_S, 1);
return 0;
}
登陆奖励数据表的设计
1: 每次登陆的时候,继续下登陆的时间,根据时间来判断是否给奖励
2: 数据表的字段设计如下: login_bonues
字段 id uid, bonues, status,bunues_time, days
3: id: 作为数据记录的唯一的主key;
uid: 每个用户登录的信息,每个用户有且只有一条记录;
4: bonues: 今天用户登录可获取的奖励数目,过后将作废不累积;
5: status: 今天的奖励是否领取0,为可以领取, 1为已经领取;
6: bunues_time: 上一次登陆的时间;
7: days: 连续登录的天数,如果大于最大的天数,就按照最大的天数来算,也可以重新计算,根据需求来做
1: 判断是否发放登陆奖励: 上一次发放奖励的时间是否小于今天 00:00:000的时间戳;
2: 判断是否是连续登录, 上一次发放奖励的时间是否大于等于昨天00:00:00的时间戳;
3: 每日登陆奖励配置: 连续登录5天后,重新开始计算登陆的天数,或都奖励最多连续天数的奖励,更具需求而定;
4: 加入登陆奖励的配置文件;
协议
enum Cmd {
eRecvLoginBonuesReq = 15;
eRecvLoginBonuesRes = 16;
}
message UserGameInfo {
required int32 uchip = 1;
required int32 uexp = 2;
required int32 uvip = 3;
required int32 uchip2 = 4;
required int32 uchip3 = 5;
required int32 udata1 = 6;
required int32 udata2 = 7;
required int32 udata3 = 8;
required int32 bonues_status = 9;
required int32 bonues = 10;
required int32 days = 11;
}
message RecvLoginBonuesRes {
required int32 status = 1;
}
客户端
//发送请求入口
public void recv_login_bonues() {
network.Instance.send_protobuf_cmd((int)Stype.System, (int)Cmd.eRecvLoginBonuesReq, null);
}
// 接收奖励入口
void on_recv_login_bonues_return(cmd_msg msg) {
RecvLoginBonuesRes res = proto_man.protobuf_deserialize<RecvLoginBonuesRes>(msg.body);
if (res == null) {
return;
}
if (res.status != Respones.OK) {
Debug.Log("recv login bonues status: " + res.status);
return;
}
ugame.Instance.ugame_info.uchip += ugame.Instance.ugame_info.bonues;
ugame.Instance.ugame_info.bonues_status = 1;
event_manager.Instance.dispatch_event("sync_ugame_info", null);
}
服务器
数据库逻辑代码
mysql_game.lua
local game_config = require("game_config")
local moba_game_config = require("moba_game_config")
local mysql_conn = nil
function mysql_connect_to_moba_game()
local conf = game_config.game_mysql
Mysql.connect(conf.host, conf.port,
conf.db_name, conf.uname,
conf.upwd, function(err, conn)
if err then
Logger.error(err)
Scheduler.once(mysql_connect_to_moba_game, 5000)
return
end
Logger.debug("connect to moba game db success!!!!")
mysql_conn = conn
end)
end
mysql_connect_to_moba_game()
function get_ugame_info(uid, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "select uchip, uchip2, uchip3, uvip, uvip_endtime, udata1, udata2, udata3, uexp, ustatus from ugame where uid = %d limit 1"
local sql_cmd = string.format(sql, uid)
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
-- 没有这条记录
if ret == nil or #ret <= 0 then
if ret_handler ~= nil then
ret_handler(nil, nil)
end
return
end
-- end
local result = ret[1]
local uinfo = {}
uinfo.uchip = tonumber(result[1])
uinfo.uchip2 = tonumber(result[2])
uinfo.uchip3 = tonumber(result[3])
uinfo.uvip = tonumber(result[4])
uinfo.uvip_endtime = tonumber(result[5])
uinfo.udata1 = tonumber(result[6])
uinfo.udata2 = tonumber(result[7])
uinfo.udata3 = tonumber(result[8])
uinfo.uexp = tonumber(result[9])
uinfo.ustatus = tonumber(result[10])
ret_handler(nil, uinfo)
end)
end
function insert_ugame_info(uid, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "insert into ugame(`uid`, `uchip`, `uvip`, `uexp`)values(%d, %d, %d, %d)"
local sql_cmd = string.format(sql, uid,
moba_game_config.ugame.uchip,
moba_game_config.ugame.uvip,
moba_game_config.ugame.uexp)
Mysql.query(mysql_conn, sql_cmd, function (err, ret)
if err then
ret_handler(err, nil)
return
else
ret_handler(nil, nil)
end
end)
end
function get_bonues_info(uid, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "select bonues, status, bonues_time, days from login_bonues where uid = %d limit 1"
local sql_cmd = string.format(sql, uid)
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
-- 没有这条记录
if ret == nil or #ret <= 0 then
if ret_handler ~= nil then
ret_handler(nil, nil)
end
return
end
-- end
local result = ret[1]
local bonues_info = {}
bonues_info.bonues = tonumber(result[1])
bonues_info.status = tonumber(result[2])
bonues_info.bonues_time = tonumber(result[3])
bonues_info.days = tonumber(result[4])
ret_handler(nil, bonues_info)
end)
end
function insert_bonues_info(uid, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "insert into login_bonues(`uid`, `bonues_time`, `status`)values(%d, %d, 1)"
local sql_cmd = string.format(sql, uid, Utils.timestamp())
Mysql.query(mysql_conn, sql_cmd, function (err, ret)
if err then
ret_handler(err, nil)
return
else
ret_handler(nil, nil)
end
end)
end
function update_login_bonues(uid, bonues_info, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "update login_bonues set status = 0, bonues = %d, bonues_time = %d, days = %d where uid = %d"
local sql_cmd = string.format(sql, bonues_info.bonues, bonues_info.bonues_time, bonues_info.days, uid)
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
if ret_handler then
ret_handler(nil, nil)
end
end)
end
function update_login_bonues_status(uid, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "update login_bonues set status = 1 where uid = %d"
local sql_cmd = string.format(sql, uid)
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
if ret_handler then
ret_handler(nil, nil)
end
end)
end
function add_chip(uid, chip, ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "update ugame set uchip = uchip + %d where uid = %d"
local sql_cmd = string.format(sql, chip, uid)
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
if ret_handler then
ret_handler(nil, nil)
end
end)
end
local mysql_game = {
get_ugame_info = get_ugame_info,
insert_ugame_info = insert_ugame_info,
get_bonues_info = get_bonues_info,
insert_bonues_info = insert_bonues_info,
update_login_bonues = update_login_bonues,
update_login_bonues_status = update_login_bonues_status,
add_chip = add_chip,
}
return mysql_game
登陆奖励逻辑文件
login_bonues.lua
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
local mysql_game = require("database/mysql_game")
local moba_game_config = require("moba_game_config")
local redis_game = require("database/redis_game")
function send_bonues_to_user(uid, bonues_info, ret_handler)
-- 要更新发放奖励;
if bonues_info.bonues_time < Utils.timestamp_today() then
if bonues_info.bonues_time >= Utils.timestamp_yesterday() then -- 连续登陆
bonues_info.days = bonues_info.days + 1
else -- 重新开始计算
bonues_info.days = 1
end
if bonues_info.days > #moba_game_config.login_bonues then
bonues_info.days = 1
end
bonues_info.status = 0
bonues_info.bonues_time = Utils.timestamp()
bonues_info.bonues = moba_game_config.login_bonues[bonues_info.days]
mysql_game.update_login_bonues(uid, bonues_info, function (err, ret)
if err then
ret_handler(err, nil)
return
end
ret_handler(nil, bonues_info)
end)
return
end
-- 把登陆奖励信息会给ugame
ret_handler(nil, bonues_info)
end
-- ret_handler(err, bonues_info),检查是否第一次登录,提供登录奖励信息给客户端显示
function check_login_bonues(uid, ret_handler)
mysql_game.get_bonues_info(uid, function (err, bonues_info)
if err then
ret_handler(err, nil)
return
end
-- 这个用户还是第一次来登陆,
if bonues_info == nil then
mysql_game.insert_bonues_info(uid, function (err, ret)
if err then
ret_handler(err, nil)
return
end
check_login_bonues(uid, ret_handler)
end)
return
end
send_bonues_to_user(uid, bonues_info, ret_handler)
end)
end
-- 每日登录奖励的响应
function recv_login_bonues(s, req)
local uid = req[3];
mysql_game.get_bonues_info(uid, function (err, bonues_info)
if err then
local msg = {Stype.System, Cmd.eRecvLoginBonuesRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
if bonues_info == nil or bonues_info.status ~= 0 then
local msg = {Stype.System, Cmd.eRecvLoginBonuesRes, uid, {
status = Respones.InvalidOpt,
}}
Session.send_msg(s, msg)
return
end
-- 有奖励可以领取
mysql_game.update_login_bonues_status(uid, function (err, ret)
if err then
local msg = {Stype.System, Cmd.eRecvLoginBonuesRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
-- 跟新数据的uchip
mysql_game.add_chip(uid, bonues_info.bonues, nil)
redis_game.add_chip_inredis(uid, bonues_info.bonues);
local msg = {Stype.System, Cmd.eRecvLoginBonuesRes, uid, {
status = Respones.OK,
}}
Session.send_msg(s, msg)
end)
end)
end
login_bonues = {
check_login_bonues = check_login_bonues,
recv_login_bonues = recv_login_bonues,
}
return login_bonues
获取用户数据逻辑文件
ugame.lua
在上一小节用户游戏数据上加了三条,并在此之前检查登录奖励
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
local mysql_game = require("database/mysql_game")
local login_bonues = require("system_server/login_bonues")
local redis_game = require("database/redis_game")
-- {stype, ctype, utag, body}
function get_ugame_info(s, req)
local uid = req[3];
mysql_game.get_ugame_info(uid, function (err, uinfo)
if err then -- 告诉客户端系统错误信息;
local msg = {Stype.System, Cmd.eGetUgameInfoRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
if uinfo == nil then -- 没有游戏信息
mysql_game.insert_ugame_info(uid, function(err, ret)
if err then -- 告诉客户端系统错误信息;
local msg = {Stype.System, Cmd.eGetUgameInfoRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
get_ugame_info(s, req)
end)
return
end
-- 读取到了
-- 找到了我们gkey所对应的游客数据;
if uinfo.ustatus ~= 0 then --账号被查封
local msg = {Stype.System, Cmd.eGetUgameInfoRes, uid, {
status = Respones.UserIsFreeze,
}}
Session.send_msg(s, msg)
return
end
-- end
redis_game.set_ugame_info_inredis(uid, uinfo)
-- 检查登陆奖励
login_bonues.check_login_bonues(uid, function (err, bonues_info)
if err then -- 告诉客户端系统错误信息;
local msg = {Stype.System, Cmd.eGetUgameInfoRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
-- 返回给客户端
local msg = { Stype.System, Cmd.eGetUgameInfoRes, uid, {
status = Respones.OK,
uinfo = {
uchip = uinfo.uchip,
uexp = uinfo.uexp,
uvip = uinfo.uvip,
uchip2 = uinfo.uchip2,
uchip3 = uinfo.uchip3,
udata1 = uinfo.udata1,
udata2 = uinfo.udata2,
udata3 = uinfo.udata3,
--这里只支持金币的奖励,如果想要其他道具作为登录奖励,可以在加上道具的id
--作为第四个字段
bonues_status = bonues_info.status,
bonues = bonues_info.bonues,
days = bonues_info.days,
}
}}
Session.send_msg(s, msg)
end)
--end
end)
end
游戏排行榜
主要利用redis的有序集合来进行排序
1: 使用redis获取排行的uid;
2: 从中心数据库里面获取用户信息;
3: 从游戏数据库里面获取游戏信息;
4: 组成协议发送给客户端;
5: 客户端获取排行榜的数据;
排行榜redis服务器
local game_config = require("game_config")
local redis_conn = nil
function redis_connect_to_rank()
local host = game_config.rank_redis.host
local port = game_config.rank_redis.port
local db_index = game_config.rank_redis.db_index
Redis.connect(host, port, function (err, conn)
if err ~= nil then
Logger.error(err)
Scheduler.once(redis_connect_to_rank, 5000)
return
end
redis_conn = conn
Logger.debug("connect to redis rank db success!!!!")
Redis.query(redis_conn, "select " .. db_index, function (err, ret)
end)
end)
end
redis_connect_to_rank()
-- redis 有序集合, key WOLD_CHIP_RANK 专门用来做世界排行的有序集合
-- 做好友排行,每个人都有它好友的一个 FRIREND_CHIP_RANK_UID有序集合;
local WOLD_CHIP_RANK = "WOLD_CHIP_RANK"
--刷新世界排行榜
function flush_world_rank_with_uchip_inredis(uid, uchip)
if redis_conn == nil then
Logger.error("redis rank disconnected")
return
end
local redis_cmd = "zadd WOLD_CHIP_RANK " .. uchip .. " " .. uid
Redis.query(redis_conn, redis_cmd, function (err, ret)
if err then
return
end
end)
end
-- n:要刷的排行榜的数目,获取前几名
-- ret_handler: 回掉函数
function get_world_rank_with_uchip_inredis(n, ret_handler)
if redis_conn == nil then
Logger.error("redis rank disconnected")
return
end
-- zrange 是由小到大的排列;
-- zrevrange 由大到小
-- local redis_cmd = "zrange WOLD_CHIP_RANK 0 " .. n
local redis_cmd = "zrevrange WOLD_CHIP_RANK 0 " .. n
Redis.query(redis_conn, redis_cmd, function (err, ret)
if err then
if ret_handler then
ret_handler("zrange WOLD_CHIP_RANK inredis error", nil)
end
return
end
-- 排行榜没有任何数据
if ret == nil or #ret <= 0 then
ret_handler(nil, nil)
return
end
-- 获取得到了排行班的数据
local rank_info = {}
local k, v
for k, v in pairs(ret) do
rank_info[k] = tonumber(v)
end
-- end
if ret_handler then
ret_handler(nil, rank_info)
end
end)
end
local redis_rank = {
flush_world_rank_with_uchip_inredis = flush_world_rank_with_uchip_inredis,
get_world_rank_with_uchip_inredis = get_world_rank_with_uchip_inredis,
}
return redis_rank
协议
enum Cmd {
eGetWorldRankUchipReq = 17;
eGetWorldRankUchipRes = 18;
}
message WorldChipRankInfo {
required string unick = 1;
required int32 uface = 2;
required int32 usex = 3;
required int32 uvip = 4;
required int32 uchip = 5;
}
message GetWorldRankUchipRes {
required int32 status = 1;
repeated WorldChipRankInfo rank_info = 2;
}
客户端
// 响应
void on_get_world_uchip_rank_info_return(cmd_msg msg) {
GetWorldRankUchipRes res = proto_man.protobuf_deserialize<GetWorldRankUchipRes>(msg.body);
if (res == null) {
return;
}
if (res.status != Respones.OK) {
Debug.Log("get_world_uchip_rank_info status: " + res.status);
return;
}
event_manager.Instance.dispatch_event("get_rank_list", res.rank_info);
}
// 请求
public void get_world_uchip_rank_info() {
network.Instance.send_protobuf_cmd((int)Stype.System, (int)Cmd.eGetWorldRankUchipReq, null);
}
服务器
拉取排行逻辑
game_rank.lua
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
local mysql_game = require("database/mysql_game")
local login_bonues = require("system_server/login_bonues")
local redis_game = require("database/redis_game")
local redis_rank = require("database/redis_rank")
local redis_center = require("database/redis_center")
function get_rank_user_center_info(index, rank_uid, success_func, failed_func)
redis_center.get_uinfo_inredis(rank_uid, function (err, uinfo)
if err or uinfo == nil then
if failed_func then
failed_func()
end
return
end
success_func(index, uinfo)
end)
end
--获取单个玩家的游戏数据
function get_rank_user_ugame_info(index, rank_uid, success_func, failed_func)
redis_game.get_ugame_info_inredis(rank_uid, function (err, ugame_info)
if err or ugame_info == nil then
if failed_func then
failed_func()
end
return
end
success_func(index, ugame_info)
end)
end
function send_rank_info_to_client(s, uid, rank_uids, rank_user_info, rank_ugame_info)
local rank_info_body = {}
local i
for i = 1, #rank_uids do
local user_rank_info = {
--用户数据
unick = rank_user_info[i].unick,
uface = rank_user_info[i].uface,
usex = rank_user_info[i].usex,
--游戏数据
uvip = rank_ugame_info[i].uvip,
uchip = rank_ugame_info[i].uchip,
}
rank_info_body[i] = user_rank_info
end
local msg = {Stype.System, Cmd.eGetWorldRankUchipRes, uid, {
status = Respones.OK,
rank_info = rank_info_body,
}}
Session.send_msg(s, msg)
end
-- {stype, ctype, utag, body}
function get_rank_ugame_info(s, uid, rank_uids, rank_uinfo)
local rank_ugame_info = {}
local failed_func = function ()
local msg = {Stype.System, Cmd.eGetWorldRankUchipRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
local success_func = function (index, ugame_info)
rank_ugame_info[index] = ugame_info
if index == #rank_uids then -- 4.获取了游戏数据,排行榜的信息全部都收集完了,我们发给客户端
send_rank_info_to_client(s, uid, rank_uids, rank_uinfo, rank_ugame_info)
else
index = index + 1
get_rank_user_ugame_info(index, rank_uids[index], success_func, failed_func)
end
end
--查询单个玩家的游戏数据
get_rank_user_ugame_info(1, rank_uids[1], success_func, failed_func)
end
-- {stype, ctype, utag, body},入口
function get_world_uchip_rank(s, req)
local uid = req[3]
-- 1. 拉取排行榜的前30个uid
redis_rank.get_world_rank_with_uchip_inredis(30, function(err, rank_uids)
if err or rank_uids == nil then
local msg = {Stype.System, Cmd.eGetWorldRankUchipRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
--排行榜没有数据
if #rank_uids <= 0 then
local msg = {Stype.System, Cmd.eGetWorldRankUchipRes, uid, {
status = Respones.OK,
}}
Session.send_msg(s, msg)
return
end
-- 获得了我们的排在前面的uid的集合{first_uid, second_uid, ...}
local rank_user_info = {}
local failed_func = function()
local msg = {Stype.System, Cmd.eGetWorldRankUchipRes, uid, {
status = Respones.SystemErr,
}}
Session.send_msg(s, msg)
return
end
local success_func = function(index, user_info)
rank_user_info[index] = user_info
if index == #rank_uids then -- 3.30个用户数据全部获取了,获取用户的游戏数据
get_rank_ugame_info(s, uid, rank_uids, rank_user_info)
else
index = index + 1
get_rank_user_center_info(index, rank_uids[index], success_func, failed_func)
end
end
--2. 根据30个uid拉取30个用户数据
get_rank_user_center_info(1, rank_uids[1], success_func, failed_func)
end)
end
local game_rank = {
get_world_uchip_rank = get_world_uchip_rank,
}
return game_rank
注册:
system_service.lua
local login_bonues = require("system_server/login_bonues");
local game_rank = require("system_server/game_rank");
local system_service_handlers = {}
system_service_handlers[Cmd.eRecvLoginBonuesReq] = login_bonues.recv_login_bonues
system_service_handlers[Cmd.eGetWorldRankUchipReq] = game_rank.get_world_uchip_rank
系统消息(邮件)
注意:这里用了数据库存储系统消息,一般的做法是策划用配置表
1: 服务器加载系统消息到内存,然后方便我们的用户请求;
2: 每天晚上固定一个时间点来更新我们的系统消息;
3: 如果有管理工具的前提下,管理工具去通知服务器更新;
1: 在main.lua里面就去 require 数据库代码,然后链接数据库;
2: 新建sys_msg.lua模块,启动定时器,去读取这个系统信息,如果读取了,就把定时器设置到明天的0点,如果没有读取,5秒以后再去更新;
3: 每次加载的时候生成一个版本号, 版本号可以根据服务器的时间戳来定;
4: 制定拉取系统消息的req/res 命令协议, 拉取缓存到本地, 根据版本号,是否刷新数据;
5: 协议
message GetSysMsgReq {
required int32 ver_num = 1;
}
message GetSysMsgRes {
required int32 status = 1;
required int32 ver_num = 2;
repeated string sys_msgs = 3;
}
6:数据库乱码: mysql_set_character_set(pConn, “utf8”);
协议
enum Cmd {
eGetSysMsgReq = 19;
eGetSysMsgRes = 20;
}
message GetSysMsgReq {
required int32 ver_num = 1;
}
message GetSysMsgRes {
required int32 status = 1;
required int32 ver_num = 2;
repeated string sys_msgs = 3;
}
客户端
// 请求
public void get_sys_msg() {
GetSysMsgReq req = new GetSysMsgReq();
req.ver_num = this.ver_num;
network.Instance.send_protobuf_cmd((int)Stype.System, (int)Cmd.eGetSysMsgReq, req);
}
// 响应
void on_get_sys_msg_return(cmd_msg msg) {
GetSysMsgRes res = proto_man.protobuf_deserialize<GetSysMsgRes>(msg.body);
if (res == null) {
return;
}
if (res.status != Respones.OK) {
Debug.Log("get system msg status: " + res.status);
return;
}
Debug.Log("get system msg success");
if (this.ver_num == res.ver_num) { // 本地和服务器的一样,使用本地的数据;
Debug.Log("the use local data");
}
else {
this.ver_num = res.ver_num;
this.sys_msgs = res.sys_msgs;
Debug.Log("sync server data");
}
if (this.sys_msgs != null) {
for (int i = 0; i < this.sys_msgs.Count; i++) {
Debug.Log(this.sys_msgs[i]);
}
event_manager.Instance.dispatch_event("get_sys_email", this.sys_msgs);
}
}
服务器
逻辑文件
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
local mysql_game = require("database/mysql_game")
local sys_msg_data = {} -- 如果你加载进来,就会存放到这个表里面
local sys_msg_version = 0 -- 什么时候时候加载的,搞一个时间戳,可以作为我们版本号;
function load_sys_msg()
--从数据库中获取消息列表
mysql_game.get_sys_msg(function (err, ret)
if err then
Scheduler.once(load_sys_msg, 5000)
return
end
sys_msg_version = Utils.timestamp()
--
if ret == nil or #ret <= 0 then
sys_msg_data = {}
return
end
--
sys_msg_data = ret
-- 过了晚上1点,在更新一下
local tormorow = Utils.timestamp_today() + 25 * 60 * 60
Scheduler.once(load_sys_msg, (tormorow - sys_msg_version) * 1000)
--[[
local k, v
for k,v in pairs(sys_msg_data) do
print(v)
end
]]
end)
end
Scheduler.once(load_sys_msg, 5000)
-- {stype, ctype, utag, body}
function get_sys_msg(s, req)
local uid = req[3]
local body = req[4]
if (body.ver_num == sys_msg_version) then
local msg = {Stype.System, Cmd.eGetSysMsgRes, uid, {
status = Respones.OK,
ver_num = sys_msg_version,
}}
Session.send_msg(s, msg)
return
end
local msg = {Stype.System, Cmd.eGetSysMsgRes, uid, {
status = Respones.OK,
ver_num = sys_msg_version,
sys_msgs = sys_msg_data,
}}
Session.send_msg(s, msg)
end
local sys_msg = {
get_sys_msg = get_sys_msg,
}
return sys_msg
注册:
local sys_msg = require("system_server/sys_msg")
system_service_handlers[Cmd.eGetSysMsgReq] = sys_msg.get_sys_msg
战斗逻辑服务器
配置文件
目前game_config.lua如下
local Stype = require("Stype")
local remote_servers = {}
-- 注册我们的服务所部署的IP地址和端口
remote_servers[Stype.Auth] = {
stype = Stype.Auth,
ip = "127.0.0.1",
port = 8000,
desic = "Auth server",
}
remote_servers[Stype.System] = {
stype = Stype.System,
ip = "127.0.0.1",
port = 8001,
desic = "System server",
}
remote_servers[Stype.Logic] = {
stype = Stype.Logic,
ip = "127.0.0.1",
port = 8002,
desic = "Logic Server"
}
local game_config = {
gateway_tcp_ip = "127.0.0.1",
gateway_tcp_port = 6080,
gateway_ws_ip = "127.0.0.1",
gateway_ws_port = 6081,
servers = remote_servers,
auth_mysql = {
host = "127.0.0.1", -- 数据库所在的host
port = 3306, -- 数据库所在的端口
db_name = "auth_center", -- 数据库的名字
uname = "root", -- 登陆数据库的账号
upwd = "123456", -- 登陆数据库的密码
},
center_redis = {
host = "127.0.0.1", -- redis所在的host
port = 6379, -- reidis 端口
db_index = 1, -- 数据1
},
game_mysql = {
host = "127.0.0.1", -- 数据库所在的host
port = 3306, -- 数据库所在的端口
db_name = "moba_game", -- 数据库的名字
uname = "root", -- 登陆数据库的账号
upwd = "123456", -- 登陆数据库的密码
},
game_redis = {
host = "127.0.0.1", -- redis所在的host
port = 6379, -- reidis 端口
db_index = 2, -- 数据库2
},
-- 做排行榜的redis服务器
rank_redis = {
host = "127.0.0.1", -- redis所在的host
port = 6379, -- reidis 端口
db_index = 3, -- 数据库3
},
}
return game_config
基础代码
main.lua
--初始化日志模块
Logger.init("logger/logic_server/", "logic", true)
--end
-- 连接到我们的mysql game 数据库
require("database/mysql_game")
--end
-- 初始化协议模块
local proto_type = {
PROTO_JSON = 0,
PROTO_BUF = 1,
}
ProtoMan.init(proto_type.PROTO_BUF)
-- 如果是protobuf协议,还要注册一下映射表
if ProtoMan.proto_type() == proto_type.PROTO_BUF then
local cmd_name_map = require("cmd_name_map")
if cmd_name_map then
ProtoMan.register_protobuf_cmd_map(cmd_name_map)
end
end
--end
local game_config = require("game_config")
local servers = game_config.servers
local Stype = require("Stype")
-- 开启网关端口监听
Netbus.tcp_listen(servers[Stype.Logic].port)
print("Logic Server Start at ".. servers[Stype.Logic].port)
--Netbus.udp_listen(8002)
--end
local logic_service = require("logic_server/logic_service")
local ret = Service.register(Stype.Logic, logic_service)
if ret then
print("register Logic service:[" .. Stype.Logic.. "] success!!!")
else
print("register Logic service:[" .. Stype.Logic.. "] failed!!!")
end
logic_server.lua
local Stype = require("Stype")
local Cmd = require("Cmd")
local logic_service_handlers = {}
-- {stype, ctype, utag, body}
function on_logic_recv_cmd(s, msg)
if logic_service_handlers[msg[2]] then
logic_service_handlers[msg[2]](s, msg)
end
end
function on_logic_session_disconnect(s, stype)
end
local logic_service = {
on_session_recv_cmd = on_logic_recv_cmd,
on_session_disconnect = on_logic_session_disconnect,
}
return logic_service
逻辑服务器登录和断线
1: 制定登陆逻辑服务器请求/返回协议
LoginLogicReq: 不需要任何数据,直接发送服务号命令号;
message LoginLogicRes { 返回状态 };
2: 新建一个game_mgr.lua文件,来定义接口 logic_login;
2: 服务器创建一个 Player的玩家类, 每登陆一次,先去Logic服务器里面查找,是否有玩家,
如果存在,就直接登陆成功,否者就读取玩家的游戏数据到Player里面,保存到
uid- player的对象表里面;
断线: 网关负责把用户短线的消息,转发给对应的服务器,逻辑服务器收到这个事件后,将玩家移除出逻辑服务器管理表,如果是游戏中的玩家,后续做断线重连的操作。
底层Service代码添加用户连接后的响应函数
service.cc,所有服务都继承的基类
class service {
public:
bool using_raw_cmd;
service();
public:
virtual bool on_session_recv_raw_cmd(session* s, struct raw_cmd* raw);
virtual bool on_session_recv_cmd(session* s, struct cmd_msg* msg);
virtual void on_session_disconnect(session* s, int stype);
virtual void on_session_connect(session* s, int stype);
};
service:man.cc,服务管理模块
service_man::on_session_connect(session* s) {
for (int i = 0; i < MAX_SERVICE; i++) {
if (g_service_set[i] == NULL) {
continue;
}
g_service_set[i]->on_session_connect(s, i);
}
}
netbus.c
// libuv连接后的回调函数
static void
uv_connection(uv_stream_t* server, int status) {
uv_session* s = uv_session::create();
uv_tcp_t* client = &s->tcp_handler;
memset(client, 0, sizeof(uv_tcp_t));
uv_tcp_init(uv_default_loop(), client);
client->data = (void*)s;
uv_accept(server, (uv_stream_t*)client);
struct sockaddr_in addr;
int len = sizeof(addr);
uv_tcp_getpeername(client, (sockaddr*)&addr, &len);
uv_ip4_name(&addr, (char*)s->c_address, 64);
s->c_port = ntohs(addr.sin_port);
s->socket_type = (int)(server->data);
uv_read_start((uv_stream_t*)client, uv_alloc_buf, after_read);
service_man::on_session_connect((session*)s);
}
lua导出
class lua_service : public service {
public:
unsigned int lua_recv_cmd_handler;
unsigned int lua_disconnect_handler;
unsigned int lua_recv_raw_handler;
unsigned int lua_connect_handler;
public:
virtual bool on_session_recv_raw_cmd(session* s, struct raw_cmd* raw);
virtual bool on_session_recv_cmd(session* s, struct cmd_msg* msg);
virtual void on_session_disconnect(session* s, int stype);
virtual void on_session_connect(session* s, int stype);
};
static int
lua_register_service(lua_State* tolua_S) {
int stype = (int)tolua_tonumber(tolua_S, 1, 0);
bool ret = false;
// table
if (!lua_istable(tolua_S, 2)) {
goto lua_failed;
}
unsigned int lua_recv_cmd_handler;
unsigned int lua_disconnect_handler;
unsigned int lua_connect_handler;
lua_getfield(tolua_S, 2, "on_session_recv_cmd");
lua_getfield(tolua_S, 2, "on_session_disconnect");
lua_getfield(tolua_S, 2, "on_session_connect");
// stack 3 on_session_recv_cmd , 4on_session_disconnect
lua_recv_cmd_handler = save_service_function(tolua_S, 3, 0);
lua_disconnect_handler = save_service_function(tolua_S, 4, 0);
lua_connect_handler = save_service_function(tolua_S, 5, 0);
if (lua_recv_cmd_handler == 0 || lua_disconnect_handler == 0) {
goto lua_failed;
}
// register service
lua_service* s = new lua_service();
s->using_raw_cmd = false;
s->lua_disconnect_handler = lua_disconnect_handler;
s->lua_recv_cmd_handler = lua_recv_cmd_handler;
s->lua_recv_raw_handler = 0;
s->lua_connect_handler = lua_connect_handler;
ret = service_man::register_service(stype, s);
// end
lua_failed:
lua_pushboolean(tolua_S, ret ? 1 : 0);
return 1;
}
static int
lua_register_raw_service(lua_State* tolua_S) {
int stype = (int)tolua_tonumber(tolua_S, 1, 0);
bool ret = false;
// table
if (!lua_istable(tolua_S, 2)) {
goto lua_failed;
}
unsigned int lua_recv_raw_handler;
unsigned int lua_disconnect_handler;
unsigned int lua_connect_handler;
lua_getfield(tolua_S, 2, "on_session_recv_raw_cmd");
lua_getfield(tolua_S, 2, "on_session_disconnect");
lua_getfield(tolua_S, 2, "on_session_connect");
// stack 3 on_session_recv_cmd , 4on_session_disconnect
lua_recv_raw_handler = save_service_function(tolua_S, 3, 0);
lua_disconnect_handler = save_service_function(tolua_S, 4, 0);
lua_connect_handler = save_service_function(tolua_S, 5, 0);
if (lua_recv_raw_handler == 0 || lua_disconnect_handler == 0) {
goto lua_failed;
}
// register service
lua_service* s = new lua_service();
s->using_raw_cmd = true;
s->lua_disconnect_handler = lua_disconnect_handler;
s->lua_recv_cmd_handler = 0;
s->lua_recv_raw_handler = lua_recv_raw_handler;
s->lua_connect_handler = lua_connect_handler;
ret = service_man::register_service(stype, s);
// end
lua_failed:
lua_pushboolean(tolua_S, ret ? 1 : 0);
return 1;
}
int
register_service_export(lua_State* tolua_S) {
init_service_function_map(tolua_S);
lua_getglobal(tolua_S, "_G");
if (lua_istable(tolua_S, -1)) {
tolua_open(tolua_S);
tolua_module(tolua_S, "Service", 0);
tolua_beginmodule(tolua_S, "Service");
tolua_function(tolua_S, "register", lua_register_service);
tolua_function(tolua_S, "register_with_raw", lua_register_raw_service);
tolua_endmodule(tolua_S);
}
lua_pop(tolua_S, 1);
return 0;
}
协议
enum Cmd {
eLoginLogicReq = 21;
eLoginLogicRes = 22;
}
message LoginLogicRes {
required int32 status = 1;
}
服务器代码
玩家类player.lua
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
local mysql_game = require("database/mysql_game")
local redis_game = require("database/redis_game")
local player = {}
function player:new(instant)
if not instant then
instant = {} --类的实例
end
setmetatable(instant, {__index = self})
return instant
end
function player:init(uid, s, ret_handler)
self.session = s
self.uid = uid
-- 数据库理面读取玩家的基本信息;
mysql_game.get_ugame_info(uid, function (err, ugame_info)
if err then
ret_handler(Respones.SystemErr)
return
end
self.ugame_info = ugame_info
ret_handler(Respones.OK)
end)
-- end
-- ...其他信息后面再读
-- end
end
function player:set_session(s)
self.session = s
end
return player
游戏管理器game_mgr.lua
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
local mysql_game = require("database/mysql_game")
local redis_game = require("database/redis_game")
local player = require("logic_server/player")
function send_status(s, stype, ctype, uid, status)
local msg = {stype, ctype, uid, {
status = status,
}}
Session.send_msg(s, msg)
end
-- uid --> player
local logic_server_players = {}
local online_player_num = 0
-- {stype, ctype, utag, body}
function login_logic_server(s, req)
local uid = req[3]
local p = logic_server_players[uid] -- player对象
if p then -- 玩家对象已经存在了,更新一下session就可以了;
p:set_session(s)
send_status(s, Stype.Logic, Cmd.eLoginLogicRes, uid, Respones.OK)
return
end
p = player:new()
p:init(uid, s, function(status)
if status == Respones.OK then
logic_server_players[uid] = p
online_player_num = online_player_num + 1
end
send_status(s, Stype.Logic, Cmd.eLoginLogicRes, uid, status)
end)
end
-- 玩家离开了,这个函数被在logic_service.lua注册为监听
function on_player_disconnect(s, req)
local uid = req[3]
-- 游戏中的玩家我们后续考虑
-- end
-- 玩家断线离开
if logic_server_players[uid] then
print("player uid " .. uid .. " disconnect!")
logic_server_players[uid] = nil
online_player_num = online_player_num - 1
end
-- end
end
---网关连接了
function on_gateway_connect(s)
local k, v
for k, v in pairs(logic_server_players) do
v:set_session(s)
end
end
--网关掉线了,不能发送信息,session设为空
function on_gateway_disconnect(s)
local k, v
for k, v in pairs(logic_server_players) do
v:set_session(nil)
end
end
local game_mgr = {
login_logic_server = login_logic_server,
on_player_disconnect = on_player_disconnect,
on_gateway_disconnect = on_gateway_disconnect,
on_gateway_connect = on_gateway_connect,
}
return game_mgr
logic_service.lua
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
local game_mgr = require("logic_server/game_mgr")
local logic_service_handlers = {}
logic_service_handlers[Cmd.eLoginLogicReq] = game_mgr.login_logic_server
--这里注册了网关发过来的玩家离开游戏请求的监听
logic_service_handlers[Cmd.eUserLostConn] = game_mgr.on_player_disconnect
-- {stype, ctype, utag, body}
function on_logic_recv_cmd(s, msg)
if logic_service_handlers[msg[2]] then
logic_service_handlers[msg[2]](s, msg)
end
end
function on_gateway_disconnect(s, stype)
print("Logic service disconnect with gateway !!!")
game_mgr.on_gateway_disconnect(s)
end
---网关的连接处理,因为其他服务器是只有网关可以链接的
function on_gateway_connect(s, stype)
-- body
print("gateway connect to Logic !!!")
game_mgr.on_gateway_connect(s)
end
local logic_service = {
on_session_recv_cmd = on_logic_recv_cmd,
on_session_disconnect = on_gateway_disconnect,
on_session_connect = on_gateway_connect
}
return logic_service
玩家匹配
1: 等待列表做多个分区, 玩家加入哪个分区就决定是哪个玩法;
2: 可以一个地图的一个玩法为服务器进程;
3: 其他的地图,可以再加服务号,加服务器进程来;
4: 一个地图的多个玩法也可以分服务器,就根据自己的需求来就可以了;
5: 我们课程一个地图,要给玩法, 所以做一个分区等待列表;
玩家请求匹配做法:
1: 发送开始匹配的请求给服务器;
2: 服务器响应请求,然后将玩家加入到等待列表;
3: 服务器启动定时器,来检查匹配玩家, 找一个空闲的房间(地图)管理对象 来接收这个玩家;当玩家进入房间后,从等待列表离开,服务器通知其他玩家,同时给原来的玩家发送房间信息
房间状态:
local State = {
InView = 1, // 集结玩家, 可以退出
Ready = 2, // 玩家集结完毕,不能退出;
Start = 3, // 玩家都进来了,可以开始游戏了;
Playing = 4, // 游戏中
CheckOut = 5, // 游戏结算
}
地图:
zone = {
SGYD = 1,
ASSY = 2,
}
协议
message EnterZoneReq { // 匹配请求
required int32 zoneid = 1;
}
message EnterZoneRes {// 匹配响应
required int32 status = 1;
}
message EnterMatch { // 服务器回复房间信息,进入了一个房间
required int32 zid = 1;
required int32 matchid = 2;
}
message UserArrived { // 服务器回复的进入房间的玩家信息
required string unick = 1;
required int32 uface = 2;
required int32 usex = 3;
}
客户端
// 请求
public void enter_zone(int zid) {
if (zid < Zone.SGYD || zid > Zone.ASSY) {
return;
}
EnterZoneReq req = new EnterZoneReq();
req.zid = zid;
network.Instance.send_protobuf_cmd((int)Stype.Logic, (int)Cmd.eEnterZoneReq, req);
}
// 响应
void on_enter_zone_return(cmd_msg msg) {
EnterZoneRes res = proto_man.protobuf_deserialize<EnterZoneRes>(msg.body);
if (res == null) {
return;
}
if (res.status != Respones.OK) {
Debug.Log("enter zone status: " + res.status);
return;
}
Debug.Log("enter zone success !!!!!");
}
void on_enter_match_return(cmd_msg msg) {
EnterMatch res = proto_man.protobuf_deserialize<EnterMatch>(msg.body);
if (res == null) {
return;
}
Debug.Log("enter success zid " + res.zid + " matchid" + res.matchid);
}
void on_user_arrived_return(cmd_msg msg) {
UserArrived res = proto_man.protobuf_deserialize<UserArrived>(msg.body);
if (res == null) {
return;
}
Debug.Log(res.unick + " user arrived !!!");
}
服务器
玩家类
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
local mysql_game = require("database/mysql_game")
local redis_game = require("database/redis_game")
local redis_center = require("database/redis_center")
local State = require("logic_server/State")
local Zone = require("logic_server/Zone")
local player = {}
function player:new(instant)
if not instant then
instant = {} --类的实例
end
setmetatable(instant, {__index = self})
return instant
end
function player:init(uid, s, ret_handler)
self.session = s
self.uid = uid
self.zid = -1 -- 玩家所在的空间, -1,不在任何游戏场
self.matchid = -1 -- 玩家所在的比赛房间的id
self.state = State.InView -- 玩家当前处于旁观状态
-- 数据库理面读取玩家的基本信息;
mysql_game.get_ugame_info(uid, function (err, ugame_info)
if err then
ret_handler(Respones.SystemErr)
return
end
self.ugame_info = ugame_info
--从用户数据库获取用户信息
redis_center.get_uinfo_inredis(uid, function (err, uinfo)
if err then
ret_handler(Respones.SystemErr)
return
end
self.uinfo = uinfo
ret_handler(Respones.OK)
end)
end)
-- end
-- ...其他信息后面再读
-- end
end
function player:get_user_arrived()
local body = {
unick = self.uinfo.unick,
uface = self.uinfo.uface,
usex = self.uinfo.usex,
}
return body
end
function player:set_session(s)
self.session = s
end
function player:send_cmd(stype, ctype, body)
if not self.session then
return
end
local msg = {stype, ctype, self.uid, body}
Session.send_msg(self.session, msg)
end
return player
房间类
match_mgr.lua
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
local mysql_game = require("database/mysql_game")
local redis_game = require("database/redis_game")
local State = require("logic_server/State")
local Zone = require("logic_server/Zone")
local player = require("logic_server/player")
local match_mgr = {}
local sg_matchid = 1
local PLAYER_NUM = 3 -- 3v3
function match_mgr:new(instant)
if not instant then
instant = {} --类的实例
end
setmetatable(instant, {__index = self})
return instant
end
function match_mgr:init(zid)
self.zid = zid
self.matchid = sg_matchid
sg_matchid = sg_matchid + 1
self.state = State.InView
self.inview_players = {} -- 旁观玩家的列表
self.lhs_players = {} -- 左右两边的玩家
self.rhs_players = {} -- 左右两边的玩家
end
function match_mgr:broadcast_cmd_inview_players(stype, ctype, body, not_to_player)
local i
for i = 1, #self.inview_players do
if self.inview_players[i] ~= not_to_player then
self.inview_players[i]:send_cmd(stype, ctype, body)
end
end
end
function match_mgr:enter_player(p)
local i
if self.state ~= State.InView or p.state ~= State.InView then
return false
end
table.insert(self.inview_players, p) --将玩家加入到集结列表里面,
p.matchid = self.matchid
-- 发送命令,告诉客户端,你进入了一个比赛, zid, matchid
local body = {
zid = self.zid,
matchid = self.matchid
}
p:send_cmd(Stype.Logic, Cmd.eEnterMatch, body)
-- 将用户进来的消息发送给房间里面的其他玩家
body = p:get_user_arrived()
self:broadcast_cmd_inview_players(Stype.Logic, Cmd.eUserArrived, body, p)
-- end
-- 玩家还有收到 其他在我们这个等待列表里面的玩家
for i = 1, #self.inview_players do
if self.inview_players[i] ~= p then
body = self.inview_players[i]:get_user_arrived()
p:send_cmd(Stype.Logic, Cmd.eUserArrived, body)
end
end
-- end
-- 判断我们当前是否集结玩家结束了
if #self.inview_players >= PLAYER_NUM * 2 then
self.state = State.Ready
for i = 1, #self.inview_players do
self.inview_players[i].state = State.Ready
end
end
return true
end
return match_mgr
```
##### 游戏管理
game_mgr.lua
```lua
local Respones = require("Respones")
local Stype = require("Stype")
local Cmd = require("Cmd")
local mysql_game = require("database/mysql_game")
local redis_game = require("database/redis_game")
local player = require("logic_server/player")
local Zone = require("logic_server/Zone")
local State = require("logic_server/State")
local match_mgr = require("logic_server/match_mgr")
-- uid --> player
--在线的所有玩家uid
local logic_server_players = {}
local online_player_num = 0
--每个地图的等待列表
local zone_wait_list = {} -- zone_wait_list[Zone.SGYD] = {} --> uid --> p;
--房间列表
local zone_match_list = {}
zone_match_list[Zone.SGYD] = {} --SGYD地图的房间列表
zone_match_list[Zone.ASSY] = {} --ASSY地图的房间列表
--end
function send_status(s, stype, ctype, uid, status)
local msg = {stype, ctype, uid, {
status = status,
}}
Session.send_msg(s, msg)
end
--搜索未开游戏的房间,如果没有就自己创建一个
function search_inview_match_mgr(zid)
local match_list = zone_match_list[zid]
for k, v in pairs(match_list) do
if v.state == State.InView then
return v
end
end
local match = match_mgr:new()
match_list[match.matchid] = match
match:init(zid)
return match
end
--匹配玩家,1秒执行一次
function do_match_players()
local zid, wait_list
--遍历每个地图的等待列表
for zid, wait_list in pairs(zone_wait_list) do
local k, v
--遍历每个等待列表的每个玩家
for k, v in pairs(wait_list) do
local match = search_inview_match_mgr(zid)
if match then
--玩家进入房间,从等待列表离开,接收房间内的玩家信息,发送进入房间信息给其他玩家
if not match:enter_player(v) then
Logger.error("match system error : player state: ", v.state)
else
wait_list[k] = nil
end
end
end
end
end
Scheduler.schedule(do_match_players, 1000, -1, 5000)
-- eEnterZoneReq响应入口函数
-- {stype, ctype, utag, body}
function enter_zone(s, req)
local stype = req[1]
local uid = req[3]
local p = logic_server_players[uid]
if not p or p.zid ~= -1 then
send_status(s, stype, Cmd.eEnterZoneRes, uid, Respones.InvalidOpt)
return
end
local zid = req[4].zid;
if zid ~= Zone.SGYD and zid ~= Zone.ASSY then
send_status(s, stype, Cmd.eEnterZoneRes, uid, Respones.InvalidParams)
return
end
if not zone_wait_list[zid] then
zone_wait_list[zid] = {}
end
--加入等待列表中,发送信息回客户端
zone_wait_list[zid][uid] = p
p.zid = zid
send_status(s, stype, Cmd.eEnterZoneRes, uid, Respones.OK)
end
local game_mgr = {
login_logic_server = login_logic_server,
on_player_disconnect = on_player_disconnect,
on_gateway_disconnect = on_gateway_disconnect,
on_gateway_connect = on_gateway_connect,
enter_zone = enter_zone,
}
return game_mgr
机器人的设计和实现
机器人拥有和玩家一样的数据,也能通过匹配系统进入游戏
1: ugame数据库中加一个字段is_robot,这个字段标记是否为机器人;
2: 创建一个机器人玩家对象robet_player, 继承扩展自player;
3: 通过不断的产生新的游客系统来生成游客账号;
6:从数据库里面将机器人的信息加载到redis;
load_robots
do_load_robot_ugame_info
do_load_robot_uinfo(now_index, robots)
1: 添加机器人的robot列表;
2: 添加接口: do_new_robot_players 来加载机器人对象到分区列表;
3: 添加接口,push robot到等待列表: do_push_robot_to_match
4: 添加函数搜索空闲的机器人:search_idle_robot
5: 将robot 放入到match_mgr里面, 走正常的流程;
服务器
机器人类
local player = require("logic_server/player")
local robot_player = player:new()
function robot_player:new()
local instant = {} --类的实例
setmetatable(instant, {__index = self})
return instant
end
function robot_player:init(uid, s, ret_handler)
player.init(self, uid, s, ret_handler)
self.is_robot = true
end
return robot_player
数据库新增逻辑
mysql_game.lua
function get_robots_ugame_info(ret_handler)
if mysql_conn == nil then
if ret_handler then
ret_handler("mysql is not connected!", nil)
end
return
end
local sql = "select uchip, uchip2, uchip3, uvip, uvip_endtime, udata1, udata2, udata3, uexp, ustatus, uid from ugame where is_robot = 1"
local sql_cmd = sql
Mysql.query(mysql_conn, sql_cmd, function(err, ret)
if err then
if ret_handler ~= nil then
ret_handler(err, nil)
end
return
end
-- 没有这条记录
if ret == nil or #ret <= 0 then
if ret_handler ~= nil then
ret_handler(nil, nil)
end
return
end
-- end
local k, v
local robots = {}
for k, v in pairs(ret) do
local result = v
local one_robot = {}
one_robot.uchip = tonumber(result[1])
one_robot.uchip2 = tonumber(result[2])
one_robot.uchip3 = tonumber(result[3])
one_robot.uvip = tonumber(result[4])
one_robot.uvip_endtime = tonumber(result[5])
one_robot.udata1 = tonumber(result[6])
one_robot.udata2 = tonumber(result[7])
one_robot.udata3 = tonumber(result[8])
one_robot.uexp = tonumber(result[9])
one_robot.ustatus = tonumber(result[10])
one_robot.uid = tonumber(result[11])
table.insert(robots, one_robot)
end
ret_handler(nil, robots)
end)
end
游戏管理新增代码
game_mgr.lua
local zone_robot_list = {} -- 存放我们当前所在区间的机器人
zone_robot_list[Zone.SGYD] = {}
zone_robot_list[Zone.ASSY] = {}
--end
--把机器人放到机器人列表中
function do_new_robot_players(robots)
if #robots <= 0 then
return
end
local half_len = #robots
local i = 1
half_len = math.floor(half_len * 0.5)
-- 前半部分放一个分区
for i = 1, half_len do
local v = robots[i]
local r_player = robot_player:new()
r_player:init(v.uid, nil, nil)
r_player.zid = Zone.SGYD
zone_robot_list[Zone.SGYD][v.uid] = r_player
end
-- 下半部分放一个分区
for i = half_len + 1, #robots do
local v = robots[i]
local r_player = robot_player:new()
r_player:init(v.uid, nil, nil)
r_player.zid = Zone.ASSY
zone_robot_list[Zone.ASSY][v.uid] = r_player
end
end
--加载机器人的用户信息
function do_load_robot_uinfo(now_index, robots)
mysql_center.get_uinfo_by_uid(robots[now_index].uid, function (err, uinfo)
if err or not uinfo then
return
end
redis_center.set_uinfo_inredis(robots[now_index].uid, uinfo)
-- Logger.debug("uid " .. robots[now_index].uid .. " load to center reids!!!")
now_index = now_index + 1
if now_index > #robots then
do_new_robot_players(robots)
return
end
do_load_robot_uinfo(now_index, robots)
end)
end
--加载机器人的游戏信息
function do_load_robot_ugame_info()
mysql_game.get_robots_ugame_info(function(err, ret)
if err then
return
end
if not ret or #ret <= 0 then
return
end
local k, v
for k, v in pairs(ret) do
redis_game.set_ugame_info_inredis(v.uid, v)
end
do_load_robot_uinfo(1, ret)
end)
end
function load_robots()
if not mysql_game.is_connected() or
not mysql_center.is_connected() or
not redis_center.is_connected() or
not redis_game.is_connected() then
Scheduler.once(load_robots, 5000)
return
end
do_load_robot_ugame_info()
end
Scheduler.once(load_robots, 5000)
function search_idle_robot(zid)
local robots = zone_robot_list[zid]
local k, v
for k, v in pairs(robots) do
if v.matchid == -1 then
return v
end
end
return nil
end
--如果有空闲的房间,给房间匹配机器人
function do_push_robot_to_match()
local zid, match_list
local k, match
for zid, match_list in pairs(zone_match_list) do
for k, match in pairs(match_list) do
if match.state == State.InView then -- 找到了一个空闲的match
local robot = search_idle_robot(zid)
if robot then
Logger.debug("[".. robot.uid .."]" .. " enter match!")
match:enter_player(robot)
end
end
end
end
end
Scheduler.schedule(do_push_robot_to_match, 1000, -1, 1000)
需要注意的是,player基类的发送信息函数需要加一个判断,如果是机器人的话,则不执行收发信息的逻辑
玩家退出匹配
1: 玩家如果不在比赛里面,直接退出;
2:找到玩家所在的比赛对象,如果比赛已经开始,不允许退出;
3:从旁观列表里面删除player对象,然后通知其他的用户这个人离开了;
4: 发送命令给客户端,表示退出成功;
1: 给玩家加上一个进入游戏等待的座位id;
2: 遍历整个inview_players,如果这个作为没有玩家就放到这里;
3: player对象里面加上seatid;
4: 当我们用户来了后,seatid也加入到协议里面;
协议
1: ExitMatchReq: 不需要发送任何数据;
2: ExitMatchRes: 退出返回协议 status;
3: UserExitMatch: 服务器主动通知,其他玩家退出;{
required int32 seatid = 1;
}
}
4:UserArrived{
required string unick = 1;
required int32 uface = 2;
required int32 usex = 3;
required int32 seated = 4;
}
服务器
房间类调整
local match_mgr = {}
local sg_matchid = 1
local PLAYER_NUM = 3 -- 3v3
function match_mgr:init(zid)
self.zid = zid
self.matchid = sg_matchid
sg_matchid = sg_matchid + 1
self.state = State.InView
self.inview_players = {} -- 旁观玩家的列表
self.lhs_players = {} -- 左右两边的玩家
self.rhs_players = {} -- 左右两边的玩家
end
function match_mgr:broadcast_cmd_inview_players(stype, ctype, body, not_to_player)
local k, v
for k, v in pairs(self.inview_players) do
if v ~= not_to_player then
v:send_cmd(stype, ctype, body)
end
end
end
function match_mgr:exit_player(p)
local body = {
seatid = p.seatid
}
--通知其他玩家
self:broadcast_cmd_inview_players(Stype.Logic, Cmd.eUserExitMatch, body, p)
self.inview_players[p.seatid] = nil -- 等待列表里面来移除掉我们的player
p.zid = -1
p.matchid = -1
p.seatid = -1
body = {status = Respones.OK}
p:send_cmd(Stype.Logic, Cmd.eExitMatchRes, body)
end
function match_mgr:enter_player(p)
local i
if self.state ~= State.InView or p.state ~= State.InView then
return false
end
p.matchid = self.matchid
--将玩家加入到集结列表里面,
for i = 1, PLAYER_NUM * 2 do
if not self.inview_players[i] then
self.inview_players[i] = p
p.seatid = i
break
end
end
-- end
-- 发送命令,告诉客户端,你进入了一个比赛, zid, matchid
local body = {
zid = self.zid,
matchid = self.matchid
}
p:send_cmd(Stype.Logic, Cmd.eEnterMatch, body)
-- 将用户进来的消息发送给房间里面的其他玩家
body = p:get_user_arrived()
self:broadcast_cmd_inview_players(Stype.Logic, Cmd.eUserArrived, body, p)
-- end
-- 玩家还有收到 其他在我们这个等待列表里面的玩家
for i = 1, #self.inview_players do
if self.inview_players[i] ~= p then
body = self.inview_players[i]:get_user_arrived()
p:send_cmd(Stype.Logic, Cmd.eUserArrived, body)
end
end
-- end
-- 判断我们当前是否集结玩家结束了
if #self.inview_players >= PLAYER_NUM * 2 then
self.state = State.Ready
for i = 1, #self.inview_players do
self.inview_players[i].state = State.Ready
end
end
return true
end
return match_mgr
游戏管理新增
game_mgr.lua
---ExitMatchReq入口函数
function do_exit_match(s, req)
local uid = req[3]
local p = logic_server_players[uid]
if not p then
send_status(req[1], Cmd.eExitMatchRes, uid, Respones.InvalidOpt)
return
end
if p.state ~= State.InView or p.zid == -1 or p.matchid == - 1 or p.seatid == -1 then
send_status(req[1], Cmd.eExitMatchRes, uid, Respones.InvalidOpt)
return
end
local match = zone_match_list[p.zid][p.matchid]
if not match or match.state ~= State.InView then
send_status(req[1], Cmd.eExitMatchRes, uid, Respones.InvalidOpt)
return
end
match:exit_player(p);
end
比赛开始
调整:
1: 当玩家进来后,直接分边;
2: 广播的玩家进来也加上分边的信息;
3:side; 0 服务器左边, 1 服务器右边
4: 玩家自己进入比赛也加上, seatid 与side;
5: 玩家选择的英雄ID heroid;
步骤:
1: 当玩家集结完毕后,修改match状态;
2: 广播给所有的客户端,所有的客户端,进入到游戏场景;
3: MatchStart 协议指定,比赛开始,指定比赛开始协议;
4: 客户端进入游戏场景;
5:heroid 这里服务器随机生成一个,也可以玩家自己选, 我们这边服务器随机生成一个;
协议
message EnterMatch {
required int32 zid = 1;
required int32 matchid = 2;
required int32 seatid = 3;
required int32 side = 4;
}
message UserArrived {
required string unick = 1;
required int32 uface = 2;
required int32 usex = 3;
required int32 seatid = 4;
required int32 side = 5;
}
message GameStart {
repeated int32 heroes = 1; // 每个玩家的英雄id
}
服务器
游戏管理新增代码
function match_mgr:game_start()
local heroes = {}
for i = 1, PLAYER_NUM * 2 do
table.insert(heroes, self.inview_players[i].heroid)
end
local body = {
heroes = heroes,
}
self:broadcast_cmd_inview_players(Stype.Logic, Cmd.eGameStart, body, nil)
end
function match_mgr:enter_player(p)
local i
if self.state ~= State.InView or p.state ~= State.InView then
return false
end
p.matchid = self.matchid
--将玩家加入到集结列表里面,
for i = 1, PLAYER_NUM * 2 do
if not self.inview_players[i] then
self.inview_players[i] = p
p.seatid = i
p.side = 0
if i > PLAYER_NUM then
p.side = 1
end
break
end
end
-- end
-- 发送命令,告诉客户端,你进入了一个比赛, zid, matchid
local body = {
zid = self.zid,
matchid = self.matchid,
seatid = p.seatid,
side = p.side,
}
p:send_cmd(Stype.Logic, Cmd.eEnterMatch, body)
-- 将用户进来的消息发送给房间里面的其他玩家
body = p:get_user_arrived()
self:broadcast_cmd_inview_players(Stype.Logic, Cmd.eUserArrived, body, p)
-- end
-- 玩家还有收到 其他在我们这个等待列表里面的玩家
-- for i = 1, #self.inview_players do
for i = 1, PLAYER_NUM * 2 do
if self.inview_players[i] and self.inview_players[i] ~= p then
body = self.inview_players[i]:get_user_arrived()
p:send_cmd(Stype.Logic, Cmd.eUserArrived, body)
end
end
-- end
-- 判断我们当前是否集结玩家结束了
if #self.inview_players >= PLAYER_NUM * 2 then
self.state = State.Ready
for i = 1, PLAYER_NUM * 2 do
self.inview_players[i].state = State.Ready
end
-- 开始游戏数据发送给客户端
-- 进入到一个选英雄的这个界面,知道所有的玩家选好英雄这样一个状态;
-- 在游戏主页里面,自己设置你用的英雄,然后你自己再用大厅那里设置的英雄;
-- 服务器随机生成英雄的id[1, 5];
for i = 1, PLAYER_NUM * 2 do
self.inview_players[i].heroid = math.floor(math.random() * 5 + 1) -- [1, 5]
end
-- end
self:game_start()
-- end
end
return true
end
帧同步
原理
1: 服务器接收 每个客户端的输入,然后每帧广播给其它的客户端;
2:所有的客户端拿到是服务器同样的输入, 然后自己独立运算;
3:输入一样,代码一样,就能得到一样的结果;
4:服务器比较简单,计算都放到了客户端上;
5: 传输的数据小, 游戏手感不错;
6: 缺点是容易作弊;
7: 玩家的操作的反应时间一般是50~100ms;
流程
1: 服务器同步的帧,我们叫做逻辑帧,每秒维持 15~20左右;
2:客户端收到逻辑帧后,先处理这帧所有用户的输入;
3: 把下一帧的当前用户操作发送给服务器;
4: 服务器会对每帧的用户的操作进行缓存,保存起来;
5: 当客户断线以后, 重新连接上后,服务器把断线没有处理的帧到最新的帧,发送给客户端,客户端自己计算,同步到当前最新状态;
6: 当网络延时,用户的操作没有发送到服务器,会选着忽略;
客户端网络模块
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class network : UnitySingletom<network> {
// tcp
private string server_ip = "127.0.0.1";
private int port = 6080;
private Socket client_socket = null;
private bool is_connect = false;
private Thread recv_thread = null;
private const int RECV_LEN = 8192;
private byte[] recv_buf = new byte[RECV_LEN];
private int recved;
private byte[] long_pkg = null;
private int long_pkg_size = 0;
// end
// udp
private string udp_server_ip = "127.0.0.1";
private int udp_port = 8800;
IPEndPoint udp_remote_point;
Socket udp_socket = null;
private byte[] udp_recv_buf = new byte[60 * 1024];
private Thread udp_recv_thread = null;
// end
// event queque
private Queue<cmd_msg> net_events = new Queue<cmd_msg>();
// event listener, stype--> 监听者;
public delegate void net_message_handler(cmd_msg msg);
// 事件和监听的map
private Dictionary<int, net_message_handler> event_listeners = new Dictionary<int, net_message_handler>();
// Use this for initialization
void Start () {
this.connect_to_server();
this.udp_socket_init();
// test udp
this.InvokeRepeating("test_udp", 5, 5);
// end
}
void test_udp() {
Debug.Log("udp send");
logic_service_proxy.Instance.send_udp_test("HelloWorld!!!");
}
void OnDestroy() {
Debug.Log("network onDestroy!");
this.close();
}
void OnApplicaitonQuit() {
Debug.Log("OnApplicaitonQuit");
this.close();
this.udp_close();
}
// Update is called once per frame
void Update () {
lock (this.net_events) {
while (this.net_events.Count > 0) {
cmd_msg msg = this.net_events.Dequeue();
// 收到了一个命令包;
if (this.event_listeners.ContainsKey(msg.stype)) {
this.event_listeners[msg.stype](msg);
}
// end
}
}
}
void on_conntect_timeout() {
}
void on_connect_error(string err) {
}
void connect_to_server() {
try {
this.client_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAddress = IPAddress.Parse(this.server_ip);
IPEndPoint ipEndpoint = new IPEndPoint(ipAddress, this.port);
IAsyncResult result = this.client_socket.BeginConnect(ipEndpoint, new AsyncCallback(this.on_connected), this.client_socket);
bool success = result.AsyncWaitHandle.WaitOne(5000, true);
if (!success) { // timeout;
this.on_conntect_timeout();
}
}
catch (System.Exception e) {
Debug.Log(e.ToString());
this.on_connect_error(e.ToString());
}
}
void on_recv_cmd(byte[] data, int start, int data_len) {
cmd_msg msg;
proto_man.unpack_cmd_msg(data, start, data_len, out msg);
if (msg != null) {
lock (this.net_events) { // recv thread
this.net_events.Enqueue(msg);
}
}
}
void on_recv_tcp_data() {
byte[] pkg_data = (this.long_pkg != null) ? this.long_pkg : this.recv_buf;
while (this.recved > 0) {
int pkg_size = 0;
int head_size = 0;
if (!tcp_packer.read_header(pkg_data, this.recved, out pkg_size, out head_size)) {
break;
}
if (this.recved < pkg_size) {
break;
}
// unsigned char* raw_data = pkg_data + head_size;
int raw_data_start = head_size;
int raw_data_len = pkg_size - head_size;
on_recv_cmd(pkg_data, raw_data_start, raw_data_len);
// end
if (this.recved > pkg_size) {
this.recv_buf = new byte[RECV_LEN];
Array.Copy(pkg_data, pkg_size, this.recv_buf, 0, this.recved - pkg_size);
pkg_data = this.recv_buf;
}
this.recved -= pkg_size;
if (this.recved == 0 && this.long_pkg != null) {
this.long_pkg = null;
this.long_pkg_size = 0;
}
}
}
void thread_recv_worker() {
if (this.is_connect == false) {
return;
}
while (true) {
if (!this.client_socket.Connected) {
break;
}
try
{
int recv_len = 0;
if (this.recved < RECV_LEN) {
recv_len = this.client_socket.Receive(this.recv_buf, this.recved, RECV_LEN - this.recved, SocketFlags.None);
}
else {
if (this.long_pkg == null) {
int pkg_size;
int head_size;
tcp_packer.read_header(this.recv_buf, this.recved, out pkg_size, out head_size);
this.long_pkg_size = pkg_size;
this.long_pkg = new byte[pkg_size];
Array.Copy(this.recv_buf, 0, this.long_pkg, 0, this.recved);
}
recv_len = this.client_socket.Receive(this.long_pkg, this.recved, this.long_pkg_size - this.recved, SocketFlags.None);
}
if (recv_len > 0) {
this.recved += recv_len;
this.on_recv_tcp_data();
}
}
catch (System.Exception e) {
Debug.Log(e.ToString());
this.client_socket.Disconnect(true);
this.client_socket.Shutdown(SocketShutdown.Both);
this.client_socket.Close();
this.is_connect = false;
break;
}
}
}
void on_connected(IAsyncResult iar) {
try {
Socket client = (Socket)iar.AsyncState;
client.EndConnect(iar);
this.is_connect = true;
this.recv_thread = new Thread(new ThreadStart(this.thread_recv_worker));
this.recv_thread.Start();
Debug.Log("connect to server success" + this.server_ip + ":" + this.port + "!");
}
catch (System.Exception e) {
Debug.Log(e.ToString());
this.on_connect_error(e.ToString());
this.is_connect = false;
}
}
void close() {
if (!this.is_connect) {
return;
}
this.is_connect = false;
// abort recv thread
if (this.recv_thread != null) {
this.recv_thread.Interrupt();
this.recv_thread.Abort();
this.recv_thread = null;
}
// end
if (this.client_socket != null && this.client_socket.Connected) {
this.client_socket.Close();
this.client_socket = null;
}
}
private void on_send_data(IAsyncResult iar)
{
try
{
Socket client = (Socket)iar.AsyncState;
client.EndSend(iar);
}
catch (System.Exception e)
{
Debug.Log(e.ToString());
}
}
public void send_protobuf_cmd(int stype, int ctype, ProtoBuf.IExtensible body) {
byte[] cmd_data = proto_man.pack_protobuf_cmd(stype, ctype, body);
if (cmd_data == null) {
return;
}
byte[]tcp_pkg = tcp_packer.pack(cmd_data);
// end
this.client_socket.BeginSend(tcp_pkg, 0, tcp_pkg.Length, SocketFlags.None, new AsyncCallback(this.on_send_data), this.client_socket);
// end
}
public void send_json_cmd(int stype, int ctype, string json_body)
{
byte[] cmd_data = proto_man.pack_json_cmd(stype, ctype, json_body);
if (cmd_data == null) {
return;
}
byte[] tcp_pkg = tcp_packer.pack(cmd_data);
// end
this.client_socket.BeginSend(tcp_pkg, 0, tcp_pkg.Length, SocketFlags.None, new AsyncCallback(this.on_send_data), this.client_socket);
// end
}
public void add_service_listener(int stype, net_message_handler handler) {
if (this.event_listeners.ContainsKey(stype)) {
this.event_listeners[stype] += handler;
}
else {
this.event_listeners.Add(stype, handler);
}
}
public void remove_service_listener(int stype, net_message_handler handler) {
if (!this.event_listeners.ContainsKey(stype)) {
return;
}
this.event_listeners[stype] -= handler;
if (this.event_listeners[stype] == null) {
this.event_listeners.Remove(stype);
}
}
void udp_thread_recv_worker() {
while (true) {
EndPoint remote = (EndPoint)new IPEndPoint(IPAddress.Parse(this.udp_server_ip), this.udp_port);
Debug.Log("Begin Recive####");
int recved = this.udp_socket.ReceiveFrom(this.udp_recv_buf, ref remote);
Debug.Log("End Recive");
this.on_recv_cmd(this.udp_recv_buf, 0, recved);
}
}
void udp_close() {
// abort recv thread
if (this.udp_recv_thread != null)
{
this.udp_recv_thread.Interrupt();
this.udp_recv_thread.Abort();
this.udp_recv_thread = null;
}
// end
if (this.udp_socket != null) {
this.udp_socket.Close();
this.udp_socket = null;
}
}
void udp_socket_init() {
// Udp 远程的端口
this.udp_remote_point = new IPEndPoint(IPAddress.Parse(this.udp_server_ip), this.udp_port);
// 创建一个udp socket
this.udp_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
// 收数据, 用另外一个线程,如果我不绑定,收无法收;
IPEndPoint local_point = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8888);
this.udp_socket.Bind(local_point);
// 启动一个线程来收Udp的数据在特定的端口上;
this.udp_recv_thread = new Thread(new ThreadStart(this.udp_thread_recv_worker));
this.udp_recv_thread.Start();
// end
}
private void on_udp_send_data(IAsyncResult iar) {
try {
Socket client = (Socket)iar.AsyncState;
client.EndSendTo(iar);
}
catch (System.Exception e) {
Debug.Log(e.ToString());
}
}
public void udp_send_protobuf_cmd(int stype, int ctype, ProtoBuf.IExtensible body) {
byte[] cmd_data = proto_man.pack_protobuf_cmd(stype, ctype, body);
if (cmd_data == null) {
return;
}
// end
this.udp_socket.BeginSendTo(cmd_data, 0, cmd_data.Length, SocketFlags.None, this.udp_remote_point, new AsyncCallback(this.on_udp_send_data), this.udp_socket);
// end
}
}
逻辑服务器添加UDP通信
local game_config = {
gateway_tcp_ip = "127.0.0.1",
gateway_tcp_port = 6080,
gateway_ws_ip = "127.0.0.1",
gateway_ws_port = 6081,
servers = remote_servers,
auth_mysql = {
host = "127.0.0.1", -- 数据库所在的host
port = 3306, -- 数据库所在的端口
db_name = "auth_center", -- 数据库的名字
uname = "root", -- 登陆数据库的账号
upwd = "123456", -- 登陆数据库的密码
},
center_redis = {
host = "127.0.0.1", -- redis所在的host
port = 6379, -- reidis 端口
db_index = 1, -- 数据1
},
game_mysql = {
host = "127.0.0.1", -- 数据库所在的host
port = 3306, -- 数据库所在的端口
db_name = "moba_game", -- 数据库的名字
uname = "root", -- 登陆数据库的账号
upwd = "123456", -- 登陆数据库的密码
},
game_redis = {
host = "127.0.0.1", -- redis所在的host
port = 6379, -- reidis 端口
db_index = 2, -- 数据库2
},
-- 做排行榜的redis服务器
rank_redis = {
host = "127.0.0.1", -- redis所在的host
port = 6379, -- reidis 端口
db_index = 3, -- 数据库3
},
-- logic server udp
logic_udp = {
host = "127.0.0.1",
port = 8800,
},
}
main.lua新增代码
Netbus.udp_listen(game_config.logic_udp.port)
测试代码
协议:
message UdpTest {
required string content = 1;
}
logic_service.lua
logic_service_handlers[Cmd.eUdpTest] = game_mgr.do_udp_test
game_mgr.lua
function do_udp_test(s, req)
local stype = req[1]
local ctype = req[2]
local body = req[4]
print(body.content)
local msg = {stype, ctype, 0, {
content = body.content,
}}
-- Session.send_msg(s, msg)
end
UDP帧事件触发
1: match_mgr向房间里面的玩家广播帧事件;
2:帧事件的协议(暂时先这样,后续还要改)
message LogicFrame{
required int32 frameid = 1;
}
3: 将帧事件的客户端与服务器调通;
4: 当客户端结束后,如果udp 读nread <= 0 判断return;
5: 玩家断线后: p:set_session(nil)
6: 加上判断机制,如果玩家断线,就不用广播udp了;
协议
message LoginLogicReq {
required string udp_ip = 1;
required int32 udp_port = 2;
}
message LogicFrame {
required int32 frameid = 1;
}
服务器
通过网关获取客户端ip地址
网关gw_service相关代码修改:
-- s 来自于客户端
function send_to_server(client_session, raw_cmd)
local stype, ctype, utag = RawCmd.read_header(raw_cmd)
print(stype, ctype, utag)
local server_session = server_session_man[stype]
if server_session == nil then --可以回一个命令给客户端,系统错误
return
end
if is_login_request_cmd(ctype) then
utag = Session.get_utag(client_session)
if utag == 0 then
utag = g_ukey
g_ukey = g_ukey + 1
Session.set_utag(client_session, utag)
end
client_sessions_ukey[utag] = client_session
elseif ctype == Cmd.eLoginLogicReq then --登录逻辑服务器请求,发送客户端ip地址
local uid = Session.get_uid(client_session)
utag = uid
if utag == 0 then --改操作要先登陆
return
end
local tcp_ip, tcp_port = Session.get_address(client_session)
local body = RawCmd.read_body(raw_cmd)
body.udp_ip = tcp_ip
local login_logic_cmd = {stype, ctype, utag, body}
Session.send_msg(server_session, login_logic_cmd)
return
else
local uid = Session.get_uid(client_session)
utag = uid
if utag == 0 then --改操作要先登陆
return
end
-- client_sessions_uid[uid] = client_session;
end
-- 打上utag然后转发给我们的服务器
RawCmd.set_utag(raw_cmd, utag)
Session.send_raw_cmd(server_session, raw_cmd)
导出发送UDP数据的lua接口
static void
udp_send_msg(char* ip, int port, struct cmd_msg* msg) {
unsigned char* encode_pkg = NULL;
int encode_len = 0;
encode_pkg = proto_man::encode_msg_to_raw(msg, &encode_len);
if (encode_pkg) {
// this->send_data(encode_pkg, encode_len);
netbus::instance()->udp_send_to(ip, port, encode_pkg, encode_len);
proto_man::msg_raw_free(encode_pkg);
}
}
// ip, port, {1: stype, 2: ctype, 3: utag, 4 body}
static int
lua_udp_send_msg(lua_State* tolua_S) {
char* ip = (char*)tolua_tostring(tolua_S, 1, NULL);
if (ip == NULL) {
goto lua_failed;
}
int port = (int)tolua_tonumber(tolua_S, 2, NULL);
if (port == 0) {
goto lua_failed;
}
// stack: 1 s, 2, table,
if (!lua_istable(tolua_S, 3)) {
goto lua_failed;
}
struct cmd_msg msg;
int n = luaL_len(tolua_S, 3);
if (n != 4 && n != 3) {
goto lua_failed;
}
lua_pushnumber(tolua_S, 1);
lua_gettable(tolua_S, 3);
msg.stype = luaL_checkinteger(tolua_S, -1);
lua_pushnumber(tolua_S, 2);
lua_gettable(tolua_S, 3);
msg.ctype = luaL_checkinteger(tolua_S, -1);
lua_pushnumber(tolua_S, 3);
lua_gettable(tolua_S, 3);
msg.utag = luaL_checkinteger(tolua_S, -1);
if (n == 3) {
msg.body = NULL;
// s->send_msg(&msg);
udp_send_msg(ip, port, &msg);
return 0;
}
lua_pushnumber(tolua_S, 4);
lua_gettable(tolua_S, 3);
if (proto_man::proto_type() == PROTO_JSON) {
msg.body = (char*)lua_tostring(tolua_S, -1);
// s->send_msg(&msg);
udp_send_msg(ip, port, &msg);
}
else {
if (!lua_istable(tolua_S, -1)) {
msg.body = NULL;
// s->send_msg(&msg);
udp_send_msg(ip, port, &msg);
}
else { // protobuf message table
const char* msg_name = proto_man::protobuf_cmd_name(msg.ctype);
msg.body = lua_table_to_protobuf(tolua_S, lua_gettop(tolua_S), msg_name);
// s->send_msg(&msg);
udp_send_msg(ip, port, &msg);
proto_man::release_message((google::protobuf::Message*)(msg.body));
}
}
lua_failed:
return 0;
}
```
```c
netbus::udp_send_to(char* ip, int port, unsigned char* body, int len) {
uv_buf_t w_buf;
w_buf = uv_buf_init((char*)body, len);
uv_udp_send_t* req = (uv_udp_send_t*)small_alloc(sizeof(uv_udp_send_t));
SOCKADDR_IN addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.S_un.S_addr = inet_addr(ip);
uv_udp_send(req, (uv_udp_t*)this->udp_handler, &w_buf, 1, (const sockaddr*)&addr, on_uv_udp_send_end);
}
玩家ip管理与UDP发送数据
player.lua
function player:init(uid, s, ret_handler)
self.session = s
self.uid = uid
self.zid = -1 -- 玩家所在的空间, -1,不在任何游戏场
self.matchid = -1 -- 玩家所在的比赛房间的id
self.seatid = -1 -- 玩家在比赛中的序列号
self.side = -1 -- 玩家在游戏里面所在的边, 0(lhs), 1(rhs)
self.heroid = -1 -- 玩家的英雄号 [1, 5]
self.state = State.InView -- 玩家当前处于旁观状态
self.is_robot = false -- 玩家是否为机器人
self.client_ip = nil -- 玩家对应客户端的 udp的ip地址
self.client_udp_port = 0 -- 玩家对应的客户端udp 的port
function player:set_udp_addr(ip, port)
self.client_ip = ip
self.client_udp_port = port
end
function player:set_session(s)
self.session = s
end
function player:udp_send_cmd(stype, ctype, body)
if not self.session or self.is_robot then --玩家已经断线或是机器人
return
end
if not self.client_ip or self.client_udp_port == 0 then
return
end
local msg = {stype, ctype, 0, body}
Session.udp_send_msg(self.client_ip, self.client_udp_port, msg)
end
game_mgr.lua在登录游戏服务器时给player的ip和port赋值
function login_logic_server(s, req)
local uid = req[3]
local stype = req[1]
local body = req[4];
local p = logic_server_players[uid] -- player对象
if p then -- 玩家对象已经存在了,更新一下session就可以了;
p:set_session(s)
p:set_udp_addr(body.udp_ip, body.udp_port)
send_status(s, stype, Cmd.eLoginLogicRes, uid, Respones.OK)
return
end
p = player:new()
p:init(uid, s, function(status)
if status == Respones.OK then
logic_server_players[uid] = p
online_player_num = online_player_num + 1
end
send_status(s, stype, Cmd.eLoginLogicRes, uid, status)
end)
p:set_udp_addr(body.udp_ip, body.udp_port)
end
match_mgr.lua
function match_mgr:init(zid)
self.zid = zid
self.matchid = sg_matchid
sg_matchid = sg_matchid + 1
self.state = State.InView
self.frameid = 0
self.inview_players = {} -- 旁观玩家的列表
self.lhs_players = {} -- 左右两边的玩家
self.rhs_players = {} -- 左右两边的玩家
end
function match_mgr:game_start()
local heroes = {}
for i = 1, PLAYER_NUM * 2 do
table.insert(heroes, self.inview_players[i].heroid)
end
local body = {
heroes = heroes,
}
self:broadcast_cmd_inview_players(Stype.Logic, Cmd.eGameStart, body, nil)
self.state = State.Playing
self:update_players_state(State.Playing)
-- 5秒以后 开始第一个帧事件, 1000 --> 20 FPS ---> 50
self.frameid = 0
self.frame_timer = Scheduler.schedule(function()
self:on_logic_frame()
end, 5000, -1, 50)
-- end
end
-------帧同步,每50毫秒执行一次
function match_mgr:on_logic_frame()
for i = 1, PLAYER_NUM * 2 do
local p = self.inview_players[i]
if p then
body = { frameid = self.frameid }
p:udp_send_cmd(Stype.Logic, Cmd.eLogicFrame, body)
end
end
self.frameid = self.frameid + 1
end
function match_mgr:update_players_state(state)
for i = 1, PLAYER_NUM * 2 do
self.inview_players[i].state = state
end
end
function match_mgr:enter_player(p)
local i
if self.state ~= State.InView or p.state ~= State.InView then
return false
end
p.matchid = self.matchid
--将玩家加入到集结列表里面,
for i = 1, PLAYER_NUM * 2 do
if not self.inview_players[i] then
self.inview_players[i] = p
p.seatid = i
p.side = 0
if i > PLAYER_NUM then
p.side = 1
end
break
end
end
-- end
-- 发送命令,告诉客户端,你进入了一个比赛, zid, matchid
local body = {
zid = self.zid,
matchid = self.matchid,
seatid = p.seatid,
side = p.side,
}
p:send_cmd(Stype.Logic, Cmd.eEnterMatch, body)
-- 将用户进来的消息发送给房间里面的其他玩家
body = p:get_user_arrived()
self:broadcast_cmd_inview_players(Stype.Logic, Cmd.eUserArrived, body, p)
-- end
-- 玩家还有收到 其他在我们这个等待列表里面的玩家
-- for i = 1, #self.inview_players do
for i = 1, PLAYER_NUM * 2 do
if self.inview_players[i] and self.inview_players[i] ~= p then
body = self.inview_players[i]:get_user_arrived()
p:send_cmd(Stype.Logic, Cmd.eUserArrived, body)
end
end
-- end
-- 判断我们当前是否集结玩家结束了
if #self.inview_players >= PLAYER_NUM * 2 then
self.state = State.Ready
self:update_players_state(State.Ready)
-- 开始游戏数据发送给客户端
-- 进入到一个选英雄的这个界面,知道所有的玩家选好英雄这样一个状态;
-- 在游戏主页里面,自己设置你用的英雄,然后你自己再用大厅那里设置的英雄;
-- 服务器随机生成英雄的id[1, 5];
for i = 1, PLAYER_NUM * 2 do
self.inview_players[i].heroid = math.floor(math.random() * 5 + 1) -- [1, 5]
end
-- end
self:game_start()
-- end
end
return true
end
帧编号与事件采集
1: 玩家操作:
位置:seatid,
操作类型1(摇杆), 2(攻击1), 3(攻击2), 4(攻击3)…
摇杆数据: int x; int y;
2: message OptionEvent {
required int32 seatid = 1;
required int32 opt_type = 2;
optional int32 x = 3; // 定点数
optional int32 y = 4; // 定点数
}
1: 服务器的每一帧操作集合
{frameid { {seatid, opt_type, x, y}, {}, …}
message FrameOpts {
required int32 frameid = 1;
repeated OptionEvent opts = 2;
}
2: 每次帧同步的时候发送未同步的操作,给客户端
message LogicFrame {
required int32 frameid = 1;
repeated FrameOpts unsync_frames = 2;
}
3: 每个玩家保存客户端已经同步完成的frameid;
协议
message OptionEvent {
required int32 seatid = 1;
required int32 opt_type = 2;
optional int32 x = 3;
optional int32 y = 4;
}
message FrameOpts {
required int32 frameid = 1;
repeated OptionEvent opts = 2;
}
message LogicFrame {
required int32 frameid = 1;
repeated FrameOpts unsync_frames = 2;
}
message NextFrameOpts {
required int32 frameid = 1;
required int32 zid = 2;
required int32 matchid = 3;
required int32 seatid = 4;
repeated OptionEvent opts = 5;
}
客户端
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using gprotocol;
// 不同的操作类型
enum OptType {
JoyStick = 1,
Attack1 = 2,
Attack2 = 3,
Attack3 = 4,
Skill1 = 5,
Skill2 = 6,
Skill7 = 7,
}
public class game_zygote : MonoBehaviour {
public joystick stick;
public GameObject[] hero_characters = null; // 男, 女;
public GameObject entry_A; //
private int sync_frameid = 0; //
// private FrameOpts last_frame_opt = null;
void Start () {
event_manager.Instance.add_event_listener("on_logic_upate", this.on_logic_upate);
GameObject hero = GameObject.Instantiate(this.hero_characters[ugame.Instance.usex]);
hero.transform.SetParent(this.transform, false);
hero.transform.position = this.entry_A.transform.position;
charactor_ctrl ctrl = hero.AddComponent<charactor_ctrl>();
ctrl.is_ghost = false; // 自己来控制;
ctrl.stick = this.stick; // 测试
}
//采集操作数据
void capture_player_opts() {
NextFrameOpts next_frame = new NextFrameOpts();
next_frame.frameid = this.sync_frameid + 1;
next_frame.zid = ugame.Instance.zid;
next_frame.matchid = ugame.Instance.matchid;
next_frame.seatid = ugame.Instance.self_seatid;
// 摇杆
OptionEvent opt_stick = new OptionEvent();
opt_stick.seatid = ugame.Instance.self_seatid;
opt_stick.opt_type = (int)OptType.JoyStick;
opt_stick.x = (int)(this.stick.dir.x * (1 << 16)); // 16.16
opt_stick.y = (int)(this.stick.dir.y * (1 << 16)); // 16.16
next_frame.opts.Add(opt_stick);
// end
// 攻击
// end
// 发送给服务器
logic_service_proxy.Instance.send_next_frame_opts(next_frame);
// end
}
// 服务器发送过来帧事件,其中包含了所有用户的操作
void on_logic_upate(string name, object udata) {
LogicFrame frame = (LogicFrame)udata;
// 如果发送过来的帧id比现在低,说明已经同步过了
if (frame.frameid < this.sync_frameid) {
return;
}
Debug.Log(frame.unsync_frames.Count);
// 打印所有玩家的操作,这是一个OptionEvent类型的集合
for (int i = 0; i < frame.unsync_frames.Count; i++) {
for (int j = 0; j < frame.unsync_frames[i].opts.Count; j++) {
Debug.Log(frame.unsync_frames[i].opts[j].x + ":" + frame.unsync_frames[i].opts[j].y);
}
}
// 同步自己客户端上一帧逻辑操作, 调整我们的位置; 调成完以后, 客户端同步到的是 sync_frameid
// end
// 从sync_frameid + 1 开始 ----> frame.frameid - 1; // 同步丢失的帧, 所有客户端数据--》同步到 frame.frameid - 1;
// end
// 获取 最后一个操作 frame.frameid 操作,根据这个操作,来处理,来播放动画;
// end
// 采集下一个帧的事件,发送给服务器;
this.sync_frameid = frame.frameid; // 同步到的事件帧ID;
// this.last_frame_opt = frame.unsync_frames[frame.unsync_frames.Count - 1];
this.capture_player_opts();
}
}
服务器
房间管理新增代码
1.接收玩家发过来的逻辑帧
logic_service.lua
logic_service_handlers[Cmd.eNextFrameOpts] = game_mgr.on_next_frame_event
2.解包数据:
game_mgr.lua
function on_next_frame_event(s, req)
local stype = req[1]
local ctype = req[2]
local body = req[4]
local match = zone_match_list[body.zid][body.matchid]
if not match or match.state ~= State.Playing then
return
end
match:on_next_frame_event(body)
end
3.处理逻辑帧
match_mgr
local match_mgr = {}
local sg_matchid = 1
local PLAYER_NUM = 2 -- 3v3
function match_mgr:init(zid)
self.zid = zid
self.matchid = sg_matchid
sg_matchid = sg_matchid + 1
self.state = State.InView
self.frameid = 0
self.inview_players = {} -- 旁观玩家的列表
self.lhs_players = {} -- 左右两边的玩家
self.rhs_players = {} -- 左右两边的玩家
self.match_frames = {} -- 保存的是游戏开始依赖所有的帧操作;
self.next_frame_opt = {} -- 当前的帧操作;
end
--当游戏开始时执行
function match_mgr:game_start()
local players_match_info = {}
for i = 1, PLAYER_NUM * 2 do
local p = self.inview_players[i]
local info = {
heroid = p.heroid,
seatid = p.seatid,
side = p.side,
}
table.insert(players_match_info, info)
end
local body = {
players_match_info = players_match_info,
}
--广播游戏开始
self:broadcast_cmd_inview_players(Stype.Logic, Cmd.eGameStart, body, nil)
self.state = State.Playing
for i = 1, PLAYER_NUM * 2 do
self.inview_players[i].state = state
end
-- 5秒以后 开始第一个帧事件, 1000 --> 20 FPS ---> 50
self.frameid = 1
self.match_frames = {} -- 保存的是游戏开始依赖所有的帧操作;
self.next_frame_opt = {frameid = self.frameid, opts = {}} -- 当前的帧玩家操作;
self.frame_timer = Scheduler.schedule(function()
self:on_logic_frame()
end, 5000, -1, 50)
-- end
end
--收到了客户端发过来的逻辑帧时
function match_mgr:on_next_frame_event(next_frame_opts)
local seatid = next_frame_opts.seatid
--print(seatid, next_frame_opts.frameid, #next_frame_opts.opts)
local p = self.inview_players[seatid]
if not p then
return
end
-- 记录客户端已经同步的帧
if p.sync_frameid < next_frame_opts.frameid - 1 then
p.sync_frameid = next_frame_opts.frameid - 1
end
-- 如果客户端发送过来的不是当前帧,直接丢弃
if (next_frame_opts.frameid ~= self.frameid) then
return
end
--记录客户端操作到当前帧,把客户端所有的操作插入当前帧
for i = 1, #next_frame_opts.opts do
table.insert(self.next_frame_opt.opts, next_frame_opts.opts[i])
end
end
function match_mgr:send_unsync_frames(p)
local opt_frams = {}
--print("^^^^", p.sync_frameid, #self.match_frames)
for i = (p.sync_frameid + 1), #self.match_frames do
table.insert(opt_frams, self.match_frames[i])
end
local body = { frameid = self.frameid, unsync_frames = opt_frams}
p:udp_send_cmd(Stype.Logic, Cmd.eLogicFrame, body)
end
--50毫秒执行一次,把该帧所有用户的操作加入表中,帧数+1,并发送所有没同步过的帧给用户
function match_mgr:on_logic_frame()
table.insert(self.match_frames, self.next_frame_opt)
for i = 1, PLAYER_NUM * 2 do
local p = self.inview_players[i]
if p then
self:send_unsync_frames(p)
end
end
self.frameid = self.frameid + 1
self.next_frame_opt = {frameid = self.frameid, opts = {}} -- 当前的帧玩家操作;
end
帧同步下的角色控制
客户端
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using gprotocol;
public enum OptType {
JoyStick = 1,
Attack1 = 2,
Attack2 = 3,
Attack3 = 4,
Skill1 = 5,
Skill2 = 6,
Skill7 = 7,
}
public class game_zygote : MonoBehaviour {
public joystick stick;
public GameObject[] hero_characters = null; // 男, 女;
public GameObject entry_A; // side A玩家的出生的中心点;
public GameObject entry_B; // side B玩家的出生的中心点;
private int sync_frameid = 0; //
private FrameOpts last_frame_opt = null;
private List<hero> heros = new List<hero>();
public const int LOGIC_FRAME_TIME = 66; // 66ms
private hero test_hero; // 测试的这个英雄
// Use this for initialization
void Start () {
//
event_manager.Instance.add_event_listener("on_logic_upate", this.on_logic_upate);
// end
// 放我们的英雄, side, 第几个;
hero h;
h = this.place_hero_at(ugame.Instance.players_math_info[0], 0); // side A, 0
this.heros.Add(h);
h = this.place_hero_at(ugame.Instance.players_math_info[1], 1); // side A, 1
this.heros.Add(h);
h = this.place_hero_at(ugame.Instance.players_math_info[2], 0); // side B 0
this.heros.Add(h);
h = this.place_hero_at(ugame.Instance.players_math_info[3], 1); // side B 1
this.heros.Add(h);
}
hero get_hero(int seatid) {
for (int i = 0; i < this.heros.Count; i++) {
if (this.heros[i].seatid == seatid) {
return this.heros[i];
}
}
return null;
}
hero place_hero_at(PlayerMatchInfo match_info, int index) {
int side = match_info.side;
user_info uinfo = ugame.Instance.get_user_info(match_info.seatid);
GameObject h_object = GameObject.Instantiate(this.hero_characters[uinfo.usex]);
h_object.name = uinfo.unick; // 设置创建的节点的英雄名字;
h_object.transform.SetParent(this.transform, false);
Vector3 center_pos;
if (side == 0) {
center_pos = this.entry_A.transform.position;
}
else {
center_pos = this.entry_B.transform.position;
}
if (index == 0) {
center_pos.z -= 3.0f;
}
else {
center_pos.z += 3.0f;
}
h_object.transform.position = center_pos;
hero ctrl = h_object.AddComponent<hero>();
ctrl.is_ghost = (match_info.seatid == ugame.Instance.self_seatid) ? false : true;
ctrl.logic_init(h_object.transform.position); // 逻辑数据部分的初始化
ctrl.seatid = match_info.seatid;
ctrl.side = side;
return ctrl;
}
void capture_player_opts() {
NextFrameOpts next_frame = new NextFrameOpts();
next_frame.frameid = this.sync_frameid + 1;
next_frame.zid = ugame.Instance.zid;
next_frame.matchid = ugame.Instance.matchid;
next_frame.seatid = ugame.Instance.self_seatid;
// 摇杆
OptionEvent opt_stick = new OptionEvent();
opt_stick.seatid = ugame.Instance.self_seatid;
opt_stick.opt_type = (int)OptType.JoyStick;
opt_stick.x = (int)(this.stick.dir.x * (1 << 16)); // 16.16
opt_stick.y = (int)(this.stick.dir.y * (1 << 16)); // 16.16
next_frame.opts.Add(opt_stick);
// end
// 攻击
// end
// 发送给服务器
logic_service_proxy.Instance.send_next_frame_opts(next_frame);
// end
}
void on_handler_frame_event(FrameOpts frame_opt) {
// 把所有英雄的输入进行处理;
for (int i = 0; i < frame_opt.opts.Count; i++) {
int seatid = frame_opt.opts[i].seatid;
hero h = this.get_hero(seatid);
if (!h) {
Debug.LogError("cannot find here: " + seatid);
continue;
}
h.on_handler_frame_event(frame_opt.opts[i]);
}
// end
// 怪物AI 根据我们的处理,来进行处理...
// end
}
void on_sync_last_logic_frame(FrameOpts frame_opt) {
// 把所有英雄的输入进行数据同步,同步的时间间隔(逻辑帧的时间间隔);
for (int i = 0; i < frame_opt.opts.Count; i++) {
int seatid = frame_opt.opts[i].seatid;
hero h = this.get_hero(seatid);
if (!h) {
Debug.LogError("cannot find here: " + seatid);
continue;
}
h.on_sync_last_logic_frame(frame_opt.opts[i]);
}
// end
}
void on_jump_to_next_frame(FrameOpts frame_opt) {
// 把所有英雄的输入进行处理;
for (int i = 0; i < frame_opt.opts.Count; i++)
{
int seatid = frame_opt.opts[i].seatid;
hero h = this.get_hero(seatid);
if (!h) {
Debug.LogError("cannot find here: " + seatid);
continue;
}
h.on_jump_to_next_frame(frame_opt.opts[i]);
}
// end
// 怪物AI 根据我们的处理,来进行处理...
// end
}
void on_logic_upate(string name, object udata) {
LogicFrame frame = (LogicFrame)udata;
if (frame.frameid < this.sync_frameid) {
return;
}
// 同步自己客户端上一帧逻辑操作,
// 调整我们的位置; 调成完以后, 客户端同步到的是 sync_frameid
if (this.last_frame_opt != null) { // 位置,同步到正确的逻辑位置
this.on_sync_last_logic_frame(this.last_frame_opt);
}
// end // 最准确的 frameid 的时候了;
// 从sync_frameid + 1 开始 ----> frame.frameid - 1; // 同步丢失的帧, 所有客户端数据--》同步到 frame.frameid - 1;
for (int i = 0; i < frame.unsync_frames.Count; i++) {
if (this.sync_frameid >= frame.unsync_frames[i].frameid) {
continue;
}
else if (frame.unsync_frames[i].frameid >= frame.frameid) {
break;
}
this.on_jump_to_next_frame(frame.unsync_frames[i]);
}
// end // 跳到你收到的最新的一个帧之前;
// 获取 最后一个操作 frame.frameid 操作,根据这个操作,来处理,来播放动画;
this.sync_frameid = frame.frameid; // 同步到的事件帧ID;
if (frame.unsync_frames.Count > 0) {
this.last_frame_opt = frame.unsync_frames[frame.unsync_frames.Count - 1];
this.on_handler_frame_event(this.last_frame_opt);
}
else {
this.last_frame_opt = null;
}
// end
// 采集下一个帧的事件,发送给服务器;
this.capture_player_opts();
}
// Update is called once per frame
void Update () {
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using gprotocol;
enum charactor_state {
walk = 1,
free = 2,
idle = 3,
attack = 4,
attack2 = 5,
attack3 = 6,
skill = 7,
skill2 = 8,
death = 9,
}
// 角色控制, 主角 3v3;
// 玩家控制得,其它玩家控制
public class hero : MonoBehaviour {
public bool is_ghost = false; // is_ghost: 标记是否为别人控制的 ghost;
public float speed = 8.0f; // 给我们的角色定义一个速度
private CharacterController ctrl;
private Animation anim;
private charactor_state anim_state = charactor_state.idle;
private Vector3 camera_offset; // 主角离摄像机的相对距离;
private int stick_x = 0;
private int stick_y = 0;
private charactor_state logic_state = charactor_state.idle;
private Vector3 logic_postion; // 保存我们当前的逻辑帧的位置;
public int seatid = -1;
public int side = -1;
// Use this for initialization
void Start () {
GameObject ring = Resources.Load<GameObject>("effect/other/guangquan_fanwei");
this.ctrl = this.GetComponent<CharacterController>();
this.anim = this.GetComponent<Animation>();
if (!this.is_ghost) { // 玩家控制角色;
GameObject r = GameObject.Instantiate(ring);
r.transform.SetParent(this.transform, false);
r.transform.localPosition = Vector3.zero;
r.transform.localScale = new Vector3(2, 1, 2);
this.camera_offset = Camera.main.transform.position - this.transform.position;
}
this.anim.Play("idle");
}
public void logic_init(Vector3 logic_pos) {
this.stick_x = 0;
this.stick_y = 0;
this.logic_postion = logic_pos;
this.logic_state = charactor_state.idle;
}
void do_joystick_event(float dt) {
if (this.stick_x == 0 && this.stick_y == 0) {
this.logic_state = charactor_state.idle;
return;
}
this.logic_state = charactor_state.walk;
float dir_x = ((float)this.stick_x / (float)(1 << 16));
float dir_y = ((float)this.stick_y / (float)(1 << 16));
float r = Mathf.Atan2(dir_y, dir_x);
float s = this.speed * dt;
float sx = s * Mathf.Cos(r - Mathf.PI * 0.25f);
float sz = s * Mathf.Sin(r - Mathf.PI * 0.25f);
this.ctrl.Move(new Vector3(sx, 0, sz));
float degree = r * 180 / Mathf.PI;
degree = 360 - degree + 90 + 45;
this.transform.localEulerAngles = new Vector3(0, degree, 0);
}
void on_joystick_anim_update() {
if (this.logic_state != charactor_state.idle &&
this.logic_state != charactor_state.walk) {
return;
}
if (this.stick_x == 0 && this.stick_y == 0) {
if (this.anim_state == charactor_state.walk) {
this.anim.CrossFade("idle");
this.anim_state = charactor_state.idle;
}
return;
}
if (this.anim_state == charactor_state.idle) {
this.anim.CrossFade("walk");
this.anim_state = charactor_state.walk;
}
this.do_joystick_event(Time.deltaTime);
if (!this.is_ghost) {
Camera.main.transform.position = this.transform.position + this.camera_offset;
}
}
// Update is called once per frame
// 播放动画
void Update () {
this.on_joystick_anim_update();
}
void handle_joystic_event(OptionEvent opt) {
this.stick_x = opt.x;
this.stick_y = opt.y;
if (this.stick_x == 0 && this.stick_y == 0) {
this.logic_state = charactor_state.idle;
}
else {
this.logic_state = charactor_state.walk;
}
}
void jump_joystic_event(OptionEvent opt) {
this.sync_last_joystic_event(opt);
}
// 英雄来处理我们的事件
public void on_handler_frame_event(OptionEvent opt) {
switch(opt.opt_type) {
case (int)OptType.JoyStick:
handle_joystic_event(opt);
break;
}
}
// 英雄跳帧
public void on_jump_to_next_frame(OptionEvent opt) {
switch (opt.opt_type) {
case (int)OptType.JoyStick:
jump_joystic_event(opt);
break;
}
}
//
void sync_last_joystic_event(OptionEvent opt) {
// logci position, ---> dt = ??
this.stick_x = opt.x;
this.stick_y = opt.y;
this.transform.position = this.logic_postion;
this.do_joystick_event((float)game_zygote.LOGIC_FRAME_TIME / 1000.0f);
this.logic_postion = this.transform.position;
}
public void on_sync_last_logic_frame(OptionEvent opt) {
switch (opt.opt_type) {
case (int)OptType.JoyStick:
sync_last_joystic_event(opt);
break;
}
}
}
部署
makefile文件的编写: