心血来潮搞了个俄罗斯方块,发上来给大家分享分享!
项目地址
demo
首先,确定模块:
1、舞台场景模块:要在这里面玩啊。。。
2、随机图形模块:不然玩什么啊。。。
3、控制模块:不然怎么玩啊。。。
4、时间计时器模块:得让2自动跑啊。。。
5、逻辑判断等等等等。。。。。。
然后,整理开发思路:
1、俄罗斯方块是典型的矩阵类型的小游戏,所以数据类型就选择矩阵来做……(也考虑过链表、图等数据结构,但可能对于这种类型的数据还是矩阵来的容易点)
2、数据类型确定了,面向对象也是必须的:我这里把整个游戏场景作为一个类,大部分的处理逻辑,游戏功能等都在这里面;把图形对象作为另外一个类,具备的功能更简单,就是描述这个图形长什么样,还有就是记录id(这个后边在源码里解释)
3、渲染部分不多说,选择vue来做渲染器,利用数据驱动视图的特点。。。可以选择任何一种前段框架,或者原生dom,canvas的各种框架都可以,这里不做解释了
还是上代码吧:
首先是自增id和图形类:
let id = -1
/**
* 自增ID
* @returns {number}
*/
function getId () {
return id++;
}
/**
* 图形对象
*/
class Cube {
constructor (index, arr) {
this.index = index
this.arr = arr
this.id = getId()
}
}
这样我就有了一个每次实例化都能自增唯一id的一个图形对象了!
这里主要讲解一下矩阵旋转,涉及到一些高数的东西,并不是特别难:
我想把一个矩阵顺时针旋转90°,我需要做的是,首先将矩阵的竖向顺序倒序,然后再求倒序之后矩阵的转置(具体可以百度,其实就是把矩阵沿对角线翻转),得到的矩阵就是旋转之后的了。
假设矩阵:
【 1 ,2 ,3】 【7 ,8 ,9】 【7 ,4 ,1】
【4 ,5 ,6】 =》倒序 =》【4 ,5 ,6】=》转置=》 【8 ,5 ,2】=》成功!
【7 ,8 ,9】 【 1 ,2 ,3】 【9 ,6 ,3】
主要判断逻辑都在canPut()这个方法上,判断一个图形可不可以放在特定位置上,下面代码敬上!
/**
* 舞台对象
*/
class Tetris {
constructor () {
/**
* 舞台的矩阵数据
* @type {null}
*/
this.cube = null
/**
* 当前正在操纵的图形对象
* @type {null}
*/
this.controlling = null
/**
* 全局计时器
* @type {null}
*/
this.ticker = null
/**
* 游戏状态
* @type {boolean}
*/
this.isPlaying = true
/**
* 图形仓库
* @type {[null,null,null,null,null]}
*/
this.shapes = [
[[0, 1], [0, 1], [0, 1, 1]],
[[0, 1, 0], [1, 1, 1]],
[[0, 1, 0], [0, 1, 0], [0, 1, 0], [0, 1, 0]],
[[0, 1], [1, 1], [1, 0]],
[[1, 0], [1, 1], [0, 1]]
]
/**
* 下一个要出现的图形
* 要利用随机数在上方仓库中随机产生一个
* @type {Cube}
*/
this.next = new Cube({x: 4, y: 0}, this.shapes[parseInt(Math.random() * this.shapes.length)])
/**
* 分数 消去一行就++
* @type {number}
*/
this.score = 0
/**
* 是否已经死掉了。。。
* @type {boolean}
*/
this.dead = false
}
/**
* 初始化舞台
* @param a 几行
* @param b 几列
*/
init (a, b) {
let arr = []
for (let i = 0; i < a; i++) {
arr[i] = []
for (let j = 0; j < b; j++) {
arr[i][j] = 0
}
}
this.cube = arr
}
/**
* 将图形渲染到舞台上
* 其实就是把图形的1写到舞台的对应位置上
* @param cube
*/
render (cube) {
let length = Math.max(cube.arr.length, cube.arr[0].length)
for (let i = 0; i < length; i++) {
if (cube.arr[i] === undefined) cube.arr[i] = []
for (let j = 0; j < length; j++) {
cube.arr[i][j] = cube.arr[i][j] === undefined ? 0 : cube.arr[i][j]
if (this.cube[i + cube.index.y] && this.cube[i + cube.index.y][j + cube.index.x] === 1 || cube.arr[i][j] === 0) continue
this.cube[i + cube.index.y][j + cube.index.x] = cube.arr[i][j]
}
}
}
/**
* 清除舞台
*/
clean () {
let temp = []
this.cube.map((val,ind)=>{
temp[ind] = []
val.map((v,i)=>{
temp[ind][i] = 0
})
})
this.cube = temp
}
/**
* 产生下一个图形
*/
getNext () {
this.next = new Cube({x: 4, y: 0}, this.shapes[parseInt(Math.random() * this.shapes.length)])
}
/**
* 将图形添加到舞台上
* @param cube
* @returns {*}
*/
add (cube) {
let temp = cube || this.next
if (!this.canPut(temp)) {
this.end()
return false
}
this.render(temp)
this.update()
this.getNext()
return temp
}
/**
* 更新舞台数据
* 之所以清空数组是为了触发vue的变化。。。这里可以不这么写。。。
*/
update () {
// this.check()
let tempt = this.cube
this.cube = []
this.cube = tempt
}
/**
* 开始游戏
*/
start () {
this.dead = false
this.clean()
this.stageTicker((e) => {
if (this.controlling === null) return
this.bottom()
})
this.controlling = this.add()
}
/**
* 结束游戏
*/
end(){
this.dead = true
this.destroy()
console.log('ends')
}
/**
* 删除第i行
* @param i
*/
removeLine (i) {
this.cube.splice(i, 1)
}
/**
* 添加一行空行到第i个位置
* @param i
*/
addLine (i) {
let temp = []
for (let i = 0; i < this.cube[0].length; i++) {
temp[i] = 0
}
this.cube.splice(i, 0, temp)
}
/**
* 将舞台中的cube图形删除
* @param cube
*/
remove (cube) {
for (let i = 0; i < cube.arr.length; i++) {
for (let j = 0; j < cube.arr[i].length; j++) {
if (this.cube[i + cube.index.y] && this.cube[i + cube.index.y][j + cube.index.x] === cube.arr[i][j]) {
this.cube[i + cube.index.y][j + cube.index.x] = 0
}
}
}
}
/**
* 判断是否可以渲染在当前位置
* @param cube
* @returns {boolean}
*/
canPut (cube) {
for (let i = 0; i < cube.arr.length; i++) {
if (this.cube[i + cube.index.y] === undefined && cube.arr[i].find(val => {return val === 1}) !== undefined) {
return false
}
for (let j = 0; j < cube.arr[i].length; j++) {
if (this.cube[i + cube.index.y] && this.cube[i + cube.index.y][j + cube.index.x] === undefined && cube.arr.find(val => {return val[j] === 1}) !== undefined) return false
if (this.cube[i + cube.index.y] && this.cube[i + cube.index.y][j + cube.index.x] === 0) {
continue
} else {
if (cube.arr[i][j] === 1) {
return false
} else {
continue
}
}
}
}
return true
}
/**
* 检查舞台状态,如果有满行就删除并计分
*/
check () {
this.cube.map((val, i) => {
if (val.find(v => {return v === 0}) === undefined) {
this.removeLine(i)
this.addLine(0)
this.score++
}
})
}
/**
* 计时器模块(本来想单独写一个类,后来发现一个方法就足够了。。。有更复杂的需求再说)
* 可传回调方法cb
* @param cb
*/
stageTicker (cb) {
let i = 0
this.ticker = setInterval(() => {
this.update()
i++
if (typeof cb === 'function' && this.isPlaying) {
cb(i++)
}
}, 500)
}
/**
* 暂停和继续
*/
pause () {
this.isPlaying = !this.isPlaying
}
/**
* 销毁计时器,可以再多些功能
* TODO 完善功能
*/
destroy () {
if(this.ticker!==null) clearInterval(this.ticker)
}
/**
* 数组深度拷贝方法
* @param from
* @returns {Array}
*/
arrayCopy (from) {
let temp = []
for (let i = 0; i < from.length; i++) {
if (Array.isArray(from[i])) {
temp[i] = this.arrayCopy(from[i])
} else {
temp[i] = from[i]
}
}
return temp
}
/**
* 矩阵旋转方法
*/
translate () {
let cube = this.controlling
let temp = this.arrayCopy(cube.arr)
temp = temp.reverse()
for (let i = 0; i < temp.length; i++) {
for (let j = 0; j < temp[i].length; j++) {
if (i >= j) continue
let tt = temp[i][j] === undefined ? 0 : temp[i][j]
if (temp[j] === undefined) temp[j] = []
temp[i][j] = temp[j][i] === undefined ? 0 : temp[j][i]
temp[j][i] = tt
}
}
this.remove(cube)
if (!this.canPut({index: cube.index, arr: temp})) {
this.add(cube.index, cube.arr)
return
}
cube.arr = temp
this.render(cube)
this.update()
}
left () {
if(!this.controlling) return
let temp = this.controlling.index.x
this.remove(this.controlling)
this.controlling.index.x--
if (!this.canPut(this.controlling)) this.controlling.index.x = temp
this.render(this.controlling)
this.update()
}
top () {
if(!this.controlling) return
let temp = this.controlling.index.y
this.remove(this.controlling)
this.controlling.index.y--
if (!this.canPut(this.controlling)) this.controlling.index.y = temp
this.render(this.controlling)
this.update()
}
right () {
if(!this.controlling) return
let temp = this.controlling.index.x
this.remove(this.controlling)
this.controlling.index.x++
if (!this.canPut(this.controlling)) this.controlling.index.x = temp
this.render(this.controlling)
this.update()
}
bottom () {
if(!this.controlling) return
let temp = this.controlling.index.y
this.remove(this.controlling)
this.controlling.index.y++
if (!this.canPut(this.controlling)) {
this.controlling.index.y = temp
this.render(this.controlling)
this.check()
this.controlling = this.add()
this.update()
return null
} else {
this.render(this.controlling)
}
this.update()
}
down(){
for(let i =0;i