Cocos2d-xでバーチャルパッド(スティック操作)を実装する

今回は、Cocos2d-x(ver. 3.17.1)でバーチャルパッド(スティックキー)を実装したいと思います。

PSやXboxなどでおなじみの360°方向の入力ができるスティックキーです。

実際に実装するとこんな感じになります。

よくコントローラが画面の下部に表示されているタイプのコントローラーがあると思いますが、私見ですがタッチパネル操作なのでスティックキーを画面下部に静的に配置する必要は無いと考えています。つまり、スティックは画面に最初に触れたところを中心にそこからスワイプした方向と操作量でスティック操作を実現したいと思います。

今回作成するパッドの仕様は概ね以下の通りです。

  • 仕様
    • タッチしたところを中心にバーチャルパッド操作ができる
      • タッチ開始位置がどこか視認できること
      • 現在タッチ位置がどこか視認できること
    • 少し倒すと少しだけ、大きく倒すと大きく操作ができる
    • 一定以上の距離にはスティックを倒せない概念を疑似的に再現する

少しコードが長いですがお付き合い頂けたらと思います。

環境

  • Cocos2d-x 3.17.1
  • VisualStudio2017 15.9.14
  • Windows10 .oO(すいません今回もモバイルで動作確認してません。多分動くの精神で…)

概要

使用する画像は以下の2種類。(真っ白でよくわかんないですが画像あります)

タッチ開始位置を表すひし形(hishi.png)

f:id:Takachan:20190926011925p:plain

指が触ってる場所を表す丸(circle.png)

f:id:Takachan:20190926011935p:plain

実装コード

ゲームパッドを表す「PadLayer」を作成してそこに処理を記述します。

PadLayer.hpp

ヘッダー側の実装は以下の通り。Layerクラスを継承して自作のレイヤーを作成します。

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

#include "CreateFunc.hpp"

#include "cocos2d.h"

/**
 * PadとかのUIを配置するレイヤー
 */
class PadLayer : public cocos2d::Layer, CreateFunc<PadLayer>
{
public:
    
    // PADが操作されたときに発生する
    //   T1 : 経過時間(秒)
    //   T2 : 角度(ラジアン)
    //   T3 : 操作量(1.0 = 100%)
    std::function<void(float delta, float rad, float amount)> onPadMoved = nullptr;

    virtual bool init();
    using CreateFunc::create;

    // 1フレームごとの処理でPAD操作を外部に通知する
    virtual void update(float delta) override;
    
    // タッチしたときの設定を行う
    void initTouchEvent();

    // Padが操作中かどうかを表すフラグを設定する(true : 操作中 / false : それ以外)
    void setInPadOperation(bool value)
    {
        this->inPadOperation = value;
    }
    
    // Pad操作量を設定する
    void setPadAmount(float rad, float perlen)
    {
        this->rad = rad;
        this->manipAmount = perlen;
    }

    // 各コントロールへのアクセス

    // 現在タッチしている位置の画像
    void set_Sprite_Point(cocos2d::Sprite* sp);
    cocos2d::Sprite* get_Sprite_Point();
    // タッチしたときに表示する中央の画像
    void set_Sprite_Hishi(cocos2d::Sprite* sp);
    cocos2d::Sprite* get_Sprite_Hishi();
    // タッチ位置と中心をつなぐ線
    void set_DrawNode_Line(cocos2d::DrawNode* dn);
    cocos2d::DrawNode* get_DrawNode_Line();

protected:

    // see the 'setInPadOperation()'
    bool inPadOperation = false;
    // see the 'onPadMoved' at T2
    float rad;
    // see the 'onPadMoved' at T3
    float manipAmount;

private:

    static const int TAG_POINT;
    static const int TAG_MASK;
    static const int TAG_HISHI;
    static const int TAG_LINE;
    static const float PAD_DISTANCE;
};

操作が発生した場合、フレームごとにonPadMovedイベントが発生して現在のパッドの傾いている方向(rad)と操作量(amount)が通知されます。レイヤー内で発生している操作をupdate()メソッドを通じてイベント呼び出しで外部に操作を通知します。

PadLayer.cpp

実装コードは以下の通り。

#include "PadLayer.hpp"

using namespace std;
using namespace cocos2d;

const int   PadLayer::TAG_POINT = 0;
const int   PadLayer::TAG_HISHI = 1;
const int   PadLayer::TAG_LINE  = 2;
const int   PadLayer::TAG_MASK  = 3;
const float PadLayer::PAD_DISTANCE = 140.0f;

bool PadLayer::init()
{
    if (!Layer::init())
    {
        return false;
    }

    // 現在タッチしている場所を表す画像
    //Sprite* psp = Images::create(c);
    Sprite* psp = Sprite::createWithSpriteFrameName("img/parts/menu2/pad_front.png");
    psp->setVisible(false);
    this->set_Sprite_Point(psp);

    // タッチ開始位置を表す画像
    //Sprite* psp = Images::create(t);
    Sprite* hsp = Sprite::createWithSpriteFrameName("img/parts/menu2/hishi.png");
    hsp->setVisible(false);
    this->set_Sprite_Hishi(hsp);

    // タッチ開始位置と現在位置をつなぐ線
    auto line = DrawNode::create();
    line->setVisible(false);
    line->setOpacity(128);
    this->set_DrawNode_Line(line);

    this->initTouchEvent();
    this->scheduleUpdate();

    return true;
}

void PadLayer::update(float delta)
{
    if (!this->inPadOperation)
    {
        return;
    }

    this->onPadMoved(delta, this->rad, this->manipAmount);
}

void PadLayer::initTouchEvent()
{
    auto li = EventListenerTouchOneByOne::create();

    li->onTouchBegan = [this](Touch* pTouch, Event* pEvent)
    {
        // 操作開始
        this->setInPadOperation(true);

        Sprite* hsp = this->get_Sprite_Hishi();
        hsp->setPosition(pTouch->getLocation());
        hsp->setVisible(true);

        Sprite* psp = this->get_Sprite_Point();
        psp->setPosition(pTouch->getLocation());
        psp->setVisible(true);

        return true;
    };

    li->onTouchMoved = [this](Touch* pTouch, Event* pEvent)
    {
        Point pos = pTouch->getLocation();
        Point s = pTouch->getStartLocation();
        Point e = pTouch->getLocation();
        Point dist = e - s;

        // 一定距離以上は丸を移動できない
        float distance = sqrt(pow(dist.x, 2) + pow(dist.y, 2));
        float rad = atan2(e.y - s.y, e.x - s.x);
        float per = distance / PAD_DISTANCE;

        if (distance > PAD_DISTANCE)
        {
            float px = PAD_DISTANCE * cos(rad);
            float py = PAD_DISTANCE * sin(rad);
            pos.x = px + s.x;
            pos.y = py + s.y;
            per = 1.0f;
        }

        // 現在位置を更新する
        this->get_Sprite_Point()->setPosition(pos);

        // 開始位置と現在位置の間に線を書く
        DrawNode* dn = this->get_DrawNode_Line();
        dn->setVisible(true);
        dn->clear();
        dn->drawLine(pTouch->getStartLocation(), pos, { 1.0f, 1.0f, 1.0f, 0.5f });

        // 操作量の変更
        this->setPadAmount(rad, per);
    };

    li->onTouchEnded = [this](Touch* pTouch, Event* pEvent)
    {
        // 操作終了
        this->setInPadOperation(false);

        // まず線を消す
        this->get_DrawNode_Line()->setVisible(false);

        this->get_Sprite_Hishi()->setVisible(false);
        this->get_Sprite_Point()->setVisible(false);
    };

    _eventDispatcher->addEventListenerWithSceneGraphPriority(li, this);
}

void PadLayer::set_Sprite_Point(cocos2d::Sprite* sp)
{
    sp->setTag(TAG_POINT);
    this->addChild(sp);
}
cocos2d::Sprite* PadLayer::get_Sprite_Point()
{
    return static_cast<Sprite*>(this->getChildByTag(TAG_POINT));
}
void PadLayer::set_Sprite_Hishi(cocos2d::Sprite* sp)
{
    sp->setTag(TAG_HISHI);
    this->addChild(sp);
}
cocos2d::Sprite* PadLayer::get_Sprite_Hishi()
{
    return static_cast<Sprite*>(this->getChildByTag(TAG_HISHI));
}
void PadLayer::set_DrawNode_Line(cocos2d::DrawNode* dn)
{
    dn->setTag(TAG_LINE);
    this->addChild(dn);
}
cocos2d::DrawNode* PadLayer::get_DrawNode_Line()
{
    return static_cast<DrawNode*>(this->getChildByTag(TAG_LINE));
}

基本的に登録したタッチイベント(initTouchEvent)内で操作量を取得してフィールドに保存 → パッド操作中であれば毎フレームごとのupdateメソッドでonPadMovedイベントを呼び出して外部に操作量を通知することの繰り返しになります。

また、ジョイスティックが最大に倒れた状態を1.0fとして、操作していない状態が0.0f、最大量に対しどれくらい倒されているのかをパーセントで外部に通知します。

// 最大操作量に対してどれくらい倒されているかの割合を出す
float per = distance / PAD_DISTANCE;

また、タッチ開始位置 ⇔ 現在の指の位置の角度を求める式は以下の通りです。(そこはかとなくVec2クラスにそんなメソッドがあった気がしますが自前で実装しています

// 2つのノード間の角度をラジアンで求める
Point s = pTouch->getStartLocation();
Point e = pTouch->getLocation();
Point dist = e - s;
float distance = sqrt(pow(dist.x, 2) + pow(dist.y, 2));
float rad = atan2(e.y - s.y, e.x - s.x);

中央から一定以上離れたらそれ以上丸を追従させない処理が以下の通りです。

if (distance > PAD_DISTANCE)
{
    // 開始位置から遠すぎる時はそれ以上丸を追従させない
    float px = PAD_DISTANCE * cos(rad);
    float py = PAD_DISTANCE * sin(rad);
    pos.x = px + s.x;
    pos.y = py + s.y;
    per = 1.0f;
}

基本的にそのまま張り付けても動画と似た動きになると思います。

使い方

上記コード例の使い方です。

#include "PadLayer.hpp"

static Scene* foo()
{
    // PADレイヤーを載せるシーンを作成
    auto s = Scene::create();
    // PADレイヤーを作成
    auto pad = PadLayer::create();
    pad.onPadMoved = [](float delta, float rad, float amount)
    {
        // PAD操作したときのイベントの登録
        CCLOG("delta=%f, rad=%f, amound=%f", delta, rad, amound);
    }
    return s;
}

そのほかのコード

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

CreateFuncクラス

各C++クラスが継承している、CreateFunc は以下を参照ください。

CRTPイデオムを使用したCreateFuncの記述の簡易化になります。

http://melpon.org/blog/cocos2dx-create-func

参考

https://qiita.com/FumioNonaka/items/c146420c3aeab27fc736

以上です。