Scrcpy Mask实现原理剖析,如何在前端实现王者荣耀中技能的准确释放?

510 阅读5分钟

写在前面

Scrcpy Mask 是我近期开发的一款跨平台桌面客户端,是为了在电脑上像模拟器一样用键鼠控制你的安卓设备(打手游)。

上一期总体介绍了一下项目的如何在前端实现配置可视化编辑、按键映射: Scrcpy Mask实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?前端可视化、按键映射篇

这期,主要讲讲如何在前端实现王者荣耀中技能键的准确释放。

技能键

技能键,在王者之类的游戏中是必不可少的。一般来说,一个技能的释放需要分为三个过程,恰好对应按键映射的三个阶段:

  1. 按下阶段,此时需要发送 touch down 来触摸技能键所在坐标
  2. 按住阶段,此时需要根据鼠标的位置发送 touch move 来移动触摸点
  3. 抬起阶段,此时需要根据鼠标的位置发送 touch up 来抬起触摸点

显然,触摸技能键所在坐标是非常简单。而难点在于如何根据鼠标位置计算出触摸点的新坐标。

我们可以从简单到困难的思考这个问题:

版本 1

触摸点相对技能键位置的偏移 = 鼠标相对蒙版中心位置的偏移。这样实现起来很简单,粗浅地实现了技能键的坐标计算。

function mouseToOffset(mouse: number, center: number) {
  return mouse-center;
}

const centerX = maskSizeW * 0.5;
const centerY = maskSizeH * 0.5;

const targetX = skillX + mouseToOffset(mouseX, centerX);
const targetY = skillY + mouseToOffset(mouseY, centerY);

但是存在一个明显的问题:鼠标相对蒙版中心位置的偏移量可能非常大,直接作为触摸点的偏移量,不太合适。

此外,鼠标的坐标是相对窗口左上角的,并不是相对蒙版左上角的,这一点也需要调整。

版本 2

为了避免偏移量过大,我们可以将偏移限制在一个圆的范围内。此外,对于鼠标的坐标还需要减去蒙版左上角的坐标来进行转换。

![[圆坐标转换.png]]

const centerX = maskSizeW * 0.5;
const centerY = maskSizeH * 0.5;
// 减去蒙版坐标后再计算偏移量
const cOffsetX = clientPos.x - 70 - centerX;
const cOffsetY = clientPos.y - 30 - centerY;

// 计算距离
const offsetD = Math.sqrt(cOffsetX ** 2 + cOffsetY ** 2);
const maxD = 120;

if(offsetD>maxD){
	offsetX = Math.round((maxD / offsetD) * cOffsetX),
	offsetY = Math.round((maxD / offsetD) * cOffsetY),
}else{
	offsetX = cOffsetX;
	offsetY = cOffsetY;
}

这样偏移量都在一个圆内,也是显得有模有样了。但还是有一些问题:

  1. 鼠标只要距离中心稍微远一点,就会达到 maxD,这样很难精细的控制施法范围较大的技能
  2. maxD=120 但是这个 120 是相对鼠标在蒙版上的距离,而蒙版的比例尺和安卓设备分辨率的比例尺是不一致的

版本 3

对于问题 1,可以通过一个缩放来解决。首先,我们假设鼠标移动的最远距离为半个屏幕高度。从而计算出鼠标实际距离和最大距离的比例,将最终 maxD 根据这个比例进行缩放。

// ..。
const rangeD = maskSizeH - centerY;
const factor = Math.max(offsetD / rangeD, 1);
offsetX = Math.round((maxD / offsetD) * cOffsetX * factor),
offsetY = Math.round((maxD / offsetD) * cOffsetY * factor),

对于问题 2,只需要将 maxD 先转换为相对设备分辨率的值即可:

const maxLength = (120 / maskSizeH) * screenSizeH;

这样处理之后,鼠标移动对于技能的偏移量就显得不再那么灵敏了。

这样又出现了一个问题:有的技能的范围很小,需要灵敏度高一点,最好这个灵敏度是可调的。

版本 4

所以,我们添加一个百分比参数 range,值为 0~100。0 代表无穷灵敏,即不论鼠标偏移量多小,都直接移动 maxLength 的距离。100 则代表版本 3 中的情况。

const rangeD = (maskSizeH - centerY) * range * 0.01;
if (offsetD >= rangeD) {
  // include the case of rangeD == 0
  return {
    offsetX: Math.round((maxLength / offsetD) * cOffsetX),
    offsetY: Math.round((maxLength / offsetD) * cOffsetY),
  };
} else {
  const factor = offsetD / rangeD;
  return {
    offsetX: Math.round((cOffsetX / rangeD) * maxLength * factor),
    offsetY: Math.round((cOffsetY / rangeD) * maxLength * factor),
  };
}

如此,这个技能释放其实就没什么问题了。

但是对于王者荣耀,还存在两个影响技能指向准确性的问题:

  1. 游戏中角色所在位置并不是屏幕的正中心,而是有一定的偏移
  2. 游戏中视角并不是垂直向下的,而是存在一个投影,这可以从技能指示范围是一个椭圆而不是圆看出

版本 5

版本 4 中的问题,可以在这个 issue 中查看:

[BUG] The skill indicator direction isn't consistent with the mouse in "The Honor of Kings" · Issue #5

image.png

其实解决并没有那么困难,只是一时可能想不到。

  1. 根据截图看出这个偏移量是蒙版高度的 0.066 倍,只要将初始的鼠标 y 坐标减去这个偏移量即可
  2. 根据截图看出椭圆的长短轴比例为 450 : 315,所以只要 cOffsetX*0.7 即可

在此给出最终的技能键偏移计算算法,现在技能释放的方向就非常准确了:

function clientPosToSkillOffset(
  clientPos: { x: number; y: number },
  range: number
): { offsetX: number; offsetY: number } {
  const maxLength = (120 / maskSizeH) * screenSizeH;
  const centerX = maskSizeW * 0.5;
  const centerY = maskSizeH * 0.5;

  // 解决问题1
  clientPos.y -= maskSizeH * 0.066;

  // 解决问题2
  const cOffsetX = (clientPos.x - 70 - centerX) * 0.7;
  const cOffsetY = clientPos.y - 30 - centerY;
  const offsetD = Math.sqrt(cOffsetX ** 2 + cOffsetY ** 2);
  if (offsetD == 0) {
    return {
      offsetX: 0,
      offsetY: 0,
    };
  }

  const rangeD = (maskSizeH - centerY) * range * 0.01;
  if (offsetD >= rangeD) {
    // include the case of rangeD == 0
    return {
      offsetX: Math.round((maxLength / offsetD) * cOffsetX),
      offsetY: Math.round((maxLength / offsetD) * cOffsetY),
    };
  } else {
    const factor = offsetD / rangeD;
    return {
      offsetX: Math.round((cOffsetX / rangeD) * maxLength * factor),
      offsetY: Math.round((cOffsetY / rangeD) * maxLength * factor),
    };
  }
}

题外话

当时想怎么写这个技能键坐标算法的时候,掏出初中的知识画了个草稿哈哈哈

image.png

最后

本项目的分享到此为止了。有想法欢迎在评论区提出来~