経験値取得アニメーションの実装
最近は技の実装が続いていたので、気分転換にフロント側の演出づくりをしていきます。その中でも今回実装するのは「経験値取得アニメーション」です。
経験値バーはポケモンの第2世代から追加実装された演出です。初代では次のレベルにアップするまでの数値を、わざわざポケモンのステータスから見る必要がありましたが、経験値バーの登場によって感覚的に「あとどれぐらい必要なのか」がわかるようになりました。
アニメーションを作成するのはフロントの処理ですが、それに必要なパラメータ等はバックエンド(PHP側)でメッセージに対して用意しなければなりません。なので、まずはバックエンド側に必要な処理から見ていきましょう。
バック(PHP)処理
HPのアニメーションを作成する際に、処理開始時のHPをコントローラーのプロパティに格納しました。ですが、今回は経験値についても取得前の最終状態を格納しておく必要があります。なので、格納する値を「ポケモンのオブジェクト」そのものに変更します。
バトルコントローラー(/App/Controller/Battle/BattleController.php)
// バトル用コントローラー
class BattleController extends Controller
{
use BattleControllerTrait;
--省略
/**
* 前ターンのポケモンの状態
* @var array
*/
protected $before = [
'friend' => null,
'enemy' => null,
];
バトルコントローラー用トレイト(/App/Traits/Controller/BattleControllerTrait.php)
<?php
/**
* バトルコントローラー用トレイト
*/
trait BattleControllerTrait
{
/**
* ポケモン情報の引き継ぎ
*
* @param Pokemon::export:array $pokemon
* @return void
*/
protected function takeOverPokemon($pokemon)
{
$class = $pokemon['class_name'];
$this->pokemon = new $class($pokemon);
// ランク(バトルステータス)の引き継ぎ
if(isset($_SESSION['__data']['rank'])){
$this->pokemon
->setRank($_SESSION['__data']['rank']['pokemon']);
}
// 状態変化の引き継ぎ
if(isset($_SESSION['__data']['sc'])){
$this->pokemon
->setSc($_SESSION['__data']['sc']['pokemon']);
}
// 前ターンの状態をプロパティに格納
$this->before['friend'] = clone $this->pokemon;
}
/**
* 相手ポケモンの引き継ぎ
*
* @param Pokemon::export:array $enemy
* @return void
*/
protected function takeOverEnemy($enemy)
{
if(!empty($enemy)){
$this->enemy = new $enemy['class_name']($enemy);
// 前ターンの状態をプロパティに格納
$this->before['enemy'] = clone $this->enemy;
}
// ランク(バトルステータス)の引き継ぎ
if(isset($_SESSION['__data']['rank'])){
$this->enemy
->setRank($_SESSION['__data']['rank']['enemy']);
}
// 状態変化の引き継ぎ
if(isset($_SESSION['__data']['sc'])){
$this->enemy
->setSc($_SESSION['__data']['sc']['enemy']);
}
}
オブジェクトはそのまま他の変数に渡しても参照渡しとなってしまい、ダメージ処理などをすればすべてのオブジェクトの値が変更されてしまいます。そうならないために、開始時の状態はcloneを使って値渡しをしましょう。
オブジェクトのクローン作成(PHP.net)
あとは、コントローラーのbeforeプロパティに格納された情報をHPや経験値、レベルなどの出力で使用します。こうすることで、画面移管時には更新前の状態を表示することが可能です。
アニメーション用レスポンスの返却
それでは実際に返却する値について見ていきましょう。
まずは、経験値取得処理部分でメッセージIDと、それに合わせたレスポンスを生成します。
ポケモンクラスへのSet処理トレイト
/**
* 経験値をセット(取得)する
* @param integer $exp
* @return object
*/
public function setExp($exp)
{
if(!is_numeric($exp)){
// 入力値のチェック
$this->setMessage('数値を入力してください', 'error');
return $this;
}
// 次のレベルに必要な経験値を取得
$next_exp = $this->getReqLevelUpExp();
// 経験値を加算
$this->exp += (int)$exp;
// メッセージIDを生成
$msg_id = $this->issueMsgId();
$this->setMessage($this->getNickname().'は経験値を'.$exp.'手に入れた!', $msg_id);
// レベル上限の確認
if($this->level >= 100){
return $this;
}
if($next_exp <= $exp){
$levelup = true;
/**
* 次のレベルに必要な経験値を超えている場合
*/
// レベルアップ処理
$this->actionLevelUp($msg_id);
// レベルアップ処理ループ
while($this->getReqLevelUpExp() < 0){
// メッセージIDを再生成
$msg_id = $this->issueMsgId();
$this->setAutoMessage($msg_id);
// レベルアップ処理
$this->actionLevelUp($msg_id);
}
// 全レベルアップ処理終了後、メッセージIDを再生成
$msg_id = $this->issueMsgId();
$this->setEmptyMessage($msg_id);
}
// 経験値バーの最終アニメーション用レスポンス
$this->setResponse([
'param' => $this->getPerCompNexExp(),
'action' => 'expbar',
], $msg_id);
// 進化判定
if(isset($levelup) && isset($this->evolve_level) && ($this->evolve_level <= $this->level)){
return $this->evolve();
}else{
return $this;
}
}
経験値取得時にメッセージIDを生成してセットします。最後(進化判定前)で次のレベルに必要な経験値量をパラメーターとしてレスポンスを返却しています。これで、経験値を貰えばexpbarのアクション分岐で経験値バーを変動させることができます。
レベルアップ
経験値処理で1点考慮しなければならないのが、レベルアップ処理です。こちらは経験値取得後に行われ、レベルがアップすることで最大HPや残HPも変動させる必要があります。
それでは、上記処理のレベルアップ部分を確認してみましょう。
if($next_exp <= $exp){
$levelup = true;
/**
* 次のレベルに必要な経験値を超えている場合
*/
// レベルアップ処理
$this->actionLevelUp($msg_id);
// レベルアップ処理ループ
while($this->getReqLevelUpExp() < 0){
// メッセージIDを再生成
$msg_id = $this->issueMsgId();
$this->setAutoMessage($msg_id);
// レベルアップ処理
$this->actionLevelUp($msg_id);
}
// 全レベルアップ処理終了後、メッセージIDを再生成
$msg_id = $this->issueMsgId();
$this->setEmptyMessage($msg_id);
}
レベルアップする際には、経験値バーは100%になります。なので、paramを100として経験値バーのアニメーションを実行させます。場合によってはレベルが連続でアップすることがあるので、ループ時も同様に処理をします。この処理はレベルアップ時に一律で発生するため、actionLevelUp内で行えるよう、引数にメッセージIDを追加して行いましょう。
ここでのポイントは、レベルアップ時に使用するメッセージIDを$msg_idに上書きするという点です。そうしておけば、レベルアップ終了後にその値を最終の経験値バーアニメーションで使用することができるからです。
では、レベルアップ処理のメソッドを見てみましょう。
ポケモンクラス(/Classes/Pokemon.php)
/**
* レベルアップ処理
*
* @param string|null $msg_id
* @return void
*/
protected function actionLevelUp($msg_id=null)
{
// メッセージIDの指定があれば、経験値バーのアニメーション用レスポンスをセット
if(!is_null($msg_id)){
$this->setResponse([
'param' => 100, # %
'action' => 'expbar',
], $msg_id);
}
// 現在のHPを取得
$before_hp = $this->getStats('HP');
// レベルアップ
$this->level++;
// HPの上昇値分だけ残りHPを加算(ひんし状態を除く)
if(!isset($this->sa['SaFainting'])){
$this->calRemainingHp('add', $this->getStats('HP') - $before_hp);
}
// メッセージIDを生成
$msg_id1 = $this->issueMsgId();
$msg_id2 = $this->issueMsgId();
// レベルアップアニメーション用レスポンス
$this->setResponse([
'param' => json_encode([
'level' => $this->level,
'remaining_hp' => $this->getRemainingHp(),
'remaining_hp_per' => $this->getRemainingHp('per'),
'max_hp' => $this->getStats('HP'),
]),
'action' => 'levelup',
], $msg_id1);
$this->setAutoMessage($msg_id1);
// レベルアップメッセージ
$this->setMessage($this->getNickName().'のレベルは'.$this->level.'になった!', $msg_id2);
// 現在のレベルで習得できる技があるか確認
$this->checkMove();
}
引数でメッセージIDをチェック後、もし引数が与えられていればパラメーターに100(%)のレスポンスを返却します。
レベルアップ処理では、最大HP、残りHP、レベルの3箇所を更新する必要があるため、json_encode関数を使用してjson形式でパラメーターをセットします。
json_encode(PHP.net)
HPバーの長さはJavaScript側でも算出が可能ですが、PHPでメソッドが用意されているため合わせて返却しています。
ゲームではレベルアップメッセージ後にステータスが表示されますが、今回は一旦無視します。
フロント(JavaScript)処理
必要データが揃ったので、アニメーションを実装するためにフロント側の処理を作成していきましょう。まずはレベルや経験値バーの変更がしやすいように、それぞれにIDを割り振っていきましょう。
バトル画面(/Resources/Pages/Battle.php)
<?php # 自ポケモン詳細 ?>
<div class="col-6 text-center">
<img src="/Assets/img/pokemon/dots/back/<?=get_class($before_pokemon)?>.gif" alt="<?=$before_pokemon->getName()?>">
</div>
<div class="col-6">
<p>
<span class="mr-2"><?=$pokemon->getNickName()?></span>
<span class="mr-2">Lv:<span id="level"><?=$before_pokemon->getLevel()?></span></span>
<span class="mr-2"><?=$before_pokemon->getSaName(false)?></span>
</p>
<div class="form-group">
<div class="progress">
<?php if($before_pokemon->getRemainingHp('per') <= 50) $hp_bar_class = 'bg-warning'; ?>
<?php if($before_pokemon->getRemainingHp('per') <= 20) $hp_bar_class = 'bg-danger'; ?>
<div id="hpbar-friend"
class="progress-bar <?=$hp_bar_class ?? 'bg-success'?>"
role="progressbar"
style="width:<?=$before_pokemon->getRemainingHp('per')?>%;"
aria-valuenow="<?=$before_pokemon->getRemainingHp()?>"
aria-valuemin="0"
aria-valuemax="<?=$before_pokemon->getStats('HP')?>"></div>
</div>
<p class="text-right px-3">
<span id="remaining-hp-count-friend"><?=$before_pokemon->getRemainingHp()?></span>
/ <span id="max-hp-count-friend"><?=$before_pokemon->getStats('HP')?></span>
</p>
<?php # 経験値バー ?>
<div class="progress" style="height:4px;">
<div id="expbar"
class="progress-bar bg-primary"
role="progressbar"
style="width:<?=$before_pokemon->getPerCompNexExp()?>%;"
aria-valuenow="<?=$before_pokemon->getPerCompNexExp()?>"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
</div>
経験値バーの変動
最初は経験値バーのアニメーションから作成します。前回HPバーを作成した際はhpbarというアクション名で分岐を作成したので、今回は新しくexpbarという名称で分岐を作ります。
メッセージ用js(/Public/Assets/js/Battle/message.js)
/**
* メッセージアクション
* @param now element
* @return Promise
**/
var actionMsgBox = function(now){
return new Promise( async (resolve, reject) => {
// 最終メッセージかどうか確認
if((now.length === 0) || now.hasClass('last-message')){
await doLastMsg();
}else{
// メッセージにアクションがセットされていれば実行
switch (now.data('action')){
// ==============================================
// HPバーの処理 =================================
//
case 'hpbar':
await doAnimateHpBar(
now.data('target'),
now.data('param')
);
break;
// ==============================================
// 経験値バーの処理 =============================
//
case 'expbar':
await doAnimateExpBar(
now.data('param')
);
break;
--省略
// ==============================================
// 経験値バーの処理 =============================
//
/**
* 経験値バーのアニメーションを実行
* @param mixed param
* @return Promise
**/
var doAnimateExpBar = function(param){
return new Promise ((resolve, reject) => {
var expbar = $("#expbar");
// EXPの現在の値を変更
expbar.attr('aria-valuenow', param);
// 経験値バーの長さを100%にする
expbar.animate({
width: param + "%"
}, {
duration: 500,
easing: 'easeOutQuad',
complete: function(){
// 処理完了(css変更のズレがあるため0.5秒後にresolveを返却)
setTimeout(function() {
resolve();
}, 500);
}
});
});
}
アクション自体はHPバーとほとんど変わらず単純です。若干のズレを合わせるために長さの変更にはanimateを使用してcssを変更しています。
レベルアップ
次にレベルアップ処理についてです。こちらも分岐を追加し、それ用の関数を作成しましょう。
メッセージ用js(/Public/Assets/js/Battle/message.js)
/**
* メッセージアクション
* @param now element
* @return Promise
**/
var actionMsgBox = function(now){
return new Promise( async (resolve, reject) => {
// 最終メッセージかどうか確認
if((now.length === 0) || now.hasClass('last-message')){
await doLastMsg();
}else{
// メッセージにアクションがセットされていれば実行
switch (now.data('action')){
// ==============================================
// HPバーの処理 =================================
//
case 'hpbar':
await doAnimateHpBar(
now.data('target'),
now.data('param')
);
break;
// ==============================================
// 経験値バーの処理 =============================
//
case 'expbar':
await doAnimateExpBar(
now.data('param')
);
break;
// ==============================================
// レベルアップ処理 =============================
//
case 'levelup':
await doAnimateLevelUp(
now.data('param')
);
break;
// ==============================================
}
// 次のメッセージへ
await nextMsg(now);
}
resolve();
});
}
--省略
// ==============================================
// レベルアップ処理 =============================
//
/**
* 経験値バーのアニメーションを実行
* @param json
* @return Promise
**/
var doAnimateLevelUp = function(param){
return new Promise ((resolve, reject) => {
var expbar = $("#expbar");
expbar.hide();
expbar.css('width', 0);
// レベルアップ
$("#level").text(param.level);
// HPバーの変更
var hpbar = $("#hpbar-friend");
hpbar.attr('aria-valuenow', param.remaining_hp);
hpbar.attr('aria-valuemax', param.max_hp);
hpbar.css('width', param.remaining_hp_per + '%');
// 経験値バーをリセット
expbar.animate({
width: 0
}, {
duration: 1,
easing: 'easeOutQuad',
complete: function(){
// 処理完了(css変更のズレがあるため0.5秒後にresolveを返却)
setTimeout(function() {
expbar.show();
resolve();
}, 500);
}
});
});
}
レベルアップ処理の際に経験値バーを0に戻す必要がありますが、その際にCSSの変更で0%にすると、減少モーションが発生してしまいます。そうならないために、一度hideで非表示にしてから、animateを使ってwidthを0に変更。完了後にshowで表示状態に戻してresolveを返却しています。
それでは実際のアニメーションを見てみましょう。
まだテンポの違和感が残っていますがアニメーション自体は問題なく実装できました。
まとめ
いかがだったでしょうか。
今回のPHPポケモンでは「経験値バーのアニメーション」と「レベルアップ処理」の実装方法をご紹介しました。
ゲームづくりやWEBプログラミングに興味がある人は、ぜひ参考にしてみてくださいね。