古之立大事者,不惟有超世之才,亦必有坚忍不拔之志。——苏轼
案例效果
原生 JavaScript 实现的 别踩方块小游戏案例效果如下所示:
案例分析
静态页面
将这个静态页面可以划分为四个部分,如下图所示:
JavaScript 行为
- 黑块的行为
- 生成新的黑块
在视觉效果的以外生成一个新的黑块,这样显得不是很怪 - 黑块下落
在定时器中将黑块的 top 值次增,移动到视觉效果外将其通过Element.remove()
方法删除掉。
- 游戏的逻辑
- 持续生成黑块 -> 游戏开始的逻辑
通过定时器重新生成新的黑块,重复下落 - 停止黑块的生成 -> 游戏结束的逻辑
停止游戏开始的逻辑 - 按键控制黑块消失 -> 游戏交互的逻辑
通过keydown
事件获取 左 下 右 三个按键,根据按键和最新一行的位置,进行删除
代码实现
别踩方块儿的骨架和肉体
HTML 和 CSS 的代码如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>别踩白块儿</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
background-color: #ddeedd;
}
main {
width: 210px;
height: 540px;
}
/* 设置文字的的样式 */
#text {
width: 210px;
height: 150px;
text-align: center;
position: absolute;
top: -3%;
left: 50%;
transform: translateX(-50%);
}
/* 中间内容区的样式 */
main,
#container,
.inst-arrow-left,
.inst-arrow-down,
.inst-arrow-right {
/* 水平垂直居中 */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#container {
height: 360px;
width: 210px;
overflow: hidden;
border: 4px solid #333;
}
table {
height: 540px;
width: 210x;
border: 1px solid #000;
/* 相邻单元格公用一条线 */
border-collapse: collapse;
position: relative;
top: -96px;
left: 0;
}
td {
border: 1px solid #333;
height: 90px;
width: 70px;
}
/* 游戏说明样式 */
#instructions {
width: 210px;
position: absolute;
top: 15%;
left: 50%;
transform: translateX(-50%);
}
#instructions>.arrow>div {
float: left;
width: 70px;
height: 90px;
position: relative;
top: 360px;
}
#instructions .arrow .arrow-img {
display: block;
width: 30px;
}
.inst-arrow-left {
transform: rotate(-90deg) translateY(-50%) translateX(50%);
}
.inst-arrow-right {
transform: rotate(90deg) translateY(50%) translateX(-50%);
}
.inst-arrow-down {
transform: rotate(180deg) translateY(50%) translateX(50%);
}
/* 黑色方块的 CSS 样式 */
.test1,
.test2,
.test3 {
width: 65.2px;
height: 88.4px;
display: table-cell;
position: absolute;
top: -92px;
background-color: #000;
}
.test1 {
left: 0%;
}
.test2 {
left: 33.33%;
}
.test3 {
left: 66.66%;
}
#button {
position: absolute;
width: 210px;
height: 50px;
}
#gameStard,
#gameEnd {
margin: 520px 15px;
}
</style>
</head>
<body>
<main>
<!-- 计时器和分数 -->
<div id="text">
<h2>别踩白块儿</h2>
<p>分数 : <stan id="span">0</stan>
</p>
<!-- <p>时间 : <span>0</span></p> -->
</div>
<!-- 中间白块区域 -->
<div id="container">
<table>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
</div>
<div id="instructions">
<div class="arrow">
<div><img class="arrow-img inst-arrow-left" src="./imgs/jiantou12-copy.png"></div>
<div><img class="arrow-img inst-arrow-down" src="./imgs/jiantou12-copy.png"></div>
<div><img class="arrow-img inst-arrow-right" src="./imgs/jiantou12-copy.png"></div>
</div>
</div>
<div id="button">
<button id="gameStard">游戏开始</button>
<button id="gameEnd">游戏结束</button>
</div>
</main>
<script src="./behavior2.js"></script>
</body>
</html>
代码中注意的点:
- 盒子模型中由于边框和边距会最终响应我们盒子的内容区的大小,我们设置属性
box-sizing: border-box;
固定盒子的大小 - 表格中上面和下面在视觉区留出一行的区域做生成和判断
效果图如下所示
别踩方块儿的灵魂
实现流程:
- 定义一个黑色方块构造函数,其中包含两个方法,一个是创建新行的方法,另一个是向下移动的方法
- 定义一个游戏的构造函数,此构造函数通过借助构造函数的方式继承于创建黑色方块的构造函数,此对象中定义三个方法:
- 游戏开始的方法
- 游戏结束的方法
- 游戏交互的方法
- 完成点击 游戏开始 按钮开始游戏,和点击 游戏结束 按钮结束游戏的逻辑
示例代码如下所示:
// 获取容器节点
var container = document.getElementById('container')
// 获取body 方便绑定事件
var body = document.body
var span = document.getElementById('span')
/*
* 定义黑块的构造函数
* 属性: flag -> 方块是否到达底部的标志
* 方法: creBlackBlockLine(container) -> 创建一个新的黑块的方法
* 方法: moveDownward(div) -> 方块移动的方法
*/
function CreBlackBlocks() {
// 方块是否到达底部的标志
this.flag = false
// 创建一个新的黑块的方法
this.creBlackBlockLine = function (container) {
// 创建 div 节点
var div = document.createElement('div')
// 位置,表示当前方块在第几行
var location
container.appendChild(div)
// 随机为 div 分配 class 属性
var random = Math.random();
if (random <= 0.33) {
div.setAttribute('class', 'test1')
location = 0
} else if (random > 0.33 && random <= 0.66) {
div.setAttribute('class', 'test2')
location = 1
} else {
div.setAttribute('class', 'test3')
location = 2
}
return {
// 返回一个对象,包含位置和节点
myElement: div,
myLocation: location
}
}
this.moveDownward = function (div) {
// 获取有效属性
var divStyles = window.getComputedStyle(div.myElement)
// 获取内联样式样
var divStyle = div.myElement.style
var divStyleTop = parseInt(divStyles.top)
var t = setInterval(() => {
// 内联盖外联
divStyleTop++
divStyle.top = divStyleTop + "px"
// 越界则删除
if (divStyleTop >= 360) {
clearInterval(t)
div.myElement.remove()
}
}, 1)
}
}
/*
* 定义游戏对象, 继承于 CreBlackBlocks 构造函数
* 属性: divElementArr -> 存储创建的数组
* 属性: divPosArr -> 每一行位置的数组
* 属性: score -> 分数属性
* 方法: theGameStartsNow() -> 开始的方法
* 方法: gameOver() -> 结束的方法
* 方法: keyReaction() -> 交互的逻辑
*/
function Game(container) {
// 存储创建每一行的返回值
var newDiv
// 存储创建的数组
this.divElementArr = []
// 每一行位置的数组
this.divPosArr = []
// 借助构造函数继承
CreBlackBlocks.call(this, container)
// 分数属性
this.score = 0
// 定义定义开始
this.theGameStartsNow = function (game) {
var t = setInterval(() => {
newDiv = this.creBlackBlockLine(container)
this.moveDownward(newDiv)
game.divElementArr.push(newDiv.myElement)
// 存储每一个方块第几行的数组
game.divPosArr.push(newDiv.myLocation)
// 没有按到方块结束的逻辑
if (parseInt(window.getComputedStyle(this.divElementArr[0]).top) >= 357) {
game.flag = true
}
// 结束执行的行为
if (game.flag == true) {
clearInterval(t)
for (div in game.divElementArr) {
game.divElementArr[div].remove()
}
alert('游戏结束!\n分数为:' + game.score)
game.flag = false
// 游戏数据初始化
game.score = 0
game.divElementArr = []
game.divPosArr = []
}
}, 360);
return t
}
// 定义游戏结束
this.gameOver = function (t, game) {
for (div in game.divElementArr) {
game.divElementArr[div].remove()
}
clearInterval(t)
alert('游戏结束!\n分数为:' + game.score)
// 游戏数据初始化
game.score = 0
game.divElementArr = []
game.divPosArr = []
}
// 键位点击做出相应的动作
this.keyReaction = function (game, t) {
body.addEventListener('keydown', function (event) {
// 通过 switch 语句 根据当前的黑块的位置进入逻辑
// Array.splice()方法 -> 作用,删除中的元素并返回一个数组,第一个参数 位置第二个参数数量
switch (event.code) {
case 'ArrowLeft':
if (game.divPosArr[0] === 0) {
game.divPosArr.splice(0, 1)
game.score += 10
span.innerHTML = game.score
game.divElementArr.splice(0, 1)[0].remove()
}
break;
case 'ArrowDown':
if (game.divPosArr[0] === 1) {
game.divPosArr.splice(0, 1)
game.score += 10
span.innerHTML = game.score
game.divElementArr.splice(0, 1)[0].remove()
}
break;
case 'ArrowRight':
if (game.divPosArr[0] === 2) {
game.divPosArr.splice(0, 1)
game.score += 10
span.innerHTML = game.score
game.divElementArr.splice(0, 1)[0].remove()
}
break;
default:
break;
}
})
}
}
var game = new Game(container)
var t
// 游戏开始
var gameStard = document.getElementById('gameStard')
gameStard.addEventListener('click', function () {
span.innerHTML = 0
t = game.theGameStartsNow(game)
game.keyReaction(game, t)
})
// 游戏结束
var gameEnd = document.getElementById('gameEnd')
gameEnd.addEventListener('click', function () {
game.gameOver(t, game)
})
遇到的问题:
- 在获取位置的时候定义一个变量用于存储第一行的位置,当生成新的行时,旧的值就会被替换,导致在删除的过程中遇到了问题
解决方案:通过这种问题想到了 队列 ,队列的特点是先进先出,队列在 JavaScript 可以使用数组实现,通过数组存储每一行的位置,最前面的一行就是最新的。 - 在删除一样的时候,遇到了和数组同样的问题,就是被重新赋值,依旧通过 JavaScript 中的数组帮我们解决这个问题。
写在最后
这就是别踩方块儿的全部代码,当然还有很多可以优化的地方,等到有时候回头写吧。