运营需要我们设计一个签到功能,策划说的很简单,但是需求并不明朗,在我多次询问之后得出了相对明确的需求:支持多日签到,可能有多种签到类型,具体多少天不确定,有起止时间,不循环,但终止后可能会再重启新一轮。登录即代表签到,可以获取当日的签到奖励。若某日未登录,则该日的签到奖励无法获取,暂时不支持补签。每个玩家的签到首日不一定相同。
根据需求可以发现一些设计上的关键点:
除此以外,这种跟时间戳关系密切的方案设计,应该尽量考虑到开发期方便调试。现在项目的服务端由c++编写框架和公共组件、lua编写业务逻辑,要注意方便代码reload,以及方便任意调整时间测试即保证时间回拨情况下的容错。尤其目前项目没有稳定可靠的类似时间任务调度的公共模块,设计上能通用尽量通用。
我把签到模块称为SignInReward。总的来说要做的事:
在我看来如果策划要求的不是登录即签到,而是客户端主动请求签到,每日0点更新的时候是不用更新所有在线玩家的。而且我们项目目前要啥啥没有,工作量也更多一点。但是没办法,需求就是这么定的。
每个符合条件的玩家都拥有一个SignInRewardComponent成员,全局有一个唯一的SignInRewardMgr管理对象。
-- 签到类型详情
SignInRewardMgr.tbSignInTypeInfo = {
-- 新手签到,类型名源自pb枚举
SIGN_IN_TYPE_ROOKIE = {
szXLSXName = "RookieSignInReward", -- 奖励配表名字
nPeriodCount = 1, -- 周期数
nStartTime = "2021-03-01", -- 开始时间
nEndTime = "2099-03-01", -- 结束时间
szTitle = "新手登录签到", -- 标题
szContent = "test", -- 其他显示信息
},
-- 其他类型...
}
-- 内存数据
SignInRewardMgr.tbSignInRewardInfo = {
szSignInType = "", -- 签到类型
bValid = false, -- 是否有效
bWaitingStart = false, -- 是否正在等待开始生效
nLocalDayMaxIndex = 0, -- 最大天数索引
nStartUnixSec = 0, -- 开始时间戳
nEndUnixSec = 0, -- 结束时间戳
nPeriodCount = 0, -- 周期数
szTitle = "", -- 标题
szContent = "", -- 其他显示信息
tbRewardDetailIndexMap = nil -- 配置表的奖励信息索引映射,奖励内容pb.CommonRewardInfo格式
}
function SignInRewardComponent:Reset()
self.bReady = false -- 准备好执行业务操作
self.tbPlayer = nil
self.szSignType = "" -- 签到类型
self.nRewardPeriodCount = 0 -- 所在的奖励周期
self.nLastSignInUnixSec = 0 -- 上次签到秒时间戳
self.nLastSignInLocalDay = 0 -- 上次签到日,0表示该周期首日,注意在线跨越一天的情况也要更新该字段
self.nLastSignInMaxIndex = 0 -- 上次签到的天数索引,0表示该周期首日,注意在线跨越一天的情况也要更新该字段
self.tbAllowIndexSet = nil -- 周期内允许获取奖励的天数索引
self.tbAlreadyIndexSet = nil -- 周期内已经获取奖励的天数索引
self.tbMissedIndexSet = nil -- 周期内错过获取奖励的天数索引
self.tbIndexStateMap = nil -- 以上各个Set的索引对应的状态
end
function SignInRewardMgr:Reset()
self.nNextTick = 0
self.nLastRecordUnixSec = 0 -- 最近记录的秒时间戳
self.nCurrentLocalDay = 0 -- 当前日,距离1970年1月1日已过的天数
self.nNextLocalDayUnixSec = 0 -- 次日零点的unix秒时间戳
self.tbNextDealingPlayerNode = nil -- 即将处理的在线玩家节点,更新时从尾向头遍历
self.tbValidSignInRewardInfoTypeMap = nil -- 有效的签到详情类型映射
self.tbWaitStartSignInRewardInfoTypeMap = nil -- 等待开始的签到详情类型映射
end
// 签到类型
enum SignInType {
SIGN_IN_TYPE_ROOKIE = 0; // 新手签到
}
// 通用奖励信息
message CommonRewardInfo {
repeated GrowthInfo growthInfos = 1;
repeated CurrencyInfo currencyInfos = 2;
repeated DropItemInfo itemInfos = 3;
}
// 一日签到信息
message SignInInfo {
enum PlayerApplyState {
APPLY_SIGN_IN_ALLOW = 0; // 允许领取
APPLY_SIGN_IN_ALREADY = 1; // 已经领取
APPLY_SIGN_IN_LOCKED = 2; // 不可领取(未解锁)
APPLY_SIGN_IN_MISSED = 3; // 不可领取(当日未签到)
}
uint32 index = 1;
PlayerApplyState applyState = 2;
CommonRewardInfo rewardInfo = 3;
}
// Sign In Reward
//请求签到类型详情
message ApplySignInInfoListReq {
SignInType signInType = 1;
}
//请求签到类型详情回复
message ApplySignInInfoListRsp {
SignInType signInType = 1;
string title = 2; // 标题
string description = 3; // 描述信息
repeated SignInInfo signInInfoArray = 4;
}
//请求签到类型某日奖励
message ApplySignInRewardReq {
SignInType signInType = 1;
uint32 index = 2;
}
//请求签到类型某日奖励回复
message ApplySignInRewardRsp {
enum PlayerApplyResult {
APPLY_SIGN_IN_REWARD_SUCCESS = 0; // 领取成功
APPLY_SIGN_IN_REWARD_ALREADY = 1; // 已经领取
APPLY_SIGN_IN_REWARD_NOT_ALLOW = 2; // 不可领取
APPLY_SIGN_IN_REWARD_INVALID_PARAMS = 3; // 无效的请求参数
APPLY_SIGN_IN_REWARD_INTERNAL_ERROR = 4; // 服务端内部错误
}
PlayerApplyResult result = 1;
SignInType signInType = 2;
uint32 index = 3;
}
function SignInRewardComponent:SerializeToDB()
if self:IsReady() then
local tb = {
signInType = self:GetSignType(),
rewardPeriodCount = self:GetRewardPeriodCount(),
lastSignInUnixSec = self:GetLastSignInUnixSec(),
lastSignInLocalDay = self:GetLastSignInLocalDay(),
lastSignInMaxIndex = self:GetLastSignInMaxIndex(),
allowIndexArray = {},
alreadyIndexArray = {},
missedIndexArray = {},
}
for index, _ in pairs(self.tbAllowIndexSet) do
table.insert(tb.allowIndexArray, index)
end
for index, _ in pairs(self.tbAlreadyIndexSet) do
table.insert(tb.alreadyIndexArray, index)
end
for index, _ in pairs(self.tbMissedIndexSet) do
table.insert(tb.missedIndexArray, index)
end
return tb
end
return nil
end
管理类在初始化时,填充当日的时间数据,并且将配置表内容复制一份到自己的内存管理。
function SignInRewardMgr:Init()
self:Reset()
self.tbNextDealingPlayerNode = nil
self.tbValidSignInRewardInfoTypeMap = self.tbValidSignInRewardInfoTypeMap or {}
self.tbWaitStartSignInRewardInfoTypeMap = self.tbWaitStartSignInRewardInfoTypeMap or {}
self:_UpdateCurrentLocalDay(KServerTime:UnixSec())
self:_LoadConfig()
return 1
end
首先要通过当前时间戳更新当日的一些信息,比如最近记录的时间戳、距离1970年1月1日的天数、次日0点的时间戳。这里有一个要注意的地方是,由于全世界各个地方的时区可能不一样,所以框架提供接口获取的时间戳跟当前所在的时区是相关的。比如我国在东八区,获得的时间戳是距离1970年1月1日早8点度过的秒数。
项目的公共库中有提供获取时差的接口,由此就可以计算出当前的天数和次日0点的时间戳。
这些数据在update过程中有用。
-- 获取时差(秒数)
function Lib:GetGMTSec()
if self.__localGmtSec then
return self.__localGmtSec;
else
self.__localGmtSec = os.difftime(GetTime(), os.time(os.date("!*t",GetTime())))
return self.__localGmtSec;
end
end
-- 根据秒数(UTC,GetTime()返回)计算当地天数
-- 1970年1月1日 返回0
-- 1970年1月2日 返回1
-- 1970年1月3日 返回2
-- ……依此类推
function Lib:GetLocalDay(nUtcSec)
local nLocalSec = (nUtcSec or GetTime()) + self:GetGMTSec();
return math.floor(nLocalSec / (3600 * 24));
end
-- 更新当日的时间信息
function SignInRewardMgr:_UpdateCurrentLocalDay(nCurrentUnixSec)
self:_SetNowUnixSec(nCurrentUnixSec)
self:_SetToday(Lib:GetLocalDay(self:GetNowUnixSec()))
self.nNextLocalDayUnixSec = (self:GetToday() + 1) * 3600 * 24 - math.floor(Lib:GetGMTSec())
end
然后才开始加载配置内容。先清空原配置内容,因为本次加载可能是程序运行过程中reload脚本,避免前后两次数据混淆。接着给每一种类型的签到,填充运行过程中需要用到的配置数据,其中细节无需一一说明。
-- 加载配置内容
function SignInRewardMgr:_LoadConfig()
self.tbValidSignInRewardInfoTypeMap = {}
self.tbWaitStartSignInRewardInfoTypeMap = {}
self:_LoadSignRewardTypeConfig()
self:_OnUpdateNecessaryInfoByNewConfig()
end
-- 加载签到类型配置
function SignInRewardMgr:_LoadSignRewardTypeConfig()
for szSignInType, tbInfo in pairs(self.tbSignInTypeInfo or Lib:GetEmptyTable()) do
local _, tbRewardInfo = self:_LoadOneSignRewardInfoConfig(szSignInType, tbInfo)
if tbRewardInfo.bValid then
self.tbValidSignInRewardInfoTypeMap[szSignInType] = tbRewardInfo
elseif tbRewardInfo.bWaitingStart then
self.tbWaitStartSignInRewardInfoTypeMap[szSignInType] = tbRewardInfo
end
end
end
-- 加载具体类型签到描述配置
function SignInRewardMgr:_LoadOneSignRewardInfoConfig(szSignInType, tbInfo)
assert(tbInfo)
local result = false
local tbRewardInfo = Lib:NewClass(self.tbSignInRewardInfo)
tbRewardInfo.szSignInType = szSignInType
tbRewardInfo.nPeriodCount = tbInfo.nPeriodCount
tbRewardInfo.szTitle = tbInfo.szTitle
tbRewardInfo.szContent = tbInfo.szContent
local szFuncName = "Get" .. tbInfo.szXLSXName .. "Map"
local fn = ConfigureManager[szFuncName]
if not fn or Lib:IsEmptyTB(fn(ConfigureManager)) then
-- 表格没有任何数据
goto EXIT0
end
if tbRewardInfo.nPeriodCount <= 0 then
-- 周期数必须大于0
goto EXIT0
end
if not type(tbInfo.nStartTime) == "string" or not type(tbInfo.nEndTime) == "string" then
goto EXIT0
end
tbRewardInfo.nStartUnixSec = Lib:GetStringDate2Time(tbInfo.nStartTime .. self.DATE_FORMAT_SUFFIX)
tbRewardInfo.nEndUnixSec = Lib:GetStringDate2Time(tbInfo.nEndTime .. self.DATE_FORMAT_SUFFIX)
if tbRewardInfo.nStartUnixSec >= tbRewardInfo.nEndUnixSec then
goto EXIT0
end
if tbRewardInfo.nStartUnixSec > self:GetNowUnixSec() then
tbRewardInfo.bWaitingStart = true
goto EXIT0
end
if tbRewardInfo.nEndUnixSec <= self:GetNowUnixSec() then
goto EXIT0
end
szFuncName = tbInfo.szXLSXName .. "ByIndex"
fn = ConfigureManager[szFuncName]
if fn then
repeat
local nCurrIndex = tbRewardInfo.nLocalDayMaxIndex + 1
local tbRow = fn(ConfigureManager, nCurrIndex)
if not tbRow then
break
end
tbRewardInfo.nLocalDayMaxIndex = nCurrIndex
tbRewardInfo.tbRewardDetailIndexMap = tbRewardInfo.tbRewardDetailIndexMap or {}
tbRewardInfo.tbRewardDetailIndexMap[nCurrIndex] = self:_GenerateCommonRewardInfo(tbRow)
until (false)
end
result = true
::EXIT0::
tbRewardInfo.bValid = result
return result, tbRewardInfo
end
function SignInRewardMgr:_GenerateCommonRewardInfo(tbConfigRow)
assert(type(tbConfigRow) == "table")
local tbRewardDetail = {}
--[[
-- exp
if tbConfigRow.exp and tbConfigRow.exp > 0 then
local tbGrowthInfo = {
growthType = SignInRewardMgr.GROWTH_TYPE_EXP,
growthNumber = tbConfigRow.exp,
}
tbRewardDetail.growthInfos = tbRewardDetail.growthInfos or {}
table.insert(tbRewardDetail.growthInfos, tbGrowthInfo)
end
-- 货币
local tbRet = Lib:SplitStr(tbConfigRow.currency, ",")
assert(math.fmod(#tbRet, 2) == 0) -- 被2整除
for i = 1, #tbRet, 2 do
local tbCurrencyInfo = {
currencyID = tonumber(tbRet[i]),
currencyNumber = tonumber(tbRet[i + 1]),
}
tbRewardDetail.currencyInfos = tbRewardDetail.currencyInfos or {}
table.insert(tbRewardDetail.currencyInfos, tbCurrencyInfo)
end
--]]
-- 道具
local tbRet = Lib:SplitStr(tbConfigRow.items, ",")
assert(math.fmod(#tbRet, 2) == 0) -- 被2整除
for i = 1, #tbRet, 2 do
local tbDropItemInfo = {
itemTemplateID = tonumber(tbRet[i]),
count = tonumber(tbRet[i + 1]),
}
tbRewardDetail.itemInfos = tbRewardDetail.itemInfos or {}
table.insert(tbRewardDetail.itemInfos, tbDropItemInfo)
end
return tbRewardDetail
end
更新完配置信息对应的内存数据后,还要更新与运行逻辑相关的内容。比如检查运营中的各个签到是否过期、检查等待开启运营的各个签到是否等够时间、更新用于分帧处理在线玩家签到信息的指针。
无论是在进入新的一天,还是配置内容更新,都希望将所有的在线玩家的签到信息都更新至当前时刻的状态。因此需要获取到所有在线玩家并一一处理。但是gameserver是分帧运行,如果在线玩家数量太多并且一口气处理所有玩家,就会导致一帧执行的时间特别长,进而导致网络数据处理和其他模块update工作滞后,严重的话可能引起套接字接受缓冲区堵满、网络延迟放大、带宽激增。避免这么严重的后果的最好方法就是分帧处理:首先保证有一个链表连接所有的在线玩家,保留一个指针,逐个遍历并处理,一帧处理几十个,保存指针,下一帧接着处理。其次保证玩家下线时能通知到该模块,比如一个尚未处理的在线玩家下线时,要检查该指针是不是指向它,如果是,将指针移动到下一个处理节点。
这里是获取在线玩家链表的最后一个节点,从后向前遍历。原因是从此以后进入游戏的玩家,在登录成功后就会有相应的检查和更新。如果从前向后遍历的话会重复处理,而且处理的个数有可能会随着大量玩家登录而增加。
-- 根据配置内容更新各个类型签到必要信息
function SignInRewardMgr:_OnUpdateNecessaryInfoByNewConfig()
self:_UpdateExpiredSignInRewardInfo(self:GetNowUnixSec())
self:_UpdateWaitStartSignInRewardInfo(self:GetNowUnixSec())
self:_PrepareUpdateOnlinePlayerSignInRewardComponentOnNextDay()
end
-- 更新可能过期的签到信息
function SignInRewardMgr:_UpdateExpiredSignInRewardInfo(nCurrentUnixSec)
local tbExpiredRewardInfos = nil
for szSignInType, tbRewardInfo in pairs(self.tbValidSignInRewardInfoTypeMap) do
if tbRewardInfo.nEndUnixSec <= nCurrentUnixSec then
tbExpiredRewardInfos = tbExpiredRewardInfos or {}
tbExpiredRewardInfos[szSignInType] = tbRewardInfo
end
end
for szSignInType, tbRewardInfo in pairs(tbExpiredRewardInfos or Lib:GetEmptyTable()) do
tbRewardInfo.bValid = false
tbRewardInfo.bWaitingStart = false
self.tbValidSignInRewardInfoTypeMap[szSignInType] = nil
end
end
-- 更新等待开始的签到信息
function SignInRewardMgr:_UpdateWaitStartSignInRewardInfo(nCurrentUnixSec)
local tbStartedRewardInfos = nil
for szSignInType, tbRewardInfo in pairs(self.tbWaitStartSignInRewardInfoTypeMap) do
if tbRewardInfo.nStartUnixSec <= nCurrentUnixSec then
tbStartedRewardInfos = tbStartedRewardInfos or {}
tbStartedRewardInfos[szSignInType] = tbRewardInfo
end
end
for szSignInType, tbRewardInfo in pairs(tbStartedRewardInfos or Lib:GetEmptyTable()) do
tbRewardInfo.bValid = true
tbRewardInfo.bWaitingStart = false
self.tbWaitStartSignInRewardInfoTypeMap[szSignInType] = nil
self.tbValidSignInRewardInfoTypeMap[szSignInType] = tbRewardInfo
end
end
-- 进入下一天时准备更新在线玩家的签到信息
function SignInRewardMgr:_PrepareUpdateOnlinePlayerSignInRewardComponentOnNextDay()
-- 原值可能不为空,说明玩家的数据没处理完,但可能性极低,因此允许丢失
self.tbNextDealingPlayerNode = nil
local player = PlayerMgr:GetLatestPlayerInGamingList()
if player then
self.tbNextDealingPlayerNode = player:GetHostNode()
end
end
update方法会每帧调用,但是除了处理在线玩家签到更新内容,其他的信息更新并不需要每帧都检查。所以根据当前业务情况,每秒检查一次足够了。每秒更新当前时间戳,并判断是否进入次日,如果是,执行进入次日的操作。
function SignInRewardMgr:Update(tick)
self:_UpdateOnlinePlayerSignInRewardComponentPerFrame()
if self.nNextTick > tick then
return
end
self.nNextTick = tick + self.CHECK_NEXT_DAY_UPDATE_INTERVAL
self:_SetNowUnixSec(KServerTime:UnixSec())
if self:GetNowUnixSec() >= self:GetNextLocalDayUnixSec() then
self:_OnEnterNextLocalDay(self:GetNowUnixSec())
end
end
-- 每帧更新在线玩家的签到信息
function SignInRewardMgr:_UpdateOnlinePlayerSignInRewardComponentPerFrame()
if not self.tbNextDealingPlayerNode then
return
end
local nDealCount = 0
local tbCurrentNode = nil
local player = nil
repeat
tbCurrentNode = self.tbNextDealingPlayerNode
self.tbNextDealingPlayerNode = tbCurrentNode:GetPrev()
player = tbCurrentNode:GetHost()
if not player then
assert(false)
break
end
-- 通过Player拿到对应组件,更新组件签到信息
for _, tbComponent in pairs(player:GetAllSignInRewardComponents() or Lib:GetEmptyTable()) do
tbComponent:CheckAndUpdate()
end
if nDealCount >= self.DEAL_ONLINE_PLAYER_COUNT_PER_FRAME then
break
end
until(not self.tbNextDealingPlayerNode)
end
-- 步入新的一天回调
function SignInRewardMgr:_OnEnterNextLocalDay(nCurrentUnixSec)
self:_UpdateCurrentLocalDay(nCurrentUnixSec)
self:_OnUpdateNecessaryInfoByNewConfig()
end
在每帧处理在线玩家签到更新的方法中,如果更新的玩家个数超过规定的一帧处理上线或者所有玩家处理完毕,就不会再进入这个操作中。否则会对每个玩家的所有签到组件进行更新,具体更新细节后面再说。
管理类步入新的一天,要做的事无非就是前边介绍过的,更新当日时间信息、更新与运行逻辑相关的签到配置信息。
一种类型签到组件只要在DB有签到数据或在有效期的情况下才会创建。相应的创建方法有2个。
function SignInRewardMgr:NewSignInRewardComponentFromDB(player, dbData)
if dbData then
local tbRewardInfo = self.tbValidSignInRewardInfoTypeMap[dbData.signInType]
if tbRewardInfo then
local tbComponent = self:_MakeSignInRewardComponent()
tbComponent:Init(player, dbData.signInType, dbData)
return tbComponent
end
end
return nil
end
function SignInRewardMgr:NewSignInRewardComponent(player, szSignInType)
local tbRewardInfo = self.tbValidSignInRewardInfoTypeMap[szSignInType]
if tbRewardInfo then
local tbComponent = self:_MakeSignInRewardComponent()
tbComponent:Init(player, szSignInType)
return tbComponent
end
return nil
end
function SignInRewardMgr:DeleteSignInRewardComponent(tbComponent)
if type(tbComponent) == "table" then
local player = tbComponent:GetBelongPlayer()
if player then
if self.tbNextDealingPlayerNode == player:GetHostNode() then
self.tbNextDealingPlayerNode = player:GetHostNode():GetPrev()
end
if tbComponent:GetSignType() == "SIGN_IN_TYPE_ROOKIE" then
player:SetRookieSignInComponent(nil)
end
end
tbComponent:UnInit()
self:_FreeSignInRewardComponent(tbComponent)
tbComponent = nil
end
end
其中所有需要通过判断组件类型而选择执行的操作都属于无奈之举,如果开发时间更多一点的话,我会把不同类型的相同模板组件统一封装起来。
组件创建的行为比较简单,有DB数据就填充,没有就取默认值。
function SignInRewardComponent:Init(tbPlayer, szSignInType, dbData)
assert(type(tbPlayer) == "table")
assert(type(szSignInType) == "string")
self:Reset()
self.tbPlayer = tbPlayer
self.szSignType = szSignInType
self.tbAllowIndexSet = self.tbAllowIndexSet or {}
self.tbAlreadyIndexSet = self.tbAlreadyIndexSet or {}
self.tbMissedIndexSet = self.tbMissedIndexSet or {}
self.tbIndexStateMap = self.tbIndexStateMap or {}
if type(dbData) == "table" then
self.nRewardPeriodCount = dbData.rewardPeriodCount or 0
self.nLastSignInUnixSec = dbData.lastSignInUnixSec or 0
self.nLastSignInLocalDay = dbData.lastSignInLocalDay or 0
self.nLastSignInMaxIndex = dbData.lastSignInMaxIndex or 0
for _, index in pairs(dbData.allowIndexArray or Lib:GetEmptyTable()) do
self:_SetAllowIndex(index, true)
end
for _, index in pairs(dbData.alreadyIndexArray or Lib:GetEmptyTable()) do
self:_SetAlreadyIndex(index, true)
end
for _, index in pairs(dbData.missedIndexArray or Lib:GetEmptyTable()) do
self:_SetMissedIndex(index, true)
end
end
end
签到组件的准备根据玩家登录过程分为2个阶段。第一个阶段是玩家初始化,第二个阶段是玩家真正进入游戏。
玩家初始化时如果有签到DB数据,就会调用AddRookieSignInComponentByDB创建组件。即使DB的签到类型可能此时已经过期,在SignInRewardMgr:NewSignInRewardComponentFromDB也会检查出来并且不会继续创建组件。
function Player:AddRookieSignInComponentByDB(dbData)
self:SetRookieSignInComponent(SignInRewardMgr:NewSignInRewardComponentFromDB(self, dbData))
end
function Player:SetRookieSignInComponent(tbComponent)
self:PlayerInfo().rookieSignInComponent = tbComponent
end
如果第一个阶段已经成功创建了签到组件,第二个阶段就会更新组件的信息。反之如果没有创建签到组件,第二个阶段就会根据当前有效的签到类型,创建签到组件并填充组件信息。
-- TODO 代码有待优化
function SignInRewardMgr:OnPlayerEnterGame(player)
local tbRookieSignInRewardComponent = nil
for szSignInType, tbRewardInfo in pairs(self.tbValidSignInRewardInfoTypeMap) do
if szSignInType == "SIGN_IN_TYPE_ROOKIE" then
tbRookieSignInRewardComponent = player:GetRookieSignInRewardComponent()
if not tbRookieSignInRewardComponent then
player:SetRookieSignInComponent(self:NewSignInRewardComponent(player, szSignInType))
end
end
end
-- 更新新手签到
tbRookieSignInRewardComponent = player:GetRookieSignInRewardComponent()
if tbRookieSignInRewardComponent then
tbRookieSignInRewardComponent:OnPlayerEnterGame()
tbRookieSignInRewardComponent = nil
end
end
-- TODO 代码有待优化
function SignInRewardMgr:OnPlayerReconnect(player)
local tbRookieSignInRewardComponent = nil
for szSignInType, tbRewardInfo in pairs(self.tbValidSignInRewardInfoTypeMap) do
if szSignInType == "SIGN_IN_TYPE_ROOKIE" then
tbRookieSignInRewardComponent = player:GetRookieSignInRewardComponent()
if not tbRookieSignInRewardComponent then
player:SetRookieSignInComponent(self:NewSignInRewardComponent(player, szSignInType))
end
end
end
-- 更新新手签到
tbRookieSignInRewardComponent = player:GetRookieSignInRewardComponent()
if tbRookieSignInRewardComponent then
tbRookieSignInRewardComponent:OnPlayerReconnect()
tbRookieSignInRewardComponent = nil
end
end
玩家进入游戏和玩家重连游戏的时候都要做相应的处理,因为玩家断线时已经从在线玩家的链表中移除了,这个过程中如果签到内容有更新并遍历在线玩家链表的话,是不会被处理到的。再重连后,必须做一次检查。
-- 用于进入游戏
function SignInRewardComponent:OnPlayerEnterGame()
self:CheckAndUpdate()
end
-- 用于重连
function SignInRewardComponent:OnPlayerReconnect()
self:CheckAndUpdate()
end
检查更新的详细操作后面再说。
组件针对自己的检查:
判断当前是否有效,若有效则检查更新签到周期,更新当日的签到信息。否则,销毁自己。
-- 检查是否需要更新自己的周期以及天数索引,或者删除自己
function SignInRewardComponent:CheckAndUpdate()
if SignInRewardMgr:IsValidSignInType(self:GetSignType()) then
self.bReady = true
self:_CheckCurrentPeriodCount()
self:_UpdateTodayRewardInfo()
else
SignInRewardMgr:DeleteSignInRewardComponent(self)
end
end
-- 检查当日奖励周期
function SignInRewardComponent:_CheckCurrentPeriodCount()
local nRewardPeriodCount = SignInRewardMgr:GetCurrentRewardPeriodCount(self:GetSignType())
assert(nRewardPeriodCount > 0)
if self:GetRewardPeriodCount() == nRewardPeriodCount then
return
end
if self:GetRewardPeriodCount() > nRewardPeriodCount then
LogErrWithFields({playerID = self:GetBelongPlayer():GetID(), playerRewardPeriodCount = self:GetRewardPeriodCount(), currentRewardPeriodCount = nRewardPeriodCount}, "player reward period is more than current !")
end
self:_ClearPeriodInfo()
self.nRewardPeriodCount = nRewardPeriodCount
end
-- 更新当日奖励信息
function SignInRewardComponent:_UpdateTodayRewardInfo()
assert(self:GetRewardPeriodCount() > 0)
local nToday = SignInRewardMgr:GetToday()
local nNowUnixSec = SignInRewardMgr:GetNowUnixSec()
local nTodayRewardIndex = 0
-- self:DebugShowInfo()
-- 该周期首次进入游戏
if self:GetLastSignInLocalDay() == 0 then
nTodayRewardIndex = 1
else
local nMissedDay = nToday - self:GetLastSignInLocalDay() - 1 -- 不能包含当日
if nMissedDay > 0 then
-- 有遗漏签到
local nMaxRewardIndex = SignInRewardMgr:GetMaxRewardIndexBySignType(self:GetSignType())
for i = 1, nMissedDay do
local nMissedIndex = self:GetLastSignInMaxIndex() + i
if nMissedIndex > nMaxRewardIndex then
break
end
self:AddMissedIndex(nMissedIndex)
nTodayRewardIndex = nMissedIndex + 1
end
-- 避免超过最大签到日
if nTodayRewardIndex > nMaxRewardIndex then
nTodayRewardIndex = 0
end
elseif nMissedDay == 0 then
-- 当日为上次签到的次日
nTodayRewardIndex = self:GetLastSignInMaxIndex() + 1
local nMaxRewardIndex = SignInRewardMgr:GetMaxRewardIndexBySignType(self:GetSignType())
-- 避免超过最大签到日
if nTodayRewardIndex > nMaxRewardIndex then
nTodayRewardIndex = 0
end
elseif nMissedDay == -1 then
-- 当日已经签到过
-- do nothing
else
-- 检测到时间回拨过
LogErrWithFields({playerID = self:GetBelongPlayer():GetID(), playerRewardPeriodCount = self:GetRewardPeriodCount(), lastSignInLocalDay = self:GetLastSignInLocalDay()}, "player reward time has been turn back !")
nTodayRewardIndex = self:GetLastSignInMaxIndex() + (nMissedDay + 1)
if nTodayRewardIndex <= 0 then
-- 时间回拨到首次签到以前
-- do nothing
end
end
end
if nTodayRewardIndex > 0 then
self:AddAllowIndex(nTodayRewardIndex)
end
self:_SetLastSignInInfo(nNowUnixSec, nToday, self:GetLastSignInMaxIndex())
-- self:DebugShowInfo()
end
更新当日的签到信息的大概步骤:
其中,涉及到的方法如下,都带有必要的检查。类似添加允许领取的天数的话,那么天数肯定是正数并且不能在已领取的集合中,其他的不一一细说了:
-- 添加允许领取的天数索引,会检查索引是否合理
function SignInRewardComponent:AddAllowIndex(nIndex)
local result = false
if not self:IsReady() then
goto EXIT0
end
if nIndex <= 0 or self.tbAlreadyIndexSet[nIndex] then
goto EXIT0
end
self:_SetMissedIndex(nIndex, false)
self:_SetAllowIndex(nIndex, true)
if self:GetLastSignInMaxIndex() < nIndex then
self:_SetLastSignInInfo(self:GetLastSignInUnixSec(), self:GetLastSignInLocalDay(), nIndex)
end
result = true
::EXIT0::
return result
end
-- 添加已经领取的天数索引,会检查索引是否合理
function SignInRewardComponent:AddAlreadyIndex(nIndex)
local result = false
if not self:IsReady() then
goto EXIT0
end
if nIndex <= 0 then
goto EXIT0
end
if not self.tbAllowIndexSet[nIndex] and not self.tbMissedIndexSet[nIndex] then
goto EXIT0
end
self:_SetAllowIndex(nIndex, false)
self:_SetMissedIndex(nIndex, false)
self:_SetAlreadyIndex(nIndex, true)
result = true
::EXIT0::
return result
end
-- 添加错过领取的天数索引,会检查索引是否合理
function SignInRewardComponent:AddMissedIndex(nIndex)
local result = false
if not self:IsReady() then
goto EXIT0
end
if nIndex <= 0 or self.tbAllowIndexSet[nIndex] or self.tbAlreadyIndexSet[nIndex] then
goto EXIT0
end
self:_SetMissedIndex(nIndex, true)
result = true
::EXIT0::
return result
end
function SignInRewardComponent:_SetAllowIndex(nIndex, bNotReset)
assert(type(nIndex) == "number")
if bNotReset then
self.tbAllowIndexSet[nIndex] = true
self.tbIndexStateMap[nIndex] = SignInRewardMgr.APPLY_SIGN_IN_ALLOW
else
self.tbAllowIndexSet[nIndex] = nil
self.tbIndexStateMap[nIndex] = nil
end
end
function SignInRewardComponent:_SetAlreadyIndex(nIndex, bNotReset)
assert(type(nIndex) == "number")
if bNotReset then
self.tbAlreadyIndexSet[nIndex] = true
self.tbIndexStateMap[nIndex] = SignInRewardMgr.APPLY_SIGN_IN_ALREADY
else
self.tbAlreadyIndexSet[nIndex] = nil
self.tbIndexStateMap[nIndex] = nil
end
end
function SignInRewardComponent:_SetMissedIndex(nIndex, bNotReset)
assert(type(nIndex) == "number")
if bNotReset then
self.tbMissedIndexSet[nIndex] = true
self.tbIndexStateMap[nIndex] = SignInRewardMgr.APPLY_SIGN_IN_MISSED
else
self.tbMissedIndexSet[nIndex] = nil
self.tbIndexStateMap[nIndex] = nil
end
end
目前只接受2种请求,获取签到状态和领取奖励。涉及到的很多判断条件就略过了,这里主要说下领取奖励功能逻辑:
-- 处理玩家领取签到奖励
function SignInRewardMgr:_HandlePlayerApplySignInReward(tbComponent, tbRewardInfo, nIndex)
assert(type(tbComponent) == "table")
assert(type(tbRewardInfo) == "table")
assert(type(nIndex) == "number")
-- 将该天数索引状态改为已领取
if not tbComponent:AddAlreadyIndex(nIndex) then
return false
end
local player = tbComponent:GetBelongPlayer()
local tbRewardDetail = tbRewardInfo.tbRewardDetailIndexMap[nIndex]
if player and tbRewardDetail and tbRewardDetail.itemInfos then
local tbAddItemInfoArray = nil
for _, tbDropItemInfo in pairs(tbRewardDetail.itemInfos) do
tbAddItemInfoArray = tbAddItemInfoArray or {}
table.insert(tbAddItemInfoArray, {
templateID = tbDropItemInfo.itemTemplateID,
count = tbDropItemInfo.count,
})
end
-- 判断背包是否有剩余空间,注意多个道具要一起判断,背包模块提供多个物品添加接口,且具备原子性
if tbAddItemInfoArray and not player:GetBags():AddMultiItem(tbAddItemInfoArray) then
assert(tbComponent:RemoveAlreadyIndex(nIndex, false, false), "can not lose !")
return false
end
end
return true
end
-- @bClear 是否完全清除该索引的存在
-- @bToMissed 不完全清除该索引的情况下生效,true转移到miss集合,false转移到allow集合
function SignInRewardComponent:RemoveAlreadyIndex(nIndex, bClear, bToMissed)
local result = false
if not self:IsReady() then
goto EXIT0
end
if nIndex <= 0 then
goto EXIT0
end
if not self.tbAlreadyIndexSet[nIndex] then
goto EXIT0
end
self:_SetAlreadyIndex(nIndex, false)
if not bClear then
if bToMissed then
self:_SetMissedIndex(nIndex, true)
else
self:_SetAllowIndex(nIndex, true)
end
end
result = true
::EXIT0::
return result
end