實作背景
該系列打了這麼久的地基,還沒寫過業務代碼。實際上我是這麼考慮的,因為不同遊戲項目的業務代碼差異性非常大,業務邏輯隻有你想不到,沒有策劃大大想不到的。要想做一個拓展性強的架構,按理說業務代碼越少越好。但再三考慮,還是要盡量構造出一些遊戲公共的業務,也算是這套架構的示例代碼吧。
玩過手機遊戲的人應該知道,不管是什麼遊戲,都會經常出現每日重置的邏輯。比如,玩家打副本每日有一個上限次數,玩家購買某種商品也會有每日購買最大次數……這些業務有個特點,就是每天有上限,隔天次數會被重置。至于隔天是0點重置,還是5點重置,就要看項目的營運更新時間了。
每日重置業務的注意事項
每日重置的邏輯看似簡單,要完美實作還是要花點功夫的。因為每日重置有這樣的特點:
1. 在家玩家和離線玩家的處理方式是不同的;
2. 每日重置的時候要考慮線程安全,避免出現并發問題。
針對第一個問題,我們的處理方式是,處理在家玩家,我們通過周遊線上玩家清單,依次執行重置業務。而離線玩家就比較麻煩,我們不可能把所有不線上的玩家從資料庫撈出來對其處理。實際上,我們可以在玩家登入的時候處理,玩家每天登入的時候,我們将玩家身上的重置辨別與系統公共的重置時間進行比較,若不相同,則可以執行重置業務。
針對第二個問題,考慮線程并發問題,就會跟系統所使用的異步線程模型相結合。由于我們的線程模型是根據玩家的分發id作負載均衡的。是以,每日重置也應該與其相适應。
quartz在遊戲中的應用
quartz是一個非常優秀的作業排程架構。在遊戲開發中,我們經常用quartz來實作定點任務和頻率任務。
使用quartz隻需要引入相關的jar包,然後使用配置檔案對任務觸發進行配置。
每日重置業務實作
1. 首先,我們在目前線程模型的基礎上,定義一種timer事件。該事件需要滿足,對玩家業務的執行來說是線程安全的。同時,為了拓展性,該事件可以設定timer的執行次數。要達到線程安全,隻要讓timer事件繼承自AbstractDistributeTask就可以了。具體原因,可參考該系列關于線程模型的文章 手遊服務端架構之消息線程模型
/**
* timer任務
* @author kingston
*/
public abstract class TimerTask extends AbstractDistributeTask {
private int currLoop;
/** 小于0表示無限任務 */
private int maxLoop;
public TimerTask(int distributeKey) {
this(distributeKey, 1);
}
public TimerTask(int distributeKey, int maxLoop) {
this.distributeKey = distributeKey;
this.maxLoop = maxLoop;
}
public void updateLoopTimes() {
this.currLoop += 1;
}
public boolean canRunAgain() {
if (this.maxLoop <= 0) {
return true;
}
return this.currLoop < this.maxLoop;
}
}
2. 玩家每日重置timer事件(DailyResetTask),隻要繼承上面的TimerTask就可以了
public class DailyResetTask extends TimerTask {
private Player player;
public DailyResetTask(int distributeKey, Player player) {
super(distributeKey);
this.player = player;
}
@Override
public void action() {
System.err.println("玩家"+player.getName()+"進行每日重置");
PlayerManager.getInstance().checkDailyReset(player);
}
}
3.編寫quartz作業。假設我們每日重置的時間發生在每天5點,那麼我們可以在jobs.xml裡加上這樣的配置
<!-- 每日重置 -->
<job>
<name>DailyResetJob</name>
<group>DEFAULT</group>
<job-class>com.kingston.game.cronjob.DailyResetJob</job-class>
</job>
<trigger>
<cron>
<name>DailyResetJobTrigger</name>
<group>DEFAULT</group>
<job-name>DailyResetJob</job-name>
<job-group>DEFAULT</job-group>
<cron-expression>0 0 5 * * ?</cron-expression>
<!-- 每天05:00運作 -->
</cron>
</trigger>
4. quartz在調試的時候,肯定是在自己的線程上跑的。重置job觸發的時候,我們周遊所有玩家,對每個玩家執行每日重置業務。這個過程中,我們需要将觸發點封裝成timer事件,丢到主業務線程裡處理。這些操作在DailyResetJob上實作。
/**
* 每日5點定時job
* @author kingston
*/
@DisallowConcurrentExecution
public class DailyResetJob implements Job {
private Logger logger = LoggerSystem.CRON_JOB.getLogger();
@Override
public void execute(JobExecutionContext arg0) throws JobExecutionException {
logger.info("每日5點定時任務開始");
long now = System.currentTimeMillis();
SystemParameters.update("dailyResetTimestamp", now);
Collection<Player> onlines = PlayerManager.getInstance().getOnlinePlayers().values();
for (Player player:onlines) {
int distributeKey = player.distributeKey();
//将事件封裝成timer任務,丢回業務線程處理
TaskHandlerContext.INSTANCE.acceptTask(new DailyResetTask(distributeKey, player));
}
}
}
5. 對于離線玩家的處理,我們需要将當次重置的時刻記錄在一張公共的系統表(systemrecord)。在DailyResetJob裡的
SystemParameters.update("dailyResetTimestamp", now);
玩家登入的時候檢查一下,在LoginManager.handlSelectPlayer()方法。登入邏輯發生在玩家發出登入消息,本來就在架構裡的線程模型,本身就是線程安全的了。
/**
* 選角登入
* @param session
* @param playerId
*/
public void handleSelectPlayer(IoSession session, long playerId) {
Player player = PlayerManager.getInstance().get(playerId);
if (player != null) {
//綁定session與玩家id
session.setAttribute(SessionProperties.PLAYER_ID, playerId);
//加入線上清單
PlayerManager.getInstance().add2Online(player);
SessionManager.INSTANCE.registerNewPlayer(playerId, session);
//推送進入場景
ResPlayerEnterSceneMessage response = new ResPlayerEnterSceneMessage();
response.setMapId(1001);
MessagePusher.pushMessage(session, response);
//檢查日重置
PlayerManager.getInstance().checkDailyReset(player);
}
}
至此,遊戲的每日重置業務的介紹到這裡就結束啦。
java遊戲伺服器架構系列完整的代碼請移步github ->> jforgame