MapKit框架详细解析(十一) —— 自定义MapKit Tiles(一)

版本记录

版本号 时间
V1.0 2020.06.19 星期五

前言

MapKit框架直接从您的应用界面显示地图或卫星图像,调出兴趣点,并确定地图坐标的地标信息。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. MapKit框架详细解析(一) —— 基本概览(一)
2. MapKit框架详细解析(二) —— 基本使用简单示例(一)
3. MapKit框架详细解析(三) —— 基本使用简单示例(二)
4. MapKit框架详细解析(四) —— 一个叠加视图相关的简单示例(一)
5. MapKit框架详细解析(五) —— 一个叠加视图相关的简单示例(二)
6. MapKit框架详细解析(六) —— 添加自定义图块(一)
7. MapKit框架详细解析(七) —— 添加自定义图块(二)
8. MapKit框架详细解析(八) —— 添加自定义图块(三)
9. MapKit框架详细解析(九) —— 地图特定区域放大和创建自定义地图annotations(一)
10. MapKit框架详细解析(十) —— 地图特定区域放大和创建自定义地图annotations(二)

开始

首先看下主要内容:

在此自定义MapKit tiles教程中,您将学习如何通过将精美的自定义tiles添加到冒险游戏中来修改默认的MapKit tiles。本文来自翻译

下面看下写作环境:

Swift 5, iOS 13, Xcode 11

地图在现代应用程序中无处不在。它们提供附近兴趣点的位置,帮助用户导航城镇或公园,找到附近的朋友,跟踪旅程的进度或为增强现实游戏提供上下文。

不幸的是,这意味着大多数地图在应用程序之间看起来都是相同的。

本教程介绍如何在应用程序中包括手绘地图,而不是像《PokémonGO》所使用的那样以编程方式生成的地图。您将学习如何:

  • 用另一组tiles替换现有的MapKit tiles
  • 创建自己的tiles以显示在地图上。
  • 将自定义叠加层(overlays)添加到您的地图。

您将通过构建基于位置的冒险游戏来了解这一点。沿着梦幻Central Park漫步,您会遇到可怕的野兽,它们会在荣耀之路中被击败!

手绘地图需要花费大量精力。考虑到planet的大小,这仅适用于定义明确的地理区域。如果您要在地图上定义一个明确的区域,则自定义地图可能会给您的应用程序增加很多麻烦。

打开起始项目,并看一下工程文件。

MapQuest是一个有趣的冒险游戏的开始。英雄在现实生活中在纽约中央公园(Central Park, NYC)附近奔跑,但在另一现实世界中冒险冒险,与怪物搏斗并收集宝藏。它采用可爱幼稚的设计,可让玩家感到舒适并表明游戏并不那么严肃。

游戏具有几个兴趣点(Points of Interest),这些兴趣点定义了玩家可以与游戏进行交互的位置。这些可以是任务,怪物,商店或其他游戏元素。进入兴趣点周围的10米区域开始相遇。就本教程而言,游戏性并不像学习如何渲染地图那么重要。

该项目中有两个繁重的文件:

  • MapViewController.swift:管理地图视图并处理用户交互逻辑和状态更改。
  • Game.swift:包含游戏逻辑并管理某些游戏对象的坐标。

游戏的主视图是MKMapViewMapKit使用各种缩放级别的图块(tiles)来填充其视图并提供有关地理特征,道路等的信息。

地图视图可以显示传统路线图或卫星图像。这对于在城市中导航很有帮助,但对于想像您正在探索中世纪世界却毫无用处。但是,MapKit允许您提供自己的地图艺术以自定义其提供的信息。

地图视图由许多图块(tiles)组成,这些图块在您平移视图时会动态加载。这些图块为256 x 256像素,并排列在与墨卡托地图投影(Mercator map projection)相对应的网格中。

要查看运行中的地图,请构建并运行该应用程序。

哇! 多么美丽的小镇。 游戏的主要界面是位置信息,这意味着您不去中央公园就不会看到任何东西。 不过,请放心,您现在不需要购买票。


Testing Location

与其他教程不同,MapQuest是功能强大的应用程序! 但是,除非您居住在纽约市,否则您将无法使用该应用程序做很多事情。 幸运的是,Xcode至少提供了两种方法来解决此问题。

1. Simulating a Location

在该应用程序仍在iPhone Simulator中运行的情况下,通过转到Features ▸ Location ▸ Custom Location…并将用户的Latitude设置为40.767769并将Longitude设置为-73.971870来设置用户的位置。

这将激活蓝色的用户位置点,并将地图聚焦在中央公园动物园上。 狂野的妖精住在这里。 您将与之抗争,然后收集其宝藏。

打败无助的goblin后,该应用程序会将您放置在动物园中。 注意蓝点。

2. Simulating an Adventure

静态位置对于测试许多基于位置的应用程序很有用。 但是,此游戏需要冒险前往多个地点。 该模拟器可以模拟跑步,骑自行车和开车等地点的变化。 这些预包含的旅行是针对Cupertino的,但MapQuest仅在纽约遇到。

诸如此类的情况要求使用GPXGPS交换格式(GPS Exchange Format)文件来模拟位置。 该文件指定了航路点,模拟器将在它们之间插入一条路线。

创建此文件不在本教程的讨论范围内,但是示例项目为您提供了一个测试GPX文件。

通过选择Product ▸ Scheme ▸ Edit Scheme…,在Xcode中打开scheme编辑器。

在左窗格中选择Run,然后在右侧选择Options选项卡。 在Core Location部分中,单击Allow Location Simulation复选框。 在Default Location下拉列表中,选择Game Test

现在,该应用程序将模拟Game Test.gpx中指定的航点之间的移动。

构建并运行。

模拟器将让您的角色从第五大道地铁步行至中央公园动物园,在那儿您必须再次与妖精战斗。 之后,您可以在自己喜欢的水果公司的旗舰店购买升级版的剑。 完成后,冒险将重新开始。

现在英雄已经沿着地图走了,您可以开始添加自定义MapKit tiles


Replacing the Tiles With OpenStreetMap

OpenStreetMap是社区支持的地图数据开放数据库。 您可以使用该数据生成Apple Maps使用的地图图块(tiles)OpenStreetMap社区不仅提供基本的路线图,还提供用于地形,自行车和艺术渲染的专业地图。

注意:OpenStreetMap tile policy对数据使用,归属和API访问有严格的要求。 在生产应用中使用tiles之前,请检查是否合规。

1. Creating a New Overlay

要替换地图图块(map tiles),您需要使用MKTileOverlay在默认的Apple Maps顶部显示新的图块(tiles)

打开MapViewController.swift并将setupTileRenderer()替换为以下内容:

private func setupTileRenderer() {
  // 1
  let template = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"

  // 2
  let overlay = MKTileOverlay(urlTemplate: template)

  // 3
  overlay.canReplaceMapContent = true

  // 4
  mapView.addOverlay(overlay, level: .aboveLabels)

  //5
  tileRenderer = MKTileOverlayRenderer(tileOverlay: overlay)
}

默认情况下,MKTileOverlay支持使用示例中的URL加载tile path来加载tiles

上面的代码是这样的:

  • 1) 首先,您声明一个URL模板以从OpenStreetMapAPI中获取tile。您可以在运行时用各个图块(tile)的坐标替换{x},{y}{z}。用户在地图上放大了多少确定了z坐标或缩放级别坐标。 x和y是您要显示的地球部分的图块索引。您需要为您支持的每个缩放级别的每个x和y坐标提供一个图块。
  • 2) 接下来,创建叠加层(overlay)
  • 3) 然后,您指示这些图块(tiles)是不透明的,并且应替换默认的地图图块(map tiles)
  • 4) 您将叠加层(overlay)添加到mapView中。自定义MapKit tiles可以位于道路上方或label上方(例如道路和地点名称)。 OpenStreetMap tiles带有预标签,因此应超出Applelabel
  • 5) 最后,您将创建一个切片渲染器(tile renderer),该渲染器可处理绘制切片(tiles)

tiles出现之前,您必须使用MKMapView设置tile renderer。因此,下一步是将以下行添加到viewDidLoad()的底部:

mapView.delegate = self

这会将MapViewController设置为其mapView的代理。

接下来,在MKMapViewDelegate扩展中,添加以下方法:

func mapView(
  _ mapView: MKMapView, 
  rendererFor overlay: MKOverlay
) -> MKOverlayRenderer {
  return tileRenderer
}

叠加层渲染器(overlay renderer)告诉地图视图如何绘制叠加层(overlay)。 切片渲染器(tile renderer)是用于加载和绘制地图切片(tiles)的特殊子类。

Build并运行以查看OpenStreetMap如何替换标准Apple地图。 但是,如何仅用几行代码就可以工作呢?

在这一点上,您真的可以看到开源地图和Apple Map之间的区别!


Dividing up the Earth

tile overlay的神奇之处在于能够从拼贴路径(tile path)转换为特定图像资产的能力。 三个坐标代表图块的路径(tile path)x,yz。 x和y对应于地图表面上的索引,其中0,0为左上图块。 z坐标表示缩放级别,并确定组成整个地图的tiles数。

缩放级别为0时,需要一个图块(tile)1×1网格代表整个世界:

在缩放级别1,您将整个世界划分为2×2的网格。 这需要四个tiles

在第2级,行和列的数量再次加倍,需要十六tiles

这种模式继续进行,每个缩放级别的细节级别和图块数量都增加了四倍。每个缩放级别需要2^(2 * z)个图块,一直到缩放级别19为止,这需要274,877,906,944个图块(tiles)

现在,您已经用OpenStreetMap替换了Apple的图块,是时候将其提升一个档次并显示您自己的完全自定义MapKit图块了!


Creating Custom MapKit Tiles

由于地图视图遵循用户的位置,因此默认缩放级别为16,该级别显示了很好的细节级别,可为用户提供其所在位置的上下文。

但是,缩放级别16需要整个星球4,294,967,296tiles!手绘这些tiles将花费一生以上的时间。

具有较小的边界区域(例如城镇或公园)可以创建自定义艺术品。对于较大范围的位置,您可以按程序从源数据生成tiles

由于入门项目包含此游戏的预渲染图块(pre-rendered tiles),因此您只需要加载它们即可。不幸的是,仅使用通用URL模板是不够的,因为如果渲染器请求应用程序未包含的数十亿个tiles之一,则您希望游戏正常友好的失败。

为此,您需要一个自定义的MKTileOverlay子类。通过打开AdventureMapOverlay.swift并添加以下代码来添加一个:

class AdventureMapOverlay: MKTileOverlay {
  override func url(forTilePath path: MKTileOverlayPath) -> URL {
    let tileUrl = 
      "https://tile.openstreetmap.org/\(path.z)/\(path.x)/\(path.y).png"
    return URL(string: tileUrl)!
  }
}

这将设置子类,并使用带有专用URL生成器的模板化URL替换基本类。

暂时保留OpenStreetMap tiles,以测试自定义叠加层。

打开MapViewController.swift并将setupTileRenderer()替换为以下内容:

private func setupTileRenderer() {
  let overlay = AdventureMapOverlay()

  overlay.canReplaceMapContent = true
  mapView.addOverlay(overlay, level: .aboveLabels)
  tileRenderer = MKTileOverlayRenderer(tileOverlay: overlay)
}

这将交换自定义子类,而不是提供OpenStreetMap tiles的叠加层(overlay)

构建并再次运行。 游戏看起来与以前完全相同。 好极了!

您正在使用MKTileOverlay子类,但仍在该子类中加载OpenStreetMap数据。 接下来,您将用自己的自定义MapKit tiles替换这些tiles

1. Loading the Pre-rendered Tiles

有趣的来了。 打开AdventureMapOverlay.swift并将url(forTilePath :)替换为以下内容:

override func url(forTilePath path: MKTileOverlayPath) -> URL {
  let tilePath = Bundle.main.url(
    forResource: "\(path.y)",
    withExtension: "png",
    subdirectory: "tiles/\(path.z)/\(path.x)",
    localization: nil)
}

在这里,您尝试使用已知的命名方案在资源包中找到匹配的tile。 这将查找项目中已加载的文件。 在入门项目中,您可以在tile文件夹中找到它们。 您会注意到,tile按其Z坐标分组在文件夹中,然后再次按其X坐标分组。 文件本身是以其y坐标命名的PNG

接下来,将以下代码添加到方法的末尾:

if let tile = tilePath {
  return tile
} else {
  return Bundle.main.url(
    forResource: "parchment",
    withExtension: "png",
    subdirectory: "tiles",
    localization: nil)!
}

返回找到的图块(tile)(如果存在)。 否则,如果缺少图块,则将其替换为羊皮纸图案(parchment pattern),以使地图具有medieval的幻想感。 这也消除了为每个tile path提供唯一资源的需要。

构建并再次运行。 现在,您将看到自定义地图。

尝试放大和缩小以查看不同级别的细节。

2. Bounding the Zoom Level

不过,您的游戏存在一个小问题。 如果放大或缩小得太远,就会完全丢失地图。

幸运的是,这很容易解决。 打开MapViewController.swift并将以下行添加到setupTileRenderer()的底部:

overlay.minimumZ = 13
overlay.maximumZ = 16

这会通知mapView您仅在这些缩放级别之间提供了图块(tiles)。 将缩放比例更改为超过该比例即可缩放应用程序中提供的平铺图像。 用户将不会获得任何其他细节,但至少现在显示的图像符合比例。

您可以限制放大范围。 打开MapViewController.swift并在viewDidLoad()中的initialRegion下面添加以下行

mapView.cameraZoomRange = MKMapView.CameraZoomRange(
  minCenterCoordinateDistance: 7000,
  maxCenterCoordinateDistance: 60000)
mapView.cameraBoundary = MKMapView.CameraBoundary(
  coordinateRegion: initialRegion)

在这里,您使用cameraZoomRangecameraBoundary将缩放功能限制为您的initialRegion


Creating Tiles

您正在阅读本教程制作幻想冒险游戏的机会非常渺茫。 在本部分中,您将了解如何构建自己的自定义MapKit tiles以适合您的需求。

注意:本节是可选的,因为它涵盖了如何绘制特定的图块(tiles)。 要跳到更多MapKit技术,请跳到Fancifying the Map部分。

此操作最困难的部分是创建适当大小的tiles并将其正确排列。 要绘制自己的自定义MapKit tiles,您需要一个数据源和一个图像编辑器。

打开项目文件夹,然后查看MapQuest / tiles / 14/4825 / 6156.png。 此图块显示缩放级别为14的中央公园的底部。该应用程序包含数十个这些小图像,形成了进行游戏的纽约市地图。 每个人都是使用基本技能和工具手工绘制的。

1. Deciding Which Tiles You Need

制作自己的地图的第一步是弄清楚您需要绘制哪些图块(tiles)。 首先,从 OpenStreetMap下载源数据,并使用MapNik之类的工具从中生成切片图像。

不幸的是,源是57GB下载! 另外,这些工具有些晦涩,不在本教程的讨论范围之内。 但是,对于像中央公园这样的边界区域,可以采用更简单的解决方法。

AdventureMapOverlay.swift中,将以下行添加到url(forTilePath :)

print("requested tile\tz:\(path.z)\tx:\(path.x)\ty:\(path.y)")

构建并运行。 现在,在缩放和平移地图时,tile path会显示在控制台输出中。 这会准确显示您需要创建的图块。

注意:如果您在模拟器上运行,则可能会在Xcode控制台中看到大量错误,形式为Compiler error: Invalid library file。 这是模拟器bug,可以安全地忽略。 不幸的是,它使控制台变得相当乱,使查看打印语句的结果更加困难。

接下来,您需要获取源tile并对其进行自定义。 您可以重用以前的URL scheme来获取OpenStreetMap tile

以下终端命令将抓取一个图块并将其存储在本地。

curl --create-dirs -o z/x/y.png https://tile.openstreetmap.org/z/x/y.png

您可以更改URL,用特定的映射路径替换xyz。 对于中央公园的南部,请尝试:

curl --create-dirs -o 14/4825/6156.png \
  https://tile.openstreetmap.org/14/4825/6156.png

此目录结构- zoom-level/x-coordinate/y-coordinate - 使以后查找和使用图块变得更加容易。

2. Customizing Appearances

下一步是将基础图像用作自定义的起点。 在您喜欢的图像编辑器中打开图块。 例如,这是Pixelmator中的样子:

现在,您可以使用画笔或铅笔工具来绘制道路,路径或有趣的特征。

如果您的工具支持图层,则在单独的图层上绘制不同的特征将使您可以调整它们以提供最佳外观。 使用图层可以使绘制更加容错,因为您可以使用其他功能来掩盖混乱的线条。

现在,对集合中的所有图块重复此过程,一切顺利。 如您所见,这将花费一些时间。

您可以使该过程更容易一些:

  • 首先将所有图块(tiles)合并为整个图层。
  • 绘制自定义地图。
  • 将地图拆分回图块。

3. Placing the Tiles

创建新的图块后,将它们放回项目中的tile / zoom-level / x-coordinate / y-coordinate文件夹结构中。 这样可以使他们井井有条,易于访问。

这也意味着您可以轻松地访问它们,就像在为url(forTilePath :)添加的代码中所做的那样。

let tilePath = Bundle.main.url(
    forResource: "\(path.y)",
    withExtension: "png",
    subdirectory: "tiles/\(path.z)/\(path.x)",
    localization: nil)

就这些,您已经准备好绘制一些精美的地图!


Fancifying the Map

该地图看起来很棒,并且符合游戏的美学要求。 但是还有更多可定制的内容!

蓝点不能很好地代表您的英雄,这就是为什么您要用一些自定义插图替换当前位置annotation的原因。

1. Replacing the User Annotation

通过打开MapViewController.swift并向MKMapViewDelegate扩展添加以下方法,开始替换英雄的图标:

func mapView(
  _ mapView: MKMapView, 
  viewFor annotation: MKAnnotation
) -> MKAnnotationView? {
  switch annotation {
  // 1
  case let user as MKUserLocation:
    // 2
    if let existingView = mapView
      .dequeueReusableAnnotationView(withIdentifier: "user") {
      return existingView
    } else {
      // 3
      let view = MKAnnotationView(annotation: user, reuseIdentifier: "user")
      view.image = #imageLiteral(resourceName: "user")
      return view
    }
  default:
    return nil
  }
}

此代码为用户annotation创建自定义视图。 就是这样:

  • 1) 如果MapKit正在请求MKUserLocation,则您将返回自定义annotation
  • 2) 地图视图维护可重用的annotation视图池以提高性能。 您首先尝试找到要重用的视图,如果有视图,则将其返回。
  • 3) 否则,您将创建一个新视图。 在这里,您使用了非常灵活的标准MKAnnotationView。 在这里,您仅用它来代表冒险者的图像。

构建并运行。 现在,您会看到一个小棍子图形,而不是蓝点。

2. Annotations for Specific Locations

MKMapView还允许您标记自己感兴趣的位置。MapQuestNYC地铁一起使用,将地铁系统视为一个强大的大型翘曲网络,可让您从一个站点传送到另一个站点。

为了让您的玩家清楚知道,您需要在地图上为附近的地铁站添加一些标记。 打开MapViewController.swift并在viewDidLoad()的末尾添加以下行:

mapView.addAnnotations(Game.shared.warps)

构建并运行。 现在,一些地铁站都有代表他们的大头针。

就像用来显示用户位置的蓝点一样,这些标准的图钉与游戏的美感不符。 自定义annotations可以做到这一点。

mapView(_:viewFor :)中,添加以下的caseswitchdefault case上:

case let warp as WarpZone:
  if let existingView = mapView.dequeueReusableAnnotationView(
    withIdentifier: WarpAnnotationView.identifier) {
    existingView.annotation = annotation
    return existingView
  } else {
    return WarpAnnotationView(
      annotation: warp, 
      reuseIdentifier: WarpAnnotationView.identifier)
  }

使用与之前相同的模式进行annotation。 如果已经存在,请return,否则,创建一个新的。 构建并再次运行。

现在,自定义annotation视图使用特定地铁线的模板图像和颜色。


Using Custom Overlay Rendering

MapKit有多种方法来修饰游戏地图。 下一步,您可以通过使用MKPolygonRenderer在水库上绘制基于渐变的微光效果。

首先将MapViewController.swift中的setupLakeOverlay()替换为:

private func setupLakeOverlay() {
  // 1
  let lake = MKPolygon(
    coordinates: &Game.shared.reservoir, 
    count: Game.shared.reservoir.count)
  mapView.addOverlay(lake)
  // 2
  shimmerRenderer = ShimmerRenderer(overlay: lake)
  shimmerRenderer.fillColor = #colorLiteral(
    red: 0.2431372549, 
    green: 0.5803921569, 
    blue: 0.9764705882, 
    alpha: 1)
  // 3
  Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
    self?.shimmerRenderer.updateLocations()
    self?.shimmerRenderer.setNeedsDisplay()
  }
}

这将通过以下方式设置新的叠加层(overlay)

  • 1) 创建一个与水库形状相同的MKPolygon。 这些坐标已在Game.swift中预先编程。
  • 2) 设置自定义渲染器以绘制具有特殊效果的多边形。 ShimmerRenderer使用Core Graphics在多边形顶部绘制多边形和渐变。
  • 3) 由于叠加层渲染器(overlay renderer)不具有动画效果,因此设置了100毫秒的计时器来更新叠加层。 每次更新叠加层时,渐变都会偏移一点,从而产生闪烁(shimmering)效果。

接下来,将mapView(_:rendererFor :)替换为:

func mapView(
  _ mapView: MKMapView, 
  rendererFor overlay: MKOverlay
) -> MKOverlayRenderer {
  if overlay is AdventureMapOverlay {
    return tileRenderer
  } else {
    return shimmerRenderer
  }
}

这将为两个叠加层中的每一个选择正确的渲染器。

进行构建并运行,然后在水库上方平移以查看闪烁的大海!

恭喜你!现在,您已经了解了如何使用MapKit为您的应用制作自定义地图。

创建手绘的自定义MapKit tiles非常耗时,但是它们为您的应用带来了独特的,身临其境的感觉。创建资源虽然需要一些努力,但使用起来却非常简单。

除了基本的图块(tiles)OpenStreetMap还提供了一系列专门的图块提供程序,用于诸如自行车和地形之类的事情。如果您要以编程方式设计自己的图块,则OpenStreetMap还提供要使用的数据。

如果您希望自定义但逼真的地图外观而不需要手工绘制所有内容,请查看第三方工具,例如MapBox,它可让您以适中的价格使用优质的工具来自定义地图的外观。

OpenStreetMap数据和图像是©OpenStreetMap贡献者。地图数据可在Open Database License下获得,而地图图块数据的许可为 licensed as CC BY-SA

后记

本篇主要讲述了自定义MapKit Tiles,感兴趣的给个赞或者关注~~~

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