门户
Portal
论坛
BBS
车友会
car
户外
outdoors
【不实言论举报】
QQ
立即注册
登录
用户名
Email
自动登录
找回密码
密码
登录
立即注册
道具
勋章
任务
设置
退出
畅行社区
»
论坛
›
畅行生活
›
网络游戏
›
HTML5游戏框架大军中的一乘轻骑Phaser
返回列表
查看:
2769
|
回复:
0
HTML5游戏框架大军中的一乘轻骑Phaser
[复制链接]
tequila_Y
tequila_Y
当前离线
积分
7
23
主题
33
帖子
7
积分
实名认证
积分
7
发消息
发表于 2018-3-21 14:32:02
|
显示全部楼层
|
阅读模式
【编者按】HTML5游戏以其“低门槛、低成本、简单有趣、易于分享”的特点受到了广大玩家的喜爱和业界人士的关注。不论是新手还是企业专业团队,从无到有制作一个游戏并不总是一个好主意,选用适合自己的游戏引擎或者框架才是正确的途径。本文将介绍HTML5游戏框架大军中的一乘轻骑--Phaser。
Phaser简介
Phaser是一个开源HTML5游戏框架,由Photon Storm公司创建,在2011年发布1.0版本后,就进入稳定期,定期更新版本,很多新的功能和修复会快速完成。大约一个月发布一次稳定版--可以在GitHub上查看完整的更新记录。Phaser的设计目标是创建运行在桌面和移动设备网页浏览器上的游戏。近几年,人们越来越关注移动设备网页浏览器的性能,以及快速增长、非常重要的网页游戏领域。Photon Storm公司早在2011年就开始制作游戏工具与游戏,其技术积累,能帮助开发者提供游戏开发中的种种需求,直面游戏开发中的痛点。
Phaser主要特色
朴素的JavaScript编程风格
这听起来好像是一个奇怪的“特性”,但实际上这点相当有吸引力。Phaser内部不使用任何人造的OO风格编程。没有大量的继承链和组件系统,你也不必强迫对象设计为任何固定的类结构。只有简单直接的原型链,JavaScript最自然的使用方式。
这并不意味着你不能以结构化方式创建游戏。这只是意味着它不是强制性的。这也意味着Phaser内部可以轻松地改造。
简单易用的资源加载
Phaser内置的资源加载器可以处理:
精灵表(固定尺寸帧)
纹理图片集(包括Texture Packer、JSON Hash、JSON Array、Flash CS6/CC以及Starling XML格式)
音频文件
数据文件(XML、JSON和文本文件)
JavaScript文件(所以你能部分加载游戏或JS资源)
Tilemaps(CSV和Tiled地图格式)
Bitmap字体
我通常从Flash直接导出纹理图片集到Phaser游戏,并且完全支持修剪空白边缘的图片集。资源可以部分加载、缓存,以及从不同URLs(为了支持CDN)拉取,你只需要一行代码就能将任何精灵变成进度条。
渲染:WebGL和Canvas
Phaser内部使用Pixi.js进行渲染。Pixil是一个专注于Canvas和WebGL非常棒和快速的渲染库。Photon Storm公司宣称将持续资助这个库不断改进和发展。
对于你的游戏,这意味着如果浏览器支持WebGL,那么玩家就会获得更加流畅的游戏体验。WebGL在桌面平台已经非常普遍,但在移动平台还在发展初期,即便如此,这也是HTML5游戏发展的未来,所以支持它非常重要。最新发布的Phaser版本引入了WebGL着色器和过滤支持,并且即将发布的版本将实现法线贴图(normal map),所以你将能使用新工具诸如Sprite Lamp。
音频:网络音频和遗留音频
音频成为HTML游戏的软肋很多年了。仅仅几年前,还面临选择一个单声道高延迟的音频还是根本不使用音频而两难。还好时代变了,Web Audio API来拯救我们了。它支持基于节点的音频,多通道,节点路由和各种效果。毫无疑问,Phaser全面支持Web Audio。
然而,很多设备,尤其是Android设备仍有不支持Web Audio--所以Phaser也支持遗留音频以及使用Audio Sprite:打包一组声音到单个文件,然后使用回放标记跳转到不同的效果。Phaser将在两种方式自带切换,取决于设备的能力,并且也包含自动为你解锁音频自系统,它捕获了许多第一批移动开发者的芳心。
输入:多点触碰,键盘,指针,鼠标
当需要支持桌面和移动平台,有越来越多的不同数量的潜在输入选项。Phaser支持键盘、鼠标、触摸、MSPointer(目前Pointer在IE11下)以及它们的组合。例如,在Windows Surface设备上,你可以在鼠标和触摸两者之间切换,或同时使用两者。
对于触摸输入,Phaser能处理单点触碰和多点触碰环境。你可以定义多达10个触碰点,并且独立地追踪它们,使用它们的事件来处理Sprite交互,诸如拖拽、轻击和碰撞。
物理、补间动画和粒子
整合进核心库的是ArcadePhysics和ArcadeParticles系统。它们是简单的AABB轻量级库,允许你对任何精灵使用重力和运动,然后进行测试以便用于碰撞和分离。使用基于世界的四叉树有助于最小化碰撞测试,你能通过耗费很少的时间得到相当好的结果。
然而,内置的物理系统不可能适合所有类型的游戏,所以物理系统可以轻易地替换,并且没有物理属性绑定到实际的精灵(而是身体组件),所以可以被替换成诸如Box2D或p2.js。补间系统也被整合进,允许你轻易地补间对象或属性。如果游戏暂停,那么所有补间也将自动暂停,需要时再继续。
插件系统
Phaser的目标是最终稳定下来,并且趋于稳定而均衡,它不太可能涉足修复和浏览器更新之外的其他主题。同时又想要Phaser保持不断增长,为各种游戏提供功能,但是又不至于导致核心库爆炸式膨胀。为此,Photon Storm公司构建了一个插件系统。
Phaser插件可以注册自己到核心框架,在核心游戏循环中进行更新,从而执行各种有用的额外任务。一个极好的例子是最近发布的Photon Storm公司将自己发布插件,同时也期待未来有大量的插件来自社区。
社区评价
在知名的NK社区,活跃的开发者有许多有价值的交流和评论。
网友balazsdavid987:尝试了数个HTML5/JS游戏框架库之后,我敢肯定Phaser是最好的2D游戏框架库。它更新频率快,有优秀的使用文档和极好的社区支持。只是阅读Phaser的源代码就可以学习到大量的游戏开发技术。我们在iOS创建了一个完整的小游戏平台,上网友dtft :面都是phaser游戏。虽然它们算不上大作,但的确非常快速和具有挑战性。我们发现phaser对于快速开发和原型制作简直完美。
网友georgefrick:我们正在使用phaser制作91个应用(系列教育游戏/互动/故事书)。进展很顺利,并且已经完成了一半以上的应用。虽然Phaser有缺陷,但任何框架都会有错误。
网友HobbesDT :对我来说,Phaser非常容易学习,即使我没有很多计算机科学经验,并且没有费多大力气就可以在移动平台(至少是iOS)运行得很好。如果你不知道如何制作游戏,并且想要快速制作一个简单的游戏,这无疑是一个很棒的框架库。官方网站有非常丰富的教程和示例,并且还能在第三方网站上找到很多资料。
网友GavinAnderegg :大约一年前,我和一些朋友在一个12个小时的黑客马拉松活动中使用Phaser制作了一个小游戏。它是一个很好的小框架,虽然我们没有真正使用它的很多功能,但它很容易上手。
可以在 http://github.com/gavinanderegg/coffeeQuest查看我们制作的游戏。
设置开发环境
安装web server
因为浏览器的一些安全机制,最好使用本地web服务器如iis,apache来开发HTML5游戏,如果直接双击打开html页面,可能有些功能不能正常运行。
推荐初学者安装服务器套件安装包,它们以单个可执行文件的形式包含了流行的网络开发工具,例如Apache, PHP和MySQL。对于Windows平台,推荐WAMP Server或XAMPP。而对于OS X平台,强烈推荐MAMP。
选择编辑器
接下来要选择一个得心应手的编辑器来编写代码。如果你是个经验丰富的开发者,我相信你已经有自己偏爱的编辑器。如果是新手,我推荐Sublime Text。Phaser还支持TypeScript,所以还可以选择使用微软的Visual Studio作为编辑器。
下载Phaser
Phaser框架开源,源代码托管在GitHub。
测试环境是否就绪
到此,你已经安装好Web服务器,设置好编辑器,Phaser也已经下载好。现在是时候创建第一个测试项目,并且测试环境是否就绪。
定位到你的“Web Root”,这是一个文件夹,是Web服务器查找文件的地方。如果你在Windows上使用WAMP,可以鼠标左键点击系统图标中的WAMP图标,在弹出菜单中选择“www目录”,即可在文件浏览器中打开这个目录。
下载这个zip文件。它包含了一个hellophaser目录,里面有一个JavaScript文件、一个index.html和一个PNG文件。拷贝Hellophaser目录到Web服务器根目录。
打开网页浏览器,然后浏览本地服务器上的Hellophaser目录。通常是在浏览器中简单地输入localhost/hellophaser或127.0.0.1/hellophaser。如果一切运行正确,将会在中央显示黑色的游戏区域,以及一个Phaser logo。
如果某种原因导致不正确,你需要打开调试窗口查看错误输出。如果只是简单的文件丢失错误,检查你的目录名,然后刷新页面。如果是复杂的错误,可以在Phaser社区发帖求助,将会得到开发团队和热心人的帮助。
使用Phaser创建第一个游戏
在这个示例中,制作的游戏名字叫怪物要糖果。首先介绍项目的结构,以便你可以理解整个游戏玩法。我们将根据游戏运行的逻辑顺序依次讲解:装载图片资源、创建主菜单、真正的游戏循环。你可以点击链接先试玩怪物要糖果。
目设置及结构
项目目录中包含index.html文件(包括HTML5结构和所有必要的JS文件)。还有两个子目录:IMG目录,里面是美术资源,src目录,里面是游戏的源代码。
下面是目录结构预览:
图1 目录结构预览
在src目录,你会看到JavaScript文件。在本教程中,我将描述在该文件夹中的所有文件的内容和用途。你可以在GitHub看到每个文件的源代码。
index.html 文件
我们从index.html文件开始。它看起来像一个HTML5网站,但没有文本和HTML元素,我们初始化Phaser框架,它将渲染所有东西到Canvas元素。
我们在标签定义文档:字符集编码,网页标题以及CSS样式。通常我们会引用外部CSS文件,但在这里不需要,正如我前面提到的,一切都将在一个canvas元素中呈现,所以我们不会有任何的HTML元素。
要做的最后一件事是包含所有的JS文件:从phaser.min.js文件,包含Phaser框架所有源代码,到包含所有游戏代码的文件。为了减少浏览器的请求数,使游戏的载入速度更快,我们将在游戏需要的时候单独载入它们。
查看标签,在这里初始化框架并启动我们的游戏。自调用函数中的第一行代码如下:
此代码将初始化Phaser:
640是游戏的画布宽度,960是游戏的画布高度。
phaser.auto通知框架我们希望游戏如何渲染到画布上。这里有三个选项:CANVAS, WEBGL和AUTO。第一个将我们的游戏渲染在2D Canvas上;第二个使用WebGL在可能的情况下渲染游戏(现在主要用于桌面游戏,但是移动端支持也会越来越好);第三个通知框架,自动检查是否支持WebGL,从而决定游戏如何呈现,如果不支持WebGL,那么2D Canvas将被使用。
该框架初始化将被赋值给一个称为game的对象,它将在引用Phaser实例时使用。
下面几行代码都是加入状态到我们的游戏:
“Boot”是一个状态名,Candy.Boot是一个对象(在下面代码中定义),我们开始进入状态时将被执行。我们为Boot(配置),Preloader(加载资源),MainMenu(你猜对了,这是游戏主菜单)和Game(游戏主循环)添加状态。最后一行,game.state.start(“Boot”),启动Boot状态,Candy.Boot对象将被执行。
你可以看到,一个主要的JavaScript游戏对象已被创建。在游戏中,我们有Boot, Preloader, MainMenu,和Game对象,我们使用原型定义它们。这些对象中的一些特殊函数名被框架本身保留(preload(),create(),update(),和render()),但我们也可以定义自己的(startgame(),spawncandy(),managepause())。如果你不确定你能理解这一切,别担心我会使用示例代码解释这一切。
游戏
现在让我们忘记Boot Preloader和MainMenu。它们将在以后详细解释;此刻你要知道的是Boot状态决定游戏的基本配置,Preloader将加载所有的美术资源,而MainMenu将显示开始游戏菜单。
让我们关注游戏本身,查看Game代码。在解释game.js代码之前,让我们从开发者的角度来谈谈游戏概念本身。
Portrait模式
游戏是竖屏模式。
在这种模式下,屏幕的高度大于其宽度。有的游戏适合竖屏(像怪物要糖果),有的游戏适合横屏(包括平台游戏,比如Craigen),甚至某些类型的游戏在两种模式下都可以运行,通常这样的游戏比较难编写。
Game.js
在我们浏览game.js文件之前,先了解它的结构。我们创建一个游戏世界,有一个玩家角色在里面,它的工作就是吃糖果。
游戏世界:怪物后面的世界是静态的。背景中有一张糖果大陆的背景图片,怪物放置在前景中,还有一个用户界面。
玩家角色:这个演示非常简单而基础,所以小怪物除了等待糖果什么也不做。玩家的主要任务就是收集糖果。
糖果:游戏的核心机制是吃到尽可能多的糖果。糖果在屏幕的顶部边缘产生,玩家必须在它们掉落的时候轻击(或点击)。如果任何糖果掉出屏幕的底部,删除它,并且玩家将受到伤害。我们还没有实现生命值系统,因此,糖果掉出屏幕后,游戏立即结束,并显示适当的消息。
好吧,现在让我们查看game.js:
Candy.Game原型有三个函数:
create()初始化
managepause()暂停和继续游戏
update()管理游戏主循环
我们将创建一个叫做item的实用对象,它表示一颗糖果。它会有一些有用的方法:
spawncandy()增加新糖果
clickcandy()当用户点击时,糖果消失
removecandy()删除糖果
让我们浏览这些代码:
在这里,我们设置所有将使用的变量。
通过定义this._name,我们限制了变量使用的范围为Candy.Game。这意味着他们不能用在其他状态中--我们在其他地方不需要它们,所以为什么要暴露它们?
通过定义Candy._name,我们允许在其他状态和对象中使用这些变量,例如,Candy._score的数值可以被Candy.item.clickCandy()函数增加。
对象初始化为null,计算需要的变量初始化为零。
我们查看candy.game.prototype的代码:
在create()函数开始,我们设置了ARCADE物理系统──Phaser中有现成的,这是最简单的一种。之后,我们为游戏添加了重力。然后添加了三个图片:背景、怪物、以及得分UI的背景。第四个增加的元素是暂停按钮,注意我们使用的是candy.GAME_WIDTH和candy.GAME_HEIGHT变量,它们定义在Candy.Preloader(),但是在整个游戏代码中都可用。
然后我们创建怪物,玩家的虚拟形象。这是一个使用了帧的动画精灵--精灵表单。为了使之看起来像是站着在呼吸,我们为它添加动画。
animations.add()函数创建帧动画,需要四个参数:
动画的名称(可以在之后引用它)
包含所有帧的table(我们可以只使用其中一些)
帧速
一个决定动画是否循环的标志
当想要播放动画,我们必须使用animations.play()播放参数指定的动画。
我们将spawncandytimer设为0,并且怪物生命health设为10。
文本样式
接下来的两行代码,可以让我们在屏幕上显示文字。this.add.text()函数有四个参数:屏幕左侧和顶部的绝对位置,实际的文本字符串和配置对象。我们可以使用CSS那样设置文本格式。代码如下:
字体是Arial,40像素高,黄色,可以设置描边(颜色和厚度),文本中心对齐。
之后,我们定义candygroup和第一颗糖果。
暂停游戏
暂停函数看起来像这样:
每次暂停按钮被点击时,我们改变this.game.paused状态为true,显示相应的提示给玩家,并为玩家单击或点击屏幕的行为创建一个事件侦听器。当单击或点击被检测到,我们删除文本并设this.game.paused为false。
paused变量在game对象中是特殊的,因为它将暂停游戏中所有动画以及计算,所以一切都被冻结,直到我们取消暂停,游戏暂停状态被设置为false。
更新循环
update()函数名是Phaser保留的函数之一。当你用这个名字命名一个函数,它将在游戏的每一帧被执行。
我们用spawncandytimer变量来跟踪时间。if语句每秒检查一次,检查是否需要重置计时器,或者是在游戏世界中产生一个新糖果(也就是说,每次看到spawncandytimer相隔1000毫秒)。然后,我们用forEach遍历糖果组中的所有糖果对象(在屏幕上可以存在多个糖果),添加固定的数值到糖果的angle变量(存储在糖果对象中的rotateMe),使它们下落时以固定的速度旋转。我们做的最后一件事是检查health是否下落到0,如果这样的话,那么我们在屏幕上显示游戏结束并暂停游戏。
管理糖果事件
为了将糖果的逻辑与主Game分开,我们使用item定义糖果,包含的函数有:spawncandy(),clickcandy()和removecandy()。为了方便使用在Game保留一些糖果的变量,而其他的一些变量则只在item的函数中,以便达到更好的维护性。
函数首先定义三个值:
糖果随机下落x坐标(数值在0和游戏画面宽度之间)
糖果随机下落y坐标,基于糖果自身的高度
随机的糖果类型(糖果一共有五个不同的图像)
然后我们添加一个糖果作为精灵,其起始位置和图像根据上面的定义。我们还需要为糖果产生过程设定动画帧。
接下来我们用物理引擎使糖果自然地从屏幕顶部坠落。然后,我们启用糖果的点击输入,加入事件监听器。
为确保糖果离开游戏屏幕时被销毁,我们将checkWorldBounds设为true。糖果离开屏幕时,函数events.onOutOfBounds()将被调用;我们用它调用removecandy()函数。设置锚点在糖果上,使其绕轴线旋转。在这里我们设置了rotateMe变量,在update()循环中转动糖果;我们选择-2和+2之间的一个值。最后一行代码将新创建的糖果加入到糖果群组,这样我们就可以不停的遍历他们。
让我们查看下一个函数clickcandy():
这里需要将一个糖果作为参数,采用Phaser自带的方法kill()删除它。我们还增加了得分1,并更新得分的文本。
重置糖果也同样简短和简单:
当糖果被点击或掉出屏幕。removecandy()函数会被调用。candy对象会被删除,玩家失去10点生命。(游戏一开始玩家有10点生命,所以有一个糖果掉出屏幕游戏就结束了)
原型和游戏状态
我们已经了解了游戏机制,核心理念,以及游戏玩法。现在是时候去看代码的其他部分了:缩放屏幕、加载资源和管理按钮点击等等。
我们已经知道了游戏状态,让我们逐个查看他们:
Boot.js
boot.js是我们将定义主要游戏对象Candy(你可以取个你喜欢的名字)的JavaScript文件。下面是boot.js文件的源代码:
正如你所看到的,我们从var Candy = {}开始,为游戏创建了一个全局对象。所有对象都存储在其中,所以我们将不会使全局名字空间膨胀。
Candy.Boot = function(game){}代码创建一个新函数称为boot()(index.html有调用)将game对象作为参数(也在index.html中被框架创建)。
Candy.Boot.prototype = {}代码是使用原型定义Candy.Boot内容的一种方式。
Phaser有一些保留的函数名称,正如我之前提到;preload()和create()就是其中的两个。preload()用于加载所有资源;create()只调用一次(在preload()之后),所以你可以把代码当对象一样安排,就像定义变量或添加精灵。
我们的Boot对象包含这两个函数,这样他们就可以被Candy.Boot.preload()和Candy.Boot.create()调用。正如你所看到boot.js文件的完整的源代码,preload()函数加载了一个图像到框架中:
this.load.image()中第一个参数是我们给图像取的名字,第二个是图像文件的路径。
为什么我们要在boot.js文件中加载图像,用preload.js不行吗?好吧,因为我们需要一个加载条来显示所有资源(preload.js)的加载状态,因此它需要第一个被加载。
缩放选项
create()函数包含了一些Phaser特定的输入和缩放设置:
第一行,input.maxpointers设为1,我们的游戏不需要多点触摸。
scale.scalemode控制游戏的缩放。可用的设置有:EXACT_FIT,NO_SCALE和SHOW_ALL。EXACT_FIT将缩放游戏到所有可用的空间(100%宽和高,不等比缩放);NO_SCALE将禁用缩放;SHOW_ALL将确保游戏符合给定的尺寸,一切都会显示在屏幕上(按比例缩放)。
将scale.pagealignhorizontally和scale.pagealignvertically设为true ,将使游戏在水平和垂直方向居中。
调用scale.setScreenSize(true) “激活”缩放。
最后一行,state.start('Preloader'),执行下一个状态,即Preloader状态。
Preloader.js
preload函数中加载了大量的图片。与preload()函数相比,create()函数非常简单,因为create()函数只需要负责切换状态。
下面是preloader.js源代码:
};与boot.js比较像;定义了Preloader对象,然后添加了两个原型函数(preload()和create())。在Prototype对象中,我们定义了两个变量:Candy.GAME_WIDTH和Candy.GAME_HEIGHTt;它们设置游戏屏幕默认的宽度和高度。
preload()的前3行代码设置舞台的背景颜色(# b4d9e7,浅蓝色),显示游戏中的精灵,setPreloadSprite()函数将负责资源的加载进度显示。我们来看add.sprite()函数:
正如你所看到的,我们需要三个值:图像x轴绝对坐标(舞台宽度减去图像宽度再除以2),图像y轴绝对坐标(类似计算)以及图像的名称(我们已经在boot.js文件中加载)。
加载spritesheets
接下来的几行是使用load.image()(你已经看到了)加载所有的图形资源。
最后3行代码有点不同:
load.spritesheet()函数,不是加载单个的图片,而是spritesheet。两个额外的参数告诉函数单个图像的尺寸。
在这里candy.png我们有5种不同类型的糖果。图片尺寸410x98px,但是单个元素的大小是82x98px,在load.spritesheet()函数中定义。玩家spritesheet以同样的方式加载。
create()函数启动游戏的下一个状态MainMenu,当所有资源加载完毕就会显示游戏菜单。
MainMenu.js
在这里渲染图片,添加按钮以及游戏循环。
MainMenu没有preload()函数,因为资源已在Preload.js加载。
这里有2个函数,create(),startGame()。先看startGame()函数:
这个函数只负责启动游戏循环,但是它不会自动执行,我们需要通过按钮来触发它。
create()有3个add.sprite()函数,它们加载图片到舞台上。我们的主菜单在背景上,小怪物在角落里,还有游戏的标题。
按钮
还有一个我们早已在Game状态使用的对象,就是按钮:
这个按钮看起来比我们之前的代码都要复杂。我们通过八种不同的参数创建按钮:x轴位置,y轴位置,图像的名称(或精灵),单击该按钮时执行的函数,该函数执行环境,指定按钮使用的图片。
这是按钮的spritesheet,包含状态标签:
与candy.png非常相似,垂直排列。
记住最后三位数字传递给函数的意义-1 0 2-它们分别代表按钮的不同状态:over(鼠标悬停),out(正常),和down(触摸或点击)。在button.png我们有不同的图片代表他们。
现在你已经了解了Phaser游戏框架的基础。恭喜你!
游戏成品
本文中使用的demo游戏已经演变成一个完整的游戏,你可以在这里玩。如你所见,有生命、成就、得分,以及其他有趣的功能,他们中的大多数都基于你已经学到的知识。
这是一段关于怪物要糖果demo代码的漫长旅程,我希望能帮助你学习Phaser,在不久的将来你可以开发很酷的游戏。
作者:郭华丰,广州微美软件有限公司CTO,专注于移动平台(Android、iOS等)游戏应用开发,拥有超过10年图形和游戏开发经验,曾供职跨国企业Gameloft。主要作品有:狂野飙车(Asphalt 6)、近地联盟先遣队2(NOVA 2)、战谷、三国我为王等。出版了《iOS应用开发》、《Android数据库应用编程》。(审校/陈秋歌 责编/钱曙光)
本文选自程序员电子版2015年10月A刊,该期更多文章请点击“阅读原文”。欢迎订阅程序员电子版(含iPad版、Android版、PDF版)。
游戏
,
我们
,
糖果
相关帖子
•
花旗:五大理由看好苹果将会继续上涨
•
情感|热闹的人易散场,慢热的人最长情
•
索尼推以旧换新活动PS4+5款游戏抵半价PS4Pro
•
郭广昌:以科技引领合作共赢的经济全球化
•
COD12媒体高分 玩家骂评如潮
•
登天露山记
•
不如任性过生活(深度好文)
•
【思南MIUMIUclub】端午节火爆现场回顾。
•
【武信人物形象设计】设计一个完美的你
•
元旦人同乐狮山地共春——师大一中元旦迎新游园会纪实
回复
使用道具
举报
返回列表
高级模式
B
Color
Image
Link
Quote
Code
Smilies
您需要登录后才可以回帖
登录
|
立即注册
本版积分规则
发表回复
回帖后跳转到最后一页
快速回复
返回顶部
返回列表