使用 Pixi.js 插件实现探险者小游戏(二)

使用 Pixi.js 插件实现探险者小游戏(一)中我们学习了如何创建精灵图,这节我们要让精灵图动起来。

精灵图布局

游戏画面如下图所示,我们要生成一个围墙,探险者、恶魔、宝物都在这个围墙里面。探险者可以上下左右移动,恶魔只能上下移动,宝物是不动的。探险者与宝物被恶魔群隔开,探险者需要移动到宝物处才能获得游戏的胜利。

游戏界面

1. 使用砖块生成围墙

loader
  .add("images/textureMap.json")
  .load(intLoad)

function intLoad() {
  const textures = _loader.resources['images/textureMap.json'].textures
  // 添加纹理墙
  const walls = addWall(textures['1.png'])
  walls.forEach(wall => {
    _stage.addChild(wall)
  })
}

function addWall(texture) {
  const walls = []
  // 生成第一堵墙
  for (let i = 0; i < 15; i++) {
    let wall = new _Sprite(texture)
    wall.height = 40
    wall.width = 40
    wall.x = i * 40
    wall.y = 0
    walls.push(wall)
  }
  // 生成第二堵墙
  for (let i = 1; i < 15; i++) {
    let wall = new _Sprite(texture)
    wall.height = 40
    wall.width = 40
    wall.x = 0
    wall.y = 40 * i
    walls.push(wall)
  }
  // 生成第三堵墙
  for (let i = 1; i < 15; i++) {
    let wall = new _Sprite(texture)
    wall.height = 40
    wall.width = 40
    wall.x = 40 * i
    wall.y = 560
    walls.push(wall)
  }
  // 生成第四堵墙
  for (let i = 1; i < 14; i++) {
    let wall = new _Sprite(texture)
    wall.height = 40
    wall.width = 40
    wall.x = 560
    wall.y = 40 * i
    walls.push(wall)
  }
  return walls
}

2. 添加探险者

探险者是可以运动的,监听的是键盘的方向键。探险者的运动只能在围墙内部进行。使用 Pixi 的 ticker 创建一个循环函数,这被称为游戏循环(game loop)。放入app.ticker中的回调函数可以被执行60次每秒。精灵的上下左右移动速度可以使用精灵的 vx、vy 属性。水平方向上 vx=0 表示水平方向不动,vx=-1 表示每次更新向左移动 1px,vx=1 表示每次更新向右移动 1px;同理,vy=0 表示垂直方向不动,vy=-1 表示每次更新向上移动 1px,vy=1 表示每次更新向下移动 1px。

loader
  .add("images/textureMap.json")
  .load(intLoad)

const wallWidth = 40 // 墙的宽度

function intLoad() {
  const textures = _loader.resources['images/textureMap.json'].textures
  // 添加纹理墙
  const walls = addWall(textures['1.png'])
  walls.forEach(wall => {
    _stage.addChild(wall)
  })
  // 添加探索者
  let explorer = addExplorer(textures['2.png'])
  _stage.addChild(explorer)
  app.ticker.add(() => explorerMove(explorer, wallWidth))
}

// 添加探索者
function addExplorer(texture) {
  let explorer = new _Sprite(texture)
  explorer.height = 40
  explorer.width = 40
  explorer.x = 60
  // 垂直居中
  explorer.y = _stage.height / 2 - explorer.height / 2
  explorer.vx = 0 // x轴速度,0为不动,-1为向左,1为向右
  explorer.vy = 0 // y轴速度,0为不动,-1为向上,1为向下
  return explorerEvent(explorer)
}

// 给探险者添加键盘事件
function explorerEvent(explorer) {
  // 为探索者绑定键盘事件
  const left = keyboard('ArrowLeft')
  const right = keyboard('ArrowRight')
  const up = keyboard('ArrowUp')
  const down = keyboard('ArrowDown')
  const wallWidth = 40 // 墙的宽度
  // 按下左箭头键
  left.press = () => {
    // 右箭头键按下时,左箭头键不生效
    if (right.isDown) return
    if (explorer.x <= wallWidth) {
      // 左移到墙的左侧时,左箭头键不生效
      explorer.vx = 0
      explorer.x = wallWidth
      return
    }
    explorer.vx = -5
    explorer.vy = 0
  }
  // 松开左箭头键
  left.release = () => {
    // 如果右箭头键没有按下,左箭头松开时精灵x轴不动
    if (!right.isDown && explorer.vy === 0) {
      explorer.vx = 0
    }
  }
  // 按下右箭头键
  right.press = () => {
    // 左箭头键按下时,右箭头键不生效
    if (left.isDown) return
    if (explorer.x >= _stage.width - explorer.width - wallWidth) {
      // 右移到墙的右侧时,右箭头键不生效
      explorer.vx = 0
      explorer.x = _stage.width - explorer.width - wallWidth
      return
    }
    explorer.vx = 5
    explorer.vy = 0
  }
  // 松开右箭头键
  right.release = () => {
    // 如果左箭头键没有按下,右箭头松开时精灵x轴不动
    if (!left.isDown && explorer.vy === 0) {
      explorer.vx = 0
    }
  }
  // 按下上箭头键
  up.press = () => {
    // 下箭头键按下时,上箭头键不生效
    if (down.isDown) return
    if (explorer.y <= wallWidth) {
      explorer.vy = 0
      explorer.y = wallWidth
      return
    }
    explorer.vx = 0
    explorer.vy = -5
  }
  // 松开上箭头键
  up.release = () => {
    // 如果下箭头键没有按下,上箭头松开时精灵y轴不动
    if (!down.isDown && explorer.vx === 0) {
      explorer.vy = 0
    }
  }
  // 按下下箭头键
  down.press = () => {
    // 上箭头键按下时,下箭头键不生效
    if (up.isDown) return
    if (explorer.y >= _stage.height - explorer.height - wallWidth) {
      explorer.vy = 0
      explorer.y = _stage.height - explorer.height - wallWidth
      return
    }
    explorer.vx = 0
    explorer.vy = 5
  }
  // 松开下箭头键
  down.release = () => {
    // 如果上箭头键没有按下,下箭头松开时精灵y轴不动
    if (!up.isDown && explorer.vx === 0) {
      explorer.vy = 0
    }
  }
  return explorer
}

// 键盘
function keyboard(value) {
  let key = {}
  key.value = value
  key.isDown = false
  key.isUp = true
  key.press = undefined
  key.release = undefined
  // 键盘按下
  key.downHandler = event => {
    if (event.key === key.value) {
      if (key.isUp && key.press) key.press()
      key.isDown = true
      key.isUp = false
      event.preventDefault()
    }
  }
  // 键盘弹起
  key.upHandler = event => {
    if (event.key === key.value) {
      if (key.isDown && key.release) key.release()
      key.isDown = false
      key.isUp = true
      event.preventDefault()
    }
  }

  // 订阅事件
  const downListener = key.downHandler.bind(key)
  const upListener = key.upHandler.bind(key)

  window.addEventListener('keydown', downListener, false)
  window.addEventListener('keyup', upListener, false)

  // 取消订阅事件
  key.unsubscribe = () => {
    window.removeEventListener('keydown', downListener)
    window.removeEventListener('keyup', upListener)
  }

  return key
}

function explorerMove(sprite, wallWidth) {
  sprite.x += sprite.vx
  if (sprite.x <= wallWidth) {
    sprite.x = wallWidth
  }
  if (sprite.x >= _stage.width - sprite.width - wallWidth) {
    sprite.x = _stage.width - sprite.width - wallWidth
  }
  sprite.y += sprite.vy
  if (sprite.y <= wallWidth) {
    sprite.y = wallWidth
  }
  if (sprite.y >= _stage.height - sprite.height - wallWidth) {
    sprite.y = _stage.height - sprite.height - wallWidth
  }
}

3. 添加宝物

loader
  .add("images/textureMap.json")
  .load(intLoad)

const wallWidth = 40 // 墙的宽度

function intLoad() {
  const textures = _loader.resources['images/textureMap.json'].textures
  // 添加纹理墙
  const walls = addWall(textures['1.png'])
  walls.forEach(wall => {
    _stage.addChild(wall)
  })
  // 添加探索者
  let explorer = addExplorer(textures['2.png'])
  _stage.addChild(explorer)
  app.ticker.add(() => explorerMove(explorer, wallWidth))
  // 添加宝物
  const treasure = addTreasure(textures['4.png'])
  _stage.addChild(treasure)
}

// 添加宝物
function addTreasure(texture) {
  let treasure = new _Sprite(texture)
  treasure.height = 40
  treasure.width = 40
  treasure.x = _stage.width - treasure.width - wallWidth
  // 垂直居中
  treasure.y = _stage.height / 2 - treasure.height / 2
  return treasure
}

4. 添加拦截者

阻拦者有很多,每个阻拦者垂直方向上的位置是随机的。

loader
  .add("images/textureMap.json")
  .load(intLoad)

const wallWidth = 40 // 墙的宽度

function intLoad() {
  const textures = _loader.resources['images/textureMap.json'].textures
  // 添加纹理墙
  const walls = addWall(textures['1.png'])
  walls.forEach(wall => {
    _stage.addChild(wall)
  })
  // 添加探索者
  let explorer = addExplorer(textures['2.png'])
  _stage.addChild(explorer)
  app.ticker.add(() => explorerMove(explorer, wallWidth))
  // 添加宝物
  const treasure = addTreasure(textures['4.png'])
  _stage.addChild(treasure)
  // 添加拦截者
  const interceptors = addInterceptor(textures['3.png'], 8)
  interceptors.forEach(interceptor => {
    _stage.addChild(interceptor)
    // 拦截者运动
    app.ticker.add(() => interceptorMove(interceptor, _stage, wallWidth))
  })
}

// 添加拦截者
function addInterceptor(texture, n) {
  const interceptors = []
  // 生成8个拦截者
  const xOffset = 120 // x偏移量
  const count = n // 拦截者数量
  const spacing = 40 // 水平间隔
  for (let i = 0; i < count; i++) {
    let interceptor = new _Sprite(texture)
    interceptor.height = 40
    interceptor.width = 40
    interceptor.x = xOffset + i * spacing
    // 随机生成y坐标
    interceptor.y = randomInt(40, _stage.height - interceptor.height - 40)
    interceptor.vy = Math.round(Math.random()) ? 1 : -1 // y轴速度,0为不动,-1为向上,1为向下
    interceptors.push(interceptor)
  }
  return interceptors
}

function interceptorMove(sprite, _stage, wallWidth) {
  sprite.y += sprite.vy
  if (sprite.y <= wallWidth) {
    sprite.y = wallWidth
    sprite.vy = 1
  }
  if (sprite.y >= _stage.height - sprite.height - wallWidth) {
    sprite.y = _stage.height - sprite.height - wallWidth
    sprite.vy = -1
  }
}

// 随机数min~max
function randomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min
}

5. 添加碰撞检测

现在按键盘方向键时探险者可以移动起来了,拦截者们也上下移动起来了,现在还有两个碰撞检测没有添加。探险者与宝物碰撞检测,检测到碰撞后游戏胜利;探险者与阻拦者之间的碰撞检测,检测到碰撞后,游戏失败。
现在对上面的代码进行改造。检测碰撞的函数hitTestRectangle不再提供,请下载源码查看。

loader
  .add("images/textureMap.json")
  .load(intLoad)

const wallWidth = 40 // 墙的宽度
let gameState = 'playing' // 游戏状态:playing、win、lose
let hitInterceptor = false // 是否碰撞拦截者
let hitTreasure = false // 是否碰撞宝物
let stateTips // 游戏状态提示

function intLoad() {
  const textures = _loader.resources['images/textureMap.json'].textures
  // 添加纹理墙
  const walls = addWall(textures['1.png'])
  walls.forEach(wall => {
    _stage.addChild(wall)
  })
  // 添加探索者
  let explorer = addExplorer(textures['2.png'])
  _stage.addChild(explorer)
  // 添加宝物
  const treasure = addTreasure(textures['4.png'])
  _stage.addChild(treasure)
  // 每次更新都要检测探险者与宝物是否碰撞
  app.ticker.add(() => updateAdnHitTest1(explorer, treasure, _stage, wallWidth))
  // 添加拦截者
  const interceptors = addInterceptor(textures['3.png'], 8)
  interceptors.forEach(interceptor => {
    _stage.addChild(interceptor)
    // 每次更新检测每个拦截者与探险者是否碰撞
    app.ticker.add(() => updateAdnHitTest2(explorer, interceptor, _stage, wallWidth))
  // 添加文字
  stateTips = addText(gameState, 60, 6)
  stateTips.style.fill = 0x000000
  _stage.addChild(stateTips)
})
}

// 检测探险者与宝物是否碰撞
function updateAdnHitTest1(explorer, treasure, _stage, wallWidth) {
  explorerMove(explorer, wallWidth)
  // 检测碰撞宝物
  hitTreasure = hitTestRectangle(explorer, treasure)
  if (hitTreasure) {
    gameState = 'win'
    stateTips.text = 'You Win'
    stateTips.style.fill = 0x00ff00
    // 停止游戏
    app.ticker.stop()
    // 停止探索者的移动
    explorer.vx = 0
    explorer.vy = 0
  }
}

// 刷新拦截者的位置及检测碰撞
function updateAdnHitTest2(explorer, interceptor, _stage, wallWidth) {
  interceptorMove(interceptor, _stage, wallWidth)
  // 探险者与拦截者进行碰撞检测
  hitInterceptor = hitTestRectangle(explorer, interceptor)
  if (hitInterceptor) {
    gameState = 'lose'
    stateTips.text = 'Game Over'
    stateTips.style.fill = 0xff0000
    // 停止游戏
    app.ticker.stop()
    // 停止探索者的移动及置灰
    explorer.vx = 0
    explorer.vy = 0
    explorer.alpha = 0.6
  }
}

到此,简单的探宝游戏就开发完成了,游戏很简单,没有难度级别和关卡设置,后续会完善上这些功能。如果觉得有收获就点赞收藏下吧或者 github 上给个星星吧。
源码地址:https://github.com/zhench0515/explorer-game

posted @ 2025-03-11 14:48  老甄Home  阅读(42)  评论(0)    收藏  举报