Fog of War(FOW) Tutorial --- UE4

最近正好有實(shí)現(xiàn)戰(zhàn)爭迷霧的需求,在forum里找到了一篇教程
Fog of War tutorial by Isvulfe

Isvulfe的思路是這樣的:
1、需要一張動(dòng)態(tài)生成的貼圖。這張貼圖將會(huì)投影到游戲場景里,因此,未被探索的部分對(duì)應(yīng)的像素黑色的,探索部分是白色的。
2、利用Post-process材質(zhì)將動(dòng)態(tài)貼圖投影到場景中。一個(gè)紋素將會(huì)對(duì)應(yīng)到游戲世界里的一個(gè)面積。材質(zhì)會(huì)用到“AbsoluteWorldPosition”節(jié)點(diǎn)作為動(dòng)態(tài)貼圖的uv坐標(biāo),將貼圖投影到游戲世界的xy平面上。貼圖的顏色與場景中的顏色相乘得到輸入的顏色。場景的顏色需通過SceneTexture:SceneColor獲得。
Post-Process的順序是很重要的,作者提及這個(gè)效果的應(yīng)用要在Mapping of Tones前,否則會(huì)產(chǎn)生奇怪的效果。
3、動(dòng)態(tài)貼圖生成用C++實(shí)現(xiàn),在藍(lán)圖中完成對(duì)Post-process材質(zhì)的使用,需要用到藍(lán)圖節(jié)點(diǎn)“SetTextureParameterValue”。
4、動(dòng)態(tài)紋理生成時(shí),利用高斯blur使得黑白區(qū)域的過渡不會(huì)生硬。
5、FOW 不是每幀都計(jì)算,而是每0.25s,這樣可以減少計(jì)算開銷。
6、由于不是逐幀計(jì)算FOW,所以為了不會(huì)有明顯的跳變感,將上一次計(jì)算的貼圖和當(dāng)前計(jì)算的貼圖混合。
7、FOW的計(jì)算放在單獨(dú)的線程中。

相關(guān)聯(lián)的概念有:
FOV(filed of vision),Blur。不同的算法可以實(shí)現(xiàn)不同的效果,效率也各不相同。
可以參考:
http://www.roguebasin.com/index.php?title=FOV


接下來是具體實(shí)現(xiàn):
首先提一句,因?yàn)榭创a的時(shí)候,你可能會(huì)想問為什么要給一堆TArray變量加UPROPERTY(),明明這些變量有的都沒暴露到Editor中,這是因?yàn)閁E的內(nèi)存管理,TArray必須加UPROPERTY(),否則會(huì)導(dǎo)致內(nèi)存管理出錯(cuò)。

FogOfWarManager.h

UCLASS() 
class RPGTEST_API AFogOfWarManager : public AActor
{
    GENERATED_BODY()    
    AFogOfWarManager(const FObjectInitializer & FOI);
    virtual ~AFogOfWarManager();
    virtual void BeginPlay() override;          
    virtual void Tick(float DeltaSeconds) override;
public: 
    //Triggers a update in the blueprint
    // 在藍(lán)圖中實(shí)現(xiàn)和調(diào)用,觸發(fā)一次紋理的更新
//感覺用BlueprintImplementableEvent就行,因?yàn)閏++版并沒有寫實(shí)現(xiàn)
    UFUNCTION(BlueprintNativeEvent)
    void OnFowTextureUpdated(UTexture2D* currentTexture, UTexture2D* lastTexture);
    
    //Register an actor to influence the FOW-texture
    // 注冊(cè)一個(gè)會(huì)影響FOW紋理的角色
    void RegisterFowActor(AActor* Actor);

    //Stolen from https://wiki.unrealengine.com/Dynamic_Textures
      //從unreal wiki上偷來的動(dòng)態(tài)生成紋理的算法,好像4.17版本后,引擎自帶了UTexture2D::UpdateTextureRegions可以不用在自己寫
    void UpdateTextureRegions(
        UTexture2D* Texture, 
        int32 MipIndex, 
        uint32 NumRegions, 
        FUpdateTextureRegion2D* Regions, 
        uint32 SrcPitch, 
        uint32 SrcBpp, 
        uint8* SrcData, 
        bool bFreeData);

    //How far will an actor be able to see
    //CONSIDER: Place it on the actors to allow for individual sight-radius
// 一個(gè)Actor的可視范圍,可以考慮將這個(gè)屬性放到Actor中,這樣就可以配置不同的可視范圍
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FogOfWar)
    float SightRange = 9.0f;

    //The number of samples per 100 unreal units
  //每100個(gè)單位,即1米需要幾個(gè)采樣
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FogOfWar)
    float SamplesPerMeter = 2.0f;

    //If the last texture blending is done
//是否完成了與上一個(gè)紋理的混合,因?yàn)榛旌喜皇窃谥骶€程中完成的
    UPROPERTY(BlueprintReadWrite)
    bool bIsDoneBlending;

    //Should we blur? It takes up quite a lot of CPU time...
//是否要用Blur,使得紋理邊緣不那么生硬,需要較大的CPU開銷
    UPROPERTY(EditAnywhere)
    bool bIsBlurEnabled = true;

    //The size of our textures
//動(dòng)態(tài)紋理的大小,1024其實(shí)蠻大了,需要大量計(jì)算,可以根據(jù)需要減小為512、256、128
    uint32 TextureSize = 1024;

    //Array containing what parts of the map we've unveiled.
//數(shù)組存儲(chǔ)地圖中哪些部分被探索了
    UPROPERTY()
    TArray<bool> UnfoggedData;

    //Temp array for horizontal blur pass
//水平方向blur后的暫時(shí)紋理副本
    UPROPERTY()
    TArray<uint8> HorizontalBlurData;

    //Our texture data (result of vertical blur pass)
//blur后的最終產(chǎn)出紋理
    UPROPERTY()
    TArray<FColor> TextureData;

    //Our texture data from the last frame
// 上一次計(jì)算的紋理
    UPROPERTY()
    TArray<FColor> LastFrameTextureData;

    //Check to see if we have a new FOW-texture.
//是否生成了新的FOW紋理,同樣是因?yàn)榧y理的產(chǎn)生是在另一個(gè)線程里
    bool bHasFOWTextureUpdate = false;

    //Blur size
//blur算法的kernel大小
    uint8 blurKernelSize = 15;

    //Blur kernel
//blur算法的kernel
    UPROPERTY()
    TArray<float> blurKernel;

    //Store the actors that will be unveiling the FOW-texture.
//保存會(huì)影響 FOW紋理的Actor 
    UPROPERTY()
    TArray<AActor*> FowActors;

    //DEBUG: Time it took to update the fow texture
//用來記錄Update FOW紋理需要的時(shí)間
    float fowUpdateTime = 0;

    //Getter for the working thread
// 供紋理生成線程調(diào)用的Getter函數(shù)
    bool GetIsBlurEnabled();
        
private:    
//好像都沒在cpp中實(shí)現(xiàn),這里作者也沒寫備注
    void UpdateFowTexture();
    
    //Triggers the start of a new FOW-texture-update
//觸發(fā)開始新的FOW紋理的更新
    void StartFOWTextureUpdate();   
    
    //Our dynamically updated texture
//我們的動(dòng)態(tài)更新紋理
    UPROPERTY()
    UTexture2D* FOWTexture;

    //Texture from last update. We blend between the two to do a smooth unveiling of newly discovered areas.
//上一次更新的FOW紋理,用來blend
    UPROPERTY()
    UTexture2D* LastFOWTexture; 
    
    //Texture regions   
    FUpdateTextureRegion2D* textureRegions; 

    //Our fowupdatethread   
// 我們辛勤工作的fow計(jì)算線程   
    AFogOfWarWorker* FowThread;
};

FogOfWarManager.cpp

AFogOfWarManager::AFogOfWarManager(const FObjectInitializer &FOI) : Super(FOI) {    
    PrimaryActorTick.bCanEverTick = true;

// 我們用來動(dòng)態(tài)生成紋理的結(jié)構(gòu)體,這里用了new,我看析構(gòu)函數(shù)里也沒delete。。。。推薦還是用TSharePtr之類來進(jìn)行內(nèi)存管理,雖然我還沒怎么用過
    textureRegions = new FUpdateTextureRegion2D(0, 0, 0, 0, TextureSize, TextureSize);      
    
    //15 Gaussian samples. Sigma is 2.0.
    //CONSIDER: Calculate the kernel instead, more flexibility...
//高斯采樣的kernel,還是用計(jì)算來的方便,這里是直接寫了
    blurKernel.Init(0.0f, blurKernelSize);
    blurKernel[0] = 0.000489f;
    blurKernel[1] = 0.002403f;
    blurKernel[2] = 0.009246f;
    blurKernel[3] = 0.02784f;
    blurKernel[4] = 0.065602f;
    blurKernel[5] = 0.120999f;
    blurKernel[6] = 0.174697f;
    blurKernel[7] = 0.197448f;
    blurKernel[8] = 0.174697f;
    blurKernel[9] = 0.120999f;
    blurKernel[10] = 0.065602f;
    blurKernel[11] = 0.02784f;
    blurKernel[12] = 0.009246f;
    blurKernel[13] = 0.002403f;
    blurKernel[14] = 0.000489f;
}

AFogOfWarManager::~AFogOfWarManager() {
//關(guān)閉線程
    if (FowThread) {        
        FowThread->ShutDown();
    }
}

void AFogOfWarManager::BeginPlay() {    
    Super::BeginPlay();
    bIsDoneBlending = true;
//為FOW紋理計(jì)算做初始化
    AFogOfWarManager::StartFOWTextureUpdate();
}

void AFogOfWarManager::Tick(float DeltaSeconds) {
    Super::Tick(DeltaSeconds);  
//判斷工作線程是否計(jì)算完了FOW紋理并完成了Blend
//如果完成了,則更新data到紋理上,并觸發(fā)藍(lán)圖中的OnFowTextureUpdated
    if (FOWTexture && LastFOWTexture && bHasFOWTextureUpdate && bIsDoneBlending) {      
        LastFOWTexture->UpdateResource();
        UpdateTextureRegions(LastFOWTexture, (int32)0, (uint32)1, textureRegions, (uint32)(4 * TextureSize), (uint32)4, (uint8*)LastFrameTextureData.GetData(), false);     
        FOWTexture->UpdateResource();
        UpdateTextureRegions(FOWTexture, (int32)0, (uint32)1, textureRegions, (uint32)(4 * TextureSize), (uint32)4, (uint8*)TextureData.GetData(), false);      
        bHasFOWTextureUpdate = false;
        bIsDoneBlending = false;
        //Trigger the blueprint update
        OnFowTextureUpdated(FOWTexture, LastFOWTexture);        
    }
}

//初始化紋理,數(shù)組,工作線程
void AFogOfWarManager::StartFOWTextureUpdate() {    
    if (!FOWTexture) {
        FOWTexture = UTexture2D::CreateTransient(TextureSize, TextureSize);
        LastFOWTexture = UTexture2D::CreateTransient(TextureSize, TextureSize);
        int arraySize = TextureSize * TextureSize;
        TextureData.Init(FColor(0, 0, 0, 255), arraySize);
        LastFrameTextureData.Init(FColor(0, 0, 0, 255), arraySize);
        HorizontalBlurData.Init(0, arraySize);
        UnfoggedData.Init(false, arraySize);    
//應(yīng)該也要delete吧   
        FowThread = new AFogOfWarWorker(this);
    }   
}

void AFogOfWarManager::OnFowTextureUpdated_Implementation(UTexture2D* currentTexture, UTexture2D* lastTexture) {
    //Handle in blueprint
}

//添加actor進(jìn)數(shù)組
void AFogOfWarManager::RegisterFowActor(AActor* Actor) {
    FowActors.Add(Actor);
}
//Getter函數(shù)供工作線程使用
bool AFogOfWarManager::GetIsBlurEnabled() {
    return bIsBlurEnabled;
}

//4.17以后的版本提供了,不用自己寫啦!!雖然也沒看懂,知道是將data數(shù)組中的數(shù)據(jù)寫進(jìn)Texture中就好了。
void AFogOfWarManager::UpdateTextureRegions(UTexture2D* Texture, int32 MipIndex, uint32 NumRegions, FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, bool bFreeData)
{
    if (Texture && Texture->Resource)
    {
        struct FUpdateTextureRegionsData
        {
            FTexture2DResource* Texture2DResource;
            int32 MipIndex;
            uint32 NumRegions;
            FUpdateTextureRegion2D* Regions;
            uint32 SrcPitch;
            uint32 SrcBpp;
            uint8* SrcData;
        };

        FUpdateTextureRegionsData* RegionData = new FUpdateTextureRegionsData;

        RegionData->Texture2DResource = (FTexture2DResource*)Texture->Resource;
        RegionData->MipIndex = MipIndex;
        RegionData->NumRegions = NumRegions;
        RegionData->Regions = Regions;
        RegionData->SrcPitch = SrcPitch;
        RegionData->SrcBpp = SrcBpp;
        RegionData->SrcData = SrcData;

        ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(
            UpdateTextureRegionsData,
            FUpdateTextureRegionsData*, RegionData, RegionData,
            bool, bFreeData, bFreeData,
            {
                for (uint32 RegionIndex = 0; RegionIndex < RegionData->NumRegions; ++RegionIndex)
                {
                    int32 CurrentFirstMip = RegionData->Texture2DResource->GetCurrentFirstMip();
                    if (RegionData->MipIndex >= CurrentFirstMip)
                    {
                        RHIUpdateTexture2D(
                            RegionData->Texture2DResource->GetTexture2DRHI(),
                            RegionData->MipIndex - CurrentFirstMip,
                            RegionData->Regions[RegionIndex],
                            RegionData->SrcPitch,
                            RegionData->SrcData
                            + RegionData->Regions[RegionIndex].SrcY * RegionData->SrcPitch
                            + RegionData->Regions[RegionIndex].SrcX * RegionData->SrcBpp
                            );
                    }
                }
                if (bFreeData)
                {
                    FMemory::Free(RegionData->Regions);
                    FMemory::Free(RegionData->SrcData);
                }
                delete RegionData;
            });
    }
}

來看看我們的工作線程:
FogOfWarWorker.h

/**
 * Worker thread for updating the fog of war data.
 */
class AFogOfWarManager;

class AFogOfWarWorker : public FRunnable
{   
    //Thread to run the FRunnable on
    FRunnableThread* Thread;    

    //Pointer to our manager
    AFogOfWarManager* Manager;

    //Thread safe counter 
    FThreadSafeCounter StopTaskCounter; 

public:
    AFogOfWarWorker();
    AFogOfWarWorker(AFogOfWarManager* manager);
    virtual ~AFogOfWarWorker();

    //FRunnable interface
    virtual bool Init();
    virtual uint32 Run();
    virtual void Stop();        

    //Method to perform work
    void UpdateFowTexture();

    bool bShouldUpdate = false;

    void ShutDown();
};

AFogOfWarWorker::AFogOfWarWorker() {}

AFogOfWarWorker::AFogOfWarWorker(AFogOfWarManager* manager){
    Manager = manager;
//創(chuàng)建線程
    Thread = FRunnableThread::Create(this, TEXT("AFogOfWarWorker"), 0U, TPri_BelowNormal);
}

AFogOfWarWorker::~AFogOfWarWorker() {
//銷毀線程
    delete Thread;
    Thread = NULL; 
}

void AFogOfWarWorker::ShutDown() {
    Stop();
    Thread->WaitForCompletion();    
}

bool AFogOfWarWorker::Init() {
    if (Manager) {
        Manager->GetWorld()->GetFirstPlayerController()->ClientMessage("Fog of War worker thread started");
        return true;
    }
    return false;
}

uint32 AFogOfWarWorker::Run() {
//盲猜這個(gè)時(shí)間是用來等manager的初始化
    FPlatformProcess::Sleep(0.03f);
    while (StopTaskCounter.GetValue() == 0) {
        float time;
        if (Manager && Manager->GetWorld()) {
            time = Manager->GetWorld()->TimeSeconds;
        }
        if (!Manager->bHasFOWTextureUpdate) {
            UpdateFowTexture();
            if (Manager && Manager->GetWorld()) {
                Manager->fowUpdateTime = Manager->GetWorld()->TimeSince(time);
            }
        }
        FPlatformProcess::Sleep(0.1f);
    }
    return 0;
}
// 功能的核心
void AFogOfWarWorker::UpdateFowTexture() {

    Manager->LastFrameTextureData = TArray<FColor>(Manager->TextureData);
    uint32 halfTextureSize = Manager->TextureSize / 2;
    int signedSize = (int)Manager->TextureSize; //For convenience....
    TSet<FVector2D> currentlyInSight;
    TSet<FVector2D> texelsToBlur;
    int sightTexels = Manager->SightRange * Manager->SamplesPerMeter;
    float dividend = 100.0f / Manager->SamplesPerMeter; 

//逐個(gè)Actor進(jìn)行循環(huán)
    for (auto Itr(Manager->FowActors.CreateIterator()); Itr; Itr++) {
        //Find actor position
        if(!*Itr) return;       
        FVector position = (*Itr)->GetActorLocation();      
        
        //We divide by 100.0 because 1 texel equals 1 meter of visibility-data.
              //  將actor的世界坐標(biāo)轉(zhuǎn)移到紋理坐標(biāo),世界的xy平面(0,0)-->紋理中心(halfTextureSize ,halfTextureSize)
        int posX = (int)(position.X / dividend) + halfTextureSize;
        int posY = (int)(position.Y / dividend) + halfTextureSize;
        float integerX, integerY;

        FVector2D fractions = FVector2D(modf(position.X / 50.0f, &integerX), modf(position.Y / 50.0f, &integerY));
        FVector2D textureSpacePos = FVector2D(posX, posY);
        int size = (int)Manager->TextureSize;
// Collision Query
        FCollisionQueryParams queryParams(FName(TEXT("FOW trace")), false, (*Itr));
        int halfKernelSize = (Manager->blurKernelSize - 1) / 2;
        
        //Store the positions we want to blur
//需要blur的紋素
        for (int y = posY - sightTexels - halfKernelSize; y <= posY + sightTexels + halfKernelSize; y++) {
            for (int x = posX - sightTexels - halfKernelSize; x <= posX + sightTexels + halfKernelSize; x++) {
                if (x > 0 && x < size && y > 0 && y < size) {
                    texelsToBlur.Add(FIntPoint(x, y));
                }
            }
        }
         

// FOV & Blur
                
    //FOV
        //Unveil the positions our actors are currently looking at
        for (int y = posY - sightTexels; y <= posY + sightTexels; y++) {
            for (int x = posX - sightTexels; x <= posX + sightTexels; x++) {
                //Kernel for radial sight
                if (x > 0 && x < size && y > 0 && y < size) {
                    FVector2D currentTextureSpacePos = FVector2D(x, y);
                    int length = (int)(textureSpacePos - currentTextureSpacePos).Size();
                    if (length <= sightTexels) {
                        FVector currentWorldSpacePos = FVector(
                            ((x - (int)halfTextureSize)) * dividend,
                            ((y - (int)halfTextureSize)) * dividend,
                            position.Z);

                        //CONSIDER: This is NOT the most efficient way to do conditional unfogging. With long view distances and/or a lot of actors affecting the FOW-data
                        //it would be preferrable to not trace against all the boundary points and internal texels/positions of the circle, but create and cache "rasterizations" of
                        //viewing circles (using Bresenham's midpoint circle algorithm) for the needed sightranges, shift the circles to the actor's location
                        //and just trace against the boundaries. 
                        //We would then use Manager->GetWorld()->LineTraceSingle() and find the first collision texel. Having found the nearest collision
                        //for every ray we would unveil all the points between the collision and origo using Bresenham's Line-drawing algorithm.
                        //However, the tracing doesn't seem like it takes much time at all (~0.02ms with four actors tracing circles of 18 texels each),
                        //it's the blurring that chews CPU..
                        if (!Manager->GetWorld()->LineTraceTest(position, currentWorldSpacePos, ECC_WorldStatic, queryParams)) {                            
                            //Unveil the positions we are currently seeing
                            Manager->UnfoggedData[x + y * Manager->TextureSize] = true;
                            //Store the positions we are currently seeing.
                            currentlyInSight.Add(FVector2D(x, y));
                        }
                    }
                }
            }
        }
    }   

//Blur
    if (Manager->GetIsBlurEnabled()) {
        //Horizontal blur pass
        int offset = floorf(Manager->blurKernelSize / 2.0f);         
        for (auto Itr(texelsToBlur.CreateIterator()); Itr; ++Itr) {
            int x = (Itr)->IntPoint().X;
            int y = (Itr)->IntPoint().Y;
            float sum = 0;
            for (int i = 0; i < Manager->blurKernelSize; i++) {
                int shiftedIndex = i - offset;
                if (x + shiftedIndex >= 0 && x + shiftedIndex <= signedSize - 1) {
                    if (Manager->UnfoggedData[x + shiftedIndex + (y * signedSize)]) {
                        //If we are currently looking at a position, unveil it completely
                        if (currentlyInSight.Contains(FVector2D(x + shiftedIndex, y))) {
                            sum += (Manager->blurKernel[i] * 255);
                        }
                        //If this is a previously discovered position that we're not currently looking at, put it into a "shroud of darkness".                          
                        else {
                            sum += (Manager->blurKernel[i] * 100);
                        }
                    }
                }
            }
            Manager->HorizontalBlurData[x + y * signedSize] = (uint8)sum;
        }
        

        //Vertical blur pass
        for (auto Itr(texelsToBlur.CreateIterator()); Itr; ++Itr) {
            int x = (Itr)->IntPoint().X;
            int y = (Itr)->IntPoint().Y;
            float sum = 0;
            for (int i = 0; i < Manager->blurKernelSize; i++) {
                int shiftedIndex = i - offset;
                if (y + shiftedIndex >= 0 && y + shiftedIndex <= signedSize - 1) {
                    sum += (Manager->blurKernel[i] * Manager->HorizontalBlurData[x + (y + shiftedIndex) * signedSize]);
                }
            }
            Manager->TextureData[x + y * signedSize] = FColor((uint8)sum, (uint8)sum, (uint8)sum, 255);
        }       
    }
    else {
        for (int y = 0; y < signedSize; y++) {
            for (int x = 0; x < signedSize; x++) {

                if (Manager->UnfoggedData[x + (y * signedSize)]) {
                    if (currentlyInSight.Contains(FVector2D(x, y))) {
                        Manager->TextureData[x + y * signedSize] = FColor((uint8)255, (uint8)255, (uint8)255, 255);
                    }
                    else {
                        Manager->TextureData[x + y * signedSize] = FColor((uint8)100, (uint8)100, (uint8)100, 255);
                    }
                }
            }
        }
    }
    Manager->bHasFOWTextureUpdate = true;
}

void AFogOfWarWorker::Stop() {
    StopTaskCounter.Increment();
}

還有部分藍(lán)圖實(shí)現(xiàn),請(qǐng)點(diǎn)擊鏈接查看原文,有大圖。
Fog of War tutorial by Isvulfe
第10頁Juancahf對(duì)代碼做了修改并post出了他的工作;第11頁有他的ue工程(UE 4.21版本)供下載。

正如原作者所說的一樣,這不是一種最好的實(shí)現(xiàn)戰(zhàn)爭迷霧的方式,僅僅是提供了一種實(shí)現(xiàn)的思路。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,477評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,617評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,807評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,049評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評(píng)論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,432評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,769評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容