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();


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

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