一種手遊中實時戰鬥系統的設計思路
引言
現在的手遊玩法越來越複雜,特別是戰鬥系統,再也不是以前那種簡單的回合制模式。越來越多的手遊採用了實時戰鬥的模式(如刀塔傳奇),玩法有點類似於以前的即時戰略遊戲,這對於程式設計提出了更高的要求。本文提出了一種手遊中實時戰鬥系統可行的設計思路。
設計需求
實時戰鬥,不同於早期頁遊和手遊單純的看戰報或回合制模式,整個戰鬥過程是流暢和連貫的,人物的移動、攻擊、技能釋放都不會讓玩家感覺到停滯,整體感覺類似於傳統的即時戰略遊戲(魔獸、星際等),玩家在遊戲中的指令(如釋放技能)可以實時得到執行。
這裡帶來的問題是,如何設計一個穩定且高效的戰鬥系統,來滿足多人戰鬥時可能的高併發;不會因為高併發對伺服器造成過重的負擔,不會對玩家帶來糟糕的延時體驗;同時數目繁多的兵種和技能要能夠穩定有序地工作在這個系統中,不會讓程式設計師疲於應付而無所適從。
下面針對這些需求提出了一種設計思路。
設計要點:
記憶體化
當玩家線上人數很多時,如果還是將每次資料修改入庫,勢必會帶來很大的cpu開銷。筆者曾經參與一個專案,當同時線上人數達到500時,伺服器用於mysql的cpu佔用率飆到了800%。後來經過分析,有很大一部分資料沒必要實時入庫,例如戰場上的NPC資料,相對不敏感,即使伺服器重啟也無所謂,這部分資料可以全走記憶體;另有一部分玩家相關資料可以採用非同步儲存的方式,戰鬥執行緒直接操作記憶體,另有一監控執行緒視情況每隔一段時間將記憶體資料刷入資料庫。
單執行緒
也許你會說,現在的多核伺服器為什麼還要用單執行緒?這是因為單執行緒有它的好處,一是不用費心費力去解決死鎖等併發問題,通常一個先後關係造成的死鎖問題會佔用程式設計師大量的解決時間;二是有了前面的記憶體化,戰鬥執行緒不再會因為資料庫讀寫等耗時操作而卡幀,所以我們完全可以用這樣一個模型來解決問題:只有一個後臺執行緒在逐幀迴圈,每幀的戰鬥資料推送前端;玩家的操作(如釋放技能)不直接執行,而是交由後臺執行緒排隊後逐個執行。執行的時刻可能是當前幀,或者推遲到下一幀,總之由後臺執行緒統籌規劃。這樣就避免了因為併發帶來的一些未知問題。
分解
我們來比較一下兩種程式設計思路:一是把所有的戰鬥邏輯都寫在後臺執行緒裡,一大堆if-else和for迴圈耦合在一起;二是將複雜問題分解到很多類中,每個類只負責處理它應該處理的事情,後臺執行緒做的事情只是按一定的順序把這些類組織起來。可以明顯看到第二種方法更加清爽,程式碼可維護性更好,程式設計師也更喜歡。事實上,很多戰鬥系統都同樣可以分解為battleUnit, state, skill, buff等基本的單元。下面會專門舉例說明。
舉例:
下面這個例子假定戰鬥發生在一個戰場(FightScene)中,戰場中有許多戰鬥單位(FightUnit),有一個戰鬥引擎(FightEngine)負責開啟後臺執行緒,每幀遍歷一次戰場中的各個戰鬥單位,進行相應的動作。玩家釋放技能的操作,由事件(Event)的方式通知對應的戰鬥單位,更改它的狀態機使之進入技能狀態(SkillState)並執行釋放技能和新增buff(Buff)的操作。整個系統只有一個執行緒,戰鬥過程模組化,結構清晰,易於擴充套件。
// buff
public interface Buff {
// 進入時呼叫
public void enter();
// 退出時呼叫
public void exit();
// 每幀執行
public void tick(long interval);
}
// 事件
public interface Event {
}
import java.util.List;
// 戰場
public class FightScene {
// 攻方列表
private List<FightUnit> attList;
// 守方列表
private List<FightUnit> defList;
// 後臺執行緒每隔一幀執行一次
public void tick(long interval) {
for (FightUnit unit : attList)
unit.tick(interval);
for (FightUnit unit : defList)
unit.tick(interval);
}
// 尋找指定戰鬥單位
public FightUnit findUnit(int side, int index) {
if (side == 1) {
return attList.get(index);
}
else {
return defList.get(index);
}
}
}
import java.util.List;
// 戰鬥單位(玩家或者npc)
public class FightUnit {
// 事件列表
private List<Event> eventList;
// buff列表
private List<Buff> buffList;
// 當前狀態(狀態機)
private State state;
// 新增事件
public void addEvent(Event event) {
eventList.add(event);
}
// 新增buff
public void addBuff(Buff buff) {
buffList.add(buff);
}
// 每幀執行
public void tick(long interval) {
for (Event event : eventList) {
if (event instanceof SkillEvent){
int skillId = ((SkillEvent)event).getSkillId();
// 退出舊的狀態,進入新的狀態
state.exit();
state = new SkillState(this, skillId);
state.enter();
}
}
for (Buff buff : buffList) {
buff.tick(interval);
}
state.tick(interval);
}
}
// 戰鬥引擎
public class FightEngine {
private FightScene fightScene;
// 幀間隔
public static final long TICK_INTERVAL = 50;
// 啟動戰鬥執行緒
public void startFightThread() {
new Thread(){
public void run() {
while (true) {
try {
long startTime = System.currentTimeMillis();
fightScene.tick(TICK_INTERVAL);
long endTime = System.currentTimeMillis();
// 補足一幀剩餘時間
Thread.sleep(TICK_INTERVAL - (endTime - startTime));
} catch (Exception e) {
// TODO
}
}
}
}.start();
}
// 釋放技能(這裡是玩家操作)
public void releaseSkill(int skillId, int side, int index) {
/*
* 判定條件(能量不足、戰鬥已結束等)
* TODO
* ...
*
* */
// 新增事件到戰鬥單元
FightUnit unit = fightScene.findUnit(side, index);
unit.addEvent(new SkillEvent(skillId));
}
}
// 技能事件(一種事件的型別)
public class SkillEvent implements Event{
// 技能id
private int skillId;
public SkillEvent(int skillId) {
this.skillId = skillId;
}
public int getSkillId(){
return skillId;
}
}
// 技能狀態(一種狀態型別)
public class SkillState implements State{
// 技能id
private int skillId;
// 戰鬥單位
private FightUnit unit;
public SkillState(FightUnit unit, int skillId) {
this.unit = unit;
this.skillId = skillId;
}
// 進入時呼叫
public void enter() {
}
// 離開時呼叫
public void exit() {
}
// 每幀執行
public void tick(long interval) {
/*
* 釋放技能的邏輯(一連串令人眼花繚亂的效果...)
* TODO
* ...
*
* */
// 新增buff(假定這個技能會給自己加buff)
Buff buff = /*...*/
unit.addBuff(buff);
buff.enter();
}
}
// 戰鬥單位的狀態
public interface State {
// 每幀執行
public void tick(long interval);
// 進入時呼叫
public void enter();
// 離開時呼叫
public void exit();
}