使用相位器进行回合制战斗

我们的英雄面对一群巨魔。 每个人都按照优先级队列系统轮流攻击。 每个单元都有一个priority编号和一个speed因数,该speed因数决定了下一个转向的时间。

一个基于回合制的简单战斗教程,灵感来自Darkest Dungeons和Xulima领主,使用优先级队列数据结构。 您可以在底部看到预览(10MB)并链接到实际演示。

排队系统

这个想法是每个人都有一个优先级数字。 每次攻击都会将攻击者的优先级编号提高到最大,并且每个周期(意味着我们执行的每个攻击场景)都会随着速度降低每个单元的优先级编号。 接下来是攻击次数最少的单位。 我们将使用优先级队列来存储这些信息。

步骤如下:

  1. create() —创建队列并放置所有人,除一个英雄的优先级数字外,其他所有事件的优先级都随机设置,以模拟战斗主动性。
  2. create() —一个英雄的优先级号码最初设置为1,因此我们的玩家将首先进行攻击。
  3. updateQueue() or in onInputDown() —当发生攻击时,我们取出队列中的第一个。
  4. attack() —执行攻击场景并提高攻击者的优先级。
  5. updateQueue () —更新队列时,我们会降低每个单元的优先级。
  6. updateQueue() —将我们前面取出的那个添加回队列

您必须尝试使用​​最大优先级数。 这个想法是,如果一个单元与下一个单元具有相同的优先级,则它不会将该单元置于其前面。 这提供了一种一致的方式来告诉我们的玩家下一个攻击者将是谁。 为您的优先级数字设置很高的最大值有时会使下一跳的单元跳到其他所有人的前面。

视觉队列

我们向玩家提供视觉反馈,以便他们了解更多信息并制定下一步的策略。 我们从单元图集上拉出头部,并使用新的sprite创建人像并将其附加到其他sprite group ,这样我们就可以将它们完全移动到一起。

 createPortrait() { 
let spriter = this .spriter
 // create a bordered portrait 
// image head pulled from the spritesheet
let portrait = game.add.image(0, 0, spriter.entity.name, 'head')
let border = game.add.graphics(0, 0)
border.lineStyle(10, 0xffffff)
border.drawRect(0, 0, portrait.width, portrait.height)
portrait.addChild(border)
// scale down
portrait.width = 70
portrait.height = 70
 // we want to show their priority number 
// style the text with a translucent background fill
let style = { font: "20px Arial", fill: "#ffffff", backgroundColor: "rgba(0, 0, 0, 0.8)" }
let text = game.add.text(0, 0, this .priority, style)
// don't scale with the portrait
text.setScaleMinMax(1, 1);
// and show it to the top left
text.anchor.set(0)
portrait.addChild(text)
 // storing references 
portrait.text = text
this .portrait = portrait
}

在显示它们时,我们从队列中拉出数组并遍历每个数组,并在sprite组中设置它们的位置。

 updateVisualQueue() { 
for ( let i = 0; i < this .queue.array.length; i++) {
// pull the portrait from the unit
let unit = this .queue.array[i].value
let portrait = unit.portrait;
// add a little margin to set them apart
let posx = i * (portrait.width + 5)
// for the portrait we're adding back in, we just want it to popout and do a fade-in
if (portrait.alpha == 0) {
portrait.x = posx
this .game.add.tween(portrait).to( { alpha: 1 }, 500, Phaser.Easing.Linear.None, true )
} else {
// all the rest moves
this .game.add.tween(portrait).to( { x: posx }, 300, Phaser.Easing.Linear.None, true )
}
 // show their priority number 
portrait.text.setText(' ' + unit.priority + ' ')
}
}

攻击场景

我们的攻击场景将从攻击者目标开始 ,并按比例调整大小和背景变暗-为我们的玩家提供更具戏剧性和吸引力的场景。

我们从取消玩家的控制权开始,因为我们需要攻击场景在再次输入之前完全播放完。

第二步是使用黑色填充的Phaser.BitmapData降低不透明度,从而使背景变暗。

 let bmd = game.add.bitmapData(game.width, game.height) 
bmd.ctx.fillStyle = "rgba(0, 0, 0, 0.7)"
bmd.ctx.fillRect(0,0, game.width, game.height)
 this .attackSceneBg = game.add.sprite(0, 0, bmd) 
this .attackSceneBg.alpha = 0

attackSceneBg.alpha设置将应用于精灵而不是位图,因此在游戏开始时不会显示,我们以后可以应用淡入淡出效果。

接下来,我们通过确定攻击者目标的大小,重新定位并显示一些动画来开始设置攻击者目标

 attack() { 
let spriter = this .spriter
 // almost double up their size 
// note that we are not setting them, but instead multiplying them to the existing value
spriter.scale.x *= 1.75
spriter.scale.y *= 1.75
// start on the center of the game, offset (and some) by the width of the attacker
spriter.x = game.world.centerX - spriter.width - 100
// play a quick animation
spriter.setAnimationSpeedPercent(200)
spriter.playAnimationByName('ATTACK')
}

目标同样适用,尽管我们需要考虑行动的顺序。

  1. 攻击者目标移动到另一个Sprite组,以便它们显示在我们之前创建的暗淡背景上
  2. 攻击者和目标都扩大了规模
  3. 攻击者启动ATTACK动画
  4. 等待攻击动画的一半,然后在目标上启动HURT动画
  5. 在目标中心显示血腥效果
  6. 在重置场景之前,请等待一秒钟以允许执行上述所有步骤
  7. 再等一秒钟,以便重置场景完全播放完,然后再将控制权交还给玩家
 hurt(blood) { 
let spriter = this .spriter
 // almost double up their size 
// note that we are not setting them, but instead multiplying them to the existing value
spriter.scale.x *= 1.75
spriter.scale.y *= 1.75
// start on the center of the game, offset (and some) by the width of the attacker
spriter.x = game.world.centerX - spriter.width - 100
 // wait for a bit for the attacker's ATTACK animation to play out a bit 
game.time.events.add(300, () => {
// and just about time the attack animation lands it's blow
// we play the target's HURT animation
spriter.setAnimationSpeedPercent(200)
spriter.playAnimationByName('HURT')
// shake the camera
game.juicy.shake(15)
 // using the spriter's position 
// we can more or less center the blood effect at the unit's body
let x = spriter.x;
let y = spriter.y
blood.position.set(x, y)
// show the blood effect once
blood.visible = true
blood.play('blood', 15, false )
})
}

重置场景只是恢复精灵的原始位置和大小,并淡化我们放在顶部的背景。 那就是alpha属性从sprite派上用场的地方。 我们还希望从视觉队列中删除攻击者的画像,并将子画面放回其原始子画面组。

 // restore starting position, size and animation 
attacker.restoreOriginal()
target.restoreOriginal()
 // fade out the background we put on top 
game.add.tween( this .attackSceneBg).to( { alpha: 0 }, 300, Phaser.Easing.Linear.None, true )
// and remove the portrait from the visual queue
game.add.tween(attacker.portrait).to( { alpha: 0 }, 500, Phaser.Easing.Linear.None, true )
 // move back to their original group 
this .spritesGroup.add(attacker.spriter)
this .spritesGroup.add(target.spriter)

请转到我的网站并检查源代码,以获取attack功能的完整摘要。

最后,我们为下一个攻击者更新队列…

 updateQueue() { 
// everytime we initiate an attack
// we descrease their each unit's priority number
// and cap it to 1, this way each unit will fall in line
// when both have the same priority number
this .monsters.forEach((monster) => {
monster.priority = game.math.max(1, monster.priority - monster.speed)
})
this .heroes.forEach((hero) => {
hero.priority = game.math.max(1, hero.priority - hero.speed)
})
 // add the unit we took out earlier, back into the end of the queue 
this .queue.add( this .attackingUnit)
// updates the queue array
this .queue.update()
// and show the contents of the queue to our player
this .updateVisualQueue()
 // peek and find out whose unit is the next one 
if ( this .queue.peek().name.includes('knight')) {
// and we can safely bring control back to our player
this .stopInput = false
} else {
// if not - randomly attack one of the player's hero
this .attackingUnit = this .queue.poll()
this .attack( this .attackingUnit, this .heroes[game.rnd.between(0,2)])
}
}

源代码

您可以在我的网站上检查所有源代码。 很遗憾,您将无法在此处查看它们,但以下是说明:

  • gameState.js对我所有的Phaser演示都是通用的
  • demo.js是重要部分
  • unit.js包含创建骑士和巨魔的代码,以及它们用来attackhurt代码
  • init.js都与为场景创建不同的精灵以及队列的其他创建有关
  • 如果您想查看我使用的开源优先级队列数据结构以及必须添加的其他update功能, stablePriorityQueue.js

它们用es6编写。 所有源文件均已完整注释。

演示和预览

这是我的演示站点链接。 在这里预览:

笔记

  • 代码中有一个全局的ANIM_SPEED ,这使我可以轻松地通过将所有内容乘以该因子来更改游戏的整个动画。 有人慢吗?

启示

  • 视觉队列的Xulima领主,玩家可以看到下一个轮到谁了。
  • 用于战斗和攻击场景设置的最黑暗地下城。 它们会模糊背景,并在发生攻击时进行一些倾斜/缩放。 我的等效方法是放一个覆盖层并调整精灵大小,并稍微摇动屏幕。

谢谢…

  • to craftpix.net,以了解他们的骑士,巨魔和游戏背景
  • 计算机科学教授Daniel Lemire提出的稳定优先级队列数据结构及其解释
  • 多汁插件产生的相机抖动
  • 战俘工作室的血腥效果
  • 我用于骨骼动画的spriter类。 这堂课就可以了!
  • 理查德·戴维(Richard Davey)出色的phaser.io 2D游戏框架

我的网站

如果您对其他Phaser游戏开发教程感兴趣,那么我的站点上还有更多内容:

  • 自上而下-图层,移动和碰撞
  • 有趣的咒语-您可以看到魔术箭飞向僵尸,暴风雨,防火墙,僵尸僵冰和闪电击中地面,导致地板破裂。
  • 阻止粒子滑动-查看骨骼如何破碎成碎片。