PixiJSをかじる

2021年1月25日

Phaserでも使っているというPixiJSの入門を読んでみた。

初心者による超初心者のためのPixiJS入門

VSCodeでコメントを解除していくだけで動作が確認できる。非常に親切な解説だった。

チュートリアル

チュートリアルもひとつだけ読んでみた。01/ Getting started with PixiJSでexamplesのコードがダウンロードできる。その中の17_treasureHunter.htmlを読んでみた。

ここで試せる。PCで矢印キーを使う。コードの概要は以下のようになっていた。Pixijsは衝突判定を自分で書く必要がある。関数も自分で呼び出している。

let loader = PIXI.loader;
let app = new Application({...});
document.body.appendChild(app.view);
loader.add(...).load(setup);
function setup() { 
 初期化コード(配置やキー処理等)
 state = play;
 app.ticker.add(delta => gameLoop(delta));
}
function gameLoop(delta) {
  state(delta); // playやendを呼び出す
}
function play(delta) {
  衝突判定
 体力ゲージ更新
  宝物を持つ
 if (体力が無くなったら) {
    state = end;
    message.text = 負け
  }
  if (宝物がドアまで来たら) {
    state = end;
    message.text = 勝ち
  }
}
function end() {
  ゲームシーン非表示
  ゲームオーバー表示
}
function contail(sprite, container) {
  containerにspriteを収める
}
function hitTestRectangle(r1, r2) {
  衝突判定
}
function randomInt(min, max) {
  整数の乱数生成
}
function keyboard(keyCode) {
  キーpress,releaseハンドラ登録
}

メイン処理の所までコメントを日本語にしてみた。

<!doctype html>
<meta charset="utf-8">
<title>Treasure hunter</title>
<body>
<script src="../pixi/pixi.min.js"></script>
<script>

// PIXIを省略するエイリアス
let Application = PIXI.Application,
    Container = PIXI.Container,
    loader = PIXI.loader,
    resources = PIXI.loader.resources,
    Graphics = PIXI.Graphics,
    TextureCache = PIXI.utils.TextureCache,
    Sprite = PIXI.Sprite,
    Text = PIXI.Text,
    TextStyle = PIXI.TextStyle;

// Pixiアプリケーション生成
let app = new Application({ 
    width: 512, 
    height: 512,                       
    antialiasing: true, 
    transparent: false, 
    resolution: 1
  }
);

// HTMLドキュメントに自動的にcanvasを追加する
document.body.appendChild(app.view);

loader
  .add("images/treasureHunter.json")
  .load(setup);

// 一つ以上の関数で使う可能性のある変数を定義
//
let state, explorer, treasure, blobs, chimes, exit, player, dungeon,
    door, healthBar, message, gameScene, gameOverScene, enemies, id;

function setup() { // 初期化関数

  // ゲームシーンを作成しステージに追加する
  gameScene = new Container();
  app.stage.addChild(gameScene);

  // スプライトを作ってゲームシーンに追加する
  // Create an alias for the texture atlas frame ids
  id = resources["images/treasureHunter.json"].textures;

  // ダンジョン
  dungeon = new Sprite(id["dungeon.png"]);
  gameScene.addChild(dungeon);

  // ドア
  door = new Sprite(id["door.png"]); 
  door.position.set(32, 0);
  gameScene.addChild(door);

  // 冒険者
  explorer = new Sprite(id["explorer.png"]);
  explorer.x = 68;
  explorer.y = gameScene.height / 2 - explorer.height / 2;
  explorer.vx = 0;
  explorer.vy = 0;
  gameScene.addChild(explorer);
  
  // 宝物
  treasure = new Sprite(id["treasure.png"]);
  treasure.x = gameScene.width - treasure.width - 48;
  treasure.y = gameScene.height / 2 - treasure.height / 2;
  gameScene.addChild(treasure);

  // ブロブ(モンスター)を作る
  let numberOfBlobs = 6,
      spacing = 48,
      xOffset = 150,
      speed = 2,
      direction = 1;

  // ブロブモンスターを格納する配列
  blobs = [];

  // numberOfBlobsの値分のブロブを作る
  for (let i = 0; i < numberOfBlobs; i++) {

    // ブロブを作る
    let blob = new Sprite(id["blob.png"]);

    // spacing毎に水平にブロブを配置
    // 最初のブロブはスクリーンの左から
    // xOffset分空けて配置
    let x = spacing * i + xOffset;

    // Y軸はランダムでブロブを配置
    let y = randomInt(0, app.stage.height - blob.height);

    // ブロブの位置を設定
    blob.x = x;
    blob.y = y;

    // ブロブの垂直速度を設定する。方向は1または-1。
    // 1は下向き-1は上向き
    // ブロブの垂直方向は
    // 方向と速度を掛け合わせることで決める。
    blob.vy = speed * direction;

    // 次のブロブ用に方向を反転させる
    direction *= -1;

    // ブロブ配列に追加する
    blobs.push(blob);

    // ゲームシーンにブロブを追加する
    gameScene.addChild(blob);
  }

  // 体力バーを作る
  healthBar = new Container();
  healthBar.position.set(app.stage.width - 170, 4)
  gameScene.addChild(healthBar);

  // (体力バーの)黒の背景の長方形を作る
  let innerBar = new Graphics();
  innerBar.beginFill(0x000000);
  innerBar.drawRect(0, 0, 128, 8);
  innerBar.endFill();
  healthBar.addChild(innerBar);

  // (体力バーの)正面の赤い長方形を作る
  let outerBar = new Graphics();
  outerBar.beginFill(0xFF3300);
  outerBar.drawRect(0, 0, 128, 8);
  outerBar.endFill();
  healthBar.addChild(outerBar);

  healthBar.outer = outerBar;

  // ゲームオーバーシーンを作る
  gameOverScene = new Container();
  app.stage.addChild(gameOverScene);

  // ゲーム開始時はゲームオーバーシーンは非表示にする
  gameOverScene.visible = false;

  // テキストスプライトを作りゲームオーバーシーンに追加する
  let style = new TextStyle({
    fontFamily: "Futura",
    fontSize: 64,
    fill: "white"
  });
  message = new Text("The End!", style);
  message.x = 120;
  message.y = app.stage.height / 2 - 32;
  gameOverScene.addChild(message);

  // キーボードの矢印キーをキャプチャーする
  let left = keyboard(37),
      up = keyboard(38),
      right = keyboard(39),
      down = keyboard(40);

  // 左矢印キーを押したときに呼ばれるpressメソッド
  left.press = function() {

    // キーが押されたとき冒険者の速度を変更する
    explorer.vx = -5;
    explorer.vy = 0;
  };

  // 左矢印キーを離したときに呼ばれるreleaseメソッド
  left.release = function() {

    // 左矢印キー離したとき、右矢印キーを押してなく
    // 冒険者が垂直方向に動いてなければ
    // 冒険者を停止させる
    if (!right.isDown && explorer.vy === 0) {
      explorer.vx = 0;
    }
  };

  // 上
  up.press = function() {
    explorer.vy = -5;
    explorer.vx = 0;
  };
  up.release = function() {
    if (!down.isDown && explorer.vx === 0) {
      explorer.vy = 0;
    }
  };

  // 右
  right.press = function() {
    explorer.vx = 5;
    explorer.vy = 0;
  };
  right.release = function() {
    if (!left.isDown && explorer.vy === 0) {
      explorer.vx = 0;
    }
  };

  // 下
  down.press = function() {
    explorer.vy = 5;
    explorer.vx = 0;
  };
  down.release = function() {
    if (!up.isDown && explorer.vx === 0) {
      explorer.vy = 0;
    }
  };

  // ゲームの状態を設定
  state = play;
 
  // ゲームループを開始
  app.ticker.add(delta => gameLoop(delta));
}


function gameLoop(delta){

  // カレントのゲーム状態を更新
  state(delta);
}

function play(delta) {


  // 冒険者の速度を使って移動する
  explorer.x += explorer.vx;
  explorer.y += explorer.vy;

  // 冒険者をダンジョンエリア内に入れる
  contain(explorer, {x: 28, y: 10, width: 488, height: 480});
  //contain(explorer, stage);

  // 衝突判定前にexplorerHitをfalseにする
  let explorerHit = false;

  // 敵の配列内のスプライトをループ
  blobs.forEach(function(blob) {

    //ブロブを移動
    blob.y += blob.vy;

    // スクリーンの境界チェック
    let blobHitsWall = contain(blob, {x: 28, y: 10, width: 488, height: 480});

    // ブロブがステージの上や下にぶつかったら
    // 方向を反転
    if (blobHitsWall === "top" || blobHitsWall === "bottom") {
      blob.vy *= -1;
    }

    // 衝突判定
    // 敵と冒険者が触れていたらexplorerHitをtrueに
    if(hitTestRectangle(explorer, blob)) {
      explorerHit = true;
    }
  });

  // 冒険者がヒットしていたら...
  if(explorerHit) {

    // 冒険者を半透明
    explorer.alpha = 0.5;

    // 体力バーの内部の長方形の幅を1pixel減らす
    healthBar.outer.width -= 1;

  } else {

    // ヒットしていなければ冒険者を不透明に
    explorer.alpha = 1;
  }

  // 冒険者と宝物の衝突判定
  if (hitTestRectangle(explorer, treasure)) {

    // 宝物が冒険者に触れていたら冒険者の近くに
    treasure.x = explorer.x + 8;
    treasure.y = explorer.y + 8;
  }

  // 冒険者に体力はあるか? If the width of the `innerBar`
  // 体力が無くなればゲームを終了し負けを表示
  if (healthBar.outer.width < 0) {
    state = end;
    message.text = "You lost!";
  }

  // 冒険者が宝物を持ち出口まで行けば
  // ゲームを終了し負けを表示
  if (hitTestRectangle(treasure, door)) {
    state = end;
    message.text = "You won!";
  } 
}

function end() {
  gameScene.visible = false;
  gameOverScene.visible = true;
}

/* Helper functions */

function contain(sprite, container) {

  let collision = undefined;

  //Left
  if (sprite.x < container.x) {
    sprite.x = container.x;
    collision = "left";
  }

  //Top
  if (sprite.y < container.y) {
    sprite.y = container.y;
    collision = "top";
  }

  //Right
  if (sprite.x + sprite.width > container.width) {
    sprite.x = container.width - sprite.width;
    collision = "right";
  }

  //Bottom
  if (sprite.y + sprite.height > container.height) {
    sprite.y = container.height - sprite.height;
    collision = "bottom";
  }

  //Return the `collision` value
  return collision;
}

//The `hitTestRectangle` function
function hitTestRectangle(r1, r2) {

  //Define the variables we'll need to calculate
  let hit, combinedHalfWidths, combinedHalfHeights, vx, vy;

  //hit will determine whether there's a collision
  hit = false;

  //Find the center points of each sprite
  r1.centerX = r1.x + r1.width / 2; 
  r1.centerY = r1.y + r1.height / 2; 
  r2.centerX = r2.x + r2.width / 2; 
  r2.centerY = r2.y + r2.height / 2; 

  //Find the half-widths and half-heights of each sprite
  r1.halfWidth = r1.width / 2;
  r1.halfHeight = r1.height / 2;
  r2.halfWidth = r2.width / 2;
  r2.halfHeight = r2.height / 2;

  //Calculate the distance vector between the sprites
  vx = r1.centerX - r2.centerX;
  vy = r1.centerY - r2.centerY;

  //Figure out the combined half-widths and half-heights
  combinedHalfWidths = r1.halfWidth + r2.halfWidth;
  combinedHalfHeights = r1.halfHeight + r2.halfHeight;

  //Check for a collision on the x axis
  if (Math.abs(vx) < combinedHalfWidths) {

    //A collision might be occurring. Check for a collision on the y axis
    if (Math.abs(vy) < combinedHalfHeights) {

      //There's definitely a collision happening
      hit = true;
    } else {

      //There's no collision on the y axis
      hit = false;
    }
  } else {

    //There's no collision on the x axis
    hit = false;
  }

  //`hit` will be either `true` or `false`
  return hit;
};


//The `randomInt` helper function
function randomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

//The `keyboard` helper function
function keyboard(keyCode) {
  var key = {};
  key.code = keyCode;
  key.isDown = false;
  key.isUp = true;
  key.press = undefined;
  key.release = undefined;
  //The `downHandler`
  key.downHandler = function(event) {
    if (event.keyCode === key.code) {
      if (key.isUp && key.press) key.press();
      key.isDown = true;
      key.isUp = false;
    }
    event.preventDefault();
  };

  //The `upHandler`
  key.upHandler = function(event) {
    if (event.keyCode === key.code) {
      if (key.isDown && key.release) key.release();
      key.isDown = false;
      key.isUp = true;
    }
    event.preventDefault();
  };

  //Attach event listeners
  window.addEventListener(
    "keydown", key.downHandler.bind(key), false
  );
  window.addEventListener(
    "keyup", key.upHandler.bind(key), false
  );
  return key;
}

</script>
</body>