2日ほどwiki作成にいそいそと励んでいましたが、開発をお休みしていたわけではありません。追加機能を実装するに辺り、色々と改善点が挙がってきたので、このタイミングでしっかりと見直しをしました。
本格的なシステム開発では、最初に仕様書や設計書が作成され、それに沿って作成していくことになります。もしシステム根幹などメイン部分を変更するほうが都合が良いという状況に直面しても、開発がある程度進んでいれば対応が難しいということも珍しくありません。
しかし、PHPポケモンは思いつき開発で、作りながらデザインパターンを模索していくことになります。そのため、それがシステム根幹部分であろうが、良いと思えばタイミングを見て実装していきます。
※良い子(特にチーム開発シている人)は真似しないでね
プレイヤーのグローバル化
まず大きな問題が、プレイヤー情報です。こちらは後付けで実装したため、コントローラーのプロパティとして保管し、セッションの引き継ぎで受け渡しをしていましたが、そのせいでサービスに引数で渡すという手間が生じていました。
今までバトル画面ではプレイヤー情報を使う機会がなく気にしていませんでしたが、アイテムの実装により、プレイヤー情報はどこからでも呼び出せる状態でなければ困るということになりました。
なので、バトル状態と同じようにグローバル化させ、画面移管での引き継ぎ処理は、コントローラーの親クラス内で行うようにしました。
プレイヤー情報のグローバル関数(/App/Globals/PlayerGlobal.php)
<?php
$global_player = null;
// クラス読み込み
require_once($root_path.'/Classes/Player.php');
/**
* プレイヤーの初期化
* @param name:string
* @return void
*/
function initPlayer(string $name)
{
global $global_player;
$global_player = new Player($name);
}
/**
* プレイヤーの格納
* @param player:object::Player
* @return void
*/
function setPlayer($player)
{
global $global_player;
$global_player = $player;
}
パーティー情報
プレイヤー情報と同じく、扱いの不便さが目立っていたのがパーティー情報です。こちらもコントローラーに保管している関係上、サービスへ経由する際の不都合が目立っていました。
バトル中は、敵味方それぞれポケモン用のプロパティを準備して、そこへアクセスして参照による値の変更を加えていたので気になりませんでしたが、へんしんや進化といったプロパティの置き換え処理が発生することで、この引き継ぎ処理だけで膨大な量になっていました。
そもそも概念としては、パーティーはプレイヤーに属するものです。なので、こちらはコントローラーではなく、プレイヤーに格納して管理をしていきます。
プレイヤー用のパーティートレイト(/App/Traits/Class/Player/ClassPlayerPartyTrait.php)
<?php
/**==================================================================
* パーティー
==================================================================**/
trait ClassPlayerPartyTrait
{
/**
* パーティーの取得
* @return array
*/
public function getParty(): array
{
return $this->party;
}
/**
* パーティーへ追加
* @param pokemon:object::Pokemon
* @return boolean
*/
public function setParty(object $pokemon): bool
{
// 立場をチェック
if($pokemon->getPosition() !== 'friend'){
return false;
}
// 所有数をチェック
if(count($this->party) <= 6){
$this->party[] = $pokemon;
return true;
}else{
return false;
}
}
/**
* パーティーから指定したポケモンを取得
* @param param:mixed
* @param judge:string::order|id
* @return object::Pokemon
*/
public function getPartner($param, $judge='order')
{
if($judge === 'id'){
// IDによる検索
$pokemon = array_filter($this->party, function($pokemon) use($param){
return $pokemon->getId() === $param;
});
return $pokemon[0];
}else{
// オーダー番号による検索
return $this->party[$param];
}
}
/**
* パーティー内のポケモンを進化ポケモンに置き換え
* @param order:integer
* @param evolve:Pokemon
* @return void
*/
public function evolvePartner($order, $evolve): void
{
if(isset($this->party[$order])){
$this->party[$order] = $evolve;
}
}
}
新しく追加したメソッドとして、evolvePartherがあります。こちらは、進化後のパーティーの更新に使います。
パーティーそのものを入れ替えるというものは、単体で使うとポケモン情報が消失してしまうことになるので、ボックス枠が完成してから「入れ替え」という機能で実装予定です。
ヘルパー関数の作成
使用頻度の高いオブジェクトは、変数名に近い関数で呼び出せる方が記述量が少なく都合が良いので、プレイヤー情報は「player()」という関数でどこからでも呼び出せるように、グローバルにヘルパー関数を追加しておきます。
/**
* プレイヤーの取得
* @return object::Player
*/
function player()
{
global $global_player;
return $global_player;
}
プレイヤーと同様に、バトル状態もバトル管理系の処理では多用するので、こちらも簡単に呼び出せるヘルパー関数を用意しておきましょう。
/**
* バトル状態の取得
* @return object::BattleState
*/
function battle_state()
{
global $global_battle_state;
return $global_battle_state;
}
あとは、これらを参照する際に上記の関数に対してアロー演算子をつなぐことでそれぞれのメソッドやプロパティにアクセスが可能です。
バトル状態の増設
プレイヤー、パーティーのグローバル化ができるようになれば、気になるのが残りのコントローラーに付属したプロパティです。
バトルポケモン
味方(pokemon)、敵(enemy)に関してはバトル画面でしか使用しませんし、毎回引き継ぎ処理や、サービスへの受け渡しをするのは手間がかかります。なので描画用のbeforeも含めて、これらもバトル状態のクラス内に格納していきましょう。
バトル状態クラス用ポケモントレイト(/App/Traits/Class/BattleState/ClassBattleStatePokemonTrait.php)
<?php
/**==================================================================
* ポケモン関係の処理
==================================================================**/
trait ClassBattleStatePokemonTrait
{
/**==================================================================
* 前ターンのポケモン関係処理
==================================================================**/
/**
* 前ターンのポケモン状態の初期値
* @return void
*/
protected function defaultBefore(): void
{
$this->before = [
'friend' => null,
'enemy' => null,
];
}
/**
* 味方ポケモンを取得
* @param position:string
* @param pokemon:null|object::Pokemon
* @return void
*/
public function setBefore(string $position='', $pokemon=null): void
{
if(in_array($position, config('pokemon.position'), true)){
// どちらか指定(第2引数でインスタンスの指定可能)
if(!is_a($pokemon, 'Pokemon')){
$pokemon = null;
}
// へんしんオブジェクトがあればgtTransformの値をクローンして格納
$this->before[$position] = clone ($pokemon ?? $this->getTransform($position) ?? $this->$position);
}else{
// 両方
$this->before['friend'] = clone ($this->getTransform('friend') ?? $this->friend);
$this->before['enemy'] = clone ($this->getTransform('enemy') ?? $this->enemy);
}
}
/**
* 描画用ポケモンの取得
* @return object::Pokemon
*/
public function getBefore($position): object
{
return $this->before[$position];
}
/**==================================================================
* 味方ポケモン関係処理
==================================================================**/
/**
* 味方ポケモンを取得
* @return object::Pokemon
*/
public function getFriend(): object
{
return $this->friend;
}
/**
* 味方を格納
* @param object::Pokemon
* @return void
*/
public function setFriend(object $pokemon): void
{
if($pokemon->getPosition() === 'friend'){
$this->friend = $pokemon;
}
}
/**==================================================================
* 敵ポケモン関係処理
==================================================================**/
/**
* 敵ポケモンを取得
* @return object::Pokemon
*/
public function getEnemy(): object
{
return $this->enemy;
}
/**
* 敵を格納
* @param object::Pokemon
* @return void
*/
public function setEnemy(object $pokemon): void
{
if($pokemon->getPosition() === 'enemy'){
$this->enemy = $pokemon;
}
}
}
beforeの格納は、標準としてへんしん状態を考慮しつつ現在のfriend、enemyのプロパティをクローンするようにしています。ただし、場合によっては変更する可能性も考えられるため、第2引数で書き換える値を指定できるようにしています。
ポケモン番号
現在選択されているポケモン番号についても、コントローラー内で管理するより、バトル状態で管理します。こちらは、最初のバトルポケモンを選出するメソッドも含めて作成します。
/**
* 現在のポケモン番号を取得
* @return integer
*/
public function getOrder(): int
{
return $this->order;
}
/**
* 開始時に戦闘に参加するポケモン番号を生成
* @return object::Pokemon
*/
public function setFightPokemonOrder(): object
{
$orders = array_filter(player()->getParty(), function($partner){
return $partner->getRemainingHp() > 0;
});
// プロパティへ格納
$this->order = array_key_first($orders);
// 選出されたポケモンを取得
return player()->getParty()[$this->order];
}
これにより、バトル開始前には残HPだけを確認、バトル開始後に選出ポケモンを選ぶということで、HomeControllerからの処理を切り離しができるようになりました。
処理によっては、切り離すことで不具合のポイントになる可能性もありますが、視認性や処理の流れを追うという観点からも、できるだけ処理はそれぞれで完結させる方が良いと感じたので、今回は後者を採用しました。
注意点(シリアライズの罠)
ポケモンプロパティをバトル状態クラス内で保管することで、それぞれ単体の引き継ぎ処理は不要になりました。ただ、注意しなければならないのが「バトル中のポケモン」と「パーティーのポケモン」のリンク(参照)状態です。
バトル状態へ格納、次の画面へ移管する間には、セッションを経由する関係上「シリアライズ(暗号化)」→「アンシリアライズ(復号化)」という処理が含まれています。これにより、格納時には参照状態になっていたfriendプロパティの値とparty内のポケモンのリンクは外れてしまいます。そうなると、バトルで体力を削られてもパーティーのポケモンは無傷という状態になってしまいます。
それを解決するためにも、バトル状態クラスをアンシリアライズ(復号化)後に、バトル中のポケモンプロパティは更新するようにしなければなりません。
バトルコントローラー用トレイト(/App/Traits/Controller/BattleControllerTrait.php)
/**
* 引き継ぎ処理
* @return void
*/
protected function inheritance()
{
// バトル状態の引き継ぎ
if(isset($_SESSION['__data']['battle_state'])){
// グローバルにセット
setBattleState(
unserializeObject($_SESSION['__data']['battle_state'])
);
// ターン最初の状態へ初期化
battle_state()->turnInit();
// 描画用ポケモンをセット(味方・敵)
battle_state()->setBefore();
// シリアライズでリンク切れしているため、味方のオブジェクトを再セット(へんしん状態を考慮)
$order = battle_state()->getOrder();
battle_state()->setFriend(
battle_state()->getTransform('friend') ?? player()->getPartner($order)
);
}
}
それぞれのプロパティにグローバルからアクセスできるようになったため、もし初ターンであればサービスでポケモン作成後に必要な値をセットできるので、処理の流れ自体はかなり読みやすくなりました。
瀕死状態
残るプロパティが「瀕死状態」です。こちらもポケモン関係のプロパティが移管したことにより、バトル状態クラス内で管理ができるようになったので、移行していきましょう。
バトル状態クラス用ポケモントレイト(/App/Traits/Class/BattleState/ClassBattleStatePokemonTrait.php)
/**==================================================================
* ひんし状態関係処理
==================================================================**/
/**
* 瀕死状態の初期値
* @return void
*/
protected function dafaultFainting(): void
{
$this->fainting = [
'friend' => false,
'enemy' => false,
];
}
/**
* 瀕死状態の確認
* @param position:string
* @return boolean
*/
public function isFainting(string $position=''): bool
{
if(in_array($position, config('pokemon.position'), true)){
// どちらか指定
if(empty($this->$position->getRemainingHp())){
// 瀕死状態
$result = true;
}
}else{
// 両方チェック
if(
empty($this->enemy->getRemainingHp()) ||
empty($this->friend->getRemainingHp())
){
// 瀕死有り
$result = true;
}
}
return $result ?? false;
}
サービス、コントローラー内で行なっていた瀕死チェックは、isFaintingのメソッド1つで確認できるようになり、チェック自体はかなり簡潔になりました。
まとめ
いかがだったでしょうか。
今回のPHPポケモンでは「プレイヤーのグローバル化」を中心に、処理内部の見直しをご紹介しました。
冒頭でも説明しましたが、システム開発では根幹部分を開発途中に変更することはほとんどありません。そのために、入念な設計や仕様書の作成があるからです。それを考えれば、ある程度開発の知識がなければ十分な設計や仕様書の作成はできないということが見えてきます。
プログラミングに興味がある方、システム開発に挑戦してみたいと考えている方は、ぜひ参考にしてみてくださいね。