Brian Bucklew กำลังพอร์ต ‘Caves of Qud’ จาก Unity ไปยัง Godot
(twitter.com/unormal)- ‘Caves of Qud’ กำลังทำการพิสูจน์เชิงเทคนิคเพื่อลดการพึ่งพา Unity ที่มีมายาวนาน และ build/boot แกนเกมบน Godot โดยเป้าหมายแรกคือให้รันได้ในโหมด ASCII+tile โดยยังไม่มี VFX หรือ UI สมัยใหม่
- งานถูกแบ่งเป็นการนำเข้า tile assets, การพอร์ต core C# assemblies และการจัดชุด rendering/input rig โดยไฟล์ BMP tile เก่า ๆ ถูกแปลงเป็น PNG แล้วโหลดเข้า Godot
- ข้อผิดพลาด 5,641 รายการ ใน build แรกเริ่มลดลงเมื่อย้าย GeneratedCode, ConsoleLib, Genkit, Language, HistoryKit, Newtonsoft JSON, CodeDom ฯลฯ และค่อย ๆ จำกัดพื้นผิวการพึ่งพา UnityEngine ด้วย stub
- จุดที่ใช้ Color, GameObject, AudioSource, Debug.Log และ Screen ของ Unity ถูกจัดการด้วย UnityEngineReplacer และคลาสทดแทน ส่วนเลเยอร์ chargen/presentation ที่ใกล้กับ PlayFab และ Unity UI ถูกถอดออกชั่วคราว หรือเก็บไว้เป็นตัวเลือกสำหรับแยกเป็น glue module
- ผลลัพธ์คือแกนเกม C# ประมาณ 500,000 บรรทัด boot บน Godot ได้จนถึงขั้นส่ง frame และรอ input แล้ว แต่ยังเหลืองาน renderer/input harness รวมถึงการ rig VFX, sound และ UI
ขอบเขตงานย้ายจาก Unity ไป Godot
- งานพอร์ตแบ่งออกเป็นสามสาย
- การนำเข้า assets: งานนำ tile assets เข้าไปในโปรเจกต์ Godot
- การพอร์ต core assemblies: งานย้าย
XRL Applicationซึ่งเป็น main game engine ที่ไม่ใช่ Unity และGameManagerฝั่ง engine - การจัด rendering rig: งานเชื่อมต่อการแสดงผลบนหน้าจอและ input หลังจาก core boot แล้ว
- สร้างโปรเจกต์ Godot “mobile” แล้วคัดลอก texture content ของ Qud ไปไว้ในโฟลเดอร์ root ของโปรเจกต์
- เนื่องจาก Godot จัดการไฟล์
.bmpเก่าไม่ได้ จึงแปลงไฟล์ต่าง ๆ รวมถึง ASCII tiles เป็น PNG แบบ batch - หลังแปลงแล้ว assets โหลดได้ แต่ใช้เวลาประมาณ 30 วินาที กว่าตัวแสดงความคืบหน้าการนำเข้าจะปรากฏ
ย้าย core assembly พร้อมลด build errors
- หลังคัดลอก
XRL Applicationแล้ว ได้สร้าง C# project และ solution ใน Godot และในสถานะเริ่มต้นเกิดข้อผิดพลาด 5,641 รายการ - ไลบรารีทั่วไปอย่าง
ConsoleLibและGenkitถูกย้ายมาทั้งชุด ส่วน Kobold ซึ่งเป็นโซลูชัน sprite/atlasing เก่า จะนำเข้าทีละไฟล์โดยตรวจสอบว่าจำเป็นหรือไม่ KoboldJSONเป็นไลบรารี JSON serialization แบบง่าย จึงย้ายมาตรง ๆ- เมื่อแทนที่การใช้
Colorทั่วทั้งโปรเจกต์ 85 จุด ข้อผิดพลาดลดลงเหลือ 4,700 รายการ - เมื่อเพิ่มโฟลเดอร์
GeneratedCodeที่ขาดอยู่ ผลลัพธ์จากการ generate โค้ดคลาสแยกตาม event ก็ถูกรวมเข้ามา และข้อผิดพลาดลดลงเหลือ 1,557 รายการ- Caves of Qud มีโครงสร้างที่ generate คลาสแยกตาม event ซึ่งมี boilerplate จำนวนมาก เพื่อให้ได้ประสิทธิภาพและ event surface ที่ strongly typed
จัดการ dependency ของ Unity และ glue layer
Embark Builderและ chargen สำหรับสร้างตัวละครมีโค้ด Unity UI จำนวนมาก จึงถอดออกจากการพิสูจน์เชิงเทคนิค ASCII ระยะแรก- ยังไม่มี console version แต่การตรวจสอบเบื้องต้นสามารถทดสอบด้วยการโหลด save game หรือเริ่มแบบสุ่มได้
- โฟลเดอร์
Gameส่วนใหญ่เป็น glue ระหว่าง Unity กับเกม แต่ก็มีโฟลเดอร์ที่ไม่ใช่ glue อย่างCodeGenerationด้วย จึงเลือกแนวทางย้ายไปไว้ที่ root แยกต่างหากหรือโฟลเดอร์Platform - เมื่อย้ายไลบรารี
LanguageและHistoryKitข้อผิดพลาดลดลงเหลือ 454 รายการ และการที่มีการแยก game/glue ไว้บ้างแล้ว แม้ไม่สมบูรณ์ ก็ช่วยได้มาก - หมวดข้อผิดพลาดขนาดใหญ่ที่ยังเหลือคือ
CodeDom/Roslyn,PlayFab,Harmony, ปัญหาบางส่วนเกี่ยวกับ compiler และ พื้นผิว UnityEngine ที่รั่วเข้ามาในเลเยอร์เกม - โค้ดที่เกี่ยวกับ
PlayFabถูก comment out ชั่วคราว และเก็บไว้เป็นตัวเลือกสำหรับยกขึ้นเป็น glue module
Stub ทดแทน Unity API และความแตกต่างของ Godot C#
Color32เป็นชนิดสีที่อิง byte จึงเขียน implementation ทดแทน ได้อย่างรวดเร็ว- สร้างโฟลเดอร์
UnityEngineReplacerแล้ว implement เฉพาะพื้นผิวที่จำเป็นโดยดูจาก compile errors โดยไม่อ้างอิงเอกสาร Unity หรือ code reference - เมื่อสร้าง stub สำหรับ
GameObjectข้อผิดพลาดแบบ “ไม่รู้จัก GameObject” ก็เปลี่ยนเป็นข้อผิดพลาดของ field และ method ที่จำเป็นจริง ๆ ทำให้เข้าใจ interface surface ที่เกมใช้งานได้ AudioSourceก็เพิ่ม member ที่ถูกเข้าถึงทีละตัวจาก error list จนได้ port interface ของ AudioSource ครบตามที่เกมใช้งานจริง- พื้นผิวอื่น ๆ ที่จัดการมีดังนี้
- shim ทดแทน
Debug.Log - implementation ทดแทน
Screen - พอร์ตไลบรารี extension method
IsNullOrEmpty - จัดการ reference ของ
Mathและความต่างระหว่างPI/Pi - จัดการความต่างของชื่อพิกัดตัวพิมพ์ใหญ่
X,Yใน Godot - แก้ปัญหา IDE import
System.Drawing.Color,System.Numerics.Vector3ผิด
- shim ทดแทน
- Newtonsoft JSON แก้ได้โดยเพิ่ม NuGet package ใน Visual Studio และ CodeDom ก็แก้ด้วย NuGet package
CodeDom
จน boot ทั้งหมดบน Godot ได้
- เมื่อข้อผิดพลาดลดลงเหลือ 11 รายการ ปัญหาที่ยังเหลือคือโค้ดเกี่ยวกับ dynamic C# compilation ของ mod management ซึ่งไม่จำเป็นแต่ซับซ้อน จึงเหลือไว้จนท้ายสุด
- จากนั้นจัดการขั้นตอน link และข้อผิดพลาดของ stub fields จนโค้ดประมาณ 500,000 บรรทัดของ C# build ได้
- ขั้นถัดไปคือสร้าง renderer ขนาดเล็กและ input harness เพื่อ boot core assembly โดยมีเป้าหมายให้เกมรันได้ในโหมด ASCII+tile
- สร้าง scene เปล่าใน Godot แล้วแนบ
GameManager.csกับ node พื้นฐาน โดยGameManagerต้องสืบทอดจากNodeและต้องใช้ partial code - ใช้
_Readyของ Godot ในบทบาทที่สอดคล้องกับAwakeของ Unity และ_Processในบทบาทที่สอดคล้องกับUpdateของ Unity - ระหว่างการ initialize ได้ย้าย load path และ mod management และไล่ติดตามปัญหาที่ type resolver หา type จากชื่อไม่เจอด้วยวิธีแบบ printf
- สาเหตุคือการจัดการ dynamic assembly
- mod assembly เป็น dynamic จึงถูกยกเว้น dynamic assembly ออกจากการตรวจ main assembly
- ใน Godot editor แม้แต่ main game assembly ก็เป็น dynamic จึงหลุดออกจากเป้าหมายการค้นหา type
- หลังแก้ไขแล้วก็ boot ได้ทั้งหมด และแกนเกม 500kloc อยู่ในสถานะที่ส่ง frame และรอ input
- งานที่เหลือถูกอธิบายว่า “just work” แต่ในความเป็นจริงยังมีงานจำนวนมากกับการ rig VFX, sound และ UI
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
เกมนี้แทบทั้งหมดใช้ เอนจินของตัวเอง และดูเหมือนว่า Unity ถูกใช้แค่เป็นชั้น abstraction ของฮาร์ดแวร์กับกรอบสำหรับพอร์ตเท่านั้น ดังนั้นในแง่ความยากของการพอร์ต นี่น่าจะใกล้เคียงกับกรณีที่ดีที่สุด
เกมลักษณะนี้มีมากกว่าที่คิด แต่แน่นอนว่าไม่ได้เป็นตัวแทนของเกม Unity ส่วนใหญ่
หลังปรับปรุง UI แล้วยังเป็นแบบนั้นอยู่ไหมก็ไม่รู้ แต่ถ้าใช่ ผมก็แปลกใจมาตลอดว่าทำไมไม่พอร์ตไป MonoGame เพื่อประหยัดค่าใช้จ่าย
Android ก็คล้ายกัน ระบบปฏิบัติการถูกใช้แค่เพื่อโหลดไลบรารีมาทดแทนสิ่งต่าง ๆ อย่างการตรวจจับกล้องหรือการเข้ารหัส จนวันหนึ่งก็รู้ตัวว่าระบบปฏิบัติการแทบจะเป็นเพียงชั้นโลหะบาง ๆ กับ bootloader เท่านั้น
https://nitter.net/unormal/status/1703163364229161236
ให้ตายสิ Elon ตอนนี้จัดการกับ Twitter น่าหงุดหงิดจริง ๆ
ว้าว เจ๋งมาก ดีที่ได้เห็น กระบวนการพอร์ตแบบทีละขั้น และน่าประหลาดใจที่เวลาที่ใช้ก็ค่อนข้างสมเหตุสมผล
ถ้ามีโค้ดคัสตอมเยอะมากและใช้ฟีเจอร์ของ editor น้อย โครงสร้างแบบนี้อาจใช้ ไลบรารีเรนเดอร์ แทนเอนจินก็ได้
ถ้ามีเอนจินเป็นฐาน คุณจะได้สิ่งเหล่านี้มาฟรี ๆ แต่ถ้าเป็น Unity ก็อาจมีค่าใช้จ่ายตามมา
ถ้าอยากรู้ว่า Brian คิดอย่างไร ลองดูวิดีโอนี้: https://www.youtube.com/watch?v=U03XXzcThGU
เมื่อก่อนผมได้เรียนรู้จากที่นี่เยอะมาก
ทำให้นึกถึง ข้อถกเถียงเรื่อง TypeScript ของ DHH เมื่อเร็ว ๆ นี้
ลองจินตนาการว่าต้องพอร์ตแบบนี้โดยไม่มี static types ทุกครั้งที่แก้ก็ต้อง build แล้วรันเพื่อหาจุด crash เวลาแบบนี้จะรู้สึกขอบคุณมากที่ทำไว้ด้วยภาษาที่ refactor ง่ายและสถาปัตยกรรมที่ดี
ไม่มีบัญชี Twitter เกิดอะไรขึ้นเหรอ?