多日签到功能设计目录

  • 游戏业务——多日签到功能设计
    • 一.需求分析
    • 二.模块结构设计
    • 三.设计详情
      • (1) 数据结构设计
      • (2) 功能逻辑设计
    • 四.不足与展望

游戏业务——多日签到功能设计

运营需要我们设计一个签到功能,策划说的很简单,但是需求并不明朗,在我多次询问之后得出了相对明确的需求:支持多日签到,可能有多种签到类型,具体多少天不确定,有起止时间,不循环,但终止后可能会再重启新一轮。登录即代表签到,可以获取当日的签到奖励。若某日未登录,则该日的签到奖励无法获取,暂时不支持补签。每个玩家的签到首日不一定相同。

一.需求分析

根据需求可以发现一些设计上的关键点:

  1. 签到模块应该是尽量通用的模板,这样才能方便生成多个签到类型;
  2. 使用配置表给策划用于配置每日签到的信息如ID、奖励等,配置多少行就最多签到多少天;
  3. 要配置的起止时间表示一种签到的有效期;
  4. 某一类型签到过期后将来再重启的话,为了防止玩家上一周期的签到信息与当前周期混淆,需要添加周期标识;
  5. 因为登录就代表本日的签到,故无需提供客户端签到请求接口;
  6. 需要给客户端提供查询个人签到情况和申请某日签到奖励的接口;
  7. 要给每日签到情况设置多个状态,包括可领取、已领取、遗漏签到;
  8. 每个玩家的每种签到信息都要存盘,存盘字段至少包含上次签到日、上次签到天数索引、当前已过的日子的签到状态。

除此以外,这种跟时间戳关系密切的方案设计,应该尽量考虑到开发期方便调试。现在项目的服务端由c++编写框架和公共组件、lua编写业务逻辑,要注意方便代码reload,以及方便任意调整时间测试即保证时间回拨情况下的容错。尤其目前项目没有稳定可靠的类似时间任务调度的公共模块,设计上能通用尽量通用。

二.模块结构设计

我把签到模块称为SignInReward。总的来说要做的事:

  1. 需要注册网络handler处理玩家请求
  2. 每帧update检查时间,达到新一天的0点时更新签到类型的信息和在线玩家的签到数据。
  3. 数据库的读写。

在我看来如果策划要求的不是登录即签到,而是客户端主动请求签到,每日0点更新的时候是不用更新所有在线玩家的。而且我们项目目前要啥啥没有,工作量也更多一点。但是没办法,需求就是这么定的。

三.设计详情

(1) 数据结构设计

  1. 模块内部数据结构

每个符合条件的玩家都拥有一个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 = nilself.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的索引对应的状态
endfunction SignInRewardMgr:Reset()self.nNextTick = 0self.nLastRecordUnixSec = 0                     -- 最近记录的秒时间戳self.nCurrentLocalDay = 0                       -- 当前日,距离1970年1月1日已过的天数self.nNextLocalDayUnixSec = 0                   -- 次日零点的unix秒时间戳self.tbNextDealingPlayerNode = nil              -- 即将处理的在线玩家节点,更新时从尾向头遍历self.tbValidSignInRewardInfoTypeMap = nil       -- 有效的签到详情类型映射self.tbWaitStartSignInRewardInfoTypeMap = nil   -- 等待开始的签到详情类型映射
end
  1. 前后端通信结构
// 签到类型
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;
}
  1. 数据库存盘结构
function SignInRewardComponent:SerializeToDB()if self:IsReady() thenlocal 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) dotable.insert(tb.allowIndexArray, index)endfor index, _ in pairs(self.tbAlreadyIndexSet) dotable.insert(tb.alreadyIndexArray, index)endfor index, _ in pairs(self.tbMissedIndexSet) dotable.insert(tb.missedIndexArray, index)endreturn tbendreturn nil
end

(2) 功能逻辑设计

  1. 管理类的配置加载

管理类在初始化时,填充当日的时间数据,并且将配置表内容复制一份到自己的内存管理。

function SignInRewardMgr:Init()self:Reset()self.tbNextDealingPlayerNode = nilself.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 thenreturn self.__localGmtSec;elseself.__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()) dolocal _, tbRewardInfo = self:_LoadOneSignRewardInfoConfig(szSignInType, tbInfo)if tbRewardInfo.bValid thenself.tbValidSignInRewardInfoTypeMap[szSignInType] = tbRewardInfoelseif tbRewardInfo.bWaitingStart thenself.tbWaitStartSignInRewardInfoTypeMap[szSignInType] = tbRewardInfoendend
end-- 加载具体类型签到描述配置
function SignInRewardMgr:_LoadOneSignRewardInfoConfig(szSignInType, tbInfo)assert(tbInfo)local result = falselocal tbRewardInfo = Lib:NewClass(self.tbSignInRewardInfo)tbRewardInfo.szSignInType = szSignInTypetbRewardInfo.nPeriodCount = tbInfo.nPeriodCounttbRewardInfo.szTitle = tbInfo.szTitletbRewardInfo.szContent = tbInfo.szContentlocal szFuncName = "Get" .. tbInfo.szXLSXName .. "Map"local fn = ConfigureManager[szFuncName]if not fn or Lib:IsEmptyTB(fn(ConfigureManager)) then-- 表格没有任何数据goto EXIT0endif tbRewardInfo.nPeriodCount <= 0 then-- 周期数必须大于0goto EXIT0endif not type(tbInfo.nStartTime) == "string" or not type(tbInfo.nEndTime) == "string" thengoto EXIT0endtbRewardInfo.nStartUnixSec = Lib:GetStringDate2Time(tbInfo.nStartTime .. self.DATE_FORMAT_SUFFIX)tbRewardInfo.nEndUnixSec = Lib:GetStringDate2Time(tbInfo.nEndTime .. self.DATE_FORMAT_SUFFIX)if tbRewardInfo.nStartUnixSec >= tbRewardInfo.nEndUnixSec thengoto EXIT0endif tbRewardInfo.nStartUnixSec > self:GetNowUnixSec() thentbRewardInfo.bWaitingStart = truegoto EXIT0endif tbRewardInfo.nEndUnixSec <= self:GetNowUnixSec() thengoto EXIT0endszFuncName = tbInfo.szXLSXName .. "ByIndex"fn = ConfigureManager[szFuncName]if fn thenrepeatlocal nCurrIndex = tbRewardInfo.nLocalDayMaxIndex + 1local tbRow = fn(ConfigureManager, nCurrIndex)if not tbRow thenbreakendtbRewardInfo.nLocalDayMaxIndex = nCurrIndextbRewardInfo.tbRewardDetailIndexMap = tbRewardInfo.tbRewardDetailIndexMap or {}tbRewardInfo.tbRewardDetailIndexMap[nCurrIndex] = self:_GenerateCommonRewardInfo(tbRow)until (false)endresult = true::EXIT0::tbRewardInfo.bValid = resultreturn result, tbRewardInfo
endfunction SignInRewardMgr:_GenerateCommonRewardInfo(tbConfigRow)assert(type(tbConfigRow) == "table")local tbRewardDetail = {}--[[-- expif tbConfigRow.exp and tbConfigRow.exp > 0 thenlocal 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 dolocal 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 dolocal tbDropItemInfo = {itemTemplateID = tonumber(tbRet[i]),count = tonumber(tbRet[i + 1]),}tbRewardDetail.itemInfos = tbRewardDetail.itemInfos or {}table.insert(tbRewardDetail.itemInfos, tbDropItemInfo)endreturn tbRewardDetail
end

更新完配置信息对应的内存数据后,还要更新与运行逻辑相关的内容。比如检查运营中的各个签到是否过期、检查等待开启运营的各个签到是否等够时间、更新用于分帧处理在线玩家签到信息的指针。

无论是在进入新的一天,还是配置内容更新,都希望将所有的在线玩家的签到信息都更新至当前时刻的状态。因此需要获取到所有在线玩家并一一处理。但是gameserver是分帧运行,如果在线玩家数量太多并且一口气处理所有玩家,就会导致一帧执行的时间特别长,进而导致网络数据处理和其他模块update工作滞后,严重的话可能引起套接字接受缓冲区堵满、网络延迟放大、带宽激增。避免这么严重的后果的最好方法就是分帧处理:首先保证有一个链表连接所有的在线玩家,保留一个指针,逐个遍历并处理,一帧处理几十个,保存指针,下一帧接着处理。其次保证玩家下线时能通知到该模块,比如一个尚未处理的在线玩家下线时,要检查该指针是不是指向它,如果是,将指针移动到下一个处理节点。

这里是获取在线玩家链表的最后一个节点,从后向前遍历。原因是从此以后进入游戏的玩家,在登录成功后就会有相应的检查和更新。如果从前向后遍历的话会重复处理,而且处理的个数有可能会随着大量玩家登录而增加。

-- 根据配置内容更新各个类型签到必要信息
function SignInRewardMgr:_OnUpdateNecessaryInfoByNewConfig()self:_UpdateExpiredSignInRewardInfo(self:GetNowUnixSec())self:_UpdateWaitStartSignInRewardInfo(self:GetNowUnixSec())self:_PrepareUpdateOnlinePlayerSignInRewardComponentOnNextDay()
end-- 更新可能过期的签到信息
function SignInRewardMgr:_UpdateExpiredSignInRewardInfo(nCurrentUnixSec)local tbExpiredRewardInfos = nilfor szSignInType, tbRewardInfo in pairs(self.tbValidSignInRewardInfoTypeMap) doif tbRewardInfo.nEndUnixSec <= nCurrentUnixSec thentbExpiredRewardInfos =  tbExpiredRewardInfos or {}tbExpiredRewardInfos[szSignInType] = tbRewardInfoendendfor szSignInType, tbRewardInfo in pairs(tbExpiredRewardInfos or Lib:GetEmptyTable()) dotbRewardInfo.bValid = falsetbRewardInfo.bWaitingStart = falseself.tbValidSignInRewardInfoTypeMap[szSignInType] = nilend
end-- 更新等待开始的签到信息
function SignInRewardMgr:_UpdateWaitStartSignInRewardInfo(nCurrentUnixSec)local tbStartedRewardInfos = nilfor szSignInType, tbRewardInfo in pairs(self.tbWaitStartSignInRewardInfoTypeMap) doif tbRewardInfo.nStartUnixSec <= nCurrentUnixSec thentbStartedRewardInfos =  tbStartedRewardInfos or {}tbStartedRewardInfos[szSignInType] = tbRewardInfoendendfor szSignInType, tbRewardInfo in pairs(tbStartedRewardInfos or Lib:GetEmptyTable()) dotbRewardInfo.bValid = truetbRewardInfo.bWaitingStart = falseself.tbWaitStartSignInRewardInfoTypeMap[szSignInType] = nilself.tbValidSignInRewardInfoTypeMap[szSignInType] = tbRewardInfoend
end-- 进入下一天时准备更新在线玩家的签到信息
function SignInRewardMgr:_PrepareUpdateOnlinePlayerSignInRewardComponentOnNextDay()-- 原值可能不为空,说明玩家的数据没处理完,但可能性极低,因此允许丢失self.tbNextDealingPlayerNode = nillocal player = PlayerMgr:GetLatestPlayerInGamingList()if player thenself.tbNextDealingPlayerNode = player:GetHostNode()end
end
  1. 管理类的update

update方法会每帧调用,但是除了处理在线玩家签到更新内容,其他的信息更新并不需要每帧都检查。所以根据当前业务情况,每秒检查一次足够了。每秒更新当前时间戳,并判断是否进入次日,如果是,执行进入次日的操作。

function SignInRewardMgr:Update(tick)self:_UpdateOnlinePlayerSignInRewardComponentPerFrame()if self.nNextTick > tick thenreturnendself.nNextTick = tick + self.CHECK_NEXT_DAY_UPDATE_INTERVALself:_SetNowUnixSec(KServerTime:UnixSec())if self:GetNowUnixSec() >= self:GetNextLocalDayUnixSec() thenself:_OnEnterNextLocalDay(self:GetNowUnixSec())end
end-- 每帧更新在线玩家的签到信息
function SignInRewardMgr:_UpdateOnlinePlayerSignInRewardComponentPerFrame()if not self.tbNextDealingPlayerNode thenreturnendlocal nDealCount = 0local tbCurrentNode = nillocal player = nilrepeattbCurrentNode = self.tbNextDealingPlayerNodeself.tbNextDealingPlayerNode = tbCurrentNode:GetPrev()player = tbCurrentNode:GetHost()if not player thenassert(false)breakend-- 通过Player拿到对应组件,更新组件签到信息for _, tbComponent in pairs(player:GetAllSignInRewardComponents() or Lib:GetEmptyTable()) dotbComponent:CheckAndUpdate()endif nDealCount >= self.DEAL_ONLINE_PLAYER_COUNT_PER_FRAME thenbreakenduntil(not self.tbNextDealingPlayerNode)
end-- 步入新的一天回调
function SignInRewardMgr:_OnEnterNextLocalDay(nCurrentUnixSec)self:_UpdateCurrentLocalDay(nCurrentUnixSec)self:_OnUpdateNecessaryInfoByNewConfig()
end

在每帧处理在线玩家签到更新的方法中,如果更新的玩家个数超过规定的一帧处理上线或者所有玩家处理完毕,就不会再进入这个操作中。否则会对每个玩家的所有签到组件进行更新,具体更新细节后面再说。

管理类步入新的一天,要做的事无非就是前边介绍过的,更新当日时间信息、更新与运行逻辑相关的签到配置信息。

  1. 签到组件的创建

一种类型签到组件只要在DB有签到数据或在有效期的情况下才会创建。相应的创建方法有2个。

function SignInRewardMgr:NewSignInRewardComponentFromDB(player, dbData)if dbData thenlocal tbRewardInfo = self.tbValidSignInRewardInfoTypeMap[dbData.signInType]if tbRewardInfo then local tbComponent = self:_MakeSignInRewardComponent()tbComponent:Init(player, dbData.signInType, dbData)return tbComponentendendreturn nil
endfunction SignInRewardMgr:NewSignInRewardComponent(player, szSignInType)local tbRewardInfo = self.tbValidSignInRewardInfoTypeMap[szSignInType]if tbRewardInfo then local tbComponent = self:_MakeSignInRewardComponent()tbComponent:Init(player, szSignInType)return tbComponentendreturn nil
endfunction SignInRewardMgr:DeleteSignInRewardComponent(tbComponent)if type(tbComponent) == "table" thenlocal player = tbComponent:GetBelongPlayer()if player thenif self.tbNextDealingPlayerNode == player:GetHostNode() thenself.tbNextDealingPlayerNode = player:GetHostNode():GetPrev()endif tbComponent:GetSignType() == "SIGN_IN_TYPE_ROOKIE" thenplayer:SetRookieSignInComponent(nil)endendtbComponent:UnInit()self:_FreeSignInRewardComponent(tbComponent)tbComponent = nilend
end

其中所有需要通过判断组件类型而选择执行的操作都属于无奈之举,如果开发时间更多一点的话,我会把不同类型的相同模板组件统一封装起来。

组件创建的行为比较简单,有DB数据就填充,没有就取默认值。

function SignInRewardComponent:Init(tbPlayer, szSignInType, dbData)assert(type(tbPlayer) == "table")assert(type(szSignInType) == "string")self:Reset()self.tbPlayer = tbPlayerself.szSignType = szSignInTypeself.tbAllowIndexSet = self.tbAllowIndexSet or {}self.tbAlreadyIndexSet = self.tbAlreadyIndexSet or {}self.tbMissedIndexSet = self.tbMissedIndexSet or {}self.tbIndexStateMap = self.tbIndexStateMap or {}if type(dbData) == "table" thenself.nRewardPeriodCount = dbData.rewardPeriodCount or 0self.nLastSignInUnixSec = dbData.lastSignInUnixSec or 0self.nLastSignInLocalDay = dbData.lastSignInLocalDay or 0self.nLastSignInMaxIndex = dbData.lastSignInMaxIndex or 0for _, index in pairs(dbData.allowIndexArray or Lib:GetEmptyTable()) doself:_SetAllowIndex(index, true)endfor _, index in pairs(dbData.alreadyIndexArray or Lib:GetEmptyTable()) doself:_SetAlreadyIndex(index, true)endfor _, index in pairs(dbData.missedIndexArray or Lib:GetEmptyTable()) doself:_SetMissedIndex(index, true)endend
end

签到组件的准备根据玩家登录过程分为2个阶段。第一个阶段是玩家初始化,第二个阶段是玩家真正进入游戏。

玩家初始化时如果有签到DB数据,就会调用AddRookieSignInComponentByDB创建组件。即使DB的签到类型可能此时已经过期,在SignInRewardMgr:NewSignInRewardComponentFromDB也会检查出来并且不会继续创建组件。

function Player:AddRookieSignInComponentByDB(dbData)self:SetRookieSignInComponent(SignInRewardMgr:NewSignInRewardComponentFromDB(self, dbData))
endfunction Player:SetRookieSignInComponent(tbComponent)self:PlayerInfo().rookieSignInComponent = tbComponent
end

如果第一个阶段已经成功创建了签到组件,第二个阶段就会更新组件的信息。反之如果没有创建签到组件,第二个阶段就会根据当前有效的签到类型,创建签到组件并填充组件信息。

-- TODO 代码有待优化
function SignInRewardMgr:OnPlayerEnterGame(player)local tbRookieSignInRewardComponent = nilfor szSignInType, tbRewardInfo in pairs(self.tbValidSignInRewardInfoTypeMap) doif szSignInType == "SIGN_IN_TYPE_ROOKIE" thentbRookieSignInRewardComponent = player:GetRookieSignInRewardComponent()if not tbRookieSignInRewardComponent thenplayer:SetRookieSignInComponent(self:NewSignInRewardComponent(player, szSignInType))endendend-- 更新新手签到tbRookieSignInRewardComponent = player:GetRookieSignInRewardComponent()if tbRookieSignInRewardComponent thentbRookieSignInRewardComponent:OnPlayerEnterGame()tbRookieSignInRewardComponent = nilend
end-- TODO 代码有待优化
function SignInRewardMgr:OnPlayerReconnect(player)local tbRookieSignInRewardComponent = nilfor szSignInType, tbRewardInfo in pairs(self.tbValidSignInRewardInfoTypeMap) doif szSignInType == "SIGN_IN_TYPE_ROOKIE" thentbRookieSignInRewardComponent = player:GetRookieSignInRewardComponent()if not tbRookieSignInRewardComponent thenplayer:SetRookieSignInComponent(self:NewSignInRewardComponent(player, szSignInType))endendend-- 更新新手签到tbRookieSignInRewardComponent = player:GetRookieSignInRewardComponent()if tbRookieSignInRewardComponent thentbRookieSignInRewardComponent:OnPlayerReconnect()tbRookieSignInRewardComponent = nilend
end

玩家进入游戏和玩家重连游戏的时候都要做相应的处理,因为玩家断线时已经从在线玩家的链表中移除了,这个过程中如果签到内容有更新并遍历在线玩家链表的话,是不会被处理到的。再重连后,必须做一次检查。

-- 用于进入游戏
function SignInRewardComponent:OnPlayerEnterGame()self:CheckAndUpdate()
end-- 用于重连
function SignInRewardComponent:OnPlayerReconnect()self:CheckAndUpdate()
end

检查更新的详细操作后面再说。

  1. 签到组件的更新逻辑

组件针对自己的检查:
判断当前是否有效,若有效则检查更新签到周期,更新当日的签到信息。否则,销毁自己。

-- 检查是否需要更新自己的周期以及天数索引,或者删除自己
function SignInRewardComponent:CheckAndUpdate()if SignInRewardMgr:IsValidSignInType(self:GetSignType()) thenself.bReady = trueself:_CheckCurrentPeriodCount()self:_UpdateTodayRewardInfo()elseSignInRewardMgr:DeleteSignInRewardComponent(self)end
end-- 检查当日奖励周期
function SignInRewardComponent:_CheckCurrentPeriodCount()local nRewardPeriodCount = SignInRewardMgr:GetCurrentRewardPeriodCount(self:GetSignType())assert(nRewardPeriodCount > 0)if self:GetRewardPeriodCount() == nRewardPeriodCount thenreturnendif self:GetRewardPeriodCount() > nRewardPeriodCount thenLogErrWithFields({playerID = self:GetBelongPlayer():GetID(), playerRewardPeriodCount = self:GetRewardPeriodCount(), currentRewardPeriodCount = nRewardPeriodCount}, "player reward period is more than current !")endself:_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 thennTodayRewardIndex = 1elselocal nMissedDay = nToday - self:GetLastSignInLocalDay() - 1    -- 不能包含当日if nMissedDay > 0 then-- 有遗漏签到local nMaxRewardIndex = SignInRewardMgr:GetMaxRewardIndexBySignType(self:GetSignType())for i = 1, nMissedDay dolocal nMissedIndex = self:GetLastSignInMaxIndex() + iif nMissedIndex > nMaxRewardIndex thenbreakendself:AddMissedIndex(nMissedIndex)nTodayRewardIndex = nMissedIndex + 1end-- 避免超过最大签到日if nTodayRewardIndex > nMaxRewardIndex thennTodayRewardIndex = 0endelseif nMissedDay == 0 then-- 当日为上次签到的次日nTodayRewardIndex = self:GetLastSignInMaxIndex() + 1local nMaxRewardIndex = SignInRewardMgr:GetMaxRewardIndexBySignType(self:GetSignType())-- 避免超过最大签到日if nTodayRewardIndex > nMaxRewardIndex thennTodayRewardIndex = 0endelseif nMissedDay == -1 then-- 当日已经签到过-- do nothingelse-- 检测到时间回拨过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 nothingendendendif nTodayRewardIndex > 0 thenself:AddAllowIndex(nTodayRewardIndex)endself:_SetLastSignInInfo(nNowUnixSec, nToday, self:GetLastSignInMaxIndex())-- self:DebugShowInfo()
end

更新当日的签到信息的大概步骤:

  • 上次签到日如果是0说明是首次签到,更新上次签到日为当天,添加当天到允许领取奖励的天数。
  • 如果当日是上次签到的次日,取上次签到的下一天数,注意不能超过该类型最大签到日。
  • 如果当日就是上次签到日,说明已经签到过,什么也不做。
  • 如果当日超过上次签到日大于1天,说明中间有遗漏的天数,把这些添加到遗漏的集合。同时将当日添加到允许领取奖励的天数。还要注意不能超过该类型最大签到日。
  • 如果当日提前于上次签到日,说明时间回拨过,打个日志,只要更新上次签到日就行。

其中,涉及到的方法如下,都带有必要的检查。类似添加允许领取的天数的话,那么天数肯定是正数并且不能在已领取的集合中,其他的不一一细说了:

-- 添加允许领取的天数索引,会检查索引是否合理
function SignInRewardComponent:AddAllowIndex(nIndex)local result = falseif not self:IsReady() thengoto EXIT0endif nIndex <= 0 or self.tbAlreadyIndexSet[nIndex] thengoto EXIT0endself:_SetMissedIndex(nIndex, false)self:_SetAllowIndex(nIndex, true)if self:GetLastSignInMaxIndex() < nIndex thenself:_SetLastSignInInfo(self:GetLastSignInUnixSec(), self:GetLastSignInLocalDay(), nIndex)endresult = true::EXIT0::return result
end-- 添加已经领取的天数索引,会检查索引是否合理
function SignInRewardComponent:AddAlreadyIndex(nIndex)local result = falseif not self:IsReady() thengoto EXIT0endif nIndex <= 0 thengoto EXIT0endif not self.tbAllowIndexSet[nIndex] and not self.tbMissedIndexSet[nIndex] thengoto EXIT0endself:_SetAllowIndex(nIndex, false)self:_SetMissedIndex(nIndex, false)self:_SetAlreadyIndex(nIndex, true)result = true::EXIT0::return result
end-- 添加错过领取的天数索引,会检查索引是否合理
function SignInRewardComponent:AddMissedIndex(nIndex)local result = falseif not self:IsReady() thengoto EXIT0endif nIndex <= 0 or self.tbAllowIndexSet[nIndex] or self.tbAlreadyIndexSet[nIndex] thengoto EXIT0endself:_SetMissedIndex(nIndex, true)result = true::EXIT0::return result
endfunction SignInRewardComponent:_SetAllowIndex(nIndex, bNotReset)assert(type(nIndex) == "number")if bNotReset thenself.tbAllowIndexSet[nIndex] = trueself.tbIndexStateMap[nIndex] = SignInRewardMgr.APPLY_SIGN_IN_ALLOWelseself.tbAllowIndexSet[nIndex] = nilself.tbIndexStateMap[nIndex] = nilend
endfunction SignInRewardComponent:_SetAlreadyIndex(nIndex, bNotReset)assert(type(nIndex) == "number")if bNotReset thenself.tbAlreadyIndexSet[nIndex] = trueself.tbIndexStateMap[nIndex] = SignInRewardMgr.APPLY_SIGN_IN_ALREADYelseself.tbAlreadyIndexSet[nIndex] = nilself.tbIndexStateMap[nIndex] = nilend
endfunction SignInRewardComponent:_SetMissedIndex(nIndex, bNotReset)assert(type(nIndex) == "number")if bNotReset thenself.tbMissedIndexSet[nIndex] = trueself.tbIndexStateMap[nIndex] = SignInRewardMgr.APPLY_SIGN_IN_MISSEDelseself.tbMissedIndexSet[nIndex] = nilself.tbIndexStateMap[nIndex] = nilend
end
  1. 客户端请求处理

目前只接受2种请求,获取签到状态和领取奖励。涉及到的很多判断条件就略过了,这里主要说下领取奖励功能逻辑:

  • 将请求的索引天数添加到已领取。
  • 得到奖励道具并添加到玩家背包。
    这里多做了一步错误处理是,如果已经更改了索引天数的状态,但是添加道具时失败。应将状态回退。
-- 处理玩家领取签到奖励
function SignInRewardMgr:_HandlePlayerApplySignInReward(tbComponent, tbRewardInfo, nIndex)assert(type(tbComponent) == "table")assert(type(tbRewardInfo) == "table")assert(type(nIndex) == "number")-- 将该天数索引状态改为已领取if not tbComponent:AddAlreadyIndex(nIndex) thenreturn falseendlocal player = tbComponent:GetBelongPlayer()local tbRewardDetail = tbRewardInfo.tbRewardDetailIndexMap[nIndex]if player and tbRewardDetail and tbRewardDetail.itemInfos thenlocal tbAddItemInfoArray = nilfor _, tbDropItemInfo in pairs(tbRewardDetail.itemInfos) dotbAddItemInfoArray = tbAddItemInfoArray or {}table.insert(tbAddItemInfoArray, {templateID = tbDropItemInfo.itemTemplateID,count = tbDropItemInfo.count,})end-- 判断背包是否有剩余空间,注意多个道具要一起判断,背包模块提供多个物品添加接口,且具备原子性if tbAddItemInfoArray and not player:GetBags():AddMultiItem(tbAddItemInfoArray) thenassert(tbComponent:RemoveAlreadyIndex(nIndex, false, false), "can not lose !")return falseendendreturn true
end-- @bClear 是否完全清除该索引的存在
-- @bToMissed 不完全清除该索引的情况下生效,true转移到miss集合,false转移到allow集合
function SignInRewardComponent:RemoveAlreadyIndex(nIndex, bClear, bToMissed)local result = falseif not self:IsReady() thengoto EXIT0endif nIndex <= 0 thengoto EXIT0endif not self.tbAlreadyIndexSet[nIndex] thengoto EXIT0endself:_SetAlreadyIndex(nIndex, false)if not bClear thenif bToMissed thenself:_SetMissedIndex(nIndex, true)elseself:_SetAllowIndex(nIndex, true)endendresult = true::EXIT0::return result
end

四.不足与展望

  1. 有些地方要根据签到类型做对应的操作,不利于拓展。可以仿照unity实现个类似的组件系统,不同组件对象有相似部分可以打同一个tag,方便批量处理。
  2. 签到类型无效的情况下,并没有机制去清除DB内的组件数据。这本身并不困难,但是如果可以实现一个组件系统的话,组件数据读写的方式和结构可能都会有些变化,现在做太多可能意义不大,而且策划需求说变就变也不是没可能。
  3. 时间检查的机制可以抽出来做一个公共的中间层,不同功能模块可以共用。否则每个人有涉及到时间的开发任务都要自己再做一遍。

游戏业务——多日签到功能设计相关推荐

  1. 英伟达Q2营收大涨50%,创下历史新高,游戏业务已不是最大收入来源

    鱼羊 发自 凹非寺 量子位 报道 | 公众号 QbitAI Q2营收同比大涨50%,达38.7亿美元,创下历史新高. 英伟达发布这样一份财报之后,股价却在盘后交易中再度下跌超2%. 8月19日盘后,英 ...

  2. 应对游戏业务的四大“崩溃”场景有妙招,安全畅玩不是梦!

    摘要:本文详细介绍了游戏类业务常见的攻击场景及影响,针对具体的受攻击场景提出有效的解决措施. [场景汇总] [解决措施] 场景一:DDoS & CC 现象描述 攻击者模拟海量正常的服务请求,占 ...

  3. 张朝阳:搜狐Q3广告业务稳健游戏业务超预期 有望全年实现盈利

    11月16日,搜狐公布2020年第三季度财报,营收1.58亿美元,其中品牌广告收入为4100万美元,环比增长8%.受游戏业务增长驱动和搜狐业务的稳定表现,搜狐预期Q4将重回盈利. 在当天的媒体沟通会上 ...

  4. 日赚4.5亿!腾讯游戏业务增长惊人:最赚钱的还是它两!

    11月12日,腾讯发布2020年第三季度财报,财务数据依然亮眼. 财报显示,腾讯第三季度营收1254.5亿元,同比增长29%:按非国际财务报告准则,权益持有人应占盈利为323.03亿元,同比增长32% ...

  5. 缓存系统在游戏业务中的特异性

    版权声明:本文由韩伟原创文章,转载请注明出处:  文章原文链接:https://www.qcloud.com/community/article/243 来源:腾云阁 https://www.qclo ...

  6. 悬赏任务h5系统源码接单发单平台支持游戏试玩签到

    #白菜兼职悬赏任务系统 ##基于thinkphp+uniapp开发,接单发单平台支持游戏试玩签到 ###主要功能 ####支持前后端发布 ####支持会员系统自定义设置 ####支持任务限时设置 ## ...

  7. 完美世界2020业绩快报:游戏业务年营收增35%

    4月14日,完美世界发布2020年业绩快报及2021年Q1业绩预告.2020年完美世界实现营收102.25亿元,同比增长27.19%:其中游戏业务实现营收92.62亿元,同比增加35%,游戏业务实现净 ...

  8. 腾讯游戏业务竟然是这样利用低代码平台的 | ArchSummit

    点击上方"服务端思维",选择"设为星标" 回复"669"获取独家整理的精选资料集 回复"加群"加入全国服务端高端社群「后 ...

  9. 阿里云网络解决方案架构师任江波:全球一张网,支撑游戏业务高效互联

    2022 年 8 月 30 日,阿里云用户组(AUG)第 9 期活动在北京举办.活动现场,阿里云网络解决方案架构师任江波,向参会企业代表分享了全球一张网,支撑游戏业务高效互联.本文根据演讲内容整理而成 ...

最新文章

  1. MDSF:DSL(Domain Specific Language)介绍
  2. boost::log::sinks::simple_event_log_backend用法的测试程序
  3. Python中is和==的区别
  4. php 如何获取表格数据类型,使用phpword获取doc中的表格数据
  5. 一个计算机爱好者的不完整回忆(二十八)关于计算机书籍
  6. linux加密框架 crypto 算法管理 - 创建哈希算法实例
  7. 解决删除镜像时image is referenced in multiple repositories
  8. 前端_网页编程 Ajax加强
  9. 信息学奥赛一本通 1073:救援 | OpenJudge NOI 1.5 19:救援
  10. 页面常见跳转的方法和选择
  11. 转iPhone开发的门槛
  12. [地图代数]处理DEM中的高程异常值——ArcGIS栅格计算的应用
  13. riso1855使用说明_理想CV1855驱动下载 理想CV1855打印机驱动 v20170627 32bit+64bit 免费安装版 下载-脚本之家...
  14. Filezilla Server使用教程
  15. 计算机ip变,为什么电脑IP地址总是自动改变
  16. 使用DebenuPDFLibrary导出微信中的照片
  17. html5 移动端上传图片插件,H5文件上传插件easyUpload.js
  18. 苹果手机网页选择框 下拉框点击放大处理
  19. STM32 汉字库+ascii字库 存放到 flash中
  20. 国内外快递公司名称一览表

热门文章

  1. 【C++】优先级队列
  2. 庞果网之寻找直方图中面积最大的矩形
  3. java 时间片_Java 实现--时间片轮转 RR 进程调度算法
  4. 身边的套套故事:有些人非得真在行
  5. 如何清理iphoto图片
  6. mysql 1819_mysql5.7报错ERROR 1819解决办法
  7. 【IDEA】打包插件
  8. 初中英语人教版教案三-Leo老师
  9. 小米发布新系统VelaOS
  10. Java自学心得,从小白到高手的蜕变