LoginSignup
2
5

More than 5 years have passed since last update.

初心者向け!cocos2d-xでの脱出ゲーム制作の手引き

Last updated at Posted at 2017-03-24

先日、cocos2d-xで初の脱出ゲームアプリを作ったので、反省も踏まえて自分なりの覚書を書いておこうと思います。
ひょっとしたら…と言うか、たぶんもっといい作り方はあるのだと思いますが、あくまで一例として参考になれば幸いです。ぶっちゃけ私も初心者なので、指摘してもらえると助かります…

そのアプリもリポジトリを公開しているので、よかったら見てみてください。
https://github.com/ugonight/rifujin1

環境構築

…はここでは割愛します。
私は現時点でcocos2d-x v3.14.1で作っていますので、そのバージョンで動作すること前提で書いていきます。
C++とcocos2d-xの基本的な知識はここでは説明しないので、ある程度勉強してきてください。
ちなみに、androidビルドはAndroid Studioでやってます。おすすめです。

クラス構成

cocos2d-xではSceneやLayer等にSprite(画像)やLabel(文字)をaddchildで配置して作っていきます。
これらのクラスには基本的な機能が備わっているのですが、自前のクラスに継承して独自にカスタマイズして使っていくことにします。

今回はシンプルな画面構成で

  • Controlクラス(Sceneクラスを継承):画面にあるものを制御する大元のクラス
    • Fieldクラス(Layerクラスを継承):場面のクラス
      • ObjectNクラス(Spriteクラスを継承):場面に配置するもので挙動を管理する必要があるもののクラス
    • Itemクラス(Layerクラスを継承):アイテムを管理するクラス

としましょう。(頭の中でツリー表示に変換してください)
他に付けたい機能があれば、同じようにクラスを継承して作ってみましょう。
ObjectクラスはもともとあったのでObjectNクラスにしてあります。
それでは、実際に脱出ゲームを作っていきましょう。

①とりあえず表示させよう

cocos newコマンドでプロジェクトを作成した後、Classesフォルダ下にcontrol.hとcontrol.cppの二つのファイルを作成し、それぞれにこんな感じに記述します。

control.h
#pragma once

#include "cocos2d.h"

class Control : public cocos2d::Scene {
public:
    virtual bool init();

    CREATE_FUNC(Control);
};
control.cpp
#include "control.h"

USING_NS_CC;

bool Control::init()
{
    if (!Scene::init())
    {
        return false;
    }

    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    return true;
}

このまま実行してもデフォルトの画面を表示してしまうので、AppDelegate.cppを書き換えます。

AppDelegate.cpp
#include "AppDelegate.h"
#include "control.h" //"HelloWorldScene.h"

~

    // create a scene. it's an autorelease object
    auto scene = Control::create(); // HelloWorld::createScene();

    // run
    director->runWithScene(scene);

    return true;
}

これで実行し、FPS表示だけが残っていれば成功です。

次は試しに、実際に画面上に場面とアイテムウィンドウを表示させてみましょう。
ささっと画像を作ります。

field1.png
field1.png
itemWindow.png
itemWindow.png

これをResourcesフォルダにぶち込みます。
続いてcontrolクラスと同じように、今度はLayerクラスを継承してクラスを作りましょう。
そこに、先ほどの画像を配置するために次のように記述します。

field.h
#pragma once

#include "cocos2d.h"

class Field : public cocos2d::Layer {
public:
    virtual bool init();

    CREATE_FUNC(Field);
};
field.cpp
#include "field.h"

USING_NS_CC;

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

    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    auto bg = Sprite::create("field1.png");
    bg->setPosition(visibleSize / 2);
    addChild(bg, 0, "bg");

    return true;
}
item.h
#pragma once

#include "cocos2d.h"

class Item : public cocos2d::Layer {
public:
    virtual bool init();

    CREATE_FUNC(Item);
};
item.cpp
#include "item.h"

USING_NS_CC;

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

    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    auto itemWindow = Sprite::create("itemWindow.png");
    itemWindow->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
    itemWindow->setPosition(Vec2::ZERO);
    addChild(itemWindow, 0, "itemWindow");

    return true;
}

「createで作成→set~で設定→addChildで配置」が一般的な流れです。
setPositionで表示する位置を設定します。visibleSize / 2では中央に表示してます。
デフォルトで中央を基準にしているので、ItemではsetAnchorPointで基準を左下に設定してます。

これをcontrolクラスに配置しましょう。

control.cpp
#include "control.h"
#include "field.h"  //追記
#include "item.h"   //追記

USING_NS_CC;

bool Control::init()
{
    if (!Scene::init())
    {
        return false;
    }

    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    //追記
    auto field = Field::create();
    addChild(field, 0, "field");

    auto item = Item::create();
    addChild(item, 1, "item");

    return true;
}

こんな感じに表示されれば成功です。自分の絵が表示されるとちょっとうれしいですね。
名称未設定.png

②場面を複数作成しよう

ここが一番初心者脳で考えた自己流のところなので、ツッコミがあったら容赦なくお願いします…

違う場面の画像を用意します。
field2.png
field2.png
引いたらタライが落ちてくる展開が容易に想像できますね。

先ほどのfieldクラスを基底クラスとして、場面ごとのクラスに派生させます。
今回のような簡単な脱出ゲームなら、わざわざ個別にクラスを作るのは面倒に感じますが、イベントなどが多く発生する場合、クラスを分けた方がコードも見やすいです。

fieldクラスを修正します。

field.h
#pragma once

#include "cocos2d.h"

class Field : public cocos2d::Layer {
public:
    virtual bool init();

    virtual void initField();

    CREATE_FUNC(Field);
};
field.cpp
#include "field.h"

USING_NS_CC;

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

    initField();

    return true;
}

void Field::initField() {
    //オブジェクトを配置したり
}

できるだけ派生クラスで同じ処理を書くのは避けたいので、物を配置する関数を個別に仮想関数として定義します。

field1.h
#pragma once

#include "field.h"
#include "cocos2d.h"

class Field1 : public Field {
public:
    virtual void initField();

    CREATE_FUNC(Field1);
};
field1.cpp
#include "field1.h"

USING_NS_CC;

void Field1::initField()
{
    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    auto bg = Sprite::create("field1.png");
    bg->setPosition(visibleSize / 2);
    addChild(bg, 0, "bg");

}
field2.h
#pragma once

#include "field.h"
#include "cocos2d.h"

class Field2 : public Field {
public:
    virtual void initField();

    CREATE_FUNC(Field2);
};
field2.cpp
#include "field2.h"

USING_NS_CC;

void Field2::initField()
{
    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    auto bg = Sprite::create("field2.png");
    bg->setPosition(visibleSize / 2);
    addChild(bg, 0, "bg");
}

ヘッダーファイルは、ほとんどコピペで無駄に感じるかもしれません。
私のゲームでは一つのファイルにまとめたりマクロで定義したりとかしてますが、あまり褒められたことではない気がするのでここでは地道に作っています。
そのほうが後で機能を追加しやすいのでいいでしょう。

ここからは、いろんな考え方があるかと思いますが、私はControlクラスを読み込んだ時に全てのFieldクラスを読み込んで管理する方法を考えました。
利点としてはField間での情報のやりとりができることで、欠点は最初のロードに時間がかかって、最悪場面数が多いとフリーズしてしまう恐れがあることです。
そこは適宜、各自で工夫することをおすすめします。

さて、controlクラスにフィールドを登録する関数を追加します。
システム面とゲームの進行に合わせて追記するスクリプト面とで別々にしたいので、新しくfieldList.cppを作成します。

control.h
#pragma once

#include "cocos2d.h"

class Field;

class Control : public cocos2d::Scene {
private:
    //追記
    std::map<std::string, Field*> mFieldList;

public:
    virtual bool init();
    void initField();   //追記

    CREATE_FUNC(Control);
};
fieldList.cpp
#include "control.h"
#include "field.h"

//Fieldクラスをinclude
#include "field1.h"
#include "field2.h"

void Control::initField() {
    //場面登録
    mFieldList["field1"] = Field1::create();
    mFieldList["field2"] = Field2::create();

    for (auto field : mFieldList) {
        field.second->retain();
    }
}

Fieldクラスはmapで管理することにします。
Field専用のLayerを作成してそこにaddChildしていってもいいかもしれません。その場合は、getChildByNameでkeyを指定してインスタンスを得ることができます。
自分でcocos2d-xのNodeクラスを管理する場合は、リファレンスカウンタに気を付けなければいけません。
addChildしないとリファレンスカウンタが0になってクラスが自殺(解放)してしまいます。そのため、retainでリファレンスカウンタに+1してクラスの自殺を阻止します。

最初にfield1を表示するようにcontrol.cppに記述したらこの項目は終わりです

control.cpp
#include "control.h"
#include "field.h"  
#include "item.h"   

USING_NS_CC;

bool Control::init()
{
    if (!Scene::init())
    {
        return false;
    }

    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    //追記
    initField();

    //変更
    addChild(mFieldList["field1"], 0, "field");

    auto item = Item::create();
    addChild(item, 1, "item");

    return true;
}

前の項と同じ画面が表示されたら成功です

③場面に物を配置しよう

背景などはSpriteクラスで配置してしまいましたが、タッチイベントを受け取ったりするような追加機能を付けたObjectNクラスを作成します。
私のゲームでは、物の有無や状態などをセーブするのに有利でした。

object.h
#pragma once
#include "cocos2d.h"

class ObjectN : public cocos2d::Sprite {
    cocos2d::Rect mArea;
    cocos2d::CallFunc *mTouchEvent;

    bool touchOn(cocos2d::Touch* touch, cocos2d::Event* event);

public:
    virtual bool init();

    void setArea(cocos2d::Rect rect);
    void setTouchEvent(cocos2d::CallFunc *func);

    CREATE_FUNC(ObjectN);
};
object.cpp
#include "object.h"

USING_NS_CC;

bool ObjectN::init() {
    if (!Sprite::init())
    {
        return false;
    }

    mArea = Rect(0, 0, 0, 0);

    mTouchEvent = CallFunc::create([]() {return; });
    mTouchEvent->retain();

    //デフォルトで中央に表示
    this->setPosition(Director::getInstance()->getVisibleSize() / 2);

    return true;
}

void ObjectN::setArea(cocos2d::Rect rect) {
    mArea = rect;

    auto listener = EventListenerTouchOneByOne::create();
    listener->onTouchBegan = CC_CALLBACK_2(ObjectN::touchOn, this);
    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);
}

bool ObjectN::touchOn(cocos2d::Touch* touch, cocos2d::Event* event) {
    if (mArea.containsPoint(Vec2(touch->getLocationInView().x, touch->getLocationInView().y))) {
        runAction(mTouchEvent);
        return true;
    }

    return false;
}

void ObjectN::setTouchEvent(cocos2d::CallFunc *func) { mTouchEvent = func; mTouchEvent->retain(); }

ちょっと記事が冗長になってきたので一気にコード書きました。すみません
でも、順に見ていきましょう。

init関数では、追加で設定しなくてもいい値がある時のためにデフォルトの値を設定して、後はいつもと同じです。
自分のゲームでフラグ代わりに空のObjectNクラスを作ったりしたので、こういうことが必要でした。

大まかにやっていることは、Rect型のmAreaという変数に設定された範囲をタッチされたらmTouchEventの処理を実行するというものです。

setAreaはFieldクラスでObjectNクラスを配置する際に、呼び出してmAreaを設定する関数です。その時にイベントリスナーを登録します。イベントリスナーを登録すればタッチを受け取ることができます。

setTouchEventでは、mTouchEventを設定しています。
cocos2d-xにはActionクラスと言う、基本的なアクションを起こしてくれる便利なクラスがあります。CallFuncはActionクラスの中で、関数を実行してくれるものです。
Actionクラスもretainしないと自殺(解放)してしまうので、設定した後にやっておきます。

touchOnはイベントリスナーに登録された、タッチされたタイミングで発生する関数です。
containsPointでタッチされたポイントがmAreaに入っているか判定します。入っていればtrueを入っていなければfalseを返して、タッチが有効か無効か判定します。
タッチのイベントリスナーには、スライドした時や離した時に呼び出される関数を指定できるのですが、onTouchBeganのクラスでtrueを返されてないとそれらが実行されません。
touch->getLocationInView()で画面の左上を0座標としたときのタッチされたポイントを取得しています。setPosition等は左下が0なんですが、個人的にはこっちの方がわかりやすいのでそうしてます。左下がいい人はtouch->getLocation()にしてください。
ActionクラスはrunActionメソッドで実行します。mTouchEventを指定して実行しています。

ゲットするアイテムを指定する関数や移動先の場面を設定する関数を個別に用意してもいいですが、作っていくうちにイレギュラーになってほとんどその類の関数は使わなかったので、mTouchEventでタッチした時のイベントを全て記述することにします。

説明が長くなりましたが、これでObjectNクラスは大体完成です。早速場面に配置してみましょう。
Fieldクラスにオブジェクトを配置できるような記述を書いていきます。

field.h
#pragma once

#include "field.h"
#include "object.h"  //追記

#include "cocos2d.h"

class Field : public cocos2d::Layer {
protected:
        //追記
    std::map<std::string, ObjectN*> mObjectList;
    void addObject(ObjectN* obj, std::string objName, int ZOder, bool addChild);

public:
    virtual bool init();

    virtual void initField();

    CREATE_FUNC(Field);
};
field.cpp
#include "field.h"

USING_NS_CC;

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

    initField();

    return true;
}

void Field::initField() {
    //オブジェクトを配置したり
}

//追記
void Field::addObject(ObjectN* obj, std::string s, int z, bool addchild) {
    mObjectList[s] = obj;
    obj->retain();
    if (addchild) this->addChild(obj, z, s);
}

ObjectNを管理するmObjectListをmapで用意し、ObjectNをFieldに登録するaddObjectN関数を作ります。
やってることはさっきのControl::initFieldとほとんど同じですが、最初にaddChildして画面に表示させておくかどうかを指定できるようにしています。これにより、後からオブジェクトを表示させたりできるようにしています。

ようやく下準備が整ってスクリプト面の記述に移れる!
…と思うでしょうが、もうちょっと頑張ってタッチされたら何が起こるかの実装をしていきましょう。

Controlクラスに「メッセージを表示する」「場面を切り替える」処理を書いていきます。

control.h
#pragma once

#include "cocos2d.h"

class Field;

class Control : public cocos2d::Scene {
private:
    std::map<std::string, Field*> mFieldList;

public:
    virtual bool init();
    void initField();

        //追記
    void changeField(std::string field);
    void showMsg(std::string msg);

    static Control* getInstance();

    CREATE_FUNC(Control);
};
control.cpp
#include "control.h"
#include "field.h"  
#include "item.h"   

USING_NS_CC;

//追記
static Control* instance;

Control *Control::getInstance() {

    return instance;
}

bool Control::init()
{
    if (!Scene::init())
    {
        return false;
    }

    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    initField();

    addChild(mFieldList["field1"], 0, "field");

    auto item = Item::create();
    addChild(item, 1, "item");

        //追記
    auto msg = Label::createWithTTF("Hellow,World", "fonts/Marker Felt.ttf", 24);
    msg->setPosition(Vec2(origin.x + visibleSize.width / 2,
        origin.y + 60));
    msg->setTextColor(Color4B::BLACK);
    this->addChild(msg, 1, "msg");

    instance = this;

    return true;
}

//追記
void Control::changeField(std::string s) {
    removeChildByName("field");
    addChild(mFieldList[s], 0, "field");
}

void Control::showMsg(std::string s) {
    ((Label*)getChildByName("msg"))->setString(s);
}

フォントはサンプルで使っていたものを使ってます。
initで登録したNodeを他の関数で参照するときは、addChildしたときに指定した名前やタグで取得します。
FieldクラスからControlクラスの関数を呼び出せるように、シングルトンにしておきます。

いよいよ、場面に物を置いていく作業に入ります。

left.png
left.png
right.png
right.png

field.h
#pragma once

#include "field.h"
#include "object.h"
#include "control.h" //追記

#include "cocos2d.h"
~
field1.cpp
#include "field1.h"

USING_NS_CC;

void Field1::initField()
{
    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    auto bg = Sprite::create("field1.png");
    bg->setPosition(visibleSize / 2);
    addChild(bg, 0, "bg");

        //追記
    auto door = ObjectN::create();
    door->setArea(Rect(190, 100, 70, 110));
    door->setTouchEvent(CallFunc::create([this] {
        Control::getInstance()->showMsg("The door is locked.");
    }));
    addObject(door, "door", 1, true);

    auto right = ObjectN::create();
    right->setArea(Rect(440, 0, 40, 200));
    right->setTexture("right.png");
    right->setTouchEvent(CallFunc::create([this] {
        Control::getInstance()->changeField("field2");
        Control::getInstance()->showMsg("turn right");
    }));
    addObject(right, "right", 2, true);

}
field2.cpp
#include "field2.h"

USING_NS_CC;

void Field2::initField()
{
    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    auto bg = Sprite::create("field2.png");
    bg->setPosition(visibleSize / 2);
    addChild(bg, 0, "bg");

        //追記
    auto left = ObjectN::create();
    left->setArea(Rect(0, 0, 40, 200));
    left->setTexture("left.png");
    left->setTouchEvent(CallFunc::create([this] {
        Control::getInstance()->changeField("field1");
        Control::getInstance()->showMsg("turn left");
    }));
    addObject(left, "left", 1, true);
}

ObjectNクラスはSpriteクラスを継承しているので、setTextureで画像を表示させることができます。
Rectは左上の座標と幅・高さを指定しています。
CallFunc::createメソッドでは引数にラムダ式で関数を渡しています。各種設定と同じ場所にかけて便利ですね。

さて、これを実行してみましょう。
名称未設定.png
きちんとメッセージ表示や、場面移動ができていたら成功です。
大分、脱出ゲームとしてできてきました。やったね!

④アイテムを制御しよう

今までずっと放置だったアイテムについて考えましょう。

まずはアイテムを管理する仕組みを考えましょう。
考えながらitem.hに次のように記述していきます。

item.h
#pragma once

#include "cocos2d.h"

class Item : public cocos2d::Layer {
public:
    virtual bool init();

    CREATE_FUNC(Item);

//追記
private:
    class ItemObj {
        std::string imgName;
        bool getFlag = 0;

    public:
        ItemObj(std::string imageName) {
            imgName = imageName;
        }

        std::string getImage() { return imgName; };
        void setGetFlag(bool i) { getFlag = i; };
        bool getGetFlag() { return getFlag; };
    };

    std::map<std::string, ItemObj*> mItemList;
    void initItem();
};

アイテム一つ一つのクラスのItemObjクラスをItemクラスのpravate内で定義します。
このクラスはアイテムの画像と所持しているかのフラグを設定したり値を返しているだけですが、これも自身でカスタマイズしていく余地はあるかと思います。
アイテムもmapで管理して、最初に全部読み込んでおきましょう。
itemList.cppを作成し、次のように記述します。

iteList.cpp
#include "item.h"

void Item::initItem() {
    mItemList["key"] = new ItemObj("key.png");
}

ItemObjは完全にオリジナルなクラスなのでretainとかもしなくていいですね。
続いては、アイテムを手に入れたら表示させるようにしてみましょう。

item.h
#pragma once

#include "cocos2d.h"

class Item : public cocos2d::Layer {
public:
    virtual bool init();

        //追記
    void itemGet(std::string itemName);
    void deleteItem(std::string itemName);

    CREATE_FUNC(Item);

private:
    class ItemObj {
        std::string imgName;
        bool getFlag = 0;

    public:
        ItemObj(std::string imageName) {
            imgName = imageName;
        }

        std::string getImage() { return imgName; };
        void setGetFlag(bool i) { getFlag = i; };
        bool getGetFlag() { return getFlag; };
    };

    std::map<std::string, ItemObj*> mItemList;
    void initItem();
        //追記
    void updateItem();
};
item.cpp
#include "item.h"

USING_NS_CC;

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

    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    auto itemWindow = Sprite::create("itemWindow.png");
    itemWindow->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
    itemWindow->setPosition(Vec2::ZERO);
    addChild(itemWindow, 0, "itemWindow");

    //追記
    auto possession = Layer::create();
    addChild(possession, 1, "possession");

    initItem();
    updateItem();

    return true;
}

//追記
void Item::updateItem() {
    auto possession = getChildByName("possession");
    possession->removeAllChildren();
    for (auto item : mItemList) {
        if (item.second->getGetFlag()) {
            auto spr = Sprite::create(item.second->getImage());
            spr->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
            spr->setPosition(Vec2(32 * possession->getChildrenCount(), 0));
            possession->addChild(spr, 1, item.first);
        }
    }
}

void Item::itemGet(std::string s) {
    mItemList[s]->setGetFlag(1);
    updateItem();
}

void Item::deleteItem(std::string s) {
    mItemList[s]->setGetFlag(0);
    updateItem();
}

possessionと言うレイヤーに所持しているアイテムを追加していっています。
毎回レイヤーを空にしてcreate&addChildしているのは若干よくないのかもしれません。ItemObjもSpriteクラスを継承してみてよかったかも…?

次はアイテムを選択する処理を書いていきましょう。
タッチした場所にアイテムがあれば黄色い枠を付けるようにします。選択されているアイテムは外部から取得できるようにしましょう。
select.png
select.png

item.h
#pragma once

#include "cocos2d.h"

class Item : public cocos2d::Layer {
public:
    virtual bool init();

    void itemGet(std::string itemName);
    void deleteItem(std::string itemName);

    //追記
    std::string getSelectedItem();
    static Item* getInstance();

    CREATE_FUNC(Item);

private:
    class ItemObj {
        std::string imgName;
        bool getFlag = 0;

    public:
        ItemObj(std::string imageName) {
            imgName = imageName;
        }

        std::string getImage() { return imgName; };
        void setGetFlag(bool i) { getFlag = i; };
        bool getGetFlag() { return getFlag; };
    };

    std::map<std::string, ItemObj*> mItemList;
    std::string mSelectedItem; //追記

    void initItem();
    void updateItem();

    bool touchEvent(cocos2d::Touch* touch, cocos2d::Event* event); //追記
};
item.cpp
#include "item.h"

USING_NS_CC;

//追記
static Item* instance;

Item *Item::getInstance() {

    return instance;
}

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

    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    auto itemWindow = Sprite::create("itemWindow.png");
    itemWindow->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
    itemWindow->setPosition(Vec2::ZERO);
    addChild(itemWindow, 0, "itemWindow");

    auto possession = Layer::create();
    addChild(possession, 1, "possession");

    //追記
    auto frame = Sprite::create("select.png");
    frame->setPosition(Vec2(-32, 0));
    frame->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
    addChild(frame, 2, "frame");

    initItem();
    updateItem();

    //追記
    auto listener = EventListenerTouchOneByOne::create();
    listener->setSwallowTouches(true);
    listener->onTouchBegan = CC_CALLBACK_2(Item::touchEvent, this);
    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);

    mSelectedItem = "";
    instance = this;

    return true;
}

void Item::updateItem() {
    auto possession = getChildByName("possession");
    possession->removeAllChildren();
    for (auto item : mItemList) {
        if (item.second->getGetFlag()) {
            auto spr = Sprite::create(item.second->getImage());
            spr->setAnchorPoint(Vec2::ANCHOR_BOTTOM_LEFT);
            spr->setPosition(Vec2(32 * possession->getChildrenCount(), 0));
            possession->addChild(spr, 1, item.first);
        }
    }
}

void Item::itemGet(std::string s) {
    mItemList[s]->setGetFlag(1);
    updateItem();
}

void Item::deleteItem(std::string s) {
    mItemList[s]->setGetFlag(0);
    updateItem();
}

//追記
bool Item::touchEvent(cocos2d::Touch* touch, cocos2d::Event* event) {
    auto window = getChildByName("itemWindow");
    auto targetBox = window->getBoundingBox();
    auto touchPoint = touch->getLocation();
    if (targetBox.containsPoint(touchPoint)) {
        for (auto item : getChildByName("possession")->getChildren()) {
            if (item->getBoundingBox().containsPoint(touchPoint)) {
                mSelectedItem = item->getName();
                getChildByName("frame")->setPositionX(item->getPositionX());
                break;
            }
        }

        return true;
    }
    return false;
}

std::string Item::getSelectedItem() { return mSelectedItem; }

タッチした座標にあるアイテムを取得しています。
ついでにControlクラス同様、Itemクラスもシングルトンにしました。

ここまで淡々と説明してきましたが、これでFieldにアイテム関連の処理を書くことができます。
紐を引くとタライが落ちてくる…と思いきや、背後にカギが落ちてくるというギミックを作ってみましょう。

field2の紐を引いたらfield1にカギを出現させるために、「他のクラスのオブジェクトを取得する」「場面切り替え時に発生するイベントを定義する」ことが必要です。

ControlクラスとFieldクラスに追記していきます。

control.h
~
public:
    virtual bool init();
    void initField();

    void changeField(std::string field);
    void showMsg(std::string msg);

    Field* getField(std::string field);  //追記

    static Control* getInstance();

    CREATE_FUNC(Control);
};
control.cpp
~
void Control::changeField(std::string s) {
    removeChildByName("field");
    addChild(mFieldList[s], 0, "field");
    mFieldList[s]->changedField(); //追記
}

void Control::showMsg(std::string s) {
    ((Label*)getChildByName("msg"))->setString(s);
}

//追記
Field* Control::getField(std::string field) {
    return mFieldList[field];
}
field.h
#pragma once

#include "field.h"
#include "object.h"
#include "control.h"
#include "item.h"  //追記

#include "cocos2d.h"

class Field : public cocos2d::Layer {
protected:
    std::map<std::string, ObjectN*> mObjectList;
    void addObject(ObjectN* obj, std::string objName, int ZOder, bool addChild);

public:
    virtual bool init();

    virtual void initField();
    virtual void changedField();

    ObjectN* getObject(std::string objName);  //追記

    CREATE_FUNC(Field);
};
field.cpp
~
void Field::changedField() {
    //現在のフィールドに移動したときに呼び出される処理を書く
}

ObjectN* Field::getObject(std::string s) {
    return mObjectList[s];
}

ふぅ、これで大丈夫でしょう…
私も疲れて来たので、 ここで一気にゲームクリアまで書いてしまいましょう!

key.png
key.png

field1.h
#pragma once

#include "field.h"
#include "cocos2d.h"

class Field1 : public Field {
public:
    virtual void initField();
    virtual void changedField();

    CREATE_FUNC(Field1);
};
field1.cpp
#include "field1.h"

USING_NS_CC;

void Field1::initField()
{
    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    auto bg = Sprite::create("field1.png");
    bg->setPosition(visibleSize / 2);
    addChild(bg, 0, "bg");

    auto door = ObjectN::create();
    door->setArea(Rect(190, 100, 70, 110));
    door->setTouchEvent(CallFunc::create([this] {
        if (Item::getInstance()->getSelectedItem() == "key") {
            Control::getInstance()->showMsg("Great!You are a friend who can escape!");
            Item::getInstance()->deleteItem("key");
        }
        else {
            Control::getInstance()->showMsg("The door is locked.");
        }
    }));
    addObject(door, "door", 1, true);

    auto right = ObjectN::create();
    right->setArea(Rect(440, 0, 40, 200));
    right->setTexture("right.png");
    right->setTouchEvent(CallFunc::create([this] {
        Control::getInstance()->changeField("field2");
        Control::getInstance()->showMsg("turn right");
    }));
    addObject(right, "right", 2, true);

    auto key = ObjectN::create();
    key->setTexture("key.png");
    key->setPosition(Vec2(300, 100));
    key->setArea(Rect(280, 200, 40, 40));
    key->setTouchEvent(CallFunc::create([this] {
        Control::getInstance()->showMsg("I got the key");
        Item::getInstance()->itemGet("key");
        removeChildByName("key");
    }));
    addObject(key, "key", 2, false);
}

void Field1::changedField() {
    auto field2 = Control::getInstance()->getField("field2");
    if (!field2->getChildByName("rope")) {
        addChild(mObjectList["key"], 2, "key");
    }
}
field2.cpp
#include "field2.h"

USING_NS_CC;

void Field2::initField()
{
    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    auto bg = Sprite::create("field2.png");
    bg->setPosition(visibleSize / 2);
    addChild(bg, 0, "bg");

    auto left = ObjectN::create();
    left->setArea(Rect(0, 0, 40, 200));
    left->setTexture("left.png");
    left->setTouchEvent(CallFunc::create([this] {
        Control::getInstance()->changeField("field1");
        Control::getInstance()->showMsg("turn left");
    }));
    addObject(left, "left", 1, true);

    auto rope = ObjectN::create();
    rope->setArea(Rect(345, 0, 30, 190));
    rope->setTouchEvent(CallFunc::create([this] {
        Control::getInstance()->showMsg("There was a noise behind.");
        removeChildByName("rope");
    }));
    addObject(rope, "rope", 2, true);
}

名称未設定.png

すごーい!君は脱出のできるフレンズなんだね!

気を付けることと言えば、初期状態でないものをaddChildするときはmObjectListから指定することでしょうか。
このままだとカギが何回も取れてしまうので、ちょっと工夫が必要です。
私はObjectNに状態を設定する機能を付けて対応しました。皆さんはどのようにするか、考えてみてください。

最後に

いかがだったでしょうか。
予想以上に長くなってしまって、場違いだって怒られないか心配です(汗)
後は適宜、場合に応じてシステムを修正していきながら、Fieldクラスを継承して場面を作成し、fieldListとitemListに追記していけばスムーズにゲームを作ることができます。

最近はUnityやツクールMV等が流行っていますが、個人的にエディタに頼らない、真心のこもったゲームがやってみたいので、是非とも、cocos2d-xでプログラムまで手作りなゲームを作ってみてほしいです。

私自身、見よう見まねで作ってる初心者ですので、繰り返しますがアドバイス等いただけると嬉しいです…

この記事で作成したサンプルゲームのリポジトリも置いておきます。ご参考にどうぞ。
https://github.com/ugonight/escTest/

最初に書いたアプリ、あんまりプレーしてもらえてなくて悲しいです。 遊んでみてね

追記

(2017/03/26)
すっかり忘れてましたが、mFieldListやmTouchEvent等のretainしたメンバ変数はデストラクタできちんと解放しましょう。
デストラクタはvirtualを付けて継承先のデストラクタが呼ばれるようにします。
CC_SAFE_RELEASE_NULL等のマクロを使って解放するとよいでしょう。
詳しくはリポジトリを参照してください。

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5