前言
本文将介绍一个基于html+js的生命游戏,该项目只有一个html代码,无任何其他以来,UI方面采用了vue+element-plus进行渲染,游戏的界面基于canvas进行渲染,先来看一下成果。
我不知道游戏规则有没有写错,感觉经常会陷入循环中。
游戏规则
这边给出文心一言给出的游戏规则
根据以上规则写的代码如下
function calIter() {
var tmp = new Array(cellHeight).fill(0).map(() => new Array(cellWidth).fill(0));
for(let i=0; i<cellWidth; i++) {
for(let j=0; j<cellHeight; j++) {
// 计算周围的细胞数
let num = 0;
if (i-1>=0 && j-1>=0 && cells[i-1][j-1]==1) num++;
if (i-1>=0 && cells[i-1][j]==1) num++;
if (i-1>=0 && cells[i-1][j+1]==1) num++;
if (i+1<cellWidth && cells[i+1][j]==1) num++;
if (i+1<cellWidth && j-1>=0 && cells[i+1][j-1]==1) num++;
if (i+1<cellWidth && j+1<cellHeight && cells[i+1][j+1]==1) num++;
if (j-1>=0 && cells[i][j-1]==1) num++;
if (j+1<cellHeight && cells[i][j+1]==1) num++;
if (cells[i][j] == 0 && num >= 3) {
tmp[i][j] = 1;
} else if (num<=1 || num>4){
tmp[i][j] = 0;
}
}
}
for(let i=0; i<cellWidth; i++) {
for(let j=0; j<cellHeight; j++) {
cells[i][j] = tmp[i][j];
}
}
rounds++;
}
代码
所有的代码都写在了一个html里面,没有任何其他依赖,复制后就能运行,不过需要联网,因为通过cdn的方式引入了vue+element-plus。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!-- 引入样式 -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/element-plus/2.3.3/index.css" rel="stylesheet">
<!-- 引入vue3 -->
<script src="https://unpkg.com/vue@3"></script>
<!-- 引入element plus -->
<script src="https://cdn.bootcdn.net/ajax/libs/element-plus/2.3.3/index.full.js"></script>
<title>hello world</title>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin: 0;
padding: 0;
}
.gameCanvas {
/* background-color: burlywood; */
/* width: 60%; */
height: 650px;
}
.infoDiv {
/* background-color: darkcyan; */
/* width: 40%; */
height: 650px;
}
.el-text {
font-size: middle;
align: left;
}
</style>
</head>
<body>
<div id="app">
<el-container>
<el-header>
<el-text style="text-align: center;" :size="large">
<h1>生命游戏</h1>
</el-text>
</el-header>
<el-row>
<el-col :span="14" class="gameCanvas">
<canvas id="gameCanvas" width="600" height="600" style="border: gray solid 5px"></canvas>
</el-col>
<el-col :span="10" class="infoDiv">
<el-descriptions title="数据面板"
:column="1"
:size="large"
:border="true">
<el-descriptions-item label="存活的细胞数:">
<el-text>
<span id="span1">{{numSurvivors}} / {{numAll}}</span>
</el-text>
</el-descriptions-item><br>
<el-descriptions-item label="迭代轮次:">
<el-text>
<span id="span2">{{rounds}}</span>
</el-text>
</el-descriptions-item>
<el-descriptions-item label="当前状态:">
<span id="span3">
<el-text v-if="state==0">未开始迭代</el-text>
<el-text v-else-if="state==1">迭代进行中</el-text>
<el-text v-else>未知状态</el-text>
</span>
</el-descriptions-item>
<el-descriptions-item label="操作">
<el-button type="primary" onclick="clickInitBtn()">初始化</el-button>
<el-button type="info" onclick="clickClearBtn()">清空面板</el-button>
<el-button type="success" onclick="clickStartBtn()">开始迭代</el-button>
<el-button type="danger" onclick="clickEndBtn()">结束迭代</el-button>
</el-descriptions-item>
</el-descriptions>
</el-col>
</el-row>
</el-container>
</div>
</body>
</html>
<script type="text/javascript">
// 定义格子数以及每个格子的大小
var cellWidth = 30;
var cellHeight = 30;
var cellSize = 20;
// 定义一个二维数组存储每个格子的值
var cells = new Array(cellHeight).fill(0).map(() => new Array(cellWidth).fill(0));
var canvas;
var ctx;
var widht;
var height;
var numSurvivors = 0;
var numAll = cellWidth*cellHeight;
var state = 0;
var rounds = 0;
// 用于启动和停止定时任务
var myInterval;
// 迭代的间隔时间,单位是毫秒
var time = 500;
// 计算存活细胞数
function calNumSurvivors() {
numSurvivors = 0;
for(let i=0; i<height/cellSize; i++) {
for(let j=0; j<width/cellSize; j++) {
if (cells[i][j] == 1) {
numSurvivors++;
}
}
}
}
// 随机初始化细胞
function randomInitLife() {
for(let i=0; i<height/cellSize; i++) {
for(let j=0; j<width/cellSize; j++) {
if (Math.random() < 0.2) {
cells[i][j] = 1;
}
}
}
}
// 清空面板中所有的细胞
function clearAllCells() {
cells = new Array(cellHeight).fill(0).map(() => new Array(cellWidth).fill(0));
}
// 绘制一帧图像
function draw() {
ctx.lineWidth = 2;
ctx.strokeStyle = "gray";
ctx.lineJoin = 'round';
for(let i=0; i<height/cellSize; i++) {
for(let j=0; j<width/cellSize; j++) {
if (cells[i][j] === 1) {
ctx.fillStyle = "LightSalmon";
} else {
ctx.fillStyle = "AliceBlue";
}
ctx.fillRect(i*cellSize, j*cellSize, cellSize, cellSize);
ctx.strokeRect(i*cellSize, j*cellSize, cellSize, cellSize);
}
}
}
// 更新数据面板
function updateInfo() {
var span1 = document.getElementById("span1");
var span2 = document.getElementById("span2");
var span3 = document.getElementById("span3");
span1.innerText = numSurvivors + " / " + numAll;
span2.innerText = rounds;
if (state == 0) {
span3.innerText = "未开始迭代";
} else if (state == 1) {
span3.innerText = "迭代进行中";
} else {
span3.innerText = "未知状态";
}
}
/**
* 每个细胞在每一轮的状态都依赖于其邻居的数量。
* 如果细胞的邻居数量少于一个,那么该细胞在下一次状态将死亡。
* 如果细胞的邻居数量超过四个,那么该细胞在下一次状态将死亡。
* 如果细胞的邻居数量为二或三个,那么该细胞下一次状态将稳定存活。
* 如果某位置原无细胞存活,但该位置的邻居数量为三个,那么该位置将复活一细胞。
*
*/
function calIter() {
var tmp = new Array(cellHeight).fill(0).map(() => new Array(cellWidth).fill(0));
for(let i=0; i<cellWidth; i++) {
for(let j=0; j<cellHeight; j++) {
// 计算周围的细胞数
let num = 0;
if (i-1>=0 && j-1>=0 && cells[i-1][j-1]==1) num++;
if (i-1>=0 && cells[i-1][j]==1) num++;
if (i-1>=0 && cells[i-1][j+1]==1) num++;
if (i+1<cellWidth && cells[i+1][j]==1) num++;
if (i+1<cellWidth && j-1>=0 && cells[i+1][j-1]==1) num++;
if (i+1<cellWidth && j+1<cellHeight && cells[i+1][j+1]==1) num++;
if (j-1>=0 && cells[i][j-1]==1) num++;
if (j+1<cellHeight && cells[i][j+1]==1) num++;
if (cells[i][j] == 0 && num >= 3) {
tmp[i][j] = 1;
} else if (num<=1 || num>4){
tmp[i][j] = 0;
}
}
}
for(let i=0; i<cellWidth; i++) {
for(let j=0; j<cellHeight; j++) {
cells[i][j] = tmp[i][j];
}
}
rounds++;
}
function run() {
calIter();
calNumSurvivors();
draw();
updateInfo();
}
function clickInitBtn(event) {
console.log("点击了 [初始化] 按钮");
if (state == 1) {
alert("还在迭代呢!");
}
clearAllCells();
randomInitLife();
calNumSurvivors();
draw();
updateInfo();
}
function clickClearBtn(event) {
console.log("点击了 [清空面板] 按钮");
rounds = 0;
clearAllCells();
calNumSurvivors();
draw();
updateInfo();
}
function clickStartBtn(event) {
console.log("点击了 [开始迭代] 按钮");
if (state == 0) {
state = 1;
}
myInterval = window.setInterval("run()", time);
updateInfo();
}
function clickEndBtn(event) {
console.log("点击了 [结束迭代] 按钮");
if (state == 1) {
state = 0;
}
clearInterval(myInterval);
}
const app = Vue.createApp({
mounted() {
canvas = document.getElementById("gameCanvas");
ctx = canvas.getContext('2d');
width = canvas.width;
height = canvas.height;
draw();
},
data() {
return {
numSurvivors : 0,
numAll : cellWidth*cellHeight,
state : 0,
rounds : 0,
}
},
methods() {
}
}).use(ElementPlus).mount('#app');
console.log("初始化结束")
</script>
思路非常简单,就是首先定义一个棋盘,然后每次迭代都计算一下结果,再将结果绘制在画布中,其中灰色表示死细胞,橙色表示活细胞。
在写代码的过程中遇到了两个坑:
1. canvas不能使用css定义大小,否则画出来的图会扭曲
2. 这段代码中的按钮的点击事件如果写在 Vue.createApp 中的 methods 中的话会调用不到,有知道为什么的小伙伴可以给我留言
3. 在 Vue.createApp 中的 data 所返回的四个变量都是外部定义的全局变量(用var修饰的),我们在外部更新变量值的时候,页面不会自动渲染,所以我写了一个函数手动进行数据更新,这个原因我也不太懂,有知道的小伙伴可以给我留言,更新数据的代码如下
function updateInfo() {
var span1 = document.getElementById("span1");
var span2 = document.getElementById("span2");
var span3 = document.getElementById("span3");
span1.innerText = numSurvivors + " / " + numAll;
span2.innerText = rounds;
if (state == 0) {
span3.innerText = "未开始迭代";
} else if (state == 1) {
span3.innerText = "迭代进行中";
} else {
span3.innerText = "未知状态";
}
}
不过我觉得,像这种简单页面的数据渲染,也用不到vue,我们自己写几个dom操作就行,不过为了使用element-plus还是需要引入vue,毕竟我不太会布局。
总结
本次项目可以算是对canvas的简单应用吧,我发现其实可以用canvas做很多东西,甚至可以用来制作一些简单的2D游戏,不过如果要做游戏的话,可能需要自己实现一下逻辑,还是挺复杂的。