暂时修改
This commit is contained in:
60
unity/Assets/enemy/BillboardSprite.cs
Normal file
60
unity/Assets/enemy/BillboardSprite.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace IndianOceanAssets.Engine2_5D
|
||||
{
|
||||
/// <summary>
|
||||
/// 场景物件面向摄像机脚本(饥荒风格)。
|
||||
/// 让物体始终朝摄像机方向旋转,默认只绕 Y 轴(保持竖直不倒)。
|
||||
///
|
||||
/// 用法:挂到需要面向摄像机的精灵/物件上即可。
|
||||
/// 适合:树木、岩石、角色、道具等 2D 精灵在 3D 场景中的朝向修正。
|
||||
/// </summary>
|
||||
public class BillboardSprite : MonoBehaviour
|
||||
{
|
||||
[Tooltip("只绕Y轴旋转(饥荒风格,保持竖直不倒)。关闭则完全面向摄像机。")]
|
||||
[SerializeField] private bool lockYAxis = true;
|
||||
|
||||
[Tooltip("平滑旋转(lerp过渡),关闭则瞬间转向")]
|
||||
[SerializeField] private bool smoothRotation = false;
|
||||
|
||||
[Tooltip("平滑旋转速度")]
|
||||
[SerializeField] private float rotationSpeed = 8f;
|
||||
|
||||
private Camera _mainCamera;
|
||||
private Transform _camTransform;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_mainCamera = Camera.main;
|
||||
if (_mainCamera != null)
|
||||
_camTransform = _mainCamera.transform;
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_camTransform == null)
|
||||
{
|
||||
// 摄像机丢失时重新获取(场景切换等情况)
|
||||
_mainCamera = Camera.main;
|
||||
if (_mainCamera != null)
|
||||
_camTransform = _mainCamera.transform;
|
||||
if (_camTransform == null) return;
|
||||
}
|
||||
|
||||
Vector3 dir = _camTransform.position - transform.position;
|
||||
|
||||
if (lockYAxis)
|
||||
dir.y = 0f; // 保持竖直
|
||||
|
||||
if (dir.sqrMagnitude < 0.0001f) return;
|
||||
|
||||
Quaternion targetRot = Quaternion.LookRotation(dir);
|
||||
|
||||
if (smoothRotation)
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRot,
|
||||
rotationSpeed * Time.deltaTime);
|
||||
else
|
||||
transform.rotation = targetRot;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
unity/Assets/enemy/BillboardSprite.cs.meta
Normal file
11
unity/Assets/enemy/BillboardSprite.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: BnwZsH+oBX9o8/qrlvnDZyZeLvCxOPjzKkntbYOjVXovzGFc2g6PBfk=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
222
unity/Assets/enemy/EnemyAI.cs
Normal file
222
unity/Assets/enemy/EnemyAI.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace IndianOceanAssets.Engine2_5D
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人AI —— 检测/追击/脱离持续/摇铃聆听响应。
|
||||
///
|
||||
/// 状态流程:
|
||||
/// - Idle:站岗。主角进入 detectionRange → Chasing
|
||||
/// - Chasing:追击主角。主角离开 detectionRange → ChasingPersist
|
||||
/// - ChasingPersist:脱离后继续追击 chasePersistTime 秒。
|
||||
/// 主角重新进入范围 → 回到 Chasing;超时 → Idle(停在当前位置,不回出生点)
|
||||
/// - BellResponding:主角在 listenRange 内按E摇铃时触发,
|
||||
/// 朝主角方向移动 bellMoveTime 秒(距离 = 速度 × 时间),然后停下 → Idle
|
||||
///
|
||||
/// 挂载位置:敌人预制体上
|
||||
/// 需要:场景中有 EchoSystem(用于摇铃事件)、玩家物体带 "Player" 标签
|
||||
/// (或手动把玩家拖到 playerTarget 字段)
|
||||
/// </summary>
|
||||
public class EnemyAI : MonoBehaviour
|
||||
{
|
||||
[Header("目标")]
|
||||
[Tooltip("玩家目标(留空则自动查找 Player 标签)")]
|
||||
[SerializeField] private Transform playerTarget;
|
||||
|
||||
[Header("检测与追击")]
|
||||
[Tooltip("发现主角的范围")]
|
||||
[SerializeField] private float detectionRange = 8f;
|
||||
|
||||
[Tooltip("追击速度")]
|
||||
[SerializeField] private float chaseSpeed = 3f;
|
||||
|
||||
[Tooltip("靠近主角到此距离时停下")]
|
||||
[SerializeField] private float stopDistance = 1.2f;
|
||||
|
||||
[Tooltip("主角脱离检测范围后,继续追击的时间(秒)")]
|
||||
[SerializeField] private float chasePersistTime = 3f;
|
||||
|
||||
[Header("聆听(摇铃响应)")]
|
||||
[Tooltip("聆听范围:主角在此范围内按E摇铃时,敌人会朝主角方向移动")]
|
||||
[SerializeField] private float listenRange = 15f;
|
||||
|
||||
[Tooltip("摇铃响应移动速度")]
|
||||
[SerializeField] private float bellMoveSpeed = 4f;
|
||||
|
||||
[Tooltip("摇铃响应移动时间(距离 = 速度 × 时间)")]
|
||||
[SerializeField] private float bellMoveTime = 1.5f;
|
||||
|
||||
private enum State { Idle, Chasing, ChasingPersist, BellResponding }
|
||||
private State _state = State.Idle;
|
||||
private float _stateTimer;
|
||||
private Vector3 _bellDirection;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (playerTarget == null)
|
||||
{
|
||||
var player = GameObject.FindWithTag("Player");
|
||||
if (player != null)
|
||||
playerTarget = player.transform;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EchoSystem.OnEchoReleased += OnBell;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EchoSystem.OnEchoReleased -= OnBell;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
switch (_state)
|
||||
{
|
||||
case State.Idle: UpdateIdle(); break;
|
||||
case State.Chasing: UpdateChasing(); break;
|
||||
case State.ChasingPersist: UpdateChasingPersist(); break;
|
||||
case State.BellResponding: UpdateBellResponding(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Idle:站岗,等主角进入检测范围 =====
|
||||
private void UpdateIdle()
|
||||
{
|
||||
if (playerTarget == null) return;
|
||||
if (XZDistance(transform.position, playerTarget.position) <= detectionRange)
|
||||
{
|
||||
_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;
|
||||
return;
|
||||
}
|
||||
|
||||
// 靠近到停止距离时停下(但仍处于追击状态)
|
||||
if (dist > stopDistance)
|
||||
MoveToward(playerTarget.position, chaseSpeed);
|
||||
}
|
||||
|
||||
// ===== ChasingPersist:脱离后继续追击,倒计时 =====
|
||||
private void UpdateChasingPersist()
|
||||
{
|
||||
if (playerTarget == null) return;
|
||||
_stateTimer -= Time.deltaTime;
|
||||
|
||||
// 超时 → 停在当前位置,回到 Idle(不回出生点)
|
||||
if (_stateTimer <= 0f)
|
||||
{
|
||||
_state = State.Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
float dist = XZDistance(transform.position, playerTarget.position);
|
||||
|
||||
// 主角重新进入检测范围 → 回到正常追击
|
||||
if (dist <= detectionRange)
|
||||
{
|
||||
_state = State.Chasing;
|
||||
return;
|
||||
}
|
||||
|
||||
if (dist > stopDistance)
|
||||
MoveToward(playerTarget.position, chaseSpeed);
|
||||
}
|
||||
|
||||
// ===== BellResponding:摇铃响应,朝主角方向移动固定时间 =====
|
||||
private void UpdateBellResponding()
|
||||
{
|
||||
_stateTimer -= Time.deltaTime;
|
||||
if (_stateTimer <= 0f)
|
||||
{
|
||||
_state = State.Idle;
|
||||
return;
|
||||
}
|
||||
// 沿摇铃时刻的方向移动(距离 = 速度 × 时间)
|
||||
Vector3 move = _bellDirection * bellMoveSpeed * Time.deltaTime;
|
||||
transform.position += new Vector3(move.x, 0f, move.z);
|
||||
}
|
||||
|
||||
// ===== 摇铃事件回调(由 EchoSystem.OnEchoReleased 触发)=====
|
||||
private void OnBell(Vector3 bellPos)
|
||||
{
|
||||
if (playerTarget == null) return;
|
||||
|
||||
// 只有在聆听范围内才响应
|
||||
if (XZDistance(transform.position, bellPos) > listenRange) return;
|
||||
|
||||
// 计算朝主角方向(摇铃时刻的位置)
|
||||
_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; // 重叠时用当前朝向
|
||||
|
||||
// 摇铃响应可打断任何当前状态
|
||||
_state = State.BellResponding;
|
||||
_stateTimer = bellMoveTime;
|
||||
}
|
||||
|
||||
// ===== 工具方法 =====
|
||||
|
||||
private void MoveToward(Vector3 target, float speed)
|
||||
{
|
||||
Vector3 dir = new Vector3(
|
||||
target.x - transform.position.x,
|
||||
0f,
|
||||
target.z - transform.position.z
|
||||
);
|
||||
float dist = dir.magnitude;
|
||||
if (dist < 0.01f) return;
|
||||
|
||||
dir /= dist;
|
||||
Vector3 move = dir * speed * Time.deltaTime;
|
||||
if (move.magnitude > dist) move = dir * dist; // 不越过目标
|
||||
transform.position += new Vector3(move.x, 0f, move.z);
|
||||
}
|
||||
|
||||
private float XZDistance(Vector3 a, Vector3 b)
|
||||
{
|
||||
float dx = a.x - b.x;
|
||||
float dz = a.z - b.z;
|
||||
return Mathf.Sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
// ===== 编辑器可视化 =====
|
||||
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);
|
||||
Gizmos.DrawWireSphere(transform.position, listenRange);
|
||||
|
||||
// 停止距离(红色)
|
||||
Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.6f);
|
||||
Gizmos.DrawWireSphere(transform.position, stopDistance);
|
||||
}
|
||||
|
||||
/// <summary>当前状态(调试用)</summary>
|
||||
public string CurrentState => _state.ToString();
|
||||
}
|
||||
}
|
||||
11
unity/Assets/enemy/EnemyAI.cs.meta
Normal file
11
unity/Assets/enemy/EnemyAI.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: CCwWtXv+BXlMo6hf6dmE2xTAlfSRx2PCnRzyAU6iQWgz40mcEU424HU=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
85
unity/Assets/enemy/EnemyManager.cs
Normal file
85
unity/Assets/enemy/EnemyManager.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace IndianOceanAssets.Engine2_5D
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人管理器 —— 在游戏开始时,于场景中所有 EnemySpawnPoint 处生成敌人。
|
||||
///
|
||||
/// 用法:
|
||||
/// 1. 在场景中放置空物体,挂 EnemySpawnPoint 组件,作为出生点
|
||||
/// 2. 在场景中创建空物体,挂此 EnemyManager 脚本
|
||||
/// 3. 把敌人预制体拖到 enemyPrefab 字段
|
||||
/// 4. 运行时自动在所有出生点生成敌人
|
||||
/// </summary>
|
||||
public class EnemyManager : MonoBehaviour
|
||||
{
|
||||
public static EnemyManager Instance { get; private set; }
|
||||
|
||||
[Header("敌人物体预制体")]
|
||||
[SerializeField] private GameObject enemyPrefab;
|
||||
|
||||
[Tooltip("勾选后游戏开始时自动在所有出生点生成敌人")]
|
||||
[SerializeField] private bool spawnOnStart = true;
|
||||
|
||||
private readonly List<EnemyAI> _enemies = new List<EnemyAI>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (spawnOnStart)
|
||||
SpawnAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在所有 EnemySpawnPoint 处生成敌人
|
||||
/// </summary>
|
||||
[ContextMenu("Spawn All")]
|
||||
public void SpawnAll()
|
||||
{
|
||||
var spawnPoints = FindObjectsByType<EnemySpawnPoint>(FindObjectsSortMode.None);
|
||||
foreach (var sp in spawnPoints)
|
||||
{
|
||||
SpawnEnemy(sp.transform.position, sp.transform.rotation);
|
||||
}
|
||||
Debug.Log($"[EnemyManager] 在 {spawnPoints.Length} 个出生点生成了 {_enemies.Count} 个敌人");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在指定位置生成一个敌人
|
||||
/// </summary>
|
||||
public GameObject SpawnEnemy(Vector3 position, Quaternion rotation)
|
||||
{
|
||||
if (enemyPrefab == null)
|
||||
{
|
||||
Debug.LogWarning("[EnemyManager] enemyPrefab 未设置!");
|
||||
return null;
|
||||
}
|
||||
|
||||
var enemy = Instantiate(enemyPrefab, position, rotation);
|
||||
var ai = enemy.GetComponent<EnemyAI>();
|
||||
if (ai != null && !_enemies.Contains(ai))
|
||||
_enemies.Add(ai);
|
||||
|
||||
return enemy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有活跃敌人(自动清理已销毁的)
|
||||
/// </summary>
|
||||
public List<EnemyAI> GetActiveEnemies()
|
||||
{
|
||||
_enemies.RemoveAll(e => e == null);
|
||||
return _enemies;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
unity/Assets/enemy/EnemyManager.cs.meta
Normal file
11
unity/Assets/enemy/EnemyManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: BigZtSusUX7LyMDm+vuceZYAyL3PPYdY87jCezHOaujUlI0euVs5iBo=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
24
unity/Assets/enemy/EnemySpawnPoint.cs
Normal file
24
unity/Assets/enemy/EnemySpawnPoint.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace IndianOceanAssets.Engine2_5D
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人出生点标记 —— 挂到场景中的空物体上,标记敌人出生位置。
|
||||
/// EnemyManager 会在游戏开始时找到所有出生点并生成敌人。
|
||||
///
|
||||
/// 用法:在场景中放置空物体,挂上此组件,调整位置即可。
|
||||
/// Scene 视图中显示红色线框球 + 竖线,方便辨认。
|
||||
/// </summary>
|
||||
public class EnemySpawnPoint : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Gizmo 球体半径(仅编辑器可视化)")]
|
||||
[SerializeField] private float gizmoRadius = 0.5f;
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, gizmoRadius);
|
||||
Gizmos.DrawLine(transform.position, transform.position + Vector3.up * 2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
unity/Assets/enemy/EnemySpawnPoint.cs.meta
Normal file
11
unity/Assets/enemy/EnemySpawnPoint.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: Cy4ftXn7US8AThuZOHodRQB7iRTlR2Qu7Jt/xviDZ91RolRZd+3dykw=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user