UI 元素位置修正
本篇定位
這是 Phase 5 的第 3 篇,解析
Client::FixUIElements()— 修正遊戲內 HUD 在非 800×600 解析度下的位置偏移問題。 上一篇解決了「視窗大了」的問題,這篇解決「東西還是擠在左上角」的問題。
一、問題:UI 跟不上解析度的放大
MapleStory v83 的 UI 元素(血條、經驗值條、迷你地圖、快捷欄…)都以 800×600 的座標系定位:
原始設計(800x600):
迷你地圖右上角:X = 785, Y = 5 (靠近 800 的右邊緣)
快捷欄中央: X = 400, Y = 580 (靠近 600 的下邊緣)
解析度改成 1280×720 後,如果不修正座標,所有 UI 都會堆在螢幕左上方,因為坐標值沒有跟著擴大。
二、座標換算的數學原理
MapleEzorsia-v2 使用等比例換算:把原始座標從 800×600 的空間映射到新解析度的空間。
新 X = 原始 X × (新寬度 / 800)
新 Y = 原始 Y × (新高度 / 600)
以迷你地圖為例(目標解析度 1280×720):
新 X = 785 × (1280 / 800) = 785 × 1.6 = 1256
新 Y = 5 × (720 / 600) = 5 × 1.2 = 6
對應到程式碼中的 helper 函式:
// 把 800x600 座標換算成新解析度的座標
int Client::ScaleX(int originalX) {
return (int)(originalX * ((float)m_nGameWidth / 800.0f));
}
int Client::ScaleY(int originalY) {
return (int)(originalY * ((float)m_nGameHeight / 600.0f));
}三、FixUIElements() 實作解析
void Client::FixUIElements() {
// ── 迷你地圖 ────────────────────────────────────────────────────────────
// 迷你地圖框架的 X、Y 座標(靠右上角對齊)
Memory::Write<int>(addys::MiniMap_X, ScaleX(785));
Memory::Write<int>(addys::MiniMap_Y, ScaleY(5));
// ── 血量 / 魔力條 ────────────────────────────────────────────────────────
// HP/MP 條在畫面右下角
Memory::Write<int>(addys::HPBar_X, ScaleX(630));
Memory::Write<int>(addys::HPBar_Y, ScaleY(545));
Memory::Write<int>(addys::MPBar_X, ScaleX(630));
Memory::Write<int>(addys::MPBar_Y, ScaleY(560));
// ── 經驗值條 ─────────────────────────────────────────────────────────────
// 橫跨整個畫面底部
Memory::Write<int>(addys::EXPBar_X, ScaleX(0));
Memory::Write<int>(addys::EXPBar_Y, ScaleY(590));
Memory::Write<int>(addys::EXPBar_Width, m_nGameWidth); // 直接用新寬度
Memory::Write<int>(addys::EXPBar_Height, ScaleY(10));
// ── 快捷欄 ───────────────────────────────────────────────────────────────
// 快捷欄在畫面正下方中央
Memory::Write<int>(addys::QuickSlot_X, ScaleX(400) - 160); // 減去快捷欄寬的一半
Memory::Write<int>(addys::QuickSlot_Y, ScaleY(560));
// ── 系統按鈕(裝備 / 物品 / 技能 / 狀態)───────────────────────────────
// 靠右下角一排按鈕
Memory::Write<int>(addys::SysBtn_X, ScaleX(680));
Memory::Write<int>(addys::SysBtn_Y, ScaleY(560));
// ── 聊天視窗 ─────────────────────────────────────────────────────────────
Memory::Write<int>(addys::ChatBox_X, ScaleX(0));
Memory::Write<int>(addys::ChatBox_Y, ScaleY(435));
Memory::Write<int>(addys::ChatBox_Width, ScaleX(500));
Memory::Write<int>(addys::ChatBox_Height, ScaleY(150));
}四、錨點類型:不同元素有不同的對齊策略
並非所有元素都用等比例換算。根據設計意圖,元素分為幾種錨點類型:
| 錨點類型 | 說明 | 換算方式 | 範例 |
|---|---|---|---|
| 右上角錨 | 貼近視窗右/上邊緣 | 新X = 新寬度 - (800 - 原X) | 迷你地圖 |
| 右下角錨 | 貼近視窗右/下邊緣 | 新Y = 新高度 - (600 - 原Y) | HP/MP 條 |
| 正下方中央錨 | 水平置中在底部 | 新X = (新寬度 / 2) + 偏移 | 快捷欄 |
| 全寬延伸 | 橫跨整個視窗寬度 | 新Width = 新寬度 | EXP 條 |
| 等比例縮放 | 跟隨解析度比例 | ScaleX / ScaleY | 大部分元素 |
右邊緣錨點的換算
如果原始 X = 785(距離右邊緣 800-785=15px),新解析度下應該也距離右邊緣 15px:
新X = 新寬度 - (800 - 原X) = 1280 - 15 = 1265這比等比例換算(785 × 1.6 = 1256)更精確,因為「貼近邊緣」的意圖是「間距固定」而不是「等比例」。
五、何時呼叫 FixUIElements?
graph TD A[CWvsApp::SetUp Hook 觸發] --> B{遊戲初始化完成?} B -->|是| C[FixLoginScreen — 修正登入畫面] C --> D[玩家進入遊戲] D --> E[CWvsApp::Run Hook 每幀觸發] E --> F{第一次進入遊戲地圖?} F -->|是| G[FixUIElements — 修正 HUD] G --> H[遊戲正常運行]
時機陷阱
FixUIElements()不能在 DLL 剛載入時就呼叫,因為那時 HUD 的記憶體結構還沒建立。必須等遊戲進入地圖(MapleStory 稱為「Field」)後,HUD 物件才會被建立,座標才能被寫入。 MapleEzorsia-v2 在CWvsApp::Run的 Hook 裡偵測「是否剛進入地圖」,再呼叫這個函式。
六、除錯:UI 位置不對怎麼辦?
實際上每個 UI 元素的地址要自己逆向工程找到。以下是驗證流程:
1. 用 Cheat Engine 附加到遊戲
2. 找到迷你地圖的 X 座標值(例如搜尋數字 785)
3. 確認地址是靜態的(不是每次啟動都不同)
4. 在 IDA Pro 中確認這個地址附近的程式碼語意
5. 更新 AddyLocations.h 的 MiniMap_X 地址
如果換算後 UI 元素的位置看起來奇怪(例如偏左或偏右),常見原因:
| 症狀 | 原因 |
|---|---|
| 元素貼左上角不動 | 地址錯誤,寫入的不是正確的座標欄位 |
| 元素超出畫面右側 | 用等比例換算,但應該用右邊緣錨點 |
| 元素垂直位置不對 | 遊戲有多個 UI「層(Layer)」,寫到了錯誤的層 |
| 部分元素正確、部分不正確 | 某些 UI 元素的座標在另一個 Hook 裡被遊戲重置 |
七、常見問題
Q:ScaleX / ScaleY 用浮點運算,會有精度問題嗎?
有,但影響很小。UI 座標是整數像素,浮點乘除後截斷(
(int)(...)),最多有 ±1 pixel 的誤差。對於 UI 對齊來說這是可接受的範圍。如果需要更精確,可以改用四捨五入((int)round(...))。
Q:有些私服的解析度是非標準比例(如 1024×768),換算公式還成立嗎?
等比例換算對任何解析度都成立,但畫面比例(Aspect Ratio)不同時,元素的相對視覺大小會改變。例如 4:3(800×600)→ 16:9(1280×720),水平方向拉伸得比垂直方向多,某些元素可能看起來被橫向壓縮。這是目前實作的限制,精確解法是針對比例差異做額外補正。
延伸閱讀
- 02_解析度修改原理與實作 — UI 修正的前提:先把視窗大小改對
- 04_登入畫面與選角畫面修正 — 同類問題在登入 / 選角畫面的特殊處理
- 03_AddyLocations.h_地址表解析 — 所有 UI 元素地址的集中管理