<canvas id="main"></canvas>
* {padding: 0; margin: 0}
const frames = {
  "frames": {
    "blob.png": {
      "frame": {
        "x": 55,
        "y": 2,
        "w": 32,
        "h": 24
      },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": {
        "x": 0,
        "y": 0,
        "w": 32,
        "h": 24
      },
      "sourceSize": {
        "w": 32,
        "h": 24
      },
      "pivot": {
        "x": 0.5,
        "y": 0.5
      }
    },
    "door.png": {
      "frame": {
        "x": 89,
        "y": 2,
        "w": 32,
        "h": 32
      },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": {
        "x": 0,
        "y": 0,
        "w": 32,
        "h": 32
      },
      "sourceSize": {
        "w": 32,
        "h": 32
      },
      "pivot": {
        "x": 0.5,
        "y": 0.5
      }
    },
    "dungeon.png": {
      "frame": {
        "x": 2,
        "y": 36,
        "w": 512,
        "h": 512
      },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": {
        "x": 0,
        "y": 0,
        "w": 512,
        "h": 512
      },
      "sourceSize": {
        "w": 512,
        "h": 512
      },
      "pivot": {
        "x": 0.5,
        "y": 0.5
      }
    },
    "explorer.png": {
      "frame": {
        "x": 2,
        "y": 2,
        "w": 21,
        "h": 32
      },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": {
        "x": 0,
        "y": 0,
        "w": 21,
        "h": 32
      },
      "sourceSize": {
        "w": 21,
        "h": 32
      },
      "pivot": {
        "x": 0.5,
        "y": 0.5
      }
    },
    "treasure.png": {
      "frame": {
        "x": 25,
        "y": 2,
        "w": 28,
        "h": 24
      },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": {
        "x": 0,
        "y": 0,
        "w": 28,
        "h": 24
      },
      "sourceSize": {
        "w": 28,
        "h": 24
      },
      "pivot": {
        "x": 0.5,
        "y": 0.5
      }
    }
  },
  "meta": {
    "app": "http://www.codeandweb.com/texturepacker",
    "version": "1.0",
    "image": "https://i.imgur.com/yxmphGW.png",
    "format": "RGBA8888",
    "size": {
      "w": 512,
      "h": 512
    },
    "antialias": true,
    "transparent": false,
    "backgroundColor": "0x000000",
    "scale": "1",
    "smartupdate": "$TexturePacker:SmartUpdate:51ede84c7a85e4d6aeb31a6020a20858:3923663e59fb40b578d66a492a2cda2d:9995f8b4db1ac3cb75651b1542df8ee2$"
  }
}

const app = new PIXI.Application({
  view: document.getElementById('main'),
  width: frames.meta.size.w,
  height: frames.meta.size.h,
  antialias: frames.meta.antialias,
  transparent: frames.meta.transparent,
  backgroundColor: frames.meta.backgroundColor,
});


const loader = new PIXI.Loader();
loader
  .add('gameimg',frames.meta.image)
  .load((loader, resource)=> {
  init(resource);
})

const gameRun = new PIXI.Container();
gameRun.visible = true;
app.stage.addChild(gameRun);

const gameStart = new PIXI.Container();
gameStart.visible = false;
app.stage.addChild(gameStart);

const gameOver = new PIXI.Container();
gameOver.visible = false;
app.stage.addChild(gameOver);

const gameWin = new PIXI.Container();
gameWin.visible = false;
app.stage.addChild(gameWin);

const hpContainer = new PIXI.Container();

function gameSprite(item, resource) {
  const pixiRectangle = new PIXI.Rectangle(frames.frames[item].frame.x, frames.frames[item].frame.y, frames.frames[item].frame.w, frames.frames[item].frame.h);
  let newTex = new PIXI.Texture(resource.gameimg.texture, pixiRectangle);
  const sprite = new PIXI.Sprite(newTex);
  return sprite;
}


let dungeon, blob, treasure, door, explorer,blobOptions;
let gameHP = 100;
let getTreasure = false;
let gamePlay = false;
const blobs = [];

function init(resource) {
  // 載入地牢
  dungeon = gameSprite('dungeon.png', resource);
  gameStart.addChild(dungeon);

  // 載入怪物
  blobOptions = {
    number: 8,
    speeds: 5,
  };

  for(let i = 0; i < blobOptions.number; i += 1) {
    blob = gameSprite('blob.png', resource);
    blob.x = 120 + (blobOptions.number + 32) * i;
    blob.y = frames.meta.size.h / 2;
    blob.vy = blobOptions.speeds + randomInt(0, 3);
    blobs.push(blob);
    gameStart.addChild(blob);
  }

  // 寶箱
  treasure = gameSprite('treasure.png', resource);
  treasure.x = 400;
  treasure.y = frames.meta.size.h / 2;
  gameStart.addChild(treasure);

  // 逃出門
  door = gameSprite('door.png', resource);
  door.x = 50;
  door.y = 0;
  gameStart.addChild(door);

  // 玩家
  explorer = gameSprite('explorer.png', resource);
  explorer.x = 50;
  explorer.y = frames.meta.size.h / 2;
  explorer.vx = 0;
  explorer.vy = 0;
  gameStart.addChild(explorer);

  HPstatus();

  const run = messages('按下 Enter 開始遊戲');
  run.x = frames.meta.size.w / 2 - run.width /2;
  run.y = frames.meta.size.h / 2 - run.height /2;
  gameRun.addChild(run);

  const over = messages('Game Over');
  over.x = frames.meta.size.w / 2 - over.width /2;
  over.y = frames.meta.size.h / 2 - over.height /2;
  gameOver.addChild(over);

  const Win = messages('你贏了!');
  Win.x = frames.meta.size.w / 2 - Win.width /2;
  Win.y = frames.meta.size.h / 2 - Win.height /2;
  gameWin.addChild(Win);

  // 重新開始按鈕繪畫
  const resetBtn = new PIXI.Container();
  gameOver.addChild(resetBtn);

  let gameGraphics = new PIXI.Graphics();
  gameGraphics.beginFill(0x33BBFF);
  gameGraphics.drawRoundedRect(0, 0, 120, 50, 25);
  gameGraphics.endFill();
  gameGraphics.x = frames.meta.size.w / 2 - gameGraphics.width / 2;
  gameGraphics.y = frames.meta.size.h / 2 - gameGraphics.height / 2 + 60;
  resetBtn.addChild(gameGraphics);

  const resetText = new PIXI.Text('重新開始', {  // 改用變數傳入
    fontFamily: 'Microsoft JhengHei',
    fontSize: 16,
    fill: [0xFFFFFF],
    align: 'center'
  });
  resetText.x = frames.meta.size.w / 2 - resetText.width / 2;
  resetText.y = frames.meta.size.h / 2 - resetText.height / 2 + 60;
  resetBtn.addChild(resetText);
  // 設置互動
  resetBtn.interactive = true;
  resetBtn.buttonMode = true;

  resetBtn.click = gameReset;

  // 遊戲獲勝繪畫
  const resetWinBtn = new PIXI.Container();
  gameWin.addChild(resetWinBtn);

  let gameWinGraphics = new PIXI.Graphics();
  gameWinGraphics.beginFill(0x33BBFF);
  gameWinGraphics.drawRoundedRect(0, 0, 120, 50, 25);
  gameWinGraphics.endFill();
  gameWinGraphics.x = frames.meta.size.w / 2 - gameGraphics.width / 2;
  gameWinGraphics.y = frames.meta.size.h / 2 - gameGraphics.height / 2 + 60;
  resetWinBtn.addChild(gameWinGraphics);

  const resetWinText = new PIXI.Text('重新挑戰', { 
    fontFamily: 'Microsoft JhengHei',
    fontSize: 16,
    fill: [0xFFFFFF],
    align: 'center'
  });
  resetWinText.x = frames.meta.size.w / 2 - resetWinText.width / 2;
  resetWinText.y = frames.meta.size.h / 2 - resetWinText.height / 2 + 60;
  resetWinBtn.addChild(resetWinText);
  // 設置互動
  resetWinBtn.interactive = true;
  resetWinBtn.buttonMode = true;

  resetWinBtn.click = gameReset;


  app.ticker.add((delta) => {
    gameLoop(delta);
  });
}

function gameReset() {
  // HP 回復
  gameHP = 100;
  // 初始化寶箱
  getTreasure = false;
  // 玩家回歸初始位置
  explorer.x = 50;
  explorer.y = frames.meta.size.h / 2;
  // 寶箱回歸初始位置
  treasure.x = 400;
  treasure.y = frames.meta.size.h / 2;
  // 重新調整怪物速率
  blob.vy = blobOptions.speeds + randomInt(0, 3);
  console.log(blob.vy);
  // 重新設置 Container 顯示
  gameRun.visible = true;
  gameStart.visible = false;
  gameOver.visible = false;
  gameWin.visible = false;
}

function gameLoop() {
  if(gameHP <= 0) {
    gameStart.visible = false;
    gameOver.visible = true;
  }

  contain(explorer, {x: 28, y: 10, width: 488, height: 480});

  let hitStatus = false;

  blobs.forEach((item) => {
    item.y += item.vy;
    const blobsHit = contain(item, {x: 28, y: 10, width: 488, height: 480})
    if(blobsHit === 'top' || blobsHit === 'bottom') {
      item.vy *= -1;
    }
    if(boxesIntersect(explorer, item)) {
      hitStatus = true;
    }
  })

  if(hitStatus) {
    // 碰撞後扣除血量
    if(explorer.alpha !== 1) return
    const getRandom = randomInt(1, 5);
    gameHP -= getRandom;
    explorer.alpha = 0.1;
    hurt(getRandom);
  } else {
    explorer.alpha = 1;
  }

  if(boxesIntersect(explorer, treasure)) {
    treasure.x = explorer.x + 8;
    treasure.y = explorer.y + 8;
    getTreasure = true;
  }

  if(boxesIntersect(explorer, door) && getTreasure) {
    gameWin.visible = true;
    gameStart.visible = false;
    // 玩家回歸初始位置,避免衝突
    explorer.x = 50;
    explorer.y = frames.meta.size.h / 2;
  }

  // 重新調整血量
  hpContainer.hpStatus.width = gameHP;
}

function HPstatus() {
  let border = new PIXI.Graphics();
  border.beginFill(0x000000);
  border.drawRect(0, 0, 100, 10, 10);
  border.endFill();
  hpContainer.addChild(border);

  let borderFill = new PIXI.Graphics();
  borderFill.beginFill(0xFF0000);
  borderFill.drawRect(0, 0, 100, 10, 10);
  borderFill.endFill();
  hpContainer.addChild(borderFill);
  hpContainer.hpStatus = borderFill;

  hpContainer.x = 340;
  hpContainer.y = 10;


  gameStart.addChild(hpContainer);
}

function hurt(item) {
  if(gameHP <= 0) return;
  const containerEffect  = new PIXI.Container();
  const graphics = new PIXI.Graphics();
  const startText = new PIXI.Text(item ,{
    fill: 0xDDD00D,
  });
  graphics.beginFill(0x9D482E);
  graphics.drawStar(0, 0, 10, 30);
  graphics.endFill();

  graphics.x = explorer.x;
  graphics.y = explorer.y - 50;

  startText.x = graphics.x - startText.width / 2;
  startText.y = graphics.y - startText.height / 2;

  containerEffect.addChild(graphics);
  containerEffect.addChild(startText);

  app.stage.addChild(containerEffect);

  removeContainer(containerEffect);
}

function removeContainer(item) {
  const s = setInterval(() => {
    item.alpha -= 0.05;
  },100);
  setTimeout(() => {
    clearInterval(s);
    app.stage.removeChild(item);
  },10000);
}

function messages(text) {
  const style = new PIXI.TextStyle({
    fontFamily: 'Microsoft JhengHei', // 風格
    fontSize: 24, // 字體大小
    fill: [0xEEEE00,0x00ff99], // 填滿,若是陣列則可以漸層效果
    align: 'center', // 對齊
    stroke: '#000000', // 外框顏色
  });
  const messages = new PIXI.Text(text, style);
  return messages;
}

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

const body = document.querySelector('body');

body.addEventListener('keydown',(e) => {
  console.log(e.keyCode)
  switch(e.keyCode) {
    case 38:
    case 87:
      if(!gamePlay) return;
      explorer.y -= 10;
      break;
    case 40:
    case 83:
      if(!gamePlay) return;
      explorer.y += 10;
      break;
    case 37:
    case 65:
      if(!gamePlay) return;
      explorer.x -= 10;
      break;
    case 39:
    case 68:
      if(!gamePlay) return;
      explorer.x += 10;
      break;
    case 13:  
      if(gameHP > 0) {
        gameRun.visible = false;
        gameStart.visible = true;
        gamePlay = true;
      }
      break;
    default:
      break;
  }
})

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;
}

function boxesIntersect(a, b){
  var ab = a.getBounds();
  var bb = b.getBounds();
  return ab.x + ab.width > bb.x && ab.x < bb.x + bb.width && ab.y + ab.height > bb.y && ab.y < bb.y + bb.height;
}

const stats = new Stats();
stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
stats.domElement.style.position = 'absolute';
stats.domElement.style.right = '0px';
stats.domElement.style.top = '0px';
document.body.appendChild(stats.domElement);
function animate() {
  stats.begin();
  // monitored code goes here
  stats.end();
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.1.3/pixi.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js
  4. https://cdnjs.cloudflare.com/ajax/libs/stats.js/r16/Stats.min.js