แก้ไขข้อบกพร่องของ 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 Mode กับการรวมอย่างต่อเนื่อง (CI) เพื่อยืนยันการใช้หน่วยความจำในบางส่วนของเกมเป็นอีกกลยุทธ์หนึ่งที่จะช่วยให้คุณได้รับข้อมูลเชิงลึกที่ดีขึ้น

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

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

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

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

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

พื้นผิว

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

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

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

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

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

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

ตาข่ายและโมเดล

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

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

วัสดุและ Shader

  • ลบตัวแปรเชเดอร์ที่ไม่ได้ใช้ออกโดยอัตโนมัติในระหว่างกระบวนการบิลด์
  • รวม Shader Variant ที่ใช้บ่อยไว้ใน Uber Shader เพื่อหลีกเลี่ยงการทำซ้ำ 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 และความเสถียรของหน่วยความจำ
  • การนำชิ้นงานออก
    • ยกเลิกการโหลดชิ้นงานอย่างมีกลยุทธ์ในช่วงเวลาที่สำคัญน้อยกว่า เช่น หน้าจอ Splash หรือหน้าจอโหลด
    • การใช้ Resources.UnloadUnusedAssets บ่อยครั้งจะทำให้การประมวลผล CPU เพิ่มขึ้น เนื่องจากการดำเนินการตรวจสอบทรัพยากร Dependency ภายในขนาดใหญ่
    • ตรวจสอบช่วงที่ 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 สำหรับการจัดการเนื้อหาและการแก้ปัญหาทรัพยากร Dependency อาจเป็นโซลูชันที่ใช้ได้ ทั้งนี้ขึ้นอยู่กับกรณีเฉพาะของเกม วิเคราะห์ห่วงโซ่การอ้างอิงด้วยเครื่องมือวิเคราะห์เพื่อระบุรายการที่ซ้ำกันหรือการอ้างอิงที่ไม่จำเป็น หรือดูเครื่องมือข้อมูล Unity หากคุณใช้ AssetBundles

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

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

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

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

  • ลดการจัดสรร Garbage Collector

    • หลีกเลี่ยง 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 เครื่องมือสร้างโปรไฟล์หน่วยความจำ เพื่อติดตามการอ้างอิงออบเจ็กต์ที่ไม่มีการจัดการซึ่งยังคงอยู่หลังจากการทำลาย

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

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

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

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

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

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

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

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

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

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

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

  • นอกเหนือจากปัญหาที่กล่าวถึงในส่วนหน่วยความจำรั่วแล้ว เมื่อเวลาผ่านไป หน่วยความจำรั่วอาจทำให้เกิดการแตกกระจายโดยการปล่อยให้หน่วยความจำที่ไม่ได้ใช้ ซึ่งจัดสรรให้กับออบเจ็กต์ที่ไม่ได้ใช้งานแล้ว
  • ตรวจสอบว่าการทำลายออบเจ็กต์ที่จัดกลุ่มจะทำให้การอ้างอิงถึงคอมโพเนนต์ของข้อความ พื้นผิว และ 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
}