ミニゲームを作ってUnityを学ぶ![タンクウォーズ編]
###第5回目: HPの管理とステージの作り込み
これまではプレイヤーの操作する戦車についてのみ機能を実装してきましたが、ここからようやく対戦相手となる戦車オブジェクトを追加していきます。
#対戦相手の追加
- PlayerTankのプレハブをシーンの適当な場所に配置
- 配置されたオブジェクトの名前を「EnemyTank」に変更
- EnemyTankのプレハブを書き出し
以上で敵キャラクターが完成しました。
こちらの戦車についてはプレイヤーが操作できないように、mIsPlayerのチェックを外しておきます。
#戦場の作成
続いて、今はまだ地面だけの戦場をゲームシステムに合わせて作りこみます。
最終的には以下のようにプレイヤーと相手が左右に分かれて弾を撃ちあう形になります。
###バリケードテープを地面に貼る
- 上の画像をダウンロード後「BarricadeTape」に名前を変更してプロジェクトにインポート
- 新しいマテリアル「Barricade」を作成してAlbedoにインポートしたBarricadeTapeを設定
- シーンに2つのPlane「BarricadeLeft」と「BarricadeRight」を作成して、それぞれに「Barricade」マテリアルをアタッチ
- BarricadeLeftとBarricadeRightのTransformを下記のように設定
Position(x, y, z) | Rotation | Scale | |
---|---|---|---|
BarricadeLeft | (-17, 0.02, 0) | (0, 90, 0) | (5, 1, 0.2) |
BarricadeRight | (17, 0.02, 0) | (0, 90, 0) | (5, 1, 0.2) |
###戦車の位置を変更
PlayerTankとEnemyTankのPositionを下記のように設定
Position(x, y, z) | |
---|---|
PlayerTank | (-20, 1.2, 0) |
EnemyTank | (20, 1.2, 0) |
これで先ほどの画像と同じような見た目になりました。
###見えないコライダーで戦車の移動を制限する
このままだと戦車が上下移動をした際に画面外へ出ていくことが可能で、最終的にはGroundを超えて奈落に落ちていってしまいます。
これを防止するため、先に進んでほしくない位置にコライダーの壁を作ることで戦車の移動を制限します。
- Cubeを2つ作成し、それぞれ名前を「BarricadeTop」「BarricadeBottom」に変更
- Mesh Rendererのチェックを外して透明状態にする
- Transformステータスを下記のように設定
Position(x, y, z) | Rotation | Scale | |
---|---|---|---|
BarricadeTop | (0, 0, 18) | (0, 0, 0) | (50, 1, 1) |
BarricadeBottom | (0, 0, -17.6) | (0, 0, 0) | (50, 1, 1) |
これで上下とも、透明な壁に当たった時点で戦車の移動が制限されます。
(厳密には制限しきれずに抜けてしまったりするのですが、今後の作り込みによって改善されますので今は気にせずに進めてください)
###戦場オブジェクトをまとめる
ここでHierarchyウインドウを一度整理します。
- 空オブジェクト「Area」をゼロポジションに作成
- 今までに作成した「Ground」「DestroyArea」「4種のBarricade」を「Area」の子に設定
#HPの概念を実装する
戦場と対戦相手が用意できましたので、ここからはスクリプトでHPの概念を実装していきます。
- TankHealthという名前でスクリプトを作成
- PlayerTankとEnemyTankにTankHealthをアタッチ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class TankHealth : MonoBehaviour
{
[SerializeField]
[Tooltip("HPの初期値")]
private int mHp = 100;
private int mDamage;
/// <summary>
/// 戦車に被弾ダメージを与える
/// </summary>
/// <param name="damage"></param>
/// <returns>HPが0になった場合はtrueを返す</returns>
public bool AddDamage(int damage)
{
mDamage += damage;
if (mDamage > mHp) mDamage = mHp;
CalHealthBarRate();
if (mDamage >= mHp) return true;
return false;
}
private void CalHealthBarRate()
{
}
}
最大時のHPと受けたダメージという2つのフィールドを持ったシンプルなクラスです。
AddDamage()は弾が当たったときに呼び出されるメソッドで、ダメージを受けた後HPが残っているかどうかの結果を返します。
CalHealthBarRate()については後述しますので今は気にしないでください。
戦車の当たり判定と弾の命中処理
これで戦車にHPが設定できましたので、次は当たり判定と弾が当たったときの処理を作っていきます。
###Box Colliderで当たり判定を設定
今回は当たり判定をよりシンプルにするため、戦車オブジェクトに最初からアタッチされている2つのMesh Colliderを外して代わりにBox Colliderをアタッチします。
- PlayerTank, EnemyTankともにアタッチされている2つのMesh Colliderを外す
- 両戦車にBoxColliderをアタッチ
- 数値を下記のように設定
Center(x, y, z) | Size |
---|---|
(0, 0.4, -0.08) | (2.6, 3, 4.8) |
- 両戦車のタグを「Tank」に設定
###BulletModelに命中処理を追加
private readonly string TAG_TANK = "Tank";
[SerializeField]
[Tooltip("爆発エフェクト小")]
1: private GameObject mExplosionSmallPrefab;
[SerializeField]
[Tooltip("爆発エフェクト大")]
2: private GameObject mExplosionLargePrefab;
/// <summary>
/// 弾が戦車に当たった際の処理
/// </summary>
/// <param name="collider"></param>
void OnTriggerEnter(Collider collider)
{
if (collider.tag == TAG_TANK)
{
// 被弾した戦車にダメージを与える
bool IsDead = collider.transform.GetComponent<TankHealth>().AddDamage(mAttackValue);
if (!IsDead)
{
// おおまかな接触点を取得(OnCollisionEnterとは異なり、triggerでは正確な座標を取得できない:でも着弾点としては十分?)
Vector3 hitPos = collider.ClosestPointOnBounds(mTrans.position);
// 接触した場所に爆発エフェクトを生成
Instantiate(mExplosionSmallPrefab, hitPos, Quaternion.identity);
}
else
{
// HPが無くなった場合は戦車中央のポイントに大きな爆発エフェクトを生成
Vector3 hitPos = collider.transform.position;
Instantiate(mExplosionLargePrefab, hitPos, Quaternion.identity);
// 対象を死亡状態へ
collider.transform.GetComponent<TankModel>().IsDead = true;
}
// この弾を休眠状態へ
Sleep();
}
}
1: プレハブExplosionSを設定
2: プレハブExplosionLを設定
弾オブジェクトがTankタグの付いたコライダーに接触した場合に、そのコライダーからTankHealthを取得してAddDamage()を実行します。
その結果、戦車のHPが残っている場合は小爆発のエフェクトが発生。
残っていない場合は大きな爆発エフェクトを発生させ、同時にTankModelの死亡フラグを立てています。
そして対象の生死に関わらず、最後に自身(弾)を休眠状態にするという流れです。
#HPを可視化する
戦車にHPの概念を実装し、そのHPが被弾で減るという処理が完成しました。
最後にこのHPを格闘ゲームのHPバーのような形で画面内に表示します。
###Spriteの準備
- 下の画像を「HealthBar」という名前でインポート
- Texture TypeをSprite(2D and UI)に変更
- Sprite ModeをMultipleに変更
- Sprite Editorを使って以下のように2つのSpriteに切り分ける
x | y | w | h | Pivot | |
---|---|---|---|---|---|
HealthBarBack(上) | 0 | 224 | 256 | 32 | Center |
HealthBarFront(下) | 0 | 192 | 256 | 32 | Center |
この際に理屈はわからないのですがSpriteのFilter Modeを「Point(no filter)」に設定しておかないとシーンに配置されたときに画像の一部分が欠けてしまうようです。
###HPバーの配置
画像が準備できたらシーンにCanvasを作成し、HPバーをちょうど良さそうな位置に配置していきます。
1. ルートパネル
- Canvas内に「PanelHealthBar」という名前でPanelを作成
- Imageのチェックを外してPanel自体は表示されないようにする
- Rect Transformを以下のように設定
2. 左側のHPバー
- PanelHealthBarの子に「HealthBarBackLeft」という名前のImageを作成
- HealthBarBackLeftのSource Imageに先ほど切り分けたHealthBarBackを設定
- PanelHealthBarの子に「HealthBarFrontLeft」という名前のImageを作成
- HealthBarFrontLeftのSource Imageに先ほど切り分けたHealthBarFrontを設定
- 両ImageのRect Transformを以下のように設定
HPバーは2つのSpriteを重ねあわせて表現していますが、もしこのときに青色のゲージが手前に表示されていない場合はインスペクタからImageの並び順を変更して青色のゲージが手前に表示されるように修正してください。
3. 右側のHPバー
左側のHPバーと同様に右側にもHPバーを表示させます。
- PanelHealthBarの子に「HealthBarBackRight」という名前のImageを作成
- HealthBarBackRightのSource Imageに先ほど切り分けたHealthBarBackを設定
- PanelHealthBarの子に「HealthBarFrontRight」という名前のImageを作成
- HealthBarFrontRightのSource Imageに先ほど切り分けたHealthBarFrontを設定
- 両ImageのRect Transformを以下のように設定
右側のHPバーについては、Rotationの値によって画像を反転させている点に注意してください。
4. HPバーを割合表示に対応させる
最後に、両方に配置したHPバーの青いゲージ部分「HealthBarFront~」のImage Typeを変更します。
Image Type | Fill Method | Fill Origin | Fill Amount |
---|---|---|---|
Filled | Horizontal | Right | 1 |
[Image Type = Filled]
この設定にした画像は描画範囲を割合で指定できるようになります。
上記の設定でいえば「水平方向について、Amountの値が0から1に近くなるほど右から左まで」
描画されていきます。
*右のHPバーは反転されているため左から右に向かって描画されます
5. HPバー上部のテキスト
HPバーの上部に「Player」と「Enemy」のテキストを追加します。
- HealthBarFrontLeft内にテキストを作成
- TextのColorを(R=255, G=255, B=255, A=255)に変更
- Textの赤枠部分とポジションを以下のように設定
- HealthBarFrontRight内にテキストを作成
- TextのColorを(R=255, G=255, B=255, A=255)に変更
- Textの赤枠部分とポジションを以下のように設定
###HPバーをスクリプトで制御する
出来上がったHPバーが実際の戦車のHPと連動するように、TankHealthクラスとTankModelクラスにコードを追加していきます。
private enum STATE
{
DEFAULT = 0,
DAMAGE,
HEAL
}
private STATE mState = STATE.DEFAULT;
private readonly float UNIT_FILL_AMOUNT = 1.0f;
[SerializeField]
1: private Image mHealthBar; // 描画を制御したい画像(HPバーの青い部分)
private float mCurrentFillAmount = 1.0f; // 現在描画されているHPバーのfillAmount
private float mPreFillAmount; // 増減アクションの終了条件となるHPバーのfillAmount
/// <summary>
/// 残りHPに合わせてHPバーの描画割合を決定し、適切なmStateを設定する
/// </summary>
private void CalHealthBarRate()
{
float currentHp = (float)(mHp - mDamage);
mPreFillAmount = Mathf.Clamp(currentHp / mHp, 0.0f, 1.0f);
if (mPreFillAmount < mCurrentFillAmount)
{
mState = STATE.DAMAGE;
}else if(mPreFillAmount > mCurrentFillAmount)
{
mState = STATE.HEAL;
}else
{
mState = STATE.DEFAULT;
}
}
public void RenewHealthBar()
{
switch (mState)
{
case STATE.DAMAGE:
mCurrentFillAmount -= UNIT_FILL_AMOUNT * Time.deltaTime;
if (mCurrentFillAmount < mPreFillAmount)
{
mCurrentFillAmount = mPreFillAmount;
mHealthBar.fillAmount = mCurrentFillAmount;
mState = STATE.DEFAULT;
return;
}
mHealthBar.fillAmount = mCurrentFillAmount;
break;
}
}
1: PlayerTankの場合はHealthBarFrontLeftを設定し、EnemyTankの場合はHealthBarFrontRightを設定。
CalHealthBarRate()は戦車が被弾した際に実行されるAddDamage()から呼び出され、残りHPの割合から青いHPゲージをどのくらい描画すれば良いかを計算しています。
また同時に、この戦車の状態mStateを「通常・被弾・回復」のいずれかに更新しています。
RenewHealthBar()では状態mStateが被弾(DAMAGE)の場合のみ、青いHPゲージの表示割合を徐々に減らす処理を行っています。
private TankMovement mMovementScript;
private TurretController mTurretScript;
private FireController mFireScript;
追加 private TankHealth mHealthScript;
void Start()
{
SetLayerCollision();
mMovementScript = GetComponent<TankMovement>();
mTurretScript = GetComponent<TurretController>();
mFireScript = GetComponent<FireController>();
追加 mHealthScript = GetComponent<TankHealth>();
}
void Update()
{
if (IsPlayer && IsActive)
{
// 移動入力の受付
mMovementScript.CheckInput();
// 砲台角度を計算
mTurretScript.CalRotation();
// 弾発射の入力を受付
mFireScript.CheckInput();
}
// HPバーの更新
追加 mHealthScript.RenewHealthBar();
}
いつもと同じようにTankModelクラスのUpdate()でHPバーの更新が行えるようコードを追加しますが、今回はプレイヤーでなくてもHPが減ること、被弾して死亡フラグが立った段階ではまだ青いHPゲージが残っている状態であることを加味して、条件式の外から呼び出します。
プロジェクトを実行して、HPバーが戦車の被弾によって減っていくことを確認します。