19
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

エキサイトAdvent Calendar 2018

Day 21

ゲーム製作超入門

Last updated at Posted at 2018-12-20

#はじめに
https://qiita.com/advent-calendar/2018/excite
21日目の記事です。
エキサイトで主に iOSアプリ開発(たまに Android )を担当しています Ast と申します。
初投稿で色々不備や物足りないところあるかもしれませんが、お手柔らかにお願いします。
今回はエキサイトでやってることは一切関係なくて自分が個人でやってることを書いていきたいと思います。
(すみません、完全に趣味枠です)

#内容について
今日の内容は「ゲーム製作超入門」ということで、すでにゲーム製作に携わっている方向けというよりは、ゲーム作りたいけど踏ん切りがつかないイマイチ分からないという方を対象にしております。
「超」ってついてますが、すごいっていう意味じゃないです。
また、ゲーム製作入門というタイトルですが、ゲーム部分は特に作りません。ごめんなさい。
使用するゲームエンジンは cocos2d-x です。C++ で書いてますが、自分は C++er じゃないので間違いあるかもです。
あと、ゲーム製作ということでプログラミング以外の話もありますので、それでも良いよという方は読んで頂ければなと思います。
途中で分からなくなったら、こちらの完成品を参考にしてもらえれば!
https://github.com/HirokiIshimori/Cocos2dSample/tree/master

#開発環境
今回の開発環境ですが、下記の通りになります。

  • Xcode10.1
  • cocos2d-x-3.17
  • GarageBand (BGM の作成に使用)

セットアップ

Xcode は Mac の App Store でダウンロード&インストールできると思います。
cocos2d-x のインストールは下記になります。

  • http://www.cocos2d-x.org/download で最新版の cocos2d-x をダウンロードする
  • DL したら好きなディレクトリに配置し、そのディレクトリに移動
cd cocos2d-x-3.17
  • setup.py を実行する (Android 環境の Path 設定はスルーで大丈夫です。あとで設定することもできます)
    (cocos2d は Python3 系でなく Python2 系だったと思うので、失敗した人は確認してみてください)
./setup.py
  • source ~/.bash_profile を行う
source ~/.bash_profile
  • ゲーム開発を行う好きなディレクトリに移動し、cocos new コマンドを実行(今回は C++ で行う)
cocos new CocosSample -p com.campany.mygame -l cpp -d ./

これでプロジェクトが自動で作成されます。
あとは出来上がったディレクトリから proj.ios_mac/ に Xcode のプロジェクトファイルがあるのでそれを開けば OK です。

起動する

とりあえず Xcode で実行してみましょう。
適当なシミュレータを選択して cmd + r する。

スクリーンショット 2018-12-03 13.34.21.png

とりあえず Hello World が実行されました。
今回、自分としては何となく縦向きが良いので縦に変更したいと思います。
(別にどうでも良い人はスルーしてもらって大丈夫です)

縦向きにする

###プロジェクトファイルの設定

  • プロジェクトファイルを選択
  • Portrait のみ選択する
スクリーンショット 2018-12-03 14.53.24.png

###RootViewController を編集

RootViewController.mm
// For ios6, use supportedInterfaceOrientations & shouldAutorotate instead
#ifdef __IPHONE_6_0
- (NSUInteger) supportedInterfaceOrientations{
    return UIInterfaceOrientationMaskPortrait;
}
#endif

UIInterfaceOrientationMaskAllButUpsideDown -> UIInterfaceOrientationMaskPortrait に変更

RootViewController.mm
- (BOOL) shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
    return UIInterfaceOrientationIsPortrait(toInterfaceOrientation);
}

を追加

これでとりあえず縦向きにはなりますが、デバッグ情報のラベルが潰れてしまっています。
なので、これらも修正していきます。

CCDirector.cpp を編集

createStatsLabel() 関数に手を加えていく

  • 下の方に _FPSLabel を初期化を実装しているところがあるので、_FPSLabel->setScale(scaleFactor); の下に下記を追記する
CCDirector.cpp
_FPSLabel->setScaleX(scaleFactor * 2);
  • 同じような形で _drawnBatchesLabel_drawnVerticesLabel も追記していく
CCDirector.cpp
_drawnBatchesLabel->setScaleX(scaleFactor * 2);
CCDirector.cpp
_drawnVerticesLabel->setScaleX(scaleFactor * 2);
  • createStatsLabel() 関数の最後の 4 行を下記のように変更する
CCDirector.cpp
const int height_spacing = 22 / CC_CONTENT_SCALE_FACTOR();
_drawnVerticesLabel->setPosition(Vec2(0, height_spacing*2) + CC_DIRECTOR_STATS_POSITION/4);
_drawnBatchesLabel->setPosition(Vec2(0, height_spacing*1) + CC_DIRECTOR_STATS_POSITION/4);
_FPSLabel->setPosition(Vec2(0, height_spacing*0)+CC_DIRECTOR_STATS_POSITION/4);
スクリーンショット 2018-12-03 15.25.42.png 縦向きになり、デバッグ表記も見えるようになった。

ゲームのベース部分を作る

無事、起動できたということで次はゲームのベース部分を作っていきたいと思います。
今回はスマホゲームということで指でゲームを操作したいので、タップやスワイプ処理が欲しいです。
そこで、下記のようなレイヤー構成で実装することにします。

multilayer.jpg

  • GameLayer は GameObject を載せている
  • TouchLayer はタップ処理を受け付ける
  • GameLayer の上に TouchLayer がある

この構成を実現するために MultiLayerScene という GameScene を作成します。
GameScene とはゲームを構成するシーン(画面)となるレイヤーです。
今は HellowWorldScene がそれになります。
また、この構成はこちらの書籍を参考にしました。

コピペではじめるiPhoneゲームプログラミング

GameLayer

GameLayer はゲーム部分を担当するレイヤーです。すでにある HelloWorldScene をベースに手を加えても良いですし、新たに作っても良いと思います。
以下、実装です。

GameLayer.hpp

#ifndef GameLayer_hpp
#define GameLayer_hpp

class GameLayer : public cocos2d::Layer
{
public:
    virtual bool init() override;
    CREATE_FUNC(GameLayer);
private:
    GameLayer(){};
    GameLayer(const GameLayer &rhs);
    GameLayer& operator=(const GameLayer &rhs);

    virtual ~GameLayer();

    void update(float delta) override;
};

#endif /* GameLayer_hpp */

GameLayer.cpp
#include "GameLayer.hpp"

using namespace cocos2d;
using namespace std;

/**
 ゲームの初期化処理.

 @return 初期化成功フラグ.
 */
bool GameLayer::init()
{
    if (!Layer::init()) {
        return false;
    }

    auto label = Label::createWithTTF("GameLayer", "fonts/Marker Felt.ttf", 24);
    auto winSize = Director::getInstance()->getWinSize();
    label->setPosition(Vec2(winSize.width * 0.5, winSize.height * 0.5));
    this->addChild(label, 1);

    // 更新処理を行うようにする.
    this->scheduleUpdate();

    return true;
}

/**
 ゲームの更新処理.

 @param delta 前のフレームからの経過時間.
 */
void GameLayer::update(float delta) {

}

GameLayer::~GameLayer() {

}
  • init 関数はレイヤーの初期化関数です。scheduleUpdate() で更新処理を行うようにします。
  • update 関数は更新処理です。引数の delta は前フレームからの経過時間を表します。今回は特に使用しません。気になる方は変動フレームなどで検索してもらえると良いと思います。

TouchLayer

TouchLayer はタップ処理を受けつけるレイヤーです。こちらは新しく実装していきます。

TouchLayer.hpp
#ifndef TouchLayer_hpp
#define TouchLayer_hpp

#include "cocos2d.h"

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

    void touchON();
    void touchOff();
    virtual ~TouchLayer(){}

private:
    TouchLayer(){};
    TouchLayer(const TouchLayer &rhs);
    TouchLayer& operator=(const TouchLayer &rhs);

    bool onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *event);
    void onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *event);
    void onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *event);
    void onTouchCancelled(cocos2d::Touch *touch, cocos2d::Event *event);

    cocos2d::EventListenerTouchOneByOne* mTouchEvent;
};

#endif /* TouchLayer_hpp */
TouchLayer.cpp
#include "TouchLayer.hpp"

using namespace cocos2d;

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

    this->touchON();
    return true;
}

/**
 Touch 登録処理.
 */
void TouchLayer::touchON()
{
    auto dispatcher = Director::getInstance()->getEventDispatcher();
    mTouchEvent = EventListenerTouchOneByOne::create();

    mTouchEvent->onTouchBegan = CC_CALLBACK_2(TouchLayer::onTouchBegan, this);
    mTouchEvent->onTouchMoved = CC_CALLBACK_2(TouchLayer::onTouchMoved, this);
    mTouchEvent->onTouchEnded = CC_CALLBACK_2(TouchLayer::onTouchEnded, this);
    mTouchEvent->onTouchCancelled = CC_CALLBACK_2(TouchLayer::onTouchCancelled, this);

    dispatcher->addEventListenerWithSceneGraphPriority(mTouchEvent, this);
}

/**
 Touch 登録削除.
 */
void TouchLayer::touchOff() {
    Director::getInstance()->getEventDispatcher()->removeEventListener(mTouchEvent);
}

/**
 Touch 開始.

 @param touch タップ情報
 @param event イベント
 @return true で onTouchMoved, onTouchEnded へ処理を流す.
 */
bool TouchLayer::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *event){
    CCLOG("touch Began");
    auto id = touch->getID();               // タッチ ID を取得できる.
    auto touchPos = touch->getLocation();   // タッチ座標を取得できる.
    return true;
}

/**
 Touch 移動.

 @param touch タップ情報
 @param event イベント
 */
void TouchLayer::onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *event){
    CCLOG("touch Moved");
    Point touchPos = touch->getLocation();
}

/**
 Touch 終了.

 @param touch タップ情報
 @param event イベント
 */
void TouchLayer::onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *event){
    CCLOG("touch Ended");
}

/**
 Touch キャンセル.

 @param touch タップ情報
 @param event イベント
 */
void TouchLayer::onTouchCancelled(cocos2d::Touch *touch, cocos2d::Event *event){
    CCLOG("touch Canceled");
}

TouchLayer ではタップイベントを実装しています。
タッチ座標は後ほど利用していきます。

MultiLayerScene

MultiLayerSceneGameLayerTouchLayer をまとめるシーンになります。
実装自体はシンプルです。

MultiLayerScene.hpp
#ifndef MultiLayerScene_hpp
#define MultiLayerScene_hpp

#include "cocos2d.h"

enum MultiLayerTag
{
    GameLayerTag,
    TouchLayerTag
};

class GameLayer;
class TouchLayer;

class MultiLayerScene : public cocos2d::Layer
{
public:
    virtual bool init();
    CREATE_FUNC(MultiLayerScene);
    static cocos2d::Scene* scene();
    static MultiLayerScene* sharedLayer();
    GameLayer* gameLayer();
    TouchLayer* touchLayer();

private:
    MultiLayerScene(){};
    MultiLayerScene(const MultiLayerScene &rhs);
    MultiLayerScene& operator=(const MultiLayerScene &rhs);
    static MultiLayerScene *sharedInstance;
};

#endif /* MultiLayerScene_hpp */
MultiLayerScene.cpp
#include "MultiLayerScene.hpp"
#include "GameLayer.hpp"
#include "TouchLayer.hpp"

using namespace cocos2d;

MultiLayerScene* MultiLayerScene::sharedInstance;

/**
 シーン生成.

 @return MultiLayerScene.
 */
Scene* MultiLayerScene::scene()
{
    Scene *scene = Scene::create();
    MultiLayerScene *layer = MultiLayerScene::create();
    scene->addChild(layer);
    return scene;
}

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

    sharedInstance = this;

    GameLayer* gameLayer = GameLayer::create();
    this->addChild(gameLayer, GameLayerTag, GameLayerTag);      // GameLayer の ZIndex と Tag を設定する.

    TouchLayer* touchLayer = TouchLayer::create();
    this->addChild(touchLayer, TouchLayerTag, TouchLayerTag);   // TouchLayer の ZIndex と Tag を設定する.

    return true;
}

MultiLayerScene* MultiLayerScene::sharedLayer()
{
    return sharedInstance;
}

/**
 GameLayer を取得.

 @return GameLayer.
 */
GameLayer* MultiLayerScene::gameLayer()
{
    cocos2d::Node* layer = this->getChildByTag(GameLayerTag);
    return (GameLayer *)layer;
}

/**
 TouchLayer を取得.

 @return TouchLayer.
 */
TouchLayer* MultiLayerScene::touchLayer()
{
    cocos2d::Node* layer = this->getChildByTag(TouchLayerTag);
    return (TouchLayer *)layer;
}

###起動するシーンを HelloWorldScene から MultiLayerScene に入れ替え
ここまで出来れば、あとは MultiLayerScene を起動するようにすれば良いだけです。
AppDelegate.cpp を書き換えます。
#include "HelloWorldScene.h" を下記のように変更します。

AppDelegate.cpp
#include "MultiLayerScene.hpp"

applicationDidFinishLaunching メソッドの最後の方の auto scene = HelloWorld::createScene(); を以下のようにします。

AppDelegate.cpp
auto scene = MultiLayerScene::scene();

これでゲーム部分の GameLayer と タッチイベントの TouchLayer が合わさりました。
cmd + r で実行してみましょう。
スクリーンショット 2018-12-03 18.25.57.png

GameLayer が見えています。さらに画面をタップするとコンソールにタップイベントのログが流れるのを確認できると思います。
一応、ここまででゲームの下地部分は出来ました。

#キャラクターを追加する

絵はどうやって用意する?

プログラマがゲームを作る上でまず課題になるのは絵をどうやって用意するかです。
絵が描ける人は問題ないかもしれませんが、大半の人はそうではないのではないでしょうか?
自分も簡単な UI パーツなら作ることはできますが、キャラクターとかは作れない or 騙し騙し作る感じです。
UnityUnrealEngine なら AssetStoreMarketPlace を利用するのが最も手軽かもしれません。ただし、有料のものなら自分のゲームに組み込めるかよく検討した方が良いでしょう。
他にもフリー素材を利用するのも良いと思います。
いずれも絶対に利用規約は確認した上で利用してください。
今回は、自分の方でよく分からない絵(なんとなく戦闘機をイメージ)を用意しました。
青い絵がプレイヤー。赤い絵がエネミーを想定しています。
myShip.png
enemy.png

Mover クラスを実装

キャラクターを作っていくのですが、先にベースクラスを作っていきたいと思います。
ZIndex は描画順を指定するための enum です。

ZIndex.hpp
#ifndef ZIndex_h
#define ZIndex_h

enum ZIndex {
    ZPlayer = 0,
    ZEnemy,
    ZWeapon,
    ZBullet
};

#endif /* ZIndex_h */

Mover クラスです。

Mover.hpp
#ifndef Mover_hpp
#define Mover_hpp

#include "cocos2d.h"
#include "ZIndex.hpp"

class Mover : public cocos2d::Ref
{
public:
    bool init(const cocos2d::Vec2 &moveVec);
    virtual ~Mover(){}
    virtual bool update(const float &delta) = 0;
protected:
    explicit Mover() {}
    cocos2d::Vec2 mMoveVec;
private:
    Mover(const Mover& rhs);
    Mover& operator=(const Mover& rhs);
};

#endif /* Mover_hpp */
Mover.cpp
#include "Mover.hpp"

using namespace cocos2d;
using namespace std;

bool Mover::init(const Vec2 &moveVec) {
    mMoveVec = moveVec;
    return true;
}

MovermMoveVec という移動量だけをとりあえず持たせています。
update 関数を純粋仮想関数にすることにより抽象クラスにしています。この関数でキャラクターなどの動きを表現することになります。
なお、経過時間である delta を受け取るようにしていますが、今回は使用しません。
この辺りの構成は下記の書籍を参考にしました。

iOSで作るシューティングゲーム

Character クラス

次に Character クラスを作ります。名前の通りキャラクターを作るときのベースクラスです。

Character.hpp
#ifndef Character_hpp
#define Character_hpp

#include "Mover.hpp"

class Character : public Mover {
public:
    bool init(const cocos2d::Vec2 &pos, const cocos2d::Vec2 &moveVec, const ZIndex &zIndex, cocos2d::Layer* layer, const std::string& fileName);
    virtual ~Character();
protected:
    explicit Character(){}
    cocos2d::Sprite* mSprite;
private:
    Character(const Character& rhs);
    Character& operator=(const Character& rhs);
};

#endif /* Character_hpp */
Character.cpp
#include "Character.hpp"

using namespace cocos2d;
using namespace std;

bool Character::init(const Vec2 &pos, const Vec2 &moveVec, const ZIndex &zIndex, Layer* layer, const string& fileName) {
    mSprite = Sprite::create(fileName);
    if (mSprite == nullptr) {
        return false;
    }
    mSprite->setPosition(pos);
    mSprite->retain();
    layer->addChild(mSprite, zIndex);
    return this->Mover::init(moveVec);
}

/**
 デストラクタ
 mSprite を解放
 @return
 */
Character::~Character() {
    mSprite->removeFromParentAndCleanup(true);
    CC_SAFE_RELEASE_NULL(mSprite);
}

Character クラスは Sprite をメンバに持ち init 関数で生成してレイヤーに追加します。Sprite は 2D の絵になります。
Character クラスは Mover クラスに準拠していますが、ここでもまだ update 関数はまだ実装しないので、これも抽象クラスです。
デストラクタで CC_SAFE_RELEASE_NULL(mSprite); を行っていますが、これは cocos2d の管理下で安全に解放し、nullptr を代入します。今回は mSprite を解放します。
これでベースクラスを用意できました。

###Player クラス
いよいよ、Player クラスを作っていきます。

Player.hpp
#ifndef Player_hpp
#define Player_hpp

#include "Character.hpp"

class Player : public Character {
public:
    static Player* create(const cocos2d::Vec2 &pos, cocos2d::Layer* layer);
    bool init(const cocos2d::Vec2 &pos, cocos2d::Layer* layer);
    virtual bool update(const float &delta) override;
    virtual ~Player();
private:
    explicit Player(){}
    Player(const Player& rhs);
    Player& operator=(const Player& rhs);
};

#endif /* Player_hpp */
Player.cpp
#include "Player.hpp"

using namespace cocos2d;
using namespace std;

Player* Player::create(const Vec2 &pos, Layer* layer) {
    Player *pPlayer = new Player();

    if (pPlayer && pPlayer->init(pos, layer)) {
        pPlayer->autorelease();
        return pPlayer;
    } else {
        delete pPlayer;
        pPlayer = nullptr;
        return nullptr;
    }
    return nullptr;
}

bool Player::init(const Vec2 &pos, Layer* layer) {
    return this->Character::init(pos, Vec2::ZERO, ZPlayer, layer, "myShip.png");
}

bool Player::update(const float &delta) {
    return true;
}

Player::~Player() {

}

update 関数を実装していますが、今はまだ何もしません。

GameLayer に Player を追加

GameLayerPlayer を追加してプレイヤーを登場させましょう。
メンバ変数に mPlayer を追加してください。

GameLayer.hpp
#ifndef GameLayer_hpp
#define GameLayer_hpp

class Player;

class GameLayer : public cocos2d::Layer
{
public:
    virtual bool init() override;
    CREATE_FUNC(GameLayer);
private:
    GameLayer(){};
    GameLayer(const GameLayer &rhs);
    GameLayer& operator=(const GameLayer &rhs);

    virtual ~GameLayer();

    void update(float delta) override;

    Player* mPlayer;
};

#endif /* GameLayer_hpp */
GameLayer.cpp
...
#include "Player.hpp"
...
bool GameLayer::init()
{
    if (!Layer::init()) {
        return false;
    }

    auto label = Label::createWithTTF("GameLayer", "fonts/Marker Felt.ttf", 24);
    auto winSize = Director::getInstance()->getWinSize();
    label->setPosition(Vec2(winSize.width * 0.5, winSize.height * 0.5));
    this->addChild(label, 1);

    mPlayer = Player::create(Vec2(winSize.width * 0.5, winSize.height * 0.25), this);
    mPlayer->retain();

    // 更新処理を行うようにする.
    this->scheduleUpdate();

    return true;
}

GameLayer::~GameLayer() {
    CC_SAFE_RELEASE_NULL(mPlayer);
}

以上でプレイヤーの追加処理はおしまいです。それでは、cmd + r で実行してみましょう。
スクリーンショット 2018-12-05 16.14.57.png
画面の下の方にプレイヤーが表示されました。

#プレイヤーの移動
プレイヤーの移動は画面をタップして、動かした量だけそのまま移動させることにします。

TouchLayer から移動量を取得

private なメンバ変数と getter を用意します。

TouchLayer.hpp
class TouchLayer: public cocos2d::Layer
{
public:
    virtual bool init();
    CREATE_FUNC(TouchLayer);

    void touchON();
    void touchOff();
    virtual ~TouchLayer(){}

    // 追加.
    cocos2d::Vec2 getStartPoint() const;
    cocos2d::Vec2 getMovePoint() const;

private:
    TouchLayer(){};
    TouchLayer(const TouchLayer &rhs);
    TouchLayer& operator=(const TouchLayer &rhs);

    bool onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *event);
    void onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *event);
    void onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *event);
    void onTouchCancelled(cocos2d::Touch *touch, cocos2d::Event *event);
    void reset();    // 追加.

    // 追加.
    cocos2d::EventListenerTouchOneByOne* mTouchEvent;
    cocos2d::Vec2 mStartPoint, mMovePoint;
    int mTouchId;
};
TouchLayer.cpp
bool TouchLayer::init()
{
    if (!Layer::init()) {
        return false;
    }

    reset();
    touchON();
    return true;
}

bool TouchLayer::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *event){
    CCLOG("touch Began");
    if (mTouchId >= 0) {
        return false;
    }
    mStartPoint = touch->getLocation();   // タッチ座標を取得できる.
    mTouchId = touch->getID();
    return true;
}

void TouchLayer::onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *event){
    CCLOG("touch Moved");
    if (mTouchId == touch->getID()) {
        mMovePoint = touch->getLocation();
    }
}

void TouchLayer::onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *event){
    CCLOG("touch Ended");
    if (mTouchId == touch->getID()) {
        reset();
    }
}

void TouchLayer::onTouchCancelled(cocos2d::Touch *touch, cocos2d::Event *event){
    CCLOG("touch Canceled");
    if (mTouchId == touch->getID()) {
        reset();
    }
}

Vec2 TouchLayer::getStartPoint() const {
    return mStartPoint;
}

Vec2 TouchLayer::getMovePoint() const {
    return mMovePoint;
}

void TouchLayer::reset() {
    mStartPoint = Vec2(-1, -1);
    mMovePoint = Vec2(-1, -1);
    mTouchId = -1;
}

touch->getLocation() でタップ座標を取得します。

#Player の移動処理を実装

移動開始点をメンバ変数に追加します。

Player.hpp
class Player : public Character {
public:
    static Player* create(const cocos2d::Vec2 &pos, cocos2d::Layer* layer);
    bool init(const cocos2d::Vec2 &pos, cocos2d::Layer* layer);
    virtual bool update(const float &delta) override;
    virtual ~Player();
private:
    explicit Player(): mAnimFrame(0), mStartMovePoint(cocos2d::Vec2(-1, -1)){}
    Player(const Player& rhs);
    Player& operator=(const Player& rhs);

    // 追加.
    cocos2d::Vec2 mStartMovePoint;
};

update 処理を実装します。

Player.cpp
...
#include "MultiLayerScene.hpp"
#include "TouchLayer.hpp
...

bool Player::update(const float &delta) {
    auto touchLayer = MultiLayerScene::sharedLayer()->touchLayer();
    auto startPoint = touchLayer->getStartPoint();
    auto winSize = Director::getInstance()->getWinSize();
    if (startPoint.x < 0 && startPoint.y < 0) {
        mStartMovePoint = Vec2(-1, -1);
        return true;
    }

    if (mStartMovePoint.x < 0 && mStartMovePoint.y < 0) {
        mStartMovePoint = mSprite->getPosition();
        return true;
    }

    auto movePoint = touchLayer->getMovePoint();

    if (movePoint.x < 0 && movePoint.y < 0) {
        return true;
    }

    auto move = movePoint - startPoint;
    auto newPos = mStartMovePoint + move;

    mSprite->setPosition(newPos);

    return true;
}
GameLayer.cpp
void GameLayer::update(float delta) {
    mPlayer->update(delta);
}
スクリーンショット 2018-12-07 12.36.06.png 移動することができました。

弾の発射処理

唐突ですがプレイヤーに弾を発射させましょう。
先ほど、プログラマは絵を用意するのは中々大変という話をしたと思いますが、エフェクトなら少し救いがあります。
パーティクルという粒子を利用したものです。
説明するよりも作った方が早いので、下記のサイトで作成します。
particle2dx.com

particle2dx

particle2dx に関してはこちらの記事を参照してもらえればと思います。

Cocos2d-xでのパーティクルの扱い方

自分が作ったのは下記になります。

スクリーンショット 2018-12-07 17.18.18.png
スクリーンショット 2018-12-07 17.21.26.png

今回、ダウンロードするときは下記のように plistpng ファイルを別で DL します。
スクリーンショット 2018-12-07 17.22.00.png

plist はこんな感じです。

bullet1.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>angle</key>
	<real>90</real>
	<key>angleVariance</key>
	<real>0</real>
	<key>blendFuncDestination</key>
	<integer>1</integer>
	<key>blendFuncSource</key>
	<integer>770</integer>
	<key>duration</key>
	<real>-1</real>
	<key>emitterType</key>
	<real>0</real>
	<key>finishColorAlpha</key>
	<real>1</real>
	<key>finishColorBlue</key>
	<real>0</real>
	<key>finishColorGreen</key>
	<real>0</real>
	<key>finishColorRed</key>
	<real>0</real>
	<key>finishColorVarianceAlpha</key>
	<real>0</real>
	<key>finishColorVarianceBlue</key>
	<real>0</real>
	<key>finishColorVarianceGreen</key>
	<real>0</real>
	<key>finishColorVarianceRed</key>
	<real>0</real>
	<key>finishParticleSize</key>
	<real>0</real>
	<key>finishParticleSizeVariance</key>
	<real>0</real>
	<key>gravityx</key>
	<real>0</real>
	<key>gravityy</key>
	<real>0</real>
	<key>maxParticles</key>
	<real>44</real>
	<key>maxRadius</key>
	<real>0</real>
	<key>maxRadiusVariance</key>
	<real>0</real>
	<key>minRadius</key>
	<real>0</real>
	<key>particleLifespan</key>
	<real>1.1</real>
	<key>particleLifespanVariance</key>
	<real>0</real>
	<key>radialAccelVariance</key>
	<real>0</real>
	<key>radialAcceleration</key>
	<real>0</real>
	<key>rotatePerSecond</key>
	<real>0</real>
	<key>rotatePerSecondVariance</key>
	<real>0</real>
	<key>rotationEnd</key>
	<real>0</real>
	<key>rotationEndVariance</key>
	<real>0</real>
	<key>rotationStart</key>
	<real>720</real>
	<key>rotationStartVariance</key>
	<real>0</real>
	<key>sourcePositionVariancex</key>
	<real>0</real>
	<key>sourcePositionVariancey</key>
	<real>0</real>
	<key>sourcePositionx</key>
	<real>156.2085308056872</real>
	<key>sourcePositiony</key>
	<real>254.0284360189573</real>
	<key>speed</key>
	<real>0</real>
	<key>speedVariance</key>
	<real>0</real>
	<key>startColorAlpha</key>
	<real>1</real>
	<key>startColorBlue</key>
	<real>0.8</real>
	<key>startColorGreen</key>
	<real>0.2</real>
	<key>startColorRed</key>
	<real>0.2</real>
	<key>startColorVarianceAlpha</key>
	<real>0.1</real>
	<key>startColorVarianceBlue</key>
	<real>0.2</real>
	<key>startColorVarianceGreen</key>
	<real>0</real>
	<key>startColorVarianceRed</key>
	<real>0</real>
	<key>startParticleSize</key>
	<integer>3</integer>
	<key>startParticleSizeVariance</key>
	<integer>5</integer>
	<key>tangentialAccelVariance</key>
	<integer>0</integer>
	<key>tangentialAcceleration</key>
	<real>0</real>
	<key>textureFileName</key>
	<string>bullet1.png</string>
	<key>textureImageData</key>
	<string></string>
</dict>
</plist>
bullet2.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>angle</key>
	<real>90</real>
	<key>angleVariance</key>
	<real>0</real>
	<key>blendFuncDestination</key>
	<integer>1</integer>
	<key>blendFuncSource</key>
	<integer>770</integer>
	<key>duration</key>
	<real>-1</real>
	<key>emitterType</key>
	<real>0</real>
	<key>finishColorAlpha</key>
	<real>1</real>
	<key>finishColorBlue</key>
	<real>0</real>
	<key>finishColorGreen</key>
	<real>0</real>
	<key>finishColorRed</key>
	<real>0</real>
	<key>finishColorVarianceAlpha</key>
	<real>0</real>
	<key>finishColorVarianceBlue</key>
	<real>0</real>
	<key>finishColorVarianceGreen</key>
	<real>0</real>
	<key>finishColorVarianceRed</key>
	<real>0</real>
	<key>finishParticleSize</key>
	<real>0</real>
	<key>finishParticleSizeVariance</key>
	<real>0</real>
	<key>gravityx</key>
	<real>0</real>
	<key>gravityy</key>
	<real>0</real>
	<key>maxParticles</key>
	<real>44</real>
	<key>maxRadius</key>
	<real>0</real>
	<key>maxRadiusVariance</key>
	<real>0</real>
	<key>minRadius</key>
	<real>0</real>
	<key>particleLifespan</key>
	<real>1.1</real>
	<key>particleLifespanVariance</key>
	<real>0</real>
	<key>radialAccelVariance</key>
	<real>0</real>
	<key>radialAcceleration</key>
	<real>0</real>
	<key>rotatePerSecond</key>
	<real>0</real>
	<key>rotatePerSecondVariance</key>
	<real>0</real>
	<key>rotationEnd</key>
	<real>0</real>
	<key>rotationEndVariance</key>
	<real>0</real>
	<key>rotationStart</key>
	<real>720</real>
	<key>rotationStartVariance</key>
	<real>0</real>
	<key>sourcePositionVariancex</key>
	<real>0</real>
	<key>sourcePositionVariancey</key>
	<real>0</real>
	<key>sourcePositionx</key>
	<real>156.2085308056872</real>
	<key>sourcePositiony</key>
	<real>253.2701421800948</real>
	<key>speed</key>
	<real>0</real>
	<key>speedVariance</key>
	<real>0</real>
	<key>startColorAlpha</key>
	<real>1</real>
	<key>startColorBlue</key>
	<real>0.8</real>
	<key>startColorGreen</key>
	<real>0.2</real>
	<key>startColorRed</key>
	<real>0.6</real>
	<key>startColorVarianceAlpha</key>
	<real>0.1</real>
	<key>startColorVarianceBlue</key>
	<real>0.2</real>
	<key>startColorVarianceGreen</key>
	<real>0</real>
	<key>startColorVarianceRed</key>
	<real>0</real>
	<key>startParticleSize</key>
	<real>3</real>
	<key>startParticleSizeVariance</key>
	<real>5</real>
	<key>tangentialAccelVariance</key>
	<real>0</real>
	<key>tangentialAcceleration</key>
	<real>0</real>
	<key>textureFileName</key>
	<string>bullet1.png</string>
	<key>textureImageData</key>
	<string></string>
</dict>
</plist>

bullet1 と bullet2 は色が違うだけです。
これで弾のアセットを準備することができました。
作成した plistpng は Resource に追加しておいてください。

MathLib 作成

弾の発射には勿論、弾のクラスを用意する必要があるのですが、その前に自前の数学ライブラリを作ります。

MathLib.hpp
#include "cocos2d.h"

class MathLib
{
private:
    MathLib(){}
public:
    static const float RAD_TO_DEG;
    static const float DEG_TO_RAD;

    static cocos2d::Vec2 radianToVec(const float &rad) {
        return cocos2d::Vec2(cosf(rad), sinf(rad));
    }
};
MathLib.cpp
#include "MathLib.hpp"

const float MathLib::RAD_TO_DEG = 57.29577951f;
const float MathLib::DEG_TO_RAD = 0.0174532925f;

radianToVec 角度からベクトルを求める関数です。
今はこれだけですが、後ほど衝突判定で機能を追加していくことになります。

Bullet クラスの実装

それでは、ようやく Bullet クラスの実装を行いましょう。

Bullet.hpp
#include "Mover.hpp"

enum BulletType {
    BulletPlayer,
    BulletEnemy
};

class Bullet : public Mover {
public:
    static Bullet* create(const cocos2d::Vec2 &pos, const cocos2d::Vec2 &moveVec, const std::string& fileName, const ZIndex& z, const BulletType &type);
    bool init(const cocos2d::Vec2 &pos, const cocos2d::Vec2 &moveVec, const std::string& fileName, const ZIndex& z, const BulletType &type);
    virtual bool update(const float& delta) override;
    virtual ~Bullet();
    static void shootDirectionalBullet(const cocos2d::Vec2 &position, const float &speed, const float &angle, const BulletType& type);
    static void clearBatchNode();
    BulletType getType() const;
protected:
    cocos2d::ParticleSystemQuad* mParticle;
    BulletType mType;
    bool mIsDie;
private:
    explicit Bullet(){}
    Bullet(const Bullet& rhs);
    Bullet& operator=(const Bullet& rhs);

    static cocos2d::ParticleBatchNode* mEffectNode;
};
Bullet.cpp
#include "Bullet.hpp"
#include "MultiLayerScene.hpp"
#include "GameLayer.hpp"
#include "MathLib.hpp"
#include "Objects.hpp"

using namespace cocos2d;
using namespace std;

ParticleBatchNode* Bullet::mEffectNode = nullptr;

Bullet* Bullet::create(const Vec2 &pos, const Vec2 &moveVec, const string& fileName, const ZIndex& z, const BulletType &type) {
    Bullet *pBullet = new Bullet();

    if (pBullet && pBullet->init(pos, moveVec, fileName, z, type)) {
        pBullet->autorelease();
        return pBullet;
    } else {
        delete pBullet;
        pBullet = nullptr;
        return nullptr;
    }
    return nullptr;
}

bool Bullet::init(const Vec2 &pos, const Vec2 &moveVec, const string& fileName, const ZIndex& z, const BulletType &type) {
    mParticle = ParticleSystemQuad::create(fileName);
    if (mParticle == nullptr) {
        return false;
    }

    auto gameLayer = MultiLayerScene::sharedLayer()->gameLayer();

    if (Bullet::mEffectNode == nullptr) {
        Bullet::mEffectNode = ParticleBatchNode::create("bullet1.png");
        gameLayer->addChild(mEffectNode, ZWeapon);
    }

    mParticle->retain();
    mParticle->resetSystem();
    mParticle->setPositionType(ParticleSystem::PositionType::GROUPED);
    mParticle->setPosition(pos);
    mParticle->setAutoRemoveOnFinish(true);
    Bullet::mEffectNode->addChild(mParticle, z);
    mType = type;
    mIsDie = false;

    return this->Mover::init(moveVec);
}

bool Bullet::update(const float& delta) {
    if (mIsDie) {
        return false;
    }
    auto newPos = mParticle->getPosition() + mMoveVec;
    auto winSize = Director::getInstance()->getWinSize();

    if (newPos.x < 0 || newPos.x > winSize.width) {
        return false;
    }

    if (newPos.y < 0 || newPos.y > winSize.height) {
        return false;
    }

    mParticle->setPosition(newPos);
    return true;
}

void Bullet::shootDirectionalBullet(const Vec2 &position, const float &speed, const float &angle, const BulletType& type) {
    string fileName;
    ZIndex z;

    switch (type) {
        case BulletPlayer:
            fileName = "bullet1.plist";
            z = ZWeapon;
            break;
        case BulletEnemy:
            fileName = "bullet2.plist";
            z = ZBullet;
        break;
    }
    auto bullet = Bullet::create(position, MathLib::radianToVec(angle) * speed, fileName, z, type);
    bullet->retain();
    Objects::addBullet(bullet);
}

void Bullet::clearBatchNode() {
    if (Bullet::mEffectNode != nullptr) {
        Bullet::mEffectNode->removeFromParentAndCleanup(true);
        Bullet::mEffectNode = nullptr;
    }
}

BulletType Bullet::getType() const {
    return mType;
}

Bullet::~Bullet() {
    mParticle->removeFromParentAndCleanup(true);
    CC_SAFE_RELEASE_NULL(mParticle);
}

initParticle を追加しています。
mParticle->setAutoRemoveOnFinish(true); と指定すると particle 終了時に自動的に解放しますが、今回の particle 自体はずっと再生し続けれるので関係ないです。
mEffectNodeParticleBatchNode という高速化のための Node です。
updateBullet の座標が画面外なら return false しています。
updatefalse を返したときは自動的にゲームから消えるようにします。
これは後ほど実装します。
shootDirectionalBullet が弾の発射処理です。
今回はざっくりと type 毎に単純な switch 文でファイルを分けています。
Objects クラスに関してはこの後作っていきます。

Objects クラスの実装

弾の配列管理を行います。

Objects.hpp
#include "cocos2d.h"

class Bullet;

class Objects {
public:
    static void reset();
    static void addBullet(Bullet* bullet);
    static void update(const float &delta);
    static std::vector<std::shared_ptr<Bullet>> getPlayerBullets();
    static std::vector<std::shared_ptr<Bullet>> getEnemyBullets();
private:
    explicit Objects();
    Objects(const Objects& rhs);
    Objects& operator=(const Objects& rhs);
    static std::vector<std::shared_ptr<Bullet>> mPlayerBullets;
    static std::vector<std::shared_ptr<Bullet>> mEnemyBullets;
};
Objects.cpp
#include "Objects.hpp"
#include "Bullet.hpp"

using namespace std;
using namespace cocos2d;

std::vector<shared_ptr<Bullet>> Objects::mPlayerBullets;
std::vector<shared_ptr<Bullet>> Objects::mEnemyBullets;

void Objects::reset() {
    mPlayerBullets.clear();
    mEnemyBullets.clear();
    Bullet::clearBatchNode();
}

void Objects::addBullet(Bullet *bullet) {
    if (bullet->getType() == BulletPlayer) {
        mPlayerBullets.push_back(shared_ptr<Bullet>(bullet));
    } else {
        mEnemyBullets.push_back(shared_ptr<Bullet>(bullet));
    }
}

void Objects::update(const float &delta) {
    auto itrPlayerNewEnd = remove_if(mPlayerBullets.begin(), mPlayerBullets.end(), [&delta](shared_ptr<Bullet> bullet) -> bool {
        return !bullet->update(delta);
    });
    mPlayerBullets.erase(itrPlayerNewEnd, mPlayerBullets.end());

    auto itrEnemyNewEnd = remove_if(mEnemyBullets.begin(), mEnemyBullets.end(), [&delta](shared_ptr<Bullet> bullet) -> bool {
        return !bullet->update(delta);
    });
    mEnemyBullets.erase(itrEnemyNewEnd, mEnemyBullets.end());
}

vector<shared_ptr<Bullet>> Objects::getPlayerBullets() {
    return mPlayerBullets;
}

vector<shared_ptr<Bullet>> Objects::getEnemyBullets() {
    return mEnemyBullets;
}

ObjectsMover の配列を管理します。
ただ、今回は オブジェクトプール では実装しません。実際のプロジェクトでは必要になるとは思いますが、今回はシンプルに実装しました。
addBullet 関数は Bullet をゲーム上に追加します。
update 関数は Bullet の配列(=ゲーム上の Bullet )を更新していき、Bulletupdate 関数が false を返したときはその要素を削除しゲーム上から除外されます。

#GameLayer に Objects を追加

GameLayer.cpp
#include "Objects.hpp"    // include.

void GameLayer::update(float delta) {
    Objects::update(delta);    // 追加.
    mPlayer->update(delta);
}

GameLayer::~GameLayer() {
    CC_SAFE_RELEASE_NULL(mPlayer);
    Objects::reset();    // 追加.
}

###Player に弾発射処理を実装

Player.hpp
class Player : public Character {
...
private:
...
    int mAnimFrame;
};
Player.cpp


bool Player::update(const float &delta) {
    auto touchLayer = MultiLayerScene::sharedLayer()->touchLayer();
    auto startPoint = touchLayer->getStartPoint();
    auto winSize = Director::getInstance()->getWinSize();

    // 追加.
    if (mAnimFrame % 4 == 0) {
        auto pos = mSprite->getPosition();
        Bullet::shootDirectionalBullet(Vec2(pos.x, pos.y + 10), 10, 90 * MathLib::DEG_TO_RAD, BulletPlayer);
    }

    ++mAnimFrame;

    if (startPoint.x < 0 && startPoint.y < 0) {
        mStartMovePoint = Vec2(-1, -1);
        return true;
    }

    if (mStartMovePoint.x < 0 && mStartMovePoint.y < 0) {
        mStartMovePoint = mSprite->getPosition();
        return true;
    }

    auto movePoint = touchLayer->getMovePoint();

    if (movePoint.x < 0 && movePoint.y < 0) {
        return true;
    }

    auto move = movePoint - startPoint;
    auto newPos = mStartMovePoint + move;

    mSprite->setPosition(newPos);

    return true;
}

これでようやく弾の発射処理ができました。確認してみましょう。
スクリーンショット 2018-12-12 11.19.31.png

弾が発射されました。

#敵を作る

一応、ゲーム製作ということで敵を作っていきましょう。

MathLib 追加

まずは MathLib に円周上の座標を求める関数を追加します。今回の敵の移動で利用します。

MathLib.hpp
    static cocos2d::Vec2 getPointOnCircumference(const cocos2d::Vec2 &centerPos, const float &r, const float &angle) {
        return cocos2d::Vec2(centerPos.x + r * cosf(angle), centerPos.y + r * sinf(angle));
    }

Bullet クラスに NWay 弾を追加

なんとなくですが、敵の攻撃は激しくしたいので NWay 弾を実装します。

Bullet.hpp
static void shootDirectionalNWayBullets(const cocos2d::Vec2 &position, const float &speed, const float &angle, const float &angleRange, const int &count, BulletType type);
Bullet.cpp
void Bullet::shootDirectionalNWayBullets(const cocos2d::Vec2 &position, const float &speed, const float &angle, const float &angleRange, const int &count, BulletType type) {
    for (int i = 0; i < count; i++) {
        shootDirectionalBullet(position, speed, angle-angleRange/2+angleRange*i/(count-1), type);
    }
}

Enemy クラス作成

Enemy クラスを作成します。

Enemy.hpp
#include "Character.hpp"

class Enemy : public Character {
public:
    static Enemy* create(const cocos2d::Vec2 &pos, cocos2d::Layer* layer);
    bool init(const cocos2d::Vec2 &pos, cocos2d::Layer* layer);
    virtual bool update(const float &delta) override;
    virtual ~Enemy();
private:
    explicit Enemy(): mAnimFrame(0){}
    Enemy(const Enemy& rhs);
    Enemy& operator=(const Enemy& rhs);
    int mAnimFrame;
};
Enemy.cpp
#include "Enemy.hpp"
#include "MultiLayerScene.hpp"
#include "Bullet.hpp"
#include "MathLib.hpp"

using namespace cocos2d;
using namespace std;

Enemy* Enemy::create(const Vec2 &pos, Layer* layer) {
    Enemy *pEnemy = new Enemy();

    if (pEnemy && pEnemy->init(pos, layer)) {
        pEnemy->autorelease();
        return pEnemy;
    } else {
        delete pEnemy;
        pEnemy = nullptr;
        return nullptr;
    }
    return nullptr;
}

bool Enemy::init(const Vec2 &pos, Layer* layer) {
    return this->Character::init(pos, Vec2::ZERO, ZEnemy, layer, "enemy.png");
}

bool Enemy::update(const float &delta) {
    auto winSize = Director::getInstance()->getWinSize();

    auto newPos = MathLib::getPointOnCircumference(Vec2(winSize.width * 0.5, winSize.height * 0.7), 60, mAnimFrame * 3 * MathLib::DEG_TO_RAD);

    if (newPos.x < 0 || newPos.x > winSize.width) {
        return false;
    }

    if (newPos.y < 0 || newPos.y > winSize.height) {
        return false;
    }

    mSprite->setPosition(newPos);

    if (mAnimFrame % 20 == 0) {
        auto pos = mSprite->getPosition();
        Bullet::shootDirectionalNWayBullets(Vec2(pos.x, pos.y - 10), 4, -90 * MathLib::DEG_TO_RAD, 10 * MathLib::DEG_TO_RAD, 3, BulletEnemy);
    }

    ++mAnimFrame;

    return true;
}

Enemy::~Enemy() {

}
auto newPos = MathLib::getPointOnCircumference(Vec2(winSize.width * 0.5, winSize.height * 0.7), 60, mAnimFrame * 3 * MathLib::DEG_TO_RAD);

x: winSize.width * 0.5, y: winSize.height * 0.7 の位置から 半径 60 の円周上の mAnimFrame * 3 の角度の位置の座標です。

GameLayer に Enemy を追加

あとは GameLayerEnemy を追加します。

GameLayer.hpp
class Player;
class Enemy;    // 追加.

class GameLayer : public cocos2d::Layer
{
public:
    virtual bool init() override;
    CREATE_FUNC(GameLayer);
private:
    GameLayer(){};
    GameLayer(const GameLayer &rhs);
    GameLayer& operator=(const GameLayer &rhs);

    virtual ~GameLayer();

    void update(float delta) override;

    Player* mPlayer;
    Enemy* mEnemy;    // 追加.
};
GameLayer.cpp

#include "GameLayer.hpp"
#include "Player.hpp"
#include "Objects.hpp"
#include "Enemy.hpp"    // 追加.
...

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

    auto label = Label::createWithTTF("GameLayer", "fonts/Marker Felt.ttf", 24);
    auto winSize = Director::getInstance()->getWinSize();
    label->setPosition(Vec2(winSize.width * 0.5, winSize.height * 0.5));
    this->addChild(label, -1);

    mPlayer = Player::create(Vec2(winSize.width * 0.5, winSize.height * 0.25), this);
    mPlayer->retain();

    // 追加.
    mEnemy = Enemy::create(Vec2(winSize.width * 0.5, winSize.height * 0.5), this);
    mEnemy->retain();

    // 更新処理を行うようにする.
    this->scheduleUpdate();

    return true;
}

void GameLayer::update(float delta) {
    Objects::update(delta);
    mPlayer->update(delta);
    mEnemy->update(delta);    // 追加.
}

GameLayer::~GameLayer() {
    CC_SAFE_RELEASE_NULL(mPlayer);
    CC_SAFE_RELEASE_NULL(mEnemy);    // 追加.
    Objects::reset();
}

これで実行してみましょう。

スクリーンショット 2018-12-12 15.35.20.png

敵が追加されました。段々とゲームらしくなってきました。

#衝突判定
ゲームはあらゆるところで衝突判定が起きています。
衝突を検知できないとゲームは作れないでしょう。
なので、実装していきましょう。

MathLib の機能追加

MathLib.hpp
    static bool getIsHitCircle(const cocos2d::Vec2 &aPos, const cocos2d::Vec2 &bPos, const float &aR, const float &bR) {
        cocos2d::Vec2 d = bPos - aPos;
        float h = aR + bR;
        return d.x * d.x + d.y * d.y < h * h;
    }

この関数は円形同士の衝突判定を行うものです。
今回は矩形は扱わずに円形のみにします。

Collision クラス実装

衝突判定用のクラスである Collision を作っていきます。

Collision.hpp
#include "cocos2d.h"

class Mover;

class Collision : public cocos2d::Ref {
public:
    static Collision* create(const unsigned int &num, const bool &enable, const cocos2d::Vec2 &point, const float &r, const std::function<void(float)> &order, const std::function<void(Mover*)> &hitHandler);
    bool init(const unsigned int &num, const bool &enable, const cocos2d::Vec2 &point, const float &r, const std::function<void(float)> &order, const std::function<void(Mover*)> &hitHandler);
    virtual ~Collision();
    void update(const float &delta);
    unsigned int getNumber() const;
    bool getEnable() const;
    cocos2d::Vec2 getPoint() const;
    float getRange() const;
    void hit();
    void setPoint(const cocos2d::Vec2& point);
    bool getIsHit() const;
    std::function<void(float)> mOrder;
    std::function<void(Mover*)> mHitHandler;
private:
    Collision(){};
    Collision(const Collision& rhs);
    Collision& operator=(const Collision& rhs);

    unsigned int mNum;
    bool mEnable;
    cocos2d::Vec2 mPoint;
    float mR;
    bool mIsHit;
};
Collision.cpp
#include "Collision.hpp"

using namespace cocos2d;
using namespace std;

Collision* Collision::create(const unsigned int &num, const bool &enable, const Vec2 &point, const float &r, const function<void (float)> &order, const function<void(Mover*)> &hitHandler) {
    Collision *pCollision = new Collision();

    if (pCollision && pCollision->init(num, enable, point, r, order, hitHandler)) {
        pCollision->autorelease();
        return pCollision;
    } else {
        delete pCollision;
        pCollision = nullptr;
        return nullptr;
    }
    return nullptr;
}

bool Collision::init(const unsigned int &num, const bool &enable, const Vec2 &point, const float &r, const function<void (float)> &order, const function<void(Mover*)> &hitHandler) {
    mNum = num;
    mEnable = enable;
    mPoint = point;
    mR = r;
    mOrder = order;
    mHitHandler = hitHandler;
    mIsHit = false;
    return true;
}

void Collision::update(const float &delta) {
    mIsHit = false;
    mOrder(delta);
}

unsigned Collision::getNumber() const {
    return mNum;
}

bool Collision::getEnable() const {
    return mEnable;
}

Vec2 Collision::getPoint() const {
    return mPoint;
}

float Collision::getRange() const {
    return mR;
}

void Collision::hit() {
    mIsHit = true;
}

void Collision::setPoint(const cocos2d::Vec2& point) {
    mPoint = point;
}

bool Collision::getIsHit() const {
    return mIsHit;
}

Collision::~Collision() {
    mHitHandler = nullptr;
    mOrder = nullptr;
}

mIndexCollision の識別ナンバーになります。
mOrderCollision の更新処理、 mHitHandler は衝突したときの処理になります。

CollisionGroupType を追加

後ほど、 Collision を管理するクラスを作るのですが、それを種別する enum です。

CollisionGroupType.hpp
enum CollisionGroupType {
    CollisionPlayers = 0,
    CollisionEnemys,
    CollisionPlayerBullets,
    CollisionEnemyBullets,
};

Mover に Collision を追加

MoverCollision を追加していきます。
それに伴って、 Player, Enemy, Bullet も変更していきます。

Mover.hpp
...
#include "CollisionGroupType.hpp"

class Collision;

class Mover : public cocos2d::Ref
{
public:
    bool init(const cocos2d::Vec2 &moveVec, Collision* collision, const CollisionGroupType& collisionGroupType);
    virtual ~Mover();
    virtual bool update(const float &delta) = 0;
    Collision* getCollision() const;
protected:
    explicit Mover() {}

    virtual void orderCollision(float delta) = 0;
    virtual void hitHandler(Mover* mover) = 0;

    cocos2d::Vec2 mMoveVec;
    Collision* mCollision;
    CollisionGroupType mCollisionGroupType;
...
};

今回は orderCollisionhitHandler を純粋仮想関数にしました。

Mover.cpp
#include "Mover.hpp"
#include "Collision.hpp"
#include "MultiLayerScene.hpp"
#include "GameLayer.hpp"

using namespace cocos2d;
using namespace std;

bool Mover::init(const Vec2 &moveVec, Collision* collision, const CollisionGroupType& collisionGroupType) {
    mMoveVec = moveVec;
    mCollision = collision;
    mCollisionGroupType = collisionGroupType;
    return true;
}

Collision* Mover::getCollision() const {
    return mCollision;
}

Mover::~Mover() {
    MultiLayerScene::sharedLayer()->gameLayer()->removeCollision(mCollision->getNumber(), mCollisionGroupType);
    CC_SAFE_RELEASE_NULL(mCollision);
}

GameLayer には removeCollision を定義していませんが、後で追加します。

Bullet.hpp
class Bullet : public Mover {
public:
...
protected:
    virtual void orderCollision(float delta) override;
    virtual void hitHandler(Mover* mover) override;
...
private:
...
    static unsigned int mIndex;
Bullet.cpp
...
#include "Collision.hpp"
#include "Player.hpp"
#include "Enemy.hpp"
...
unsigned int Bullet::mIndex = 0;
...

bool Bullet::init(const Vec2 &pos, const Vec2 &moveVec, const string& fileName, const ZIndex& z, const BulletType &type) {
    mParticle = ParticleSystemQuad::create(fileName);
    if (mParticle == nullptr) {
        return false;
    }

    auto gameLayer = MultiLayerScene::sharedLayer()->gameLayer();

    if (Bullet::mEffectNode == nullptr) {
        Bullet::mEffectNode = ParticleBatchNode::create("bullet1.png");
        gameLayer->addChild(mEffectNode, ZWeapon);
    }

    mParticle->retain();
    mParticle->resetSystem();
    mParticle->setPositionType(ParticleSystem::PositionType::GROUPED);
    mParticle->setPosition(pos);
    mParticle->setAutoRemoveOnFinish(true);
    Bullet::mEffectNode->addChild(mParticle, z);
    mType = type;
    mIsDie = false;

    auto order = [this](float delta){
        this->orderCollision(delta);
    };

    auto hitHandler = [this](Mover* mover){
        this->hitHandler(mover);
    };

    auto collision = Collision::create(Bullet::mIndex++, true, pos, 1, order, hitHandler);
    collision->retain();

    auto collisionGroupType = mType == BulletPlayer ? CollisionPlayerBullets : CollisionEnemyBullets;
    gameLayer->addCollision(collision, collisionGroupType);
    return this->Mover::init(moveVec, collision, collisionGroupType);
}

void Bullet::orderCollision(float delta) {
    mCollision->setPoint(mParticle->getPosition());
}

void Bullet::hitHandler(Mover* mover) {
    if ( (dynamic_cast<Player*>(mover) != nullptr && mType == BulletEnemy) || (dynamic_cast<Enemy*>(mover) != nullptr && mType == BulletPlayer) ) {
        mIsDie = true;
    }
}

GameLayeraddCollision も後で実装します。

Collision::create の際に orderhitHandler を渡しています。中身は orderCollision 関数と hitHandler 関数です。

hitHandler 関数では Player が発射した弾が Enemy にヒットした or Enemy が発射した弾が Player にヒットしたなら自身を消すように mIsDietrue にします。
ただ、この型チェックに dynamic_cast でやっているのですが、ちょっと自信ないです。動作自体は問題なかったのですが、、、すみません。

Character.hpp
...
class Collision;
class GameLayer;

class Character : public Mover {
public:
    bool init(const cocos2d::Vec2 &pos, const cocos2d::Vec2 &moveVec, const ZIndex &zIndex, GameLayer* layer, const std::string& fileName, Collision* collision, const CollisionGroupType& collisionGroupType);
    virtual ~Character();
...
protected:
    virtual void orderCollision(float delta) override;
...
Character.cpp
...
#include "GameLayer.hpp"
#include "Collision.hpp"

using namespace cocos2d;
using namespace std;

bool Character::init(const Vec2 &pos, const Vec2 &moveVec, const ZIndex &zIndex, GameLayer* layer, const string& fileName, Collision* collision, const CollisionGroupType& collisionGroupType) {
    mSprite = Sprite::create(fileName);
    if (mSprite == nullptr || collision == nullptr) {
        return false;
    }
    mSprite->setPosition(pos);
    mSprite->retain();
    layer->addChild(mSprite, zIndex);
    layer->addCollision(collision, collisionGroupType);
    return this->Mover::init(moveVec, collision, collisionGroupType);
}

void Character::orderCollision(float delta) {
    mCollision->setPoint(mSprite->getPosition());
}

orderCollision はキャラクターで共通なので Character クラスで実装することにしました。

Player.hpp
...
class GameLayer;

class Player : public Character {
public:
    static Player* create(const cocos2d::Vec2 &pos, GameLayer* layer);
    bool init(const cocos2d::Vec2 &pos, GameLayer* layer);
    virtual bool update(const float &delta) override;
    virtual ~Player();
protected:
    virtual void hitHandler(Mover* mover) override;
...
Player.cpp
...
#include "Collision.hpp"
...
Player* Player::create(const Vec2 &pos, GameLayer* layer) {
...
}

bool Player::init(const Vec2 &pos, GameLayer* layer) {
    auto order = [this](float delta){
        this->orderCollision(delta);
    };

    auto hitHandler = [this](Mover* mover){
        this->hitHandler(mover);
    };

    auto collision = Collision::create(0, true, pos, 10, order, hitHandler);
    collision->retain();
    return this->Character::init(pos, Vec2::ZERO, ZPlayer, layer, "myShip.png", collision, CollisionPlayers);
}

void Player::hitHandler(Mover* mover) {
    auto bullet = dynamic_cast<Bullet*>(mover);
    if (bullet != nullptr) {
        if (bullet->getType() != BulletEnemy) {
            return;
        }
    }
    CCLOG("damage");
}

dynamic_cast を使った時点で設計として微妙かもしれませんが、今回は気にせずこうしました。

Enemy.hpp
...
class GameLayer;

class Enemy : public Character {
public:
    static Enemy* create(const cocos2d::Vec2 &pos, GameLayer* layer);
    bool init(const cocos2d::Vec2 &pos, GameLayer* layer);
    virtual bool update(const float &delta) override;
    virtual ~Enemy();

    virtual void hitHandler(Mover* mover) override;
...
Enemy.cpp
...
#include "GameLayer.hpp"
#include "Collision.hpp"

using namespace cocos2d;
using namespace std;

Enemy* Enemy::create(const Vec2 &pos, GameLayer* layer) {
...
}

bool Enemy::init(const Vec2 &pos, GameLayer* layer) {
    auto order = [this](float delta){
        this->orderCollision(delta);
    };

    auto hitHandler = [this](Mover* mover){
        this->hitHandler(mover);
    };

    auto collision = Collision::create(0, true, pos, 10, order, hitHandler);
    collision->retain();
    return this->Character::init(pos, Vec2::ZERO, ZEnemy, layer, "enemy.png", collision, CollisionEnemys);
}

void Enemy::hitHandler(Mover* mover) {
    auto bullet = dynamic_cast<Bullet*>(mover);
    if (bullet != nullptr && bullet->getType() == BulletPlayer) {
        CCLOG("Enemy Damage");
    }
}

CollisionGroup を実装

Collision を管理する CollisionGroup を実装します。

CollisionGroup.hpp
#include "cocos2d.h"
#include "CollisionGroupType.hpp"

class Collision;
class Mover;

class CollisionGroup : public cocos2d::Ref {
public:
    static CollisionGroup* create();
    bool init();
    void setLayer(cocos2d::Layer* layer);
    virtual ~CollisionGroup();
    void addCollision(Collision* collision, const CollisionGroupType &type);
    void remove(const unsigned int &num, const CollisionGroupType &type);
    void update(const float &delta);
    void setPlayer(Mover* player);
    void setEnemy(Mover* enemy);
private:
    explicit CollisionGroup(){}
    CollisionGroup(const CollisionGroup& rhs);
    CollisionGroup& operator=(const CollisionGroup& rhs);

    void hitOrder(Mover* mover, std::vector<Collision*> &collisions, const bool eachOtherEffect = false, Mover* otherMover = nullptr);

    cocos2d::Layer* mLayer;

    std::vector<Collision*> mPlayers;
    std::vector<Collision*> mEnemys;
    std::vector<Collision*> mPlayerBullets;
    std::vector<Collision*> mEnemyBullets;

    Mover* mPlayerMover;
    Mover* mEnemyMover;
};
CollisionGroup.cpp
#include "CollisionGroup.hpp"
#include "Collision.hpp"
#include "Objects.hpp"
#include "Mover.hpp"
#include "MathLib.hpp"
#include "Bullet.hpp"

using namespace cocos2d;
using namespace std;

CollisionGroup* CollisionGroup::create() {
    CollisionGroup *pCollisionGroup = new CollisionGroup();

    if (pCollisionGroup && pCollisionGroup->init()) {
        pCollisionGroup->autorelease();
        return pCollisionGroup;
    } else {
        delete pCollisionGroup;
        pCollisionGroup = nullptr;
        return nullptr;
    }
    return nullptr;
}

bool CollisionGroup::init() {
    return true;
}

void CollisionGroup::setLayer(Layer *layer) {
    mLayer = layer;
}

void CollisionGroup::addCollision(Collision* collision, const CollisionGroupType &type) {
    switch (type) {
        case CollisionPlayers:
            mPlayers.push_back(collision);
            break;
        case CollisionEnemys:
            mEnemys.push_back(collision);
            break;
        case CollisionPlayerBullets:
            mPlayerBullets.push_back(collision);
            break;
        case CollisionEnemyBullets:
            mEnemyBullets.push_back(collision);
            break;
    }
}

void CollisionGroup::remove(const unsigned int &num, const CollisionGroupType &type) {

    vector<Collision*>* collisions;

    switch (type) {
        case CollisionPlayers:
            collisions = &mPlayers;
            break;
        case CollisionEnemys:
            collisions = &mEnemys;
            break;
        case CollisionPlayerBullets:
            collisions = &mPlayerBullets;
            break;
        case CollisionEnemyBullets:
            collisions = &mEnemyBullets;
            break;
    }

    auto itrNewEnd = remove_if(collisions->begin(), collisions->end(), [&num](Collision* collision) -> bool {
        return collision->getNumber() == num;
    });
    collisions->erase(itrNewEnd, collisions->end());
}

void CollisionGroup::update(const float &delta) {
    for_each(mPlayers.begin(), mPlayers.end(), [&delta](Collision* col ){
        col->update(delta);
    });

    for_each(mEnemys.begin(), mEnemys.end(), [&delta](Collision* col ){
        col->update(delta);
    });

    for_each(mPlayerBullets.begin(), mPlayerBullets.end(), [&delta](Collision* col ){
        col->update(delta);
    });

    for_each(mEnemyBullets.begin(), mEnemyBullets.end(), [&delta](Collision* col ){
        col->update(delta);
    });

    auto playerBullets = Objects::getPlayerBullets();
    for_each(playerBullets.begin(), playerBullets.end(), [this](shared_ptr<Bullet> bullet){
        this->hitOrder(bullet.get(), this->mEnemys, true, this->mEnemyMover);
    });

    auto enemyBullets = Objects::getEnemyBullets();
    for_each(enemyBullets.begin(), enemyBullets.end(), [this](shared_ptr<Bullet> bullet){
        this->hitOrder(bullet.get(), this->mPlayers, true, this->mPlayerMover);
    });

    hitOrder(mEnemyMover, mPlayers);
}

void CollisionGroup::hitOrder(Mover* mover, std::vector<Collision*> &collisions, const bool eachOtherEffect, Mover* otherMover) {
    auto moverCol = mover->getCollision();
    if (!moverCol->getEnable()) {
        return;
    }

    for (auto collision : collisions) {
        if (!collision->getEnable()) {
            continue;
        }

        bool isHit = MathLib::getIsHitCircle(moverCol->getPoint(), collision->getPoint(), moverCol->getRange(), collision->getRange());
        if (isHit) {
            collision->hit();
            collision->mHitHandler(mover);

            if (eachOtherEffect) {
                moverCol->hit();
                moverCol->mHitHandler(otherMover);
            }
        }
    }
}

void CollisionGroup::setPlayer(Mover* player) {
    mPlayerMover = player;
}

void CollisionGroup::setEnemy(Mover* enemy) {
    mEnemyMover = enemy;
}

CollisionGroup::~CollisionGroup() {
    mPlayers.clear();
    mEnemys.clear();
    mPlayerBullets.clear();
    mEnemyBullets.clear();
}

update 関数は全 Collision を更新し、そのあと hitOrder メソッドで衝突判定と衝突したときの処理を実行します。
この辺りの衝突判定処理は下記の書籍を参考にしました。

Unity Fragments Vol.1

GameLayer に反映

ここまで Collision の追加は大変でしたが、あとは GameLayer に CollisinGroup を追加するだけです。

GameLayer.hpp
...
#include "CollisionGroupType.hpp"
...
class CollisionGroup;
class Collision;

class GameLayer : public cocos2d::Layer
{
public:
...
    void addCollision(Collision* collision, const CollisionGroupType &type);
    void removeCollision(const unsigned int &num, const CollisionGroupType &type);
private:
...
    CollisionGroup* mCollisionGroup;
GameLayer.cpp
...
#include "CollisionGroup.hpp"
...
bool GameLayer::init()
{
    if (!Layer::init()) {
        return false;
    }

    auto label = Label::createWithTTF("GameLayer", "fonts/Marker Felt.ttf", 24);
    auto winSize = Director::getInstance()->getWinSize();
    label->setPosition(Vec2(winSize.width * 0.5, winSize.height * 0.5));
    this->addChild(label, -1);

    // 追加.
    mCollisionGroup = CollisionGroup::create();
    mCollisionGroup->retain();
    mCollisionGroup->setLayer(this);

    mPlayer = Player::create(Vec2(winSize.width * 0.5, winSize.height * 0.25), this);
    mPlayer->retain();
    mCollisionGroup->setPlayer(mPlayer);    // 追加.

    mEnemy = Enemy::create(Vec2(winSize.width * 0.5, winSize.height * 0.5), this);
    mEnemy->retain();
    mCollisionGroup->setEnemy(mEnemy);      // 追加.

    // 更新処理を行うようにする.
    this->scheduleUpdate();

    return true;
}

void GameLayer::update(float delta) {
    Objects::update(delta);
    mPlayer->update(delta);
    mEnemy->update(delta);
    mCollisionGroup->update(delta);    // 追加.
}

void GameLayer::addCollision(Collision* collision, const CollisionGroupType &type) {
    mCollisionGroup->addCollision(collision, type);
}

void GameLayer::removeCollision(const unsigned int &num, const CollisionGroupType &type) {
    mCollisionGroup->remove(num, type);
}

GameLayer::~GameLayer() {
    CC_SAFE_RELEASE_NULL(mPlayer);
    CC_SAFE_RELEASE_NULL(mEnemy);
    CC_SAFE_RELEASE_NULL(mCollisionGroup);    // 追加.
    Objects::reset();
}

それでは、実行してみましょう。
スクリーンショット 2018-12-17 18.02.46.png

スクショだとちょっと分かりにくいですが、弾が当たると消えていると思います。
(青い弾が少し途切れている)

これで衝突判定は実装できたので、この段階でゲームのもっとも基本的なところではありますが実装できたと言えると思います。
プレイヤーとエネミーにライフを追加して、弾に当たるたびにライフを減らして 0 になったら爆発する演出とか作ってもいいかもしれません。
これ以降はオマケみたいなものになります。

衝突判定の可視化

衝突判定を作りましたが、これを可視化した方がデバッグが捗ると思います。
逆にこれを実装しないと衝突判定の状態がわかりにくいので、不具合修正や調整が大変になると思います。
なお、今回はやっていないのですが、デバッグビルドのみ衝突判定の可視化を行うようにすれば良いと思います。

CollisionView の実装

という訳で  CollisionView を実装します。 Collision クラス内で描画する実装でも良かったのですが、今回は分けて実装しました。

ZIndex.hpp
enum ZIndex {
    ZPlayer = 0,
    ZEnemy,
    ZWeapon,
    ZBullet,
    ZCollision,    // 追加.
};
CollisionView.hpp
class GameLayer;

class CollisionView : public cocos2d::Ref {
public:
    static CollisionView* create(const int &num, const cocos2d::Vec2 &point, const float &r, cocos2d::Layer* layer);
    bool init(const int &num, const cocos2d::Vec2 &point, const float &r, cocos2d::Layer* layer);
    virtual ~CollisionView();
    void order(const cocos2d::Vec2& point, const bool &isHit, const bool &enable);
    void draw();

    cocos2d::Vec2 mPoint;
    bool mIsHit;
    bool mEnable;
    float mRange;
    int mNum;
private:
    CollisionView(){};
    CollisionView(const CollisionView& rhs);
    CollisionView& operator=(const CollisionView& rhs);

    cocos2d::DrawNode* mDrawNode;
};
CollisionView.cpp
#include "CollisionView.hpp"
#include "ZIndex.hpp"

using namespace cocos2d;
using namespace std;

CollisionView* CollisionView::create(const int &num, const Vec2 &point, const float &r, cocos2d::Layer* layer) {
    CollisionView *pCollisionView = new CollisionView();

    if (pCollisionView && pCollisionView->init(num, point, r, layer)) {
        pCollisionView->autorelease();
        return pCollisionView;
    } else {
        delete pCollisionView;
        pCollisionView = nullptr;
        return nullptr;
    }
    return nullptr;
}

bool CollisionView::init(const int &num, const Vec2 &point, const float &r, cocos2d::Layer* layer) {
    mNum = num;
    mPoint = point;
    mIsHit = false;
    mEnable = true;
    mRange = r;
    mDrawNode = DrawNode::create();
    mDrawNode->retain();
    layer->addChild(mDrawNode, ZCollision, ZCollision);
    return true;
}

void CollisionView::order(const Vec2 &point, const bool &isHit, const bool &enable) {
    mPoint = point;
    mIsHit = isHit;
    mEnable = enable;
}

void CollisionView::draw() {
    mDrawNode->clear();

    if (!mEnable) {
        return;
    }
    mDrawNode->drawCircle(mPoint, mRange, 0, 8, false, 1, 1, mIsHit ? Color4F::RED : Color4F::GREEN);
}

CollisionView::~CollisionView() {
    mDrawNode->clear();
    CC_SAFE_RELEASE_NULL(mDrawNode);
}

DrawNode は図形を描画するクラスです。今回は drawCircle メソッドで円を描画していますが、線のみを描画しています。
通常時は緑色で衝突したときは赤色になります。

CollisionGroup に CollisionView を追加

ColliisionGroupCollisionView 関連を追加します。

CollisionGroup.hpp
...
class CollisionView;

class CollisionGroup : public cocos2d::Ref {
public:
...
    void addCollisionView(CollisionView* collisionView, const CollisionGroupType &type);
    void removeView(const unsigned int &num, const CollisionGroupType &type);
    void drawView();
private:
...
    std::vector<CollisionView*> mPlayerViews;
    std::vector<CollisionView*> mEnemyViews;
    std::vector<CollisionView*> mPlayerBulletViews;
    std::vector<CollisionView*> mEnemyBulletViews;
...
};
CollisionGroup.cpp
...
#include "CollisionView.hpp"
...
void CollisionGroup::addCollisionView(CollisionView* collisionView, const CollisionGroupType &type) {
    switch (type) {
        case CollisionPlayers:
            mPlayerViews.push_back(collisionView);
            break;
        case CollisionEnemys:
            mEnemyViews.push_back(collisionView);
            break;
        case CollisionPlayerBullets:
            mPlayerBulletViews.push_back(collisionView);
            break;
        case CollisionEnemyBullets:
            mEnemyBulletViews.push_back(collisionView);
            break;
    }
}

void CollisionGroup::removeView(const unsigned int &num, const CollisionGroupType &type) {
    vector<CollisionView*>* collisionViews;

    switch (type) {
        case CollisionPlayers:
            collisionViews = &mPlayerViews;
            break;
        case CollisionEnemys:
            collisionViews = &mEnemyViews;
            break;
        case CollisionPlayerBullets:
            collisionViews = &mPlayerBulletViews;
            break;
        case CollisionEnemyBullets:
            collisionViews = &mEnemyBulletViews;
            break;
    }

    auto itrNewEnd = remove_if(collisionViews->begin(), collisionViews->end(), [&num](CollisionView* view) -> bool {
        return view->mNum == num;
    });
    collisionViews->erase(itrNewEnd, collisionViews->end());
}

void CollisionGroup::update(const float &delta) {
...
    drawView();
}

void CollisionGroup::drawView() {
    for_each(mPlayerViews.begin(), mPlayerViews.end(), [this](CollisionView* view) {
        auto itr = find_if(mPlayers.begin(), mPlayers.end(), [view](Collision* col) { return col->getNumber() == view->mNum; });
        if (itr != mPlayers.end()) {
            Collision* col = *itr;
            view->order(col->getPoint(), col->getIsHit(), col->getEnable());
            view->draw();
        }
    });

    for_each(mEnemyViews.begin(), mEnemyViews.end(), [this](CollisionView* view) {
        auto itr = find_if(mEnemys.begin(), mEnemys.end(), [view](Collision* col) { return col->getNumber() == view->mNum; });
        if (itr != mEnemys.end()) {
            Collision* col = *itr;
            view->order(col->getPoint(), col->getIsHit(), col->getEnable());
            view->draw();
        }
    });

    for_each(mPlayerBulletViews.begin(), mPlayerBulletViews.end(), [this](CollisionView* view) {
        auto itr = find_if(mPlayerBullets.begin(), mPlayerBullets.end(), [view](Collision* col) { return col->getNumber() == view->mNum; });
        if (itr != mPlayerBullets.end()) {
            Collision* col = *itr;
            view->order(col->getPoint(), col->getIsHit(), col->getEnable());
            view->draw();
        }
    });

    for_each(mEnemyBulletViews.begin(), mEnemyBulletViews.end(), [this](CollisionView* view) {
        auto itr = find_if(mEnemyBullets.begin(), mEnemyBullets.end(), [view](Collision* col) { return col->getNumber() == view->mNum; });
        if (itr != mEnemyBullets.end()) {
            Collision* col = *itr;
            view->order(col->getPoint(), col->getIsHit(), col->getEnable());
            view->draw();
        }
    });
}

CollisionGroup::~CollisionGroup() {
...
    mPlayerViews.clear();
    mEnemyViews.clear();
    mPlayerBulletViews.clear();
    mEnemyBulletViews.clear();
}

GameLayer に CollisionView を追加

GameLayer.hpp
...
class CollisionView;

class GameLayer : public cocos2d::Layer
{
public:
...
    void addCollisionView(CollisionView* collisionView, const CollisionGroupType &type);
    void removeCollisionView(const unsigned int &num, const CollisionGroupType &type);
...
};
GameLayer.cpp
void GameLayer::addCollisionView(CollisionView* collisionView, const CollisionGroupType &type) {
    mCollisionGroup->addCollisionView(collisionView, type);
}

void GameLayer::removeCollisionView(const unsigned int &num, const CollisionGroupType &type) {
    mCollisionGroup->removeView(num, type);
}

Mover に CollisionView を追加

CollisionView をどこに追加するか色々迷ったのですが、結局 Mover に追加することにしました。

Mover.hpp
...
class CollisionView;
class GameLayer;

class Mover : public cocos2d::Ref
{
public:
    bool init(const cocos2d::Vec2 &moveVec, Collision* collision, const CollisionGroupType& collisionGroupType, GameLayer* layer);
...
protected:
...
    CollisionView* mCollisionView;
...
};
Mover.cpp
...
#include "CollisionView.hpp"

...
bool Mover::init(const Vec2 &moveVec, Collision* collision, const CollisionGroupType& collisionGroupType, GameLayer* layer) {
    mMoveVec = moveVec;
    mCollision = collision;
    mCollisionView = CollisionView::create(collision->getNumber(), collision->getPoint(), collision->getRange(), layer);
    mCollisionView->retain();
    layer->addCollisionView(mCollisionView, collisionGroupType);
    mCollisionGroupType = collisionGroupType;
    return true;
}

Mover::~Mover() {
    auto gameLayer = MultiLayerScene::sharedLayer()->gameLayer();
    gameLayer->removeCollision(mCollision->getNumber(), mCollisionGroupType);
    gameLayer->removeCollisionView(mCollision->getNumber(), mCollisionGroupType);
    CC_SAFE_RELEASE_NULL(mCollisionView);
    CC_SAFE_RELEASE_NULL(mCollision);
}
Character.cpp
bool Character::init(const Vec2 &pos, const Vec2 &moveVec, const ZIndex &zIndex, GameLayer* layer, const string& fileName, Collision* collision, const CollisionGroupType& collisionGroupType) {
    mSprite = Sprite::create(fileName);
    if (mSprite == nullptr || collision == nullptr) {
        return false;
    }
    mSprite->setPosition(pos);
    mSprite->retain();
    layer->addChild(mSprite, zIndex);
    layer->addCollision(collision, collisionGroupType);
    return this->Mover::init(moveVec, collision, collisionGroupType, layer);    // 変更.
}
Bullet.cpp
bool Bullet::init(const Vec2 &pos, const Vec2 &moveVec, const string& fileName, const ZIndex& z, const BulletType &type) {
...
    return this->Mover::init(moveVec, collision, collisionGroupType, gameLayer);    // 変更.
}

それでは、実行してみましょう。

スクリーンショット 2018-12-18 16.50.43.png

衝突判定が可視化されたと思います。

BGM

音や音楽はゲームの雰囲気を決定づけるとても大事な要素です。フリー素材でもいいですが、ここまで作ったなら音楽も作ってしまいましょう。
尚、自分は作曲に関しては素人です。ご了承頂ければと思います。

GarageBand

Mac にプリインストールされている DTM ソフトです。
スクリーンショット 2018-12-19 8.15.03.png
↑のアイコンをクリックして起動しましょう。
起動したら cmd + n で新規ファイルを作成します。
スクリーンショット 2018-12-19 7.59.59.png

こんな感じの画面になると思います。

スクリーンショット 2018-12-19 8.00.28.png

コントロール から ループブラウザを表示 を選択します。
画面右側にループ音源のブラウザが表示されていると思います。
今回はこのループ音源を組み合わせて音楽を作って行きましょう。

ドラム の追加

まずドラムを追加してみましょう。
ここからは一例という感じで好きなループ素材を追加してもらえればいいと思います。
(むしろそれがいいです)

  • すべてのドラムを選択
  • 80s Pop Beat 08 を選択しトラックにドラッグ

スクリーンショット 2018-12-19 9.03.14.png

↑の画像のような形になれば画面下部バーの再生ボタンをクリックしましょう。トラックが再生されると思います。

ループの設定

ループ再生してみましょう。
その前に追加した 80s Pop Beat 08 を伸ばしたいと思います。
伸ばすには 80s Pop Beat 08 の右端にマウスカーソルを移動させるとマウスカーソルがループのアイコンに切り替わるので、その状態で右にドラッグすると伸びます。今回は 9 のところまで伸ばしましょう。

それが出来たら画面下端のループボタンをクリックしてください。
画面上端に黄色いバーが出てきたと思います。この黄色いバーの端っこをドラッグして 9 のところまで伸ばします。
スクリーンショット 2018-12-19 9.10.09.png

これで今回のドラムが出来た感じです。
(すごく短いですが)

トラックの追加はこんな感じで行い、以降はこれを繰り返すので画像は省略します。
なので、先に今回の完成図を載せておきます。
スクリーンショット 2018-12-19 9.24.03.png

メインのトラック追加

メインとなるトラックを追加します。
メインと表現しているのですが、特に区別している訳ではなく、曲で一番印象が強いトラック、、、な感じのやつ(ギターとか)です。
なので、ここからはドラムを追加した時と同じ作業を繰り返します。
今回は以下のようにしましたが、好きにやっちゃってください。

  • ループブラウザの エレクトリック を選択(この際、すべてのドラム は無効にしておく)
  • ループ素材は Dance Floor Pattern 30 を選択
  • トラックにドラッグする(新しいトラックを追加する形にする)
  • 追加した Dance Floor Pattern 30 は 5 のところまで伸ばす
  • ループ素材 Dance Floor Pattern 45 を追加する。その際、先ほどのトラックと同じトラックに追加するが、場所は Dance Floor Pattern 30 に続くようにする
  • 追加した Dance Floor Pattern 45 を 9 のところまで伸ばす

ベースの追加

ベースを追加します。

  • ループブラウザの ベース を選択
  • ループ素材は Synthbass Sequence 03 を選択
  • トラックにドラッグする(新しいトラックを追加する形にする)
  • 追加した Synthbass Sequence 03 を 9 のところまで伸ばす

外しの音を入れる

最後に外しの音を追加します(表現間違っているかも)。

  • ループブラウザの 単音源 を選択
  • ループ素材は Long Crash Cymbal 04 を選択
  • トラックにドラッグする(新しいトラックを追加する形にする)

以上で終了です。
あとは各トラックの音量の調整(トラックにあるスライダ)してください。
他にも今回は解説しませんが、エフェクトをかけても面白いと思います。

書き出し

出来上がったら曲を mp3 で書き出しましょう。

  • ツールバーの 共有 をクリック
  • 曲をディスクに 書き出し を選択
  • 圧縮方法を MP3 エンコーダ に選択
  • 書き出し をクリック

で任意のディレクトリに mp3 を書き出します。
writeBgm.png

スクリーンショット 2018-12-19 10.21.09.png

mp3 を書き出したら Xcode を開き Resources に追加してください。

今回のこの作成方法もこちらの書籍を参考にしました。

コピペではじめるiPhoneゲームプログラミング

BGM を再生する

最後に作成した BGM をゲームで再生するようにしましょう。
やることは簡単です。

GameLayer.cpp
...
#include "SimpleAudioEngine.h"
...
using namespace CocosDenshion;

bool GameLayer::init()
{
...
    SimpleAudioEngine::getInstance()->playBackgroundMusic("Game.mp3", true);

    return true;
}

cocos2d-x では SimpleAudioEngine というサウンドエンジンが用意されています。
(実際はあまり使い勝手は良くないのですが、名前の通りシンプルな再生なら問題ないです)
SimpleAudioEngine::getInstance()->playBackgroundMusic("Game.mp3", true); で BGM をループ再生しています。

それでは、実行してみましょう。自作の BGM がループ再生されて入れば OK です。
これでおしまいです。
完成品はこちらになります。
https://github.com/HirokiIshimori/Cocos2dSample/tree/master

SE について

今回、SE の製作は行っていません。
SE も自作したいなら有料ですが下記のツールがオススメみたいです。
http://tsugi-studio.com/web/jp/products.html

感想と反省

まず、大変なボリュームになってしまって申し訳なかったです。
元々は自作で HTML5 のゲームを作っているので、そのネタを何か書こうかな程度だったのですが、こちらまだリリースできていないので断念しました。
(リリース次第、また投稿しようと思います)
ただ、他に書けることもあまりなかったのでこのような記事になりました。
実装も色々うまくいっていないところありますが、まず cocos2d のリファレンスカウンタを使わず、スマートポインタで良かったんじゃないかと思いました。
それと Javascript から移植したコードもあってテンヤワンヤしてました。
後、スクリプト作成と実行あたりも書きたかったのですが、ちょっと時間足りなかったです。
でも、こうやってまとめて書けて良かったと思います。
ゲームエンジンって今は Unity とか Unreal Engine とか使うのがメジャーだと思うんですけど、Cocos2d でコードベースでの入門もいいのかなと思って書いてみました。
今回、 cocos2d-x で書いていたのですが、 C++ でのクリーンなコードを書きたいという方はこちらの記事と読むといいかもしれません。

C++でクリーンなコードの書き方

参考

コピペではじめるiPhoneゲームプログラミング

iOSで作るシューティングゲーム

Unity Fragments Vol.1

19
11
1

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
19
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?