写在前面
Scrcpy Mask 是我近期开发的一款跨平台桌面客户端,是为了在电脑上像模拟器一样用键鼠控制你的安卓设备(打手游)。
上一期总体介绍了一下项目的如何在前端实现配置可视化编辑、按键映射: Scrcpy Mask实现原理剖析,如何像模拟器一样用键鼠控制你的安卓设备?前端可视化、按键映射篇
这期,主要讲讲如何在前端实现王者荣耀中技能键的准确释放。
技能键
技能键,在王者之类的游戏中是必不可少的。一般来说,一个技能的释放需要分为三个过程,恰好对应按键映射的三个阶段:
- 按下阶段,此时需要发送
touch down
来触摸技能键所在坐标 - 按住阶段,此时需要根据鼠标的位置发送
touch move
来移动触摸点 - 抬起阶段,此时需要根据鼠标的位置发送
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;
}
这样偏移量都在一个圆内,也是显得有模有样了。但还是有一些问题:
- 鼠标只要距离中心稍微远一点,就会达到
maxD
,这样很难精细的控制施法范围较大的技能 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),
};
}
如此,这个技能释放其实就没什么问题了。
但是对于王者荣耀,还存在两个影响技能指向准确性的问题:
- 游戏中角色所在位置并不是屏幕的正中心,而是有一定的偏移
- 游戏中视角并不是垂直向下的,而是存在一个投影,这可以从技能指示范围是一个椭圆而不是圆看出
版本 5
版本 4 中的问题,可以在这个 issue 中查看:
其实解决并没有那么困难,只是一时可能想不到。
- 根据截图看出这个偏移量是蒙版高度的 0.066 倍,只要将初始的鼠标 y 坐标减去这个偏移量即可
- 根据截图看出椭圆的长短轴比例为 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),
};
}
}
题外话
当时想怎么写这个技能键坐标算法的时候,掏出初中的知识画了个草稿哈哈哈
最后
本项目的分享到此为止了。有想法欢迎在评论区提出来~