とつぜんゲームを開発したくなりenchant.jsと物理エンジンのBox2Dでツムツム風パズルゲームをつくりました。
動作デモ・ソースコード
時間内にできるだけボールを消して得点を競うゲームです。
画面をタッチするとゲームがはじまります。 同色のボールをマウス(モバイルなら指)で3つ以上つなげると消えます。
マウスでも操作できますが、スマホかタブレットの方が快適です。 動作確認はPCとモバイルのChrome、Safariでおこないました。
See the Pen Tsum Tsum Like Game with enchant.js framework by matagotch (@matagotch) on CodePen.
enchant.js入門・チュートリアル
enchant.jsはHTML5・Javascriptのゲーム制作フレームワークです。
チュートリアルは公式サイト上に丁寧にまとめられており、初心者でもウェブ上の情報だけでサクッとゲーム開発をはじめられます。
親切なチュートリアルがたくさんあるので、記事中では主にゲームロジックについて解説します。
ソースコードの解説
初期設定
Codepenでenchant.js、Box2D、Box2Dプラグインの3つの依存ライブラリを読みこんでいます。 HTMLで書くならこのようなコードです。
<script src="https://cdn.rawgit.com/uei/enchant.js-builds/master/enchant.min.js"></script> <script src="https://cdn.rawgit.com/uei/enchant.js-builds/master/libs/Box2dWeb-2.1.a.3.js"></script> <script src="https://cdn.rawgit.com/uei/enchant.js-builds/master/build/plugins/box2d.enchant.js"></script>
スマホで表示される「Tap To Start」画面をスキップします。
enchant.ENV.USE_TOUCH_TO_START_SCENE = false;
配列の最終要素を返すメソッドです。 はじめはenchant.Groupでオブジェクトを管理していたところ、消去したりステータスを変えたりしたとき問題が起きて、逆に複雑さが増したのでふつうに配列で管理しています。
Array.prototype.last = function() {return this[this.length - 1];}
画面の幅・高さ、ボールの数、1ゲームのプレイ時間(秒数)を定義します。
var WIDTH = 150, HEIGHT = 200, BALL_NUM = 100, PLAYTIME = 30;
コードの最後の方で、画像リソースを事前ロードしています。
game.preload(iconPath);
Cursorクラス
enchant.jsで、マウスホバーを扱うために定義したクラスです。 画面には表示されませんがenchantのSpriteを継承しています。
マウスカーソル上(またはスワイプ中の指の位置)にCursorスプライトを置いて、Ballスプライトと衝突判定しています。
ゲーム画面はブラウザの表示領域に合わせて自動リサイズされるので、ゲーム画面上のカーソル位置は、実際のマウス・タッチ位置をgame.scaleで割って算出しています。
var Cursor = Class.create(Sprite, { size: 15, initialize: function() { Sprite.call(this, this.size, this.size); this.x = this.y = -100; var _cursor = this; document.ontouchmove = function(event) { _cursor.x = event.touches[0].pageX / game.scale - _cursor.size / 2; _cursor.y = event.touches[0].pageY / game.scale - _cursor.size / 2; }; document.onmousemove = function(event) { _cursor.x = event.x / game.scale - _cursor.size / 2; _cursor.y = event.y / game.scale - _cursor.size / 2; }; } });
Score.charge()
つなげた数が多いほど得点が高くなるよう、消したボール数の乗数を加点しています。
charge: function(ballNum) { this.lastScore += ballNum * ballNum; this.updateText(); },
Ballクラス
ボールの画像は、enchant.jsに付属のリソース画像をつかっています。 8ピクセルで区切られた横長のpngファイルです。
frameプロパティで画像を抜き出す位置が設定できるので、ついでにボールの種類をきめます。
ボールは上から降るよう、高さ(height)を反転しています。
Box2Dの命令は「PhyCircleSprite」のようにPhy(=Physics)からはじまります。 Box2DプラグインのAPIリファレンスはこちらにあります。
var Ball = Class.create(PhyCircleSprite, { size: 8, variation: 6, initialize: function(x, y) { PhyCircleSprite.call(this, this.size, enchant.box2d.DYNAMIC_SPRITE, 1.5, 1.0, 0.3, true); this.image = game.assets[iconPath]; this.frame = Math.floor(Math.random() * this.variation); this.x = Math.random() * WIDTH - this.size; this.y = Math.random() * HEIGHT * -1; }, ...
Ball.select()
選択中のボールは透明度(opacity)を0.5にします。
select: function() { if(this.opacity === 0.5) return; this.opacity = 0.5; selectedBalls.push(this); },
Ball.ontouchstart()
クリック・タッチで最初のボールを選択します。
ontouchstart: function() { this.select(); },
Ball.onenterframe()
ボールをドラッグ・スワイプでなぞれるようにします。
if文で以下の条件に当てはまるボールだけ選択できるようにします。
- ドラッグ・スワイプ中である(ボールが1つ以上選択されている)
- 最後に選択したボールと同じ種類(frame)である
- カーソル下にある
- 前に選択したボールと繋がっている(最後に選択したボールもカーソル内にある)
ボールがマウス・指の下にあるかどうかは、Cursorスプライトと衝突判定(within)で確認しています。
onenterframe: function() { if( selectedBalls.length !== 0 && this.frame === selectedBalls.last().frame && game.cursor.within(this) && game.cursor.within(selectedBalls.last()) ){ this.select(); }; },
Ball.ontouchend()
同じ種類(frame)のボールを3個以上つなげると消えます。 消したのと同数のボールを新しくつくります。
ontouchend: function() { if(selectedBalls.length < 3) { selectedBalls.forEach(function(ball) {ball.opacity = 1.0;}); }else{ game.score.charge(selectedBalls.length); this.parentNode.spawnBalls(selectedBalls.length); selectedBalls.forEach(function(ball) {ball.destroy();}); } selectedBalls = []; }
GameSceneクラス
ゲームの中心となるプレイ画面です。
GameScene.initialize()
はじめにキャンバスの左右下3方に壁を設置。カーソル、点数表示、タイマー、ボールクラスを呼びだします。
var GameScene = Class.create(Scene, { initialize: function() { Scene.call(this); this.world = new PhysicsWorld(0, 9.8); this.addChild(new Wall(1000000, 1, 500000, HEIGHT)); // floor this.addChild(new Wall(1, 1000000, 0, 500000)); // left wall this.addChild(new Wall(1, 1000000, WIDTH - 1, 500000)); // right wall this.addChild(game.cursor); this.addChild(game.score); this.spawnBalls(BALL_NUM); this.addChild(new Timer()); }, ...
GameScene.spawnBalls()
複数のボールを作成します。Ballクラスからも循環的に呼んでいます。
spawnBalls: function(count) { for(var i = 0; i < count; i += 1) this.addChild(new Ball()); },
まとめ
enchant.jsのAPIは直感的で把握しやすく、ゲームロジックの作成にすぐ専念できました。 似たコードをp5.jsで書いたときはパフォーマンスの問題にぶつかりましたが、enchant.jsの処理速度は良好で、Macbookだとボールを500個に増やしても遊べます。
ただしCPUの遅い格安スマホだと100個でも遅延します。 そのへんは物理エンジンをつかっているので仕方ありません。
ゲームとしての体裁もきちんと整えようとスコアとシーン管理を追加したら、当初は100行だったコードが2倍にふくらんでしまいました。 サンプルとして長いので、次回からは100行程度に収めます。
PhaserJSとUnityも気になるので、そちらでも作ろうと思います。