Cocos2d-x 10円ゲーム講座 第13回、デモ画面を実装します。
これまででゲーム部分は完成しています。デモ画面は蛇足といっても過言ではありません(笑)。
タイトルを作ってシーン遷移、というのがチュートリアル的にはシーン遷移を学べて良いかもしれません。しかし、10円ゲームにタイトルからのシーンチェンジは不要でしょう。コインを入れてサクッとスタート。現実世界でもサクサク感は10円ゲームの醍醐味の1つだと思います。
私の思い出ですが、駄菓子屋に置いてあるゲーム機のデモ画面が好きでした。今でもそうですが、筐体機のデモ画面って非常に楽しそうな感じがしますよね。お菓子にするか、ゲームをするか……どれにお金を使おうか迷ってた子供時代、デモ画面は効果的な宣伝だったように思います。
そんなわけで、10円ゲームにコインを入れるまではデモ画面にしてみます。現実世界の10円ゲーム機のデモプレイはありえませんけどね。
前置きが長くなりましたが、10円ゲームのデモ画面というのを考えてみます。
こんな感じでしょうか。
デモプレイ画面であることは、このDEMO PLAY文字を明滅させることにします。
説明する部分は少ないのでコードを載せていきます。
class OneCoinGame : public cocos2d::Layer { public: // DEMO画面用update void demoUpdate(float delta); private: // デモモード bool _isDemo; cocos2d::Sprite *_demoSprite; };
デモ画面用のスケジュール関数と変数の定義です。
// デモモードかどうか _isDemo = false; // カスタム定期処理の開始(5.0秒毎の呼び出し) this->schedule(schedule_selector(OneCoinGame::demoUpdate), 5.0f);
コインの監視の際に説明したscheduleUpdate()メソッドはフレーム毎にupdate(float delta)を繰り返す呼び出す処理と決まっていましたが、デモ画面用のスケジュール処理は毎フレーム呼び出す必要はありません。そういった場合にschedule()を使用することで任意のメソッドを任意の間隔で呼び出す処理が可能です。今回は5.0秒毎に1回demoUpdate()を呼び出します。
// デモモード時の更新処理 void OneCoinGame::demoUpdate(float delta) { // デモ状態かどうか if (!_isDemo) { return; } // coinの状態を確認 // Enableがfalseなら、コイン投入処理 if (!_coin->getPhysicsBody()->isEnabled()) { this->coinEntry(); } // コインがレバー位置にあるなら、レバー処理 for (int i = 0; i < 4; i++) { // 範囲内ならコインにランダムな力を加える if (leverArea[i].containsPoint(_coin->getPosition())) { auto power = CCRANDOM_0_1(); if (i == 0 || i == 2) { this->applyForceCoin(1.0f, power); _lever[i]->setRotation(power * -90); _lever[i]->runAction(RotateTo::create(0.2, 0.0f)); } else { this->applyForceCoin(-1.0f, power); _lever[i]->setRotation(power * 90); _lever[i]->runAction(RotateTo::create(0.2, 0.0f)); } // 効果音を鳴らす CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("se/lever.wav"); } } }
CCRANDOM_0_1()はCocos2d-xでランダムを扱う場合に便利なマクロです。0~1までのfloat値を返してくれます。その他の説明は特に新しいことをしていないので省きます。
DEMO PLAYの明滅は、フェードイン・アウトの繰り返し処理で実現しています。Blinkを使わない理由としては、Blinkの点滅処理はbVisibleを操作するため、ボタンを押した際のsetVisibile処理と不整合が出るためです。ちなみにFadeIn, FadeOutはopacityを操作します。
デモプレイ。どうでしょうか。ソースコードはこちら
私はこういう勝手に操作しているのを眺めるのは大好きです。ランダムに力を加えているだけですが、見ていると結構楽しいですよね。
]]>Cocos2d-x 10円ゲーム講座 第12回、パーティクルを使ってエフェクトを発生させます。
10円ゲームにパーティクルを使ってエフェクト処理を施してみます。
パーティクルとは粒子のことで、小さな画像を多量に使い、ひとつひとつに動きをつけることでエフェクトを表現するものです。これはもう実際にパーティクルを作成するエディタを触ってもらった方が早いと思います。
有名なパーティクルエディタ。パーティクルエディタはいくつかありますが、こちらはフリーで使えるのでオススメです。
ササッとあれこれ弄ってパーティクルが出来たら、
出力はExport → COCOS2DXをクリックすればparticle_texture.plist(デフォルトファイル名)がダウンロードできます。
今回作成したパーティクルはこちら。コイン投入時に火花を散らすようなシンプルなエフェクトです。
Cocos2d-xではパーティクルのplistをそのまま読み込んで使用することができます。
// パーティクル処理 auto particle = ParticleSystemQuad::create("particle/particle_texture.plist"); particle->setPosition(Vec2(visibleSize.width*0.7, visibleSize.height*0.9)); particle->setAutoRemoveOnFinish(true); // パーティクルが終了したらNodeを削除する this->addChild(particle, 3);
パーティクルを使うにはParticleSystemQuadクラスを利用します。setAutoRemoveOnFinish(true)をしておけば、パーティクルが終了した際に自動で削除してくれるので便利です。
無限ループを設定しているパーティクルは当たり前ですが自動で削除されないので注意しましょう。
ソースコードはこちら
エフェクトの回でも書きましたが、10円ゲームはレトロさがウリでもあるので過度なエフェクトは不要な気もしますね。
次回はデモ画面の実装です。
]]>
Cocos2d-x 10円ゲーム講座 第11回、スプライトアクションを使ってユーザビリティを向上させる方法についです。
ゲームの骨格となる部分は完成しました。1つ問題点を上げるならボタンがボタンと解りづらいことです。つまりユーザビリティが悪いということです。
レバーは見た感じでわかるかもしれませんが、お金の投入口がボタンになっていることは、パッと見ただけではよくわからない可能性もあります。
スマートフォンの台頭で、マウスではなくタッチ操作が主流になってきました。マウスの場合は、マウスオーバーという手法によりボタンがボタンであることを明示できました。
しかし、タッチ操作ではマウスオーバーに相当する操作がありません。
ボタンを明示するよくある方法としてはグロウ処理を行うことです。ボタンを淡く光らせることで、ユーザーに装置であることを明示することができます。今回はボタン投入口とレバーにグロウ処理を施してみます。
用意したグロウ画像です。
グロウ画像をこれまで同様、Spriteクラスを使って表示します。
auto sprite = Sprite::create("coin_entry_glow.png"); sprite->setPosition(Vec2(visibleSize.width*0.7, visibleSize.height*0.9)); this->addChild(sprite, 2);
グロウ処理のためにSpriteにアクションを設定します。
// アクション auto blink = Blink::create(1.5f, 1); // 点滅 1.5秒で1回点滅 auto action = RepeatForever::create(blink); // blinkを永久に繰り返す sprite->runAction(action);
点滅を永久に繰り返すというアクションを設定します。
レバーも同様に設定します。
// レバーのglow処理 auto leverGlow = Sprite::create("lever_glow.png"); leverGlow->setAnchorPoint(Vec2(0.0f, 0.0f)); _lever[i]->addChild(leverGlow, 2); // 1.5秒に1回の点滅を永久に繰り返す leverGlow->runAction(RepeatForever::create(Blink::create(1.5f, 1)));
Cocos2d-xが使えるアクションの詳細についてはReferenceを見ると良いでしょう。
ソースコードはこちら
10円ゲームはレトロさがウリでもあるので、ボタンやレバーのグロウ処理はレトロさを損なう結果になりますが、ボタンとしての理解度は向上したと思います。
リアリティかユーザビリティか。どちらも良いに越したことはありませんが、ゲームとしてユーザビリティを重視する場面は多いと思います。
次回はパーティクルを使用します。
]]>
Cocos2d-x 10円ゲーム講座 第10回、効果音を鳴らします。
前回までの実装により、コインは転がり穴には落ちる。ゴールすれば景品も出る。しかし何か物足りませんね。何かというか、音です。
コインがぶつかる音があるだけで、リアリティがまるで変わります。そんなわけで、ゲームの大切な要素、効果音を鳴らしてみましょう。
今回は効果音を作成することにしました。ネット上にフリー素材が山ほどありますが、それは使用に関してフリーなだけで大体は再配布禁止です。githubでの公開には問題があるため、自分で作ることにしました。
コインの音は簡単に録音できます。一人ではっちゃけて歌いたくなったのでこんなマイクを持っていますが、別にヘッドセットのマイクや、スマートフォンなどでも十分録音できます。
コイン音は3つ録音しました。
ゴールした時の電子音などはBeam2002を使って作りました。Beam2002はアナログシンセ風の効果音作成ソフトウェアです。ADSRを操作するいわゆるエンベロープ・ジェネレーターのようなことができるソフトです。
レバー操作時の音と、ゴール時の音を作成しました。
次はこれらを10円ゲームに組み込み、再生します。
効果音が用意できたので鳴らしてみることにします。Cocos2d-xで効果音の再生はCocosDenshionクラスを使用します。Denshion、でんしおん、つまりは電子音です。
init()で使用する効果音をプリロードしておきます。
#include <SimpleAudioEngine.h> bool OneCoinGame::init() { /*--- 略 ---*/ // サウンドのプリロード CocosDenshion::SimpleAudioEngine::getInstance()->preloadEffect("se/coin_entry.wav"); CocosDenshion::SimpleAudioEngine::getInstance()->preloadEffect("se/coin_contact.wav"); CocosDenshion::SimpleAudioEngine::getInstance()->preloadEffect("se/coin_hole.wav"); CocosDenshion::SimpleAudioEngine::getInstance()->preloadEffect("se/goal.wav"); CocosDenshion::SimpleAudioEngine::getInstance()->preloadEffect("se/lever.wav"); }
CocosDenshionは拡張クラスなので、SimpleAudioEngine.hをインポートします。
後は効果音を鳴らしたい箇所でplayEffect()を実行するだけで、効果音を鳴らすことが出来ます。
// コイン投入ボタン void OneCoinGame::insertCoinCallback(cocos2d::Ref* pSender) { /*--- 略 ---*/ // 効果音を鳴らす CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("se/coin_entry.wav"); }
これでコイン投入口をタッチした時に、お金を投入する効果音が鳴ります。
同じように、コインが穴に落ちた時の音、ゴール時、レバー操作時も該当箇所にコードを実装するだけです。
「コインがレールなどに接触した音」を作成しましたが、この音を鳴らすには、コインの接触を検知する必要があります。
コインの接触を検知するには、剛体が接触した時のイベントを受け取るイベントリスナーを使います。
接触イベントを受け取るメソッドを定義
class OneCoinGame : public cocos2d::Layer { public: // 接触イベントの検知 bool onContactBegin(cocos2d::PhysicsContact& contact); };
タッチイベントと同様に、接触イベント用のリスナーを生成して定義したonContactBeginメソッドを設定、イベントリスナーへ登録することで準備完了です。
// 衝突イベントの検知 auto contactListener = EventListenerPhysicsContact::create(); contactListener->onContactBegin = CC_CALLBACK_1(OneCoinGame::onContactBegin, this); this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(contactListener, this);
これで剛体同士が接触する時のイベントを受け取ることができるようになりますが、接触イベントはまだ発生する設定になっていません。
接触イベントを発生させるには、各剛体に対してsetContactTestBitmask(int bitmask)でマスクの設定を行う必要があります。この値はデフォルトで0のため、0以外に設定しないと接触イベントが発生せず、イベントを受け取ることができません。
デフォルトが0なのは剛体の接触イベントは大量に検知してしまうため、必要なものだけ検知してパフォーマンスを維持するためです。
筐体内部
wall->getPhysicsBody()->setContactTestBitmask(0x00000001); // 接触イベントの検知マスク
レール
rail->getPhysicsBody()->setContactTestBitmask(0x00000001); // 接触イベントの検知マスク
コイン
coinPhysics->setContactTestBitmask(0x00000001); // 接触イベントの検知マスク coinPhysics->setName("coin");
接触イベントのビットマスク設定をそれぞれ設定しました。コインは接触時に識別したいのでsetName()で名前を付けて置きます。
ビットマスクの設定ができたら、onContactBegin(PhysicsContact& contact)で衝突を検知できるので、効果音を鳴らすコードを実装します。
// 衝突が発生した際に呼ばれるイベント bool OneCoinGame::onContactBegin(PhysicsContact& contact) { // 衝突した2つの物体について取得 auto bodyA = contact.getShapeA()->getBody(); auto bodyB = contact.getShapeB()->getBody(); // 衝突した物体がコインかどうかを調べる if ("coin" == bodyA->getName() || "coin" == bodyB->getName()) { // コインなら効果音を鳴らす CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("se/coin_contact.wav"); } return true; }
onContactBegin(PhysicsContact& contact)は、2つの物体が衝突した際に呼ばれます。
引数contactにより、衝突した2つの物体を取得することができますが、getShapeA()かgetShapeB()がコインかどうかであることと、コインが接触した場合どちらがコインかがわかりません。コインに名前を付けたのはこの判断をするためです。
名前を取得してコインなら効果音を鳴らすことで、コイン接触音を適切なタイミングで鳴らすことができるようになります。
※音が小さいかもしれません。
ソースコードはこちら
次回はユーザビリティを向上のためにスプライトアクションを実装します。
]]>
Cocos2d-x 10円ゲーム講座 第9回、はずれ穴とゴールの処理を追加します。
今回ははずれ穴とゴールの処理です。
現実の10円ゲームのはずれ穴は奥行きを持っていて、奥に10円玉が落ちることで10円ゲームの失敗となります。Cocos2d-xの物理エンジンで奥行きをもたせることは出来ないので、穴にコインが入ったかどうかをコインの位置で判定することにします。ゴールも同様です。
コインの位置を監視するには、Cocos2d-xのスケジュール機能を使うとフレーム毎にコインの位置を監視することができます。コインの位置の監視によってはずれ穴とゴール位置に来た事を検知します。
class OneCoinGame : public cocos2d::Layer { public: // スケジュール処理 void update(float delta); };
init()内でthis->scheduleUpdate();を実行します。
// スケジュール処理の開始 this->scheduleUpdate();
これでフレーム毎にupdate(float delta)が呼ばれるようになります。
続いてはupdate(float delta)の中身を実装していきます。
// 穴の矩形領域 Rect coinHole[] = { Rect(245, 430, 30, 30), Rect(67, 335, 30, 30), Rect(213, 335, 30, 30), Rect(260, 258, 30, 30), Rect(182, 136, 30, 30), Rect(61, 67, 30, 30), }; // ゴールの矩形領域 Rect goalHole = { Rect(117, 67, 30, 30), }; // フレーム毎の処理 void OneCoinGame::update(float delta) { ///////////////////////////// // コインがはずれ穴とゴールに入っている場合の処理 // コインの速度の取得 auto velocity = _coin->getPhysicsBody()->getVelocity(); float speed = velocity.getDistance(Vec2::ZERO); // コインの速度が一定速度以下かつコインの物理が有効でかどうか if (speed < 3.0f && _coin->getPhysicsBody()->isEnabled()) { // はずれ穴に入っているかどうかの確認 for (int i = 0; i < 6; i++) { if (coinHole[i].containsPoint(_coin->getPosition())) { // コインがはずれ穴に落ちた時の処理 // コインの物理動作を停止し、非表示にする _coin->getPhysicsBody()->setEnabled(false); _coin->setVisible(false); } } // ゴールに入っているかどうかの確認 if (goalHole.containsPoint(_coin->getPosition())) { // コインがゴールに到達した時の処理 // コインの物理動作を停止し、非表示にする _coin->getPhysicsBody()->setEnabled(false); _coin->setVisible(false); } } }
穴に触れた瞬間にコインが消えると、少し理不尽な感じが出るので、コインの速度を取得しコインの速度が遅い時だけ穴に落ちたことにします。これは実際の10円ゲームでは奥行きがあることを、2Dゲームで擬似的に表現するための処理です。コインの速度が速い時は穴を通り抜ける方が理不尽に感じないためです。
はずれ穴、ゴールどちらに到達してもコインは無くなることになるため、コインの物理動作を停止し非表示にします。
_coin->getPhysicsBody()->setEnabled(false); _coin->setVisible(false);
ゴールの場合はクリア報酬として景品を出します。ついでなので、この景品も剛体にして物理を適用してみます。
gift.png
景品。おもちゃ箱っぽいイメージです。
景品口の物理領域を実装。
// 景品口の物理領域 Vec2 vec[4] = { Vec2(202, 65), // 左上 Vec2(202, 23), // 左下 Vec2(301, 23), // 右下 Vec2(301, 65), // 右上 }; auto wall = Node::create(); wall->setPhysicsBody(PhysicsBody::createEdgeChain(vec, 4, PhysicsMaterial(1.0f, 0.5f, 0.2f))); wall->getPhysicsBody()->setDynamic(false); this->addChild(wall);
筐体と同じように実装します。
続いて景品を出す処理をゴールに到達した際に追加します。
// 景品を出す処理 auto gift = Sprite::create("gift.png"); gift->setPosition(Vec2(250.0f, 74.0f)); this->addChild(gift, 1); // 景品の物理設定 auto giftPhysics = PhysicsBody::createBox(Size(26.0f, 14.0f), PhysicsMaterial(0.6f, 0.3f, 6.0f)); giftPhysics->setMass(1.0f); // 重さ giftPhysics->setMoment(10.0f); // モーメント(大きいほど回転しにくい) gift->setPhysicsBody(giftPhysics);
景品口の物理設定を作り、少し上から景品を出現させて落とします。
これを実行してみると問題があることがわかります。
景品が筐体の衝突領域に接触し、景品口に出てくれません。
ゲームで物理物体を扱っていると、ある物体とは衝突して欲しいけど、こっちの物体とは衝突してほしくない、といったことが出てきます。その解決方法として、Cocos2d-xでは物体にカテゴリーやマスクが設定できます。
今回は景品をコインや筐体と接触しないようにカテゴリ設定を行います。
剛体をカテゴライズするにはsetCategoryBitmask()を使用します。また、setContactTestBitmask()で剛体の衝突するカテゴリを指定できます。
コインとレールはそれぞれ衝突するようにカテゴリを指定します。
coinPhysics->setCategoryBitmask(0x00000001); // 物体のカテゴリ coinPhysics->setCollisionBitmask(0x00000001); // 接触する物体のカテゴリを指定
wall->getPhysicsBody()->setCategoryBitmask(0x00000001); // 物体のカテゴリ wall->getPhysicsBody()->setCollisionBitmask(0x00000001); // 接触する物体のカテゴリを指定
rail->getPhysicsBody()->setCategoryBitmask(0x00000001); // 物体のカテゴリ rail->getPhysicsBody()->setCollisionBitmask(0x00000001); // 接触する物体のカテゴリを指定
コイン、レール、筐体のカテゴリを1(0x00000001)に設定し、接触する物体のカテゴリを同じく1(0x00000001)に設定します。これでコインとレール、筐体はそれぞれ接触することになります。動作としては今まで通りです。
次に景品の設定です。景品と景品口は接触して欲しいが、コインやレールとは接触して欲しくないので、コインやレールとは別のカテゴリである2(0x00000010)を設定します。
wall->getPhysicsBody()->setCategoryBitmask(0x00000010); // 物体のカテゴリ wall->getPhysicsBody()->setCollisionBitmask(0x00000010); // 接触する物体のカテゴリを指定
giftPhysics->setCategoryBitmask(0x00000010); // 物体のカテゴリ giftPhysics->setCollisionBitmask(0x00000010); // 接触する物体のカテゴリを指定
これで物体の衝突を制御できました。
ことが確認できます。
ゴールに導くのが難しくて動画撮れませんでした(笑)。内部ではテスト用のコインを作成して確認しています。
ソースコードはこちら
次回は効果音を鳴らすです。
]]>Cocos2d-x 10円ゲーム講座 第8回、レバー操作とコインをはじく処理を追加します。
今回は10円ゲームのキモ、レバー操作の実装です。
レバーを引いて離すと、引いた分に応じて力強くコインをはじきます。
ドラッグ処理でレバーのはじく強さを実現します。
レバーをタッチすることでレバー操作を開始、ドラッグすることでレバーの力強さを決定します。タッチを離した時にコインをはじく処理を行います。ドラッグ中はレバーをアニメーションさせます。
使用する画像はこちら。
lever.png
lever_background.png
レバーは、スティック部分とレバー背景の穴の画像を使います。
画像が用意できたら実装していきます。レバーははじく際にアニメーションさせるので、メンバ変数にします。その他タッチした際に使用する変数も定義しておきます。
class OneCoinGame : public cocos2d::Layer { private: cocos2d::Sprite *_lever[4]; // レバー // レバータッチ処理用 bool _isTouchLever; // レバーのタッチ状態 int _touchLeverNumber; // レバーのナンバー cocos2d::Vec2 _touchlocation; // タッチしたポイント };
// レバー関連初期処理 _isTouchLever = false; _touchLeverNumber = 0; _touchlocation = Vec2(0.0f, 0.0f);
特に変わったことはしていません。
レバーの表示処理
// レバー位置設定 Vec2 vec[4] = { Vec2(45, 400), Vec2(315, 321), Vec2(45, 255), Vec2(315, 160), }; for (int i = 0; i < 4; i++) { // レバーの背景 auto leverBackground = Sprite::create("lever_background.png"); leverBackground->setPosition(vec[i]); leverBackground->setAnchorPoint(Vec2(0.5f, 0.28f)); this->addChild(leverBackground, 2); // レバー部分 _lever[i] = Sprite::create("lever.png"); _lever[i]->setPosition(vec[i]); _lever[i]->setAnchorPoint(Vec2(0.5f, 0.28f)); this->addChild(_lever[i], 2); }
いつも通りSpriteクラスを使って画像を描画しているだけですが、1点だけポイントがあります。それがsetAnchorPoint()です。setAnchorPoint()は画像の基準となる点で、回転させる際に軸となる点にもなります。
AnchorPointを軸に回転する
レバーの棒はレバー穴を軸に回転して欲しいので、Vec2(0.5f, 0.28f)を設定しています。
レバーにタッチ処理を使うので、タッチイベントを受け取れるようにします。
タッチイベントには、onTouchBegan()、onTouchMoved()、onTouchEnded()があり、それぞれタッチ開始時、移動時、終了時に呼ばれます。
実際の操作と対応しているので、イメージしやすく直感的に理解できると思います。
シーンでタッチイベントを受け取るには、タッチイベントのクラスメソッドを定義します。
class OneCoinGame : public cocos2d::Layer { public: // タッチ処理 bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event); void onTouchMoved(cocos2d::Touch* touch, cocos2d::Event* event); void onTouchEnded(cocos2d::Touch* touch, cocos2d::Event* event); };
タッチイベントを受け取るためのリスナーイベントを登録します。
// タッチイベントを使用するための初期処理 // タッチに関するイベントリスナーを生成 auto listener = EventListenerTouchOneByOne::create(); listener->onTouchBegan = CC_CALLBACK_2(OneCoinGame::onTouchBegan, this); listener->onTouchMoved = CC_CALLBACK_2(OneCoinGame::onTouchMoved, this); listener->onTouchEnded = CC_CALLBACK_2(OneCoinGame::onTouchEnded, this); // タッチイベントをリスナーに登録 auto dip = Director::getInstance()->getEventDispatcher(); dip->addEventListenerWithSceneGraphPriority(listener, this);
これでこのシーンでタッチイベントを受け取れるようになりました。
レバーのタッチ処理を実装します。
// タッチ開始時の処理 bool OneCoinGame::onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event) { auto location = touch->getLocation(); bool isTouchLever = false; // タッチしたレバーの調査 for (int i = 0; i < 4; i++) { Rect rect = _lever[i]->getBoundingBox(); // タッチした点がレバー領域内かどうか if (rect.containsPoint(location)){ isTouchLever = true; // タッチしたレバーを記憶 _touchLeverNumber = i; // タッチした位置を記憶 _touchlocation = location; } } // レバーをタッチした場合、レバーのタッチ状態を設定する if (isTouchLever == true) { _isTouchLever = true; } else { _isTouchLever = false; } // return true; } // タッチ移動時の処理 void OneCoinGame::onTouchMoved(cocos2d::Touch* touch, cocos2d::Event* event) { auto location = touch->getLocation(); if (_isTouchLever) { float d = _touchlocation.getDistance(location); if (d >= 100.0f) { d = 100.0f; } // レバースプライトの更新 if (_touchLeverNumber == 0 || _touchLeverNumber == 2) { _lever[_touchLeverNumber]->setRotation(-90.0f * (d / 100.0f)); // 0 - 90 max } else { _lever[_touchLeverNumber]->setRotation(90.0f * (d / 100.0f)); // 0 - 90 max } } } // タッチ終了時の処理 void OneCoinGame::onTouchEnded(cocos2d::Touch* touch, cocos2d::Event* event) { auto location = touch->getLocation(); if (_isTouchLever) { float d = _touchlocation.getDistance(location); if (d >= 100.0f) { d = 100.0f; } // レバースプライトを初期位置に戻す _lever[_touchLeverNumber]->setRotation(0); // レバーのタッチ状態を解除 _isTouchLever = false; } }
レバーをタッチしたかどうかを判定するには、レバーの矩形とタッチした点の衝突判定の結果を調査します。
onTouchBegan()がタッチ開始時に呼ばれるイベントのため、onTouchBegan()でレバーがタッチされたかどうかを判定しています。
touch->getLocation()でタッチした点を取得できます。レバー画像側はSpriteクラスのgetBoundingBox()で画像の矩形領域が取得できるため、この矩形領域とタッチした点で衝突判定を行います。矩形領域と点の衝突判定には、矩形領域のクラスであるRectクラスのメソッドcontainsPoint()で判定することができます。
onTouchMoved()はドラッグ動作に対してのイベントです。onTouchMoved()でレバーのアニメーションを行い、レバーの引く処理を視覚的に表示しています。タッチを開始した点から、距離が離れるほどレバーを倒す(回転)する処理を入れます。
画像の回転処理は、SpriteクラスのメソッドsetRotation()で回転できます。
onTouchEnded()はタッチ終了時のイベントです。ここではonTouchMoved()で回転させたレバーを元に戻しています。
静止画では解りづらいですが、レバーがドラッグ処理で傾いています。
タッチイベントを利用してレバー操作が実現できました。次はタッチを離した際にコインをはじく処理を実装します。
class OneCoinGame : public cocos2d::Layer { public: // コインに力を加える void applyForceCoin(float vecx, float power); };
// コインに力学的な力を加える void OneCoinGame::applyForceCoin(float vecx, float power) { _coin->getPhysicsBody()->applyImpulse(Vec2(400.0f * power * vecx, 0.0f)); }
appliImpulse(Vec2 Impulse)で剛体(コイン)に対して瞬間的に力を加えることができます。
タッチを離した際に呼ばれるoneTouchEnded()にコインに力を加える処理を追加します。
// レバーの接触部分設定 Rect leverArea[4] = { Rect(45, 410, 30, 30), Rect(285, 330, 30, 30), Rect(45, 270, 30, 30), Rect(285, 170, 30, 30), }; // タッチ終了時の処理 void OneCoinGame::onTouchEnded(cocos2d::Touch* touch, cocos2d::Event* event) { auto location = touch->getLocation(); if (_isTouchLever) { float d = _touchlocation.getDistance(location); if (d >= 100.0f) { d = 100.0f; } // レバースプライトを初期位置に戻す _lever[_touchLeverNumber]->setRotation(0); // コインがレバー範囲にあるか確認 if (leverArea[_touchLeverNumber].containsPoint(_coin->getPosition())) { // 範囲内ならコインに力を加える if (_touchLeverNumber == 0 || _touchLeverNumber == 2) { this->applyForceCoin(1.0f, (d / 100.0f)); } else { this->applyForceCoin(-1.0f, (d / 100.0f)); } } // レバーのタッチ状態を解除 _isTouchLever = false; } }
レバーを動作させた際に、コインに逆方向への力を加えて動かします。
実際の10円ゲームは、レバーとハンマー機構によりコインを叩くわけですが、今回は擬似的に実装しています。ハンマー機構を作っても面白いですが、物理で動く部分が増えるとコインをはじく結果への不安定さが増えることになり、ゲームとして面白くなるか、運ゲーになるか微妙なところなので擬似的な方を採用しました。
レバーの領域を表示しています。この範囲にコインがあれば、レバー処理でコインに力を加える処理が働きます。
ここまでのソースコードはこちら
次回ははずれ穴とゴールの処理の追加です。
]]>Cocos2d-x 10円ゲーム講座 第7回、10円ゲーム筐体の物理設定を追加してコインと衝突させます。
前回はコインが重力の影響を受けて落下するようになりました。しかし他の物理設定は何もしていないため、筐体などにぶつかることなく落下し、ウィンドウから出ていくだけです。
今回は筐体の形に合わせて、物理設定を追加していきます。
筐体のハコの形に物理領域を設定して、コインと衝突させてみます。
コインの剛体はPhysicsBody::createCircle()を使って円形に設定しましたが、筐体のハコはPhysicsBody::createEdgeChain()を使って線の連続で設定することにします。
// 10円ゲーム筐体内部の領域設定 { Vec2 vec[4] = { Vec2(60, 600), Vec2(60, 60), Vec2(300, 60), Vec2(300, 600), }; auto wall = Node::create(); wall->setPhysicsBody(PhysicsBody::createEdgeChain(vec, 4, PhysicsMaterial(1.0f, 0.3f, 0.5f))); wall->getPhysicsBody()->setDynamic(false); this->addChild(wall); }
今回のポイントはsetDynamic(false)です。setDynamic()にfalseを指定することで、その物体は静止物体となり、重力の影響や物理の影響を受けません。この設定はデフォルトでtrueです。
コイン投入口をタッチしてコインが落下、コインがハコとぶつかって静止することが確認できます。
今回は10円ゲームで一番重要なレールの物理設定です。
レールで名前が合っているかどうかはわかりませんが、とにかくコインが転がる部分を便宜上レールということにします。
今まではコインと筐体のハコの衝突領域を設定していたため、特に考えることもなく円形と矩形で設定すれば大丈夫でした。
しかし、レールなどのちょっとした形の衝突領域はどう設定するか考える必要があります。これは主に3つ程考え方があります。
1.は理想。
物理エンジンの演算回数や、衝突領域の調整が問題点となります。
2.は矩形で近似させる方法。
2Dゲームでは角の部分の衝突判定に違和感を覚えることが多いので、右図のように小さめの矩形にすることも多いです。ただし、今回の場合はコインがレールにめり込んでしまうのが考えものです。
3.の独自に衝突領域を設定する方法
例えば上図の赤いラインのような形。10円ゲームの性質上、レールの下部分に触れることはあまり無い上に、下部分にはめり込んでも問題なさそうなので、今回はこの方法で設定しようと想います。
前回の筐体のハコと同じく、PhysicsBody::createEdgeChainで設定していきます。ただし、座標を打ち込んで行くのは非常に面倒なので、一工夫します。
この筐体のレールはPhotoshopのパスで作っているので、そのパスの座標を抽出してcsv化します(Photoshopは左上原点なので、Y座標は反転させる必要があります)。このcsvを読み込んでPhysicsBody::createEdgeChain()で設定します。
#include <vector> #include <fstream> bool OneCoinGame::init() { /*--- 略 ---*/ // railの物理設定 { std::vector<Vec2> railData; std::string filePath = FileUtils::getInstance()->fullPathForFilename("raildata.csv"); std::ifstream ifs(filePath.c_str()); std::string sheetLine; // ファイルが存在しない場合 if (!ifs) { CCLOG("raildata.csv file not found"); } // 1行ずつ読み込む while (getline(ifs, sheetLine)) { std::istringstream sheetStream(sheetLine); std::string temp; std::vector<std::string> cellData; // 1行分の文字列のリスト while (getline(sheetStream, temp, ',')) { // 1セル分の文字列をリストに追加する cellData.push_back(temp); } auto pos = Vec2(std::stoi(cellData[0]), std::stoi(cellData[1])); if (0 == std::stoi(cellData[2])) { // 終端以外 railData.push_back(pos); } else { // 終端 railData.push_back(pos); auto rail = Node::create(); rail->setPhysicsBody(PhysicsBody::createEdgeChain(railData.data(), railData.size(), PhysicsMaterial(1.0f, 0.5f, 0.2f))); rail->getPhysicsBody()->setDynamic(false); this->addChild(rail); // vectorをクリアする railData.clear(); railData.shrink_to_fit(); } } } }
raildata.csvについてはこちらに置いています。
Cocos2d-xのファイルの読み込みはFileUtilsを使います。プラットフォームの面倒なファイルのやりとりを簡単にしてくれます。特にAndroidやiOSで嬉しい機能です。
ファイルの読み込み後はセル毎に読み込み、筐体と同じように物理設定を行っていきます。
コイン投入口ボタンを押してコインを落下させると、コインがレールにぶつかって転がっていくことが確認できます。
ここまでのソースコードはこちら
次回はレバー操作とコインをはじく処理の追加です。
]]>Cocos2d-x 10円ゲーム講座 第6回、物理エンジンを使用してコインを動かします。
前回、コイン投入口ボタンを押すとコインが表示できるようになりました。
しかし、コインはまだ静止したまま動きません。理由はもちろん、コインの表示コードを実装しただけでコインの動きについては何一つ実装していないからです。
そこで今回は、コインが重力に従って落下したり、自然に動くように物理エンジンの設定を行います。
コインを物理物体として動作させる前提として、現在のシーンに物理エンジンを使用できるようにします。
現在のシーンを生成する時に、Scene::createWithPhysics()を実行することで、そのシーンで物理エンジンを使用できるようになります。
Scene* OneCoinGame::createScene() { // シーンを物理エンジンが使用できるシーンにする auto scene = Scene::createWithPhysics(); /*--- 略 ---*/ // 物理エンジン用設定 auto world = scene->getPhysicsWorld(); // 物理シーンの計算速度 world->setSpeed(3.0f); // 物理シーンのサブステップ数 world->setSubsteps(5); // デバッグ用の領域を表示 world->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
OneCoinGame::createScene()内のScene::create();をScene::createWithPhysics();に置き換えます。これだけで物理エンジンが使用できるようになりました。合わせて物理エンジンの設定を行っておきます。
setSpeed(float speed)は物理シミュレーションの実行速度の設定です。デフォルトは1.0で3.0にすると3倍速で動作することになります。デフォルトだと少し遅いので、3.0を設定しています。
setSubsteps(int steps)は物理シミュレーションのサブステップ数を設定します。サブステップ数は物理シミュレーションが次のフレームまでに計算する回数で、デフォルトは1ですが、大きくすることで物理シミュレーションの精度が大きく向上します。今回はコインの動きにリアリティを持たせるために、サブステップ数を5としました。
setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL)を実行すると、剛体などの領域が表示されます。デバッグに便利な機能です。
Cocos2d-xの物理エンジンは剛体力学です。まずはコインという剛体に対して設定したいマテリアルを設定します。
// マテリアルの設定 PhysicsMaterial coinMaterial; coinMaterial.density = 0.1f; // 密度 coinMaterial.restitution = 0.5f; // 反発係数 coinMaterial.friction = 0.4f; // 摩擦係数
マテリアルの設定項目は3つあります。
densityは密度。単位体積あたりの質量を設定します。衝突した際の運動量に影響します。
restitutionは反発係数。衝突した際の跳ね返り具合を設定します。スーパーボールで0.9程度、野球のボール(硬球)で0.4程度という数値です。
frictionは摩擦係数。0にすると転がらずに氷の上を滑っているようになります。コインとしてある程度転がって欲しいので0.4を設定しました。
マテリアルを決定したら、次は剛体の設定です。コインは円形なので、PhysicsBody::createCircle()を使って、円形の衝突領域を設定します。
// 円形の物理設定 auto coinPhysics = PhysicsBody::createCircle(12.5f, coinMaterial); coinPhysics->setMass(1.0); // 重さ coinPhysics->setMoment(1.0f); // モーメント。回転させるための力。大きいほど回転しにくい // コインに剛体を関連付ける _coin->setPhysicsBody(coinPhysics); // コインを停止し、非表示にしておく _coin->getPhysicsBody()->setEnabled(false); _coin->setVisible(false);
PhysicsBody::createCircle()の第一引数は半径です。コインが直径25ピクセルなので、12.5を設定。第二引数のマテリアルは上で設定したマテリアルです。
物理の設定時に、重さとモーメントも設定します。モーメントとは回転させるための数値で、大きいほど回転しにくくなります。どちらもデフォルトではPHYSICS_INFINITYという最大値が設定されるため注意が必要です。
Spriteクラスの_coinにsetPhysicsBody(coinPhysics);と設定することで、コインが剛体となり物理的な影響を受けるようになります。
コインを剛体にできたら、コイン投入口ボタンを押すまで動作を停止するためにsetEnabled(false)を設定して動作を停止しておきます。setVisible(false)はスプライトを非表示にするだけで、物理シミュレーションが停止しないからです。
続いてコイン投入口ボタンを押した時の動作を変更します。
// コイン投入ボタン void OneCoinGame::insertCoinCallback(cocos2d::Ref* pSender) { Size visibleSize = Director::getInstance()->getVisibleSize(); // コインに働いている物理の力をリセットする _coin->getPhysicsBody()->resetForces(); _coin->getPhysicsBody()->setVelocity(Vec2(0.0f, 0.0f)); // 位置のリセット(投入口ボタンにセットする) _coin->setPosition(Vec2(visibleSize.width*0.7, visibleSize.height*0.9)); // コインの物理を有効にして表示する _coin->getPhysicsBody()->setEnabled(true); _coin->setVisible(true); }
resetForces()やsetVelocity()で、コインに働いている物理の力をリセットします。
コイン投入口ボタンを押すと、コインが重力に従って落下していきます。
筐体の物理設定を一切やっていないので、ただ落下していくだけですね。
ここまでのソースコードはこちら
]]>Cocos2d-x 10円ゲーム講座 第5回、画像(スプライト)を表示してみます。
4つ用意しました。
背景画像とフレームの画像です。
コイン投入口。コインを投入するボタンの役も兼ねます
コイン。
これらの画像をプロジェクト内のResourcesフォルダに配置します。
Resourceフォルダ + background.png + coin.png + coin_entry.png + frame.png
画像(スプライト)の表示にはSpriteクラスを利用します。
// 筐体背景画像の描画 auto background = Sprite::create("background.png"); // Resources内の画像を指定 background->setPosition(Vec2(180.0f, 320.0f)); // 表示位置の設定 this->addChild(background); // 現在のシーンに加える
1行目はSpriteクラスの生成と共に画像のファイル名を指定しています。Cocos2d-xプロジェクトはResourcesフォルダにパスが通っているため、Resourcesフォルダにbackground.pngを置くことですぐさまスプライトを使用できます。
setPositionは位置の設定です。Cocos2d-xでは左下原点なので、上のソースコードでは左から180ピクセル、下から320ピクセルの位置に画像を配置します。
Spriteクラスを定義しただけでは画像は表示されません、現在のシーン(this)に対してスプライトを子として追加し、シーンに結びつけることで表示されるようになります。3行目のaddChildはそのためのコードになります。
このコードの実装はinit()で行います。init()はシーン遷移時に1度だけ呼ばれるので、初期化処理を実装する所に最適です。
背景画像が描画できました。
スプライトの表示がたった数行のコードでできるところが、Cocos2d-xなどゲーム開発プラットフォームの大きな利点でもあります。
背景画像は表示できました。これから他の画像を表示していきますが、その前にレイヤーの概念について説明しておきます。
レイヤーとは表示順序のことです。今回配置した背景画像の裏にコインが表示されても意味がありません。そこで、用意した画像の表示順序を決めておく必要があります。
10円ゲームでは、筐体の画像は最下層になり、その上にコイン、コインの上に筐体のフレーム、筐体のフレームの上にコイン投入口やレバーがあって欲しいので、
上記のような画像の表示順序が良いことがわかります。
このレイヤーの順序をコードで反映させていきます。レイヤーの順序は、前回の画像の表示でも使用したaddChild()メソッドの第二引数がレイヤー順序に相当します。
void Node::addChild(Node *child, int zOrder)
第二引数のint zOrderは、数値が高くなるほど上位層のレイヤーとなります。
addChild()はCocos2d-xのほとんどのクラス(Nodeを継承している)が持つため、孫Nodeなども簡単に作ることができますが、孫のレイヤー順序は、子のレイヤー順序を越えて表示されることがありません。あくまで孫同士のレイヤー順序の関係を指定するものです。
// 筐体画像(フレーム)の描画 auto frame = Sprite::create("frame.png"); frame->setPosition(Vec2(visibleSize.width*0.5f, visibleSize.height*0.5f)); this->addChild(frame, 2);
addChild()の第二引数を2にしています。
コイン投入口はボタンにして、コイン投入口ボタンをタッチした際にはコインを表示するようにしてみます。
コインは後々位置を移動させたりするので、OneCoinGameクラスのメンバ変数として定義しておきます。ついでにコイン投入口のボタンを押した時のコールバック関数も定義しておきます。
class OneCoinGame : public cocos2d::Layer { // コイン投入口ボタン押下時のコールバック void insertCoinCallback(cocos2d::Ref* pSender); private: cocos2d::Sprite *_coin; // コイン };
コインの画像表示はこれまで通りinit()内でSpriteクラスを使って描画します。
// コインの描画 _coin = Sprite::create("coin.png"); _coin->setPosition(Vec2(visibleSize.width*0.5, visibleSize.height*0.5)); this->addChild(_coin, 1); // コインを非表示にしておく _coin->setVisible(false);
setVisible(bool bVisible)でスプライトの表示と非表示を切り替えることができるため、最初は非表示にしておきます。
続いてコイン投入口ボタンの追加。これもinit()内で実装します。
// コイン投入口(ボタン) auto insertCoinItem = MenuItemImage::create( "coin_entry.png", "coin_entry.png", CC_CALLBACK_1(OneCoinGame::insertCoinCallback, this)); insertCoinItem->setPosition(Vec2(visibleSize.width*0.7, visibleSize.height*0.9)); auto menu = Menu::create(insertCoinItem, NULL); menu->setPosition(Vec2::ZERO); this->addChild(menu, 2);
Cocos2d-xのボタンは、メニューアイテムとしてボタンの元となるパーツを作成し、メニューに追加することで実装できます。
画像のメニューアイテムには、MenuItemImageクラスを使用します。今回は通常時と選択時のボタン画像を同じにしています。コールバック関数の指定には、Cocos2d-xのマクロであるCC_CALLBACK_1を利用して、定義した関数を指定します。
ボタンを押した際に呼ばれるコールバック関数の中身を実装します。
// コイン投入ボタン void OneCoinGame::insertCoinCallback(cocos2d::Ref* pSender) { // コインの表示 _coin->setVisible(true); }
先ほどのsetVisible()を使ってコインを表示します。
ビルドして実行するとコイン投入口が表示できました。またコイン投入口を押すとコインが表示されます。ただしコインはまだ動きません。
ソースコードはこちら
MenuItemImage::create()はいくつか引数のパターンがあり、無効時のボタンイメージなども指定できます。
メニューアイテムにはMenuItemLabelクラスを使用することで、文字列をボタンにすることができます。
ラベルボタンのコード例
// コイン投入口(ボタン) auto insertCoinItem = MenuItemLabel::create( Label::create("コイン投入口", "fonts/Marker Felt.ttf", 12), CC_CALLBACK_1(OneCoinGame::insertCoinCallback, this)); insertCoinItem->setPosition(Vec2(visibleSize.width*0.5, visibleSize.height*0.5)); auto menu = Menu::create(insertCoinItem, NULL); menu->setPosition(Vec2::ZERO); this->addChild(menu, 2);
]]>
Cocos2d-x 10円ゲーム講座 第4回、10円ゲームの進め方と画面の縦長使用についてです。
10円ゲームの遊び方をふまえて、今回のゲームの遊び方を決めてみます。
10円ゲームは大体こんな感じだと思います。
筐体としてこんな画像を用意しました。
コイン投入口ボタンを押すことで、コイン投入口からコインを投下、重力とレールに従って左に転がっていきます。そこからレバーで4回はじき、下部の橙色のゴールに入ればゴールとして、景品を出力することにします。
10円玉ではなくコインにしましたが、この画像を元に10円ゲームを作っていきます。
コインゲームはゲーム的に縦長の方が良いと思います。用意した筐体画像も縦長なので、画面を縦長にします。
Cocos2d-xではデザイン解像度と画面解像度があります。
static cocos2d::Size designResolutionSize = cocos2d::Size(480, 320);
designResolutionSizeがデザイン解像度の定義。Cocos2d-x内で指定する座標系は、全てこの座標を元にした値になります。この例では180, 320が中心位置ということになり、画像表示位置を180,320にすると中心に貼り付けることになるということです。(AnchorPointなどによって中心位置の取り扱い方を変えることはできます)
static cocos2d::Size smallResolutionSize = cocos2d::Size(480, 320); static cocos2d::Size mediumResolutionSize = cocos2d::Size(1024, 768); static cocos2d::Size largeResolutionSize = cocos2d::Size(2048, 1536);
こちらha
画面解像度の定義。スマートフォンなどの解像度に合わせて、画面のスケーリング(拡縮)を決定します。
今回は縦長9:16の360×640を解像度として設定します。9:16なのでスマフォでもピッタリになることが多いでしょう。
static cocos2d::Size designResolutionSize = cocos2d::Size(360, 640); static cocos2d::Size smallResolutionSize = cocos2d::Size(360, 640); static cocos2d::Size mediumResolutionSize = cocos2d::Size(640, 1136); static cocos2d::Size largeResolutionSize = cocos2d::Size(1080, 1920);
デザイン解像度と画面解像度を360×640とその倍率に合わせて変更します。
実行結果。縦長になりました。
ソースコードはこちら
現在のファイル構成は上記のようになっています。
初期プロジェクトでは、アプリケーション起動後、各種初期化を行ってからHelloWorldシーンに遷移してCocos2d-xロゴを表示するのが流れになります。
bool AppDelegate::applicationDidFinishLaunching() { // ... 省略 ... // // create a scene. it's an autorelease object auto scene = HelloWorld::createScene(); // run director->runWithScene(scene); }
HelloWorld::createScne()はHelloWorldScene.cppで定義されており、プロジェクト生成時に作られるCocos2dのロゴを表示するサンプルシーンになります。
そのまま利用しても良いですが、HelloWorldという名前がいまいちなので変更して使用します。
HelloWorldScene.hとHelloWorldScene.cppを、それぞれOneCoinGameScene.hとOneCoinGameScene.cppに変更します。
変更に合わせて、AppDelegate.cppのapplicationDidFinishLaunching()で指定するシーンも変更します。
// create a scene. it's an autorelease object auto scene = OneCoinGame::createScene();
OneCoinGameScene.hとOneCoinGameScene.cppのコードも載せておきます。
#ifndef __ONECOINGAME_SCENE_H__ #define __ONECOINGAME_SCENE_H__ #include "cocos2d.h" class OneCoinGame : public cocos2d::Layer { public: static cocos2d::Scene* createScene(); virtual bool init(); // implement the "static create()" method manually CREATE_FUNC(OneCoinGame); }; #endif // __ONECOINGAME_SCENE_H__
元々のHelloWorldScene.cppにあったサンプル表示コードは削除して、何も表示しないようにします。
#include "OneCoinGameScene.h" USING_NS_CC; Scene* OneCoinGame::createScene() { // 'scene' is an autorelease object auto scene = Scene::create(); // 'layer' is an autorelease object auto layer = OneCoinGame::create(); // add layer as a child to scene scene->addChild(layer); // return the scene return scene; } // on "init" you need to initialize your instance bool OneCoinGame::init() { ////////////////////////////// // 1. super init first if ( !Layer::init() ) { return false; } Size visibleSize = Director::getInstance()->getVisibleSize(); Vec2 origin = Director::getInstance()->getVisibleOrigin(); return true; }
実行結果。サンプルコードの内容を空にしたので、実行すると真っ黒です。
ソースコードはこちら
次回は画像(スプライト)を表示します。
]]>