一、前言
(Java代码的实现是基于另外一篇博客,我精简了计算方法而成,参考博客地址http://blog.csdn.net/lihushiwoa/article/details/78942322)
先说一说我的感受,之前觉得能做出做出辅助工具的技术要求一定很高,然而当自己真正分析原理并且动手实现之后发现门槛没有那么高。
想做这个工具首先得知道adb是什么,其次是会一门编程语言。我了解adb之后瞬间觉得思路豁然开朗。
adb是Android Debug Bridge。他的作用是可以通过在计算机cmd输入命令控制Android手机,比如马上要用到的效果----模拟实现触控屏幕和触控多长时间。所以你在java代码里控制adb命令是可行的。
二、思路与原理
想象一下手动能让棋子命中下一个platform的场景:
1:测量棋子到目标坐标的距离S;
2:按压时间T=S*时间系数;(时间系数:单位位移的用时,200ms的n倍,多次尝试的大致确认)
然后你按屏幕时间T就可以了。
在本代码中我发现每个跳板的连线与竖直方向大约呈60°夹角(跳板和跳板虽然是60°,但是棋子不一定与目标是60°角关系呀?没关系,经过测试与棋子的角度没太大关系),所以如果我们知道棋子和目标点的距离就可以算S了。
代码实现也一样:
1:adb命令截屏,存放到指定文件夹。adb命令分别是:
adb shell screencap -p /sdcard/current.png
adb pull /sdcard/current.png d:/jump_screencapture
2:扫描棋子的X轴坐标。
图片是一个个像素点组成,从上往下遍历每一行像素点,如果某像素点的R、G、B值在棋子的RGB值区间(R∈(50,60),G∈(53,63),B∈(95,110)),那么可以确定该像素点在棋子内了,然后记下这是第几个(halmaXCount),并且计算横坐标的和halmaSum+=halmaSum(原理:多次测量取平均值,精确棋子的X轴坐标)。
3:扫描目标跳板的横坐标。原理与扫描棋子相似,但又不同。
原理:由于跳板是左右对称的,也就是说假如目标跳板的上表面是标准的菱形,那么一旦遍历到菱形上方的顶点,那么该顶点的X坐标就是目标跳板上表面中心的X坐标。
不同之处是:遍历完目标调班的首行像素点便停止,(缺点:样本空间略小,可能导致目标坐标不精确,留日后优化。)。记录每行首个像素点的RGB值之后,在接下来的遍历中如果发现本行与首个像素点RGB差异太大,则认为该像素点已经在目标跳板之内。找到所有不同于第一个像素点的X坐标求平均数,然后结束。
4:计算棋子与目标点的距离,和按压时间。
三、ADB配置
ADB主要是Path配置,配置完,在cmd中输入adb,如下图便是配置成功。需要的可以去http://download.csdn.net/download/thread_cooperation/10212920下载
四、代码实现
注意:不要在开发工具里跑,可能无法调用ADB。在cmd中是可行的。
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import javax.imageio.ImageIO;
/**
* 参考CSDN
* @link <a target="_blank" href="http://blog.csdn.net/lihushiwoa/article/details/78942322">http://blog.csdn.net/lihushiwoa/article/details/78942322</a>
*
* 说明:本版本是在上述参考CSDN的基础上,精简计算而成,目前支持分辨率1920x1080的安卓机(在下没有其他分辨率的手机用于测试)
*
* 跳一跳辅助
* @author ykq
*/
public class Jump {
private static final String IMAGE_NAME = "current.png";
private static final String STORE_DIR = "d:/jump_screencapture";
//截图的数量上限
private static final int imageLengthLength = 5;
//存放图片的数组
private static final long[] imageLength = new long[imageLengthLength];
private final RGBInfo rgbInfo = new RGBInfo();
//adb脚本
private final String[] ADB_SCREEN_CAPTURE_CMDS =
{ "adb shell screencap -p /sdcard/" + IMAGE_NAME,
"adb pull /sdcard/current.png " + STORE_DIR };
//截屏中游戏分数显示区域最下方的Y坐标,300是 1920x1080的值,根据实际情况修改
private final int gameScoreBottomY = 310;
//按压的时间系数,可根据具体情况适当调节
private final double pressTimeCoefficient = 1.37;
//按压的起始点坐标,也是再来一局的起始点坐标
private final int swipeX = 550;
private final int swipeY = 1580;
//棋子的宽度,从截屏中量取,自行调节
private final int halmaBodyWidth = 74;
//csc(pi/6)
private final static double proportion = 2/Math.sqrt(3);
public static void main(String[] args){
try{
File storeDir = new File(STORE_DIR); //构建文件目录
if (!storeDir.exists()) {
boolean flag = storeDir.mkdir(); //创建文件夹
if (!flag) {
System.err.println("创建图片存储目录失败");
return;
}
}
Jump jumpjumpHelper = new Jump();
//执行次数
int executeCount = 0;
for (;;){
//执行ADB命令,获取安卓截屏
jumpjumpHelper.executeADBCaptureCommands();
File currentImage = new File(STORE_DIR, IMAGE_NAME);
if (!currentImage.exists()){
System.out.println("图片不存在");
continue;
}
long length = currentImage.length();
imageLength[executeCount % imageLengthLength] = length;
//查看是否需要重新开局
jumpjumpHelper.checkDoReplay();
executeCount++;
System.out.println("当前第" + executeCount + "次执行!");
//获取跳棋和底板的中心坐标
int[] result = jumpjumpHelper.getHalmaAndBoardXYValue(currentImage); //Halma跳棋
if (result == null){
System.out.println("The result of method getHalmaAndBoardXYValue is null!");
continue;
}
int halmaX = result[0];
int boardX = result[1];
System.out.println("halmaX: " + halmaX + " **** " + "boardX: " + boardX);
//计算跳跃的距离
double jumpDistance = Math.abs(boardX-halmaX)*proportion;
jumpjumpHelper.doJump(jumpDistance);
//每次停留2.5秒
TimeUnit.MILLISECONDS.sleep(2500);
}
}
catch (Exception e){
e.printStackTrace();
}
}
/**
* 获取跳棋以及下一块跳板的中心坐标
*
* @return
* @throws IOException
*/
public int[] getHalmaAndBoardXYValue(File currentImage) throws IOException{
BufferedImage bufferedImage = ImageIO.read(currentImage);
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
System.out.println("宽度:" + width + ",高度:" + height);
int halmaXSum = 0;
int halmaXCount = 0;
int boardX = 0;
//从截屏从上往下逐行遍历像素点,以棋子颜色作为位置识别的依据,最终取出棋子颜色最低行所有像素点的平均值,即计算出棋子所在的坐标
for (int y = gameScoreBottomY; y < height; y++){
for (int x = 0; x < width; x++){
processRGBInfo(bufferedImage, x, y); //获取指定坐标的RGB值
int rValue = this.rgbInfo.getRValue();
int gValue = this.rgbInfo.getGValue();
int bValue = this.rgbInfo.getBValue();
//根据RGB的颜色来识别棋子的位置,
if (rValue > 50 && rValue < 60 && gValue > 53 && gValue < 63 && bValue > 95 && bValue < 110){
halmaXSum += x;
halmaXCount++;
}
}
}
if (halmaXSum != 0 && halmaXCount != 0){
//棋子底行的X坐标值
int halmaX = halmaXSum / halmaXCount;
//从gameScoreBottomY开始,获取棋子的x坐标
for (int y = gameScoreBottomY; y < height; y++){
processRGBInfo(bufferedImage, 0, y); //每行第一个像素的RGB(获取指定坐标的RGB值 )
int lastPixelR = this.rgbInfo.getRValue();
int lastPixelG = this.rgbInfo.getGValue();
int lastPixelB = this.rgbInfo.getBValue();
//只要计算出来的boardX的值大于0,就表示下个跳板的中心坐标X值取到了。
if (boardX > 0){
break;
}
int boardXSum = 0;
int boardXCount = 0;
for (int x = 0; x < width; x++){
processRGBInfo(bufferedImage, x, y);
int pixelR = this.rgbInfo.getRValue();
int pixelG = this.rgbInfo.getGValue();
int pixelB = this.rgbInfo.getBValue();
//处理棋子头部比下一个跳板还高的情况 跳过棋子所在的x轴区域不遍历(防止检索到棋子的颜色)
if (Math.abs(x - halmaX) < halmaBodyWidth){ //abs绝对值
continue;
}
//从上往下逐行扫描至下一个跳板的顶点位置,下个跳板可能为圆形,也可能为方框,取多个点,求平均值
if ((Math.abs(pixelR - lastPixelR) + Math.abs(pixelG - lastPixelG) + Math.abs(pixelB - lastPixelB)) > 8){
boardXSum += x;
boardXCount++;
}
}
if (boardXSum > 0){
boardX = boardXSum / boardXCount;
}
}
if (boardX > 0){
int[] result = new int[2];
//棋子的X坐标
result[0] = halmaX;
//下一块跳板的X坐标
result[1] = boardX;
return result;
}
}
return null;
}
/**
* 执行命令
*
* @param command
*/
private void executeCommand(String command){
Process process = null;
try{
process = Runtime.getRuntime().exec(command);
System.out.println("exec command start: " + command);
process.waitFor();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line = bufferedReader.readLine();
if (line != null){
System.out.println(line);
}
System.out.println("exec command end: " + command);
}
catch (Exception e){
e.printStackTrace();
}
finally{
if (process != null){
process.destroy();
}
}
}
/**
* ADB获取安卓截屏
*/
public void executeADBCaptureCommands(){
for (String command : ADB_SCREEN_CAPTURE_CMDS)
{
executeCommand(command);
}
}
/**
* 跳一下
*
* @param distance
*/
public void doJump(double distance){
System.out.println("distance: " + distance);
//计算按压时间,最小200毫秒
int pressTime = (int) Math.max(distance * pressTimeCoefficient, 200);
System.out.println("pressTime: " + pressTime);
//执行按压操作
String command = String.format("adb shell input swipe %s %s %s %s %s", swipeX, swipeY, swipeX, swipeY,
pressTime);
System.out.println(command);
executeCommand(command);
System.out.println();
System.out.println();
}
/**
* 再来一局
*/
private void replayGame(){
String command = String.format("adb shell input tap %s %s", swipeX, swipeY); //adb shell模拟点击事件 input
executeCommand(command);
}
/**
* 检查是否需要重新开局
*/
public void checkDoReplay(){
if (imageLength[0] > 0 && imageLength[0] == imageLength[1] && imageLength[1] == imageLength[2]
&& imageLength[2] == imageLength[3] && imageLength[3] == imageLength[4]){
//此时表示已经连续5次图片大小一样了,可知当前屏幕处于再来一局
Arrays.fill(imageLength, 0);
//模拟点击再来一局按钮重新开局
replayGame();
}
}
/**
* 获取指定坐标的RGB值
*
* @param bufferedImage
* @param x
* @param y
*/
private void processRGBInfo(BufferedImage bufferedImage, int x, int y){
this.rgbInfo.reset();
int pixel = bufferedImage.getRGB(x, y);
//转换为RGB数字
this.rgbInfo.setRValue((pixel & 0xff0000) >> 16);
this.rgbInfo.setGValue((pixel & 0xff00) >> 8);
this.rgbInfo.setBValue((pixel & 0xff));
}
/************************** RGBInfo ****************************/
class RGBInfo{
private int RValue;
private int GValue;
private int BValue;
public int getRValue(){
return RValue;
}
public void setRValue(int rValue){
RValue = rValue;
}
public int getGValue(){
return GValue;
}
public void setGValue(int gValue){
GValue = gValue;
}
public int getBValue(){
return BValue;
}
public void setBValue(int bValue){
BValue = bValue;
}
public void reset(){
this.RValue = 0;
this.GValue = 0;
this.BValue = 0;
}
}
}
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome/css/font-awesome.min.css"/>
<script src="https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget/autoload.js"></script>