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

相关推荐
Sheldon一蓑烟雨任平生7 小时前
Vue3 插件(可选独立模块复用)
vue.js·vue3·插件·vue3 插件·可选独立模块·插件使用方式·插件中的依赖注入
鱼与宇9 小时前
苍穹外卖-VUE
前端·javascript·vue.js
用户47949283569159 小时前
Safari 中文输入法的诡异 Bug:为什么输入 @ 会变成 @@? ## 开头 做 @ 提及功能的时候,测试同学用 Safari 测出了个奇怪的问题
前端·javascript·浏览器
裴嘉靖9 小时前
Vue 生成 PDF 完整教程
前端·vue.js·pdf
毕设小屋vx ylw2824269 小时前
Java开发、Java Web应用、前端技术及Vue项目
java·前端·vue.js
冴羽10 小时前
今日苹果 App Store 前端源码泄露,赶紧 fork 一份看看
前端·javascript·typescript
蒜香拿铁10 小时前
Angular【router路由】
前端·javascript·angular.js
时间的情敌10 小时前
Vite 大型项目优化方案
vue.js
西洼工作室11 小时前
高效管理搜索历史:Vue持久化实践
前端·javascript·vue.js
樱花开了几轉11 小时前
element ui下拉框踩坑
开发语言·javascript·ui