
本文详解在使用 Phaser-Raycaster 插件时,如何正确销毁动态生成的游戏对象(如车辆),避免因残留 raycaster 映射引发的 Cannot read properties of undefined (reading 'destroy') 错误。核心在于将清理逻辑封装至对象自身的 destroy() 方法,并统一通过 Group 的安全移除方式触发。
本文详解在使用 phaser-raycaster 插件时,如何正确销毁动态生成的游戏对象(如车辆),避免因残留 raycaster 映射引发的 `cannot read properties of undefined (reading 'destroy')` 错误。核心在于将清理逻辑封装至对象自身的 `destroy()` 方法,并统一通过 group 的安全移除方式触发。
在基于 Phaser 构建的实时交通模拟或竞速类游戏中,常需动态创建/销毁大量实体(如车辆),并借助 PhaserRaycaster 实现彼此间的距离感知与避障逻辑。但若仅调用 car.destroy() 或手动从 Group 中移除对象,而未同步解除其在其他对象 raycaster 中的映射关系,就会导致后续 raycast 更新时尝试访问已销毁对象的 raycasterMap 数据——最终抛出 TypeError: Cannot read properties of undefined (reading 'destroy')。
根本原因在于:raycaster.mapGameObjects(target, true) 建立的是强引用映射,而 raycaster.removeMappedObjects(target) 必须在目标对象仍处于有效生命周期内时调用;一旦对象已被 destroy(),其内部 data、raycaster 等属性即被清空,再调用 removeMappedObjects 将失败。
✅ 正确做法是:将清理责任下沉至被销毁对象自身,并在 Car.destroy() 中完成三重清理:
- 解除本体对其他对象的映射(遍历当前所有已注册的映射对象并移除);
- 通知其他对象移除对本体的映射(需提前维护映射关系索引,见下文优化);
- 销毁自身 raycaster 与 ray 实例,释放资源。
以下是推荐的实现结构(精简关键逻辑):
class Car extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y, rotation, speed) {
super(scene, x, y, "car");
// ... 初始化代码(略)
this.raycaster = scene.raycasterPlugin.createRaycaster();
this.ray = this.raycaster.createRay();
// ✅ 关键:维护一个「被谁映射」的弱引用列表(避免循环引用)
this.mappedBy = []; // 存储其他 Car 实例的引用(用于反向清理)
scene.add.existing(this);
}
// ✅ 核心:重写 destroy,确保资源与映射双向清理
destroy(fromScene = false) {
if (this.scene && !this.isPendingDestroy) {
// 1️⃣ 通知所有映射过自己的对象:移除对本车的引用
this.mappedBy.forEach(otherCar => {
if (otherCar && otherCar.raycaster && !otherCar.isPendingDestroy) {
otherCar.raycaster.removeMappedObjects(this);
}
});
// 2️⃣ 清理自身 raycaster 和 ray
if (this.ray) this.ray.destroy();
if (this.raycaster) this.raycaster.destroy();
// 3️⃣ 清空引用防止内存泄漏
this.mappedBy.length = 0;
}
super.destroy(fromScene);
}
}
同时,在 createCar 中更新映射逻辑,双向记录引用关系:
createCar(scene, x, y, angle, speed, name) {
const car = new Car(scene, x, y, angle, speed);
car.name = name;
// ✅ 双向映射 + 双向注册引用
this.carsGroup.children.iterate((c) => {
// 当前车映射对方
car.raycaster.mapGameObjects(c, true);
// 对方也映射当前车,并记录「被 c 映射」
c.raycaster.mapGameObjects(car, true);
if (!c.mappedBy.includes(car)) {
c.mappedBy.push(car);
}
if (!car.mappedBy.includes(c)) {
car.mappedBy.push(c);
}
});
this.carsGroup.add(car);
}
最后,在 update() 中只需安全移除并触发销毁,无需手动遍历清理:
update() {
this.carsGroup.children.iterate((car) => {
// ✅ 首先检查对象有效性(防御性编程)
if (!car || car.isPendingDestroy || car.destroyed) return;
// ✅ 判断是否出屏(使用 worldView 更可靠)
if (!this.cameras.main.worldView.contains(car.x, car.y)) {
// ✅ 使用 Group.remove(..., true, true) → 自动调用 car.destroy(true)
this.carsGroup.remove(car, true, true); // 第二个 true 表示 destroy
} else {
car.update();
car.debug();
}
});
}
⚠️ 注意事项:
- 不要直接调用 car.destroy() 后再调用 group.remove() —— 这会导致重复销毁;
- group.remove(car, true, true) 是唯一推荐方式:它会自动从 Group 移除、从世界中移除,并触发 car.destroy(true);
- mappedBy 数组建议使用 WeakRef(现代环境)或定期 filter(Boolean) 清理,防止僵尸引用;
- 若项目使用 TypeScript,可在 Car 类中定义 mappedBy: WeakRef
[] 提升类型安全性。
总结:Raycaster 的生命周期必须与游戏对象严格对齐。将清理逻辑内聚于对象自身 destroy() 方法,配合 Group 的原子化移除操作,可彻底规避因引用残留导致的运行时异常,保障大规模动态对象系统的健壮性与可维护性。











