一、框架视图
二、关键代码
ChatGptManager
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
/// <summary>
/// ChatGpt的管理器。
/// </summary>
public class ChatGptManager : MonoBehaviour
{
//构造方法私有化,防止外部new对象。
private ChatGptManager() { }
//提供一个属性给外部访问,这个属性就相当于是单例对象。
private static ChatGptManager instance;
public static ChatGptManager Instance
{
get
{
if (instance==null)
{
instance = FindObjectOfType<ChatGptManager>();
if (instance == null)
{
GameObject go = new GameObject("ChatGptManager");//创建游戏对象
instance=go.AddComponent<ChatGptManager>();//挂载脚本到游戏对象身上
DontDestroyOnLoad(go);
}
}
return instance;
}
}
//要调用ChatGpt的API的网址。
string chatGptUrl = "https://api.openai.com/v1/chat/completions";
//使用的ChatGPT的模型
string chatGptModel = "gpt-3.5-turbo";
//使用的ChatGPT的API Key
string chatGptApiKey = "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";
//AI人设的提示词
public string aiRolePrompt = "和我是青梅竹马的女孩子";
//与ChatGPT的聊天记录。
public List<PostDataBody> chatRecords = new List<PostDataBody>();
void Awake()
{
//给AI设定的人设。
chatRecords.Add(new PostDataBody("system", aiRolePrompt));
}
/// <summary>
/// 异步向ChatGPT发送消息(不连续对话)
/// </summary>
/// <param name="message">询问ChatGPT的内容</param>
/// <param name="callback">回调</param>
/// <param name="aiRole">ChatGPT要扮演的角色</param>
public void ChatDiscontinuously(string message,UnityAction<string> callback,string aiRole="")
{
//构造要发送的数据。
PostData postData = new PostData
{
//使用的ChatGPT的模型
model = chatGptModel,
//要发送的消息
messages = new List<PostDataBody>()
{
new PostDataBody("system",aiRole),
new PostDataBody("user",message)
}
};
//异步向ChatGPT发送数据。
SendPostData(postData, callback);
}
/// <summary>
/// 异步向ChatGPT发送消息(连续对话)
/// </summary>
/// <param name="message">询问ChatGPT的内容</param>
/// <param name="callback">回调</param>
public void ChatContinuously(string message,UnityAction<string> callback)
{
//缓存聊天记录
chatRecords.Add(new PostDataBody("user",message));
//构造要发送的数据。
PostData postData = new PostData
{
//使用的ChatGPT的模型
model = chatGptModel,
//要发送的消息
messages = chatRecords
};
//异步向ChatGPT发送数据。
SendPostData(postData, callback);
}
/// <summary>
/// 清空ChatGPT的聊天记录,并重新设置连续对话时,AI的人设。
/// </summary>
/// <param name="aiRolePrompt">AI的人设。我们可以用一段话来描述这个人设。</param>
public void ClearChatRecordsAndSetAiRole(string aiRolePrompt="")
{
//清空聊天记录。
chatRecords.Clear();
//给AI设定人设。
chatRecords.Add(new PostDataBody("system", aiRolePrompt));
}
public void SendPostData(PostData postData, UnityAction<string> callback)
{
StartCoroutine(SendPostDataCoroutine(postData,callback));
}
IEnumerator SendPostDataCoroutine(PostData postData, UnityAction<string> callback)
{
//创建一个UnityWebRequest类的对象用于发送网络请求。POST表示向服务器发送数据。using关键字用于在执行完这段语句之后释放这个UnityWebRequest类的对象。
using (UnityWebRequest request = new UnityWebRequest(chatGptUrl, "POST"))
{
//把传输的消息的对象转换为JSON格式的字符串。
string jsonString = JsonUtility.ToJson(postData);
//把JSON格式的字符串转换为字节数组,以便进行网络传输。
byte[] data = System.Text.Encoding.UTF8.GetBytes(jsonString);
//设置要上传到远程服务器的主体数据。
request.uploadHandler = (UploadHandler)new UploadHandlerRaw(data);
//设置从远程服务器接收到的主体数据。
request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
//设置HTTP网络请求的标头。表示这个网络请求的正文采用JSON格式进行编码。
request.SetRequestHeader("Content-Type", "application/json");
//设置HTTP网络请求的标头。这里的写法是按照OpenAI官方要求来写的。
request.SetRequestHeader("Authorization", string.Format("Bearer {0}", chatGptApiKey));
//等待ChatGPT回复。
yield return request.SendWebRequest();
//回复码是200表示成功,404表示未找到,500表示服务器内部错误。
if (request.responseCode == 200)
{
//获取ChatGPT回复的字符串,此时它是一个JSON格式的字符串。
string respondedString = request.downloadHandler.text;
//将ChatGPT回复的JSON格式的字符串转换为指定的类的对象。
RespondedData respondedMessages = JsonUtility.FromJson<RespondedData>(respondedString);
//如果ChatGPT有回复我们,则我们就挑第0条消息来显示。
if (respondedMessages != null && respondedMessages.choices.Count > 0)
{
//获取第0条消息的字符串。
string respondedMessage = respondedMessages.choices[0].message.content;
//执行回调。
callback?.Invoke(respondedMessage);
}
}
}
}
//发送给ChatGPT的数据
[Serializable]
public class PostData
{
//使用哪一个ChatGPT的模型
public string model;
//发送给ChatGPT的消息。
//如果发送的列表含有多条消息,则ChatGPT会根据上下文来回复。
public List<PostDataBody> messages;
}
[Serializable]
public class PostDataBody
{
//说话的角色
public string role;
//说话的内容
public string content;
//构造方法
public PostDataBody() { }
public PostDataBody(string role, string content)
{
this.role = role;
this.content = content;
}
}
//ChatGPT回复我们的数据
[Serializable]
public class RespondedData
{
public string id;
public string created;
public string model;
public List<RespondedChoice> choices;
}
[Serializable]
public class RespondedChoice
{
public RespondedDataBody message;
public string finish_reason;
public int index;
}
[Serializable]
public class RespondedDataBody
{
public string role;
public string content;
}
}
ChatPanel
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
using UnityEngine.UI;
/// <summary>
/// 对话面板
/// </summary>
public class ChatPanel : MonoBehaviour
{
//说话者的名字
Text speakerName;
//对话内容
Text content;
//对话框
InputField inputField;
//发送按钮
Button sendButton;
//关闭按钮
Button closeButton;
//逐字显示的协程
Coroutine verbatimCoroutine;
//逐字显示时,每两个字之间的间隔时间。
public float verbatimIntervalTime=0.1f;
//角色的名字
public string characterName="拉媞珐";
int thinkingAnimationIndex = 0;
void Awake()
{
//获取引用
speakerName = transform.Find("BG/SpeakerName").GetComponent<Text>();
content = transform.Find("BG/Content").GetComponent<Text>();
inputField = transform.Find("BG/InputField").GetComponent<InputField>();
sendButton = transform.Find("BG/SendButton").GetComponent<Button>();
closeButton = transform.Find("BG/CloseButton").GetComponent<Button>();
//添加按钮事件
sendButton.onClick.AddListener(Send);
closeButton.onClick.AddListener(Close);
}
//关闭对话面板
void Close()
{
gameObject.SetActive(false);
FirstPersonController firstPersonController = FindObjectOfType<FirstPersonController>();
if (firstPersonController!=null)
{
firstPersonController.enabled = true;
}
Cursor.lockState = CursorLockMode.Locked;
FindObjectOfType<CheckTalkTip>().isTalking = false;
}
void Send()
{
//清空上一次的对话内容。
ShowDialogue("");
//取消发送按钮的互动
sendButton.interactable = false;
//每隔0.5秒显示一次“对方正在思考中”。
InvokeRepeating("Thinking", 0.5f, 0.5f);
//与ChatGPT交互
ChatGptManager.Instance.ChatContinuously(inputField.text, (content) => {
//思考结束,取消“对方正在思考中”的显示。
CancelInvoke();
thinkingAnimationIndex = 0;
ShowDialogue(characterName, content);
sendButton.interactable = true;
});
//清空输入框
inputField.text = "";
}
void Thinking()
{
if (thinkingAnimationIndex==0)
{
content.text = "对方正在思考中.";
thinkingAnimationIndex += 1;
}else if (thinkingAnimationIndex == 1)
{
content.text = "对方正在思考中..";
thinkingAnimationIndex += 1;
}
else
{
content.text = "对方正在思考中...";
thinkingAnimationIndex = 0;
}
}
/// <summary>
/// 显示对话
/// </summary>
/// <param name="speakerName">说话者的名字</param>
/// <param name="content">对话内容</param>
public void ShowDialogue(string speakerName,string content,bool isVerbatim=true)
{
this.speakerName.text = speakerName;
if (!isVerbatim)
{
this.content.text = content;
}
else
{
//清空上一次的对话内容
this.content.text = "";
//关闭上一次的协程
if (verbatimCoroutine!=null)
StopCoroutine(verbatimCoroutine);
//开启逐字显示的协程
verbatimCoroutine=StartCoroutine(VerbatimCoroutine(content));
}
}
/// <summary>
/// 显示对话(旁白)
/// </summary>
/// <param name="content">对话内容</param>
public void ShowDialogue(string content, bool isVerbatim = true)
{
ShowDialogue("", content, isVerbatim);
}
//逐字显示对话内容
IEnumerator VerbatimCoroutine(string content)
{
//暂时等待1帧,用于跳过外部,把协程记录起来。
yield return null;
//记录当前显示到哪一个字
int letter = 0;
//开始逐字显示
while (letter<content.Length)
{
//读到一个小于号,就判断它是不是富文本的标签。
if (content[letter]=='<')
{
//截取第一个<号及其后面的内容作为子字符串
string remainingString = content.Substring(letter);
//获取子字符串中开始标签的长度。
int startTagLength= remainingString.IndexOf('>') + 1;
if (startTagLength!=0)
{
//截取<号和>号及其之间的内容,用于判断是不是开始标签。
string startTag=remainingString.Substring(0, startTagLength);
if (startTag == "<b>")
{
//结束标签。
string endTag = "</b>";
//获取结束标签的<号的索引
int endTagIndex = remainingString.Substring(startTagLength).IndexOf(endTag);
if (endTagIndex != -1)
{
//截取第一个>号及其之后的字符串
string tempString = remainingString.Substring(startTagLength);
//真正的字符串的内容
string stringContent = tempString.Substring(0, endTagIndex);
//显示文本
this.content.text += $"<b>{stringContent}</b>";
letter += startTagLength + stringContent.Length + endTag.Length;
yield return new WaitForSeconds(verbatimIntervalTime);
continue;
}
}
else if (startTag == "<i>")
{
//结束标签。
string endTag = "</i>";
//获取结束标签的<号的索引
int endTagIndex = remainingString.Substring(startTagLength).IndexOf(endTag);
if (endTagIndex != -1)
{
//截取第一个>号及其之后的字符串
string tempString = remainingString.Substring(startTagLength);
//真正的字符串的内容
string stringContent = tempString.Substring(0, endTagIndex);
//显示文本
this.content.text += $"<i>{stringContent}</i>";
letter += startTagLength + stringContent.Length + endTag.Length;
yield return new WaitForSeconds(verbatimIntervalTime);
continue;
}
}else if (startTag.StartsWith("<size")&&startTag.EndsWith(">") )
{
//结束标签。
string endTag = "</size>";
//截取=号后面的值(不包括=号,也不包括后面的>号)
string value = startTag.Substring(startTag.IndexOf('=') + 1).TrimEnd('>');
//开始标签之后的字符串
string tempString = remainingString.Substring(startTagLength);
//结束标签的<号的索引
int endTagIndex=tempString.IndexOf(endTag);
if (endTagIndex!=-1)
{
//获取标签包裹的文本内容
string stringContent = tempString.Substring(0, endTagIndex);
//显示文本
this.content.text += $"<size={value}>{stringContent}</size>";
letter += startTagLength + stringContent.Length + endTag.Length;
yield return new WaitForSeconds(verbatimIntervalTime);
continue;
}
}
else if (startTag.StartsWith("<color") && startTag.EndsWith(">"))
{
//结束标签。
string endTag = "</color>";
//截取=号后面的值(不包括=号,也不包括后面的>号)
string value = startTag.Substring(startTag.IndexOf('=') + 1).TrimEnd('>');
//开始标签之后的字符串
string tempString = remainingString.Substring(startTagLength);
//结束标签的<号的索引
int endTagIndex = tempString.IndexOf(endTag);
if (endTagIndex != -1)
{
//获取标签包裹的文本内容
string stringContent = tempString.Substring(0, endTagIndex);
//显示文本
this.content.text += $"<color={value}>{stringContent}</color>";
letter += startTagLength + stringContent.Length + endTag.Length;
yield return new WaitForSeconds(verbatimIntervalTime);
continue;
}
}
else
{
this.content.text += content[letter];
letter++;
yield return new WaitForSeconds(verbatimIntervalTime);
continue;
}
}
}
this.content.text += content[letter];
letter++;
yield return new WaitForSeconds(verbatimIntervalTime);
}
}
}
CheckTalkTip
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 检测对话的提示
/// </summary>
public class CheckTalkTip : MonoBehaviour
{
public GameObject talkTipPanel;
public GameObject chatPanel;
public KeyCode chatPanelKeyCode = KeyCode.F;
public float rayCastDistance = 5f;
RaycastHit hitInfo;
Vector3 origin;
public bool isTalking = false;
void Update()
{
//如果已经进入了对话的状态,则直接返回。
if (isTalking) return;
origin.Set(Screen.width / 2, Screen.height / 2, 0);
if (Physics.Raycast(Camera.main.ScreenPointToRay(origin),out hitInfo, rayCastDistance, 1<<6))
{
talkTipPanel.SetActive(true);
if (Input.GetKeyDown(chatPanelKeyCode))
{
//进入了对话的状态
isTalking = true;
//让NPC瞬间朝向我们玩家
hitInfo.transform.LookAt(transform);
hitInfo.transform.eulerAngles = new Vector3(0, hitInfo.transform.eulerAngles.y, hitInfo.transform.eulerAngles.z);
//禁用交谈的提示的面板
talkTipPanel.SetActive(false);
//显示对话面板
chatPanel.SetActive(true);
//把玩家的移动给禁止掉
FirstPersonController firstPersonController = FindObjectOfType<FirstPersonController>();
if (firstPersonController != null)
{
firstPersonController.enabled = false;
}
//解放鼠标的光标
Cursor.lockState = CursorLockMode.None;
//显示NPC说得话
chatPanel.GetComponent<ChatPanel>().ShowDialogue("拉媞珐","主人,你有什么吩咐吗?都可以向我提问哦~");
}
}
else
{
talkTipPanel.SetActive(false);
}
}
}
DialoguePanel
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
using UnityEngine.UI;
/// <summary>
/// 对话面板
/// </summary>
public class DialoguePanel : MonoBehaviour
{
//说话者的名字
Text speakerName;
//对话内容
Text content;
//对话框
InputField inputField;
//发送按钮
Button sendButton;
//逐字显示的协程
Coroutine verbatimCoroutine;
//逐字显示时,每两个字之间的间隔时间。
public float verbatimIntervalTime=0.1f;
void Awake()
{
//获取引用
speakerName = transform.Find("BG/SpeakerName").GetComponent<Text>();
content = transform.Find("BG/Content").GetComponent<Text>();
inputField = transform.Find("BG/InputField").GetComponent<InputField>();
sendButton = transform.Find("BG/SendButton").GetComponent<Button>();
//添加按钮事件
sendButton.onClick.AddListener(Send);
}
void Send()
{
//清空上一次的对话内容。
ShowDialogue("");
//取消发送按钮的互动
sendButton.interactable = false;
//与ChatGPT交互
ChatGptManager.Instance.ChatContinuously(inputField.text, (content) => {
ShowDialogue("<color=green>小青</color>", content);
sendButton.interactable = true;
});
//清空输入框
inputField.text = "";
}
/// <summary>
/// 显示对话
/// </summary>
/// <param name="speakerName">说话者的名字</param>
/// <param name="content">对话内容</param>
public void ShowDialogue(string speakerName,string content,bool isVerbatim=true)
{
this.speakerName.text = speakerName;
if (!isVerbatim)
{
this.content.text = content;
}
else
{
//清空上一次的对话内容
this.content.text = "";
//关闭上一次的协程
if (verbatimCoroutine!=null)
StopCoroutine(verbatimCoroutine);
//开启逐字显示的协程
verbatimCoroutine=StartCoroutine(VerbatimCoroutine(content));
}
}
/// <summary>
/// 显示对话(旁白)
/// </summary>
/// <param name="content">对话内容</param>
public void ShowDialogue(string content, bool isVerbatim = true)
{
ShowDialogue("", content, isVerbatim);
}
//逐字显示对话内容
IEnumerator VerbatimCoroutine(string content)
{
//暂时等待1帧,用于跳过外部,把协程记录起来。
yield return null;
//记录当前显示到哪一个字
int letter = 0;
//开始逐字显示
while (letter<content.Length)
{
//读到一个小于号,就判断它是不是富文本的标签。
if (content[letter]=='<')
{
//截取第一个<号及其后面的内容作为子字符串
string remainingString = content.Substring(letter);
//获取子字符串中开始标签的长度。
int startTagLength= remainingString.IndexOf('>') + 1;
if (startTagLength!=0)
{
//截取<号和>号及其之间的内容,用于判断是不是开始标签。
string startTag=remainingString.Substring(0, startTagLength);
if (startTag == "<b>")
{
//结束标签。
string endTag = "</b>";
//获取结束标签的<号的索引
int endTagIndex = remainingString.Substring(startTagLength).IndexOf(endTag);
if (endTagIndex != -1)
{
//截取第一个>号及其之后的字符串
string tempString = remainingString.Substring(startTagLength);
//真正的字符串的内容
string stringContent = tempString.Substring(0, endTagIndex);
//显示文本
this.content.text += $"<b>{stringContent}</b>";
letter += startTagLength + stringContent.Length + endTag.Length;
yield return new WaitForSeconds(verbatimIntervalTime);
continue;
}
}
else if (startTag == "<i>")
{
//结束标签。
string endTag = "</i>";
//获取结束标签的<号的索引
int endTagIndex = remainingString.Substring(startTagLength).IndexOf(endTag);
if (endTagIndex != -1)
{
//截取第一个>号及其之后的字符串
string tempString = remainingString.Substring(startTagLength);
//真正的字符串的内容
string stringContent = tempString.Substring(0, endTagIndex);
//显示文本
this.content.text += $"<i>{stringContent}</i>";
letter += startTagLength + stringContent.Length + endTag.Length;
yield return new WaitForSeconds(verbatimIntervalTime);
continue;
}
}else if (startTag.StartsWith("<size")&&startTag.EndsWith(">") )
{
//结束标签。
string endTag = "</size>";
//截取=号后面的值(不包括=号,也不包括后面的>号)
string value = startTag.Substring(startTag.IndexOf('=') + 1).TrimEnd('>');
//开始标签之后的字符串
string tempString = remainingString.Substring(startTagLength);
//结束标签的<号的索引
int endTagIndex=tempString.IndexOf(endTag);
if (endTagIndex!=-1)
{
//获取标签包裹的文本内容
string stringContent = tempString.Substring(0, endTagIndex);
//显示文本
this.content.text += $"<size={value}>{stringContent}</size>";
letter += startTagLength + stringContent.Length + endTag.Length;
yield return new WaitForSeconds(verbatimIntervalTime);
continue;
}
}
else if (startTag.StartsWith("<color") && startTag.EndsWith(">"))
{
//结束标签。
string endTag = "</color>";
//截取=号后面的值(不包括=号,也不包括后面的>号)
string value = startTag.Substring(startTag.IndexOf('=') + 1).TrimEnd('>');
//开始标签之后的字符串
string tempString = remainingString.Substring(startTagLength);
//结束标签的<号的索引
int endTagIndex = tempString.IndexOf(endTag);
if (endTagIndex != -1)
{
//获取标签包裹的文本内容
string stringContent = tempString.Substring(0, endTagIndex);
//显示文本
this.content.text += $"<color={value}>{stringContent}</color>";
letter += startTagLength + stringContent.Length + endTag.Length;
yield return new WaitForSeconds(verbatimIntervalTime);
continue;
}
}
else
{
this.content.text += content[letter];
letter++;
yield return new WaitForSeconds(verbatimIntervalTime);
continue;
}
}
}
this.content.text += content[letter];
letter++;
yield return new WaitForSeconds(verbatimIntervalTime);
}
}
}