cocos2d-x Atlas化した画像の扱い


開発ではTexturePackerを使用してAtlas化をしています。
TexturePackerの解説は多くの人が行っているので不要でしょう。
この記事では、Atlas化した後のデータの読み込み方について説明します。

##今回使用する素材
素材は[こちら](http://sozai.akuseru-design.com/category/sozai/button/)からお借りしました。
ボタン画像：**button.png**
内包している画像：**button00.png～button09.png**
![sharing.png](https://qiita-image-store.s3.amazonaws.com/0/64194/c5bd1407-6164-9cb9-725e-43a6054c2f2b.png)
プロパティリスト：**button.plist**

```xml:button.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>frames</key>
        <dict>
            <key>button00.png</key>
            <dict>
                <key>frame</key>
                <string>{{2,2},{180,36}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{180,36}}</string>
                <key>sourceSize</key>
                <string>{180,36}</string>
            </dict>
            <key>button01.png</key>
            <dict>
                <key>frame</key>
                <string>{{2,40},{180,36}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{180,36}}</string>
                <key>sourceSize</key>
                <string>{180,36}</string>
            </dict>
            <key>button02.png</key>
            <dict>
                <key>frame</key>
                <string>{{2,78},{180,36}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{180,36}}</string>
                <key>sourceSize</key>
                <string>{180,36}</string>
            </dict>
            <key>button03.png</key>
            <dict>
                <key>frame</key>
                <string>{{2,116},{180,36}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{180,36}}</string>
                <key>sourceSize</key>
                <string>{180,36}</string>
            </dict>
            <key>button04.png</key>
            <dict>
                <key>frame</key>
                <string>{{2,154},{180,36}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{180,36}}</string>
                <key>sourceSize</key>
                <string>{180,36}</string>
            </dict>
            <key>button05.png</key>
            <dict>
                <key>frame</key>
                <string>{{2,192},{180,36}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{180,36}}</string>
                <key>sourceSize</key>
                <string>{180,36}</string>
            </dict>
            <key>button06.png</key>
            <dict>
                <key>frame</key>
                <string>{{2,230},{180,36}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{180,36}}</string>
                <key>sourceSize</key>
                <string>{180,36}</string>
            </dict>
            <key>button07.png</key>
            <dict>
                <key>frame</key>
                <string>{{2,268},{180,36}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{180,36}}</string>
                <key>sourceSize</key>
                <string>{180,36}</string>
            </dict>
            <key>button08.png</key>
            <dict>
                <key>frame</key>
                <string>{{2,306},{180,36}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{180,36}}</string>
                <key>sourceSize</key>
                <string>{180,36}</string>
            </dict>
            <key>button09.png</key>
            <dict>
                <key>frame</key>
                <string>{{2,344},{180,36}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{180,36}}</string>
                <key>sourceSize</key>
                <string>{180,36}</string>
            </dict>
        </dict>
        <key>metadata</key>
        <dict>
            <key>format</key>
            <integer>2</integer>
            <key>realTextureFileName</key>
            <string>button.pkm</string>
            <key>size</key>
            <string>{256,512}</string>
            <key>smartupdate</key>
            <string>$TexturePacker:SmartUpdate:c2e6edb4396ed85cc4195eadf9c6b637:dd253020b44f76765fb5f0d91f3030b0:065d2ddc663aa81a825884afda64891a$</string>
            <key>textureFileName</key>
            <string>button.png</string>
        </dict>
    </dict>
</plist>
```

##単純な読み込み
赤のボタンを画面中央に表示する最もシンプルなプログラムです。

```cpp:SceneSpriteCache.cpp
bool SceneSpriteCache::init() {
	// レイヤー初期化
	if (!Layer::init()) return false;

	Size visibleSize = Director::getInstance()->getVisibleSize();

	// スプライトキャッシュに登録
	SpriteFrameCache::getInstance()->addSpriteFramesWithFile("button.plist");

	// スプライトを生成し、画面中央に配置
	auto sprite = Sprite::createWithSpriteFrameName("button00.png");
	sprite->setPosition(Point(visibleSize.width / 2, visibleSize.height / 2));
	this->addChild(sprite);
}
```

##高度な読み込み
最終的な開発では環境によって読み込む画像ファイルは異なります。
私はWindowsベースで開発を行っており、WindowsではPNG(.png)
iPhoneではPVRTC(.pvr)、AndroidではETC1(.pkm)を使うでしょう。
ただし、iPhoneやAndroidでも高画質の画像を扱うときはpngファイルを扱います。
これらに対応するために、plistを読み込む処理を拡張します。
<dl>
  <dt>前提条件</dt>
  <dd>plistと同一ディレクトリに同一名の[png,pvr,pkm]の何れかが存在している</dd>
  <dt>例</dt>
  <dd>Resourceフォルダに、button.plistと[button.png,button.pvr,button.pkm]の何れかが存在している</dd>
</dl>

```cpp:SceneSpriteCache.cpp
bool CreateSpriteCacheFromFile(const std::string& plist_name)
{
	// 拡張子の位置を取得し、無い場合は終了
	size_t pos = plist_name.find_last_of('.');
	if (pos == std::string::npos) return false;	

	// plistのファイル名で、png,pvr,pkmの拡張子をもつ文字列を用意
	std::string base = plist_name.substr(0, pos);
	std::array<std::string, 3> paths = {
		base + ".png",
		base + ".pvr",
		base + ".pkm",
	};
	auto fileutil = FileUtils::getInstance();
	for (const auto& path : paths) {
		// 画像ファイルの存在チェック
		if (fileutil->isFileExist(path)) {
			// 存在したファイルを元にスプライトキャッシュを生成する
			SpriteFrameCache::getInstance()->addSpriteFramesWithFile(plist_name, path);
			return true;
		}
	}
	return false;
}

bool SceneSpriteCache::init() {
	// レイヤー初期化
	if (!Layer::init()) return false;

	Size visibleSize = Director::getInstance()->getVisibleSize();

	// ★★★　自前のスプライトキャッシュ読み込みに変更　★★★
	CreateSpriteCacheFromFile("button.plist");

	// スプライトを生成し、画面中央に配置
	auto sprite = Sprite::createWithSpriteFrameName("button00.png");
	sprite->setPosition(Point(visibleSize.width / 2, visibleSize.height / 2));
	this->addChild(sprite);
}
```

##テクスチャーファイルの圧縮に対応する
pvrとetcはzip圧縮が非常に有効です。
画像ファイルがzip化されている場合のやり方を紹介します。
<dl>
  <dt>前提条件</dt>
  <dd>plistと同一ディレクトリに同一名のzipが存在している</dd>
  <dd>zip内に同一名の[png,pvr,pkm]の何れかが存在している</dd>
  <dt>例</dt>
  <dd>Resourceフォルダに、button.plistとbutton.zipが存在している</dd>
  <dd>button.zipは、[button.png,button.pvr,button.pkm]の何れかを圧縮している</dd>
</dl>

```cpp:SceneSpriteCache.cpp
bool CreateSpriteCacheFromZip(const std::string& plist_name)
{
	// 拡張子の位置を取得し、無い場合は終了　
	size_t pos = plist_name.find_last_of('.');
	if (pos == std::string::npos) return false;

	// zipファイルが存在しなければ終了　
	std::string base = plist_name.substr(0, pos);
	std::string zip_file = base + ".zip";
	auto fileutil = FileUtils::getInstance();
	if (!fileutil->isFileExist(zip_file)) return false;

	// zipファイルから抜き出すpng,pvr,pkmのファイル名を生成
	pos = base.find_last_of('/');
	if (pos != std::string::npos) {
		base = base.substr(pos + 1);
	}
	std::array<std::string, 3> image_files = {
		base + ".png",
		base + ".pvr",
		base + ".pkm",
	};

	// zip解凍ではzipファイルのフルパスが必要になる　
	std::string full_path = fileutil->fullPathForFilename(zip_file);
	ssize_t size;
	for (const auto& image_file : image_files) {
		// zipファイルの解凍を試みる　
		auto buffer = fileutil->getFileDataFromZip(full_path, image_file, &size);
		if (buffer != nullptr) {
			// 解凍が成功した場合、Imageを作成する　
			auto img = new Image();
			if (img != nullptr && img->initWithImageData(buffer, size)) {
				CC_SAFE_FREE(buffer);
				img->autorelease();
				// Imageが正常に作成できたらTextureを作成する　
				auto texture = new Texture2D();
				if (texture != nullptr && texture->initWithImage(img)) {
					texture->autorelease();
					// 作成したテクスチャーを元にスプライトキャッシュを生成する
					SpriteFrameCache::getInstance()->addSpriteFramesWithFile(plist_name, texture);
				}
				return true;
			}
			CC_SAFE_FREE(buffer);
		}
	}

	return false;
}
```

##更に高度な対応をする
zipファイルの解凍が出来るのであれば、いっその事plistもzipにまとめてしまいましょう。
plistをファイルではなく文字列として読み込む場合、キャッシュ生成メソッドが変更します。
今までは、SpriteFrameCache::addSpriteFramesWithFileを使ってキャッシュを生成していましたが
文字列からはSpriteFrameCache::addSpriteFramesWithFileContentを使ってキャッシュを生成します。
<dl>
  <dt>前提条件</dt>
  <dd>zipファイルの中にzipファイルと同名のplistと[png,pvr,pkm]の何れかが存在している</dd>
  <dt>例</dt>
  <dd>Resourceフォルダに、button.zipが存在している</dd>
  <dd>button.zip内は、button.plistと[button.png,button.pvr,button.pkm]の何れかが存在している</dd>
</dl>

```cpp:SceneSpriteCache.cpp
bool CreateSpriteCacheFromPlistZip(const std::string& zip_name)
{
	// 拡張子の位置を取得し、無い場合は終了　
	size_t pos = zip_name.find_last_of('.');
	if (pos == std::string::npos) return false;

	// zipファイルが存在しなければ終了　
	auto fileutil = FileUtils::getInstance();
	if (!fileutil->isFileExist(zip_name)) return false;

	// zipファイルから抜き出すpng,pvr,pkmのファイル名を生成　
	std::string base = zip_name.substr(0, pos);
	pos = base.find_last_of('/');
	if (pos != std::string::npos) {
		base = base.substr(pos + 1);
	}
	std::string plist_name = base + ".plist";
	std::array<std::string, 3> image_files = {
		base + ".png",
		base + ".pvr",
		base + ".pkm",
	};

	// zip解凍ではzipファイルのフルパスが必要になる　
	ssize_t size;
	std::string full_path = fileutil->fullPathForFilename(zip_name);

	// まずはplistを取り出し、存在しなければ終了　
	auto buffer = fileutil->getFileDataFromZip(full_path, plist_name, &size);
	if (buffer == nullptr) return false;
	std::string plist = std::string(buffer, buffer + size);
	CC_SAFE_FREE(buffer);

	// 次にテクスチャーを取り出す　
	for (const auto& image_file : image_files) {
		// zipファイルの解凍を試みる　
		auto buffer = fileutil->getFileDataFromZip(full_path, image_file, &size);
		if (buffer != nullptr) {
			// 解凍が成功した場合、Imageを作成する　
			auto img = new Image();
			if (img != nullptr && img->initWithImageData(buffer, size)) {
				CC_SAFE_FREE(buffer);
				img->autorelease();
				// Imageが正常に作成できたらTextureを作成する　
				auto texture = new Texture2D();
				if (texture != nullptr && texture->initWithImage(img)) {
					texture->autorelease();
					// 作成したテクスチャーを元にスプライトキャッシュを生成する(メソッドが異なるので注意)
					SpriteFrameCache::getInstance()->addSpriteFramesWithFileContent(plist, texture);
				}
				return true;
			}
			CC_SAFE_FREE(buffer);
		}
	}

	return false;
}

```

##もう少しだけ最適化する
上記のプログラムではpng,pvr,pkmのファイルを検索しているため
該当項目がヒットしない場合の処理が無駄となります。
そこでpvrでもpkmでも全て拡張子をpngとしてしまい、検索項目を１つに絞りましょう。
cocos2d-xのテクスチャー生成は拡張子は確認せず
バイナリのフォーマットを確認するので動作は問題ありません。

隠蔽のために、拡張子をdatなどに変更するべきだと思う人もいると思いますが
開発中にdatのようなファイル形式ですと、使用している画像の確認が困難になってしまいます。
開発効率を上げるためにも、ファイルはpngで扱うことをお勧めします。

##データ保護のお話
昨今ではスマホでもデータの抜き出しが容易となっており画像ファイルなどは簡単に見られます。
高度な暗号化をかけても複合に時間が掛かってしまいますし
高レベルのプログラマであれば複合プログラムを解読して複合化してしまいます。
どのレベルが最適というのは個人によりますが
私の基準としてはロードに影響を及ぼさない程度としていますので
先頭100byteをビット反転させるか、特定キーでXORをかけることとしています。

##最後に
■単純な読み込み
■plistとテクスチャーを指定した読み込み
■テクスチャーを圧縮した読み込み
■plistとテクスチャーを圧縮した読み込み
今回は上記の4点のやり方を紹介しました。
実際の開発では4番を使うと皆さんは思うでしょうが、実は2番と3番を使います。
理由は4番とCocos Studioの相性が非常に悪いことです。
この内容については、後日Cocos Studioの記事を書くときにまとめて記述する予定です。
それではまた次回！！！