开发前言
很多游戏都存在一些重复性操作,而脚本可以帮助我们完成这些繁琐的机械操作。脚本分为前台脚本和后台脚本。前台脚本需要操作鼠标和键盘,虽然编码简单,但用户也无法做其他事情了。后台脚本需要后台截图,向游戏窗口发送鼠标或键盘事件,功能相对复杂,但用户可以边挂脚本边做其他事情。当然,市面上还存在一些脱机脚本,它们只服务于某个游戏,需要开发者破解游戏包的加密算法,难度比较高,但这些脚本能给用户带来最优质的体验,能快速完成各种复杂的任务!
脚本思路
脚本需要先获取游戏窗口的图像,然后对这些图像做分析,最后做出对应的鼠标或键盘响应
前台脚本的实现
以下将以代码的方式,实现一个简单的前台脚本,算是抛砖引玉了
private Robot robot = new Robot();
/**
* 获取桌面截图
*/
public static BufferedImage getDesktopImage() {
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
Rectangle screenRectangle = new Rectangle(screenSize);
return robot.createScreenCapture(screenRectangle);
}
/**
* 获取当前鼠标位置
*/
public static Point getMousePoint() {
return MouseInfo.getPointerInfo().getLocation();
}
/**
* 获取指定位置的颜色
*/
public static Color getPointColor(Point point) {
return robot.getPixelColor(point.x, point.y);
}
/**
* 单击桌面的指定位置
*/
public static void mouseClick(int x, int y, long pressTime) {
try {
// 需要调用多次才能移动到正确位置
for (int i = 0; i < 6; i++) {
robot.mouseMove(x, y);
}
robot.mousePress(InputEvent.BUTTON1_MASK);
// 这里设置一下鼠标按下与释放的间隔
// 这里需要模拟人的单击间隔, 推荐随机值[100-800]
// 防止游戏公司检测到脚本行为, 然后号被封了
// 鼠标点击的位置最好也要随机, 一直点同一个位置容易被抓
Thread.sleep(pressTime);
robot.mouseRelease(InputEvent.BUTTON1_MASK);
} catch (Exception e) {
e.printStackTrace();
}
}
通过以上代码,我们可以拿到屏幕图像,然后做复杂的图像识别,也可以做简单的取色处理,这里介绍一下取色处理,我们拿到图像后,只对某个像素点做取色,比如【点击挑战】按钮位置(100,100)的颜色可能是黄色或者灰色,如果是黄色则允许点击。我们只需要获取该位置的颜色,即可判断出是否需要点击这个按钮
/**
* 颜色匹配, color1通过截图取色获得, color2是当前桌面的某点颜色
*/
public static boolean needMouseClick(Color color1, Color color2) {
int R1 = color1.getRed(), G1 = color1.getGreen(), B1 = color1.getBlue();
int R2 = color2.getRed(), G2 = color2.getGreen(), B2 = color2.getBlue();
// 游戏是实时渲染的, 假设游戏的某个点在正常情况下是纯白色(255,255,255)
// 有时候色素会存在偏差, 比如渲染成(254,255,255)
// 虽然人眼看不出颜色发生改变, 但计算机可以察觉到颜色变化
// 所以这里设置了一个偏差值5, 确保两种颜色能完全匹配上
return Math.abs(R1 - R2) < 5 && Math.abs(G1 - G2) < 5 && Math.abs(B1 - B2) < 5;
}
后台脚本的实现
第一步,需要导入以下依赖,该依赖的某些底层指令需要IDEA以管理员身份运行
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>5.12.1</version>
</dependency>
第二步,实现一切后台操作都需要先获取到游戏窗口的句柄【HWND】
private static final User32 user32 = User32.INSTANCE;
/**
* 返回当前正在激活的窗口句柄
*/
public static HWND getActiveHwnd() {
return user32.GetForegroundWindow();
}
/**
* 根据窗口标题返回窗口句柄
*/
public static HWND getHwnd(String title) {
return user32.FindWindow(null, title);
}
/**
* 返回当前全部窗口信息
*/
public static List<DesktopWindow> getAllWin() {
return WindowUtils.getAllWindows(true)
.stream()
.filter(win -> !win.getTitle().equals(""))
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(DesktopWindow::getTitle))), ArrayList::new));
}
第三步,后台操作API如下
/**
* 返回窗口标题
*/
public static String getWinTitle(HWND hwnd) {
return WindowUtils.getWindowTitle(hwnd);
}
/**
* 返回窗口的坐标和尺寸
*/
public static Rectangle getWinSize(HWND hwnd) {
return WindowUtils.getWindowLocationAndSize(hwnd);
}
/**
* 移动窗口
*/
public static void moveWin(HWND hwnd, int x, int y, int w, int h) {
// 设置指定窗口的显示状态
user32.ShowWindow(hwnd, 1);
// 激活指定窗口
user32.SetForegroundWindow(hwnd);
// 移动指定窗口的位置
user32.MoveWindow(hwnd, x, y, w, h, true);
}
/**
* 截取窗口, 无法截取以下情况的窗口
* 1. 窗口最小化
* 2. Open GL渲染
* 3. Direct X引擎
*/
public static BufferedImage getWinImage(HWND hwnd) {
return GDI32Util.getScreenshot(hwnd);
}
/**
* 向窗口发起点击单击鼠标事件
*/
public static void mouseClick(HWND hwnd, int x, int y, long pressTime) {
try {
WinDef.LPARAM param = new WinDef.LPARAM(x + ((long) y << 16));
user32.PostMessage(hwnd, 513, new WinDef.WPARAM(513), param);
Thread.sleep(pressTime);
user32.PostMessage(hwnd, 514, new WinDef.WPARAM(514), param);
} catch (Exception e) {
e.printStackTrace();
}
}
这里讲一下简单的图像识别思路:取两张宽高相同的图形做对比,对比每个像素点的颜色,只要有一个像素点颜色不同,则认为图像不同。这种思路是与之前的取色处理类似,虽然简单,但是精确率不高,无法识别动态图像。目前常见的JAVA图像识别框架是Open CV,使用它需要安装额外的软件,它的相关文档也很全,如果不想安装额外软件,也可以用Java CV,只需要引入它的依赖包即可,它封装了Open CV和其他组件,缺点是依赖包非常大,相关文档很少
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.7</version>
</dependency>
最后结语
本人制作了一个简易的脚本,成品截图和项目源码如下