YDブログ

(Y)やりたいことしか(D)できない病ブログです。

本サイトはプロモーションが含まれています

Javascriptで200行ツムツム風ゲーム作成


とつぜんゲームを開発したくなり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ファイルです。

https://cdn.rawgit.com/wise9/enchant.js/master/images/icon1.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も気になるので、そちらでも作ろうと思います。