用户信息发不出去,没有出现在对话框中,但是AI收到了,并且AI的回复正常可见:using UnityEngine.UI;
using System.Collections;
using DG.Tweening;
using System.Collections.Generic;
using UnityEngine.EventSystems;
using UnityEngine;
public class 聊天系统 : MonoBehaviour
{
// UI组件引用
public GameObject 聊天窗口;
public InputField 消息输入框;
public Button 发送按钮;
public Button 关闭按钮;
public Transform 消息容器; // 消息项的父物体(必须是ScrollView的Content)
public GameObject 玩家消息预制体; // 玩家消息预制体(靠右)
public GameObject 朋友消息预制体; // 朋友消息预制体(靠左)
public ScrollRect 聊天滚动视图; // 手动指定滚动视图
public RectTransform 标题栏; // 用于拖拽的标题栏
// 聊天设置
public string 玩家名称 = "我";
public string 朋友名称 = "AI助手";
public Color 玩家消息颜色 = Color.blue;
public Color 朋友消息颜色 = Color.green;
public int 消息分割长度 = 20; // 消息分割长度
// 显示相关设置
public RectTransform 聊天内容区域;
public bool 显示关键错误 = true; // 只保留关键错误显示控制
public Color 调试文本颜色 = Color.red;
// 聊天状态
private bool 聊天窗口是否打开 = false;
public 聊天邀请管理器 邀请管理器;
// 字体相关
public Font 聊天字体;
public int 字体大小 = 16;
// UI层级相关
public Canvas 聊天画布;
public int 消息文本层级 = 5;
// 动画设置
public float 动画持续时间 = 0.3f;
public float 缩放比例 = 0.8f;
public float 弹性系数 = 1.1f;
// 消息历史记录
private List<消息数据> 消息历史 = new List<消息数据>();
public int 最大消息数量 = 50; // 限制最大消息数量
// 拖拽相关变量
private bool 正在拖拽 = false;
private Vector2 拖拽偏移量;
private RectTransform 聊天窗口Rect;
// 引用AI管理器
public AIManager ai管理器;
public void 收到AI消息(string 内容)
{
// 使用朋友的名称和颜色显示AI消息
收到消息(朋友名称, 内容);
}
void Start()
{
聊天窗口.SetActive(false);
聊天窗口是否打开 = false;
邀请管理器 = FindObjectOfType<聊天邀请管理器>();
// 获取聊天窗口的RectTransform用于拖拽
聊天窗口Rect = 聊天窗口.GetComponent<RectTransform>();
// 自动查找AI管理器
if (ai管理器 == null)
{
ai管理器 = FindObjectOfType<AIManager>();
if (ai管理器 != null)
{
ai管理器.聊天系统 = this; // 双向关联
}
else if (显示关键错误)
{
Debug.LogWarning("场景中未找到AIManager组件,AI功能将不可用");
}
}
// 确保标题栏存在,若未指定则尝试查找
if (标题栏 == null)
{
Transform 标题栏Transform = 聊天窗口.transform.Find("标题栏");
if (标题栏Transform != null)
{
标题栏 = 标题栏Transform.GetComponent<RectTransform>();
}
else
{
标题栏 = 聊天窗口Rect;
}
}
// 自动查找滚动视图
if (聊天滚动视图 == null)
聊天滚动视图 = 消息容器.GetComponentInParent<ScrollRect>();
// 验证并修复显示问题
验证消息显示相关组件();
修复输入框();
修复消息容器布局();
修复UI层级问题();
// 绑定事件
发送按钮.onClick.RemoveAllListeners();
发送按钮.onClick.AddListener(发送消息);
关闭按钮.onClick.RemoveAllListeners();
关闭按钮.onClick.AddListener(关闭聊天窗口);
消息输入框.onEndEdit.RemoveAllListeners();
消息输入框.onEndEdit.AddListener(输入框回车发送);
// 添加输入框按键监听,支持Ctrl+Enter换行
消息输入框.onValueChanged.AddListener(OnInputValueChanged);
// 添加拖拽事件监听
添加拖拽事件监听();
StartCoroutine(监控输入框状态());
}
// 添加拖拽事件监听
private void 添加拖拽事件监听()
{
if (标题栏 == null) return;
// 添加事件触发器用于拖拽
EventTrigger trigger = 标题栏.GetComponent<EventTrigger>();
if (trigger == null)
{
trigger = 标题栏.gameObject.AddComponent<EventTrigger>();
}
// 清除现有事件,避免重复添加
trigger.triggers.Clear();
// 鼠标按下事件
EventTrigger.Entry 按下事件 = new EventTrigger.Entry();
按下事件.eventID = EventTriggerType.PointerDown;
按下事件.callback.AddListener((data) => 开始拖拽((PointerEventData)data));
trigger.triggers.Add(按下事件);
// 鼠标拖动事件
EventTrigger.Entry 拖动事件 = new EventTrigger.Entry();
拖动事件.eventID = EventTriggerType.Drag;
拖动事件.callback.AddListener((data) => 执行拖拽((PointerEventData)data));
trigger.triggers.Add(拖动事件);
// 鼠标释放事件
EventTrigger.Entry 释放事件 = new EventTrigger.Entry();
释放事件.eventID = EventTriggerType.PointerUp;
释放事件.callback.AddListener((data) => 结束拖拽());
trigger.triggers.Add(释放事件);
// 鼠标离开事件
EventTrigger.Entry 离开事件 = new EventTrigger.Entry();
离开事件.eventID = EventTriggerType.PointerExit;
离开事件.callback.AddListener((data) => 结束拖拽());
trigger.triggers.Add(离开事件);
}
// 开始拖拽
private void 开始拖拽(PointerEventData 事件数据)
{
正在拖拽 = true;
// 计算鼠标位置与窗口位置的偏移量
RectTransformUtility.ScreenPointToLocalPointInRectangle(
聊天窗口Rect, 事件数据.position, 事件数据.pressEventCamera, out 拖拽偏移量);
}
// 执行拖拽
private void 执行拖拽(PointerEventData 事件数据)
{
if (!正在拖拽 || 聊天窗口Rect == null) return;
Vector2 本地鼠标位置;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
聊天窗口Rect.parent.GetComponent<RectTransform>(),
事件数据.position,
事件数据.pressEventCamera,
out 本地鼠标位置))
{
// 根据偏移量调整窗口位置
聊天窗口Rect.localPosition = 本地鼠标位置 - 拖拽偏移量;
}
}
// 结束拖拽
private void 结束拖拽()
{
正在拖拽 = false;
}
// 处理输入框内容变化,支持Ctrl+Enter换行
private void OnInputValueChanged(string input)
{
if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl))
{
if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))
{
消息输入框.text += "\n";
消息输入框.caretPosition = 消息输入框.text.Length;
}
}
}
// 修复UI层级问题
private void 修复UI层级问题()
{
// 确保有Canvas组件
if (聊天画布 == null)
{
聊天画布 = GetComponentInParent<Canvas>();
if (聊天画布 == null)
{
聊天画布 = 聊天窗口.AddComponent<Canvas>();
聊天窗口.AddComponent<CanvasScaler>();
聊天窗口.AddComponent<GraphicRaycaster>();
}
}
// 确保消息容器在正确层级
if (消息容器 != null)
{
CanvasRenderer cr = 消息容器.GetComponent<CanvasRenderer>();
if (cr == null)
{
cr = 消息容器.gameObject.AddComponent<CanvasRenderer>();
}
// 设置消息容器的层级
RectTransform rt = 消息容器.GetComponent<RectTransform>();
if (rt != null)
{
rt.SetAsLastSibling(); // 确保在最上层
}
}
}
// 验证消息显示相关组件
private void 验证消息显示相关组件()
{
// 验证消息容器
if (消息容器 == null)
{
if (显示关键错误) Debug.LogError("错误:消息容器未赋值!");
return;
}
// 验证滚动视图
if (聊天滚动视图 == null)
{
// 移除布局提示
}
else
{
// 确保消息容器是ScrollView的Content
if (聊天滚动视图.content != 消息容器.GetComponent<RectTransform>())
{
聊天滚动视图.content = 消息容器.GetComponent<RectTransform>();
}
// 修复滚动视图掩码问题
Mask mask = 聊天滚动视图.GetComponent<Mask>();
if (mask != null && !mask.showMaskGraphic)
{
mask.showMaskGraphic = true;
}
// 修复滚动条设置
if (聊天滚动视图.verticalScrollbar != null)
{
聊天滚动视图.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHideAndExpandViewport;
聊天滚动视图.verticalScrollbarSpacing = 2;
}
}
// 验证预制体
if (玩家消息预制体 == null)
{
if (显示关键错误) Debug.LogError("错误:玩家消息预制体未赋值!");
}
if (朋友消息预制体 == null)
{
if (显示关键错误) Debug.LogError("错误:朋友消息预制体未赋值!");
}
// 检查预制体是否激活
if (玩家消息预制体 != null && !玩家消息预制体.activeSelf)
{
玩家消息预制体.SetActive(true);
}
if (朋友消息预制体 != null && !朋友消息预制体.activeSelf)
{
朋友消息预制体.SetActive(true);
}
// 验证文本组件
if (玩家消息预制体 != null)
{
Text 发送者文本 = 玩家消息预制体.transform.Find("发送者文本")?.GetComponent<Text>();
Text 内容文本 = 玩家消息预制体.transform.Find("内容文本")?.GetComponent<Text>();
if (发送者文本 == null || 内容文本 == null)
{
修复消息项预制体(玩家消息预制体);
}
}
if (朋友消息预制体 != null)
{
Text 发送者文本 = 朋友消息预制体.transform.Find("发送者文本")?.GetComponent<Text>();
Text 内容文本 = 朋友消息预制体.transform.Find("内容文本")?.GetComponent<Text>();
if (发送者文本 == null || 内容文本 == null)
{
修复消息项预制体(朋友消息预制体);
}
}
}
// 修复消息项预制体
private void 修复消息项预制体(GameObject 预制体)
{
if (预制体 == null) return;
// 添加发送者文本
if (预制体.transform.Find("发送者文本") == null)
{
GameObject 发送者文本Object = new GameObject("发送者文本");
发送者文本Object.transform.SetParent(预制体.transform);
Text 发送者文本 = 发送者文本Object.AddComponent<Text>();
发送者文本.font = 聊天字体 ?? Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
发送者文本.fontSize = 字体大小;
发送者文本.color = Color.black;
发送者文本.enabled = true;
发送者文本.raycastTarget = false;
// 设置位置
RectTransform rt = 发送者文本Object.GetComponent<RectTransform>();
rt.anchorMin = new Vector2(0, 1);
rt.anchorMax = new Vector2(0, 1);
rt.pivot = new Vector2(0, 1);
rt.offsetMin = new Vector2(5, -25);
rt.offsetMax = new Vector2(100, 0);
}
// 添加内容文本
if (预制体.transform.Find("内容文本") == null)
{
GameObject 内容文本Object = new GameObject("内容文本");
内容文本Object.transform.SetParent(预制体.transform);
Text 内容文本 = 内容文本Object.AddComponent<Text>();
内容文本.font = 聊天字体 ?? Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
内容文本.fontSize = 字体大小;
内容文本.color = Color.black;
内容文本.enabled = true;
内容文本.raycastTarget = false;
// 设置位置
RectTransform rt = 内容文本Object.GetComponent<RectTransform>();
rt.anchorMin = new Vector2(0, 1);
rt.anchorMax = new Vector2(1, 1);
rt.pivot = new Vector2(0, 1);
rt.offsetMin = new Vector2(110, -25);
rt.offsetMax = new Vector2(-5, 0);
}
}
// 修复消息容器布局
private void 修复消息容器布局()
{
if (消息容器 == null) return;
// 确保容器激活
消息容器.gameObject.SetActive(true);
// 添加垂直布局组
VerticalLayoutGroup layout = 消息容器.GetComponent<VerticalLayoutGroup>();
if (layout == null)
{
layout = 消息容器.gameObject.AddComponent<VerticalLayoutGroup>();
}
// 修复间距和布局冲突问题
layout.spacing = 20;
layout.childAlignment = TextAnchor.UpperLeft;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = false;
layout.padding = new RectOffset(10, 10, 10, 10);
// 添加内容大小适配
ContentSizeFitter fitter = 消息容器.GetComponent<ContentSizeFitter>();
if (fitter == null)
{
fitter = 消息容器.gameObject.AddComponent<ContentSizeFitter>();
}
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
// 滚动视图设置优化
if (聊天滚动视图 != null)
{
聊天滚动视图.vertical = true;
聊天滚动视图.horizontal = false;
聊天滚动视图.movementType = ScrollRect.MovementType.Clamped;
聊天滚动视图.content.sizeDelta = new Vector2(0, 0);
聊天滚动视图.scrollSensitivity = 40f;
// 确保滚动条正确关联
if (聊天滚动视图.verticalScrollbar != null)
{
聊天滚动视图.verticalScrollbar.value = 0; // 初始位置在底部
}
}
// 修复两个预制体的RectTransform
修复预制体RectTransform(玩家消息预制体, true);
修复预制体RectTransform(朋友消息预制体, false);
}
// 修复预制体的RectTransform
private void 修复预制体RectTransform(GameObject 预制体, bool 是玩家消息)
{
if (预制体 == null) return;
RectTransform rt = 预制体.GetComponent<RectTransform>();
if (rt != null)
{
rt.anchorMin = new Vector2(0, 1);
rt.anchorMax = new Vector2(1, 1);
rt.pivot = new Vector2(0.5f, 1);
rt.offsetMin = new Vector2(0, 0);
rt.offsetMax = new Vector2(0, 0);
rt.localScale = Vector3.one;
}
// 确保文本可见
Text[] texts = 预制体.GetComponentsInChildren<Text>();
foreach (var text in texts)
{
text.enabled = true;
text.font = 聊天字体 ?? Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
text.fontSize = 字体大小;
text.horizontalOverflow = HorizontalWrapMode.Wrap;
text.verticalOverflow = VerticalWrapMode.Overflow;
}
}
// 分割消息的方法
private List<string> 分割消息(string 内容)
{
List<string> 分割后的消息 = new List<string>();
// 如果消息长度小于等于分割长度,直接返回原消息
if (string.IsNullOrEmpty(内容) || 内容.Length <= 消息分割长度)
{
分割后的消息.Add(内容);
return 分割后的消息;
}
// 否则按指定长度分割消息
for (int i = 0; i < 内容.Length; i += 消息分割长度)
{
int 截取长度 = Mathf.Min(消息分割长度, 内容.Length - i);
string 子消息 = 内容.Substring(i, 截取长度);
分割后的消息.Add(子消息);
}
return 分割后的消息;
}
// 显示消息项
private void 显示消息项(string 发送者, string 内容, Color 颜色, bool 是玩家消息, bool 显示发送者 = true)
{
if (消息容器 == null)
{
if (显示关键错误) Debug.LogError("消息容器未赋值!");
return;
}
// 根据是否是玩家消息选择不同的预制体
GameObject 消息预制体 = 是玩家消息 ? 玩家消息预制体 : 朋友消息预制体;
if (消息预制体 == null)
{
if (显示关键错误) Debug.LogError("消息项预制体未赋值!");
return;
}
// 实例化消息项
GameObject 消息项 = Instantiate(消息预制体, 消息容器);
消息项.name = $"{(是玩家消息 ? "玩家" : "朋友")}_消息_{System.DateTime.Now.Ticks}";
消息项.SetActive(true);
消息项.transform.SetAsLastSibling();
// 初始状态设置为缩放为0,为动画做准备
消息项.transform.localScale = Vector3.zero;
// 设置发送者文本
Text 发送者文本 = 消息项.transform.Find("发送者文本")?.GetComponent<Text>();
if (发送者文本 != null)
{
发送者文本.text = 发送者 + ":";
发送者文本.color = 颜色;
发送者文本.font = 聊天字体 ?? Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
发送者文本.fontSize = 字体大小;
发送者文本.enabled = 显示发送者;
发送者文本.rectTransform.sizeDelta = new Vector2(100, 发送者文本.preferredHeight);
}
else if (显示关键错误)
{
Debug.LogError("未找到'发送者文本'组件!");
}
// 设置内容文本
Text 内容文本 = 消息项.transform.Find("内容文本")?.GetComponent<Text>();
if (内容文本 != null)
{
内容文本.text = 内容;
内容文本.font = 聊天字体 ?? Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
内容文本.fontSize = 字体大小;
内容文本.enabled = true;
// 计算最大宽度
float 最大宽度 = 聊天滚动视图 != null ?
聊天滚动视图.GetComponent<RectTransform>().rect.width - 120 : 300;
内容文本.rectTransform.sizeDelta = new Vector2(最大宽度, 内容文本.preferredHeight);
// 强制更新文本尺寸
Canvas.ForceUpdateCanvases();
LayoutRebuilder.ForceRebuildLayoutImmediate(消息项.GetComponent<RectTransform>());
}
else if (显示关键错误)
{
Debug.LogError("未找到'内容文本'组件!");
}
// 强制刷新布局
LayoutRebuilder.ForceRebuildLayoutImmediate(消息容器.GetComponent<RectTransform>());
if (聊天内容区域 != null)
LayoutRebuilder.ForceRebuildLayoutImmediate(聊天内容区域);
// 应用Q弹动画效果
应用消息动画(消息项.transform);
// 保存消息到历史记录
消息历史.Add(new 消息数据(发送者, 内容, System.DateTime.Now, 是玩家消息));
// 检查并移除超出最大数量的消息
清理旧消息();
// 滚动到最新消息
StartCoroutine(滚动到最新消息());
}
// 应用消息弹出的Q弹动画
private void 应用消息动画(Transform 消息变换)
{
// 确保DOTween可用
if (消息变换 == null) return;
// 重置缩放
消息变换.localScale = Vector3.one * 缩放比例;
// 使用DOTween创建Q弹效果
消息变换.DOScale(Vector3.one * 弹性系数, 动画持续时间 / 2)
.SetEase(Ease.OutQuad)
.OnComplete(() => {
// 回弹效果
消息变换.DOScale(Vector3.one, 动画持续时间 / 2)
.SetEase(Ease.OutElastic);
});
}
// 滚动到最新消息 - 优化滚动效果
private IEnumerator 滚动到最新消息()
{
// 等待布局刷新
yield return new WaitForEndOfFrame();
if (聊天滚动视图 != null)
{
// 强制更新内容大小
ContentSizeFitter fitter = 消息容器.GetComponent<ContentSizeFitter>();
if (fitter != null)
{
fitter.SetLayoutVertical();
}
// 使用动画平滑滚动到底部
float targetPos = 0; // 0表示滚动到底部
float startPos = 聊天滚动视图.verticalNormalizedPosition;
float elapsedTime = 0;
float duration = 0.2f;
while (elapsedTime < duration)
{
elapsedTime += Time.deltaTime;
聊天滚动视图.verticalNormalizedPosition = Mathf.Lerp(startPos, targetPos, elapsedTime / duration);
yield return null;
}
// 确保最终位置正确
聊天滚动视图.verticalNormalizedPosition = targetPos;
}
}
// 清理超出最大数量的旧消息
private void 清理旧消息()
{
if (消息历史.Count <= 最大消息数量) return;
int 要删除的数量 = 消息历史.Count - 最大消息数量;
// 从容器中移除最早的消息对象
for (int i = 0; i < 要删除的数量 && 消息容器.childCount > 0; i++)
{
Destroy(消息容器.GetChild(0).gameObject);
}
// 从历史记录中移除
消息历史.RemoveRange(0, 要删除的数量);
}
private void 验证必要组件()
{
if (消息输入框 == null && 显示关键错误)
Debug.LogError("错误:消息输入框未赋值!");
if (发送按钮 == null && 显示关键错误)
Debug.LogError("错误:发送按钮未赋值!");
if (关闭按钮 == null && 显示关键错误)
Debug.LogError("错误:关闭按钮未赋值!");
if (聊天字体 == null)
{
聊天字体 = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
if (聊天字体 == null && 消息输入框 != null && 消息输入框.textComponent != null)
聊天字体 = 消息输入框.textComponent.font;
}
}
private void 修复输入框()
{
if (消息输入框 == null) return;
消息输入框.gameObject.SetActive(true);
消息输入框.enabled = true;
消息输入框.interactable = true;
if (消息输入框.textComponent == null)
{
Text textComp = 消息输入框.GetComponentInChildren<Text>();
if (textComp == null) textComp = 消息输入框.gameObject.AddComponent<Text>();
消息输入框.textComponent = textComp;
}
if (消息输入框.textComponent != null)
{
if (聊天字体 != null) 消息输入框.textComponent.font = 聊天字体;
消息输入框.textComponent.fontSize = 字体大小;
消息输入框.textComponent.color = Color.black;
}
if (消息输入框.placeholder != null)
{
Text placeholder = 消息输入框.placeholder.GetComponent<Text>();
if (placeholder != null)
{
if (聊天字体 != null) placeholder.font = 聊天字体;
placeholder.text = "输入消息...(Ctrl+Enter换行)";
}
}
}
private IEnumerator 监控输入框状态()
{
while (true)
{
if (聊天窗口是否打开 && 消息输入框 != null && !消息输入框.interactable)
消息输入框.interactable = true;
yield return new WaitForSeconds(0.5f);
}
}
public void 打开聊天窗口()
{
if (聊天窗口是否打开) return;
聊天窗口.SetActive(true);
聊天窗口是否打开 = true;
if (消息输入框 != null) 消息输入框.text = "";
StartCoroutine(延迟聚焦输入框(0.1f));
if (邀请管理器 != null) 邀请管理器.设置正在聊天(true);
}
public void 关闭聊天窗口()
{
if (!聊天窗口是否打开) return;
聊天窗口.SetActive(false);
聊天窗口是否打开 = false;
if (邀请管理器 != null) 邀请管理器.设置正在聊天(false);
}
public void 发送消息()
{
if (!聊天窗口是否打开 || 消息输入框 == null) return;
string 消息内容 = 消息输入框.text.Trim();
if (string.IsNullOrEmpty(消息内容)) return;
List<string> 分割后的消息 = 分割消息(消息内容);
// 发送分割后的消息
for (int i = 0; i < 分割后的消息.Count; i++)
{
// 只有第一条消息显示发送者,后续分割的消息隐藏发送者
bool 显示发送者 = (i == 0);
显示消息项(玩家名称, 分割后的消息[i], 玩家消息颜色, true, 显示发送者);
}
// 发送消息给AI
if (ai管理器 != null)
{
ai管理器.发送消息给AI(消息内容);
}
else if (显示关键错误)
{
Debug.LogWarning("AI管理器未找到,无法发送消息给AI");
}
消息输入框.text = "";
消息输入框.ActivateInputField();
}
private IEnumerator 延迟聚焦输入框(float 延迟时间)
{
yield return new WaitForSeconds(延迟时间);
if (消息输入框 != null)
{
消息输入框.Select();
消息输入框.ActivateInputField();
}
}
private void 输入框回车发送(string 内容)
{
if ((Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter)) &&
!(Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)))
{
if (聊天窗口是否打开) 发送消息();
消息输入框.DeactivateInputField();
消息输入框.ActivateInputField();
}
}
public void 收到消息(string 发送者, string 内容)
{
List<string> 分割后的消息 = 分割消息(内容);
for (int i = 0; i < 分割后的消息.Count; i++)
{
bool 显示发送者 = (i == 0);
显示消息项(发送者, 分割后的消息[i], 朋友消息颜色, false, 显示发送者);
}
}
// 消息数据类,用于存储消息历史
private class 消息数据
{
public string 发送者;
public string 内容;
public System.DateTime 时间;
public bool 是玩家消息;
public 消息数据(string 发送者, string 内容, System.DateTime 时间, bool 是玩家消息)
{
this.发送者 = 发送者;
this.内容 = 内容;
this.时间 = 时间;
this.是玩家消息 = 是玩家消息;
}
}
private static 聊天系统 _instance;
public static 聊天系统 Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<聊天系统>();
if (_instance == null)
{
GameObject go = new GameObject("聊天系统");
_instance = go.AddComponent<聊天系统>();
DontDestroyOnLoad(go);
}
}
return _instance;
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using System.Threading;
using Newtonsoft.Json;
// 错误响应模型
public class ErrorResponse
{
public ErrorDetails error;
}
public class ErrorDetails
{
public string message;
public string code;
}
// 聊天响应模型(阻塞模式)
public class ChatCompletionResponse
{
[JsonProperty("event")]
public string @event;
[JsonProperty("task_id")]
public string task_id;
[JsonProperty("id")]
public string id;
[JsonProperty("message_id")]
public string message_id;
[JsonProperty("conversation_id")]
public string conversation_id;
[JsonProperty("mode")]
public string mode;
[JsonProperty("answer")]
public string answer;
[JsonProperty("metadata")]
public Metadata metadata;
[JsonProperty("created_at")]
public long created_at;
}
// 元数据模型
public class Metadata
{
[JsonProperty("usage")]
public Usage usage;
[JsonProperty("retriever_resources")]
public List<RetrieverResource> retriever_resources;
}
// 用量信息模型
public class Usage
{
[JsonProperty("prompt_tokens")]
public int prompt_tokens;
[JsonProperty("prompt_unit_price")]
public string prompt_unit_price;
[JsonProperty("prompt_price_unit")]
public string prompt_price_unit;
[JsonProperty("prompt_price")]
public string prompt_price;
[JsonProperty("completion_tokens")]
public int completion_tokens;
[JsonProperty("completion_unit_price")]
public string completion_unit_price;
[JsonProperty("completion_price_unit")]
public string completion_price_unit;
[JsonProperty("completion_price")]
public string completion_price;
[JsonProperty("total_tokens")]
public int total_tokens;
[JsonProperty("total_price")]
public string total_price;
[JsonProperty("currency")]
public string currency;
[JsonProperty("latency")]
public float latency;
}
// 检索资源模型
public class RetrieverResource
{
[JsonProperty("position")]
public int position;
[JsonProperty("dataset_id")]
public string dataset_id;
[JsonProperty("dataset_name")]
public string dataset_name;
[JsonProperty("document_id")]
public string document_id;
[JsonProperty("document_name")]
public string document_name;
[JsonProperty("segment_id")]
public string segment_id;
[JsonProperty("score")]
public float score;
[JsonProperty("content")]
public string content;
}
// 文件信息模型
public class FileInfo
{
[JsonProperty("type")]
public string type;
[JsonProperty("transfer_method")]
public string transfer_method;
[JsonProperty("url")]
public string url;
[JsonProperty("upload_file_id")]
public string upload_file_id;
}
// 请求数据模型
public class ChatRequestData
{
[JsonProperty("inputs")]
public Dictionary<string, string> inputs;
[JsonProperty("query")]
public string query;
[JsonProperty("response_mode")]
public string response_mode;
[JsonProperty("user")]
public string user;
[JsonProperty("conversation_id")]
public string conversation_id;
[JsonProperty("files")]
public List<FileInfo> files;
[JsonProperty("auto_generate_name")]
public bool? auto_generate_name;
[JsonProperty("workflow_id")]
public string workflow_id;
[JsonProperty("trace_id")]
public string trace_id;
}
// 简单的错误对象模型(替代dynamic)
public class SimpleErrorObject
{
[JsonProperty("message")]
public string message;
[JsonProperty("code")]
public string code;
}
public class AIManager : MonoBehaviour
{
private static AIManager _instance;
public static AIManager Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<AIManager>();
if (_instance == null)
{
GameObject go = new GameObject("AIManager");
_instance = go.AddComponent<AIManager>();
DontDestroyOnLoad(go);
}
}
return _instance;
}
}
[Header("API设置")]
[SerializeField] private string apiUrl = "http://localhost/v1/chat-messages";
[SerializeField] private string apiKey = "aapp-wRDYHNHA5BmBsGsMZjDPqfL1";
[Header("响应模式")]
[SerializeField] private string responseMode = "blocking"; // blocking 或 streaming
[Header("引用")]
public 聊天系统 聊天系统;
[Header("标志位")]
public bool 是否转发给聊天系统 = true; // ✅ 新增标志位
private string _conversationId = "";
private bool _isProcessing = false;
private int _mainThreadId;
private readonly Queue<Action> _mainThreadActions = new Queue<Action>();
private string _当前回复 = null;
public string 当前回复
{
get { return _当前回复; }
set { _当前回复 = value; }
}
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_mainThreadId = Thread.CurrentThread.ManagedThreadId;
if (聊天系统 == null)
{
聊天系统 = FindObjectOfType<聊天系统>();
}
}
private void Update()
{
lock (_mainThreadActions)
{
while (_mainThreadActions.Count > 0)
{
try
{
_mainThreadActions.Dequeue().Invoke();
}
catch (Exception ex)
{
Debug.LogError($"主线程任务执行错误: {ex.Message}");
}
}
}
}
/// <summary>
/// 发送消息到AI
/// </summary>
public void 发送消息给AI(string query, Dictionary<string, string> inputs = null)
{
if (_isProcessing)
{
Debug.LogWarning("正在处理前一个请求,请等待完成");
return;
}
if (string.IsNullOrEmpty(query))
{
Debug.LogWarning("发送的消息不能为空");
return;
}
StartCoroutine(PostChatMessage(query, inputs));
}
/// <summary>
/// 发送带图片的消息
/// </summary>
public void 发送带图片的消息(string query, string imageUrl, string transferMethod = "remote_url")
{
var inputs = new Dictionary<string, string>();
var files = new List<FileInfo>
{
new FileInfo
{
type = "image",
transfer_method = transferMethod,
url = imageUrl
}
};
StartCoroutine(PostChatMessageWithFiles(query, inputs, files));
}
/// <summary>
/// 开始新对话
/// </summary>
public void 开始新对话()
{
_conversationId = "";
Debug.Log("开始新对话,会话ID已重置");
}
/// <summary>
/// 使用UnityWebRequest发送POST请求
/// </summary>
private IEnumerator PostChatMessage(string query, Dictionary<string, string> inputs = null)
{
_isProcessing = true;
// 构建请求数据 - 根据Dify API文档要求
var requestData = new ChatRequestData
{
inputs = inputs ?? new Dictionary<string, string>(),
query = query,
response_mode = responseMode,
user = SystemInfo.deviceUniqueIdentifier,
conversation_id = _conversationId,
auto_generate_name = true,
files = new List<FileInfo>() // 初始化为空列表
};
// 使用Newtonsoft.Json序列化,忽略null值
string jsonBody = JsonConvert.SerializeObject(requestData,
new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.None
});
byte[] rawData = Encoding.UTF8.GetBytes(jsonBody);
Debug.Log($"发送请求到: {apiUrl}");
Debug.Log($"请求数据: {jsonBody}");
using (UnityWebRequest request = new UnityWebRequest(apiUrl, "POST"))
{
request.uploadHandler = new UploadHandlerRaw(rawData);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Authorization", $"Bearer {apiKey}");
// 设置超时
request.timeout = 60;
yield return request.SendWebRequest();
Debug.Log($"响应状态: {request.result}");
Debug.Log($"HTTP状态码: {request.responseCode}");
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log($"API响应成功: {request.downloadHandler.text}");
ProcessSuccessResponse(request.downloadHandler.text);
}
else
{
Debug.LogError($"API请求失败: {request.error}");
ProcessErrorResponse(request.error, request.downloadHandler.text, request.responseCode);
}
}
_isProcessing = false;
}
/// <summary>
/// 发送带文件的聊天消息
/// </summary>
private IEnumerator PostChatMessageWithFiles(string query, Dictionary<string, string> inputs, List<FileInfo> files)
{
_isProcessing = true;
var requestData = new ChatRequestData
{
inputs = inputs ?? new Dictionary<string, string>(),
query = query,
response_mode = responseMode,
user = SystemInfo.deviceUniqueIdentifier,
conversation_id = _conversationId,
files = files,
auto_generate_name = true
};
string jsonBody = JsonConvert.SerializeObject(requestData,
new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.None
});
byte[] rawData = Encoding.UTF8.GetBytes(jsonBody);
Debug.Log($"发送带文件的请求: {jsonBody}");
using (UnityWebRequest request = new UnityWebRequest(apiUrl, "POST"))
{
request.uploadHandler = new UploadHandlerRaw(rawData);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Authorization", $"Bearer {apiKey}");
request.timeout = 60;
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
ProcessSuccessResponse(request.downloadHandler.text);
}
else
{
ProcessErrorResponse(request.error, request.downloadHandler.text, request.responseCode);
}
}
_isProcessing = false;
}
/// <summary>
/// 处理成功响应
/// </summary>
private void ProcessSuccessResponse(string responseText)
{
try
{
if (responseMode == "blocking")
{
// 阻塞模式响应
ChatCompletionResponse response = JsonConvert.DeserializeObject<ChatCompletionResponse>(responseText);
if (response == null)
{
throw new Exception("解析响应结果为空");
}
// 更新会话ID
if (!string.IsNullOrEmpty(response.conversation_id))
{
_conversationId = response.conversation_id;
Debug.Log($"更新会话ID: {_conversationId}");
}
string message = string.IsNullOrEmpty(response.answer) ? "收到空消息" : response.answer;
_当前回复 = message;
// ✅ 仅当标志位为 true 时才转发给聊天系统
if (是否转发给聊天系统)
{
ExecuteOnMainThread(() =>
{
if (聊天系统 != null)
{
聊天系统.收到AI消息(message);
}
});
}
// 记录用量信息
if (response.metadata?.usage != null)
{
Debug.Log($"用量信息 - Tokens: {response.metadata.usage.total_tokens}, 价格: {response.metadata.usage.total_price} {response.metadata.usage.currency}");
}
}
else
{
// 流式模式响应处理(如果需要)
Debug.LogWarning("流式模式响应,需要特殊处理");
ProcessStreamingResponse(responseText);
}
}
catch (Exception ex)
{
Debug.LogError($"解析响应失败: {ex.Message}\n原始响应: {responseText}");
ExecuteOnMainThread(() =>
{
if (聊天系统 != null)
{
聊天系统.收到AI消息($"解析响应时发生错误: {ex.Message}");
}
});
}
}
/// <summary>
/// 处理流式响应(简化版)
/// </summary>
private void ProcessStreamingResponse(string responseText)
{
// 这里简化处理,实际需要解析SSE格式
try
{
// 尝试直接解析为JSON,如果不是SSE格式
ChatCompletionResponse response = JsonConvert.DeserializeObject<ChatCompletionResponse>(responseText);
if (response != null && !string.IsNullOrEmpty(response.answer))
{
// ✅ 仅当标志位为 true 时才转发给聊天系统
if (是否转发给聊天系统)
{
ExecuteOnMainThread(() =>
{
if (聊天系统 != null)
{
聊天系统.收到AI消息(response.answer);
}
});
}
}
else
{
// 如果不是标准格式,显示原始内容
if (是否转发给聊天系统)
{
ExecuteOnMainThread(() =>
{
if (聊天系统 != null)
{
聊天系统.收到AI消息($"流式响应: {responseText}");
}
});
}
}
}
catch
{
// 如果不是JSON格式,可能是SSE,这里简单显示原始内容
if (是否转发给聊天系统)
{
ExecuteOnMainThread(() =>
{
if (聊天系统 != null)
{
聊天系统.收到AI消息($"流式响应内容: {responseText}");
}
});
}
}
}
/// <summary>
/// 处理错误响应
/// </summary>
private void ProcessErrorResponse(string error, string responseText, long responseCode)
{
string errorMessage = $"API调用失败: {error} (HTTP {responseCode})";
try
{
// 检查是否是HTML响应
if (!string.IsNullOrEmpty(responseText) &&
(responseText.Contains("<!DOCTYPE html>") || responseText.Contains("<html")))
{
errorMessage = $"服务器返回了HTML页面而不是JSON响应 (HTTP {responseCode})\n";
errorMessage += "可能的原因:\n";
errorMessage += "1. API端点URL不正确\n";
errorMessage += "2. 认证失败\n";
errorMessage += "3. 服务器配置问题";
Debug.LogError(errorMessage);
// 记录部分响应内容
string shortResponse = responseText.Length > 500 ?
responseText.Substring(0, 500) + "..." : responseText;
Debug.LogError($"HTML响应内容: {shortResponse}");
}
else if (!string.IsNullOrEmpty(responseText))
{
// 尝试解析JSON错误响应
ErrorResponse errorResponse = JsonConvert.DeserializeObject<ErrorResponse>(responseText);
if (errorResponse?.error != null)
{
errorMessage = $"API错误: {errorResponse.error.message} (代码: {errorResponse.error.code})";
Debug.LogError(errorMessage);
}
else
{
// 尝试解析其他格式的错误
try
{
// 使用简单的错误对象模型替代dynamic
SimpleErrorObject errorObj = JsonConvert.DeserializeObject<SimpleErrorObject>(responseText);
if (errorObj != null && !string.IsNullOrEmpty(errorObj.message))
{
errorMessage = $"错误: {errorObj.message}";
if (!string.IsNullOrEmpty(errorObj.code))
{
errorMessage += $" (代码: {errorObj.code})";
}
}
else
{
errorMessage = $"错误响应: {responseText}";
}
}
catch
{
errorMessage = $"原始错误: {error}\n响应内容: {responseText}";
}
}
}
}
catch (Exception ex)
{
errorMessage = $"处理错误响应时发生异常: {ex.Message}\n原始错误: {error}";
Debug.LogError(errorMessage);
}
// ✅ 仅当标志位为 true 时才转发给聊天系统
if (是否转发给聊天系统)
{
ExecuteOnMainThread(() =>
{
if (聊天系统 != null)
{
聊天系统.收到AI消息(errorMessage);
}
});
}
}
/// <summary>
/// 停止响应(仅流式模式)
/// </summary>
public void 停止响应(string taskId)
{
if (responseMode != "streaming")
{
Debug.LogWarning("停止响应功能仅适用于流式模式");
return;
}
StartCoroutine(StopChatMessage(taskId));
}
private IEnumerator StopChatMessage(string taskId)
{
string stopUrl = $"http://localhost/v1/chat-messages/{taskId}/stop";
var stopData = new
{
user = SystemInfo.deviceUniqueIdentifier
};
string jsonBody = JsonConvert.SerializeObject(stopData);
byte[] rawData = Encoding.UTF8.GetBytes(jsonBody);
using (UnityWebRequest request = new UnityWebRequest(stopUrl, "POST"))
{
request.uploadHandler = new UploadHandlerRaw(rawData);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Authorization", $"Bearer {apiKey}");
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log("成功停止响应");
}
else
{
Debug.LogError($"停止响应失败: {request.error}");
}
}
}
/// <summary>
/// 在主线程执行操作
/// </summary>
private void ExecuteOnMainThread(Action action)
{
if (action == null) return;
if (Thread.CurrentThread.ManagedThreadId == _mainThreadId)
{
action.Invoke();
}
else
{
lock (_mainThreadActions)
{
_mainThreadActions.Enqueue(action);
}
}
}
/// <summary>
/// 测试API连接
/// </summary>
public void 测试连接()
{
StartCoroutine(TestAPIConnection());
}
private IEnumerator TestAPIConnection()
{
Debug.Log("测试API连接...");
using (UnityWebRequest request = UnityWebRequest.Get("http://localhost/v1/"))
{
request.SetRequestHeader("Authorization", $"Bearer {apiKey}");
request.timeout = 10;
yield return request.SendWebRequest();
Debug.Log($"测试URL: http://localhost/v1/");
Debug.Log($"响应状态: {request.responseCode}");
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log("API连接测试成功");
Debug.Log($"响应内容: {request.downloadHandler.text}");
}
else
{
Debug.LogError("API连接测试失败");
Debug.LogError($"错误信息: {request.error}");
Debug.LogError($"响应内容: {request.downloadHandler.text}");
}
}
}
/// <summary>
/// 获取当前会话ID
/// </summary>
public string 获取当前会话ID()
{
return _conversationId;
}
/// <summary>
/// 设置会话ID(用于恢复对话)
/// </summary>
public void 设置会话ID(string conversationId)
{
_conversationId = conversationId;
Debug.Log($"设置会话ID: {_conversationId}");
}
/// <summary>
/// 切换响应模式
/// </summary>
public void 切换响应模式(string mode)
{
if (mode == "blocking" || mode == "streaming")
{
responseMode = mode;
Debug.Log($"响应模式已切换为: {responseMode}");
}
else
{
Debug.LogWarning("无效的响应模式,只能使用 'blocking' 或 'streaming'");
}
}
}