# BounceCell **Repository Path**: ExFTLPT/bounce-cell ## Basic Information - **Project Name**: BounceCell - **Description**: 弹力细胞,一个由JavaScript写的网页小游戏 - **Primary Language**: JavaScript - **License**: BSD-3-Clause - **Default Branch**: master - **Homepage**: http://serendipitous-clue.com/BounceCell/ - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-05-03 - **Last Updated**: 2022-05-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 弹力细胞 (BounceCell) > 一个由JavaScript写的网页小游戏 作为大一菜鸟,这是我第一次比较正式的写文章 [害臊] ## 游戏玩法 通过鼠标或触屏控制屏幕底部的滑动弹板将发射的小球反弹出去撞击方块,清空所有可撞击方块后游戏获胜,若场上无小球、小球已打空且场上还剩可撞击方块则游戏失败,若是无尽模式,所有小球消失或方块低于红线则游戏失败。 ![游戏截图](./game1.png) ![游戏截图](./game2.png) ![游戏截图](./game3.png) ![游戏截图](./game4.png) ## 运行思路 分别创建小球( *Bullet* )、方块( *Block* )、背景( *Block* )、反弹板等实体( *Entity* ),通过对其赋予相应规则、进行相应逻辑判断即可实现游戏运行。 ## 游戏规则 1. 小球数量有限。 2. 普通小球碰到分裂触发方块后会在当前位置新增一定数量的小球。 3. 方块被撞击一定次数后会被破坏。 4. 拖动或鼠标滑动可移动反弹板。 5. 反弹板反弹规则为越靠近反弹板顶部的中部,小球反弹越接近正上方,反之朝向两侧。 6. 爆发效果中反弹板可以在被拖动的状态持续快速发球。 ## 游戏组成 ### Entity类 由于所有实体都有很多共有属性,所以我们创建了一个父类 *Entity* 来简化代码。 ```javascript // 实体基础类 const Entity = function (x, y, w, h) { this.type = "Entity"; // 实体类标识 this.x = x; // 中心x坐标 this.y = y; // 中心y坐标 this.w = w; // 宽 this.h = h === undefined ? w : h; // 高 this.w2 = this.w / 2; this.h2 = this.h / 2; this.noBounce = false; // 不触发反弹事件 this.color = "#000"; // 颜色 this.color2 = "#fff"; // 渐变背色 this.opacity = 1.0; // 透明度 this.txt = ""; // 显示文本 this.fontSize = vm(4); // 显示文本的文字大小 this.txtColor = "#fff"; // 显示文本的文字颜色 this.score = 0; // 价值分数 // 获取矩形信息(获取中心点以及宽高) this.rect = () => rectD(this.x, this.y, this.w, this.h); // 获取矩形对角信息(获取左上角坐标以及右下角坐标) this.vector = () => rectF(this.x, this.y, this.w, this.h); // 移动实体坐标 this.move = (x, y) => { if (x !== undefined) this.x = x; if (y !== undefined) this.y = y; } // 显示条件 this.visible = () => true; } ``` > PS. 在后续数值计算中大量用到了实体宽高的一半作为参数,所以设置了 *w2 h2* 来简化代码,优化运行。 ### Block类 然后我们就创建作为游戏主体的方块类,方块类既可以用来做背景装饰,也可以用来当做游戏内容,实现这一点我们只需要设置其不触发反弹事件即可,其他实体也可以同理实现。 ```javascript // 方块类 const Block = function (x, y, w, h, hth, nb, color) { Entity.call(this, ...arguments); // 继承实体类 this.type = "Block"; this.color = color; // 方块颜色 this.health = hth; // 方块可受击次数 this.newBullet = nb; // 被动子弹分裂数 this.effectBoom = false; // 爆发效果 // 受击事件 this.hit = (index, bullet) => { ... } } ``` > PS. 方块类我们不做主动更新状态,采用被动更新(既是被撞击后才更新状态),因为在运行逻辑中,方块更多时间处于挂起状态。 #### 方块的分类 1. 装饰方块 `newBadblock()` - 表现见上图中背景,以及白色条纹,装饰方块有各自的绘图接口,同时位于第一绘图层。 ````javascript // 装饰方块绘图接口 block.update = (block) => { // 可通过对draw进行操作绘图 ... } ```` 2. 永驻方块 - 是无法被摧毁,但是有反弹事件的方块,表现如反弹板 `Launcher` 、边界。 3. 可摧毁方块 - 是游戏中作为摧毁目标的方块,表现如上图中的灰色方块。 4. 分裂触发方块 - 是游戏中被碰撞后出发小球分裂的方块,表现如上图中的绿色方块。 5. 爆发效果方块 - 是游戏中被碰撞后给予反弹板被拖动能快速大量发球的效果,表现如红色方块。 ### Bullet类 作为玩家主要攻击工具,其必须具有可动性,反弹能力,以及在飞出边界后被回收的特性,其实现是本游戏的 **难点**。 ```javascript // 子弹类 const Bullet = function (x, y, r, deg, speed) { Entity.call(this, x, y, r, r); // 继承实体类 this.type = "Bullet"; this.r = r; this.color = cfg.c.bt; // 子弹颜色 this.deg = deg; // 方向角度 this.speed = speed; // 初始速度 this.addSpeed = 1; // 速度倍率 this.st = dt(); // 位移发生时间 this.lx = x; // 上一次中心X this.ly = y; // 上一次中心Y this.ia = false; // 为分裂子弹标识 // 反弹实现(计算反弹角度) var bounceImpl = (block, isC, isR) => { ... } // 不同反弹对象的不同处理 const event = [ ... ], // 反弹实现(检测碰撞) call = () => { ... } // 反弹事件 this.bounce = () => { ... } // 主动状态更新 this.update = () => { ... } } ``` > PS. 反弹事件 `bounce()` 在这儿并没有直接进行反弹处理,首先遍历所有可触发碰撞实体,判断当前小球是否与之相碰,然后再进行反弹处理,反弹处理的实现在 `bounceImpl()` 中。 一个完整的游戏单次迭代 `animloop()` 包含游戏逻辑一次加上绘图一次,游戏逻辑运行的作用是为绘图提供参数于内容,在每次游戏逻辑运行中,会遍历所有小球实体,并调用 `update()` 主动触发小球状态更新达到给予小球可动性与反弹能力,同时判断小球是否飞出边界做回收处理。 ### 小球实现的难点 难点在于如何检测小球与其他实体是否相撞 `call()`,且计算小球反弹角度 `bounceImpl()`,*在实现这个目的的时候,我总共实现了两种方案*: * 方案一:检测两个实体是否相邻或者相交,使用了 `Frontier.rectWith()` 具体过程为:遍历所有实体,分别于当前小球做检测,若发生相邻或者相交,则进入计算反弹角度的逻辑,反之进入下一次迭代。它只能提供两个实体是否发生了碰撞。 . 这种方案存在特别严重的漏洞,若小球移速过快,完成一次游戏单次迭代后,容易越过中途的方块,所以弃用这个方案。 * 方案二:**投影检测**,获取小球移动方向上第一个相交或相邻的方块,达到检测目的。它能提供两个实体是否发生了碰撞和碰撞的边是哪个。同时规避了移速过快时越过方块的漏洞。 实现反弹调度的前提是获取小球碰撞的边是哪个,从而用数学的方式实现对称反弹,公式如 `deg * -1 + 180`。 > 由于是小球去检测是否与其他实体发生碰撞,若其他实体在这个过程中移动将容易判断失误,现在还有小球莫名其妙检测碰撞失败的情况,(希望大佬能提出宝贵意见) ### 概率实现 关于概率,我想如何较为均衡的实现概率发生的判断,单纯的对计算机生成的随机数进行大小比较产生的结果的概率是很不可靠的,我们采用相对均衡的取模后对比。 ### 暂停游戏实现 暂停游戏就是暂时停止游戏的进度,我们只需要跳过游戏逻辑运行即可,但同时我们还要考虑游戏内时间的变化,我将当前时间戳作为游戏的一个进展参数,这不利于我们实现暂停,所以将其改成每次完成完整的游戏单次迭代都会给游戏时间增加时间戳使用其均衡。 > 当暂停游戏后,跳过了游戏逻辑运行,时间刻度自然就不会变化,这能保证进展的正常。 ### 关于Buff的实现 我们对相应目标添加对应开关与逻辑,在原有的游戏时间上增加时长判断即可。 ### 游戏分数的统计 对每个实体我们添加了各自的价值分数,在完成指定事件后会根据价值分数对游戏分数进行影响,然后再通知玩家。 ## 运行流程 先按照默认参数创建游戏,然后创建面板,在玩家自定义参数点击开始游戏后,再次按照当前参数创建游戏,丢弃之前创建的所有游戏状态。 > 之前创建游戏并不是浪费资源,而是可以将创建出来的界面作为面板背景。 创建游戏:清空所有实体,创建所需实体。 ```javascript Game.bullets = []; // 清空所有小球 Game.blocks = []; // 清空所有方块 newRoadblock(); // 创建新的方块 newLauncher(); // 创建新反弹板 changePaused(false); // 强制更改暂停状态 toggleVisibility(document.querySelector("#op")); // 切换面板显示 toggleVisibility(document.querySelector("#m")); // 切换面板显示 ``` 完整的游戏单次迭代:先更新反弹板位置,然后迭代所有小球,若小球发生碰撞,则触发碰撞事件,反之进入下一次迭代,完成迭代后对所有实体进行绘制,然后判断当前游戏是否结束。 > 绘制的顺序:杂项 > 方块 > 反弹板 > 小球 > 无论输赢如何都直接弹出最开始的面板,问就是 **懒**。 ## 游戏逻辑 游戏代码大致可分为三部分,第一部分是一些全局参数和公共方法的声明,第二部分是游戏核心逻辑实现,第三部分是游戏流程的控制。 > 在内部有以下全局接口供使用: > 1. [ *cfg* ] 参数配置,存放游戏所有可自定义参数。 > 2. [ *MouseEvent* ] 为控制接口,提供鼠标、触控接口。 > 3. [ *Frontier* ] 为边缘碰撞,提供点与形状的相交相邻检测。 > 4. [ *Game* ] 为游戏的环境,提供游戏内容与游戏实现。 > 5. [ *draw* ] 为canvas(画布)的绘图环境,提供绘图相关功能。 由于游戏设计采用状态保存的逻辑,每运行完一次完整的游戏单次迭代都会更新所有需要更新的实体的状态,然后再绘图,所以我们很方便的动态调整游戏流程与内容,比如突然的结束游戏、清空所有实体等,这有很大一部分原因是我对游戏实体进行了分类存贮,对一部分进行清空或其他处理即可达到想要的效果。 ```javascript // 游戏实体分类存贮 const game = { // 所有障碍方块 blocks: [], // 所有小球 bullets: [], // 杂项 common: [], // 无尽模式标识 infinity: false, // 无尽模式下落时间戳 infinity_dt: dt(), // 游戏暂停标识 paused: false, // 游戏分数 score: 0 }; ``` > 在存贮方块的时候采用从头插入,在存贮小球以及杂项的时候采用尾部插入,因为小球于方块检测碰撞时,有限考虑最下方的方块可以减少计算量。 ### cfg 由于每个实体的状态都是确定的,既可以实现当某一个参数被修改后很快就可以看到效果,我们将参数单独提取出来,命名为 `cfg` ,玩家可以对其自定义。 > 我有一个想法,在后续更新中推出一个无限模式,我想这个 `cfg` 会对我有很大的帮助。 ### MouseEvent 对控制游戏的监听是游戏操作性中很重要的一点,准确性由浏览器绝对,我们可以决定对事件划分的灵活性,在处理不同的事件的时候,要确保事件能够被快速找到获快速替换修改,我不打算使用切片编程,在结构上使用字典来存储,不同的事件划分不同地方存贮。 ```javascript // 按下 移动 抬起 点击 const TODO = { down: {}, move: {}, up: {}, click: {} }; // 更新鼠标坐标 MouseEvent.move.Mouse = (x, y, isDown) => { ... } // 使平台可以自主发射小球 MouseEvent.up.Launcher = (x, y, dx, dy) => { ... } ``` > 在使用时可以直接将对应事件赋值既可以完成绑定。 为了兼容手机上能够正常的控制游戏,对原本的 `MouseEvent` 进行了封装,参数上的复制,对于是那个手指进行的控制并未进行分类,因为个人觉得一个手指足够控制游戏了。 ```javascript const trans = fn => { return e => { const target = e.targetTouches[0]; // 暂时支持单指 if (target !== undefined) { e.clientX = target.clientX; e.clientY = target.clientY; tx = e.clientX; ty = e.clientY; } else { e.clientX = tx; e.clientY = ty; } fn(e); } } ``` ### Frontier 存放一些点与矩形的相邻相交的检测方法。 1. 检测点与矩形 * 只需判断点的横坐标与纵坐标是在矩形上下左右内即可。 2. 检测矩形与矩形 * 先判断俩矩形横向上的中心点的距离是否小于或等于俩矩形宽度之和的一半,在判断矩形纵向上的中心点的距离是否小于或等于俩矩形高度之和的一半,若同时满足以上两个条件,则两矩形相交或相邻。 ### Game 这里主要是游戏核心与实现部分,对其控制可以达到对整个游戏进行控制的效果,主要逻辑是创建所有实体,并定义其规则,已对外开发 `G` 作为浏览器调试接口,效果与内部访问 `Game` 一致,同时 `G` 也链接了 `cfg` 可访问 `G.cfg` 达到访问 `cfg` 的效果。 ### draw 通过 `requestAnimationFrame` 对每一帧进行绘制,同时可以达到设备支持的理论最大帧数,将每个实体按照不同规格绘制出,绘制原理详见 [Canvas](https://www.w3school.com.cn/tags/html_ref_canvas.asp)。 ```javascript draw.push = (...args) => { // 绘制的先后顺序为从左到右,后绘制的内容将覆盖上一次绘制的内容 ... } ``` > 我们在纯色的基础上添加了渐变效果,使得界面看起来更加丰富。 ## 作者的一些话 咱就是说因为想起小时候电脑前的快乐,才有了这个尝试,我想我会继续写出很多以前玩过的游戏,并丰富它们,你对游戏有什么意见、疑问、想法都可以留言。 本项目遵循BSD开源协议,创作不易感谢理解 To be continued...