TimeScaleを手軽に変更できるEditor拡張

似たようなことやっている人は多いと思いますが、一応。

Editorフォルダーの下にTimescaleChange.csとして配置すればすぐ使えます。

using UnityEngine;
using UnityEditor;

public class TimescaleChanger : EditorWindow
{
    [MenuItem("Tools/TimescaleChanger")]
    public static void OpenWindow()
    {
        TimescaleChanger window = EditorWindow.GetWindow<TimescaleChanger>();
        window.minSize = new Vector2(300, 40);
    }

    private void OnGUI()
    {
        GUILayout.Label("TimeScaleの値を変更します。現在値:" + Time.timeScale);

        EditorGUILayout.BeginHorizontal();

        if (GUILayout.Button("x10", GUILayout.Width(50)))
        {
            Time.timeScale = 10.0f;
        }
        if (GUILayout.Button("x2", GUILayout.Width(50)))
        {
            Time.timeScale = 2.0f;
        }
        if (GUILayout.Button("x1", GUILayout.Width(50)))
        {
            Time.timeScale = 1.0f;
        }
        if (GUILayout.Button("x0.5", GUILayout.Width(50)))
        {
            Time.timeScale = 0.5f;
        }
        if (GUILayout.Button("x0.1", GUILayout.Width(50)))
        {
            Time.timeScale = 0.1f;
        }

        EditorGUILayout.EndHorizontal();
    }
}

こんな感じです。

座標と回転などについて備忘録

自キャラの位置をmyPos、ターゲットの位置をtargetPosとしたとき

・自キャラとターゲットの距離

float d = Vector3.Distance(targetPos, myPos);

・自キャラとターゲットの中心

Vector3 centerPos = Vector3.Lerp(myPos, targetPos, 0.5f);

・自キャラからターゲットの方に向かう単位ベクトル

Vector3 toVector = (targetPos - myPos).normalized;

・自キャラからターゲットの方に向かうベクトルをy軸で90度回転

Vector3 toVector = (targetPos - myPos).normalized;
Vector3 rightVector = Quaternion.Euler(0, 90, 0) * toVector;

・自キャラのまわりをカメラが回転

float r = 2; // 半径
float speed = 3; // 速度
float h = 1; // 高さ
float time = 0;
void LateUpdate()
{
  time = time + Time.deltaTime * speed;
  Camera.main.transform.position = myPos + new Vector3(Mathf.Cos(time * Mathf.Deg2Rad) * r, h, Mathf.Sin(time * Mathf.Deg2Rad) * r);
  Camera.main.transform.LookAt(myPos);
}

・自キャラの後ろにカメラを追従させ、ターゲットの方を見る

float d = 2; // 後ろの距離
float h = 1; // 高さ

void LateUpdate()
{
  Vector3 toTarget = (targetPos - myPos).normalized;
  Camera.main.transform.position = myPos - (toTarget * d) + Vector3.up * h;
  Camera.main.transform.LookAt(targetPos);
}

UniRxで、uGUIのボタンを長押しするとエネルギーを溜めて、ボタンを離すと発射という処理にチャレンジ

UniRxを始めてみました。

uGUIのボタンを押し始めたらエネルギーチャージが始まり、押してる時間によって三段階のパワーがあり、
ボタンを離したら発射するみたいな処理をUniRxを使ってやってみようとチャレンジしてみました。


今回使用したバージョン

  • Unity 5.4.1
  • UniRx 5.4.1.1
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System;
using UniRx;
using UniRx.Triggers;
using Unity.Linq;

...
	Button chargeButton;

...
	void Start () {
		chargeButton.UpdateAsObservable()
			.SkipUntil(chargeButton.OnPointerDownAsObservable())
			.TakeUntil(chargeButton.OnPointerUpAsObservable().Do(_ => {
						// ボタンを離したときの処理

					}))
			.Select(_ => 1)
			.Scan((sum, addCount) => {
				return sum + addCount;
			})
			.Repeat()
			.Subscribe(totalCount => {
				if(totalCount == 1)	// 0だと発生しない
				{
					// 1フレーム目での処理

				}
				if(totalCount == 60)
				{
					// 60フレーム目での処理

				}
				if(totalCount == 120)
				{
					// 120フレーム目での処理
					
				}
			});
	}

こんな形であっているのだろうか・・・

iTunesConnectの審査でInformation Neededなどでリジェクトされた時

めったにないと思いますが、またあった時に忘れそうなので自分用メモとして。

Information Neededの場合は単純に情報が足りないということなので、Resolution Centerで必要な情報とスクリーンショットなどを返信すれば大丈夫。
この時、提出したバージョンは「却下済み」になっていても、再提出する必要はありません。

私の場合、次の条件の時にInformation Neededを喰らいました。

  • アプリ内課金をあるバージョンから実装した
  • アプリを始めた時にはアプリ内課金ボタンが表示されず、ストーリーをある程度進めるとそのボタンが表示される

で、Appleからは「App内課金の申請があるが、アプリ内で機能が見当たらない」のようなことを言われました。

この場合、「ストーリーボタンを押してストーリーをXXXまで進めればこの場所にボタンが表示される」という説明をしたら審査が通りました。
日本語で返信しても大丈夫というサイトをたまに見かけますが、私は試したことがないのでわかりません。できるなら英語がいいでしょう。



ちなみに余談ですが、あるバージョンからアプリ内課金を追加した時に、iTunesConnect上のアプリバージョンにアイテムを設定せずに申請してしまい、さらに審査が通ってしまったことがあり、リリースされたアプリでアイテム購入ボタンを押してもなにも反応しないという現象が発生してしまいました。

アプリ購入ボタンがあるにも関わらず審査が通ってしまったのが、

  • 審査時はApp内課金はsandbox環境で行われている
  • 今回のアプリはストーリーを進めないとアプリ内課金ボタンが表示されない

のどちらの理由なのかはわかりませんが、まぁ課金アイテムを追加した時はバージョンへのひも付けを忘れないように、ということでした。

Cocos2d-x Androidでマルチタップを無効にする

Cocos2d-xでシングルタップの判定を行うときはEventListenerTouchOneByOneを使いますが、何故かAndroidではこれを使ってもマルチタップができてしまうので、これを無効にする方法です。

今回使用したバージョン

  • Cocos2d-x 3.9

ググってみると、2つの方法が見つかりました。

Cocos2d-xのコードに手を入れて、プロジェクト全体をマルチタップ無効にする方法
http://discuss.cocos2d-x.org/t/disabling-multitouch-in-android/9649/3

Cocos2dxGLSurfaceView.javaクラスの218行目付近でMotionEvent.ACTION_POINTER_DOWNの部分をコメントアウトすればいいようです。

        switch (pMotionEvent.getAction() & MotionEvent.ACTION_MASK) {
            /*
            case MotionEvent.ACTION_POINTER_DOWN:
                final int indexPointerDown = pMotionEvent.getAction() >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                final int idPointerDown = pMotionEvent.getPointerId(indexPointerDown);
                final float xPointerDown = pMotionEvent.getX(indexPointerDown);
                final float yPointerDown = pMotionEvent.getY(indexPointerDown);

                this.queueEvent(new Runnable() {
                    @Override
                    public void run() {
                        Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, yPointerDown);
                    }
                });
                break;
            */

            case MotionEvent.ACTION_DOWN:
                // there are only one finger on the screen
                final int idDown = pMotionEvent.getPointerId(0);
                final float xDown = xs[0];
                final float yDown = ys[0];

                this.queueEvent(new Runnable() {
                    @Override
                    public void run() {
                        Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleActionDown(idDown, xDown, yDown);
                    }
                });
                break;


イベントリスナー毎に変数を用意して制御する方法
もうひとつの方法は、タッチIDを記録しておく方法のようです。
http://stackoverflow.com/questions/25322900/how-to-disable-multitouch-in-android-in-cocos2d-x-3-2

cocos2d-xの画像のキャッシュについてちょっと見てみる

cocos2d-xでSpriteを使って画像を画面上に表示するのは非常に簡単ですが、
扱う画像について意識せずに使いまくってしまうと、メモリ大量に消費してしまい、メモリ不足でクラッシュしたりすることがあります。
この場合、原因の特定は非常に困難で、修正に何日も費やしてしまう可能性もあります。

画像の形や大きさなどについても意識しておくことで、メモリを効率的に使う事ができ、
結果的にアプリが安定して動作し、レスポンスも良くなることもあります。

今回使用したバージョン

  • Cocos2d-x 3.9
  • TexturePacker 4.2.3

たとえばこんな感じの画面を作りたいとします。

上部にヘッダー、下部にフッターがあり、中央の黒い部分はコンテンツエリアとします。

透過部分が多い大きな画像を使うとどうなるか
ヘッダーとフッターを画像で表現するために、下のような画像を一枚用意したとします。
サイズは640x960で、中央部分は透明、画像サイズは約21KBです。

コードはこんな感じでしょう。

	Size winSize = Director::getInstance()->getWinSize();
	
	// 大きな画像一枚を画面の中央に配置
	Sprite *bigImageSprite = Sprite::create("res/HeaderAndFooter.png");
	bigImageSprite->setPosition(winSize * 0.5f);
	this->addChild(bigImageSprite);

たしかに作りたい画面の形にはなると思いますが、ゲーム起動中にこの画像はどのぐらいのサイズでキャッシュされているのでしょうか。
addChildした後に次のコードを入れてみます。

	std::string s = Director::getInstance()->getTextureCache()->getCachedTextureInfo();
	CCLOG("Cached texture info : %s",s.c_str());

この記述により、TextureCache上に保存されているサイズを見ることができます。

Cached texture info : "/Users/xxx.app/res/HeaderAndFooter.png" rc=2 id=1 640 x 960 @ 32 bpp => 2400 KB

なんと約2.4MB!
たかだか1枚の画像で2.4MBもの容量をメモリ上に確保していることになります。

なんでこんなに大きくなるかというと、単純に透明の部分も画像の一部として扱われるからです。
(当たり前といえば当たり前ですね)

必要な部分のみの画像に分ける
今回の例の場合、中央のコンテンツ表示部分は透明の画像である必要は全く無いので、ヘッダーとフッター部分をそれぞれ別の画像に分けることでサイズを小さくできそうです。



	// ヘッダーのみの画像を画面上部に配置
	Sprite *headerSprite = Sprite::create("res/Header.png");
	headerSprite->setPosition(Vec2(winSize.width * 0.5f,winSize.height - headerSprite->getContentSize().height * 0.5f));
	this->addChild(headerSprite);
	// フッターのみの画像を画面上部に配置
	Sprite *footerSprite = Sprite::create("res/Footer.png");
	footerSprite->setPosition(Vec2(winSize.width * 0.5f,footerSprite->getContentSize().height * 0.5f));
	this->addChild(footerSprite);

これで同じようにTextureCache上のサイズを見てみると

Cached texture info : "/Users/xxx/res/Footer.png" rc=2 id=3 640 x 76 @ 32 bpp => 190 KB
"/Users/xxx/res/Header.png" rc=2 id=1 640 x 72 @ 32 bpp => 180 KB

Header.pngとFooter.pngの2つに分かれましたが、それでも合計370KBまで減りました。

テクスチャアトラスにしてみる
次はTexturePackerを使ってテクスチャアトラスにしてみます。ついでに画像の暗号化も行います。

  • Framework:cocos2d-x
  • Texture format:PVR+zlib(.pvr.ccz)
  • Pixel format:RGBA4444
  • Dithering:FloydSteinberg

  • Content protectionボタンを押して、暗号キーを作成しておきます。

Pixel formatをRGBA4444にして16bitカラーにしています。
画像の劣化が気になるようであれば、RGBA8888のままにしたほうが良いでしょう。

cocos2d-xで読み込みます。

	Size winSize = Director::getInstance()->getWinSize();
	
	// テクスチャアトラス暗号化の設定
	ZipUtils::setPvrEncryptionKeyPart(0, 0x8a7bbc96);
	ZipUtils::setPvrEncryptionKeyPart(1, 0x5fd39bef);
	ZipUtils::setPvrEncryptionKeyPart(2, 0x00c5d294);
	ZipUtils::setPvrEncryptionKeyPart(3, 0x35dcae06);
	// テクスチャアトラスの読み込み
	if(!SpriteFrameCache::getInstance()->isSpriteFramesWithFileLoaded("res/UI.plist"))
	{
		SpriteFrameCache::getInstance()->addSpriteFramesWithFile("res/UI.plist");
	}
	
	// ヘッダーのみの画像を画面上部に配置
	Sprite *headerSprite = Sprite::createWithSpriteFrameName("Header.png");
	headerSprite->setPosition(Vec2(winSize.width * 0.5f,winSize.height - headerSprite->getContentSize().height * 0.5f));
	this->addChild(headerSprite);
	// フッターのみの画像を画面上部に配置
	Sprite *footerSprite = Sprite::createWithSpriteFrameName("Footer.png");
	footerSprite->setPosition(Vec2(winSize.width * 0.5f,footerSprite->getContentSize().height * 0.5f));
	this->addChild(footerSprite);

また同じようにTextureCache上のサイズを見てみると

Cached texture info : "/Users/xxx/res/UI.pvr.ccz" rc=5 id=1 638 x 150 @ 16 bpp => 186 KB

今回はHeader.pngとFooter.pngが両方ともテクスチャアトラスに含まれますが、16bitカラーにしたので2つで186KBになりました。
これはTexturePackerのPixel formatをRGBA4444にしたからなので、RGBA8888だと当然倍になります。

不要なキャッシュを削除する
適切な場所で、使われていないキャッシュを削除するようにするといいと思います。

	// 不要なキャッシュをクリア
	Director::getInstance()->getTextureCache()->removeUnusedTextures();
	SpriteFrameCache::getInstance()->removeUnusedSpriteFrames();


まとめ
ゲームの仕様や状況によって変わってきますが、画像を扱うときは下記のようなことを意識しておくと良いと思います。

  • 不必要に大きな画像を使わない
    • 大きな画像を読み込んで、ゲーム中では縮小してしか使わないのであれば、最初から縮小した画像を読み込むようにする
  • 透過部分はなるべく減らす
    • 画像の中心点を調整するために敢えて余白を置く場合などもありますが、不要であれば画像を分けたりトリミングする
  • テクスチャアトラスをなるべく使う
  • キャッシュのクリアを適切に行う

cocos2d-xでSQLiteを使う

珍しい内容でもないですが、自分なりのやり方を備忘録も兼ねてまとめておきます。

wxSplite3を使って暗号化も行いますが、その手順についてはこちらを参考にさせていただきました。
cocos2d-xでwxSqlite3を使ってみた | SystemTelescope

今回使用したバージョン

  • Cocos2d-x 3.9
  • wxSplite3 3.1.1

動作するサンプルはこちら。 https://github.com/okahiro/Cocos2dxSQLite
(Cocos2d-xのフルプロジェクトなので容量大きいです。)

データベース関連の処理をまとめるクラスを作成
データベースに関連する処理を全て行うDataManagerクラスを作成します。

DataManager.hpp

#include "cocos2d.h"
#include "sqlite3.h"

#define DB_FILE_NAME_ENC "EncryptedDB.db"	// データベースを暗号化する場合のファイル名
#define ENCRYPT_DB_PASSWORD "23da3i3kJLale"	// データベースを暗号化する場合のパスワード

class DataManager
{
private:
	static DataManager* mManager;
	
	// データベースファイルを開いた時に記憶しておく時間(処理時間計測用)
	std::chrono::system_clock::time_point _dbOpenTime;
public:
	DataManager();
	
	static DataManager* getInstance();
};

DataManager.cpp

#include "DataManager.hpp"

DataManager* DataManager::mManager = NULL;

#pragma mark - 初期化

DataManager::DataManager()
{
	
}

DataManager* DataManager::getInstance()
{
	if(mManager == NULL)
	{
		mManager = new DataManager();
	}
	
	return mManager;
}

また、データベースのテーブル毎に、1件分の情報を格納するエンティティクラスを作成しておくと、
情報の管理がしやすくなります。
たとえばunit_dataというテーブルがあり、idとnameとweightカラムを持つとします。

class UnitData : public Node
{
public:
	int _id;
	std::string _name;
	float _weight;
	
	CREATE_FUNC(UnitData);
};


データベースのオープンとクローズ
データベースにアクセスする前にはデータベースをオープンし、アクセスが終わったらクローズする必要があります。
毎回行うので、これらの処理は専用のメソッドとして用意します。
また、更新系処理ではトランザクションを使ったほうがよいので、これも別にします。

DataManager.cpp

// DBをオープン
sqlite3* DataManager::openDB()
{
	// DBファイルを開いた時間を記憶しておく(処理時間計測のため)
	_dbOpenTime = std::chrono::system_clock::now();
	
	// SQLiteから読込
	std::string dbPath = FileUtils::getInstance()->getWritablePath() + DB_FILE_NAME_ENC;
	
	sqlite3 *db = nullptr;
	// DBファイルオープン
	auto status = sqlite3_open(dbPath.c_str(), &db);
	if(status != SQLITE_OK)
	{
		CCLOG("▼sqlite3_open failed.");
		return nullptr;
	}
	// 暗号化する
	status = sqlite3_key(db, ENCRYPT_DB_PASSWORD, (int)strlen(ENCRYPT_DB_PASSWORD));
	if(status != SQLITE_OK)
	{
		CCLOG("▼sqlite3_key failed.");
		return nullptr;
	}
	
	CCLOG("○DB opened successfully. File : %s",dbPath.c_str());
	
	return db;
}
// DBをオープンしてトランザクションをスタートする
sqlite3* DataManager::openDBAndStartTransaction()
{
	sqlite3 *db = this->openDB();
	
	if(db)
	{
		// トランザクション開始
		auto status = sqlite3_exec(db, "BEGIN;", nullptr, nullptr, nullptr);
		if(status != SQLITE_OK)
		{
			CCLOG("▼Starting transaction failed.");
			return nullptr;
		}
	}
	
	return db;
}
// DBをクローズ
bool DataManager::closeDB(sqlite3 *db)
{
	auto status = sqlite3_close(db);
	if(status != SQLITE_OK)
	{
		CCLOG("▼Closing DB failed.");
		return false;
	}
	
	auto duration = std::chrono::system_clock::now() - _dbOpenTime;
	CCLOG("○DB Closed. time : %dms.",(int)std::chrono::duration_cast<std::chrono::milliseconds>(duration).count());
	
	return true;
}
// トランザクションをコミットしてDBをクローズする
bool DataManager::commitTransactionAndCloseDB(sqlite3 *db)
{
	// トランザクションコミット
	auto status = sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);
	if(status != SQLITE_OK)
	{
		CCLOG("▼Commiting transaction failed.");
		return false;
	}
	
	return this->closeDB(db);
}


テーブル作成
テーブルの作成はSQLで行います。
create table文は外部SQLファイルにしておくと管理が楽です。

createTable.sql

create table if not exists unit_data(
	id integer primary key,
	name text,
	weight real
);

DataManager.cpp

// テーブルを作成する
void DataManager::createTable()
{
	// エラーメッセージ格納用
	char* errorMessage = NULL;
	
	sqlite3 *db = this->openDB();	// トランザクションなし接続にする
	int status;
	
	// SQLファイル読み込み
	std::string createDBSQL = FileUtils::getInstance()->getStringFromFile("res/createTable.sql");
	// テーブル作成
	status = sqlite3_exec(db,createDBSQL.c_str(),nullptr,nullptr,&errorMessage);
	if(status != SQLITE_OK)
	{
		cocos2d::log("▼Creating table failed. Message : %s",errorMessage);
		CCASSERT(false, errorMessage);
	}
	
	// DBファイルクローズ
	this->closeDB(db);
}

create table文でif not existsを使うことで、テーブルがなければ作成、あればなにもしないということができます。


データの追加/更新
テーブルに主キーが設定されていれば、insert or replace文が使えます。
SQL文を作成するときはsqlite3_mprintfを使うと、文字列のシングルクォーテーションエスケープなどを行ってくれます。
その場合、%Qを使うといいと思います。

DataManager.cpp

// データを登録もしくは更新する
void DataManager::insertOrUpdateUnitData(int id, std::string name, float weight)
{
	sqlite3 *db = this->openDBAndStartTransaction();
	int status = 0;
	// エラーメッセージ格納用
	char* errorMessage = NULL;
	
	// InsertSQL
	auto insertSQL = sqlite3_mprintf("insert or replace into unit_data(id,name,weight) values(%d,%Q,%f)",
									 id,name.c_str(),weight
									 );
	status = sqlite3_exec(db, insertSQL, nullptr, nullptr, &errorMessage);
	if(status != SQLITE_OK)
	{
		CCLOG("▼Inserting UnitData data failed. Message : %s",errorMessage);
		CCASSERT(false, errorMessage);
	}
	else
	{
		CCLOG("○UnitData data successfully updated. no : %d",id);
	}
	
	// 解放(忘れてはいけない)
	sqlite3_free(insertSQL);
	
	this->commitTransactionAndCloseDB(db);
}


データを検索
検索した結果は一件ずつ先ほど作成したエンティティに格納し、Vectorに追加して返すようにします。

DataManager.cpp

// データを検索する
Vector<UnitData*> DataManager::selectUnitDataList()
{
	Vector<UnitData*> unitDataList;
	
	sqlite3 *db = this->openDB();
	
	// Select
	sqlite3_stmt *stmt = nullptr;
	auto selectSQL = "select id,name,weight from unit_data order by id desc";
	if(sqlite3_prepare_v2(db,selectSQL,-1,&stmt,nullptr) == SQLITE_OK)
	{
		while(sqlite3_step(stmt) == SQLITE_ROW)
		{
			UnitData *unitData = UnitData::create();
			unitData->_id = (int)sqlite3_column_int(stmt, 0);
			unitData->_name = StringUtils::format("%s",sqlite3_column_text(stmt, 1));
			unitData->_weight = (float)sqlite3_column_double(stmt, 2);
			
			unitDataList.pushBack(unitData);
		}
	}
	else
	{
		CCASSERT(false,"Select UnitDataList error.");
	}
	
	// Statementをクローズ
	sqlite3_reset(stmt);
	sqlite3_finalize(stmt);
	
	this->closeDB(db);
	
	return unitDataList;
}