JS写小游戏「跳一跳」外挂之Canvas图像识别

Posted 三水清

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JS写小游戏「跳一跳」外挂之Canvas图像识别相关的知识,希望对你有一定的参考价值。

17 年结尾的时候微信发布新版重点推出了「小游戏」概念,H5 的游戏再次火了起来,新版微信开屏的游戏就是「跳一跳」游戏可玩度很高,网上也出现了各种语言版本的外挂,前几天看到一篇用 nodejs 搭建的外挂,需要手动点击截屏图片来判断当前和下一步的位置然后跳转,于是就起了用 Canvas 来实现图像的想法,后面有实现了自动跳转,算是齐活了。今天来完整说下图像识别。

先看下最终效果:


代码都放到了:https://github.com/ksky521/wechat-jump-game-hack 欢迎自己去尝试

Canvas 图像处理的原理

Canvas 可以通过 drawImage在上面添加图片,然后通过 getImageData方法获取一个 imageData对象,此对象包括了 data、 width和 height,其中 data 为图片 widthheight4 长度的数组,每个像素点表现在数组内为:RGBA 四个 0~255 的值,即 Red、Green、Blue 和 Alpha 值。

通过对这个 imageData.data进行遍历操作,可以利用图像差值比较找出图片内物体的边缘、物体的中心点,也可以根据图像中某个固定颜色范围的物体,进行匹配,从而找到「小人」的位置。

颜色值差值比较函数

先介绍一个函数 tolerenceHelper,用来比较颜色差值,即传入需要比较的 r、 g和 b,然后跟对比的 rt、 gt、 bt和差值范围的 t进行对比的函数,在范围内则返回 true

 
   
   
 
  1. function tolerenceHelper(r, g, b, rt, gt, bt, t) {

  2.    return r > rt - t && r < rt + t

  3.            && g > gt - t && g < gt + t

  4.            && b > bt - t && b < bt + t;

  5. }

获取小人当前位置

小人获取位置用的方式是差值比较,首先通过截屏中的紫色小人颜色范围,可以大致拿到小人的颜色值为:

 
   
   
 
  1. // 小人的颜色值

  2.    const playerR = 40;

  3.    const playerG = 43;

  4.    const playerB = 86;

这个值可以从小人的底部中心取色得到。所以找到小人的底部中心点的方式就是,在一定范围内(即 tolerenceHelper的 t 参数,这里取值为 16)查找,如果像素点 rgb 在这个范围内,则加入待选,最后像素点集合中最低点(最大 y)的位置就是小人底部中心所在点的 y,x 为最大和最小宽度的中心位置。为了好理解,我画了图:

下面三图是 t=16、 t=26、 t=36分别识别的效果,为了便于分辨,我将匹配到的像素点颜色都设置为了红色(rgb=255,0,0)。

JS写小游戏「跳一跳」外挂之Canvas图像识别

JS写小游戏「跳一跳」外挂之Canvas图像识别

JS写小游戏「跳一跳」外挂之Canvas图像识别

为了准确,防止相近颜色的干扰 t=16就够用了,这样小人的底部位置 pos就得到了:

 
   
   
 
  1. // x, y

  2. pos[0] = Math.floor((maxX + minX) / 2);

  3. pos[1] = maxY;

优化

很容易一眼就看出来小人不能在图片的顶部和底部,而是在画面的中心区域范围内,所以可以直接从图片高度的 height/4height*3/4的范围内查找,这样可以提高不必要的工作量。

完整查找小人点代码如下

 
   
   
 
  1. function getCurCenter(data, width, height) {

  2.    // 小人的颜色值

  3.    const playerR = 40;

  4.    const playerG = 43;

  5.    const playerB = 86;

  6.    let minX = Infinity;

  7.    let maxX = -1;

  8.    let maxY = -1;

  9.    // 找到小人当前的底部位置

  10.    let pos = [0, 0];

  11.    let startY = Math.floor(height / 4);

  12.    let endY = Math.floor(height * 3 / 4);

  13.    for (let x = 0; x < width; x++) {

  14.        for (let y = startY; y < endY; y++) {

  15.            let i = y * (width * 4) + x * 4;

  16.            let r = data[i];

  17.            let g = data[i + 1];

  18.            let b = data[i + 2];

  19.            if (y > pos[1] && tolerenceHelper(r, g, b, playerR, playerG, playerB, 16)) {

  20.                minX = Math.min(minX, x);

  21.                maxX = Math.max(maxX, x);

  22.                maxY = Math.max(maxY, y);

  23.            }

  24.        }

  25.    }

  26.    pos[0] = Math.floor((maxX + minX) / 2);

  27.    pos[1] = maxY;

  28.    // console.log(`player position (x, y)= (${pos[0]}, ${pos[1]})`);

  29.    return pos;

  30. }

获取的跳转位置

怎样获取小人下一步跳转的位置呢?

按照上面的逻辑,我们还是从图片高度的 height/4height*3/4的范围查找,这是我们先取出当前的背景色,然后在高度范围内扫描图片,当出现跟背景色相差很大的第一个点时,这时候就是下一个物体的主颜色值了!如果为四边体之类的,则这个点就是顶点了!

知道这个物体的主体颜色值,我们就可以以这个值为基准继续扫描,在这个颜色值范围的像素点就是物体的顶面,然后根据顶面像素点坐标 minY和 maxY得到中心点的坐标(圆形和正方形都是对称的,所以都可以用这个方法)。

看图理解下:

JS写小游戏「跳一跳」外挂之Canvas图像识别

下图是将背景色涂红,这样就可以看到识别出来的第一个点就是顶点(圆形一样)

JS写小游戏「跳一跳」外挂之Canvas图像识别

优化

  1. 找到顶点之后,下一行肯定不是 maxY,一次类推,可以大胆将 Y 的值增加 60 个像素,即从顶点往下的 60 个像素重新开始查找中心点;

  2. 另外可以将 Y 的查找范围缩小到上一步找到小人的中心点 Y 值,即 y 取值为 height/4~Math.min(height*3/4, 小人中心Y) ,这样即使 maxY 我们没有找到,也可以以小人为底取中心点,保证下一步的跳转位置尽量不会超出物体顶面范围。

  3. 我们找的是 maxY,所以只要出现了跟顶点像素点颜色一致(范围内)的点,这一行(坐标 Y 相同,X 不同)就不需要查找了,因为查找也没有意义,Y 值不变了,所以可以直接 break出循环,进行下一个 Y 的查找

完整代码

 
   
   
 
  1. function getNextCenter(data, width, height, y = -1) {

  2.    let startY = Math.floor(height / 4);

  3.    let endY = Math.floor(height * 3 / 4);

  4.    // 去除背景色

  5.    let startX = startY * width * 4;

  6.    let r = data[startX],

  7.        g = data[startX + 1],

  8.        b = data[startX + 2];

  9.    let maxY = -1;

  10.    let apex = [];

  11.    let pos = [0, 0];

  12.    // 保证从当前小人位置底部点往上

  13.    endY = Math.min(endY, y);

  14.    let endX = width;

  15.    let gapCount = 0;

  16.    for (let y = startY; y < endY; y++) {

  17.        let find = 0;

  18.        for (let x = 1; x < endX; x++) {

  19.            let i = y * (width * 4) + x * 4;

  20.            let rt = data[i];

  21.            let gt = data[i + 1];

  22.            let bt = data[i + 2];

  23.            // 不是默认背景颜色

  24.            if (!tolerenceHelper(rt, gt, bt, r, g, b, 30)) {

  25.                if (apex.length === 0) {

  26.                    if (!tolerenceHelper(data[i + 4], data[i + 5], data[i + 6], r, g, b, 30)) {

  27.                        //椭圆形找中心,往后找30个像素点

  28.                        let len = 2;

  29.                        while (len++ !== 30) {

  30.                            i += len * 4;

  31.                            if (tolerenceHelper(data[i + 4], data[i + 5], data[i + 6], r, g, b, 30)) {

  32.                                break;

  33.                            }

  34.                        }

  35.                        x += len;

  36.                    }

  37.                    //找出顶点

  38.                    apex = [rt, gt, bt, x, y];

  39.                    pos[0] = x;

  40.                    // 减少循环范围

  41.                    endX = x;

  42.                    break;

  43.                } else if (tolerenceHelper(rt, gt, bt, apex[0], apex[1], apex[2], 5)) {

  44.                    //存在顶点了,则根据颜色值开始匹配

  45.                    maxY = Math.max(maxY, y);

  46.                    find = x;

  47.                    break;

  48.                }

  49.            }

  50.        }

  51.        if (apex.length !== 0 && !find) {

  52.            gapCount++;

  53.        }

  54.        if (gapCount === 3) {

  55.            break;

  56.        }

  57.    }

  58.    pos[1] = Math.floor((maxY + apex[4]) / 2);

  59.    // console.log(points_top, points_left, points_right);

  60.    console.log(`next position center (x,y)=${pos[0]},${pos[1]}`);

  61.    return pos;

  62. }

最后

在整个测试的过程中,还尝试了其他的方式,比如先将边缘找出再找中心点,各种尝试,想练手的可以直接改下看看。

为了调试方便,我将这部分代码单独一个 router,可以直接 github clone 下来代码访问 localhost:3000/test ,然后边改边尝试边看效果。

本篇文章介绍了怎么识别出来图片中「小人」和下一个跳转的位置,下一篇介绍下怎么让「小人」自动跳转过去,敬请期待。


EOF
@三水清
未经允许,请勿转载。


感觉有用,欢迎关注我的公众号



以上是关于JS写小游戏「跳一跳」外挂之Canvas图像识别的主要内容,如果未能解决你的问题,请参考以下文章

《跳一跳》当天开挂!游戏外挂咋能这么猛?

原创,微信跳一跳外挂源码热门游戏,轻松上千分

揭密微信跳一跳小游戏那些外挂

揭密微信跳一跳小游戏那些外挂

除了在坦克大战吃鸡跳一跳上作弊,物理外挂带给我们什么脑洞?

由跳一跳外挂说起——初识WebDriverAgent