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

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

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

一.需求分析

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

  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 = 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

  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() 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
(2) 功能逻辑设计
  1. 管理类的配置加载

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

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(<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值