OpenXR开发实战项目之VR接入ChatGPT:实现游戏AI对话系统

一、框架视图

二、关键代码

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);
        }



    }

}

三、效果展示

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容