最近在 MapleStory Worlds(楓之谷世界,下稱 MSW) 做一個楓之谷風格的橫向小世界:走跳、打怪、升級、裝備、城鎮和任務都有。專案變大之後,我發現最常做的不是寫新程式,而是「改一個數字、加一隻怪、把歪掉的格子移回去」。

這篇是我給自己(也給想試試看的人)的操作手冊,只講怎麼調整、怎麼使用。

為了不公開專案裡的素材與資料,文中所有編號、名稱、資源碼都是示範用的假資料,圖也是另外畫的示意圖。照著做時,請換成你自己專案裡的值。

這篇會依序講:

  1. 改檔之後一定要做的事
  2. 專案資料夾怎麼看
  3. 資料表(CSV)的規則和關係
  4. 實作:新增一件裝備
  5. 實作:新增一隻怪物,讓牠掉這件裝備
  6. 實作:把裝備放進商店
  7. 實作:新增一張地圖並接上傳送門
  8. 介面跑位怎麼調
  9. 常見問題排查
  10. 搭配 AI 助手開發的守則

1. 改檔之後一定要做的事

改任何檔案之後:停止 Play、Maker 按 Refresh、再 Play 驗證

不管你改的是資料表、地圖還是介面,都照這個順序:

  1. 如果正在 Play,先停止。
  2. 在 Maker 按 Refresh,讓它重新讀檔。
  3. 再 Play,走到現場實際確認。

遊戲執行中會把資料留在記憶體裡,改完「畫面沒變」,九成是少做了其中一步。我後來養成習慣:每改一個東西就跑一次這三步,不要一次改十個地方再一起測,出錯時才找得到是哪一個。


2. 專案資料夾怎麼看

專案資料夾:RootDesk 是主要工作區,map 和 ui 用 Maker 或 Builder 改,Global 小心,Environment 只讀

資料夾 放什麼 能不能改
RootDesk/MyDesk/ 自己寫的 .mlua 腳本、.model、資料表 CSV ✅ 主要工作區
map/ .map 地圖檔 ✅ 但要透過 Maker 或 Builder
ui/ .ui 介面檔 ✅ 但要透過 Maker 或 Builder
Global/ 引擎預設、世界設定 ⚠️ 只能就地修改,不要新增或刪除
Environment/ .d.mlua API 定義 ❌ 只讀,拿來查 API

新手最容易踩的三個雷:

  • 新檔案只能放在 RootDesk/。 放在 Global/ 的新檔,Maker 不會認得,也不會報錯,就是「不存在」。
  • .mlua 要跟同名的 .codeblock 一起存在。 .codeblock 是自動產生的描述檔,不要手改;只有 .mlua 時,腳本不會被註冊。
  • .map、.ui、.model 不要用文字編輯器手改 JSON。 這幾種檔裡有大量 UUID 和路徑互相參照,手改很容易壞,而且壞了也不一定有錯誤訊息。

3. 資料表(CSV)的規則和關係

我把常改的數值(裝備、怪物、商店、掉落)都放在 DataSet 的 CSV 裡。好處是調平衡不用碰程式。

每張表都是一組兩個檔:

  • XxxDataSet.csv:真正的資料,改這個。
  • XxxDataSet.userdataset:只負責讓 Maker 認得這張表的名字,不要動。

3.1 改 CSV 的共通規則

  1. 第一列是欄位名,不要改、不要刪、不要多空白。
  2. 新增資料時,從最接近的一列整列複製到最後面再改,比從零開始打安全很多。
  3. 存成 UTF-8,不然中文會變亂碼。
  4. 每一列的逗號數量要跟表頭一樣,空欄位也要留著逗號。
  5. 同一個編號全表只能有一列。
  6. 改之前先備份原檔。

3.2 用 Excel 編輯要特別小心

Excel 打開 CSV 很方便,但它會「好心」幫你改資料:

  • 前面的 0 會被吃掉:0190001 會變成 190001,對遊戲來說是另一個編號。
  • 長數字會變成科學記號,例如 1.01E+06。
  • 存檔編碼可能不是 UTF-8,中文名稱整排變亂碼。

我現在都用 VS Code 直接改 CSV。真的要用 Excel,就用「資料 → 從文字/CSV」匯入,把編號欄設成「文字」,存檔時選「CSV UTF-8」。

3.3 資料表怎麼串在一起

資料表關係:地圖上的怪物用 MobId 對怪物表;掉落表用 MobId、ItemId 對怪物和裝備;商店表用 ItemId 對裝備

這張圖是後面所有實作的基礎。資料表之間靠編號對應:

  • 地圖上那隻怪的 Monster 元件,用 MobId 去怪物表找數值。
  • 掉落表的一列寫「哪隻怪(MobId)掉什麼(ItemId)」。
  • 商店表的一列寫「哪位 NPC 賣什麼(ItemId)、賣多少」。

所以只要其中一邊打錯一個字,就會對不上,而且通常不會報錯,只是「沒效果」。


4. 實作:新增一件裝備

目標:做一頂「練習頭盔」,防禦 20,所有職業都能穿。

步驟 1:複製一列

打開裝備表,找一件同部位的裝備(例如另一頂帽子),整列複製到最後面。表頭大概長這樣(示範):

Id,Name,Slot,ReqJob,ReqLevel,ReqStr,ReqDex,ReqInt,ReqLuk,Str,Dex,Int,Luk,Pad,Mad,Pdd,Mdd,Hp,Mp,...,Price,AvatarRuid,AvatarCat,Icon

步驟 2:改身份欄位

1009999,練習頭盔,Cap,0,1,...
  • Id:換一個還沒用過的編號。我的專案裡裝備用固定的一段號碼區間,程式靠這個區間判斷「這是裝備」,所以新編號要落在同一段。
  • Name:畫面上的名字。
  • Slot:穿到哪一格,必須用程式認得的英文,大小寫要一樣,例如 Cap(帽子)、Coat(上衣)、Weapon(武器)。

步驟 3:設定誰能穿

欄位 意思
ReqLevel 需要等級
ReqStr/ReqDex/ReqInt/ReqLuk 需要的四屬
ReqJob 職業限制,用數字加總

ReqJob 用的是「位元加總」,每個職業一個數字:

數字 職業
0 全職業
1 劍士
2 法師
4 弓箭手
8 盜賊
16 海盜

多個職業就相加:劍士+法師是 3,弓箭手+盜賊是 12。

步驟 4:設定能力值

穿上後加的數值,照欄位填就好。常用的幾個:

欄位 意思
Pad/Mad 物理/魔法攻擊
Pdd/Mdd 物理/魔法防禦
Str/Dex/Int/Luk 四屬加成
Hp/Mp 最大 HP/MP
Price 賣回 NPC 的價格(商店買入價在商店表)

練習頭盔就是把 Pdd 那格改成 20。

步驟 5:外觀和圖示

裝備圖示的判斷順序:先看 Icon,沒有才用 AvatarRuid 的縮圖,兩個都沒有就顯示預設圖

  • AvatarRuid:穿上後角色身上的造型。
  • AvatarCat:造型的種類。衣服、武器這類會被程式拿來過濾,填錯不會報錯,只是穿上去身上沒換裝。
  • Icon:背包、裝備欄、掉落物用的小圖。

最常見的需求是「背包圖不好看想換」:只改 Icon 就好,不用動身上的造型。

步驟 6:拿到手上測試

表裡有這一列,不代表地圖上撿得到。我在專案裡做了一個 GM 指令,在聊天框輸入:

/item 1009999

就會直接塞進背包。打開背包和裝備窗,確認三件事:圖示正常、說明框的數字對、穿得上身。

如果裝備有「卷軸強化」機制,畫面數字=表上的基礎值+這一件自己的強化值。改 CSV 不會清掉玩家已經強化的部分。


5. 實作:新增一隻怪物,讓牠掉這件裝備

目標:做一隻「練習木樁」,血量 30、經驗 8,打死有機率掉練習頭盔。

步驟 1:在怪物表新增一列

一樣從最像的一隻怪整列複製,改成新的 MobId:

MobId,Name,Level,MaxHp,Pad,Pdd,Exp,...
0190001,練習木樁,3,30,18,0,8,...
  • MobId 是文字:0190001 和 190001 是兩個不同的編號,前面的 0 要保留。
  • 想沿用原本那隻怪的外觀,就把外觀資源碼和身體範圍那幾欄原樣抄過來。
  • 「碰到會不會扣血」通常由兩個欄位一起決定(例如「不攻擊」旗標和「身體攻擊」旗標),兩個都要設對。

步驟 2:放到地圖上

  1. 在 Maker 的 Hierarchy 找一隻已經在地圖上、外觀想沿用的怪。
  2. 複製它,拖到新的位置。
  3. 選中複製出來的那隻,打開 Monster 元件,把 MobId 改成 0190001。

步驟 3:搞懂遊戲怎麼「對表」

遊戲決定怪物數值的順序:先看 MobId,找不到才用站立動畫資源碼反查,都沒有就維持原值

這是我踩過最久的坑:新怪放上去,血量卻還是舊怪的。原因是表裡沒有這個 MobId(或打錯),遊戲就改用外觀去反查,結果對回「長得一樣的那隻舊怪」。

所以新怪一定要 表和元件都填同一個編號,一字不差。

步驟 4:讓牠掉裝備

在掉落表加一列:

MobId,ItemId,Chance,Min,Max
0190001,1009999,1500,1,1
  • Chance 我用「百萬分之幾」,比較好微調:1500 就是 0.15%。
  • 如果程式對裝備另外乘了掉率倍數(例如 ×10),實際機率要再乘上去,也就是 1.5%。
  • 裝備一次掉一件,Min、Max 都填 1。

步驟 5:只想讓「某一隻」變強

不要去改共用的那一列,地圖上所有同編號的怪會一起變。給牠一個新的 MobId 和新的一列才對。


6. 實作:把裝備放進商店

在商店表加一列:

NpcId,ShopId,ItemId,Price,Position
9000001,1,1009999,500,10
  • NpcId、ShopId:抄同一位 NPC 現有的值。
  • Price:玩家買入的價格。
  • Position:在清單裡的排序,數字小的排前面。

Refresh 之後找那位 NPC 打開商店,練習頭盔就會在清單裡。


7. 實作:新增一張地圖並接上傳送門

7.1 先選地圖模式

三種地圖模式:模式 0 MapleTile 配 Rigidbody、模式 1 RectTile 配 Kinematicbody、模式 2 SideViewRectTile 配 Sideviewbody

模式決定了整套物理。 配錯 Body 不會報錯,角色就是不會動,偶爾會看到 [LEA-3004] MissingComponent 這類訊息。楓之谷風格的橫向地圖選模式 0。

模式要在建立前決定,之後再切換會清掉地形,所有會動的實體也要重新檢查。

7.2 從模板建立地圖

不要複製舊地圖再改檔名。模板建立會重新產生 UUID 和內部路徑;用複製的,兩張地圖常會共用舊的 ID,出現很難查的怪問題。

我的專案用 MapBuilder 從模板建立(示範):

MapBuilder
  .fromTemplate(MapBuilder.templatePath('maple'), 'myfield01')
  .write('map/myfield01.map')

地圖 ID 只用英文和數字,例如 myfield01。

7.3 註冊到 Sector

只有 .map 檔還不夠,要把它加進世界的 Sector 設定:

"entries": [
  "map://townmap",
  "map://myfield01"
]

這裡要寫 map://。漏了這步,傳送時會說找不到地圖。

7.4 畫地形、放出生點

  1. 在 Maker 開新地圖,用地形筆刷畫地板和平台。
  2. 模式 0 要確認平台有 Foothold,角色才站得住。
  3. 從正常的地圖複製一個 SpawnLocation 過來,放在平台上方。

世界座標:1 單位等於 100 像素,怪物座標 (4, 1) 代表往右 400 像素、往上 100 像素

座標單位是「世界單位」,不是像素。 1 單位=100 像素,PC 畫面大約 12.8 × 7.2 單位。如果你習慣填像素,填了 400,那個東西已經在 4 萬像素外了。

7.5 做雙向傳送門

新地圖要做去程和回程兩扇門;DestX、DestY 是目的地的安全落點

只做「舊地圖 → 新地圖」的門,玩家進去就出不來。至少兩扇:

舊地圖的去程門

DestinationMapId = myfield01
DestX、DestY    = 新地圖裡的安全落點
AutoEnter       = false

新地圖的回程門

DestinationMapId = townmap
DestX、DestY    = 舊地圖裡的安全落點
AutoEnter       = false

三個重點:

  • 傳送門的目的地寫純名稱 myfield01;Sector 註冊才寫 map://myfield01。兩個地方規則不同。
  • DestX、DestY 是目的地的落點,不是這扇門自己的位置。
  • 落點不要跟門重疊;AutoEnter 關掉、讓玩家按 ↑ 進門,才不會兩扇門互相彈來彈去。

7.6 修改既有地圖

想改的 去哪改
移動怪物、NPC、傳送門、裝飾 Maker Hierarchy 直接拖
平台、地板、牆 Maker 的地形筆刷
傳送目的地 傳送門元件的目的地與落點座標
地圖要不要被載入 Sector 設定

不要去移動承載 TileMap 的地形容器,它被鎖在原點,改了也會被還原。


8. 介面跑位怎麼調

介面座標:畫面正中心是 (0, 0),往右 X 變大、往上 Y 變大;格子是視窗的子物件,整窗一動就一起動

先判斷是「哪一層」歪掉,再去改對應的地方:

你看到的 改哪裡
整扇視窗偏了、跑出畫面 改視窗本身的位置,子格子會跟著動
只有某一格對不上底圖 改那一格的 X、Y
只有某一件物品的圖歪,其他件正常 是圖本身的問題,換那件裝備的 Icon
背包格子一起偏,或越往下越歪 改背包排版公式的起點與間距
說明框被聊天列蓋住 調整說明框的底部保留高度

8.1 底圖放大 2 倍時怎麼換算格子位置

我的裝備窗底圖是原圖放大 2 倍顯示,格子的錨點在左上角,所以:

介面 X = 原圖上格子左上角的 X × 2
介面 Y = −(原圖上格子左上角的 Y × 2)

例如原圖上某一格在 (40, 30),介面座標就是 (80, −60)。Y 取負號是因為介面座標「往上才是正的」。

8.2 只偏一點點的時候

以一格目前在 (80, −60) 為例:

畫面上的樣子 改成
偏右 8 X = 80 − 8 = 72
偏左 8 X = 80 + 8 = 88
偏上 6 Y = −60 − 6 = −66
偏下 6 Y = −60 + 6 = −54

口訣:往左 X 減、往右 X 加、往下 Y 減、往上 Y 加。 一次只移 4~8 像素,Refresh 再看。

8.3 背包這種排成格子的

背包不是一格一格寫死的,而是用「起點+間距」算出來:

第 n 欄的 X = 起點 X + (n − 1) × 欄距
第 m 列的 Y = −(起點 Y + (m − 1) × 列距)

全部一起偏,改起點;越往右(或越往下)越歪,改欄距(或列距)。不要去逐格改,下一次重排還是會被公式蓋掉。


9. 常見問題排查

症狀 先檢查
改了 CSV,遊戲裡還是舊數字 有沒有停止 → Refresh → 再 Play
新怪血量還是舊怪的 表和地圖上元件的 MobId 是否一字不差(含前面的 0)
裝備穿上了,身上沒換裝 AvatarCat 是否填對、AvatarRuid 是否有值
背包裡的圖特別大或偏位 Icon 是否空白,正在用造型縮圖
中文名稱變亂碼 CSV 是否存成 UTF-8;是不是被 Excel 改過
傳送時說找不到地圖 Sector 是否有 map://名稱;傳送門是否寫純名稱
進新地圖後一直往下掉 落點下方有沒有 Foothold,Y 是否放在平台上方
角色完全不動 地圖模式和 Body 是否配對
傳送後馬上被送回去 落點和門重疊,或 AutoEnter 開著
東西看不見,也沒有錯誤 SpriteRUID 是不是空字串;座標是不是填成像素
新加的腳本沒作用 檔案是否放在 RootDesk/;.codeblock 是否存在

10. 搭配 AI 助手開發的守則

我開發時會搭配 AI 助手,並在專案根目錄放一份 AGENTS.md,把 MSW 的規則寫給它看。實際用下來,最有用的是這幾條:

  1. 結構化檔案一律透過 Builder 修改:.map、.ui、.model 不准直接改 JSON。
  2. 沒有實際執行就不能說「可以了」:要它真的 Play、讀 log 再回報,不能只看程式碼就宣稱正常。
  3. 在關鍵位置留驗證 log:例如 log("[VRF] wave spawned n=3"),驗證時只讀這類 log,一次確認全部改動。
  4. 大功能先寫規劃文件:用 GDD 和分階段文件切開需求,每段做完、驗收完再做下一段。
  5. 生成物件時要指定 parent:SpawnByModelId 的 parent 要給目前的地圖實體,不能給 nil。

給 AI 的指令越具體越好,幾個我常用的寫法:

請建立模式 0、ID 為 myfield01 的新地圖,加入 Sector,
並在 townmap 與 myfield01 之間做雙向傳送門,落點放在平台上方。
在怪物表新增 MobId 0190001「練習木樁」(複製最接近的一列),
在 myfield01 放 3 隻,並讓牠以 0.15% 掉落裝備 1009999。
做完請 Play 驗證,回報 [VRF] log。
裝備窗的頭盔格往右偏了大約 8 像素,只調整那一格,不要動整個視窗。

改完之後的檢查清單

  • 停止 Play → Refresh → 再 Play
  • 新資料的編號沒有重複,CSV 逗號數量正確、UTF-8 存檔
  • 地圖上實體的編號和資料表一字不差
  • 用 GM 指令拿到新物品:圖示、說明、穿上身都正常
  • 新怪:血量、經驗、掉落都符合設定
  • 新地圖:去程、回程都走過,落點站得住、怪不會掉出地圖
  • 介面:點擊區蓋住底圖的空格,圖示在格子正中

遇到 MSW 本身的問題、文件也查不到時,官方 Discord 社群問得到人:https://discord.com/invite/maplestoryworlds。