diff --git a/Code/['World']['Global']['Define']['GlobalDataModule'].ModuleScript.lua b/Code/['World']['Global']['Define']['GlobalDataModule'].ModuleScript.lua deleted file mode 100644 index 2e15412..0000000 --- a/Code/['World']['Global']['Define']['GlobalDataModule'].ModuleScript.lua +++ /dev/null @@ -1,9 +0,0 @@ ---- 全局变量的定义,全部定义在GlobalData这张表下面,用于全局可修改的参数 ---- @module GlobalData Defines ---- @copyright Lilith Games, Avatar Team -local GlobalData = {} - --- Test only -GlobalData.PlayerData = {} - -return GlobalData diff --git a/Code/['World']['Global']['Framework']['ClientBaseModule'].ModuleScript.lua b/Code/['World']['Global']['Framework']['Client']['ClientBaseModule'].ModuleScript.lua similarity index 55% rename from Code/['World']['Global']['Framework']['ClientBaseModule'].ModuleScript.lua rename to Code/['World']['Global']['Framework']['Client']['ClientBaseModule'].ModuleScript.lua index 1deb376..f084843 100644 --- a/Code/['World']['Global']['Framework']['ClientBaseModule'].ModuleScript.lua +++ b/Code/['World']['Global']['Framework']['Client']['ClientBaseModule'].ModuleScript.lua @@ -10,9 +10,18 @@ end --- 加载的时候运行的代码 function ClientBase:InitDefault(_module) - -- print(string.format('[ClientBase][%s] InitDefault()', self.name)) + -- --print(string.format('[ClientBase][%s] InitDefault()', self.name)) -- 初始化默认监听事件 EventUtil.LinkConnects(localPlayer.C_Event, _module, self) end +--- Debug模式下打印日志 +-- self.debug 针对模块本身的debug开关 +-- FrameworkConfig.DebugMode 框架中的全局debug开关 +function ClientBase:Log(...) + if self.debug and FrameworkConfig.DebugMode then + --print(string.format('[%s]', self.name), ...) + end +end + return ClientBase diff --git a/Code/['World']['Global']['Framework']['Client']['ClientDataSyncModule'].ModuleScript.lua b/Code/['World']['Global']['Framework']['Client']['ClientDataSyncModule'].ModuleScript.lua new file mode 100644 index 0000000..1b1cd4e --- /dev/null +++ b/Code/['World']['Global']['Framework']['Client']['ClientDataSyncModule'].ModuleScript.lua @@ -0,0 +1,101 @@ +--- 游戏客户端数据同步 +--- @module Client Sync Data, Client-side +--- @copyright Lilith Games, Avatar Team +--- @author Yuancheng Zhang +local ClientDataSync = {} + +-- Localize global vars +local FrameworkConfig, MetaData = FrameworkConfig, MetaData + +-- 客户端私有数据 +local rawDataGlobal = {} +local rawDataPlayer = {} + +--- 打印数据同步日志 +local PrintLog = FrameworkConfig.DebugMode and FrameworkConfig.Debug.ShowDataSyncLog and function(...) + --print('[DataSync][Client]', ...) + end or function() + end + +--! 初始化 + +--- 数据初始化 +function ClientDataSync.Init() + --print('[DataSync][Client] Init()') + InitEventsAndListeners() + InitDataDefines() +end + +--- 初始化事件和绑定Handler +function InitEventsAndListeners() + if localPlayer.C_Event == nil then + world:CreateObject('FolderObject', 'S_Event', localPlayer) + end + + -- 数据同步事件 + world:CreateObject('CustomEvent', 'DataSyncS2CEvent', localPlayer.C_Event) + localPlayer.C_Event.DataSyncS2CEvent:Connect(DataSyncS2CEventHandler) + + -- 长期存储成功事件 + if not localPlayer.C_Event.LoadPlayerDataSuccessEvent then + world:CreateObject('CustomEvent', 'LoadPlayerDataSuccessEvent', localPlayer.C_Event) + end +end + +--- 校验数据定义 +function InitDataDefines() + --* 客户端全局数据 + Data.Global = Data.Global or MetaData.New(rawDataGlobal, MetaData.Enum.GLOBAL, MetaData.Enum.CLIENT) + -- 默认赋值 + for k, v in pairs(Data.Default.Global) do + Data.Global[k] = v + end + + --* 客户端玩家数据 + local uid = localPlayer.UserId + local path = MetaData.Enum.PLAYER .. uid + Data.Player = Data.Player or MetaData.New(rawDataPlayer, path, uid) + -- 默认赋值 + for k, v in pairs(Data.Default.Player) do + Data.Player[k] = v + end +end + +--- 开始同步 +function ClientDataSync.Start() + --print('[DataSync][Client] 客户端数据同步开启') + MetaData.ClientSync = true +end + +--! Event handler + +--- 数据同步事件Handler +function DataSyncS2CEventHandler(_path, _value) + if not MetaData.ClientSync then + return + end + + PrintLog(string.format('收到 _path = %s, _value = %s', _path, table.dump(_value))) + + local uid = localPlayer.UserId + + --* 收到服务器数据 + if string.startswith(_path, MetaData.Enum.GLOBAL) then + --* Data.Global 全局数据 + MetaData.Set(rawDataGlobal, _path, _value, uid, false) + elseif string.startswith(_path, MetaData.Enum.PLAYER .. uid) then + --* Data.Player 玩家数据 + MetaData.Set(rawDataPlayer, _path, _value, uid, false) + else + error( + string.format( + '[DataSync][Client] _path错误 _player = %s, _path = %s, _value = %s', + localPlayer, + _path, + table.dump(_value) + ) + ) + end +end + +return ClientDataSync diff --git a/Code/['World']['Global']['Framework']['ClientHeartbeatModule'].ModuleScript.lua b/Code/['World']['Global']['Framework']['Client']['ClientHeartbeatModule'].ModuleScript.lua similarity index 83% rename from Code/['World']['Global']['Framework']['ClientHeartbeatModule'].ModuleScript.lua rename to Code/['World']['Global']['Framework']['Client']['ClientHeartbeatModule'].ModuleScript.lua index 07f4be4..da70480 100644 --- a/Code/['World']['Global']['Framework']['ClientHeartbeatModule'].ModuleScript.lua +++ b/Code/['World']['Global']['Framework']['Client']['ClientHeartbeatModule'].ModuleScript.lua @@ -5,17 +5,17 @@ local ClientHeartbeat = {} -- Localize global vars -local Setting = FrameworkConfig.Client +local FrameworkConfig = FrameworkConfig -- 心跳包间隔时间,单位:秒 -local HEARTBEAT_DELTA = Setting.HeartbeatDelta +local HEARTBEAT_DELTA = FrameworkConfig.Client.HeartbeatDelta -- 心跳阈值,单位:秒,范围定义如下: -- 0s -> threshold_1 : connected -- threshold_1 -> threshold_2 : disconnected, weak network -- threshold_2 -> longer : disconnected, quit server -local HEARTBEAT_THRESHOLD_1 = Setting.HeartbeatThreshold1 * 1000 -- second => ms -local HEARTBEAT_THRESHOLD_2 = Setting.HeartbeatThreshold2 * 1000 -- second => ms +local HEARTBEAT_THRESHOLD_1 = FrameworkConfig.Client.HeartbeatThreshold1 * 1000 -- second => ms +local HEARTBEAT_THRESHOLD_2 = FrameworkConfig.Client.HeartbeatThreshold2 * 1000 -- second => ms -- 玩家心跳连接状态 local HeartbeatEnum = { @@ -37,8 +37,8 @@ local diff -- 时间戳插值 local sTmpTs, cTmpTs -- 时间戳缓存 --- 打印心跳日志 -local PrintHb = Setting.ShowHeartbeatLog and function(...) - print('[Heartbeat][Client]', ...) +local PrintHb = FrameworkConfig.DebugMode and FrameworkConfig.Debug.ShowHeartbeatLog and function(...) + --print('[Heartbeat][Client]', ...) end or function() end @@ -46,14 +46,14 @@ local PrintHb = Setting.ShowHeartbeatLog and function(...) --- 初始化心跳包 function ClientHeartbeat.Init() - print('[Heartbeat][Client] Init()') + --print('[Heartbeat][Client] Init()') CheckSetting() InitEventsAndListeners() end --- 开始发出心跳 function ClientHeartbeat.Start() - print('[Heartbeat][Client] Start()') + --print('[Heartbeat][Client] Start()') local cTimestamp running = true while (running) do @@ -64,7 +64,7 @@ end -- 停止心跳 function ClientHeartbeat.Stop() - print('[Heartbeat][Client] Stop()') + --print('[Heartbeat][Client] Stop()') running = false end @@ -127,12 +127,12 @@ end function CheckPlayerJoin(_player, _sTimestamp) if not cache.sTimestamp then --* 玩家新加入 OnPlayerJoinEvent - print('[Heartbeat][Client] OnPlayerJoinEvent, 新玩家加入,', localPlayer) + --print('[Heartbeat][Client] OnPlayerJoinEvent, 新玩家加入,', localPlayer, localPlayer.UserId) NetUtil.Fire_C('OnPlayerJoinEvent', localPlayer) cache.state = HeartbeatEnum.CONNECT elseif cache.state == HeartbeatEnum.DISCONNECT then --* 玩家断线重连 OnPlayerReconnectEvent - print('[Heartbeat][Client] OnPlayerReconnectEvent, 玩家断线重连,', localPlayer) + --print('[Heartbeat][Client] OnPlayerReconnectEvent, 玩家断线重连,', localPlayer, localPlayer.UserId) NetUtil.Fire_C('OnPlayerReconnectEvent', localPlayer) cache.state = HeartbeatEnum.CONNECT end @@ -150,7 +150,7 @@ function CheckPlayerState(_player, _cTimestamp) cache.state = HeartbeatEnum.CONNECT elseif cache.state == HeartbeatEnum.CONNECT and diff >= HEARTBEAT_THRESHOLD_1 then --* 玩家断线,弱网环境 - print('[Heartbeat][Client] OnPlayerDisconnectEvent, 玩家离线, 弱网环境,', localPlayer) + --print('[Heartbeat][Client] OnPlayerDisconnectEvent, 玩家离线, 弱网环境,', localPlayer) NetUtil.Fire_C('OnPlayerDisconnectEvent', localPlayer) cache.state = HeartbeatEnum.DISCONNECT elseif cache.state == HeartbeatEnum.DISCONNECT and diff >= HEARTBEAT_THRESHOLD_2 then @@ -162,7 +162,7 @@ end --- 退出游戏 function QuitGame() - print('[Heartbeat][Client] Game.Quit(), 玩家退出游戏') + --print('[Heartbeat][Client] Game.Quit(), 玩家退出游戏', localPlayer, localPlayer.UserId) Game.Quit() end diff --git a/Code/['World']['Global']['Framework']['ClientModule'].ModuleScript.lua b/Code/['World']['Global']['Framework']['Client']['ClientModule'].ModuleScript.lua similarity index 82% rename from Code/['World']['Global']['Framework']['ClientModule'].ModuleScript.lua rename to Code/['World']['Global']['Framework']['Client']['ClientModule'].ModuleScript.lua index fc59abd..eaccec4 100644 --- a/Code/['World']['Global']['Framework']['ClientModule'].ModuleScript.lua +++ b/Code/['World']['Global']['Framework']['Client']['ClientModule'].ModuleScript.lua @@ -16,14 +16,14 @@ local initDefaultList, initList, updateList = {}, {}, {} --- 运行客户端 function Client:Run() - print('[Client] Run()') + --print('[Client] Run()') InitClient() StartUpdate() end --- 停止Update function Client:Stop() - print('[Client] Stop()') + --print('[Client] Stop()') running = false ClientHeartbeat.Stop() end @@ -33,11 +33,11 @@ function InitClient() if initialized then return end - print('[Client] InitClient()') + --print('[Client] InitClient()') InitRandomSeed() InitHeartbeat() + InitDataSync() InitClientCustomEvents() - PreloadCsv() GenInitAndUpdateList() RunInitDefault() InitOtherModules() @@ -50,6 +50,12 @@ function InitHeartbeat() ClientHeartbeat.Init() end +--- 初始化数据同步 +function InitDataSync() + assert(ClientDataSync, '[Server][DataSync] 找不到ClientDataSync,请联系张远程') + ClientDataSync.Init() +end + --- 初始化客户端的CustomEvent function InitClientCustomEvents() if localPlayer.C_Event == nil then @@ -77,9 +83,18 @@ end --- 生成需要Init和Update的模块列表 function GenInitAndUpdateList() + -- TODO: 改成在FrameworkConfig中配置 + -- Init Default + ModuleUtil.GetModuleListWithFunc(Module.UI_Module, 'InitDefault', initDefaultList) ModuleUtil.GetModuleListWithFunc(Module.C_Module, 'InitDefault', initDefaultList) + -- Init + ModuleUtil.GetModuleListWithFunc(Define, 'Init', initList) + ModuleUtil.GetModuleListWithFunc(Module.UI_Module, 'Init', initList) ModuleUtil.GetModuleListWithFunc(Module.C_Module, 'Init', initList) + -- Update + ModuleUtil.GetModuleListWithFunc(Module.UI_Module, 'Update', updateList) ModuleUtil.GetModuleListWithFunc(Module.C_Module, 'Update', updateList) + -- Plugin for _, m in pairs(Config.PluginModules) do ModuleUtil.GetModuleListWithFunc(m, 'InitDefault', initDefaultList) ModuleUtil.GetModuleListWithFunc(m, 'Init', initList) @@ -99,14 +114,6 @@ function InitRandomSeed() math.randomseed(os.time()) end ---- 预加载所有的CSV表格 -function PreloadCsv() - print('[Client] PreloadCsv()') - if Config.ClientPreload and #Config.ClientPreload > 0 then - CsvUtil.PreloadCsv(Config.ClientPreload, Csv, Config) - end -end - --- 初始化包含Init()方法的模块 function InitOtherModules() for _, m in ipairs(initList) do @@ -116,7 +123,7 @@ end --- 开始Update function StartUpdate() - print('[Client] StartUpdate()') + --print('[Client] StartUpdate()') assert(not running, '[Client] StartUpdate() 正在运行') running = true @@ -126,6 +133,9 @@ function StartUpdate() invoke(ClientHeartbeat.Start) end + -- 开启数据同步 + ClientDataSync.Start() + local dt = 0 -- delta time 每帧时间 local tt = 0 -- total time 游戏总时间 local now = Timer.GetTimeMillisecond --时间函数缓存 diff --git a/Code/['World']['Global']['Framework']['DataModule'].ModuleScript.lua b/Code/['World']['Global']['Framework']['DataModule'].ModuleScript.lua new file mode 100644 index 0000000..621752a --- /dev/null +++ b/Code/['World']['Global']['Framework']['DataModule'].ModuleScript.lua @@ -0,0 +1,18 @@ +--- 游戏数据 +--- @module Game Data, Both-side +--- @copyright Lilith Games, Avatar Team +--- @author Yuancheng Zhang +local Data = {} + +-- 客户端 +-- 1. Data.Global +-- 2. Data.Player + +-- 服务器 +-- 1. Data.Global +-- 2. Data.Players + +--! 这个Module为空 +--! 数据定义在:ClientDataSyncModule、ServerDataSyncModule + +return Data diff --git a/Code/['World']['Global']['Framework']['FrameworkConfigModule'].ModuleScript.lua b/Code/['World']['Global']['Framework']['FrameworkConfigModule'].ModuleScript.lua index f0a515c..5e2b5a6 100644 --- a/Code/['World']['Global']['Framework']['FrameworkConfigModule'].ModuleScript.lua +++ b/Code/['World']['Global']['Framework']['FrameworkConfigModule'].ModuleScript.lua @@ -3,8 +3,15 @@ --- @copyright Lilith Games, Avatar Team --- @author Yuancheng Zhang local FrameworkConfig = { + --! Debug模式 + DebugMode = true, -- 启动心跳 HeartbeatStart = true, + -- 长期存储:玩家数据定时保存时间间隔(秒) + DatabaseAutoSaveTime = 30, + -- 长期存储:重新读取游戏数据时间间隔(秒) + DatabaseReloadTimeAfterFailed = 1, + -- 服务器配置 Server = { -- 心跳包间隔时间,单位:秒 HeartbeatDelta = 1, @@ -14,13 +21,12 @@ local FrameworkConfig = { -- threshold_2 -> longer : disconnected, remove player HeartbeatThreshold1 = 5, HeartbeatThreshold2 = 10, - -- 显示心跳日志 - ShowHeartbeatLog = false, -- 插件中需要使用声明周期的服务器模块目录 PluginModules = {}, -- 插件中服务器需要生成的CustomEvent, 模块中必须得有ServerEvents PluginEvents = {} }, + -- 客户端配置 Client = { -- 心跳包间隔时间,单位:秒 HeartbeatDelta = 1, @@ -30,12 +36,17 @@ local FrameworkConfig = { -- threshold_2 -> longer : disconnected, quit server HeartbeatThreshold1 = 5, HeartbeatThreshold2 = 10, - -- 显示心跳日志 - ShowHeartbeatLog = false, -- 插件中需要使用声明周期的客户端模块目录 PluginModules = {}, -- 插件中客户端需要生成的CustomEvent,模块中必须得有ClientEvents PluginEvents = {} + }, + --! Debug相关 + Debug = { + -- 显示心跳日志 + ShowHeartbeatLog = false, + -- 显示数据同步日志 + ShowDataSyncLog = false } } diff --git a/Code/['World']['Global']['Framework']['MetaDataModule'].ModuleScript.lua b/Code/['World']['Global']['Framework']['MetaDataModule'].ModuleScript.lua new file mode 100644 index 0000000..7a40ea6 --- /dev/null +++ b/Code/['World']['Global']['Framework']['MetaDataModule'].ModuleScript.lua @@ -0,0 +1,274 @@ +--- 游戏同步数据基类 +--- @module Sync Data Base, Both-side +--- @copyright Lilith Games, Avatar Team +--- @author Yuancheng Zhang +local MetaData = {} + +-- Localize global vars +local FrameworkConfig = FrameworkConfig + +--* 开关:Debug模式,开启后会打印日志 +local debugMode = false +--* 开关:数据校验 +local valid = true + +-- enum +MetaData.Enum = {} +-- 数据类型:全局 or 玩家 +MetaData.Enum.GLOBAL = 'Global' +MetaData.Enum.PLAYER = 'Player' + +-- 是否进行同步,数据初始化之后在开启同步 +MetaData.ServerSync = false +MetaData.ClientSync = false + +--! 说明:两种双向同步机制 +--* 1. Data.Global +-- a. 客户端和服务器持有相同的数据类型 Data.Global +-- b. C=>S,某一客户端更新,自动发送给服务器,服务器更新,然后再同步给全部客户端 +-- c. S=>C,服务器更新,广播给所有客户端,客户端各自更新 +--* 2. Data.Player +-- a. 客户端只持有自己的 Data.Player +-- b. 服务器持有全部玩家的 Data.Players +-- c. C=>S,客户端更新,自动发送给服务器,服务器更新对应玩家数据 +-- d. S=>C,服务器更新,自动发送给对应客户端,客户端更新玩家数据 + +--! 私有方法 + +--- 新建一个MetaData的proxy,用于数据同步 +-- @param _data 真实数据 +-- @param _path 当前节点索引路径 +-- @param _uid UserId +-- @return proxy 代理table,没有data,元表内包含方法和path +function NewData(_data, _path, _uid) + local proxy = {} + local mt = { + _data = _data, + _path = _path, + _uid = _uid, + __index = function(_t, _k) + local mt = getmetatable(_t) + local newpath = mt._path .. '.' .. _k + PrintLog('__index,', '_k = ', _k, ', _path = ', mt._path, ', newpath = ', newpath) + return _data[newpath] + end, + __newindex = function(_t, _k, _v) + local mt = getmetatable(_t) + local newpath = mt._path .. '.' .. _k + PrintLog('__newindex,', '_k =', _k, ', _v =', _v, ', _path = ', mt._path, ', newpath = ', newpath) + SetData(_data, newpath, _v, _uid, true) + end, + __pairs = function() + -- pairs()需要返回三个参数:next, _t, nil + -- https://www.lua.org/pil/7.3.html + -- 得到rd(raw data),从rd中进行遍历 + local rd = GetData(_data, _path) + return next, rd, nil + end + } + setmetatable(proxy, mt) + return proxy +end + +--- 获得原始数据 +-- @param _data 真实数据的存储位置 +-- @param _path 当前节点索引路径 +-- @return rawData 纯数据table,不包含元表 +function GetData(_data, _path) + local rawData = {} + GetDataAux(_data, _path, rawData) + return rawData +end + +--- GetData的辅助函数 +-- @param _data 真实数据的存储位置 +-- @param _path 当前节点索引路径 +-- @param _rawData 纯数据table,不包含元表 +function GetDataAux(_data, _path, _rawData) + local key, i + local q, elem = Queue:New(), {} + elem.path = _path + elem.rd = _rawData + q:Enqueue(elem) + while not q:IsEmpty() do + elem = q:Dequeue() + for k, v in pairs(_data) do + i = string.find(k, elem.path .. '.') + -- 筛选出当前直接层级的path,剪裁后作为rawData的key + if i == 1 and #elem.path < #k then + key = string.sub(k, #elem.path + 2, #k) + if not string.find(key, '%.') then + key = tonumber(key) or key + if type(v) == 'table' then + elem.rd[key] = {} + q:Enqueue( + { + path = k, + rd = elem.rd[key] + } + ) + else + elem.rd[key] = v + end + end + end + end + end +end + +--- 设置原始数据 +-- @param _data 真实数据的存储位置 +-- @param _path 当前节点索引路径 +-- @param _value 传入的数据 +-- @param _uid UserId +-- @param _sync true:同步数据 +function SetData(_data, _path, _value, _uid, _sync) + --* 数据同步:赋值的时候只要同步一次就可以的,存下newpath和_v,对方收到后赋值即可 + if _sync and (MetaData.ServerSync or MetaData.ClientSync) then + SyncData(_path, _value, _uid) + end + + local args, newpath = {} + + local q = Queue:New() + q:Enqueue({_data, _path, _value, _uid, _sync}) + + while not q:IsEmpty() do + _data, _path, _value, _uid, _sync = table.unpack(q:Dequeue()) + + --* 数据校验 + Validators(SetData)(_data, _path, _value, _uid, _sync) + + --* 检查现有数据 + if type(_data[_path]) == 'table' then + -- TODO: 这里可以优化,不必要每次都删除 + -- 如果现有数据是个table,删除所有子数据 + for k, _ in pairs(_data[_path]) do + -- 同等于 _data[_path][k] = nil,但是不同步 + newpath = _path .. '.' .. k + q:Enqueue({_data, newpath, nil, _uid, false}) + end + end + + --* 检查新数据 + if type(_value) == 'table' then + -- 若新数据是table,建立一个mt + _data[_path] = NewData(_data, _path, _uid) + for k, v in pairs(_value) do + -- 同等于 _data[_path][k] = v,但是不同步 + newpath = _path .. '.' .. k + q:Enqueue({_data, newpath, v, _uid, false}) + end + else + -- 一般数据,直接赋值 + _data[_path] = _value + end + end +end + +--- 数据同步 +-- @param _path 当前节点索引路径 +-- @param _value 传入的数据 +-- @param _uid UserId +function SyncData(_path, _value, _uid) + if MetaData.ServerSync and MetaData.ClientSync and localPlayer then + -- 服务器/客户端 同虚拟机 + local player = world:GetPlayerByUserId(_uid) + assert(player == localPlayer, string.format('[MetaData] 玩家不存在 uid = %s', _uid)) + PrintLog(string.format('[Server] 发出 player = %s, _path = %s, _value = %s', player, _path, table.dump(_value))) + NetUtil.Fire_C('DataSyncS2CEvent', player, _path, _value) + NetUtil.Fire_S('DataSyncC2SEvent', localPlayer, _path, _value) + elseif localPlayer == nil and string.isnilorempty(_uid) and MetaData.ServerSync then + -- 服务器 => 客户端,Global 全局数据 + NetUtil.Broadcast('DataSyncS2CEvent', _path, _value) + elseif localPlayer == nil and MetaData.ServerSync then + -- 服务器 => 客户端,Player 玩家数据 + local player = world:GetPlayerByUserId(_uid) + assert(player, string.format('[MetaData] 玩家不存在 uid = %s', _uid)) + PrintLog(string.format('[Server] 发出 player = %s, _path = %s, _value = %s', player, _path, table.dump(_value))) + NetUtil.Fire_C('DataSyncS2CEvent', player, _path, _value) + elseif localPlayer and localPlayer.UserId == _uid and MetaData.ClientSync then + -- 客户端 => 服务器 + PrintLog( + string.format('[Client] 发出 player = %s, _path = %s, _value = %s', localPlayer, _path, table.dump(_value)) + ) + NetUtil.Fire_S('DataSyncC2SEvent', localPlayer, _path, _value) + end +end + +--! 公开API + +--- 新建数据 +MetaData.New = NewData + +--- 设置数据 +MetaData.Set = SetData + +--- 从proxy中生成一个纯数据表格 +MetaData.Get = function(_proxy) + local mt = getmetatable(_proxy) + assert(mt, string.format('[MetaData] metatable为空,proxy = %s', table.dump(_proxy))) + return GetData(mt._data, mt._path) +end + +--! 辅助方法 + +--- 打印数据同步日志 +PrintLog = FrameworkConfig.DebugMode and debugMode and function(...) + --print('[MetaData]', ...) + end or function() + end + +-- 数据校验 +function Validators(func) + if not valid then + return function() + end + end + + if func == SetData then + return function(_data, _path, _value, _uid, _sync) + assert( + _data, + string.format( + '[MetaData] data为空 data = %s, path = %s, uid = %s, sync = %s, value = %s', + _data, + _path, + _uid, + _sync, + table.dump(_value) + ) + ) + assert( + not string.isnilorempty(_path), + string.format( + '[MetaData] path为空 data = %s, path = %s, uid = %s, sync = %s, value = %s', + _data, + _path, + _uid, + _sync, + table.dump(_value) + ) + ) + end + end +end + +return MetaData + +--! Command Test only +--[[ +Data.Global.a = 11 +Data.Global.b = {22, 33} +Data.Global.c = {c1 = {44, 55}, c2 = 66} +Data.Global.c.c3 = {c4 = 77} +Data.Global.d = {'88', Vector3(9,9,9)} +--print(table.dump(Data.Global)) +--print(table.dump(MetaData.Get(Data.Global))) + +--print(table.dump(Data.Player)) + +--print(table.dump(Data.Players)) + +--print(table.dump(Data.Players['pid:local_1'])) +]] diff --git a/Code/['World']['Global']['Framework']['ServerBaseModule'].ModuleScript.lua b/Code/['World']['Global']['Framework']['Server']['ServerBaseModule'].ModuleScript.lua similarity index 54% rename from Code/['World']['Global']['Framework']['ServerBaseModule'].ModuleScript.lua rename to Code/['World']['Global']['Framework']['Server']['ServerBaseModule'].ModuleScript.lua index 213f46e..bec7b32 100644 --- a/Code/['World']['Global']['Framework']['ServerBaseModule'].ModuleScript.lua +++ b/Code/['World']['Global']['Framework']['Server']['ServerBaseModule'].ModuleScript.lua @@ -10,9 +10,18 @@ end --- 加载的时候运行的代码 function ServerBase:InitDefault(_module) - -- print(string.format('[ServerBase][%s] InitDefault()', self.name)) + -- --print(string.format('[ServerBase][%s] InitDefault()', self.name)) -- 初始化默认监听事件 EventUtil.LinkConnects(world.S_Event, _module, self) end +--- Debug模式下打印日志 +-- self.debug 针对模块本身的debug开关 +-- FrameworkConfig.DebugMode 框架中的全局debug开关 +function ServerBase:Log(...) + if self.debug and FrameworkConfig.DebugMode then + --print(string.format('[%s]', self.name), ...) + end +end + return ServerBase diff --git a/Code/['World']['Global']['Framework']['Server']['ServerDataSyncModule'].ModuleScript.lua b/Code/['World']['Global']['Framework']['Server']['ServerDataSyncModule'].ModuleScript.lua new file mode 100644 index 0000000..69fe9a1 --- /dev/null +++ b/Code/['World']['Global']['Framework']['Server']['ServerDataSyncModule'].ModuleScript.lua @@ -0,0 +1,285 @@ +--- 游戏服务器数据同步 +--- @module Server Sync Data, Server-side +--- @copyright Lilith Games, Avatar Team +--- @author Yuancheng Zhang +local ServerDataSync = {} + +-- Localize global vars +local FrameworkConfig, MetaData, DataStore = FrameworkConfig, MetaData, DataStore + +-- 服务器端私有数据 +local rawDataGlobal = {} +local rawDataPlayers = {} + +-- 玩家数据定时保存时间间隔(秒) +local AUTO_SAVE_TIME = FrameworkConfig.DatabaseAutoSaveTime +-- 重新读取游戏数据时间间隔(秒) +local RELOAD_TIME = 1 + +-- 玩家数据表格 +local sheet + +--- 打印数据同步日志 +local PrintLog = FrameworkConfig.DebugMode and FrameworkConfig.Debug.ShowDataSyncLog and function(...) + --print('[DataSync][Server]', ...) + end or function() + end + +--! 初始化 + +--- 数据初始化 +function ServerDataSync.Init() + --print('[DataSync][Server] Init()') + InitEventsAndListeners() + InitDefines() + sheet = DataStore:GetSheet('PlayerData') +end + +--- 初始化事件和绑定Handler +function InitEventsAndListeners() + if world.S_Event == nil then + world:CreateObject('FolderObject', 'S_Event', world) + end + + -- 数据同步事件 + world:CreateObject('CustomEvent', 'DataSyncC2SEvent', world.S_Event) + world.S_Event.DataSyncC2SEvent:Connect(DataSyncC2SEventHandler) + + -- 玩家加入事件 + local onPlayerJoinEvent = world.S_Event.OnPlayerJoinEvent + assert(onPlayerJoinEvent, '[DataSync][Server] 不存在 OnPlayerJoinEvent') + onPlayerJoinEvent:Connect(OnPlayerJoinEventHandler) + + -- 玩家离开事件 + local onPlayerLeaveEvent = world.S_Event.OnPlayerLeaveEvent + assert(onPlayerLeaveEvent, '[DataSync][Server] 不存在 OnPlayerLeaveEvent') + onPlayerLeaveEvent:Connect(OnPlayerLeaveEventHandler) + + -- 长期存储成功事件 + if not world.S_Event.LoadPlayerDataSuccessEvent then + world:CreateObject('CustomEvent', 'LoadPlayerDataSuccessEvent', world.S_Event) + end +end + +--- 校验数据定义 +function InitDefines() + --* 服务器全局数据 + InitDataGlobal() + + --* 服务器玩家数据, key是uid + Data.Players = {} +end + +--- 初始化Data.Global +function InitDataGlobal() + --* 服务器全局数据 + Data.Global = Data.Global or MetaData.New(rawDataGlobal, MetaData.Enum.GLOBAL, nil) + -- 默认赋值 + for k, v in pairs(Data.Default.Global) do + Data.Global[k] = v + end +end + +--- 初始化Data.Players中对应玩家数据 +function InitDataPlayer(_uid) + assert(not string.isnilorempty(_uid)) + --* 服务器端创建Data.Player + local path = MetaData.Enum.PLAYER .. _uid + rawDataPlayers[_uid] = {} + Data.Players[_uid] = MetaData.New(rawDataPlayers[_uid], path, _uid) + + -- 默认赋值 + for k, v in pairs(Data.Default.Player) do + Data.Players[_uid][k] = v + end + + -- 设置uid + Data.Players[_uid].uid = _uid +end + +--- 开始同步 +function ServerDataSync.Start() + --print('[DataSync][Server] 服务器数据同步开启') + MetaData.ServerSync = true + + -- 启动定时器 + TimeUtil.SetInterval(SaveAllGameDataAsync, AUTO_SAVE_TIME) +end + +--! 长期存储:读取 + +--- 下载玩家的游戏数据 +--- @param _uid string 玩家ID +function LoadGameDataAsync(_uid) + sheet = DataStore:GetSheet('PlayerData') + assert(sheet, '[DataSync][Server] DataPlayers的sheet不存在') + sheet:GetValue( + _uid, + function(_val, _msg) + LoadGameDataAsyncCb(_val, _msg, _uid) + end + ) +end + +--- 下载玩家的游戏数据回调 +--- @param _val table 数据 +--- @param _msg int 消息码 +--- @param _uid string 玩家ID +function LoadGameDataAsyncCb(_val, _msg, _uid) + local player = world:GetPlayerByUserId(_uid) + assert(player, string.format('[DataSync][Server] 玩家不存在, uid = %s', _uid)) + if _msg == 0 or _msg == 101 then + --print('[DataSync][Server] 获取玩家数据成功', player.Name) + local hasData = _val ~= nil + if hasData then + --print('[DataSync][Server] 玩家数据,存在', player.Name) + --若以前的数据存在,更新 + -- TODO: 数据兼容的处理 + local data = _val + assert(data.uid == _uid, string.format('[DataSync][Server] uid校验不通过, uid = %s', _uid)) + --若已在此服务器的数据总表存在,则更新数据 + for k, v in pairs(data) do + Data.Players[_uid][k] = data[k] + end + else + -- 不存在数据,用之前生成的默认数据 + --print('[DataSync][Server] 玩家数据,不存在', player.Name) + end + NetUtil.Fire_S('LoadPlayerDataSuccessEvent', player, hasData) + NetUtil.Fire_C('LoadPlayerDataSuccessEvent', player, hasData) + return + end + print( + string.format( + '[DataSync][Server] 获取玩家数据失败,%s秒后重试, uid = %s, player = %s, msg = %s', + RELOAD_TIME, + _uid, + player.Name, + _msg + ) + ) + --若失败,则1秒后重新再读取一次 + invoke( + function() + LoadGameDataAsync(_uid) + end, + RELOAD_TIME + ) +end + +--! 长期存储:保存 + +--- 上传玩家的游戏数据 +--- @param _userId string 玩家ID +--- @param _delete string 保存成功后是否删除缓存数据 +function SaveGameDataAsync(_uid, _delete) + sheet = DataStore:GetSheet('PlayerData') + assert(sheet, '[DataSync][Server] DataPlayers的sheet不存在') + assert(not string.isnilorempty(_uid), '[DataSync][Server] uid不存在或为空') + assert(Data.Players[_uid], string.format('[DataSync][Server] Data.Players[_uid]不存在 uid = %s', _uid)) + local newData = MetaData.Get(Data.Players[_uid]) + assert(newData, string.format('[DataSync][Server] 玩家数据不存在, uid = %s', _uid)) + assert(newData.uid == _uid, string.format('[DataSync][Server] uid校验不通过, uid = %s', _uid)) + sheet:SetValue( + _uid, + newData, + function(_val, _msg) + SaveGameDataAsyncCb(_val, _msg, _uid, _delete) + end + ) +end + +--- 上传玩家的游戏数据回调 +--- @param _val table 数据 +--- @param _msg int 消息码 +--- @param _uid string 玩家ID +function SaveGameDataAsyncCb(_val, _msg, _uid, _delete) + -- 保存成功 + if _msg == 0 then + --print('[DataSync][Server] 保存玩家数据,成功', _uid) + if _delete == true then + --print('[DataSync][Server] 删除服务器玩家数据', _uid) + rawDataPlayers[_uid] = nil + --* 删除玩家端数据 + Data.Players[_uid] = nil + end + return + end + + -- 保存失败 + --print(string.format('[DataSync][Server] 保存玩家数据失败,%s秒后重试, uid = %s, msg = %s', RELOAD_TIME, _uid, _msg)) + --若失败,则1秒后重新再读取一次 + invoke( + function() + SaveGameDataAsync(_uid, _delete) + end, + RELOAD_TIME + ) +end + +--- 存储全部玩家数据 +function SaveAllGameDataAsync() + if not MetaData.ServerSync then + --print('[DataSync][Server] ServerSync未开始') + return + end + --print('[DataSync][Server] 尝试保存全部玩家数据……') + for uid, data in pairs(Data.Players) do + if not string.isnilorempty(uid) and data then + SaveGameDataAsync(uid, false) + end + end +end + +--! Event handler + +--- 数据同步事件Handler +function DataSyncC2SEventHandler(_player, _path, _value) + if not MetaData.ServerSync then + return + end + + PrintLog(string.format('收到 player = %s, _path = %s, _value = %s', _player, _path, table.dump(_value))) + + local uid = _player.UserId + + if string.startswith(_path, MetaData.Enum.GLOBAL) then + --* Data.Global:收到客户端改变数据的时候需要同步给其他玩家 + MetaData.Set(rawDataGlobal, _path, _value, nil, true) + elseif string.startswith(_path, MetaData.Enum.PLAYER .. uid) then + --* Data.Players + MetaData.Set(rawDataPlayers[uid], _path, _value, uid, false) + else + error( + string.format( + '[DataSync][Server] _path错误 _player = %s, _path = %s, _value = %s', + _player, + _path, + table.dump(_data) + ) + ) + end +end + +--- 新玩家加入事件Handler +function OnPlayerJoinEventHandler(_player) + --print('[DataSync][Server] OnPlayerJoinEventHandler', _player, _player.UserId) + --* 向客户端同步Data.Global + NetUtil.Fire_C('DataSyncS2CEvent', _player, MetaData.Enum.GLOBAL, MetaData.Get(Data.Global)) + + local uid = _player.UserId + InitDataPlayer(uid) + + --* 获取长期存储,成功后向客户端同步 + LoadGameDataAsync(uid) +end + +--- 玩家离开事件Handler +function OnPlayerLeaveEventHandler(_player, _uid) + --print('[DataSync][Server] OnPlayerLeaveEventHandler', _player, _uid) + assert(not string.isnilorempty(_uid), '[ServerDataSync] OnPlayerLeaveEventHandler() uid不存在') + --* 保存长期存储:rawDataPlayers[_uid] 保存成功后删掉 + SaveGameDataAsync(_uid, true) +end + +return ServerDataSync diff --git a/Code/['World']['Global']['Framework']['ServerHeartbeatModule'].ModuleScript.lua b/Code/['World']['Global']['Framework']['Server']['ServerHeartbeatModule'].ModuleScript.lua similarity index 75% rename from Code/['World']['Global']['Framework']['ServerHeartbeatModule'].ModuleScript.lua rename to Code/['World']['Global']['Framework']['Server']['ServerHeartbeatModule'].ModuleScript.lua index d79c0bf..d0e0f5b 100644 --- a/Code/['World']['Global']['Framework']['ServerHeartbeatModule'].ModuleScript.lua +++ b/Code/['World']['Global']['Framework']['Server']['ServerHeartbeatModule'].ModuleScript.lua @@ -5,17 +5,17 @@ local ServerHeartbeat = {} -- Localize global vars -local Setting = FrameworkConfig.Server +local FrameworkConfig = FrameworkConfig -- 心跳包间隔时间,单位:秒 -local HEARTBEAT_DELTA = Setting.HeartbeatDelta +local HEARTBEAT_DELTA = FrameworkConfig.Server.HeartbeatDelta -- 心跳阈值,单位:秒,范围定义如下: -- 0s -> threshold_1 : connected -- threshold_1 -> threshold_2 : disconnected, but player can rejoin -- threshold_2 -> longer : disconnected, remove player -local HEARTBEAT_THRESHOLD_1 = Setting.HeartbeatThreshold1 * 1000 -- second => ms -local HEARTBEAT_THRESHOLD_2 = Setting.HeartbeatThreshold2 * 1000 -- second => ms +local HEARTBEAT_THRESHOLD_1 = FrameworkConfig.Server.HeartbeatThreshold1 * 1000 -- second => ms +local HEARTBEAT_THRESHOLD_2 = FrameworkConfig.Server.HeartbeatThreshold2 * 1000 -- second => ms -- 玩家心跳连接状态 local HeartbeatEnum = { @@ -34,8 +34,8 @@ local diff -- 时间戳插值 local sTmpTs, cTmpTs -- 时间戳缓存 --- 打印心跳日志 -local PrintHb = Setting.ShowHeartbeatLog and function(...) - print('[Heartbeat][Server]', ...) +local PrintHb = FrameworkConfig.DebugMode and FrameworkConfig.Debug.ShowHeartbeatLog and function(...) + --print('[Heartbeat][Server]', ...) end or function() end @@ -43,14 +43,14 @@ local PrintHb = Setting.ShowHeartbeatLog and function(...) --- 初始化心跳包 function ServerHeartbeat.Init() - print('[Heartbeat][Server] Init()') + --print('[Heartbeat][Server] Init()') CheckSetting() InitEventsAndListeners() end --- 开始发出心跳 function ServerHeartbeat.Start() - print('[Heartbeat][Server] Start()') + --print('[Heartbeat][Server] Start()') running = true while (running) do Update() @@ -60,7 +60,7 @@ end --- 停止心跳 function ServerHeartbeat.Stop() - print('[Heartbeat][Server] Stop()') + --print('[Heartbeat][Server] Stop()') running = false end @@ -100,9 +100,12 @@ function InitEventsAndListeners() -- 玩家退出,发出OnPlayerLeaveEvent world.OnPlayerRemoved:Connect( function(_player) - if cache[_player] then - print('[Heartbeat][Server] OnPlayerLeaveEvent, 玩家主动离开游戏,', _player) - NetUtil.Fire_S('OnPlayerLeaveEvent', _player) + local player = _player + local uid = player.UserId + if cache[player] then + --print('[Heartbeat][Server] OnPlayerLeaveEvent, 玩家主动离开游戏,', player, uid) + NetUtil.Fire_S('OnPlayerLeaveEvent', player, uid) + cache[player] = nil end end ) @@ -139,14 +142,15 @@ end function CheckPlayerJoin(_player) if not cache[_player] then --* 玩家新加入 OnPlayerJoinEvent - print('[Heartbeat][Server] OnPlayerJoinEvent, 新玩家加入,', _player) + --print('[Heartbeat][Server] OnPlayerJoinEvent, 新玩家加入,', _player) NetUtil.Fire_S('OnPlayerJoinEvent', _player) + print('已经广播了') cache[_player] = { state = HeartbeatEnum.CONNECT } elseif cache[_player].state == HeartbeatEnum.DISCONNECT then --* 玩家断线重连 OnPlayerReconnectEvent - print('[Heartbeat][Server] OnPlayerReconnectEvent, 玩家断线重连,', _player) + --print('[Heartbeat][Server] OnPlayerReconnectEvent, 玩家断线重连,', _player) NetUtil.Fire_S('OnPlayerReconnectEvent', _player) cache[_player].state = HeartbeatEnum.CONNECT end @@ -164,16 +168,18 @@ function CheckPlayerStates(_player, _sTimestam) cache[_player].state = HeartbeatEnum.CONNECT elseif cache[_player].state == HeartbeatEnum.CONNECT and diff >= HEARTBEAT_THRESHOLD_1 then --* 玩家断线 OnPlayerDisconnectEvent - print('[Heartbeat][Server] OnPlayerDisconnectEvent, 玩家离线, 等待断线重连,', _player) + --print('[Heartbeat][Server] OnPlayerDisconnectEvent, 玩家离线, 等待断线重连,', _player, _player.UserId) NetUtil.Fire_S('OnPlayerDisconnectEvent', _player) cache[_player].state = HeartbeatEnum.DISCONNECT elseif cache[_player].state == HeartbeatEnum.DISCONNECT and diff >= HEARTBEAT_THRESHOLD_2 then - --* 玩家彻底断线,剔除玩家 OnPlayerLeaveEvent - print('[Heartbeat][Server] OnPlayerLeaveEvent, 剔除离线玩家,', _player) - NetUtil.Fire_S('OnPlayerLeaveEvent', _player) - print('[Heartbeat][Server] OnPlayerLeave, 发送客户端离线事件,', _player) - NetUtil.Fire_C('OnPlayerLeaveEvent', _player) - cache[_player] = nil + --* 玩家彻底断线,剔除玩家 + local player = _player + local uid = player.UserId + --print('[Heartbeat][Server] OnPlayerLeaveEvent, 剔除离线玩家,', player, uid) + NetUtil.Fire_S('OnPlayerLeaveEvent', player, uid) + --print('[Heartbeat][Server] OnPlayerLeaveEvent, 发送客户端离线事件,', player, uid) + NetUtil.Fire_C('OnPlayerLeaveEvent', player, uid) + cache[player] = nil end end diff --git a/Code/['World']['Global']['Framework']['ServerModule'].ModuleScript.lua b/Code/['World']['Global']['Framework']['Server']['ServerModule'].ModuleScript.lua similarity index 80% rename from Code/['World']['Global']['Framework']['ServerModule'].ModuleScript.lua rename to Code/['World']['Global']['Framework']['Server']['ServerModule'].ModuleScript.lua index 199de55..d4ab259 100644 --- a/Code/['World']['Global']['Framework']['ServerModule'].ModuleScript.lua +++ b/Code/['World']['Global']['Framework']['Server']['ServerModule'].ModuleScript.lua @@ -16,14 +16,14 @@ local initDefaultList, initList, updateList = {}, {}, {} --- 运行服务器 function Server:Run() - print('[Server] Run()') + --print('[Server] Run()') InitServer() StartUpdate() end --- 停止Update function Server:Stop() - print('[Server] Stop()') + --print('[Server] Stop()') running = false ServerHeartbeat.Stop() end @@ -33,9 +33,10 @@ function InitServer() if initialized then return end - print('[Server] InitServer()') + --print('[Server] InitServer()') InitRandomSeed() InitHeartbeat() + InitDataSync() InitServerCustomEvents() InitCsvAndXls() GenInitAndUpdateList() @@ -46,7 +47,7 @@ end --- 初始化服务器的CustomEvent function InitServerCustomEvents() - print('[Server] InitServerCustomEvents()') + --print('[Server] InitServerCustomEvents()') if world.S_Event == nil then world:CreateObject('FolderObject', 'S_Event', world) end @@ -76,6 +77,12 @@ function InitHeartbeat() ServerHeartbeat.Init() end +--- 初始化数据同步 +function InitDataSync() + assert(ServerDataSync, '[Server][DataSync] 找不到ServerDataSync,请联系张远程') + ServerDataSync.Init() +end + --- 生成框架需要的节点 function InitCsvAndXls() if not world.Global.Csv then @@ -88,9 +95,15 @@ end --- 生成需要Init和Update的模块列表 function GenInitAndUpdateList() + -- TODO: 改成在FrameworkConfig中配置 + -- Init Default ModuleUtil.GetModuleListWithFunc(Module.S_Module, 'InitDefault', initDefaultList) + -- Init + ModuleUtil.GetModuleListWithFunc(Define, 'Init', initList) ModuleUtil.GetModuleListWithFunc(Module.S_Module, 'Init', initList) + -- Update ModuleUtil.GetModuleListWithFunc(Module.S_Module, 'Update', updateList) + -- Plugin for _, m in pairs(FrameworkConfig.Server.PluginModules) do ModuleUtil.GetModuleListWithFunc(m, 'InitDefault', initDefaultList) ModuleUtil.GetModuleListWithFunc(m, 'Init', initList) @@ -119,7 +132,7 @@ end --- 开始Update function StartUpdate() - print('[Server] StartUpdate()') + --print('[Server] StartUpdate()') assert(not running, '[Server] StartUpdate() 正在运行') running = true @@ -129,6 +142,9 @@ function StartUpdate() invoke(ServerHeartbeat.Start) end + -- 开启数据同步 + ServerDataSync.Start() + local dt = 0 -- delta time 每帧时间 local tt = 0 -- total time 游戏总时间 local now = Timer.GetTimeMillisecond --时间函数缓存 @@ -140,9 +156,24 @@ function StartUpdate() tt = tt + dt prev = curr UpdateServer(dt, tt) + --[[xpcall( + function() + --local a = 10 / nil + + end, + function(err) + ErrorShow(err) + error(err) + end + )]] end end +function ErrorShow(err) + world.Global.ErrorGUI:SetActive(true) + world.Global.ErrorGUI.Error.Text = err +end + --- Update函数 --- @param dt delta time 每帧时间 function UpdateServer(_dt, _tt)