Cocos2d-xで画像を任意の色で点滅させる(3.x系)

Cocos2d-xで画像を点滅させるには、Blinkを使います。このBlinkの動作は、画像を「表示」⇔「非表示」動きをやってくれます。ただ、自分の好きな色で点滅などは出来ません。画像(Sprite)に弾がヒットした時に一瞬白く点滅する等の2Dゲームに割とよくある表現ができません。

そこで、今回はタイトルの通りBlinkのようにNode::runAction()できるBlinkColorクラスを作成したいと思います。今回もシェーダーを使用したいと思います。

実際に作成したものは以下のような動きになります。

f:id:Takachan:20190929232651g:plain

実装環境

今回の実装&確認環境は以下の通りです。

  • Cocos2d-x 3.17.1
  • VisualStudio2017 15.9.14
  • Windows10

使い方

先に使い方を紹介します。基本的にBlinkと同じです。点滅の間隔もBlinkと同じです。違いは第三引数に点滅させたい色を指定する部分だけです。Sequenceに入れて連続で動かしても普通に動作します。

#include "BlinkColor.hpp"

static void foo()
{
    // 点滅させたい画像
    auto sp = Sprite::create("carrot_t_2.png");
    sp->setPosition(__SIZE_HALF(this));
    sp->setPositionY(sp->getPositionY() + 100.0f);
    this->addChild(sp);
    
    // 0.3秒間に2回、白色に点滅させる
    sp->runAction(BlinkColor::create(0.3f, 2, { 0xFF, 0xFF, 0xFF }));
}

たったこれだけです。簡単ですね。

実装

実装は相当長いです。(シェーダーも使用するので長くなってしまいました…

シェーダーの準備

今回もシェーダーを使用します。準備自体は以前記事にしたCocos2d-xでルール画像を使ってシーントランシジョンする(シェーダー使用)とほぼ一緒です。

頂点シェーダー

C++のコードとして直接記述するので以下のように特殊文字をエスケープしてファイルに保存します。所謂何もしない頂点シェーダーです。

// blink.vert

const GLchar* BlinkVert =
STRINGIFY(

attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec4 a_color;

\n#ifdef GL_ES\n
varying lowp vec4 v_fragmentColor;
varying mediump vec2 v_texCoord;
\n#else\n
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
\n#endif\n

void main(void)
{
    gl_Position = CC_PMatrix * a_position;
    v_fragmentColor = a_color;
    v_texCoord = a_texCoord;
}
);
フラグメントシェーダー

フラグメントシェーダーはアプリ側から渡されるフラグが有効な時だけ、指定された色で画像の透過していない部分を塗りつぶす処理を記述します。

// blink.frag

const GLchar* BlinkFrag =
STRINGIFY(

\n#ifdef GL_ES\n
precision mediump float;
\n#endif\n

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform float u_percent;

uniform int u_is_enabled;
uniform vec3 u_color3f;

void main(void)
{
    vec4 c = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
    if(u_is_enabled == 1)
    {
        c.r = u_color3f.r * c.a;
        c.g = u_color3f.g * c.a;
        c.b = u_color3f.b * c.a;
    }
    
    gl_FragColor = c;
}
);

「c.r = u_color3f.r * c.a;」こうやって書かないと不透過の部分まで塗りつぶされて四角い塗りつぶしになってしまいます。そのため不透明度を各色に乗算しています。

シェーダーをC++コードから利用する

上記シェーダー文字列をC++側から使用するため以下のコードを記述します。

// Shaders.hp

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

#include "cocos2d.h"

NS_CC_BEGIN

// 点滅するアクションの時に使用するシェーダーの定義
extern const GLchar* BlinkVert;
extern const GLchar* BlinkFrag;

NS_CC_END

で、実体の展開は#includeでやります。

// Shaders.cpp

#include "Shaders.hpp"

#define STRINGIFY(A) #A

NS_CC_BEGIN

#include "blink.frag"
#include "blink.vert"

NS_CC_END

BlinkColorクラス

上記シェーダーを利用してrunActionから利用できるように「cocos2d::ActionInterval」を継承した「BlinkColor」クラスを作成します。

BlinkColor.hpp

ヘッダー側の実装です。Cocos2d-xに元々存在するBlinkクラスをほぼそのまま持ってきています。ActionIntervalを継承したら実装しないといけないお約束の宣言が多いです。

// BlinkColor.hpp

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

#include "cocos2d.h"

/**
 * 任意の色で画像を点滅させるクラス
 */
class BlinkColor : public cocos2d::ActionInterval
{
public:

    /**
     * Creates the action.
     * @param duration Duration time, in seconds.
     * @param blinks Blink times.
     * @param color Blink color.
     * @return An autoreleased Blink object.
     */
    static BlinkColor* create(float duration, int blinks, const cocos2d::Color3B& color);

    bool initWithDurationAndColor(float duration, int blinks, const cocos2d::Color3B& color);

    //
    // Overrides
    //
    virtual BlinkColor* clone() const override;
    virtual BlinkColor* reverse() const override;
    /**
     * @param time In seconds.
     */
    virtual void startWithTarget(cocos2d::Node *target) override;
    virtual void update(float time) override;
    virtual void stop() override;

protected:

    void setEnabled(cocos2d::Node* target, bool value);
    cocos2d::Vec3 toVec3(cocos2d::Color3B color);

protected:

    int times;
    bool is_enabled = false;
    cocos2d::Color3B color;
};
BlinkColor.cpp

実装側のコードです。こちらもBlinkからほとんどそのまま持ってきています。ただし自作したシェーダーを起動時にNodeに設定して、表示・非表示の代わりにシェーダーに有効無効のフラグを渡しています。

#include "ChangeColorBy.hpp"
#include "BlinkColor.hpp"
#include "Shaders.hpp"
#include "ShaderUtil.hpp"

using namespace std;
using namespace cocos2d;

BlinkColor* BlinkColor::create(float duration, int blinks, const cocos2d::Color3B& color)
{
    BlinkColor *blink = new (std::nothrow) BlinkColor();
    if (blink && blink->initWithDurationAndColor(duration, blinks, color))
    {
        blink->autorelease();
        return blink;
    }

    delete blink;
    return nullptr;
}

bool BlinkColor::initWithDurationAndColor(float duration, int blinks, const cocos2d::Color3B& color)
{
    CCASSERT(blinks >= 0, "blinks should be >= 0");
    if (blinks < 0)
    {
        log("BlinkColor::initWithDuration error:blinks should be >= 0");
        return false;
    }

    if (ActionInterval::initWithDuration(duration) && blinks >= 0)
    {
        this->times = blinks;
        this->color = color;
        return true;
    }

    return false;
}

BlinkColor* BlinkColor::clone() const
{
    return BlinkColor::create(_duration, this->times, this->color);
}

BlinkColor* BlinkColor::reverse() const
{
    return BlinkColor::create(_duration, this->times, this->color);
}

void BlinkColor::startWithTarget(cocos2d::Node* target)
{
    // 点滅用のシェーダーの設定
    GLProgramState* state = ShaderUtil::setShaderWithTarget(target, cocos2d::BlinkVert, cocos2d::BlinkFrag);
    state->setUniformVec3("u_color3f", toVec3(this->color));
    ActionInterval::startWithTarget(target);
}

void BlinkColor::update(float time)
{
    if (_target && !isDone())
    {
        float slice = 1.0f / this->times;
        float m = fmodf(time, slice);
        bool status = m > slice / 2 ? true : false;
        this->setEnabled(_target, status);
    }
}

void BlinkColor::stop()
{
    if (nullptr != _target)
    {
        this->setEnabled(_target, false);
    }
    ActionInterval::stop();
}

void BlinkColor::setEnabled(cocos2d::Node* target, bool value)
{
    GLProgramState* state = target->getGLProgramState();
    state->setUniformInt("u_is_enabled", value == true ? 1 : 0);
    this->is_enabled = true;
}

cocos2d::Vec3 BlinkColor::toVec3(cocos2d::Color3B color)
{
    static constexpr float p = 1.0f / 255.0f;
    return Vec3(color.r * p, color.g * p, color.b * p);
}

updateメソッドの中身はBlinkと完全に同じです。こうすると既存のBlinkと同じ動きになります。

そのほかのコード

上記コード中で使用している未説明のコードは以下の通りです。

ShaderUtil.hpp

シェーダーの設定は定型的なコード記述になるのでUtilityにまとめています。

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

#include "cocos2d.h"

/**
 * シェーダーに関係する汎用操作を提供する
 */
class ShaderUtil
{
    // 指定したノードにシェーダーを設定する
    static cocos2d::GLProgramState* setShaderWithTarget(cocos2d::Node* target,
        const GLchar* vert, const GLchar* flag)
    {
        if (target == nullptr)
        {
            CCLOG("target is null.");
            CC_ASSERT(target != nullptr);
        }

        auto s = cocos2d::GLProgram::createWithByteArrays(vert, flag);
        auto state = cocos2d::GLProgramState::getOrCreateWithGLProgram(s);
        target->setGLProgramState(state);

        return state;
    }
};

参考

takachan.hatenablog.com