HPバーアニメーション
それでは前回に続き、HPバーのアニメーションづくりをしていきましょう。前回、メッセージに合わせてレスポンスを返却するというサーバー側の仕組みを作成しました。なので、今回はそれをフロント側で受け取り、タイミングよくアニメーションで再現します。
フロント側(js)の処理
前回も説明したとおり、画面移管したタイミングには「処理が終了している状態」です。なので、JavaScript(jQuery)を使った処理はあくまで見た目だけの演出をするだけです。
最初に変更する点は、HPバーの値です。こちらは現在、計算後の結果が出力されているため、前回作成した計算前のHP(getBeforeRemainingHp)をセットし直しましょう。
相手のHP
<?php # 敵ポケモン詳細 ?>
<div class="col-6">
<p><?=$enemy->getName()?> Lv:<?=$enemy->getLevel()?> <?=$enemy->getSaName(false)?></p>
<div class="form-group">
<div class="progress">
<div id="hpbar-enemy"
class="progress-bar bg-success"
role="progressbar"
style="width:<?=$controller->getBeforeRemainingHp($enemy, 'per')?>%;"
aria-valuenow="<?=$controller->getBeforeRemainingHp($enemy)?>"
aria-valuemin="0"
aria-valuemax="<?=$enemy->getStats('HP')?>"></div>
</div>
</div>
</div>
味方のHP
<?php # 自ポケモン詳細 ?>
<div class="col-6 text-center">
<img src="/Assets/img/pokemon/dots/back/<?=get_class($pokemon)?>.gif" alt="<?=$pokemon->getName()?>">
</div>
<div class="col-6">
<p><?=$pokemon->getNickName()?> Lv:<?=$pokemon->getLevel()?> <?=$pokemon->getSaName(false)?></p>
<div class="form-group">
<div class="progress">
<?php if($controller->getBeforeRemainingHp($pokemon, 'per') <= 50) $hp_bar_class = 'bg-warning'; ?>
<?php if($controller->getBeforeRemainingHp($pokemon, 'per') <= 20) $hp_bar_class = 'bg-danger'; ?>
<div id="hpbar-friend"
class="progress-bar <?=$hp_bar_class ?? 'bg-success'?>"
role="progressbar"
style="width:<?=$controller->getBeforeRemainingHp($pokemon, 'per')?>%;"
aria-valuenow="<?=$controller->getBeforeRemainingHp($pokemon)?>"
aria-valuemin="0"
aria-valuemax="<?=$pokemon->getStats('HP')?>"></div>
</div>
<p class="text-right px-3">
<span id="remaining-hp-count-friend"><?=$controller->getBeforeRemainingHp($pokemon)?></span>
/ <?=$pokemon->getStats('HP')?>
</p>
<?php # 経験値バー ?>
<div class="progress" style="height:4px;">
<div class="progress-bar bg-primary" role="progressbar" style="width:<?=$pokemon->getPerCompNexExp()?>%;" aria-valuenow="<?=$pokemon->getPerCompNexExp()?>" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
このHPバーが今回の操作対象となるため、それぞれに判別値としてidにhpbar-enemyとhpbar-friendを割り当てています。
パラメーターのセット
次に、パラメーターのセットです。これはメッセージに合わせてIDを振ったレスポンスが該当します。メッセージが進んだらアニメーションを実行させるので、このレスポンスデータは対象のメッセージに対して割当てしておきましょう。
メッセージボックス
<div class="message-box border p-3 mb-3">
<?php # メッセージエリア ?>
<?php foreach($controller->getMessages() as $key => list($msg, $status, $auto)): ?>
<?php $class = $key === $controller->getMessageFirstKey() ? 'active' : ''; ?>
<?php $last_class = $key === $controller->getMessageLastKey() ? 'last-message' : ''; ?>
<p class="result-message <?=$class?> <?=$last_class?> <?=$status ?? ''?>"
data-action="<?=$responses[$status]['action'] ?? ''?>"
data-target="<?=$responses[$status]['target'] ?? ''?>"
data-param="<?=$responses[$status]['param'] ?? ''?>"
data-auto="<?=$auto ?? ''?>">
<?=$msg?>
</p>
<?php endforeach; ?>
<span class="message-scroll-icon">▼</span>
</div>
返却したパラメーターをそれぞれdata要素としてセットしました。これで、どのメッセージでどのアクションをすれば良いのか、js側でも判別することができます。
アニメーションの実装
それでは本格的なアニメーションを再現するために、jQueryの処理に入りましょう。今までjQueryはslim版を使用していましたが、今回animateなどのメソッドを使用したいのでフル版を読み込むようにしておいてください。また、イージングなどの指定で必要になる可能性があるため、jQueryのUIも追加で読み込んでいます。
今回カスタマイズするのはmessage.jsです。
メッセージ用JS(Public/Assets/js/Battle/message.js)
/**
* メッセージボックスクリック時の関数
* @function click
* @return void
**/
var clickMsgBoxInit = function(){
var click = true;
$('.message-box').click(async function(){
if(click === false) return;
// メッセージボックスを処理終了まで無効化
click = false;
// 現在のメッセージ
var now = $('.result-message.active');
await actionMsgBox(now);
// メッセージボックスを有効可
click = true;
});
}
まずはメッセージクリック時の処理からです。メッセージボックスを連打されてしまうとアニメーションが複数回実行されてしまうため、まずclick変数にtrueをセットしておきます。クリックされたらすぐにその値をチェックして、falseであれば処理をストップ、trueであればfalseをセットしてから処理に移ります。
処理が終了すれば、trueを再度セットしてメッセージボックスのクリック処理を有効可します。こうして置けば、処理中に追加で処理の入力が入らないためおかしな挙動を制御することができます。
非同期処理とは
ここで非同期処理について簡単に触れておきます。アニメーションなど時間がかかる処理の殆どが「非同期処理」です。これはサーバー側(PHP)では馴染みがないので、慣れていない人は悩まされるポイントです。
今回のケースであれば、HPを減少させる際にanimateというメソッドを使用しています。これを5秒掛けて実行させた場合、その5秒間の間にjsは次の処理をどんどんと進めて行ってしまい、animateの処理は置いてけぼりとなってしまいます。そうなれば、処理終了判定がされた時点ではまだ処理が終わっていない、といったことにもなりかねません。それを「処理が終了してから次の処理へ移行する」とするために、関数手前にasyncを付与して、非同期処理が含まれている関数(actionMsgBox)の前にawaitを付与しました。
では、そのactionMsgBox関数の中身を見てみましょう。
/**
* メッセージアクション
* @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('action'),
now.data('target'),
now.data('param')
);
// ==============================================
}
// 次のメッセージへ
await nextMsg(now);
}
resolve();
});
}
ただawaitが付いていても、すべての処理が終わったかどうかを勝手に判断してくれるわけではありません。「完了しました」ということを伝えるためにはPromiseというクラスを使用します。
クラス内で処理が終われば、resolveというメソッドを呼び出すことで、そのPromiseクラスのステータスが完了状態へと移行してくれ、awaitに対して処理が終わったことを伝えてくれます。もしresolveが呼び出されていなければ、一生そこで処理が止まってしまうことになります。
更にこの処理の中でもHPバーの変動処理などは行わないため、更にPromiseクラスで実行するコールバック関数にもasyncを付与して、內部で呼び出す関数に対してawaitを付与していきます。
このように、アニメーションによる処理を行う際には連鎖的につなげていくことになります。jQueryは導入が簡単に出来ますが、フロント側で多くの処理をさせるためにはこれを連連と書いていかなければならなく、規模が大きくなればなるほど視認性が悪くなり保守性も落ちてしまいます。なので、本格的なアプリケーション開発ではvueなどのフレームワークの導入を検討した方が良いでしょう。PHPポケモンではjQueryで押し切る予定です。
残りの処理も一気に見ていきましょう。
/**
* HPバーのアニメーションを実行
* @param string action
* @param string target
* @param mixed param
* @param now element
* @return Promise
**/
var doAnimateHpBar = function(action, target, param){
return new Promise((resolve, reject) => {
// 対象のHPバーを取得
var hpbar = $('#hpbar-' + target);
var hp = hpbar.attr('aria-valuenow') - param;
// 最小値の処理
if(hp < 0){
hp = 0;
}
// 最大値の処理
if(hp > hpbar.attr('aria-valuemax')){
hp = hpbar.attr('aria-valuemax');
}
// 処理後のHPバーの長さを算出
var width = hp / hpbar.attr('aria-valuemax') * 100;
// 非同期1
var promise1 = new Promise((resolve, reject) => {
// 長さを変更
hpbar.animate({
width: width + "%"
}, {
duration: 500,
easing: 'easeOutQuad',
complete: function(){
// 処理完了(css変更のズレがあるため0.5秒後にresolveを返却)
setTimeout(function() {
resolve();
}, 500);
}
});
});
// 非同期2
var promise2 = new Promise( async (resolve, reject) => {
// HPカウンターのアニメーション
if(target === 'friend'){
// 引数は数値でセット
await countHp(
parseInt(hpbar.attr('aria-valuenow'), 10),
parseInt(hp, 10)
);
}
return resolve();
});
Promise.all([promise1, promise2]).then(() => {
// 残HPの値を変更
hpbar.attr('aria-valuenow', hp);
return resolve();
});
// ==============================================
});
}
/**
* HPの数値カウント処理
* @param integer start
* @param integer end
* @return Promise
**/
var countHp = function(start, end){
return new Promise((resolve, reject) => {
// もし開始と終了が同じであれば処理不要
var diff = start - end;
if(diff === 0){
return resolve();
}
// 数値の変動処理関数
var counter = function(){
// 実行回数が+かーかで数値の変動方向を判定
if((start - end) > 0){
// 減算(ダメージ)
start--;
}else{
// 加算(回復)
start++;
}
$('#remaining-hp-count-friend').text(start);
}
// 繰り返し関数
var time = parseInt(1000 / diff, 10);
var interval_id = setInterval(function(){
counter();
if(start === end){
clearInterval(interval_id);
return resolve();
}
}, time);
});
}
パラメーターにセットされた値を取得しながら計算を行い、animate内でcssのwidthを変更するといった至って単純な処理です。味方のHPバーを操作する際は、数値をカウントする必要があるのでsetIntervalを使って1ずつ回転させています。
この辺りに関しては、深く触れていきません。理由としては「あくまで簡易的なアニメーションであり、そこまで正確にゲームを再現できていない」からです。今回はサーバー側の処理を優先、連動させるために行なっているため、サーバー側から返却した値が正確に受け取れていればOKとしています。
オートメッセージ
オートメッセージについても触れておきましょう。今回メッセージの処理を関数にまとめたのは、このオートメッセージが理由の1つです。
/**
* メッセージボックスクリック時の関数
* @function click
* @return void
**/
var clickMsgBoxInit = function(){
var click = true;
// 変数をリセット
auto_msg = false;
$('.message-box').click(async function(){
if(click === false) return;
// メッセージボックスを処理終了まで無効化
click = false;
// 現在のメッセージ
var now = $('.result-message.active');
await actionMsgBox(now);
// 次がオートメッセージの場合は再度実行
while(auto_msg){
now = now.next();
await actionMsgBox(now);
}
// メッセージボックスを有効可
click = true;
});
}
whileによるループ処理を追加しました。もし現在選択されているメッセージにautoのパラメーターが付与されていれば、次に進んで再度actionMsgBoxを実行させています。こうすることで、やどりぎのタネなどの処理はボタンを押さずとも順番に自動進行してくれます。
/**
* 次のメッセージへ移行する処理
* @param now element
* @return Promise
**/
var nextMsg = function(now){
return new Promise( async (resolve, reject) => {
// 現在のメッセージのactiveを解除
now.removeClass('active');
// 次のメッセージにactiveを付与
var next = now.next();
next.addClass('active');
/**
* メッセージのステータスに合わせた分岐
**/
// バトル終了
if(next.hasClass('battle-end')){
$('#remote-form-action').val('end');
$('#remote-form').submit();
return;
}
// 最終メッセージかどうかの判別
if(next.hasClass('last-message')){
// 最終メッセージ
doLastMsg();
}else{
// 最終メッセージではない
doNotLastMsg();
}
// 次のメッセージがオートメッセージかどうかの判定
if(next.data('auto')){
auto_msg = true;
}else{
auto_msg = false;
}
// 処理終了
resolve();
});
}
nextMsgの関数でもPromiseを使って非同期処理として結果を返せるようにしました。
実際に動きを確認してみましょう。
大分それっぽくなってきましたね。ゲームのようになめらかな動きにはまだまだほど遠いですが、簡易的なものとしては十分です。
残る問題点
ある程度遊べるレベルになってきたので、ここで現在残る問題点を挙げておきます。ちなみに、セーブ機能やアイテム、ポケモン交代など機能的に実装していないものは今回除きます。
HPバーの色
こちらは長さに合わせてクラスを変更するだけで実装できますが、現在はまだ未実装のため残る問題としておきます。色を変えるタイミングなど、本来は50%以下になった時点で黄色、20%以下になった時点で赤と変化させなければならないのですが、クラスを変更したタイミングで変わってしまうためどうしようかと悩み中です。
HPゲージの減り時間なども、もう少し本物に近づけたいなとは考えているので、もし良い案があって暇な人はお問い合わせで教えてください。
ちなみに、ライブラリで良いものはどんどん導入していきますが、フレームワークを導入するようなことはないと思います。
レベルアップ
こちらは経験値バーの処理も合わせてなのですが、レベルアップ処理をどうしようかと悩み中です。初代では一気に次のレベルへ押上げしていましたが、レベルは1ずつあがり経験値バーが変動してレベルアップしていく仕組みを実装したいと考えています。ただレベルアップ処理だけを実装するのであれば、HPバーの応用で再現ができそうなのですが、これに合わせて残る問題が次の2つです。
技の習得
レベルアップ処理に関連して起こる問題の1つが技の習得です。レベルアップに合わせて技を習得させてあげなければならないのですが、その仲介処理を挟むにはどうしたものかと悩み中です。また、現在の仕組み上は技は順番に上から上書きされるようにしていますが、通常であれば選択できなければならないため、画面移管を途中で挟む必要があります。
ajaxによる書き換えをするという方法もありますが、正直こちらは最終手段としてしか考えていません。なぜならDBを使っていない関係上、ajaxでapi接続するにはいろんなパラメーターを投げて変更しなければいけないのでは?と薄っすら思っています。悩ましい。
進化
レベルアップに関係する処理の1つとして、進化があります。現在はサーバー側で一括変更しているため、進化すれば画面移管後には進化後の姿に変化しています。こちらはバトル終了のタイミングに移すだけで良いのかも知れませんが、進化キャンセルなども含めていろんな問題やクリアしなければならない問題があり、良い案が浮かぶまでは後回しとしています。
ちなみに、レベルアップや進化をすると最大HPが変更になるため、そのタイミングだけ一時的に表示されている最大HPと残りHPに乖離が発生します。
まとめ
いかがだったでしょうか。
今回のPHPポケモンは「HPバーアニメーション」についてフロント側の対応方法をご紹介しました。
サーバー側と異なり、ほぼノー説明でのコード紹介となりましたが、その点についてはご容赦ください。なぜならjsは必要に応じて触る程度なのでそこまで詳しく説明しろと言われても難しいというのが本音です。記述しているものはもちろん分かって書いてはいますが、他に比較対象となる知識量も少なく、良し悪しや裏側では実際にどう動いているのかまで突き詰められるとそこまで詳しくは紹介できないのが理由の1つです。
また、前述しましたがあくまでフロント側の動きはサーバー側のパラメーターを受け取って流通りにアクションを実行していくことができるかどうかを再現しただけに過ぎません。なので、ある程度方向性が固まればその際には説明できるようにしておきます。
最後にまとめた残る問題点についてですが、フロント側との処理と連動していくとなり色々と浮き彫りになってきました。開発者目線からしたら大きな進歩ではあるのですが、プレイヤー側からすれば「おかしくない?」となってしまう部分が多いでしょう。
実際のゲーム開発ではどの言語をどのように使っているか詳しく知りませんが、WEBプログラミングではこのように複数言語とマークアップを組み合わせながら作成しています。環境準備のしやすさや、すぐに形として見られるという点に置いては入りやすいかも知れませんが、プログラミング言語としての不備などは業界からすれば気になる人も多いはずです。
ただ、プログラミングというものに興味を持つということに関しては、良いきっかけになり学習コストやハードルも適切なものではないかと考えています。
いつもよりも少し長めのまとめとなりましたが、ゲーム開発やアプリケーション開発、サイト制作などプログラミングに興味がある人は、ぜひ参考にしてくださいね。