バトルシステムの実装
今回はバトルシステムの中でもメインとなるダメージ計算と、命中判定の部分を実装していきます。
ダメージ計算
ポケモンのダメージ計算は初代から現在までそこまで大きな変化はありません。最新世代ではダメージに関係する要素(アイテム等)が多く、それにより補正値の修正はありますが、基本的な計算は以下の式で成り立っています。
(((レベル×2/5+2)×威力×A/D)/50+2)*M
以下が計算式で当てはめられる値になります。
レベル → 攻撃ポケモンのレベル
威力 → 攻撃技の威力
A → 攻撃ポケモンの攻撃値(物理技:こうげき、特殊技:とくこう)
D → 防御ポケモンの防御値(物理技:ぼうぎょ、特殊技:とくぼう)
M → 補正値(タイプ相性補正、タイプ一致補正、急所補正、乱数補正)
※初代では急所補正はMに当てはまらず、レベルの後に乗算されますが、今回はMへ補正を入れます。また第2〜5世代までは2倍の補正が入っていましたが、こちらも最新世代を参考にして1.5倍の補正値とします。
ダメージ計算では、要素が多く複雑に見えてしまいますが、基本的には必要な値を算出して式へ当てはめるだけです。必要な値についても、補正値の要素が多いだけで他はステータスや技情報から参照するだけです。
むしろ、計算以上に処理の順番が重要になります。こちらは後日触れるとして、今回は必要最低限のダメージ計算だけを実装します。
必要値の算出
まずは、ダメージ計算に必要なレベル(L)、威力(P)、攻撃値(A)、防御値(D)、補正値(M)をattackメソッドで取得します。補正値に関しては前回作成したタイプ相性補正のみを仮で使用します。
攻撃判定用トレイト(/Traits/Battle/AttackTrait.php)
<?php
trait AttackTrait
{
/**
* 省略記号
*
* @var L レベル
* @var A 攻撃値
* @var D 防御値
* @var P 威力
* @var M 補正値
*/
/**
* 攻撃する
*
* @param object $atk_pokemon
* @param object $def_pokemon
* @param string $move_class
* @return array
*/
protected function attack($atk_pokemon, $def_pokemon, $move_class)
{
// 技のインスタンスを取得
$move = $this->getInstance($move_class);
// 攻撃メッセージを格納
$this->setMessage($atk_pokemon->getName().'は'.$move->getName().'を使った!');
// タイプ相性チェック
$type_comp = $this->checkTypeCompatibility($move->getType(), $def_pokemon->getTypes());
// 技種類での分岐
switch ($move->getSpecies()) {
// 物理
case 'physical':
$atk = $atk_pokemon->getStats('Attack');
$def = $def_pokemon->getStats('Defense');
// タイプ相性のメッセージを返却
$this->setMessage($type_comp['message']);
break;
// 特殊
case 'special':
$atk = $atk_pokemon->getStats('SpAtk');
$def = $def_pokemon->getStats('SpDef');
// タイプ相性のメッセージを返却
$this->setMessage($type_comp['message']);
break;
// 変化
case 'status':
// ここに変化技の処理
$atk = 0;
$def = 0;
break;
}
}
それではswitch部分から見ていきましょう。
// 技種類での分岐
switch ($move->getSpecies()) {
// 物理
case 'physical':
$atk = $atk_pokemon->getStats('Attack');
$def = $def_pokemon->getStats('Defense');
// タイプ相性のメッセージを返却
$this->setMessage($type_comp['message']);
break;
// 特殊
case 'special':
$atk = $atk_pokemon->getStats('SpAtk');
$def = $def_pokemon->getStats('SpDef');
// タイプ相性のメッセージを返却
$this->setMessage($type_comp['message']);
break;
// 変化
case 'status':
// ここに変化技の処理
$atk = 0;
$def = 0;
break;
}
物理技、特殊技によって参照するステータスが異なるため、技種別に合わせた分岐を作成しました。必要値通り、物理技なら「こうげき」「ぼうぎょ」、特殊技「とくこう」「とくぼう」を取得しています。
ポケモンのインスタンスに対して使用しているgetStatsのメソッドについては以下の用に修正を加えて、必要な値を取得できるようにしています。
Get格納トレイト(/Traits/Pokemon/GetTrait.php)
/**
* ステータスの取得
*
* @param string|null
* @return array|integer
*/
public function getStats($param=null)
{
// ポケモンのステータス(実数値)を計算して返却
foreach($this->base_stats as $key => $val){
/**
* ステータスの計算式(小数点以下は切り捨て)
* HP:(種族値×2+個体値+努力値÷4)×レベル÷100+レベル+10
* HP以外:(種族値×2+個体値+努力値÷4)×レベル÷100+5
*/
if($key === 'HP'){
$correction = $this->level + 10;
}else{
$correction = 5;
}
$stats[$key] = (int)(($val * 2 + $this->iv[$key] + $this->ev[$key] / 4) * $this->level / 100 + $correction);
}
if(is_null($param)){
// 指定がなければ全ステータスを返却
return $stats;
}else{
// 指定されたステータスを取得
return $stats[$param];
}
}
引数を指定すれば、該当するステータスを返却するようにしています。これで、ステータスの実数値から必要な値が取得できます。
では、攻撃用トレイトに戻り、物理技の分岐を例に見てみましょう。
// 物理
case 'physical':
$atk = $atk_pokemon->getStats('Attack');
$def = $def_pokemon->getStats('Defense');
// タイプ相性のメッセージを返却
$this->setMessage($type_comp['message']);
break;
$atk(攻撃値)と$def(防御値)にそれぞれ必要なステータスを当てはめています。その後、タイプ相性のメッセージをsetMessageを使ってコントローラーのメッセージとして格納しました。
物理技・特殊技の場合は一部を除いて相性判定が必要になりますが、ダメージを直接与えない変化技に関しては「こうかがない」を除いて判定は不要です。攻撃判定すべてを作り終える時点では仕様を変更する可能性がありますが、一旦仮として攻撃技が実行された後にメッセージの返却を行います。
次に変化技の分岐についてです。
// 変化
case 'status':
// ここに変化技の処理
$atk = 0;
$def = 0;
break;
変化技の判定は上2つと比べて少しことなります。今回はエラーが出ないように、一旦0をセットしてメッセージもなしで返却しています。
レベルと技威力はそれぞれのインスタンスから取得することができ、補正値も生成したタイプ相性補正のみを使用するので、これで一旦必要値の準備が整いました。
計算式への当てはめ
それでは計算式への当てはめを行います。可視性を維持するためにも計算式用のメソッド(calDamage)を追加しましょう。
攻撃用トレイト(/Traits/Battle/AttackTrait.php)
/**
* ダメージ計算(カッコ毎に小数点の切り捨てをする)
* floor(floor(floor(レベル×2/5+2)×威力×A/D)/50+2)*M
*
* @param integer $l レベル
* @param integer $a 攻撃値
* @param integer $d 防御値
* @param integer $p 威力
* @param integer $m 補正値
* @return integer
*/
private function calDamage($l, $a, $d, $p, $m)
{
// 計算式を当てはめる
return floor(floor(floor($l * 2 / 5 + 2) * $p * $a / $d) / 50 + 2) * $m;
}
式はそのままです。カッコごとに小数点の切り捨てをしているようなので、カッコをfloor関数に置き換えています。あとは、引数で受け取った必要値を元の式の値として埋め込んでいるだけです。
必要値(引数)に間違いがなければ、これで正しいダメージが算出されるはずです。
では次に、メソッドの呼び出し(attackメソッド内)を作成しましょう。
/**
* 攻撃する
*
* @param object $atk_pokemon
* @param object $def_pokemon
* @param string $move_class
* @return array
*/
protected function attack($atk_pokemon, $def_pokemon, $move_class)
{
// 技のインスタンスを取得
$move = $this->getInstance($move_class);
// 攻撃メッセージを格納
$this->setMessage($atk_pokemon->getName().'は'.$move->getName().'を使った!');
// タイプ相性チェック
$type_comp = $this->checkTypeCompatibility($move->getType(), $def_pokemon->getTypes());
// 技種類での分岐
switch ($move->getSpecies()) {
// 物理
case 'physical':
$atk = $atk_pokemon->getStats('Attack');
$def = $def_pokemon->getStats('Defense');
// タイプ相性のメッセージを返却
$this->setMessage($type_comp['message']);
break;
// 特殊
case 'special':
$atk = $atk_pokemon->getStats('SpAtk');
$def = $def_pokemon->getStats('SpDef');
// タイプ相性のメッセージを返却
$this->setMessage($type_comp['message']);
break;
// 変化
case 'status':
// ここに変化技の処理
$atk = 0;
$def = 0;
break;
}
if($move->getSpecies() !== 'status'){
// ダメージ計算※整数で取得
$damage = (int)$this->calDamage(
$atk_pokemon->getLevel(), # 攻撃ポケモンのレベル
$atk, # 攻撃ポケモンの攻撃値
$def, # 防御ポケモンの防御値
$move->getPower(), # 技の威力
$type_comp['m'], # 補正値
);
}else{
$damage = 0;
}
// ダメージを返却
return $damage;
}
※タイプ相性での補正値返却キーをmに変更しました
switchの下が追記部分です。
まず、技種別による分岐をします。変化技であればダメージ計算は不要になるため、直接ダメージ($damage)に0をセットします。物理、特殊であれば、calDamageのメソッドに対して必要な引数を割り当てています。
※技の威力取得用メソッド(getPower)が抜けていたので、技クラス(親)に追加しておいてください
calDamageのメソッド内でfloorを使用している関係上、0ではありますが小数点第一位までの数値が返ってきます。なので、整数としてダメージ量を返却するためintを使って整数型になおしてから変数へ格納しています。
今回メッセージにはsetMessageを使用したので、与えるダメージ量だけを最終的な返り値としています。それに合わせてコントローラーを変更しましょう。
バトル用コントローラー(/Classes/Controller/BattleController.php)
/**
* アクション
*
* @param string $action
* @param mixed $param
* @return void
*/
private function action($action, $param)
{
switch ($action) {
// にげる
case 'run':
unset($_SESSION['enemy']);
header("Location: ./home.php", true, 307) ;
exit;
// たたかう
case 'fight':
$response = $this->attack($this->pokemon, $this->enemy, $param);
$this->setResponse($response, 'damage');
break;
}
}
ダメージはメッセージで表示する必要がないので、setResponseを使って返却します。あとは、この値が正常に表示されているかを確認するだけです。
命中判定
出力結果を確認する前に、合わせて命中判定も実装します。こちらも比較的簡単ではありますが、判定のタイミングに少し工夫が必要です。
まずはattackのメソッドに命中判定を追加しましょう。
攻撃用トレイト(/Traits/Battle/AttackTrait.php)
/**
* 攻撃する
*
* @param object $atk_pokemon
* @param object $def_pokemon
* @param string $move_class
* @return array
*/
protected function attack($atk_pokemon, $def_pokemon, $move_class)
{
// 技のインスタンスを取得
$move = $this->getInstance($move_class);
// 攻撃メッセージを格納
$this->setMessage($atk_pokemon->getName().'は'.$move->getName().'を使った!');
// タイプ相性チェック
$type_comp = $this->checkTypeCompatibility($move->getType(), $def_pokemon->getTypes());
// 命中判定
$hit = $this->checkHit($move->getAccuracy());
if(!$hit){
// 攻撃失敗
$this->setMessage('しかし'.$atk_pokemon->getName().'の攻撃は外れた!');
return;
}
--省略
命中判定は、タイプ相性判定〜攻撃種別の分岐の間に実行します。これは後ほど説明します。
では、攻撃判定を行っているcheckHitのメソッドを見てみましょう。
/**
* 命中判定
*
* @param integer|null
* @return boolean
*/
private function checkHit($accuracy)
{
// nullの場合は命中率関係無し
if(is_null($accuracy)){
return true;
}
/**
* 0〜100からランダムで数値を取得して、それより小さければ命中
* 例:命中80%→mt_randで60が生成されたら成功、90なら失敗
*/
if($accuracy >= mt_rand(0, 100)){
return true;
}
return false;
}
パラメーターとして技の命中率を受け取り、それを元に攻撃が当たる(true)か、外れる(false)を返却します。
命中率が関係ない技(自分強化、必中技)であれば、引数にはnullが入っているため、問答無用でtrueを返却します。
命中の判定については、mt_rand関数を使います。
/**
* 0〜100からランダムで数値を取得して、それより小さければ命中
* 例:命中80%→mt_randで60が生成されたら成功、90なら失敗
*/
if($accuracy >= mt_rand(0, 100)){
return true;
}
mt_randで0〜100の間でランダム(疑似)の数値を取得します。その数値が命中率より大きければ攻撃失敗、以下であれば攻撃が成功です。
【例】
ランダムで取得した値が90、命中率が80
→ 失敗
ランダムで取得した値が50、命中率が75
→ 成功
ランダムで取得した値が60、命中率が60
→ 成功
※ランダムで取得する値を「必要命中率」と考えると理解しやすいです。
命中とタイプ相性
ここで、命中率判定のタイミングについて解説します。もしゴーストタイプにノーマル技を使用した場合、命中判定の結果ではなく「こうかがない」というメッセージが返却されます。これは、命中判定より前にタイプ相性判定が行われているということです。
なので、その間に「こうかがない」かどうかのチェックをいれます。
/**
* 攻撃する
*
* @param object $atk_pokemon
* @param object $def_pokemon
* @param string $move_class
* @return array
*/
protected function attack($atk_pokemon, $def_pokemon, $move_class)
{
// 技のインスタンスを取得
$move = $this->getInstance($move_class);
// 攻撃メッセージを格納
$this->setMessage($atk_pokemon->getName().'は'.$move->getName().'を使った!');
// タイプ相性チェック
$type_comp = $this->checkTypeCompatibility($move->getType(), $def_pokemon->getTypes());
// 「こうかがない」の判定(命中率がnullではなく、タイプ相性補正が0の場合)
if(!is_null($move->getAccuracy()) && ($type_comp['m'] === 0)){
// こうかがない
$this->setMessage($def_pokemon->getName().'には'.$type_comp['message']);
return;
}
// 命中判定
$hit = $this->checkHit($move->getAccuracy());
if(!$hit){
// 攻撃失敗
$this->setMessage('しかし'.$atk_pokemon->getName().'の攻撃は外れた!');
return;
}
もし命中率がnullでなく、タイプ補正値が0(こうがかない)であれば、「こうかがない」という旨のメッセージを返却しています。
他の補正値計算をしていくことで、このあたりはもう少し正規化できるかも知れませんので、バトルシステムの攻撃判定がすべて実装できた段階でまとめていきます。
最終的な出力結果を見てみましょう。
それぞれタイプ相性に合わせたダメージ計算が行われていることがわかりますね。
補正値がすべて整っていない状態なので、完璧な値は算出出来ていませんが、比較的それに近い値が出ていることが分かれば問題ありません。
まとめ
いかがだったでしょうか。
今回のPHPポケモンではダメージ計算の方法をご紹介しました。
このあたりは出来上がれば面白くなる部分であり、システムを作る側としても楽しい要素の1つです。現在プログラミング学習に励んでいる人は、ぜひ参考にしてみてくださいね。