硬件介绍:funpack第十期带来的Kitronik ARCADE是一款功能丰富的可编程游戏手柄,搭配微软MakeCode Arcade编辑器编程使用。处理器 Atmel SAMD51J19A,工作电压 3节5号电池(3.6-4.5V)或USB(通常为5V)。经测试在3.6v的外接电源也能正常工作,也就是说这个游戏机可以直接改为锂电池供电。
任务:这里选择的是任务一,在这个游戏手柄上实现经典游戏——俄罗斯方块。
实现过程:
1、选择任务。拿到开发板后,非常开心,板子很漂亮。两层透明亚克力外壳,非常适合手握。PCB貌似是2mm的板子,强度很不错,按键手感也很好。按funpack的页面介绍,了解了一些微软的makecode编程,我的理解makecode基本是一个图形化编程方式,感觉面向青少年的,自己不是太喜欢,总感觉很多逻辑用图形方式展示不清楚。任务有两个,想跳过makecode直接对芯片编程,实现莫斯电码。花了一段时间,找了些资料,又看了直播课,感觉直接芯片编程应该是不可行。于是就切换任务,来实现任务一,来实现玩俄罗斯方块。选定了任务,上网寻找相关资源,发现关于makecode很多教程,都是教青少年在这个硬件上做游戏的,按着教程入门了一下,发现一个问题,几乎所有的教程教的游戏都是关于角色扮演类的(+﹏+) ,俄罗斯方块类型的游戏资料很少!但是既然选了这个任务,就一定要完成!
2、游戏介绍。俄罗斯方块可是个经典老游戏啦!通过不停第掉落不同形状的方块,用手柄控制姿态,然后摆放,当一行摆满了就消除一行,直到无法摆放则游戏失败。俄罗斯方块有很多变种,有更加复杂的方块玩法,这里做了简化,仅仅实现最简单的游戏。掉落的方块有7种类型。条状(最让人喜欢的,能救命的,由四个方块组成的直线)、L状(4个方块组成,带尾巴),L状的镜像、正方形(4个小方块组成的大方块)、T型(中间凸起的)、Z型及其镜像(最烦人的形状)。屏幕每行为19个方块,高度为18个方块。
3、实现。通过直播课和网上的资源,慢慢地觉得图形化编程真心不合适做俄罗斯方块这样类型的游戏。好在微软还提供了javascript和python的方式编程。但是提供的python很奇怪,感觉和自己了解的python不太一样,而且感觉对python支持也不是太好。在实现过程中发现有时javascript的代码能够通过,但是转到图形界面就会出错,原因暂时未知。所以这里就使用了单纯的javascript编程。
首先定义7种不同的方块,每种方块有4种变形,对应着4个方向的旋转。这里每个图形都用数组矩阵常量来定义,每种图形用一个字母来标识
const I = [
[
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
],
[
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
],
[
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
],
[
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
]
]
const J = [
[
[1, 0, 0],
[1, 1, 1],
[0, 0, 0]
],
[
[0, 1, 1],
[0, 1, 0],
[0, 1, 0]
],
[
[0, 0, 0],
[1, 1, 1],
[0, 0, 1]
],
[
[0, 1, 0],
[0, 1, 0],
[1, 1, 0]
]
]
const L = [
[
[0, 0, 1],
[1, 1, 1],
[0, 0, 0]
],
[
[0, 1, 0],
[0, 1, 0],
[0, 1, 1]
],
[
[0, 0, 0],
[1, 1, 1],
[1, 0, 0]
],
[
[1, 1, 0],
[0, 1, 0],
[0, 1, 0]
]
]
const O = [
[
[0, 0, 0, 0],
[0, 1, 1, 0],
[0, 1, 1, 0],
[0, 0, 0, 0],
],
[
[0, 0, 0, 0],
[0, 1, 1, 0],
[0, 1, 1, 0],
[0, 0, 0, 0],
],
[
[0, 0, 0, 0],
[0, 1, 1, 0],
[0, 1, 1, 0],
[0, 0, 0, 0],
],
[
[0, 0, 0, 0],
[0, 1, 1, 0],
[0, 1, 1, 0],
[0, 0, 0, 0],
]
]
const S = [
[
[0, 1, 1],
[1, 1, 0],
[0, 0, 0]
],
[
[0, 1, 0],
[0, 1, 1],
[0, 0, 1]
],
[
[0, 0, 0],
[0, 1, 1],
[1, 1, 0]
],
[
[1, 0, 0],
[1, 1, 0],
[0, 1, 0]
]
]
const T = [
[
[0, 1, 0],
[1, 1, 1],
[0, 0, 0]
],
[
[0, 1, 0],
[0, 1, 1],
[0, 1, 0]
],
[
[0, 0, 0],
[1, 1, 1],
[0, 1, 0]
],
[
[0, 1, 0],
[1, 1, 0],
[0, 1, 0]
]
]
const Z = [
[
[1, 1, 0],
[0, 1, 1],
[0, 0, 0]
],
[
[0, 0, 1],
[0, 1, 1],
[0, 1, 0]
],
[
[0, 0, 0],
[1, 1, 0],
[0, 1, 1]
],
[
[0, 1, 0],
[1, 1, 0],
[1, 0, 0]
]
]
每个方块在运动中,遇到边界或其它方块则阻挡时,无法旋转。这个逻辑单独做出一个函数,这里也是无法使用图形化编程的主要原因,这个函数的入口参数有个数组,在图形界面中总是不行。
function collision(piece: number[][], x: number, y: number) {
for (let row = 0; row <= piece.length - 1; row++) {
for (let col = 0; col <= piece[row].length - 1; col++) {
if (piece[row][col] == 0) {continue}
if (pieceY < 0) {continue}
if ((pieceX + col + x < 0) || (pieceX + col + x > COL) || (pieceY + row + y > ROW))
{
music.playTone(Note.F, BeatFraction.Half)
return true
}
if (grid[pieceY + row + y][pieceX + col + x] != VACANT)
{
music.playTone(Note.E5, BeatFraction.Half)
return true
}
}
}
return false
}
主要的处理事件方法是定时处理,这里选用每500毫秒调用一次。如果觉得难度简单,可以调小这个函数game.onUpdateInterval的第一个入口参数,单位是毫秒。
这里的处理逻辑是先调用collision函数判断是否能移动,不能移动 判断是否堆满了,如果堆满了,当然是游戏结束(失败)。没堆满的话,检查是否一行方块凑齐了,凑齐了的话消减一行并播放音乐,产生新的块。能移动的话,就向下移动。
game.onUpdateInterval(500, function () {
if (collision(currPiece[pieceRot], 0, 1)) {
if (pieceY > 0) {
lockPiece(currPiece[pieceRot])
checkLine()
newPiece()
}else{
game.over(false)
}
} else {
drawPiece(currPiece[pieceRot], pieceX, pieceY, VACANT)
pieceY++
drawPiece(currPiece[pieceRot], pieceX, pieceY, pieceColor)
}
})
按键处理事件。左右键按下事件,对应当前方块向左或向右移动。下键,对应两个触发事件,一个是下键按下事件,当前方块向下移动一格;下键被重复按动了,当前方块加速下移。A键按下,对应顺时针旋转90度事件,B键对应逆时针旋转90度事件。
// Controls
controller.A.onEvent(ControllerButtonEvent.Pressed, function () {
let newRot: number
if (pieceRot != 3) {
newRot = pieceRot + 1
} else {
newRot = 0
}
if (collision(currPiece[newRot], 0, 0)) {
} else {
drawPiece(currPiece[pieceRot], pieceX, pieceY, VACANT)
pieceRot = newRot
drawPiece(currPiece[pieceRot], pieceX, pieceY, pieceColor)
}
})
controller.B.onEvent(ControllerButtonEvent.Pressed, function () {
let newRot: number
if (pieceRot != 0) {
newRot = pieceRot - 1
} else {
newRot = 3
}
if (collision(currPiece[newRot], 0, 0)) {
} else {
drawPiece(currPiece[pieceRot], pieceX, pieceY, VACANT)
pieceRot = newRot
drawPiece(currPiece[pieceRot], pieceX, pieceY, pieceColor)
}
})
controller.down.onEvent(ControllerButtonEvent.Pressed, function () {
if (collision(currPiece[pieceRot], 0, 1)) {
} else {
drawPiece(currPiece[pieceRot], pieceX, pieceY, VACANT)
pieceY++
drawPiece(currPiece[pieceRot], pieceX, pieceY, pieceColor)
}
})
controller.down.onEvent(ControllerButtonEvent.Repeated, function () {
if (collision(currPiece[pieceRot], 0, 1)) {
} else {
drawPiece(currPiece[pieceRot], pieceX, pieceY, VACANT)
pieceY++
drawPiece(currPiece[pieceRot], pieceX, pieceY, pieceColor)
}
})
controller.left.onEvent(ControllerButtonEvent.Pressed, function () {
if (collision(currPiece[pieceRot], -1, 0)) {
} else {
drawPiece(currPiece[pieceRot], pieceX, pieceY, VACANT)
pieceX--
drawPiece(currPiece[pieceRot], pieceX, pieceY, pieceColor)
}
})
controller.right.onEvent(ControllerButtonEvent.Pressed, function () {
if (collision(currPiece[pieceRot], 1, 0)) {
} else {
drawPiece(currPiece[pieceRot], pieceX, pieceY, VACANT)
pieceX++
drawPiece(currPiece[pieceRot], pieceX, pieceY, pieceColor)
}
})
完成的程序:俄罗斯方块
遇到的问题:1 想在game.onUpdateInterval函数中将速度做为变量,当消掉一行后提高速度,但是不起作用。该函数应该是没有重入 2 程序启动后屏幕颜色会变换,不知道为什么,多次reset后就正常了。3 感觉在实体机上不如在模拟器上灵活,有时感觉按键响应不是太灵敏,重复按下键后,有时会突然加速多次。
心得体会:感谢funpack带来的这期活动。见识了makecode图形化编程。才发现面对青少年编程教育也是进行的如火如荼,将枯燥的编程,变得有趣着实不易,同时拓宽了视野,同样的问题,不同角度去解决,总有意想不到的惊喜。