登入畫面與選角畫面修正

本篇定位

這是 Phase 5 的第 4 篇,解析 Client::FixLoginScreen()Client::FixSelectCharScreen()。 登入畫面和選角畫面的 UI 修正比一般 HUD 更複雜,因為它們有動畫、背景圖、文字輸入框等需要特殊處理的元素。


一、登入畫面有哪些元素需要修正?

MapleStory v83 的登入畫面元素(在 800×600 座標系):

┌──────────────────── 800 ────────────────────────┐
│                                                  │
│          [遊戲標誌 / Logo]                       │
│                  ↑                               │
│              Y = 150                             │
│                                                  │
│    帳號輸入框 (X=280, Y=330, W=240, H=25)        │
│    密碼輸入框 (X=280, Y=365, W=240, H=25)        │
│                                                  │
│    [登入按鈕]  (X=280, Y=400)                    │
│    [新手按鈕]  (X=400, Y=400)                    │
│                                                  │
│    版本號文字  (X=700, Y=580)                    │
└──────────────────────────────────────── 600 ──┘

這些元素在改成 1280×720 後,都會擠在左上角。


二、FixLoginScreen() 實作

void Client::FixLoginScreen() {
    int newW = Client::m_nGameWidth;
    int newH = Client::m_nGameHeight;
 
    // ── 背景圖層 ──────────────────────────────────────────────────────────────
    // 登入背景要拉伸填滿整個新視窗
    Memory::Write<int>(addys::LoginBG_Width,  newW);
    Memory::Write<int>(addys::LoginBG_Height, newH);
 
    // ── 遊戲 Logo ────────────────────────────────────────────────────────────
    // Logo 水平置中,垂直位置等比例縮放
    Memory::Write<int>(addys::LoginLogo_X, (newW / 2) - 200);  // Logo 寬約 400px,置中
    Memory::Write<int>(addys::LoginLogo_Y, ScaleY(150));
 
    // ── 帳號 / 密碼輸入框 ─────────────────────────────────────────────────────
    // 輸入框整體置中
    int inputBoxLeft = (newW / 2) - 120;   // 240px 寬的輸入框,置中
 
    Memory::Write<int>(addys::IDInput_X,  inputBoxLeft);
    Memory::Write<int>(addys::IDInput_Y,  ScaleY(330));
    Memory::Write<int>(addys::IDInput_W,  240);   // 輸入框寬度固定,不等比
    Memory::Write<int>(addys::IDInput_H,  25);
 
    Memory::Write<int>(addys::PWInput_X,  inputBoxLeft);
    Memory::Write<int>(addys::PWInput_Y,  ScaleY(365));
    Memory::Write<int>(addys::PWInput_W,  240);
    Memory::Write<int>(addys::PWInput_H,  25);
 
    // ── 按鈕列 ───────────────────────────────────────────────────────────────
    int btnBaseY = ScaleY(400);
 
    Memory::Write<int>(addys::LoginBtn_X,  (newW / 2) - 120);
    Memory::Write<int>(addys::LoginBtn_Y,  btnBaseY);
 
    Memory::Write<int>(addys::NewUserBtn_X, (newW / 2));
    Memory::Write<int>(addys::NewUserBtn_Y, btnBaseY);
 
    // ── 版本號文字(右下角錨點)─────────────────────────────────────────────
    Memory::Write<int>(addys::VersionText_X, newW - ScaleX(800 - 700));  // 右邊距固定
    Memory::Write<int>(addys::VersionText_Y, newH - ScaleY(600 - 580));  // 下邊距固定
}

為什麼輸入框寬度固定不縮放?

輸入框(TextBox)的寬度代表「可輸入文字的視覺長度」,和解析度大小是獨立的。 帳號最多 13 個字元,對應 240px 就夠了。等比例放大反而讓它看起來過寬、不美觀。


三、登入畫面的特殊問題:動態背景

MapleStory v83 的登入畫面有一個會移動的雲朵背景動畫,實作是循環捲動一張 800px 寬的背景圖

// 遊戲內的背景捲動邏輯(偽碼)
bgScrollX += scrollSpeed;
if (bgScrollX >= 800) bgScrollX = 0;
DrawBackground(bgScrollX, 0, 800, 600);

解析度改成 1280 後,這個捲動最大值還是 800,所以背景只能覆蓋左邊 800px,右邊 480px 是黑色。

修正方式:

// 把捲動邊界從 800 改成新寬度
Memory::WriteProtected<int>(addys::BgScrollBound, newW);
 
// 把背景渲染寬度從 800 改成新寬度
Memory::WriteProtected<int>(addys::BgDrawWidth, newW);

副作用

背景圖本身只有 800px 寬,強制拉伸到 1280px 會讓紋理模糊。這是 v83 登入器開發的已知視覺問題,無法在不修改美術資源的情況下完全解決。


四、FixSelectCharScreen():選角畫面的特殊挑戰

選角畫面比登入畫面複雜,因為它同時顯示:

  • 最多 3 個角色(站在不同位置)
  • 每個角色的名稱、等級、職業圖示
  • 左右箭頭按鈕(翻頁)
  • 「開始遊戲」、「建立角色」、「刪除角色」按鈕
void Client::FixSelectCharScreen() {
    int newW = Client::m_nGameWidth;
    int newH = Client::m_nGameHeight;
 
    // ── 三個角色的站立位置 ─────────────────────────────────────────────────
    // 原始:三個角色分別在 X = 200, 400, 600(平均分布於 800px 內)
    // 新位置:平均分布於新寬度
 
    int charSpacing = newW / 4;   // 四等分,角色站在 1/4, 2/4, 3/4 的位置
 
    Memory::Write<int>(addys::CharSlot1_X, charSpacing * 1);
    Memory::Write<int>(addys::CharSlot2_X, charSpacing * 2);
    Memory::Write<int>(addys::CharSlot3_X, charSpacing * 3);
 
    // 所有角色的 Y 座標等比例換算
    int charBaseY = ScaleY(380);
    Memory::Write<int>(addys::CharSlot1_Y, charBaseY);
    Memory::Write<int>(addys::CharSlot2_Y, charBaseY);
    Memory::Write<int>(addys::CharSlot3_Y, charBaseY);
 
    // ── 角色名稱 / 等級標籤(跟隨角色位置) ─────────────────────────────────
    Memory::Write<int>(addys::CharName1_X, charSpacing * 1);
    Memory::Write<int>(addys::CharName2_X, charSpacing * 2);
    Memory::Write<int>(addys::CharName3_X, charSpacing * 3);
    int nameY = ScaleY(420);
    Memory::Write<int>(addys::CharName1_Y, nameY);
    Memory::Write<int>(addys::CharName2_Y, nameY);
    Memory::Write<int>(addys::CharName3_Y, nameY);
 
    // ── 底部按鈕列(水平置中) ────────────────────────────────────────────
    int btnCenterX = newW / 2;
    int btnY = newH - ScaleY(600 - 520);   // 下邊距固定
 
    Memory::Write<int>(addys::StartGameBtn_X,   btnCenterX - 120);
    Memory::Write<int>(addys::StartGameBtn_Y,   btnY);
    Memory::Write<int>(addys::CreateCharBtn_X,  btnCenterX);
    Memory::Write<int>(addys::CreateCharBtn_Y,  btnY);
    Memory::Write<int>(addys::DeleteCharBtn_X,  btnCenterX + 120);
    Memory::Write<int>(addys::DeleteCharBtn_Y,  btnY);
 
    // ── 翻頁箭頭(貼近視窗左右邊緣) ─────────────────────────────────────
    Memory::Write<int>(addys::PrevPageArrow_X, ScaleX(30));
    Memory::Write<int>(addys::NextPageArrow_X, newW - ScaleX(30) - 20);  // 減去箭頭寬度
    Memory::Write<int>(addys::PrevPageArrow_Y, charBaseY);
    Memory::Write<int>(addys::NextPageArrow_Y, charBaseY);
}

五、呼叫時機總覽

flowchart TD
    A[DllMain 載入] --> B[MainMain 讀設定]
    B --> C[安裝 Hook]
    C --> D[Client::UpdateGameStartup]
    D --> E[Client::UpdateResolution]
    E --> F[遊戲啟動]

    F --> G{Hook_CWvsApp_SetUp 觸發}
    G --> H[Client::FixLoginScreen]
    H --> I{玩家登入成功}

    I --> J{Hook_CWvsApp_SelectChar 觸發}
    J --> K[Client::FixSelectCharScreen]
    K --> L{玩家選擇角色進遊戲}

    L --> M{Hook_CWvsApp_EnterField 觸發}
    M --> N[Client::FixUIElements]
    N --> O[正常遊玩]

六、Phase 5 回顧:Client 模組的四個任務

函式時機任務
UpdateGameStartup()DLL 載入後立即視窗模式、停用解析度警告
UpdateResolution()DLL 載入後立即全域變數、機器碼 Patch、Viewport
FixLoginScreen()觸發 SetUp Hook 後登入畫面 UI 重新置中定位
FixSelectCharScreen()觸發選角 Hook 後角色位置重分配、按鈕置中
FixUIElements()觸發 EnterField Hook 後HUD 各元素的等比例換算

七、常見問題

Q:為什麼登入按鈕要「水平置中」而不是等比例換算?

等比例換算 ScaleX(280) = 448,但新畫面寬 1280,中心是 640。把輸入框和按鈕放在 448 會明顯偏左,視覺上很奇怪。「置中」才符合使用者對「登入表單在畫面中央」的直覺。不同元素有不同的設計意圖,不能一律等比例換算。

Q:角色動畫(行走、揮劍)的位置也需要修正嗎?

不需要。角色動畫是遊戲引擎用角色物件的 position 座標渲染的,而 position 是以地圖座標系(而非螢幕座標系)為基礎。解析度改變只影響「攝影機把哪個範圍的地圖投影到螢幕上」,不影響角色在地圖裡的位置。

Q:如果私服改了登入畫面的美術(不同背景圖),修正還有效嗎?

大部分情況有效,因為我們修改的是 UI 元素的位置座標,和背景圖的內容無關。但如果私服的登入畫面使用了完全不同的 UI 結構(不同的元素數量或佈局),就需要重新逆向找到新的地址。


延伸閱讀