Chapter 04

ピースの移動

alkn203
alkn203
2022.11.14に更新

今回の目標

今回は、ピースをタッチで移動できるようにします。

15puzzle-tut-4.gif

完成コード

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

コードを見る
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();
      });
    });
    // 参照用
    this.pieceGroup = pieceGroup;
  },
  // 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) {
    // 空白ピースを得る
    var blank = this.getBlankPiece();
    // 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 tmpX = blank.x;
      var tmpY = blank.y;
      // 位置入れ換え
      blank.setPosition(piece.x, piece.y);
      piece.setPosition(tmpX, tmpY);
    }
  },
});
// ピースクラス
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プロジェクト

https://runstant.com/alkn203/projects/c7da26e6

ピース移動の考え方

  • 15パズルにおいて移動できるピースは、空白と隣り合わせのピースだけです。よって、タッチされたピースと空白の位置を入れ替えることが移動とみなせます。これまで、空白をあえて見えない16番ピースにしたのは、このためです。
  • 隣り合わせについては、x座標が同じでy座標の差の絶対値がグリッド1個分のサイズ、またはその逆パターンの時で判定しています。

コード説明

ピースの移動処理

まず、見えない16番ピースを返すだけのgetBlankPiece関数をMainSceneに追加します。コードは以下のとおりです。

// 16番ピース(空白)を取得
getBlankPiece: function() {
  var result = null;
  this.pieceGroup.children.some(function(piece) {
    // 16番ピースを結果に格納
    if (piece.num === 16) {
      result = piece;
      return true;
    }
  });
  return result;
},

pieceGroupの子要素配列をsome関数でループして、16番ピースがあったらreturn trueとしてループを抜けるようにしています。
次に、ピースの移動処理を行うmovePiece関数をMainSceneに追加します。コードは以下のとおりです。

// ピースの移動処理
movePiece: function(piece) {
  // 空白ピースを得る
  var blank = this.getBlankPiece();
  // 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 tmpX = blank.x;
    var tmpY = blank.y;
    // 位置入れ換え
    blank.setPosition(piece.x, piece.y);
    piece.setPosition(tmpX, tmpY);
  }
},
  • タッチされたピースを引数で受け取り、空白ピースと位置を入れ替えます。先に述べたとおり、隣り合わせについては、x座標が同じでy座標の差の絶対値がピース1個分のサイズまたはその逆パターンの時で判定しています。
  • 位置の入れ替えは、一方の座標を一時変数に待避させて、どちらかが上書きされないようにします。

最後に、MainSceneの一部を以下のように変更します。

// ピースグループ
var pieceGroup = CanvasElement().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();
  });
});
// 参照用
this.pieceGroup = pieceGroup;
  • thisの参照が正しくなるように、MainSceneを指すthisselfという変数に代入しています。
  • piece.onpointendの中身をself.movePiece(this)として、タッチされたピースが引数として渡されるようにしています。ここでのselfMainScenethispieceを指しています。
  • pieceGroupMainScene全体から参照できるようにthis.pieceGroupという変数に代入しています。