こんにちは。 shakezoomerです。
前回、いまから始めるゲーム開発入門!cocos2d-jsでHello Worldを表示してみようという記事でcocos2d-jsを使ってHello Worldするところまで紹介しました。
今回は、cocos2d-jsを使って実際にカンタンなゲームを作ってみようと思います。
今まで全くゲームを作ったことが無い、あるいはjavascriptをまだ勉強中、といった初心者の方でも、とにかく動くものをまずは作ってみる!ということができればと思います。
完成品は、こちらのようなものです。ソースコードはGithubにあげておいたので、必要に応じて参考にしてください。
ではいってみましょう。
index.html、main.jsの追加
前回の記事の終わり(あるいはcocos2d-jsをDLしてきて、zipを展開した状態も同じです)では以下の様なファイル構成になっているかと思います。
前回は、HelloWorld.htmlを編集していましたが、今回は新しくindex.htmlを作りましょう。
<!DOCTYPE html> <html> <head> <title>Go!Go!Mr.Snake!</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <script type="text/javascript" src="cocos2d-js-v3.10-lite.js" charset="UTF-8"></script> <canvas id="gameCanvas" width="640" height="1136"></canvas> <script type="text/javascript" src="main.js"></script> </body>
内容はほとんどHelloWorld.htmlと同じです。変更点は、
- titleを変えた(ゲームの名前にしました)
- cocos2d-js-v3.10-lite.jsを読み込むタイミングを変えた
- main.jsというファイルを読み込むようにした
- canvasのサイズを変更した
の4つくらいでしょうか。
cocos2d-js-v3.10-lite.jsの読み込みタイミングを変えたのは、iPhoneからアクセスしたときに何も表示されないことがあるので、その対策です。
また、新しくmain.jsというファイルが追加されていますね。前回まではHelloWorld.htmlに直接javascriptのコードを書いていましたが、別のjsファイルに分離することですっきりさせています。
canvasのサイズに関しては、今回用意した画像がiPhone5の画面サイズである640×1136の大きさだったので、それに合わせたかたちです。
リソースファイルの準備
ゲームを作るにあたって、画像や音源が必要になります。それらを`Resources`というディレクトリの下にまとめておいておきましょう。
この記事でつかうリソースファイルは、GithubからDLできます。
また、これらのファイルをcocos2dから呼び出す準備として、main.jsを開き、以下のコードを追加しておきましょう。
var res = { img_bg : "Resources/bg.png", img_coin : "Resources/pluskun.png", img_enemy : "Resources/rectman.png", img_snake : "Resources/snake.png", se_hitwall : "Resources/hitWall.mp3", se_getPoint : "Resources/getPoint.mp3", se_dead : "Resources/dead.mp3", se_changeDir : "Resources/changeDirection.mp3" }
これで、例えば`res.img_bg`という形で背景画像のパスを呼び出すことができます。こうしておくと、後で画像のファイル名が変わったとき等にここを修正するだけですむようになります。
背景と主人公の表示
では、まずは背景と主人公のヘビを表示させてみましょう。
以下のコードをmain.jsの末尾に追加します。
window.onload = function(){ cc.game.onStart = function(){ //load resources var preload_res = [ res.img_bg, res.img_coin, res.img_enemy, res.img_snake, res.se_hitwall, res.se_getPoint, res.se_dead, res.se_changeDir ] cc.LoaderScene.preload(preload_res, function () { var MyScene = cc.Scene.extend({ _snake:null, _enemies: [], _coins: [], _dx: 10, _score: 0, _scoreLabel: null, onEnter:function () { this._super(); var size = cc.director.getWinSize(); // 背景の作成 var bg = cc.Sprite.create(res.img_bg); bg.setPosition(size.width/2, size.height/2); this.addChild(bg); // ヘビの作成 var sprite = cc.Sprite.create(res.img_snake); sprite.setPosition(size.width / 2, size.height / 2); sprite.setFlippedX(true); this.addChild(sprite, 0); this._snake = sprite; // this._snakeにヘビのSpriteを保持する } }); cc.director.runScene(new MyScene()); }, this); }; cc.game.run("gameCanvas"); };
大体は前回のHelloWorldのときと同じコードですね。
ポイントとしては、
1. 画像の読み込みに先ほど準備した`res.img_bg`等を使っている
2. ヘビのSpriteを、MySceneのインスタンス変数`_snake`として保持している
というところです。`_snake`に関して、このヘビのSpriteは今後他の場所で何度も使うことになるため、このようにインスタンス変数として保持してあります。
この状態でブラウザからindex.htmlにアクセスすれば、背景の真ん中にヘビが表示されると思います。
ヘビを動かす
では、このヘビを左右に動くようにしましょう。
どのゲームエンジンを使っても、大体のものが60fpsといって一秒間に60フレームの速さで動くように出来ています。また、cocos2d-jsでは、`scheduleUpdate()`を呼ぶことで、`update`という関数を毎フレーム呼び出すようにできます。なので、この`update`関数のなかでヘビを動かすようにしてみましょう。
var MyScene = cc.Scene.extend({ _snake:null, _dx: 10, // 1. 追加 onEnter:function () { // 省略 this._snake = sprite; this.scheduleUpdate(); // 2. updateを呼び出すようにする }, // 3. update関数 update:function(dt){ // これが毎フレーム呼び出される var snakeX = this._snake.getPositionX(); var newX = snakeX + this._dx; // 4. 新しいX座標を計算 this._snake.setPositionX(newX); var size = cc.director.getWinSize(); // 5. 画面の外に行ってしまわないようにする if (newX > size.width || newX < 0) { cc.audioEngine.playEffect(res.se_hitwall); this.changeSnakeDirection(); } }, // 6. ヘビの向きを変える関数 changeSnakeDirection: function(){ this._dx = -this._dx; // 7. ヘビの画像を反転させる if (this._dx > 0) { this._snake.setFlippedX(true); }else{ this._snake.setFlippedX(false); } cc.audioEngine.playEffect(res.se_changeDir); } });
さて、上記のコードを順番に説明していきます。
まず、1で`_dx`というものを10に設定しています。これは毎フレームヘビが動く量になります。
2で、`this.scheduleUpdate();`を呼ぶことで`update`が毎フレーム呼ばれるように設定しています。
3が呼び出されるupdate関数の本体です。
update関数の中では、まずヘビの現在のX座標を取得して、それに`_dx`を加算し、新しくヘビのX座標としてセットしています。(4の部分)
ちなみに、cocos2d-jsでは左下が(x, y) = (0, 0)で、右上に行くに従ってx, yの値が大きくなっていきます。
これで毎フレームupdateが呼び出されて、ヘビのX座標が10ずつ右に進んでいくわけなのですが、このままだとすぐに画面の外に行ってしまいます。なので、5でヘビの新しい座標が画面の横幅を超える、あるいは0以下になる場合にはヘビの向きを変える`changeSnakeDirection`という関数を呼び出しています。この関数はすぐ下で実装されています。
ちなみに`cc.audioEngine.playEffect(res.se_hitwall);`というのは効果音をならす関数で、画面の端にいって方向転換するときに効果音を鳴らしています。
6で、`changeSnakeDirection`というヘビの向きを変える関数の中身を実装しています。`this._dx = -this._dx;`で`_dx`の符号を反転させていますね。`_dx`が+10のときは-10に、-10のときは+10に変更する形です。これで`changeSnakeDirection`を呼び出すたびにヘビの進む向きが左右入れ替わります。ついでに、7でヘビの画像を反転させることで、ヘビが進んでいく方向を向くようにしています。
これをmain.jsに追記し、index.htmlを開くと以下のようにヘビが左右いったり来たりするようになると思います!
敵とコインを出現させる
ではまず、数秒に一回、敵を出現させるようにしましょう。これが敵です。
とても凶悪そうですね。
まずはMySceneのインスタンス変数に`_enemies`と`_coins`という配列を追加してから、
var MyScene = cc.Scene.extend({ _snake:null, _enemies: [], _coins: [], // ..以下略 ``` 次に`changeSnakeDirection`のすぐ下に、以下のコードを追加しましょう。 ``` spawnEnemy: function(){ var size = cc.director.getWinSize(); // 敵Spriteの生成 var enemy = cc.Sprite.create(res.img_enemy); var x = Math.floor( Math.random() * size.width ) ; var y = 0; enemy.setPosition(x , y); // 敵の出現時の座標 this.addChild(enemy, 0); this._enemies.push(enemy); // _enemiesという配列に追加して保持しておく var randDuration = Math.random() * 2; var baseDuration = 2; var duration = baseDuration + randDuration; // 2~4の間の数字を生成 var move = new cc.MoveBy(duration, cc.p(0, size.height)); // MoveByというアクションを生成 var remove = new cc.RemoveSelf(true); // 自身を削除するアクションを生成 var action = new cc.Sequence([move, remove]); // 各アクションを順番に実行するアクションを生成 enemy.runAction(action); // 敵にアクションを実行させる },
敵のSpriteを生成し、`Math.random()`を使って敵の出現座標をランダムにしています。
また、生成した敵のSpriteは`_enemies`という配列に追加し、保持しておくことで、後で敵とヘビの衝突判定に使えるようにします。
さて、この敵を動かすのに、先ほどのヘビのように毎フレーム呼び出される`update`関数の中で座標を少しずつ動かす、というようにしてもいいのですが、今回は別の実装をしてみましょう。
cocos2d-jsでは、カンタンにアニメーションやアクション等を実装できる機能があります。例えば、
var move = new cc.MoveBy(duration, cc.p(0, size.height));
ここで`MoveBy`というアクションを生成しています。これは、`duration`秒で`cc.p(x, y)`で指定した量だけ移動する、というアクションです。
今回は、2秒〜4秒の間でランダムな秒数を設定し、画面の一番下から上まで動くので`cc.p(0, size.height)`というように画面縦サイズをセットしています。
ここで少しだけアクションについて捕捉しておきます。
この`MoveBy`というアクションは、「現在の位置からcc.p(x, y)で指定した分だけ座標を移動する」というアクションなのですが、似たようなものに`MoveTo`というものがあります。
`MoveTo`の場合は、「cc.p(x, y)で指定した座標へ移動する」というものです。この2つは似ていますがアクションの中身としては異なるので注意しましょう。
例えば、`(x, y) = (0, 0)`の位置にいるSpriteに対して`MoveBy(1, cc.p(10, 10))`というのを3回実行させた場合、「x方向へ10, y方向へ10ずつ、1秒かけて移動する」というアクションを3回行うので3秒後には`(x, y) = (30, 30)`の場所にいます。
これに対して、`MoveTo(1, cc.p(10, 10))`というのを3回実行した場合、「(x, y) = (10, 10)の座標へ1秒かけて移動する」というアクションを3回行うので、1回目は(0, 0)から(10, 10)へ1秒かけて移動しますが、2回目、3回目はすでに(10, 10)の位置にいるため移動アクションは何も起きません。
移動アクションを生成したあとは、`cc.RemoveSelf`という、自分自身を削除するアクションを生成し、それらを順番に実行させる`cc.Sequence`というアクションを生成しています。
これにより、「数秒かけて画面の一番上に移動したあと、自分自身を削除する」というアクションが完成します。
最後に、`enemy.runAction(action)`で敵Spriteにそのアクションを実行させています。
これで敵を生成する`spawnEnemy`関数の完成です。これを1秒に一回呼び出されるようにしましょう。
onEnter:function () { // 省略 this._snake = sprite; this.scheduleUpdate(); this.schedule(this.spawnEnemy, 1); // 追加 },
onEnterで`schedule`を使って、`spawnEnemy`を1秒毎に呼び出すようにしました。
これを実行すると、以下のようになります。
敵がたくさん出てきましたね!
同様にして、コインも生成するようにします。
以下のコードを`spawnEnemy`の下に追加します。
spawnCoin: function(){ var size = cc.director.getWinSize(); var coin = cc.Sprite.create(res.img_coin); var x = Math.floor( Math.random() * size.width ) ; var y = 0; coin.setPosition(x , y); this.addChild(coin, 0); this._coins.push(coin) var randDuration = Math.random() * 4; var duration = 5 + randDuration; var move = new cc.MoveBy(duration, cc.p(0, size.height)); var remove = new cc.RemoveSelf(true); var action = new cc.Sequence([move, remove]) coin.runAction(action); },
コインは動きを少し遅くしてみました。また、コインは1.5秒に1回のペースで出るようにしてみましょう。
onEnter:function () { // 省略 this._snake = sprite; this.scheduleUpdate(); this.schedule(this.spawnEnemy, 1); this.schedule(this.spawnCoin, 1.5); // 追加 },
こうですね。これで、定期的に敵やコインが画面に現れるようになって、ゲームらしくなってきました。
衝突判定をする
このままだと、ヘビが敵にあたってもゲームオーバーにならないし、コインをゲットしても何も起きません。
これだとつまらないので、衝突判定を行うようにしましょう。
衝突判定には色々な方法がありますが、今回はシンプルに2つのSpriteが重なっているかどうかで判定するようにします。
cocos2d-jsでは、`cc.rectIntersectsRect`を使うと2つの四角形が重なっているか判定することができます。
`update`の末尾に、以下のコードを追加しましょう
update:function(dt){ // .. 省略 // 衝突判定 var snakeRect = this._snake.getBoundingBox(); // 敵と衝突しているか for(var i = 0; i < this._enemies.length; i++){ if (cc.rectIntersectsRect(snakeRect, this._enemies[i].getBoundingBox())) { // ゲームオーバー } } // コインと衝突しているか var i = this._coins.length; while(i--){ if (cc.rectIntersectsRect(snakeRect, this._coins[i].getBoundingBox())) { this._coins[i].removeFromParent(); this._coins.splice(i,1); this._score++; cc.audioEngine.playEffect(res.se_getPoint); // スコア獲得 } } },
まずは` var snakeRect = this._snake.getBoundingBox();`で、ヘビのSpriteの矩形を取得しています。敵の配列`_enemies`をforループでまわして、それぞれの敵に対してヘビと衝突しているかをチェックしています。衝突していた場合、ゲームオーバーですね。
次にコインとの衝突をチェックしていますが、敵との衝突とは違う方法でチェックしています。
敵と衝突した場合はその場でゲームオーバーとなり、ゲームがストップするので問題ないのですが、コインと衝突した場合はそのコインを配列`_coins`から取り除かないと、次のフレームにまた同じコインで衝突判定をしてしまい、同じコインを何回もゲットできてしまいます。
javascriptの説明になってしまうので詳しくは説明しませんが、ここで、敵と同じようにforループを使って配列内の全てのコインをチェックした場合、ループ中にコインを配列から削除することができません。なので、上記のやり方でループ内で配列から削除ができるようにしています。
これで、敵と衝突した場合はゲームオーバーになり、コインと衝突した場合はコインを獲得できるようになりました。一気にゲームらしくなりましたね!
完成・・・?
さて、これであとは
- 画面をタップしたらヘビの移動方向が変わる
- ゲームオーバー時にゲームを終了する
- コイン獲得時にスコアを1増やす
- スコアを表示する
等を実装すれば、ゲームとして完成です!
これらの実装は、これまでの知識でおおよそ実装できるはずです。また、詳しく説明していなかったところも、Githubに完成系のコードをおいているので、そちらを参考にしてゲームを完成してもらえればと思います。
完成品はこちらで遊ぶことができます。もちろんスマートフォンからでも遊べます。
さて、これで一応このゲームは完成ですが、いかがでしょうか。遊んでみて、「もうちょっとこうしたいなぁ」という欲は湧いてきましたか?
- 動きが単調すぎる?では敵の動きにバリエーションを付けてみましょう。`cc.RotateBy`をつかえば敵を回転させることが出来ますし、`cc.Spawn`にアクションの配列を渡せば、複数のアクションを同時に実行できるので、「回転しながら左右に移動しながら画面を縦に移動する」といったことができます。
- コードが読みにくい?ではコードのリファクタリングをしていきましょう。Githubに上がっているコードも、まだまだ改善する余地はあります。
- バグをみつけた?それは直さないといけませんね・・・。ちなみにヘビが画面の端に来た時に画面を連打すると、ヘビが画面外にいって帰ってこなくなるバグは既にみつかっています・・・。
- お金を稼ぎたい?ゲーム画面の下に広告を載せてみたり、cocos2d-xでもっとリッチなゲームにしてAppStoreやGoogle Playでアプリとして配布すれば、マネタイズも可能でしょう。
- ヘビや敵がかわいくない?もっとイケてる絵に交換しましょう。数枚の絵をパラパラマンガのようにアニメーションさせるのもいいでしょう。
まだまだシンプルなゲームですが、それでも改良できる点はとてもたくさんあります。
この記事を読んで勉強したあなたが、このゲームをいじって更におもしろいものに進化させてくれるととても嬉しいです。
最後に
cocos2d-jsを使ったゲーム開発、いかがでしたか。
このヘビのゲームはとても単純でカンタンですが、リッチなグラフィックや長大なストーリーが無いシンプルなゲームでも、クロッシーロードのように全世界で大ヒットすることだってありえます。
この記事で学んだ技術をもう少しブラッシュアップすれば、あなたも世界でヒットするようなゲームを開発できるかもしれません。