在游戏的设计中,我们需要设计一个贸易利润和综合实力排行榜。在排行榜中,我们有这样的需求:
A: 排行榜4天更新一次,也就是,需要统计玩家4天内贸易利润和综合实力
B: 上榜的玩家有奖励发送。
C: 综合实力是统计全服玩家,如果玩家分库存储,需要遍历所有的玩家库
D:4天后,删除旧数据,更新原来的数据。
E:排行榜是全服共享的
F:上榜的玩家的信息是动态的,如,玩家的等级,玩家的国籍(游戏中,可以允许玩家修改国籍)
设计思路:
创建一个rank表,统计玩家4天内的贸易利润。
创建一个奖励配置rank_award表,给上榜的玩家发放奖励。
详细设计:
在设计rank表的时候,我们需要对“4”天这个时间段作限制。我们考虑的是,以某一个时间作为“标准”,计算4天后的时间,作为一个截止时间,也就是在这个截止时间内,统计玩家的获取的利润,到了这个时间点后,停止统计玩家获取的利润,同时,结算给玩家奖励,然后清除排行榜,同时重新统计玩家的信息。
游戏奖励表的设计:
award 的对应是: 1:100;2:200;这样的格式。
rank表的设计:
this_year 这个字段是一个截止时间的概念,在我们游戏中,有一个游戏年的概念,是4天作为一个游戏年,所以,我们正好使用这个游戏年,作为排行榜时间段。
这个this_year是这样被计算出来的:
游戏的开始元年是: startTime = 2013/1/1
当前游戏的天数: day = max(1, ceil(time() - strtotime(startTime))/24*60*60);
当前游戏的年数(4天作为一年): year = str_pad(ceil(day/4), 4,0 str_pad_left);
当前游戏的季节: season= (day%4) (0:春天 1:夏天 2:秋天 3:冬天)
这样做的目的,避免了每次插入用户的一条记录时,都需要做时间段的比对:
public static function incrYearlyRankStats($uid, $field, $step = 1) { $where = array( 'this_year' => $year, 'uid' => $uid, ); $result = $this->where($where)->increment($field, $step); // 更新影响行数为0 if (! $result) { // 如果记录确实不存在,则插入一条新记录 if (! $this->where($where)->fetchCount()) { // 执行插入 $setArr = array( 'this_year' => $year, 'uid' => $uid, $field => $step, ); $this->insert($setArr); } } return true; }
获取我的排名:
public function getMyRankNo($year, $uid, $field, $value = null) { if (null === $value) { $value = $this->getMyFieldStats($year, $uid, $field); } $where = array( 'this_year' => $year, $field => array('>', $value) ); $rank = $this->where($where)->fetchCount(); // 这里是一个比较新颖的做法,得到我的排行 return $rank + 1; }
注意并列排名的问题:
/** * 对列表数组标记排位序号(支持并列排位) * * @param array $topList 已降序排好的列表数组 * @param string $scoreField 排名比较字段 * @return void */ public static function decorateRankNo(array $topList, $scoreField) { $rankNo = 1; $lastScore = 0; foreach ($topList as &$value) { if ($value[$scoreField] < $lastScore) { $rankNo++; } $lastScore = $value[$scoreField]; $value['rank_no'] = $rankNo; } return $topList; }
综合实力榜,涉及到玩家的分库问题,需要遍历所有的玩家库:
public static function getCombatPowerRank() { $list = array(); // 遍历所有用户分库,执行清理 for ($i = 1; $i <= DIST_USER_DB_NUM; $i++) { if ($distList = Dao('Dist_User')->setDs($i)->getCombatPowerTopUsers(self::RANK_LIMIT)) { $list = array_merge($list, $distList); // 使用array_merge()做数组的合并 } } // 重新排序 $list = Helper_Array::multiSort($list, array( 'combat_power' => SORT_DESC )); // 截取前N名 $list = array_slice($list, 0, self::RANK_LIMIT); // 使用array_slice()对数组进行切割 // 对列表数组标记排位序号(支持并列排位) return self::decorateRankNo($list, 'combat_power'); }
给玩家发放奖励:
在游戏中,我们经常需要给玩家发放奖励,所以我们把发奖励这样的一个操作作为了独立的类封装起来。这样的做法,避免了很多代码的重复。在我们游戏奖励的格式是key=>value 例如,有一种奖励是: 100个金币和200个银币 award = 100:glod; 200:silver;
在我们的游戏中,有两种发奖方式: 按照权重发奖,和发指定的所有的奖励。
按照权重发奖:
/** * 按权重发放其中一种奖励 * * @param string $arrStr * @return array */ public function sendOneByWeight($awardStr) { $return = array();
// 解析奖励的权重 if (! $awardConfig = self::decodeAwardString($awardStr)) { return $return; } // 按权重命中一种奖励 $awardKey = Helper_Probability::hitByWeightField($awardConfig, 'weight'); // 执行发奖 $return = $this->__sendProcess($awardConfig[$awardKey]); return $return; }
解析奖励的权重:
/** * 解析奖励配置字符串 * * @param string $arrStr 格式:奖励类型:ID:数量:权重;... * @return array */ public static function decodeAwardString($arrStr) { if (! $arrStr || ! $arrStr = explode(';', $arrStr)) { return array(); } $return = array(); foreach ($arrStr as $str) { $strs = explode(':', $str); // 奖励类型 $awardType = $strs[0]; // 1:银币 2:金块 3:声望 4:角色经验 5:船只经验 6:行动力 7:精力 8:HP 9:属性点 if ($awardType >= 1 && $awardType <= 9) { $uniqId = null; $amount = isset($strs[1]) ? $strs[1] : null; $weight = isset($strs[2]) ? $strs[2] : null; } // 其他类型 else { $uniqId = isset($strs[1]) ? $strs[1] : null; $amount = isset($strs[2]) ? $strs[2] : null; $weight = isset($strs[3]) ? $strs[3] : null; } $return[] = array( 'type' => $awardType, 'uniq_id' => $uniqId, 'amount' => $amount, 'weight' => $weight, ); } return $return; }
按照权重获取命中率:
public static function hitByWeightField($probArr, $weightField = 'weight') { if (count($probArr) == 1) { return key($probArr); } // 概率数组的总概率精度 $proSum = Helper_Array::sum($probArr, $weightField); // 概率数组循环 foreach ($probArr as $key => $value) { $weight = $value[$weightField]; $randNum = mt_rand(1, $proSum); if ($randNum <= $weight) { return $key; } $proSum -= $weight; } return false; }
执行发放奖励:
/** * 内部方法:执行发奖 * * @param array $award * @return mixed */ private function __sendProcess(array $award) { // 返回结果集 $return = array(); switch ($award['type']) { // 银币 case 1: $this->_user->base->addSilver($award['amount']); $return['message'] = ICO_SILVER . $award['amount']; break; // 金块 case 2: $this->_user->base->addGold($award['amount']); $return['message'] = ICO_GOLD . $award['amount']; break; // 声望 case 3: $this->_user->base->addReputation($award['amount']); $return['message'] = ICO_REPT . $award['amount']; break; // 主人公经验 case 4: $this->_user->base->addExp($award['amount']); $return['message'] = ICO_EXP . $award['amount']; break; // 舰队船只经验 case 5: $this->_user->ship->addFleetExp($award['amount']); $return['message'] = $award['amount'] . _('船只EXP'); break; // 行动力 case 6: $this->_user->restore->updateWithOffset(array('move' => $award['amount'])); $return['message'] = ICO_MOVE . $award['amount']; break; // 精力 case 7: $this->_user->restore->updateWithOffset(array('energy' => $award['amount'])); $return['message'] = ICO_ENERGY . $award['amount']; break; }