第09回 プレイヤーの移動制限と様々な修正
前章では音を付けました。 この章では小さなことですが、実装や修正した方が良い部分を説明していきます。
今のままではプレイヤーは画面外へ移動できてしまいます。これを画面内のみ移動できるようにしましょう。 今回は移動の制限にカメラのビューポートとMathf.Clampを使用します。
カメラが映る範囲のことを指します。ビューポートでは正規化された値を使用するので範囲は0から1の間となります。 普段は画面いっぱいに表示されるので値は X 0 Y 0 Width 1 Height 1 となります。
詳しくはドキュメントのCameraを御覧ください。
これから実装するプレイヤーの移動制限はゲームの画面サイズが600x450でなければうまく動作しません。もう一度正しく設定されているか確認しましょう。
DestroyAreaの範囲を背景やカメラの表示範囲と同じにします。Box Collider 2DのSizeを X 8 Y 6にします。
図9.1: 背景とカメラのと同じ範囲
ここでプレイヤーの移動方法を変更します。 プレイヤーの移動制限を行うのにMathf.Clampを使用しますが、これはtransform.positionの位置情報を使用して制限を行います。 ですが自動で位置情報を更新する物理挙動(今回はrigidbody2D.velocity)とMathf.Clampでの移動制限を行うと、不都合なことが起こります。 それは、物理挙動によって移動した後に移動制限をかけなければいけないということです。つまり一度移動範囲外に出た後、範囲内に強制的に移動する事になります。 その状態を確認してみましょう。 Player.csに移動制限のコードを追加します(Clampメソッド)
Player.cs
using UnityEngine;
using System.Collections;
public class Player : MonoBehaviour
{
// Spaceshipコンポーネント
Spaceship spaceship;
IEnumerator Start ()
{
// Spaceshipコンポーネントを取得
spaceship = GetComponent<Spaceship> ();
while (true) {
// 弾をプレイヤーと同じ位置/角度で作成
spaceship.Shot (transform);
// ショット音を鳴らす
GetComponent<AudioSource>().Play();
// shotDelay秒待つ
yield return new WaitForSeconds (spaceship.shotDelay);
}
}
void Update ()
{
// 右・左
float x = Input.GetAxisRaw ("Horizontal");
// 上・下
float y = Input.GetAxisRaw ("Vertical");
// 移動する向きを求める
Vector2 direction = new Vector2 (x, y).normalized;
// 移動
spaceship.Move (direction);
// 移動の制限
Clamp();
}
void Clamp ()
{
// 画面左下のワールド座標をビューポートから取得
Vector2 min = Camera.main.ViewportToWorldPoint(new Vector2(0, 0));
// 画面右上のワールド座標をビューポートから取得
Vector2 max = Camera.main.ViewportToWorldPoint(new Vector2(1, 1));
// プレイヤーの座標を取得
Vector2 pos = transform.position;
// プレイヤーの位置が画面内に収まるように制限をかける
pos.x = Mathf.Clamp (pos.x, min.x, max.x);
pos.y = Mathf.Clamp (pos.y, min.y, max.y);
// 制限をかけた値をプレイヤーの位置とする
transform.position = pos;
}
// ぶつかった瞬間に呼び出される
void OnTriggerEnter2D (Collider2D c)
{
// レイヤー名を取得
string layerName = LayerMask.LayerToName(c.gameObject.layer);
// レイヤー名がBullet (Enemy)の時は弾を削除
if( layerName == "Bullet (Enemy)")
{
// 弾の削除
Destroy(c.gameObject);
}
// レイヤー名がBullet (Enemy)またはEnemyの場合は爆発
if( layerName == "Bullet (Enemy)" || layerName == "Enemy")
{
// 爆発する
spaceship.Explosion();
// プレイヤーを削除
Destroy (gameObject);
}
}
}
この状態で、ゲームを再生して画面端に移動してみてください。プレイヤーが削除されるはずです。
図9.2は画面端(-4,-3)に移動した時の画像です。本来であればプレイヤーのコライダーはDestroyAreaのコライダー内に含まれているのでOnTriggerExit2Dは呼ばれないはずですが、呼ばれてしまいます。これは、DeastoryAreaのコライダー外に移動してしまっている事になります。
図9.2:
そこで、プレイヤーの移動はrigidbody2D.velocityを使用せず、transform.positionのみで行うようにします。
Player.cs
using UnityEngine;
using System.Collections;
public class Player : MonoBehaviour
{
// Spaceshipコンポーネント
Spaceship spaceship;
IEnumerator Start ()
{
// Spaceshipコンポーネントを取得
spaceship = GetComponent<Spaceship> ();
while (true) {
// 弾をプレイヤーと同じ位置/角度で作成
spaceship.Shot (transform);
// ショット音を鳴らす
GetComponent<AudioSource>().Play();
// shotDelay秒待つ
yield return new WaitForSeconds (spaceship.shotDelay);
}
}
void Update ()
{
// 右・左
float x = Input.GetAxisRaw ("Horizontal");
// 上・下
float y = Input.GetAxisRaw ("Vertical");
// 移動する向きを求める
Vector2 direction = new Vector2 (x, y).normalized;
// 移動の制限
Move (direction);
}
// 機体の移動
void Move (Vector2 direction)
{
// 画面左下のワールド座標をビューポートから取得
Vector2 min = Camera.main.ViewportToWorldPoint(new Vector2(0, 0));
// 画面右上のワールド座標をビューポートから取得
Vector2 max = Camera.main.ViewportToWorldPoint(new Vector2(1, 1));
// プレイヤーの座標を取得
Vector2 pos = transform.position;
// 移動量を加える
pos += direction * spaceship.speed * Time.deltaTime;
// プレイヤーの位置が画面内に収まるように制限をかける
pos.x = Mathf.Clamp (pos.x, min.x, max.x);
pos.y = Mathf.Clamp (pos.y, min.y, max.y);
// 制限をかけた値をプレイヤーの位置とする
transform.position = pos;
}
// ぶつかった瞬間に呼び出される
void OnTriggerEnter2D (Collider2D c)
{
// レイヤー名を取得
string layerName = LayerMask.LayerToName(c.gameObject.layer);
// レイヤー名がBullet (Enemy)の時は弾を削除
if( layerName == "Bullet (Enemy)")
{
// 弾の削除
Destroy(c.gameObject);
}
// レイヤー名がBullet (Enemy)またはEnemyの場合は爆発
if( layerName == "Bullet (Enemy)" || layerName == "Enemy")
{
// 爆発する
spaceship.Explosion();
// プレイヤーを削除
Destroy (gameObject);
}
}
}
ゲームを再生してみてください。プレイヤーが画面外へ移動しなくなり、OnTriggerExit2Dが呼ばれなくなります。
今回Transformの位置を直接変更する移動方法に変更しました。このことでDestroyAreaとPlayerの間では当たり判定は発生しなくなります。これはRigidbody2Dの物理挙動によって移動を行わなくなったためです。
Player.csとEnemy.cs共通の処理を記述しているSpaceship.csにMoveメソッドがありますが、Player.csで使用しなくなったので修正を行います。
Spaceship.cs
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public class Spaceship : MonoBehaviour
{
// 移動スピード
public float speed;
// 弾を撃つ間隔
public float shotDelay;
// 弾のPrefab
public GameObject bullet;
// 弾を撃つかどうか
public bool canShot;
// 爆発のPrefab
public GameObject explosion;
// 爆発の作成
public void Explosion ()
{
Instantiate (explosion, transform.position, transform.rotation);
}
// 弾の作成
public void Shot (Transform origin)
{
Instantiate (bullet, origin.position, origin.rotation);
}
}
Enemy.cs
using UnityEngine;
using System.Collections;
public class Enemy : MonoBehaviour
{
// Spaceshipコンポーネント
Spaceship spaceship;
IEnumerator Start ()
{
// Spaceshipコンポーネントを取得
spaceship = GetComponent<Spaceship> ();
// ローカル座標のY軸のマイナス方向に移動する
Move (transform.up * -1);
// canShotがfalseの場合、ここでコルーチンを終了させる
if (spaceship.canShot == false) {
yield break;
}
while (true) {
// 子要素を全て取得する
for (int i = 0; i < transform.childCount; i++) {
Transform shotPosition = transform.GetChild (i);
// ShotPositionの位置/角度で弾を撃つ
spaceship.Shot (shotPosition);
}
// shotDelay秒待つ
yield return new WaitForSeconds (spaceship.shotDelay);
}
}
// 機体の移動
public void Move (Vector2 direction)
{
GetComponent<Rigidbody2D>().velocity = direction * spaceship.speed;
}
void OnTriggerEnter2D (Collider2D c)
{
// レイヤー名を取得
string layerName = LayerMask.LayerToName (c.gameObject.layer);
// レイヤー名がBullet (Player)以外の時は何も行わない
if (layerName != "Bullet (Player)") return;
// 弾の削除
Destroy(c.gameObject);
// 爆発
spaceship.Explosion ();
// エネミーの削除
Destroy (gameObject);
}
}
Spaceship.csを抽象クラスとして扱うのが適切な方法かもしれません。ですが、プログラマー以外の混乱を避けるため今回のチュートリアルはその方法では実装しません。
Spaceship.cs
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public abstract class Spaceship : MonoBehaviour
{
// 移動スピード
public float speed;
// 弾を撃つ間隔
public float shotDelay;
// 弾のPrefab
public GameObject bullet;
// 弾を撃つかどうか
public bool canShot;
// 爆発のPrefab
public GameObject explosion;
// 爆発の作成
public void Explosion ()
{
Instantiate (explosion, transform.position, transform.rotation);
}
// 弾の作成
public void Shot (Transform origin)
{
Instantiate (bullet, origin.position, origin.rotation);
}
protected abstract void Move (Vector2 direction);
}
今回はここで終了です。つまずいてしまった方はプロジェクトファイルをダウンロードして新たな気持ちで次の回へ進みましょう。
第01回 スプライトとスプライトアニメーションの作成
第02回 プレイヤーの移動
第03回 プレイヤーから弾を撃つ
第04回 敵を作成しよう
第05回 当たり判定とアニメーションイベントとレイヤー
第06回 背景を作る
第07回 Wave型の仕組み作り
第08回 音をつける
第09回 プレイヤーの移動制限と様々な修正
第10回 タイトルを付ける
第11回 エネミーのHP、弾の攻撃力、アニメーションの追加
第12回 Waveを5個にする、スコアの実装
第01回 複数の解像度に対応する(黒帯を追加する)
第02回 複数の解像度に対応する(引き伸ばす)
第03回 タッチパネル対応
第04回 バーチャルジョイスティック対応