一百新、框架視圖
二仿贬、關(guān)鍵代碼
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
{
//構(gòu)造方法私有化系枪,防止外部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");//創(chuàng)建游戲?qū)ο? instance=go.AddComponent<ChatGptManager>();//掛載腳本到游戲?qū)ο笊砩? DontDestroyOnLoad(go);
}
}
return instance;
}
}
//要調(diào)用ChatGpt的API的網(wǎng)址宠纯。
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人設(shè)的提示詞
public string aiRolePrompt = "和我是青梅竹馬的女孩子";
//與ChatGPT的聊天記錄。
public List<PostDataBody> chatRecords = new List<PostDataBody>();
void Awake()
{
//給AI設(shè)定的人設(shè)层释。
chatRecords.Add(new PostDataBody("system", aiRolePrompt));
}
/// <summary>
/// 異步向ChatGPT發(fā)送消息(不連續(xù)對話)
/// </summary>
/// <param name="message">詢問ChatGPT的內(nèi)容</param>
/// <param name="callback">回調(diào)</param>
/// <param name="aiRole">ChatGPT要扮演的角色</param>
public void ChatDiscontinuously(string message,UnityAction<string> callback,string aiRole="")
{
//構(gòu)造要發(fā)送的數(shù)據(jù)婆瓜。
PostData postData = new PostData
{
//使用的ChatGPT的模型
model = chatGptModel,
//要發(fā)送的消息
messages = new List<PostDataBody>()
{
new PostDataBody("system",aiRole),
new PostDataBody("user",message)
}
};
//異步向ChatGPT發(fā)送數(shù)據(jù)。
SendPostData(postData, callback);
}
/// <summary>
/// 異步向ChatGPT發(fā)送消息(連續(xù)對話)
/// </summary>
/// <param name="message">詢問ChatGPT的內(nèi)容</param>
/// <param name="callback">回調(diào)</param>
public void ChatContinuously(string message,UnityAction<string> callback)
{
//緩存聊天記錄
chatRecords.Add(new PostDataBody("user",message));
//構(gòu)造要發(fā)送的數(shù)據(jù)贡羔。
PostData postData = new PostData
{
//使用的ChatGPT的模型
model = chatGptModel,
//要發(fā)送的消息
messages = chatRecords
};
//異步向ChatGPT發(fā)送數(shù)據(jù)廉白。
SendPostData(postData, callback);
}
/// <summary>
/// 清空ChatGPT的聊天記錄,并重新設(shè)置連續(xù)對話時乖寒,AI的人設(shè)猴蹂。
/// </summary>
/// <param name="aiRolePrompt">AI的人設(shè)。我們可以用一段話來描述這個人設(shè)楣嘁。</param>
public void ClearChatRecordsAndSetAiRole(string aiRolePrompt="")
{
//清空聊天記錄磅轻。
chatRecords.Clear();
//給AI設(shè)定人設(shè)。
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)
{
//創(chuàng)建一個UnityWebRequest類的對象用于發(fā)送網(wǎng)絡(luò)請求逐虚。POST表示向服務(wù)器發(fā)送數(shù)據(jù)聋溜。using關(guān)鍵字用于在執(zhí)行完這段語句之后釋放這個UnityWebRequest類的對象。
using (UnityWebRequest request = new UnityWebRequest(chatGptUrl, "POST"))
{
//把傳輸?shù)南⒌膶ο筠D(zhuǎn)換為JSON格式的字符串叭爱。
string jsonString = JsonUtility.ToJson(postData);
//把JSON格式的字符串轉(zhuǎn)換為字節(jié)數(shù)組撮躁,以便進行網(wǎng)絡(luò)傳輸。
byte[] data = System.Text.Encoding.UTF8.GetBytes(jsonString);
//設(shè)置要上傳到遠程服務(wù)器的主體數(shù)據(jù)买雾。
request.uploadHandler = (UploadHandler)new UploadHandlerRaw(data);
//設(shè)置從遠程服務(wù)器接收到的主體數(shù)據(jù)把曼。
request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
//設(shè)置HTTP網(wǎng)絡(luò)請求的標頭杨帽。表示這個網(wǎng)絡(luò)請求的正文采用JSON格式進行編碼。
request.SetRequestHeader("Content-Type", "application/json");
//設(shè)置HTTP網(wǎng)絡(luò)請求的標頭祝迂。這里的寫法是按照OpenAI官方要求來寫的睦尽。
request.SetRequestHeader("Authorization", string.Format("Bearer {0}", chatGptApiKey));
//等待ChatGPT回復(fù)。
yield return request.SendWebRequest();
//回復(fù)碼是200表示成功型雳,404表示未找到当凡,500表示服務(wù)器內(nèi)部錯誤。
if (request.responseCode == 200)
{
//獲取ChatGPT回復(fù)的字符串纠俭,此時它是一個JSON格式的字符串沿量。
string respondedString = request.downloadHandler.text;
//將ChatGPT回復(fù)的JSON格式的字符串轉(zhuǎn)換為指定的類的對象。
RespondedData respondedMessages = JsonUtility.FromJson<RespondedData>(respondedString);
//如果ChatGPT有回復(fù)我們冤荆,則我們就挑第0條消息來顯示朴则。
if (respondedMessages != null && respondedMessages.choices.Count > 0)
{
//獲取第0條消息的字符串。
string respondedMessage = respondedMessages.choices[0].message.content;
//執(zhí)行回調(diào)钓简。
callback?.Invoke(respondedMessage);
}
}
}
}
//發(fā)送給ChatGPT的數(shù)據(jù)
[Serializable]
public class PostData
{
//使用哪一個ChatGPT的模型
public string model;
//發(fā)送給ChatGPT的消息乌妒。
//如果發(fā)送的列表含有多條消息,則ChatGPT會根據(jù)上下文來回復(fù)外邓。
public List<PostDataBody> messages;
}
[Serializable]
public class PostDataBody
{
//說話的角色
public string role;
//說話的內(nèi)容
public string content;
//構(gòu)造方法
public PostDataBody() { }
public PostDataBody(string role, string content)
{
this.role = role;
this.content = content;
}
}
//ChatGPT回復(fù)我們的數(shù)據(jù)
[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;
//對話內(nèi)容
Text content;
//對話框
InputField inputField;
//發(fā)送按鈕
Button sendButton;
//關(guān)閉按鈕
Button closeButton;
//逐字顯示的協(xié)程
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);
}
//關(guān)閉對話面板
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()
{
//清空上一次的對話內(nèi)容损话。
ShowDialogue("");
//取消發(fā)送按鈕的互動
sendButton.interactable = false;
//每隔0.5秒顯示一次“對方正在思考中”侦啸。
InvokeRepeating("Thinking", 0.5f, 0.5f);
//與ChatGPT交互
ChatGptManager.Instance.ChatContinuously(inputField.text, (content) => {
//思考結(jié)束,取消“對方正在思考中”的顯示丧枪。
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">對話內(nèi)容</param>
public void ShowDialogue(string speakerName,string content,bool isVerbatim=true)
{
this.speakerName.text = speakerName;
if (!isVerbatim)
{
this.content.text = content;
}
else
{
//清空上一次的對話內(nèi)容
this.content.text = "";
//關(guān)閉上一次的協(xié)程
if (verbatimCoroutine!=null)
StopCoroutine(verbatimCoroutine);
//開啟逐字顯示的協(xié)程
verbatimCoroutine=StartCoroutine(VerbatimCoroutine(content));
}
}
/// <summary>
/// 顯示對話(旁白)
/// </summary>
/// <param name="content">對話內(nèi)容</param>
public void ShowDialogue(string content, bool isVerbatim = true)
{
ShowDialogue("", content, isVerbatim);
}
//逐字顯示對話內(nèi)容
IEnumerator VerbatimCoroutine(string content)
{
//暫時等待1幀光涂,用于跳過外部,把協(xié)程記錄起來拧烦。
yield return null;
//記錄當前顯示到哪一個字
int letter = 0;
//開始逐字顯示
while (letter<content.Length)
{
//讀到一個小于號忘闻,就判斷它是不是富文本的標簽。
if (content[letter]=='<')
{
//截取第一個<號及其后面的內(nèi)容作為子字符串
string remainingString = content.Substring(letter);
//獲取子字符串中開始標簽的長度屎篱。
int startTagLength= remainingString.IndexOf('>') + 1;
if (startTagLength!=0)
{
//截取<號和>號及其之間的內(nèi)容服赎,用于判斷是不是開始標簽。
string startTag=remainingString.Substring(0, startTagLength);
if (startTag == "<b>")
{
//結(jié)束標簽交播。
string endTag = "</b>";
//獲取結(jié)束標簽的<號的索引
int endTagIndex = remainingString.Substring(startTagLength).IndexOf(endTag);
if (endTagIndex != -1)
{
//截取第一個>號及其之后的字符串
string tempString = remainingString.Substring(startTagLength);
//真正的字符串的內(nèi)容
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>")
{
//結(jié)束標簽重虑。
string endTag = "</i>";
//獲取結(jié)束標簽的<號的索引
int endTagIndex = remainingString.Substring(startTagLength).IndexOf(endTag);
if (endTagIndex != -1)
{
//截取第一個>號及其之后的字符串
string tempString = remainingString.Substring(startTagLength);
//真正的字符串的內(nèi)容
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(">") )
{
//結(jié)束標簽。
string endTag = "</size>";
//截取=號后面的值(不包括=號秦士,也不包括后面的>號)
string value = startTag.Substring(startTag.IndexOf('=') + 1).TrimEnd('>');
//開始標簽之后的字符串
string tempString = remainingString.Substring(startTagLength);
//結(jié)束標簽的<號的索引
int endTagIndex=tempString.IndexOf(endTag);
if (endTagIndex!=-1)
{
//獲取標簽包裹的文本內(nèi)容
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(">"))
{
//結(jié)束標簽缺厉。
string endTag = "</color>";
//截取=號后面的值(不包括=號,也不包括后面的>號)
string value = startTag.Substring(startTag.IndexOf('=') + 1).TrimEnd('>');
//開始標簽之后的字符串
string tempString = remainingString.Substring(startTagLength);
//結(jié)束標簽的<號的索引
int endTagIndex = tempString.IndexOf(endTag);
if (endTagIndex != -1)
{
//獲取標簽包裹的文本內(nèi)容
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()
{
//如果已經(jīng)進入了對話的狀態(tài),則直接返回提针。
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))
{
//進入了對話的狀態(tài)
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;
//對話內(nèi)容
Text content;
//對話框
InputField inputField;
//發(fā)送按鈕
Button sendButton;
//逐字顯示的協(xié)程
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()
{
//清空上一次的對話內(nèi)容。
ShowDialogue("");
//取消發(fā)送按鈕的互動
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">對話內(nèi)容</param>
public void ShowDialogue(string speakerName,string content,bool isVerbatim=true)
{
this.speakerName.text = speakerName;
if (!isVerbatim)
{
this.content.text = content;
}
else
{
//清空上一次的對話內(nèi)容
this.content.text = "";
//關(guān)閉上一次的協(xié)程
if (verbatimCoroutine!=null)
StopCoroutine(verbatimCoroutine);
//開啟逐字顯示的協(xié)程
verbatimCoroutine=StartCoroutine(VerbatimCoroutine(content));
}
}
/// <summary>
/// 顯示對話(旁白)
/// </summary>
/// <param name="content">對話內(nèi)容</param>
public void ShowDialogue(string content, bool isVerbatim = true)
{
ShowDialogue("", content, isVerbatim);
}
//逐字顯示對話內(nèi)容
IEnumerator VerbatimCoroutine(string content)
{
//暫時等待1幀嗜价,用于跳過外部艇抠,把協(xié)程記錄起來。
yield return null;
//記錄當前顯示到哪一個字
int letter = 0;
//開始逐字顯示
while (letter<content.Length)
{
//讀到一個小于號久锥,就判斷它是不是富文本的標簽家淤。
if (content[letter]=='<')
{
//截取第一個<號及其后面的內(nèi)容作為子字符串
string remainingString = content.Substring(letter);
//獲取子字符串中開始標簽的長度。
int startTagLength= remainingString.IndexOf('>') + 1;
if (startTagLength!=0)
{
//截取<號和>號及其之間的內(nèi)容瑟由,用于判斷是不是開始標簽絮重。
string startTag=remainingString.Substring(0, startTagLength);
if (startTag == "<b>")
{
//結(jié)束標簽。
string endTag = "</b>";
//獲取結(jié)束標簽的<號的索引
int endTagIndex = remainingString.Substring(startTagLength).IndexOf(endTag);
if (endTagIndex != -1)
{
//截取第一個>號及其之后的字符串
string tempString = remainingString.Substring(startTagLength);
//真正的字符串的內(nèi)容
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>")
{
//結(jié)束標簽歹苦。
string endTag = "</i>";
//獲取結(jié)束標簽的<號的索引
int endTagIndex = remainingString.Substring(startTagLength).IndexOf(endTag);
if (endTagIndex != -1)
{
//截取第一個>號及其之后的字符串
string tempString = remainingString.Substring(startTagLength);
//真正的字符串的內(nèi)容
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(">") )
{
//結(jié)束標簽青伤。
string endTag = "</size>";
//截取=號后面的值(不包括=號,也不包括后面的>號)
string value = startTag.Substring(startTag.IndexOf('=') + 1).TrimEnd('>');
//開始標簽之后的字符串
string tempString = remainingString.Substring(startTagLength);
//結(jié)束標簽的<號的索引
int endTagIndex=tempString.IndexOf(endTag);
if (endTagIndex!=-1)
{
//獲取標簽包裹的文本內(nèi)容
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(">"))
{
//結(jié)束標簽殴瘦。
string endTag = "</color>";
//截取=號后面的值(不包括=號潮模,也不包括后面的>號)
string value = startTag.Substring(startTag.IndexOf('=') + 1).TrimEnd('>');
//開始標簽之后的字符串
string tempString = remainingString.Substring(startTagLength);
//結(jié)束標簽的<號的索引
int endTagIndex = tempString.IndexOf(endTag);
if (endTagIndex != -1)
{
//獲取標簽包裹的文本內(nèi)容
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);
}
}
}