377 lines
9.9 KiB
Markdown
377 lines
9.9 KiB
Markdown
# Enemy双重寻敌机制实现计划
|
|
|
|
## 需求总结
|
|
|
|
1. **规则一:视觉检测与追击**
|
|
- 怪物通过射线检测看到玩家(360°全向视野)
|
|
- 看到玩家后追击
|
|
- 玩家脱离视线3秒后停止追击
|
|
|
|
2. **规则二:铃铛响应**
|
|
- 玩家摇铃铛时,铃铛声波范围内的怪物获取铃铛位置
|
|
- 怪物追击到铃铛位置
|
|
- 追击过程中如果看到玩家,切换为追击玩家(执行规则一)
|
|
- 到达铃铛位置后,如果没有看到玩家,在周围巡逻一段时间
|
|
|
|
## 状态机设计
|
|
|
|
```
|
|
Idle(待机)
|
|
├─ 射线检测到玩家 → ChasingPlayer
|
|
└─ 收到铃铛信号 → ChasingBell
|
|
|
|
ChasingPlayer(追击玩家)
|
|
├─ 持续看到玩家 → 继续追击
|
|
├─ 玩家脱离视线 → 开始计时(3秒)
|
|
│ └─ 3秒内重新看到 → 继续追击
|
|
│ └─ 3秒后仍未看到 → Idle
|
|
└─ 收到铃铛信号 → ChasingBell(打断当前状态)
|
|
|
|
ChasingBell(追击铃铛)
|
|
├─ 追击过程中射线检测到玩家 → ChasingPlayer
|
|
├─ 到达铃铛位置 → PatrollingBell
|
|
└─ 超时(10秒未到达)→ Idle
|
|
|
|
PatrollingBell(铃铛位置巡逻)
|
|
├─ 巡逻过程中射线检测到玩家 → ChasingPlayer
|
|
└─ 巡逻时间结束(5秒)→ Idle
|
|
```
|
|
|
|
## 实现步骤
|
|
|
|
### 步骤1:修改EnemyAI.cs - 添加新状态和参数
|
|
|
|
**文件**: `Assets/enemy/EnemyAI.cs`
|
|
|
|
**新增状态**:
|
|
```csharp
|
|
private enum State
|
|
{
|
|
Idle,
|
|
ChasingPlayer, // 追击玩家
|
|
ChasingBell, // 追击铃铛
|
|
PatrollingBell // 铃铛位置巡逻
|
|
}
|
|
```
|
|
|
|
**新增参数**:
|
|
```csharp
|
|
[Header("视觉检测")]
|
|
[Tooltip("射线检测层(哪些物体会阻挡视线)")]
|
|
[SerializeField] private LayerMask obstacleLayers;
|
|
|
|
[Tooltip("射线起点偏移(避免从地面发射)")]
|
|
[SerializeField] private float rayOffsetY = 0.5f;
|
|
|
|
[Header("脱离计时")]
|
|
[Tooltip("玩家脱离视线后,继续追击的时间(秒)")]
|
|
[SerializeField] private float loseSightTime = 3f;
|
|
|
|
[Header("铃铛追击")]
|
|
[Tooltip("到达铃铛位置后的容差距离")]
|
|
[SerializeField] private float bellArriveDistance = 1f;
|
|
|
|
[Tooltip("追击铃铛的最大时间(超时则放弃)")]
|
|
[SerializeField] private float bellChaseTimeout = 10f;
|
|
|
|
[Header("铃铛巡逻")]
|
|
[Tooltip("到达铃铛位置后的巡逻时间")]
|
|
[SerializeField] private float bellPatrolTime = 5f;
|
|
|
|
[Tooltip("巡逻半径")]
|
|
[SerializeField] private float bellPatrolRadius = 3f;
|
|
```
|
|
|
|
**新增变量**:
|
|
```csharp
|
|
private float _loseSightTimer; // 脱离视线计时器
|
|
private Vector3 _bellTargetPosition; // 铃铛目标位置
|
|
private float _bellChaseTimer; // 铃铛追击计时器
|
|
private Vector3 _patrolCenter; // 巡逻中心点
|
|
private float _patrolTimer; // 巡逻计时器
|
|
private Vector3 _patrolTarget; // 当前巡逻目标点
|
|
```
|
|
|
|
### 步骤2:实现射线检测视线
|
|
|
|
**新增方法**:
|
|
```csharp
|
|
/// <summary>
|
|
/// 检测是否能看到玩家(射线检测)
|
|
/// </summary>
|
|
private bool CanSeePlayer()
|
|
{
|
|
if (playerTarget == null) return false;
|
|
|
|
// 射线起点(稍微抬高,避免从地面发射)
|
|
Vector3 rayStart = transform.position + Vector3.up * rayOffsetY;
|
|
Vector3 rayEnd = playerTarget.position + Vector3.up * rayOffsetY;
|
|
Vector3 dir = rayEnd - rayStart;
|
|
float dist = dir.magnitude;
|
|
|
|
// 发射射线,只检测障碍物层
|
|
if (Physics.Raycast(rayStart, dir.normalized, out RaycastHit hit, dist, obstacleLayers))
|
|
{
|
|
// 如果击中的是玩家,说明可以看到
|
|
return hit.transform == playerTarget;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
```
|
|
|
|
### 步骤3:修改状态机逻辑
|
|
|
|
**Idle状态**:
|
|
```csharp
|
|
private void UpdateIdle()
|
|
{
|
|
// 检测是否看到玩家
|
|
if (CanSeePlayer())
|
|
{
|
|
_state = State.ChasingPlayer;
|
|
_loseSightTimer = 0f;
|
|
return;
|
|
}
|
|
}
|
|
```
|
|
|
|
**ChasingPlayer状态**:
|
|
```csharp
|
|
private void UpdateChasingPlayer()
|
|
{
|
|
// 检测是否看到玩家
|
|
if (CanSeePlayer())
|
|
{
|
|
_loseSightTimer = 0f; // 重置计时器
|
|
|
|
// 追击玩家
|
|
float dist = XZDistance(transform.position, playerTarget.position);
|
|
if (dist > stopDistance)
|
|
{
|
|
MoveToward(playerTarget.position, chaseSpeed);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 玩家脱离视线,开始计时
|
|
_loseSightTimer += Time.deltaTime;
|
|
|
|
// 继续朝玩家最后已知位置移动
|
|
if (playerTarget != null)
|
|
{
|
|
float dist = XZDistance(transform.position, playerTarget.position);
|
|
if (dist > stopDistance)
|
|
{
|
|
MoveToward(playerTarget.position, chaseSpeed);
|
|
}
|
|
}
|
|
|
|
// 超过3秒,停止追击
|
|
if (_loseSightTimer >= loseSightTime)
|
|
{
|
|
_state = State.Idle;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**ChasingBell状态**:
|
|
```csharp
|
|
private void UpdateChasingBell()
|
|
{
|
|
// 追击过程中检测是否看到玩家
|
|
if (CanSeePlayer())
|
|
{
|
|
_state = State.ChasingPlayer;
|
|
_loseSightTimer = 0f;
|
|
return;
|
|
}
|
|
|
|
// 追击计时
|
|
_bellChaseTimer += Time.deltaTime;
|
|
if (_bellChaseTimer >= bellChaseTimeout)
|
|
{
|
|
_state = State.Idle;
|
|
return;
|
|
}
|
|
|
|
// 检测是否到达铃铛位置
|
|
float dist = XZDistance(transform.position, _bellTargetPosition);
|
|
if (dist <= bellArriveDistance)
|
|
{
|
|
// 到达铃铛位置,开始巡逻
|
|
_state = State.PatrollingBell;
|
|
_patrolCenter = _bellTargetPosition;
|
|
_patrolTimer = 0f;
|
|
PickNewPatrolPoint();
|
|
return;
|
|
}
|
|
|
|
// 朝铃铛位置移动
|
|
MoveToward(_bellTargetPosition, chaseSpeed);
|
|
}
|
|
```
|
|
|
|
**PatrollingBell状态**:
|
|
```csharp
|
|
private void UpdatePatrollingBell()
|
|
{
|
|
// 巡逻过程中检测是否看到玩家
|
|
if (CanSeePlayer())
|
|
{
|
|
_state = State.ChasingPlayer;
|
|
_loseSightTimer = 0f;
|
|
return;
|
|
}
|
|
|
|
// 巡逻计时
|
|
_patrolTimer += Time.deltaTime;
|
|
if (_patrolTimer >= bellPatrolTime)
|
|
{
|
|
_state = State.Idle;
|
|
return;
|
|
}
|
|
|
|
// 朝巡逻点移动
|
|
float dist = XZDistance(transform.position, _patrolTarget);
|
|
if (dist <= 0.5f)
|
|
{
|
|
// 到达巡逻点,选择新的巡逻点
|
|
PickNewPatrolPoint();
|
|
}
|
|
else
|
|
{
|
|
MoveToward(_patrolTarget, chaseSpeed * 0.7f); // 巡逻时速度稍慢
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 选择新的巡逻点
|
|
/// </summary>
|
|
private void PickNewPatrolPoint()
|
|
{
|
|
// 在巡逻中心周围随机选择一个点
|
|
Vector2 randomDir = Random.insideUnitCircle * bellPatrolRadius;
|
|
_patrolTarget = _patrolCenter + new Vector3(randomDir.x, 0f, randomDir.y);
|
|
}
|
|
```
|
|
|
|
### 步骤4:修改铃铛响应逻辑
|
|
|
|
**OnBell方法**:
|
|
```csharp
|
|
private void OnBell(Vector3 bellPos)
|
|
{
|
|
if (playerTarget == null) return;
|
|
|
|
// 只有在聆听范围内才响应
|
|
if (XZDistance(transform.position, bellPos) > listenRange) return;
|
|
|
|
// 设置铃铛目标位置
|
|
_bellTargetPosition = bellPos;
|
|
_bellChaseTimer = 0f;
|
|
|
|
// 切换到追击铃铛状态(可以打断任何状态)
|
|
_state = State.ChasingBell;
|
|
}
|
|
```
|
|
|
|
### 步骤5:更新状态机调用
|
|
|
|
**Update方法**:
|
|
```csharp
|
|
private void Update()
|
|
{
|
|
switch (_state)
|
|
{
|
|
case State.Idle: UpdateIdle(); break;
|
|
case State.ChasingPlayer: UpdateChasingPlayer(); break;
|
|
case State.ChasingBell: UpdateChasingBell(); break;
|
|
case State.PatrollingBell: UpdatePatrollingBell(); break;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 步骤6:更新编辑器可视化
|
|
|
|
**OnDrawGizmos方法**:
|
|
```csharp
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
// 根据状态显示不同颜色
|
|
Color stateColor = Color.white;
|
|
switch (_state)
|
|
{
|
|
case State.Idle: stateColor = Color.gray; break;
|
|
case State.ChasingPlayer: stateColor = Color.red; break;
|
|
case State.ChasingBell: stateColor = Color.yellow; break;
|
|
case State.PatrollingBell: stateColor = Color.cyan; break;
|
|
}
|
|
|
|
// 绘制当前状态指示
|
|
Gizmos.color = stateColor;
|
|
Gizmos.DrawWireSphere(transform.position, 0.5f);
|
|
|
|
// 绘制铃铛目标(如果在追击铃铛)
|
|
if (_state == State.ChasingBell || _state == State.PatrollingBell)
|
|
{
|
|
Gizmos.color = Color.yellow;
|
|
Gizmos.DrawWireSphere(_bellTargetPosition, bellArriveDistance);
|
|
|
|
// 绘制到铃铛的连线
|
|
Gizmos.DrawLine(transform.position, _bellTargetPosition);
|
|
}
|
|
|
|
// 绘制巡逻范围(如果在巡逻)
|
|
if (_state == State.PatrollingBell)
|
|
{
|
|
Gizmos.color = Color.cyan;
|
|
Gizmos.DrawWireSphere(_patrolCenter, bellPatrolRadius);
|
|
|
|
// 绘制当前巡逻目标
|
|
Gizmos.color = Color.green;
|
|
Gizmos.DrawWireSphere(_patrolTarget, 0.3f);
|
|
}
|
|
}
|
|
```
|
|
|
|
## 配置参数建议
|
|
|
|
| 参数 | 建议值 | 说明 |
|
|
|-----|--------|------|
|
|
| `obstacleLayers` | Obstacle | 阻挡视线的物体层 |
|
|
| `rayOffsetY` | 0.5 | 射线起点高度 |
|
|
| `loseSightTime` | 3.0 | 脱离视线后继续追击时间 |
|
|
| `bellArriveDistance` | 1.0 | 到达铃铛位置的容差 |
|
|
| `bellChaseTimeout` | 10.0 | 追击铃铛最大时间 |
|
|
| `bellPatrolTime` | 5.0 | 铃铛位置巡逻时间 |
|
|
| `bellPatrolRadius` | 3.0 | 巡逻半径 |
|
|
|
|
## 测试要点
|
|
|
|
1. **视觉检测测试**
|
|
- 在Enemy和玩家之间放置障碍物,验证射线被阻挡
|
|
- 移除障碍物,验证能检测到玩家
|
|
|
|
2. **脱离视线测试**
|
|
- 玩家跑出Enemy视线,验证3秒后停止追击
|
|
- 3秒内重新出现,验证继续追击
|
|
|
|
3. **铃铛响应测试**
|
|
- 玩家摇铃铛,验证Enemy朝铃铛位置移动
|
|
- 铃铛追击过程中看到玩家,验证切换为追击玩家
|
|
|
|
4. **巡逻测试**
|
|
- 到达铃铛位置后,验证在周围巡逻
|
|
- 巡逻过程中看到玩家,验证切换为追击玩家
|
|
|
|
## 文件清单
|
|
|
|
- 修改: `Assets/enemy/EnemyAI.cs`
|
|
|
|
## 预计工作量
|
|
|
|
- 实现时间: 2-3小时
|
|
- 调试时间: 1-2小时
|
|
- 总计: 3-5小时 |