Files
zeling_v2/Assets/_Game/Scripts/Dialogue/InteractableNPC.cs
2026-05-25 11:54:37 +08:00

147 lines
7.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using BaseGames.World;
using BaseGames.Localization;
namespace BaseGames.Dialogue
{
/// <summary>
/// 可交互 NPC 基类(架构 14_NarrativeModule §5
/// 实现 IInteractable触发对话序列子类如 QuestGiver可覆盖对话选择逻辑。
/// 程序集: BaseGames.Dialogue
/// </summary>
public class InteractableNPC : MonoBehaviour, IInteractable
{
[Header("NPC 基础")]
[Tooltip("NPC 唯一 ID如 \"NPC_Elder\")。对话结束时随 EVT_NpcDialogueCompleted 广播,用于驱动对话类任务目标进度。\n" +
"需与 QuestSO 目标中 targetNpcId 保持一致。")]
[SerializeField] protected string _npcId;
[Tooltip("默认对话序列。无其他逻辑覆盖时播放此序列。NarrativeNPC/QuestGiver 子类通过 GetCurrentDialogue() 返回更精确的版本。")]
[SerializeField] protected DialogueSequenceSO _defaultDialogue;
[Tooltip("玩家进入此半径单位Unity 单位)后显示交互提示。建议 1.02.0。\n编辑器下在场景视图中以黄色圆圈可视化。")]
[SerializeField] protected float _interactRadius = 1.5f;
[Tooltip("交互提示本地化 Key如 \"INTERACT_Talk\")。运行时通过 LocalizationManager 解析为实际文字。\n" +
"留空时回退到内置字符串 \"对话\"。")]
[SerializeField] protected string _interactPromptKey = "INTERACT_Talk";
[Header("范围检测")]
[Tooltip("玩家所在的物理层。OnTriggerEnter2D / OnTriggerExit2D 仅响应属于此层的碰撞体,\n" +
"实现 NPC 自包含的交互范围检测,无需外部 PlayerInteractionDetector 组件。\n" +
"将玩家 GameObject 的 Layer 与此 Mask 对齐即可(推荐专用 \"Player\" 层)。\n" +
"若留空(值为 0则跳过层级过滤任意碰撞体均可触发调试用不推荐上线。")]
[SerializeField] protected LayerMask _playerLayer;
// ── IInteractable ──────────────────────────────────────────────────
public virtual bool CanInteract => true;
public virtual string InteractPrompt
{
get
{
if (!string.IsNullOrEmpty(_interactPromptKey))
{
var resolved = LocalizationManager.Get(_interactPromptKey, LocalizationTable.UI);
if (!string.IsNullOrEmpty(resolved)) return resolved;
}
return "对话";
}
}
// ── 范围进出通知(供子组件订阅,如 InteractionPromptController──────
/// <summary>玩家进入交互范围时触发。参数为玩家 Transform。</summary>
public event System.Action<Transform> PlayerEnteredRange;
/// <summary>玩家离开交互范围时触发。</summary>
public event System.Action PlayerExitedRange;
public void Interact(Transform player)
{
if (!CanInteract) return;
Interact_Internal(player);
var dialogue = GetCurrentDialogue();
if (dialogue != null)
PlayDialogue(dialogue, player);
}
public virtual void OnPlayerEnterRange(Transform player) { PlayerEnteredRange?.Invoke(player); }
public virtual void OnPlayerExitRange() { PlayerExitedRange?.Invoke(); }
// ── 自包含物理范围检测 ─────────────────────────────────────────────
// 需在 NPC Prefab 上挂载 Collider2D设为 IsTrigger并将 Collider2D.size/radius
// 配置为期望的交互半径。OnTriggerEnter2D / Exit2D 会自动过滤非玩家碰撞体。
private void OnTriggerEnter2D(Collider2D other)
{
if (_playerLayer.value != 0 && ((1 << other.gameObject.layer) & _playerLayer.value) == 0) return;
OnPlayerEnterRange(other.transform);
}
private void OnTriggerExit2D(Collider2D other)
{
if (_playerLayer.value != 0 && ((1 << other.gameObject.layer) & _playerLayer.value) == 0) return;
OnPlayerExitRange();
}
// ── 子类覆盖点 ──────────────────────────────────────────────────────
/// <summary>组件启用时调用。子类可覆盖且应调用 base.OnEnable()。</summary>
protected virtual void OnEnable() { }
/// <summary>组件禁用时调用。子类可覆盖且应调用 base.OnDisable()。</summary>
protected virtual void OnDisable() { }
/// <summary>交互前置逻辑(如任务接收/完成判断)。子类覆盖此方法。</summary>
protected virtual void Interact_Internal(Transform player) { }
/// <summary>返回当前应播放的对话序列。子类根据任务状态返回不同版本。</summary>
protected virtual DialogueSequenceSO GetCurrentDialogue() => _defaultDialogue;
// ── 对话播放 ───────────────────────────────────────────────────────
protected virtual void PlayDialogue(DialogueSequenceSO sequence, Transform player)
{
if (sequence == null) return;
var manager = BaseGames.Core.ServiceLocator.GetOrDefault<IDialogueService>();
if (manager == null)
{
Debug.LogWarning($"[InteractableNPC:{_npcId}] DialogueManager 未注册到 ServiceLocator无法播放对话。");
return;
}
manager.StartDialogue(sequence, _npcId);
}
// ── 编辑器辅助 ────────────────────────────────────────────────────
// 注意OnValidate 声明在 #if 外,确保子类在非编辑器构建中调用 base.OnValidate() 不会编译失败。
protected virtual void OnValidate()
{
#if UNITY_EDITOR
if (_playerLayer.value == 0)
Debug.LogWarning(
$"[InteractableNPC:{name}] _playerLayer 未设置value=0。" +
"OnTriggerEnter2D 将响应所有层,建议在 Inspector 中指定玩家所在层。", this);
// 检测 _interactRadius 与 CircleCollider2D.radius 是否同步(仅输出一次,非逐帧)
var circle = GetComponent<UnityEngine.CircleCollider2D>();
if (circle != null && !Mathf.Approximately(circle.radius, _interactRadius))
Debug.LogWarning(
$"[InteractableNPC:{name}] _interactRadius({_interactRadius:F2}) 与 " +
$"CircleCollider2D.radius({circle.radius:F2}) 不一致," +
"交互范围视觉Gizmos与物理碰撞可能不匹配请手动对齐。", this);
#endif
}
#if UNITY_EDITOR
protected virtual void OnDrawGizmosSelected()
{
// Collider2D 不一致时改绘红色(警告已在 OnValidate 中输出,此处不重复 LogWarning
bool mismatch = false;
var circle = GetComponent<UnityEngine.CircleCollider2D>();
if (circle != null && !Mathf.Approximately(circle.radius, _interactRadius))
mismatch = true;
UnityEditor.Handles.color = mismatch
? new Color(1f, 0.2f, 0.2f, 0.8f)
: new Color(1f, 0.92f, 0.016f, 0.6f);
UnityEditor.Handles.DrawWireDisc(transform.position, Vector3.forward, _interactRadius);
}
#endif
}
}