添加mcp服务器
优化敌人AI,修改成双重规则寻敌
This commit is contained in:
15
unity/.mcp.json
Normal file
15
unity/.mcp.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"unity-api": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["unity-api-mcp"],
|
||||||
|
"env": {
|
||||||
|
"UNITY_VERSION": "2022"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fetch": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["mcp-server-fetch"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
unity/.vscode/settings.json
vendored
3
unity/.vscode/settings.json
vendored
@@ -51,5 +51,6 @@
|
|||||||
"temp/": true,
|
"temp/": true,
|
||||||
"Temp/": true
|
"Temp/": true
|
||||||
},
|
},
|
||||||
"dotnet.defaultSolution": "unity.sln"
|
"dotnet.defaultSolution": "unity.sln",
|
||||||
|
"dotnet.preferCSharpExtension": true
|
||||||
}
|
}
|
||||||
@@ -213,13 +213,18 @@ MonoBehaviour:
|
|||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
playerTarget: {fileID: 0}
|
playerTarget: {fileID: 0}
|
||||||
detectionRange: 8
|
|
||||||
chaseSpeed: 3
|
|
||||||
stopDistance: 1.2
|
stopDistance: 1.2
|
||||||
chasePersistTime: 3
|
chaseSpeed: 3
|
||||||
|
obstacleLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
rayOffsetY: 0.5
|
||||||
|
loseSightTime: 3
|
||||||
listenRange: 15
|
listenRange: 15
|
||||||
bellMoveSpeed: 4
|
bellArriveDistance: 1
|
||||||
bellMoveTime: 1.5
|
bellChaseTimeout: 10
|
||||||
|
bellPatrolTime: 5
|
||||||
|
bellPatrolRadius: 3
|
||||||
--- !u!114 &2877034408413616420
|
--- !u!114 &2877034408413616420
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -235,6 +240,8 @@ MonoBehaviour:
|
|||||||
lockYAxis: 1
|
lockYAxis: 1
|
||||||
smoothRotation: 0
|
smoothRotation: 0
|
||||||
rotationSpeed: 8
|
rotationSpeed: 8
|
||||||
|
snapAngle: 0.5
|
||||||
|
deadZone: 0.2
|
||||||
--- !u!1 &8532340994753211982
|
--- !u!1 &8532340994753211982
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
102
unity/Assets/Editor/TilemapYSortTool.cs
Normal file
102
unity/Assets/Editor/TilemapYSortTool.cs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine.Tilemaps;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tilemap Y-Sort工具 - 快速给Tilemap添加YSorter
|
||||||
|
/// </summary>
|
||||||
|
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<TilemapRenderer>();
|
||||||
|
if (renderer == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"跳过 {go.name} - 没有TilemapRenderer组件");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已有YSorter
|
||||||
|
if (go.GetComponent<YSorter>() != null)
|
||||||
|
{
|
||||||
|
Debug.Log($"跳过 {go.name} - 已有YSorter");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加YSorter
|
||||||
|
YSorter ySorter = go.AddComponent<YSorter>();
|
||||||
|
|
||||||
|
// 设置默认的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<TilemapRenderer>(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<YSorter>() != null)
|
||||||
|
{
|
||||||
|
Debug.Log($"跳过 {go.name} - 已有YSorter");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
YSorter ySorter = go.AddComponent<YSorter>();
|
||||||
|
|
||||||
|
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", "确定");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
unity/Assets/Editor/TilemapYSortTool.cs.meta
Normal file
11
unity/Assets/Editor/TilemapYSortTool.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: CH5JsXykVyiioh6PdpvMd04QErrpba3U3rPu7hQrZpCz8EMMsg29s0w=
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,20 +1,9 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.Tilemaps;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Y-Sort 排序器 —— 根据物体的Z坐标(世界空间)动态调整Sprite的sortingOrder。
|
/// Y-Sort 排序器 —— 根据物体的Z坐标(世界空间)动态调整Sprite的sortingOrder。
|
||||||
///
|
/// 支持SpriteRenderer和TilemapRenderer。
|
||||||
/// 原理:
|
|
||||||
/// 在2.5D等距视角游戏中,摄像机从斜上方45°俯视,Z坐标代表"前后"关系。
|
|
||||||
/// Z值越大(离相机越远=越靠前),sortingOrder越大,会遮挡Z值小的物体。
|
|
||||||
///
|
|
||||||
/// 用法:
|
|
||||||
/// 1. 挂到所有需要参与排序的Sprite物体上(玩家、敌人、树木、石头等)
|
|
||||||
/// 2. 设置 baseSortingOrder 区分不同类型的物体(如地面=0,角色=1000)
|
|
||||||
/// 3. 确保所有参与排序的Sprite在同一个Sorting Layer上
|
|
||||||
///
|
|
||||||
/// 注意:
|
|
||||||
/// - 本脚本假设摄像机从斜上方俯视(Z轴代表前后)
|
|
||||||
/// - 所有参与排序的物体必须在同一个Sorting Layer
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class YSorter : MonoBehaviour
|
public class YSorter : MonoBehaviour
|
||||||
{
|
{
|
||||||
@@ -28,21 +17,32 @@ public class YSorter : MonoBehaviour
|
|||||||
[SerializeField] private bool updateEveryFrame = true;
|
[SerializeField] private bool updateEveryFrame = true;
|
||||||
|
|
||||||
private SpriteRenderer _spriteRenderer;
|
private SpriteRenderer _spriteRenderer;
|
||||||
|
private TilemapRenderer _tilemapRenderer;
|
||||||
private Vector3 _lastPosition;
|
private Vector3 _lastPosition;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
|
// 尝试获取SpriteRenderer
|
||||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||||
|
|
||||||
|
// 如果没有SpriteRenderer,尝试获取TilemapRenderer
|
||||||
if (_spriteRenderer == null)
|
if (_spriteRenderer == null)
|
||||||
{
|
{
|
||||||
Debug.LogWarning($"YSorter: 物体 {gameObject.name} 没有找到SpriteRenderer组件");
|
_tilemapRenderer = GetComponentInChildren<TilemapRenderer>();
|
||||||
|
|
||||||
|
if (_tilemapRenderer == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"YSorter: 物体 {gameObject.name} 没有找到SpriteRenderer或TilemapRenderer组件");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_lastPosition = transform.position;
|
_lastPosition = transform.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LateUpdate()
|
private void LateUpdate()
|
||||||
{
|
{
|
||||||
if (_spriteRenderer == null) return;
|
// 如果没有任何渲染器,返回
|
||||||
|
if (_spriteRenderer == null && _tilemapRenderer == null) return;
|
||||||
|
|
||||||
// 如果设置了不是每帧更新,只在位置改变时更新
|
// 如果设置了不是每帧更新,只在位置改变时更新
|
||||||
if (!updateEveryFrame && transform.position == _lastPosition)
|
if (!updateEveryFrame && transform.position == _lastPosition)
|
||||||
@@ -55,15 +55,25 @@ public class YSorter : MonoBehaviour
|
|||||||
private void UpdateSortingOrder()
|
private void UpdateSortingOrder()
|
||||||
{
|
{
|
||||||
// 使用物体中心(transform.position)的Z坐标作为排序依据
|
// 使用物体中心(transform.position)的Z坐标作为排序依据
|
||||||
// 这样物品中心在地面上方时,会正确遮挡地面
|
|
||||||
float sortZ = transform.position.z;
|
float sortZ = transform.position.z;
|
||||||
|
|
||||||
// Z值越大(离相机越远),sortingOrder越大
|
// Z值越大(离相机越远),sortingOrder越大
|
||||||
int sortingOrder = baseSortingOrder + Mathf.RoundToInt(sortZ * scaleFactor);
|
int sortingOrder = baseSortingOrder + Mathf.RoundToInt(sortZ * scaleFactor);
|
||||||
|
|
||||||
|
// 更新对应的渲染器
|
||||||
|
if (_spriteRenderer != null)
|
||||||
|
{
|
||||||
if (_spriteRenderer.sortingOrder != sortingOrder)
|
if (_spriteRenderer.sortingOrder != sortingOrder)
|
||||||
{
|
{
|
||||||
_spriteRenderer.sortingOrder = sortingOrder;
|
_spriteRenderer.sortingOrder = sortingOrder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (_tilemapRenderer != null)
|
||||||
|
{
|
||||||
|
if (_tilemapRenderer.sortingOrder != sortingOrder)
|
||||||
|
{
|
||||||
|
_tilemapRenderer.sortingOrder = sortingOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,5 +162,34 @@ namespace IndianOceanAssets.Engine2_5D
|
|||||||
}
|
}
|
||||||
|
|
||||||
public bool IsActive => _state != State.Idle;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,13 @@ using UnityEngine;
|
|||||||
namespace IndianOceanAssets.Engine2_5D
|
namespace IndianOceanAssets.Engine2_5D
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 敌人AI —— 检测/追击/脱离持续/摇铃聆听响应。
|
/// 敌人AI v2 —— 双重寻敌机制(视觉检测 + 铃铛响应)
|
||||||
///
|
///
|
||||||
/// 状态流程:
|
/// 状态流程:
|
||||||
/// - Idle:站岗。主角进入 detectionRange → Chasing
|
/// - Idle:待机。射线检测到玩家 → ChasingPlayer;收到铃铛信号 → ChasingBell
|
||||||
/// - Chasing:追击主角。主角离开 detectionRange → ChasingPersist
|
/// - ChasingPlayer:追击玩家。持续看到玩家则追击;脱离视线3秒后 → Idle
|
||||||
/// - ChasingPersist:脱离后继续追击 chasePersistTime 秒。
|
/// - ChasingBell:追击铃铛。追击过程中看到玩家 → ChasingPlayer;到达铃铛位置 → PatrollingBell
|
||||||
/// 主角重新进入范围 → 回到 Chasing;超时 → Idle(停在当前位置,不回出生点)
|
/// - PatrollingBell:铃铛位置巡逻。巡逻过程中看到玩家 → ChasingPlayer;巡逻时间结束 → Idle
|
||||||
/// - BellResponding:主角在 listenRange 内按E摇铃时触发,
|
|
||||||
/// 朝主角方向移动 bellMoveTime 秒(距离 = 速度 × 时间),然后停下 → Idle
|
|
||||||
///
|
///
|
||||||
/// 挂载位置:敌人预制体上
|
/// 挂载位置:敌人预制体上
|
||||||
/// 需要:场景中有 EchoSystem(用于摇铃事件)、玩家物体带 "Player" 标签
|
/// 需要:场景中有 EchoSystem(用于摇铃事件)、玩家物体带 "Player" 标签
|
||||||
@@ -24,32 +22,63 @@ namespace IndianOceanAssets.Engine2_5D
|
|||||||
[SerializeField] private Transform playerTarget;
|
[SerializeField] private Transform playerTarget;
|
||||||
|
|
||||||
[Header("检测与追击")]
|
[Header("检测与追击")]
|
||||||
[Tooltip("发现主角的范围")]
|
[Tooltip("靠近主角到此距离时停下")]
|
||||||
[SerializeField] private float detectionRange = 8f;
|
[SerializeField] private float stopDistance = 1.2f;
|
||||||
|
|
||||||
[Tooltip("追击速度")]
|
[Tooltip("追击速度")]
|
||||||
[SerializeField] private float chaseSpeed = 3f;
|
[SerializeField] private float chaseSpeed = 3f;
|
||||||
|
|
||||||
[Tooltip("靠近主角到此距离时停下")]
|
[Header("视觉检测")]
|
||||||
[SerializeField] private float stopDistance = 1.2f;
|
[Tooltip("射线检测层(哪些物体会阻挡视线)")]
|
||||||
|
[SerializeField] private LayerMask obstacleLayers;
|
||||||
|
|
||||||
[Tooltip("主角脱离检测范围后,继续追击的时间(秒)")]
|
[Tooltip("射线起点偏移(避免从地面发射)")]
|
||||||
[SerializeField] private float chasePersistTime = 3f;
|
[SerializeField] private float rayOffsetY = 0.5f;
|
||||||
|
|
||||||
|
[Header("脱离计时")]
|
||||||
|
[Tooltip("玩家脱离视线后,继续追击的时间(秒)")]
|
||||||
|
[SerializeField] private float loseSightTime = 3f;
|
||||||
|
|
||||||
[Header("聆听(摇铃响应)")]
|
[Header("聆听(摇铃响应)")]
|
||||||
[Tooltip("聆听范围:主角在此范围内按E摇铃时,敌人会朝主角方向移动")]
|
[Tooltip("聆听范围:主角在此范围内按E摇铃时,敌人会朝铃铛位置移动")]
|
||||||
[SerializeField] private float listenRange = 15f;
|
[SerializeField] private float listenRange = 15f;
|
||||||
|
|
||||||
[Tooltip("摇铃响应移动速度")]
|
[Header("铃铛追击")]
|
||||||
[SerializeField] private float bellMoveSpeed = 4f;
|
[Tooltip("到达铃铛位置后的容差距离")]
|
||||||
|
[SerializeField] private float bellArriveDistance = 1f;
|
||||||
|
|
||||||
[Tooltip("摇铃响应移动时间(距离 = 速度 × 时间)")]
|
[Tooltip("追击铃铛的最大时间(超时则放弃)")]
|
||||||
[SerializeField] private float bellMoveTime = 1.5f;
|
[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 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()
|
private void Start()
|
||||||
{
|
{
|
||||||
@@ -76,79 +105,127 @@ namespace IndianOceanAssets.Engine2_5D
|
|||||||
switch (_state)
|
switch (_state)
|
||||||
{
|
{
|
||||||
case State.Idle: UpdateIdle(); break;
|
case State.Idle: UpdateIdle(); break;
|
||||||
case State.Chasing: UpdateChasing(); break;
|
case State.ChasingPlayer: UpdateChasingPlayer(); break;
|
||||||
case State.ChasingPersist: UpdateChasingPersist(); break;
|
case State.ChasingBell: UpdateChasingBell(); break;
|
||||||
case State.BellResponding: UpdateBellResponding(); break;
|
case State.PatrollingBell: UpdatePatrollingBell(); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Idle:站岗,等主角进入检测范围 =====
|
// ===== Idle:待机,等待检测 =====
|
||||||
private void UpdateIdle()
|
private void UpdateIdle()
|
||||||
{
|
{
|
||||||
if (playerTarget == null) return;
|
// 检测是否看到玩家
|
||||||
if (XZDistance(transform.position, playerTarget.position) <= detectionRange)
|
if (CanSeePlayer())
|
||||||
{
|
{
|
||||||
_state = State.Chasing;
|
_state = State.ChasingPlayer;
|
||||||
|
_loseSightTimer = 0f;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Chasing:追击主角 =====
|
// ===== ChasingPlayer:追击玩家 =====
|
||||||
private void UpdateChasing()
|
private void UpdateChasingPlayer()
|
||||||
{
|
{
|
||||||
if (playerTarget == null) return;
|
// 检测是否看到玩家
|
||||||
|
if (CanSeePlayer())
|
||||||
|
{
|
||||||
|
_loseSightTimer = 0f; // 重置计时器
|
||||||
|
|
||||||
|
// 追击玩家
|
||||||
float dist = XZDistance(transform.position, playerTarget.position);
|
float dist = XZDistance(transform.position, playerTarget.position);
|
||||||
|
if (dist > stopDistance)
|
||||||
// 主角脱离检测范围 → 继续追击一段时间
|
|
||||||
if (dist > detectionRange)
|
|
||||||
{
|
{
|
||||||
_state = State.ChasingPersist;
|
MoveToward(playerTarget.position, chaseSpeed);
|
||||||
_stateTimer = chasePersistTime;
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== ChasingBell:追击铃铛 =====
|
||||||
|
private void UpdateChasingBell()
|
||||||
|
{
|
||||||
|
// 追击过程中检测是否看到玩家
|
||||||
|
if (CanSeePlayer())
|
||||||
|
{
|
||||||
|
_state = State.ChasingPlayer;
|
||||||
|
_loseSightTimer = 0f;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 靠近到停止距离时停下(但仍处于追击状态)
|
// 追击计时
|
||||||
if (dist > stopDistance)
|
_bellChaseTimer += Time.deltaTime;
|
||||||
MoveToward(playerTarget.position, chaseSpeed);
|
if (_bellChaseTimer >= bellChaseTimeout)
|
||||||
}
|
|
||||||
|
|
||||||
// ===== ChasingPersist:脱离后继续追击,倒计时 =====
|
|
||||||
private void UpdateChasingPersist()
|
|
||||||
{
|
|
||||||
if (playerTarget == null) return;
|
|
||||||
_stateTimer -= Time.deltaTime;
|
|
||||||
|
|
||||||
// 超时 → 停在当前位置,回到 Idle(不回出生点)
|
|
||||||
if (_stateTimer <= 0f)
|
|
||||||
{
|
{
|
||||||
_state = State.Idle;
|
_state = State.Idle;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float dist = XZDistance(transform.position, playerTarget.position);
|
// 检测是否到达铃铛位置
|
||||||
|
float dist = XZDistance(transform.position, _bellTargetPosition);
|
||||||
// 主角重新进入检测范围 → 回到正常追击
|
if (dist <= bellArriveDistance)
|
||||||
if (dist <= detectionRange)
|
|
||||||
{
|
{
|
||||||
_state = State.Chasing;
|
// 到达铃铛位置,开始巡逻
|
||||||
|
_state = State.PatrollingBell;
|
||||||
|
_patrolCenter = _bellTargetPosition;
|
||||||
|
_patrolTimer = 0f;
|
||||||
|
PickNewPatrolPoint();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dist > stopDistance)
|
// 朝铃铛位置移动
|
||||||
MoveToward(playerTarget.position, chaseSpeed);
|
MoveToward(_bellTargetPosition, chaseSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== BellResponding:摇铃响应,朝主角方向移动固定时间 =====
|
// ===== PatrollingBell:铃铛位置巡逻 =====
|
||||||
private void UpdateBellResponding()
|
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;
|
_state = State.Idle;
|
||||||
return;
|
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 触发)=====
|
// ===== 摇铃事件回调(由 EchoSystem.OnEchoReleased 触发)=====
|
||||||
@@ -159,20 +236,77 @@ namespace IndianOceanAssets.Engine2_5D
|
|||||||
// 只有在聆听范围内才响应
|
// 只有在聆听范围内才响应
|
||||||
if (XZDistance(transform.position, bellPos) > listenRange) return;
|
if (XZDistance(transform.position, bellPos) > listenRange) return;
|
||||||
|
|
||||||
// 计算朝主角方向(摇铃时刻的位置)
|
// 设置铃铛目标位置
|
||||||
_bellDirection = new Vector3(
|
_bellTargetPosition = bellPos;
|
||||||
bellPos.x - transform.position.x,
|
_bellChaseTimer = 0f;
|
||||||
0f,
|
|
||||||
bellPos.z - transform.position.z
|
|
||||||
);
|
|
||||||
if (_bellDirection.sqrMagnitude > 0.001f)
|
|
||||||
_bellDirection.Normalize();
|
|
||||||
else
|
|
||||||
_bellDirection = transform.forward; // 重叠时用当前朝向
|
|
||||||
|
|
||||||
// 摇铃响应可打断任何当前状态
|
// 切换到追击铃铛状态(可以打断任何状态)
|
||||||
_state = State.BellResponding;
|
_state = State.ChasingBell;
|
||||||
_stateTimer = bellMoveTime;
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 选择新的巡逻点
|
||||||
|
/// </summary>
|
||||||
|
private void PickNewPatrolPoint()
|
||||||
|
{
|
||||||
|
// 在巡逻中心周围随机选择一个点
|
||||||
|
Vector2 randomDir = Random.insideUnitCircle * bellPatrolRadius;
|
||||||
|
_patrolTarget = _patrolCenter + new Vector3(randomDir.x, 0f, randomDir.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
|
||||||
|
// 调试:在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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 调试用:根据是否能看到玩家返回不同颜色
|
||||||
|
/// </summary>
|
||||||
|
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()
|
private void OnDrawGizmosSelected()
|
||||||
{
|
{
|
||||||
// 检测范围(黄色)
|
// 根据状态显示不同颜色
|
||||||
Gizmos.color = new Color(1f, 0.85f, 0f, 0.6f);
|
Color stateColor = Color.white;
|
||||||
Gizmos.DrawWireSphere(transform.position, detectionRange);
|
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 = new Color(0f, 0.85f, 1f, 0.6f);
|
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.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);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>当前状态(调试用)</summary>
|
/// <summary>当前状态(调试用)</summary>
|
||||||
|
|||||||
17
unity/CLAUDE.md
Normal file
17
unity/CLAUDE.md
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user