4889软件园:电脑手机软件下载大全,热门手机游戏免费下载

4889软件园 > 资讯文章 > 安卓游戏俄罗斯方块(鸿蒙上做个俄罗斯方块小游戏)

安卓游戏俄罗斯方块(鸿蒙上做个俄罗斯方块小游戏)

作者:佚名 来源:4889软件园 时间:2023-04-18 03:39:04

安卓游戏俄罗斯方块(鸿蒙上做个俄罗斯方块小游戏)

安卓游戏俄罗斯方块文章列表:

安卓游戏俄罗斯方块(鸿蒙上做个俄罗斯方块小游戏)

鸿蒙上做个俄罗斯方块小游戏

小时候有个游戏叫俄罗斯方块,大人小孩都喜欢玩,我们就一起看看如何能用 OpenHarmony 学习做个 Tetris。

效果如下图:

开发

①HAP 应用建立

此前文章我们介绍了简单的 Hap 应用的开发以及基础控件的介绍,这里我们就不赘述 Hap 项目的建立过程。

以下就是基础的 Hap 的 page 文件:index.ets。

build() { Row() { Column() { Canvas(this.context) .width('100%') .height('100%') .onClick((ev: ClickEvent) => { console.info("click!!") this.doClick() }) .onReady(() =>{ this.context.imageSmoothingEnabled = false this.randomType() this.drawall() }) } .width('100%') } .height('100%') .backgroundColor("#cccccc") }

build 是基础页面的构造函数,用于界面的元素构造,其他的页面的生命周期函数如下:

declare class CustomComponent { /** * Customize the pop-up content constructor. * @since 7 */ build(): void; /** * aboutToAppear Method * @since 7 */ aboutToAppear?(): void; /** * aboutToDisappear Method * @since 7 */ aboutToDisappear?(): void; /** * onPageShow Method * @since 7 */ onPageShow?(): void; /** * onPageHide Method * @since 7 */ onPageHide?(): void; /** * onBackPress Method * @since 7 */ onBackPress?(): void;}

②Canvas 介绍

canvas 是画布组件用于自定义绘制图形,具体的 API 页面如下:

https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-components-canvas-canvas-0000001333641081

页面显示前会调用 aboutToAppear() 函数,此函数为页面生命周期函数。

canvas 组件初始化完毕后会调用 onReady() 函数,函数内部实现小游戏的初始页面的绘制

初始化页面数据:

drawall() { this.drawBox() this.drawSideBlock() this.drawBoxBlock() this.drawScore() }

因为都是画布画的,所以布局有点麻烦,需要画几个部分:

中间的大框:方块下落和堆叠区域

右边提升框:下个方块类型

中间方块:方块运动和堆叠

下方计分:行数得分

绘制大框:

drawBox() { this.context.lineWidth = 4 this.context.beginPath() this.context.lineCap = 'butt' this.context.moveTo(0, 100) this.context.lineTo(270, 100) this.context.moveTo(270, 100) this.context.lineTo(270, 690) this.context.moveTo(0, 690) this.context.lineTo(270, 690) }

绘制提示方块:

drawSideBlock() { this.context.fillStyle = 'rgb(250,0,0)' let bs = this.blockSize let coords = this.blockShapBasic[this.blockType] let x = this.sideStartX coords[0][0]*this.blockSize let y = this.sideStartY coords[0][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) x = this.sideStartX coords[1][0]*this.blockSize y = this.sideStartY coords[1][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) x = this.sideStartX coords[2][0]*this.blockSize y = this.sideStartY coords[2][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) x = this.sideStartX coords[3][0]*this.blockSize y = this.sideStartY coords[3][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) this.context.stroke() }

绘制运动方块:

drawBoxBlock() { this.setDirection() this.context.fillStyle = 'rgb(250,0,0)' let bs = this.blockSize let coords = this.curBlockShap let starty = this.slotStartY this.step * this.blockSize let x = this.slotStartX coords[0][0]*this.blockSize let y = starty coords[0][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) x = this.slotStartX coords[1][0]*this.blockSize y = starty coords[1][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) x = this.slotStartX coords[2][0]*this.blockSize y = starty coords[2][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) x = this.slotStartX coords[3][0]*this.blockSize y = starty coords[3][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) this.context.stroke() this.slotBottomY = y}

绘制得分区域:

drawScore() { this.context.fillStyle = 'rgb(0,0,0)' this.context.font = '80px sans-serif' this.context.fillText("Score:" this.score.toString(), 20, 740) }

③游戏逻辑

简单的小游戏主体游戏逻辑为:等待开始,开始。

结束流程图如下:

graph LRtimer开始 --> 方块下落timer开始 --> click[点击]click[点击] --> 方块变形方块下落 --> |落到底| 能消除 --> 计分 --> 堆积方块下落 --> |落到底| 不能消除 --> 堆积堆积 --> |堆积到顶| 满了 --> 游戏结束堆积 --> |堆积到顶| 未满 --> 方块下落

doClick() { this.direction = 1 }

完整逻辑:

@Entry@Componentstruct Index { @State message: string = 'Hello World' private settings: RenderingContextSettings = new RenderingContextSettings(true); private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); private blockType: number = 0 private blockSize: number = 30 private blockShapBasic = [ [[0,0],[0,1],[0,2],[0,3]], [[0,0],[0,1],[0,2],[1,2]], [[0,0],[0,1],[1,1],[0,2]], [[0,0],[0,1],[1,1],[1,2]], [[0,0],[0,1],[1,0],[1,1]], ] private blockShap = [ [[0,0],[0,1],[0,2],[0,3]], [[0,0],[0,1],[0,2],[1,2]], [[0,0],[0,1],[1,1],[0,2]], [[0,0],[0,1],[1,1],[1,2]], [[0,0],[0,1],[1,0],[1,1]], ] private curBlockShap = [] private sideStartX = 300; private sideStartY = 150; private slotStartX = 120; private slotStartY = 150; private slotBottomY = 150;; private score = 0; private step = 0; private direction = 0; aboutToDisappear() { } aboutToAppear() { this.sleep(1000) } async sleep(ms: number) { return new Promise((r) => { setInterval(() => { console.log(this.message) this.drawStep() }, ms) }) } doClick() { this.direction = 1 } drawBox() { this.context.lineWidth = 4 this.context.beginPath() this.context.lineCap = 'butt' this.context.moveTo(0, 100) this.context.lineTo(270, 100) this.context.moveTo(270, 100) this.context.lineTo(270, 690) this.context.moveTo(0, 690) this.context.lineTo(270, 690) } setDirection() { this.curBlockShap = this.blockShap[this.blockType] if (this.direction > 0) { for (let i=0;i<4;i ) { let x = this.curBlockShap[i][0] this.curBlockShap[i][0] = this.curBlockShap[i][1] this.curBlockShap[i][1] = x } this.direction = 0 } } drawSideBlock() { this.context.fillStyle = 'rgb(250,0,0)' let bs = this.blockSize let coords = this.blockShapBasic[this.blockType] let x = this.sideStartX coords[0][0]*this.blockSize let y = this.sideStartY coords[0][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) x = this.sideStartX coords[1][0]*this.blockSize y = this.sideStartY coords[1][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) x = this.sideStartX coords[2][0]*this.blockSize y = this.sideStartY coords[2][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) x = this.sideStartX coords[3][0]*this.blockSize y = this.sideStartY coords[3][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) this.context.stroke() } drawBoxBlock() { this.setDirection() this.context.fillStyle = 'rgb(250,0,0)' let bs = this.blockSize let coords = this.curBlockShap let starty = this.slotStartY this.step * this.blockSize let x = this.slotStartX coords[0][0]*this.blockSize let y = starty coords[0][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) x = this.slotStartX coords[1][0]*this.blockSize y = starty coords[1][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) x = this.slotStartX coords[2][0]*this.blockSize y = starty coords[2][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) x = this.slotStartX coords[3][0]*this.blockSize y = starty coords[3][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("x,y" x.toString() ":" y.toString()) this.context.stroke() this.slotBottomY = y } drawScore() { this.context.fillStyle = 'rgb(0,0,0)' this.context.font = '80px sans-serif' this.context.fillText("Score:" this.score.toString(), 20, 740) } randomType() { this.blockType = Math.floor(Math.random()*5) console.info("blocktype:" this.blockType.toString()) } drawStep() { this.context.clearRect(0,0,this.context.width,this.context.height) this.step = 1 this.drawBox() this.drawSideBlock() this.drawBoxBlock() this.drawScore() if (this.slotBottomY >= 660) { this.step = 0 this.randomType() } } drawall() { this.drawBox() this.drawSideBlock() this.drawBoxBlock() this.drawScore() } build() { Row() { Column() { Canvas(this.context) .width('100%') .height('100%') .onClick((ev: ClickEvent) => { console.info("click!!") this.doClick() }) .onReady(() =>{ this.context.imageSmoothingEnabled = false this.randomType() this.drawall() }) } .width('100%') } .height('100%') .backgroundColor("#cccccc") }}

遗留问题:

没实现堆积计分(接下来会做)

可实现网络对战(分布式对战)

下面我们将继续完善页面,增加了左右按键和之前方块显示,方块消除。

①按键增加

之前我们布局一直是只有个 Canvas 控件,现在我们需要设置高度后增加一个 Row 的布局,并增加两个 Button 控件,以下就是基础的 Hap 的 page 文件:index.ets。

build() { Column() { Column() { Canvas(this.context) .width('100%') .height('100%') .onClick((ev: ClickEvent) => { console.info("click!!") this.doClick() }) .onTouch((ev) => { console.info("touch:" ev.type.toString()) console.info("touch x:" ev.touches[0].screenX.toString()) console.info("touch y:" ev.touches[0].screenY.toString()) }) .onReady(() =>{ this.context.imageSmoothingEnabled = false this.randomType() this.drawall() }) } .height('92%') .width('100%') Row() { Button() { //按钮控件 Text('左') .fontSize(20) .fontWeight(FontWeight.Bold) }.type(ButtonType.Capsule) .width('20%') .height('6%') .backgroundColor('#0D9FFB') .onClick(() => { //点击事件 if (this.left > 0) { this.moveAction -= 1 } }) Button() { //按钮控件 Text('右') .fontSize(20) .fontWeight(FontWeight.Bold) }.type(ButtonType.Capsule) .width('20%') .height('6%') .backgroundColor('#0D9FFB') .onClick(() => { //点击事件 if (this.rightX < 240) { this.moveAction = 1 } }) } } .width('100%') .height('100%') .backgroundColor("#cccccc") }

②游戏完善的说明

之前我们的游戏只有布局,方块显示和变形,在完善后我们增加了积累方块的显示,消除,计分,游戏结束判断等。

积累方块显示:

drawBlockmap() { let bs = this.blockSize this.context.fillStyle = 'rgb(250,0,0)' for (let i=0;i<23;i ) { for (let j=0;j<9;j ) { //是否有方块 if (this.blockmap[i][j] == 1) { let y = i * this.blockSize let x = j * this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("drawBlockmap:" x.toString() ":" y.toString()) } } this.context.stroke() console.info("drawBlockmap:" this.storeBlock.length.toString()) } }

因为都是画布画的,为了重绘已经存在的方块,我们应用二维数组 blockmap 表示,值为 1 则有方块,数组索引则表示绘制坐标位置。

判断是否到底还是到顶:

checkBlockmap() { if (this.storeBlock.length == 0) { if (this.slotBottomY >= 660) { return 1 } } else { let coords = this.curBlockShap let startx = this.slotStartX this.moveAction * this.blockSize let starty = this.slotStartY this.step * this.blockSize for (let i=0;i<4;i ) { let x = startx coords[i][0]*this.blockSize let y = starty coords[i][1]*this.blockSize this.blockSize for (let k=0;k<22;k ) { for (let l=0;l<9;l ) { if (this.blockmap[k][l] == 1) { let blocky = k * this.blockSize let blockx = l * this.blockSize //判断是否到底 if ((x == blockx && y == blocky) || y > 660) { //判断是否到顶 if (y == 210) { this.context.drawImage( this.gameoverImg,this.startX,this.startY,300,90) //到顶回2 return 2 } //到底回1 return 1 } } } } } } return 0 }

先判断是否到底,到底的同时判断是否到顶,如果到顶了就是满了,如果只是到底则表明游戏可以继续。

到底积累方块:

stackBlock() { let block = [] let coords = this.curBlockShap let startx = this.slotStartX this.moveAction * this.blockSize let starty = this.slotStartY this.step * this.blockSize for (let i=0;i<4;i ) { let x = startx coords[i][0]*this.blockSize let y = starty coords[i][1]*this.blockSize console.info("stackBlock x:" x.toString() "y:" y.toString()) let indexX = x/this.blockSize let indexY = y/this.blockSize this.blockmap[indexY][indexX] = 1 console.info("stackBlock:" indexX ":" indexY) block.push([x,y]) } this.storeBlock.push(block) console.info("stackBlock:" this.storeBlock.length.toString()) }

如果到底了,就用坐标计算出 索引,然后在 blockmap 里标识为 1,说明此处有方块,这样方便后面的绘制的显示。

清除方块:

cleanBlockmap() { //检查是否一行满了 let needMove = 0 for (let i=22;i>=0;i--) { let linecnt = 0 for (let j = 8;j >= 0; j--) { //是否有方块 if (this.blockmap[i][j] == 1) { linecnt = 1 } } if (linecnt == 9) { //此行都是方块,消除,计分 for (let j = 8;j >= 0; j--) { this.blockmap[i][j] = 0 } needMove = 1 this.score = 1 } else if (needMove > 0) { for (let j = 8;j >= 0; j--) { this.blockmap[i needMove][j] = this.blockmap[i][j] this.blockmap[i][j] = 0 } } } }

二维数组的好处就是方便每行计数清除,然后从底向上再逐层替换。

绘制每一步:

drawStep() { this.context.clearRect(0,0,this.context.width,this.context.height) this.step = 1 this.drawBox() this.drawBlockmap() this.cleanBlockmap() this.drawSideBlock() this.drawBoxBlock() this.drawScore() let stepType = this.checkBlockmap() if ( stepType == 1) { this.stackBlock() this.step = 0 this.randomType() } else if (stepType == 2) { this.state= 2 this.context.drawImage( this.gameoverImg,this.startX,this.startY,300,90) } }

绘制每一步其实就是重绘界面,包括如果游戏结束显示 game over。

③游戏逻辑

简单的小游戏主体游戏逻辑为:等待开始,开始,结束流程图如下:

graph LRtimer开始 --> 方块下落timer开始 --> click[点击]click[点击] --> 方块变形方块下落 --> |落到底| 能消除 --> 计分 --> 堆积方块下落 --> |落到底| 不能消除 --> 堆积堆积 --> |堆积到顶| 满了 --> 游戏结束堆积 --> |堆积到顶| 未满 --> 方块下落

doClick() { if (this.state == 0) { this.direction = 1 } else if (this.state == 2) { this.state = 0 this.score = 0 this.storeBlock = [] this.initMap() } }

游戏结束后需要重新初始化内部数据。

④完整逻辑

@Entry@Componentstruct Index { @State message: string = 'Hello World' private settings: RenderingContextSettings = new RenderingContextSettings(true); private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); private gameoverImg:ImageBitmap = new ImageBitmap("common/images/gameover.png") private blockType: number = 0 private blockSize: number = 30 private blockShapBasic = [ [[0,0],[0,1],[0,2],[0,3]], [[0,0],[0,1],[0,2],[1,2]], [[0,0],[0,1],[1,1],[0,2]], [[0,0],[0,1],[1,1],[1,2]], [[0,0],[0,1],[1,0],[1,1]], ] private blockShap = [ [[0,0],[0,1],[0,2],[0,3]], [[0,0],[0,1],[0,2],[1,2]], [[0,0],[0,1],[1,1],[0,2]], [[0,0],[0,1],[1,1],[1,2]], [[0,0],[0,1],[1,0],[1,1]], ] private blockmap = []; private curBlockShap = [] private storeBlock = [] private sideStartX = 300; private sideStartY = 150; private startX = 20 private startY = 300 private slotStartX = 120; private slotStartY = 150; private slotBottomY = 150; private xleft = 0; private rightX = 0; private score = 0; private step = 0; private direction = 0; private moveAction = 0; private state = 0; aboutToDisappear() { } aboutToAppear() { this.initMap() this.sleep(1000) } initMap() { for (let i=0;i<23;i ) { let item = [] for (let j=0;j<9;j ) { item.push(0) } this.blockmap.push(item) } } async sleep(ms: number) { return new Promise((r) => { setInterval(() => {// console.log(this.message) if (this.state == 0) { this.drawStep() } }, ms) }) } doClick() { if (this.state == 0) { this.direction = 1 } else if (this.state == 2) { this.state = 0 this.score = 0 this.storeBlock = [] this.initMap() } } drawBox() { this.context.lineWidth = 4 this.context.beginPath() this.context.lineCap = 'butt' this.context.moveTo(0, 100) this.context.lineTo(270, 100) this.context.moveTo(270, 100) this.context.lineTo(270, 690) this.context.moveTo(0, 690) this.context.lineTo(270, 690) } setDirection() { this.curBlockShap = this.blockShap[this.blockType] if (this.direction > 0) { for (let i=0;i<4;i ) { let x = this.curBlockShap[i][0] this.curBlockShap[i][0] = this.curBlockShap[i][1] this.curBlockShap[i][1] = x } this.direction = 0 } } drawSideBlock() { this.context.fillStyle = 'rgb(250,0,0)' let bs = this.blockSize let coords = this.blockShapBasic[this.blockType] for (let i=0;i<4;i ) { let x = this.sideStartX coords[i][0]*this.blockSize let y = this.sideStartY coords[i][1]*this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs)// console.info("x,y" x.toString() ":" y.toString()) } this.context.stroke() } drawBoxBlock() { let min = 690 let max = 0 this.setDirection() this.context.fillStyle = 'rgb(250,0,0)' let bs = this.blockSize let coords = this.curBlockShap let startx = this.slotStartX this.moveAction * this.blockSize let starty = this.slotStartY this.step * this.blockSize for (let i=0;i<4;i ) { let x = startx coords[i][0]*this.blockSize let y = starty coords[i][1]*this.blockSize min = min > x ? x:min max = max < x ? x:max this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs)// console.info("x,y" x.toString() ":" y.toString()) this.slotBottomY = y this.xleft = min this.rightX = max } this.context.stroke()// console.info("min,max" min.toString() ":" max.toString()) } stackBlock() { let block = [] let coords = this.curBlockShap let startx = this.slotStartX this.moveAction * this.blockSize let starty = this.slotStartY this.step * this.blockSize for (let i=0;i<4;i ) { let x = startx coords[i][0]*this.blockSize let y = starty coords[i][1]*this.blockSize console.info("stackBlock x:" x.toString() "y:" y.toString()) let indexX = x/this.blockSize let indexY = y/this.blockSize this.blockmap[indexY][indexX] = 1 console.info("stackBlock:" indexX ":" indexY) block.push([x,y]) } this.storeBlock.push(block) console.info("stackBlock:" this.storeBlock.length.toString()) } checkBlockmap() { if (this.storeBlock.length == 0) { if (this.slotBottomY >= 660) { return 1 } } else { let coords = this.curBlockShap let startx = this.slotStartX this.moveAction * this.blockSize let starty = this.slotStartY this.step * this.blockSize for (let i=0;i<4;i ) { let x = startx coords[i][0]*this.blockSize let y = starty coords[i][1]*this.blockSize this.blockSize for (let k=0;k<22;k ) { for (let l=0;l<9;l ) { if (this.blockmap[k][l] == 1) { let blocky = k * this.blockSize let blockx = l * this.blockSize //判断是否到底 if ((x == blockx && y == blocky) || y > 660) { //判断是否到顶 if (y == 210) { this.context.drawImage( this.gameoverImg,this.startX,this.startY,300,90) //到顶回2 return 2 } //到底回1 return 1 } } } } } } return 0 } cleanBlockmap() { //检查是否一行满了 let needMove = 0 for (let i=22;i>=0;i--) { let linecnt = 0 for (let j = 8;j >= 0; j--) { //是否有方块 if (this.blockmap[i][j] == 1) { linecnt = 1 } } if (linecnt == 9) { //此行都是方块,消除,计分 for (let j = 8;j >= 0; j--) { this.blockmap[i][j] = 0 } needMove = 1 this.score = 1 } else if (needMove > 0) { for (let j = 8;j >= 0; j--) { this.blockmap[i needMove][j] = this.blockmap[i][j] this.blockmap[i][j] = 0 } } } } drawBlockmap() { let bs = this.blockSize this.context.fillStyle = 'rgb(250,0,0)' for (let i=0;i<23;i ) { for (let j=0;j<9;j ) { //是否有方块 if (this.blockmap[i][j] == 1) { let y = i * this.blockSize let x = j * this.blockSize this.context.fillRect(x, y, bs, bs) this.context.rect(x, y, bs, bs) console.info("drawBlockmap:" x.toString() ":" y.toString()) } } this.context.stroke() console.info("drawBlockmap:" this.storeBlock.length.toString()) } } drawScore() { this.context.fillStyle = 'rgb(0,0,0)' this.context.font = '80px sans-serif' this.context.fillText("Score:" this.score.toString(), 20, 140) } randomType() { this.blockType = Math.floor(Math.random()*5) console.info("blocktype:" this.blockType.toString()) } drawStep() { this.context.clearRect(0,0,this.context.width,this.context.height) this.step = 1 this.drawBox() this.drawBlockmap() this.cleanBlockmap() this.drawSideBlock() this.drawBoxBlock() this.drawScore() let stepType = this.checkBlockmap() if ( stepType == 1) { this.stackBlock() this.step = 0 this.randomType() } else if (stepType == 2) { this.state= 2 this.context.drawImage( this.gameoverImg,this.startX,this.startY,300,90) } } drawall() { this.drawBox() this.drawSideBlock() this.drawBoxBlock() this.drawScore() } build() { Column() { Column() { Canvas(this.context) .width('100%') .height('100%') .onClick((ev: ClickEvent) => { console.info("click!!") this.doClick() }) .onTouch((ev) => { console.info("touch:" ev.type.toString()) console.info("touch x:" ev.touches[0].screenX.toString()) console.info("touch y:" ev.touches[0].screenY.toString()) }) .onReady(() =>{ this.context.imageSmoothingEnabled = false this.randomType() this.drawall() }) } .height('92%') .width('100%') Row() { Button() { //按钮控件 Text('左') .fontSize(20) .fontWeight(FontWeight.Bold) }.type(ButtonType.Capsule) .width('20%') .height('6%') .backgroundColor('#0D9FFB') .onClick(() => { //点击事件 if (this.xleft > 0) { this.moveAction -= 1 } }) Button() { //按钮控件 Text('右') .fontSize(20) .fontWeight(FontWeight.Bold) }.type(ButtonType.Capsule) .width('20%') .height('6%') .backgroundColor('#0D9FFB') .onClick(() => { //点击事件 if (this.rightX < 240) { this.moveAction = 1 } }) } } .width('100%') .height('100%') .backgroundColor("#cccccc") }}

遗留问题:

有 bug,方块变形不完整

可实现网络对战(分布式对战)

⑤获取源码

等游戏完整发布,会有两个版本,单机和联机版本。

总结

本文主要介绍了小游戏的开发,画布功能的使用,游戏逻辑,分布式。

作者:王石

Pygame实战妈耶~这款经典的俄罗斯方块儿竟这么厉害

前言

哈喽!小伙伴儿下午好今天给大家更新一款消除类的游戏代码!

俄罗斯方块自然是俄罗斯人发明的。这人叫阿列克谢·帕基特诺夫。

?

俄罗斯方块原名是俄语Тетрис,这个名字来源于希腊语tetra,意思是“四”,而游戏的作者最喜

欢网球(tennis)。于是,他把两个词tetra和tennis合而为一,命名为Tetris,这也就是俄罗斯

方块名字的由来。

中文通称为方块共有7种,分别以S、Z、L、J、I、O、T这7个字母的形状来命名。拼图的过程

有“从混乱中寻找秩序”的成就感。

休闲游戏,更是年代回忆。香葱过去寻找童年的记忆吗,手机康康······

?

正文

一)运行环境

本文用到的环境:Python3.6、Pycharm社区版、Pygame游戏模块自带的就不展示啦。

pip install -i https://pypi.douban.com/simple/ 模块名

?

这款经典俄罗斯方块游戏注解中出现的术语解释:

舞台:整个游戏界面,包括堆叠区、成绩等显示区,下个出现方块预告区。
堆叠区:游戏方块和活动方块形状堆放区域,游戏中主要互动区。
方块(基础方块):这里的方块是对基础的小四方形统称,每个方块就是一个正方形。
方块形状:指一组以特定方式组合在一起的方块,也就是大家常说的下落方块形状,比如长
条,方形,L形等。
固实方块:特指堆叠区中不能再进行移动,可被消除的基础方块集合。

二)代码展示

主程序:

import sys reload(sys) sys.setdefaultencoding('utf8')import random,copyimport pygame as pgfrom pygame.locals import *'''常量声明'''EMPTY_CELL=0 #空区标识,表示没有方块FALLING_BLOCK=1 #下落中的方块标识,也就是活动方块。static_BLOCK=2 #固实方块标识'''全局变量声明变量值以sysInit函数中初始化后的结果为准'''defaultFont=None #默认字体screen=None #屏幕输出对象backSurface=None #图像输出缓冲画板score=0 #玩家得分记录clearLineScore=0 #玩家清除的方块行数level=1 #关卡等级clock=None #游戏时钟nowBlock=None #当前下落中的方块nextBlock=None #下一个将出现的方块fallSpeed=10 #当前方块下落速度beginFallSpeed=fallSpeed #游戏初始时方块下落速度speedBuff=0 #下落速度缓冲变量keyBuff=None #上一次按键记录maxBlockWidth=10 #舞台堆叠区X轴最大可容纳基础方块数maxBlockHeight=18 #舞台堆叠区Y轴最大可容纳基础方块数blockWidth=30 #以像素为单位的基础方块宽度blockHeight=30 #以像素为单位的基础方块高度blocks=[] #方块形状矩阵四维列表。第一维为不同的方块形状,第二维为每个方块形状不同的方向(以0下标起始,一共四个方向),第三维为Y轴方块形状占用情况,第四维为X轴方块形状占用情况。矩阵中0表示没有方块,1表示有方块。stage=[] #舞台堆叠区矩阵二维列表,第一维为Y轴方块占用情况,第二维为X轴方块占用情况。矩阵中0表示没有方块,1表示有固实方块,2表示有活动方块。gameOver=False #游戏结束标志pause=False #游戏暂停标志def printTxt(content,x,y,font,screen,color=(255,255,255)): '''显示文本 args: content:待显示文本内容 x,y:显示坐标 font:字体 screen:输出的screen color:颜色 ''' imgTxt=font.render(content,True,color) screen.blit(imgTxt,(x,y)) class point(object): '''平面坐标点类 attributes: x,y:坐标值 ''' def __init__(self,x,y): self.__x=x self.__y=y def getx(self): return self.__x def setx(self,x): self.__x=x x=property(getx,setx) def gety(self): return self.__y def sety(self,y): self.__y=y y=property(gety,sety) def __str__(self): return "{x:" "{:.0f}".format(self.__x) ",y:" "{:.0f}".format(self.__y) "}"class blockSprite(object): ''' 方块形状精灵类 下落方块的定义全靠它了。 attributes: shape:方块形状编号 direction:方块方向编号 xy,方块形状左上角方块坐标 block:方块形状矩阵 ''' def __init__(self,shape,direction,xy): self.shape=shape self.direction=direction self.xy=xy def chgDirection(self,direction): ''' 改变方块的方向 args: direction:1为向右转,0为向左转。 ''' dirNumb=len(blocks[self.shape])-1 if direction==1: self.direction =1 if self.direction>dirNumb: self.direction=0 else: self.direction-=1 if self.direction<0: self.direction=dirNumb def clone(self): ''' 克隆本体 return: 返回自身的克隆 ''' return blockSprite(self.shape,self.direction,point(self.xy.x,self.xy.y)) def _getBlock(self): return blocks[self.shape][self.direction] block = property(_getBlock)def getConf(fileName): ''' 从配置文件中读取方块形状数据 每个方块以4*4矩阵表示形状,配置文件每行代表一个方块,用分号分隔矩阵行,用逗号分隔矩阵列,0表示没有方块,1表示有方块。 因为此程序只针对俄罗斯方块的经典版,所以方块矩阵大小以硬编码的形式写死为4*4。 args: fileName:配置文件名 ''' global blocks #blocks记录方块形状。 with open(fileName,'rt') as fp: for temp in fp.readlines(): blocks.append([]) blocksNumb=len(blocks)-1 blocks[blocksNumb]=[] #每种方块形状有四个方向,以0~3表示。配置文件中只记录一个方向形状,另外三个方向的矩阵排列在sysInit中通过调用transform计算出来。 blocks[blocksNumb].append([]) row=temp.split(";") for r in range(len(row)): col=[] ct=row[r].split(",") #对矩阵列数据做规整,首先将非“1”的值全修正成“0”以过滤空字串或回车符。 for c in range(len(ct)): if ct[c]!="1": col.append(0) else: col.append(1) #将不足4列的矩阵通过补“0”的方式,补足4列。 for c in range(len(ct)-1,3): col.append(0) blocks[blocksNumb][0].append(col) #如果矩阵某行没有方块,则配置文件中可以省略此行,程序会在末尾补上空行数据。 for r in range(len(row)-1,3): blocks[blocksNumb][0].append([0,0,0,0]) blocks[blocksNumb][0]=formatBlock(blocks[blocksNumb][0])def sysInit(): ''' 系统初始化 包括pygame环境初始化,全局变量赋值,生成每个方块形状的四个方向矩阵。 ''' global defaultFont,screen,backSurface,clock,blocks,stage,gameOver,fallSpeed,beginFallSpeed,nowBlock,nextBlock,score,level,clearLineScore,pause #pygame运行环境初始化 pg.init() screen=pg.display.set_mode((500,550)) backSurface=pg.Surface((screen.get_rect().width,screen.get_rect().height)) pg.display.set_caption("block") clock=pg.time.Clock() pg.mouse.set_visible(False) #游戏全局变量初始化 defaultFont=pg.font.Font("msyh.ttc",16) #yh.ttf这个字体文件请自行上网搜索下载,如果找不到就随便用个ttf格式字体文件替换一下。 nowBlock=None nextBlock=None gameOver=False pause=False score=0 level=1 clearLineScore=0 beginFallSpeed=20 fallSpeed=beginFallSpeed-level*2 #初始化游戏舞台 stage=[] for y in range(maxBlockHeight): stage.append([]) for x in range(maxBlockWidth): stage[y].append(EMPTY_CELL) #生成每个方块形状4个方向的矩阵数据 for x in range(len(blocks)): #因为重新开始游戏时会调用sysinit对系统所有参数重新初始化,为了避免方向矩阵数据重新生成,需要在此判断是否已经生成,如果已经生成则跳过。 if len(blocks[x])<2: t=blocks[x][0] for i in range(3): t=transform(t,1) blocks[x].append(formatBlock(t)) #transform,removeTopBlank,formatBlock这三个函数只为生成方块形状4个方向矩阵使用,在游戏其他环节无作用,在阅读程序时可以先跳过。def transform(block,direction=0): ''' 生成指定方块形状转换方向后的矩阵数据 args: block:方块形状矩阵参数 direction:转换的方向,0代表向左,1代表向右 return: 变换方向后的方块形状矩阵参数 ''' result=[] for y in range(4): result.append([]) for x in range(4): if direction==0: result[y].append(block[x][3-y]) else: result[y].append(block[3-x][y]) return result def removeTopBlank(block): ''' 清除方块矩阵顶部空行数据 args: block:方块开关矩阵 return: 整理后的方块矩阵数据 ''' result=copy.deepcopy(block) blankNumb=0 while sum(result[0])<1 and blankNumb<4: del result[0] result.append([0,0,0,0]) blankNumb =1 return result def formatBlock(block): ''' 整理方块矩阵数据,使方块在矩阵中处于左上角的位置 args: block:方块开关矩阵 return: 整理后的方块矩阵数据 ''' result=removeTopBlank(block) #将矩阵右转,用于计算左侧X轴线空行,计算完成后再转回 result=transform(result, 1) result=removeTopBlank(result) result=transform(result,0) return resultdef checkDeany(sprite): ''' 检查下落方块是否与舞台堆叠区中固实方块发生碰撞 args: sprite:下落方块 return: 如果发生碰撞则返回True ''' topX=sprite.xy.x topY=sprite.xy.y for y in range(len(sprite.block)): for x in range(len(sprite.block[y])): if sprite.block[y][x]==1: yInStage=topY y xInStage=topX x if yInStage>maxBlockHeight-1 or yInStage<0: return True if xInStage>maxBlockWidth-1 or xInStage<0: return True if stage[yInStage][xInStage]==STATIC_BLOCK: return True return Falsedef checkLine(): ''' 检测堆叠区是否有可消除的整行固实方块 根据检测结果重新生成堆叠区矩阵数据,调用updateScore函数更新玩家积分等数据。 return: 本轮下落周期消除的固实方块行数 ''' global stage clearCount=0 #本轮下落周期消除的固实方块行数 tmpStage=[] #根据消除情况新生成的堆叠区矩阵,在有更新的情况下会替换全局的堆叠区矩阵。 for y in stage: #因为固实方块在堆叠矩阵里以2表示,所以判断方块是否已经满一整行只要计算矩阵行数值合计是否等于堆叠区X轴最大方块数*2就可以。 if sum(y)>=maxBlockWidth*2: tmpStage.insert(0,maxBlockWidth*[0]) clearCount =1 else: tmpStage.append(y) if clearCount>0: stage=tmpStage updateScore(clearCount) return clearCount def updateStage(sprite,updateType=1): ''' 将下落方块坐标数据更新到堆叠区数据中。下落方块涉及的坐标在堆叠区中用数字1标识,固实方块在堆叠区中用数字2标识。 args: sprite:下落方块形状 updateType:更新方式,0代表清除,1代表动态加入,2代表固实加入。 ''' global stage topX=sprite.xy.x topY=sprite.xy.y for y in range(len(sprite.block)): for x in range(len(sprite.block[y])): if sprite.block[y][x]==1: if updateType==0: if stage[topY y][topX x]==FALLING_BLOCK: stage[topY y][topX x]=EMPTY_CELL elif updateType==1: if stage[topY y][topX x]==EMPTY_CELL: stage[topY y][topX x]=FALLING_BLOCK else: stage[topY y][topX x]=STATIC_BLOCKdef updateScore(clearCount): ''' 更新玩家游戏记录,包括积分、关卡、消除方块行数,并且根据关卡数更新方块下落速度。 args: clearCount:本轮下落周期内清除的方块行数。 return: 当前游戏的最新积分 ''' global score,fallSpeed,level,clearLineScore prizePoint=0 #额外奖励分数,同时消除的行数越多,奖励分值越高。 if clearCount>1: if clearCount<4: prizePoint=clearCount**clearCount else: prizePoint=clearCount*5 score =(clearCount prizePoint)*level #玩得再牛又有何用? :) if score>99999999: score=0 clearLineScore =clearCount if clearLineScore>100: clearLineScore=0 level =1 if level>(beginFallSpeed/2): level=1 fallSpeed=beginFallSpeed fallSpeed=beginFallSpeed-level*2 return scoredef drawStage(drawScreen): ''' 在给定的画布上绘制舞台 args: drawScreen:待绘制的画布 ''' staticColor=30,102,76 #固实方块颜色 activeColor=255,239,0 #方块形状颜色 fontColor=200,10,120 #文字颜色 baseRect=0,0,blockWidth*maxBlockWidth 1,blockHeight*maxBlockHeight 1 #堆叠区方框 #绘制堆叠区外框 drawScreen.fill((180,200,170)) pg.draw.rect(drawScreen, staticColor, baseRect,1) #绘制堆叠区内的所有方块,包括下落方块形状 for y in range(len(stage)): for x in range(len(stage[y])): baseRect=x*blockWidth,y*blockHeight,blockWidth,blockHeight if stage[y][x]==2: pg.draw.rect(drawScreen, staticColor, baseRect) elif stage[y][x]==1: pg.draw.rect(drawScreen, activeColor, baseRect) #绘制下一个登场的下落方块形状 printTxt("Next:",320,350,defaultFont,backSurface,fontColor) if nextBlock!=None: for y in range(len(nextBlock.block)): for x in range(len(nextBlock.block[y])): baseRect=320 x*blockWidth,380 y*blockHeight,blockWidth,blockHeight if nextBlock.block[y][x]==1: pg.draw.rect(drawScreen, activeColor, baseRect) #绘制关卡、积分、当前关卡消除整行数 printTxt("Level:%d" % level,320,40,defaultFont,backSurface,fontColor) printTxt("Score:%d" % score,320,70,defaultFont,backSurface,fontColor) printTxt("Clear:%d" % clearLineScore,320,100,defaultFont,backSurface,fontColor) #特殊游戏状态的输出 if gameOver: printTxt("GAME OVER",230,200,defaultFont,backSurface,fontColor) printTxt("<PRESS RETURN TO REPLAY>",200,260,defaultFont,backSurface,fontColor) if pause: printTxt("Game pausing",230,200,defaultFont,backSurface,fontColor) printTxt("<PRESS RETURN TO CONTINUE>",200,260,defaultFont,backSurface,fontColor) def process(): ''' 游戏控制及逻辑处理 ''' global gameOver,nowBlock,nextBlock,speedBuff,backSurface,keyBuff,pause if nextBlock is None: nextBlock=blockSprite(random.randint(0,len(blocks)-1),random.randint(0,3),point(maxBlockWidth 4,maxBlockHeight)) if nowBlock is None: nowBlock=nextBlock.clone() nowBlock.xy=point(maxBlockWidth//2,0) nextBlock=blockSprite(random.randint(0,len(blocks)-1),random.randint(0,3),point(maxBlockWidth 4,maxBlockHeight)) #每次生成新的下落方块形状时检测碰撞,如果新的方块形状一出现就发生碰撞,则显然玩家已经没有机会了。 gameOver=checkDeany(nowBlock) #游戏失败后,要将活动方块形状做固实处理 if gameOver: updateStage(nowBlock,2) ''' 对于下落方块形状操控以及移动,采用影子形状进行预判断。如果没有碰撞则将变化应用到下落方块形状上,否则不变化。 ''' tmpBlock=nowBlock.clone() #影子方块形状 ''' 处理用户输入 对于用户输入分为两部分处理。 第一部分,将退出、暂停、重新开始以及形状变换的操作以敲击事件处理。 这样做的好处是只对敲击一次键盘做出处理,避免用户按住单一按键后程序反复处理影响操控,特别是形状变换操作,敲击一次键盘换变一次方向,玩家很容易控制。 ''' for event in pg.event.get(): if event.type== pg.QUIT: sys.exit() pg.quit() elif event.type==pg.KEYDOWN: if event.key==pg.K_ESCAPE: sys.exit() pg.quit() elif event.key==pg.K_RETURN: if gameOver: sysInit() return elif pause: pause=False else: pause=True return elif not gameOver and not pause: if event.key==pg.K_SPACE: tmpBlock.chgDirection(1) elif event.key==pg.K_UP: tmpBlock.chgDirection(0) if not gameOver and not pause: ''' 用户输入处理第二部分,将左右移动和快速下落的操作以按下事件处理。 这样做的好处是不需要玩家反复敲击键盘进行操作,保证了操作的连贯性。 由于连续移动的速度太快,不利于定位。所以在程序中采用了简单的输入减缓处理,即通过keyBuff保存上一次操作按键,如果此次按键与上一次按键相同,则跳过此轮按键处理。 ''' keys=pg.key.get_pressed() if keys[K_DOWN]: tmpBlock.xy=point(tmpBlock.xy.x,tmpBlock.xy.y 1) keyBuff=None elif keys[K_LEFT]: if keyBuff!=pg.K_LEFT: tmpBlock.xy=point(tmpBlock.xy.x-1,tmpBlock.xy.y) keyBuff=pg.K_LEFT else: keyBuff=None elif keys[K_RIGHT]: if keyBuff!=pg.K_RIGHT: tmpBlock.xy=point(tmpBlock.xy.x 1,tmpBlock.xy.y) keyBuff=pg.K_RIGHT else: keyBuff=None if not checkDeany(tmpBlock): updateStage(nowBlock,0) nowBlock=tmpBlock.clone() #处理自动下落 speedBuff =1 if speedBuff>=fallSpeed: speedBuff=0 tmpBlock=nowBlock.clone() tmpBlock.xy=point(nowBlock.xy.x,nowBlock.xy.y 1) if not checkDeany(tmpBlock): updateStage(nowBlock,0) nowBlock=tmpBlock.clone() updateStage(nowBlock,1) else: #在自动下落过程中一但发生活动方块形状的碰撞,则将活动方块形状做固实处理,并检测是否有可消除的整行方块 updateStage(nowBlock,2) checkLine() nowBlock=None else: updateStage(nowBlock,1) drawStage(backSurface) screen.blit(backSurface,(0,0)) pg.display.update() clock.tick(40) def main(): ''' 主程序 ''' getConf("elsfk.cfg") sysInit() while True: process() if __name__ == "__main__": main()

?三)效果展示

1)游戏开始

?

?

2)游戏运行

?

3)游戏结束

?

总结

好啦!经典的俄罗斯方块儿到这里就正式结束啦~喜欢的小伙伴二老规矩找我拿源码撒

完整的项目源码免费领取:私信小编06就可啦~

99块砖之巫师学院登安卓 非一般的俄罗斯方块

【着迷裤裤编译报道】开发商WeirdBeard在不久前推出了IOS版本的《99块砖之巫师学院》(99 Bricks Wizard Academy),目前这款另类的俄罗斯方块游戏也终于来到了安卓平台。

《99块砖之巫师学院》的核心玩法在于建造一座越高越好的巨塔,而建塔的材料就是随机降落的彩色俄罗斯方块,由于采用了真实物理模拟效果,所以初期的小小缝隙到了高处都会成为巨大的倒塌隐患。

除此之外,旁边的小魔法师还可以利用不断学到的魔法来帮助你,比如用闪电将上一块没有放好的方块给消灭掉,但这些魔法都需要冷却时间。

目前《99块砖之巫师学院》已经登陆安卓平台,玩家可以从各大安卓渠道平台进行下载,或者到谷歌商店进行查看。

查看原文有下载地址

30年传奇:首款正版俄罗斯方块手游今日首测

提起俄罗斯的文化名片,相信你第一时间就能想到《俄罗斯方块》,这款风靡全球的游戏从未停下它的脚步,陪伴了好几代人度过了无忧无虑的童年。如今,中国首款正版《俄罗斯方块环游记Tetris Journey》今日11:00正式在TapTap首次对外开放安卓版本。

【全球官方通用规则 助中国玩家冲击世界马拉松吉尼斯纪录】

2009年,一位叫做Harry Hong的玩家成了第一个有记录突破了红白机版《俄罗斯方块》999999分上限的人。身为俄罗斯方块粉丝的电影制作人亚当科涅利为了这件事情拍摄了短片《满分》,并且想要众筹拍一部关于最厉害的俄罗斯方块玩家的纪录片。

拥有7个俄罗斯方块冠军的高手乔纳斯,在许多年前,他就因为能够突破红白机版俄罗斯方块的999999分上限而在“游戏界的吉尼斯世界纪录”Twin Galaxies的榜单里有自己的一席之地。2018年,他更是一度打破了300行消除的世界纪录。但是直到他遇到16岁的天才少年约瑟夫,新人连胜三场,用3:0的比分战胜了强大的乔纳斯,出乎许多人的意料,正式加冕成为新的世界最强俄罗斯方块玩家。

而本次“俄罗斯方块环游记Tetris Journey”将首次为中国开放马拉松竞赛的体验版,在本次放出的马拉松模块采用世界通用官方规则:包含高级玩家使用的T-SPIN,Mini-spin,B2B等。助推中国的Tetris高手能尽快加入挑战世界锦标赛与世界吉尼斯记录之列。

“俄罗斯方块环游记Tetris Journey”包含三个大系列玩法:策略休闲关卡,天梯赛事竞技(本次开放经典赛与道具赛试玩),经典系列马拉松。

【传承经典 玩法大集合】

本次开放版本包含196个关卡(180个主线关卡和16个彩蛋神秘关卡)里融入了16个玩法,28个元素的精髓。玩家玩的时候既想起小时候的玩游戏的情景(剧透一点,里面有的关卡是30年前的像素风),也有很多最新的规则,大家也会看到一个完全不一样的,一个集亿万玩家喜欢的游戏内容于一身的全新俄罗斯方块大集合。关卡后续版本会保持不断更新。

要体验纯正的俄罗斯方块,记得打开游戏声音!30年前的正版俄罗斯方块音乐,世界各地的特色音乐,8Bite的经典声音,俄罗斯方块环游记,什么都有?(?????????)?!!

【Q版名胜 足不出户环游世界】

本次开放挑战国家地图:俄罗斯,日本,中国,法国,巴西,美国。并放出了4个彩蛋供玩家找寻,玩家在不停通关的同时,要仔细留意世界地图上的一些内容哦!另外剧透一点点,有三个彩蛋是需要通关才能发现的,具体怎么找到,留下一个谜题吧~

竞技对战加入《俄罗斯方块环游记》

对于一个天生自带竞技buff的游戏,俄罗斯方块的竞技将支持线上线下模式,甚至好友同学私下就可以随时随地进行对战。后续官方会举办俄罗斯方块比赛,寻找全国的高手,和国外俄罗斯方块高手一决高下!

【TapTap社区活动同步开启 助你领取超爽福利】

测试过程中,TapTap社区还同步开启了“记录方块心动瞬间”、“送祝福”、“测试反馈”等论坛活动,现在参与就有丰厚的奖励哦!期待大家能多多为游戏提供修改意见,若您的建议获得官方大力点赞,还会受邀参观或参与官方举办的比赛中!

30余年经典,首款正版《俄罗斯方块环游记》终于来到中国,千万不要错过1月10日tap首次试玩。

端午假期玩什么:免费的俄罗斯方块吃鸡或是马里欧网球?

端午假期到了,“周末玩什么”这个触乐编辑们的每周游戏推荐节目同样提前到今天推出了。当你还在赖床,没决定接下来玩点什么好的时候,不如来看看我们的选择里面是否有你感兴趣的,也欢迎读者和开发者朋友们向我们寻求报导。

窦宇萌:《Reventure》(Steam)

关键词:像素、RPG、解谜、作死

一句话点评:勇者妙趣横生的作死之旅,什么,救公主?先让我多死几次再说。

《Reventure》是一款像素风横版RPG游戏,于6月4日在Steam平台上线。游戏中,玩家将扮演一位勇士,被国王委托前去拯救困在黑暗城堡中的公主。游戏画面简洁,手感优秀,并且自带简体中文汉化,十分良心。

听上去,这似乎像一款骑士拯救公主的王道RPG,但只要玩上几分钟,就会发现,这款游戏其实可以用以下4个字来概括——作死之旅。

游戏画风清新明快,有点像十余年前的老款任天堂风格

游戏共有100种各不相同的结局(死法),每一种都非常有趣。虽然玩家最开始的目的是“拯救公主”,但很快就会迷失在一大堆有趣的结局中,转而热情地探索起主角死亡的各种方式。

最开始的时候,我还没明白《Reventure》这款游戏在平和表象下的阴险诡计,只要看到宝箱,就会兴奋地冲上前去拿取,于是,我的背包越来越沉,跳跃高度越来越低,最后结局就变成了这样。

宝箱内的装备最多拿取3个,如果贪多求全,就会有这样的结果……

不过,随时保持谨小慎微,什么都不拿也不行。许多结局需要特定道具触发,还有很多通路只有手持某样物品才能开启,因此,每一次开局都是富有策略性的——这次我要手持什么装备,去哪个区域探险呢?

游戏中充满了大量有趣的细节,例如,我在上一次冒险中刺杀了国王,自己坐上了宝座,在重启冒险后,我就发现王宫的墙上多了两幅壁画,分别是之前的国王和我自己。

我记得,这面墙上之前分明什么都没有……

同时,游戏的地图设计也可圈可点,游玩过程中时常会有类似“黑暗之魂”系列的“柳暗花明又一村”之感。不大的地图中,各个区域相互连通,纵横交错,每条道路开启的条件都不尽相同,在这次冒险能走进的道路,下一次冒险中,因为角色携带的装备不同,就会变得不能使用。

总而言之,《Reventure》是一款精巧、用心、满是乐趣的游戏作品,现在Steam售价19元,喜欢探索、解谜和像素风冒险的玩家可以在端午节假期里试试看。

store.steampowered.com/widget/900270/

牛旭:《汤姆·克兰西的幽灵行动:荒野》(PC、PS4、Xbox One)

关键词:再开放就要砸电脑的开放世界、没啥动作、没什么战术可讲、多人模式游乐园、玻利维亚旅游模拟器

一句话点评:它可以让你足不出户体验南美风情。

居住在京郊大地并不是一件很轻松的事情,家门口永远拥堵的高速路会毁掉我对远方的向往,而即使不远行,光去银行存个钱都有可能在路上堵几个小时的车。再加上北京的夏天一如既往地惹人烦躁,不下雨的时候刮风、不刮风的时候暴晒、不暴晒的时候打雷又下雨……总而言之,基本上是逼着你待在家里和猫作伴。

万幸,我还可以带上“相机”前往玻利维亚度过小长假。那是个美丽的国度,不光景色宜人,气温也始终保持在26度,居住在那儿的居民淳朴而热情,在这里,我可以驾驶数不过来多的载具穿越平原、山脉或湖泊,也能徒步登上高山,观看日出日落,甚至还可以体验一把“真人野外CS”,把不听话的毒贩们绳之以法。

假如你也因为这样那样的原因只能在家里度过小长假,那么尝试《汤姆·克兰西的幽灵行动:荒野》(Tom Clancy's Ghost Recon Wildlands)应该可以在很大程度上满足你对野外旅游的憧憬。

看看这天多蓝!

《幽灵行动:荒野》是育碧旗下“幽灵行动”系列游戏的第7部作品。主要讲述了一批由专门干“脏活”的特工所组成的特战小队深入异国收集情报,利用暗杀和绑架等方式把玻利维亚贩毒集团逐步瓦解的故事,而这样的设定本应烘托出一种严肃,且带有一定反思的故事氛围。

不过育碧这次对于地图规模执着得有些过火。上百个城镇、接近400平方千米的不同地形组成了有史以来最庞大的游戏地图,但过于重复的收集要素,并不过瘾的战斗环节,以及稀碎的任务线路又让不少玩家感觉自己像是真正踏足“荒野”,没有动力去完成剧情和要素收集。

强迫症患者当然可以按区域梳理地图上的图标,但这样做真的没什么意思

在战斗方面,单人游玩的体验并不舒服。经常瞬移的电脑队友会在潜行环节里时不时暴露出自己的方位,导致偷袭失效。在玩家驾驶载具时,因为没有弹药限制,这些电脑队友会不间断地倾泻火力,就像你在车上安了3座全自动炮塔那样。总之,除了同步射击环节比人类玩家打得准,其他时候他们就像“路人甲”一样缺乏存在感,纯属于“工具人”范畴。

这游戏的乐趣之一就包括开车,倒不是因为它有多重要,而是很多时候你不得不开着它跑很远很远

多人游玩倒是很有乐趣。和好友们一起在一张宽广的地图上四处飙车,“假模假样”地玩一玩潜入作战都很有趣。况且,游戏里提供了海量的支线和关卡,不调最高难度的话,和朋友一边闲谈胡闹,一边推任务过关也是个不错的周末消遣。

尽管又大又空的地图是《幽灵行动:荒野》的缺点之一,但凡事都有它的另一面。在景色的刻画上,育碧已经做到炉火纯青,无论是宽广无垠的盐湖、高耸在半空的山石,或者是藏身在海湾的走私者洞穴,不同地标性建筑配合出色的光影效果,能带给玩家非常过瘾的“旅游”体验。多样式的载具又让玩家在观赏景色时能够选择最适合自己的交通工具,驾驶直升飞机一览众山小、骑着山地摩托感受极限运动的刺激,或者驾车穿越被夜色笼罩的平原,探索这个世界的乐趣可要比手感奇怪的战斗系统令人舒服得多。

的确挺像“登山队”

趁着夜色穿越盐湖,这是限期3天的郊游看不到的美景

令人欣慰的是,育碧很注意展示这些优美的景色。在单人游戏中,玩家可以按下对应按键开启“拍照”模式,在这一模式下,玩家可以像调整真正的相机那样设置焦距、光圈和景深,借助游戏精致的图像效果,为自己的“玻利维亚之旅”留念。除了过场动画,“相机键”可以应用在一切场景,不管玩家正在和敌人交火,还是在套取情报,只要开启“相机”就能让时间静止。

不仅如此,在相机模式下玩家还可以自由放置镜头位置,玩家可以从多个不同角度记录下自己帅气的造型,或是敌人狼狈的死相。车辆追逐时的尘土飞扬、大规模爆炸时的冲天火光,只要玩家想得到,就都能留下纪念。

“正中靶眼!”

潜行的过程可能没那么精彩,但是记录这些瞬间的乐趣是无价的

“路人甲”队友们也非常上镜

在“玻利维亚”消磨的十几个小时里,我四处拍照片和观赏景色的时间已经要比正经八百做任务要多了。继续促使我在地图上收集武器的理由,也是因为这些武器和装备更上镜,能拍出更好看的“纪念照”来。虽然我可能这辈子都不会亲临玻利维亚,可通过《幽灵行动:荒野》和它带给我的这些照片和回忆,我甚至已经对这个远在地球另一边的美洲国家产生了些许感情,做梦时也曾听到奇怪的西班牙语在耳边吆喝,我想这也是电子游戏的魅力之一吧。

《幽灵行动:荒野》标准版在Steam平台售价208元人民币,和一些新出的3A大作相差不多。如果想节省一点开支,也可以移步育碧商城,在E3大促期间,“前往玻利维亚”的季票折后只要72元人民币,看在游戏本体漂亮的景色和丰富的内容上,这已经是非常划算的价格了。

store.steampowered.com/widget/460930/

熊宇:《蒸汽世界:挖掘》(PS4、Steam、Xbox One)

关键词:采矿、平台跳跃、探索、积累

一句话点评:从一无所有开始,直到挖到一身神装。

《蒸汽世界:挖掘》(SteamWorld Dig)是一款老游戏了,它在2013年首先发售于3DS,同年登陆Steam时加入了1080p画面与成就系统,在此之后它登陆了PS4和Xbox One。即便过去了这么多年,游戏的玩法仍然不过时——如果你是初次接触游戏的话,仍然能够享受到它的乐趣。

在《蒸汽世界:挖掘》中,玩家在一个颇具西部风格的世界中扮演采矿机器人,所需要做的事情很简单——不断向下挖掘。在游戏中玩家将会穿梭于地底和地表——每当玩家在地底所获颇丰,就需要来到地表清空行囊并获得新的升级,从而让挖掘工作更加顺利。

西部的风格仅限于地表,在地下可没什么西部风景

游戏的目标很明确,甚至可以说很简单,不过也正是这种简单使得玩家从游戏中获得的快乐简单而直接。往下挖,获取新的矿石、在各种洞穴中解决自己所遇到的谜题,同时积累升级资源;往上去,花费自己的货币,获取新的装备。

敲碎土块,获得矿石,这非常简单。困难之处是,在地底世界你得依靠火炬照明,随着时间的流逝,火光的范围会越来越小

所有矿石都将被换为金钱,金钱可以用来购买各种升级

简单的玩法没有给玩家留下太多选择的空间,因此对节奏的控制也就显得至关重要,而这也恰恰是《蒸汽世界:挖掘》的成功之处:每当玩家在挖掘中略感疲惫时,总会跟随任务的指引解锁新的要素,或者是因为包裹满了等原因来到地面——这使得他们能够获得新的刺激,因而总是能够获得新鲜感。

在游戏中,发现特定的遗迹、通关解谜关卡、获得一定数量的金钱都可能解锁新的要素,而新要素会给玩家带来完全不同的游戏体验

如果你还没有体验过这款老游戏的话,现在Steam平台的游戏正处于打折状态,售价9元。

store.steampowered.com/widget/252410/

陈静:《社畜三文鱼》(Android、iOS)

关键词:跳跃、魔性、恶搞、寓言

一句话推荐:游戏通关了,人生则未必。

端午节应该是今年最干净利落的假期,你既不用调休来调休去,调到身心俱疲,也不用计算自己到底需要怎么安排年假,才能让本就可怜兮兮的假期看上去(也仅仅是看上去)更顺眼一些——当然,作为社畜,我们也明白公司在这件事上同样无能为力,但那就是另一个话题了。

有些朋友会说,比起那些动辄996的人,有假休已经不错了——这大概也是我想推荐《社畜三文鱼》(Corporate Salmon)的原因。

第一眼真没看出是三文鱼

《社畜三文鱼》拥有一个极为简单的流程:一早醒来,你发现自己变成了一条粉红色的三文鱼——好吧,其实看图片你不太能分辨出自己是什么鱼,但既然作者说是三文鱼,那就是三文鱼。洗漱过后,你就要进入公司,开始无止境地向上攀登。

攀登方法同样很简单:点击屏幕任何位置即可让你的鱼跳起来,在向下运行的自动扶梯上一路往上跳,跳到顶端或途中攒到足够的钱即可过关。一开始我不知道后一条规则有什么用,但很快就明白了。

在这个游戏里,你只能“跳”,无法控制跳跃的方向、力度和落点,有时明明再跳一两下就过关,然而一处微妙的碰撞却可以让你瞬间跌下电梯。如果没有“积攒金钱”这个备选过关条件,你很可能一辈子也过不了关——顺带一提,过关会显示“升职”,Game Over会显示“被炒”,结合现实考虑,简直要让人双目含泪。这么说可能夸张了点儿,但上一次让我在游戏里如此堵心,还要追溯到“掘地求升”……

“升职”和“被炒”

进入游戏流程后半,游戏的人间真实感逐渐提升:踩着其他咸鱼跳得更高,但你也有可能被其他咸鱼挤下电梯;屏幕上会随机出现“机会线”(大概相当于紧急工作),踩到之后可以跳得很高。绿线代表赚钱,红线代表赔钱,然而有时为了生存下去,即使赔钱也要踩线跳得更高。

终于爬到顶层,游戏会鼓励你:“干得漂亮!”“CEO办公室!”“就在前方!”你奋力一步一步跳过去,终于坐上老板椅,享受摩天大楼最高处的风景,背后墙上挂着的大幅照片是你成功的最佳写照。然而……

升职加薪走上人生巅峰……真的吗?

你只高兴了1秒,照片竟然从你的脸变成了一头正在盯着你的熊,定睛一瞧,熊的领带还是鱼骨头花纹的。这个时候你还能怎么办?当然是要熊口脱险了!

开心1秒

游戏里还有一些吸引人的小设计,比如换装,虽然服装不会给你的鱼增加任何正面属性,但有些设计得还挺可爱,值得刷钱购买。此外,游戏通关一次之后会自动解锁“自然”(Natural)服装,也就是全裸。这倒也不意外,毕竟一条鱼穿不穿衣服也就那么一回事。

想把游戏里的服装全买下来,需要刷很久

很难说《社畜三文鱼》真的有多么好玩,把它称作“人生寓言”也略单调了点儿。但假如你是一个朝九晚五、生活日复一日,甚至要“996”燃烧生命的上班族,这个游戏一定能让你感慨万千。假如你还没有过上这样的日子,那么我也建议你玩玩这个游戏——玩过之后,你或许还有机会选择不一样的生活。

谭一尘:《马力欧网球Ace》(Switch)

关键词:健身、格斗、送俄罗斯方块

一句话点评:强身健体的体感网球,免费体验还送7天会员。

作为一名网球爱好者,去年的这个时候我正在纠结买《网球世界巡回赛》还是《马力欧网球Ace》。但随着前者的媒体评价和玩家口碑双双崩盘,我最终还是冲着体感操作,选择了更具娱乐性的《马力欧网球Ace》(Mario Tennis Ace)。

玩家3D打印的体感球拍,我只能羡慕而无法拥有

马力欧主题的背景、卡通渲染的画面,加上马力欧要救的对象终于不是桃花公主而是路易基,起初我还是非常喜欢这款游戏的。但是随着我结束了优秀的故事模式的部分,开始进行线上对战后,我发现这个游戏根本不是个轻松愉快的网球游戏,而是一款硬核对战格斗游戏——正经的“杀人网球”。

游戏里对真实网球的操作还原的还算不错,平击球、弧线球、高球、急坠球、扣杀、截击等都可以在游戏中使用,在此基础上又加入了技术击球(杂耍)、狙击(射击)、时间停止(超能力)等需要能量才能施放的超级招式。之所以叫做“杀人网球”,不是因为和《网球王子》有什么联动,而是游戏里真的可以通过网球击打对方选手而获得胜利。当然,任天堂的游戏里不会出现这么血腥的设定,取而代之的是球拍被击断就需要退场的设计。

手无缚鸡之力的酷霸王被马力欧的“狙击球”打断了球拍

但是,我最期待的体感网球部分却做得非常不尽如人意。蹩脚的操作和糟糕的动作判定都让人失望,剥离了超能力部分又显得十分单调,只有两只用力挥动的手臂撞在一起时的痛感我还记忆犹新。于是我雪藏了这一盒游戏。

一整年过去了,《马力欧网球Ace》一直在持续运营更新,加入了宝琳、吞食花头头等十数名新角色,增加了“合作任务”和“圆环击球”两种全新模式,娱乐模式更加多样;横扫天梯的酷霸王Jr.也不再无敌,竞技模式也更加平衡。但是,游戏玩法的两极分化依然严重,要么是一个极度考验技术的竞技游戏,要么是一个轻松欢乐的健身工具。

“圆环击球”模式,通过击打圆环得分决出胜负

尽管吐槽了这么多,我依然推荐了这款游戏,那是因为在这个假期里(从6月4日9时到本周日23时)可以从港服eShop免费下载《马力欧网球Ace》体验版,虽然这个试玩版没有体感模式、不能本地双人、不能在线双人,新手玩家可能一时也难以适应“线上锦标赛”模式紧凑的战斗节奏——但是,下载了体验版的玩家还可以获得7天会员在线服务体验券。

“线上锦标赛”模式于今天上午8点正式开启

参加本次活动还会赠送“背带裤形态”马力欧,让你吃饱了还能兜着走

所以,如果你没有购买会员服务,这意味着你不仅可以体验到马网大部分在线内容,还可以在7天内体验到《俄罗斯方块99》等联机对战游戏。

“俄罗斯方块大逃杀”真是有魔力的模式,虽然完全没吃到过鸡

你可能觉得我真正推荐的游戏是“俄罗斯方块吃鸡”,我承认,但《马力欧网球Ace》确实也还挺强身健体的……