初识小游戏

1.初识

微信小游戏目前的主要入口有下面几个:
群或好友分享
识别小游戏二维码
微信聊天列表页面下拉后出现最近玩过的小游戏
发现 - 小程序
发现 - 游戏 - 我的小游戏

小游戏的运行环境其实是微信的原生环境,游戏的 JavaScript 代码并不是通过浏览器来执行的,而是通过图中 JS VM 层独立的 JavaScript 引擎来执行的。 在 Android 平台使用 Google 的 v8 引擎,而在 iOS 上则使用苹果的 JavaScript Core 引擎。

当然 JS 引擎只负责解释执行 JS 逻辑,并没有支持渲染接口,那么渲染接口和诸多的微信功能接口又是怎么实现的呢?这就不得不提到脚本绑定技术,这种技术可以将某种原生语言的接口桥接到脚本接口上,当在脚本层调用接口时,会自动转发到原生层,调用原生接口。微信小游戏环境用的就是这样的技术,将 iOS / Android 原生平台实现的渲染、用户、网络、音视频等接口绑定为 JavaScript 接口。这也就是图中的微信原生层模块到小游戏层模块的原理。

不过除了这些常规玩法以外,最让人欣喜的是通过转发小游戏,可以完成玩家在游戏中的组队或对战,加上小游戏即点即玩的特点,这种邀战的游戏体验可以说是天衣无缝。

游戏资质提交及类目确认
确认游戏类目,并且提交资质文档。

  • 非个人主体需提交:《广电总局版号批文》 、《文化部备案信息》、《计算机软件著作权登记证书》、《游戏自审自查报告》
  • 个人主体需提交:《计算机软件著作权登记证书》、《游戏自审自查报告》

组成

1.底层技术
首先是开发语言,微信小游戏只支持 JavaScript,当然可以编译为 JS 的 TypeScript 以及 CoffeeScript 都可以作为开发语言使用。
其次是小游戏所支持的游戏库 API,主要包含 HTML5 的 Canvas 2D API 和 WebGL 1.0 API,使用任何一种 API 都可以完成游戏最重要的渲染功能,不过不能够混用,除此之外,只有 WebGL 渲染模式可以支持 3D 渲染。

2.中间件:游戏引擎
当然,直接使用 Canvas 2D 或 WebGL 来制作游戏是门槛很高,也非常费时费力的一件事,你肯定不希望一个小游戏项目拖上一年半载吧?所以使用 HTML5 游戏引擎其实是非常明智的选择,引擎封装出的高层接口可以大大降低开发者的开发门槛,缩短项目周期。目前国内的三家主流引擎 Cocos Creator、Egret、Laya 均已支持小游戏发布,Phaser.js、Three.js 等国外 HTML5 引擎虽然并没有支持直接发布,经过一些定制也是可以成功运行在小游戏环境中。

3.微信 SDK
除此之外,微信小游戏还提供了丰富的微信内部 SDK 供开发者调用,使用这些接口可以完成用户登陆、转发、排行榜等常规的社交功能。

文件结构

  • game.js 小游戏入口文件
  • game.json 配置文件

小游戏开发者通过在根目录编写一个 game.json 文件进行配置,开发者工具和客户端需要读取这个配置,完成相关界面渲染和属性设置。

key 数据类型 说明 默认值
deviceOrientation String 支持的屏幕方向 portrait
showStatusBar Boolean 是否显示状态栏 false
networkTimeout Number 网络请求的超时时间,单位:毫秒 60000
networkTimeout.request Number wx.request 的超时时间,单位:毫秒 60000
networkTimeout.connectSocket Number wx.connectSocket 的超时时间,单位:毫秒 60000
networkTimeout.uploadFile Number wx.uploadFile 的超时时间,单位:毫秒 60000
networkTimeout.downloadFile Number wx.downloadFile 的超时时间,单位:毫秒 60000
workers String 多线程 Worker 配置项,详细请参考 Worker文档

生命周期

1.wx.exitMiniProgram(Object object)
退出当前小游戏

2.LaunchOption wx.getLaunchOptionsSync()
返回小程序启动参数

3.wx.onHide(function callback)
监听小游戏隐藏到后台事件。锁屏、按 HOME 键退到桌面、显示在聊天顶部等操作会触发此事件。

4.wx.offHide(function callback)
取消监听小游戏隐藏到后台事件。锁屏、按 HOME 键退到桌面、显示在聊天顶部等操作会触发此事件。

5.wx.onShow(function callback)
监听小游戏回到前台的事件

6.wx.offShow(function callback)
取消监听小游戏回到前台的事件

2.wx API

你只能使用 JavaScript 来编写小游戏。小游戏的运行环境是一个 绑定了一些方法的 JavaScript VM。不同于浏览器,这个运行环境没有 BOM 和 DOM API,只有 wx API。接下来我们将介绍如何用 wx API 来完成创建画布、绘制图形、显示图片以及响应用户交互等基础功能。

创建 Canvas

调用 [wx.createCanvas()] 接口,可以创建一个 [Canvas]对象。

var canvas = wx.createCanvas()

此时创建的 canvas 是一个上屏 Canvas,已经显示在了屏幕上,且与屏幕等宽等高。

console.log(canvas.width, canvas.height)

在整个小游戏代码中首次调用 wx.createCanvas() 创建的是上屏 Canvas,之后调用则创建的是离屏 Canvas。如果你的项目中使用了官方提供的 [Adapter] 即 weapp-adapter.js(关于什么是 Adpater 请参考官方教程 [Adapter],那么你此时创建的会是一个离屏 Canvas。因为在 weapp-adapter.js 已经调用了一次 wx.createCanvas(),并把返回的 canvas 作为全局变量暴露出来

在 Canvas 上进行绘制
但是由于没有在 canvas 上进行绘制,所以 canvas 是透明的。使用 2d 渲染上下文的进行简单的绘制,可以在屏幕左上角看到一个 100x100 的红色矩形。

var context = canvas.getContext('2d')
context.fillStyle = 'red'
context.fillRect(0, 0, 100, 100)

显示图片

通过 [wx.createImage()]接口,可以创建一个 [Image] 对象。Image 对象可以加载图片。当 Image 对象被绘制到 Canvas 上时,图片才会显示在屏幕上。

var image = wx.createImage()

设置 Image 对象的 src 属性可以加载一张本地图片或网络图片,当图片加载完毕时会执行注册的 onload 回调函数,此时可以将 Image 对象绘制到 Canvas 上。

image.onload = function () {
    console.log(image.width, image.height)
    context.drawImage(image, 0, 0)
}
image.src = 'logo.png'

动画

在 JavaScript 中,一般通过 setInterval/setTimeout/requestAnimationFrame 来实现动画效果。小游戏对这些 API 提供了支持:

  • [setInterval()]
  • [setTimeout()]
  • [requestAnimationFrame()]
  • [clearInterval()]
  • [clearTimeout()]
  • [cancelAnimationFrame()]
    另外,还可以通过 [wx.setPreferredFramesPerSecond()]修改执行 requestAnimationFrame 回调函数的频率,以降低性能消耗。

触摸事件

响应用户与屏幕的交互是游戏中必不可少的部分,小游戏参照 DOM 中的 TouchEvent 提供了以下监听触摸事件的 API:

  • wx.onTouchStart()
  • wx.onTouchMove()
  • wx.onTouchEnd()
  • wx.onTouchCancel()

全局对象

window 对象是浏览器环境下的全局对象。小游戏的运行环境中没有 BOM API,因此没有 window 对象。但是提供了全局对象 GameGlobal,所有全局定义的变量都是 GameGlobal 的属性。

console.log(GameGlobal.setTimeout === setTimeout)
console.log(GameGlobal.requestAnimationFrame === requestAnimationFrame)
// true
开发者可以根据需要把自己封装的类和函数挂载到 GameGlobal 上。

GameGlobal.render = function () {
    //省略方法的具体实现...
}

render()
GameGlobal 是一个全局对象,本身也是一个存在循环引用的对象。

console.log(GameGlobal === GameGlobal.GameGlobal)
console.log 无法在真机上将存在循环引用的对象输出到 vConsole 中。因此真机调试时请注释 console.log(GameGlobal) 这样的代码,否则将会产生这样的错误

An object width circular reference can't be logged

3.Adapter

小游戏的运行环境在 iOS 上是 [JavaScriptCore],在 Android 上是 [V8],都是没有 BOM 和 DOM 的运行环境,没有全局的 document 和 window 对象。因此当你希望使用 DOM API 来创建 Canvas 和 Image 等元素的时候,会引发错误。

var canvas = document.createElement('canvas')

但是我们可以使用 wx.createCanvas 和 wx.createImage 来封装一个 document。

var document = {
    createElement: function (tagName) {
        tagName = tagName.toLowerCase()
        if (tagName === 'canvas') {
            return wx.createCanvas()
        }
        else if (tagName === 'image') {
            return wx.createImage()
        }
    }
}

这时代码就可以像在浏览器中创建元素一样创建 Canvas 和 Image 了。

var canvas = document.createElement('canvas')
var image = document.createImage('image')

同样,如果想实现 new Image() 的方式创建 Image 对象,只须添加如下代码。

function Image () {
    return wx.createImage()
}

weapp-adapter 对浏览器环境的模拟远不完整的,仅仅只针对游戏引擎可能访问的属性和调用的方法进行了模拟,也不保证所有游戏引擎都能通过 weapp-adapter 顺利无缝接入小游戏。直接将 weapp-adapter 提供给开发者,更多地是作为参考,开发者可以根据需要在 weapp-adapter 的基础上进行扩展,以适配自己项目使用的游戏引擎。

4.对引擎的支持

随着用户的交互更新画面和播放声音。小游戏的开发语言是 JavaScript,那么在引擎的底层就需要通过 JavaScript 调用绘制 API 和音频 API。

小游戏的运行环境是一个不同于浏览器的宿主环境,没有提供 BOM 和 DOM API,提供的是 wx API。通过 wx API,开发者可以调用 Native 提供的绘制、音视频、网络、文件等能力。

创建画布,你需要调用 wx.createCanvas()

let canvas = wx.createCanvas()
let context = canvas.getContext('2d')

创建一个音频对象,你需要调用 wx.createInnerAudioContext()

let audio = wx.createInnerAudioContext()
// src 地址仅作演示,并不真实存在
audio.src = 'bgm.mp3'
audio.play()

获取屏幕的宽高,你需要调用 [wx.getSystemInfoSync()]

let { screenWidth, screenHeight } = wx.getSystemInfoSync()

5.模块化

小游戏提供了 CommonJS 风格的模块 API,可以通过 module.exports 和 exports 导出模块,通过 require 引入模块。

drawLogo.js 模块封装的是一个用来把 logo 画到指定位置的方法。

module.exports = function (canvas, x, y) {
    var image = new Image()
    image.onload = function () {
        var context = canvas.getContext('2d')
        context.drawImage(image, x, y)
    }
    image.src = 'res/image/logo.png'
}

注意,当用加载本地的图片、音频、视频资源时,必须写从代码包根目录开始的绝对路径。如果写以 drawLogo.js 所在目录的相对路径,则会导致系统找不到资源文件,加载失败。

image.src = '../../res/image/logo.png'

在 game.js 中 require drawLogo,就可以调用 drawLogo 模块导出的方法。

var drawLogo = require('./src/util/drawLogo')
var canvas = wx.createCanvas()
drawLogo(canvas, 40, 40)

6.音频播放

小游戏内只有一种音频播放的方式,即使用 InnerAudioContext 来播放。

使用 InnerAudioContext 播放

通过 [wx.createInnerAudioContext()]接口可以创建一个音频实例 [innerAudioContext],通过这个实例可以播放音频。

var audio = wx.createInnerAudioContext()
audio.src = url // src 可以设置 http(s) 的路径,本地文件路径或者代码包文件路径
audio.play()

在 iOS 系统上,默认遵循静音键设置。如果希望在静音时也能播放声音,可以设置 obeyMuteSwitchfalse

audio.obeyMuteSwitch = false

自动播放和循环播放

设置 autoplay 和 loop 属性可以自动播放和循环播放音频,一般适用于背景音乐。

var bgm = wx.createInnerAudioContext()
bgm.autoplay = true
bgm.loop = true
bgm.src = url

回到前台时恢复背景音乐

当小游戏被隐藏到后台时,所有音频会被暂停,并在回到前台之前都不能再播放成功。

回到前台之后,被暂停的音频不会自动继续播放,如果小游戏有背景音乐的话,需要监听回到前台事件,并在收到回到前台事件之后调用背景音乐继续播放。

wx.onShow(function () {
  bgm.play()
})

处理音频中断事件

音频中断事件指的是在游戏期间,音频被系统打断时触发的事件。音频中断事件分为中断开始和中断结束事件,分别使用 wx.onAudioInterruptionBegin()和 wx.onAudioInterruptionEnd()来监听。

以下事件会触发音频中断开始事件:接到电话、闹钟响起、系统提醒、收到微信好友的语音/视频通话请求。被中断之后,小游戏内所有音频会被暂停,并在中断结束之前都不能再播放成功。

中断结束之后,被暂停的音频不会自动继续播放,如果小游戏有背景音乐的话,需要监听音频中断结束事件,并在收到中断结束事件之后调用背景音乐继续播放。

wx.onAudioInterruptionEnd(function () {
  bgm.play()
})

如果小游戏的逻辑强依赖音乐的播放,则需要在音频开始中断的时候暂停游戏

wx.onAudioInterruptionBegin(function () {
  // 暂停游戏
})

及时销毁不需要的音频实例

如果一个音频不再需要使用了,可以调用 InnerAudioContext.destroy() 接口提前销毁这个实例。

Android 同时播放的音频数量限制

由于系统限制,在 Android 上最多同时播放 10 个音频,超过的部分会做有损处理,对开发者来说不感知,但开发者应尽量避免同时播放过多音频。

7.文件系统

文件系统有两类文件:代码包文件和本地文件。

文件系统管理接口

通过 [wx.getFileSystemManager()]可以获取到全局唯一的文件系统管理器,所有文件系统的管理操作通过 [FileSystemManager]来调用。

var fs = wx.getFileSystemManager()

代码包文件

代码包文件指的是在项目目录中添加的文件。由于代码包文件大小限制,代码包文件适用于放置首次加载时需要的文件,对于内容较大或需要动态替换的文件,不推荐用添加到代码包中,推荐在小游戏启动之后再用下载接口下载到本地。

修改代码包文件

代码包内的文件无法在运行后动态修改或删除,修改代码包文件需要重新发布版本。

本地文件

本地文件指的是小程序被用户添加到手机后,会有一块独立的文件存储区域,以用户维度隔离。即同一台手机,每个微信用户不能访问到其他登录用户的文件,同一个用户不同 appId 之间的文件也不能互相访问。

本地文件的文件路径均为以下格式:

{{协议名}}://文件路径

其中,协议名在 iOS/Android 客户端为 "wxfile",在开发者工具上为 "http",开发者无需关注这个差异,也不应在代码中去硬编码完整文件路径。

本地临时文件

本地临时文件只能通过调用特定接口产生,不能直接写入内容。本地临时文件产生后,仅在当前生命周期内有效,重启之后即不可用。因此,不可把本地临时文件路径存储起来下次使用。如果需要下次在使用,可通过 FileSystemManager.saveFile()或 FileSystemManager.copyFile()接口把本地临时文件转换成本地存储文件或本地用户文件。
示例

wx.chooseImage({
  success: function (res) {
    var tempFilePaths = res.tempFilePaths // tempFilePaths 的每一项是一个本地临时文件路径
  }
})

本地缓存文件

本地存储文件只能通过调用特定接口产生,不能直接写入内容。本地缓存文件产生后,重启之后仍可用。本地缓存文件只能通过 FileSystemManager.saveFile()接口将本地临时文件保存获得。
示例

fs.saveFile({
  tempFilePath: '', // 传入一个本地临时文件路径
  success(res) {
    console.log(res.savedFilePath) // res.savedFilePath 为一个本地缓存文件路径
  }
})

本地用户文件

本地用户文件是从 1.7.0 版本开始新增的概念。我们提供了一个用户文件目录给开发者,开发者对这个目录有完全自由的读写权限。通过 wx.env.USER_DATA_PATH 可以获取到这个目录的路径。

示例

// 在本地用户文件目录下创建一个文件 a.txt,写入内容 "hello,world"
const fs = wx.getFileSystemManager()
fs.writeFileSync(`${wx.env.USER_DATA_PATH}/hello.txt`, 'hello, world', 'utf8')

读写权限:

接口、组件
代码包文件
本地临时文件
本地缓存文件
本地用户文件

8. 垃圾回收

小游戏中,JavaScript 中的每一个 Canvas 或 Image 对象都会有一个客户端层的实际纹理储存,实际纹理储存中存放着 Canvas、Image 的真实纹理,通常会占用相当一部分内存。 每个客户端实际纹理储存的回收时机依赖于 JavaScript 中的 Canvas、Image 对象回收。在 JavaScript 的 Canvas、Image 对象被回收之前,客户端对应的实际纹理储存不会被回收。通过调用 [wx.triggerGC()]方法,可以加快触发 JavaScriptCore Garbage Collection(垃圾回收),从而触发 JavaScript 中没有引用的 Canvas、Image 回收,释放对应的实际纹理储存。 但 GC 具体触发时机还要取决于 JavaScriptCore 自身机制,并不能保证调用 [wx.triggerGC()]能马上触发回收,建议在每局游戏开始或结束触发一下。

9.更新

小游戏更新

小游戏启动会有两种情况,一种是「冷启动」,一种是「热启动」。 假如用户已经打开过某小游戏,然后在一定时间内再次打开该小游戏,此时无需重新启动,只需将后台态的小游戏切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小游戏被微信主动销毁后再次打开的情况,此时小游戏需要重新加载启动。

更新机制

小游戏冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地已有的包进行启动,即新版本的小游戏需要等下一次冷启动才会应用上。 如果需要马上应用最新版本,可以使用 wx.getUpdateManager() API 进行处理。

getUpdateManager 的使用示例

v1.9.90 基础库以后,可以通过 [wx.getUpdateManager()]获取全局唯一的版本更新管理器,用于管理小游戏更新;另外请下载最新版本的开发者工具(1.02.1803130 以上)才支持在开发者工具上调试。

由于是新版本才支持的 API,请在使用前先判断是否支持,例如:

if (wx.getUpdateManager) {
  console.log('支持 wx.getUpdateManager')
}

获取到 updateManager 实例后,updateManager 包含以下方法:

方法 参数 说明
onCheckForUpdate callback 当向微信后台请求完新版本信息,会进行回调
onUpdateReady callback 当新版本下载完成,会进行回调
onUpdateFailed callback 当新版本下载失败,会进行回调
applyUpdate 当新版本下载完成,调用该方法会强制当前小游戏应用上新版本并重启

onCheckForUpdate(callback) 回调结果说明:

属性 类型 说明
hasUpdate Boolean 是否有新的版本

注: 检查更新操作由微信在小游戏冷启动时自动触发,不需由开发者主动触发,开发者只需监听检查结果即可。

onUpdateReady(callback) 回调结果说明:

当微信检查到小游戏有新版本,会主动触发下载操作(无需开发者触发),当下载完成后,会通过 onUpdateReady 告知开发者。

onUpdateFailed(callback) 回调结果说明:

当微信检查到小游戏有新版本,会主动触发下载操作(无需开发者触发),如果下载失败(可能是网络原因等),会通过 onUpdateFailed 告知开发者。

applyUpdate() 说明:

当小游戏新版本已经下载时(即收到 onUpdateReady 回调),可以通过这个方法强制重启小游戏并应用上最新版本。

完整使用示例

if (typeof wx.getUpdateManager === 'function') {
  const updateManager = wx.getUpdateManager()

  updateManager.onCheckForUpdate(function (res) {
    // 请求完新版本信息的回调
    console.log(res.hasUpdate)
  })

  updateManager.onUpdateReady(function () {
    // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
    updateManager.applyUpdate()
  })

  updateManager.onUpdateFailed(function () {
    // 新的版本下载失败
  })
}

10. 多线程 Worker

对于游戏来说,每帧 16ms 是极其宝贵的,如果有一些可以异步处理的任务,可以放置于 [Worker]中运行,待运行结束后,再把结果返回到主线程。[Worker]运行于一个单独的全局上下文与线程中,不能直接调用主线程的方法,[Worker]也不具备渲染的能力。[Worker]与主线程之间的数据传输,双方使用 Worker.postMessage()来发送数据,Worker.onMessage()来接收数据,传输的数据并不是直接共享,而是被复制的。

步骤

  1. 配置 Worker 信息
    在 game.json 中可配置 Worker 代码放置的目录,目录下的代码将被打包成一个文件:

配置示例:

{
  "workers": "workers"
}
  1. 添加 Worker 代码文件
    根据步骤 1 中的配置,在代码目录下新建以下两个入口文件:
workers/request/index.js
workers/request/utils.js
workers/response/index.js
  1. 编写 Worker 代码
    在 workers/request/index.js 编写 Worker 响应代码
var utils = require('./utils')

// 在 Worker 线程执行上下文会全局暴露一个 `worker` 对象,直接调用 worker.onMeesage/postMessage 即可
worker.onMessage(function (res) {
  console.log(res)
})
  1. 在主线程中初始化 Worker
    在主线程的代码 game.js 中初始化 Worker
var worker = wx.createWorker('workers/request/index.js') // 文件名指定 worker 的入口文件路径,绝对路径
  1. 主线程向 Worker 发送消息
worker.postMessage({
  msg: 'hello worker'
})

Tips

  1. Worker 最大并发数量限制为 1 个,创建下一个前请用 Worker.terminate() 结束当前 Worker
  2. Worker 内代码只能 require 指定 Worker 路径内的文件,无法引用其它路径
  3. Worker 的入口文件由 wx.createWorker()时指定,开发者可动态指定 Worker 入口文件
  4. Worker 内不支持 wx 系列的 API
  5. Workers 之间不支持发送消息
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容