小游戏《俄罗斯方块》开发
前述
《俄罗斯方块》这款游戏大家应该都不陌生吧,以前的老爷手机上都会内置这款游戏,本篇我们一起使用白鹭引擎开发一款简易版的《俄罗斯方块》小游戏。
演示地址:点击查看
开始
运行效果:
(说明:帧数做了删减)
界面中,中间是游戏界面,底部是三个操作按钮,右边是游戏信息展示。图形下落过程中,玩家可以操作按钮控制图形。
首先将相应图片资源复制一份到我们的项目中,然后回到编辑器头部的资源,弹出添加提示,点击添加,这样资源配置信息就会自动添加到 default.res.json 中。
设计游戏主界面
设计好的皮肤文件效果:
首先我们将界面大小设置为 400 * 500 ,界面上添加相关控件:图形方块容器 scrollBox
,下一个图形方块预览容器 nextShapeBox
,分数显示控件 scoreLabel
,底部三个操作按钮 leftBtn
、rotateShapeBtn
、rightBtn
(已开启触摸监听)。
新建ts文件 Pannel.ts
,创建类 Pannel
,将皮肤引入,绑定自定义事件:
// Pannel.ts
class Pannel extends eui.Component {public scrollBox: eui.Group;public nextShapeBox: eui.Group;public leftBtn: eui.Button;public rotateShapeBtn: eui.Button;public rightBtn: eui.Button;public scoreLabel: eui.Label;// 分数 private _score: number = 0;public constructor() {super();this.skinName = "resource/skins/Pannel.exml";this.event();}private event() {const LeftEvent:MainEvent = new MainEvent(MainEvent.Left);const RightEvent:MainEvent = new MainEvent(MainEvent.Right);const RotateShapeEvent:MainEvent = new MainEvent(MainEvent.RotateShape);/**点击按钮'左边' */this.leftBtn.addEventListener(egret.TouchEvent.TOUCH_TAP, () => {this.dispatchEvent(LeftEvent);}, this);/**点击按钮'右边' */this.rightBtn.addEventListener(egret.TouchEvent.TOUCH_TAP, () => {this.dispatchEvent(RightEvent);}, this);/**点击按钮'翻转' */this.rotateShapeBtn.addEventListener(egret.TouchEvent.TOUCH_TAP, () => {this.dispatchEvent(RotateShapeEvent);}, this);}}
当点击底部三个按钮,触发对应的自定义事件,然后在父层上监听事件:
// Main.ts
this.pannelUI = new Pannel();
this.pannelUI.addEventListener(MainEvent.Left, this.translateAction, this);
this.pannelUI.addEventListener(MainEvent.Right, this.translateAction, this);
this.pannelUI.addEventListener(MainEvent.RotateShape, this.rotateShape, this);
自定义事件类 MainEvent
的实现:
// MainEvent.ts
class MainEvent extends egret.Event {/**往左边移动 */public static Left:string = '左移';/**往右边移动 */public static Right:string = '右移';/**图形翻转 */public static RotateShape:string = '图形翻转';/**重新开始 */public static Restart:string = '重新开始';private _resName: string = ""; public constructor(type:string, resName:string="", bubbles:boolean=false, cancelable:boolean=false) {super(type, bubbles, cancelable);this._resName = resName;}public get resName(): string {return this._resName;}
}
当点击左右按钮时,触发的回调方法 translateAction
中,我们通过属性 type
来确定是左移还是右移 :
// Main.ts
private translateAction(event: MainEvent): void {if (event.type === '左移') {this.translateXShape(-1);} elseif (event.type === '右移') {this.translateXShape(1);}
}
最后我们给游戏主容器添加一个黑色矩形边框
// Pannel.ts
// 给主容器添加一个矩形边框
const shp:egret.Shape = new egret.Shape();
shp.graphics.lineStyle( 2, 0xffffff );
shp.graphics.beginFill( 0x000000, 1);
shp.graphics.drawRect( 0, 0, this.scrollBox.width, this.scrollBox.height);
shp.graphics.endFill();
this.scrollBox.addChild( shp );
设计重新开始界面
设计好的皮肤文件效果:
首先我们将界面大小设置为 400 * 500 ,界面上添加相关控件:先添加一个透明度为0.8
、背景颜色为黑色的矩形,然后再添加按钮“来一局”(已开启触摸监听)。
新建ts文件 Restart.ts,创建类 Restart ,将皮肤引入,绑定自定义事件:
// Restart.ts
class Restart extends eui.Component {public restart: eui.Button;public constructor() {super();this.skinName = "resource/skins/Restart.exml";this.event();}private event() {const RestartEvent:MainEvent = new MainEvent(MainEvent.Restart);/**点击按钮 `再来一局` */this.restart.addEventListener(egret.TouchEvent.TOUCH_TAP, () => {this.dispatchEvent(RestartEvent);}, this);}
}
这里提下,如果编译后提示如:Duplicate identifier 'xxx'
等,如果没有问题怎么也通过不了,此时我们先运行 egret clean
清除 ,然后重新编译。
当点击按钮“再来一局”,触发对应的自定义事件,然后在父层上监听事件:
// Main.ts
this.restartUI = new Restart();
this.restartUI.addEventListener(MainEvent.Restart, this.start, this);
方块图形
俄罗斯方块一共有七种形状,如图:
设置方块坐标
本文我们就讲解第一种图形,其它图形读者可参考下面内容自行研究。
第一种方块图形的在坐标轴上的表示:
上图我们在坐标轴上绘制了第一种形状,由四个格子组成,每个格子的起点为:A(1 , 0)、B(1, 1)、C(1, 2)、D(0, 2)。(这里每个格子的长度为单位长度)
这样我们就获取到第一种图形的起点坐标:shapeArr = [[1, 0],[1, 1],[1, 2],[0, 2]]。
格子 A 的起点坐标转为实际坐标:
x = Main.Gridsize * shapeArr[0][0];
y = Main.Gridsize * shapeArr[0][1];
Main.Gridsize
是每个正方形格子的大小。
然后我们将格子 x
轴方向居中放置到容器 this.pannelUI.nextShapeBox
中。x轴上居中的位置坐标:shapeX = this.pannelUI.scrollBox.width / 2,y轴: shapeY = Main.Gridsize * 2。
此时格子 A 的起点坐标转为实际坐标:
x = Main.Gridsize * shapeArr[0][0] + shapeX;
y = Main.Gridsize * shapeArr[0][1] + shapeY;
这里 x 值最终要等于值 shapeX。 但由于格子 A 起点坐标为 (1, 0),并不满足需求。这里我们直接将格子 A 起点坐标的 X 轴方向往左移动一个单位距离,转换后坐标为: (0, 1)。
故该形状的坐标表示变为:shapeArr = [[0, 0],[0, 1],[0, 2],[-1, 2]]。
整理后实现转换实际坐标方法:
// Main.ts
private transitionCoordinate(shapeArr, shapeX, shapeY) {const arr = [];for (let i = 0; i < shapeArr.length; i++) {arr.push([ Main.Gridsize * shapeArr[i][0] + shapeX, Main.Gridsize * shapeArr[i][1] + shapeY]);}return arr;
}
添加方块
获取到图形的实际坐标后,我们在父层添加图形。图形是由 20 * 20 大小的格子组成,格子的图片资源为 rect_png
。
一个格子:
grid = Util.createBitmapByName('rect_png');
在辅助类 Util
中我们封装了获取资源位图的方法。
因为所有的方块都是由 grid
组成,随着游戏的持续,生成的格子对象越来越多,会影响性能。我们有必要从回收池中获取格子对象。
从回收池中获取格子:
// Main.ts
private getGrid():egret.Bitmap {let grid;if (this.poolList.length) {// 取出队列的最后一个grid = this.poolList.pop();} else {grid = Util.createBitmapByName('rect_png');}return grid;
}
属性 this.poolList
是格子回收池列表。 获取一个格子时,先从列表中取,没有的话再实例一个格子对象。
从显示对象列表中移除格子:
// Main.ts
private destroyGrid(grid: egret.Bitmap, layer:eui.Group) {layer.removeChild(grid);this.poolList.push(grid);
}
当在父容器上的格子移除后,放回到回收池中。
然后根据坐标绘制图形:
// Main.ts
/**绘制图形 */
private drawShape(shape?:any, layer?: eui.Group): void {const shapeObject = shape || this.nowShape;const container = layer || this.pannelUI.scrollBox;const arr = this.transitionCoordinate(shapeObject.data, shapeObject.x, shapeObject.y);for (let i = 0; i < arr.length; i++) {const grid = this.getGrid();grid.x = arr[i][0];grid.y = arr[i][1];grid.name = 'grid' + '_' + shapeObject.index;container.addChild(grid);}
}
方法内 this.nowShape
是当前要添加的图形属性。参数 shape
是要添加的图形属性,参数layer
是图形容器。
一个图形的属性对象组成:
// Main.ts
// 默认Y轴超出容器范围,x轴居中
this.nowShape = {x: this.pannelUI.scrollBox.width / 2,y: -40,shapeIndex: this.nextShapeIndex,index: this.index,data: JSON.parse(JSON.stringify(this.shapeList[this.nextShapeIndex]))
};
属性 this.shapeList
是方块形状的集合,属性 this.nextShapeIndex
是下一个要添加的方块形状索引。
创建一个新的图形方法如下:
// Main.ts
private createNewShape(): void {this.nowShape = {...}this.index ++;// 随机赋值下一个方块形状索引this.nextShapeIndex = Math.floor(Math.random() * this.shapeList.length);// 将下一个图形添加到预览容器中const nextShape = {x: 40,y: 40,index: 0,data: JSON.parse(JSON.stringify(this.shapeList[this.nextShapeIndex]))};this.clearNextShape();this.drawShape(nextShape, this.pannelUI.nextShapeBox);// 添加心跳监听,图形不断往下移动egret.startTick(this.translateYShape, this);
}
创建一个图形的流程:我们先设置好当前要添加的图形属性,然后随机设置下一个图形的类型索引,再将下一个图形添加到预览容器中。最后添加心跳监听,让当前方块不断往下移动。
清除预览容器内的格子方法 this.clearNextShape
内,我们调用方法 this.clearShape
。
清除父层上所有的子对象,我们可直接调用 this.removeChildren
方法。但因为我们需要将界面上要移除的格子对象保存到回收池中,所以需要先获取父层上的格子对象。
// Main.ts
/**获取指定容器中格子对象列表 */
private getGridFromLayer(layer: eui.Group, index?: number): egret.Bitmap[] {let arr = [];for (let i = 0; i < layer.numChildren; i++) {const grid = layer.getChildAt(i);if (grid) {if (typeof index === 'undefined') {if (grid.name.indexOf('grid') > -1) {arr.push(grid);}} elseif (grid.name === ('grid_'+index) ) {arr.push(grid);}}}return arr;
}
获取到格子列表后,再移除:
// Main.ts
/**清除图形显示容器的当前图形 */
private clearShape(layer: eui.Group, index?: number):void {let gridArr = this.getGridFromLayer(layer, index);let grid;while(gridArr.length) {grid = gridArr.shift();this.destroyGrid(<egret.Bitmap>grid, layer);}
}
方块往下移动
方块添加到容器后,就不断往下移动,移动速度越快,游戏难度就越高。
首先定义时间阈值:
// Main.ts
private timeNum: number = 0;
// 移动速度,阈值
private timeMax = 300;
private time = 0;
添加一个新的方块后,就会监听心跳,执行回调方法 this.translateYShape
,实现下降速率控制:
// Main.ts
// timeStamp 是心跳回调时的时间戳
private translateYShape(timeStamp:number): boolean {const now = timeStamp;const time = this.time;const pass = now - time;this.timeNum += pass;// 超出阈值if (this.timeNum > this.timeMax) {this.timeNum = 0;// 更新当前图形Y轴值,重绘当前图形this.clearShape(this.pannelUI.scrollBox, this.nowShape.index);this.nowShape.y += Main.Gridsize;this.drawShape();}this.time = now;return false;
}
执行后,方块就不断往下移动,一直超出游戏场景范围。我们希望方块碰到容器底部后,就停止运动,然后新增一个方块到容器,假设我们已经实现了检测方块能否继续往下移动方法,修改如下:
// Main.ts
private translateYShape(timeStamp:number): boolean {//其它代码省略......// 检测方块是否可以继续运行const checkedBool = this.checkYBoundary();if (checkedBool) {// 更新当前图形Y轴值,重绘当前图形this.clearShape(this.pannelUI.scrollBox, this.nowShape.index);this.nowShape.y += Main.Gridsize;this.drawShape();} else {// 停止心跳监听egret.stopTick(this.translateYShape, this);// 新增下一个图形方块到容器this.createNewShape();}//其它代码省略......
}
接下我们实现方法 this.checkYBoundary
。
方块是由多个大小相同的小格子组成的,每次往下移动都是一个格子大小。判断方块能否继续往下移动,需要检查三个情况:
- 当前方块中有个小格子的 Y 轴已经在整个容器最底部,那就不能往下移动;
- 当前方块中有个小格子,如果下个移动位置已经被占用,那就不能往下移动;
- 如果满足条件2后,如果此时有个小格子超出顶部位置,那么游戏结束;
在次之前,我们将容器分隔成多个小格子(容器大小设置成可以被 Main.Gridsize
整除):
// Main.ts
private createMatrix():void {// grids[i] Y轴 grids[i][j] X轴,注意我们的坐标轴是左上角开始,往下是Y轴,往右是X轴this.grids = <any>[];for (let i = 0; i < this.pannelUI.scrollBox.height / Main.Gridsize; i++) {this.grids[i] = <any>[];for (let j = 0; j < this.pannelUI.scrollBox.width / Main.Gridsize; j++) {this.grids[i][j] = false;}}
}
初始时,我们给每个格子的值都设置为 false ,表示没有被占用。游戏每次开始前,运行上面方法。
有了上面的准备,我们实现方法 checkYBoundary
。
首先获取当前方块(移动中)的所有小格子实际坐标,然后再获取每个小格子在容器格子集合( this.grids
)的位置:
// Main.ts
private checkYBoundary() : boolean {let bool = true;// 获取当前方块的小格子的实际坐标,const arr = this.transitionCoordinate(this.nowShape.data, this.nowShape.x, this.nowShape.y);for (let i = 0; i < arr.length; i++) {// xNum,yNum 是方块的小格子在容器的位置索引const xNum = arr[i][0] / Main.Gridsize;const yNum = arr[i][1] / Main.Gridsize;}return bool;
}
在循环体中,先判断是否已经在最底部:
// Main.ts
// 注意:坐标轴的原点是左上角
if (yNum === (this.grids.length - 1)) {bool = false;break;
}
再判断下一步是否被占用:
// Main.ts
if ( (typeof this.grids[yNum + 1] !== 'undefined') && this.grids[yNum + 1][xNum]) {bool = false;break;
}
最后再判断此时的方块是否还未进入容器(每次新增的方块 Y 轴值都是负值):
// Main.ts
if ( (typeof this.grids[yNum + 1] !== 'undefined') && this.grids[yNum + 1][xNum]) {if (yNum === -1) {// 游戏结束this.restart();}bool = false;break;
}
一旦游戏结束就不能再自动新增图形,所以我们定义了属性 isPause
,标记游戏是否暂停。
方法 restart
:
// Main.ts
private restart(): void {console.log('游戏结束')this.isPause = true;egret.stopTick(this.translateYShape, this);this.addChild(this.restartUI);
}
重写修改方法 translateYShape
:
// Main.ts
private translateYShape(timeStamp:number): boolean {// 省略其它代码......if (!this.isPause) {const checkedBool = this.checkYBoundary();if (checkedBool) {// 省略} else {// 省略 }}// 省略
}
上面我们实现了方块下降和下降的检测。当停止下降后,我们需要将容器的相应格子标记为占用,如果此时某行都被占用后就得销毁同时增加分数。
当停止下降后,我们执行方法 this.drawWall
,再次改造方法 translateYShape
:
// Main.ts
private translateYShape(timeStamp:number): boolean {// 省略其它代码......if (!this.isPause) {const checkedBool = this.checkYBoundary();if (checkedBool) {// 省略} else {this.drawWall();// 省略 }}// 省略
}
分数
方块停止下降后,当检测到某行全被占用,就更新分数,销毁该行的小格子,下面我们讲解如何实现。
方块停止下降后,我们需要把方块所在的容器小格子标记为已占用,跟方法 checkYBoundary
一样,我们先获取当前图形的坐标信息,然后再获取索引,最后标记。
我们实现方法 drawWall
。
// Main.ts
private drawWall():void {const arr = this.transitionCoordinate(this.nowShape.data, this.nowShape.x, this.nowShape.y);let i = 0;try {for (i = 0; i < arr.length; i++) {const yNum = arr[i][1] / Main.Gridsize;const xNum = arr[i][0] / Main.Gridsize;//停止下降后,此时要把所在的格子标志为已占用(true)this.grids[yNum][xNum] = true;}} catch (error) {this.restart();}
}
然后检测某行都被占用的格子,设置分数,并重新赋予每个格子的值(最终表现为堆叠的格子整体下降了):
// Main.ts
// 方法 drawWall
for (i = 0; i < this.grids.length; i++) {// 当前循环的行是否满格,值默认占满let mark = true;// 循环某个行,该行上的所有小格子都被占用for (let k = 0; k < this.grids[i].length; k++) {if (!this.grids[i][k]) {mark = false;break;}}if (mark) {this.changeScore();}
}
当检测到满格时,就更新分数:
// Main.ts
private changeScore(score?:number): void {if (typeof score === 'undefined') {this.score += 1;} else {this.score = score;}this.pannelUI.score = this.score;
}
我们在类 Pannel
里新增更新分数方法:
// Pannel.ts
public get score(): number {return this._score;
}/**设置分数 */
public set score(score: number) {this._score = score;this.scoreLabel.text = this._score + '分';
}
分数更新完毕后,接下来将该行以上占用格子往下移动,我们只需将前一行值赋予当前行,循环赋予即可。
// Main.ts
// 方法 drawWall
if (mark) {this.changeScore();// i 是此时占满格子的行所在的索引for (let j = i; j > 0; j--) {for (let h = 0; h < this.grids[i].length; h++) {// 将上一行的占用值赋予当前行this.grids[j][h] = this.grids[j-1][h];}}
}
然后重新绘制被占用的格子,先清除再绘制:
// Main.ts
// 方法 drawWall
this.clearShape(this.pannelUI.scrollBox);
// 绘制已被占的格子
for (let g = 0; g < this.grids.length; g++) {for (let s = 0; s < this.grids[g].length; s++) {if (this.grids[g][s]) {const grid = this.getGrid();grid.x = s * Main.Gridsize;grid.y = g * Main.Gridsize;this.pannelUI.scrollBox.addChild(grid);}}
}
小结
本小结讲解了方块的形状坐标表示,添加方块,往下移动方块,检测Y轴移动、分数的设置。
操作方块
游戏界面上设计了三个按钮,分别为:左移、翻转、右移,这小结我们实现这三个功能。
- 左右移动
左右移动实现是一样的,我们在父层已经监听了点击左右移动的事件,执行回调方法 translateAction
:
// Main.ts
private translateAction(event: MainEvent): void {if (event.type === '左移') {this.translateXShape(-1);} elseif (event.type === '右移') {this.translateXShape(1);}
}
跟Y轴下降一样,每次左右移动的距离都是属性值 Main.Gridsize
的 n 倍。
// Main.ts
// num 负往左边移动;正往右边移动
private translateXShape(num: number): void {this.nowShape.x += Main.Gridsize * num;this.clearShape(this.pannelUI.scrollBox, this.nowShape.index);this.drawShape();
}
当不断点击往左或往右后,方块就超出容器范围,我们希望当处于边界时,不能移动,假设已经实现了x轴左右移动检测方法 this.checkXBoundary
, 上面重新改造:
// Main.ts
// num 负往左边移动;正往右边移动
private translateXShape(num: number): void {// x轴可左右移动检测if (this.checkXBoundary()) {this.nowShape.x += Main.Gridsize * num;this.clearShape(this.pannelUI.scrollBox, this.nowShape.index);this.drawShape();}
}
- 翻转
跟左右按钮一样,我们也实现监听回调,先放代码:
private rotateShape():void {// 田字图形无需翻转if (this.nowShape.shapeIndex === 5) {return;}const data = this.nowShape.data;const temp = [];for (let i = 0; i < data.length; i++) {// 关键代码temp.push([data[i][1], -data[i][0] ]);}this.nowShape.data = temp;this.clearShape(this.pannelUI.scrollBox, this.nowShape.index);this.drawShape();
}
上面我们实现了图形绕原点逆时针旋转,读者可能对这部分不是很清楚,接下来我们简单推导下公式。
我们先看在直角坐标系上一个点围绕原点逆时针旋转的效果图:
现在问题为:已知旋转前点的坐标为 P,方向角度为 a ,绕原点的旋转角度为 b ,点到原点的距离为 R,求旋转后点的坐标 P’ 。
根据三角函数,我们可以得出坐标 P’ :
x' = Rcos(a+b)
y' = Rsin(a+b)
根据和差公式,得到如下等式:
x' = Rcos(a)cos(b) - Rsin(a)sin(b) y' = Rsin(a)cos(b) + Rcos(a)sin(b)
观察上式,Rcos(a) = x, Rsin(a) = y, 带入等式:
x' = xcos(b) - ysin(b)y' = xsin(b) + ycos(b)
我们每次翻转都是 90° ,因为 cos(90) = 0, sin(90) = 1 带入上面公式,得出:
x' = -y y' = x
上面公式是通过直角坐标系得出,我们的画布坐标轴值是相反的,故:
x' = -(-y) = y y' = (-x) = -x
这里顺便提下,因为 π 精度和浮点运算问题, js根据角度计算sin和cos值的计算方式可如下:
//角度
var vAngle=90;
//正弦值
var vSin= Math.round(Math.sin((vAngle * Math.PI/180)) * 1000000) / 1000000;
//余弦值
var vCos= Math.round(Math.cos((vAngle * Math.PI/180)) * 1000000) / 1000000;
实现了逆时针翻转后,我们需要再次检测边界问题,重新改造方法 rotateShape
:
// Main.ts
private rotateShape():void {// 代码省略......if (this.checkXBoundary()) {// 通过后才重新绘制,代码省略......}
}
x轴左右移动检测有三种情况是无法通过的:
- 超出容器左边;
- 超出容器右边;
- 碰到被占用的格子;
我们在方法 checkXBoundary
中实现三种情况的检测。
先获取组成当前方块的小格子坐标,获取所在容器的位置:
// Main.ts
private checkXBoundary(): Boolean {let bool = true;const arr = this.transitionCoordinate(this.nowShape.data, this.nowShape.x, this.nowShape.y);try {for (let i = 0; i < arr.length; i++) {const xNum = arr[i][0] / Main.Gridsize;const yNum = arr[i][1] / Main.Gridsize;// 实现条件判断}}catch(e) {}return bool;
}
判断超出左边边界:
// Main.ts
private checkXBoundary(): Boolean {// 省略代码......if (xNum < 0) {bool = false;break;}
}
判断超出右边边界:
// Main.ts
private checkXBoundary(): Boolean {// 省略代码...... this.grids[0].length 取第一行的格子数if (xNum > this.grids[0].length - 1) {bool = false;break;}
}
判断碰到被占用的格子:
// Main.ts
private checkXBoundary(): Boolean {// 省略代码......if (this.grids[yNum][xNum]) {bool = false;break;}
}
我们在翻转图形时,如果已经在边界,或者左右两边已经有被占用的格子,此时不能翻转:
// Main.ts
private rotateShape():void {// 省略代码......、// 只有检测通过才能重绘实现翻转效果if (this.checkXBoundary()) {this.clearShape(this.pannelUI.scrollBox, this.nowShape.index);this.drawShape();}
}
假如翻转超出了左右,我们也允许翻转的话,那么此时应该将图形往左移或右移,具体实现请读者参考示例。
小结
本小结讲解了方块的左右移动控制,x轴移动检测,图形翻转的实现。
最后
本篇我们从头讲解了一个简单俄罗斯方块游戏的实现,希望读者能够学到使用白鹭引擎开发小游戏。本篇中方块图形一共有7种,希望读者能够按照教程自行推出坐标集,另外上面实现图形翻转效果也可以进行改进,这些请读者自行实现,本篇不再细述。
小游戏《俄罗斯方块》开发相关推荐
- 鸿蒙小游戏-俄罗斯方块
作者:225王宗振 前言 为了更好地熟练掌握鸿蒙手机应用开发,查阅资料和算法尝试开发鸿蒙小游戏--俄罗斯方块. 概述 完成鸿蒙小游戏APP在手机上的编译在项目中所使用到的软件为DevEco Studi ...
- 关于2048小游戏的开发感想
最近通过python实现2048小游戏的开发,在开发过程中遇到了一些问题,发现了自己的不足之处,特此进行简单的反思. 一.状态机: 状态机是表示有限个状态以及这些状态之间转移和动作等行为的数学模型,状 ...
- 基于Unity的2D小游戏 SpeedDown 开发笔记(学习bilibili@[M_Studio]的教学视频
基于Unity的2D小游戏 SpeedDown 开发笔记(学习bilibili@M_Studio的教学视频) 主要内容:在Sunnyland游戏的设计基础上,新增了物理组件Joint系列.DrawGi ...
- 微信小游戏客户端开发环境搭建
微信小游戏客户端开发环境搭建 开发工具 环境配置 发布小游戏 一直以来,弄App形式的游戏比较多,近年来,微信小游戏火了起来.出于好奇,研究了一番,觉得还是挺有意思的,想和大家分享下. 官方手册网址: ...
- Stack Ball 堆栈球小游戏unity3d开发教程
Stack Ball 堆栈球小游戏unity3d开发教程 介绍 <Stack Ball>是一款3D街机游戏,玩家需要通过旋转的螺旋平台来打碎.撞击和弹跳,以达到终点. 听起来很容易?你可错 ...
- 原生js小游戏——俄罗斯方块
还记得童年时期的小游戏俄罗斯方块吗?我发现用js就可以写出来 页面效果如下: 具体代码如下: 首先展示css样式: .c {margin: 1px;width: 19px;height: 19px;b ...
- 基于cocoCreator版本2.4.5整理一款2D小游戏快速开发的游戏框架
前言:基于cocoCreator版本2.4.5整理一款2D小游戏快速开发的游戏框架. 一.cocosCreator的UI框架. 中心思想, 将所有的UI窗体分为3类管理(1级窗体, 2级窗体, 3级窗 ...
- 微信小游戏云开发数据库
关于微信小游戏云开发数据库的使用 初始化云开发 // 可以传入一个默认使用的环境名称 wx.cloud.init() 初始化数据库 // 这里的环境参数应传入云开发数据库中对应环境id const d ...
- python tkinter火柴人_用Python实现童年小游戏俄罗斯方块!别说还挺好玩!
原标题:用Python实现童年小游戏俄罗斯方块!别说还挺好玩! 前言 大三上学期的程序设计实训大作业,挑了其中一个我认为最简单的的<图书管理系统>来写.用python写是因为py有自带的G ...
- CocosCreator微信小游戏排行榜开发
CocosCreator微信小游戏排行榜开发 开发前言 步骤1 开发前言 第一次接触微信小游戏开发的小伙伴,可以看看排行榜的开发和注意事项: 一.开发微信排行榜的话,用户的信息需要从微信提供的API获 ...
最新文章
- IOS开发UISearchBar失去第一响应者身份后,取消按钮不执行点击事件的问题
- 预计2020年传感器需求超一万亿个
- 北京点击科技有限公司董事长兼总裁——王志东经典语录4
- 51nod 1092 回文字符串
- Windows下使用MinGw和gcc构建第一个C程序、g++构建第一个C++程序
- 迁移上云方法论-6R
- Common Used Excel Formulas
- leetcode55. 跳跃游戏
- 如何赛筛选出多列内容相同的数据??
- 简单实用的多线程学习实例
- [UE4] Load Class Assest 返回 Null 的解决方法:在资产路径后面加 _C
- Python URL编码
- mongodb数据库安装和启动及操作笔记
- 【测试】软件测试之测试用例的设计方法
- qq2018旧版本7.3.1下载_qq下载2020最新版下载安装-qq2020最新版下载V8.4.8-西西软件下载...
- 如何通俗的解释全微分?
- Python“Non-ASCII character 'xe5' in file”报错问题
- 最简单DIY基于ESP8266的智能彩灯③(在网页用按钮+滑动条+手机APP控制RGB灯)
- 牛客网暑期ACM多校训练营(第二场)G.transform (二分+思维)
- stm32项目_stm32f103c8t6项目_循迹避障小车完整制作过程_智能小车设计_STM32智能小车教程-循迹-避障-蓝牙遥控-跟随
热门文章
- 【AVD】NDK MediaCodec 编码中的坑 configure: err(-2147479551) error -38 Fatal signal 4 (SIGILL) ILL_ILLOPC
- JS 数字转换为EXCEL字母列
- Git:每一行命令都算数
- AXI协议中的BURST
- 嵌入式算法8---空间向量夹角公式及其应用
- HDU 2567 寻梦
- Adobe Illustrator【印前角线X2.0】脚本源码
- Lowest Common Ancestor
- EDIFACT 标准
- 鹿客、小米、智汀、德施曼这几款热门智能门锁测评:谁最安全?谁最便捷?