不用游戏引擎,怎么写游戏?
来跟着作者用 JavaScript 写一个 H5 游戏案例《辩色》
本 Chat 中包含以下内容:
- 需求分析、数据结构设计
- HSL 调色模式
- MVC 设计模式
- 完整的实现过程
适合人群: 对入门游戏开发有兴趣的人员
前言本篇文章具体介绍:如何循序渐进用 200 行左右的代码,实现一个代码高可读性的“辩色”。
目录 准备工作- 运行环境:Web (Chrome 浏览器)
- 开发语言:JavaScript
- 开发工具:VS Code
以上是已经完成的案例的演示图,需求拆解出来还是比较简单的。
- 对象:格子(地图的基本单位)、地图(二位数组)
- 属性:每个格子是一个对象,记录着当前的颜色,已经是否是那个位移的颜色标识
- 行为:点击格子,加分数,减生命值
- 格子:地图的基本单位,有以下属性
{ color: {h:0,s:0,l:0}, //用 HSL 调色模式记录颜色 isOnly: true //记录是否是唯一的颜色}
- 地图:格子的二维数组,
坐标 | x0 | x1-|-|-y0 | { color: {h:0,s:0,l:0} } | { color: {h:0,s:0,l:0} } |y1 | { color: {h:0,s:0,l:0} } | { color: {h:0,s:10,l:0},isOnly:true } |
完整的实现过程打开 VS Code,新建一个 HTML,内容如下。body 里面只有一个 Canvas 的标签,表示全部显示都用 Canvas 的 API 绘制而成,脚本那边先把 MVC 的框架定好,接下来把 mvc 各个层级的数据和方法定好,顺着这个逻辑,理清楚思路,把方法补全。很容易就能实现这个案例了。
当然,这个案例只有 "辩色" 的核心玩法,读者完全可以根据自己的想象力,或者参考别的游戏的灵感,进行改造
//======== 模型 let m = {} //======== 视图 let v = {} //======== 控制 let c = { main () { }, } c.main()
MVC 架构
通过 MVC 架构,可以把整个程序的核心思路理清楚,具体的编码可以在这之后。整个程序按照 MVC 分别进行介绍,最后再进行串联。
M 模型层: 负责存储数据、管理数据逻辑 let m = { //===常量=== //地图宽度 MapWidth: 6, //地图高度 MapHeight: 6, //单位格子大小(像素) UnitSize: 30, //最大生命值 MaxHp: 3, //差异(难度 越小难度越大 1-20 Diff: 5, //颜色 Color: { grey: "#4a4a4a", red: "#cc2222", white: "#ffffff", }, //===变量=== //地图 map: [[]], //游戏状态 0 游戏中 1 失败 status: 0, //生命值 hp: 0, //分数 score: 0, //鼠标移动到哪个坐标 mouseMoveVec: null, //===逻辑=== //初始化模型层 init() { //初始化游戏数据 }, //下一关 next() { //执行下一关的数据生成转换 }, //鼠标移动到哪里 onMouseMove(offsetX, offsetY) { //如果游戏结束的状态,不处理 //点击坐标转为转成地图坐标 //记录当前鼠标选择的坐标 //如果坐标点超出地图,移除当前鼠标选择的坐标 }, //点击了格子 onClick(offsetX, offsetY) { //如果游戏结束的状态,则重新开始游戏 //点击坐标转为转成地图坐标 //选对了 得分 //选错了 //扣血 //游戏结束 }, //新的地图数据 newMapData() { //根据配置生成一个对应的地图数据 }, //新的颜色 newColor(color) { //如果有传入颜色,则生成一个和这个颜色有一定差异的颜色 //随机生成一个 HSL 模式的颜色 },
V 视图层: 将数据进行显示表达
let v = { //初始化 init() { }, //执行绘制 onDraw() { //1.绘制地图 //绘制格子颜色 //如果游戏失败 //高亮显示唯一的颜色 //绘制鼠标选中的格子 //2.绘制游戏中、失败等显示状态 //绘制得分 //绘制生命值 //3.绘制游戏名 },
C 控制层: 负责从控制用户输入,并向模型发送数据,执行主循环
let c = { //入口函数 main() { //各种初始化 }, //初始化输入监听 initInputListener() { //鼠标点击事件 //鼠标移动事件 //鼠标移出事件 }, }
以上为整体流程的思路,按照 MVC 来设计,再设计好数据结构,把对象和行为这些都列好,接下来就非常容易实现了。文末附上所有完整源码。
结语本文章对编程的基础其实要求并不高,仅需要一些 JavaScript 的基础语法。更多的是想传达一个思路,条条大路通罗马,"辩色"的实现可能有无数种方式,这是其中一种。作者认为有一定编程基础,找对方向可以做出很多有趣的东西。如果你对我的内容感兴趣,可以关注我的订阅号“怪诞编程”找到我。一起探索技术变成产品的有趣过程。
如果你的电脑一无所有,且对文章也看得不明白,没关系,右键新建一个文本文档 .txt,改名为 index.html。然后把以下代码复制进去,保存,再双击打开。就是这篇文章的全部内容了。
请收下这份 273 行的"辩色"源码:
- 扣掉注释 58 行
- 实际的代码行数 215 行
谢谢。
index.html
//======== 模型 let m = { //===常量=== //地图宽度 MapWidth: 6, //地图高度 MapHeight: 6, //单位格子大小(像素) UnitSize: 30, //最大生命值 MaxHp: 3, //差异(难度 越小难度越大 1-20 Diff: 5, //颜色 Color: { grey: "#4a4a4a", red: "#cc2222", white: "#ffffff", }, //===变量=== //地图 map: [[]], //游戏状态 0 游戏中 1 失败 status: 0, //生命值 hp: 0, //分数 score: 0, //鼠标移动到哪个坐标 mouseMoveVec: null, //===逻辑=== //初始化模型层 init() { //初始化游戏数据 m.status = 0 m.hp = m.MaxHp m.score = 0 this.next() }, //下一关 next() { //执行下一关的数据生成转换 let list = this.newMapData() let map = [] for (let y = 0; y < m.MapHeight; y++) { map[y] = [] for (let x = 0; x < m.MapWidth; x++) { map[y][x] = list.pop() } } m.map = map }, //鼠标移动到哪里 onMouseMove(offsetX, offsetY) { //如果游戏结束的状态,不处理 if (m.status == 1) { return } //点击坐标转为转成地图坐标 let x = Math.floor(offsetX / m.UnitSize) let y = Math.floor(offsetY / m.UnitSize) if (m.map[y] && m.map[y][x]) { //记录当前鼠标选择的坐标 m.mouseMoveVec = { x: x, y: y } } else { //如果坐标点超出地图,移除当前鼠标选择的坐标 m.mouseMoveVec = null } }, //点击了格子 onClick(offsetX, offsetY) { //如果游戏结束的状态,则重新开始游戏 if (m.status == 1) { m.init() return } //点击坐标转为转成地图坐标 let x = Math.floor(offsetX / m.UnitSize) let y = Math.floor(offsetY / m.UnitSize) let bean = m.map[y][x] if (bean && bean.isOnly) { //选对了 得分 m.score++ m.next() } else { //选错了 if (m.hp > 1) { //扣血 m.hp -= 1 } else { //游戏结束 m.status = 1 m.mouseMoveVec = null } } }, //新的地图数据 newMapData() { //根据配置生成一个对应的地图数据 var color = this.newColor() var colorBadGuy = this.newColor(color) var list = new Array(m.MapWidth * m.MapHeight) var randomIndex = Math.floor(Math.random() * list.length) for (var i = 0; i < list.length; i++) { if (i == randomIndex) { list[i] = { color: colorBadGuy, isOnly: true } } else { list[i] = { color: color } } } return list }, //新的颜色 newColor(color) { if (color) { //如果有传入颜色,则生成一个和这个颜色有一定差异的颜色 var randmoIndex = Math.floor(Math.random() * 3) var h = color.h var s = color.s var l = color.l if (s < m.Diff) { s += m.Diff } else { s -= m.Diff } if (l < m.Diff) { l += m.Diff } else { l -= m.Diff } return { h: h, s: s, l: l } } else { //随机生成一个 HSL 模式的颜色 var h = Math.floor(Math.random() * 360); var s = Math.floor(Math.random() * 100); var l = Math.floor(Math.random() * 100); return { h: h, s: s, l: l } } }, } //======== 视图 let v = { //初始化 init() { let canvas = document.getElementById("canvas") v.canvas = canvas //设置 canvas 的宽高 canvas.width = m.MapWidth * m.UnitSize canvas.height = m.MapHeight * m.UnitSize + m.UnitSize * 1.5 //宽高变化重新调整 canvas 在页面中的位置,使居中 canvas.style.marginLeft = -canvas.width / 2 canvas.style.marginTop = -canvas.height / 2 //拿到 canvas 绘制的上下文,方便之后直接进行绘制 this.context = canvas.getContext("2d") }, //执行绘制 onDraw() { let context = this.context let canvas = v.canvas context.clearRect(0, 0, canvas.width, canvas.height) context.lineWidth = 1 //1.绘制地图 context.fillStyle = m.Color.grey for (let y = 0; y < m.map.length; y++) { for (let x = 0; x < m.map[y].length; x++) { //绘制格子颜色 let h = m.map[y][x].color.h let s = m.map[y][x].color.s let l = m.map[y][x].color.l context.fillStyle = `HSL(` + h + `,` + s + `%,` + l + `%)` context.beginPath() context.rect( x * m.UnitSize, y * m.UnitSize, m.UnitSize, m.UnitSize) context.fill() //如果游戏失败 if (m.map[y][x].isOnly && m.status == 1) { //高亮显示唯一的颜色 context.fillStyle = m.Color.red context.beginPath() context.arc( (x + 0.5) * m.UnitSize, (y + 0.5) * m.UnitSize, 2, 0, 2 * Math.PI) context.fill() } } } //绘制鼠标选中的格子 if (m.mouseMoveVec) { context.strokeStyle = m.Color.white context.beginPath() context.rect( m.mouseMoveVec.x * m.UnitSize, m.mouseMoveVec.y * m.UnitSize, m.UnitSize, m.UnitSize) context.stroke() } //2.绘制游戏中、失败等显示状态 context.fillStyle = m.Color.grey context.font = 'bold ' + m.UnitSize / 2 + 'px arial' context.textBaseline = "bottom" if (m.status == 1) { context.textAlign = "center" context.fillText("结束 点击重来", canvas.width / 2, canvas.height - 5) } else { context.textAlign = "right" //绘制得分 context.fillText(m.score + "分", canvas.width, canvas.height - 5) for (let x = 0; x < m.hp; x++) { //绘制生命值 context.fillStyle = m.Color.red context.beginPath() let unit = m.UnitSize / 5 let centerX = (x + 0.5) * m.UnitSize let centerY = canvas.height - m.UnitSize / 2 - unit / 2 context.moveTo(centerX, centerY); context.lineTo(centerX + unit, centerY - unit); context.lineTo(centerX + unit * 2.5, centerY); context.lineTo(centerX, centerY + unit * 2.5); context.lineTo(centerX - unit * 2.5, centerY); context.lineTo(centerX - unit, centerY - unit); context.closePath(); context.fill() } } //3.绘制游戏名 context.font = 'bold ' + m.UnitSize * 0.45 + 'px arial' context.fillStyle = m.Color.grey context.textAlign = "left" context.fillText("辨色", canvas.width * 0.01, canvas.height - m.UnitSize) }, } //======== 控制 let c = { //入口函数 main() { c.initInputListener() v.init() m.init() v.onDraw() }, //初始化输入监听 initInputListener() { //鼠标点击事件 document.getElementById("canvas").addEventListener('click', function (event) { m.onClick(event.offsetX, event.offsetY) v.onDraw() }, false); //鼠标移动事件 document.getElementById("canvas").addEventListener('mousemove', function (event) { m.onMouseMove(event.offsetX, event.offsetY) v.onDraw() }, false); //鼠标移出事件 document.getElementById("canvas").addEventListener('mouseout', function (event) { m.onMouseMove(event.offsetX, event.offsetY) v.onDraw() }, false); }, } //=== 启动 c.main()
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。
阅读全文: http://gitbook.cn/gitchat/activity/5e9f1ac4df43a81990578dda
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。