今回のPHPポケモンでは主に画面の作り込みをしていきます。
とは言っても、ガッチリCSSを書いてよりゲームらしい見た目にするわけではなく、あくまで「ゲームシステムを再現するため」だけに整えていくのが目的です。
ということで、今回はPHPよりもBootstrapさんとjQueryさんに活躍してもらいます。
メッセージの表示
現在、攻撃時のメッセージ等をすべて一括出力をしていますが、実際のゲームではメッセージに合わせて動きがあり、すべて読み終わればアクションを選択できるという仕様です。これが再現できれば、各メッセージのタイミングでアクションを起こしたり、ひんし判定が行いやすくなるので上手く活用しながら今後のシステムづくりにつなげていきます。
まずはメッセージの出力について変更します。
バトル画面(/battle.php)
<div class="col-12 col-sm-6">
<div class="message-box border p-3 mb-3">
<?php foreach($controller->getMessages() as $key => list($msg, $status)): ?>
<?php $class = $key === $controller->getMessageFirstKey() ? 'active' : ''; ?>
<?php $msg_id = $key === $controller->getMessageLastKey() ? 'last-message' : ''; ?>
<p class="result-message <?=$class ?? ''?>" id="<?=$msg_id ?? ''?>"><?=$msg?></p>
<?php endforeach; ?>
<span class="message-scroll-icon">▼</span>
</div>
</div>
※result-boxをmessage-boxに変更しました
今まではforeachを使ってメッセージを1行づつ出力しているだけでしたが、最初のメッセージにはactiveというクラス、最後のメッセージにはlast-messageというidを割り当てるようにしました。
コントローラーから呼び出しているgetMessageFirstKey、getMessageLastKeyというメソッドはレスポンストレイトに追加しています。
レスポンストレイト(/Traits/ResponseTrait.php)
/**
* メッセージの最初のキーを取得
*
* @return void
*/
public function getMessageFirstKey()
{
return array_key_first($this->msgs);
}
/**
* メッセージの最後のキーを取得
*
* @return void
*/
public function getMessageLastKey()
{
return array_key_last($this->msgs);
}
array_key_firstとarray_key_lastの関数を使用して、メッセージの最初と最後を判別しています。こちらで分岐をかけ、それぞれに必要な値を割り当てているという仕組みです。
CSSもメッセージの出力に合わせて簡単に追記しています。
スタイルシート(/Resources/Assets/css/style.css)
.message-box{
position: relative;
height: 90px;
overflow-y: scroll;
cursor: pointer;
}
.result-message{
display: none;
}
.result-message.active{
display: block;
}
.message-scroll-icon{
position: absolute;
right: 8px;
bottom: 8px;
}
基本的にメッセージはdisplay:noneで非表示にしておき、activeが付けば表示させるという単純な仕様です。こうすることで、出力時には最初のメッセージのみが表示されるようになります。
アクションの制御
メッセージが最初の一つだけ表示されても、次のメッセージへ以降できなければ意味がありません。なので、メッセージを進めていく処理をjQueryを使って作成します。
合わせて、アクションの制御もかけます。メッセージが最後(id=”last-message)”)であればアクションを選択可能、そうでなければ無効化(disabled)にします。
バトル画面(/battle.php)
<div class="col-12 col-sm-6">
<div class="row">
<div class="col-6 mb-2">
<button type="button" class="btn btn-outline-light btn-block action-btn" data-toggle="modal" data-target="#select-move-modal" id="action-btn-fight">たたかう</button>
</div>
<div class="col-6 mb-2">
<button type="button" class="btn btn-outline-light btn-block action-btn" id="action-btn-item">どうぐ</button>
</div>
<div class="col-6 mb-2">
<button type="button" class="btn btn-outline-light btn-block action-btn" id="action-btn-pokemon">ポケモン</button>
</div>
<div class="col-6 mb-2">
<form action="" method="post">
<div class="input-group mb-3">
<input type="hidden" name="action" value="run">
<input class="btn btn-outline-light btn-block action-btn" id="action-btn-run" type="submit" value="逃げる">
</div>
</form>
</div>
</div>
</div>
せっかくなので、メッセージの右側に配置してどうぐとポケモンのボタンを追加しました。この2つはまだ未実装ですが見た目だけのために用意しています。
次にjQueryを使った制御です。
メッセージ用js(/Resources/Assets/js/Battle/message.js)
/*----------------------------------------------------------
// 初期化する関数
----------------------------------------------------------*/
/**
* 画面読み込み時の関数
* @function ready
* @return void
**/
var startInit = function(){
// 現在のメッセージ
var now = $('.result-message.active');
if((now.length === 0) || (now.attr('id') === 'last-message')){
doLastMsg();
return;
}
doNotLastMsg();
}
/**
* メッセージボックスクリック時の関数
* @function click
* @return void
**/
var clickMsgBoxInit = function(){
$('.message-box').click(function(){
// 現在のメッセージ
var now = $('.result-message.active');
// 最終メッセージかどうか確認
if((now.length === 0) || (now.attr('id') === 'last-message')){
doLastMsg();
return;
}
// 現在のメッセージのactiveを解除
now.removeClass('active');
// 次のメッセージにactiveを付与
var next = now.next();
next.addClass('active');
// 最後のメッセージであれば操作ボタンを有効可
if(next.attr('id') === 'last-message'){
doLastMsg();
return;
}
doNotLastMsg();
});
}
/*----------------------------------------------------------
// 処理内で呼び出す関数
----------------------------------------------------------*/
/**
* 最終メッセージの処理
* @return void
**/
var doLastMsg = function(){
// スクロールアイコンを非表示
$('.message-scroll-icon').hide();
// 操作ボタンの無効化解除
$('.action-btn').prop('disabled', false);
// ボタンに色付け
$('.action-btn').each(function(){
$(this).removeClass('btn-outline-light');
$(this).addClass('btn-outline-success');
});
}
/**
* 最終メッセージではない場合の処理
* @return void
**/
var doNotLastMsg = function(){
// スクロールアイコンを非表示
$('.message-scroll-icon').show();
// 操作ボタンの無効化
$('.action-btn').prop('disabled', true);
// ボタンの色消し
$('.action-btn').each(function(){
$(this).removeClass('btn-outline-success');
$(this).addClass('btn-outline-light');
});
}
/*----------------------------------------------------------
// 初期化
----------------------------------------------------------*/
jQuery(function($){
startInit();
clickMsgBoxInit();
});
まずは読み込み時の処理とクリック時の処理についてです。
メッセージボックス内がクリックされればメッセージを進めるという仕組みになっています。もし、最後のメッセージ(#last-message)でなければボタンを無効化、最後のメッセージであれば有効化させるという単純な処理です。見た目で判断できるように、クラスを割り当てて表現していますが、このあたりは好みに合わせてください。
出力結果は以下の通りです。
※メッセージの最後にコントローラーで「行動を選択してください」をセットしています
これで大分わかりやすくなり、簡易ながら画面の制御もできるようになりました。
モーダルの活用
今までは技選択がそのままできていましたが、ゲーム再現のため「たたかう」を選択したら技のウィンドウを表示させるという仕様に変更しましょう。Bootstrapではおなじみのモーダルウィンドウを使用します。
技選択モーダル(/Resources/Partials/Battle/Modals/move.php)
<!-- Modal -->
<div class="modal fade" id="select-move-modal" tabindex="-1" role="dialog" aria-labelledby="select-move-modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="select-move-modal-title">技を選択</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<?php include(__DIR__.'/../Forms/move.php'); # たたかう ?>
</div>
</div>
</div>
</div>
battle.phpの記述量が多くなってきたのでパーツ分けしました。bodyの最後で読み込んでおいてください。
※jsやmetaなどもパーツ分けして読み込むように変更しています
モーダルを活用することで、画面の余裕が出てきました。アイテムやポケモンの選択を作る際にもモーダルを使用する予定です。
見た目の作り込み
処理そのものとは大きく関係ありませんが、見た目もそれらしく整えていきましょう。見た目が良くなれば、それだけで「ゲームらしさ」が演出できますし、より楽しく感じさせるための重要なポイントの一つです。
HPバー
前回input rangeの使いみちについて舞い上がっていましたが、Bootstrap4.2の公式マニュアルをじっくり読んでいくと、よりそれらしいProgressというものが見つかったのでそちらを採用します。
Progress(Bootstrap)
これこそHPバーを再現するためだけに用意されたようなものです。ただ、rangeと違って値を割り当てるだけでは残ゲージは表示されず、結局widthを指定しなければならないという仕様だということが発覚したので、これに合わせて「残りHPの割合」を取得するためのメソッドを用意します。
Get格納トレイト
/**
* 残りHPを取得
* @return string $param
* @return integer
*/
public function getRemainingHp($param='')
{
if($param === 'per'){
// 最大HPとの比率を%で取得(数値で返却)
return $this->remaining_hp / $this->getStats('HP') * 100;
}else{
return $this->remaining_hp;
}
}
残りHPを取得するgetRemainingHpというメソッドに引数を追加し、%の状態にして返却する機能を追加しました。これをwidthに突っ込めばProgressバーでも十分にHPが再現可能です。
バトル画面(/battle.php)
<div class="row mb-3">
<?php # 自ポケモン詳細 ?>
<div class="col-6 text-center">
<img src="Resources/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()?></p>
<div class="form-group">
<div class="progress">
<?php if($pokemon->getRemainingHp('per') <= 50) $hp_bar_class = 'bg-warning'; ?>
<?php if($pokemon->getRemainingHp('per') <= 20) $hp_bar_class = 'bg-danger'; ?>
<div class="progress-bar <?=$hp_bar_class ?? 'bg-success'?>" role="progressbar" style="width:<?=$pokemon->getRemainingHp('per')?>%;" aria-valuenow="<?=$pokemon->getRemainingHp()?>" aria-valuemin="0" aria-valuemax="<?=$pokemon->getStats('HP')?>"></div>
</div>
<p class="text-right px-3"><?=$pokemon->getRemainingHp()?> / <?=$pokemon->getStats('HP')?></p>
</div>
</div>
</div>
最新世代と同じように、残りHPが50%以下なら黄色ゲージ、20%以下なら赤ゲージで表示されるようにクラスを振り分けました。この辺りは動的に再現するとjQueryでの処理になりますが、現状はこのまま進めていきます。ゲージ色の変化は敵ポケモンには不要です。
色ゲージの判定が完成しました。改めてBootstrapの優秀さが実感させられます。
ホーム画面
ホーム画面もせっかくなのでモーダルやプログレスバーを使って見やすく整えていきます。
ホーム画面(/home.php)
<?php
require_once(__DIR__.'/Classes/Controller/HomeController.php');
require_once(__DIR__.'/Resources/Lang/Translation.php');
session_start();
$controller = new HomeController();
$pokemon = $controller->getPokemon();
$_SESSION['pokemon'] = $pokemon->export(); # ポケモンの情報をセッションに格納
?>
<!DOCTYPE html>
<html lang="jp" dir="ltr">
<head>
<?php
# metaの読み込み
include(__DIR__.'/Resources/Partials/Layouts/Head/meta.php');
# cssの読み込み
include(__DIR__.'/Resources/Partials/Layouts/Head/css.php');
?>
</head>
<body>
<header>
<div class="container">
<section>
<div class="row">
<div class="col-12">
<h1 class="py-3">PHPポケモン</h1>
</div>
</div>
</section>
</div>
</header>
<main>
<div class="container">
<section>
<div class="row my-5">
<div class="col-3 offset-md-2 text-center">
<img src="Resources/Assets/img/pokemon/dots/front/<?=get_class($pokemon)?>.gif" alt="<?=$pokemon->getName()?>" style="cursor:pointer;" data-toggle="modal" data-target="#pokemon-details-modal">
</div>
<div class="col-9 col-md-5">
<p><?=$pokemon->getNickName()?> Lv:<?=$pokemon->getLevel()?> <?=$pokemon->getSaName()?></p>
<div class="form-group">
<div class="progress">
<?php if($pokemon->getRemainingHp('per') <= 50) $hp_bar_class = 'bg-warning'; ?>
<?php if($pokemon->getRemainingHp('per') <= 20) $hp_bar_class = 'bg-danger'; ?>
<div class="progress-bar <?=$hp_bar_class ?? 'bg-success'?>" role="progressbar" style="width:<?=$pokemon->getRemainingHp('per')?>%;" aria-valuenow="<?=$pokemon->getRemainingHp()?>" aria-valuemin="0" aria-valuemax="<?=$pokemon->getStats('HP')?>"></div>
</div>
<p class="text-right px-3"><?=$pokemon->getRemainingHp()?> / <?=$pokemon->getStats('HP')?></p>
</div>
</div>
</div>
</section>
<section>
<div class="row">
<div class="col-12 col-sm-6">
<div class="message-box border p-3 mb-3">
<?php foreach($controller->getMessages() as list($msg, $status)): ?>
<p><?=$msg?></p>
<?php endforeach; ?>
</div>
</div>
<div class="col-12 col-sm-6">
<?php include('Resources/Partials/Home/Forms/change_nickname.php'); # ニックネームの変更?>
<?php include('Resources/Partials/Home/Forms/add_exp.php'); # 経験値の取得 ?>
<?php include('Resources/Partials/Home/Forms/battle.php'); # バトル ?>
<?php include('Resources/Partials/Home/Forms/reset.php'); # リセット ?>
</div>
</div>
</section>
</div>
</main>
<?php
# JSの読み込み
include(__DIR__.'/Resources/Partials/Layouts/Foot/js.php');
?>
<?php
# モーダルの読み込み
include(__DIR__.'/Resources/Partials/Home/Modals/details.php');
?>
</body>
</html>
詳細モーダル(/Resources/Partials/Home/Modals/details.php)
<!-- Modal -->
<div class="modal fade" id="pokemon-details-modal" tabindex="-1" role="dialog" aria-labelledby="pokemon-details-modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="pokemon-details-modal-title">ポケモン詳細</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body overflow-auto my-2" style="height:360px;">
<div class="row my-3">
<div class="col-5 offset-1 text-center">
<img src="Resources/Assets/img/pokemon/dots/front/<?=get_class($pokemon)?>.gif" alt="<?=$pokemon->getName()?> 前">
</div>
<div class="col-5 text-center">
<img src="Resources/Assets/img/pokemon/dots/back/<?=get_class($pokemon)?>.gif" alt="<?=$pokemon->getName()?> 後">
</div>
</div>
<nav class="nav nav-pills nav-justified btn-group mb-3" id="pokemon-details-tab">
<a class="btn btn-outline-secondary nav-item nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">詳細</a>
<a class="btn btn-outline-secondary nav-item nav-link" id="stats-tab" data-toggle="tab" href="#stats" role="tab" aria-controls="stats" aria-selected="true">ステータス</a>
<a class="btn btn-outline-secondary nav-item nav-link" id="move-tab" data-toggle="tab" href="#move" role="tab" aria-controls="move" aria-selected="true">使える技</a>
</nav>
<div class="tab-content" id="pokemon-details-tabContent">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
<?php # 詳細 ?>
<table class="table table-bordered table-sm table-hover">
<tbody>
<?php foreach($pokemon->getDetails() as $key => $val): ?>
<tr>
<th scope="row" class="w-50"><?=transJp($key)?></th>
<td><?=$val?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="tab-pane fade show" id="stats" role="tabpanel" aria-labelledby="stats-tab">
<?php # ステータス ?>
<table class="table table-bordered table-sm table-hover">
<tbody>
<?php foreach($pokemon->getStats() as $key => $val): ?>
<tr>
<th scope="row" class="w-50"><?=transJp($key)?></th>
<td><?=$val?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="tab-pane fade show" id="move" role="tabpanel" aria-labelledby="move-tab">
<?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 $move): ?>
<tr>
<th scope="row" class="w-50"><?=$move->getName()?></th>
<td><?=$move->getType()->getName()?></td>
<td><?=$move->getPp()?>/<?=$move->getPp()?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
Bootstrapは(ry
所有者判定
最後に所有者の判定を追加します。これは対戦時に「相手の〇〇のなきごえ」というのを表示させるために使います。こちらも使いやすさ重視でポケモンに対してプロパティを用意し、引き継ぎの対象としましょう。
ポケモンクラス(/Classes/Pokemon.php)
/**
* ポケモンの立場
* @var string (enemy:敵|friend:味方)
*/
protected $position = 'enemy';
初期値には相手(enemy)をセットしています。ポケモンをゲットした段階で味方(friend)をセットしましょう。
Set格納トレイト(/Traits/Pokemon/SetTrait.php)
/**
* ポケモンの立場をセットする
* @param string (friend|enemy)
* @return void
*/
public function setPosition($param='friend')
{
// 入力制限
if(in_array($param, ['enemy', 'friend'], true)){
$this->position = $param;
}
}
ホームコントローラー(/Classes/Controller/HomeController.php)
/**
* ポケモンクラスの存在チェック
*
* @param string $pokemon(class_name)
* @var void
*/
private function checkPokemon($pokemon)
{
if(class_exists($pokemon)){
$this->pokemon = new $pokemon;
// Pokemonのインスタンスに溜まったメッセージを取得
$this->setMessage($this->pokemon->getName().'をゲットした!', 'success');
// ポケモンの立場を味方にする
$this->pokemon
->setPosition();
}else{
$this->setMessage('選択されたポケモンは存在しません', 'error');
}
}
技や行動に合わせて「相手の」を分岐させて付与するのも大変なので、接頭語付きの名称を取得できるようにメソッドを作成します。
Get格納トレイト(/Traits/Pokemon/GetTrait.php)
/**
* 名前の手前に接頭語を付ける(相手の)
* @return string
*/
public function getPrefixName()
{
if($this->position === 'enemy'){
return '相手の'.$this->name;
}else{
return $this->name;
}
}
あとは、状態異常や技のメッセージを生成する際に、getPrefixNameを使って名前を取得するだけです。もし味方であればそのままnameプロパティに格納された値が返却されるので、それぞれの処理で分岐を作成する必要がありません。
まとめ
いかがだったでしょうか。
今回のPHPポケモンでは、メッセージの制御と画面の作り込みを重点的にご紹介しました。画面制御ができるようになれば、これから作成する処理と組み合わせてより動的且つゲームを再現することができるようになります。
ゲームづくりに興味がある人、プログラミング学習中の方はぜひ参考にしてみてくださいね。