服务器分布式部署与通用服务器


本教程基于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文件的编写:



文章作者: 微笑紫瞳星
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 微笑紫瞳星 !
  目录