セッション経由でのオブジェクト引き継ぎ
技習得の処理が整ってきたので、ここで連続技習得・連続レベルアップ時にも問題なく動作するように作り込んでいきます。ですが、現状のモーダルをレスポンスやメッセージと同様に、そのまま引き継いだとしてもエラーが発生します。
その原因がセッション経由でのオブジェクト渡しです。
第17回で取り上げましたが、オブジェクトはセッションで次のページへ受け渡しすると、不完全なオブジェクト(__PHP_Incomplete_Class)になります。技習得時のレスポンスにオブジェクトは格納されていませんでしたが、モーダルでは新しい技のオブジェクトなどが含まれていますね。
現状のシステム構成のまま進めていくのであれば、解決策は2通りです。
- オブジェクトではなく配列形式で受け渡しをする
- オブジェクトをシリアライズする
まず、配列形式での受け渡しについてです。こちらは、現在のポケモン情報のようにexportなどのメソッドを用意して、画面移管後に新しくインスタンスを生成する方法です。
ですがポケモンのオブジェクトとは違い、レスポンスやモーダルは今後も増えていく可能性が高いです。その都度、配列形式を意識したデータの受け渡しをするとなれば大変ですね。
なので、今回は「オブジェクトのシリアライズ」で実装していきましょう。
シリアライズ(文字列化)
まず、シリアライズについて簡単に説明しておきます。PHPの関数を使って、オブジェクトを文字列化させることができます。jsonに似たような形式です。
シリアライズすれば、受け取り画面ではアンシリアライズの関数を実行することでオブジェクトを復元することが可能です。
今回使用するシリアライズ用の関数はserializeです。
serialize(PHP.net)
一括でシリアライズさせるために、トレイトを用意してコントローラーとサービスで呼び出せるようにしておきましょう。
シリアライズトレイト(/App/Traits/SerializeTrait.php)
<?php
trait SerializeTrait
{
/**
* オブジェクトのシリアライズ化
* @param arg:mixed
* @return mixed
*/
public function serializeObject($arg)
{
if(is_array($arg)){
// 配列の場合はループ
$result = [];
foreach($arg as $key => $val){
$result[$key] = $this->serializeObject($val);
}
$arg = $result;
}else if(is_object($arg)){
// オブジェクトの場合はシリアライズ
$arg = ['__serialize' => serialize($arg)];
}
return $arg;
}
}
一括の場合は配列、単体の場合はオブジェクトで受け取ることになるので、引数はmixed想定で作成しています。
もし配列であれば、foreachを使って再度serializeObjectを呼び出しています。
受け取った値がオブジェクトであれば、serialize関数で文字列に変換します。ここで、そのまま返してしまうと復号化(アンシリアライズ)させる時に「どれを復号化すれば良いのか」がわからなくなってしまいます。個別で行なっても構いませんが、できれば一括で復号化させたいので、配列形式で返却しましょう。判別用として、キーを「__serialize」としています。
コントローラーとサービスの親クラスでトレイトを読み込み、格納時にシリアライズ化しましょう。
バトル画面ヘッド(/Resources/Partials/Layouts/Head/battle.php)
// レスポンスはシリアライズ化
$_SESSION['__data']['before_reponses'] = $controller->serializeObject(
$controller->getResponses()
);
$_SESSION['__data']['before_messages'] = $controller->getMessages();
現状、メッセージにはオブジェクトを格納することがありませんので、レスポンスのみシリアライズさせています。
アンシリアライズ(復号化)
次にアンシリアライズ(復号化)についてです。こちらもシリアライズトレイト内に、配列形式を想定してメソッドを作成しましょう。
シリアライズトレイト(/App/Traits/SerializeTrait.php)
/**
* オブジェクトのアンシリアライズ化
* @param arg:mixed
* @return mixed
*/
public function unserializeObject($arg)
{
// 配列の処理
if(is_array($arg)){
if(
count($arg) === 1
&& isset($arg['__serialize'])
){
// 配列の中身がシリアライズされたオブジェクトであればオブジェクト化
$arg = unserialize($arg['__serialize']);
}else{
// 通常配列の場合はループ
$result = [];
foreach($arg as $key => $val){
$result[$key] = $this->unserializeObject($val);
}
$arg = $result;
}
}
return $arg;
}
配列の場合はループをさせていますが、もし配列内の個数が1つで__serializeというキーを持っていれば、unserialize関数を呼び出しています。
unserialize(PHP.net)
アンシリアライズ化のメソッドは、サービスでデータを受け取った際に使用します。
技習得用サービス(/App/Serivces/Battle/LearnMoveService.php)
/**
* @param Pokemon:object
* @param before_response:array
* @param before_messages:array
* @param request:array
* @return void
*/
public function __construct($pokemon, $before_responses, $before_messages, $request)
{
$this->pokemon = $pokemon;
$this->before_responses = $this->unserializeObject($before_responses);
$this->before_messages = $before_messages;
$this->before_modals = $this->unserializeObject($before_modals);
$this->request = $request;
}
これでオブジェクトの状態でデータを引き継ぐことができます。
※シリアライズする際は、データ改ざんの可能性を想定してハッシュ化させて照合したり、様々な対策をした上で行うことが推奨されています。また、現在はjsonを使ったデータの受け渡しが主流かつ安全と言われているため、本格的なシステム開発では十分注意して導入しましょう。
モーダルの引き継ぎ
それでは、モーダルの引き継ぎ処理を作成していきましょう。
まずは、レスポンスやメッセージと同様に前回情報をセッションに格納し、技習得モーダルで受け取ります。
バトル画面ヘッド(/Resources/Partials/Layouts/Head/battle.php)
//モーダルはシリアライズ化
$_SESSION['__data']['before_modals'] = $controller->serializeObject(
$controller->getModalss()
);
バトルコントローラー(/App/Controller/Battle/BattleController.php)
// branchメソッドswitch分岐
/******************************************
* 技の習得
*/
case 'learn_move':
// サービス実行
$service = new LearnMoveService(
$this->pokemon,
$_SESSION['__data']['before_reponses'],
$_SESSION['__data']['before_messages'],
$_SESSION['__data']['before_modals'],
$this->request('param')
);
$service->execute();
// 描画するポケモン情報を置き換え
$this->before['friend'] = $service->getTmpPokemon();
break;
技習得用サービス(/App/Serivces/Battle/LearnMoveService.php)
<?php
$root_path = __DIR__.'/../../..';
// 親クラス
require_once($root_path.'/App/Services/Service.php');
// トレイト
require_once($root_path.'/App/Traits/Service/Battle/ServiceBattleCheckTrait.php');
/**
* 技の習得処理
*/
class LearnMoveService extends Service
{
use ServiceBattleCheckTrait;
/**
* @var Pokemon:object
*/
protected $pokemon;
/**
* @var Pokemon:object
*/
protected $tmp_pokemon;
/**
* @var array
*/
protected $before_responses;
/**
* @var array
*/
protected $request;
/**
* @param Pokemon:object
* @param before_response:array
* @param before_messages:array
* @param before_modals:array
* @param request:array
* @return void
*/
public function __construct($pokemon, $before_responses, $before_messages, $before_modals, $request)
{
$this->pokemon = $pokemon;
$this->before_responses = $this->unserializeObject($before_responses);
$this->before_messages = $before_messages;
$this->before_modals = $this->unserializeObject($before_modals);
$this->request = $request;
}
/**
* @return void
*/
public function execute()
{
// 描画用ポケモンオブジェクトの作成
$this->tmp_pokemon = $this->createTmpPokemon();
// 技の置き換え
$this->replaceMove();
// レスポンスの引き継ぎ
$this->setResponse(
$this->getUntreatedResponses($this->before_responses, $this->request['id'])
);
// メッセージの引き継ぎ
$this->setMessage(
$this->getUntreatedResponses($this->before_messages, $this->request['id'], 'message')
);
// モーダルの引き継ぎ
$this->setModal(
$this->getUntreatedResponses($this->before_modals, $this->request['id'], 'modal'), true
);
}
/**
* @return Pokomon:object
*/
public function getTmpPokemon()
{
return $this->tmp_pokemon;
}
/**
* 表示用のポケモンオブジェクトを生成
* @return Pokemon:object
*/
private function createTmpPokemon()
{
$pokemon = clone $this->pokemon;
// クローンオブジェクトにレベルと残HPをセット
$pokemon->setLevel($this->request['level']);
$pokemon->setRemainingHp($this->request['hp']);
$pokemon->setDefaultExp();
return $pokemon;
}
/**
* 技の置き換え
* @return void
*/
private function replaceMove()
{
// 忘れる技を取得
$forget_move = $this->pokemon
->getMove($this->request['forget']);
// 覚えさせる技を取得
$new_move = new $this->before_responses[$this->request['id']]['move'];
// 技を覚えさせる
$this->pokemon
->setMove($new_move, $this->request['forget']);
// メッセージの返却
$this->setMessage('1 2の ……ポカン!');
$this->setMessage($this->pokemon->getNickname().'は、'.$forget_move->getName().'の使い方をキレイに忘れた!そして......');
$this->setMessage($this->pokemon->getNickname().'は新しく、'.$new_move->getName().'を覚えた!');
}
/**
* 未処理レスポンス・メッセージ・モーダルの引き継ぎ処理
* @param response:array
* @param msg_id:string
* @param param:string::response|message|modal
* @return array
*/
private function getUntreatedResponses(array $responses, string $msg_id, string $param='response')
{
$cnt = 1;
switch ($param) {
/********
* メッセージの引き継ぎ
*/
case 'message':
$key = array_search(
$msg_id,
array_column($responses, 1), # メッセージIDの位置は1番目
true
);
// 対象メッセージを含め3つ目までを削除
$cnt = 3;
break;
/********
* モーダルの引き継ぎ
*/
case 'modal':
$key = array_search(
$msg_id,
array_column($responses, 0), # メッセージIDの位置は0番目
true
);
break;
/********
* レスポンスの引き継ぎ
*/
default:
// メッセージIDのレスポンスが入った位置を取得
$key = array_search(
$msg_id,
array_keys($responses),
true
);
break;
}
// 未処理だけを切り出して返却
return array_splice($responses, $key + $cnt);
}
}
現在覚えている技
ここから前回までに実装した機能の修正箇所です。まずは技モーダルの返却値についてです。
ポケモンクラスチェック用トレイト(/App/Traits/Class/Pokemon/ClassPokemonCheckTrait.php)
// checkLevelMoveメソッド(旧:checkMove)
// モーダル用のレスポンスをセット
$this->setModal([
'id' => $msg_id,
'modal' => 'selectmove',
'new_move' => $move
]);
前回は現在覚えている技に新しい技をマージして返却していましたが、それでは連続して技を覚える際に、新しく覚えた技が反映されていないオブジェクトが格納されたままになります。
これを回避するために、モーダルには現在の技+返却された技を出力させます。
忘れさせる技選択用モーダル(/Resources/Partials/Battle/Modals/selectmove.php)
<div class="modal-body">
<?php # 覚えている技 ?>
<table class="table table-bordered table-sm table-hover">
<thead class="thead-light">
<tr>
<th scope="col">覚えている技</th>
<th scope="col">タイプ</th>
<th scope="col">PP</th>
</tr>
</thead>
<tbody>
<?php foreach($pokemon->getMove() as $key => $move): ?>
<tr class="move-detail-link forget-selectmove <?php if($key === 4) echo 'active new-move'; ?>"
data-modal="#<?=$modal['id']?>-modal"
data-target="#<?=$modal['id']?>_<?=get_class($move['class'])?>-content"
data-name="<?=$move['class']->getName()?>"
data-num="<?=$key?>">
<th scope="row" class="w-50"><?=$move['class']->getName()?></th>
<td><?=$move['class']->getType()->getName()?></td>
<td><?=$move['remaining']?>/<?=$move['class']->getPp($move['correction'])?></td>
</tr>
<?php endforeach; ?>
<?php # 覚えようとしている技 ?>
<tr class="move-detail-link forget-selectmove active new-move"
data-modal="#<?=$modal['id']?>-modal"
data-target="#<?=$modal['id']?>_<?=get_class($modal['new_move'])?>-content"
data-name="<?=$modal['new_move']->getName()?>"
data-num="<?=$key?>">
<th scope="row" class="w-50"><?=$modal['new_move']->getName()?></th>
<td><?=$modal['new_move']->getType()->getName()?></td>
<td><?=$modal['new_move']->getPp()?>/<?=$modal['new_move']->getPp()?></td>
</tr>
</tbody>
</table>
<?php # 技説明 ?>
<div class="overflow-auto p-3 border" style="height:160px;">
<?php foreach($pokemon->getMove() as $key => $move): ?>
<div class="move-detail-content" id="<?=$modal['id']?>_<?=get_class($move['class'])?>-content">
<h6><?=$move['class']->getName()?></h6>
<hr>
<p><?=$move['class']->getDescription()?></p>
</div>
<?php endforeach; ?>
<?php # 覚えようとしている技 ?>
<div class="move-detail-content active" id="<?=$modal['id']?>_<?=get_class($modal['new_move'])?>-content">
<h6><?=$modal['new_move']->getName()?></h6>
<hr>
<p><?=$modal['new_move']->getDescription()?></p>
</div>
</div>
</div>
IDの重複回避
次にIDの重複回避です。モーダルは複数呼び出されることがあるため、IDを使用する場合は重複回避対策が必須となります。
ボタン等でIDを使用していたため、こちらをクラスに置き換えています。
<div class="modal-footer">
<?php # 忘れさせるボタン ?>
<button type="button"
class="btn btn-danger btn-sm btn-forget-move"
data-modal="#<?=$modal['id']?>-modal"
data-msg_id="<?=$modal['id']?>"
style="display:none;">
<span class="move-name"></span>を忘れる</button>
<?php # 諦めるボタン ?>
<button type="button"
class="btn btn-secondary btn-sm action-message-box btn-abandon-move"
data-dismiss="modal">
<?=$modal['new_move']->getName()?>を諦める</button>
</div>
また、jsで対象を指定する際に別のモーダルの値を取ってきてしまわないように、クリック発火するエレメントにはdata属性のmodalに現在のモーダルの判別値を格納しています。
これに合わせて、js側も修正しましょう。
バトル画面用js(/Public/Assets/js/Battle/fight.js)
/*----------------------------------------------------------
// 初期化する関数
----------------------------------------------------------*/
/**
* 技テーブルクリック時の関数
* @function click
* @return void
**/
var clickMoveInit = function(){
$('.move-table-row').on('click', function(){
// 技をフォームへセット
$('#fight-form-param').val($(this).data('key'));
// サブミット実行
$('#fight-form').submit();
});
}
/**
* 技テーブルクリック時の関数
* @function click
* @return void
**/
var selectForgetMoveInit = function(){
$('.forget-selectmove').on('click', function(){
var modal = $(this).data('modal');
// 諦めるボタンの無効化切り替え
if($(this).hasClass('new-move')){
$(modal).find(".btn-abandon-move")
.prop('disabled', false);
$(modal).find('.btn-forget-move')
.hide();
return;
}else{
$(modal).find("#btn-abandon-move")
.prop('disabled', true);
}
// 技名を取得
var name = $(this).data('name');
// ボタンに技名をセット
$(modal).find('.btn-forget-move .move-name')
.text(name);
$(modal).find('.btn-forget-move')
.show();
});
}
/**
* 技テーブルクリック時の関数
* @function click
* @return void
**/
var submitForgetMoveInit = function(){
$('.btn-forget-move').on('click', function(){
var modal = $(this).data('modal');
// 技をフォームへセット
var data = {
id: $(this).data('msg_id'),
forget: $(modal).find('.forget-selectmove.active').data('num'),
level: $('#level').text(),
hp: $('#hpbar-friend').attr('aria-valuenow')
};
// フォームを用意
$.each(data, function(key, val){
// フォームの最初にパラメーターを追加
$('#remote-form').append(
'<input type="hidden" name="param[' + key + ']" value="' + val + '">'
);
})
// サブミット実行
$('#remote-form-action').val('learn_move');
$('#remote-form').submit();
});
}
/*----------------------------------------------------------
// 初期化
----------------------------------------------------------*/
jQuery(function($){
clickMoveInit();
selectForgetMoveInit();
submitForgetMoveInit();
});
これで連続レベルアップと複数の技習得にも対応させることができました。実際に動きを見てみましょう。
1つのレベルに数個の技、レベルアップ時後にも技の習得を設定していましたが、問題なく動作してくれました。
これで技習得処理は完成です。
まとめ
いかがだったでしょうか。
今回のPHPポケモンではセッションを経由したオブジェクトの引き渡し方法をご紹介しました。
システム開発では方法を模索することがとても大切です。そして、複雑化しそうなときは1から考え直してみると、意外な突破口が見えてきます。
今回もオブジェクトをどう処理しようか、既にモーダルにセットした技を更新するにはどうすれば良いか、などを考えていましたが、考えれば考えるほど処理が複雑化しそうだったので、一度最初から考え直し、そもそも「すべての技を格納しておく必要がない」という答えに行き着きました
現在プログラミング学習に取り組んでいる方、ゲームづくりに興味がある方は、ぜひ参考にしてみてくださいね。