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),水平方向拉伸得比垂直方向多,某些元素可能看起來被橫向壓縮。這是目前實作的限制,精確解法是針對比例差異做額外補正。


延伸閱讀