Unity3Dで2Dシューティングゲームの作り方、パート5 (複数の画面サイズ、タッチパネル、バーチャルジョイスティック、iOSシェア機能、オブジェクトプーリング)

Unity3Dで2Dシューティングゲームの作り方、パート5です。

前回でゲームとしては完成しましたが、今回で細かいことや少し応用的なことをやって2Dシューティングゲームの作り方の締めくくりにしたいと思います。

複数の解像度に対応する(黒帯を追加する)

Unity Japan: モバイル編 第01回 複数の解像度に対応する(黒帯を追加する)

ゲームビューの画面サイズ変更
File → Project Settings
PlatformをAndroidに変更します。
GameビューのFree Aspectを16:10 Landscapeへ変更します。

黒帯の追加
余白の青い部分を黒帯で隠します。

黒帯のテクスチャをインポート
黒帯の画像をダウンロードします。
ダウンロードしたファイルをAssetsのSpritesへドラッグしてドロップします。
blackScreenのPixels To Unitsを4に変更します。
blackScreenをシーンビューにドラッグしてドロップします。
blackScreenオブジェクトのPosition X:5 Y:0 Z:0
blackScreenオブジェクトのScale X:2 Y:6 Z:1
blackScreenオブジェクトをDuplicateします。
片方のblackScreenオブジェクトの位置Xを-5に変更します。

黒帯のソーティングレイヤーを設定する
Playerが黒帯の下にいくようにします。
blackScreenオブジェクトのSorting LayerでAdd Sorting Layerを選択します。
Sorting LayerにBlackScreenを追加します。
2つのblackScreenオブジェクトのSorting LayerをBlackScreenに変更します。

プレイヤーの移動制限
プレイヤーの移動制限をViewPortで行っているため黒帯の中までPlayerが移動してしまいます。
プレイヤーは背景が表示されている範囲内でのみ動けるように変更します。

Playerスクリプト
Spaceship spaceship;
Background background; #これを追加する

IEnumerator Start() {
	spaceship = GetComponent<Spaceship> ();

	background = FindObjectOfType<Background>(); #これを追加する
	#以下同じMove()まで同じ

void Move (Vector2 direction){
	Vector2 scale = background.transform.localScale;

	Vector2 min = scale * -0.5f;
	Vector2 max = scale * 0.5f;
	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;
}
これでPlayerが黒帯の中へは行かなくなります。

複数の解像度に対応する(引き伸ばす)

Unity Japan: モバイル編 第02回 複数の解像度に対応する(引き伸ばす)

今回は黒帯を追加して複数の解像度に対応するのではなく、背景画像を引き伸ばして複数の解像度に対応してみます。backScreenオブジェクトを削除してPlayerスクリプトをbackgroundを使わないもとの状態に戻します。

元に戻し方忘れた人用にPlayerスクリプトのMove()だけ載せておきます。
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;
}

BackgroundとDestroyAreaの変更
BackgroundスクリプトにStart()を追加してscaleをViewportに合う大きさに変更します。
void Start(){
	Vector2 max = Camera.main.ViewportToWorldPoint (new Vector2(1,1));

	Vector2 scale = max * 2;

	transform.localScale = scale;
}
これでBackgroundが横に拡大され青い余白がなくなりました。

DestroyAreaのスケール変更
DestroyAreaスクリプトにStart()を追加してColliderのサイズをスクリーンの大きさに合わせます。
void Start(){
	Vector2 max = Camera.main.ScreenToWorldPoint (new Vector2(1,1));

	Vector2 size = max * 2;

	GetComponent<BoxCollider2D> ().size = size;
}
以上で画面いっぱいに引き伸ばされてPlayerも画面上を問題なく動くことができるようになりました。

モバイル編: タッチパネル対応

Unity Japan: モバイル編 第03回 タッチパネル対応

タイトルの変更
タイトルのPress XをTap to Startに変更します。

Touchクラスでタッチ情報を取得する
Managerスクリプトにタッチ情報を実装します。

画面をタッチするかマウスでクリックするかでゲームをスタートさせます。
ManagerスクリプトのUpdate()だけ変更です。
void Update () {
	for(int i=0; i<Input.touchCount; i++){
		Touch touch = Input.GetTouch(i);
		if(IsPlaying() == false && touch.phase == TouchPhase.Began){
			GameStart();
		}	
	}

	if(IsPlaying() == false && Input.GetMouseButtonDown (0)){
		GameStart();
	}	
}

Eventからタッチ情報を取得する
Update()の代わりにOnGUIメソッドでタッチ情報・クリック情報を取得してゲームをスタートさせます。
ManagerスクリプトのUpdate()を削除してOnGUI()を追加します。
void OnGUI(){
	if(IsPlaying() == false && Event.current.type == EventType.MouseDown){
		GameStart();
	}
}
画面をタッチしたりマウスでクリックするとゲームが開始されます。

バーチャルジョイスティック対応

Unity Japan: モバイル編 第04回 バーチャルジョイスティック対応

バーチャルジョイスティックをアセットストアからダウンロード
Sample Assetsにある“Unityで開く”をクリックします。UnityでAsset Storeが出てくるので、そこのSample Assetsというキーワードで検索します。Sample Assetsが出てくるのでそれを選択して、次に出てくるDownloadをクリックします。

しばらくするとImporting Packageという画面が出てくるので、Cross Platform Inputだけをチェックして他はチェックをはずしてImportします。他のをImportするとうまく動かなくなるらしいです。

Unityに戻ります。
AssetsにSample Assetsができます。その中にあるCross Platform InputのPrefabsのMobile Single Stick Control Rigを選択してドラッグしてシーンビューにドロップします。
Cross Platform Inputオブジェクトを展開してJump Buttonを削除します。

これでバーチャルジョイスティック設置ができました。

バーチャルジョイスティックの入力を受け付けるためにPlayerスクリプトでCrossPlatformInputクラスを使います。

PlayerスクリプトUpdateメソッドだけ変更します。
void Update () {
	float x = CrossPlatformInput.GetAxisRaw ("Horizontal");
	float y = CrossPlatformInput.GetAxisRaw ("Vertical");

	Vector2 direction = new Vector2 (x, y).normalized;

	Move (direction);
}
これでタッチができればバーチャルジョイスティックを動かせるはず・・。マウスやキーボードじゃバーチャルジョイスティックは反応しません。


左下の円がバーチャルジョイスティックです。



プラグインを作成する(シェア機能)

Unity Japan: iOS編 第01回 プラグインを作成する(シェア機能)

とりあえずバーチャルジョイスティック対応すると面倒なので、PlayerのUpdateメソッドを元に戻して、Cross Platform Inputオブジェクトを無効にしておきます。

シェア機能を実装する
Unity側の実装
使用するテクスチャをダウンロードしてAssetsのTextureへドロップします。
ManagerスクリプトでシェアするボタンとiOSの処理を呼び出すコードを追加します。

Managerスクリプトの変更点
public GameObject player;
private GameObject title;
public Texture2D shareButtonImage;

[DllImport("__Internal")]
private static extern void Shooting_Share(string text, string url, string textureUrl);

void Start () {
	title = GameObject.Find ("Title");	
}
	
void OnGUI(){
	if(GUILayout.Button(shareButtonImage, GUIStyle.none, GUILayout.Width(128), GUILayout.Height(128))){
		StartCoroutine (Share ());
	}

	if(IsPlaying() == false && Event.current.type == EventType.MouseDown){
		GameStart();
	}
}

IEnumerator Share(){
	Application.CaptureScreenshot ("screenShot.png");

	yield return new WaitForEndOfFrame();

	string text = "2Dシューティング チュートリアル #unity";
	string url = "http://japan.unity3d.com/developer/document/tutorial/2d-shooting-game/ios/01.html";

	string textureUrl = Application.persistentDataPath + "/screenShot.png";

	Shooting_Share (text, url, textureUrl);
}

TextureのshareをManagerオブジェクトのShare Button Imageにアタッチします。
これでゲーム画面の右上のShareボタンが表示されるようになります。


プラグイン側の実装
AssetsフォルダにPluginsを作成して、Pluginsフォルダの中にiOSフォルダを作成します。
iOSフォルダにShare.mmファイルを作成します。これはUnity上ではできないので、該当するフォルダにいき自分で作成します。Share.mmにコードを書きますがUnityのエディタでは書けないので各自のテキストエディタでコードを書き込みます。

Share.mm
#if UNITY_VERSION <= 434

#import "iPhone_View.h"

#endif

extern "C" {
	void Shooting_Share(const char *text, const char *url, const char *textureURL){
		NSString *_text = [NSString stringWithUTF8String:text];
		NSString *_url = [NSString stringWithUTF8String:url];
		NSString *_textureURL = [NSString stringWithUTF8String:textureURL];

		UIImage *image = nil;

		if([_textureURL length] != 0){
			image = [UIImage imageWithContentsOfFile:_textureURL];
		}

		NSArray *actItems = [NSArray arrayWithObjects:_text, _url, image, nil];

		UIActivityViewController *uiActivityViewController = [[[UIActivityViewController alloc] initWithActivityItems:actItems applicationActivities:nil] autorelease];

		[UnityGetGLViewController() presentViewController:uiActivityViewController animated:YES completion:nil];
	}
}

File → Build Settings
PlatformをiOSに変更してBuildします。

Simulatorではプラグインが動かないので実機でテストを行う必要があります。

例のごとく_clock$UNIX2003というエラーが発生するのでmain.mmに下記のコードを追加します。
#include <time.h>

extern "C"
{
    clock_t
    clock$UNIX2003(void)
    {
        return clock();
    }
}
これでShareボタンがiOSデバイスに表示されて、
そのボタンを押すとShare機能が動作するようになります。

最後のゲーム中にShareボタンを押さないようにManagerスクリプトのOnGUIメソッドをを変更します。
void OnGUI(){
	if(IsPlaying() == false){
		if(GUILayout.Button(shareButtonImage, GUIStyle.none, GUILayout.Width(128), GUILayout.Height(128))){
			StartCoroutine (Share ());
		}
			
		if(Event.current.type == EventType.MouseDown){
			GameStart();
		}
	}
}

ついでにバーチャルジョイスティックを再度有効にしてからBuildします。
いつも通り_clock$UNIX2003の対策してXcodeで実行しました。

ジョイスティックもShareボタンも機能していました。

あれ・・・、どうも敵が枠外に出た後に削除されていないようです。
そのため次のWaveが開始されません。。

iOS固有の問題じゃなくて、どこか知らないうちにいじったのかな?


原因がわかったけど、直し方がわからない。。
DestroyAreaスクリプトのStartメソッド
void Start(){
	Vector2 max = Camera.main.ScreenToWorldPoint (new Vector2(1,1));

	Vector2 size = max * 2;

	GetComponent<BoxCollider2D> ().size = size;
}
これを削除すると敵が枠外に行ったとき削除される。。

すごく悩んだ数時間後・・・。
私は超バカだった。


Vector2 max = Camera.main.ScreenToWorldPoint (new Vector2(1,1));
じゃない!!!

正解
Vector2 max = Camera.main.ViewportToWorldPoint (new Vector2(1,1));

Viewportというのは画面枠のことで、それをWorldPointに変換しています。
Screenとうのは何なんでしょう?
何でScreenToWorldPointがいたのでしょう?(逆切れ
どうもこいつはマウスがタッチされている座標を取り出すのに使うとか・・・。
何でこいつがいるんだよ・・・。


ということでScreenToWorldPointをViewportToWorldPointに変えることで普通に動くようになりました。。疲れた。

オブジェクトプーリング

Unity Japan: 最適化編 第01回 オブジェクトプーリング

とりあえずバーチャルジョイスティックは削除してキーボードのマウスで自機が動くように戻します。


InstantiateとDestroyは呼び出しコストが高い
作成と破壊はコストが高いらしいです。そこでプーリングして再利用していきます。ちなみにプーリングのほうがコストが高いこともあるらしいのでケースバイケースでの対応が必要だとか。

ObjectPoolスクリプトの作成
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ObjectPool : MonoBehaviour {

	private static ObjectPool _instance;

	public static ObjectPool instance {
		get{
			if(_instance == null){
				_instance = FindObjectOfType<ObjectPool>();

				if(_instance == null){
					_instance = new GameObject("ObjectPool").AddComponent<ObjectPool>();
				}
			}
			return _instance;
		}
	}

	private Dictionary<int, List<GameObject>> pooledGameObjects = new Dictionary<int, List<GameObject>>();

	public GameObject GetGameObject(GameObject prefab, Vector2 position, Quaternion rotation){
		int key = prefab.GetInstanceID ();

		if(pooledGameObjects.ContainsKey(key) == false){
			pooledGameObjects.Add (key, new List<GameObject>());
		}

		List<GameObject> gameObjects = pooledGameObjects[key];

		GameObject go = null;

		for(int i=0; i<gameObjects.Count; i++){
			go = gameObjects[i];

			if(go.activeInHierarchy == false){
				go.transform.position = position;
				go.transform.rotation = rotation;
				go.SetActive (true);
				return go;
			}
		}

		go = (GameObject)Instantiate (prefab, position, rotation);

		go.transform.parent = transform;

		gameObjects.Add (go);

		return go;
	}

	public void ReleaseGameObject(GameObject go){
		go.SetActive (false);
	}
}
シングルトンでデータを出し入れ。。今回は一定数の弾を作って再利用します。

Bulletスクリプトオブジェクトプールを使うように変更
using UnityEngine;
using System.Collections;

public class Bullet : MonoBehaviour {

	public int speed = 10;
	public float lifeTime = 5;
	public int power = 1;

	void OnEnable(){
		rigidbody2D.velocity = transform.up.normalized * speed;
	}

	void OnTriggerExit2D(Collider2D other){
		ObjectPool.instance.ReleaseGameObject (gameObject);
	}
}

Spaceship、Enemy、Player、DestoryAreaスクリプトを変更します。

SpaceshipスクリプトのShotメソッドを変更
public void Shot (Transform origin) {
	ObjectPool.instance.GetGameObject (bullet, origin.position, origin.rotation);
}

EnemyスクリプトのOnTriggerEnter2DメソッドのDestroyを変更
hp = hp - bullet.power;
//Destroy (c.gameObject);
ObjectPool.instance.ReleaseGameObject (c.gameObject);

PlayerスクリプトのOnTriggerEnter2DメソッドのDestroyを変更
if(layerName == "Bullet(Enemy)"){
	//Destroy (c.gameObject);
	ObjectPool.instance.ReleaseGameObject (c.gameObject);
}

DestoryAreaスクリプトのOnTriggerExit2Dメソッドを変更
void OnTriggerExit2D(Collider2D c){
	string layerName = LayerMask.LayerToName (c.gameObject.layer);
		
	if (layerName == "Bullet(Enemy)" || layerName == "Bullet(Player)") {
		ObjectPool.instance.ReleaseGameObject (c.gameObject);
	} else {
		Destroy (c.gameObject);
	}
}

以上で弾の最適化が完了です。

録画テストをしてみよう

Unity Japan: ゲーム録画編 第01回 録画テストをしてみよう

ゲーム作っても録画機能をつけようとは思わないのでゲームの録画に関してはスキップしてしまいます。
EveryplayというサイトとSDKで録画できるようになるということだけ覚えておきましょう!



以上でUnity3Dで2Dシューティングゲームの作り方は終了です。
お疲れ様でした。

コメントを残す

メールアドレスが公開されることはありません。