feat(Dialogue): 实现基本的对话系统

This commit is contained in:
2025-10-23 15:46:17 +08:00
parent 869d5794f3
commit efcbc8eea5
29 changed files with 1398 additions and 86 deletions

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 290c1bc2a04e492c9676d8230845ff58
timeCreated: 1761189130

View File

@@ -0,0 +1,22 @@
using System;
using UnityEngine;
using Script.Gameplay.Interface;
using UnityEngine.Events;
namespace Script.Gameplay.Dialogue
{
public class BaseDialogueItem : MonoBehaviour, IDialogue
{
[TextArea] [SerializeField] private string dialogueContent; // 对话内容
public UnityEvent<bool> OnBeginDialogue { get; set; }
public UnityEvent<bool> OnEndDialogue { get; set; }
public bool IsRoad { get; set; }
public int GetDialogueCount { get; set; }
public string DialogueContent
{
get => dialogueContent;
set => dialogueContent = value;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 11e2853ce9054f6b90407f3c56563502
timeCreated: 1761189140

View File

@@ -127,6 +127,15 @@ namespace Script.Gameplay.Input
""processors"": """",
""interactions"": """",
""initialStateCheck"": false
},
{
""name"": ""ShowNextDialogue"",
""type"": ""Button"",
""id"": ""a8bce90a-e5e4-45fe-8189-cfb48debddf5"",
""expectedControlType"": """",
""processors"": """",
""interactions"": """",
""initialStateCheck"": false
}
],
""bindings"": [
@@ -294,6 +303,17 @@ namespace Script.Gameplay.Input
""action"": ""CutLine"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""86dadc63-5591-4a7b-8fec-e5cfdb31b787"",
""path"": ""<Mouse>/leftButton"",
""interactions"": """",
""processors"": """",
""groups"": """",
""action"": ""ShowNextDialogue"",
""isComposite"": false,
""isPartOfComposite"": false
}
]
}
@@ -313,6 +333,7 @@ namespace Script.Gameplay.Input
m_Player_SetOutput = m_Player.FindAction("SetOutput", throwIfNotFound: true);
m_Player_SetInput = m_Player.FindAction("SetInput", throwIfNotFound: true);
m_Player_CutLine = m_Player.FindAction("CutLine", throwIfNotFound: true);
m_Player_ShowNextDialogue = m_Player.FindAction("ShowNextDialogue", throwIfNotFound: true);
}
~@PlayerInputActions()
@@ -390,6 +411,7 @@ namespace Script.Gameplay.Input
private readonly InputAction m_Player_SetOutput;
private readonly InputAction m_Player_SetInput;
private readonly InputAction m_Player_CutLine;
private readonly InputAction m_Player_ShowNextDialogue;
public struct PlayerActions
{
private @PlayerInputActions m_Wrapper;
@@ -405,6 +427,7 @@ namespace Script.Gameplay.Input
public InputAction @SetOutput => m_Wrapper.m_Player_SetOutput;
public InputAction @SetInput => m_Wrapper.m_Player_SetInput;
public InputAction @CutLine => m_Wrapper.m_Player_CutLine;
public InputAction @ShowNextDialogue => m_Wrapper.m_Player_ShowNextDialogue;
public InputActionMap Get() { return m_Wrapper.m_Player; }
public void Enable() { Get().Enable(); }
public void Disable() { Get().Disable(); }
@@ -447,6 +470,9 @@ namespace Script.Gameplay.Input
@CutLine.started += instance.OnCutLine;
@CutLine.performed += instance.OnCutLine;
@CutLine.canceled += instance.OnCutLine;
@ShowNextDialogue.started += instance.OnShowNextDialogue;
@ShowNextDialogue.performed += instance.OnShowNextDialogue;
@ShowNextDialogue.canceled += instance.OnShowNextDialogue;
}
private void UnregisterCallbacks(IPlayerActions instance)
@@ -484,6 +510,9 @@ namespace Script.Gameplay.Input
@CutLine.started -= instance.OnCutLine;
@CutLine.performed -= instance.OnCutLine;
@CutLine.canceled -= instance.OnCutLine;
@ShowNextDialogue.started -= instance.OnShowNextDialogue;
@ShowNextDialogue.performed -= instance.OnShowNextDialogue;
@ShowNextDialogue.canceled -= instance.OnShowNextDialogue;
}
public void RemoveCallbacks(IPlayerActions instance)
@@ -514,6 +543,7 @@ namespace Script.Gameplay.Input
void OnSetOutput(InputAction.CallbackContext context);
void OnSetInput(InputAction.CallbackContext context);
void OnCutLine(InputAction.CallbackContext context);
void OnShowNextDialogue(InputAction.CallbackContext context);
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using UnityEngine;
using UnityEngine.Events;
namespace Script.Gameplay.Interface
{
public interface IDialogue
{
public UnityEvent<bool> OnBeginDialogue { get; set; }
public UnityEvent<bool> OnEndDialogue { get; set; }
public bool IsRoad { get; set; }
public int GetDialogueCount { get; set; }
public string DialogueContent { get; set; }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6aa3fac9c1bc4bfaa9c14c987146f934
timeCreated: 1761189058

View File

@@ -30,20 +30,25 @@ namespace Script.Gameplay.Player
[SerializeField] private FirstPersonRaycaster raycaster; // 第一人称射线检测器
[SerializeField] private float maxConnectDistance = 10f; // 最大连接距离
private IConnectable previousTarget;
private IConnectable currentTarget;
public IConnectable CurrentTarget
{
get => currentTarget;
set
{
if (value != null)
previousTarget = currentTarget;
currentTarget = value;
if (previousTarget != currentTarget)
{
currentTarget = value;
OnGazeEnter?.Invoke(currentTarget.GetGameObject());
}
else
{
OnGazeExit?.Invoke(null);
if (currentTarget != null)
{
OnGazeEnter?.Invoke((currentTarget as MonoBehaviour)?.gameObject);
}
if (previousTarget != null)
{
OnGazeExit?.Invoke((previousTarget as MonoBehaviour)?.gameObject);
}
}
}
} // 当前注视的可连接对象
@@ -89,12 +94,6 @@ namespace Script.Gameplay.Player
GameObject lookAtObj = raycaster.CurrentLookAtObject;
IConnectable hitConnectable = lookAtObj != null ? lookAtObj.GetComponent<IConnectable>() : null;
// 注视对象变化时触发进入/离开(使用接口方法)
// if (hitConnectable != previousGazedTarget)
// {
// previousGazedTarget = hitConnectable;
// }
CurrentTarget = hitConnectable;
}

View File

@@ -0,0 +1,155 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Core;
using UnityEngine;
using UnityEngine.Events;
using Script.Gameplay.Interface;
using Script.Gameplay.Input;
using UI;
namespace Script.Gameplay.Player
{
public class PlayerDialogueController : MonoBehaviour
{
[SerializeField] private bool isEnablePlayerDialogue = true; // 是否启用玩家对话功能
[SerializeField] private FirstPersonRaycaster firstPersonRaycaster; // 第一人称射线检测器
private bool isReadingDialogue = false;
public event Action OnPlayerBeginDialogue;
public event Action OnPlayerEndDialogue;
public event Action<GameObject> OnGazeEnterDialogue;
public event Action<GameObject> OnGazeExitDialogue;
private Queue<string> dialogueQueue = new Queue<string>();
private PlayerDialogueViewer playerDialogueViewer;
private IDialogue previousDialogue;
private IDialogue currentDialogueTarget;
public IDialogue CurrentDialogueTarget
{
get => currentDialogueTarget;
set
{
previousDialogue = currentDialogueTarget;
currentDialogueTarget = value;
if (previousDialogue != currentDialogueTarget)
{
if (currentDialogueTarget != null)
{
OnGazeEnterDialogue?.Invoke((currentDialogueTarget as MonoBehaviour)?.gameObject);
}
if (previousDialogue != null)
{
OnGazeExitDialogue?.Invoke((previousDialogue as MonoBehaviour)?.gameObject);
}
}
}
}
void Start()
{
if (firstPersonRaycaster == null)
firstPersonRaycaster = GetComponent<FirstPersonRaycaster>() ??
GetComponentInChildren<FirstPersonRaycaster>();
if (firstPersonRaycaster == null)
firstPersonRaycaster = FindObjectOfType<FirstPersonRaycaster>();
if (firstPersonRaycaster == null)
Debug.LogWarning("FirstPersonRaycaster not found! Please assign or add it to the player.");
var input = InputManager.Instance.Input;
input.Player.Interact.performed += ctx =>
{
if (!isEnablePlayerDialogue) return;
if (CurrentDialogueTarget == null) return;
if (isReadingDialogue) return;
BeginDialogue();
};
input.Player.ShowNextDialogue.performed += ctx =>
{
if (!isEnablePlayerDialogue) return;
if (CurrentDialogueTarget == null) return;
if (!isReadingDialogue) return;
PassNextDialogue();
};
ControllerLocator.Instance.Register(this);
}
void Update()
{
DetectDialogue();
}
private void DetectDialogue()
{
if (!isEnablePlayerDialogue || firstPersonRaycaster == null) return;
GameObject lookAtObj = firstPersonRaycaster.CurrentLookAtObject;
IDialogue hitDialogue = lookAtObj != null ? lookAtObj.GetComponent<IDialogue>() : null;
CurrentDialogueTarget = hitDialogue;
}
private void BeginDialogue()
{
if (CurrentDialogueTarget != null)
{
isReadingDialogue = true;
OnPlayerBeginDialogue?.Invoke();
CurrentDialogueTarget.OnBeginDialogue?.Invoke(true);
dialogueQueue = SplitDialogue(CurrentDialogueTarget.DialogueContent);
PassNextDialogue();
}
}
private void PassNextDialogue()
{
if (CurrentDialogueTarget != null)
{
if (dialogueQueue.Count == 0)
{
EndDialogue();
return;
}
string nextDialogue = dialogueQueue.Dequeue();
if (playerDialogueViewer != null)
{
playerDialogueViewer.ReceiveDialogue(nextDialogue);
}
}
}
private void EndDialogue()
{
if (CurrentDialogueTarget != null)
{
isReadingDialogue = false;
OnPlayerEndDialogue?.Invoke();
CurrentDialogueTarget.OnEndDialogue?.Invoke(true);
if (playerDialogueViewer != null)
{
playerDialogueViewer.ClosePanel();
}
// 可扩展隐藏UI、恢复输入等
}
}
private Queue<string> SplitDialogue(string content)
{
// 按换行分段
return new(content.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries));
}
public void RegisterDialogueViewer(PlayerDialogueViewer viewer)
{
playerDialogueViewer = viewer;
}
public void SetPlayerDialogueEnabled(bool isEnabled)
{
isEnablePlayerDialogue = isEnabled;
if (!isEnabled)
{
CurrentDialogueTarget = null;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 504b772e6a254764388ec3b68f4c9ad0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -11,27 +11,28 @@ namespace Script.Gameplay.Player
[SerializeField] private FirstPersonRaycaster raycaster; // 新增:第一人称射线检测器
[SerializeField] private bool isEnablePlayerInteraction = true; // 是否启用玩家交互功能
private IInteractable previousTarget;
private IInteractable currentTarget;
private IInteractable CurrentTarget
{
get => currentTarget;
set
{
previousTarget = currentTarget;
currentTarget = value;
if (currentTarget != null)
if (previousTarget != currentTarget)
{
// 可以在这里添加一些逻辑比如显示交互提示UI
OnGazeEnter?.Invoke(this.gameObject);
}
else
{
// 可以在这里隐藏交互提示UI
OnGazeExit?.Invoke(this.gameObject);
if (currentTarget != null)
{
OnGazeEnter?.Invoke((currentTarget as MonoBehaviour)?.gameObject);
}
if (previousTarget != null)
{
OnGazeExit?.Invoke((previousTarget as MonoBehaviour)?.gameObject);
}
}
}
} // 被射线命中的当前可交互对象(用于按键交互)
private IInteractable previousGazedTarget; // 上一次注视的对象(用于注视进入/离开事件)
public event Action<GameObject> OnGazeEnter;
public event Action<GameObject> OnGazeExit;
@@ -68,34 +69,14 @@ namespace Script.Gameplay.Player
GameObject lookAtObj = raycaster.CurrentLookAtObject;
IInteractable hitInteractable = lookAtObj != null ? lookAtObj.GetComponent<IInteractable>() : null;
// 如果命中对象与之前注视的不一样,触发进入/离开事件
if (hitInteractable != previousGazedTarget)
{
// if (previousGazedTarget != null)
// {
// previousGazedTarget.OnGazeExit(this.gameObject);
// }
//
// if (hitInteractable != null)
// {
// hitInteractable.OnGazeEnter(this.gameObject);
// // 这里可以显示交互提示UI例如 “E - 开门”
// //Debug.Log(hitInteractable.GetInteractPrompt());
// }
previousGazedTarget = hitInteractable;
}
CurrentTarget = hitInteractable;
}
public void SetPlayerInteractionEnabled(bool isEnabled)
{
isEnablePlayerInteraction = isEnabled;
if (!isEnabled && previousGazedTarget != null)
if (!isEnabled)
{
// previousGazedTarget.OnGazeExit(this.gameObject);
previousGazedTarget = null;
CurrentTarget = null;
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using Core;
using Script.Gameplay.Input;
using Script.Gameplay.Player;
using TMPro;
using UnityEngine;
namespace UI
{
// 负责显示玩家对话内容的UI组件
public class PlayerDialogueViewer : UIBase
{
[SerializeField] private TMP_Text dialogueText;
[SerializeField] private GameObject panel;
private bool isShowingPanel = false;
private PlayerDialogueController playerDialogueController;
private InputManager inputManager;
protected override void Awake()
{
base.Awake();
inputManager = InputManager.Instance;
ControllerLocator.Instance.TryGetWait<PlayerDialogueController>(OnGetPlayerDialogueController);
}
private void OnGetPlayerDialogueController(PlayerDialogueController controller)
{
playerDialogueController = controller;
playerDialogueController.RegisterDialogueViewer(this);
}
public void ReceiveDialogue(string content)
{
if(!isShowingPanel) OpenPanel();
dialogueText.text = content;
}
private void OpenPanel()
{
isShowingPanel = true;
Show();
}
public void ClosePanel()
{
isShowingPanel = false;
Hide();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c0395910b982454e9d80e05e192178e8
timeCreated: 1761184075

View File

@@ -14,50 +14,45 @@ namespace UI
[SerializeField] private GameObject reminderDeleteLinePrefab;
private PlayerInteractorController playerInteractorController;
private PlayerConnectController playerConnectController;
private PlayerDialogueController playerDialogueController;
protected override void Awake()
{
base.Awake();
ControllerLocator.Instance.TryGetWait<PlayerInteractorController>(OnGetInteractorController);
ControllerLocator.Instance.TryGetWait<PlayerConnectController>(OnGetConnectController);
ControllerLocator.Instance.TryGetWait<PlayerDialogueController>(OnGetDialogueController);
}
private void OnGetInteractorController(PlayerInteractorController controller)
{
playerInteractorController = controller;
playerInteractorController.OnGazeEnter += HandleInteractGazeEnter;
playerInteractorController.OnGazeExit += HandleInteractGazeExit;
}
private void HandleInteractGazeEnter(GameObject obj)
{
reminderInteractPrefab.SetActive(true);
}
private void HandleInteractGazeExit(GameObject obj)
{
reminderInteractPrefab.SetActive(false);
playerInteractorController.OnGazeEnter += (obj) => reminderInteractPrefab.SetActive(true);
playerInteractorController.OnGazeExit += (obj) => reminderInteractPrefab.SetActive(false);
}
private void OnGetConnectController(PlayerConnectController controller)
{
playerConnectController = controller;
playerConnectController.OnGazeEnter += HandleConnectGazeEnter;
playerConnectController.OnGazeExit += HandleConnectGazeExit;
playerConnectController.OnGazeEnter += (obj) =>
{
reminderConnectPrefab.SetActive(true);
reminderSetPointPrefab.SetActive(true);
reminderDeleteLinePrefab.SetActive(true);
};
playerConnectController.OnGazeExit += (obj) =>
{
reminderConnectPrefab.SetActive(false);
reminderSetPointPrefab.SetActive(false);
reminderDeleteLinePrefab.SetActive(false);
};
}
private void HandleConnectGazeEnter(GameObject obj)
private void OnGetDialogueController(PlayerDialogueController controller)
{
reminderConnectPrefab.SetActive(true);
reminderSetPointPrefab.SetActive(true);
reminderDeleteLinePrefab.SetActive(true);
}
private void HandleConnectGazeExit(GameObject obj)
{
reminderConnectPrefab.SetActive(false);
reminderSetPointPrefab.SetActive(false);
reminderDeleteLinePrefab.SetActive(false);
playerDialogueController = controller;
playerDialogueController.OnGazeEnterDialogue += (obj) => reminderInteractPrefab.SetActive(true);
playerDialogueController.OnGazeExitDialogue += (obj) => reminderInteractPrefab.SetActive(false);
}
}
}