基於C++ Qt實現的紅色警戒3修改器(Github開源)
前言
這部修改器製作有一段時間了,但是一直沒出教程。今天利用週末空閒寫篇教程,給後來者指路的同時也加深自己對遊戲修改器的理解,大佬就隨便看看吧
瀏覽了一下網路,形形色色的單機遊戲修改器教程,但是基本只實現了一到兩個功能,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", "\347\255\226\347\225\245\345\212\240\346\273\241", Q_NULLPTR)); ui->pushButton_3->setText(QApplication::translate("Ra3Window", "\347\253\213\345\215\263\347\224\237\346\225\210", Q_NULLPTR)); ui->label_4->setText(QApplication::translate("Ra3Window", "\351\200\211\345\217\226\345\215\225\344\275\215", Q_NULLPTR)); ui->comboBox->clear(); ui->comboBox->insertItems(0, QStringList() << QApplication::translate("Ra3Window", "\345\260\217", Q_NULLPTR) << QApplication::translate("Ra3Window", "\346\240\207\345\207\206", Q_NULLPTR) << QApplication::translate("Ra3Window", "\345\244\247", Q_NULLPTR) ); ui->pushButton_4->setText(QApplication::translate("Ra3Window", "\347\253\213\345\215\263\347\224\237\346\225\210", 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\">\n" "<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n" "p, li { white-space: pre-wrap; }\n" "</style></head><body style=\" font-family:'SimSun'; font-size:9pt; font-weight:400; font-style:normal;\">\n" "<p align=\"center\" style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">\350\257\264\346\230\216</p>\n" "<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">\351\207\221\351\222\261\357\274\232\350\276\223\345\205\245\351\207\221\351\222\261\346\225\260\351\242\235\357\274\214\345\206\215\347\202\271\345\207\273\345\217\263\344\276\247\342\200\234\347\253\213\345\215\263\347\224\237\346\225\210\342\200\235\346\214\211\351\222\256\357\274\214\345\215\263\345\217\257\350\216\267\345\276\227\346\214\207\345\256\232\347\232\204\351\207\221\351\222\261\346" "\225\260</p>\n" "<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">\347\224\265\345\212\233\357\274\232\350\276\223\345\205\245\347\224\265\345\212\233\345\200\274\357\274\214\345\206\215\347\202\271\345\207\273\345\217\263\344\276\247\342\200\234\347\253\213\345\215\263\347\224\237\346\225\210\342\200\235\346\214\211\351\222\256\357\274\214\345\215\263\345\217\257\350\216\267\345\276\227\346\214\207\345\256\232\347\232\204\347\224\265\345\212\233\345\200\274</p>\n" "<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">\347\255\226\347\225\245\345\212\240\346\273\241\357\274\232\344\270\215\351\234\200\350\246\201\346\214\207\345\256\232\357\274\214\351\273\230\350\256\244\345\212\240\346\273\241</p>\n" "<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">\351\200\211\345\217\226\345\215\225\344\275\215\357" "\274\232\345\205\210\351\200\211\346\213\251\344\270\200\344\270\252\345\215\225\344\275\215\357\274\214\344\270\213\346\213\211\346\241\206\351\200\211\346\213\251\345\244\247\345\260\217\357\274\214\345\206\215\347\202\271\345\207\273\345\217\263\344\276\247\342\200\234\347\253\213\345\215\263\347\224\237\346\225\210\342\200\235\346\214\211\351\222\256\357\274\214\345\217\257\344\273\245\347\234\213\345\210\260\345\215\225\344\275\215\347\232\204\345\244\247\345\260\217\345\217\230\345\214\226\357\274\233\346\240\207\345\207\206\357\274\232\346\255\243\345\270\270\345\244\247\345\260\217\343\200\202</p>\n" "<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>\n" "<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>\n" "<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>\n" "<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>\n" "<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;\">\345\215\232\345\256\242\345\234\260\345\235\200\357\274\232https://www.cnblogs.com/Java-Starter/</span></p>\n" "<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;\">\344\273\205\344\276\233\345\255\246\344\271\240\344\272\244\346\265\201\357\274\214\344\270\245\347\246\201\345\225\206\347\224\250</span></p>\n" "<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;\"> \344\275\234\350" "\200\205\357\274\232Jonas</span></p></body></html>", Q_NULLPTR)); ui->label_5->setText(QApplication::translate("Ra3Window", "\350\257\255\350\250\200", Q_NULLPTR)); //清空comboBox_2 ui->comboBox_2->clear(); // ui->comboBox_2->insertItems(0, QStringList() // << QApplication::translate("Ra3Window", "\347\256\200\344\275\223\344\270\255\346\226\207", Q_NULLPTR) // << QApplication::translate("Ra3Window", "\350\213\261\350\257\255", Q_NULLPTR) // << QApplication::translate("Ra3Window", "\345\276\267\350\257\255", Q_NULLPTR) // ); ui->lineEdit->setPlaceholderText(QApplication::translate("Ra3Window", "\350\276\223\345\205\245\351\207\221\351\222\261\346\225\260\351\242\235", Q_NULLPTR)); ui->label->setText(QApplication::translate("Ra3Window", "\351\207\221\351\222\261", Q_NULLPTR)); ui->pushButton->setText(QApplication::translate("Ra3Window", "\347\253\213\345\215\263\347\224\237\346\225\210", Q_NULLPTR)); ui->label_2->setText(QApplication::translate("Ra3Window", "\347\224\265\345\212\233", Q_NULLPTR)); ui->lineEdit_2->setPlaceholderText(QApplication::translate("Ra3Window", "\350\276\223\345\205\245\347\224\265\345\212\233\345\200\274", Q_NULLPTR)); ui->pushButton_2->setText(QApplication::translate("Ra3Window", "\347\253\213\345\215\263\347\224\237\346\225\210", 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即可執行
寫得很累,希望新人看了我的教程就可以學會製作單機遊戲外掛,知其然,知其所以然。大佬可以隨便看看