在一篇文章中,我更多的是從遊戲理論的角度,討論了戰鬥的系統的設計。這篇文章中,我將從程序的角度,以一款航海類遊戲爲例,實現戰鬥系統。
在航海類遊戲中,戰鬥角色是出海的船隻,一次出海的船隻的數量有限定,船隻可以裝配火炮,護甲,船帆等裝備,船隻還可以通過裝配船長來獲取技能。技能的發動是有概率的。戰鬥規則是,在20個回合內,如果把對方所有的船隻擊沉,即贏得戰鬥勝利,否則未平局。
戰鬥流程大致是這樣:
這個開炮的具體的算法,我會在後面詳細的貼出代碼。
當我們瞭解了戰鬥的大致情況,並把所有的準備工作做好之後 (選擇了參戰的船隻, 船隻搭配了裝備,選擇了船長),還需要考慮的事情有如下幾點:
對手:在航海類遊戲中,對手有幾種:常規玩家,常規NPC, 世界BOSS,海盜等。不同的對手有某些細微的差別,例如,和玩家對戰的時候,需要檢測玩家的參戰條件,和NPC對戰的時候不需要。
技能:技能的類型可能有多種,每一種技能都需要考慮到技能的釋放對象,技能的影響,判斷技能是否發動成功等。
戰鬥的核心行爲:設置攻擊者,防禦者,執行傷害程序等。
戰鬥的輔助操作:尋找艦隊中目前存活的第一艘船,獲取艦隊中現在還存活船隻的數目等。
戰鬥獎賞:處理每次戰鬥的獎賞。
戰鬥結算:不同的對手,結算的方式不同,最後提示的信息也不同。
單次戰鬥記錄:需要記錄戰鬥的信息,便於前端解析這些信息,呈現戰鬥動畫
戰鬥觀察員:記錄整場戰鬥的信息,便於戰鬥的回放。
前端表現:解析戰鬥信息,播放戰鬥動畫。
針對以上的需要考慮的事情,我們可以做出多個類,每個類都有特定的職責,實現這些類中方法的思維過程就是把一個複雜的戰鬥動作拆分成各種小動作。這是一種組合類的設計方式。由於這個類圖畫的過於大,無法貼圖,只有做出鏈接。
在戰鬥模型父類裏面,主要實現的戰鬥的初始化工作和戰鬥的具體實現流程。
/**
* 戰鬥進行(外觀模式)
*
* @return const
*/
public function process()
{
// 懸賞戰初始化
if ($this->_inBounty) {
$this->_initBounty();
}
// 創建戰鬥記錄器
$this->_recorder = new Model_Battle_Recorder($this->_self, $this->_enemy);
// 船隻初始化
$this->_initShips();
// 船長初始化
$this->_initCaptains();
// 戰鬥進行
$this->_result = $this->_process();
// 通知記錄器記錄 戰鬥結果
$this->_recorder->setResult($this->_result);
// 默認顯示“再次攻擊”按鈕
$this->_recorder->setData('isAllowCombatAgain', true);
// 新增一場戰鬥記錄
$this->_battleId = $this->_recorder->create();
return $this->_result;
}
這裏最最要的方法當然是_process()了,它是戰鬥流程的入口。根據以上我畫出來的戰鬥流程圖,這個函數需要做的事情有如下幾點:
1,如何確定回合制度。
2,如何雙方的船交替開火權。
3,如何找到該開火的船隻,並找到它的攻擊對象。
4,什麼時候對艦隊做出整理,剔除沉沒的船隻。
5,單次戰鬥的結算,如扣血,技能發動。
6,怎樣判定對方所有的船隻都沉沒了。
/**
* 戰鬥進行
*
* @return const Model_Battle_VS_Abstract::WIN/LOSE/DRAW
*/
protected function _process()
{
// 循環N個回合
for ($round = 1; $round <= self::MAX_ROUNDS; $round++) {
// 根據“艦隊射速”決定誰先開火(返回 self/enemy)
$curFireTurn = Model_Battle_Util::calcFirePriority($this->_selfShips, $this->_enemyShips);
// 每回合開始,雙方重整艦隊(剔除被擊沉的船)
$this->_selfShips = Model_Battle_Util::filterSankShips($this->_selfShips);
$this->_enemyShips = Model_Battle_Util::filterSankShips($this->_enemyShips);
// 過濾掉已失效的buff
Model_Battle_InSkill::filterExpiredBuffs($this->_selfShips);
Model_Battle_InSkill::filterExpiredBuffs($this->_enemyShips);
$maxShipCount = max(count($this->_selfShips), count($this->_enemyShips));
// 每艘船交替開火(1個回合內,每艘船有僅只有一次開火權)
for ($shipNo = 0; $shipNo <= $maxShipCount; $shipNo++) {
// 循環兩次來實現“射速”先後手的邏輯
for ($i = 0; $i < 2; $i++) {
// 我船攻擊敵船
if ($curFireTurn == 'self') {
if (Model_Battle_Util::isShipAliveByNo($this->_selfShips, $shipNo)) {
// 我方勝利:找不到下一個攻擊目標了(即對方船全被擊沉)
if (! Model_Battle_Util::getAliveShipCount($this->_enemyShips)) {
return self::WIN;
}
// 執行攻擊流程
$this->__attackProcess($round, $this->_selfShips[$shipNo], 'self');
}
// 敵船攻擊我船
} elseif ($curFireTurn == 'enemy') {
if (Model_Battle_Util::isShipAliveByNo($this->_enemyShips, $shipNo)) {
// 對方勝利:找不到下一個攻擊目標了(即我方船全被擊沉)
if (! Model_Battle_Util::getAliveShipCount($this->_selfShips)) {
return self::LOSE; // 對方的勝利即我方的失敗
}
// 執行攻擊流程
$this->__attackProcess($round, $this->_enemyShips[$shipNo], 'enemy');
}
}
// 下一次開火權交給對方
$curFireTurn = $curFireTurn == 'self' ? 'enemy' : 'self';
}
}
}
// 雙方戰平:N回合內仍未見勝負
return self::DRAW;
}
爲什麼每個回合開始前,都需要重新整理艦隊,因爲上一個回合可能是某些船沉沒了,我們必須剔除那些已經被沉沒了船隻。
至於交替開火權,我們這裏做了一個好的思維方式是,把開火權做成一個變量,並循環賦值。
如何找到開火的船隻,我們的做法是把艦隊裏面的船編號,從0-maxshipamount, 循環這些船隻,根據編號就可以找到要開火的船隻。這裏或許你有個問題是,比如,我第一艘船開火,把你的第一艘船擊沉了,那麼,該你開會的時候,你的第一艘船上沒有船隻了,按照我程序上面寫的,如果根據編號找不到船,那麼開火權又交給他對方,這樣,又該我開火了。其實這樣做也可以,但是我們不是這樣做的,我們的做法是,如果我擊沉了你的0號位上的船,那麼你的1號位的船自動變爲0號位上的。 要實現這樣的功能,需要,在執行開火之後,再一次整理船隻。
關於尋找攻擊對象,就是找對方第一艘存活的船隻,每次攻擊的時候,實際上都是攻擊0號位的船,這裏大家或許又有一個疑問,既然每次都是攻擊0號位置上的船隻,那麼1號位置的船誰來攻擊,大家別忘了重整船隻後,1號位置會補充到2號位置上去的。
具體的開火代碼如下:
/**
* 進一步細化的攻擊流程(可供雙方調用)
*
* @param int $round 第幾回合
* @param Model_Ship $attackerShip 攻船實例
* @param string $attackerSide self/enemy
* @return void
*/
protected function __attackProcess($round, Model_Ship $attackerShip, $attackerSide)
{
// 如果攻船正處於“混亂”狀態(即什麼都不能做)直接略過本次攻擊邏輯
if (Model_Battle_InSkill::isFrozen($attackerShip)) {
return null;
}
// 在攻擊前,攻船是否因爲DOT而沉沒
$attackerShipIsSank = false;
// 攻方信息
$attacker = $this->{'_' . $attackerSide};
// 設置本次攻擊的攻船信息
$logger->setRound($round)
->setAttackerShip($attackerShip)
->setAttackerUid($attacker['uid']);
// 守方是誰
$defenderSide = $attackerSide == 'self' ? 'enemy' : 'self';
// 攻防雙方艦隊數組
$attackerShips = Model_Battle_Util::filterSankShips($this->{'_' . $attackerSide . 'Ships'});
$defenderShips = Model_Battle_Util::filterSankShips($this->{'_' . $defenderSide . 'Ships'});
// 如果攻船正處於“封印”狀態,則不能發動技能
if (Model_Battle_InSkill::isSealed($attackerShip)) {
$skillId = 0;
}
// 否則可以觸發攻船的戰鬥內技能
else {
// 返回0表示觸發失敗,觸發成功則返回技能Id
$skillId = Model_Battle_InSkill::triggerSkill($attacker, $attackerShip);
}
// 普通攻擊
if ($skillId < 1) {
// 定位受擊對象(對方艦隊中存活的第一艘船)
$defenderShip = Model_Battle_Util::findFirstAliveShip($defenderShips);
// 執行開火
$fire = new Model_Battle_Fire();
$fire->setLogger($logger)
->setAttacker($attackerShip)
->setDefender($defenderShip)
->execute();
// 記錄受擊方
$logger->setTargetShip($defenderShip);
// 技能攻擊
} else {
// 創建本次技能實例
$skill = Model_Battle_InSkill::factory($skillId);
// 設置雙方艦隊
$skill->setSelfShips($attackerShips);
$skill->setEnemyShips($defenderShips);
// 設置技能發動母體船
$skill->setAttacker($attackerShip);
// 獲取受技能方(們)
$targetShips = $skill->getTargetShips();
// 是否攻擊(傷害)型技能
if ($skill->isAttack()) {
// 執行羣傷開火
foreach ($targetShips as $targetShip) {
$fire = new Model_Battle_Fire();
$fire->setLogger($logger)
->setAttacker($attackerShip)
->setDefender($targetShip)
->setSkill($skill)
->execute();
}
}
// 設置船隻buffs
$skill->setBuffs();
// 通知記錄器記錄技能相關信息
$logger->setSkill($skill);
// 記錄受技能方(們)
$logger->setTargetShips($targetShips);
}
// 通知戰場觀察員增加本條開火記錄
$this->_recorder->add($round, $logger->build());
}
請注意,這裏的攻擊對象永遠是第一艘船隻。
開戰之前,也要重整艦隊,理由上面我已經說了。
這個函數會依賴很對類,戰鬥助手類,戰鬥技能類等。這裏我們可以參考的一個設計思維是,多用組合,少用繼承。這樣我們可以便於擴展和修改,缺點是可能類的數量爲增加。