【phina.js】ゲーム作成チュートリアル(15パズル)(最終回)【ゲーム的要素の追加】

※これまでのTipsはphina.js チュートリアル集にまとめています。

前回

前回の【phina.js】ゲーム作成チュートリアル(15パズル)(その5)【tweenerで移動処理】では、tweenerを使ってピースをアニメーション移動させました。

今回の目標

今回は、もう少しゲームらしくするために以下の要素を追加したいと思います。

  • ピースのシャッフル処理
  • クリア判定

ピースのシャッフル

ピースをシャッフルするボタンを追加します。

15puzzle-tut-6.png

コードは以下のとおりです。

phina.globalize();
// 定数
var SCREEN_WIDTH = 640;            // 画面横サイズ
var SCREEN_HEIGHT = 960;           // 画面縦サイズ
var GRID_SIZE = SCREEN_WIDTH / 4;  // グリッドのサイズ
var PIECE_SIZE = GRID_SIZE * 0.95; // ピースの大きさ
var PIECE_NUM_XY = 4;              // 縦横のピース数
var PIECE_OFFSET = GRID_SIZE / 2;  // オフセット値
// メインシーン
phina.define('MainScene', {
  superClass: 'DisplayScene',
  // コンストラクタ
  init: function() {
    // 親クラス初期化
    this.superInit();
    // 背景色
    this.backgroundColor = 'gray';
    // グリッド
    var grid = Grid(SCREEN_WIDTH, PIECE_NUM_XY);
    // ピースグループ
    var pieceGroup = DisplayElement().addChildTo(this);
    var self = this;
    // ピース配置
    PIECE_NUM_XY.times(function(spanX) {
      PIECE_NUM_XY.times(function(spanY) {
        // 番号
        var num = spanY * PIECE_NUM_XY + spanX + 1;
        // ピース作成
        var piece = Piece(num).addChildTo(pieceGroup);
        // Gridを利用して配置
        piece.x = grid.span(spanX) + PIECE_OFFSET;
        piece.y = grid.span(spanY) + PIECE_OFFSET;
        // タッチを有効にする
        piece.setInteractive(true);
        // タッチされた時の処理
        piece.onpointend = function() {
          // ピース移動処理
          self.movePiece(this);
        };
        // 16番のピースは非表示
        if (num === 16) piece.hide();
      });
    });
    // シャッフルボタン
    var shuffleButton = Button({
      text: 'SHUFFLE',
    }).addChildTo(this).setPosition(this.gridX.center(), this.gridY.span(13));
    // ボタンプッシュ時処理
    shuffleButton.onpush = function() {
      // ピースをシャッフル
      (100).times(function() {
        self.shufflePieces();  
      });
    };
    // 参照用
    this.pieceGroup = pieceGroup;
    this.shuffleButton = shuffleButton;
  },
  // 16番ピース(空白)を取得
  getBlankPiece: function() {
    var result = null;
    this.pieceGroup.children.some(function(piece) {
      // 16番ピースを結果に格納
      if (piece.num === 16) {
        result = piece;
        return true;
      }
    });
    return result;
  },
  // ピースの移動処理
  movePiece: function(piece, isInstantly) {
    // 空白ピースを得る
    var blank = this.getBlankPiece();
    // 即入れ替え
    if (isInstantly) {
      var tmpX = piece.x;
      var tmpY = piece.y;
      piece.setPosition(blank.x, blank.y);
      blank.setPosition(tmpX, tmpY);
      return;
    }
    // x, yの座標差の絶対値
    var dx = Math.abs(piece.x - blank.x);
    var dy = Math.abs(piece.y - blank.y);
    // 隣り合わせの判定
    if ((piece.x === blank.x && dy === GRID_SIZE) || 
        (piece.y === blank.y && dx === GRID_SIZE)) {
      // タッチされたピース位置を記憶
      var touchX = piece.x;
      var touchY = piece.y;
      var self = this;
      // tweenerで移動処理
      piece.tweener.clear()
                   .to({x: blank.x, y: blank.y}, 200, "easeOutCubic")
                   .call(function() {
                     // 空白ピースをタッチされたピースの位置へ
                     blank.setPosition(touchX, touchY);
                   });
    }
  },
  // 指定の位置のピースを返す
  getPieceByXY: function(x, y) {
    var result = null;
    this.pieceGroup.children.some(function(piece) {
      // 指定した座標なら
      if (piece.x === x && piece.y === y) {
        result = piece;
        return true;
      }
    });
    return result;
  },
  // ピースをシャッフルする
  shufflePieces: function() {
    var self = this;
    // 隣接ピース格納用
    var pieces = [];
    // 空白ピースを得る
    var blank = this.getBlankPiece();
    // 上下左右隣りのピースがあれば配列に追加
    [1, 0, -1].each(function(i) {
      [1, 0, -1].each(function(j) {
        if (i != j) {
          var x = blank.x + i * GRID_SIZE;
          var y = blank.y + j * GRID_SIZE;
          var target = self.getPieceByXY(x, y);
          if (target) pieces.push(target);
        }
      });
    });
    // 隣接ピースからランダムに選択して空白ピースと入れ替える
    this.movePiece(pieces.random(), 'instantly');
    pieces.clear();
  },
});
// ピースクラス
phina.define('Piece', {
  // RectangleShapeを継承
  superClass: 'phina.display.RectangleShape',
    // コンストラクタ
    init: function(num) {
      // 親クラス初期化
      this.superInit({
        width: PIECE_SIZE,
        height: PIECE_SIZE,
        cornerRadius: 10,
        fill: 'silver',
        stroke: 'white',
      });
      // 数字
      this.num = num;
      // 数字表示用ラベル
      this.label = Label({
        text: this.num + '',
        fontSize: PIECE_SIZE * 0.8,
        fill: 'white',
      }).addChildTo(this);
    },
});
// メイン
phina.main(function() {
  var app = GameApp({
    startLabel: 'main',
  });
  app.run();
});

[runstantで確認]

コード説明

// シャッフルボタン
var shuffleButton = Button({
  text: 'SHUFFLE',
}).addChildTo(this).setPosition(this.gridX.center(), this.gridY.span(13));
// ボタンプッシュ時処理
shuffleButton.onpush = function() {
    // ピースをシャッフル
    (100).times(function() {
      self.shufflePieces();  
    });
};
  • MainScneneButtonクラスを使ってシャッフルボタンを追加しています。
  • shuffleButton.onpushにボタンが押された時の処理を記述しています。timesを使って100回シャッフルしています。shufflePieces関数については後述します。
// 指定の位置のピースを返す
  getPieceByXY: function(x, y) {
    var result = null;
    this.pieceGroup.children.some(function(piece) {
      // 指定した座標なら
      if (piece.x === x && piece.y === y) {
        result = piece;
        return true;
      }
    });
    return result;
  },

getPieceByXYは、指定された位置のピースを返します。該当ピースがあればそのピース、なければnullを返します。この後のshufflePiecesから呼ばれます。

// ピースをシャッフルする
  shufflePieces: function() {
    var self = this;
    // 隣接ピース格納用
    var pieces = [];
    // 空白ピースを得る
    var blank = this.getBlankPiece();
    // 上下左右隣りのピースがあれば配列に追加
    [1, 0, -1].each(function(i) {
      [1, 0, -1].each(function(j) {
        if (i != j) {
          var x = blank.x + i * GRID_SIZE;
          var y = blank.y + j * GRID_SIZE;
          var target = self.getPieceByXY(x, y);
          if (target) pieces.push(target);
        }
      });
    });
    // 隣接ピースからランダムに選択して空白ピースと入れ替える
    this.movePiece(pieces.random(), 'instantly');
    pieces.clear();
  },
  • 空白ピースの上下左右隣りのピースがあるか調べて、その中からランダムに選んで移動させるという処理をしています。
  • movePieceの引数を一つ増やして、tweenerを使わずに即座に移動するかどうかを指定しています。
  // ピースの移動処理
  movePiece: function(piece, isInstantly) {
    // 空白ピースを得る
    var blank = this.getBlankPiece();
    // 即入れ替え
    if (isInstantly) {
      var tmpX = piece.x;
      var tmpY = piece.y;
      piece.setPosition(blank.x, blank.y);
      blank.setPosition(tmpX, tmpY);
      return;
    }
    // x, yの座標差の絶対値
    var dx = Math.abs(piece.x - blank.x);
    var dy = Math.abs(piece.y - blank.y);
    // 隣り合わせの判定
    if ((piece.x === blank.x && dy === GRID_SIZE) ||
        (piece.y === blank.y && dx === GRID_SIZE)) {
      // タッチされたピース位置を記憶
      var touchX = piece.x;
      var touchY = piece.y;
      var self = this;
      // tweenerで移動処理
      piece.tweener.clear()
                   .to({x: blank.x, y: blank.y}, 200, "easeOutCubic")
                   .call(function() {
                     // 空白ピースをタッチされたピースの位置へ
                     blank.setPosition(touchX, touchY);
                   });
    }
  • 第二引数が指定されていた場合、シャッフル処理の一環と判断してtweenerでアニメーションさせずに即座に移動させます。最初に書いてた移動処理を復活させただけです。
  • 15パズルのシャッフルのポイントは、実際に可能な移動をさせるという点です。そうすればパズルが解けなくなるという心配もありません。

クリア判定

15パズルにおけるクリアとは、言うまでもなく全てのピースが正しい位置に収まった時です。
シャッフルボタンを押した後にピースを正しい位置に揃えると、リザルトシーンに移行するようにします。
コードは以下のとおりです。

phina.globalize();
// 定数
var SCREEN_WIDTH = 640;            // 画面横サイズ
var SCREEN_HEIGHT = 960;           // 画面縦サイズ
var GRID_SIZE = SCREEN_WIDTH / 4;  // グリッドのサイズ
var PIECE_SIZE = GRID_SIZE * 0.95; // ピースの大きさ
var PIECE_NUM_XY = 4;              // 縦横のピース数
var PIECE_OFFSET = GRID_SIZE / 2;  // オフセット値
// メインシーン
phina.define('MainScene', {
  superClass: 'DisplayScene',
  // コンストラクタ
  init: function() {
    // 親クラス初期化
    this.superInit();
    // 背景色
    this.backgroundColor = 'gray';
    // グリッド
    var grid = Grid(SCREEN_WIDTH, PIECE_NUM_XY);
    // ピースグループ
    var pieceGroup = DisplayElement().addChildTo(this);
    var self = this;
    // ピース配置
    PIECE_NUM_XY.times(function(spanX) {
      PIECE_NUM_XY.times(function(spanY) {
        // 番号
        var num = spanY * PIECE_NUM_XY + spanX + 1;
        // ピース作成
        var piece = Piece(num).addChildTo(pieceGroup);
        // Gridを利用して配置
        piece.x = grid.span(spanX) + PIECE_OFFSET;
        piece.y = grid.span(spanY) + PIECE_OFFSET;
        // タッチを有効にする
        piece.setInteractive(true);
        // タッチされた時の処理
        piece.onpointend = function() {
          // ピース移動処理
          self.movePiece(this);
        };
        // 16番のピースは非表示
        if (num === 16) piece.hide();
      });
    });
    // シャッフルボタン
    var shuffleButton = Button({
      text: 'SHUFFLE',
    }).addChildTo(this).setPosition(this.gridX.center(), this.gridY.span(13));
    // ボタンプッシュ時処理
    shuffleButton.onpush = function() {
      // ピースをシャッフル
      (100).times(function() {
        self.shufflePieces();  
      });
    };
    // 参照用
    this.pieceGroup = pieceGroup;
    this.shuffleButton = shuffleButton;
  },
  // 16番ピース(空白)を取得
  getBlankPiece: function() {
    var result = null;
    this.pieceGroup.children.some(function(piece) {
      // 16番ピースを結果に格納
      if (piece.num === 16) {
        result = piece;
        return true;
      }
    });
    return result;
  },
  // ピースの移動処理
  movePiece: function(piece, isInstantly) {
    // 空白ピースを得る
    var blank = this.getBlankPiece();
    // 即入れ替え
    if (isInstantly) {
      var tmpX = piece.x;
      var tmpY = piece.y;
      piece.setPosition(blank.x, blank.y);
      blank.setPosition(tmpX, tmpY);
      return;
    }
    // x, yの座標差の絶対値
    var dx = Math.abs(piece.x - blank.x);
    var dy = Math.abs(piece.y - blank.y);
    // 隣り合わせの判定
    if ((piece.x === blank.x && dy === GRID_SIZE) || 
        (piece.y === blank.y && dx === GRID_SIZE)) {
      // タッチされたピース位置を記憶
      var touchX = piece.x;
      var touchY = piece.y;
      var self = this;
      // tweenerで移動処理
      piece.tweener.clear()
                   .to({x: blank.x, y: blank.y}, 200, "easeOutCubic")
                   .call(function() {
                     // 空白ピースをタッチされたピースの位置へ
                     blank.setPosition(touchX, touchY);
                   });
    }
  },
  // 指定の位置のピースを返す
  getPieceByXY: function(x, y) {
    var result = null;
    this.pieceGroup.children.some(function(piece) {
      // 指定した座標なら
      if (piece.x === x && piece.y === y) {
        result = piece;
        return true;
      }
    });
    return result;
  },
  // ピースをシャッフルする
  shufflePieces: function() {
    var self = this;
    // 隣接ピース格納用
    var pieces = [];
    // 空白ピースを得る
    var blank = this.getBlankPiece();
    // 上下左右隣りのピースがあれば配列に追加
    [1, 0, -1].each(function(i) {
      [1, 0, -1].each(function(j) {
        if (i != j) {
          var x = blank.x + i * GRID_SIZE;
          var y = blank.y + j * GRID_SIZE;
          var target = self.getPieceByXY(x, y);
          if (target) pieces.push(target);
        }
      });
    });
    // 隣接ピースからランダムに選択して空白ピースと入れ替える
    this.movePiece(pieces.random(), 'instantly');
    pieces.clear();
  },
});
// ピースクラス
phina.define('Piece', {
  // RectangleShapeを継承
  superClass: 'phina.display.RectangleShape',
    // コンストラクタ
    init: function(num) {
      // 親クラス初期化
      this.superInit({
        width: PIECE_SIZE,
        height: PIECE_SIZE,
        cornerRadius: 10,
        fill: 'silver',
        stroke: 'white',
      });
      // 数字
      this.num = num;
      // 数字表示用ラベル
      this.label = Label({
        text: this.num + '',
        fontSize: PIECE_SIZE * 0.8,
        fill: 'white',
      }).addChildTo(this);
    },
});
// メイン
phina.main(function() {
  var app = GameApp({
    startLabel: 'main',
  });
  app.run();
});

[runstantで確認]

コード説明

// Gridを利用して配置
piece.x = grid.span(spanX) + PIECE_OFFSET;
piece.y = grid.span(spanY) + PIECE_OFFSET;
// 正解の位置を記憶させておく
piece.correctX = piece.x;
piece.correctY = piece.y;

ピースの初期位置をプロパティとして保存しておきます。

// 一回でも押されたかどうか
shuffleButton.isPushed = false;
// ボタンプッシュ時処理
shuffleButton.onpush = function() {
    // ピースをシャッフル
    (2).times(function() {
      self.shufflePieces();  
    });
    // 残りステップリセット
    self.step = 200;
    // 押されたフラグ立て
    this.isPushed = true;
};
  • シャッフルボタンにisPushedというフラグを作っています。これはシャッフルされた後だけにクリア判定するために使われます。
  • スコア用に残りステップ数を設けています。
// tweenerで移動処理
piece.tweener.clear()
             .to({x: blank.x, y: blank.y}, 200, "easeOutCubic")
             .call(function() {
               // 空白ピースをタッチされたピースの位置へ
               blank.setPosition(touchX, touchY);
               // 残りステップを減らす
               self.step--;
               // クリアチェック
               if (self.shuffleButton.isPushed) self.checkPiecePosition();
             });

ピースが移動し終わったタイミングでクリア判定を行っています。checkPiecePositionについては後述します。

// クリア判定
checkPiecePosition: function() {
  // 正しくない位置のピースがあるかチェックする
  var result = this.pieceGroup.children.some(function(piece) {
    if (piece.x != piece.correctX || piece.y != piece.correctY) return true;
  });
  // 全て正しい位置ならクリア画面へ
  var score = this.step;
  if (!result) {
    this.exit({
      score: score,
      message: '15 Puzzle Clear!'
    });
  }
},
  • 「全てのピースが正しい位置」ということは「正しくない位置のピースが一つでもある」と同じ意味なので、該当ピースがあったら即座に関数を抜けるようにしています。
  • 全て正しい位置なら、残りステップ数をscoreに渡してexitでリザルトシーンに遷移させます。

まとめ

今回でphina.js使った15パズル作成のチュートリアルは終わりです。分かりづらい部分も多々あったかと思いますが、少しでもphina.jsを使ったゲーム作りのヒントになれば幸いです。最後まで読んで頂きありがとうございました。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.