文章目录
零、前言
- 目的:以纯原生 HTML、JS 实现射击类的俄罗斯方块。
- 本文内容:从零开始一步一步介绍如何完成全部代码。
- 本文缺点:太多了。全局变量太多、太散;没有设计动画;没有游戏计分;游戏只能玩一次,再玩要刷新等等。
- 本文适合读者:有一些html、js基础或没有基础但有灵性,想探索性了解更多。
- 本文不适合读者:前端大佬,你们看了满脑子只想喷了,我自己看了都想喷。
- 游戏编写耗时:不到3小时。速成品,轻喷,多谢。
- 代码总行数:二百五左右,比较符合我的气质。
- 技术点:canvas、requestAnimationFrame
- 游戏预览:
一、编写 HTML 和 CSS 代码
1. H5 基本代码
很简单,啥也没有:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Game</title>
</head>
<body>
</body>
</html>
2. 游戏外框、开始按钮
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Game</title>
</head>
<body>
<div id="container">
<canvas id="game" width="132" height="240"></canvas>
</div>
<div id="start">开始游戏</div>
</body>
</html>
3. 增加一些样式
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Game</title>
<style>
#container {
width: min-content;
margin: auto;
padding: 1px;
border: 1px solid #000;
}
#start {
border: 1px solid #000;
width: 100px;
line-height: 30px;
text-align: center;
cursor: pointer;
margin: 10px auto
}
</style>
</head>
<body>
<div id="container">
<canvas id="game" width="132" height="240"></canvas>
</div>
<div id="start">开始游戏</div>
</body>
</html>
二、编写 js 代码
本文从这里开始的以下内容不再附全部代码,因为全是js代码。
1. 定义全局变量
const canvas = document.getElementById("game"),
ctx = canvas.getContext("2d"),
dataList = [], // 存储全部数据
w = 20, h = 20, // 每个小方块的宽高
hw = w * 0.5, hh = h * 0.5, // 每个小方块的宽高的一半
dis = 4, // 小方块之间的间距
dw = w + dis, dh = h + dis, // 算上间距小方块占的宽高
countOfOneLine = 11, // 一行有多少个小方块
initLineCount = 5, // 初始化有多少行
maxLineCount = 20, // 最大行数,超出则游戏失败
difficulty = 0.5; // 游戏难度,即每个位置有小方块的概率(范围在 0-1 之间)
let carIndex, // 定义发射车位置
lastTimestamp, // 用来记录增加一行的时间戳
status = "stop", // 用来控制游戏状态:stop、playing、pause
speed = 3000; // 游戏增加一行的速度时间
canvas.width = countOfOneLine * dw - 0.5 * dis;
canvas.height = maxLineCount * dh + 0.5 * dis;
2. 定义数据相关的方法
// 生成一行
function generateOneLine() {
const oneLine = [];
for (let i = 0; i < countOfOneLine; i++) {
oneLine[oneLine.length] = Math.random() < difficulty;
}
return oneLine;
}
// 真正地增加一行
function increaseOneLine() {
while (true) {
const oneLine = generateOneLine();
// 避免全是 true 和全是 false
if (!(oneLine.every(i => i) || oneLine.every(i => !i))) {
dataList.splice(0, 0, oneLine);
break;
}
}
}
// 方法里判断是否应该增加一行
function increase() {
let timestamp = Date.now();
if (timestamp - lastTimestamp > speed) {
lastTimestamp = timestamp;
increaseOneLine();
}
}
// 清理那些可以清理的行
function clearLine() {
for (let i = dataList.length - 1; i >= 0; i--) {
if (dataList[i].every(data => data)) {
dataList.splice(i, 1);
}
}
}
// 判断是否游戏存活
function isAlive() {
return dataList.length < maxLineCount - 2;
}
3. 定义初始化方法(游戏开始)
function startGame() {
dataList.length = 0;
for (let i = 0; i < initLineCount; i++) {
const oneLine = generateOneLine();
// 初始化不做全空全有校验,看天意
dataList[dataList.length] = oneLine;
}
carIndex = Math.floor(countOfOneLine * 0.5);
lastTimestamp = Date.now();
status = "playing";
}
4. 定义小车相关方法
// 发射车左移
function moveLeft() {
if (carIndex === 0) {
return;
}
carIndex--;
}
// 发射车右移
function moveRight() {
if (carIndex === countOfOneLine - 1) {
return;
}
carIndex++;
}
5. 定义 canvas 绘制相关方法
下面绘制的方法中有加 0.5,原因自行查询哦,或者你可以试试不加 0.5 看看有啥区别。
/**
* 绘制一个小方块
* @param {number} x
* @param {number} y
*/
function drawOne(x, y) {
ctx.beginPath();
ctx.moveTo(x - hw, y - hh);
ctx.lineTo(x + hw, y - hh);
ctx.lineTo(x + hw, y + hh);
ctx.lineTo(x - hw, y + hh);
ctx.closePath();
ctx.stroke();
}
// 绘制发射车
function drawCar() {
drawOne(carIndex * dw + hw + 0.5, maxLineCount * dh - hh + 0.5);
drawOne((carIndex + 1) * dw + hw + 0.5, maxLineCount * dh - hh + 0.5);
drawOne((carIndex - 1) * dw + hw + 0.5, maxLineCount * dh - hh + 0.5);
drawOne(carIndex * dw + hw + 0.5, (maxLineCount - 1) * dh - hh + 0.5);
}
// 绘制死亡提示线(游戏结束线)
function drawDeadline() {
ctx.beginPath();
ctx.moveTo(0, (maxLineCount - 3) * dh - dis * 0.5 + 0.5);
ctx.lineTo(canvas.width, (maxLineCount - 3) * dh - dis * 0.5 + 0.5);
ctx.stroke();
}
function pausing() {
let x = Math.round(canvas.width * 0.5), y = Math.round(canvas.height * 0.5);
ctx.clearRect(x - 41, y - 21, 82, 42);
ctx.beginPath();
ctx.moveTo(x - 40.5, y - 20.5);
ctx.lineTo(x + 40.5, y - 20.5);
ctx.lineTo(x + 40.5, y + 20.5);
ctx.lineTo(x - 40.5, y + 20.5);
ctx.closePath();
ctx.stroke();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "16px Arial";
ctx.fillText("暂停中", x, y + 2);
}
function gameOver() {
let x = Math.round(canvas.width * 0.5), y = Math.round(canvas.height * 0.5);
ctx.clearRect(x - 41, y - 21, 82, 42);
ctx.beginPath();
ctx.moveTo(x - 40.5, y - 20.5);
ctx.lineTo(x + 40.5, y - 20.5);
ctx.lineTo(x + 40.5, y + 20.5);
ctx.lineTo(x - 40.5, y + 20.5);
ctx.closePath();
ctx.stroke();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "16px Arial";
ctx.fillText("游戏结束", x, y + 2);
}
6. 添加一些事件
document.getElementById("start").addEventListener("click", () => status === "stop" && startGame());
document.addEventListener("keydown", e => {
if (status !== "playing") {
if(status === "pause" && e.key === " "){
status = "playing";
}
return;
}
switch (e.key) {
case "ArrowLeft":
moveLeft();
break;
case "ArrowRight":
moveRight();
break;
case "x":
if (dataList.length === 0 || dataList[dataList.length - 1][carIndex]) {
const arr = new Array(countOfOneLine).fill(false);
arr[carIndex] = true;
dataList[dataList.length] = arr;
} else {
let flag = true;
for (let i = dataList.length - 1; i >= 0; i--) {
if (dataList[i][carIndex]) {
dataList[i + 1][carIndex] = true;
flag = false;
break;
}
}
if (flag) {
dataList[0][carIndex] = true;
}
}
clearLine();
break;
case " ": // 空格
status = "pause";
break;
}
});
window.addEventListener("blur", () => status === "playing" && (status = "pause"));
7. 重点来了
function animation() {
switch (status) {
case "playing":
increase();
ctx.clearRect(0, 0, canvas.width, canvas.height);
dataList.forEach((data, y) => data.forEach((d, x) => d && drawOne(x * dw + hw + 0.5, y * dh + hh + 0.5)));
drawCar();
drawDeadline();
if (!isAlive()) {
gameOver();
status = "stop";
}
break;
case "pause":
pausing();
break;
}
window.requestAnimationFrame(animation);
}
animation();