基于html+js编写的生命游戏

前言

本文将介绍一个基于html+js的生命游戏,该项目只有一个html代码,无任何其他以来,UI方面采用了vue+element-plus进行渲染,游戏的界面基于canvas进行渲染,先来看一下成果。

我不知道游戏规则有没有写错,感觉经常会陷入循环中。

游戏规则

这边给出文心一言给出的游戏规则

根据以上规则写的代码如下

javascript 复制代码
    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。

html 复制代码
<!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修饰的),我们在外部更新变量值的时候,页面不会自动渲染,所以我写了一个函数手动进行数据更新,这个原因我也不太懂,有知道的小伙伴可以给我留言,更新数据的代码如下

javascript 复制代码
    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游戏,不过如果要做游戏的话,可能需要自己实现一下逻辑,还是挺复杂的。

相关推荐
customer0822 分钟前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
清灵xmf22 分钟前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据29 分钟前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_3901617738 分钟前
防抖函数--应用场景及示例
前端·javascript
334554321 小时前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test1 小时前
js下载excel示例demo
前端·javascript·excel
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript