Hello. I am making a simple isometric game and at certain collisions the whole game freezes mid attacking animation and the following “Uncaught TypeError: can’t access property “x”, h.positionImpulse is undefined” happens. It does not show an error line for my source code, only for Phaser’s source code. The image bellow illustrates.
I use a complex set of sensors/hitboxes for collisions:
Some things I observed:
- It only happens when my “pointerdown” attack event is triggered
- It seems to happen more often when clicking profusely
Below is my code, any help is appreciated!
const CWALK = 0x0001;
const CBODY = 0x0002;
class Player extends Phaser.GameObjects.Sprite {
constructor(scene, physics, x, y, texture, frame) { super(scene, x, y, texture, frame); // Add this Game Object to the Scene's display list and update list scene.add.existing(this); // Enable physics for this sprite if needed this.physics = physics; this.physics.add.gameObject(this, {mass:0, label:'player'}); // Set custom properties or methods this.textureName = texture; this.setScale(1); this.setSensor(true); this.setFixedRotation(); this.speed = 5; this.health = 100; this.weaponName = 'longsword'; this.lastDir = 'S'; this.mouseDir = 'S'; this.isAttacking = false; this.isKnockedBack = false; this.depth = this.y+this.height/2; this.anims.play(this.textureName+'standN', true); // Weapon setup this.weapon = scene.add.sprite(x, y, this.weaponName); this.physics.add.gameObject(this.weapon, {mass:0, label:'player_weapon'}); this.weapon.setScale(1); this.weapon.anims.play(this.weaponName+'standN', true); this.weapon.setSensor(true); // Add a body collider for damage this.bodyCollider = this.physics.add.rectangle(this.x, this.y, 10, 20, {isSensor: false, density:0.01, mass:1, restitution: 0, frictionAir: 1, label:'player_body', friction: 0, frictionStatic: 0, slop: 1, fixedRotation: true, collisionFilter:{category:CWALK, mask:CWALK, group:CWALK}}); // Add a circle collider to player for walking this.walkingCollider = this.physics.add.circle(this.x, this.y, 10, {isSensor: false, density:0.01, mass:20, restitution: 0, frictionAir: 1, label:'player_walker', friction: 0, frictionStatic: 0, slop: 1, fixedRotation: true, collisionFilter:{category:CBODY, mask:CBODY, group:CBODY}}); // Add sensors for attacking this.attackWBox = this.physics.add.rectangle(this.x, this.y, 30, 10, {isSensor: true, fixedRotation: true, label:'player_attack_W', collisionFilter:{category:CBODY, mask:CBODY, group:CBODY}}); this.attackEBox = this.physics.add.rectangle(this.x, this.y, 30, 10, {isSensor: true, fixedRotation: true, label:'player_attack_E', collisionFilter:{category:CBODY, mask:CBODY, group:CBODY}}); this.attackNBox = this.physics.add.rectangle(this.x, this.y, 10, 15, {isSensor: true, fixedRotation: true, label:'player_attack_N', collisionFilter:{category:CBODY, mask:CBODY, group:CBODY}}); this.attackSBox = this.physics.add.rectangle(this.x, this.y, 10, 15, {isSensor: true, fixedRotation: true, label:'player_attack_S', collisionFilter:{category:CBODY, mask:CBODY, group:CBODY}}); this.attackNWBox = this.physics.add.rectangle(this.x, this.y, 10, 20, {isSensor: true, fixedRotation: true, label:'player_attack_NW', angle: -45, collisionFilter:{category:CBODY, mask:CBODY, group:CBODY}}); this.attackNEBox = this.physics.add.rectangle(this.x, this.y, 10, 20, {isSensor: true, fixedRotation: true, label:'player_attack_NE', angle: 45, collisionFilter:{category:CBODY, mask:CBODY, group:CBODY}}); this.attackSWBox = this.physics.add.rectangle(this.x, this.y, 10, 40, {isSensor: true, fixedRotation: true, label:'player_attack_SW', angle: 95, collisionFilter:{category:CBODY, mask:CBODY, group:CBODY}}); this.attackSEBox = this.physics.add.rectangle(this.x, this.y, 10, 40, {isSensor: true, fixedRotation: true, label:'player_attack_SE', angle: -95, collisionFilter:{category:CBODY, mask:CBODY, group:CBODY}}); this.attackWBox.parent = this; this.attackEBox.parent = this; this.attackNBox.parent = this; this.attackSBox.parent = this; this.attackNWBox.parent = this; this.attackNEBox.parent = this; this.attackSWBox.parent = this; this.attackSEBox.parent = this; this.sensorMap = { 'W': this.attackWBox, 'E': this.attackEBox, 'N': this.attackNBox, 'S': this.attackSBox, 'NW': this.attackNWBox, 'NE': this.attackNEBox, 'SW': this.attackSWBox, 'SE': this.attackSEBox }; //Join player and weapon in a constraint this.physics.add.constraint(this, this.weapon, 0, 1, {pointA: {x: 0, y: 0},pointB: {x: 0, y: 0}}); //Join player and body collider in a constraint this.physics.add.constraint(this.bodyCollider, this, 0, 1, {pointA: {x: 0, y: 0},pointB: {x: 0, y: +3}}); //Join player and walking collider in a constraint this.physics.add.constraint(this.walkingCollider, this, 0, 1, {pointA: {x: 0, y: 0},pointB: {x: 0, y: +30}}); //Join player and attack sensors in constraints this.physics.add.constraint(this.attackWBox, this, 0, 1, {pointA: { x: 0, y: 0 }, pointB: { x: -15, y: 0 }}); this.physics.add.constraint(this.attackEBox, this, 0, 1, {pointA: { x: 0, y: 0 }, pointB: { x: +15, y: 0 }}); this.physics.add.constraint(this.attackNBox, this, 0, 1, {pointA: { x: 0, y: 0 }, pointB: { x: 0, y: -10 }}); this.physics.add.constraint(this.attackSBox, this, 0, 1, {pointA: { x: 0, y: 0 }, pointB: { x: 0, y: +10 }}); this.physics.add.constraint(this.attackNWBox, this, 0, 1, {pointA: { x: 0, y: 0 }, pointB: { x: -15, y: -10 }}); this.physics.add.constraint(this.attackNEBox, this, 0, 1, {pointA: { x: 0, y: 0 }, pointB: { x: +15, y: -10 }}); this.physics.add.constraint(this.attackSWBox, this, 0, 1, {pointA: { x: 0, y: 0 }, pointB: { x: -10, y: +10 }}); this.physics.add.constraint(this.attackSEBox, this, 0, 1, {pointA: { x: 0, y: 0 }, pointB: { x: +10, y: +10 }}); // Setup movement behavior this.on('animationcomplete', this.controlEndofAnimation, this); } activateAttackSensor(dir) { let sensor = this.sensorMap\[dir\]; sensor.isSensor = false; } deactivateAttackSensor(dir) { let sensor = this.sensorMap\[dir\]; sensor.isSensor = true; } movePlayer(action, bdir, mdir, reverse=false){ let speedMults = this.speedMultipliers\[bdir\]; if (action === 'stand'){ this.setVelocity(0,0); }else{ this.setVelocity(this.speed\*speedMults\[0\],this.speed\*speedMults\[1\]); } if (reverse){ this.playReverse(this.textureName+action+mdir, true); this.weapon.playReverse(this.weaponName+action+mdir, true); } else{ this.play(this.textureName+action+mdir, true); this.weapon.play(this.weaponName+action+mdir, true); } } controlEndofAnimation() { let animKey = this.anims.currentAnim.key; if (animKey.includes('attack')) { this.isAttacking = false; this.deactivateAttackSensor(this.mouseDir); } /\* else if (animKey.includes('hit')) { this.isKnockedBack = false; } \*/ }}
class Example extends Phaser.Scene
{
constructor () { super(); } preload () { this.load.spritesheet('warrior', 'assets/isometric_hero/main_male.png', { frameWidth: 128, frameHeight: 128 }); this.load.spritesheet('longsword', 'assets/isometric_hero/longsword.png', { frameWidth: 128, frameHeight: 128 }); this.load.image('follower', 'assets/bomb.png'); } getButtonDir(player, cursors) { //let dir = this.directionMap\[\[this.cursors.left.isDown,this.cursors.right.isDown,this.cursors.up.isDown,this.cursors.down.isDown\]\]; let dir = this.directionMap\[\[this.AKey.isDown,this.DKey.isDown,this.WKey.isDown,this.SKey.isDown\]\]; if (dir === undefined) { return \[this.player.lastDir, 'stand'\] }else{ this.player.lastDir = dir; return \[dir, 'run'\]; } } getMouseDir() { let playerMidX = this.player.x; let playerMidY = this.player.y; let angle = Phaser.Math.Angle.Between(playerMidX, playerMidY, this.pointer.x, this.pointer.y); let deg = Phaser.Math.RadToDeg(angle); if (deg < 0) { deg = 360 + deg; } if (deg >= 337.5 || deg < 22.5) { return 'E'; } if (deg >= 22.5 && deg < 67.5) { return 'SE'; } if (deg >= 67.5 && deg < 112.5) { return 'S'; } if (deg >= 112.5 && deg < 157.5) { return 'SW'; } if (deg >= 157.5 && deg < 202.5) { return 'W'; } if (deg >= 202.5 && deg < 247.5) { return 'NW'; } if (deg >= 247.5 && deg < 292.5) { return 'N'; } if (deg >= 292.5 && deg < 337.5) { return 'NE'; } } getStance() { //console.log(this.pointer.leftButtonDown(), this.pointer.rightButtonDown()); if (this.pointer.leftButtonDown()) { return 'attack'; }else if (this.pointer.rightButtonDown()) { return 'bow'; } else { return 'peace'; } } update () { let \[bdir, action\] = this.getButtonDir(); let stc = this.getStance(); this.player.mouseDir = this.getMouseDir(); //Only move if not attacking or being knocked back if (!this.player.isAttacking || this.player.isKnockedBack) { if (this.oppositesReverseMap\[bdir\]\[this.player.mouseDir\]) { this.movePlayer(action, bdir, this.player.mouseDir, true); } else { this.movePlayer(action, bdir, this.player.mouseDir, false); } } //TESTING DEPTH SORTING /\* this.playerContainer.depth = this.hitbox.body.y;//+(this.player.height\*this.player.scale); this.blockp.x = this.hitbox.x; this.blockp.y = this.playerContainer.depth; console.log('Hitbox: ', this.blockp.x, this.blockp.y); this.blockp.depth = this.playerContainer.depth + 100; //Make sure follower is always on top \*/ // /\* this.block.depth = this.block.body.y+(this.block.height\*this.block.scale); this.blockf.x = this.block.x; this.blockf.y = this.block.depth; this.blockf.depth = this.block.depth + 1; //Make sure follower is always on top \*/ //console.log('Depth: ', this.player.depth, this.block.depth); } movePlayer(action, bdir, mdir, reverse=false){ let speedMults = this.speedMultipliers\[bdir\]; if (action === 'stand'){ this.player.setVelocity(0,0); }else{ this.player.setVelocity(this.player.speed\*speedMults\[0\],this.player.speed\*speedMults\[1\]); } if (reverse){ this.player.playReverse(this.player.textureName+action+mdir, true); this.player.weapon.playReverse(this.player.weaponName+action+mdir, true); } else{ this.player.play(this.player.textureName+action+mdir, true); this.player.weapon.play(this.player.weaponName+action+mdir, true); } } getSpeedMultipliers() { let sm = {}; sm\['N'\] = \[0, -1\]; sm\['S'\] = \[0, 1\]; sm\['E'\] = \[1, 0\]; sm\['W'\] = \[-1, 0\]; sm\['NE'\] = \[0.707, -0.707\]; sm\['NW'\] = \[-0.707, -0.707\]; sm\['SE'\] = \[0.707, 0.707\]; sm\['SW'\] = \[-0.707, 0.707\]; return sm; } loadAnims(sheet){ let compass = \['W', 'NW', 'N', 'NE', 'E', 'SE', 'S', 'SW'\]; //Load all 8 directions of standing animations this.standAnims = \[\]; let prefix = sheet+'stand'; for (let i = 0; i < compass.length; i++) { let offset = i\*32; this.anims.create({ key: prefix+compass\[i\], frames: this.anims.generateFrameNumbers(sheet, { frames: \[offset, offset+1, offset+2, offset+3 \] }), frameRate: 18, repeat: -1, yoyo: true }); this.standAnims.push(prefix+compass\[i\]); } //Load all 8 directions of running animations this.runAnims = \[\]; prefix = sheet+'run'; for (let i = 0; i < compass.length; i++) { let offset = i\*32+4; let frameseq = \[offset, offset+1, offset+2, offset+3, offset+4, offset+5, offset+6, offset+7 \]; this.anims.create({ key: prefix+compass\[i\], frames: this.anims.generateFrameNumbers(sheet, { frames: frameseq }), frameRate: 20, repeat: -1, yoyo: false }); this.runAnims.push(prefix+compass\[i\]); } //Load all 8 directions of attacking animations this.attackAnims = \[\]; prefix = sheet+'attack'; for (let i = 0; i < compass.length; i++) { let offset = i\*32+12; let frameseq = \[offset, offset+1, offset+2, offset+3 \]; this.anims.create({ key: prefix+compass\[i\], frames: this.anims.generateFrameNumbers(sheet, { frames: frameseq }), frameRate: 20, repeat: 0, yoyo: false }); this.attackAnims.push(prefix+compass\[i\]); } //Load all 8 directions of defend animations this.dieAnims = \[\]; prefix = sheet+'defend'; for (let i = 0; i < compass.length; i++) { let offset = i\*32+16; //i = line, 32 = frames per line, +16 = starting frame for die animations let frameseq = \[offset, offset+1\];; this.anims.create({ key: prefix+compass\[i\], frames: this.anims.generateFrameNumbers(sheet, { frames: frameseq }), frameRate: 20, repeat: 0, yoyo: false }); this.dieAnims.push(prefix+compass\[i\]); } //Load all 8 directions of dying animations this.dieAnims = \[\]; prefix = sheet+'die'; for (let i = 0; i < compass.length; i++) { let offset = i\*32+18; //i = line, 32 = frames per line, +16 = starting frame for die animations let frameseq = \[offset, offset+1, offset+2, offset+3, offset+4, offset+5\];; this.anims.create({ key: prefix+compass\[i\], frames: this.anims.generateFrameNumbers(sheet, { frames: frameseq }), frameRate: 20, repeat: 0, yoyo: false }); this.dieAnims.push(prefix+compass\[i\]); } //Load all 8 directions of getting hit animations this.hitAnims = \[\]; prefix = sheet+'hit'; for (let i = 0; i < compass.length; i++) { let offset = i\*32+24; let frameseq = \[offset, offset+1, offset+2, offset+3\];; this.anims.create({ key: prefix+compass\[i\], frames: this.anims.generateFrameNumbers(sheet, { frames: frameseq }), frameRate: 20, repeat: 0, yoyo: false }); this.hitAnims.push(prefix+compass\[i\]); } //Load all 8 directions of using bow animations this.bowAnims = \[\]; prefix = sheet+'bow'; for (let i = 0; i < compass.length; i++) { let offset = i\*32+28; let frameseq = \[ offset, offset+1, offset+2, offset+3\];; this.anims.create({ key: prefix+compass\[i\], frames: this.anims.generateFrameNumbers(sheet, { frames: frameseq }), frameRate: 20, repeat: 0, yoyo: false }); this.bowAnims.push(prefix+compass\[i\]); } } getDirectionMaps() { //Data: this.cursors.left.isDown+" "+this.cursors.right.isDown+" "+this.cursors.up.isDown+" "+this.cursors.down.isDown let d = {}; d\[\[false,false,false,true\]\] = 'S'; d\[\[false,false,true,false\]\] = 'N'; d\[\[false,true,false,false\]\] = 'E'; d\[\[true,false,false,false\]\] = 'W'; d\[\[false,true,true,false\]\] = 'NE'; d\[\[true,false,true,false\]\] = 'NW'; d\[\[false,true,false,true\]\] = 'SE'; d\[\[true,false,false,true\]\] = 'SW'; //Opposites map for reversing animations let opr = {}; opr\['N'\] = {'S':true, 'E':false, 'W':false, 'NE':false, 'NW':false, 'SE':true, 'SW':true}; opr\['S'\] = {'N':true, 'E':false, 'W':false, 'NE':true, 'NW':true, 'SE':false, 'SW':false}; opr\['E'\] = {'W':true, 'N':false, 'S':false, 'NE':false, 'NW':true, 'SE':false, 'SW':true}; opr\['W'\] = {'E':true, 'N':false, 'S':false, 'NE':true, 'NW':false, 'SE':true, 'SW':false}; opr\['NE'\] = {'SW':true, 'N':false, 'E':false, 'W':true, 'S':true, 'NW':false, 'SE':false}; opr\['NW'\] = {'SE':true, 'N':false, 'E':true, 'W':false, 'S':true, 'NE':false, 'SW':false}; opr\['SE'\] = {'NW':true, 'N':true, 'E':false, 'W':true, 'S':false, 'NE':false, 'SW':false}; opr\['SW'\] = {'NE':true, 'N':true, 'E':true, 'W':false, 'S':false, 'NW':false, 'SE':false}; //Simple direct opposites map let ops = {}; ops\['N'\] = 'S'; ops\['S'\] = 'N'; ops\['E'\] = 'W'; ops\['W'\] = 'E'; ops\['NE'\] = 'SW'; ops\['NW'\] = 'SE'; ops\['SE'\] = 'NW'; ops\['SW'\] = 'NE'; return \[d, opr, ops\]; } create () { // Animation set this.loadAnims('warrior'); this.loadAnims('longsword'); // Player setup let startX = this.sys.game.canvas.width/2; let startY = this.sys.game.canvas.height/2; this.player = new Player(this, this.matter, startX, startY, 'warrior'); this.player.setSensor(true); this.player.anims.play('warriorstandN', true); // Block to hit this.enemy = new Player(this, this.matter, startX-100, startY-100, 'warrior'); // Input Events this.cursors = this.input.keyboard.createCursorKeys(); this.WKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W); this.AKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A); this.SKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S); this.DKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D); // Mouse setup this.pointer = this.input.activePointer; //this.pointer = this.input.activePointer; // On pointer down, attack this.input.on('pointerdown', this.playerAttack, this); // Directional setup \[this.directionMap, this.oppositesReverseMap, this.oppositesSimpleMap\] = this.getDirectionMaps(); this.speedMultipliers = this.getSpeedMultipliers(); this.matter.world.on('collisionactive', (event) => this.handleActiveCollisions(event)); //this.matter.world.on('collisionactive', (event, bodyA, bodyB) => this.handleCollisions(event, bodyA, bodyB)); /\* this.matter.world.on('collisionactive', (event, bodyA, bodyB) => { if (bodyA === this.player.collider && bodyB === this.enemy.collider) { console.log("Collision active:"); } }); \*/ } handleActiveCollisions(event) { event.pairs.forEach(pair => { const { bodyA, bodyB } = pair; if (bodyA.label.includes("attack") && bodyB.label.includes("attack")) { let player1 = bodyA.parent; let player2 = bodyB.parent; if (player1!==player2){ if (player1.isAttacking && player2.isAttacking) { //player1.isKnockedBack = true; //player2.isKnockedBack = true; //player1.play(bodyA.parent.textureName+'hit'+bodyA.parent.mouseDir, true); //player2.play(bodyB.parent.textureName+'hit'+bodyB.parent.mouseDir, true); if (player1.mouseDir === this.oppositesSimpleMap\[player2.mouseDir\]) { player1.setVelocity(-this.speedMultipliers\[player1.mouseDir\]\[0\]\*bodyA.parent.speed\*2, -this.speedMultipliers\[player1.mouseDir\]\[1\]\*bodyA.parent.speed\*2); player2.setVelocity(-this.speedMultipliers\[player2.mouseDir\]\[0\]\*bodyB.parent.speed\*2, -this.speedMultipliers\[player2.mouseDir\]\[1\]\*bodyB.parent.speed\*2); } } } } }); } //Handle all collisions in the game handleCollisions(event, bodyA, bodyB){ console.log("Collision active between two attack sensors:", bodyA.label, bodyB.label); /\* if (bodyA.label.includes("attack") && bodyB.label.includes("attack")) { let player1 = bodyA.parent; let player2 = bodyB.parent; if (player1!==player2){ if (player1.isAttacking && player2.isAttacking) { player1.isAttacking = false; player2.isAttacking = false; console.log("yo: ", bodyA.label, bodyB.label); player1.isKnockedBack = true; player2.isKnockedBack = true; player1.play(bodyA.parent.textureName+'hit'+bodyA.parent.mouseDir, true); player2.play(bodyB.parent.textureName+'hit'+bodyB.parent.mouseDir, true); player1.setVelocity(-bodyA.parent.speed\*2, -bodyA.parent.speed\*2); player2.setVelocity(-bodyB.parent.speed\*2, -bodyB.parent.speed\*2); } } } \*/ /\* event.pairs.forEach(pair => { const { bodyA, bodyB } = pair; console.log("This is a collision: ", bodyA.label, bodyB.label); }); \*/ } playerAttack() { // If left button is down, attack if (this.pointer.leftButtonDown()) { this.player.activateAttackSensor(this.player.mouseDir); this.player.isAttacking = true; this.player.play(this.player.textureName+'attack'+this.player.mouseDir, true); this.player.weapon.play(this.player.weaponName+'attack'+this.player.mouseDir, true); //Make enemy attack in the opposite direction this.enemy.isAttacking = true; this.enemy.mouseDir = this.oppositesSimpleMap\[this.player.mouseDir\]; this.enemy.play(this.enemy.textureName+'attack'+this.oppositesSimpleMap\[this.player.mouseDir\], true); this.enemy.weapon.play(this.player.weaponName+'attack'+this.oppositesSimpleMap\[this.player.mouseDir\], true); } }}
const config = {
type: Phaser.AUTO, parent: 'phaser-example', backgroundColor: '#999999', // pixelArt: true, scene: \[ Example \], scale: { mode: Phaser.Scale.ENVELOP, autoCenter: Phaser.Scale.CENTER_BOTH, width: 1920, height: 1080, }, physics: { default: 'matter', matter: { gravity: { y: 0 }, debug: false } }};
const game = new Phaser.Game(config);

