当前位置: 首页 > news >正文

【uniapp】元胞自动机GameOfLife生命游戏项目开发流程详解

听说有一个生命游戏,会不会演化出一些数字生命呢,想做出来玩玩,看能不能发现什么规律,感觉实现起来简单,对此感兴趣的新手可以拿来练习。

这个生命游戏的出现,来自英国数学家约翰·何顿·康威在1970年发明的细胞自动机,也叫元胞自动机,英文名Game of Life

这里使用HBuilderX工具开发一个项目-生命游戏,用uniapp创建,它是可以编译多平台上运行,下面详细讲一下开发过程。

萌新小白学习需前要具备的以下知识点:

  • 熟悉前端开发,有Web网页设计基础,会使用HTML,CSS,JavaScript;
  • 在Web网页中使用vue.js;
  • 使用HBuilderX开发工具;

如果以上都具备了,请继续往下看

首先,从电脑上打开已安装的HBuilderX开发工具,选择菜单栏上的文件新建项目

创建项目

在新建的项目窗口中,如下图

图片描述

选择对应的选项uni-app默认模板,越简单越好,

图中填入的uniapp-simple-app是项目文件夹名称,这里改成uni-app-game-of-life,

若勾选了 uni-app x,那就要把原来的javascript开发语言改成用uts开发语言写,若你还不会用uts,就不要勾选它哦

最后点击创建按钮,开发工具下就会出现一个创建好的项目文件夹uni-app-game-of-life

页面文件

项目中所有文件名带后缀.vue的都是页面文件,当打开查看后,

学过vue.js的同学都知道,文件内容里面划分为三段,用标签对<>表示,分别是:

  • template 页面模板,类似HTML页面
  • script 页面脚本,这里写页面的脚本JavaScript代码
  • style 页面样式,写样式表,用于控制页面显示特效

开始页面

开始页面已自动新建好了,就在项目文件夹里,从根目录下找,

文件位置在pages/index/index.vue,打开查看,

此文件里修改,试试添加一个开始游戏按钮,

开始按钮

在文件内容的一段布局模板template标签里,往里面添加一个按钮控件,添加的内容如下

<template><view class="content"><!-- 此处省略 --><view class="footer"><!-- 此处省略 --><button class="btn" @click="enter" type="primary">开始</button><!-- 此处省略 --></view></view>
</template>

在文件的脚本script标签里添加代码,实现点击按钮去打开游戏页面,代码如下

//...
export default {data() {return {//...}},//...methods: {//.../*** 点击开始按钮*/enter(){//进入游戏页面uni.navigateTo({url: '/pages/game/game',success: res => {},fail: () => {},complete: () => {}})}}
}

还有最后的样式style标签里,内容如下

<style lang="scss">
.footer {//...
}
</style>

那是改控件的显示效果,和HTML网页设计的样式一样的,不是重点

开始页面改好了,可以点击编译运行看看,如下图
图片描述

编译运行选运行到内置浏览器看效果默认是H5的页面,操作没有外置的浏览器查看方便

编译运行通常是选择运行到Chrome浏览器上看效果,打开开发者工具(按组合键Ctrl+Shift+I,再按Ctrl+Shift+M),模拟移动端,如下图
图片描述

看着反应快,可边改边保存看运行效果,
看页面上还有控件输入框,主题按钮,这些是后面完善的细节,实现了修改游戏数据和主题颜色,但那些不是重点,这里就不展开讲

当点击开始游戏,它就会在开发工具的控制台Console下显示报错,不用看就知道,还有游戏页面没有做出来呢

游戏页面

游戏页面需要自己添加,有两种方式:

  1. 新建页面向导
  2. 修改配置文件

添加页面

  1. 这里采用新建页面向导,对新手来说,最简单又好理解,

选中项目里的文件夹pages,点鼠标右键,选择新建页面,如下图

图片描述

新建页面弹出一个窗口,如下图

图片描述

这样填:

  • 输入页面名称game,这是页面文件名,
  • 默认勾选了创建同名目录, 那就是先新建文件夹game, 再往里面新建页面文件game.vue
  • 还有可修改页面标题,为"navigationBarTitleText": "生命游戏"

点击确定后,会留意到项目中会多了个页面文件,是游戏页面,

文件位置/pages/game/game.vue,打开查看,

会发现基本的结构,已经自动填写好了,

  1. 若要采用修改配置文件来建立游戏页面,

就去看看项目文件夹下的文件pages.json,内容如下

{"pages": [ //pages数组中第一项表示应用启动页{"path": "pages/index/index","style": {"navigationBarTitleText": "生命游戏"}},{"path" : "pages/game/game","style" : {"navigationBarTitleText" : "生命游戏"}}],"globalStyle": {//..."navigationBarTitleText": "生命游戏",//...},//..
}

会发现上面配置项有好多,这里省略了,不要关心那些,那都是开发工具自动填写的,可以自己填写,这对新手来说不友好,

改配置如有不懂的,可参考文档全局配置-页面路由

在游戏页面文件里面,添加游戏页面的布局,内容如下

<template><view class="page"><canvas class="canvas" id="CSDN_zs1028" canvas-id="CSDN_zs1028" @touchstart="ontouchstart" @touchmove="ontouchmove" @touchend="ontouchend" @touchcancel="ontouchend"></canvas><view class="footer"><!-- 此处省略 --></view>
</template>

主要看canvas控件,是一个画布,用来绘制游戏画面

然后,就是添加游戏页面的脚本,代码如下

//使用app对象,将来调用全局方法和修改数据
const app = getApp()export default {data() {return {//...speed: 500, //初始速度maxSpeed: 1000, //最大速度cols: 32, //网格的初始列数isPlay: false, //是否动画isShowFPS: true, //显示刷新帧率,评估机器运行性能isDrawing: false, isCloseLoop: true, //是否在画布范围内移动,否则移动会超出范围//游戏数据data: [//...],};},/*** 页面加载*/onLoad(){//...},/*** 页面卸载*/onUnload(){			//...},/*** 准备就绪*/onReady(){//...},//...methods:{//...}

从上面看出,执行页面加载的顺序是先调用onLoad, 再调用onReady方法,

页面加载

当进入页面时,需要先从页面加载的方法onLoad里做些初始化数据,代码如下

let data = app.getLocalData()
//将获取到的本地数据设置到页面
Object.assign(this,{//...cols : data.cols,speed : data.speed,data : data.data,
})

页面卸载

当退出页面时,会调用页面卸载的方法onUnload

就在这里做个处理,保存一下数据,代码如下

let data = app.getLocalData()
data.data = this.data
data.cols = this.cols
data.speed = this.speed
//将页面的数据保存到本地
app.setLocalData(data)

准备就绪

待页面加载完成,就会调用一个方法onReady

在这里就可处理操作页面上的控件,做初始化处理,代码如下

//创建一个查询工具,通过id查找页面的一个控件canvas实例
uni.createSelectorQuery().select('#CSDN_zs1028').fields({size:true,context:true,
}).exec(res=>{//得到一个画布控件的宽和高const { width, height } = res[0]//获取控件canvas的上下文contextconst ctx = uni.createCanvasContext('CSDN_zs1028')//存起来,也就是设置到之前定义的data,多了一个属性canvas数据Object.assign(this,{canvas:{width,height,ctx,//...}})//加载数据this.reload()//画坐标轴和网格this.drawCoordinateAxisAndGrid(()=>{//画对象this.drawObjects(()=>{//开始this.start()})})
})

从上面可以看出,有四个自定义方法需要一步一步来实现,

自定义方法通常是写在methods里面,代码如下

{/*** 重新加载*/reload(){//...},/*** 绘制网格*/drawCoordinateAxisAndGrid(callback){//...},/*** 绘制所有对象*/drawObjects(callback){//...},/*** 实现动画循环的方法*/requestAnimationFrame(callback){//...},/*** 开始游戏*/start(){//...},/*** 处理更新下一步的数据*/nextStep() {//...},/*** 触摸画布开始*/ontouchstart(e){//...},/*** 触摸画布点移动*/ontouchmove(e){//...},/*** 触摸画布结束*/ontouchend(){//...},//...
}

游戏逻辑

一些游戏逻辑都是写在methods里面,供后面需要的时候调用

重新加载

初始化游戏数据,就调用重新加载方法reload,代码如下

const { cols, data, canvas } = this//校验配置,网格列数必须是偶数
if (cols%2!=0) throw new Error(`grid cols value not is odd number ${cols}`)
//其它判断省略...//从配置拿出padding的边距值...
const { width, height, offsetTop:paddingT } = canvas
//计算出每一个单元格的大小
let gw = (width-padding*2)/cols
//计算出网格的行数
let rows = parseInt((height-padding*2-paddingT)/gw)
//定义一个网格的单元格数量,等于列数乘以行
let grids = new Array(cols*rows)
for(let i=0; i<grids.length; i++){//给每个单元格重新赋初值,x和y分别为第几列和几行,方便定位grids[i]={x:i%cols,y:parseInt(i/cols)}
}
//将计算出的每个属性存起来
Object.assign(this, {gw,rows,grids})

绘制网格

接下来,就会先绘制一个网格出来,看看之前初始化的网格数据有没有问题,

绘制网格的方法drawCoordinateAxisAndGrid(callback),代码如下

const { canvas, grids, gw } = this
const { ctx, width, height, offsetTop:paddingT } = canvas
const { paddingLR:padding } = Config
//绘制一个边框
ctx.fillStyle = '#ffffff'
ctx.fillRect(0,0,width,height)//绘制坐标轴x和y
//省略了...
let p = 2;
//...
ctx.strokeText('0',p,paddingT)
//...
ctx.strokeText('x',width-p,paddingT)
//...
ctx.strokeText('y',p,height-p)//绘制网格
ctx.beginPath()
grids.forEach((g,i)=>{let x = padding + g.x*gwlet y = paddingT + padding + g.y*gwctx.rect(x,y,gw,gw)
})
ctx.stroke()
ctx.draw(true,callback)

这uniapp项目的canvas绘制方法是draw(),它是异步绘制的,等绘制完就需要调用传入的第二个参数callback,才能继续下一个绘制,保证绘制顺序不会出错

写到这里,可以编译运行看看绘制的网格,效果如下图

图片描述

这是参考的一个数学坐标系,从左上角开始,水平方向为x轴,垂直方向为y轴

绘制对象

绘制的对象就是指网格里面的元胞,对象数据用之前的data()方法返回的数据属性data表示,

游戏数据,用默认属性data的值,可以是这样的,代码如下

data: [[5,2],[6,3],[5,4],[7,3],[6,4],
],

看不懂是什么数据,没关系,接下来将数据可视化

调用绘制的方法drawObjects(callback)就能把上面数据对象画出来,实现代码如下

const { themeStyleIndex, canvas, data, gw } = this
const { ctx, width, height, offsetTop:paddingT } = canvas
const { paddingLR:padding, themeStyles } = Config
//按主题样式指定颜色,绘制一个背景
ctx.fillStyle = themeStyles[themeStyleIndex][0]
ctx.fillRect(0,paddingT,width,height-paddingT)
//下面继续,将对象数据通过单元格绘制出来
ctx.beginPath()
ctx.fillStyle = themeStyles[themeStyleIndex][1]
data.forEach((d,i)=>{let x = padding + d[0]*gwlet y = padding + d[1]*gw + paddingTctx.fillRect(x,y,gw,gw)
})
ctx.draw(true,callback)

写到这里,可以编译运行看看效果,绘制出是所有数据对象,效果如下图
图片描述

对比上面属性data的值,可以看出规律来,
例如5,2,对应网格中一个在5列2行的一个点(单元格),类似点亮LED灯珠的效果

绘制出来的所有对象,发现一个问题:绘制的网格不见了

那之前绘制的网格是被覆盖了,若不需要绘制网格,可以注释掉绘制网格的方法

开始游戏

开始游戏,让绘制出来的所有数据对象(活)动起来,

需要调用方法·start·,代码如下

//...//实现下一帧方法,更新画面
const nextFPS = ()=>{//...
}
//执行更新动画
nextFPS()

方法里面还有一个自定义方法nextFPS,作为递归调用,

实现循环动画,让所有数据对象不要停下来,代码如下

const { canvas, isPlay, isShowFPS, themeStyleIndex, speed, maxSpeed } = this
const { paddingLR:padding, themeStyles } = Config
const { ctx, offsetTop:paddingT } = canvas
//若没有暂停,就继续
if (isPlay) {let timeSpan = Date.now() - timer//此处省略...//判断时间,如果到了,就处理更新下一步的数据,让对象活过来if (timeSpan > maxSpeed - speed) {this.nextStep()timer2 = Date.now()}//绘制所有对象this.drawObjects()//...//绘制完成后,就到下次更新ctx.draw(true,()=>this.requestAnimationFrame(nextFPS))return
}
//下次更新
this.requestAnimationFrame(nextFPS)

从上面看出,里面还调用了方法nextFPS,实现循环动画

其中调用了一个方法requestAnimationFrame,这个方法是自定义的,

有个自带的方法,如下

uni.requestAnimationFrame(callback)

为什么不用它,因为它能在以前的旧版本上跨多端平台上使用,现在继续用它还是存在问题的,

uniapp官方未及时解决这个问题,就只有开发者自己解决,作者这里实现中用到了条件编译,这里不展开讲,具体看源码,可以保证能编译在H5,和小程序上顺利执行动画

其中还调用了一个方法nextStep,这个方法也是自定义的,

这是游戏里主要的游戏规则逻辑,实现稍微复杂点,新手可能看不懂,作者会多加注释,若能看懂那就学到了吧

实现处理更新下一步的对象数据,代码如下

const { data, rows, isCloseLoop, cols } = this
//定义个记录死亡的
const delIds = []
//定义个记录存活的
const data2 = []
//定义个不同方向的偏移数据
const dires = [//...
]
//遍历处理对象的每一个单元格数据
data.forEach((d,i)=>{let x = d[0] //所在列let y = d[1] //所在行//计数let count = 0//通过不同的方向去计算,记录一个单元格周围存活的countdires.forEach(e=>{//此处省略... 请自己多想想,怎么实现})//孤单,或者拥挤,标记为死亡if (count<2 || count>3) delIds.unshift(i)//超出范围,标记为死亡else if (x<0 || x>=cols || y<0 || y>=rows) delIds.unshift(i)
})
//删除数据对象中死亡的
delIds.forEach(i=>data.splice(i,1))
data2.forEach(d=>{//满足三个,则为存活,加入数据对象if (d.count==3) data.push([d.x,d.y])
})

游戏交互

以为这就结束了?还有最后的步骤,

游戏是开始了,可怎么操作没有反应,需要实现游戏的交互逻辑,

当手指触摸画布canvas时,会调用三个自定义方法,

触摸画布开始时,调用的方法是ontouchstart(e),代码如下

this.isDrawing=false

就这?没错,这是为了判断触摸后有没有移动的情况,看不懂没关系,慢慢研究就懂了

触摸画布点移动时,调用的方法是ontouchmove(e),代码如下

//当触摸移动,表示在绘制,应该改成暂停游戏
this.isPlay=false
//表示在触摸移动
this.isDrawing=trueconst { touches } = e
const { gw, canvas, grids, data } = this
//计算出触摸点在画布的位置
let left = touches[0].pageX - canvas.offsetLeft
let top = touches[0].pageY - canvas.offsetTop
//...
//计算位置所在列和行
let x = parseInt(left/gw)
let y = parseInt(top/gw)
//找出位置对应的一个单元格
let g = grids.find(g=>g.x==x && g.y==y)
if(g){let d = data.find(d=>d[0]==x && d[1]==y)//如果这个不是对象的一部分,就加入if (!d) {data.push([x,y])//重新绘制对象this.drawObjects(()=>{})}
}

触摸结束时,调用的方法是ontouchend(),代码如下

const { isDrawing, isPlay } = this
if(!isDrawing) this.isPlay=!isPlay

看上面就知道,这个处理和触摸开始的方法是对应的,重置设置

运行测试

做到这里,基本上就算完成了,可以点击运行测试看看效果,

项目运行测试效果如下图
图片描述

录制的效果看着有掉帧的情况,与实际效果是不一样的,估计是作者电脑当时多开后台程序,内存要爆,就有点卡,
想看是否效果流畅的的可以亲自下来体验

小提示:
生命游戏这个项目,编译出来的App软件在不同平台上硬件性能表现不同,发现有的卡,有的流畅,针对这个问题,想做游戏可以考虑在哪个平台上开发,保证用户体验上不会很差。

写到了这里,初步明确了项目开发的方向,可以尝试按照文章步骤做出来,具体细节可参考项目源码,

此项目源码已上传到CSDN资源,在此文章头有展示,更多源码资源,请点击此处查看

以上为全部内容,感谢阅读,再见!

图片描述

http://www.dtcms.com/a/278116.html

相关文章:

  • Java SE--图书管理系统模拟实现
  • 模型占用显存大小评估
  • 【AI大模型】ComfyUI:Stable Diffusion可视化工作流
  • java基础编程(入门)
  • C++多线程知识点梳理
  • 深入理解 Java Map 与 Set
  • 每天学一个八股(二)——详解HashMap
  • 封装---优化try..catch错误处理方式
  • 【echarts踩坑记录】为什么第二个Y轴最大值不整洁
  • Acrobat 表单中的下拉菜单(附示例下载)
  • 使用docker的常用命令
  • RS4585自动收发电路原理图讲解
  • 从 Manifest V2 升级到 Manifest V3 的注意事项
  • Extended Nested Arrays for Consecutive Virtual Aperture Enhancement
  • 财务管理体系——解读大型企业集团财务管理体系解决方案【附全文阅读】
  • Python异步编程
  • 57.第二阶段x64游戏实战-实时监控抓取lua内容
  • 利用低汇率国家苹果订阅,120 元开通 ChatGPT Plus
  • 14.使用GoogleNet/Inception网络进行Fashion-Mnist分类
  • docker基础部署
  • ID生成策略
  • 在新版本的微信开发者工具中使用npm包
  • 用信号量实现进程互斥,进程同步,进程前驱关系(操作系统os)
  • DOS下EXE文件的分析 <1>
  • MacBook Air通过VMware Fusion Pro安装Win11
  • 从代码学习深度强化学习 - DDPG PyTorch版
  • [Python 基础课程]列表
  • 【DataLoader的使用】
  • 力扣 hot100 Day43
  • Actor-Critic重要性采样原理