15.2 ข้อจำกัดมือถือและข้ามแพลตฟอร์ม

เฟส 15 · Optimization และมือถือ · เวลาเรียน: 25–45 h

ความจริงของมือถือ — thermal throttling, แบตเตอรี่, GPU หลายระดับ — และการปล่อยเกมเดียวข้ามมือถือ พีซี และคอนโซล

เกมที่สร้างด้วย Unity หรือ Unreal มักจะรันบน PC ของนักพัฒนาเป็นที่แรก ซึ่งไม่ขาดอะไรเลย มี RAM เหลือเฟือ มีการ์ดจอที่มีหน่วยความจำเป็นของตัวเอง มีปลั๊กไฟไม่จำกัด และมีพัดลมช่วยระบายความร้อนออกไป พอเกมเดียวกันต้องไปรันบนมือถือ สมมติฐานเกือบทั้งหมดนั้นพังทันที มือถือไม่มีพัดลม มีแบตเตอรี่แทนปลั๊กไฟ และมีชิปตัวเดียวทำหน้าที่แทนชิ้นส่วนหลายชิ้นของ PC พร้อมกัน บทนี้จะพูดถึงข้อจำกัดที่มาจากความต่างนี้ — thermal throttling, การกินแบต, เพดานหน่วยความจำที่เข้มงวด, GPU คนละแบบ, และหน้าจอกับอินพุตที่หลากหลายกว่า PC มาก — พร้อมทั้งวิธีปฏิบัติทางวิศวกรรมที่เป็นรูปธรรม ส่วนใหญ่เป็น C# สำหรับ Unity ที่ทำให้เกม live-service บนมือถือเล่นได้ลื่นทั้งบนมือถือราคาประหยัดอายุสามปีและเรือธงรุ่นใหม่ล่าสุดในเวลาเดียวกัน

1. ทำไมมือถือถึงไม่ใช่ PC ตัวเล็ก

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

เกือบทุกชิ้นส่วนใน PC สำหรับเล่นเกมเป็นชิ้นส่วนแยกกันทางกายภาพ: CPU, การ์ดจอที่มีหน่วยความจำวิดีโอ (VRAM) ของตัวเอง, แหล่งจ่ายไฟที่เสียบเข้าปลั๊กไฟ, และพัดลมหรือ heat pipe ที่พาความร้อนออกจากเคส มือถือบีบอัด CPU, GPU, และตัวควบคุมหน่วยความจำเข้าไปในชิปตัวเดียวที่เรียกว่า SoC (system on a chip — ชิปที่รวมชิ้นส่วนหลักของคอมพิวเตอร์ทั้งหมดผลิตมาด้วยกันบนซิลิคอนแผ่นเดียว) รันด้วยแบตเตอรี่ ปิดผนึกอยู่ในเคสที่ไม่มีพัดลมเลย เพราะ CPU กับ GPU ใช้ชิปหน่วยความจำตัวเดียวกันแทนที่ GPU จะมี VRAM แยกเป็นของตัวเอง พูลหน่วยความจำที่ใช้ร่วมกันนี้มักถูกเรียกว่า unified memory

typical gaming PC typical mid-range phone cooling fans + large heatsink none -- sealed case, no fan power source wall outlet, unlimited one battery, a few thousand mAh CPU + GPU two separate chips one SoC (system on a chip) memory 16-32 GB RAM, plus 4-6 GB RAM, shared between separate dedicated VRAM CPU and GPU (unified memory) over-budget memory OS slows down, may use OS kills the app outright, disk as overflow space no warning screen shape one flat rectangle notches, rounded corners, gesture bars, many sizes

ความต่างแต่ละอย่างนี้กลายเป็นข้อจำกัดทางวิศวกรรมจริง ๆ และบทนี้จะพูดถึงแต่ละอย่างตามลำดับ: ความร้อนที่ไม่มีทางไป (thermal throttling), แบตเตอรี่ที่หมดเร็วขึ้นเมื่อชิปทำงานหนักขึ้น, ขีดจำกัดหน่วยความจำที่ระบบปฏิบัติการบังคับใช้ด้วยการปิดเกมของคุณทิ้ง, GPU ที่สร้างขึ้นบนหลักการเรนเดอร์ที่ต่างจาก GPU ของ PC โดยสิ้นเชิง, และความหลากหลายของหน้าจอกับวิธีการอินพุตที่มากกว่าที่ PC ต้องรับมือมาก เกม RPG live-service บนมือถือต้องรับมือกับทั้งหมดนี้พร้อมกัน ทุกวัน บนอุปกรณ์จริงนับล้านเครื่องที่ไม่เคยถูกทดสอบโดยตรงมาก่อน

2. Thermal Throttling: ชิปหรี่ตัวเองลง

Thermal throttling คือการที่ firmware ของชิปตั้งใจลด clock speed ของ CPU/GPU (จำนวนคำสั่งต่อวินาทีที่ชิปรันได้) ลง เมื่อเซนเซอร์วัดอุณหภูมิภายในข้ามเกณฑ์ความปลอดภัยที่ตั้งไว้ นี่ไม่ใช่บัค และไม่ใช่สิ่งที่คุณสามารถปิดได้จากโค้ดของเกม — มันคือกลไกป้องกันที่ฝังอยู่ในฮาร์ดแวร์เพื่อไม่ให้ชิปทำร้ายตัวเองหรือทำให้มือผู้เล่นร้อนจนไหม้

PC จัดการความร้อนด้วยอากาศที่เคลื่อนที่: พัดลมพัดอากาศเย็นผ่าน heatsink พาความร้อนออกจากเคส มือถือไม่มีสิ่งเหล่านั้นเลย ทางเดียวที่ความร้อนจะออกจากมือถือได้คือการนำความร้อนผ่านตัวเครื่องออกสู่อากาศรอบข้างอย่างช้า ๆ ซึ่งช้ากว่าพัดลมมาก รันชิปหนัก ๆ ไปสักสองสามนาที ความร้อนจะสะสมเร็วกว่าที่มันระบายออกได้ ดังนั้น firmware จึงลด clock speed ลงจนอุณหภูมิคงที่

ส่วนที่อันตรายสำหรับนักพัฒนาคือ throttling จะแสดงผลก็ต่อเมื่อมีโหลดต่อเนื่องเท่านั้น นี่คือสคริปต์ง่าย ๆ ที่บันทึกค่าเฉลี่ย frames per second (fps) ทุกช่วง 30 วินาที เพื่อให้คุณเห็นสิ่งที่เกิดขึ้นจริงตลอดทั้งเซสชัน ไม่ใช่แค่ช่วงเวลาเดียว:

using UnityEngine;

public class FpsSessionLogger : MonoBehaviour
{
    float windowTime = 0f;
    int windowFrames = 0;
    float windowFpsSum = 0f;

    void Update()
    {
        float fps = 1f / Time.unscaledDeltaTime;
        windowFpsSum += fps;
        windowFrames++;
        windowTime += Time.unscaledDeltaTime;

        if (windowTime >= 30f)
        {
            float avgFps = windowFpsSum / windowFrames;
            Debug.Log("avg fps this 30s window: " + avgFps.ToString("F1"));
            windowTime = 0f;
            windowFrames = 0;
            windowFpsSum = 0f;
        }
    }
}

ตัวอย่าง log ที่เป็นตัวแทนจากมือถือระดับกลางจริง ๆ ที่เล่นฉากที่กราฟิกหนักเป็นเวลา 20 นาที อาจมีหน้าตาแบบนี้ (ตัวเลขจริงต่างกันไปตามอุปกรณ์ แต่รูปแบบนี้พบได้บ่อยมาก):

[0:30] avg fps: 59.8
[1:00] avg fps: 59.6
[1:30] avg fps: 58.1
[2:00] avg fps: 54.3
[2:30] avg fps: 47.9
[3:00] avg fps: 41.2
[3:30] avg fps: 33.6
[4:00] avg fps: 30.4
[4:30] avg fps: 30.1
[10:00] avg fps: 29.9
[20:00] avg fps: 29.8
fps 60 |** | ** 50 | *** | *** 40 | **** | *** 30 | *********************************** | 0 +------------------------------------------------------------- 0 1 2 3 4 5 6 8 10 15 20 (minutes) [ 30s benchmark ] --> "60 fps! ship it." a real play session keeps going past that point: the chip is now hot, clock speed has been throttled down by firmware, and fps settles around 30 for the rest of the session

30 วินาทีแรกดูดีมาก — การรัน benchmark ระยะสั้นแบบนี้จะรายงานค่าใกล้ 60 fps และผ่าน แต่ ณ จุดนั้นชิปยังไม่มีเวลาร้อนขึ้นเลย พอถึงนาทีที่สามหรือสี่ โหลดต่อเนื่องทำให้อุณหภูมิของชิปสูงพอที่ firmware throttling จะเริ่มทำงาน และ frame rate จะตกลงมาคงที่ที่ค่าใหม่ที่ต่ำกว่า อย่างต่อเนื่อง ตลอดเซสชันที่เหลือ เซสชันของ RPG live-service มักจะยาว 15-30 นาทีต่อครั้ง (ดันเจี้ยนรายวัน, อีเวนต์, บอสไฟต์) — นานพอที่ throttling จะมีผลบนอุปกรณ์จริงเกือบทุกเครื่อง ทุกวัน

ข้อผิดพลาดที่พบบ่อย เชื่อตัวเลข "average fps" เดียวจากเครื่องมือ benchmark สั้น ๆ โดยไม่ดูว่ามันเปลี่ยนแปลงยังไงในแต่ละนาที benchmark ยาว 20-30 วินาทีเกือบทุกครั้งจะรันก่อนที่ชิปจะร้อนเต็มที่ ดังนั้นมันวัด กรณีที่ดีที่สุด ของเกม ไม่ใช่สิ่งที่ผู้เล่นเจอจริงตอนสิบนาทีเข้าไปในเซสชันจริง
Tip ทดสอบด้วยเซสชันการเล่นอย่างน้อย 15-20 นาทีบนฮาร์ดแวร์จริงเสมอ ไม่ใช่ simulator หรือคลิปสั้น ๆ และดูกราฟ frame time ตลอดช่วงเวลานั้น ไม่ใช่แค่ไม่กี่วินาทีแรก

3. การกินแบตและทำไมผู้เล่นถึงเลิกเล่นเกมที่ "ทำมือถือร้อน"

การรัน CPU และ GPU ที่ clock speed สูงไม่ได้แค่สร้างความร้อนเท่านั้น — ความร้อนคือส่วนของพลังงานไฟฟ้าที่ชิปดึงมาแล้วสูญเสียไป ยิ่งชิปทำงานหนักเท่าไหร่ มันก็ยิ่งดึงพลังงานจากแบตเตอรี่มากขึ้นและสร้างความร้อนมากขึ้นในเวลาเดียวกัน ด้วยสาเหตุเดียวกัน thermal throttling (ข้อ 2) และการกินแบตเป็นอาการสองอย่างจากสาเหตุเดียวกันเป๊ะ ๆ

แบตเตอรี่มือถือมักถูกระบุหน่วยเป็น mAh (milliamp-hours หน่วยของประจุไฟฟ้า — พูดง่าย ๆ คือแบตเตอรี่จ่ายกระแสไฟได้เท่าไหร่นานแค่ไหน) ที่แรงดันไฟตามมาตรฐานประมาณ 3.85V คูณสองค่านี้เข้าด้วยกันจะได้พลังงานรวมของแบตเตอรี่เป็น Wh (watt-hours):

battery capacity   = 4000 mAh = 4.0 Ah
nominal voltage    = 3.85 V
total energy       = 4.0 Ah * 3.85 V = 15.4 Wh

demanding 3D game drawing ~4.0 W continuously:
  playtime = 15.4 Wh / 4.0 W = 3.85 hours

well-optimized game drawing ~2.0 W continuously:
  playtime = 15.4 Wh / 2.0 W = 7.7 hours

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

ผู้เล่นสังเกตเห็นสองอย่างที่ต่างกันเมื่อเกมกินพลังงานมากเกินไป และทั้งสองอย่างทำร้าย retention:

ยังมีผลที่สามที่เงียบกว่า: เกมที่กินพลังงานมากพอจะกระตุ้น thermal throttling (ข้อ 2) จะทำให้ตัวเองรู้สึกแย่ลงในเวลาเดียวกับที่มันกินแบตเร็วที่สุด เพราะ frame rate ตกลงพอดีตอนที่มือถือร้อนที่สุด ประสบการณ์ที่แย่ที่สุดของผู้เล่นกับความไม่อยากเล่นต่อของพวกเขาเกิดขึ้นพร้อมกัน ไม่ได้แยกกัน

Tip สำหรับเกม live-service ที่สร้างขึ้นรอบเซสชันรายวันสั้น ๆ (เควสประจำวัน, ดันเจี้ยน, อีเวนต์) เซสชันที่ทำให้มือถือร้อนและแบตหมดเห็นได้ชัด ทำงานตรงข้ามกับเป้าหมายที่อยากให้ผู้เล่นเปิดแอปอีกครั้งในวันเดียวกัน

4. เพดานหน่วยความจำ: เมื่อ OS ฆ่าแอปของคุณ

PC ที่ RAM เหลือน้อยจะทำงานช้าลง — ระบบปฏิบัติการเริ่มใช้พื้นที่ดิสก์เป็นหน่วยความจำสำรอง (เรียกว่า paging หรือ swapping) ซึ่งช้ากว่า RAM มากแต่ยังทำให้โปรแกรมทำงานต่อไปได้ มือถือส่วนใหญ่ไม่มีตาข่ายนิรภัยแบบนั้นให้แอป ระบบปฏิบัติการมือถือหลักทั้งสองระบบคอยจับตาดูว่าแต่ละแอปใช้หน่วยความจำเท่าไหร่ และเมื่อแอปข้ามขีดจำกัด — ไม่ว่าจะเป็นขีดจำกัดตายตัวต่อแอป หรือระบบทั้งหมดขาดหน่วยความจำ — OS ก็แค่ฆ่าแอปทิ้งเลย ไม่มีกล่องเตือน ไม่มี exception ที่ผู้เล่นแคปหน้าจอได้ ไม่มี crash log ให้ส่งมาหาคุณ เว้นแต่คุณจะสร้างระบบรายงานของตัวเองไว้ก่อน เกมแค่หายไปจากหน้าจอเฉย ๆ

Android เรียกกลไกนี้ว่า low memory killer / out-of-memory (OOM) killer; iOS เรียกเวอร์ชันของตัวเองว่า jetsam ชื่อต่างกันแต่แนวคิดเดียวกัน: แรงกดดันหน่วยความจำข้ามเส้น แล้วแอปที่อยู่หน้าจอถูกปิดเพื่อปกป้องระบบส่วนที่เหลือ

ทั้งสองบริษัทไม่ได้เผยแพร่ตัวเลขที่แน่นอนตายตัวว่าเส้นนั้นอยู่ตรงไหน — มันขึ้นอยู่กับ RAM รวมของอุปกรณ์, เวอร์ชัน OS, ซอฟต์แวร์เฉพาะของผู้ผลิตที่ครอบทับ Android อยู่, และมีแอปอื่นเปิดอยู่เบื้องหลังกี่ตัว ตัวเลขที่ตามมาต่อไปนี้เป็นตัวเลขคร่าว ๆ สำหรับวางแผนเท่านั้น ไม่ใช่การรับประกัน และคุณควรตรวจสอบพฤติกรรมจริงกับอุปกรณ์จริงและ kill log จริงเสมอก่อนจะเชื่อ budget ใด ๆ:

approximate memory budget before real risk of an OS kill (illustrative only -- verify against real devices) Low tier (~2-3 GB total RAM) [###### ] ~200 MB Mid tier (~4-6 GB total RAM) [################## ] ~550 MB High tier (~8-12 GB total RAM) [######################################] ~1500 MB

สังเกตรูปแบบนี้: budget ของอุปกรณ์ระดับล่างไม่ได้แค่ "น้อยกว่า" เรือธงนิดหน่อย — มันอาจต่างกันเป็นสิบเท่าได้เลย ชุดเท็กซ์เจอร์, audio bank, หรือด่านที่พอดีกับเรือธงสบาย ๆ สามารถทำให้เกมเดียวกันนั้น crash ทันทีบนมือถือราคาประหยัดอายุสามปี โดยไม่มีข้อความ error อะไรเลยนอกจาก "แอปปิดไปแล้ว"

คุณสามารถดูการใช้หน่วยความจำของเกมตัวเองระหว่างรันได้ด้วย profiler API ของ Unity ซึ่งมีประโยชน์ในการจับ memory leak (หน่วยความจำที่โตขึ้นเรื่อย ๆ และไม่เคยถูกปล่อยคืน) ก่อนที่อุปกรณ์ของผู้เล่นจะจับมันแทนคุณ:

using UnityEngine;
using UnityEngine.Profiling;

public class MemoryWatcher : MonoBehaviour
{
    public float warnThresholdMB = 500f;

    void Update()
    {
        long usedBytes = Profiler.GetTotalAllocatedMemoryLong();
        float usedMB = usedBytes / (1024f * 1024f);

        if (usedMB > warnThresholdMB)
        {
            Debug.LogWarning("Memory over budget: " + usedMB.ToString("F1") + " MB");
        }
    }
}
ข้อผิดพลาดที่พบบ่อย ทดสอบการใช้หน่วยความจำด้วยอุปกรณ์ที่เพิ่ง reboot ใหม่และไม่มีอะไรรันอยู่เลยเท่านั้น ผู้เล่นจริงมีเบราว์เซอร์, แอปแชท, และแอปเพลงเปิดอยู่เบื้องหลังอยู่แล้ว ทั้งหมดแย่ง RAM ที่จำกัดชุดเดียวกัน budget ที่ปลอดภัยจริงของเกมคุณในสนามจริงเล็กกว่าที่คุณวัดได้บนอุปกรณ์ทดสอบที่สะอาด

5. Tile-Based Deferred Rendering: GPU มือถือทำงานยังไงจริง ๆ

GPU เดสก์ท็อปและคอนโซลส่วนใหญ่เรนเดอร์ด้วยวิธีที่เรียกว่า immediate mode: แต่ละ draw call ระบายสี pixel ของตัวเองแล้วเขียนออกไปยังพูล VRAM (video memory) ขนาดใหญ่โดยตรง ผ่านบัสหน่วยความจำที่กว้างและเร็วมาก ถ้าสามเหลี่ยมสองรูปซ้อนทับกันบนหน้าจอ ทั้งคู่จะถูกระบายสีและเขียนลง VRAM — รูปที่สองก็แค่เขียนทับรูปแรก งานที่ทำซ้ำบน pixel ที่ซ้อนทับกันแบบนี้เรียกว่า overdraw และ GPU ของ PC มักรับมือกับมันได้พอสมควรเพราะบัสหน่วยความจำมี bandwidth เหลือเฟือ

GPU มือถือเกือบทั้งหมดทำงานต่างออกไป ด้วยวิธีที่เรียกว่า tile-based deferred rendering (TBDR): หน้าจอถูกแบ่งออกเป็น tile เล็ก ๆ และแต่ละ tile ถูกประมวลผลเกือบทั้งหมดด้วยหน่วยความจำ on-chip ที่เล็กแต่เร็วมาก ก่อนที่ผลลัพธ์สุดท้ายจะถูกเขียนออกไปยังหน่วยความจำหลักแค่ครั้งเดียว

IMMEDIATE MODE (typical desktop/console GPU) draw call 1 --> shade pixels --> write straight to VRAM (off-chip) draw call 2 --> shade pixels --> write straight to VRAM (off-chip) draw call 3 --> shade pixels --> write straight to VRAM (off-chip) overlapping pixels get shaded and written more than once (overdraw), paid for with a very wide, very fast memory bus TILE-BASED DEFERRED RENDERING (typical mobile GPU) screen split into small tiles, e.g. 32x32 pixels +--------+--------+--------+--------+ | tile | tile | tile | tile | +--------+--------+--------+--------+ | tile | tile | tile | tile | +--------+--------+--------+--------+ for each tile, the GPU: 1. collects every triangle that touches this tile 2. resolves which surface is visible per pixel (hidden-surface removal) 3. shades ONLY the visible pixels, using small, fast on-chip memory 4. writes the finished tile out to main memory once overdraw of OPAQUE geometry inside a tile is almost free, because step 2 throws away hidden pixels before step 3 ever shades them

นี่เป็นข่าวดีจริง ๆ สำหรับ geometry แบบทึบแสง (วัตถุทึบที่ไม่มีความโปร่งใส) — GPU มือถือมักรับมือกับวัตถุทึบที่ซ้อนทับกันได้ถูกกว่าที่คุณคาดไว้ เพราะมันคำนวณหาก่อนว่า pixel ไหนจะปรากฏจริง ๆ ก่อนที่จะเสียงานระบายสีไปกับมัน แต่มีเทคนิคการเรนเดอร์ที่พบบ่อยสองอย่างที่ทำลายกลไกที่ทำให้มันเร็วนี้:

เรื่องนี้มีผลโดยตรงกับ mobile action RPG ที่สร้างขึ้นรอบการต่อสู้ที่หวือหวา: สกิลธาตุของตัวละครหนึ่งตัวอาจสร้าง particle effect ที่ซ้อนทับกันสิบกว่าตัว, แฟลชเต็มจอ, และ bloom pass ทั้งหมดพร้อมกัน ทุกอย่างนั้นคือประเภทงานที่ TBDR รับมือได้แย่ที่สุด บนฮาร์ดแวร์ที่มีที่ว่างเหลือน้อยที่สุดพอดี — นี่คือเหตุผลที่ mobile build ของเกมพวกนี้มักจำกัดจำนวน particle และลดความซับซ้อนของ post-processing เมื่อเทียบกับ PC build ของสกิลเดียวกัน

Tip เมื่อ fps ของ mobile build ตกลงเฉพาะตอนใช้สกิลต่อสู้ใหญ่ ๆ หรือฉากต่อสู้ที่แน่นขนัด ให้ดู transparent VFX ที่ซ้อนทับกันและ full-screen effect ที่ซ้อนกันก่อน ก่อนที่จะสรุปว่าปัญหาคือจำนวน triangle หรือขนาดเท็กซ์เจอร์

6. Texture Compression ต่อแพลตฟอร์ม

ไฟล์เท็กซ์เจอร์อย่าง PNG หรือ JPEG บนดิสก์ถูกบีบอัดเพื่อขนาดไฟล์ แต่พอโหลดขึ้นมาแล้ว GPU ต้องการข้อมูล pixel ดิบที่ไม่บีบอัดเพื่อเรนเดอร์มัน — ดังนั้นเกมมักจะคลายบีบอัดทั้งหมดลงในหน่วยความจำก่อน GPU texture compression format ต่างออกไป: มันเป็นฟอร์แมตที่ฮาร์ดแวร์ของ GPU เองอ่านและถอดรหัสได้แบบ on the fly ทีละ pixel ระหว่างเรนเดอร์ ทำให้เท็กซ์เจอร์คงอยู่ในสถานะบีบอัดตลอดเวลาที่ถูกใช้งาน ความต่างนี้คือคันโยกที่ใหญ่ที่สุดที่คุณมีต่อว่าเท็กซ์เจอร์ของคุณกินหน่วยความจำจริงเท่าไหร่

ASTC (Adaptive Scalable Texture Compression) คือฟอร์แมต GPU compression มาตรฐานสมัยใหม่ทั้งบน GPU Android รุ่นปัจจุบันและอุปกรณ์ iOS/Metal จุดเด่นของมันคือสามารถเลือก block size ได้ (ขนาดพิกเซลของกลุ่ม pixel ที่ถูกบีบอัดรวมกัน) ตั้งแต่ 4x4 (คุณภาพสูงสุด ขนาดใหญ่สุด) ไปจนถึง 12x12 (คุณภาพต่ำสุด ขนาดเล็กสุด) ทุก ASTC block ไม่ว่า block size จะเป็นเท่าไหร่ ใช้พื้นที่จัดเก็บ 128 บิต (16 ไบต์) เสมอ ดังนั้น block size ที่ใหญ่ขึ้นก็แค่กระจาย 16 ไบต์เท่าเดิมไปยัง pixel มากขึ้น:

1024 x 1024 texture, one mip level

format          bytes per pixel     total size
RGBA32 (raw)    4.00                4.00 MB
ASTC 4x4        1.00                1.00 MB
ASTC 6x6        0.44                0.44 MB
ASTC 8x8        0.25                0.25 MB
ASTC 12x12      0.11                0.11 MB

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

ใน Unity texture compression ถูกตั้งค่าแยกต่อแพลตฟอร์มผ่าน import settings ของเท็กซ์เจอร์ และควรตั้งค่ามันอย่างชัดเจนแทนที่จะเชื่อค่า default:

#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;

public class SetMobileTextureFormat
{
    [MenuItem("Tools/Set ASTC 6x6 For Selected Textures")]
    static void SetAstc()
    {
        foreach (Object obj in Selection.objects)
        {
            string path = AssetDatabase.GetAssetPath(obj);
            TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;
            if (importer == null) continue;

            TextureImporterPlatformSettings settings = importer.GetPlatformTextureSettings("Android");
            settings.overridden = true;
            settings.format = TextureImporterFormat.ASTC_6x6;
            settings.compressionQuality = 50;
            importer.SetPlatformTextureSettings(settings);
            importer.SaveAndReimport();
        }
    }
}
#endif

กฎที่ใช้ได้จริงที่โปรเจกต์มือถือส่วนใหญ่ลงตัว: ใช้ block size ที่เล็กและคุณภาพสูงกว่าอย่าง ASTC 4x4 หรือ 6x6 สำหรับสิ่งที่ผู้เล่นมองใกล้ ๆ และบ่อย ๆ — หน้าตัวละคร, ไอคอน UI, สกินอาวุธฮีโร่ — และ block size ที่ใหญ่และถูกกว่าอย่าง ASTC 8x8 สำหรับสิ่งที่เห็นจากระยะไกลหรือแค่ชั่วครู่ — ฉากหลัง, ของประดับสภาพแวดล้อม, ภูมิประเทศระยะไกล การสูญเสียคุณภาพที่ block size ใหญ่ขึ้นสังเกตเห็นได้ยากกว่ามากบนฉากหลังที่รกกว่าบนหน้าตัวละครที่ซูมใกล้

7. Resolution Scaling และ Dynamic Resolution

ความละเอียดที่เกมเรนเดอร์ไม่จำเป็นต้องตรงกับความละเอียด native ของหน้าจอมือถือ การเรนเดอร์ที่ความละเอียดภายในที่ต่ำกว่าแล้วขยายภาพที่เสร็จแล้วให้เต็มหน้าจอ ช่วยลดจำนวน pixel ที่ GPU ต้องระบายสีในแต่ละเฟรม (ต้นทุนนี้มักถูกเรียกว่า fill-rate จำนวน pixel ที่ GPU ระบายสีได้ต่อวินาที) ซึ่งมีผลมากที่สุดตรงจุดที่ข้อ 5 บอกว่า GPU มือถืออ่อนแอที่สุด: overdraw, alpha blending, และ full-screen effect ซึ่งล้วนแพงขึ้นเมื่อ pixel ยิ่งเยอะ

เวอร์ชันที่ง่ายที่สุดของสิ่งนี้คือ render scale แบบตายตัวที่เลือกตาม device tier (ข้อ 9) — เช่น เรนเดอร์ที่ 75% resolution บนมือถือระดับล่างและ 100% บนเรือธง เวอร์ชันที่ก้าวหน้ากว่าคือ dynamic resolution ซึ่งปรับ render scale อย่างต่อเนื่องระหว่างรัน โดยอิงจากประสิทธิภาพจริงของเกม ณ ตอนนั้น แทนที่จะยึดตัวเลขตายตัวค่าเดียว:

using UnityEngine;

public class DynamicResolutionController : MonoBehaviour
{
    public float targetFrameTimeMs = 16.6f; // 60 fps budget
    public float minScale = 0.6f;
    float currentScale = 1f;

    void Update()
    {
        float frameMs = Time.unscaledDeltaTime * 1000f;

        if (frameMs > targetFrameTimeMs * 1.15f && currentScale > minScale)
        {
            currentScale = Mathf.Max(minScale, currentScale - 0.05f);
            ScalableBufferManager.ResizeBuffers(currentScale, currentScale);
        }
        else if (frameMs < targetFrameTimeMs * 0.9f && currentScale < 1f)
        {
            currentScale = Mathf.Min(1f, currentScale + 0.02f);
            ScalableBufferManager.ResizeBuffers(currentScale, currentScale);
        }
    }
}

ไล่ trace ไม่กี่เฟรมของฉากต่อสู้ที่วุ่นวาย โดยตั้งเป้าไว้ที่ 16.6ms (60 fps):

frame   frameMs   action                          scale after
100     15.9      within budget, no change         1.00
101     19.8      over 1.15x budget -> scale down   0.95
102     20.1      still over -> scale down          0.90
103     18.4      still over -> scale down          0.85
104     16.0      within budget, no change          0.85
105     13.2      under 0.9x budget -> scale up      0.87

scale จะลดลงอย่างรวดเร็วเมื่อ frame time พุ่งขึ้น (ช่วงต่อสู้หนัก ๆ) และค่อย ๆ ไต่กลับขึ้นอย่างช้า ๆ เมื่อทุกอย่างสงบลง เกมจึงใช้เวลาส่วนใหญ่อยู่ใกล้ full resolution เท่าที่มันจะรับไหว แทนที่จะเลือกจุดประนีประนอมค่าเดียวตายตัวแล้วทนใช้มันทั้งช่วงที่สงบและช่วงที่วุ่นวาย

Tip เปลี่ยน resolution scale อย่างค่อยเป็นค่อยไป ไม่ใช่กระโดดทีเดียวใหญ่ ๆ การเปลี่ยน resolution แบบฉับพลันจะรบกวนสายตา แต่การไต่ระดับช้า ๆ ผู้เล่นแทบสังเกตไม่เห็น แม้ว่าเกมจะปรับตัวอยู่ตลอดเวลาก็ตาม

8. เลือก 30 fps กับ 60 fps

ทุกเฟรมมี time budget: ที่ 30 fps เกมมีเวลา 33.3 มิลลิวินาทีในการทำทุกอย่างที่จำเป็นสำหรับหนึ่งเฟรม; ที่ 60 fps มันมีแค่ 16.6 มิลลิวินาที — สั้นกว่าครึ่งหนึ่ง การทำ 60 fps ให้คงที่ โดยเฉพาะเมื่อมือถือเล่นมาแล้วสิบนาทีและ thermal throttling (ข้อ 2) ได้ลดพลังงานที่ชิปใช้ได้ไปแล้ว เป็นเป้าหมายที่ยากกว่าการทำ 30 fps มาก

30 fps frame budget = 1000 ms / 30  = 33.3 ms per frame
60 fps frame budget = 1000 ms / 60  = 16.6 ms per frame

Frame rate ไม่ใช่แค่ตัวเลขบนหน้าจอตั้งค่า — มันเปลี่ยนความรู้สึกในการเล่นเกม เกมแอคชันต่อสู้ที่ผู้เล่นต้องจับจังหวะหลบและคอมโบกับการโจมตีของศัตรูที่รวดเร็ว ได้ประโยชน์มากจาก 60 fps เพราะทุกเฟรมที่เพิ่มขึ้นคือข้อมูลเพิ่มเติมว่าควรตอบสนองตอนไหนพอดี เกมแบบ turn-based ที่ผู้เล่นเลือกแอคชันแล้วดูผลลัพธ์ที่คลี่คลายแล้ว ทนต่อ 30 fps ได้มากกว่า เพราะไม่มีอินพุตที่จับจังหวะแบบไวต่อปฏิกิริยาที่จะรู้สึกหน่วง

เพราะความสามารถของอุปกรณ์ต่างกันมาก (ข้อ 9) และความชอบของผู้เล่นก็ต่างกันด้วย — ผู้เล่นบางคนอยากได้มือถือที่เย็นกว่าและอยู่ได้นานกว่ามากกว่า frame rate ที่ลื่นที่สุด — เกม live-service บนมือถือส่วนใหญ่เปิดให้เลือกตัวเลือกนี้ตรง ๆ ในเมนูตั้งค่า แทนที่จะเลือกเป้าหมายตายตัวค่าเดียวสำหรับทุกคน:

using UnityEngine;

public class FrameRateOption : MonoBehaviour
{
    public void SetFrameRateOption(int fps)
    {
        QualitySettings.vSyncCount = 0; // targetFrameRate is ignored while vSync is on
        Application.targetFrameRate = fps; // pass 30 or 60
    }
}
ข้อผิดพลาดที่พบบ่อย ตั้งค่า Application.targetFrameRate โดยไม่ตั้งค่า QualitySettings.vSyncCount = 0 ควบคู่ไปด้วย ถ้า vSync เปิดอยู่ Unity จะเพิกเฉยต่อ targetFrameRate ทั้งหมด และล็อกไว้ที่อัตราที่ vSync ของจอยอมให้แทน ซึ่งทำให้ตัวเลือก frame rate ที่ผู้เล่นคิดว่าตัวเองเลือกไว้ใช้ไม่ได้ผลโดยไม่รู้ตัว

เสนอค่า default ที่สมเหตุสมผลตาม device tier (อุปกรณ์ระดับล่างไม่ควรตั้งค่า default เป็นตัวเลือก 60 fps ที่มันรับไหวจริง ๆ ไม่ได้) แต่ให้ผู้เล่น override ได้เสมอ — ผู้เล่นบางคนบนมือถือระดับกลางจะยินดีรับ frame rate ที่ต่ำกว่าแต่คงที่ แลกกับมือถือที่เย็นตลอดเซสชันยาว ๆ

9. Device Tier: ตรวจจับมือถือระดับล่าง

แทนที่จะปล่อยค่าคุณภาพตายตัวค่าเดียวสำหรับทุกอุปกรณ์ เกมมือถือส่วนใหญ่จัดอุปกรณ์เข้า device tier (กลุ่มอย่าง Low, Mid, และ High) โดยใช้ heuristic ที่เร็ว (กฎประมาณการที่รวดเร็วแทนที่จะเป็นการวัดที่แม่นยำสมบูรณ์) ตอนเปิดแอปครั้งแรก จากนั้นใช้ preset คุณภาพที่ตรงกับ tier ที่ตรวจจับได้โดยอัตโนมัติ แล้วยังคงให้ผู้เล่นเปลี่ยนการตั้งค่าแต่ละอย่างได้ทีหลัง

public enum DeviceTier { Low, Mid, High }

public static class DeviceTierDetector
{
    public static DeviceTier Detect()
    {
        int ramMB = SystemInfo.systemMemorySize;

        if (ramMB <= 3072) return DeviceTier.Low;
        if (ramMB <= 6144) return DeviceTier.Mid;
        return DeviceTier.High;
    }
}
using UnityEngine;

public class QualityManager : MonoBehaviour
{
    void Start()
    {
        DeviceTier tier = DeviceTierDetector.Detect();
        ApplyTier(tier);
    }

    void ApplyTier(DeviceTier tier)
    {
        switch (tier)
        {
            case DeviceTier.Low:
                QualitySettings.SetQualityLevel(0, true);
                Application.targetFrameRate = 30;
                QualitySettings.shadowDistance = 0f;
                ScalableBufferManager.ResizeBuffers(0.75f, 0.75f);
                break;

            case DeviceTier.Mid:
                QualitySettings.SetQualityLevel(1, true);
                Application.targetFrameRate = 30;
                QualitySettings.shadowDistance = 20f;
                break;

            case DeviceTier.High:
                QualitySettings.SetQualityLevel(2, true);
                Application.targetFrameRate = 60;
                QualitySettings.shadowDistance = 40f;
                break;
        }
    }
}
app starts | v read SystemInfo.systemMemorySize (RAM in MB) | ------------------------------------------------ | | | RAM <= 3072 3072 < RAM <= 6144 RAM > 6144 | | | v v v Tier: LOW Tier: MID Tier: HIGH 30 fps cap 30 fps cap 60 fps offered shadows off low shadows full shadows render scale 0.75 render scale 0.9 render scale 1.0 ASTC 8x8 textures ASTC 6x6 textures ASTC 4x4 textures

การเช็ค RAM ล้วน ๆ เป็นจุดเริ่มต้นที่ดี แต่ไม่สมบูรณ์แบบ — ทุกรุ่นมีมือถือบางรุ่นที่ RAM รวมดูดีบนกระดาษแต่ GPU จริงอ่อนแอ หรือกลับกัน โปรเจกต์มือถือที่จริงจังมักเก็บตารางแทนที่ (override) เล็ก ๆ ไว้สำหรับอุปกรณ์ที่รู้ว่ามีปัญหา (จับคู่ด้วย SystemInfo.deviceModel) เช็คก่อนที่จะ fallback ไปใช้ heuristic แบบ RAM แทนที่จะเชื่อตัวเลขเดียวว่าบอกความจริงเสมอ

Tip ไม่ว่า detector จะเลือกอะไร ให้มองมันเป็นแค่ค่าตั้งต้น เริ่มต้น เท่านั้น เปิดให้ตั้งค่าแต่ละอย่างได้เสมอ (frame rate, คุณภาพเงา, render scale) ในเมนูตั้งค่า เพราะการเดาแบบ heuristic ไม่มีทางถูกต้องสำหรับทุกอุปกรณ์ที่มันจะเจอ

10. ความแตกต่างของอินพุต: Touch, Gamepad, และ Mouse

เกม live-service แบบ cross-platform มักต้องรองรับ touch บนมือถือ, mouse และ keyboard บน PC, และ gamepad ทั้งบน PC และคอนโซล — สามวิธีที่ต่างกันจริง ๆ ในการบอกเกมว่าผู้เล่นต้องการอะไร ไม่ใช่แค่ผังปุ่มที่ต่างกันสามแบบสำหรับแนวคิดเดียวกัน:

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

using UnityEngine;
using UnityEngine.InputSystem;

public class InputModeSwitcher : MonoBehaviour
{
    public GameObject virtualJoystick;

    void Update()
    {
        bool gamepadConnected = Gamepad.current != null;
        virtualJoystick.SetActive(!gamepadConnected && Application.isMobilePlatform);
    }
}

เช็คนี้รันทุกเฟรมโดยเฉพาะเพราะอินพุตที่เชื่อมต่ออยู่อาจเปลี่ยนกลางเซสชันได้ — ผู้เล่นอาจเสียบ gamepad เข้ามาระหว่างเล่น — และ UI ควรตอบสนองทันทีแทนที่จะต้องรีสตาร์ทก่อน

11. Safe Area และ Notch

หน้าจอมือถือสมัยใหม่ไม่ใช่สี่เหลี่ยมผืนผ้าที่สะอาดในมุมมองของเกม กล้องที่เจาะไว้ (notch หรือรูเจาะที่กล้องหน้าอยู่) อาจซ้อนทับอยู่บนขอบบนของหน้าจอ มุมมักถูกมนจริง ๆ ทางกายภาพ และแถบท่าทางระบบ (gesture bar) อาจอยู่ที่ขอบด้านหนึ่ง — ทั้งหมดนี้คือบริเวณที่ระบบปฏิบัติการอาจวาด UI ของตัวเองทับ UI ของคุณ หรือที่ UI ของคุณจะถูกซ่อนหรือถูกตัดถ้าคุณวางมันตรงนั้น

Unity เปิดให้ใช้ Screen.safeArea: สี่เหลี่ยมที่รายงานโดย OS อธิบายบริเวณของหน้าจอที่รับประกันว่าจะไม่ถูกบดบังโดยสิ่งเหล่านั้น การ anchor UI สำคัญ — health bar, ตัวนับเงินในเกม, ปุ่ม "skip" — ไว้ในสี่เหลี่ยมนี้แทนที่จะเป็นขอบหน้าจอดิบ ทำให้มันมองเห็นได้บนทุกอุปกรณ์ ไม่ว่าจะมี notch หรือไม่ก็ตาม:

using UnityEngine;

public class SafeAreaFitter : MonoBehaviour
{
    RectTransform panel;

    void Awake()
    {
        panel = GetComponent<RectTransform>();
        Apply();
    }

    void Apply()
    {
        Rect safe = Screen.safeArea;

        Vector2 anchorMin = safe.position;
        Vector2 anchorMax = safe.position + safe.size;

        anchorMin.x /= Screen.width;
        anchorMin.y /= Screen.height;
        anchorMax.x /= Screen.width;
        anchorMax.y /= Screen.height;

        panel.anchorMin = anchorMin;
        panel.anchorMax = anchorMax;
    }
}
physical screen +---------------------------------------+ | (camera notch area) | system camera cutout -- |-----------------------------------------| nothing important goes here | | | Screen.safeArea | | +---------------------------------+ | | | [ HP bar ] | | UI anchored inside this | | | | rect is guaranteed visible | | [ Skip ] | | on every device, notch or not | +---------------------------------+ | | | +---------------------------------------+ rounded corners / gesture bar area can also clip UI placed exactly at the raw screen edge
ข้อผิดพลาดที่พบบ่อย anchor UI ไว้ที่มุมหน้าจอดิบแทนที่จะใช้ Screen.safeArea มันดูถูกต้องบนอุปกรณ์ทดสอบของนักพัฒนาเอง แล้วก็ไปซ่อนตัวนับเงินในเกมไว้หลังกล้องที่เจาะไว้ หรือตัดปุ่ม "skip" ใต้มุมมน บนอุปกรณ์ที่นักพัฒนาไม่เคยทดสอบเองเลย

12. ทดสอบบนมือถือที่ถูกที่สุดที่หาได้

มือถือของนักพัฒนาเองแทบทุกครั้งดีกว่ามือถือที่ผู้เล่นส่วนใหญ่มีจริง ๆ มาก — สตูดิโอซื้อฮาร์ดแวร์ที่แรงให้วิศวกรของตัวเอง และมือถือที่แรงมี headroom ด้าน thermal, memory, และ GPU เหลือมากพอที่จะซ่อนปัญหาที่บทนี้พูดถึงพอดี การทดสอบบนอุปกรณ์พัฒนาระดับเรือธงเท่านั้นไม่ได้แค่พลาดบัคบางตัว — มันไม่สามารถหา thermal throttling, memory kill, หรือปัญหาการเรนเดอร์บน GPU ระดับล่างได้เลยโดยโครงสร้าง เพราะเรือธงมี margin เหลือมากพอที่ปัญหาเหล่านั้นจะไม่มีวันถูกกระตุ้นระหว่างเซสชันทดสอบปกติ

same scene, same build, two devices, 12-minute play session

                    flagship device        budget device
start fps           60                      45
fps at 12 min       58                      22
skin temperature    38C                     44C
memory at 12 min    620 MB (fine)           340 MB
result              smooth session          app killed by OS at 11:40

ไม่มีอะไรในการรันบนเรือธงเลยที่จะบอกคุณได้ว่าอุปกรณ์ราคาประหยัดจะถูก OS ฆ่าทิ้งก่อนที่เซสชันจะจบด้วยซ้ำ บัคนี้มีอยู่จริงเฉพาะบนฮาร์ดแวร์ที่มี headroom ให้ดูดซับน้อยกว่าเท่านั้น

ทางแก้ที่ใช้ได้จริงคือเก็บ device lab ทางกายภาพเล็ก ๆ ไว้ ไม่ใช่พึ่ง simulator หรือมือถือของตัวเองอย่างเดียว: อย่างน้อยหนึ่งเครื่อง Android ที่เก่าหรือถูกโดยตั้งใจ (อายุสองถึงสามปี, RAM น้อย, GPU ระดับล่าง) คู่กับอุปกรณ์ระดับกลาง และรันเซสชันการเล่นเต็ม 15-20 นาทีบนมันเป็นประจำ — ไม่ใช่แค่ smoke test แบบติดตั้งแล้วเปิดดู เพราะทั้ง throttling (ข้อ 2) และการโตของหน่วยความจำ (ข้อ 4) ต้องการเวลาต่อเนื่องถึงจะแสดงผลออกมาให้เห็น

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

13. อภิธานศัพท์

14. แบบฝึกหัด

แบบฝึกหัดที่ 1 — อ่าน Log ของ Throttling เซสชันทดสอบหนึ่งบันทึกค่าเฉลี่ย fps ทุก 30 วินาทีสำหรับ 6 นาทีแรกของการเล่น:
[0:30] 58.9
[1:00] 58.5
[1:30] 57.8
[2:00] 55.0
[2:30] 49.6
[3:00] 42.1
[3:30] 35.4
[4:00] 30.8
[4:30] 30.2
[5:00] 30.0
[5:30] 29.9
[6:00] 29.8
(a) frame rate เริ่มตกลงมาต่ำกว่า 90% ของค่าในช่วงแรกที่จุดไหนโดยประมาณ และมันคงอยู่แบบนั้นต่อไปไหม? (b) คำนวณค่าเฉลี่ย fps จากทั้ง 12 ช่วงที่แสดง (c) อธิบายในหนึ่งหรือสองประโยคว่าทำไมการรัน benchmark 20 วินาทีตอนเริ่มต้นเซสชันนี้จะรายงานตัวเลขที่ทำให้เข้าใจผิด
Show answer

(a) 90% ของค่าในช่วงแรก (58.9) อยู่ที่ประมาณ 53.0 ช่วง 2:00 (55.0) ยังสูงกว่าค่านั้น แต่ช่วง 2:30 (49.6) ตกลงมาต่ำกว่ามัน และทุกช่วงหลังจากนั้นก็ยังคงต่ำกว่าเช่นกัน ดังนั้น throttling จึงเริ่มเห็นได้ชัดตั้งแต่ช่วงประมาณ 2:30-3:00

sum = 58.9+58.5+57.8+55.0+49.6+42.1+35.4+30.8+30.2+30.0+29.9+29.8
    = 508.0
average = 508.0 / 12 = 42.3 fps

(b) ค่าเฉลี่ยตลอด 6 นาทีเต็มอยู่ที่ประมาณ 42.3 fps

(c) การรัน benchmark 20 วินาทีตอนเริ่มต้นเซสชันจะตกอยู่ในช่วง 0:30 พอดี รายงานค่าใกล้เคียง 58.9 fps — ตัวเลขที่สูงกว่าค่าเฉลี่ยจริงตลอด 6 นาทีมากกว่า 16 fps และสูงเกือบสองเท่าของ 29-30 fps ที่อุปกรณ์ตกลงมาคงที่จริง ๆ เมื่อมันรันนานพอที่จะร้อนขึ้น

แบบฝึกหัดที่ 2 — เพิ่ม Manual Override ให้ Detector DeviceTierDetector จากข้อ 9 จัดอุปกรณ์ตาม RAM ล้วน ๆ สมมติว่ารุ่นหนึ่งโดยเฉพาะ "BudgetPhone X9" รายงาน RAM 4096 MB (ซึ่งการเช็ค RAM อย่างเดียวจะจัดเป็น Mid tier) แต่รู้กันว่ามี GPU ที่อ่อนแอมากจนรับ Mid-tier setting จริง ๆ ไม่ไหว แก้ไข DeviceTierDetector.Detect() ให้รุ่นนี้ถูกบังคับเป็น DeviceTier.Low เสมอ โดยเช็คก่อนที่ heuristic แบบ RAM จะทำงาน ในขณะที่อุปกรณ์อื่นทั้งหมดยัง fallback ไปใช้ตรรกะแบบ RAM ปกติ
Show answer
public static class DeviceTierDetector
{
    static readonly string[] ForcedLowTierModels = { "BudgetPhone X9" };

    public static DeviceTier Detect()
    {
        foreach (string model in ForcedLowTierModels)
        {
            if (SystemInfo.deviceModel == model)
                return DeviceTier.Low; // known weak GPU -- override before the RAM check
        }

        int ramMB = SystemInfo.systemMemorySize;
        if (ramMB <= 3072) return DeviceTier.Low;
        if (ramMB <= 6144) return DeviceTier.Mid;
        return DeviceTier.High;
    }
}

รายการ override ถูกเช็คก่อนและ return ทันทีเมื่อเจอที่ตรงกัน ดังนั้น "BudgetPhone X9" จึงไม่มีวันไปถึงการเปรียบเทียบ RAM เลย ไม่ว่า SystemInfo.systemMemorySize จะรายงานค่าอะไรสำหรับมันก็ตาม อุปกรณ์รุ่นอื่นทั้งหมดจะผ่าน loop นี้ไปโดยไม่ถูกแตะต้อง และถูกจัดประเภทเหมือนเดิมทุกประการ ด้วย RAM เพียงอย่างเดียว

แบบฝึกหัดที่ 3 — คำนวณ Texture Memory Budget โมเดลตัวละครหนึ่งใช้เท็กซ์เจอร์ 8 ชิ้น ขนาด 2048x2048 แต่ละชิ้น ปัจจุบันเก็บแบบไม่บีบอัดเป็น RGBA32 ส่วนที่เหลือของฉาก (mesh, เสียง, UI, และอื่น ๆ ทั้งหมด) ใช้หน่วยความจำอยู่แล้วประมาณ 300 MB device tier แบบ Mid จากข้อ 4 มี budget โดยประมาณอยู่ที่ราว 400 MB ก่อนที่จะเสี่ยงจริง ๆ ที่จะถูก OS ฆ่าทิ้ง (a) คำนวณหน่วยความจำรวมที่เท็กซ์เจอร์ RGBA32 ทั้ง 8 ชิ้นใช้ (b) แปลงเป็น ASTC 6x6 แล้วคำนวณผลรวมใหม่ (c) ฉากนี้พอดีกับ budget 400 MB ทั้งก่อนและหลังการแปลงหรือไม่?
Show answer
(a) RGBA32, 2048x2048:
    pixels    = 2048 * 2048 = 4,194,304
    bytes     = 4,194,304 * 4 bytes/pixel = 16,777,216 bytes = 16.00 MB
    8 textures = 8 * 16.00 MB = 128.00 MB

(b) ASTC 6x6:
    bytes per pixel = 16 bytes / 36 pixels = 0.444 bytes/pixel
    bytes per texture = 4,194,304 * 0.444 = 1,864,135 bytes = 1.78 MB
    8 textures = 8 * 1.78 MB = 14.22 MB

(c) before: 300 MB + 128.00 MB = 428.00 MB  -> over the 400 MB budget
    after:  300 MB + 14.22 MB  = 314.22 MB  -> comfortably under budget

เมื่อเก็บเป็น RGBA32 ตัวละครตัวเดียวนี้ดันให้ฉากเกิน Mid-tier memory budget ไปประมาณ 28 MB ทำให้เสี่ยงจริง ๆ ที่จะถูก OS ฆ่าทิ้งบนอุปกรณ์ระดับกลาง การแปลงเท็กซ์เจอร์ทั้ง 8 ชิ้นเดียวกันนี้เป็น ASTC 6x6 ลดต้นทุนหน่วยความจำลงประมาณ 9 เท่า (จาก 128.00 MB เหลือ 14.22 MB) โดยไม่เปลี่ยน pixel แม้แต่จุดเดียวบนดิสก์ในแบบที่ผู้เล่นส่วนใหญ่จะสังเกตเห็น และทำให้ทั้งฉากอยู่ใต้ budget สบาย ๆ โดยมี headroom เหลืออีกประมาณ 85 MB

← กลับไปหน้ารวมบท