javascriptphaser-framework

Ghost AI not moving through intersections (Phaser 3, PacMan)


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.


Solution

  • 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".