【AI学习笔记】基于Unity+DeepSeek开发的一些BUG记录&解决方案
背景前摇:(省流可不看)
Unity是大学学的,AI是研究生学的,DeepSeek是第一份实习偷师的,三合一的梦是最近开始做的,BUG是今天遇到并且解决的。
关于Unity和大模型结合的教程网上并不多,正好符合Unity+DeepSeek的我目前只看到这一篇《【Unity+AI01】在Unity中调用DeepSeek大模型!实现AI对话功能!》:https://blog.csdn.net/leoysq/article/details/139547284
传送门
阅读全文后,发现这篇文章工程量不大,涉及的知识点(UGUI,C#编程,DeepSeek的API接入)都是我目前比较熟悉的,这个项目可以跟着做,于是就愉快地开始了尝试。
在复现工程的时候我遇到了一些奇奇怪怪的问题,查了CSDN和百度以后发现相关的解释帖子还很少,决定自己开个帖子记录下来。
————————————————————————————————
BUG集锦:
Unity版本:Unity6
1.Unity报错Error: HTTP/1.1 401 Unauthorized
解决方法:看看API有没有写对。
UnityEngine.Debug:LogError (object)
DeepSeekChat/d__8:MoveNext () (at Assets/Scripts/DeepSeekChat.cs:93)
UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr),
这种联网报错我处理经验不多,看红感叹号出来人都麻了,GPT查了一下,这个报错表示未授权。
上网搜了一圈,没有我这种情境下报错的解决方案,我先在原作者的评论区下方留言等待好心人答复,也厚着脸皮通过DeepSeek官网的聊天渠道反馈了BUG。
很快我就收到了邮件和聊天界面双重回复——未授权一般是API没写对。
我觉得DeepSeek的这一点做得特别好,聊天界面和邮件都可以看到过往记录并且回复,这样沟通就很方便自由。
我检查了一下我的代码,确实,API抄错了,写成隔壁智谱清言的了。
同时我也获得了教程原作者的友善回复:
2.base_url连接报错Error: HTTP/1.1 404 Not Found
解决方法:根据需要,换形如https://api.deepseek.com/chat/completions 这样的完整接口地址
UnityEngine.Debug:LogError (object)
DeepSeekChat/d__8:MoveNext () (at Assets/Scripts/DeepSeekChat.cs:94)
UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)
当时在跟着写代码的时候,我就发现一件很奇怪的事情——原教程作者写的url很特别。
我之前用Python+DeepSeek的时候,用的是官网的base_url,一切都很顺利。
https://platform.deepseek.com/api-docs/zh-cn/
DeepSeek官网传送门
但是我发现,如果Unity里面也用这个链接,Unity会报错。
但如果用教程作者老师的这个我没见过的“野生”链接,就没事。
这我就不明白了,因为我之前用python做过DeepSeek开发,一直照着官方文档这么用都没问题。
于是我又厚着脸皮请教了DeepSeek的老师们。
很快我得到了详细的答案:
我理解的一句话总结是:使用Openai python sdk最省事,base_url能应付所有问题。
但如果是其他平台,就得根据功能需要换具体的链接了。
比如我现在用Unity做的功能可以归类为【对话补全】,那么就要需要这个页面。
https://platform.deepseek.com/api-docs/zh-cn/api/create-chat-completion/
传送门
复制下图红框所示的链接,也就是原教程的”野生“链接:
这样就可以在Unity里顺利调用DeepSeek的API啦!
3.TextMeshPro不显示AI的回复
解决方案:看看UGUI的排列、颜色
按代码逻辑,成功发送消息以后,AI应该回复,并且在背景板上显示出来。
但现在什么也没有,我首先排除了最不应该的UI层级错误(背景白色image把text盖住了),不是这个原因。
然后也Debug.Log了各个核心环节,发现AI其实回复了,但我的TextMeshPro没有正常显示内容。
所以大概BUG是从下面这一步开始的。
我按GPT给的办法加了个存在与否的判断,但我感觉不该是这种错误,因为确实把按钮拖进了脚本对应位置,要是没找到对象的话早ERROR了。
我觉得还是UI本身的问题,想着会不会还是我层级搞错了,于是就尝试把image隐藏看看是不是字在背后,结果——
破案了,原来是白色字体和背景融为一体了……
啥好玩意默认字体是白色啊,我以为默认黑色字来着,真是给我整的哭笑不得。
于是我给image换了个底色,问题解决了。
以及,因为要显示聊天记录,这个TextMeshPro不会在发信息后清空原来的内容,要美观的话记得把默认的New Text删除了。
4.安装Unity的可集成HTTP请求库(原作者的NuGet For Unity插件)
解决方法:从GitHub下载NuGet For Unity插件:https://github.com/GlitchEnzo/NuGetForUnity
Unity的HTTP请求库有UnityWebRequest,或第三方库如LitJson、Newtonsoft.Json等,可以将这三者类比为Python的Request、BeautifulSoup、Scrapy库,它们可以实现(指定数据格式)+(指定目标网页)的情况下爬取网站内容的任务。
-UnityWebRequest:Unity内置的HTTP客户端库,不需要额外安装,集成在Unity引擎中。其他的第三方库需要自己安装,比如原作者使用的NuGet For Unity。
第一步的下载链接:https://github.com/GlitchEnzo/NuGetForUnity
传送门
这个下载下来是个Unitypackage,直接导入Unity就行。
第4步的详细操作如下:
其他可能的优化方案
已实现:
1.点击Send按钮以后自动清空InputField的内容。
2.对话界面也加上用户的输入。
3.显示历史记录的TextMeshPro添加滚轮滑动条。
这个问题我困扰很久,于是上b站搜集了一些教程,发现这个问题很早之前就有人提了。
随机尝试后,发现这位UP的方法跟着做完全程以后,能实现我需要的功能:
https://www.bilibili.com/video/BV1Wv41137ZU?p=2&vd_source=cdfd0a0810bcc0bcdbcf373dafdf6a82
传送门
具体操作直接看这位大佬的视频就行,这里我补记几个我个人觉得比较关键的地方:
第一,用滑动条滑动的比例 x 可滑动的总长度 = 新的文字展示区域y轴相对父物体的坐标。
这段代码有点抽象,让AI智谱清言帮忙解释一下:
第二,Text组件本身Vertical Overflow这里要设置为Overflow,这样多余的文字才能在下方显示出来,而不是就被原来的大小框死不显示, 这样就没法滑动了啊。
Area会添加一个Content Size Fitter组件,这个VerticalFit要设置为Preferred Size,不然这个Area没法和Text一样随地大小变,然后又会滑动了个寂寞。
第三,给Mask对象添加一个Mask组件,超出Area的文字就看不到了,这样就实现了限定范围内滑动的效果。
外面这一圈突兀的Mask只是用来直观展示效果的,如果是自己做项目的话把Area和Mask设置为同样大小(见下图),添加完Mask组件就是实际使用的效果了。
特别说明:Mask物体上的Image组件不能删除,否则会失去遮挡效果。文字会溢出Mask的范围。
第四,在第三步的基础上,给Area上的VerticalLayout Group组件适当加一些Padding,会让文字和Area范围四周有一些空隙,显得更加美观。
4.加入中文字库允许中文交互。
中文字体这个网上有很多解决方案,我之前查了一下觉得很麻烦,暂时没精力(懒得)搞。
一句话我的理解就是要自己导入个字体,而且要尽可能包含多的中文字,不然遇到一个生字就会显示为下图这种方框。
第二份实习做过一些编辑工作,使用了两款商用字体:中文——阿里普惠体,英文——Monsterrate(拼写不确定),有精力的码友可以试试这两款字体。
但如果想偷懒的话,这件事情其实很好办——不要用TextMeshPro系列的UI,就用老款的Legacy的就没事了。
项目运行效果如图,文本框滑动效果还不是很完美,但比起之前好多了。
我个人总结了一下,自适应的核心就是放文本的Text+ContentSizeFitter,然后装Text的容器也加ContentSizeFitter,配合水平或者垂直的LayoutGroup。这样就已经有了自适应的文本框了。
在这个基础上再添加Slider配合代码,就是可滑动的自适应文本框了。
关于这个自适应文本框的其他学习视频链接:
https://www.bilibili.com/list/watchlater?oid=531535469&bvid=BV1Bu411G7Rj&spm_id_from=333.1296.top_right_bar_window_view_later.content.click
https://www.bilibili.com/list/watchlater?oid=723054334&bvid=BV1hS4y1Z7zD&spm_id_from=333.1296.top_right_bar_window_view_later.content.click
————————————————————————————————————————————
添加注释&功能后的完整代码:
工程我发到了CSDN个人资源,有需要的朋友可以自行下载。
把参考的原博主的DeepSeekChat.cs代码和B站视频里的TextArea2.cs代码整合了,并且把所有按钮都换成了代码内找引用和绑定事件,就不需要每次改一下UI或者脚本还需要重新拖动UI了。
usingUnityEngine;usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine.Networking;usingNewtonsoft.Json;usingSystem.Text;usingTMPro;usingUnityEngine.UI;publicclassDeepSeekChat:MonoBehaviour{//参考链接:https://blog.csdn.net/leoysq/article/details/139547284//DeepSeek API配置privatestring apikey ="你的apiKey";//注意不要粘贴成别家大模型api了privatestring apiURL ="https://api.deepseek.com/chat/completions";// private string apiURL = "https://api.deepseek.com";//Unity UI元素privateInputField userInputField;privateText chatOutputText;privateButton sendButton;privateRectTransform maskRectTra;privateRectTransform areaRectTra;privateSlider slider;//存储对话历史privateList<Dictionary<string,string>> messages =newList<Dictionary<string,string>>();privatevoidAwake(){
userInputField = GameObject.Find("UserInputField").GetComponent<InputField>();
chatOutputText = GameObject.Find("Mask/Area/chatOutputText").GetComponent<Text>();
sendButton = GameObject.Find("SendButton").GetComponent<Button>();
maskRectTra = GameObject.Find("Mask").GetComponent<RectTransform>();
areaRectTra = GameObject.Find("Area").GetComponent<RectTransform>();
slider = GameObject.Find("Slider").GetComponent<Slider>();// 为滑动条的值改变事件添加监听器
slider.onValueChanged.AddListener(delegate{ToDrag();});}// Start is called once before the first execution of Update after the MonoBehaviour is createdvoidStart(){if(sendButton !=null){
sendButton.onClick.AddListener(OnSendButtonClicked);
Debug.Log("Click Send Button!");}//初始化系统消息
messages.Add(newDictionary<string,string>{{"role","system"},{"content","You are a helpful assistant."}});}publicvoidToDrag(){//当滑动变化时,修改Area的y轴位置//获取滑动值(一个0-1的比例)float tempSliderValue = slider.value;//获取总滑动高度(一个具体的长度值)float tempTotalHeight = areaRectTra.sizeDelta.y - maskRectTra.sizeDelta.y;//x轴不变,设置y轴的新坐标(相对父物体的)等于前两者相乘,注意位置用的锚点坐标
areaRectTra.anchoredPosition =newVector2(areaRectTra.anchoredPosition.x, tempSliderValue * tempTotalHeight);}publicvoidOnSendButtonClicked(){//用户没说话,returnstring userMessage = userInputField.text;if(string.IsNullOrEmpty(userMessage)){
Debug.Log("Empty Input!");return;}//显示响应if(chatOutputText !=null){
chatOutputText.text +="\nUser: "+ userMessage;//将AI的回复追加到聊天输出文本框中,以显示给用户。}else{
Debug.LogError("Text component not assigned.");}// 清空输入框
userInputField.text ="";//添加用户消息到对话历史
messages.Add(newDictionary<string,string>{{"role","user"},{"content", userMessage }});//调用DeepSeek APIStartCoroutine(CallDeepSeekAPI());}privateIEnumeratorCallDeepSeekAPI(){
Debug.Log("CallDeepSeekAPI!");//创建请求数据var requestData =new//定义一个匿名对象requestData{
model ="deepseek-chat",//表示使用的模型,这里是"deepseek-chat"
messages = messages,//一个变量,应该是一个包含消息的数组,用于与聊天模型交互
stream =false//一个布尔值,表示是否以流的形式接收响应,这里设置为false};string jsonData = JsonConvert.SerializeObject(requestData);//将requestData对象序列化为JSON格式的字符串jsonData。//创建UnityWebRequest 向指定的API URL发送一个POST请求,请求体为JSON格式的数据,并设置必要的请求头UnityWebRequest request =newUnityWebRequest(apiURL,"POST");//创建一个网络请求对象request,指定请求的URL和请求方法(POST)byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonData);//将序列化后的JSON数据转换为字节数组bodyRaw
request.uploadHandler =newUploadHandlerRaw(bodyRaw);//创建一个UploadHandlerRaw对象,并将其赋值给request的uploadHandler属性,这样就将JSON数据作为请求体发送
request.downloadHandler =newDownloadHandlerBuffer();//创建一个DownloadHandlerBuffer对象作为request的downloadHandler,用于接收服务器响应的数据
request.SetRequestHeader("Content-Type","application/json");//通过SetRequestHeader方法设置请求的Content-Type为application/json,表明发送的是JSON数据
request.SetRequestHeader("Authorization","Bearer "+ apikey);//同时设置Authorization请求头,包含一个API密钥apiKey,用于身份验证。//发送请求 发送一个网络请求到服务器,获取AI模型的回复,并将该回复显示在聊天界面中,同时更新对话历史。如果请求失败,则输出错误信息。yieldreturn request.SendWebRequest();//使用yield return来异步发送网络请求,而不会阻塞主线程if(request.result == UnityWebRequest.Result.Success)//检查网络请求是否成功{
Debug.Log("Success!");//解析响应var response = JsonConvert.DeserializeObject<DeepSeekResponse>(request.downloadHandler.text);//将下载的响应文本(JSON格式)反序列化为一个DeepSeekResponse类型的对象string botMessage = response.choices[0].message.content;//从反序列化后的响应对象中提取AI回复的消息内容。
Debug.Log("botMessage = "+ botMessage);//显示响应if(chatOutputText !=null){
chatOutputText.text +="\nAI: "+ botMessage;//将AI的回复追加到聊天输出文本框中,以显示给用户。}else{
Debug.LogError("TextMeshPro component not assigned.");}//添加AI消息到对话历史
messages.Add(newDictionary<string,string>{{"role","assistant"},{"content", botMessage }});// 将AI的回复添加到消息列表中}else{
Debug.LogError("Error: "+ request.error);}}// 使用这些类,Unity将能够将JSON响应反序列化为一个DeepSeekResponse对象,你可以通过DeepSeekResponse对象访问choices数组,然后访问第一个Choice对象的message属性,最后得到content字符串,即AI的回复。[System.Serializable]//标记为[System.Serializable]的公共类,表示它可以被Unity的序列化系统序列化和反序列化。publicclassDeepSeekResponse{publicChoice[] choices;//包含一个名为choices的数组,该数组应该包含多个Choice对象}[System.Serializable]publicclassChoice{publicMessage message;//包含一个名为message的属性,该属性是Message类型}[System.Serializable]publicclassMessage{publicstring content;//包含一个名为content的字符串属性,该属性用于存储消息的实际内容}}
版权归原作者 百里香酚兰 所有, 如有侵权,请联系我们删除。