Cocos2d-xで画像を切り出してアニメーションする。

Cocos2d-xで配布されている素材のキャラクターチップを切り出して歩行アニメーションをつけようと思います。

素材配布サイトさんに以下のような形式で歩行グラフィックが配置されているのを見たことがあるとあると思いますがこの画像、毎回全部分割して画像ファイルやplistにするのはなかなか大変なのでロード時に画像を分割してアニメーションを作成しようと思います。

ウディタさんからお借りした画像は以下の画像です。

f:id:Takachan:20170815194635p:plain

完成すると以下のような動きになります。

一枚の画像を切りだしてSpriteを作成する

Animationクラスにパラパラ漫画風のアニメーションを1コマ追加するメソッドに以下のメソッドがあります。これは対象のテクスチャー(画像)を指定して、Rectの範囲をAnimationに追加するメソッドになります。

void addSpriteFrameWithTexture(Texture2D* pobTexture, const Rect& rect);

画像のテクスチャはTexture2Dクラスを使用すれば取れるのですが、使い方が大変面倒なのでSpriteのgetTextureメソッドを使用します。その場合のコードは大体以下の通りになります。

// 対象の画像の読み取り(読み捨て用)
auto baseSprite = Sprite::create(name);

// Spriteからテクスチャーの取得(読み捨て用)
auto textureSource = baseSprite->getTexture();

// アニメーションを格納するオブジェクトの作成
auto animation = Animation::create();
animation->setDelayPerUnit(0.3f); // 0.3秒間隔で無限に繰り返す
animation->setRestoreOriginalFrame(true);

// テクスチャーから画像を切り出して追加
animation->addSpriteFrameWithTexture(textureSource, Rect(0, 0, 20, 20));
animation->addSpriteFrameWithTexture(textureSource, Rect(20, 0, 20, 20));
animation->addSpriteFrameWithTexture(textureSource, Rect(40, 0, 20, 20));

8方向キャラクターチップクラスを作成

毎回上記処理を記述すると面倒なため、上記処理をクラス化します。

方向を示す識別子

まず、画像は8方向分あるので以下の通り方向を示すenumを定義します。

#pragma once
#pragma execution_character_set("utf-8")

// キャラクターの方向を表す識別子
enum class Direction : unsigned int
{
    Front = 0,
    Left,
    Right,
    Back,
    FrontLeft,
    FrontRight,
    BackLeft,
    BackRight,
};

そのあと、画像を指定してcreateしたら全部の方向を読み込んでくれるクラスをSpriteを継承して作成します。

ヘッダー

ヘッダーは以下のように作成します。initで画像を8方向分読み込んでchangeDirectionで向きを変更します。

#pragma once
#pragma execution_character_set("utf-8")

#include "cocos2d.h"
#include "Direction.h"

class CharacterChip : public cocos2d::Sprite
{
    // 現在向いている方向
    Direction _currentDir = (Direction)-1;

    // 各方向のアニメーションを記憶しておく入れ物
    cocos2d::Map<Direction, cocos2d::Action*> animatonTable;

public:

    // リソースを解放してオブジェクトを破棄する
    ~CharacterChip();
    
    // いつもの
    virtual bool init(std::string name);
    
    // いつもの
    static CharacterChip* create(std::string name);
    
    // 向いている方向の変更
    void changeDirection(Direction dir);
};

実装

実装は以下の通り。左半分の上下左右と右半分の斜めの画像を一緒に考えて1つのループ内で処理しています。

#include "CharacterChip.h"

using namespace std;
using namespace cocos2d;

CharacterChip::~CharacterChip()
{
    this->removeFromParentAndCleanup(true);
    this->animatonTable.clear(); // 念のため
}

bool CharacterChip::init(string name)
{
    if (!Sprite::init())
    {
        return false;
    }

    // 対象の画像の読み取り(読み捨て用)
    auto baseSprite = Sprite::create(name);



    // Spriteからテクスチャーの取得(読み捨て用)
    auto textureSource = baseSprite->getTexture();

    // 今回はドット絵なのでアンチエイリアスをOFF
    textureSource->setAliasTexParameters();

    // 1コマの大きさを取得しておく
    int frameWidth = baseSprite->getContentSize().width / 6;
    int frameHeight = baseSprite->getContentSize().height / 4;

    this->setContentSize(Size(frameWidth, frameHeight));

    // 画像の半分の大きさを取っておく
    int harfWidth = baseSprite->getContentSize().width / 2;

    for (int y = 0; y < 8; y++)
    {
        // アニメーションを格納するオブジェクトの作成
        auto animation = Animation::create();
        animation->setDelayPerUnit(0.3f);
        animation->setRestoreOriginalFrame(true);

        for (int x : { 0, 1, 2, 1 })
        {
            // 左半分(4未満)と右半分(4以上)を両方考慮する
            int _x = frameWidth * x + (y >= 4 ? harfWidth : 0);
            int _y = (y >= 4 ? y - 4 : y) * frameHeight;

            CCLOG("(%d, %d)",_x, _y);

            // テクスチャーから画像を切り出し
            animation->addSpriteFrameWithTexture(textureSource, Rect(_x, _y, frameWidth, frameHeight));
        }

        this->animatonTable.insert((Direction)y, RepeatForever::create(Animate::create(animation)));
    }

    return true;
}

CharacterChip* CharacterChip::create(string name)
{
    CharacterChip *pRet = new(std::nothrow) CharacterChip();
    if (pRet && pRet->init(name))
    {
        pRet->autorelease();
        return pRet;
    }
    else
    {
        delete pRet;
        pRet = nullptr;
        return nullptr;
    }
}

void CharacterChip::changeDirection(Direction dir)
{
    if (this->_currentDir == dir)
    {
        return; // 同じ方向は無視
    }

    if (this->animatonTable.find(dir) == this->animatonTable.end())
    {
        return; // 見つからない
    }

    this->runAction(this->animatonTable.at(dir));
}

使い方

実際の使用方法は以下の通りです。

bool HelloWorld::init()
{
    // ...省略...
    
    // 8方向の規格画像を指定してcreateする
    CharacterChip* chip = CharacterChip::create("hero_1.png");
    
    // お好みで位置と大きさを指定する
    chip->setPosition(visibleSize.width / 2 + origin.x, visibleSize.height / 2 + origin.y);
    chip->setScale(2.0f);
    
    // 表示前に方向を表示
    chip->changeDirection(Direction::FrontRight);

    // Spriteを継承しているのでLayerに追加する
    this->addChild(chip);
    
    return true;
}