Hi there. So I’ve been working on this prototype in phaser 3. I’m documenting my progress and am posting here to get some feedback, and maybe get some help or suggestions.
The first prototype I worked on uses state machines which are each declared in the player and npc class, respectively. I was also creating animations inside the entities classes, and playing the animations from within the state machines.
Heres what some of the npc and state code looks like…
export class NPC extends Entity {
constructor(scene, x, y, texture, key) {
super(scene, x, y, texture, key)
this.scene = scene
let walkSfxConfig = {
mute: false,
volume: 1,
rate: 1,
detune: 0,
seek: 0,
loop: true,
delay: 0
}
this.footstepsSFX = scene.sound.add("walk1", walkSfxConfig);
this.healthBar = new Bar(this.scene, 0, 2, 100, 16, 12);
let idleAnimConfig = {
key: 'minotaur-idle',
frames: this.anims.generateFrameNames(texture, {
start: 0,
end: 4
}),
frameRate: 8,
yoyo: false,
repeat: -1,
};
let runAnimConfig = {
key: 'minotaur-run',
frames: this.anims.generateFrameNames(texture, {
start: 10,
end: 17
}),
frameRate: 8,
yoyo: false,
repeat: -1,
};
let weakAttackAnimConfig = {
key: 'minotaur-weakattack',
frames: this.anims.generateFrameNames(texture, {
start: 40,
end: 44
}),
frameRate: 8,
yoyo: false,
repeat: 0,
};
let strongAttackAnimConfig = {
key: 'minotaur-strongattack',
frames: this.anims.generateFrameNames(texture, {
start: 30,
end: 38
}),
frameRate: 8,
yoyo: false,
repeat: 0,
};
let deathAnimConfig = {
key: 'minotaur-death',
frames: this.anims.generateFrameNames(texture, {
start: 90,
end: 95
}),
frameRate: 10,
yoyo: false,
repeat: 0,
};
this.anims.create(idleAnimConfig)
this.anims.create(runAnimConfig)
this.anims.create(weakAttackAnimConfig)
this.anims.create(strongAttackAnimConfig)
this.anims.create(deathAnimConfig)
this.isNPCDead = false
this.npcStateMachine = new StateMachine('idle', {
idle: new NpcIdleState(),
hostile: new NpcHostileState(),
move: new NpcMoveState(),
attack: new NpcAttackState(),
death: new NpcDeathState(),
dead: new NpcDeadState()
}, [this, scene])
this.setInteractive(new Phaser.Geom.Rectangle(28, 20, 40, 40), Phaser.Geom.Rectangle.Contains);
this.body.setSize(20, 20)
// this.setActive(true)
}
injure(damageAmount) {
this.healthBar.decrease(damageAmount)
}
handleLook(player) {
if (player.body.x < this.x) {
this.flipX = true
} else {
this.flipX = false
}
}
update(delta) {
this.body.x = this.body.x + this.body.velocity.x * delta;
this.body.y = this.body.y + this.body.velocity.y * delta;
// INSTANTIATE THE NPCS HEALTH BAR
this.healthBar.bar.x = this.x - 10
this.healthBar.bar.y = this.y - 35
this.healthBar.bar.setScale(0.3, 0.3)
// SET STATE MACHINE UPDATE
this.npcStateMachine.step()
}
}
export class NpcIdleState extends State {
enter(npc, scene) {
this.player = scene.players.getChildren()[0]
npc.play("minotaur-idle", true)
npc.isNPCDead = false
npc.footstepsSFX.stop()
}
execute(npc, scene) {
let distance = Phaser.Math.Distance.Between(npc.x, npc.y, this.player.x, this.player.y);
if (distance < 140) {
this.stateMachine.transition("hostile")
}
}
}
export class NpcHostileState extends State {
enter(npc, scene) {
this.player = scene.players.getChildren()[0]
npc.play("minotaur-idle", true)
npc.isNPCDead = false
npc.footstepsSFX.stop()
}
execute(npc, scene) {
let distance = Phaser.Math.Distance.Between(npc.x, npc.y, this.player.x, this.player.y);
if (distance < 140) {
this.stateMachine.transition("move")
}
}
}
export class NpcMoveState extends State {
enter(npc, scene) {
npc.play("minotaur-run", true)
npc.footstepsSFX.play()
scene.sound.play("bullsound1")
npc.isNPCDead = false
this.player = scene.players.getChildren()[0]
this.randomSpeedValue = Phaser.Math.Between(38, 69);
if (npc.healthBar.value == 0) {
this.stateMachine.transition("death")
}
}
execute(npc, scene) {
scene.physics.moveTo(npc, this.player.x, this.player.y, this.randomSpeedValue)
let distance = Phaser.Math.Distance.Between(npc.x, npc.y, this.player.x, this.player.y);
if (npc.body.velocity.x < 0.1) {
npc.setFlip(true)
}
if (npc.body.velocity.x > 0.1) {
npc.setFlip(false)
}
if (npc.body.speed > 0) {
// 4 is our distance tolerance, i.e. how close the source can get to the target
// before it is considered as being there. The faster it moves, the more tolerance is required.
if (distance < 30) {
npc.body.velocity.x = 0
npc.body.velocity.y = 0
this.stateMachine.transition("attack")
}
}
}
}
export class NpcAttackState extends State {
enter(npc, scene) {
npc.footstepsSFX.stop()
npc.isNPCDead = false
this.player = scene.players.getChildren()[0]
this.gameScene = scene.scene.get("GameUIScene")
this.attackTime = 0
this.amountAttack = 0
npc.on("animationupdate", (currentAnim, currentFrame, sprite) => {
if (currentFrame.textureFrame == 43) {
scene.sound.play("attack1")
}
if (currentFrame.textureFrame == 31) {
scene.sound.play("attack2")
}
})
this.randomAttackValue = Phaser.Math.Between(1, 4);
}
execute(npc, scene) {
if (this.attackTime == 30) {
this.amountAttack = this.amountAttack + 1
}
this.attackTime++
if (this.attackTime == 30) {
if (this.amountAttack > this.randomAttackValue) {
npc.play("minotaur-strongattack").on("animationcomplete", (anim) => {
})
// handle npc attack points
let ap = 4
let newAP = ap * 10 //level of npc
scene.players.getChildren()[0].health = scene.players.getChildren()[0].health - newAP
this.gameScene.bar.decrease(newAP)
} else {
npc.play("minotaur-weakattack").on("animationcomplete", (anim) => {
})
// handle npc attack points
let ap = 1
let newAP = ap * 11 //level of npc
scene.players.getChildren()[0].health = scene.players.getChildren()[0].health - newAP
this.gameScene.bar.decrease(newAP)
}
}
if (this.attackTime == 140) {
this.attackTime = 0
if (this.amountAttack > 3) {
this.amountAttack = 0
}
}
if (npc.healthBar.value == 0) {
this.stateMachine.transition("death")
}
if (this.gameScene.bar.value == 0) {
this.stateMachine.transition("idle")
this.attackTime = 0
}
let distance = Phaser.Math.Distance.Between(npc.x, npc.y, this.player.x, this.player.y);
if (distance > 30) {
this.stateMachine.transition("hostile")
}
}
}
export class NpcDeathState extends State {
enter(npc, scene) {
npc.isNPCDead = true
npc.healthBar.destroy()
let gameScene = scene.scene.get("GameScene")
gameScene.dropItem(npc.x, npc.y, npc, this.randomNumber)
npc.disableInteractive()
npc.body.destroy()
scene.sound.play("bullsound2")
npc.play("minotaur-death").on("animationupdate", (anim, frame, sprite, a) => {
if(a == 95) {
npc.anims.stop("minotaur-idle")
this.stateMachine.transition("dead")
}
})
}
execute(npc, scene) {
}
}
export class NpcDeadState extends State {
enter(npc, scene) {
console.log("is really dead")
// npc.anims.pause(npc.anims.currentAnim.frames[4]);
this.timer = scene.time.delayedCall(9000, () => {
npc.destroy()
});
}
execute() {
}
}
This was all going very well but…
I started to notice the frame rate drop, and decided to use in-browser fps counter in chrome. It initially ran from 30 fps, the npc would die (death state) and then destroy the npc. The frame rate would progressively get worse and worse as I keep killing the npcs. If I roam the game world without killing npcs, it seems fine. (or at least I think so?)
I changed the render option in the config from WEBGL to CANVAS. It seemed even worse in canvas.
I decided to go back to square one and watch a video tutorial which uses a simpler state manager. In this prototype, I added a boot and preload scene, with a loading bar, which I had borrowed from the tutorial. The animations are also initiated in the preload scene. This all works fine except the frame rate still decreases, particularly when I’m destroying npcs. I was hoping the animation change would ensure a steady frame.
Heres what my npc class currently looks like.
export class NPCMinotaur extends Entity {
constructor(ctx, x, y, texture) {
super(ctx, x, y, texture)
this.ctx = ctx
this.states = {
idle: true,
run: false,
attack: false,
death: false
}
// experience system
if (this.states.idle) {
this.startNewAnim("idle")
}
this.healthBar = new Bar(this.ctx, 0, 2, 100, 16, 12);
this.gameUiScene = this.ctx.scene.get("GameUIScene")
this.attackTime = 0
this.randomAttackValue = Phaser.Math.Between(1, 4);
this.randomSpeedValue = Phaser.Math.Between(3, 12);
this.body.setSize(20, 23)
this.startNewAnim('idle')
this.player = this.ctx.players.getChildren()[0]
this.setInteractive(new Phaser.Geom.Rectangle(28, 20, 40, 40), Phaser.Geom.Rectangle.Contains);
}
update(time, delta) {
this.updateSpriteDirection()
// INSTANTIATE THE NPCS HEALTH BAR
this.healthBar.bar.x = this.x - 10
this.healthBar.bar.y = this.y - 35
this.healthBar.bar.setScale(0.3, 0.3)
// if (this.states.death) return;
if (this.states.idle) {
// check if dead
if (this.healthBar.value == 0) {
this.startNewAnim('npc-death')
this.setState('death')
}
this.isAttacking = false
// this.ctx.physics.moveTo(this, this.ctx.players.getChildren()[0].x, this.ctx.players.getChildren()[0].y, 90)
let distance = Phaser.Math.Distance.Between(this.ctx.players.getChildren()[0].x, this.ctx.players.getChildren()[0].y, this.x, this.y);
if (distance < 120) {
this.startNewAnim('run')
this.body.velocity.x = 0
this.body.velocity.y = 0
this.setState('run')
}
}
if (this.states.run) {
this.isAttacking = false
target = { x: this.ctx.players.getChildren()[0].x, y: this.ctx.players.getChildren()[0].y }
this.moveSprite(target, delta)
}
if (this.states.attack) {
// check if dead
if (this.healthBar.value == 0) {
this.death()
this.startNewAnim('npc-death')
this.on('animationupdate', (anim, frame, sprite, frameKey) => {
if (frame.index == 4) {
this.stopAnim()
this.ctx.players.getChildren()[0].setState('idle')
}
})
this.setState('death')
}
this.isAttacking = true
this.body.velocity.x = 0
this.body.velocity.y = 0
this.attack()
}
}
moveSprite(target, delta) {
this.isAttacking = false
this.ctx.physics.moveToObject(this, target, this.randomSpeedValue)
let distance = Phaser.Math.Distance.Between(this.x, this.y, target.x, target.y);
if (this.body.speed > 0) {
// 4 is our distance tolerance, i.e. how close the source can get to the target
// before it is considered as being there. The faster it moves, the more tolerance is required.
if (distance < 30) {
this.setState('attack')
}
}
}
attack() {
this.isAttacking = true
if (this.attackTime == 30) {
this.amountAttack = this.amountAttack + 1
}
if (this.isAttacking) {
this.attackTime++
if (this.attackTime == 6) {
if (this.amountAttack > this.randomAttackValue) {
} else {
this.startNewAnim('weakattack')
this.on('animationupdate', (anim, frame, sprite, frameKey) => {
if (frame.index == 5) {
this.startNewAnim('idle')
}
})
let ap = 1
let newAP = ap * 11 // level of npc (str?)
// this.ctx.players.getChildren()[0].health = this.ctx.players.getChildren()[0] - newAP
this.gameUiScene.bar.decrease(newAP)
}
}
if (this.attackTime == 190) {
this.attackTime = 0
if (this.amountAttack > 3) {
this.amountAttack = 0
}
}
}
let distance = Phaser.Math.Distance.Between(this.x, this.y, this.ctx.players.getChildren()[0].x, this.ctx.players.getChildren()[0].y);
// let distance = Phaser.Math.Distance.Between(this.x, this.y, this.ctx.players.getChildren()[0].x, this.ctx.players,getChildren()[0].y);
if (distance > 30) {
// this.timer.paused = true
this.off('animationupdate')
this.startNewAnim('run')
this.setState('run')
// this.stateMachine.transition("hostile")
}
}
death() {
this.body.destroy()
this.healthBar.bar.destroy()
this.timer = this.ctx.time.delayedCall(9000, () => {
this.destroy()
});
}
updateSpriteDirection() {
if (this.body.angle > 0 && this.body.angle < 1.5) {
this.setFlip(false)
}
if (this.body.angle < 0 && this.body.angle > -1.5) {
this.setFlip(false)
}
if (this.body.angle > 1.5 && this.body.angle < 3.15) {
this.setFlip(true)
}
if (this.body.angle < -1.5 && this.body.angle > -3.15) {
this.setFlip(true)
}
}
setState(key) {
if (!this.states.hasOwnProperty(key)) {
console.log(this.key + "invalid STATE key" + key)
return
}
if (!this.states.last === key) {
return
}
this.resetStates()
this.states[key] = true
this.states.last = key
}
resetStates() {
for (let key in this.states) {
this.states[key] = false
}
}
}
I turn on the fps counter in chrome, and was using a second screen, and suddenly the framerate has jumped to 60 fps. I dont think I did anything for that to happen (edit: but after running the prototype I notice it has changed back to 30 when I’m using the laptopscreen), seeing as the previous prototype ran at 60 fps. I wasnt sure why the frame rate increase happened. Anyway, the frame rate continues to decrease. I guess I would expect the frame rate to decrease, but not in a way that affects gameplay. It starts at 60 fps, and as I destroy npcs, it reaches lows at 40 fps and further down to 30 fps.
I’ve looked at some old posts about how to better your chances at improving framerate. I would assume there are many different aspects that would contribute to a decreasing framerate. Things that I’m not aware of.
I tried using delta in the update functions. I’m actually unsure and have no idea how to use delta. I had come across using delta in the past, but hadn’t used delta with phaser. I’m unsure if this is a problem or not.
I’m hoping someone here can lend me suggestions. I’m thinking of uploading the code to github, so anyone can have a better look.
I have a lot to learn, and have come close to giving up on this, but am willing to push along to get something a bit more playable. Excuse the messy code, I’m still learning and willing to improve. Thanks.
Any suggestions or comments will be much appreciated.