遊戲存檔的「存」,其實是複製整塊記憶體

gaming%20setup%20with%20RGB%20keyboard%2C%20gaming...
發表時間:2026 年 10 月 29 日 | 更新日期:2026 年 10 月 29 日 | 編輯:雅寶社區編輯團隊
遊戲存檔的「存」,其實是複製整塊記憶體 - 雅寶社區 · 頂客論壇

一個背包陣列,裡面掛著幾十個道具物件

  • 一堆狀態旗標:isPoisoned = false、hasKey_A = true、questStage = 3
  • 當你按下存檔,程式就是把這些變數一個一個讀出來,按照約定好的格式排好隊,寫成檔案。所謂「複製整塊記憶體」,指的就是這個過程:它不是抽象的「進度」,而是具體的位元組。

    二、遊戲狀態在記憶體裡長什麼樣子

    要理解存檔,就得先理解記憶體的分區。一款遊戲的行程(process)大致可以分成幾個區塊,而存檔只會碰其中一部分。

    2-1 程式碼區、堆疊區、堆積區、全域資料區

  • 程式碼區(text segment):放的是遊戲的執行指令,唯讀、不會被存檔碰到。你不會把「怎麼跑步」這段程式碼存進存檔裡,因為遊戲本體已經有了。
  • 全域/靜態資料區(data / bss):放的是生命週期跟整個程式一樣長的變數,例如遊戲設定、關卡索引、已解鎖成就旗標。這區的內容常常是存檔的重點之一。
  • 堆疊區(stack):放函式的區域變數與回傳位址,生命週期很短暫。存檔通常不會保存堆疊,因為讀檔時你不可能從「某個函式執行到一半」的狀態接回去。
  • 堆積區(heap):放動態配置的物件——敵人、子彈、掉落物、NPC、任務資料。這是存檔最主要、也最麻煩的來源。
  • 換句話說,存檔的本質是:把全域資料區與堆積區裡「屬於遊戲世界」的那部分,抽出來序列化。程式碼區不用存,堆疊區不能存,其他周邊資源(GPU 緩衝、音效解碼暫存)不值得存。

    2-2 物件與物件之間的「指標」才是真正的難題

    如果遊戲世界只是幾百個獨立變數,存檔就跟把 Excel 表格存成 CSV 一樣簡單。問題在於:物件之間會互相引用。例如「玩家」物件裡有一個指標指向「目前裝備的武器」物件;「敵人」物件裡有一個指標指向「正在追蹤的目標」;「任務」物件裡有一個串列掛著「已完成的步驟」。

    這種用指標串起來的結構,在程式裡叫「物件圖」(object graph)。存檔時,你不能直接把指標的數值(也就是記憶體位址)寫進檔案,因為:

    下次開啟遊戲時,記憶體佈局會變,同一個物件不會剛好落在同一個位址。

    作業系統有位址空間隨機化(ASLR),每次啟動的基底位址都不同。

    32 位元與 64 位元的指標大小不同,跨平台存檔會直接爆炸。

    正確做法是:存檔時把「指標」轉換成「識別碼」。例如每個物件都有一個唯一的 objectId,存檔時寫入 weaponId = 1042,讀檔時再根據這個 ID 把對應的武器物件找回來、重新接上指標。這個「把指標換成 ID、讀檔時再重建」的過程,就是序列化與反序列化的核心。

    2-3 有些東西「長得像資料」,其實不能存

    另外一類常見錯誤,是把「執行時期的暫存物」也存進去。例如:

    渲染用的頂點緩衝、貼圖控制代碼(handle)

    音效混音器的內部狀態

    指向檔案系統的控制代碼

    計時器、執行緒鎖、網路連線

    這些東西在讀檔時往往已經失效,硬存進去只會讓存檔變大,還可能導致讀檔後行為詭異。這也是為什麼「整塊記憶體直接複製」在一般遊戲裡行不通——你必須挑選,不能無腦全存。

    三、從記憶體到檔案:序列化的五個步驟

    我們可以把「按存檔」到「檔案落地」的過程拆成五個階段。理解這五步,你就能理解大部分存檔現象,包括存檔損壞、存檔膨脹、跨版本不相容。

    3-1 建立存檔標頭:先寫一張身分證

    存檔檔案開頭通常會有一段「標頭」(header),內容大致包含:

  • 魔術數字(magic number):例如 0x53 0x41 0x56 0x45 代表「SAVE」,用來快速判斷這是不是自家存檔。
  • 格式版本號:讓遊戲知道要用哪一版規則來解讀。

    遊戲版本、平台代號:避免 PC 存檔被主機版誤讀。

    時間戳、遊玩時數、角色名稱:給讀檔介面顯示用。

  • 校驗碼(checksum):通常是 CRC32 或雜湊值,先保留位置,最後再回填。
  • 標頭的重要性在於「可辨識」與「可驗證」。沒有版本號的存檔,等於沒有戶籍謄本的資料,改版後遊戲根本不知道該怎麼讀。

    3-2 標記根物件:從哪裡開始走?

    記憶體裡有成千上萬個物件,程式不可能全部掃一遍。所以它會先指定幾個「根」(root),例如:

    玩家物件

    世界/關卡物件

    全域遊戲管理器

    任務與成就系統

    然後從這些根出發,沿著指標把「可達」(reachable)的物件一個一個抓出來。這種做法叫「走訪物件圖」。好處是:只存真正屬於遊戲世界的東西,不會撈到無關的暫存資料。

    3-3 走訪與編碼:把物件攤平成位元組

    走訪的過程中,程式會遇到各式各樣的資料型別:

    基本型別:整數、浮點數、布林、字串 → 直接寫入位元組。

    陣列與串列 → 先寫「長度」,再依序寫入每個元素。

    物件 → 先寫型別 ID,再寫欄位。

    指標 → 轉成物件 ID 或索引。

    實際使用的格式有很多種:

    自訂二進位格式:最省空間、最快,但最難除錯,也最難跨版本。

  • JSON / YAML:人類可讀,方便 mod 與除錯,但體積大、解析慢。
  • NBT(Named Binary Tag):《Minecraft》使用的格式,兼具結構化與二進位效率。
  • MessagePack / Protocol Buffers / FlatBuffers:兼顧体积与解析速度的折衷方案。
  • 選哪一種,取決於遊戲規模與需求。獨立遊戲用 JSON 存檔完全沒問題;3A 大作如果每個存檔都轉 JSON,存一次可能要卡三秒。

    3-4 寫入、校驗、原子替換

    資料編碼完畢後,最後一步是「安全地寫進硬碟」。這裡有一個很容易被忽略、卻決定存檔生死的細節:不能直接覆蓋舊存檔。

    正確流程通常是:

    先把新存檔寫到一個暫存檔(例如 save.tmp)。

  • 呼叫系統的「刷新到磁碟」(fsync / FlushFileBuffers),確保資料真的落地,而不是躺在快取裡。
  • 把暫存檔「原子性地」改名成正式檔(rename / MoveFileEx),這一步在多數檔案系統上是不可分割的。
  • 保留前一份存檔作為備份槽(backup slot),以防新檔有問題。

    為什麼要這麼麻煩?因為如果直接覆蓋舊檔,寫到一半停電,你就同時失去「舊存檔」和「新存檔」,這就是玩家俗稱的「存檔全毀」。原子替換至少保證你永遠有一份完整的版本。

    3-5 校驗與版本遷移

    寫入完成後,程式會回頭計算校驗碼並填回標頭。讀檔時先驗證校驗碼,不對就拒絕載入或警告玩家。這能擋掉一部分「檔案被外力改壞」與「下載不完整」的情況。

    另外,當遊戲更新、存檔格式改變時,通常會寫一段「遷移函式」:讀到舊版存檔,先補上預設值、轉換欄位,再升格成新版。這也是為什麼有些遊戲更新後,舊存檔還能讀,只是某些新功能會用預設狀態開啟。

    四、兩種極端:模擬器即時存檔 vs 遊戲內存檔

    前面說「遊戲通常不能真的整塊複製記憶體」,但有一類程式可以,那就是模擬器。理解這兩者的差異,你會對「存檔」有更立體的认识。

    4-1 模擬器 Savestate:真正的記憶體快照

    像 Dolphin、PCSX2、mGBA、RetroArch 這類模擬器,有所謂的「即時存檔」(savestate)。它做的事情非常暴力也非常優雅:把虛擬主機的整塊記憶體、CPU 暫存器、影音晶片的狀態,全部打包成一個檔案。

    具體來說,一份 savestate 可能包含:

    主記憶體(RAM)的完整映像

    顯示記憶體(VRAM)

    CPU 的程式計數器、堆疊指標、通用暫存器

    DMA 控制器、計時器、中斷控制器狀態

    音效晶片的暫存器與緩衝

    控制器輸入狀態

    載入時,模擬器把這些資料原封不動倒回去,然後讓 CPU 從當初保存的位址繼續執行。所以 savestate 可以「隨時存、隨時讀」,甚至可以在過場動畫播到一半、頭目戰雷射打到一半的時候存。它不需要理解遊戲邏輯,它只是在備份一台虛擬機器。

    這也解釋了 savestate 的兩個典型缺點:

  • 跨版本不相容:換了模擬器版本、換了外掛、改了設定,記憶體佈局可能不同,舊 savestate 直接讀不進去。
  • 可能保存「不合法狀態」:因為它不管你遊戲內部是否處於一致狀態,只要記憶體當下長那樣就存。有些遊戲會因此出現資料錯亂。
  • 4-2 遊戲內存檔:選擇性地序列化

    反過來說,商業遊戲為什麼不做 savestate?原因不是技術上做不到,而是「不能這樣做」:

  • 平台不允許:主機與 PC 的作業系統不會讓你隨便把整個行程的記憶體倒出來再還原,這涉及安全與排程。
  • 存檔要跨版本、跨平台、上雲端:如果把指標位址、GPU 控制代碼都存進去,換一台機器就廢了。
  • 存檔要能被理解與維護:開發者需要除錯、需要做存檔編輯器、需要支援舊存檔。
  • 所以遊戲會定義一套「乾淨的資料模型」:只存邏輯狀態,不存執行細節。這是一種刻意的抽象,也是一種工程妥協。

    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」時,遊戲可能直接拒絕載入,或載入後出現詭異行為。動手前請務必備份原始檔案,並且一次只改一個地方,方便回推。

    七、給開發者的備忘:把「複製記憶體」做好

    如果你正在做遊戲,或未來想自己做,以下幾點是存檔系統的實務建議:

  • 明確定義「存檔邊界」。只存邏輯狀態,不存渲染、音效、網路連線等執行期資源。
  • 用 ID 取代指標。所有跨物件引用都應該用穩定 ID,讀檔時再重建關聯。
  • 版本號從第一天就要有。並且預留「遷移函式」的設計,不要等改版才補。

    原子寫入+備份槽。暫存檔、刷盤、改名,三步不能省。

    加上校驗碼。CRC32 就很夠用,能擋掉大部分檔案損毀。

  • 壓縮與分區。大型世界可以按區域或按系統拆分檔案,避免每次存檔都重寫全部。
  • 非同步存檔。把序列化與寫入放到背景執行緒,避免玩家存檔時畫面卡頓。

  • 考慮存檔安全。如果遊戲有連線要素,存檔可能需要簽章或伺服器驗證,避免被隨意竄改。
  • 八、結語:存檔是一張記憶體的快照,也是一場工程妥協

    回到最初的那句話:遊戲存檔的「存」,其實是複製整塊記憶體。更精確地說,是把記憶體中屬於遊戲世界的那部分狀態,經過走訪、編碼、校驗之後,安全地寫進儲存裝置。它不是錄影,而是拍照;不是日記,而是 X 光片。

    理解了這一點,你就能解釋很多現象:為什麼存檔會隨遊戲進度變大、為什麼改版後舊存檔可能讀不進去、為什麼模擬器的即時存檔可以隨時亂存、為什麼存檔寫到一半斷電會直接毀檔。這些問題看似雜亂,其實都指向同一個核心——記憶體與檔案之間的那道轉換橋樑。

    下次你在遊戲裡按下「儲存」,不妨想像一下:此刻你的電腦正把整個世界——座標、血量、背包、任務旗標、敵人位置——一個一個抓出來,排列成位元組,寫進硬碟。那個看似平凡的動作,其實是一場沉默而精密的記憶體搬運工程。

    常見問答

    Q1:為什麼有些遊戲的存檔很小,有些卻好幾百 MB?

    存檔大小取決於「要保存的狀態有多少」。只存角色數值與 flag 的遊戲,存檔可能只有幾 KB;保存整個世界區塊、建築、地形修改的沙盒遊戲,存檔自然會到數百 MB。這跟遊戲時數沒有絕對關係,而是跟「世界裡有多少東西需要被記住」有關。

    Q2:即時存檔(savestate)跟遊戲內存檔可以互相轉換嗎?

    通常不行。savestate 保存的是虛擬硬體的完整快照,包含 CPU 暫存器與晶片狀態;遊戲內存檔保存的是遊戲邏輯層的資料。兩者的抽象層級不同,除非有工具專門做轉換,否則不能直接互換。

    Q3:存檔可以跨平台使用嗎?

    要看遊戲有沒有刻意支援。如果存檔格式只包含邏輯資料、沒有平台相關的位址或控制代碼,跨平台是有機會的;但如果格式牽涉到位元組序、指標大小、平台專屬欄位,就需要額外的轉換工具。

    Q4:為什麼修改存檔有時候會導致遊戲崩潰?

    因為存檔不只是數值,還有結構:長度欄位、物件 ID、校驗碼、版本號。你改了數值卻沒有同步更新長度或校驗碼,讀檔程式就會解析到錯誤的位置,輕則數值亂跳,重則直接當機。

    Q5:雲端存檔衝突時該選哪一份?

    先確認哪一份的遊玩進度比較新,並注意時間戳記可能不可靠(离線遊玩、時區差異)。如果不確定,先把兩份都備份下來,再選一份載入;有些平台會保留衝突副本,可以之後手動還原。

    🏠 返回首頁