这次给大家带来的是通过Egret实现密室逃生小游戏的教程。该游戏包括人物状态机、MVC设计模式和单例模式,该游戏在1.5s内通过玩家点击操作寻找安全点,方可进入下一关,关卡无限,分数无限。下面是具体的模块介绍和代码实现。

该游戏主要内容包括

**开始游戏场景
游戏场景
游戏结束结算场景
全局常量类
人物状态机类**

游戏源码素材下载:https://github.com/shenysun/R...

创建全局常量类

在所有舞台搭建之前先写一个全局的静态方法类,取名为GameConst。这个类里面的方法和常量可以供全局使用,例如舞台宽高、通过名字获取位图、通过名字获取纹理集精灵等等。这个类可以大大减少后期的代码量,降低整体的耦合度。

/**常用常量类 */
class GameConst {/**舞台宽度 */public static StageW:number;/**舞台高度 */public static StageH:number;/**根据名字创建位图 */public static CreateBitmapByName(name:string):egret.Bitmap {let texture:egret.Texture = RES.getRes(name);let bitmap:egret.Bitmap = new egret.Bitmap(texture);return bitmap;}/*** 根据name关键字创建一个Bitmap对象。此name 是根据TexturePacker 组合成的一张位图*/public static createBitmapFromSheet(name:string, sheetName:string):egret.Bitmap {let texture:egret.Texture = RES.getRes(`${sheetName}_json.${name}`);let result:egret.Bitmap = new egret.Bitmap(texture);return result;}public static getTextureFromSheet(name:string, sheetName:string):egret.Texture {let result:egret.Texture = RES.getRes(`${sheetName}_json.${name}`);return result;}/**移除子类方法 */public static removeChild(child:egret.DisplayObject) {if(child && child.parent) {if((<any>child.parent).removeElement) {(<any>child.parent).removeElement(<any>(child));}else {child.parent.removeChild(child);}}}
}

如果游戏中设置图片锚点较多也可以在这个类里面加一个设置锚点的方法,传入对象,横轴锚点和纵轴锚点坐标三个参数。

开始场景

开始页面比较简洁,有一个LOGO和两个按钮分别是开始游戏,更多游戏。

/**游戏开始场景 */
class StartGameLayer extends egret.Sprite {/**开始按钮 */private startBtn:MyButton;/**更多按钮 */private moreBtn:MyButton;/**LOGO */private titleImage:egret.Bitmap;public constructor() {super();this.init();}private init():void {/**添加游戏LOGO */this.titleImage = GameConst.createBitmapFromSheet("logo_mishitaosheng", "ui");this.titleImage.x = 51;this.titleImage.y = 161;this.addChild(this.titleImage);//开始按钮设置this.startBtn = new MyButton("btn_y", "btn_kaishi");this.addChild(this.startBtn);this.startBtn.x = (GameConst.StageW - this.startBtn.width) / 2;this.startBtn.y = GameConst.StageH / 2 - 75;this.startBtn.setClick(this.onStartGameClick);//更多按钮设置this.moreBtn = new MyButton("btn_b", "btn_gengduo");this.moreBtn.x = (GameConst.StageW - this.startBtn.width) / 2;this.moreBtn.y =GameConst.StageH / 2 + 75;this.addChild(this.moreBtn);this.moreBtn.setClick(this.onMoreBtnClick);//文本let tex:egret.TextField = new egret.TextField();tex.width = GameConst.StageW;tex.textAlign = egret.HorizontalAlign.CENTER;tex.strokeColor = 0x403e3e;tex.stroke = 1;tex.bold = true;tex.y = GameConst.StageH / 2 + 250;tex.text = "Powered By ShenYSun";this.addChild(tex);}private onStartGameClick() {GameControl.Instance.onGameScenesHandler();}private onMoreBtnClick() {console.log("更多游戏");platform.GetInfo();}
}

点击startBtn按钮执行GameControl类的切换场景方法。

场景控制类

/**游戏管理 */
class GameControl extends egret.Sprite {private static _instance:GameControl;public static get Instance() {if(!GameControl._instance) {GameControl._instance = new GameControl();}return GameControl._instance;}/**当前场景 */private currentStage:egret.DisplayObjectContainer;//开始游戏private startGame:StartGameLayer;/**游戏场景 */private gameScenes:GameScenesLayer;/**结束场景 */private overScenes:GameOverLayer;/**背景 */private bgImg:egret.Bitmap;public constructor() {super();this.startGame = new StartGameLayer();this.gameScenes = new GameScenesLayer();this.overScenes = new GameOverLayer();}public setStageHandler(stage:egret.DisplayObjectContainer):void {/**设置当前场景的背景 */this.currentStage = stage;this.bgImg = GameConst.CreateBitmapByName("bg_jpg");this.bgImg.width = GameConst.StageW;this.bgImg.height = GameConst.StageH;//把背景添加到当期场景this.currentStage.addChild(this.bgImg);}/**开始游戏的场景 */public startGameHandler():void {if(this.gameScenes && this.gameScenes.parent) {GameConst.removeChild(this.gameScenes);}if(this.gameScenes && this.overScenes.parent) {GameConst.removeChild(this.overScenes);}this.currentStage.addChild(this.startGame);GameApp.xia.visible = true;}/**游戏场景 */public onGameScenesHandler():void {if(this.startGame && this.startGame.parent) {GameConst.removeChild(this.startGame);}if(this.overScenes && this.overScenes.parent) {GameConst.removeChild(this.overScenes);}this.currentStage.addChild(this.gameScenes);GameApp.xia.visible = false;}/**游戏结束场景 */public showGameOverSceneHandler():void{if(this.startGame && this.startGame.parent){GameConst.removeChild(this.startGame)}if(this.gameScenes && this.gameScenes.parent){GameConst.removeChild(this.gameScenes)}this.currentStage.addChild(this.overScenes);GameApp.xia.visible = true;}public getGameOverDisplay():GameOverLayer {return this.overScenes;}}

场景切换贯穿游戏全局,封装成类方便调用,以及后期扩展只需要加上新场景类的实例,便可以切换自如。

自定义按钮类

不难发现上面的开始游戏界面的按钮是MyButton类型,在MyButton类的构造函数中传入背景图和显示文字,创建出一个按钮。此类有一个设置点击事件的方法,按钮调用此公开方法传入触发事件即可设置点击事件。

/**自定义按钮类 */
class MyButton extends egret.Sprite {private _bg:egret.Bitmap;private title:egret.Bitmap;private onClick:Function;public constructor(bgName:string, titleName:string) {super();this._bg = GameConst.createBitmapFromSheet(bgName, "ui");this.addChild(this._bg);this.title = GameConst.createBitmapFromSheet(titleName, "ui");this.title.x = (this._bg.width - this.title.width) >> 1;this.title.y = (this._bg.height - this.title.height) >> 1;this.addChild(this.title);}//设置点击触发事件public setClick(func:Function):void {this.touchEnabled = true;this.addEventListener(egret.TouchEvent.TOUCH_TAP, this.onClickEvent, this);this.onClick = func;}//点击触发的事件private onClickEvent() {this.onClick();}public setTitle(title:string):void {this.title = GameConst.CreateBitmapByName(title);}public get bg() {return this._bg;}public set bg(bg:egret.Bitmap) {this._bg = bg;}
}

自定义特殊数字类

一般游戏中的分数、时间等数字组成的UI为了美观都会使用位图文本,但是当游戏逻辑跑起来需要不断的刷新游戏的分数,每次改变分数的时候都要从纹理集里面调用对应位图,在时间上是一个大大的浪费,所以创建一个特殊字符类SpecialNumber,让这个类替我们实现转换特殊字符。

具体代码如下:

/**特殊字符数字类 */
class SpecialNumber extends egret.DisplayObjectContainer {public constructor() {super();}public gap:number = 0;/**设置显示的字符串 */public setData(str:string):void {this.clear();if(str == "" || str == null) {return;}//把所有数字每一个都存进数组中let chars:Array<string> = str.split("");let w:number = 0;//所有的长度let length:number = chars.length;for(let i:number = 0; i < length; i++) {try {let image:egret.Bitmap = GameConst.createBitmapFromSheet(chars[i], "ui");if(image) {image.x = w;w += image.width + this.gap;this.addChild(image);}} catch (error) {console.log(error);}}this.anchorOffsetX = this.width / 2;}public clear() {while(this.numChildren) {this.removeChildAt(0);}}
}

在体验过游戏的时候会发现任务会根据不一样的墙体高度摆不一样的poss,这才poss全是来自于帧动画纹理集,只需要把对应一套的动画解析出来人物就会跳起舞来。下面是人物状态类。

状态机类

人物共有五个状态,其中一个是默认状态state为跳舞状态STAGE1,还有设置当前状态的方法setState

/**角色动作类 */
class Role extends egret.Sprite{//状态public static STATE1:number = 0;public static STATE2:number = 1;public static STATE3:number = 2;public static STATE4:number = 3;public static STATE5:number = 4;/**人物状态集合 */public static FRAMES:Array<any> = [["0020003", "0020004", "0020005", "0020006","0020007"],["0020008"],["0020009", "0020010"],["0020011", "0020012"],["xue0001", "xue0002", "xue0003", "xue0004", "xue0005"]]//身体private Body:egret.Bitmap;private state:number;private currFrames:Array<any>;private currFramesIndex:number = 0;private runFlag:number;private isLoop:boolean;public constructor() {super();this.Body = new egret.Bitmap;//人物初始状态this.Body = GameConst.createBitmapFromSheet("Role.FRAMES[0][0]", "Sprites");//设置锚点this.Body.anchorOffsetX = this.Body.width * 0.5;this.addChild(this.Body);}/**设置状态 */public setState(state:number) :void {this.state = state;//死亡状态if(this.state == Role.STATE5) {this.isLoop = false;this.Body.anchorOffsetY = this.Body.height * 0;}else{this.isLoop = true;this.Body.anchorOffsetY = this.Body.height * 1;}if(this.state == Role.STATE3 || this.state == Role.STATE4){this.currFrames = [];if(Math.random() > 0.5){this.currFrames.push(Role.FRAMES[this.state][0]);}else{this.currFrames.push(Role.FRAMES[this.state][3]);}}else{this.currFrames = Role.FRAMES[this.state];}this.currFramesIndex = 0;this.setBody();}private setBody() {this.Body.texture = GameConst.getTextureFromSheet(this.currFrames[this.currFramesIndex], "Sprites");this.Body.anchorOffsetX = this.Body.width * 0.5;if(this.state == Role.STATE5){this.isLoop = false;this.Body.anchorOffsetY = this.Body.height * 0;}else{this.isLoop = true;this.Body.anchorOffsetY = this.Body.height * 1;}}public run():boolean{this.runFlag ++;if(this.runFlag > 4){this.runFlag = 0;}if(this.runFlag != 0){return;}var gotoFrameIndex:number = this.currFramesIndex + 1;if(gotoFrameIndex == this.currFrames.length){if(this.isLoop){gotoFrameIndex = 0;}else{gotoFrameIndex = this.currFramesIndex;}}if(gotoFrameIndex != this.currFramesIndex){this.currFramesIndex = gotoFrameIndex;this.setBody();}return false;}public play():void{egret.startTick(this.run,this);this.runFlag = 0;}public stop():void{egret.stopTick(this.run,this);}
}

游戏场景

一切工作准备就绪,下面就是本文的重点-----游戏场景的搭建以及逻辑的实现。先看一下游戏内的主要内容

首先是蓝色的游戏背景,和开始游戏界面背景如出一辙不用更换,在场景管理的时候注意背景保留一下继续使用。

其次分数、关卡、上背景图等等这些只需要调用常量类的获取纹理集图片的方法调整位置即可实现。

最后重点介绍一下内容:

墙体生成和运动

人物运动和状态切换

分数和关卡数改变并记录最高分数

下面是重要代码片段

墙体生成和运动

墙体分别包括上半部分和下半部分

/**上部分墙体容器 */private topContianer:egret.Sprite;/**下部分墙体容器 */private bottomContianer:egret.Sprite;

容器内又包含了上下部分的墙体图片,上下边界线

/**上下墙体填充图 */private topSprite:egret.Sprite;private bottomSprite:egret.Sprite;/**上下边界线 */private topLine:egret.Shape;private bottomLine:egret.Shape;

把填充图和边界线加到容器内(以上边界为例)

this.topContianer = new egret.Sprite();this.addChild(this.topContianer);this.topSprite = new egret.Sprite();this.topContianer.addChild(this.topSprite);this.topContianer.addChild(this.topLine);

定义一个top和bottom范围区间,随机在舞台范围内取值。

let min:number = 150;let flag:boolean = false;let len:number = 8;let w:number = GameConst.StageW / len;        for(let i:number = 0; i < len; i++) {var h:number = min + Math.floor(Math.random() * 8) * 10;this.bottomRects.push(new egret.Rectangle(i * w, GameConst.StageH - h, w, h));h = GameConst.StageH - h; if (Math.random() < 0.2 || (!flag && i == len - 1)) {var index:number = Math.floor(Math.random() * this.spaceArr.length);h -= this.spaceArr[index];flag = true;}this.topRects.push(new egret.Rectangle(i * w, 0, w, h));}

这是随机取区域已经完成,不过都是理想的区域,并没有填充实际上的图片,下面写一个方法通过区域来填充背景墙。

private fullFront(bgSptite:egret.Sprite, rects:Array<egret.Rectangle>, isBottom:boolean = false):void {bgSptite.cacheAsBitmap = false;this.clearBg(bgSptite);var len:number = rects.length;for (var i:number = 0; i < len; i++) {var rec:egret.Rectangle = rects[i];var bitmap:egret.Bitmap;if (this.bgBitmaps.length) {bitmap = this.bgBitmaps.pop();} else {bitmap = new egret.Bitmap();bitmap.texture = this.bg;}bitmap.scrollRect = rec;bitmap.x = rec.x;bitmap.y = rec.y;bgSptite.addChild(bitmap);}}

关键代码bitmap.scrollRect = rec是把位图按照区域进行分割,显示对象的滚动矩形范围。显示对象被裁切为矩形定义的大小,当您更改 scrollRect 对象的 x 和 y 属性时,它会在矩形内滚动。

上下背景位图填充完毕,下面可是画上下边界线,同样是写了一个方法(以上边界为例),如下:

private drawLine():void {var lineH:number = 10;this.topLine.graphics.clear();this.topLine.graphics.lineStyle(lineH, 0x33E7FE);this.bottomLine.graphics.clear();this.bottomLine.graphics.lineStyle(lineH, 0x33E7FE);this.drawTopLine(lineH / 2);this.drawBottomLine(lineH / 2);this.topLine.graphics.endFill();this.bottomLine.graphics.endFill();}private drawTopLine(lineH:number):void {var len:number = this.topRects.length;for (var i:number = 0; i < len; i++) {var rec:egret.Rectangle = this.topRects[i];if (i == 0) {this.topLine.graphics.moveTo(rec.x, rec.height);this.topLine.graphics.lineTo(rec.x + rec.width, rec.height);} else {this.topLine.graphics.lineTo(rec.x, rec.height);this.topLine.graphics.lineTo(rec.x + rec.width, rec.height);}}}

此时,背景填充完毕,但墙体还不能运动。前面this.topContianer.y = -200;把上部分墙体的的纵轴设置在-200的位置,等到游戏开始执行Tween动画,使this.topContianer.y = 0,为了有更好的效果游戏开始延迟1.5s再调用墙体运动,Tween动画如下:

let self = this;setTimeout(function() {// self.shakeRun();//上面的模块往下运动egret.Tween.get(this.topContianer).to({"y":0}, 100).call(function():void {self.landOver();})}, 1500);

人物运动和状态切换

人物运动:给舞台添加点击事件,判断点击位置并移动。

/**点击事件 */
private onClick(e:egret.TouchEvent):void {let len:number = this.bottomRects.length;for(let i:number = 0; i < len; i++) {let rec:egret.Rectangle = this.bottomRects[i];if(e.stageX > rec.x && e.stageX < rec.x + rec.width) {this.setRolePos(i);break;}}
}

操作角色所在位置全部是根据上面定义的人物所在位置下标rolePosIndex的相对位置来决定的。

private setRolePos(index:number, offY:number = 17, offX:number = 0, isInit:boolean = false):void {if (!isInit) {//人物每次移动一个格子if (this.rolePosIndex > index) {index = this.rolePosIndex - 1;}else if (this.rolePosIndex < index) {index = this.rolePosIndex + 1;}}this.rolePosIndex = index;var rec:egret.Rectangle = this.bottomRects[index];//一次只移动一格this.role.x = rec.x + rec.width / 2 + offX;this.role.y = rec.y + offY;}

状态切换:

墙体运动完毕之后,通过人物所在位置下标找到上半部分墙体和下半部分墙体对应的位置的差值,并根据差值判断人物是否存活,如果存活应该表现出什么状态。

获取人物所在位置上下墙体的距离:

 privategetSpace():number{lettop:egret.Rectangle=this.topRects[this.rolePosIndex];letbottom:egret.Rectangle=this.bottomRects[this.rolePosIndex];returnGameConst.StageH-top.height-bottom.height;}

根据返回的距离差值判断人物的状态:

privatecheckState() {letspace:number=this.getSpace();if(space==0) {this.role.setState(Role.STATE5);} elseif(space==this.spaceArr[2]) {this.role.setState(Role.STATE4);} elseif(space==this.spaceArr[0]) {this.role.setState(Role.STATE3);} elseif(space==this.spaceArr[1]) {this.role.setState(Role.STATE2);}if(space==0) {this.setRolePos(this.rolePosIndex, -10, 4);}}

根据返回的距离判断游戏状态,若返回值为0,游戏结束;不为0,进入下一关:

/**检验这关结束主角是否存活 */privatecheckResult() {letspace:number=this.getSpace();letself=this;if(space==0) {this.dieNum++;if(this.dieNum==1) {this.role.stop();setTimeout(function() {//游戏结束GameControl.Instance.getGameOverDisplay().setGameOverDataHandler(self.score, self.curretMaxScore); GameControl.Instance.showGameOverSceneHandler();}, 500);return;}}//进入下一关else{this.curretLevel++;this.score+=10;if(this.score>this.curretMaxScore) {this.curretMaxScore=this.score;}//刷新成绩this.refreshScore();}setTimeout(function() {self.refurbish()}, 1000);}

分数和关卡

接着上一步此时如果人物存活进入下一关,那么就要刷新游戏成绩和关卡数,并检验此时是否为最高成绩:

/**刷新成绩数据 */
privaterefreshScore() {
this.LvNum.setData(this.curretLevel.toString());
this.recodeNum.setData(this.score.toString());
}

游戏进入下一关卡:

 /**刷新游戏关卡 */privaterefreshPoint() {this.initData();this.start();}

游戏结算界面

游戏结算界面效果图

和开始界面差不多,有所不同的是需要从游戏场景中传入本局分数和做高分数,在这个页面写一个公开的setGameOverDataHandler方法,游戏结束是调用此方法传入数值。

/**游戏结束页面分数最高分数 */
publicsetGameOverDataHandler(score:number=0, maxScore:number=0):void{
this.scoreNum.setData(score.toString());
this.maxScore.setData(maxScore.toString());
}

扩展

墙体晃动效果

游戏场景内当墙体要下落的时候墙体会晃动一下,晃动墙体不仅提醒玩家这个墙体即将下落,同时也增加了这个游戏的可玩性,下面是控制墙体晃动的Shake类。

/**墙体晃动 */
classShake{
privateinitY:number;
privateshakeNum:number;
privateoverFunc:Function;
privateobj:egret.DisplayObject;
privatenum:number;
privateflag:number;
​
publicrun(obj:egret.DisplayObject, shakeNum:number, overFunc:Function=null) {
this.obj=obj;
this.initY=obj.y;
this.shakeNum=shakeNum;
this.overFunc=overFunc;
egret.startTick(this.loop, this);
this.num=0;
this.flag=0;
}
privateloop():boolean{
if(this.flag==0) {
if(this.obj.y<=this.initY) {
this.obj.y+=5;
} else{
this.obj.y-=5;
}
if(this.obj.y==this.initY) {
this.num++;
if(this.num==this.shakeNum) {
egret.stopTick(this.loop, this);
if(this.overFunc) {
this.overFunc();
}
}
}
}
this.flag++;
if(this.flag==2) {
this.flag=0;
}
returnfalse;
}
}

小结
本文通过对一个简单小游戏进行模块化的分析,并介绍了模块化的好处,降低这些模块之间的耦合度,后期如果要加新功能对旧的代码无需进行大的修改。

密室逃生Egret游戏教程相关推荐

  1. egret游戏入门之学习资源篇

    最近因需要,入手H5游戏. 写游戏当然需要有引擎. H5游戏开发:游戏引擎入门推荐 如何选择 H5 游戏引擎 白鹭引擎和layabox哪个好用,哪个技术更成熟 ? LayaBox 与 Egret 选择 ...

  2. egret 菜鸟教程| 密室逃生游戏实战

    这次给大家带来的是通过Egret实现密室逃生小游戏的教程.该游戏包括人物状态机.MVC设计模式和单例模式,该游戏在1.5s内通过玩家点击操作寻找安全点,方可进入下一关,关卡无限,分数无限.下面是具体的 ...

  3. 密室逃生游戏【C语言】

    /* [字符串 逻辑分析] 小强在参加<密室逃生>游戏,当前关卡要求找到符合给定密码K(升序的不重复小写字母组成)的箱子,并给出箱子编号,箱子编号为1~N. 每个箱子中都有一个字符串s,字 ...

  4. 华为OD机试 - 密室逃生游戏(Python)

    密室逃生游戏 题目 小强增在参加<密室逃生>游戏,当前关卡要求找到符合给定 密码 K(升序的不重复小写字母组成) 的箱子, 并给出箱子编号,箱子编号为 1~N . 每个箱子中都有一个 字符 ...

  5. 【华为OD机试真题 python】密室逃生游戏【2022 Q4 | 100分】

    ■ 题目描述 [密室逃生游戏] 小强增在参加<密室逃生>游戏,当前关卡要求找到符合给定 密码K(升序的不重复小写字母组成) 的箱子, 并给出箱子编号,箱子编号为 1~N . 每个箱子中都有 ...

  6. 【100%通过率】华为OD机试C++/python【密室逃生游戏】【2022.11 Q4 新题|100分】

    华为OD机试- 题目列表 2023Q1 点这里!! 2023华为OD机试-刷题指南 点这里!! 题目描述 小强在参加<密室逃生>游戏,当前关卡要求找到符合给定 密码K(升序的不重复小写字母 ...

  7. 华为OD机试真题Java实现【密室逃生游戏】真题+解题思路+代码(20222023)

    密室逃生游戏 小强增在参加<密室逃生>游戏,当前关卡要求找到符合给定 密码K(升序的不重复小写字母组成) 的箱子, 并给出箱子编号,箱子编号为 1~N . 每个箱子中都有一个 字符串s , ...

  8. 华为OD机试 - 密室逃生游戏(Java JS Python)

    题目描述 小强正在参加<密室逃生>游戏,当前关卡要求找到符合给定 密码K(升序的不重复小写字母组成) 的箱子,并给出箱子编号,箱子编号为 1~N . 每个箱子中都有一个 字符串s ,字符串 ...

  9. 【华为OD机试 2023最新 】 密室逃生游戏(C++ 100%)

    题目描述 小强在参加<密室逃生>游戏,当前关卡要求找到符合给定 密码K(升序的不重复小写字母组成) 的箱子,并给出箱子编号,箱子编号为 1~N . 每个箱子中都有一个 字符串s ,字符串由 ...

最新文章

  1. dos2unix install on mac_mac2019新品唇釉来啦!MAC有色唇油试色
  2. Binutils工具集 GCC工具集介绍
  3. 使用OpenCV进行图像创建、保存和复制
  4. 【Android Studio】分类整理res/Layouts中的布局文件(创建子目录)
  5. (JAVA)String类之比较方法
  6. TYVJ P1012 火柴棒等式 Label:枚举
  7. kafka发送mysql数据丢失_Kafka 如果丢了消息,怎么处理的?
  8. 20-10-025-安装-KyLin-2.6.0-单机版安装(MAC官网下载)成功
  9. Linux里sra文件是什么,prefetch命令下载SRA文件
  10. 通达oa系统怎么转移到服务器,通达OA升级心通达OA操作步骤规范
  11. 中医针灸学综合练习题库【10】
  12. 历史的天空-【1】配置
  13. 什么是路由器的光模块-华为路由器
  14. c# DGV导出excel 使用object类型数组,解决string类型需双击后或分列才可运算的异常
  15. Android息屏与亮屏
  16. 百度TTS,支持离线环境下使用
  17. 【host文件的用法】windows的hosts文件
  18. 通信工程与计算机考研学校排名,2019-2020信息与通信工程专业考研学校排名
  19. Tkinter之pack所有属性详解
  20. 6-1健壮性与正确性

热门文章

  1. win10 实用快捷键
  2. pdfjs 插件进行 自定义 遮罩 打印
  3. Unity 关于艺术字用法实现
  4. php发送邮件验证码,PHP如何发送邮件来进行用户验证
  5. Condition 是什么?怎么用?
  6. 安装Visio与已安装的office冲突的解决方案
  7. thymeleaf frame 局部刷新_明日方舟公开招募标签强制刷新 黑赫拉格加入
  8. TrackPopupMenu和DoModal
  9. 电脑上jpg图片怎么改大小kb?如何缩小jpg文件大小?
  10. java中sql查询语句_JAVA中用 SQL语句操作小结