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)