遊戲存檔的「存」,其實是複製整塊記憶體
一個背包陣列,裡面掛著幾十個道具物件
isPoisoned = false、hasKey_A = true、questStage = 3當你按下存檔,程式就是把這些變數一個一個讀出來,按照約定好的格式排好隊,寫成檔案。所謂「複製整塊記憶體」,指的就是這個過程:它不是抽象的「進度」,而是具體的位元組。
二、遊戲狀態在記憶體裡長什麼樣子
要理解存檔,就得先理解記憶體的分區。一款遊戲的行程(process)大致可以分成幾個區塊,而存檔只會碰其中一部分。
2-1 程式碼區、堆疊區、堆積區、全域資料區
換句話說,存檔的本質是:把全域資料區與堆積區裡「屬於遊戲世界」的那部分,抽出來序列化。程式碼區不用存,堆疊區不能存,其他周邊資源(GPU 緩衝、音效解碼暫存)不值得存。
2-2 物件與物件之間的「指標」才是真正的難題
如果遊戲世界只是幾百個獨立變數,存檔就跟把 Excel 表格存成 CSV 一樣簡單。問題在於:物件之間會互相引用。例如「玩家」物件裡有一個指標指向「目前裝備的武器」物件;「敵人」物件裡有一個指標指向「正在追蹤的目標」;「任務」物件裡有一個串列掛著「已完成的步驟」。
這種用指標串起來的結構,在程式裡叫「物件圖」(object graph)。存檔時,你不能直接把指標的數值(也就是記憶體位址)寫進檔案,因為:
下次開啟遊戲時,記憶體佈局會變,同一個物件不會剛好落在同一個位址。
作業系統有位址空間隨機化(ASLR),每次啟動的基底位址都不同。
32 位元與 64 位元的指標大小不同,跨平台存檔會直接爆炸。
正確做法是:存檔時把「指標」轉換成「識別碼」。例如每個物件都有一個唯一的 objectId,存檔時寫入 weaponId = 1042,讀檔時再根據這個 ID 把對應的武器物件找回來、重新接上指標。這個「把指標換成 ID、讀檔時再重建」的過程,就是序列化與反序列化的核心。
2-3 有些東西「長得像資料」,其實不能存
另外一類常見錯誤,是把「執行時期的暫存物」也存進去。例如:
渲染用的頂點緩衝、貼圖控制代碼(handle)
音效混音器的內部狀態
指向檔案系統的控制代碼
計時器、執行緒鎖、網路連線
這些東西在讀檔時往往已經失效,硬存進去只會讓存檔變大,還可能導致讀檔後行為詭異。這也是為什麼「整塊記憶體直接複製」在一般遊戲裡行不通——你必須挑選,不能無腦全存。
三、從記憶體到檔案:序列化的五個步驟
我們可以把「按存檔」到「檔案落地」的過程拆成五個階段。理解這五步,你就能理解大部分存檔現象,包括存檔損壞、存檔膨脹、跨版本不相容。
3-1 建立存檔標頭:先寫一張身分證
存檔檔案開頭通常會有一段「標頭」(header),內容大致包含:
0x53 0x41 0x56 0x45 代表「SAVE」,用來快速判斷這是不是自家存檔。格式版本號:讓遊戲知道要用哪一版規則來解讀。
遊戲版本、平台代號:避免 PC 存檔被主機版誤讀。
時間戳、遊玩時數、角色名稱:給讀檔介面顯示用。
標頭的重要性在於「可辨識」與「可驗證」。沒有版本號的存檔,等於沒有戶籍謄本的資料,改版後遊戲根本不知道該怎麼讀。
3-2 標記根物件:從哪裡開始走?
記憶體裡有成千上萬個物件,程式不可能全部掃一遍。所以它會先指定幾個「根」(root),例如:
玩家物件
世界/關卡物件
全域遊戲管理器
任務與成就系統
然後從這些根出發,沿著指標把「可達」(reachable)的物件一個一個抓出來。這種做法叫「走訪物件圖」。好處是:只存真正屬於遊戲世界的東西,不會撈到無關的暫存資料。
3-3 走訪與編碼:把物件攤平成位元組
走訪的過程中,程式會遇到各式各樣的資料型別:
基本型別:整數、浮點數、布林、字串 → 直接寫入位元組。
陣列與串列 → 先寫「長度」,再依序寫入每個元素。
物件 → 先寫型別 ID,再寫欄位。
指標 → 轉成物件 ID 或索引。
實際使用的格式有很多種:
自訂二進位格式:最省空間、最快,但最難除錯,也最難跨版本。
選哪一種,取決於遊戲規模與需求。獨立遊戲用 JSON 存檔完全沒問題;3A 大作如果每個存檔都轉 JSON,存一次可能要卡三秒。
3-4 寫入、校驗、原子替換
資料編碼完畢後,最後一步是「安全地寫進硬碟」。這裡有一個很容易被忽略、卻決定存檔生死的細節:不能直接覆蓋舊存檔。
正確流程通常是:
先把新存檔寫到一個暫存檔(例如 save.tmp)。
保留前一份存檔作為備份槽(backup slot),以防新檔有問題。
為什麼要這麼麻煩?因為如果直接覆蓋舊檔,寫到一半停電,你就同時失去「舊存檔」和「新存檔」,這就是玩家俗稱的「存檔全毀」。原子替換至少保證你永遠有一份完整的版本。
3-5 校驗與版本遷移
寫入完成後,程式會回頭計算校驗碼並填回標頭。讀檔時先驗證校驗碼,不對就拒絕載入或警告玩家。這能擋掉一部分「檔案被外力改壞」與「下載不完整」的情況。
另外,當遊戲更新、存檔格式改變時,通常會寫一段「遷移函式」:讀到舊版存檔,先補上預設值、轉換欄位,再升格成新版。這也是為什麼有些遊戲更新後,舊存檔還能讀,只是某些新功能會用預設狀態開啟。
四、兩種極端:模擬器即時存檔 vs 遊戲內存檔
前面說「遊戲通常不能真的整塊複製記憶體」,但有一類程式可以,那就是模擬器。理解這兩者的差異,你會對「存檔」有更立體的认识。
4-1 模擬器 Savestate:真正的記憶體快照
像 Dolphin、PCSX2、mGBA、RetroArch 這類模擬器,有所謂的「即時存檔」(savestate)。它做的事情非常暴力也非常優雅:把虛擬主機的整塊記憶體、CPU 暫存器、影音晶片的狀態,全部打包成一個檔案。
具體來說,一份 savestate 可能包含:
主記憶體(RAM)的完整映像
顯示記憶體(VRAM)
CPU 的程式計數器、堆疊指標、通用暫存器
DMA 控制器、計時器、中斷控制器狀態
音效晶片的暫存器與緩衝
控制器輸入狀態
載入時,模擬器把這些資料原封不動倒回去,然後讓 CPU 從當初保存的位址繼續執行。所以 savestate 可以「隨時存、隨時讀」,甚至可以在過場動畫播到一半、頭目戰雷射打到一半的時候存。它不需要理解遊戲邏輯,它只是在備份一台虛擬機器。
這也解釋了 savestate 的兩個典型缺點:
4-2 遊戲內存檔:選擇性地序列化
反過來說,商業遊戲為什麼不做 savestate?原因不是技術上做不到,而是「不能這樣做」:
所以遊戲會定義一套「乾淨的資料模型」:只存邏輯狀態,不存執行細節。這是一種刻意的抽象,也是一種工程妥協。
4-3 主機的快速恢復:介於兩者之間
近幾年的主機開始玩「整台機器快照」的概念。Xbox 的 Quick Resume 可以在多款遊戲之間快速切換,玩家離開時遊戲幾乎不用重新載入;Steam Deck 的休眠也是類似思路,把整個遊戲行程凍結、把記憶體內容寫到儲存裝置,下次唤醒再倒回去。
這種機制在系統層面運作,玩家不需要按「存檔」,但它本質上就是一種「記憶體快照」。不過它通常不保證跨更新、跨裝置可用,也不取代遊戲內的存檔——它只是「暫停」,不是「保存」。
五、為什麼存檔會壞?從原理看災難
理解存檔是「記憶體快照的序列化」之後,各種存檔災難的原因就變得很好推理。
5-1 寫入中斷:只寫了半個世界
最常見的存檔損壞,發生在「寫入過程中斷」。停電、當機、強制關機、儲存空間不足,都可能讓檔案只寫了一半。這時候檔案結構可能前段完整、後段是垃圾,讀檔程式一解析就爆炸。
解法就是前面提過的:暫存檔+原子替換+備份槽。有些遊戲還會用「雙緩衝」寫法,A、B 兩個槽輪流寫,永遠有一個是完整的。
5-2 指標外洩與版本錯位
如果開發者不小心把「指標位址」直接寫進存檔,就會出現一種玄學現象:同一份存檔,在你電腦上讀得出來,換一台電腦就讀出鬼資料。因為記憶體佈局不同,那個位址指向的東西完全不一樣。
另一種是「版本錯位」。遊戲更新後,存檔結構從 [血量, 魔力] 變成 [血量, 魔力, 精力],但沒有寫版本遷移。舊存檔讀進來時,程式把「精力」的位置讀成了下一個欄位,數值就會亂跳,甚至造成當機。
5-3 存檔膨脹與效能雪崩
存檔大小通常會隨遊戲進度增長,因為世界裡的物件變多了。《Minecraft》就是經典例子:你探索的區塊越多,世界檔案就越大。如果遊戲沒有做好分區儲存(例如只存有變動的區塊),每次存檔都要重寫整個世界,就會出現「存檔卡頓」。
有些遊戲還會把「歷史紀錄」也存進去,例如對話紀錄、擊殺統計、地圖探索百分比。這些資料如果不定期裁剪,存檔就會像滾雪球一樣膨脹。
5-4 雲端存檔衝突
雲端存檔本質上是「同一份序列化資料在不同裝置間同步」。當兩台裝置都玩過、都存過檔,就會出現版本衝突。多數平台會比較時間戳記,但時間戳記不可靠(時區、手動改系統時間、離線遊玩),所以最後往往要玩家自己選「要用哪一份」。這也是為什麼有些遊戲寧可做「伺服器端權威存檔」,以避免本機檔案被亂改。
六、給玩家的觀察法:從存檔檔案反推遊戲內部
如果你對遊戲內部有興趣,存檔檔案其實是一個很好的入口。它不像執行檔那麼複雜,又直接對應到遊戲狀態。
6-1 先看檔案大小與變化量
找一個「存檔前」與「存檔後」只差一件小事的時間點,例如買了一把劍、走進一個新區域,然後比較存檔大小。如果檔案大小幾乎沒變,代表遊戲用的是「固定欄位」或「原地覆寫」;如果大小明顯增加,代表它把新物件追加進去了。這能幫你判斷遊戲的存檔策略。
6-2 用十六進位編輯器找規律
打開存檔,先看開頭幾個位元組,通常能看到魔術數字與版本號。接著搜尋你已知的數值,例如角色名稱、金幣數量。整數通常以小端序(little-endian)存放,例如 1000 會寫成 E8 03。找到之後,前後文往往就是相鄰的屬性欄位。
6-3 用記憶體掃描工具對照
像 Cheat Engine 這類工具可以掃描遊戲執行中的記憶體,找出「金幣」的位址,再對照存檔中的位元組,就能理解遊戲怎麼把記憶體數值映射到檔案格式。這條路也是許多存檔編輯器與 mod 工具的起點。
6-4 風險提醒:改存檔之前先備份
存檔編輯很好玩,但也很容易把資料改壞。尤其當你動到「長度欄位」「校驗碼」「物件 ID」時,遊戲可能直接拒絕載入,或載入後出現詭異行為。動手前請務必備份原始檔案,並且一次只改一個地方,方便回推。
七、給開發者的備忘:把「複製記憶體」做好
如果你正在做遊戲,或未來想自己做,以下幾點是存檔系統的實務建議:
版本號從第一天就要有。並且預留「遷移函式」的設計,不要等改版才補。
原子寫入+備份槽。暫存檔、刷盤、改名,三步不能省。
加上校驗碼。CRC32 就很夠用,能擋掉大部分檔案損毀。
非同步存檔。把序列化與寫入放到背景執行緒,避免玩家存檔時畫面卡頓。
八、結語:存檔是一張記憶體的快照,也是一場工程妥協
回到最初的那句話:遊戲存檔的「存」,其實是複製整塊記憶體。更精確地說,是把記憶體中屬於遊戲世界的那部分狀態,經過走訪、編碼、校驗之後,安全地寫進儲存裝置。它不是錄影,而是拍照;不是日記,而是 X 光片。
理解了這一點,你就能解釋很多現象:為什麼存檔會隨遊戲進度變大、為什麼改版後舊存檔可能讀不進去、為什麼模擬器的即時存檔可以隨時亂存、為什麼存檔寫到一半斷電會直接毀檔。這些問題看似雜亂,其實都指向同一個核心——記憶體與檔案之間的那道轉換橋樑。
下次你在遊戲裡按下「儲存」,不妨想像一下:此刻你的電腦正把整個世界——座標、血量、背包、任務旗標、敵人位置——一個一個抓出來,排列成位元組,寫進硬碟。那個看似平凡的動作,其實是一場沉默而精密的記憶體搬運工程。
常見問答
Q1:為什麼有些遊戲的存檔很小,有些卻好幾百 MB?
存檔大小取決於「要保存的狀態有多少」。只存角色數值與 flag 的遊戲,存檔可能只有幾 KB;保存整個世界區塊、建築、地形修改的沙盒遊戲,存檔自然會到數百 MB。這跟遊戲時數沒有絕對關係,而是跟「世界裡有多少東西需要被記住」有關。
Q2:即時存檔(savestate)跟遊戲內存檔可以互相轉換嗎?
通常不行。savestate 保存的是虛擬硬體的完整快照,包含 CPU 暫存器與晶片狀態;遊戲內存檔保存的是遊戲邏輯層的資料。兩者的抽象層級不同,除非有工具專門做轉換,否則不能直接互換。
Q3:存檔可以跨平台使用嗎?
要看遊戲有沒有刻意支援。如果存檔格式只包含邏輯資料、沒有平台相關的位址或控制代碼,跨平台是有機會的;但如果格式牽涉到位元組序、指標大小、平台專屬欄位,就需要額外的轉換工具。
Q4:為什麼修改存檔有時候會導致遊戲崩潰?
因為存檔不只是數值,還有結構:長度欄位、物件 ID、校驗碼、版本號。你改了數值卻沒有同步更新長度或校驗碼,讀檔程式就會解析到錯誤的位置,輕則數值亂跳,重則直接當機。
Q5:雲端存檔衝突時該選哪一份?
先確認哪一份的遊玩進度比較新,並注意時間戳記可能不可靠(离線遊玩、時區差異)。如果不確定,先把兩份都備份下來,再選一份載入;有些平台會保留衝突副本,可以之後手動還原。