Search code examples
javascriptphaser-frameworkraycastingphaserjs

How to delete an object if its out of screen and it was used in raycasters of other objects?


On my main game class I have a createCar method, which dynamically creates a car and iterates over other cars and maps raycast between each other (so cars can detect distance between each other):

  createCar(scene, x, y, angle, speed, name) {
    const car = new Car(scene, x, y, angle, speed);
    car.name = name; // for debug
    this.carsGroup.children.iterate((c) => {
      c.raycaster.mapGameObjects(car, true);
      car.raycaster.mapGameObjects(c, true);
    });
    this.carsGroup.add(car);
  }

It works, unless I want to delete a car:

  update() {
    this.carsGroup.children.iterate((car) => {
      // Car is out of screen
      if (!this.cameras.main.worldView.contains(car.x, car.y)) {
        this.carsGroup.remove(car);
        this.carsGroup.children.iterate((c) => {
          c.raycaster.removeMappedObjects(car);
          car.raycaster.removeMappedObjects(c);
        });
        car.destroy();
      } else {
        car.update();
        car.debug();
      }
    });
  }

Then it fails here when internally it tries to retrieve the "raycasterMap" and destroy it:

object.data.get('raycasterMap').destroy();

raycaster-core.js:396 Uncaught TypeError: Cannot read properties of undefined (reading 'destroy') at Raycaster.removeMappedObjects (raycaster-core.js:396:48) at game.js:51:23

I can't figure out how to properly remove a car from scene and memory. If I just call car.destroy() other cars throw exceptions, because the raycast still has references.

Full codepen sample: https://codepen.io/vvyshko/pen/bGOxpbG


Solution

  • I think the issue is, that the clean up is not optimal. I would suggest, put the cleanup into the destroy function of the car - class, and just add validation in the update function of the scene, checking if the car- child is a valid object,

    Here the alterations:

    class Car extends Phaser.Physics.Arcade.Sprite {
      // ...
      destroy(){
        super.destroy();
        // Cleanup data
        this.ray.destroy();
        this.raycaster.destroy();
      }
      // ...
    }
    
    class Example extends Phaser.Scene {
      //...
      update() {
        this.carsGroup.children.iterate((car) => {
          // Check if the list item is valid
          if (!car) {
            return;
          }
    
          if (!this.cameras.main.worldView.contains(car.x, car.y)) {
            // Remove Car from Group and destroy Object
            this.carsGroup.remove(car, true, true);
          } else {
            car.update();
            car.debug();
          }
        });
      }
    }
    

    Code copied and adapted from Codepen:

    class Car extends Phaser.Physics.Arcade.Sprite {
        constructor(scene, x, y, rotation, speed) {
          super(scene, x, y, "car");
          this.debugGraphics = scene.add.graphics();
          this.speed = speed;
          this.initialSpeed = speed;
          this.rayLength = 100;
          this.rayColor = 0xffffff;
          this.rotation = rotation;
          this.setDisplaySize(130, 80);
      
          this.debugColor = new Phaser.Display.Color();
          // Set the color to a random value
          this.debugColor.random();
    
          // Create a raycaster for the car
          this.raycaster = scene.raycasterPlugin.createRaycaster();
          // Create a ray for the car
          this.ray = this.raycaster.createRay();
          // Set the ray origin to the car position
          this.ray.setOrigin(this.x, this.y);
          // Set the ray angle to the car angle
          this.ray.setAngle(this.rotation);
      
          scene.physics.world.enable(this); // Enable physics for the car
          scene.add.existing(this); // Add the car to the scene
      
          //this.raycaster.mapGameObjects(scene.rectangle);
        }
      
        update() {
          // console.log('update', this.name, this.speed);
          window.lastUpdatedCar = this;
          this.raycaster.update();
          // Move the car forward
          this.scene.physics.velocityFromRotation(
            this.rotation,
            this.speed,
            this.body.velocity
          );
          // Update the ray origin and angle
          this.ray.setOrigin(this.x, this.y);
          this.ray.setAngle(this.rotation);
          // Cast the ray and get the intersection
         // console.log(this.name, 'removed=', this.removed)
    
          let intersection ;
          try {
            intersection = this.ray.cast()
          } catch (e) {}
          this.intersection = intersection;
          // If there is an intersection, reduce the speed or stop
          if (intersection) {
          //  console.log('!!!', intersection)
          }
          if (this.crashed) {
              this.setTint(0x555888);
            this.speed = 0;
          } else if (intersection && intersection.object) {
           // console.log('!!!', intersection)
            let distance = Phaser.Math.Distance.BetweenPoints(this, intersection);
            if (distance < this.rayLength) {
              // Determine a deceleration rate (you can adjust this value)
              const decelerationRate = 1; // Adjust as needed
      
              // Calculate the new speed with deceleration
              this.speed -= decelerationRate;
      
              // Ensure the speed doesn't become negative
              if (this.speed < 0) {
                this.speed = 0;
              }
            } else {
              if (this.speed < this.initialSpeed) {
                this.speed++;
              }
            }
          }
        }
    
      
      
      
        handleCollision(o1, o2) {
          console.log('CRASH', o1, o2)
          o1.crashed = true;
          o2.crashed = true;
        }
      
        debug() {
          this.debugGraphics.clear();
          this.debugGraphics.setDepth(this.depth + 1); // Set the depth
          
          this.debugGraphics.lineStyle(1, this.debugColor.color);
          this.debugGraphics.beginPath();
          this.debugGraphics.moveTo(this.x, this.y);
          this.debugGraphics.lineTo(
            this.x + Math.cos(this.rotation) * this.rayLength,
            this.y + Math.sin(this.rotation) * this.rayLength
          );
          this.debugGraphics.closePath();
          this.debugGraphics.strokePath();
      
          if (this.intersection) {
            this.debugGraphics.fillStyle(0xff0000, 0.5);
            this.debugGraphics.strokeCircle(
              this.intersection.x,
              this.intersection.y,
              5
            );
          }
      
          if (this.crashed) {
              // Draw a red cross
              this.debugGraphics.lineStyle(2, 0xff0000);
              this.debugGraphics.beginPath();
              this.debugGraphics.moveTo(this.x - 10, this.y - 10);
              this.debugGraphics.lineTo(this.x + 10, this.y + 10);
              this.debugGraphics.moveTo(this.x + 10, this.y - 10);
              this.debugGraphics.lineTo(this.x - 10, this.y + 10);
              this.debugGraphics.closePath();
              this.debugGraphics.strokePath();
          }
        }
      
        destroy(){
          super.destroy();
          // Cleanup data
          //this.ray.destroy();
          //this.raycaster.destroy();
        }
      }
    
    
    class Example extends Phaser.Scene {
      constructor() {
        super();
      }
    
      preload() {
        this.load.setBaseURL("https://labs.phaser.io");
        this.load.image("bg", "assets/skies/space2.png");
        this.load.image("car", "assets/sprites/car-yellow.png");
        window.scene = this;
      }
    
      create() {
        const bg = this.add.image(0, 0, "bg");
        bg.setOrigin(0, 0); // Set the origin to the top-left corner
        bg.displayWidth = this.sys.game.config.width; // Stretch width to match canvas width
        bg.displayHeight = this.sys.game.config.height;
    
        // //create game object
        // this.rectangle = this.add
        //   .rectangle(1000, 300, 50, 200)
        //   .setStrokeStyle(1, 0xff0000);
    
        this.carsGroup = this.physics.add.group();
        window.carsGroup = this.carsGroup;
        this.createCar(this, 100, 300, 0, 10, "A");
        this.createCar(this, 600, 600, -1, 270, "B");
        this.createCar(this, 400, 300, -0.44, 10, "C");
        this.createCar(this, 600, 700, 0, 170, "D");
        // this.createCar(this, 700, 600, -1.44, 120);
      }
    
      createCar(scene, x, y, angle, speed, name) {
        const car = new Car(scene, x, y, angle, speed);
        car.name = name; // for debug
        this.carsGroup.children.iterate((c) => {
          c.raycaster.mapGameObjects(car, true);
          car.raycaster.mapGameObjects(c, true);
          c.scene.physics.add.collider(c, car, c.handleCollision);
          car.scene.physics.add.collider(car, c, car.handleCollision);
        });
        this.carsGroup.add(car);
      }
    
      update() {
        this.carsGroup.children.iterate((car) => {
           // Check if the list item is valid
          if (!car) {
            return;
          }
    
          if (!this.cameras.main.worldView.contains(car.x, car.y)) {
            // Remove Car from Group and destroy Object
            this.carsGroup.remove(car, true, true);
          } else {
            car.update();
            car.debug();
          }
        });
      }
    }
    
    const config = {
      type: Phaser.WEBGL,
      width: 1280,
      height: 1024,
      parent: "phaser-example",
      physics: {
        default: "arcade",
      },
      plugins: {
        scene: [
          {
            key: "PhaserRaycaster",
            plugin: PhaserRaycaster,
            mapping: "raycasterPlugin",
          },
        ],
      },
      scene: Example,
    };
    
    const game = new Phaser.Game(config);
     <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser-arcade-physics.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser-raycaster.js"></script>