赞
踩
GPT for unity插件地址: GitHub - sunsvip/ChatGPTForUnity: ChatGPT for unity
用法:
————————————————————————————————————
几个资本大佬花钱让一群程序员研发出了AI,砸了程序员的大锅。尤其是ChatGPT 4.0发布后,认识的同事朋友们都在恐慌AI的发展,甚至有不少人开始抗拒。我的观点是:人工智能是大势所趋,势不可挡,那就驾驭它吧!
我司已经在商业项目中实际使用了AI, 包括Stable Diffusion及其扩展插件,当然也有爆火的GPT。
Midjourney + Stable Diffusion + ControlNET + Lora以及Photoshop Stable Diffusion 等插件的结合使用已经强大到基本取代初中级原画师;
GPT 4.0势头已经咄咄逼人,据说下一代会进化到运行时处理功能,比如对磁盘的读写等。用户让“创建一个cs代码”,它就能在电脑硬盘里创建一个代码文件。个人觉得任重道远,功能越强责任越大,如果没有有效解决安全问题之前,这一步很难到来。
GPT已经能全面应用到游戏开发的各个环节了,策划的剧情设定、技术的代码编写优化、测试的测试用例等。我从去年12月开始使用,完全把它当成了一个搜索引擎,自从有了它几乎没使用多少次谷歌百度。4.0的到来彻底激发了我对ChatGPT实际应用的思考。
首先第一步肯定是先把GPT接入Unity,先建立起通讯,以此为基础各种应用功能才能百花齐放。
工具效果预览:
用UnityWebRequest实现一个与ChatGPT通讯的类,之后的各种功能基于此类。
GPT提供了开放接口,仅需一个个人版的API Key即可, API Key获取入口:https://platform.openai.com/account/api-keys
GPT URL: https://api.openai.com/v1/chat/completions
上行数据结构如下:
- {
- "messages": [
- {
- "role": "user",
- "content": "你是机器人吗"//要发送的问题
- }
- ],
- "model": "gpt-3.5-turbo",//AI数据模型
- "temperature": 0.7 //默认是1, 数值越大结果随机性越高
- }
需要注意的是,messages是个数组,如果想连续对话就需要把发送历史塞到这个messages数组,gpt根据你的发送历史分析上下文给出处理结果。gpt消耗token不是根据发送请求次数计算,聊天历史越多单次发送请求消耗的token越多,所以及时清除历史(新建话题)可以节省token消耗。
下行数据结构如下:
- {
- "id": "chatcmpl-xxxxxxxxxxxxxxxxxx",
- "object": "chat.completion",
- "created": 1678987654,
- "model": "gpt-3.5-turbo-0301",
- "usage": {
- "prompt_tokens": 14,
- "completion_tokens": 23,
- "total_tokens": 37
- },
- "choices": [
- {
- "message": {
- "role": "assistant",
- "content": "\n\n是的,我是一个AI语言模型,也可以被称为机器人。" //得到的回复
- },
- "finish_reason": "stop",
- "index": 0
- }
- ]
- }
使用UnityWebRequest发送Post请求:
- private IEnumerator Request(string input, Action<bool, string> onComplete, Action<float> onProgressUpdate)
- {
- var msg = new Message()
- {
- role = UserId,
- content = input,
- };
- requestData.AppendChat(msg);
- messageHistory.Add(msg);
-
- using (webRequest = new UnityWebRequest(ChatgptUrl, "POST"))
- {
- var jsonDt = UtilityBuiltin.Json.ToJson(requestData);
- Debug.Log(jsonDt);
- byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonDt);
- webRequest.uploadHandler = new UploadHandlerRaw(bodyRaw);
- webRequest.downloadHandler = new DownloadHandlerBuffer();
- webRequest.SetRequestHeader("Content-Type", "application/json");
- webRequest.SetRequestHeader("Authorization", $"Bearer {this.ApiKey}");
- //webRequest.certificateHandler = new ChatGPTWebRequestCert();
- var req = webRequest.SendWebRequest();
- while (!webRequest.isDone)
- {
- onProgressUpdate?.Invoke((webRequest.downloadProgress + webRequest.uploadProgress) / 2f);
- yield return null;
- }
-
- if (webRequest.result != UnityWebRequest.Result.Success)
- {
- Debug.LogError($"---------ChatGPT请求失败:{webRequest.error}---------");
- onComplete?.Invoke(false, string.Empty);
- }
- else
- {
- var json = webRequest.downloadHandler.text;
- Debug.Log(json);
- try
- {
- ChatCompletion result = UtilityBuiltin.Json.ToObject<ChatCompletion>(json);
- int lastChoiceIdx = result.choices.Count - 1;
- var replyMsg = result.choices[lastChoiceIdx].message;
- replyMsg.content = replyMsg.content.Trim();
- messageHistory.Add(replyMsg);
- onComplete?.Invoke(true, replyMsg.content);
- }
- catch (System.Exception e)
- {
- Debug.LogError($"---------ChatGPT返回数据解析失败:{e.Message}---------");
- onComplete?.Invoke(false, e.Message);
- }
- }
- webRequest.Dispose();
- webRequest = null;
- }
- }
以上就是向ChatGPT发送请求并接受回复的核心代码,非常简单。然而不出意外的话会出现请求报错:Cert verify failed: UNITYTLS_X509VERIFY_FLAG_CN_MISMATCH, https证书验证失败。
所以还需要自定义验证类,直接跳过验证返回true:
- class ChatGPTWebRequestCert : UnityEngine.Networking.CertificateHandler
- {
- protected override bool ValidateCertificate(byte[] certificateData)
- {
- //return base.ValidateCertificate(certificateData);
- return true;
- }
- }
然后为UnityWebRequest势力指定验证Handler:
webRequest.certificateHandler = new ChatGPTWebRequestCert();
再次运行就能正常接收数据了。
完整代码:
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Text;
- using System.Threading.Tasks;
- using Unity.EditorCoroutines.Editor;
- using UnityEditor;
- using UnityEngine;
- using UnityEngine.Networking;
-
- namespace UnityGameFramework.Editor.AIAssistant
- {
- public class ChatGPT
- {
- const string ChatgptUrl = "https://api.openai.com/v1/chat/completions";
- const string DefaultAPIKey = "替换自己的ChatGPT API Key";
- const string DefaultModel = "gpt-3.5-turbo";
- const float DefaultTemperature = 0;
- const string DefaultUserId = "user";
- string ApiKey;
- string UserId;
- List<Message> messageHistory;
- public List<Message> MessageHistory => messageHistory;
- ChatGPTRequestData requestData;
- UnityWebRequest webRequest;
- public float ChatGPTRandomness { get => requestData.temperature; set { requestData.temperature = Mathf.Clamp(value, 0, 2); } }
- public bool IsRequesting => webRequest != null && !webRequest.isDone;
- public float RequestProgress => IsRequesting ? (webRequest.uploadProgress + webRequest.downloadProgress) / 2f : 0f;
- public ChatGPT(string apiKey = DefaultAPIKey, string userId = DefaultUserId, string model = DefaultModel, float temperature = DefaultTemperature)
- {
- this.ApiKey = apiKey;
- this.UserId = string.IsNullOrWhiteSpace(userId) ? DefaultUserId : userId;
- messageHistory = new List<Message>();
- requestData = new ChatGPTRequestData(model, temperature);
- }
- /// <summary>
- /// 接着上次的话题
- /// </summary>
- public void RestoreChatHistory()
- {
- var chatHistoryJson = EditorPrefs.GetString("ChatGPT.Settings.ChatHistory", string.Empty);
- var requestDataJson = EditorPrefs.GetString("ChatGPT.Settings.RequestData", string.Empty);
- if (!string.IsNullOrEmpty(chatHistoryJson))
- {
- var jsonObj = UtilityBuiltin.Json.ToObject<ChatGPTRequestData>(requestDataJson);
- if (jsonObj != null)
- {
- requestData.messages = jsonObj.messages;
- }
- }
- if (!string.IsNullOrEmpty(requestDataJson))
- {
- var jsonObj = UtilityBuiltin.Json.ToObject<List<Message>>(chatHistoryJson);
- if (jsonObj != null)
- {
- messageHistory = jsonObj;
- }
- }
- }
- public void SaveChatHistory()
- {
- var chatHistoryJson = UtilityBuiltin.Json.ToJson(messageHistory);
- var requestDataJson = UtilityBuiltin.Json.ToJson(requestData);
- EditorPrefs.SetString("ChatGPT.Settings.ChatHistory", chatHistoryJson);
- EditorPrefs.SetString("ChatGPT.Settings.RequestData", requestDataJson);
- }
- public void Send(string message, Action<bool, string> onComplete = null, Action<float> onProgressUpdate = null)
- {
- EditorCoroutineUtility.StartCoroutine(Request(message, onComplete, onProgressUpdate), this);
- }
-
- public async Task<string> SendAsync(string message)
- {
- bool isCompleted = false;
- string result = string.Empty;
- Action<bool, string> onComplete = (success, str) =>
- {
- isCompleted = true;
- if (success) result = str;
- };
-
- EditorCoroutineUtility.StartCoroutine(Request(message, onComplete, null), this);
- while (!isCompleted)
- {
- await Task.Delay(10);
- }
- return result;
- }
- private IEnumerator Request(string input, Action<bool, string> onComplete, Action<float> onProgressUpdate)
- {
- var msg = new Message()
- {
- role = UserId,
- content = input,
- };
- requestData.AppendChat(msg);
- messageHistory.Add(msg);
-
- using (webRequest = new UnityWebRequest(ChatgptUrl, "POST"))
- {
- var jsonDt = UtilityBuiltin.Json.ToJson(requestData);
- Debug.Log(jsonDt);
- byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonDt);
- webRequest.uploadHandler = new UploadHandlerRaw(bodyRaw);
- webRequest.downloadHandler = new DownloadHandlerBuffer();
- webRequest.SetRequestHeader("Content-Type", "application/json");
- webRequest.SetRequestHeader("Authorization", $"Bearer {this.ApiKey}");
- webRequest.certificateHandler = new ChatGPTWebRequestCert();
- var req = webRequest.SendWebRequest();
- while (!webRequest.isDone)
- {
- onProgressUpdate?.Invoke((webRequest.downloadProgress + webRequest.uploadProgress) / 2f);
- yield return null;
- }
-
- if (webRequest.result != UnityWebRequest.Result.Success)
- {
- Debug.LogError($"---------ChatGPT请求失败:{webRequest.error}---------");
- onComplete?.Invoke(false, string.Empty);
- }
- else
- {
- var json = webRequest.downloadHandler.text;
- Debug.Log(json);
- try
- {
- ChatCompletion result = UtilityBuiltin.Json.ToObject<ChatCompletion>(json);
- int lastChoiceIdx = result.choices.Count - 1;
- var replyMsg = result.choices[lastChoiceIdx].message;
- replyMsg.content = replyMsg.content.Trim();
- messageHistory.Add(replyMsg);
- onComplete?.Invoke(true, replyMsg.content);
- }
- catch (System.Exception e)
- {
- Debug.LogError($"---------ChatGPT返回数据解析失败:{e.Message}---------");
- onComplete?.Invoke(false, e.Message);
- }
- }
- webRequest.Dispose();
- webRequest = null;
- }
- }
- public void NewChat()
- {
- requestData.ClearChat();
- messageHistory.Clear();
- }
- public bool IsSelfMessage(Message msg)
- {
- return this.UserId.CompareTo(msg.role) == 0;
- }
- }
-
- class ChatGPTRequestData
- {
- public List<Message> messages;
- public string model;
- public float temperature;
-
- public ChatGPTRequestData(string model, float temper)
- {
- this.model = model;
- this.temperature = temper;
- this.messages = new List<Message>();
- }
-
- /// <summary>
- /// 同一话题追加会话内容
- /// </summary>
- /// <param name="chatMsg"></param>
- /// <returns></returns>
- public ChatGPTRequestData AppendChat(Message msg)
- {
- this.messages.Add(msg);
- return this;
- }
- /// <summary>
- /// 清除聊天历史(结束一个话题), 相当于新建一个聊天话题
- /// </summary>
- public void ClearChat()
- {
- this.messages.Clear();
- }
- }
-
- class ChatGPTWebRequestCert : UnityEngine.Networking.CertificateHandler
- {
- protected override bool ValidateCertificate(byte[] certificateData)
- {
- //return base.ValidateCertificate(certificateData);
- return true;
- }
- }
- class Usage
- {
- public int prompt_tokens;
- public int completion_tokens;
- public int total_tokens;
- }
-
- public class Message
- {
- public string role;
- public string content;
- }
-
- class Choice
- {
- public Message message;
- public string finish_reason;
- public int index;
- }
-
- class ChatCompletion
- {
- public string id;
- public string @object;
- public int created;
- public string model;
- public Usage usage;
- public List<Choice> choices;
- }
- }
-
使用方法一, 同步获取结果:
- var ai = new ChatGPT();
- var str = await ai.SendAsync("你好");
- Debug.Log(str);
使用方法二, 异步获取结果:
new ChatGPT().Send("你好", (success, message) => { if (success) Debug.Log(message); }, requestProgress => { Debug.Log($"Request progress:{requestProgress}"); });
为什么要写个聊天窗口:
1. https://chat.openai.com/chat 网页版登陆锁IP,并且会验证时区,如果用美国的节点时区时间对不上拒绝登录。
2. 虽然登录成功后不科学也能用,但是同一话题很快就会超时无法应答,刷新界面或新建话题才能正常使用,总之非常鸡肋。
而通过开放接口就没有这些问题,API请求会更加爽快。
聊天窗口功能设计:
1. 需要一个滚动列表展示双方对话记录,对话文本内容支持选择复制。
2. 问题输入框和发送按钮、新建话题(清除话题对话)
3. 对话历史存档。
代码实现,比较简单就不解释了,直接上源码:
- using System;
- using UnityEditor;
- using UnityEngine;
-
- namespace UnityGameFramework.Editor.AIAssistant
- {
- [EditorToolMenu("AI助手/ChatGPT", 5)]
- public class ChatGPTWindow : EditorToolBase
- {
- public override string ToolName => "ChatGPT";
-
- Vector2 scrollPos = Vector2.zero;
-
- ChatGPT ai;
- private bool settingFoldout = false;
- string message;
- const string aiRoleName = "AI";
- private float chatBoxWidthRatio = 0.85f;
- private float iconSizeRatio = 0.6f;
- private float chatBoxPadding = 20;
- private float chatBoxEdgePadding = 10;
-
- GUIStyle myChatStyle;
- GUIStyle aiChatStyle;
-
- GUIStyle aiIconStyle;
- GUIStyle myIconStyle;
- GUIStyle txtAreaStyle;
-
- GUIContent chatContent;
-
- bool isEditorInitialized = false;
- private float scrollViewHeight;
-
- private void OnEnable()
- {
- EditorApplication.update += OnEditorUpdate;
- ai = new ChatGPT(AppBuildSettings.Instance.ChatGPTKey);
- ai.ChatGPTRandomness = AppBuildSettings.Instance.ChatGPTRandomness;
- chatContent = new GUIContent();
- ai.RestoreChatHistory();
- }
-
- private void OnEditorUpdate()
- {
- if (EditorApplication.isCompiling || EditorApplication.isUpdating)
- {
- return;
- }
- try
- {
- InitGUIStyles();
- isEditorInitialized = true;
- EditorApplication.update -= OnEditorUpdate;
- }
- catch (Exception)
- {
-
- }
- }
-
- private void InitGUIStyles()
- {
- aiChatStyle = new GUIStyle(EditorStyles.selectionRect);
- aiChatStyle.wordWrap = true;
- aiChatStyle.normal.textColor = Color.white;
- aiChatStyle.fontSize = 18;
- aiChatStyle.alignment = TextAnchor.MiddleLeft;
-
- myChatStyle = new GUIStyle(EditorStyles.helpBox);
- myChatStyle.wordWrap = true;
- myChatStyle.normal.textColor = Color.white;
- myChatStyle.fontSize = 18;
- myChatStyle.alignment = TextAnchor.MiddleLeft;
-
-
- txtAreaStyle = new GUIStyle(EditorStyles.textArea);
- txtAreaStyle.fontSize = 18;
-
- aiIconStyle = new GUIStyle();
- aiIconStyle.wordWrap = true;
- aiIconStyle.alignment = TextAnchor.MiddleCenter;
- aiIconStyle.fontSize = 18;
- aiIconStyle.fontStyle = FontStyle.Bold;
- aiIconStyle.normal.textColor = Color.black;
- aiIconStyle.normal.background = EditorGUIUtility.FindTexture("sv_icon_dot5_pix16_gizmo");
-
- myIconStyle = new GUIStyle(aiIconStyle);
- myIconStyle.normal.background = EditorGUIUtility.FindTexture("sv_icon_dot2_pix16_gizmo");
- }
-
- private void OnDisable()
- {
- ai.SaveChatHistory();
- }
- private void OnGUI()
- {
- if (!isEditorInitialized) return;
- EditorGUILayout.BeginVertical();
- {
- scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
- {
- scrollViewHeight = 0;
- foreach (var msg in ai.MessageHistory)
- {
- var msgRect = EditorGUILayout.BeginVertical();
- {
- EditorGUILayout.BeginHorizontal();
- {
- bool isMyMsg = ai.IsSelfMessage(msg);
- var labelStyle = isMyMsg ? myChatStyle : aiChatStyle;
- chatContent.text = msg.content;
- float chatBoxWidth = this.position.width * chatBoxWidthRatio;
- float iconSize = (this.position.width - chatBoxWidth) * iconSizeRatio;
- float chatBoxHeight = Mathf.Max(iconSize, chatBoxEdgePadding + labelStyle.CalcHeight(chatContent, chatBoxWidth - chatBoxEdgePadding));
- if (isMyMsg) { GUILayout.FlexibleSpace(); }
- else
- {
- EditorGUILayout.LabelField(aiRoleName, aiIconStyle, GUILayout.Width(iconSize), GUILayout.Height(iconSize));
- }
- EditorGUILayout.SelectableLabel(msg.content, labelStyle, GUILayout.Width(chatBoxWidth), GUILayout.Height(chatBoxHeight));
- if (!isMyMsg) { GUILayout.FlexibleSpace(); }
- else
- {
- EditorGUILayout.LabelField(msg.role, myIconStyle, GUILayout.Width(iconSize), GUILayout.Height(iconSize));
- }
- EditorGUILayout.EndHorizontal();
- }
- EditorGUILayout.EndVertical();
- }
- EditorGUILayout.Space(chatBoxPadding);
- scrollViewHeight += msgRect.height;
- }
- EditorGUILayout.EndScrollView();
- }
-
- if (ai.IsRequesting)
- {
- var barWidth = position.width * 0.8f;
- var pBarRect = new Rect((position.width - barWidth) * 0.5f, (position.height - 30f) * 0.5f, barWidth, 30f);
- EditorGUI.ProgressBar(pBarRect, ai.RequestProgress, $"请求进度:{ai.RequestProgress:P2}");
- }
- GUILayout.FlexibleSpace();
- if (settingFoldout = EditorGUILayout.Foldout(settingFoldout, "展开设置项:"))
- {
- EditorGUILayout.BeginVertical("box");
- {
- EditorGUILayout.BeginHorizontal();
- {
- EditorGUILayout.LabelField("ChatGPT API Key:", GUILayout.Width(170));
- AppBuildSettings.Instance.ChatGPTKey = EditorGUILayout.TextField(AppBuildSettings.Instance.ChatGPTKey);
- EditorGUILayout.EndHorizontal();
- }
-
- EditorGUILayout.BeginHorizontal();
- {
- EditorGUILayout.LabelField("结果随机性:", GUILayout.Width(170));
- ai.ChatGPTRandomness = AppBuildSettings.Instance.ChatGPTRandomness = EditorGUILayout.Slider(AppBuildSettings.Instance.ChatGPTRandomness, 0, 2);
- EditorGUILayout.EndHorizontal();
- }
- EditorGUILayout.EndVertical();
- }
-
- }
- //EditorGUILayout.LabelField(scrollPos.ToString());
- EditorGUILayout.BeginHorizontal();
- {
- message = EditorGUILayout.TextArea(message, txtAreaStyle, GUILayout.MinHeight(80));
-
- EditorGUI.BeginDisabledGroup(ai.IsRequesting);
- {
- if (GUILayout.Button("发送消息", GUILayout.MaxWidth(120), GUILayout.Height(80)))
- {
- if (!string.IsNullOrWhiteSpace(message))
- {
- ai.Send(message, OnChatGPTMessage);
- }
- }
- if (GUILayout.Button("新话题", GUILayout.MaxWidth(80), GUILayout.Height(80)))
- {
- ai.NewChat();
- }
- EditorGUI.EndDisabledGroup();
- }
-
- EditorGUILayout.EndHorizontal();
- }
- EditorGUILayout.EndVertical();
- }
- }
-
- private void OnChatGPTMessage(bool arg1, string arg2)
- {
- scrollPos.y = scrollViewHeight;
- if (arg1)
- {
- message = string.Empty;
- }
- Repaint();
- }
- }
- }
添加代码后,Toolbar的Tools工具栏会自动识别这个工具菜单,点击即可打开AI对话窗口:
下一步就是为GPT赋予双手,添加各种指令Handler。比如生成json文件、生成语言国际化Excel文件、修改优化代码、生成代码文件、生成Shader、一键拼UI等等。
分为三个模块:
1. 描述文本(发送给GPT)
2. 动态追加描述内容:有时需要追加一些文本数据,比如让GPT优化一段代码,需要动态追加把代码补充到描述文本。
3. 结果解析。得到目标结果,使用Handler解析GPT返回结果,达成某种功能。GPT返回的代码或json等数据都会用标签包住,通过解析标签就可以生成各种类型的文件。
最终就可以不用写程序,只写问题的描述文本,确保能从GPT得到满意答案就可以实现某项功能。想要新增新的工具,扩展新的功能只需要在文件中添加修改问题描述文本即可。
比如语言国际化,我通过问题描述让GPT从工程代码中扫描所有语言本地化函数GF.Localization.GetText()传入的国际化文本,并且把文本翻译成中文,把结果以key,value键值对保存输出json文件。最后我得到了GPT返回的诸如{"Hello":”你好“}的所有国际化文本和翻译结果。
然后我只需要解析GPT返回结果,生成语言国际化文件到工程中就完成了AI自动处理语言国际化的问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。