Cinemachine(二)制作不会穿墙(会避开障碍物)的摄像头(Cinemachine Collider)

前言

在许多的第三人称游戏中,我们的摄像机往往保持着一定的偏移,跟随着我们的角色。但是常常会出现的一种情况就是,当我们的角色走进室内或者靠近墙边或障碍物的时候,如果这个偏移保持不变的话,我们的摄像机就可能会跑到墙体里,或者看向角色时被墙体或障碍物所挡住。因此这种情况下我们的相机往往需要一些特殊的处理,例如修改偏移量来拉近相机与角色的距离等,来防止上述情况的出现。

在以往我们可能需要通过射线检测或者碰撞检测等,然后编写一大堆的代码做一堆复杂的判断来修改我们的摄像机的位置。但是有了Cinemachine后,我们只需要通过一些简单的设置,就可以不用写任何代码来实现上面的效果了,并且效果也是很棒的,可以大大提高了我们的研发效率。

视频链接:https://www.bilibili.com/video/BV1Kt411Y71n

Demo:https://github.com/luckyWjr/Demo Cinemachine文件夹下的DemoCollider

 

准备

我们可以先创建一个测试场景,添加一个胶囊体来模拟角色。根据第一篇所提到的知识,我们可以在测试场景中创建一个VirtualCamera,将其的Follow和Look At属性都设置成我们添加的胶囊体,这样一个简单的第三人称摄像头就实现了。接着我们可以简单的添加一些障碍物,然后移动我们的胶囊体,使其经过障碍物,例如下图:

我们可以发现在未经特殊处理的情况下,障碍物就会遮住我们的角色,这必然是一种不友好的游戏体验。我们

<think>在障碍物控制器中,我们需要添加一个变量来标记障碍物是否已经被销毁,以防止次销毁。这个变量可以在障碍物开始销毁时设置为true,并在重置时设置为false。这样,在销毁过程中如果再次被触发销毁,就可以跳过。 以下是修改后的代码: 1. 在 `ObstaclesController` 类中添加一个私有变量 `_isDestroyed`,初始值为 `false`。 2. 在 `destroyObstacle` 方法开始时检查 `_isDestroyed`,如果已经销毁则直接返回。 3. 在开始销毁流程时,将 `_isDestroyed` 设置为 `true`。 4. 在 `reset` 方法中,将 `_isDestroyed` 重置为 `false`。 具体修改如下: ```typescript export class ObstaclesController extends Component { // ... 已有代码 ... // 标记是否已被销毁 private _isDestroyed: boolean = false; // ... 其他代码 ... public destroyObstacle(): void { // 防止次销毁 if (this._isDestroyed) { return; } this._isDestroyed = true; // 标记为已销毁 // 1. 禁用碰撞体 const collider = this.node.getChildByName('Body')?.getComponent(Collider2D); if (collider) { collider.enabled = false; } // 2. 禁用Label if (this.labelNode) { this.labelNode.enabled = false; } // 3. 播放死亡动画(非Obstacle7类型) if (this._obstacleType !== ObstacleType.OBSTACLE7) { // 播放销毁特效(根据障碍物名称) const effectPos = this.ParticlePos ? this.ParticlePos.worldPosition : this.node.worldPosition; EffectController.getInstance().playDeathEffect(this.node.name, effectPos); AudioMgr.inst.playOneShot('/audio/Monster_Death',0.8) this.switchAnimationState(AnimationState.DEATH); return; // 动画播放完成后会调用completeDestruction } // 4. Obstacle7类型的特殊处理 const sprite = this.bodyNode.getComponent(Sprite); // 5. 创建禁用动画 if (this._obstacleType === ObstacleType.OBSTACLE7){ tween(this.bodyNode) .to(0.3, { scale: new Vec3(0.3, 0.3, 1) }) .call(() => { if (sprite) { sprite.enabled = false; } // 0.5秒后禁用整个节点 this.scheduleOnce(() => { if (sprite) { sprite.enabled = true; } this.node.active = false; }, 0.5); }) .start(); } else { // 0.7秒后禁用整个节点 this.scheduleOnce(() => { this.node.active = false; }, 0.7); } // 在销毁障碍物后检查状态 CreateObstacles.getInstance().checkObstaclesStatus(); } public reset(): void { // 重置销毁标记 this._isDestroyed = false; // 重置节点激活状态 this.node.active = true; // ... 其他重置代码 ... } // ... 其他代码 ... } ``` 这样,我们就通过 `_isDestroyed` 变量确保了每个障碍物只会被销毁一次,避免了重复销毁的问题。 注意:在 `completeDestruction` 方法中,我们只是将节点设为非激活,并没有改变 `_isDestroyed` 的状态。因为重置操作是在 `reset` 方法中统一处理的,当障碍物被重置时,`reset` 方法会将 `_isDestroyed` 重置为 `false`。 §§相关问题§§: 1. 在游戏开发中,如何避免游戏对象被重复销毁? 2. 使用状态标记管理游戏对象生命周期有哪些优缺点? 3. 在Cocos Creator中,节点销毁和节点禁用有什么区别? 4. 如何设计一个安全的游戏对象池来管理频繁创建销毁的对象? </think>为了解决障碍物次销毁的问题,我将添加一个状态变量 `_isDestroyed` 来标记障碍物是否已被销毁。这样可以防止在销毁过程中重复调用销毁方法。 ### 修改后的障碍物控制器代码 ```typescript export class ObstaclesController extends Component { // ... 其他代码 ... // 添加销毁状态变量 private _isDestroyed: boolean = false; // ... 其他代码 ... public destroyObstacle(): void { // 检查是否已被销毁,防止次销毁 if (this._isDestroyed) { return; } // 标记为已销毁 this._isDestroyed = true; // 1. 禁用碰撞体 const collider = this.node.getChildByName('Body')?.getComponent(Collider2D); if (collider) { collider.enabled = false; } // 2. 禁用Label if (this.labelNode) { this.labelNode.enabled = false; } // 3. 播放死亡动画(非Obstacle7类型) if (this._obstacleType !== ObstacleType.OBSTACLE7) { // 播放销毁特效(根据障碍物名称) const effectPos = this.ParticlePos ? this.ParticlePos.worldPosition : this.node.worldPosition; EffectController.getInstance().playDeathEffect(this.node.name, effectPos); AudioMgr.inst.playOneShot('/audio/Monster_Death', 0.8); this.switchAnimationState(AnimationState.DEATH); // 在动画完成后禁用节点 this.scheduleOnce(() => { this.node.active = false; }, 0.7); return; // 动画播放完成后会调用completeDestruction } // 4. Obstacle7类型的特殊处理 const sprite = this.bodyNode.getComponent(Sprite); // 5. 创建禁用动画 if (this._obstacleType === ObstacleType.OBSTACLE7) { tween(this.bodyNode) .to(0.3, { scale: new Vec3(0.3, 0.3, 1) }) .call(() => { if (sprite) { sprite.enabled = false; } // 0.5秒后禁用整个节点 this.scheduleOnce(() => { if (sprite) { sprite.enabled = true; } this.node.active = false; }, 0.5); }) .start(); } // 在销毁障碍物后检查状态 CreateObstacles.getInstance().checkObstaclesStatus(); } public reset(): void { // 重置销毁标记 this._isDestroyed = false; // 重置节点激活状态 this.node.active = true; // ... 其他重置代码 ... } // ... 其他代码 ... } ``` ### 修改说明 1. **添加销毁状态变量 `_isDestroyed`**: ```typescript private _isDestroyed: boolean = false; ``` - 初始设置为 `false` - 在调用 `destroyObstacle()` 方法时检查这个变量 - 在销毁开始时将其设置为 `true` 2. **防止次销毁**: ```typescript public destroyObstacle(): void { // 检查是否已被销毁,防止次销毁 if (this._isDestroyed) { return; } // 标记为已销毁 this._isDestroyed = true; // ... 后续销毁逻辑 ... } ``` 3. **重置状态**: ```typescript public reset(): void { // 重置销毁标记 this._isDestroyed = false; // ... 其他重置代码 ... } ``` - 在重置方法中将 `_isDestroyed` 重置为 `false` - 确保障碍物重新激活时可再次被销毁 4. **完善销毁流程**: ```typescript // 普通障碍物类型 this.scheduleOnce(() => { this.node.active = false; }, 0.7); // OBSTACLE7类型 tween(this.bodyNode) .call(() => { // ... this.scheduleOnce(() => { this.node.active = false; }, 0.5); }) ``` - 确保在所有销毁路径中最终都设置了 `this.node.active = false` - 使用 `scheduleOnce` 确保异步操作完成后才禁用节点 ### 为什么需要这个修改 1. **防止重复调用**: - 在游戏开发中,物理引擎或碰撞检测可能多次触发同一事件 - 如果不加保护,可能导致同一障碍物被多次销毁 2. **状态一致性**: - 确保销毁状态在游戏逻辑中保持一致 - 避免因次销毁导致的游戏状态错误 3. **性能优化**: - 避免不必要的销毁操作和资源浪费 - 防止重复播放特效和声音 ### 最佳实践建议 1. **状态机模式**: ```typescript enum ObstacleState { ACTIVE, DESTROYING, DESTROYED } private state: ObstacleState = ObstacleState.ACTIVE; public destroyObstacle(): void { if (this.state !== ObstacleState.ACTIVE) return; this.state = ObstacleState.DESTROYING; // ... } ``` 使用状态机可以更清晰地管理物体生命周期 2. **事件驱动**: ```typescript // 在销毁完成时触发事件 onDestroyComplete(callback: Function) { // ... callback(); } // 在游戏管理器中监听销毁事件 obstacle.onDestroyComplete(() => { // 更新游戏状态 }); ``` 使用事件驱动可以解耦游戏逻辑
评论 7
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值