摘要:在弹一弹游戏中,小球不能向上发射。这里又有一个坑弹一弹游戏中,刚射击出去的小球是不受重力影响的不然瞄准还有什么意义。
前言
半年前用js和canvas仿了热血传奇网游(地址),基本功能写完之后,剩下的都是堆数据、堆时间才能完成的任务了,没什么新鲜感,因此进度极慢。这次看到微信《弹一弹》比较火,因为涉及到物理引擎(为了真实),于是动手试了一下。一共用了10个小时,不仅完成了这个demo,<删除线>并且打上了弹一弹好友排行榜的第一页删除线>。
资料汇总在线demo:点击即玩
代码:400行带注释
canvas渲染库:支持物理引擎及chrome调试工具,这里
准备工作微信这个小游戏的游戏规则很简单,看图就能看明白,这里不再赘述。涉及到的几个开发难度:
1.物理引擎当然不用也可以,无非就是改改图片的位置,可以自己模拟掉落和碰撞效果。不过由于我追(wu)求(li)体(hen)验(cha),因此开始寻找第三方的物理引擎。
最后我使用的是chipmunk的js版(这个库是底层计算库,因此star不多,但是比较有名气的hilo和cocos2d的物理引擎用到了这个库)。主要原因之一是,这个库的功能只是进行了物理运算,并且支持重力、弹性、摩擦、浮力等功能。当然体积也比较小。毕竟我们只是写一个小demo,引入一个游戏框架的话很可能徒增成本。
不过我使用的时候遇到了几个罕见的bug,应该是作者的疏漏,在issue中也有人反馈。看作者更新频率很低,我拿来用的时候有一些修改。如果其它人使用的时候遇到js报错,可以试试这里
2.UI渲染我选择的是canvas,因为涉及到频繁的样式更新,每帧都去改写style的话太占性能。而且用canvas写的话,以后还可以迭代一些碰撞产生的画效。
之前封装了一个easycanvas库,可以将树形数据结构“翻译”成“canvas画布中的一个个对象”。这次又顺手补充了一个支持chipmunk的插件,这样整个“弹一弹”的开发就完全只需要管理数据即可,渲染工作很少。
开始开发 html及背景由于项目较小,我把html、css、js堆在了一个文件(最后写完之后,发现一共连同注释才400行)。
首先创建一个空html,为了看起来高大上,我搜了一张天空主题的背景图。
可能用到的变量
接下来,准备一些我们需要用到的数据。例如游戏的宽高、小球的大小、当前游戏状态(是否可以射击)、每次可以射出的小球数、玩家的分数,blabla。
由于是直接在html里写码,为了兼容老浏览器,只能var来var去。
// 在html直接写代码,不编译、不构建,不然应该用const的 var width = 400, height = 600, ballSize = 20; // 游戏状态 var canShoot = true; var score = 0, ballLeft = 0, ballCount = 5; var blockArray = []; // 图片 var BALL = Easycanvas.imgLoader("./ball.png"); var BLOCK = Easycanvas.imgLoader("./block.jpg"); var TRIANGLE = Easycanvas.imgLoader("./triangle.png"); // 给每个东西起一个type,后面会用来做碰撞检测 var BALL_TYPE = 1, BLOCK_TYPE = 2, BORDER_TYPE = 3, BOTTOM_TYPE = 4, BONUS_TYPE = 5;顶部文本
接下来先将分数和小球个数写到canvas中。首先创建一个easycanvas实例,宽400,高600。然后add2个对象。一个以左上角(5,5)为顶点,向右下方写分数。一个以右上角(395, 5)为顶点,向左下角写当前小球个数。
// 初始化easycanvas实例 var $Painter = new Easycanvas.painter(); $Painter.register(el, { width: width, height: height, }); $Painter.start(); $Painter.add({ content: { text: function () { return "得分:" + score; } }, style: { tx: 5, ty: 5, textAlign: "left", textVerticalAlign: "top", color: "black" } }); $Painter.add({ content: { text: function () { return "小球个数:" + ballCount; } }, style: { tx: 395, ty: 5, textAlign: "right", textVerticalAlign: "top", color: "black" } });添加方块
接下来,设置整个场景的重力,并且添加一些方块进去。每个方块对象含有一个child,用来展示数字(还可以撞几下)。为了避免方块重叠,我们让方块的x坐标在50、100、150、……、300、350循环。同时,为了避免看起来“太整齐”,每次添加一个小的随机数,让这些方块们错落有致。(“错落”指参差不齐,“致”指情趣。形容事物的布局虽然参差不齐,但却极有情趣,使人看了有好感。——某度)
每个方块的大小是30x30,因此shapes包括4条边,例如(0,0)到(30,0)是一条边。这些方块是失重的(不会掉下去),因此static设置为true。为了更加错落有致,我们给他一个随机的角度rotate。
每个方块含有一个child,写着一个数字。不需要给数字设置rotate,否则6和9可能就分不清了。
// 初始化easycanvas物理引擎,添加一个有物理树形的空容器 var $space = new Easycanvas.class.sprite({ physics: { gravity: 2, // 重力默认为1,但是游戏进程有点慢,看着不够爽 accuracy: 2, }, }); $Painter.add($space); var space = $space.launch(); // 防止方块重叠,记录上一次方块的X坐标 var lastBlockPositionX = 50; function addBlock (max, boolAddToBottom) { var deg = Math.floor(Math.random() * 360); var sprite = $space.add(new Easycanvas.class.sprite({ name: "block", content: { img: BLOCK, }, physics: { shape: [ [[0, 0], [0, 30]], [[0, 30], [30, 30]], [[30, 30], [30, 0]], [[30, 0], [0, 0]] ], mass: 1, friction: 0.1, elasticity: 0.9, collisionType: BLOCK_TYPE, static: true, }, style: { tw: 30, th: 30, tx: lastBlockPositionX + Math.floor(Math.random() * 20 - 10), ty: boolAddToBottom ? 500 : height - 100 - Math.floor(Math.random() * 100), locate: "lt", rotate: deg, }, children: [{ content: { text: Math.floor(Math.random() * max) + 1, }, style: { color: "yellow", textAlign: "center", textVerticalAlign: "middle", textFont: "28px Arial", tx: 15, ty: 10 } }] })); sprite.physicsOn(); blockArray.push(sprite); lastBlockPositionX += 50; if (lastBlockPositionX > 350) { lastBlockPositionX = 50; } }
接下来,我们做瞄准部分。大致功能是,有一排小圆点,会随着鼠标运动,并且有弹簧的感觉。
首先要记录鼠标的轨迹,我们给easycanvas实例$Painter加上事件监听。在“弹一弹”游戏中,小球不能向上发射。因此记录鼠标的Y坐标值的时候,我们让他至少为30。
// 记录鼠标轨迹 var mouse = {x: 300, y: 50}; var mouseRecord = function ($e) { mouse.x = $e.canvasX; mouse.y = Math.max(30, $e.canvasY); }; $Painter.register(el, { width: width, height: height, events: { mousemove: mouseRecord, touchmove: mouseRecord, mouseup: shoot, touchend: shoot, } });小球瞄准
接下来,我们添加7个小球,让他们排列在一条线上,从游戏正上方的(300, 20)点到鼠标位置均匀铺开。具体逻辑就是,我们将鼠标位置和(300, 20)的坐标差进行6等分,第一个球的坐标向鼠标位置偏移0/6、第二个球偏移1/6……,最后一个球偏移6/6(正好落在了鼠标位置)。这几个球我们给他们一个透明度,并且不启用物理规则(因为这个阶段小球不能掉下来)。我们在每个小球上设置一个shoot钩子,当玩家射出真实的小球时,删除这个瞄准用的小球。
// 显示瞄准轨迹 var startAim = function () { for (var i = 0; i < 7; i ++) { $Painter.add({ content: { img: BALL, }, data: { gap: i / 6, }, style: { tx: function () { return 200 + (mouse.x - 200) * this.data.gap; }, ty: function () { return 20 + (mouse.y - 20) * this.data.gap; }, tw: 20, th: 20, opacity: 0.4, }, hooks: { shoot: function () { this.remove(); } } }); } }; startAim();发射小球
接下来,我们添加真实的小球(受到物理规则影响的小球)。
当射击时,我们广播shoot事件,移除刚才瞄准用的小球。
之后,我们间隔100毫秒,连续调用addBall方法来创建小球。addBall方法中,我们为每个小球设置物理规则。包括形状、弹性、摩擦等。
这里有一个坑,就是一旦开始射击,不管鼠标怎么移动,射击的方向都不能变化。因此我们要先记录下当前的mouse值,这里用的是JSON.parse(JSON.stringify(mouse))来copy一个简单对象。
这里又有一个坑:“弹一弹”游戏中,刚射击出去的小球是不受重力影响的(不然瞄准还有什么意义)。因此,我们在每个小球上增加一个和重力相反的作用力,抵消重力。(在其它部分的代码中,有着“当小球发生一次碰撞后,取消这个作用力”的实现,这里为了清晰没有一起贴出来)。
同时,我们给小球加上初速度。
这里又又又又又有一个坑(好烦啊):不管怎么射击,小球初始获得的速度是相同的。哪怕小球的瞄准位置距离射出位置很近,速度也不能慢。这里需要修正一下初始速度,这里用到了著名的Pythagoras theorem定理:直角三角形的两条直角边的平方和等于斜边的平方。
function shoot () { if (!canShoot) return; $Painter.broadcast("shoot"); canShoot = false; var currentMouse = JSON.parse(JSON.stringify(mouse)); for (var i = 0; i < ballCount; i++) { setTimeout(function () { addBall(currentMouse); }, i * 100); } }; function addBall (mouse) { ballLeft++; var $ball = new Easycanvas.class.sprite({ name: "ball", content: { img: BALL, }, physics: { shape: [ // 形状是一个以(ballSize / 2, ballSize / 2)为圆心的,半径也是ballSize / 2的圆 // 改成位运算符吧,看着能高大上一点(其实在这里卵用没有) [ballSize >> 1, ballSize >> 1, ballSize >> 1] ], mass: 1, // 质量 friction: 0.1, // 摩擦(摩擦太大了会损失能量) elasticity: 0.8, // 弹性 collisionType: BALL_TYPE, }, style: { tw: ballSize, th: ballSize, sx: 0, sy: 0, tx: 200, ty: 20, zIndex: 1, }, }); $space.add($ball); $ball.physicsOn(); // 抵消重力 $ball.$physics.body.applyForce({x: 0, y: 1000}, {x: 0, y: 0}); // 初速度 var speed = { x: (mouse.x - 200) / (20 - mouse.y), y: 1 }; // 修正速度,确保从各个角度射出小球的速度差不多 // 这里用到的著名的高等数学知识:勾股定理 var muti = Math.sqrt(Math.pow(speed.x, 2) + Math.pow(speed.y, 2)) / 700; $ball.$physics.body.setVel({ x: -speed.x / muti, y: -speed.y / muti, }); }其它
轮廓已经有了,后面的部分不再是难点。不过做到最后,坑还是比较多的:
例如小球可能会停在方块上(就是这么巧),这是需要人为给予小球一个速度(“弹一弹”游戏里也是这样做的)。
例如小球撞到方块上,可能会触发2次碰撞,因为影响不大,我先搁置了。这个是因为时间精度没有太细,小球在上一帧没有发生碰撞,因为速度较快,下一帧同时撞到了2个边界。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/94760.html
摘要:直到年,世界上第一部动画片滑稽脸的幽默相问世。上一次视神经传递的图像将会在大脑中存留,直到下一次神经信号到达。移动设备还是相当惨烈,并没有开始支持。市面上有很多动画库,大家可以开箱即用。有一些是针对操作的,也有一些是针对对象。 背景 138.2亿年前,世界上没有时间和空间,或许世界都不存在,在一个似有似无的点上,汇集了所有的物质,它孕育着无限的能量与可能性。 宇宙大爆炸 巨大的内力已无...
摘要:于是,我决定厚着脸皮来宣传一下我的几个开源项目,虽然大多数都是一些比较简单的游戏,但是这可以让更多人看到我的项目,也可以让我自己知道哪里地方做得不好,并且加以改进。正文清技背单词使用开发的背单词应用,开发时间为一个月,目前是版本。 前言 之前陆陆续续在 GitHub 上创建了几个项目,奈何没人关注(可能我的项目太垃圾了)。于是,我决定厚着脸皮来宣传一下我的几个开源项目,虽然大多数都是一...
阅读 1747·2023-04-25 16:28
阅读 683·2021-11-23 09:51
阅读 1466·2019-08-30 15:54
阅读 1148·2019-08-30 15:53
阅读 2815·2019-08-30 15:53
阅读 3412·2019-08-30 15:43
阅读 3249·2019-08-30 11:18
阅读 3260·2019-08-26 10:25