单机 手游绘制教程,包含矩阵算法、内存读取、ceserver等,无需root,不含手机端绘图
本系列教程最终目标:某国外单机手游的敌人血量只有血条,不显示数字,为了制定更好的作战计划,给敌人身上加个血量数字~编程语言: c、 java(负责绘图,可以用其他语言替代)
学习本套教程无需root和虚拟机,但是需要一点点C语言基础,
教程不包含手机端绘图部分,绘图程序运行在电脑,这是为了测试起来方便。
内容比较基础,大牛不喜勿喷,搞教程真不容易,而且这套教程纯属是为爱发电,
本人水平有限,教程仅供参考,如果发现错误,欢迎指出~~
教程经过剪辑+配音了,有些地方可能有一点音画不同步,需要多结合上下文;字幕会尽快补上。
最终效果
https://static.52pojie.cn/static/image/hrline/4.gif
在线观看:
单机安卓手游分屏绘制教程01: 使用java swing创建透明窗口 https://www.bilibili.com/video/BV1wr4y1y7WX?spm_id_from=333.999.0.0
单机安卓手游分屏绘制教程02: 使用ndk编译c语言生成可执行程序
https://www.bilibili.com/video/BV1xT4y197oL?spm_id_from=333.999.0.0
单机安卓手游绘制教程03: 安卓c语言socket udp发送数据
https://www.bilibili.com/video/BV16S4y1978J?spm_id_from=333.999.0.0
单机安卓手游绘制教程04-1: java字节转整数型和浮点数的方法
https://www.bilibili.com/video/BV1Nr4y1Q79f?spm_id_from=333.999.0.0
单机安卓手游绘制教程04-2: java端接收socket udp数据
https://www.bilibili.com/video/BV1HL4y1p73K?spm_id_from=333.999.0.0
教程05-1:免root使用ceserver桥接真机的准备工作
https://www.bilibili.com/video/BV14R4y147gv?spm_id_from=333.999.0.0
教程05-2:CE查看安卓手机内存中的数据(ceserver桥接教程)
https://www.bilibili.com/video/BV1444y1h73y?spm_id_from=333.999.0.0
单机游戏绘制教程06-1: 投影矩阵,视图矩阵,NDC坐标
https://www.bilibili.com/video/BV1GP4y1G7KQ?spm_id_from=333.999.0.0
单机游戏绘制教程06-2: 用IDA快速找到矩阵基址偏移(国外单机安卓手游《坦克英雄激光战争》)
https://www.bilibili.com/video/BV18a411r714?spm_id_from=333.999.0.0
单机游戏绘制教程06-3: 用矩阵将敌人3D坐标转为NDC坐标
https://www.bilibili.com/video/BV1o44y1E7Wc?spm_id_from=333.999.0.0
单机安卓手游绘制教程07:读写内存思路,以及如何通过maps文件获取模块地址
https://www.bilibili.com/video/BV1rM4y1c7Gr?spm_id_from=333.999.0.0
单机安卓手游绘制教程08-1:用读取文件的函数跨进程读内存
https://www.bilibili.com/video/BV1Hi4y1R7By?spm_id_from=333.999.0.0
单机安卓手游绘制教程08-2:C语言 投影矩阵乘视图矩阵 ,以及通过socket udp发送矩阵数据
https://www.bilibili.com/video/BV1Eq4y117nN?spm_id_from=333.999.0.0
单机游戏绘制教程09:矩阵算法,敌人坐标转屏幕坐标;测试绘制地图的四条边
https://www.bilibili.com/video/BV1ZS4y1K7c6?spm_id_from=333.999.0.0
https://static.52pojie.cn/static/image/hrline/4.gif
教程10是获取敌人数组基址偏移,主要演示下IDA基础操作,这个不是视频教程(因为制作视频实在太累了),有些长,稍后会发到移动安全区,
教程11是绘图,仍然是文字教程,直接搬运到本帖子吧:
单机手游绘制教程11-1:定义socket通信格式 :
这期先简单谈谈,如何定义通信数据的结构。最舒服的方式肯定是java端用jni,两边都定义结构体,这样很直观,也方便解析数据,但jni也有一些学习成本,前面已经提到大小端序、字节转int和float的方法了,那绘图端还是手动解析字节吧。另外,本人C语言编程习惯不是很好(比如函数、变量命名,小驼峰和下划线随缘,etc),代码格式仅供参考。
首先是64字节的矩阵,以及4字节的敌人数量,接下来就考虑每个敌人需要占用多少字节了,本系列教程主题是绘制血量数字,所以要有8字节表示当前血量和最大血量,要绘制血量,还要有12字节的坐标(实际上坐标只需要x和z,这个游戏的y总是0,多传几个字节也无妨)。我打算顺便画个方框,那么就要有敌人的大小,因为坦克看起来似乎都是正方体,4字节就够了(这个后面再说)。还可以加一个结构体地址,这样方便将来对比数据找其他功能。
所以,每绘制一帧,需要传输64+4+(8+12+4)*n字节,接下来先通过C语言实现,定义一个结构体,如图:
struct AITank{
int curHp;
int maxHp;
float x;
float y;
float z;
float size;
};
struct{
float matrix;
int count; //敌人当前数量
struct AITank obj; //前面分析过,场景中最多有50个敌人
} draw_data;
然后如图所示,把前面写过的calc_matrix函数改一改:
main函数,把sendto的参数改一改,如图:
test函数注释掉,新增一个readAITank,作用类似于calc_matrix,负责往draw_data里面塞数据。
void readAITank(){
// 某一级地址在每次打开后就不会再变了,可以定义成全局变量,但我不想再扣这些细节了
uintptr_t gmc = readPtr(so_addr + 0x689CC);
uintptr_t p1 = readPtr( gmc + 0x28 * readPtr(gmc + 0x120) + 0x20 );
uintptr_t p2 = readPtr( p1 + 0x20 );
uintptr_t p3 = readPtr( p2 + 0xB890 );
uintptr_t p4 = readPtr( p3 + 0x20 );
int count = readInt(p4 + 0x14);
int i = 0, index = 0;
while( i < count){
uintptr_t p5 = p4 + 0x2d4 * i ;
if( readByte(p5 + 0x48) ){
draw_data.obj.x = readFloat(p5 + 0x19c);
draw_data.obj.y = readFloat(p5 + 0x19c + 4);
draw_data.obj.z = readFloat(p5 + 0x19c + 8);
draw_data.obj.size = readFloat(p5 + 0x40 + 0x214);
draw_data.obj.curHp = readInt(p5 + 0x40 + 0x4);
draw_data.obj.maxHp = readInt(p5 + 0x40 + 0x258);
index++;
}
i++;
}
draw_data.count = index;
}
C代码差不多了,接下来搞接收端。
ds.receive(dp);
int count = byteToInt(b, 64);
System.out.println(count);
for(int i = 0 ; i < count ; i++) {
int offset = 68 + i * 24;
int curHp = byteToInt(b, offset);
int maxHp = byteToInt(b, offset + 4);
float x = Float.intBitsToFloat(byteToInt(b, offset + 8));
float y = Float.intBitsToFloat(byteToInt(b, offset + 12));
float z = Float.intBitsToFloat(byteToInt(b, offset + 16));
float size = Float.intBitsToFloat(byteToInt(b, offset + 20));
System.out.printf("hp=%d/%d, pos=[%.2f, %.2f, %.2f], size=%.2f\n", curHp, maxHp, x, y, z, size);
}
成功接收到数据的效果图
单机手游绘制教程11-2:java绘制方框 :
血条没啥好说的,drawLine就完事。游戏本身提供血条了,再画一个显得多余,咱把数字给搞出来就行了。
先把坐标搞明白,咱获取的坐标是一个点,而敌人身上的点多得是。如果获取坐标就是敌人坐标,而不是什么骨骼、武器坐标,那么通常就两种,xyz表示敌人这个立体图形的正中心,要么就是表示敌人脚下平面的正中心,本教程的T游戏是后者这种。
那么如果画一个点上去,这个点就绘制在坦克这个立方体的底面的正中心,前面已经得到了坦克的尺寸size(经过测试,差不多是坦克高度,高度差不多是边长的一半),那么(x,y+size,z)表示坦克立方体的顶面的正中心的点坐标,(x,y,z)和(x,y+size,z)这两个点和矩阵运算得到的结果,符合“近大远小(指的是敌人远近、方框的大小)”,可以用作方框的顶边和底边的屏幕y坐标,再根据敌人的长宽比例,比如FPS的敌人宽高比差不多是0.5,本游戏的宽高比差不多是2,那么结合根据底边中心和顶边中心这两个点计算出的两个屏幕坐标的y坐标的差值,就可以得到方框的宽度,然后再通过这两个点的x坐标就可以得到方框的左边和右边的屏幕x坐标。
经常能看到有人问为啥偏框,情况很多,下图这种比较常见(红色框),框框尺寸没问题,但是总是偏高半个人,说明找到的坐标是敌人中心点坐标,可以读取出这个xyz坐标后,给高度(大多数是y坐标)减去半个人物高度的值就好了。。。总之就是一件事,要用头顶的xyz和脚底的xyz一起和矩阵去运算,才能分别得到头顶在屏幕上的坐标,和脚底在屏幕的坐标....另外,头顶脚底的x和z坐标通常相等,这里可以减少一些运算(本文没体现出来,我不在乎这点运算量...)
这是最常见的画方框思路,画一个方框没啥意思,这里干脆搞个3D框吧,注意接下来这个3D框和敌人朝向无关,角度只和相机有关,
如图,我们需要8个平面坐标系上的点,说白了就是8个3D坐标点转成的8个屏幕上的x,y点,那么我们已经知道底面的中心点是(x,y,z),对于本系列教程的T游戏,也就是(x,0,z),1~4这四个点就是(x-size, 0, z-size), (x-size, 0, z+size), (x+size, 0, z-size), (x+size, 0, z+size),5~8这四个点3d坐标是(x-size, size, z-size), (x-size, size, z+size), (x+size, size, z-size), (x+size, size, z+size)。
如果想搞和朝向相关的3D框,获取到敌人旋转角,把这8个点都绕着y轴旋转这个角度就行了,实际上和y轴没啥关系(因为是旋转角),可以参考这篇:https://blog.csdn.net/sinat_33425327/article/details/78333946
再顺便提一句吧,我前面视频说y表示高度,这是通常情况,比如本游戏,以及unity引擎的游戏。UE4就比较特殊,(好像是)用z表示高度,我也很无语...
理论说完了,其实等于没说,也没啥可说的,接下来搞代码。
首先搞一个类,用来封装数据,这种不到200行的小项目直接弄个静态内部类就行了,为了省事,全都搞成static,
矩阵乘坐标会频繁调用,把3d坐标转屏幕坐标进行封装:
算了,直接发了吧,毕竟java端代码不像C语言端那么敏感,也不能直接套在其他游戏:
package main;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JRootPane;
import javax.swing.WindowConstants;
public class Main extends JFrame{
private static final long serialVersionUID = 1L;
private static JComponent view;
// private static int[][] screen = new int;
static class Role{
public int curHp;
public int maxHp;
public float size;
public float x;
public float y;
public float z;
//screen~screen是底面四个点的屏幕坐标,screen~screen是顶面四个点屏幕坐标
public int[][] screen = new int;
// (x,y+size+2,z)的坐标对应的屏幕坐标,血量画在这个位置
public int[] hpPos = new int;
}
static Role[] role = new Role; //最多50个敌人
static int count = 0; //敌人数量
public Main() {
for(int i = 0 ; i < 50 ; i ++) {
role = new Role();
}
this.setSize(800, 600);
this.setTitle(" 赶码人");
this.getRootPane().setWindowDecorationStyle(JRootPane.FRAME);
this.setUndecorated(true);
this.setOpacity(0.5f);
// this.setBackground(new Color(0,0,0,0));
this.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
view = new JComponent() {
private static final long serialVersionUID = 1L;
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D)g;
g2d.setStroke(new BasicStroke(2));
g2d.setColor(Color.RED);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
//抗锯齿
g2d.setFont(new Font("宋体", Font.PLAIN, 18));
g2d.drawString(" 赶码人", 30, 30);
for(int i = 0 ; i < count ; i ++) {
g2d.setColor(Color.RED);
g2d.drawLine(role.screen, role.screen, role.screen, role.screen);
g2d.drawLine(role.screen, role.screen, role.screen, role.screen);
g2d.drawLine(role.screen, role.screen, role.screen, role.screen);
g2d.drawLine(role.screen, role.screen, role.screen, role.screen);
g2d.drawLine(role.screen, role.screen, role.screen, role.screen);
g2d.drawLine(role.screen, role.screen, role.screen, role.screen);
g2d.drawLine(role.screen, role.screen, role.screen, role.screen);
g2d.drawLine(role.screen, role.screen, role.screen, role.screen);
g2d.drawLine(role.screen, role.screen, role.screen, role.screen);
g2d.drawLine(role.screen, role.screen, role.screen, role.screen);
g2d.drawLine(role.screen, role.screen, role.screen, role.screen);
g2d.drawLine(role.screen, role.screen, role.screen, role.screen);
String hp = role.curHp + "/" + role.maxHp;
g2d.setColor(Color.YELLOW);
g2d.drawString(hp, role.hpPos - 20, role.hpPos);
}
}
};
this.add(view);
this.setVisible(true);
}
public static void main(String[] args) {
new Main();
byte[] b = new byte;
try(DatagramSocket ds = new DatagramSocket(6666)) {
DatagramPacket dp = new DatagramPacket(b, b.length);
float[] matrix = new float;
while(true) {
ds.receive(dp);
for(int i = 0 ; i < 16; i++) {
matrix = Float.intBitsToFloat(byteToInt(b, i * 4));
}
count = byteToInt(b, 64);
// System.out.println(count);
for(int i = 0 ; i < count ; i++) {
int offset = 68 + i * 24;
role.curHp = byteToInt(b, offset);
role.maxHp = byteToInt(b, offset + 4);
role.x = Float.intBitsToFloat(byteToInt(b, offset + 8));
role.y = Float.intBitsToFloat(byteToInt(b, offset + 12));
role.z = Float.intBitsToFloat(byteToInt(b, offset + 16));
role.size = Float.intBitsToFloat(byteToInt(b, offset + 20));
// System.out.printf("hp=%d/%d, pos=[%.2f, %.2f, %.2f], size=%.2f\n", curHp, maxHp, x, y, z, size);
calcScreenPos(matrix, role.x, role.y + role.size + 2, role.z, role.hpPos);
calcScreenPos(matrix,
role.x - role.size,
role.y,
role.z + role.size,
role.screen);
calcScreenPos(matrix,
role.x - role.size,
role.y,
role.z - role.size,
role.screen);
calcScreenPos(matrix,
role.x + role.size,
role.y,
role.z - role.size,
role.screen);
calcScreenPos(matrix,
role.x + role.size,
role.y,
role.z + role.size,
role.screen);
calcScreenPos(matrix,
role.x - role.size,
role.y + role.size,
role.z + role.size,
role.screen);
calcScreenPos(matrix,
role.x - role.size,
role.y + role.size,
role.z - role.size,
role.screen);
calcScreenPos(matrix,
role.x + role.size,
role.y + role.size,
role.z - role.size,
role.screen);
calcScreenPos(matrix,
role.x + role.size,
role.y + role.size,
role.z + role.size,
role.screen);
}
// System.out.println();
view.repaint();
}
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void calcScreenPos(float[] matrix, float x, float y, float z, int[] screenPos) {
float res0 = matrix * x + matrix * y + matrix * z + matrix;
float res1 = matrix * x + matrix * y + matrix * z + matrix;
// res = matrix * x + matrix * y + matrix * z + matrix;
float res3 = matrix * x + matrix * y + matrix * z + matrix;
float ndcX = res0 / res3;
float ndcY = res1 / res3;
// float ndcZ = res / res;
screenPos = (int) ((ndcX + 1) / 2f * view.getWidth());
screenPos = (int) ( (1- (ndcY + 1) / 2.0) * view.getHeight());
}
public static int byteToInt(byte[] b, int index) {
int i = b;
i = i << 8;
i = i | (b & 0xff);
i = i << 8;
i = i | (b & 0xff);
i = i << 8;
i = i | (b & 0xff);
return i;
}
}
https://static.52pojie.cn/static/image/hrline/4.gif
C代码不提供了,也不多,照着视频写就可以
另外,本人对这款游戏的理解就是和植物大战僵尸1差不多(游戏玩家不多,逆向玩家多,属于是适合用于学习逆向、编程的这类游戏),
前几个月在他们官网看到他们开发团队把开发重心放到了PC端,该游戏很多年没动静了,在国内也非常冷门,所以我觉得用于逆向研究的风险不大,(本系列教程有风险的地方还请指出,我积极整改)
最后,请不要用这些教程的内容实施到其他游戏,本系列教程仅供提高对逆向和编程的兴趣。
lzl1983 发表于 2022-2-11 07:13
看着不错的样子辛苦了虽然我不懂
哈哈哈哈哈哈我也是 这个我等只能看看而已
感觉太复杂了 感谢大佬分享 等你的第10集了{:301_1003:} qqcs6 发表于 2022-2-1 18:21
等你的第10集了
感谢支持,审核中: IDA找敌人数组基址偏移(单机手游,新手向) 这个厉害了大佬
感谢大佬分享 太全了.感谢感谢. 感谢大佬分享,学习了 太棒了,谢谢大佬无私贡献