I am trying to make a pac_man game with Phaser 3. I have implemented the pac_man movement, however, I really need help with the ghost AI. The ghost is moving by its self, however, it is not passing through any intersections. Like, it only moves in a different direction when it collides with something else. I am trying to fix this, and yet I don't know how. I am using physics, with arcade. Here is some of the code:
newDirections(sprite) {
let directions = ['left', 'right', 'down', 'up']
return directions.filter(d => !sprite.body.blocked[d]);
}
This function is supposed to see any other paths the ghost can take. I am highly depended on sprite.body.blocked
for this (especially since I am using tiles), but it only reads when the sprite is colliding with something else.
The problem is, the sprite.body.blocked
only returns the accurate dictionary of of blocked areas on a sprite when the sprite is colliding with something. Otherwise, it only returns {none: true, up: false, down: false, left: false, right: false}
. Because of this, the ghost, 'sprite' only moves when it collides with something. Thus, it doesn't pass through any intersections.
So basically, I want something that will actively say whether or not it is blocked (touching) by an object or not, not only when it is colliding with an object.
I heavily appreciate any help or advice you'd give.
Well it depends on how you are moving the ghost, but an option would be to add a collider, that checks for overlaps (this.physics.add.overlap(...)
) and just querying the tiles around of the current tile the ghost is on (layer.getTileAt(...)
), if movement is possible.
Short Demo (Updated):
(ShowCasing movement, just demo code will need tweaking)
Update Demo incorporates the random new direction selection, with special intersection tiles
Letters in SquareBrackes show possible direction change options.
const speed = 10;
// Map with only 2 Tiles
// 1 ... wall
// 0 ... walkable tiles
// 2 ... walkable tile, invisible marker to turn
const level = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 2, 0, 2, 0, 0, 2, 0, 2, 1],
[1, 0, 1, 0, 1, 1, 0, 1, 0, 1],
[1, 2, 0, 2, 0, 0, 2, 0, 2, 1],
[1, 0, 1, 0, 1, 1, 0, 1, 0, 1],
[1, 2, 0, 2, 0, 0, 2, 0, 2, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
];
// demo helper variables
// needs to be improved
let lastDirection = 'down';
// quickfix
// prevent double direction switch
let lastTile;
class DemoScene extends Phaser.Scene {
create() {
this.createDemoAssets();
this.info = this.add.text(2, 2, '[]').setOrigin(0).setDepth(100).setSize(1);
const map = this.make.tilemap({ data: level, tileWidth: 8, tileHeight: 8, width: 10, height: 8 });
map.setCollision([1]);
const tiles = map.addTilesetImage('tiles', 'tiles', 8, 8);
const layer = map.createLayer(0, tiles, 0, 0);
let ghost = this.physics.add.sprite(12, 12, 'ghost').setOrigin(0.5); // set Sprite Origin to center
ghost.setVelocityY(speed);
// bind ghost to scene
this.ghost = ghost;
console.info(ghost.body.velocity.y * -1);
this.physics.add.collider(ghost, layer);
this.physics.add.overlap(ghost, layer, (ghost, tile) => {
// quick fix double direction switch
if (lastTile == tile) {
return;
}
//reset label
this.info.setText(`[]`);
// only check on special intersection tiles
if (tile.index == 2) {
let directions = [
{ x: 0, y: -1, name: 'up' },
{ x: 1, y: 0, name: 'right' },
{ x: 0, y: 1, name: 'down' },
{ x: -1, y: 0, name: 'left' },
]
// selects all nodes that are "walkable" "index == 0"
.filter((node) => {
return layer.getTileAt(tile.x + node.x, tile.y + node.y).index == 0;
})
// creates a new array, with only the name
.map((x) => x.name);
console.info(directions);
this.info.setText(`[${directions.map((x) => x[0]).join(',')}]`);
// +4 is half tile width == middle of the tile (quickfix for demo)
if (Phaser.Math.Distance.Between(tile.pixelX + 4, tile.pixelY + 4, ghost.x, ghost.y) < 1) {
lastTile = tile;
// select new direction an set speed
this.setGhostdirection(directions);
}
}
});
}
// from here helper functions for demo, should be improved/removed
createDemoAssets() {
let graphics = this.make.graphics();
// tilesheet
graphics.fillStyle(0xcdcdcd);
graphics.fillRect(0, 0, 8, 8);
graphics.fillStyle(0x7a7a7a);
graphics.fillRect(8, 0, 8, 8);
graphics.fillStyle(0x00ff00);
graphics.fillRect(16, 0, 8, 8);
graphics.generateTexture(`tiles`, 24, 8);
//ghost
graphics.fillStyle(0xff0000);
graphics.fillRect(0, 0, 8, 8);
graphics.generateTexture(`ghost`, 5, 5);
}
setGhostdirection(options) {
// don't turn 180 degrees
let goodOptions = options.filter((option) => option != lastDirection);
let selectedOption = Phaser.Math.RND.pick(goodOptions);
console.info(selectedOption);
// set lastDirection
lastDirection = selectedOption;
// ghost speed
switch (selectedOption) {
case 'down':
this.ghost.setVelocity(0, speed);
break;
case 'up':
this.ghost.setVelocity(0, -speed);
break;
case 'left':
this.ghost.setVelocity(-speed, 0);
break;
case 'right':
this.ghost.setVelocity(speed, 0);
break;
}
}
}
var config = {
width: 10 * 8,
height: 8 * 8,
zoom: 4,
physics: {
default: 'arcade',
},
scene: DemoScene,
};
new Phaser.Game(config);
console.clear();
document.body.style = 'margin:0;';
<script src="//cdn.jsdelivr.net/npm/phaser/dist/phaser.min.js"></script>
Info: This is just demo code, it has a few quick fixes, just to make it run smooth for the demo (and to keep the code short), should be improved/optimized for "production".