From 6cc4f1035defd1a047cab051eb43163684445eed Mon Sep 17 00:00:00 2001
From: keshaohong <740612340@qq.com>
Date: Sun, 28 Jun 2026 23:08:20 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0mcp=E6=9C=8D=E5=8A=A1?=
=?UTF-8?q?=E5=99=A8=20=E4=BC=98=E5=8C=96=E6=95=8C=E4=BA=BAAI=EF=BC=8C?=
=?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=88=90=E5=8F=8C=E9=87=8D=E8=A7=84=E5=88=99?=
=?UTF-8?q?=E5=AF=BB=E6=95=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
unity/.mcp.json | 15 +
unity/.vscode/settings.json | 3 +-
unity/Assets/2.5D Engine/Enemy.prefab | 17 +-
unity/Assets/Editor/TilemapYSortTool.cs | 102 ++++++
unity/Assets/Editor/TilemapYSortTool.cs.meta | 11 +
unity/Assets/camera/YSorter.cs | 46 ++-
unity/Assets/eco/EchoSystem.cs | 29 ++
unity/Assets/enemy/EnemyAI.cs | 349 ++++++++++++++-----
unity/CLAUDE.md | 17 +
9 files changed, 473 insertions(+), 116 deletions(-)
create mode 100644 unity/.mcp.json
create mode 100644 unity/Assets/Editor/TilemapYSortTool.cs
create mode 100644 unity/Assets/Editor/TilemapYSortTool.cs.meta
create mode 100644 unity/CLAUDE.md
diff --git a/unity/.mcp.json b/unity/.mcp.json
new file mode 100644
index 0000000..88aef4d
--- /dev/null
+++ b/unity/.mcp.json
@@ -0,0 +1,15 @@
+{
+ "mcpServers": {
+ "unity-api": {
+ "command": "uvx",
+ "args": ["unity-api-mcp"],
+ "env": {
+ "UNITY_VERSION": "2022"
+ }
+ },
+ "fetch": {
+ "command": "uvx",
+ "args": ["mcp-server-fetch"]
+ }
+ }
+}
diff --git a/unity/.vscode/settings.json b/unity/.vscode/settings.json
index dd0fb74..16b93bc 100644
--- a/unity/.vscode/settings.json
+++ b/unity/.vscode/settings.json
@@ -51,5 +51,6 @@
"temp/": true,
"Temp/": true
},
- "dotnet.defaultSolution": "unity.sln"
+ "dotnet.defaultSolution": "unity.sln",
+ "dotnet.preferCSharpExtension": true
}
\ No newline at end of file
diff --git a/unity/Assets/2.5D Engine/Enemy.prefab b/unity/Assets/2.5D Engine/Enemy.prefab
index 4cf9307..529b712 100644
--- a/unity/Assets/2.5D Engine/Enemy.prefab
+++ b/unity/Assets/2.5D Engine/Enemy.prefab
@@ -213,13 +213,18 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
playerTarget: {fileID: 0}
- detectionRange: 8
- chaseSpeed: 3
stopDistance: 1.2
- chasePersistTime: 3
+ chaseSpeed: 3
+ obstacleLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ rayOffsetY: 0.5
+ loseSightTime: 3
listenRange: 15
- bellMoveSpeed: 4
- bellMoveTime: 1.5
+ bellArriveDistance: 1
+ bellChaseTimeout: 10
+ bellPatrolTime: 5
+ bellPatrolRadius: 3
--- !u!114 &2877034408413616420
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -235,6 +240,8 @@ MonoBehaviour:
lockYAxis: 1
smoothRotation: 0
rotationSpeed: 8
+ snapAngle: 0.5
+ deadZone: 0.2
--- !u!1 &8532340994753211982
GameObject:
m_ObjectHideFlags: 0
diff --git a/unity/Assets/Editor/TilemapYSortTool.cs b/unity/Assets/Editor/TilemapYSortTool.cs
new file mode 100644
index 0000000..c21165d
--- /dev/null
+++ b/unity/Assets/Editor/TilemapYSortTool.cs
@@ -0,0 +1,102 @@
+using UnityEngine;
+using UnityEditor;
+using UnityEngine.Tilemaps;
+
+///
+/// Tilemap Y-Sort工具 - 快速给Tilemap添加YSorter
+///
+public class TilemapYSortTool : Editor
+{
+ [MenuItem("Tools/Tilemap/给选中Tilemap添加YSorter")]
+ public static void AddYSorterToTilemap()
+ {
+ if (Selection.gameObjects.Length == 0)
+ {
+ EditorUtility.DisplayDialog("提示", "请先选择Tilemap物体", "确定");
+ return;
+ }
+
+ int count = 0;
+ foreach (GameObject go in Selection.gameObjects)
+ {
+ // 检查是否有TilemapRenderer
+ TilemapRenderer renderer = go.GetComponentInChildren();
+ if (renderer == null)
+ {
+ Debug.LogWarning($"跳过 {go.name} - 没有TilemapRenderer组件");
+ continue;
+ }
+
+ // 检查是否已有YSorter
+ if (go.GetComponent() != null)
+ {
+ Debug.Log($"跳过 {go.name} - 已有YSorter");
+ continue;
+ }
+
+ // 添加YSorter
+ YSorter ySorter = go.AddComponent();
+
+ // 设置默认的baseSortingOrder
+ SerializedObject so = new SerializedObject(ySorter);
+ SerializedProperty prop = so.FindProperty("baseSortingOrder");
+ if (prop != null)
+ {
+ prop.intValue = renderer.sortingOrder;
+ so.ApplyModifiedProperties();
+ }
+
+ EditorUtility.SetDirty(go);
+ count++;
+ Debug.Log($"✓ 已添加YSorter到 {go.name} (baseSortingOrder: {renderer.sortingOrder})");
+ }
+
+ if (count > 0)
+ {
+ EditorUtility.DisplayDialog("完成", $"已给 {count} 个Tilemap添加YSorter", "确定");
+ }
+ }
+
+ [MenuItem("Tools/Tilemap/批量添加YSorter到所有Tilemap")]
+ public static void AddYSorterToAllTilemaps()
+ {
+ TilemapRenderer[] allRenderers = FindObjectsByType(FindObjectsSortMode.None);
+
+ if (allRenderers.Length == 0)
+ {
+ EditorUtility.DisplayDialog("提示", "场景中没有找到TilemapRenderer", "确定");
+ return;
+ }
+
+ int count = 0;
+ foreach (TilemapRenderer renderer in allRenderers)
+ {
+ GameObject go = renderer.gameObject;
+
+ if (go.GetComponent() != null)
+ {
+ Debug.Log($"跳过 {go.name} - 已有YSorter");
+ continue;
+ }
+
+ YSorter ySorter = go.AddComponent();
+
+ SerializedObject so = new SerializedObject(ySorter);
+ SerializedProperty prop = so.FindProperty("baseSortingOrder");
+ if (prop != null)
+ {
+ prop.intValue = renderer.sortingOrder;
+ so.ApplyModifiedProperties();
+ }
+
+ EditorUtility.SetDirty(go);
+ count++;
+ Debug.Log($"✓ 已添加YSorter到 {go.name} (baseSortingOrder: {renderer.sortingOrder})");
+ }
+
+ if (count > 0)
+ {
+ EditorUtility.DisplayDialog("完成", $"已给 {count} 个Tilemap添加YSorter", "确定");
+ }
+ }
+}
diff --git a/unity/Assets/Editor/TilemapYSortTool.cs.meta b/unity/Assets/Editor/TilemapYSortTool.cs.meta
new file mode 100644
index 0000000..3fa3c87
--- /dev/null
+++ b/unity/Assets/Editor/TilemapYSortTool.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: CH5JsXykVyiioh6PdpvMd04QErrpba3U3rPu7hQrZpCz8EMMsg29s0w=
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/Assets/camera/YSorter.cs b/unity/Assets/camera/YSorter.cs
index abaf178..ae34768 100644
--- a/unity/Assets/camera/YSorter.cs
+++ b/unity/Assets/camera/YSorter.cs
@@ -1,20 +1,9 @@
using UnityEngine;
+using UnityEngine.Tilemaps;
///
/// Y-Sort 排序器 —— 根据物体的Z坐标(世界空间)动态调整Sprite的sortingOrder。
-///
-/// 原理:
-/// 在2.5D等距视角游戏中,摄像机从斜上方45°俯视,Z坐标代表"前后"关系。
-/// Z值越大(离相机越远=越靠前),sortingOrder越大,会遮挡Z值小的物体。
-///
-/// 用法:
-/// 1. 挂到所有需要参与排序的Sprite物体上(玩家、敌人、树木、石头等)
-/// 2. 设置 baseSortingOrder 区分不同类型的物体(如地面=0,角色=1000)
-/// 3. 确保所有参与排序的Sprite在同一个Sorting Layer上
-///
-/// 注意:
-/// - 本脚本假设摄像机从斜上方俯视(Z轴代表前后)
-/// - 所有参与排序的物体必须在同一个Sorting Layer
+/// 支持SpriteRenderer和TilemapRenderer。
///
public class YSorter : MonoBehaviour
{
@@ -28,21 +17,32 @@ public class YSorter : MonoBehaviour
[SerializeField] private bool updateEveryFrame = true;
private SpriteRenderer _spriteRenderer;
+ private TilemapRenderer _tilemapRenderer;
private Vector3 _lastPosition;
private void Awake()
{
+ // 尝试获取SpriteRenderer
_spriteRenderer = GetComponentInChildren();
+
+ // 如果没有SpriteRenderer,尝试获取TilemapRenderer
if (_spriteRenderer == null)
{
- Debug.LogWarning($"YSorter: 物体 {gameObject.name} 没有找到SpriteRenderer组件");
+ _tilemapRenderer = GetComponentInChildren();
+
+ if (_tilemapRenderer == null)
+ {
+ Debug.LogWarning($"YSorter: 物体 {gameObject.name} 没有找到SpriteRenderer或TilemapRenderer组件");
+ }
}
+
_lastPosition = transform.position;
}
private void LateUpdate()
{
- if (_spriteRenderer == null) return;
+ // 如果没有任何渲染器,返回
+ if (_spriteRenderer == null && _tilemapRenderer == null) return;
// 如果设置了不是每帧更新,只在位置改变时更新
if (!updateEveryFrame && transform.position == _lastPosition)
@@ -55,15 +55,25 @@ public class YSorter : MonoBehaviour
private void UpdateSortingOrder()
{
// 使用物体中心(transform.position)的Z坐标作为排序依据
- // 这样物品中心在地面上方时,会正确遮挡地面
float sortZ = transform.position.z;
// Z值越大(离相机越远),sortingOrder越大
int sortingOrder = baseSortingOrder + Mathf.RoundToInt(sortZ * scaleFactor);
- if (_spriteRenderer.sortingOrder != sortingOrder)
+ // 更新对应的渲染器
+ if (_spriteRenderer != null)
{
- _spriteRenderer.sortingOrder = sortingOrder;
+ if (_spriteRenderer.sortingOrder != sortingOrder)
+ {
+ _spriteRenderer.sortingOrder = sortingOrder;
+ }
+ }
+ else if (_tilemapRenderer != null)
+ {
+ if (_tilemapRenderer.sortingOrder != sortingOrder)
+ {
+ _tilemapRenderer.sortingOrder = sortingOrder;
+ }
}
}
}
diff --git a/unity/Assets/eco/EchoSystem.cs b/unity/Assets/eco/EchoSystem.cs
index 2c1abe7..aa7e5a8 100644
--- a/unity/Assets/eco/EchoSystem.cs
+++ b/unity/Assets/eco/EchoSystem.cs
@@ -162,5 +162,34 @@ namespace IndianOceanAssets.Engine2_5D
}
public bool IsActive => _state != State.Idle;
+
+ // ===== 编辑器可视化 =====
+ private void OnDrawGizmosSelected()
+ {
+ // 绘制回声最大范围(青色线框)
+ Gizmos.color = new Color(0f, 1f, 1f, 0.3f);
+ Gizmos.DrawWireSphere(transform.position, maxRadius);
+
+ // 绘制当前波纹半径(如果在扩散中)
+ if (_state != State.Idle && _radius > 0)
+ {
+ Gizmos.color = new Color(1f, 1f, 0f, 0.5f);
+ Gizmos.DrawWireSphere(new Vector3(_center.x, transform.position.y, _center.y), _radius);
+
+ // 绘制波纹中心
+ Gizmos.color = Color.yellow;
+ Gizmos.DrawWireSphere(new Vector3(_center.x, transform.position.y, _center.y), 0.3f);
+ }
+
+ // 绘制冷却时间指示
+ if (Time.time < _lastEchoTime + cooldown)
+ {
+ float remaining = (_lastEchoTime + cooldown) - Time.time;
+ float cooldownRatio = remaining / cooldown;
+
+ Gizmos.color = new Color(1f, 0f, 0f, 0.3f);
+ Gizmos.DrawWireSphere(transform.position, maxRadius * cooldownRatio);
+ }
+ }
}
}
diff --git a/unity/Assets/enemy/EnemyAI.cs b/unity/Assets/enemy/EnemyAI.cs
index ff1e0e9..eabedff 100644
--- a/unity/Assets/enemy/EnemyAI.cs
+++ b/unity/Assets/enemy/EnemyAI.cs
@@ -3,15 +3,13 @@ using UnityEngine;
namespace IndianOceanAssets.Engine2_5D
{
///
- /// 敌人AI —— 检测/追击/脱离持续/摇铃聆听响应。
+ /// 敌人AI v2 —— 双重寻敌机制(视觉检测 + 铃铛响应)
///
/// 状态流程:
- /// - Idle:站岗。主角进入 detectionRange → Chasing
- /// - Chasing:追击主角。主角离开 detectionRange → ChasingPersist
- /// - ChasingPersist:脱离后继续追击 chasePersistTime 秒。
- /// 主角重新进入范围 → 回到 Chasing;超时 → Idle(停在当前位置,不回出生点)
- /// - BellResponding:主角在 listenRange 内按E摇铃时触发,
- /// 朝主角方向移动 bellMoveTime 秒(距离 = 速度 × 时间),然后停下 → Idle
+ /// - Idle:待机。射线检测到玩家 → ChasingPlayer;收到铃铛信号 → ChasingBell
+ /// - ChasingPlayer:追击玩家。持续看到玩家则追击;脱离视线3秒后 → Idle
+ /// - ChasingBell:追击铃铛。追击过程中看到玩家 → ChasingPlayer;到达铃铛位置 → PatrollingBell
+ /// - PatrollingBell:铃铛位置巡逻。巡逻过程中看到玩家 → ChasingPlayer;巡逻时间结束 → Idle
///
/// 挂载位置:敌人预制体上
/// 需要:场景中有 EchoSystem(用于摇铃事件)、玩家物体带 "Player" 标签
@@ -24,32 +22,63 @@ namespace IndianOceanAssets.Engine2_5D
[SerializeField] private Transform playerTarget;
[Header("检测与追击")]
- [Tooltip("发现主角的范围")]
- [SerializeField] private float detectionRange = 8f;
+ [Tooltip("靠近主角到此距离时停下")]
+ [SerializeField] private float stopDistance = 1.2f;
[Tooltip("追击速度")]
[SerializeField] private float chaseSpeed = 3f;
- [Tooltip("靠近主角到此距离时停下")]
- [SerializeField] private float stopDistance = 1.2f;
+ [Header("视觉检测")]
+ [Tooltip("射线检测层(哪些物体会阻挡视线)")]
+ [SerializeField] private LayerMask obstacleLayers;
- [Tooltip("主角脱离检测范围后,继续追击的时间(秒)")]
- [SerializeField] private float chasePersistTime = 3f;
+ [Tooltip("射线起点偏移(避免从地面发射)")]
+ [SerializeField] private float rayOffsetY = 0.5f;
+
+ [Header("脱离计时")]
+ [Tooltip("玩家脱离视线后,继续追击的时间(秒)")]
+ [SerializeField] private float loseSightTime = 3f;
[Header("聆听(摇铃响应)")]
- [Tooltip("聆听范围:主角在此范围内按E摇铃时,敌人会朝主角方向移动")]
+ [Tooltip("聆听范围:主角在此范围内按E摇铃时,敌人会朝铃铛位置移动")]
[SerializeField] private float listenRange = 15f;
- [Tooltip("摇铃响应移动速度")]
- [SerializeField] private float bellMoveSpeed = 4f;
+ [Header("铃铛追击")]
+ [Tooltip("到达铃铛位置后的容差距离")]
+ [SerializeField] private float bellArriveDistance = 1f;
- [Tooltip("摇铃响应移动时间(距离 = 速度 × 时间)")]
- [SerializeField] private float bellMoveTime = 1.5f;
+ [Tooltip("追击铃铛的最大时间(超时则放弃)")]
+ [SerializeField] private float bellChaseTimeout = 10f;
+
+ [Header("铃铛巡逻")]
+ [Tooltip("到达铃铛位置后的巡逻时间")]
+ [SerializeField] private float bellPatrolTime = 5f;
+
+ [Tooltip("巡逻半径")]
+ [SerializeField] private float bellPatrolRadius = 3f;
+
+ // 状态枚举
+ private enum State
+ {
+ Idle, // 待机
+ ChasingPlayer, // 追击玩家
+ ChasingBell, // 追击铃铛
+ PatrollingBell // 铃铛位置巡逻
+ }
- private enum State { Idle, Chasing, ChasingPersist, BellResponding }
private State _state = State.Idle;
- private float _stateTimer;
- private Vector3 _bellDirection;
+
+ // 脱离视线计时器
+ private float _loseSightTimer;
+
+ // 铃铛相关
+ private Vector3 _bellTargetPosition; // 铃铛目标位置
+ private float _bellChaseTimer; // 铃铛追击计时器
+
+ // 巡逻相关
+ private Vector3 _patrolCenter; // 巡逻中心点
+ private float _patrolTimer; // 巡逻计时器
+ private Vector3 _patrolTarget; // 当前巡逻目标点
private void Start()
{
@@ -75,104 +104,209 @@ namespace IndianOceanAssets.Engine2_5D
{
switch (_state)
{
- case State.Idle: UpdateIdle(); break;
- case State.Chasing: UpdateChasing(); break;
- case State.ChasingPersist: UpdateChasingPersist(); break;
- case State.BellResponding: UpdateBellResponding(); break;
+ case State.Idle: UpdateIdle(); break;
+ case State.ChasingPlayer: UpdateChasingPlayer(); break;
+ case State.ChasingBell: UpdateChasingBell(); break;
+ case State.PatrollingBell: UpdatePatrollingBell(); break;
}
}
- // ===== Idle:站岗,等主角进入检测范围 =====
+ // ===== Idle:待机,等待检测 =====
private void UpdateIdle()
{
- if (playerTarget == null) return;
- if (XZDistance(transform.position, playerTarget.position) <= detectionRange)
+ // 检测是否看到玩家
+ if (CanSeePlayer())
{
- _state = State.Chasing;
- }
- }
-
- // ===== Chasing:追击主角 =====
- private void UpdateChasing()
- {
- if (playerTarget == null) return;
- float dist = XZDistance(transform.position, playerTarget.position);
-
- // 主角脱离检测范围 → 继续追击一段时间
- if (dist > detectionRange)
- {
- _state = State.ChasingPersist;
- _stateTimer = chasePersistTime;
+ _state = State.ChasingPlayer;
+ _loseSightTimer = 0f;
return;
}
-
- // 靠近到停止距离时停下(但仍处于追击状态)
- if (dist > stopDistance)
- MoveToward(playerTarget.position, chaseSpeed);
}
- // ===== ChasingPersist:脱离后继续追击,倒计时 =====
- private void UpdateChasingPersist()
+ // ===== ChasingPlayer:追击玩家 =====
+ private void UpdateChasingPlayer()
{
- if (playerTarget == null) return;
- _stateTimer -= Time.deltaTime;
+ // 检测是否看到玩家
+ 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);
+ }
+ }
+
+ // 超过设定时间,停止追击
+ if (_loseSightTimer >= loseSightTime)
+ {
+ _state = State.Idle;
+ }
+ }
+ }
- // 超时 → 停在当前位置,回到 Idle(不回出生点)
- if (_stateTimer <= 0f)
+ // ===== ChasingBell:追击铃铛 =====
+ 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, playerTarget.position);
-
- // 主角重新进入检测范围 → 回到正常追击
- if (dist <= detectionRange)
+
+ // 检测是否到达铃铛位置
+ float dist = XZDistance(transform.position, _bellTargetPosition);
+ if (dist <= bellArriveDistance)
{
- _state = State.Chasing;
+ // 到达铃铛位置,开始巡逻
+ _state = State.PatrollingBell;
+ _patrolCenter = _bellTargetPosition;
+ _patrolTimer = 0f;
+ PickNewPatrolPoint();
return;
}
-
- if (dist > stopDistance)
- MoveToward(playerTarget.position, chaseSpeed);
+
+ // 朝铃铛位置移动
+ MoveToward(_bellTargetPosition, chaseSpeed);
}
- // ===== BellResponding:摇铃响应,朝主角方向移动固定时间 =====
- private void UpdateBellResponding()
+ // ===== PatrollingBell:铃铛位置巡逻 =====
+ private void UpdatePatrollingBell()
{
- _stateTimer -= Time.deltaTime;
- if (_stateTimer <= 0f)
+ // 巡逻过程中检测是否看到玩家
+ if (CanSeePlayer())
+ {
+ _state = State.ChasingPlayer;
+ _loseSightTimer = 0f;
+ return;
+ }
+
+ // 巡逻计时
+ _patrolTimer += Time.deltaTime;
+ if (_patrolTimer >= bellPatrolTime)
{
_state = State.Idle;
return;
}
- // 沿摇铃时刻的方向移动(距离 = 速度 × 时间)
- Vector3 move = _bellDirection * bellMoveSpeed * Time.deltaTime;
- transform.position += new Vector3(move.x, 0f, move.z);
+
+ // 朝巡逻点移动
+ float dist = XZDistance(transform.position, _patrolTarget);
+ if (dist <= 0.5f)
+ {
+ // 到达巡逻点,选择新的巡逻点
+ PickNewPatrolPoint();
+ }
+ else
+ {
+ MoveToward(_patrolTarget, chaseSpeed * 0.7f); // 巡逻时速度稍慢
+ }
}
// ===== 摇铃事件回调(由 EchoSystem.OnEchoReleased 触发)=====
private void OnBell(Vector3 bellPos)
{
if (playerTarget == null) return;
-
+
// 只有在聆听范围内才响应
if (XZDistance(transform.position, bellPos) > listenRange) return;
+
+ // 设置铃铛目标位置
+ _bellTargetPosition = bellPos;
+ _bellChaseTimer = 0f;
+
+ // 切换到追击铃铛状态(可以打断任何状态)
+ _state = State.ChasingBell;
+ }
- // 计算朝主角方向(摇铃时刻的位置)
- _bellDirection = new Vector3(
- bellPos.x - transform.position.x,
- 0f,
- bellPos.z - transform.position.z
- );
- if (_bellDirection.sqrMagnitude > 0.001f)
- _bellDirection.Normalize();
- else
- _bellDirection = transform.forward; // 重叠时用当前朝向
+ ///
+ /// 选择新的巡逻点
+ ///
+ private void PickNewPatrolPoint()
+ {
+ // 在巡逻中心周围随机选择一个点
+ Vector2 randomDir = Random.insideUnitCircle * bellPatrolRadius;
+ _patrolTarget = _patrolCenter + new Vector3(randomDir.x, 0f, randomDir.y);
+ }
- // 摇铃响应可打断任何当前状态
- _state = State.BellResponding;
- _stateTimer = bellMoveTime;
+ ///
+ /// 检测是否能看到玩家(射线检测)
+ ///
+ 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;
+
+ // 调试:在Scene视图中绘制射线
+ Debug.DrawLine(rayStart, rayEnd, CanSeePlayerDebugColor());
+
+ // 发射射线,只检测障碍物层
+ if (Physics.Raycast(rayStart, dir.normalized, out RaycastHit hit, dist, obstacleLayers))
+ {
+ // 击中了物体
+ Debug.DrawLine(rayStart, hit.point, Color.red);
+
+ // 如果击中的是玩家,说明可以看到
+ if (hit.transform == playerTarget)
+ {
+ return true;
+ }
+
+ // 击中了其他物体(被遮挡)
+ return false;
+ }
+
+ // 没有击中任何物体 = 没有遮挡 = 可以看到玩家
+ return true;
+ }
+
+ ///
+ /// 调试用:根据是否能看到玩家返回不同颜色
+ ///
+ private Color CanSeePlayerDebugColor()
+ {
+ if (playerTarget == null) return Color.gray;
+
+ 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 ? Color.green : Color.red;
+ }
+ return Color.green; // 没有遮挡
}
// ===== 工具方法 =====
@@ -203,17 +337,48 @@ namespace IndianOceanAssets.Engine2_5D
// ===== 编辑器可视化 =====
private void OnDrawGizmosSelected()
{
- // 检测范围(黄色)
- Gizmos.color = new Color(1f, 0.85f, 0f, 0.6f);
- Gizmos.DrawWireSphere(transform.position, detectionRange);
-
- // 聆听范围(青色)
- Gizmos.color = new Color(0f, 0.85f, 1f, 0.6f);
+ // 根据状态显示不同颜色
+ 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);
+
+ // 绘制聆听范围
+ Gizmos.color = new Color(0f, 0.85f, 1f, 0.3f);
Gizmos.DrawWireSphere(transform.position, listenRange);
-
- // 停止距离(红色)
- Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.6f);
+
+ // 绘制停止距离
+ Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.3f);
Gizmos.DrawWireSphere(transform.position, stopDistance);
+
+ // 绘制铃铛目标(如果在追击铃铛或巡逻)
+ 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);
+ }
}
/// 当前状态(调试用)
diff --git a/unity/CLAUDE.md b/unity/CLAUDE.md
new file mode 100644
index 0000000..b402f24
--- /dev/null
+++ b/unity/CLAUDE.md
@@ -0,0 +1,17 @@
+## Unity API Lookup (unity-api MCP)
+
+Use the `unity-api` MCP tools to verify Unity API usage instead of guessing. **Do not hallucinate signatures.**
+
+| When | Tool | Example |
+|------|------|---------|
+| Unsure about a method's parameters or return type | `get_method_signature` | `get_method_signature("UnityEngine.Tilemaps.Tilemap.SetTile")` |
+| Need the `using` directive for a type | `get_namespace` | `get_namespace("SceneManager")` |
+| Want to see all members on a class | `get_class_reference` | `get_class_reference("InputAction")` |
+| Searching for an API by keyword | `search_unity_api` | `search_unity_api("async load scene")` |
+| Checking if an API is deprecated | `get_deprecation_warnings` | `get_deprecation_warnings("FindObjectOfType")` |
+
+**Rules:**
+- Before writing a Unity API call you haven't used in this conversation, verify the signature with `get_method_signature`
+- Before adding a `using` directive, verify with `get_namespace` if unsure
+- Covers: all UnityEngine/UnityEditor modules, Input System, Addressables, uGUI, TextMeshPro, AI Navigation, and Netcode
+- Does NOT cover: DOTween, VContainer, Newtonsoft.Json (third-party assets)