StrangeIOC游戏编程框架

160010517611623.png
这是一个MVCS的游戏编程框架,下面是这个编程框架的框架图,在做项目的时候可以多对照这张框架图去实现模块与模块之间的对接。
ROOT:会去 启动整个StringeIOC框架
MVCS ConText:进行框架的绑定[图片上传中...(7SF3LKON@%M8FOZ%{RO]R@S.png-89eb36-1510043753510-0)]

Controller:控制逻辑层
View:视图层,通过Mediator去与Controller模块交互
Services:服务层,在这里取得数据
Models:模型层,在这里将数据存储起来

2345_image_file_copy_1.jpg
下面我们做个小案例,通过这个案例更彻底了解这个框架的执行流程
Paste_Image.png

这个小Demo是这样的:创建一个Cube,Cube上有一个text的UI显示一个随机的分数,运行的时候CUbe会不停的在频幕上随机移动,Text上的值也是随机的,当我们鼠标点击中一次Cube分数就会增加一。

运行逻辑是:开始的时候View通过Mediator向Controller请求数据,然后Controller向服务端请求数据,服务端Service返回一个随机值给Controller里面创建的请求数据的Command,Controller逻辑层再将数据传递给Mediator,Mediator再负责显示到View上,然后为了将数据保存下来Controller再将数据传给Models模型层。然后我们再创建一个点击更新数据的Command,在这个Command里面我们将模型层的数据加一,然后再将数据返回给Services.
首先我们的代码有这些,我们分为Command层Model层,Service层,View层,就是MVCS,Command负责传递命名,Model负责保存数据,Service负责与服务器交互,View负责视图的显示,

7SF3LKOS.png

首先创建ContextView负责开启整个框架,

using strange.extensions.context.impl;
//启动整个StringeIOC
public class Demo1ContextView : ContextView {
    private void Awake()
    {
        this.context = new Demo1Cntext(this);//启动StrangeIOC框架
    }
}

然后创建MVCSContext负责绑定各个事件

using strange.extensions.context.api;
using strange.extensions.context.impl;
using UnityEngine;

public class Demo1Cntext : MVCSContext {

    public Demo1Cntext(MonoBehaviour View):base(View) { }

    protected override void mapBindings()//进行绑定映射
    {
        //model    M
        injectionBinder.Bind<ScorgModel>().To<ScorgModel>().ToSingleton();
       
        //service   S
        injectionBinder.Bind<IScoreService>().To<ScireService>().ToSingleton();//ToSingleton表示这个对象只会在整个工程中生成一个


        //command    C
        commandBinder.Bind(Demo1CommandEvent.RequeestScore).To<RequestscoreCommand>();
        commandBinder.Bind(Demo1CommandEvent.UpdataScore).To<UpdataScoreCommand>();

        //mediator   V
        mediationBinder.Bind<CubeView>().To<CubeMediator>();//完成View和mediator的绑定


        //绑定开始事件  一个StartCommand  这个StartCommand会立即调用
        commandBinder.Bind(ContextEvent.START).To<StartCommand>().Once();//把哪个事件与自己的StartCommand绑定上
    }
}

首先是开始的命令

using strange.extensions.command.impl;

//开始命令
public class StartCommand : Command {


    /// <summary>
    ///重写  当这个命令被执行的时候默认会调用Execute方法
    /// </summary>
    public override void Execute()
    {
   
    }
}

接下来是视图层的脚本显示UI部分

using strange.extensions.dispatcher.eventdispatcher.api;
using strange.extensions.mediation.impl;
using UnityEngine;
using UnityEngine.UI;

public class CubeView : View {

    [Inject]
    public IEventDispatcher dispacher { get; set; }

    private Text scoreText;

    /// <summary>
    /// 做初始化
    /// </summary>
    public void Init() {
        scoreText =transform.Find("Canvas/ScoreText").GetComponent<Text>();
    }
    public void Update()
    {
        transform.Translate(new Vector3(Random.Range(-1,2), Random.Range(-1, 2), Random.Range(-1, 2))*.2f);
    }
    private void OnMouseDown()
    {
        //加分
        Debug.Log("OnMouDown");
        dispacher.Dispatch(Demo1MediatorEvent.ClickDown);
    }
    public void Updatescore(int score) {
        scoreText.text = score.ToString();
    }
}

接下来是视图层通过View层 Mediator向Command发起分数的请求

using strange.extensions.context.api;
using strange.extensions.dispatcher.eventdispatcher.api;
using strange.extensions.mediation.impl;


public class CubeMediator : Mediator {

    [Inject]//可以访问到与自身绑定的 CubeView  完成注入的意思
    public CubeView cubeView { get; set; }

    [Inject(ContextKeys.CONTEXT_DISPATCHER)]//表示全局的派发器
    public IEventDispatcher dispatcher { get; set; }



    public override void OnRegister()//注册  当属性都调用完成后就会去调用这个方法  在OnRegister里面可以访问这些属性
    {
        cubeView.Init();

        dispatcher.AddListener(Demo1MediatorEvent.ScoreChange, OnScoreChange);//监听注册方法返回分数
        cubeView.dispacher.AddListener(Demo1MediatorEvent.ClickDown,OnClickDown);

        //通过dispatcher发起请求分数的命令
        dispatcher.Dispatch(Demo1CommandEvent.RequeestScore);

        base.OnRegister();
    }
    public override void OnRemove()//当取消运行的时候会调用这个   当Mediator对应的View的视图被销毁的时候会调用OnRemove
    {

        dispatcher.RemoveListener(Demo1MediatorEvent.ScoreChange, OnScoreChange);//移除监听
        cubeView.dispacher.RemoveListener(Demo1MediatorEvent.ClickDown, OnClickDown);
    }
    //将返回的分数传递给View层
    public void OnScoreChange(IEvent evt) {
        cubeView.Updatescore((int)evt.data);
    }

    //加分
    public void OnClickDown()
    {
        dispatcher.Dispatch(Demo1CommandEvent.UpdataScore);
    }
}

下面是几个发起请求需要的枚举值,有CommandEvent、ServiceEvent、MediatorEvent。

public enum Demo1CommandEvent  {
       RequeestScore,
       UpdataScore
}

public enum Demo1ServiceEvent  {

RequestScore
}

public enum Demo1MediatorEvent
{
    ScoreChange,
    ClickDown
}

接下来就是Command层了,有请求分数和更新分数的命令,所以两个Command

using strange.extensions.command.impl;
using strange.extensions.dispatcher.eventdispatcher.api;
using UnityEngine;

public class RequestscoreCommand : EventCommand {

    [Inject]
    public IScoreService scoreService { get; set; }
    [Inject]
    public ScorgModel scoreModel { get; set; }

    //[Inject(ContextKeys.CONTEXT_DISPATCHER)]//全局的dispatcher
    //public IEventDispatcher dispacher { get; set; }

    public override void Execute()//表示命名执行的时候
    {
        Retain();//表示让这个请求先不销毁,等接收到数据后进行释放
        //添加监听器,监听OnComplete方法,第一个参数表示方法的事件枚举类型, 第二个参数表示一个方法
        scoreService.dispatcher.AddListener(Demo1ServiceEvent.RequestScore, OnComplete);

        scoreService.RequestScore("http://xx/xx/xxx");
    }

    //这个方法表示当scoreService请求分数完成后就会调用这个方法去取得数据
    private void OnComplete(IEvent evt) {//IEvent存储的就是参数

        Debug.Log("request score complete"+evt.data);
        scoreService.dispatcher.RemoveListener(Demo1ServiceEvent.RequestScore, OnComplete);//移除对OnComplete的监听
        scoreModel.score = (int)evt.data;
        dispatcher.Dispatch(Demo1MediatorEvent.ScoreChange, evt.data);

      

        Release();//释放请求,销毁当前对象
    }
}
using strange.extensions.command.impl;

public class UpdataScoreCommand : EventCommand
{
    [Inject]
    public ScorgModel ScoreModel { get; set; }
    [Inject]
    public IScoreService scoreServer { get; set; }

    public override void Execute()
    {
        ScoreModel.score++;
        scoreServer.UpdateScore("http://xx/xx", ScoreModel.score);

        dispatcher.Dispatch(Demo1MediatorEvent.ScoreChange, ScoreModel.score);
    }

}

然后就是Servicevice层了,一个是接口,一个是实现接口的类

using strange.extensions.dispatcher.eventdispatcher.api;

public interface IScoreService  {

    void RequestScore(string url);//请求分数

    void  OnReceiveScore();//收到服务器端发送的分数

    void UpdateScore(string url, int Score);//更新分数

    IEventDispatcher dispatcher { get; set; }
}

using strange.extensions.dispatcher.eventdispatcher.api;
using UnityEngine;

public class ScireService : IScoreService
{

    [Inject]
    public IEventDispatcher dispatcher { get; set; }


    public void RequestScore(string url)////请求分数
    {
        Debug.Log("Request Score from url:" + url);
        OnReceiveScore();
    }

    public void  OnReceiveScore()////收到服务器端发送的分数
    {
        int score = Random.Range(0, 100);
        dispatcher.Dispatch(Demo1ServiceEvent.RequestScore, score);//通过Demo1CommandEvent.RequeestScore这个事件将数据发送出去
    }
    public void UpdateScore(string url, int Score)//更新分数
    {
        Debug.Log("Update score to url:"+url+"new score:"+Score);
    }
}


最后就是模型层,保存分分数,数据。

public class ScorgModel  {
    public int score { get; set; }
}

这样就完成了,开始请求获取一个初始分数,往后每点击Cube一次数据就会更新一次数据+1。
1510050484(1).jpg

创建编辑器扩展,用来添加需要的音效并用AudioManager管理起来

首先创建一个Editor文件夹,里面再创建一个编辑器扩展类AudioWindowEditor用来创建一个音效添加的面板,并将音效的路径内容保存到Text文本里面存储到Resources文件夹下,代码如下

using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;

public class AudioWindowEditor : EditorWindow
{

    //创建自定义窗口
    [MenuItem("Manager/AudioManager")]
    static void CreateWindow()
    {
        AudioWindowEditor audio = EditorWindow.GetWindow<AudioWindowEditor>("音效管理");

        //Rect rect = new Rect(400, 400, 300, 300);
        //AudioWindowEditor audio = EditorWindow.GetWindowWithRect (typeof(AudioWindowEditor),rect)as AudioWindowEditor;
        audio.Show();
    }

    private string audioname;
    private string audiopath;
    private Dictionary<string, string> audioDict = new Dictionary<string, string>();//存储音效路径


    private void Awake()
    {
        LoadAudioList();
    }

    //OnGUI绘制
    private void OnGUI()
    {
        GUILayout.BeginHorizontal();
        GUILayout.Label("音效名称");
        GUILayout.FlexibleSpace();
        GUILayout.Label("音效路径");
        GUILayout.FlexibleSpace();
        GUILayout.Label("操作");
        EditorGUILayout.EndHorizontal();
        foreach (string key in audioDict.Keys)
        {
            string value;
            audioDict.TryGetValue(key, out value);
            GUILayout.BeginHorizontal();
            GUILayout.Label(key);
            GUILayout.FlexibleSpace();
            GUILayout.Label(value);
            GUILayout.FlexibleSpace();
            if (GUILayout.Button("删除", GUILayout.Width(100), GUILayout.Height(20)))
            {
                audioDict.Remove(key);
                SaveAudioList();
                return;
            }
            GUILayout.EndHorizontal();
        }

        audioname = EditorGUILayout.TextField("音效名字", audioname);
        audiopath = EditorGUILayout.TextField("音效路径", audiopath);
        if (GUILayout.Button("添加音效"))
        {
            object o = Resources.Load(audiopath);//加载音效路径
            if (o == null)
            {
                Debug.LogWarning("输入的音效不存在于:" + audiopath + ",添加不成功");
                audiopath = "";
            }
            else
            {
                if (audioDict.ContainsKey(audioname))
                {

                    Debug.LogWarning("已添加成功,请勿重复添加");
                }
                else
                {
                    audioDict.Add(audioname, audiopath);
                    SaveAudioList();
                }
            }
        }
    }
    //窗口面板被更新时调用
    private void OnInspectorUpdate()
    {
        LoadAudioList();
    }

    //存储成文本
    private void SaveAudioList()
    {
        StringBuilder sb = new StringBuilder();

        foreach (string key in audioDict.Keys)
        {
            string value;
            audioDict.TryGetValue(key, out value);
            sb.Append(key + "," + value + "\n");//组拼字符串
        }
        //AudioManager.AudioTextPath里面是创建并保存文本的路径 第二个参数是保存内容的路径
        File.WriteAllText(AudioManager.AudioTextPath, sb.ToString());//可覆盖之前内容
        //File.AppendAllText(savePath,sb.ToString());
    }
    //读取音效文本里面的内容并加载到字典里面显示到面版上
    private void LoadAudioList()
    {
        audioDict = new Dictionary<string, string>();
        if (File.Exists(AudioManager.AudioTextPath) == false) return;
        string[] lines = File.ReadAllLines(AudioManager.AudioTextPath);
        foreach (string line in lines)
        {
            if (string.IsNullOrEmpty(line)) continue;
            string[] keyvalue = line.Split(',');
            audioDict.Add(keyvalue[0], keyvalue[1]);
        }

    }
}

,这样音效添加面板创建出来了并将其内容保存到了Text文本里,然后再Scripts文件夹下创建音效管理脚本AudioManager,用来管理所有的音效,并写入播放音效的方法,代码下

using System.Collections.Generic;
using UnityEngine;

public class AudioManager
{

    private static string audioTextPathPrefix = Application.dataPath + "\\FrameworkAudio\\Resources\\";
    private const string audioTextPathMiddle = "audiolist";
    private const string audioTextPathPostfix = ".txt";


    public static string AudioTextPath
    {
        get
        {
            return audioTextPathPrefix + audioTextPathMiddle + audioTextPathPostfix;
        }
    }




    Dictionary<string, AudioClip> AudioClipDict = new Dictionary<string, AudioClip>();//存储音效

    private bool IsMult = false;//是否静音

    //public AudioManager()
    //{
    //    LoadAudioClip();
    //}

        //初始化
    public void Init()
    {

        LoadAudioClip();
    }
    //加载解析音效并保存到字典里
    private void LoadAudioClip()
    {
        AudioClipDict = new Dictionary<string, AudioClip>();
        TextAsset ta = Resources.Load<TextAsset>(audioTextPathMiddle);
        string[] lines = ta.text.Split('\n');

        foreach (string line in lines)
        {
            if (string.IsNullOrEmpty(line)) return;
            string[] keyvalue = line.Split(',');
            string key = keyvalue[0];
            AudioClip value = Resources.Load<AudioClip>(keyvalue[1]);
            AudioClipDict.Add(key, value);
        }
    }
    //播放音效
    public void playAudio(string name)
    {
        if (IsMult) return;
        AudioClip ac;
        AudioClipDict.TryGetValue(name, out ac);
        if (ac != null)
        {
            AudioSource.PlayClipAtPoint(ac, Vector3.zero);
        }
    }
    //有位置的音效播放
    public void playAudio(string name, Vector3 position)
    {
        if (IsMult) return;
        AudioClip ac;
        AudioClipDict.TryGetValue(name, out ac);
        if (ac != null)
        {
            AudioSource.PlayClipAtPoint(ac, position);
        }
    }
}

这样就可以在面板里面创建音效并保存到文本里面,然后管理到了AudioManager里面,下次需要用的时候只需要添加路径然后调用AudioManager里面的playAudio方法就可以播放音效了,非常方便


D`0DQ2.png

下面去框架里面使用下音效试下效果,首先去Demo1Cntext里面绑定我们的音效的管理类。


QERMSA.png
然后在调用开始命令StartCommand的时候去初始化就是读取解析文本里面的音效内容存储到字典里,所以在StartCommand里面写入
using strange.extensions.command.impl;

//开始命令
public class StartCommand : Command {

    [Inject]
    public AudioManager audioManager { get; set; }
    /// <summary>
    ///重写  当这个命令被执行的时候默认会调用Execute方法
    /// </summary>
    public override void Execute()
    {
        audioManager.Init();
    }
}

接下来我们就可以在每次点击的时候使用文本里面的一个音效了,在视图层CubeView的OnMouseDown方法里面,每次点击Cube的时候调用一下音效管理里面的播放方法,把需要播放的音效名字输入上去就OK了

    [Inject]
    public AudioManager audiomanger { get; set; }
  private void OnMouseDown()
    {
        //加分
        Debug.Log("OnMouDown");
        audiomanger.playAudio("Hit");
        dispacher.Dispatch(Demo1MediatorEvent.ClickDown);
    }

这样就完成了一个音效的播放

PoolManager资源池

创建资源池,就好比子弹需要不断的生成创建这样会很耗费性能,这时候我们就可以把子弹放到List集合里面,比如我们有十发子弹,在最开始的时候List会保存这十发子弹,后面这十发子弹是不会销毁的,只需要把其SetActive设置成False就行,下次再生成子弹的时候只需要去List集合里面去取得自动将其SetActive设置成True就行。这样这些子弹就会不断的循环利用。当然不止子弹,或者其他的物体需要不停的创建的就可以使用PoolManager去管理,例如一些特效、NPC等等。

下面我来做一个简单的池子,这里可以使用Unity里面的一个工具:使用定制资源配置文件
相关学习文档:http://www.360doc.com/content/14/0323/13/12282510_363016017.shtml
首先创建GameobjectPool一个资源池里面写入需要的属性,让其成为可序列化的

using System.Collections.Generic;
using UnityEngine;
using System;

/// <summary>
/// 资源池  用来单独编辑一个资源池
/// </summary>
[Serializable]//序列化  可以把这个类保存到本地文件
public class GameobjectPool  {

    [SerializeField]
    private  string name;//表示这个池子的名字
    [SerializeField]
    private  GameObject prefab;
    [SerializeField]
    private  int maxAmount;//池子最大可容纳对象

    [NonSerialized]//不需要序列化
    private List<GameObject> goList = new List<GameObject>();

}

然后再创建一个资源管理器GameobjectPoolList

using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 管理所有的资源池
/// </summary>
public class GameobjectPoolList : ScriptableObject
{//表示把GameobjectPoolList变成可以自定义资源配置的文件

    public List<GameobjectPool> poolList;
}

接着创建一个编辑器类PoolManagerEditor,让其可生成一个配置文件

using UnityEditor;
using UnityEngine;

public class PoolManagerEditor
{
    [MenuItem("Manager/Crate GameobjectPoolConfig")]
    static void CreateGameObjectPoolList()
    {
        GameobjectPoolList poolList =ScriptableObject.CreateInstance<GameobjectPoolList>();
        //创建一个资源,c参数一创建什么类型的资源,参数二 资源的路径
        AssetDatabase.CreateAsset(poolList, "Assets/FrameworkAudio/Resources/gameobjectpool.asset");//gameobjectpool.property文件名+后缀
        AssetDatabase.SaveAssets();
    }
}

这样我们只要一点击打Manager下Crate GameobjectPoolConfig,就会在Resouces下生成一个资源配置文件,里面可以管理我们所有的资源池,这里我在里面创建了两个资源池,一个子弹Bullet,一个特效HitEff,并设置了其属性


W`HBTHRD%8L.png

下面再两个Prefaba两个添加禁用的脚本DeactiveForTime,让其在三秒的时候禁用.

using UnityEngine;

public class DeactiveForTime : MonoBehaviour {

    private void OnEnable()
    {
        Invoke("Deactive", 3);//5秒后禁用 
    }
    void Deactive()
    {
        this.gameObject.SetActive(false);
    }
}

这样在生成后的三秒就会禁用。
接着创建一个PoolManager脚本去解析gameobjectpool配置文件里面属性,将里面所有的资源池放到Dictionary里面管理起来,需要用的时候只有根据PoolName就可以获取到相应的资源池了,所以在PoolManager脚本里面的代码如下

using System.Collections.Generic;
using UnityEngine;

public class PoolManager : MonoBehaviour {

    private static PoolManager _instance;
    public static PoolManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new PoolManager();
            }
            return _instance;      
        }
    }

    private static string poolConfigPathPrefix = "Assets/FrameworkAudio/Resources/";
    private const string pooConfigPathMiddle = "gameobjectpool";
    private const string pooConfigPathPostfix = ".asset";


    public static string pooConfigPath
    {
        get
        {
            return poolConfigPathPrefix + pooConfigPathMiddle + pooConfigPathPostfix;
        }
    }
    private Dictionary<string, GameobjectPool> poolDict;
    private PoolManager()
    {
        GameobjectPoolList pooList = Resources.Load<GameobjectPoolList>(pooConfigPathMiddle);//加载资源池
        poolDict = new Dictionary<string, GameobjectPool>();
        foreach (GameobjectPool pool in pooList.poolList)
        {
            poolDict.Add(pool.name,pool);//将所有资源池里面的属性存储进poolDict里面
        }
    }
    public void Init()
    {
        //Do nothing

    }
    public GameObject GetInstBullet(string poolName)
    {
        GameobjectPool pool;
        //判断有没有这个资源池
        if (poolDict.TryGetValue(poolName, out pool))
        {
            return pool.GetInstance();
        }
        Debug.LogWarning("Pool:"+poolName+"is not exits!!");
        return null;
    }

}

接下来去GameobjectPool里面写入实例化的代码,在GameobjectPool里面添加代码

using System.Collections.Generic;
using UnityEngine;
using System;

/// <summary>
/// 资源池  用来单独编辑一个资源池
/// </summary>
[Serializable]//序列化  可以把这个类保存到本地文件
public class GameobjectPool  {


    public   string name;//表示这个池子的名字
    [SerializeField]
    private  GameObject prefab;
    [SerializeField]
    private  int maxAmount;//池子最大可容纳对象


    [NonSerialized]//不需要序列化
    private List<GameObject> goList = new List<GameObject>();

    /// <summary>
    /// 表示从资源池中获取一个实例
    /// </summary>
    public GameObject GetInstance()
    {
        foreach (GameObject go in goList)
        {
            //判断里面的每个游戏对象是否启用,如果里面有没有启用的表示这个游戏对象可用,返回当前游戏对象
            if (go.activeInHierarchy == false)
            {
                go.SetActive(true);
                return go;
            }

        }
        //如果池子里的游戏对象大于或等于我们需要的最大的容量
        if (goList.Count>=maxAmount )
        {
            //从集合里面销毁一个
            GameObject.Destroy(goList[0]);
            goList.RemoveAt(0);
        }

        //如果池子里面既没有可用的并且池子里还有容量那么就需要创建新的游戏对象了
        GameObject temp=GameObject.Instantiate(prefab);
 
        goList.Add(temp);

        return temp;
    }
}

接下来我们就需要去框架里面应用一下了,只需要在游戏的视图层里面调用PoolManager里面的 GetInstBullet()方法,将需要的资源池PoolName传递上去就OK了。我这里在CubeView里面的Updata里面写入以下代码,让每点击一次就生产一个Bullet。

 if (Input.GetMouseButtonDown(0))
        {
            PoolManager.Instance.GetInstBullet("Bullet");
        }

这样就完成了所有资源池的管理。

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

推荐阅读更多精彩内容