js+canvas仿微信《弹一弹》小游戏

2023-11-12

前言

半年前用js和canvas仿了热血传奇网游(地址),基本功能写完之后,剩下的都是堆数据、堆时间才能完成的任务了,没什么新鲜感,因此进度极慢。这次看到微信《弹一弹》比较火,因为涉及到物理引擎(为了真实),于是动手试了一下。一共用了10个小时,不仅完成了这个demo,<删除线>并且打上了弹一弹好友排行榜的第一页</删除线>。

资料汇总

准备工作

微信这个小游戏的游戏规则很简单,看图就能看明白,这里不再赘述。涉及到的几个开发难度:

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,为了看起来高大上,我搜了一张天空主题的背景图。

<style>
    body {
        margin: 0;
        text-align: center;
        background: black;
    }
    canvas {
        border: 1px solid grey;
        height: 100%;
        max-width: 100%;
        background-image: url(http://a3.topitme.com/2/d4/ff/1144306867e94ffd42o.jpg);
        background-size: auto 100%;
    }
</style>
<body>
    <canvas id="el"></canvas>
</body>
复制代码

可能用到的变量

接下来,准备一些我们需要用到的数据。例如游戏的宽高、小球的大小、当前游戏状态(是否可以射击)、每次可以射出的小球数、玩家的分数,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个边界。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

js+canvas仿微信《弹一弹》小游戏 的相关文章

  • 在 ASP.NET Core 2.0 Web Api 中返回“原始”json

    AFAIK 在 ASP NET Core Web Api 中返回数据的标准方法是使用IActionResult并提供例如一个OkObject结果 这对于对象来说效果很好 但是如果我以某种方式获得了一个 JSON 字符串 并且我只想将该 JS
  • SQL Server OPENJSON读取嵌套json

    我有一些想要在 SQL Server 2016 中解析的 json 有一个项目 gt 结构 gt 属性的层次结构 我想编写一个解析整个层次结构的查询 但我不想通过索引号指定任何元素 即我不想做这样的事情 openjson json 0 or
  • 在 Android 中使用 DataOutputStream 在 POST 正文中发送特殊字符 (ë ä ï)

    我目前正在开发一个具有大量服务器端通信的 Android 应用程序 昨天 我收到一份错误报告 称用户无法发送 简单 特殊字符 例如 我搜索过但没有找到任何有用的东西 可能重复 没有答案 https stackoverflow com que
  • 发送 POST 请求时 JSON 原语无效

    我有以下 ajax 请求 其中我尝试将 JSON 对象发送到服务器 function sendData subscriptionJson ajax type POST url Url Action SubscribeSecurities S
  • Jackson 将单个项目反序列化到列表中

    我正在尝试使用一项服务 该服务为我提供了一个带有数组字段的实体 id 23233 items name item 1 name item 2 但是 当数组包含单个项目时 将返回该项目本身 而不是包含一个元素的数组 id 43567 item
  • 通过标识引用对象的标准方法(例如循环引用)?

    JSON 中是否有通过身份引用对象的标准方法 例如 具有大量 可能是循环 引用的图形和其他数据结构可以被合理地序列化 加载吗 Edit 我知道做一次性解决方案很容易 列出图中所有节点的列表 然后 我想知道是否有一个标准的 通用的解决方案来解
  • JSON 到 hashmap (杰克逊)

    我想将 JSON 转换为 HashMapJackson http jackson codehaus org 这是我的 JSON String json Opleidingen name Bijz trajecten zorg en welz
  • 从 Twitter API 2.0 获取 user.fields 时出现问题

    我想从 Twitter API 2 0 端点加载推文 并尝试获取标准字段 作者 文本 和一些扩展字段 尤其是 用户 字段 端点和参数的定义工作没有错误 在生成的 json 中 我只找到标准字段 但没有找到所需的 user fields 用户
  • PrototypeJS 版本 1.6.0.2 覆盖 JSON.parse 和 JSON.stringify 并破坏 socket.io 功能

    基本上 socket io 使用 nativeJSON 来编码和解码数据包 而我的问题是我必须使用这个版本的原型来改变 JSON 行为 当我应该进入服务器时 如下所示 socket on event function a b c 我明白了s
  • 列出 JSON 的所有键和值

    假设我有一些如下所示的 JSON items item id 0001 type donut name Cake ppu 0 55 batters batter
  • 用数组反向查找对象

    假设我有一个这样的对象 resourceMap a 0 1 2 3 4 5 6 7 8 9 10 b 11 12 c 21 23 d 54 55 56 57 510 确定是否的最佳方法是什么resourceId 21将会 c 我们不知道钥匙
  • 如何在 C# 中获取 Json 数组?

    我有一个像这样的 Json 字符串 我想将它加载到 C 数组中 当我尝试这样做时 我收到异常 我的字符串 customerInformation customerId 123 CustomerName Age 39 Gender Male
  • 动态创建 JSON 对象

    我正在尝试使用以下格式创建 JSON 对象 tableID 1 price 53 payment cash quantity 3 products ID 1 quantity 1 ID 3 quantity 2 我知道如何使用 JSONOb
  • PHP 不使用“json_decode()”转换 JSON

    我有一段非常简单的代码 pc1 POST post code1 pc2 POST post code2 url http maps google com maps nav q from pc1 20to pc2 url data file
  • json文件格式的升级路径

    我们将 Java 应用程序的用户首选项存储在 JSON 文件中 使用Jackson http jackson codehaus org 随着我们继续开发该应用程序 我们将添加首选项 重命名首选项并删除过时的首选项 当用户将应用程序升级到下一
  • 未捕获的类型错误:未定义不是函数

    我收到消息Uncaught TypeError Undefined is not a function当我尝试调用家庭控制器中的方法时 也许关于我为什么收到此消息的建议 findIdpActivities function pernr ca
  • Python - UnicodeDecodeError:“charmap”编解码器无法解码位置 44 中的字节 0x81:字符映射到 <未定义>

    在 Python 3 Jupyter 笔记本上使用 pandas 我得到了 UnicodeDecodeError charmap 编解码器无法解码字节 0x81 位置 44 字符映射到 尝试读取如下所示的 json 文件时出错 Test1
  • Jackson Kotlin - 反序列化 JsonNode

    Problem 我有字符串形式的 JSON 内容 我首先想用 Jackson 以编程方式遍历它 然后 当我有感兴趣的节点时 我想反序列化它 我尝试过的 我已使用 mapper readValue 成功反序列化字符串 但现在我想在 jsonN
  • 在 Mono 中反序列化 JSON 数据

    使用 Monodroid 时 是否有一种简单的方法可以将简单的 JSON 字符串反序列化为 NET 对象 System Json 只提供序列化 不提供反序列化 我尝试过的各种第三方库都会导致 Mono Monodroid 出现问题 谢谢 f
  • GSON 将带有日历的对象反序列化为带有 Mongo 日期的 json 并返回

    我有一些实体 其中包含一些日历属性 我想以将它们存储为 GSON 序列化 JSON 中的日期的方式对其进行序列化 因为 Mongo 可以将 date 存储为 new ISODate 我们通常通过使用 ExclusionStrategy 忽略

随机推荐

  • 在不影响线上服务情况下,删除大表数据表

    在不影响线上数据库服务情况下 如何删除数据库中的大表 分析 数据库中表涉及到db和os两个层面 1 db层面删表涉及到table cache的全局唯一锁 一旦数据表过大 会长时间占用全局为一锁 导致db卡死 2 os层面涉及到数据表物理文件
  • 伤腰的Python爬虫案例,零基础必备实战教程。

    前言 今天带大家采集一个二次元图片网站 里面漂亮的小姐姐层出不穷 图片的数据量也是比较大的 来一睹为快吧 开发环境介绍 python 3 6 pycharm requests parsel os 爬虫案例数据采集一般步骤 找数据对应的链接地
  • conan的学习与使用

    conan学习与使用 资源 官方地址 C C Open Source Package Manager conan io 重要概念 什么是包管理工具 包管理工具的主要作用是管理第三方依赖 也可以看成一个 轮子 工厂 每个人都可以上传自己造的
  • verdi显示数据

    在波形数据上点右键 2 s complement 就是大家计算机课上学的 补码 1 s complement 是课上讲的 反码 signed magnitude 最高位是符号位 0 正数 1 负数 低位是绝对值 另外 ncverilog v
  • FFmpeg命令行工具学习(五):FFmpeg 调整音视频播放速度

    转自 https www cnblogs com renhui p 10709074 html FFmpeg对音频 视频播放速度的调整的原理不一样 下面简单的说一下各自的原理及实现方式 一 调整视频速率 调整视频速率的原理为 修改视频的pt
  • QT之实现简陋聊天

    相关知识 QT 数据库 TCP IP Socket 1 登陆界面 包含登陆和注册两种功能 思路如下 难点 建立服务器和数据库 数据库保存数据 服务器与数据库产生联系 解决 数据库与服务器放在同一个类中 登陆和注册时 客户端与服务端连接 传输
  • 数据结构与算法(Java描述)-19、哈夫曼树、哈夫曼编码算法

    一 哈夫曼树的基本概念 在一棵二叉树中 定义从A结点到B结点所经过的分支序列叫做从A结点到B结点的路径 从A结点到B结点所经过的分支个数叫做从A结点到B结点的路径长度 从二叉树的根结点到二叉树中所有叶结点的路径长度之和称作该二叉树的路径长度
  • ctfshow 网络迷踪做题记录(1)

    ctfshow 网络迷踪做题记录 1 新手上路 找桥的名字 附件为一张海边图片 百度识图为蜈支洲岛 得到地点名 但还需要具体桥的名字 再用搜索引擎搜索关键字 就可以看到结果中的 情人桥 初学乍练 题目描述 提交这架飞机的目的地 附件图片为一
  • 十分钟让你搞懂会用Spring Retry

    一 项目的配置 为了启用 Spring Retry 的支持 首先要在pom xml 文件中添加以下依赖项
  • 良许Linux

    一个系统管理员可能会同时管理着多台服务器 这些服务器也许会放在不同的地方 要亲自一台一台的去访问来管理它们显然不是最好的方法 通过远程控制的方法应该是最有效的 Linux系统的远程管理工具大概有几种 telnet ssh vnc等 其中ss
  • motionface respeak新的aigc视频与音频对口型数字人

    在当今的数字化时代 人工智能 AI 正在逐渐渗透到我们生活的方方面面 其中 AI技术在视频制作和处理领域的应用也日益广泛 本文将探讨如何利用AI技术实现视频中人脸与音频同步对口型的方法 旨在进一步丰富视频制作的效果和表现形式 数字人一件对口
  • 由于无法验证发布者,Windows已经阻止此软件

    Windows系统都很注重系统的安全性 在提高安全性的同时 也给我们某些应用带来不便 比如在日常工作中经常会到某些网站上进行登录 需要安装该站点的ActiveX控件 否则无法正常加载 这时可能会弹出 由于无法验证发行者 所以WINDOWS已
  • matlab中由离散点生成云图,[转载]在matlab中由离散点生成云图

    首先 有离散点的数据如下 x 376 82 377 56 379 74 421 20 419 41 417 82 418 80 458 86 457 72 459 55 461 64 500 27 501 51 499 48 498 02
  • U盾的工作原理

    你的数字证书有一对 一份在U盾里的私钥 一份在银行的公钥 其实两份银行都有 U盾的原理很类似于双向认证的TLS SSL 或者其它用到RSA的双向证书验证手段 以下步骤可能和U盾实际执行的有所区别 但本质相同 银行先给你一个 冲击 它包含了随
  • No module named ‘cv2‘ 解决办法 (No module named ‘numpy‘ 等所有报错均可解决)

    更多视觉额自动驾驶项目请见 自动驾驶项目 实在不行可以私信我解决 0 常规解决方案 1 当出现No module named cv2 解决方案 pip install opencv python i https pypi tuna tsin
  • 等价类划分法设计测试用例

    等价类划分法 一 方法简介 1 定义 是把所有可能输入的数据 即程序的输入域划分策划国内若干部分 子集 然后从每一个子集中选取少数具有代表性的数据作为测试用例 方法是一种重要的 常用的黑盒测试用例设计方法 2 划分等价类 等价类是指某个输入
  • C++ 使用类成员函数的地址

    include
  • Linux基础笔记3

    操作系统基本认识 Linux 是什么 百度百科是这样定义 Linux Linux 全称GNU Linux 是一种免费使用和自由传播的类UNIX操作系统 其内核由林纳斯 本纳第克特 托瓦兹于1991年10月5日首次发布 它主要受到Minix和
  • 栈的实现(C语言版)

    大家好 这篇我们继续讲解数据结构里的栈 文章目录 栈的概念 栈的实现 栈的结构 栈的初始化 栈的销毁 栈是否为空 删除函数 取栈顶的数据 栈里数据的个数 插入函数 栈的概念 栈 一种特殊的线性表 其只允许在固定的一端进行插入和删除元素操作
  • js+canvas仿微信《弹一弹》小游戏

    前言 半年前用js和canvas仿了热血传奇网游 地址 基本功能写完之后 剩下的都是堆数据 堆时间才能完成的任务了 没什么新鲜感 因此进度极慢 这次看到微信 弹一弹 比较火 因为涉及到物理引擎 为了真实 于是动手试了一下 一共用了10个小时