⚠️文章內有遊戲解包、注入和逆向的技術,不要在未經許可情況下在其他遊戲中嘗試並公開解包資源。以及請勿用於不正當的用途,尊重知識產權。
代碼參考: OrangeEgg1937/Unity-IL2CPP-Based-Translator
前置要求
在開始之前,以下是我們需要準備的一些環境和一些知識
所需環境
- Android Studio (Android開發環境)
- 任一Android Inline Hook庫,這裡會使用ShadowHook
- 動態載入SO方法(非必要,但方便進行Debug),可參考
- 一個 Android Unity 遊戲(這裡我使用了我的Demo,純apk可以在這裡下載)
- VSCode + APKLab (或其他APK反編譯工具)
前置知識
- 基礎 Android 逆向工程知識
- 基礎 Unity 引擎知識 (如果沒有也沒問題,下面也將談到)
- C++ & Java
Unity IL2CPP 簡介
這裡只是簡單介紹什麼是 Il2CPP,如需詳細瞭解其運作方式,可以參考: https://katyscode.wordpress.com/category/reverse-engineering/il2cpp/
目前,大多數 Android 手機遊戲都是使用 IL2CPP scripting backend,根據官方文件,IL2CPP提供了更好的跨平台應用程式支援,以及在 Android 中更好的效能 (相較於 Mono)。在下面的文本替代過程中,很多事情都會與IL2CPP相關,因此有必要知道 IL2CPP 是什麼。
IL2CPP 工作流程
根據官方文檔,IL2CPP(Intermediate Language To C++)是一個 Unity 的腳本後端,它將 C# 腳本轉換為 C++ 代碼,然後編譯成原生機器碼。整個過程可簡單描述如下:

這與 Mono 虛擬機器非常不同,遊戲邏輯會以靜態方式執行,而非由 C# 虛擬機器管理。但另一方面,遊戲符號和相關邏輯會在產生的libil2cpp.so內有固定的偏移量(offset)。因此,可以使用Hook Function Address來修改文字。
其中一個工具,Il2CppDumper,可以幫助我們取得 il2cpp 內所有符號的函數偏移量,這可以輕鬆地識別目標函數。但在本教程中會使用用另一種方法來做 – 使用 IL2CPP API
IL2CPP API
除了官方文檔外,官方Blog有詳細介紹IL2CPP:
What is IL2CPP?
The technology that we refer to as IL2CPP has two distinct parts.
- An ahead-of-time (AOT) compiler
- A runtime library to support the virtual machine
The AOT compiler translates Intermediate Language (IL), the low-level output from .NET compilers, to C++ source code. The runtime library provides services and abstractions like a garbage collector, platform-independent access to threads and files, and implementations of internal calls (native code which modifies managed data structures directly).
― JOSH PETERSON / UNITY TECHNOLOGIES, Unity Blog
在此部分中Il2CPP的另一個重要特點是它包含了支援VM的執行時的函式庫,這表示 IL2CPP 實作了 C# 環境,提供了一種不需動態編譯即可執行 C# 程式碼的靜態方式。透過閱讀 Unity Il2CPP 原始碼 (可在 UnityEditor 路徑中找到, {UnityEditorPath}/il2cpp/libil2cpp/il2cpp-api.cpp
),有一些與 C# 環境相關的 API:

透過使用這些API,我們可以在不需要修改libil2cpp.so的情況下,直接在遊戲中使用類似 C# 環境來實現文字替換。
在以下教學,會使用下列 API 來取得Class info、相關的Methods和Property,以及相關的函式庫 (.dll):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Reference from ./il2cpp/libil2cpp/il2cpp-api.cpp
// 用於取得文字相關的 .dll, 從這2個 API 尋找 Unity.TextMeshPro.dll
const Il2CppImage* il2cpp_assembly_get_image(const Il2CppAssembly *assembly)
size_t il2cpp_image_get_class_count(const Il2CppImage * image);
// 用於取得文字相關的 class, 從Unity.TextMeshPro.dll尋找TMP_Text
const Il2CppClass* il2cpp_image_get_class(const Il2CppImage * image, size_t index);
const char* il2cpp_class_get_name(Il2CppClass *klass);
// 用於取得TMP_Text的 methods, 從得TMP_Text class尋找相關的Method
const MethodInfo* il2cpp_class_get_methods(Il2CppClass *klass, void* *iter);
const char* il2cpp_method_get_name(const MethodInfo *method);
uint32_t il2cpp_method_get_param_count(const MethodInfo *method);
const Il2CppType* il2cpp_method_get_param(const MethodInfo *method, uint32_t index);
Il2CppClass* il2cpp_class_from_il2cpp_type(const Il2CppType* type);
// 用於轉換 C# 字串為 C++ 字串
Il2CppChar* il2cpp_string_chars(Il2CppString* str);
|
漢化插件注入 (APK Injection) 和熱補丁 (Hot Patch)
在開始使用 IL2CPP API 之前,先注入必要的函式庫和插件(假設是依照 ShadowHook 安裝):
- 在 build.gradle 中,新增下列設定與任務(pushSo)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| android {
...
defaultConfig {
...
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
...
}
task pushSo(dependsOn: 'externalNativeBuildDebug') {
doLast {
exec {
commandLine 'adb', 'shell', 'mkdir', '-p', '/sdcard/Android/data/com.DefaultCompany.DemoChatGame/files/custom/arm64-v8a'
}
def soFiles = fileTree(dir: 'build/intermediates/cmake/debug/obj/arm64-v8a', include: '*.so')
soFiles.each { file ->
exec {
commandLine 'adb', 'push', file.absolutePath, '/sdcard/Android/data/com.DefaultCompany.DemoChatGame/files/custom/arm64-v8a/' + file.name
}
}
}
}
|
建立 APK,然後反編譯取得 smali 程式碼和必要的原生函式庫 (./lib/arm64-v8a)
使用 APKLab 來反編譯 Unity APK,將這些檔案放入 Unity 遊戲資料夾,並在第一個Activity中的onCreate()方法插入程式碼,以初始化我們的函式庫
將APK重新編譯, 然後安裝至裝置
之後,當我們要更新原生程式碼時,只要執行「pushSo」任務就可以了,不需要重新編譯Unity APK
IL2CPP API Hook和使用

在這部分,將使用IL2CPP API,並取得相關的類別和方法來實現文字替換。
一開始,我們需要從 libil2cpp.so 載入符號,這可以通過使用dlopen()
& dlsym()
,為了方便,我直接使用ShadowHook 的 shadowhook_dlopen
和 shadowhook_dlsym
來實現。以下是一些必要的函式和變數:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| #define LIB_NAME "libil2cpp.so"
/**
* Function pointers for IL2CPP functions
*/
size_t (*il2cpp_image_get_class_count)(const Il2CppImage *image) = nullptr;
void *(*il2cpp_image_get_class)(const Il2CppImage *image, size_t index) = nullptr;
const char *(*il2cpp_class_get_name)(const Il2CppClass *klass) = nullptr;
const MethodInfo *(*il2cpp_class_get_methods)(const Il2CppClass *klass, void **iter) = nullptr;
const char *(*il2cpp_method_get_name)(const MethodInfo *method) = nullptr;
uint32_t (*il2cpp_method_get_param_count)(const MethodInfo *method) = nullptr;
const Il2CppType* (*il2cpp_method_get_param)(const MethodInfo *method, uint32_t index) = nullptr;
Il2CppClass* (*il2cpp_class_from_il2cpp_type)(const Il2CppType* type) = nullptr;
/**
* Function pointers for Convert C# string to C++ string
*/
Il2CppChar *(*il2cpp_string_chars)(const Il2CppString *str) = nullptr;
/**
* Function pointers for TMPro functions
*/
Il2CppString *(*TMP_Text_get_text)(void *instance) = nullptr;
void (*TMP_Text_set_text)(void *instance, Il2CppString *text) = nullptr;
void (*TextMeshProUGUI_onEnable)(void *instance) = nullptr;
void (*TMP_Text_SetText)(void *instance, Il2CppString *text) = nullptr;
void il2cpp_manager_init() {
handle = shadowhook_dlopen(LIB_NAME);
if (handle) {
il2cpp_image_get_class_count = (size_t (*)(const Il2CppImage *)) shadowhook_dlsym(handle,
"il2cpp_image_get_class_count");
il2cpp_image_get_class = (void *(*)(const Il2CppImage *, size_t)) shadowhook_dlsym(handle,
"il2cpp_image_get_class");
il2cpp_class_get_name = (const char *(*)(const Il2CppClass *)) shadowhook_dlsym(handle,
"il2cpp_class_get_name");
il2cpp_class_get_methods = (const MethodInfo *(*)(const Il2CppClass *,
void **)) shadowhook_dlsym(handle,
"il2cpp_class_get_methods");
il2cpp_method_get_name = (const char *(*)(const MethodInfo *)) shadowhook_dlsym(handle,
"il2cpp_method_get_name");
il2cpp_string_chars = (Il2CppChar *(*)(const Il2CppString *)) shadowhook_dlsym(handle,
"il2cpp_string_chars");
il2cpp_method_get_param_count = (uint32_t (*)(const MethodInfo *)) shadowhook_dlsym(handle,
"il2cpp_method_get_param_count");
il2cpp_method_get_param = (const Il2CppType *(*)(const MethodInfo *, uint32_t)) shadowhook_dlsym(handle,
"il2cpp_method_get_param");
il2cpp_class_from_il2cpp_type = (Il2CppClass *(*)(const Il2CppType *)) shadowhook_dlsym(handle,
"il2cpp_class_from_il2cpp_type");
} else {
__android_log_print(ANDROID_LOG_ERROR, "Il2cpp-Manager", "Failed to open %s: %s", LIB_NAME,
dlerror());
return;
}
il2cpp_assembly_get_image_hook::hook();
}
|
String/Text 如何顯示在螢幕上?
在 Unity 中,有多種不同的方式將文字顯示在使用者的螢幕上,常用的有 TextMeshPro (UGUI)
或 UI Toolkit
。這兩種方法都使用不同的類別結構來實作,但它們共享相同的概念,我們的目標是取得設定的文字和取得文字相關的方法。在此教學中,將使用 TextMeshPro
為例。
根據官方文檔在TextMeshPro
中,主要相關Class有 TMP_Text
和 TextMeshProUGUI
。TextMeshProUGUI
是用於顯示在UGUI的,而TMP_Text
是所有TextMeshPro類別的基礎類別,所有 set/get Text 方法都以此為基礎。因此,我們的目標如下:
Class | Method | Description |
---|
TMP_Text | get_text() | 獲取文字內容 |
TMP_Text | set_text() | 設定文字內容 |
TextMeshProUGUI | onEnable() | 當物件啟用時,會呼叫此方法來更新顯示的文字 |
Hook IL2CPP API取得Unity.TextMeshPro.dll
要獲得 TMP_Text
和 TextMeshProUGUI
Class,我們需要使用 IL2CPP API 來取得 Unity.TextMeshPro.dll 中的類別。由於 IL2CPP 會在遊戲Activity開始時啟動並載入 .dll (static offset) 和指定Assembly位址,我們可以Hook載入Assembly API來取得Class Pointer。
- 匯入 IL2CPP Type 頭文件 (
il2cpp-class-internals.h
),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| typedef struct Il2CppAssemblyName
{
const char* name;
const char* culture;
const uint8_t* public_key;
uint32_t hash_alg;
int32_t hash_len;
uint32_t flags;
int32_t major;
int32_t minor;
int32_t build;
int32_t revision;
uint8_t public_key_token[PUBLIC_KEY_BYTE_LENGTH];
} Il2CppAssemblyName;
typedef struct Il2CppAssembly
{
Il2CppImage* image; // 由於我們不需要 Il2CppImage 資訊,可以在這裡將它轉換為 void*
uint32_t token;
int32_t referencedAssemblyStart;
int32_t referencedAssemblyCount;
Il2CppAssemblyName aname; // <-- 這是用於儲存Assembly名稱, 可以用它來識別.dll
} Il2CppAssembly;
|
- 使用 ShadowHook 來 Hook
il2cpp_assembly_get_image
Function,並取得 Unity.TextMeshPro.dll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| namespace il2cpp_assembly_get_image_hook {
void *orig;
void *stub;
void *proxy(Il2CppAssembly *assembly) {
SHADOWHOOK_STACK_SCOPE();
auto image_ptr = SHADOWHOOK_CALL_PREV(proxy, assembly);
if (strcmp(assembly->aname.name, "Unity.TextMeshPro") == 0) {
LOGD("Found Unity.TextMeshPro.dll");
...
}
return image_ptr;
}
void hook() {
stub = shadowhook_hook_sym_name(
LIB_NAME,
"il2cpp_assembly_get_image",
(void *) proxy,
&orig
);
int err = shadowhook_get_errno();
const char *error_msg = shadowhook_to_errmsg(err);
__android_log_print(ANDROID_LOG_WARN, TAG, "hook return: %p, %d - %s", stub, err,
error_msg);
}
}
|
- 取得 Unity.TextMeshPro.dll後,我們可以使用
il2cpp_image_get_class_count
來獲取 Class 數量,然後使用 il2cpp_image_get_class
來獲取全部Class Pointer (將其儲存為std::map
)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| void *proxy(Il2CppAssembly *assembly) {
SHADOWHOOK_STACK_SCOPE();
auto image_ptr = SHADOWHOOK_CALL_PREV(proxy, assembly);
if (strcmp(assembly->aname.name, "Unity.TextMeshPro") == 0) {
LOGD("Found Unity.TextMeshPro.dll");
// 來獲取 Class 數量
size_t class_count = il2cpp_image_get_class_count((const Il2CppImage *) image_ptr);
// 獲取全部 Class Pointer
for (size_t i = 0; i < class_count; ++i) {
auto *klass = (Il2CppClass *) il2cpp_image_get_class(
(const Il2CppImage *) image_ptr, i);
if (!klass) {
continue;
}
auto *class_name = il2cpp_class_get_name(klass);
if (!class_name) {
continue;
}
TMP_klass_map[class_name] = (Il2CppClass *) klass;
}
...
}
return image_ptr;
}
|
取得 TMP_Text, TextMeshProUGUI 類別
現在我們已經取得了 Unity.TextMeshPro.dll 中的所有 Class Pointer,接下來我們需要找到 TMP_Text
和 TextMeshProUGUI
這兩個類別。我們可以從Class Map取得TMP_Text
和 TextMeshProUGUI
pointer,然後使用 il2cpp_class_get_methods
來獲取相關的 Methods。
當我們準備TMP_Text
和 TextMeshProUGUI
代理函數(proxey function)時,我們需要再加入一個參數,也就是物件的實體指標(instance pointer) void *instance
。
ℹ️ onEnable() 和 set_text() 的代理函式稍後會談到
取得TextMeshProUGUI.onEnable() Method Pointer以及Hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Get the TextMeshProUGUI Class out
auto it = TMP_klass_map.find("TextMeshProUGUI");
if (it != TMP_klass_map.end()) {
Il2CppClass *tmp_text_class = it->second;
LOGD("Found TextMeshProUGUI class: %p", tmp_text_class);
void *_iter = nullptr;
while (auto method_info = il2cpp_class_get_methods(tmp_text_class, &_iter)) {
auto method_name = il2cpp_method_get_name(method_info);
if (strcmp(method_name, "OnEnable") == 0) {
TextMeshProUGUI_onEnable = (void (*)(void *)) method_info->methodPointer;
// Hook the OnEnable method
TextMeshProUGUI_onEnable_hook::hook();
}
}
} else {
LOGD("TMP_Text class not found in Unity.TextMeshPro.dll");
}
|
取得 TMP_Text.set_text() Method Pointer以及Hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // Get the TMP_Text Class out
auto it = TMP_klass_map.find("TMP_Text");
if (it != TMP_klass_map.end()) {
Il2CppClass *tmp_text_class = it->second;
LOGD("Found TMP_Text class: %p", tmp_text_class);
void *_iter = nullptr;
while (auto method_info = il2cpp_class_get_methods(tmp_text_class, &_iter)) {
auto method_name = il2cpp_method_get_name(method_info);
auto param_count = il2cpp_method_get_param_count(method_info);
LOGD("[TMP_Text] Found method: %s", method_name);
if (strcmp(method_name, "get_text") == 0) {
LOGD("Found method: %s", method_name);
TMP_Text_get_text = (Il2CppString *(*)(void *)) method_info->methodPointer;
}
if (strcmp(method_name, "set_text") == 0) {
LOGD("Found method: %s", method_name);
TMP_Text_set_text = (void (*)(void *,
Il2CppString *)) method_info->methodPointer;
TMP_Text_set_text_hook::hook();
}
if (strcmp(method_name, "SetText") == 0 && param_count == 1) {
auto *param_type = il2cpp_method_get_param(method_info, 0);
auto *param_class = il2cpp_class_from_il2cpp_type(param_type);
auto *param_class_name = il2cpp_class_get_name(param_class);
LOGD("Found method: %s (param=%s)", method_name, param_class_name);
if (strcmp(param_class_name, "String") == 0) {
LOGD("Found method: %s", method_name);
TMP_Text_SetText = (void (*)(void *,
Il2CppString *)) method_info->methodPointer;
TMP_Text_SetText_hook::hook();
}
}
}
} else {
LOGD("TMP_Text class not found in Unity.TextMeshPro.dll");
}
|
⚠️補充:在這裡額外Hook了TMP_Text.SetText()
方法,這是因為雖然SetText()
與 set_Text()
可以變更文字,但它們不是相同的方法。set_text()
是一個屬性方法,而 SetText()
是一個實際的方法。在Demo中,有些文字使用SetText()
方法來設定,因此我們也需要Hook它。
文字替換
現在我們已經取得了 TMP_Text
和 TextMeshProUGUI
的相關方法,也Hook了,接下來我們可以實現文字替換的功能。首先,研究一下get_text()
和set_text()
方法的參數和返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public virtual string text
{
get
{
if (m_IsTextBackingStringDirty)
return InternalTextBackingArrayToString();
return m_text;
}
set
{
if (m_IsTextBackingStringDirty == false && m_text != null && value != null && m_text.Length == value.Length && m_text == value)
return;
m_IsTextBackingStringDirty = false;
m_text = value;
m_inputSource = TextInputSources.TextString;
m_havePropertiesChanged = true;
SetVerticesDirty();
SetLayoutDirty();
}
}
|
在這裡,我們可以看到它是一個string
屬性,並且有一個get
和set
方法。當我們調用get_text()
時,它會返回一個字串,而當我們調用set_text()
時,它會接受一個string
作為參數。但小心,這是 C# 中的字串類型(UTF-16),而不是 C++ 中的字串類型。,正如我們之前所說,IL2CPP 將會實作 C# 環境,因此我們無法在 C++ 中直接將其轉換為 utf-16字串。但幸運的是,Il2CPP API 提供了一種方法,可將 C# 字串轉換為 C++ 字串。
1
| Il2CppChar* il2cpp_string_chars(Il2CppString* str);
|
C#與C++字串轉換
在 il2cpp-object-internals.h 中,註解給了我們一個提示,Il2CppString 是 System.String,也就是 C# 的 String。
- 匯入 IL2CPP 字串相關的 Type 頭文件 (il2cpp-api-types.h)
1
2
3
4
5
6
7
8
9
| typedef char16_t Il2CppChar;
// System.String
typedef struct Il2CppString
{
Il2CppObject object;
int32_t length; ///< Length of string *excluding* the trailing null (which is included in 'chars').
Il2CppChar chars[0];
} Il2CppString;
|
- 使用 API 將其轉換回 C++ 字串 (
std::u16string
)
1
2
| Il2CppChar *chars = il2cpp_string_chars(text);
std::u16string str(chars, text->length);
|
onEnable()
和 set_text()
的代理函式 (Proxy Function)
最後,透過使用找到的Method Pointer,我們使用 shadowhook_hook_func_addr() 來 Hook onEnable()
和 set_text()
方法,並在代理函式中實現文字替換。
1
2
3
4
5
| stub = shadowhook_hook_func_addr(
(void *) TMP_Text_set_text,
(void *) proxy,
&orig
);
|
完整程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| namespace TMP_Text_set_text_hook {
void *orig;
void *stub;
void proxy(void *instance, Il2CppString *text) {
SHADOWHOOK_STACK_SCOPE();
LOGD("TMP_Text_set_text_hook: instance=%p, text=%p", instance, text);
if (text != nullptr) {
Il2CppChar *chars = il2cpp_string_chars(text);
std::u16string str(chars, text->length);
LOGD("TMP_Text_set_text_hook: current text=%s",
std::string(str.begin(), str.end()).c_str());
}
SHADOWHOOK_CALL_PREV(proxy, instance, text);
}
void hook() {
if (TMP_Text_set_text == nullptr) {
__android_log_print(ANDROID_LOG_ERROR, TAG, "TMP_Text_set_text is NULL");
return;
}
stub = shadowhook_hook_func_addr(
(void *) TMP_Text_set_text,
(void *) proxy,
&orig
);
int err = shadowhook_get_errno();
const char *error_msg = shadowhook_to_errmsg(err);
__android_log_print(ANDROID_LOG_WARN, TAG, "(%p)hook return: %p, %d - %s",
TMP_Text_set_text, stub, err,
error_msg);
}
}
namespace TMP_Text_SetText_hook {
void *orig;
void *stub;
void proxy(void *instance, Il2CppString *text) {
SHADOWHOOK_STACK_SCOPE();
LOGD("TMP_Text_SetText_hook: instance=%p, text=%p", instance, text);
if (text != nullptr) {
Il2CppChar *chars = il2cpp_string_chars(text);
std::u16string str(chars, text->length);
LOGD("TMP_Text_SetText_hook: current text=%s",
std::string(str.begin(), str.end()).c_str());
}
SHADOWHOOK_CALL_PREV(proxy, instance, text);
}
void hook() {
if (TMP_Text_SetText == nullptr) {
__android_log_print(ANDROID_LOG_ERROR, TAG, "TMP_Text_SetText is NULL");
return;
}
stub = shadowhook_hook_func_addr(
(void *) TMP_Text_SetText,
(void *) proxy,
&orig
);
int err = shadowhook_get_errno();
const char *error_msg = shadowhook_to_errmsg(err);
__android_log_print(ANDROID_LOG_WARN, TAG, "(%p)hook return: %p, %d - %s",
TMP_Text_SetText, stub, err,
error_msg);
}
}
|
最後,我們可以在 Logcat 中看到遊戲文字可以成功顯示

取代原文效果實現
在這部分,我們將實現取代原文的效果。這可以通過在代理函式中修改文字來完成。

修改文字
這部分,我們需要建立一個新的char[],透過il2cpp_string_new()
API建立C# String,然後將其傳遞給 set_text()
方法。以下是修改後的代理函式:
1
2
3
4
5
6
7
8
9
10
11
12
13
| void proxy(void *instance, Il2CppString *text) {
SHADOWHOOK_STACK_SCOPE();
LOGD("TMP_Text_set_text_hook: instance=%p, text=%p", instance, text);
if (text != nullptr) {
Il2CppChar *chars = il2cpp_string_chars(text);
std::u16string str(chars, text->length);
LOGD("TMP_Text_set_text_hook: current text=%s",
std::string(str.begin(), str.end()).c_str());
}
auto new_text = il2cpp_string_new("Replaced Text");
// SHADOWHOOK_CALL_PREV(proxy, instance, text);
SHADOWHOOK_CALL_PREV(proxy, instance, new_text);
}
|
⚠️注意:TextMeshProUGUI_onEnable()
是沒有C# String參數的,我們需要在之後手動呼叫set_text()方法來更新文字。

進行翻譯
在 TMP_Text_set_text_hook
和 TMP_Text_SetText_hook
的代理函式中,我們可以檢查當前的文字內容,然後將其替換為我們想要的文字。建立一個translator函式來實現翻譯功能:
1
2
3
4
5
6
7
| #include <string>
namespace translator {
std::string translate(std::string orig);
} // namespace translator
|
假設我已經準備好了一個翻譯對照表,將原文與翻譯後的文字存儲在一個map中。以下是翻譯函式的實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| #include "translator.h"
#include <map>
std::map<std::string, std::string> translation_map = {
{"Hello, welcome to the test Demo!", "你好歡迎來到測試Demo!"},
{"You can press the button nearby!", "你可以按下這附近的按鈕!"},
{"You can also press the speed button!", "也可以按一下速度按鈕!"},
{"This message keeps looping!", "此訊息不斷循環!"},
{"This is the last message.", "這是最後一則訊息!"},
{"Pressed the settings button.", "按下設定按鈕"},
{"You clicked the save button.", "你點擊了儲存按鈕"},
{"Setting", "[設定]"},
{"Save", "[儲存]"},
};
namespace translator {
std::string translate(std::string orig) {
auto it = translation_map.find(orig);
if (it != translation_map.end()) {
return it->second;
}
return orig; // Return original if no translation found
}
} // namespace translator
|
然後我們得出的結果是…

😲全部都變成方格了!立即查看Logcat,發現不斷出現這個警告。
1
2
3
4
5
6
| The character with Unicode value \u9215 was not found in the [LiberationSans SDF] font asset or any potential fallbacks. It was
UnityEngine.DebugLogHandler:Internal_Log(LogType, LogOption, String, Object)
TMPro.TextMeshProUGUI:SetArraySizes(TextProcessingElement[])
TMPro.TextMeshProUGUI:OnPreRenderCanvas()
TMPro.TextMeshProUGUI:Rebuild(CanvasUpdate)
UnityEngine.UI.CanvasUpdateRegistry:PerformUpdate()
|
發現原來是因為LiberationSans SDF字體資源中沒有這個字元,這是因為我們使用的字體不包含中文字符。要解決這個問題,我們需要將字體替換為支持中文的字體…
如果是在Unity Editor中,這很簡單,只需將字體替換為支持中文的字體即可。但在已經建立的Game中,我們可以怎樣做呢…
這個留在下一篇文章中討論…