• 基于C++ Qt实现的红色警戒3修改器


    前言

    这部修改器制作有一段时间了,但是一直没出教程。今天利用周末空闲写篇教程,给后来者指路的同时也加深自己对游戏修改器的理解,大佬就随便看看吧

    浏览了一下网络,形形色色的单机游戏修改器教程,但是基本只实现了一到两个功能,GUI图形界面也没有。网站上能下载到的实现很多功能的修改器却又不开源,对新手不够友好

    为什么选择红警3而不是其他游戏呢?

    其一,它是单机游戏,制作网络游戏修改器(外挂)是违法的,根据《计算机信息网络国际联网安全保护管理办法》第六条规定:“任何单位和个人不得从事下列危害计算机信息网络安全的活动",尤其不能制作网游外挂并拿它去盈利

    我不知道做网游外挂开源算不算违法,总之,与违法沾边的事我们别去触碰

    其二,是一种情结,我玩的第一部真正意义上的游戏是红色警戒2尤里的复仇(扫雷,三维弹球不算),那时候是2002年,我在上一年级,接触到这种RTSG游戏是爱不释手,从那时起,我就想成为一名游戏开发工程师,然而现在并不是

    其三,画面还可以,本来想做红警2外挂的,奈何画面太老,观赏性差

    其四,难度适中,网上有很多外挂入门教程是按照植物大战僵尸这款游戏制作的,难度过于入门,基址偏移量太少,偏移一般是直接偏移,只能说是小游戏,稍微大点的单机游戏,基址偏移次数可能会超过10次,比如在红色警戒3中,基址偏移次数最多达到9次,并且偏移量有坑等我们踩

    最后一点,红警3在单机游戏中具有很强的代表性,只要学会了制作该外挂,其他单机游戏外挂原理是一样的

    答疑解惑:

    Q:这是脚本吗?

    A:不是,这是通过修改内存,改变指定地址操作数实现的修改器,通俗地说,可以直接改变游戏数据。我写过简单的回合制脚本,请参考这篇博文

    Q:这种外挂可以在红警3联机的时候使用吗?

    A:不行,仅限于单人模式

    Q:这款外挂支持的红警3版本

    A:红色警戒3原版Version 1.00

    已完成的GUI(基于C++Qt5.7)如下,支持中英德三种语言;同时,我为萌新准备了C++实现的控制台版,不需要了解Qt即可实现本教程外挂的功能

    开发环境

    C++11

    Qt 5.7 mingw53_32(控制台版不需要)

    工具

    QtCreator:制作Qt图形界面所需的开发工具,支持C++库

    Visual Studio(版本最好2010以后):为控制台版而准备

    Cheat Engine:寻找游戏基址所使用的工具,找基址的过程是枯燥乏味的,不用担心,我们有现成的基址大全

    红色警戒3原版V1.00:逗游上可以下载

    汇编知识准备

    我只讲解制作该外挂过程中需要用到的汇编知识,不展开叙述。扩展知识园友可以自己去了解下

    汇编语言MOV指令:

      基本传送指令,Movement,把源操作数传送到目的操作数中

      如MOV EAX 1000H,将十六进制数1000H传送给EAX(累加器)

    寻址方式:

      说明操作数所在地址的方法,有若干种寻址方式,不展开叙述,我们主要用到寄存器相对寻址

    寄存器相对寻址:

      有效地址 = 基址+变址+位移量

      操作数的有效地址为基址寄存器(EBX)或变址寄存器(ESI或者EDI)的内容和指令中指定的位移量之和

      如MOV ESI,[EDI + 00000768]

    游戏基址:

      也叫作基地址,顾名思义就可以理解为基本地址,他是相对偏移量的计算基准
      在实模式下,通常都是以段+偏移来定位地址,因此说,这时,段地址是基地址的一种  

      "----->"表示"指针指向"

      基址(存放的内容是一级基址起始地址)——>一级基址(存放的内容是二级基址的起始地址:假定为a)

      [一级基址(a) + 偏移量]------>二级基址(存放的内容是三级基址的起始地址:假定为b);

      [二级基址(b)+偏移量]-------->三级基址

      ······

      n级基址-------->游戏界面

      自己制作游戏修改器必须要找到一级基址

         Cheat Engine的思路是根据偏移量从n级基址逆向找到一级基址

    寻找基址

    右键图标属性,添加“ -win -xres 1024 -yres 768”参数,1024 768是分辨率,自由指定你的分辨率,窗口启动红警3,方便调试

    不知为何,我这台电脑窗口运行红警3写入内存的时候时常崩溃,所以我用的全屏启动

    打开Cheat Engine,打开游戏进程

    进入游戏

    以查找金钱基址为例

    搜索金钱数字10000,点击First Scan

    搜索到一系列值,此时改变游戏金钱,建造建筑或单位

    输入改变后的金钱9200,再点击Next Scan,可以看到只有四个地址,缩小了范围

     

     修改它们的Value,看哪个是真的地址,结果第三个是真的(不一定都是第三个,每次搜索都不一样,切勿教条)

    游戏金钱发生了变化

     对下方选中的地址,按右键选中Find out what writes to this address

    出现Confirmation,选yes

    再次改变游戏的金钱,监测到MOV指令

    双击进入MOV指令,其他操作一概不看,只看标红字的MOV指令

    我们来分析下这些内容

    EAX=(0001675B)16 = (91995)10

    EAX的值就是游戏中的金钱,是所谓的操作数

    MOV[ESI+04],EAX

    将EAX的值传送到以"ESI+04"为地址的内存区域

    以"ESI+04"为地址的内存区域指向EAX

    CE提示了我们,地址可能是ESI,记下ESI(源变址寄存器)的地址和偏移量04

     

    输入ESI的十六进制地址值,勾上Hex,New Scan->First Scan

     

    只搜索到一个地址,对其右键进入"Find out what accesses this Address",再次改变游戏金钱

    MOV EAX,[ECX+EAX*4]

    EAX*4的结果会非常的大,CE提示的地址和上一步一样,陷入了循环。似乎我们在这就要止步不前了

    其实EAX = 0,0*4=0,所以偏移量为0

    不经我们要思考,为什么EAX=0?

    我用OllyDbg启动红警3,断点调试到这一步,请看右侧变量,EAX=00000000

    在红色警戒3中,但凡遇到偏移量由乘法组成,如[ECA+EAX*4],默认EAX为0就好了,不要被它吓到,不知道游戏开发商为什么这样子设计偏移量

    干嘛偏移量不直接+0?也许就是为了给我们设一道难题吧

    为了对新手足够友好,我不说OllyDbg,感兴趣的可以了解下

    所以这里EAX等于以ECX为地址的值,不需要用CE推荐的地址了,

    因为这里EAX=0,没有意义,记下来ECX的地址和偏移量0

    输入上步ECX的地址,搜索到一大堆结果,有点绝望,只能一个个试了,在这里没有技术含量,需要耐心

    对每个地址右键"Find out what accesses this Address",从上往下找,列举在上面的地址可能性最大,运气好第一个就是真实地址。记住,只看传送指令MOV

    果然,第一个是真实地址,记下ECX的地址和偏移量E4

    输入10C815A0,并搜索,又是一堆

    老规矩,"Find out what accesses this Address",在这记录下偏移量2C,我们看到ECX地址和上一步搜索的地址一致,陷入了循环

    弃用之,采用CE推荐的基址10CA2728。这里你就要自己做判断,一般地址为0,地址和上步骤是一样的,不能用他们,

    灵活地选用其他地址

    输入10CA2728搜索,可以看到绿色结果,此地址为一级基址,也就是静态基址,我们终于找到了

    在CE中手动添加基址来测试找到的基址是否正确,单机Add Address Manually,输入我们刚才寻找的偏移量和基址

     

    可以看到,一级基址经过四次偏移指向的地址,是五级地址,就是我们第一个扫描出来的地址

    现在我们来总结

    金钱的基址和偏移量如下

    [[[[00DFBD74]+2C]+e4]+0]+4

    [00DFBD74]是一个值,这个值不是00DFBD74,00DFBD74值存放的地址

    在高级语言C++中,可以理解为

    int *p;
    
    p=00DFBD74;
    
    *p=10CA2728;

    一级基址:

    [00DFBD74]=10CA2728

    二级基址:

    [一级基址]+偏移

    [10CA2728+2C]=10237DB0

    三级基址:

    [[一级基址]+偏移]+偏移

    [10237DB0+E4]=10C815A0

    四级基址:

    [[[一级基址]+偏移]+偏移]+偏移

    [10C815A0+0]=1169BBF0

    五级基址:

    [[[[一级基址]+偏移]+偏移]+偏移]+偏移

    1169BBF0+4=1169BBF4

    大功告成,找其他基址方法类似,不做赘述。

    我在找金钱基址花了四十分钟,在EAX=0那里卡了好久,被第二次的偏移量坑了,最后终于找到。

    找电力基址花了我将近一个小时才找到,所以我不建议大家在寻找基址上花费大量时间。在这里获取现成的基址

    我们应该把精力放在高级语言如何实现功能上

    C++重要函数详解

    ReadProcessMemory

    读取内存我们要用到ReadProcessMemory函数

    函数功能:该函数从指定的进程中读入内存信息,被读取的区域必须具有访问权限。

    函数原型:BOOL ReadProcessMemory(HANDLE hProcess,LPCVOID lpBaseAddress,LPVOID lpBuffer,DWORD nSize,LPDWORD lpNumberOfBytesRead);

    參数:

    hProcess:进程句柄

    lpBaseAddress:读出数据的地址

    lpBuffer:存放读取数据的地址

    nSize:读入数据的字节数

    lpNumberOfBytesRead:数据的实际大小

    WriteProcessMemory

    写入内存我们需要WriteProcessMemory函数

    BOOL WriteProcessMemory(HANDLE hProcess,LPVOID lpBaseAddress,LPVOID lpBuffer,DWORD nSize,LPDWORD lpNumberOfBytesWritten
    );

    参数:

    hProcess:由OpenProcess返回的进程句柄。如参数传数据为 INVALID_HANDLE_VALUE 【即-1】目标进程为自身进程

    lpBaseAddress:要写的内存首地址,在写入之前,此函数将先检查目标地址是否可用,并能容纳待写入的数据、

    lpBuffer:指向要写的数据的指针

    nSize:要写入数据的字节数

    lpNumberOfBytesWritten:写入数据的大小

     

    C++控制台版

    全部代码

    #include <atlstr.h>
    #include <Windows.h>
    #include <iostream>
    using namespace std;
    /*
        作者:Jonas
        时间:2018/11/17
    */
    //游戏基址1
    int g_nBaseAddr = 0x00DFBD74;
    //游戏基址2
    int g_otherBaseAddr = 0x00DEEA3C;
    //游戏句柄
    HANDLE g_hProcess;
    
    //根据基址计算出两次偏移后的地址
    int *get2Point(int g_nBaseAddr, int p1, int p2)
    {
        //iBase存储基地址指向的值,即iBase = [g_nBaseAddr]
        //iP1存储以iBase指向的值+偏移为地址所指向的值,即iP1 = [iBase]+p1
        //iP2存储最终地址
        int iBase, iP1, *iP2;
        if (!ReadProcessMemory(g_hProcess, (LPVOID)g_nBaseAddr, &iBase, 4, NULL))
        {
            return NULL;
        }
    
        if (!ReadProcessMemory(g_hProcess, (LPVOID)(iBase + p1), &iP1, 4, NULL))
        {
            return NULL;
        }
    
        //返回最终地址
        iP2 = (int *)(iP1 + p2);
        return iP2;
    }
    
    //根据基址计算出三次偏移后的地址
    int *get3Point(int g_nBaseAddr, int p1, int p2, int p3)
    {
        //原理同上,以此类推
        int iBase, iP1, iP2, *iP3;
    
        if (!ReadProcessMemory(g_hProcess, (LPVOID)g_nBaseAddr, &iBase, 4, NULL))
        {
            return NULL;
        }
    
        if (!ReadProcessMemory(g_hProcess, (LPVOID)(iBase + p1), &iP1, 4, NULL))
        {
            return NULL;
        }
    
        if (!ReadProcessMemory(g_hProcess, (LPVOID)(iP1 + p2), &iP2, 4, NULL))
        {
            return NULL;
        }
        iP3 = (int *)(iP2 + p3);
        return iP3;
    }
    
    //根据基址计算出四次偏移后的地址
    int *get4Point(int g_nBaseAddr, int p1, int p2, int p3, int p4)
    {
        ////原理同上,以此类推
        int iBase, iP1, iP2, iP3, *iP4;
    
        if (!ReadProcessMemory(g_hProcess, (LPVOID)g_nBaseAddr, &iBase, 4, NULL))
        {
            return NULL;
        }
    
        if (!ReadProcessMemory(g_hProcess, (LPVOID)(iBase + p1), &iP1, 4, NULL))
        {
            return NULL;
        }
    
        if (!ReadProcessMemory(g_hProcess, (LPVOID)(iP1 + p2), &iP2, 4, NULL))
        {
            return NULL;
        }
    
        if (!ReadProcessMemory(g_hProcess, (LPVOID)(iP2 + p3), &iP3, 4, NULL))
        {
            return NULL;
        }
        iP4 = (int *)(iP3 + p4);
        return iP4;
    }
    
    //改变电力
    void ModifyElectricity()
    {
        //获取电力所在地址
        int *pElec = get3Point(g_nBaseAddr, 0x2c, 0x74, 0x4);
        //将电力修改为目标值
        int nElecValue = 9999;
        //修改
        WriteProcessMemory(g_hProcess, pElec, &nElecValue, 4, NULL);
    }
    
    //修改策略值
    void ModifyStrategy()
    {
        //获取策略所在地址
        int *pStrategy = get3Point(g_nBaseAddr, 0x2c, 0x1320, 0x2c);
        //将策略修改为目标值
        //策略值类型为float
        float nElecStrategy = 4320;
        //修改
        WriteProcessMemory(g_hProcess, pStrategy, &nElecStrategy, 4, NULL);
    }
    
    //修改金钱
    void ModifyMoney()
    {
        //获取金钱所在地址
        int *pMoney = get4Point(g_nBaseAddr, 0x2c, 0xe4, 0x0, 0x4);
        //将金钱修改为目标值
        int nElecMoney = 11111;
        //修改
        WriteProcessMemory(g_hProcess, pMoney, &nElecMoney, 4, NULL);
    }
    
    //修改选取单位的大小
    //支持选择单个单位对大小进行修改,多选会导致错乱
    void ModifySizeOfUnit()
    {
        //获取单位大小所在地址
        int *pSizeOfUnit = get3Point(g_otherBaseAddr, 0x50, 0x8, 0x25c);
        //将单位大小修改为目标值
        float nElecSizeOfUnit = 2;
        //修改
        WriteProcessMemory(g_hProcess, pSizeOfUnit, &nElecSizeOfUnit, 4, NULL);
    }
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        //获取游戏窗口所在进程的进程ID,也就是PID
        HWND hWnd = FindWindow(NULL, TEXT("終極動員令:紅色警戒3"));
        if (NULL == hWnd)
        {
            cout<<"查找窗口失败"<<endl;
            getchar();
            return 0;
        }
    
        DWORD dwProcessId;
        GetWindowThreadProcessId(hWnd, &dwProcessId);
        cout<<"进程ID:"<<dwProcessId<<endl;
    
        //获取进程句柄
        g_hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
        if (NULL == g_hProcess)
        {
            cout<<"打开进程失败"<<endl;
            getchar();
            return 0;
        }
    
        ModifyElectricity();
        ModifyMoney();
        ModifyStrategy();
        ModifySizeOfUnit();
        getchar();
        return 0;
    }

    运行效果如下,黑灯瞎火

    打开游戏看看,金钱变成了11111,策略点加满,电力变成了9999,我选中的发电厂的大小是不是有些违和?

    图形界面Qt版

    好,接下来,进入重头戏

    ui界面布局设计如下

    要点解析

    初始化句柄

    运行前要初始化句柄和检测进程是否打开

    否则修改金钱、电力等方法句柄为空

    //查看当前游戏进程是否打开
    //初始化游戏句柄
    void Ra3Window::checkProcessState()
    {
        //获取游戏窗口所在进程的进程ID,也就是PID
            HWND hWnd = FindWindow(NULL, TEXT("終極動員令:紅色警戒3"));
            if (NULL == hWnd)
            {
                //qDebug()<<"查找窗口失败"<<endl;
                QMessageBox::information(this,"警告","未找到红色警戒3窗口");
            }
    
            DWORD dwProcessId;
            GetWindowThreadProcessId(hWnd, &dwProcessId);
            qDebug()<<"进程ID:"<<dwProcessId<<endl;
    
            //获取进程句柄
            g_hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
            if (NULL == g_hProcess)
            {
                QMessageBox::information(this,"警告","打开红色警戒3进程失败");
            }
    }

    Qt国际化

    在项目.pro添加

    TRANSLATIONS = Translate_EN.ts
                    Translate_CN.ts
                    Translate_DE.ts

    工具-外部-Qt语言家-更新翻译

    用以在项目根目录下生成刚才指定名称的ts文件

     

    打开Linguist

    打开ts文件,开始翻译吧。。。有道,德语助手各显神通。在译文出输入你的翻译内容

    翻译过后,点击文件-发布全部。会在项目根目录下生成qm文件

    代码中引用这些qm文件

    //语言comboBox触发
    void Ra3Window::on_comboBox_2_activated(int index)
    {
        switch(index)
        {
            case 0:
                m_Translator->load("./Translate_CN.qm");
                break;
            case 1:
                m_Translator->load("./Translate_EN.qm");
                break;
            case 2:
                m_Translator->load("./Translate_DE.qm");
                break;
            default :
                break;
        }
        qApp->installTranslator(m_Translator);
    }

    重写retranslateUi

    修复comboBox更换语言重置ui后不能保持原来选中的状态,意思是我在语言comboBox选中了英语,界面语言变化了,但是语言comboBox还是简体中文

    观察源码发现,该函数把comboBox清空了

    //重写retranslateUi
    //注释掉语言comboBox清空,修复语言状态错乱(只显示简体中文)
    //不建议直接修改源码,复制出来重写
    void Ra3Window::retranslateUi(QMainWindow *Ra3Window)
    {
        Ra3Window->setWindowTitle(QApplication::translate("Ra3Window", "Ra3Window", Q_NULLPTR));
        ui->label_3->setText(QApplication::translate("Ra3Window", "347255226347225245345212240346273241", Q_NULLPTR));
        ui->pushButton_3->setText(QApplication::translate("Ra3Window", "347253213345215263347224237346225210", Q_NULLPTR));
        ui->label_4->setText(QApplication::translate("Ra3Window", "351200211345217226345215225344275215", Q_NULLPTR));
        ui->comboBox->clear();
        ui->comboBox->insertItems(0, QStringList()
         << QApplication::translate("Ra3Window", "345260217", Q_NULLPTR)
         << QApplication::translate("Ra3Window", "346240207345207206", Q_NULLPTR)
         << QApplication::translate("Ra3Window", "345244247", Q_NULLPTR)
        );
        ui->pushButton_4->setText(QApplication::translate("Ra3Window", "347253213345215263347224237346225210", Q_NULLPTR));
        ui->textEdit->setHtml(QApplication::translate("Ra3Window", "<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
    "
    "<html><head><meta name="qrichtext" content="1" /><style type="text/css">
    "
    "p, li { white-space: pre-wrap; }
    "
    "</style></head><body style=" font-family:'SimSun'; font-size:9pt; font-weight:400; font-style:normal;">
    "
    "<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">350257264346230216</p>
    "
    "<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">351207221351222261357274232350276223345205245351207221351222261346225260351242235357274214345206215347202271345207273345217263344276247342200234347253213345215263347224237346225210342200235346214211351222256357274214345215263345217257350216267345276227346214207345256232347232204351207221351222261346"
                        "225260</p>
    "
    "<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">347224265345212233357274232350276223345205245347224265345212233345200274357274214345206215347202271345207273345217263344276247342200234347253213345215263347224237346225210342200235346214211351222256357274214345215263345217257350216267345276227346214207345256232347232204347224265345212233345200274</p>
    "
    "<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">347255226347225245345212240346273241357274232344270215351234200350246201346214207345256232357274214351273230350256244345212240346273241</p>
    "
    "<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">351200211345217226345215225344275215357"
                        "274232345205210351200211346213251344270200344270252345215225344275215357274214344270213346213211346241206351200211346213251345244247345260217357274214345206215347202271345207273345217263344276247342200234347253213345215263347224237346225210342200235346214211351222256357274214345217257344273245347234213345210260345215225344275215347232204345244247345260217345217230345214226357274233346240207345207206357274232346255243345270270345244247345260217343200202</p>
    "
    "<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
    "
    "<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
    "
    "<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0p"
                        "x; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
    "
    "<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
    "
    "<p align="right" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">345215232345256242345234260345235200357274232https://www.cnblogs.com/Java-Starter/</span></p>
    "
    "<p align="right" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">344273205344276233345255246344271240344272244346265201357274214344270245347246201345225206347224250</span></p>
    "
    "<p align="right" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">                   344275234350"
                        "200205357274232Jonas</span></p></body></html>", Q_NULLPTR));
        ui->label_5->setText(QApplication::translate("Ra3Window", "350257255350250200", Q_NULLPTR));
    //清空comboBox_2    ui->comboBox_2->clear();
    //    ui->comboBox_2->insertItems(0, QStringList()
    //     << QApplication::translate("Ra3Window", "347256200344275223344270255346226207", Q_NULLPTR)
    //     << QApplication::translate("Ra3Window", "350213261350257255", Q_NULLPTR)
    //     << QApplication::translate("Ra3Window", "345276267350257255", Q_NULLPTR)
    //    );
        ui->lineEdit->setPlaceholderText(QApplication::translate("Ra3Window", "350276223345205245351207221351222261346225260351242235", Q_NULLPTR));
        ui->label->setText(QApplication::translate("Ra3Window", "351207221351222261", Q_NULLPTR));
        ui->pushButton->setText(QApplication::translate("Ra3Window", "347253213345215263347224237346225210", Q_NULLPTR));
        ui->label_2->setText(QApplication::translate("Ra3Window", "347224265345212233", Q_NULLPTR));
        ui->lineEdit_2->setPlaceholderText(QApplication::translate("Ra3Window", "350276223345205245347224265345212233345200274", Q_NULLPTR));
        ui->pushButton_2->setText(QApplication::translate("Ra3Window", "347253213345215263347224237346225210", Q_NULLPTR));
    } // retranslateUi

    配置启动程序图标

    在项目根目录下新建ico.rc

    记事本编辑之

    IDI_ICON1 ICON   DISCARDABLE   "./images/ra3.ico"

    准备好ico图标

    在项目.pro加入

    OTHER_FILES += ico.rc
    RC_FILE += ico.rc

     Qt relase模式编译,即可看到生成图标

    Qt项目打包发布

    打包Qt会给我们项目加上依赖环境,使项目在其他电脑上也可运行

    园友可以试试不打包直接打开exe程序,会报各种dll找不到的错误

    打开Qt 5.7 for Desktop

    键入命令

    windeployqt RA3_Cheat.exe

    圆满完成,结束。

    Qt版运行效果

     

    修改金钱、电力,加满策略值,修改单位大小没什么用,就是好玩,可以改的非常大,设置10以上比将军刽子手还大,设置成0单位会“消失”

    Qt源码

    Qt源码在这里:https://github.com/cjy513203427/RedAlert3_Cheater

    哈哈,我怎么放在CSDN上呢?。。。

    直接可执行程序在/release目录下,打开RA3_Cheat.exe即可运行

    写得很累,希望新人看了我的教程就可以学会制作单机游戏外挂,知其然,知其所以然。大佬可以随便看看

  • 相关阅读:
    寒假学习记录19
    寒假学习记录18
    寒假学习记录17
    寒假学习记录16
    寒假学习记录15
    寒假学习记录14
    寒假学习记录13
    寒假学习记录12
    寒假学习记录11
    学习进度(10)
  • 原文地址:https://www.cnblogs.com/Java-Starter/p/9979565.html
Copyright © 2020-2023  润新知