九、发射物

我们的游戏全是键盘控制,但是鼠标呢?这一章都是关于射弹的;如何解雇他们,他们如何移动。

在这一章,我们将添加一些新的,流行的键盘控制(WASD),鼠标瞄准和射击。在这个过程中,我们将添加一个自定义的十字准线。这将会很有趣....

自定义十字线

在显示自定义十字光标之前,我们需要一种方法来禁用默认的鼠标光标。幸运的是,CSS 已经包含了这种机制:

body {
  background: url("path/to/sprites/background.png");
  color: grey;
  font-family: helvetica, arial;
  font-size: 20px;
  cursor: none;
}

这是出自 http://codepen.io/assertchris/pen/YGaYvy

只需将cursor: none加到body上,我们就可以在游戏屏幕上隐藏默认光标。然后,让我们添加十字准线作为新的贴花:

const crosshair = new Decal(
  new PIXI.Sprite.fromImage(
    "path/to/sprites/crosshair.png",
  ),
  new PIXI.Rectangle(
    0, 0, 18, 18,
  ),
)

game.addObject(crosshair)
game.crosshair = crosshair

这是出自 http://codepen.io/assertchris/pen/YGaYvy

十字准线现在可以在游戏屏幕的左上角看到(缩小你的浏览器窗口)。不过,这不是我们想要的。我们真正想要的是它靠近玩家,但是在玩家中心和鼠标光标之间有一个角度。

让我们捕捉光标位置并计算鼠标光标和玩家之间的角度:

element.addEventListener("mousemove", (event) => {
  this.state.mouse.clientX = event.clientX
  this.state.mouse.clientY = event.clientY

  const rect = this.player.rectangle

  const centerX = (window.innerWidth / 2) + (rect.width / 2)
  const centerY = (window.innerHeight / 2) + (rect.height / 2)

  const deltaX = event.clientX - centerX
  const deltaY = centerY - event.clientY

  this.state.angle = Math.atan2(deltaY, deltaX)
})

这段代码出自 http://codepen.io/assertchris/pen/YGaYvy

这段代码读起来有点棘手,但它所做的只是获取鼠标光标和播放器之间的三角形的宽度和高度。我们从三角形中获取角度,并将其存储在游戏状态中。光标将使用这个(一旦我们用cursor = new Cursor替换cursor = new Decal):

class Ladder extends Box {
  get collides() {
    return false
  }
}

class LeftSlope extends Box {
  get collides() {
    return false
  }
}

class RightSlope extends Box {
  get collides() {
    return false
  }
}

class Decal extends Box {
  get collides() {
    return false

  }
}

class Crosshair extends Decal {
  animate(state) {
    const rect = state.player.rectangle

    const centerX = rect.x + (rect.width / 2)
    const centerY = rect.y + (rect.height / 2)
    const radius = 70

    const targetX = centerX + Math.cos(state.angle) * radius
    const targetY = centerY - Math.sin(state.angle) * radius

    this.sprite.x = targetX
    this.sprite.y = targetY
  }
}

这段代码出自 http://codepen.io/assertchris/pen/YGaYvy

在我们深入研究Crosshair.animate方法之前,请注意我是如何缩短LadderLeftSlopeRightSlopeDecal的定义的?我们可以使用extends关键字来继承Box的行为。

Note

继承并不总是带来好的代码架构,但是在这个简单的例子中,它对我们来说很好。

使用我们计算的角度(在mousemove事件监听器中),我们计算十字准线需要渲染的点(见图 9-1 )。

A435434_1_En_9_Fig1_HTML.jpg

图 9-1。

Fixed crosshair

自定义键

到目前为止,我们一直用箭头键移动播放器。如果我们只需要使用键盘,那没问题,但我们刚刚添加了鼠标目标。让我们把流行的 WASD 机芯钥匙添加到Player.animate:

animate(state) {
  if (state.keys[37] || state.keys[65]) { // left
    this.velocityX = Math.max(
      this.velocityX - this.accelerationX,
      this.maximumVelocityX * -1,
    )
  }

  if (state.keys[39] || state.keys[68]) { // right
    this.velocityX = Math.min(
      this.velocityX + this.accelerationX,
      this.maximumVelocityX,
    )
  }

   // ...velocity calculations

  state.objects.forEach((object) => {
    if (object === this) {
      return
    }

    const me = this.rectangle
    const you = object.rectangle
    const collides = object.collides

    if (me.x < you.x + you.width &&
        me.x + me.width > you.x &&
        me.y < you.y + you.height &&
        me.y + me.height > you.y) {

      if (object.constructor.name === "Ladder") {
        if (state.keys[38] || state.keys[40] ||
          state.keys[87] || state.keys[83]) {
          this.isOnLadder = true
          this.isOnGround = false
          this.velocityY = 0
          this.velocityX = 0
        }

        if (state.keys[38] || state.keys[87]) {
          this.rectangle.y -= this.climbingSpeed
        }

        if (state.keys[40] || state.keys[83] &&
          me.y + me.height < you.y + you.height) {
          this.rectangle.y += this.climbingSpeed
        }

        if (me.y <= you.x - me.height) {
          this.isOnLadder = false
        }

        return
      }

      // ...remaining collision detection
    }
  })

  // ...remaining calculations
}

This is from http://codepen.io/assertchris/pen/YGaYvy.

我们添加了state.keys[65]state.keys[68]作为可选的左右键。然后,为了上下移动梯子,我们还增加了state.keys[87]state.keys[83]

射击

最后,让我们添加向十字准线方向射击的功能。为此,我们需要创建另一种类型的对象:

class Bullet extends Decal {
  animate(state) {
    const rect = state.player.rectangle

    this.x = this.x || rect.x + rect.width
    this.y = this.y || rect.y + (rect.height / 2)

    this.angle = this.angle || state.angle
    this.rotation = this.rotation || state.rotation

    this.radius = this.radius || 0
    this.radius += 15

    const targetX = this.x + Math.cos(this.angle) * this.radius
    const targetY = this.y - Math.sin(this.angle) * this.radius

    this.sprite.x = targetX
    this.sprite.y = targetY
    this.sprite.rotation = this.rotation
  }
}

这是出自 http://codepen.io/assertchris/pen/YGaYvy

与大多数其他对象类型的动画不同,它会随着每一次滴答修改其状态。this.x = this.x || something的模式对于初始化一次值很有用。重要的是,我们只在子弹发射时存储像xyanglerotation这样的东西,否则它们会随着我们移动玩家或十字准线而不断变化。

radius从玩家的前方开始,随着子弹射出时十字准线的方向增加。我们也旋转子弹精灵来匹配相同的角度。这要求我们将rotationangle一起存储在鼠标事件监听器中。我们还必须在单击鼠标时创建新的项目符号:

element.addEventListener("mousedown", (event) => {
  this.state.clicks[event.which] = {
    "clientX": event.clientX,
    "clientY": event.clientY,
  }

  if (event.button === 0) { // left click
    const rect = this.player.rectangle

    const bullet = new Bullet(
      new PIXI.Sprite.fromImage(
        "path/to/sprites/bullet.png",
      ),
      new PIXI.Rectangle(
        rect.x + rect.width, rect.y, 8, 8,
      ),
    )

    this.addObject(bullet)

    setTimeout(() => {
      this.removeObject(bullet)
    }, 250)
  }
})

element.addEventListener("mousemove", (event) => {
  this.state.mouse.clientX = event.clientX
  this.state.mouse.clientY = event.clientY

  const rect = this.player.rectangle

  const centerX = (window.innerWidth / 2) + (rect.width / 2)
  const centerY = (window.innerHeight / 2) + (rect.height / 2)

  const deltaX = event.clientX - centerX
  const deltaY = centerY - event.clientY

  const rotationX = event.clientX - centerX
  const rotationY = event.clientY - centerY

  this.state.angle = Math.atan2(deltaY, deltaX)
  this.state.rotation = Math.atan2(rotationY, rotationX)
})

// ...remaining event listeners

这是出自 http://codepen.io/assertchris/pen/YGaYvy

我们计算rotation的方式与计算angle的方式类似,但是我们翻转了垂直轴。当玩家点击时,我们创建一个新的bullet(位于玩家前面)并将其添加到游戏中。

250毫秒后,我们要取出子弹。这是为了当不再需要项目符号时,它们不会减慢动画周期。我们需要将removeObject方法添加到Game类中:

removeObject(object) {
  this.state.objects = this.state.objects.filter(
    function(next) {
      return next !== object
    }
  )

  this.stage.removeChild(object.sprite)
}

这是出自 http://codepen.io/assertchris/pen/YGaYvy

我们将在下一章创建怪物射击时探索更多的投射行为。

摘要

在这一章中,我们看了创建自定义光标和发射物体的方法。我们发现了一点有用的三角学,将十字准线限制在玩家周围的某个半径范围内,然后将子弹推过这个范围。

试验你的投射物的速度和外观。也许你正在建造一个中世纪的游戏,你的投射物是魔法箭。考虑一下增加垂直加速度,这样你的子弹会在一段距离后落到地上。