#はじめに
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
する。
とりあえず Hello World
が実行されました。
今回、自分としては何となく縦向きが良いので縦に変更したいと思います。
(別にどうでも良い人はスルーしてもらって大丈夫です)
縦向きにする
###プロジェクトファイルの設定
- プロジェクトファイルを選択
- Portrait のみ選択する
###RootViewController を編集
// For ios6, use supportedInterfaceOrientations & shouldAutorotate instead
#ifdef __IPHONE_6_0
- (NSUInteger) supportedInterfaceOrientations{
return UIInterfaceOrientationMaskPortrait;
}
#endif
UIInterfaceOrientationMaskAllButUpsideDown
-> UIInterfaceOrientationMaskPortrait
に変更
- (BOOL) shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
return UIInterfaceOrientationIsPortrait(toInterfaceOrientation);
}
を追加
これでとりあえず縦向きにはなりますが、デバッグ情報のラベルが潰れてしまっています。
なので、これらも修正していきます。
CCDirector.cpp を編集
createStatsLabel()
関数に手を加えていく
- 下の方に
_FPSLabel
を初期化を実装しているところがあるので、_FPSLabel->setScale(scaleFactor);
の下に下記を追記する
_FPSLabel->setScaleX(scaleFactor * 2);
- 同じような形で
_drawnBatchesLabel
と_drawnVerticesLabel
も追記していく
_drawnBatchesLabel->setScaleX(scaleFactor * 2);
_drawnVerticesLabel->setScaleX(scaleFactor * 2);
-
createStatsLabel()
関数の最後の 4 行を下記のように変更する
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);
ゲームのベース部分を作る
無事、起動できたということで次はゲームのベース部分を作っていきたいと思います。
今回はスマホゲームということで指でゲームを操作したいので、タップやスワイプ処理が欲しいです。
そこで、下記のようなレイヤー構成で実装することにします。
- GameLayer は GameObject を載せている
- TouchLayer はタップ処理を受け付ける
- GameLayer の上に TouchLayer がある
この構成を実現するために MultiLayerScene という GameScene を作成します。
GameScene とはゲームを構成するシーン(画面)となるレイヤーです。
今は HellowWorldScene
がそれになります。
また、この構成はこちらの書籍を参考にしました。
GameLayer
GameLayer はゲーム部分を担当するレイヤーです。すでにある HelloWorldScene をベースに手を加えても良いですし、新たに作っても良いと思います。
以下、実装です。
#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 */
#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 はタップ処理を受けつけるレイヤーです。こちらは新しく実装していきます。
#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 */
#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
MultiLayerScene
は GameLayer
と TouchLayer
をまとめるシーンになります。
実装自体はシンプルです。
#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 */
#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"
を下記のように変更します。
#include "MultiLayerScene.hpp"
applicationDidFinishLaunching
メソッドの最後の方の auto scene = HelloWorld::createScene();
を以下のようにします。
auto scene = MultiLayerScene::scene();
これでゲーム部分の GameLayer
と タッチイベントの TouchLayer
が合わさりました。
cmd + r
で実行してみましょう。
GameLayer
が見えています。さらに画面をタップするとコンソールにタップイベントのログが流れるのを確認できると思います。
一応、ここまででゲームの下地部分は出来ました。
#キャラクターを追加する
絵はどうやって用意する?
プログラマがゲームを作る上でまず課題になるのは絵をどうやって用意するかです。
絵が描ける人は問題ないかもしれませんが、大半の人はそうではないのではないでしょうか?
自分も簡単な UI パーツなら作ることはできますが、キャラクターとかは作れない or 騙し騙し作る感じです。
Unity
や UnrealEngine
なら AssetStore
や MarketPlace
を利用するのが最も手軽かもしれません。ただし、有料のものなら自分のゲームに組み込めるかよく検討した方が良いでしょう。
他にもフリー素材を利用するのも良いと思います。
いずれも絶対に利用規約は確認した上で利用してください。
今回は、自分の方でよく分からない絵(なんとなく戦闘機をイメージ)を用意しました。
青い絵がプレイヤー。赤い絵がエネミーを想定しています。
Mover クラスを実装
キャラクターを作っていくのですが、先にベースクラスを作っていきたいと思います。
ZIndex
は描画順を指定するための enum です。
#ifndef ZIndex_h
#define ZIndex_h
enum ZIndex {
ZPlayer = 0,
ZEnemy,
ZWeapon,
ZBullet
};
#endif /* ZIndex_h */
Mover
クラスです。
#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 */
#include "Mover.hpp"
using namespace cocos2d;
using namespace std;
bool Mover::init(const Vec2 &moveVec) {
mMoveVec = moveVec;
return true;
}
Mover
は mMoveVec
という移動量だけをとりあえず持たせています。
update
関数を純粋仮想関数にすることにより抽象クラスにしています。この関数でキャラクターなどの動きを表現することになります。
なお、経過時間である delta
を受け取るようにしていますが、今回は使用しません。
この辺りの構成は下記の書籍を参考にしました。
Character クラス
次に Character
クラスを作ります。名前の通りキャラクターを作るときのベースクラスです。
#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 */
#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
クラスを作っていきます。
#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 */
#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 を追加
GameLayer
に Player
を追加してプレイヤーを登場させましょう。
メンバ変数に mPlayer
を追加してください。
#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 */
...
#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
で実行してみましょう。
画面の下の方にプレイヤーが表示されました。
#プレイヤーの移動
プレイヤーの移動は画面をタップして、動かした量だけそのまま移動させることにします。
TouchLayer から移動量を取得
private
なメンバ変数と getter を用意します。
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;
};
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 の移動処理を実装
移動開始点をメンバ変数に追加します。
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
処理を実装します。
...
#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;
}
void GameLayer::update(float delta) {
mPlayer->update(delta);
}
弾の発射処理
唐突ですがプレイヤーに弾を発射させましょう。
先ほど、プログラマは絵を用意するのは中々大変という話をしたと思いますが、エフェクトなら少し救いがあります。
パーティクルという粒子を利用したものです。
説明するよりも作った方が早いので、下記のサイトで作成します。
particle2dx.com
particle2dx
particle2dx に関してはこちらの記事を参照してもらえればと思います。
自分が作ったのは下記になります。
今回、ダウンロードするときは下記のように plist
と png
ファイルを別で DL します。
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>
<?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 は色が違うだけです。
これで弾のアセットを準備することができました。
作成した plist
と png
は Resource に追加しておいてください。
MathLib 作成
弾の発射には勿論、弾のクラスを用意する必要があるのですが、その前に自前の数学ライブラリを作ります。
#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));
}
};
#include "MathLib.hpp"
const float MathLib::RAD_TO_DEG = 57.29577951f;
const float MathLib::DEG_TO_RAD = 0.0174532925f;
radianToVec
角度からベクトルを求める関数です。
今はこれだけですが、後ほど衝突判定で機能を追加していくことになります。
Bullet クラスの実装
それでは、ようやく Bullet クラスの実装を行いましょう。
#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;
};
#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);
}
init
で Particle
を追加しています。
mParticle->setAutoRemoveOnFinish(true);
と指定すると particle 終了時に自動的に解放しますが、今回の particle 自体はずっと再生し続けれるので関係ないです。
mEffectNode
は ParticleBatchNode
という高速化のための Node です。
update
は Bullet
の座標が画面外なら return false
しています。
update
で false
を返したときは自動的にゲームから消えるようにします。
これは後ほど実装します。
shootDirectionalBullet
が弾の発射処理です。
今回はざっくりと type
毎に単純な switch
文でファイルを分けています。
Objects
クラスに関してはこの後作っていきます。
Objects クラスの実装
弾の配列管理を行います。
#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;
};
#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;
}
Objects
は Mover
の配列を管理します。
ただ、今回は オブジェクトプール
では実装しません。実際のプロジェクトでは必要になるとは思いますが、今回はシンプルに実装しました。
addBullet
関数は Bullet
をゲーム上に追加します。
update
関数は Bullet
の配列(=ゲーム上の Bullet
)を更新していき、Bullet
の update
関数が false
を返したときはその要素を削除しゲーム上から除外されます。
#GameLayer に Objects を追加
#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 に弾発射処理を実装
class Player : public Character {
...
private:
...
int mAnimFrame;
};
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;
}
これでようやく弾の発射処理ができました。確認してみましょう。
弾が発射されました。
#敵を作る
一応、ゲーム製作ということで敵を作っていきましょう。
MathLib 追加
まずは MathLib
に円周上の座標を求める関数を追加します。今回の敵の移動で利用します。
static cocos2d::Vec2 getPointOnCircumference(const cocos2d::Vec2 ¢erPos, const float &r, const float &angle) {
return cocos2d::Vec2(centerPos.x + r * cosf(angle), centerPos.y + r * sinf(angle));
}
Bullet クラスに NWay 弾を追加
なんとなくですが、敵の攻撃は激しくしたいので NWay 弾を実装します。
static void shootDirectionalNWayBullets(const cocos2d::Vec2 &position, const float &speed, const float &angle, const float &angleRange, const int &count, BulletType type);
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
クラスを作成します。
#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;
};
#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 を追加
あとは GameLayer
に Enemy
を追加します。
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; // 追加.
};
#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();
}
これで実行してみましょう。
敵が追加されました。段々とゲームらしくなってきました。
#衝突判定
ゲームはあらゆるところで衝突判定が起きています。
衝突を検知できないとゲームは作れないでしょう。
なので、実装していきましょう。
MathLib の機能追加
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
を作っていきます。
#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;
};
#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;
}
mIndex
は Collision
の識別ナンバーになります。
mOrder
は Collision
の更新処理、 mHitHandler
は衝突したときの処理になります。
CollisionGroupType を追加
後ほど、 Collision
を管理するクラスを作るのですが、それを種別する enum です。
enum CollisionGroupType {
CollisionPlayers = 0,
CollisionEnemys,
CollisionPlayerBullets,
CollisionEnemyBullets,
};
Mover に Collision を追加
Mover
に Collision
を追加していきます。
それに伴って、 Player
, Enemy
, Bullet
も変更していきます。
...
#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;
...
};
今回は orderCollision
と hitHandler
を純粋仮想関数にしました。
#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
を定義していませんが、後で追加します。
class Bullet : public Mover {
public:
...
protected:
virtual void orderCollision(float delta) override;
virtual void hitHandler(Mover* mover) override;
...
private:
...
static unsigned int mIndex;
...
#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;
}
}
GameLayer
の addCollision
も後で実装します。
Collision::create
の際に order
と hitHandler
を渡しています。中身は orderCollision
関数と hitHandler
関数です。
hitHandler
関数では Player
が発射した弾が Enemy
にヒットした or Enemy
が発射した弾が Player
にヒットしたなら自身を消すように mIsDie
を true
にします。
ただ、この型チェックに dynamic_cast
でやっているのですが、ちょっと自信ないです。動作自体は問題なかったのですが、、、すみません。
...
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;
...
...
#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
クラスで実装することにしました。
...
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;
...
...
#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
を使った時点で設計として微妙かもしれませんが、今回は気にせずこうしました。
...
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;
...
...
#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
を実装します。
#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;
};
#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
メソッドで衝突判定と衝突したときの処理を実行します。
この辺りの衝突判定処理は下記の書籍を参考にしました。
GameLayer に反映
ここまで Collision
の追加は大変でしたが、あとは GameLayer に CollisinGroup
を追加するだけです。
...
#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;
...
#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();
}
スクショだとちょっと分かりにくいですが、弾が当たると消えていると思います。
(青い弾が少し途切れている)
これで衝突判定は実装できたので、この段階でゲームのもっとも基本的なところではありますが実装できたと言えると思います。
プレイヤーとエネミーにライフを追加して、弾に当たるたびにライフを減らして 0 になったら爆発する演出とか作ってもいいかもしれません。
これ以降はオマケみたいなものになります。
衝突判定の可視化
衝突判定を作りましたが、これを可視化した方がデバッグが捗ると思います。
逆にこれを実装しないと衝突判定の状態がわかりにくいので、不具合修正や調整が大変になると思います。
なお、今回はやっていないのですが、デバッグビルドのみ衝突判定の可視化を行うようにすれば良いと思います。
CollisionView の実装
という訳で CollisionView
を実装します。 Collision
クラス内で描画する実装でも良かったのですが、今回は分けて実装しました。
enum ZIndex {
ZPlayer = 0,
ZEnemy,
ZWeapon,
ZBullet,
ZCollision, // 追加.
};
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;
};
#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 を追加
ColliisionGroup
に CollisionView
関連を追加します。
...
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;
...
};
...
#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 を追加
...
class CollisionView;
class GameLayer : public cocos2d::Layer
{
public:
...
void addCollisionView(CollisionView* collisionView, const CollisionGroupType &type);
void removeCollisionView(const unsigned int &num, const CollisionGroupType &type);
...
};
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
に追加することにしました。
...
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;
...
};
...
#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);
}
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); // 変更.
}
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); // 変更.
}
それでは、実行してみましょう。
衝突判定が可視化されたと思います。
BGM
音や音楽はゲームの雰囲気を決定づけるとても大事な要素です。フリー素材でもいいですが、ここまで作ったなら音楽も作ってしまいましょう。
尚、自分は作曲に関しては素人です。ご了承頂ければと思います。
GarageBand
Mac にプリインストールされている DTM ソフトです。
↑のアイコンをクリックして起動しましょう。
起動したら cmd
+ n
で新規ファイルを作成します。
こんな感じの画面になると思います。
コントロール
から ループブラウザを表示
を選択します。
画面右側にループ音源のブラウザが表示されていると思います。
今回はこのループ音源を組み合わせて音楽を作って行きましょう。
ドラム の追加
まずドラムを追加してみましょう。
ここからは一例という感じで好きなループ素材を追加してもらえればいいと思います。
(むしろそれがいいです)
- すべてのドラムを選択
- 80s Pop Beat 08 を選択しトラックにドラッグ
↑の画像のような形になれば画面下部バーの再生ボタンをクリックしましょう。トラックが再生されると思います。
ループの設定
ループ再生してみましょう。
その前に追加した 80s Pop Beat 08
を伸ばしたいと思います。
伸ばすには 80s Pop Beat 08
の右端にマウスカーソルを移動させるとマウスカーソルがループのアイコンに切り替わるので、その状態で右にドラッグすると伸びます。今回は 9 のところまで伸ばしましょう。
それが出来たら画面下端のループボタンをクリックしてください。
画面上端に黄色いバーが出てきたと思います。この黄色いバーの端っこをドラッグして 9 のところまで伸ばします。
これで今回のドラムが出来た感じです。
(すごく短いですが)
トラックの追加はこんな感じで行い、以降はこれを繰り返すので画像は省略します。
なので、先に今回の完成図を載せておきます。
メインのトラック追加
メインとなるトラックを追加します。
メインと表現しているのですが、特に区別している訳ではなく、曲で一番印象が強いトラック、、、な感じのやつ(ギターとか)です。
なので、ここからはドラムを追加した時と同じ作業を繰り返します。
今回は以下のようにしましたが、好きにやっちゃってください。
- ループブラウザの
エレクトリック
を選択(この際、すべてのドラム
は無効にしておく) - ループ素材は
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 を書き出したら Xcode を開き Resources に追加してください。
今回のこの作成方法もこちらの書籍を参考にしました。
BGM を再生する
最後に作成した BGM をゲームで再生するようにしましょう。
やることは簡単です。
...
#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++
でのクリーンなコードを書きたいという方はこちらの記事と読むといいかもしれません。