Flutter+Flame:如何实现带视野半径、盲区、受遮挡的敌人视野
Flutter + Flame 潜行类游戏敌人遮挡视野实现方案
核心实现逻辑
整个功能不用引入额外依赖,靠Flame自带的射线检测、多边形渲染能力就能实现,拆成三个核心模块:
- 基于扇形范围发射射线,被障碍物截断后生成带遮挡的视野轮廓
- 排除近身/角度外的盲区范围
- 基于轮廓渲染可视区域,同时做玩家进入视野的判定
分步实现
1. 敌人基础属性定义
先给敌人组件配置视野相关的基础参数,后续所有逻辑都围绕这些参数计算:
import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; class StealthEnemy extends PositionComponent with HasGameRef<StealthGame> { StealthEnemy({required super.position, required super.size}) : super(anchor: Anchor.center); // 视野配置参数,可根据不同敌人类型调整 final double viewMaxRadius = 160; // 视野最大距离 final double viewFOV = 110 * degrees2Radians; // 视野张角(110度,左右各55度) final double blindRadius = 28; // 近身盲区半径,进入这个范围敌人看不见 final double turnSpeed = 0.8; // 巡逻时转头速度 Vector2 faceDir = Vector2(0, -1); // 初始面朝方向(这里是朝上) List<Vector2> viewPolygon = []; // 存最终生成的视野轮廓点 late PolygonComponent viewRenderComp; // 视野渲染组件 }
2. 射线检测生成带遮挡的视野轮廓
在敌人面朝的视野角度范围内,每隔固定小角度发射一条射线,射线碰到障碍物就取碰撞点,没碰到就取最大视野半径处的点,所有点连起来就是不规则的视野形状:
List<Vector2> _calcViewShape() { final shapePoints = <Vector2>[]; final baseAngle = faceDir.screenAngle() - viewFOV / 2; // 每1.5度发一条射线,平衡精度和性能,移动端可以调到2度 final rayStep = 1.5 * degrees2Radians; final rayCount = (viewFOV / rayStep).round(); for (var i = 0; i <= rayCount; i++) { final curAngle = baseAngle + i * rayStep; final rayDir = Vector2(cos(curAngle), sin(curAngle)); // 射线只检测场景里的障碍物碰撞盒 final hitResult = game.collisionDetection.raycast( Ray2(origin: absolutePosition, direction: rayDir), maxDistance: viewMaxRadius, hitboxes: game.obstacleHitboxList, ); Vector2 hitPoint; if (hitResult != null) { hitPoint = hitResult.hitpoint!; } else { hitPoint = absolutePosition + rayDir * viewMaxRadius; } // 处理近身盲区:碰撞点在盲区内的话,取盲区边界作为点 final dist = absolutePosition.distanceTo(hitPoint); if (dist < blindRadius) { hitPoint = absolutePosition + rayDir * blindRadius; } shapePoints.add(hitPoint); } return shapePoints; }
提前把场景里所有墙体、障碍物的碰撞盒存在游戏全局的
obstacleHitboxList里,不用每次射线检测都遍历所有组件,性能提升很明显。
3. 视野区域渲染
用多边形组件渲染半透明的视野区域,记得调整渲染层级,让视野在障碍物下层、地面上层,自然呈现被遮挡的效果:
@override Future<void> onLoad() async { // 初始化视野渲染组件,默认半透明黄色,警觉状态可以改成红色 viewRenderComp = PolygonComponent( [Vector2.zero()], paint: Paint()..color = const Color.fromARGB(90, 255, 230, 0), anchor: Anchor.center, priority: 10, // 优先级比地面高,比障碍物(priority>=20)低 ); add(viewRenderComp); // 敌人自身的碰撞盒、巡逻逻辑省略 return super.onLoad(); } @override void update(double dt) { super.update(dt); // 这里可以加巡逻转头、移动的逻辑 // 只有当敌人移动/转头时才重新计算视野,静止时直接缓存不用重复算 viewPolygon = _calcViewShape(); // 世界坐标转组件本地坐标,给多边形组件用 viewRenderComp.points = viewPolygon.map((p) => p - absolutePosition).toList(); }
如果要做更自然的视野边缘渐变,可以给Paint加径向渐变shader,从靠近敌人的半透明过渡到边缘完全透明,更贴近参考图的效果。
4. 玩家被发现判定
不用做点在多边形内的复杂计算,三个条件同时满足就判定玩家被发现:
- 玩家和敌人的距离在盲区外、最大视野半径内
- 玩家相对于敌人的角度在视野张角范围内
- 敌人到玩家的射线没有被障碍物挡住
bool checkPlayerDetected(Player player) { final toPlayer = player.absolutePosition - absolutePosition; final dist = toPlayer.length; // 距离校验 if (dist < blindRadius || dist > viewMaxRadius) return false; // 角度校验 final angleDiff = toPlayer.angleToSigned(faceDir).abs(); if (angleDiff > viewFOV / 2) return false; // 遮挡校验 final blockHit = game.collisionDetection.raycast( Ray2(origin: absolutePosition, direction: toPlayer.normalized()), maxDistance: dist, hitboxes: game.obstacleHitboxList, ); return blockHit == null; }
性能优化建议
- 敌人静止、朝向不变时直接缓存视野多边形,不用每帧重算射线,只有移动/转头/障碍物位移时再更新
- 玩家检测不用每帧跑,每3~4帧检测一次完全不会有感知延迟
- 低端设备可以把射线间隔调到2~2.5度,减少射线数量,视觉上几乎没有差异
常见坑
- 所有射线计算统一用世界坐标,渲染时再转组件本地坐标,不然会出现视野偏移错位
- 障碍物碰撞盒要和美术资源完全对齐,不然会出现视野穿模、该挡住的地方没挡住的问题
- 如果要做背后盲区,只要调整射线的角度覆盖范围即可,不用额外加逻辑
内容的提问来源于stack exchange,提问作者Abdurraheem Abdulhakeem
相关产品推荐
相关产品推荐

