แก้ไขข้อบกพร่องของ LMK

การแก้ปัญหา LMK ในเกม Unity เป็นกระบวนการที่เป็นระบบ ดังนี้

รูปที่ 1 ขั้นตอนในการแก้ไขการสิ้นสุดเนื่องจากหน่วยความจำเหลือน้อย (LMK) ในเกม Unity

รับสแนปชอตหน่วยความจำ

ใช้ Unity Profiler เพื่อดูภาพรวมหน่วยความจำที่ Unity จัดการ รูปที่ 2 แสดงเลเยอร์การจัดการหน่วยความจำที่ Unity ใช้เพื่อจัดการหน่วยความจำใน เกมของคุณ

รูปที่ 2 ภาพรวมการจัดการหน่วยความจำของ Unity

หน่วยความจำที่มีการจัดการ

การจัดการหน่วยความจำของ Unity จะใช้เลเยอร์หน่วยความจำที่ควบคุม ซึ่งใช้ฮีปที่มีการจัดการและตัวเก็บขยะเพื่อจัดสรร และกำหนดหน่วยความจำโดยอัตโนมัติ ระบบหน่วยความจำที่มีการจัดการคือสภาพแวดล้อมการเขียนสคริปต์ C# ที่อิงตาม Mono หรือ IL2CPP ข้อดีของระบบหน่วยความจำที่มีการจัดการคือระบบจะใช้ตัวเก็บขยะเพื่อ ยกเลิกการจัดสรรหน่วยความจำโดยอัตโนมัติ

หน่วยความจำที่ไม่มีการจัดการของ C#

เลเยอร์หน่วยความจำ C# ที่ไม่มีการจัดการช่วยให้เข้าถึงเลเยอร์หน่วยความจำดั้งเดิม ได้ ซึ่งช่วยให้ควบคุมการจัดสรรหน่วยความจำได้อย่างแม่นยำขณะใช้โค้ด C# คุณเข้าถึงเลเยอร์การจัดการหน่วยความจำนี้ได้ผ่านเนมสเปซ Unity.Collections และฟังก์ชันต่างๆ เช่น UnsafeUtility.Malloc และ UnsafeUtility.Free

หน่วยความจำดั้งเดิม

แกนหลัก C/C++ ภายในของ Unity ใช้ระบบหน่วยความจำดั้งเดิมเพื่อ จัดการฉาก, ชิ้นงาน, API กราฟิก, ไดรเวอร์, ระบบย่อย และบัฟเฟอร์ปลั๊กอิน แม้ว่าการเข้าถึงโดยตรงจะถูกจำกัด แต่คุณก็สามารถจัดการข้อมูลได้อย่างปลอดภัยด้วย C# API ของ Unity และรับประโยชน์จากโค้ดเนทีฟที่มีประสิทธิภาพ หน่วยความจำดั้งเดิมไม่ค่อยจำเป็นต้องมีการโต้ตอบโดยตรง แต่คุณสามารถตรวจสอบผลกระทบของหน่วยความจำดั้งเดิมต่อประสิทธิภาพได้โดยใช้ Profiler และปรับการตั้งค่าเพื่อเพิ่มประสิทธิภาพ

หน่วยความจำจะไม่แชร์ระหว่าง C# กับโค้ดแบบเนทีฟตามที่แสดงในรูปที่ 3 ระบบจะจัดสรรข้อมูลที่ C# ต้องการในพื้นที่หน่วยความจำที่มีการจัดการทุกครั้งที่จำเป็น

หากต้องการให้โค้ดของเกมที่มีการจัดการ (C#) เข้าถึงข้อมูลหน่วยความจำดั้งเดิมของเอนจิน เช่น การเรียกใช้ GameObject.transform จะทำให้เกิดการเรียกใช้ดั้งเดิม เพื่อเข้าถึงข้อมูลหน่วยความจำในพื้นที่ดั้งเดิม จากนั้นจะส่งคืนค่าไปยัง C# โดยใช้ Bindings การเชื่อมโยงช่วยให้มั่นใจได้ว่าการเรียกใช้แต่ละแพลตฟอร์มจะเป็นไปตามรูปแบบที่เหมาะสม และจัดการการส่งผ่านข้อมูลอัตโนมัติของประเภทที่จัดการไปยัง ประเภทเทียบเท่าในระบบ

การดำเนินการนี้จะเกิดขึ้นครั้งแรกเท่านั้น เนื่องจากเชลล์ที่มีการจัดการสำหรับการเข้าถึงพร็อพเพอร์ตี้ transform จะได้รับการเก็บรักษาไว้ในโค้ดดั้งเดิม การแคชพร็อพเพอร์ตี้ transform จะช่วยลดจำนวนการเรียกแบบไปกลับระหว่างโค้ดที่จัดการและโค้ดดั้งเดิมได้ แต่ประโยชน์ของการแคชขึ้นอยู่กับความถี่ในการใช้พร็อพเพอร์ตี้ นอกจากนี้ โปรดทราบว่า Unity จะไม่คัดลอกส่วนต่างๆ ของหน่วยความจำเนทีฟ ไปยังหน่วยความจำที่มีการจัดการเมื่อคุณเข้าถึง API เหล่านี้

รูปที่ 3 การเข้าถึงหน่วยความจำดั้งเดิมจากโค้ดที่มีการจัดการ C#

ดูข้อมูลเพิ่มเติมได้ที่ข้อมูลเบื้องต้นเกี่ยวกับหน่วยความจำใน Unity

นอกจากนี้ การกำหนดงบประมาณหน่วยความจำยังมีความสำคัญต่อการทำให้เกมทำงานได้อย่างราบรื่น และการใช้ระบบวิเคราะห์หรือการรายงานการใช้หน่วยความจำ จะช่วยให้มั่นใจได้ว่าแต่ละเวอร์ชันใหม่จะไม่เกินงบประมาณหน่วยความจำ การผสานรวมการทดสอบโหมด Play กับการผสานรวมอย่างต่อเนื่อง (CI) เพื่อยืนยัน การใช้หน่วยความจำในบางส่วนของเกมเป็นอีกกลยุทธ์หนึ่งที่จะช่วยให้คุณได้รับข้อมูลเชิงลึกที่ดีขึ้น

จัดการเนื้อหา

นี่คือส่วนที่ส่งผลกระทบมากที่สุดและดำเนินการได้จริงในการใช้หน่วยความจำ โปรไฟล์ โดยเร็วที่สุด

การใช้หน่วยความจำในเกม Android อาจแตกต่างกันอย่างมาก ขึ้นอยู่กับประเภทของ เกม จำนวนและประเภทของชิ้นงาน รวมถึงกลยุทธ์การเพิ่มประสิทธิภาพหน่วยความจำ อย่างไรก็ตาม โดยทั่วไปแล้ว องค์ประกอบที่มักทำให้เกิดการใช้หน่วยความจำ ได้แก่ เท็กซ์เจอร์ เมช ไฟล์เสียง เชเดอร์ ภาพเคลื่อนไหว และสคริปต์

ตรวจหาชิ้นงานที่ซ้ำกัน

ขั้นตอนแรกคือการตรวจหาชิ้นงานที่กำหนดค่าไม่ดีและชิ้นงานที่ซ้ำกันโดยใช้โปรไฟล์หน่วยความจำ เครื่องมือรายงานการสร้าง หรือProject Auditor

พื้นผิว

วิเคราะห์การรองรับอุปกรณ์ของเกมและตัดสินใจเลือกรูปแบบเท็กซ์เจอร์ที่ถูกต้อง คุณแยกแพ็กเกจพื้นผิวสำหรับอุปกรณ์ระดับสูงและระดับล่างได้โดยใช้ Play Asset Delivery, Addressable หรือกระบวนการที่กำหนดเองมากขึ้น ด้วย AssetBundle

ทำตามคำแนะนำที่รู้จักกันดีที่สุดซึ่งมีอยู่ใน เพิ่มประสิทธิภาพเกมบนอุปกรณ์เคลื่อนที่และในโพสต์สนทนา การเพิ่มประสิทธิภาพการตั้งค่าการนำเข้าพื้นผิวของ Unity จากนั้น ลองใช้วิธีแก้ปัญหาต่อไปนี้

  • บีบอัดพื้นผิวด้วยรูปแบบ ASTC เพื่อลดร่องรอยหน่วยความจำ และทดลองใช้บล็อกเรตที่สูงขึ้น เช่น 8x8

    หากจำเป็นต้องใช้ ETC2 ให้แพ็กเท็กซ์เจอร์ใน Atlas การวางเท็กซ์เจอร์หลายรายการลงในเท็กซ์เจอร์เดียวจะช่วยให้เท็กซ์เจอร์มีขนาดเป็นกำลังสอง (POT) ลดจำนวน การเรียกวาด และเพิ่มความเร็วในการแสดงผล

  • เพิ่มประสิทธิภาพรูปแบบและขนาดของเท็กซ์เจอร์ RenderTarget หลีกเลี่ยง พื้นผิวที่มีความละเอียดสูงโดยไม่จำเป็น การใช้พื้นผิวขนาดเล็กบนอุปกรณ์เคลื่อนที่จะช่วยประหยัดหน่วยความจำ

  • ใช้การจัดแพ็กเกจแชแนลพื้นผิวเพื่อประหยัดหน่วยความจำของพื้นผิว

Mesh และโมเดล

เริ่มต้นด้วยการตรวจสอบการตั้งค่าพื้นฐาน (หน้า 27) และ ยืนยันการตั้งค่าการนำเข้า Mesh ดังนี้

  • ผสานรวมเมชที่ซ้ำซ้อนและมีขนาดเล็ก
  • ลดจำนวนจุดยอดสำหรับออบเจ็กต์ในฉาก (เช่น ออบเจ็กต์แบบคงที่หรือออบเจ็กต์ที่อยู่ไกล ออกไป)
  • สร้างกลุ่มระดับรายละเอียด (LOD) สำหรับชิ้นงานที่มีรูปทรงเรขาคณิตสูง

วัสดุและ Shader

  • ลบตัวแปรเชเดอร์ที่ไม่ได้ใช้ออกโดยอัตโนมัติในระหว่างกระบวนการบิลด์
  • รวมตัวแปรเชเดอร์ที่ใช้บ่อยไว้ใน Uber Shader เพื่อหลีกเลี่ยงการทำซ้ำเชเดอร์
  • เปิดใช้การโหลด Shader แบบไดนามิกเพื่อแก้ไขร่องรอยหน่วยความจำขนาดใหญ่ของ Shader ที่โหลดล่วงหน้าใน VRAM/RAM อย่างไรก็ตาม โปรดสังเกตหากการคอมไพล์ Shader ทำให้เฟรมกระตุก
  • ใช้การโหลด Shader แบบไดนามิกเพื่อป้องกันไม่ให้โหลดตัวแปรทั้งหมด ดูข้อมูลเพิ่มเติมได้ที่บล็อกโพสต์การปรับปรุงเวลาในการสร้าง Shader และ การใช้งานหน่วยความจำ
  • ใช้การสร้างอินสแตนซ์ของวัสดุอย่างเหมาะสมโดยใช้ประโยชน์จาก MaterialPropertyBlocks

เสียง

เริ่มต้นด้วยการตรวจสอบการตั้งค่าพื้นฐาน (หน้า 41) และ ยืนยันการตั้งค่าการนำเข้า Mesh ดังนี้

  • นำAudioClipการอ้างอิงที่ไม่ได้ใช้หรือซ้ำซ้อนออกเมื่อใช้เครื่องมือเสียงของบุคคลที่สาม เช่น FMOD หรือ Wwise
  • โหลดข้อมูลเสียงล่วงหน้า ปิดใช้การโหลดล่วงหน้าสำหรับคลิปที่ไม่จำเป็นต้องใช้ทันที ในระหว่างรันไทม์หรือการเริ่มต้นฉาก ซึ่งจะช่วยลดค่าใช้จ่ายด้านหน่วยความจำ ในระหว่างการเริ่มต้นฉาก

ภาพเคลื่อนไหว

  • ปรับการตั้งค่าการบีบอัดภาพเคลื่อนไหวของ Unity เพื่อลด จำนวนคีย์เฟรมและกำจัดข้อมูลที่ซ้ำกัน
    • การลดคีย์เฟรม: นำคีย์เฟรมที่ไม่จำเป็นออกโดยอัตโนมัติ
    • การบีบอัดควอเทอร์เนียน: บีบอัดข้อมูลการหมุนเพื่อลดการใช้หน่วยความจำ

คุณปรับการตั้งค่าการบีบอัดได้ในการตั้งค่านำเข้าภาพเคลื่อนไหวใต้แท็บริกหรือภาพเคลื่อนไหว

  • นำคลิปภาพเคลื่อนไหวมาใช้ซ้ำแทนการทำซ้ำคลิปภาพเคลื่อนไหวสำหรับออบเจ็กต์ต่างๆ

    ใช้ Animator Override Controllers เพื่อนำ Animator Controller กลับมาใช้ซ้ำและแทนที่คลิปที่เฉพาะเจาะจงสำหรับตัวละครต่างๆ

  • อบภาพเคลื่อนไหวตามหลักฟิสิกส์: หากภาพเคลื่อนไหวของคุณขับเคลื่อนด้วยฟิสิกส์หรือเป็นแบบขั้นตอน ให้อบภาพเคลื่อนไหวลงในคลิปภาพเคลื่อนไหวเพื่อหลีกเลี่ยงการคำนวณรันไทม์

  • เพิ่มประสิทธิภาพริกโครงกระดูก: ใช้กระดูกในริกให้น้อยลงเพื่อลดความซับซ้อนและ การใช้หน่วยความจำ

    • หลีกเลี่ยงการใช้กระดูกมากเกินไปสำหรับออบเจ็กต์ขนาดเล็กหรือออบเจ็กต์แบบคงที่
    • หากกระดูกบางส่วนไม่ได้เคลื่อนไหวหรือไม่มีความจำเป็น ให้นำกระดูกเหล่านั้นออกจากริก
  • ลดความยาวคลิปภาพเคลื่อนไหว

    • ตัดคลิปภาพเคลื่อนไหวให้มีเฉพาะเฟรมที่จำเป็น หลีกเลี่ยงการจัดเก็บภาพเคลื่อนไหวที่ไม่ได้ใช้หรือยาวเกินไป
    • ใช้ภาพเคลื่อนไหวแบบวนซ้ำแทนการสร้างคลิปยาวสำหรับการเคลื่อนไหวซ้ำๆ
  • ตรวจสอบว่าได้แนบหรือเปิดใช้งานคอมโพเนนต์ภาพเคลื่อนไหวเพียงรายการเดียว เช่น ปิดใช้หรือนำคอมโพเนนต์ภาพเคลื่อนไหวเดิมออกหากคุณ ใช้ Animator

  • หลีกเลี่ยงการใช้ Animator หากไม่จำเป็น สำหรับ VFX แบบง่าย ให้ใช้ไลบรารีการเคลื่อนไหวระหว่างเฟรม หรือใช้เอฟเฟกต์ภาพในสคริปต์ ระบบ Animator อาจใช้ทรัพยากรมาก โดยเฉพาะในอุปกรณ์เคลื่อนที่ระดับล่าง

  • ใช้ระบบงานสำหรับภาพเคลื่อนไหวเมื่อต้องจัดการภาพเคลื่อนไหวจำนวนมาก เนื่องจากระบบดังกล่าวได้รับการออกแบบใหม่ทั้งหมดเพื่อให้ประหยัดหน่วยความจำมากขึ้น

ฉาก

เมื่อโหลดฉากใหม่ ระบบจะนำเข้าชิ้นงานเป็นทรัพยากร Dependency อย่างไรก็ตาม หากไม่มีการจัดการวงจรของเนื้อหาที่เหมาะสม ระบบจะไม่ตรวจสอบการอ้างอิงเหล่านี้ ด้วยตัวนับการอ้างอิง ด้วยเหตุนี้ ชิ้นงานจึงอาจยังคงอยู่ในหน่วยความจำแม้หลังจากที่ฉากที่ไม่ได้ใช้ถูกนำออกแล้ว ซึ่งจะทำให้เกิดการกระจายหน่วยความจำ

  • ใช้การจัดกลุ่มออบเจ็กต์ของ Unity เพื่อนำอินสแตนซ์ GameObject มาใช้ซ้ำสำหรับองค์ประกอบการเล่นเกมที่เกิดซ้ำ เนื่องจาก Object Pooling ใช้สแต็กเพื่อเก็บคอลเล็กชันของอินสแตนซ์ออบเจ็กต์เพื่อนำมาใช้ซ้ำและไม่ปลอดภัยสำหรับเธรด การลดขนาด InstantiateและDestroyจะช่วยปรับปรุงทั้งประสิทธิภาพของ CPU และความเสถียรของหน่วยความจำ
  • การเลิกโหลดชิ้นงาน
    • ยกเลิกการโหลดชิ้นงานอย่างมีกลยุทธ์ในช่วงเวลาที่สำคัญน้อยกว่า เช่น หน้าจอเริ่มต้นหรือหน้าจอการโหลด
    • การใช้ Resources.UnloadUnusedAssets บ่อยครั้งจะทำให้การประมวลผล CPU เพิ่มขึ้น เนื่องจากการดำเนินการตรวจสอบการอ้างอิงภายในขนาดใหญ่
    • ตรวจสอบการเพิ่มขึ้นของ CPU อย่างมากในเครื่องหมายโปรไฟล์ GC.MarkDependencies นำออกหรือลดความถี่ในการดำเนินการ และยกเลิกการโหลดทรัพยากรที่เฉพาะเจาะจงด้วยตนเองแทนโดยใช้ Resources.UnloadAsset แทนที่จะ อาศัยResources.UnloadUnusedAssets() ที่ครอบคลุมทุกอย่าง
  • ปรับโครงสร้างฉากแทนการใช้ Resources.UnloadUnusedAssets อยู่ตลอดเวลา
  • การเรียกใช้ Resources.UnloadUnusedAssets() สำหรับ Addressables อาจยกเลิกการโหลดแพ็กเกจที่โหลดแบบไดนามิกโดยไม่ได้ตั้งใจ จัดการวงจรของชิ้นงานที่โหลดแบบไดนามิกอย่างรอบคอบ

เบ็ดเตล็ด

  • การกระจายที่เกิดจากการเปลี่ยนฉาก - เมื่อมีการเรียกใช้เมธอด Resources.UnloadUnusedAssets() Unity จะทำสิ่งต่อไปนี้

    • เพิ่มหน่วยความจำสำหรับชิ้นงานที่ไม่ได้ใช้งานแล้ว
    • เรียกใช้การดำเนินการที่คล้ายกับตัวเก็บขยะเพื่อตรวจสอบฮีปของออบเจ็กต์ที่มีการจัดการและฮีปของออบเจ็กต์เนทีฟ เพื่อหาชิ้นงานที่ไม่ได้ใช้และยกเลิกการโหลด
    • ล้างหน่วยความจำของพื้นผิว, Mesh และชิ้นงาน หากไม่มีการอ้างอิงที่ใช้งานอยู่
  • AssetBundle หรือ Addressable - การเปลี่ยนแปลงในส่วนนี้มีความซับซ้อนและ ต้องอาศัยความร่วมมือจากทีมในการใช้กลยุทธ์ อย่างไรก็ตาม เมื่อเชี่ยวชาญกลยุทธ์เหล่านี้แล้ว กลยุทธ์เหล่านี้จะช่วยปรับปรุงการใช้หน่วยความจำ ลดขนาดการดาวน์โหลด และลดต้นทุนระบบคลาวด์ได้อย่างมาก ดูข้อมูลเพิ่มเติมเกี่ยวกับ การจัดการชิ้นงานใน Unity ได้ที่ Addressables

  • ทรัพยากร Dependency ที่แชร์แบบรวมศูนย์ &mdash: จัดกลุ่มทรัพยากร Dependency ที่แชร์ เช่น Shader, เท็กซ์เจอร์ และแบบอักษร อย่างเป็นระบบลงในแพ็กเกจหรือAddressableกลุ่มเฉพาะ ซึ่งจะช่วยลดการทำซ้ำและทำให้มั่นใจได้ว่าระบบจะเลิกโหลดชิ้นงานที่ไม่จำเป็นได้อย่างมีประสิทธิภาพ

  • ใช้ Addressables สำหรับการติดตามทรัพยากร Dependency - Addressables ช่วยให้การโหลดและเลิกโหลดง่ายขึ้น และสามารถเลิกโหลดทรัพยากร Dependency ที่ ไม่มีการอ้างอิงอีกต่อไปได้โดยอัตโนมัติ การเปลี่ยนไปใช้ Addressables สำหรับการจัดการเนื้อหา และการแก้ปัญหาการขึ้นต่อกันอาจเป็นโซลูชันที่ใช้ได้ ทั้งนี้ขึ้นอยู่กับกรณีเฉพาะของเกม วิเคราะห์ห่วงโซ่การอ้างอิงด้วยเครื่องมือวิเคราะห์เพื่อระบุรายการที่ซ้ำกันหรือการอ้างอิงที่ไม่จำเป็น หรือดูเครื่องมือข้อมูล Unity หากคุณใช้ AssetBundles

  • TypeTrees - หากคุณสร้างและติดตั้งใช้งาน Addressables และ AssetBundles ของเกมโดยใช้ Unity เวอร์ชันเดียวกับเพลเยอร์ และไม่จำเป็นต้องมีความเข้ากันได้แบบย้อนหลังกับบิลด์เพลเยอร์อื่นๆ ให้พิจารณาปิดใช้การเขียน TypeTree ซึ่งจะช่วยลดขนาดของบันเดิลและร่องรอยหน่วยความจำของออบเจ็กต์ไฟล์ที่ซีเรียลไลซ์ แก้ไขกระบวนการบิลด์ในการตั้งค่าแพ็กเกจ Addressables ในเครื่อง ContentBuildFlags เป็น DisableWriteTypeTree

เขียนโค้ดที่เป็นมิตรกับตัวเก็บขยะ

Unity ใช้การเก็บขยะ (GC) เพื่อจัดการหน่วยความจำโดยการระบุและเพิ่มหน่วยความจำที่ไม่ได้ใช้โดยอัตโนมัติ แม้ว่า GC จะมีความสำคัญ แต่ก็อาจทำให้เกิดปัญหาด้านประสิทธิภาพ (เช่น อัตราเฟรมพุ่งสูงขึ้น) หากไม่ได้รับการจัดการอย่างเหมาะสม เนื่องจากกระบวนการนี้อาจหยุดเกมชั่วคราว ซึ่งส่งผลให้ประสิทธิภาพไม่ราบรื่นและประสบการณ์ของผู้ใช้ไม่ดีเท่าที่ควร

ดูเทคนิคที่มีประโยชน์ในการลดความถี่ของการจัดสรรฮีปที่มีการจัดการได้ในคู่มือ Unity และดูตัวอย่างได้ในUnityPerformanceTuningBible หน้า 271

  • ลดการจัดสรรตัวเก็บขยะ

    • หลีกเลี่ยง LINQ, แลมบ์ดา และ Closure ซึ่งจัดสรรหน่วยความจำฮีป
    • ใช้ StringBuilder สำหรับสตริงที่เปลี่ยนแปลงได้แทนการต่อสตริง
    • นำคอลเล็กชันกลับมาใช้ซ้ำโดยเรียกใช้ COLLECTIONS.Clear() แทนการสร้างอินสแตนซ์ใหม่

    ดูข้อมูลเพิ่มเติมได้ใน eBook Ultimate Guide to Profiling Unity games

  • จัดการการอัปเดต Canvas ของ UI

    • การเปลี่ยนแปลงองค์ประกอบ UI แบบไดนามิก - เมื่อมีการอัปเดตองค์ประกอบ UI เช่น ข้อความ รูปภาพ หรือพร็อพเพอร์ตี้ RectTransform (เช่น การเปลี่ยนเนื้อหาข้อความ การปรับขนาดองค์ประกอบ หรือการเคลื่อนไหวตำแหน่ง) เครื่องมืออาจจัดสรรหน่วยความจำสำหรับออบเจ็กต์ชั่วคราว
    • การจัดสรรสตริง - องค์ประกอบ UI เช่น ข้อความ มักต้องมีการอัปเดตสตริง เนื่องจากสตริงไม่สามารถเปลี่ยนแปลงได้ในภาษาโปรแกรมส่วนใหญ่
    • Canvas ที่มีการเปลี่ยนแปลง — เมื่อมีการเปลี่ยนแปลงใน Canvas (เช่น การปรับขนาด การเปิดและปิดใช้องค์ประกอบ หรือการแก้ไขพร็อพเพอร์ตี้เลย์เอาต์) ระบบอาจทําเครื่องหมายทั้ง Canvas หรือบางส่วนเป็นมีการเปลี่ยนแปลงและสร้างใหม่ ซึ่งอาจทําให้เกิด การสร้างโครงสร้างข้อมูลชั่วคราว (เช่น ข้อมูลตาข่าย บัฟเฟอร์จุดยอด หรือการคํานวณเลย์เอาต์) ซึ่งจะเพิ่มการสร้างขยะ
    • การอัปเดตที่ซับซ้อนหรือบ่อยครั้ง - หาก Canvas มีองค์ประกอบจำนวนมากหรือมีการอัปเดตบ่อยครั้ง (เช่น ทุกเฟรม) การสร้างใหม่เหล่านี้อาจทำให้เกิดการใช้หน่วยความจำมาก
  • เปิดใช้ GC แบบเพิ่มทีละส่วนเพื่อลดการเพิ่มขึ้นของการรวบรวมขนาดใหญ่โดย กระจายการล้างการจัดสรรในหลายเฟรม โปรไฟล์เพื่อตรวจสอบว่าตัวเลือกนี้ช่วยปรับปรุงประสิทธิภาพและร่องรอยหน่วยความจำของเกมหรือไม่

  • หากเกมของคุณต้องใช้แนวทางที่ควบคุมได้ ให้ตั้งค่าโหมด การเก็บขยะเป็นแบบแมนนวล จากนั้นเมื่อมีการเปลี่ยนเลเวลหรือในเวลาอื่น ที่ไม่มีการเล่นเกมที่ใช้งานอยู่ ให้เรียกใช้การเก็บขยะ

  • เรียกใช้การเรียกเก็บขยะด้วยตนเอง GC.Collect() สำหรับการเปลี่ยนสถานะของเกม (เช่น การเปลี่ยนเลเวล)

  • เพิ่มประสิทธิภาพอาร์เรย์โดยเริ่มจากแนวทางปฏิบัติในการเขียนโค้ดแบบง่าย และหากจำเป็น ให้ใช้อาร์เรย์ดั้งเดิมหรือคอนเทนเนอร์ดั้งเดิมอื่นๆ สำหรับอาร์เรย์ขนาดใหญ่

  • ตรวจสอบออบเจ็กต์ที่จัดการโดยใช้เครื่องมือต่างๆ เช่น Unity Memory Profiler เพื่อติดตาม การอ้างอิงออบเจ็กต์ที่ไม่ได้จัดการซึ่งยังคงอยู่หลังจากการทำลาย

    ใช้ Profiler Marker เพื่อส่งไปยังเครื่องมือรายงาน ประสิทธิภาพสำหรับแนวทางอัตโนมัติ

หลีกเลี่ยงการรั่วไหลและการแตกกระจายของหน่วยความจำ

หน่วยความจำรั่วไหล

ในโค้ด C# เมื่อมีการอ้างอิงถึงออบเจ็กต์ Unity หลังจาก ทำลายออบเจ็กต์แล้ว ออบเจ็กต์ Wrapper ที่มีการจัดการซึ่งรู้จักกันในชื่อManaged Shell จะยังคงอยู่ในหน่วยความจำ ระบบจะปล่อยหน่วยความจำดั้งเดิมที่เชื่อมโยงกับการอ้างอิงเมื่อเลิกโหลดฉาก หรือเมื่อทำลาย GameObject ที่เชื่อมโยงหน่วยความจำ หรือออบเจ็กต์ระดับบนสุดของ GameObject ผ่านเมธอด Destroy() อย่างไรก็ตาม หากไม่ได้ล้างข้อมูลอ้างอิงอื่นๆ ของ Scene หรือ GameObject หน่วยความจำที่มีการจัดการอาจยังคงเป็นออบเจ็กต์เปลือกที่รั่วไหล ดูรายละเอียดเพิ่มเติมเกี่ยวกับออบเจ็กต์ Managed Shell ได้ที่คู่มือออบเจ็กต์ Managed Shell

นอกจากนี้ หน่วยความจำรั่วอาจเกิดจากการสมัครรับข้อมูลเหตุการณ์ แลมบ์ดาและ การปิด การต่อสตริง และการจัดการออบเจ็กต์ที่จัดกลุ่มอย่างไม่เหมาะสม

  • หากต้องการเริ่มต้นใช้งาน โปรดดูหัวข้อค้นหาหน่วยความจำรั่วเพื่อเปรียบเทียบสแนปชอตหน่วยความจำของ Unity อย่างถูกต้อง
  • ตรวจสอบการสมัครใช้บริการเหตุการณ์และหน่วยความจำรั่ว หากออบเจ็กต์สมัครรับข้อมูลเหตุการณ์ (เช่น โดยใช้ตัวแทนหรือ UnityEvents) แต่ไม่ได้ยกเลิกการสมัครรับข้อมูลอย่างถูกต้อง ก่อนที่จะถูกทำลาย ตัวจัดการหรือผู้เผยแพร่เหตุการณ์อาจเก็บการอ้างอิง ไปยังออบเจ็กต์เหล่านั้นไว้ ซึ่งจะป้องกันไม่ให้ระบบเก็บขยะของออบเจ็กต์เหล่านั้น ซึ่งจะทำให้เกิดการรั่วไหลของหน่วยความจำ
  • ตรวจสอบเหตุการณ์ของคลาสส่วนกลางหรือคลาสเดี่ยวที่ไม่ได้ยกเลิกการลงทะเบียนเมื่อทำลายออบเจ็กต์ เช่น ยกเลิกการสมัครรับข้อมูลหรือยกเลิกการเชื่อมต่อตัวแทนในตัวทำลายออบเจ็กต์
  • ตรวจสอบว่าการทำลายออบเจ็กต์ที่จัดกลุ่มจะทำให้การอ้างอิงถึงคอมโพเนนต์ข้อความแบบเมช พื้นผิว และ GameObject หลักเป็นค่าว่างอย่างสมบูรณ์
  • โปรดทราบว่าเมื่อเปรียบเทียบสแนปชอตของ Unity Memory Profiler และสังเกตเห็นความแตกต่างในการใช้หน่วยความจำโดยไม่มีเหตุผลที่ชัดเจน ความแตกต่างอาจเกิดจากไดรเวอร์กราฟิกหรือระบบปฏิบัติการเอง

การกระจายหน่วยความจำ

การกระจายหน่วยความจำเกิดขึ้นเมื่อมีการปล่อยการจัดสรรขนาดเล็กจำนวนมากตามลำดับแบบสุ่ม การจัดสรรฮีปจะทำตามลำดับ ซึ่งหมายความว่าระบบจะสร้างกลุ่มหน่วยความจำใหม่เมื่อกลุ่มก่อนหน้ามีพื้นที่ไม่เพียงพอ ดังนั้น ออบเจ็กต์ใหม่จึงไม่เติมเต็มพื้นที่ว่างของก้อนข้อมูลเก่า ซึ่งทำให้เกิดการกระจาย นอกจากนี้ การจัดสรรชั่วคราวขนาดใหญ่อาจทำให้เกิดการกระจายตัวถาวรตลอดระยะเวลา เซสชันของเกม

ปัญหานี้จะร้ายแรงเป็นพิเศษเมื่อมีการจัดสรรขนาดใหญ่ที่มีอายุสั้น ใกล้กับการจัดสรรที่มีอายุยาว

จัดสรรกลุ่มตามอายุการใช้งาน โดยควรจัดสรรการใช้งานที่มีอายุการใช้งานยาวนาน ร่วมกันตั้งแต่เนิ่นๆ ในวงจรของแอปพลิเคชัน

ผู้สังเกตการณ์และผู้จัดการกิจกรรม

  • นอกเหนือจากปัญหาที่กล่าวถึงในส่วน (Memory Leaks)77 แล้ว เมื่อเวลาผ่านไป Memory Leak อาจทำให้เกิดการแตกกระจายโดยการปล่อยให้หน่วยความจำที่ไม่ได้ใช้งาน จัดสรรให้กับออบเจ็กต์ที่ไม่ได้ใช้งานอีกต่อไป
  • ตรวจสอบว่าการทำลายออบเจ็กต์ที่จัดกลุ่มจะทำให้การอ้างอิงถึงคอมโพเนนต์ของข้อความ พื้นผิว และ GameObjects ระดับบนสุดเป็นค่าว่างโดยสมบูรณ์
  • ผู้จัดการกิจกรรมมักสร้างและจัดเก็บรายการหรือพจนานุกรมเพื่อจัดการการสมัครรับข้อมูลกิจกรรม หากมีการขยายและลดขนาดแบบไดนามิกระหว่างรันไทม์ อาจทำให้เกิดการกระจายหน่วยความจำเนื่องจากการจัดสรรและยกเลิกการจัดสรรบ่อยครั้ง

โค้ด

  • บางครั้ง Coroutine จะจัดสรรหน่วยความจำ ซึ่งหลีกเลี่ยงได้ง่ายๆ โดยการแคชคำสั่ง return ของ IEnumerator แทนที่จะประกาศคำสั่งใหม่ทุกครั้ง
  • ตรวจสอบสถานะวงจรของออบเจ็กต์ที่จัดกลุ่มอย่างต่อเนื่องเพื่อหลีกเลี่ยงการเก็บUnityEngine.Objectการอ้างอิงที่ไม่มีอยู่จริง

ชิ้นงาน

  • ใช้ระบบสำรองแบบไดนามิกสำหรับประสบการณ์การเล่นเกมที่ขับเคลื่อนด้วยข้อความเพื่อหลีกเลี่ยง การโหลดแบบอักษรทั้งหมดล่วงหน้าสำหรับกรณีที่มีหลายภาษา
  • จัดระเบียบชิ้นงาน (เช่น พื้นผิวและอนุภาค) ตามประเภทและ วงจรที่คาดไว้
  • ย่อชิ้นงานที่มีแอตทริบิวต์วงจรที่ไม่ใช้งาน เช่น รูปภาพ UI ที่ซ้ำกันและ ตาข่ายแบบคงที่

การจัดสรรตามอายุ

  • จัดสรรชิ้นงานที่มีอายุการใช้งานยาวนานตั้งแต่เริ่มต้นวงจรแอปพลิเคชันเพื่อให้มั่นใจว่า การจัดสรรมีขนาดกะทัดรัด
  • ใช้ NativeCollections หรือตัวจัดสรรที่กำหนดเองสำหรับโครงสร้างข้อมูลที่ใช้หน่วยความจำมากหรือชั่วคราว (เช่น คลัสเตอร์ฟิสิกส์)

นอกจากนี้ ไฟล์ปฏิบัติการของเกมและปลั๊กอินยังส่งผลต่อการใช้หน่วยความจำด้วย

ข้อมูลเมตา IL2CPP

IL2CPP จะสร้างข้อมูลเมตาสำหรับทุกประเภท (เช่น คลาส Generic และ Delegate) ในเวลาบิลด์ ซึ่งจะใช้ในเวลาเรียกใช้สำหรับการสะท้อน การตรวจสอบประเภท และการดำเนินการอื่นๆ ที่เฉพาะเจาะจงเวลาเรียกใช้ ข้อมูลเมตานี้จะจัดเก็บไว้ในหน่วยความจำและอาจส่งผลต่อร่องรอยหน่วยความจำทั้งหมดของแอปพลิเคชันอย่างมาก แคชข้อมูลเมตาของ IL2CPP มีส่วนสำคัญในการเริ่มต้นและเวลาในการโหลด นอกจากนี้ IL2CPP ยังไม่ขจัดข้อมูลที่ซ้ำกันขององค์ประกอบข้อมูลเมตาบางอย่าง (เช่น ประเภททั่วไปหรือข้อมูลที่ทำให้เป็นอนุกรม) ซึ่งอาจส่งผลให้การใช้หน่วยความจำเพิ่มขึ้น ปัญหานี้จะรุนแรงขึ้นเมื่อมีการใช้ประเภทซ้ำๆ หรือซ้ำซ้อนในโปรเจ็กต์

คุณลดข้อมูลเมตา IL2CPP ได้โดยทำดังนี้

  • หลีกเลี่ยงการใช้ Reflection API เนื่องจากอาจเป็น ปัจจัยสำคัญที่ทำให้เกิดการจัดสรรข้อมูลเมตาของ IL2CPP
  • การปิดใช้แพ็กเกจในตัว
  • การใช้ full generic sharing ของ Unity 2022 ซึ่งควรช่วยลดค่าใช้จ่ายที่เกิดจาก Generics อย่างไรก็ตาม หากต้องการช่วยลดการจัดสรรเพิ่มเติม ให้ลดการใช้ Generics

การลบโค้ด

นอกเหนือจากการลดขนาดบิลด์แล้ว การลบโค้ดยังช่วยลดการใช้หน่วยความจำด้วย เมื่อสร้างเทียบกับแบ็กเอนด์การเขียนสคริปต์ IL2CPP การลบไบต์โค้ดที่มีการจัดการ (ซึ่งเปิดใช้งานโดยค่าเริ่มต้น) จะนำโค้ดที่ไม่ได้ใช้ออกจากแอสเซมบลีที่มีการจัดการ กระบวนการนี้ทำงานโดยการกำหนดแอสเซมบลีรูท แล้วใช้การวิเคราะห์โค้ดแบบคงที่เพื่อพิจารณาว่าโค้ดที่มีการจัดการอื่นๆ ที่แอสเซมบลีรูทเหล่านั้นใช้คืออะไร ระบบจะนำโค้ดที่เข้าถึงไม่ได้ออก ดูข้อมูลเพิ่มเติมเกี่ยวกับการลบโค้ดที่มีการจัดการได้ที่บล็อกโพสต์TTales from the optimization trenches: Better managed code stripping with Unity 2020 LTS และเอกสารประกอบการลบโค้ดที่มีการจัดการ

เครื่องมือจัดสรรเริ่มต้น

ทดลองใช้ตัวจัดสรรหน่วยความจำของระบบเพื่อปรับแต่งตัวจัดสรรหน่วยความจำ หากเกมมีหน่วยความจำเหลือน้อย ให้ใช้บล็อกหน่วยความจำขนาดเล็ก แม้ว่า การดำเนินการนี้จะเกี่ยวข้องกับตัวจัดสรรที่ช้ากว่าก็ตาม ดูข้อมูลเพิ่มเติมได้ที่ตัวอย่าง ตัวจัดสรรฮีปแบบไดนามิก

จัดการปลั๊กอินและ SDK ดั้งเดิม

  • ค้นหาปลั๊กอินที่มีปัญหา - นำปลั๊กอินแต่ละตัวออกและเปรียบเทียบภาพรวมหน่วยความจำของเกม ซึ่งเกี่ยวข้องกับการปิดใช้ฟังก์ชันการทำงานของโค้ดจำนวนมากด้วยการเขียนสคริปต์ กำหนดสัญลักษณ์และการปรับโครงสร้างคลาสที่มีการเชื่อมโยงสูงด้วย อินเทอร์เฟซ ดูยกระดับโค้ดด้วยรูปแบบการเขียนโปรแกรมเกมเพื่ออำนวยความสะดวกในกระบวนการปิดใช้ การอ้างอิงภายนอกโดยไม่ทำให้เกมเล่นไม่ได้

  • ติดต่อผู้เขียนปลั๊กอินหรือ SDK - ปลั๊กอินส่วนใหญ่ไม่ใช่โอเพนซอร์ส

  • จำลองการใช้งานหน่วยความจำของปลั๊กอิน - คุณสามารถเขียนปลั๊กอินอย่างง่าย (ใช้ปลั๊กอิน Unity นี้เป็นข้อมูลอ้างอิง) ที่ทำการจัดสรรหน่วยความจำ ตรวจสอบสแนปชอตหน่วยความจำโดยใช้ Android Studio (เนื่องจาก Unity ไม่ได้ติดตามการจัดสรรเหล่านี้) หรือเรียกใช้คลาส MemoryInfo และเมธอด Runtime.totalMemory() ในโปรเจ็กต์เดียวกัน

ปลั๊กอิน Unity จัดสรรหน่วยความจำ Java และหน่วยความจำดั้งเดิม โดยวิธีมีดังนี้

Java

byte[] largeObject = new byte[1024 * 1024 * megaBytes];
list.add(largeObject);

เนทีฟ

char* buffer = new char[megabytes * 1024 * 1024];

// Random data to fill the buffer
for (int i = 1; i < megabytes * 1024 * 1024; ++i) {
   buffer[i] = 'A' + (i % 26); // Fill with letters A-Z
}