บทความ › พัฒนาเกม

ทำไมเกมวิ่งเร็วขึ้นบนจอ 144Hz: game loop แบบ fixed timestep และ interpolation

เกมที่ขยับตามจำนวนเฟรมจะเร็วขึ้นบนจอรีเฟรชสูง แก้ด้วย fixed timestep + accumulator, interpolation ตอนวาด และเพดานกัน spiral of death พร้อมตัวเลข 60/144 Hz และโค้ด JS

กลาง อ่าน 11 นาที 22 ตุลาคม 2569

อ่านจบแล้วจะรู้
  • ขยับของต่อเฟรมทำให้เกมเร็วตามจอ บนจอ 144Hz เร็วกว่า 60Hz 2.4 เท่า
  • คูณ delta time แก้เรื่องความเร็ว แต่ฟิสิกส์ยังได้ผลต่างกันตาม frame rate เช่นกระโดดสูงไม่เท่ากัน
  • fixed timestep + accumulator ทำให้ฟิสิกส์เดินทีละก้าวเท่ากันทุกเครื่อง แล้วใช้ interpolation ให้ภาพลื่น
  • ต้องมีเพดานเวลาต่อเฟรมหรือจำนวน step ไม่งั้นเครื่องช้าจะตกลง spiral of death

เกมที่ทดสอบบนจอ 60Hz มาทั้งเดือน พอผู้เล่นเปิดบนจอ 144Hz ตัวละครวิ่งเร็วจนคุมไม่ได้ หรือกระโดดข้ามสิ่งที่ตั้งใจให้ข้ามไม่ได้ ต้นเหตุคือ game loop ผูกการเคลื่อนที่ไว้กับจำนวนเฟรม บทความนี้ไล่จากบั๊กนั้น ไปถึง fixed timestep แบบที่ Glenn Fiedler เขียนไว้ใน “Fix Your Timestep!” (2004) และดูว่า Unity, Godot, Roblox เปิดเรื่องนี้ให้เราใช้ยังไง

ตัวเลขในบทความคำนวณด้วย Node.js 20.14 ข้อมูลเอนจินตรวจกับเอกสาร Unity 6.6, Godot 4.7 และ Roblox Creator Docs ณ ตุลาคม 2026

บั๊กแรก: ขยับต่อเฟรม

function frame() {
  player.x += 5;               // 5 px ต่อเฟรม
  draw();
  requestAnimationFrame(frame);
}

requestAnimationFrame เรียกตามรีเฟรชเรตของจอ MDN ระบุว่าปกติ 60Hz แต่ 75, 120, 144Hz ก็ใช้กันทั่วไป และเตือนตรง ๆ ว่าถ้าไม่คิดจากเวลา แอนิเมชันจะเร็วขึ้นบนจอรีเฟรชสูง

จอเฟรมต่อวินาทีความเร็วจริง
60 Hz605 × 60 = 300 px/s
144 Hz1445 × 144 = 720 px/s

720 / 300 = เร็วขึ้น 2.4 เท่า และกลับกัน เครื่องที่เฟรมตกเหลือ 30 ก็ช้าลงครึ่งหนึ่ง

แก้ครึ่งเดียว: คูณ delta time

ทางแก้แรกที่ทุกคนเจอคือ วัดเวลาที่ผ่านไป (dt) แล้วคูณ: player.x += 300 * dt ตอนนี้ความเร็วคงที่ทุกจอแล้ว แต่ฟิสิกส์ที่มีความเร่งยังเพี้ยน เพราะการ integrate ทีละก้าวให้ผลขึ้นกับขนาดก้าว

ตัวอย่างคำนวณ: กระโดดสูงไม่เท่ากัน

กระโดดด้วยความเร็วต้น 600 px/s แรงโน้มถ่วง 1,800 px/s² แบบ semi-implicit Euler (ลดความเร็วก่อน แล้วค่อยขยับตำแหน่ง) ซึ่งเป็นแบบที่โค้ดเกมส่วนใหญ่เขียน

vy -= 1800 * dt;
y  += vy * dt;

สูตรฟิสิกส์จริงให้จุดสูงสุด v²/2g = 600² / 3,600 = 100 px แต่ในเกม จุดสูงสุดคือผลรวมของก้าว ถ้ามี N ก้าวจนความเร็วเหลือ 0 (N = 600 / (1800·dt))

ความสูง = dt × (N × 600 − 1800·dt × N(N+1)/2)

frame ratedtก้าว Nคำนวณสูงสุด
30 Hz1/3010(6,000 − 60×55) / 3090.0 px
60 Hz1/6020(12,000 − 30×210) / 6095.0 px
144 Hz1/14448(28,800 − 12.5×1,176) / 14497.9 px

ด่านที่มีขอบสูง 96 px ผู้เล่นจอ 144Hz กระโดดขึ้นได้ ผู้เล่น 60Hz ขึ้นไม่ได้ เกมเดียวกัน ด่านเดียวกัน ทั้งที่คูณ dt ครบทุกบรรทัด ถ้าเฟรมกระตุกบ้างแล้ว dt ไม่เท่ากันทุกเฟรม ผลยิ่งสุ่ม และ dt ก้อนใหญ่ครั้งเดียวพอให้ของเร็วทะลุกำแพงบาง ๆ

คูณ dt แก้เรื่องความเร็ว แต่ไม่ทำให้ฟิสิกส์ deterministic ต้องให้ฟิสิกส์ก้าวขนาดเท่ากันทุกครั้ง

Fixed timestep + accumulator

ความคิดของ Fiedler: จอ “ผลิต” เวลา ฟิสิกส์ “กิน” เวลาทีละก้อนขนาดคงที่ เศษที่เหลือเก็บไว้ในถัง (accumulator) ไปรอบหน้า ฟิสิกส์จึงเดินด้วย dt = 1/60 เสมอ ไม่ว่าจอจะกี่ Hz กระโดดสูง 95.0 px ทุกเครื่อง

ฟิสิกส์ 60 Hz บนจอ 144 Hz: เฟรมละ 1/144 s = 6.94 ms ก้าวละ 16.67 ms

เฟรมถังหลังเติมจำนวน stepเหลือ (alpha)
16.94 ms00.42
213.89 ms00.83
320.83 ms10.25
411.11 ms00.67
518.06 ms10.08
68.33 ms00.50

12 เฟรมของจอ = 1/12 วินาที = ฟิสิกส์ 5 step พอดี (60 / 12 = 5) เฉลี่ย 0.42 step ต่อเฟรม เฟรมส่วนใหญ่ไม่มีฟิสิกส์เลย บนจอ 30 Hz กลับกัน ทุกเฟรมรัน 2 step

จอ 144 Hz (6.94 ms/เฟรม)12 เฟรมฟิสิกส์ 60 Hz (16.67 ms/step)5 stepเฟรมที่ไม่มี step เลย7 เฟรมในช่วงเวลาเดียวกัน 1/12 วินาที
12 เฟรมของจอ = ฟิสิกส์ 5 step เฟรมส่วนใหญ่ไม่มีฟิสิกส์เลย ภาพจึงต้อง interpolate

Interpolation: ทำให้ภาพไม่กระตุก

ปัญหาใหม่: ถ้าวาดตำแหน่งจากฟิสิกส์ตรง ๆ บนจอ 144 Hz ภาพจะนิ่งอยู่ 2-3 เฟรมแล้วกระโดด เพราะฟิสิกส์เปลี่ยนแค่ 60 ครั้งต่อวินาที วิธีแก้คือเก็บสถานะก่อนหน้า (prev) ไว้ แล้ววาดตำแหน่งระหว่าง prev กับ curr ตามเศษในถัง

alpha = accumulator / dt แล้ววาดที่ prev × (1 − alpha) + curr × alpha

เฟรม 2 ในตาราง alpha = 0.83 ภาพอยู่ 83% ของทางจากสถานะก่อนไปสถานะล่าสุด ราคาที่จ่ายคือภาพช้ากว่าฟิสิกส์ไม่เกินหนึ่ง step (16.7 ms ที่ 60 Hz) สำหรับการหมุน Fiedler แนะนำให้ใช้ slerp ของ quaternion แทนการ lerp ตรง ๆ

Spiral of death และเพดาน

ถ้าคำนวณ step หนึ่งใช้เวลาจริงนานกว่าเวลาที่ step นั้นจำลอง เช่นฟิสิกส์หนักจน step ละ 20 ms ทั้งที่ dt = 16.7 ms ถังจะโตทุกเฟรม เฟรมต่อไปต้องรันหลาย step ขึ้น ยิ่งช้า ยิ่งต้องรันมากขึ้น จนเกมค้าง นี่คือ spiral of death

ทางแก้คือใส่เพดาน แล้วยอมให้เกม “ช้าลง” แทนที่จะค้าง

  • Fiedler ตัดเวลาต่อเฟรมไว้ที่ 0.25 s ที่ 60 Hz = รันได้สูงสุด 0.25 × 60 = 15 step ต่อเฟรม
  • Godot ตั้ง max_physics_steps_per_frame = 8 ค่าเริ่มต้น ถ้าเฟรมตกต่ำกว่า 60 / 8 = 7.5 fps เกมจะดูช้าลง
  • Unity ตั้ง Maximum Allowed Timestep ค่าเริ่มต้น 1/3 วินาที เวลาที่เกินจะไม่ถูกจำลอง

โค้ดเต็ม (JavaScript)

const DT = 1 / 60;           // ฟิสิกส์ 60 Hz
const MAX_FRAME = 0.25;      // เพดานกัน spiral of death
let acc = 0, last = 0;
let prev = { x: 0 }, curr = { x: 0 };

function update(s, dt) {     // ฟิสิกส์ล้วน รับ dt คงที่เสมอ
  return { x: s.x + 300 * dt };
}

function frame(nowMs) {
  const now = nowMs / 1000;
  const frameTime = Math.min(now - last, MAX_FRAME);
  last = now;
  acc += frameTime;
  while (acc >= DT) {        // กินเวลาทีละก้อน
    prev = curr;
    curr = update(curr, DT);
    acc -= DT;
  }
  const alpha = acc / DT;    // เศษ = สัดส่วนระหว่างสอง step
  draw(prev.x * (1 - alpha) + curr.x * alpha);
  requestAnimationFrame(frame);
}

requestAnimationFrame(t => { last = t / 1000; requestAnimationFrame(frame); });

draw(x) เป็นฟังก์ชันวาดของเกมคุณ ลองป้อนเวลาจำลองเข้า frame 144 ครั้งในหนึ่งวินาที curr.x ได้ 300 พอดี ส่วนตำแหน่งที่วาดได้ 295 คือช้ากว่าหนึ่ง step (300 × 1/60 = 5 px) ตามที่คาด MAX_FRAME ยังช่วยอีกกรณี: เบราว์เซอร์หยุด requestAnimationFrame ตอนแท็บอยู่เบื้องหลัง กลับมาแล้ว now - last อาจเป็นหลายนาที ไม่มีเพดานเกมจะพยายามจำลองย้อนทั้งหมดในเฟรมเดียว

เอนจินเปิดเรื่องนี้ให้ยังไง

เอนจินฟิสิกส์ / fixed stepทุกเฟรมทำให้ภาพลื่น
UnityFixedUpdate ทุก Time.fixedDeltaTime (เอกสารยกตัวอย่าง 0.02 = 50 ครั้ง/วินาที)UpdateRigidbody.interpolation (ปิดเป็นค่าเริ่มต้น)
Godot_physics_process(delta) ตาม physics_ticks_per_second (ค่าเริ่มต้น 60)_process(delta)Project Settings > Physics > Common > Physics Interpolation
Robloxฟิสิกส์เอนจินทำเอง PreSimulation ก่อน, PostSimulation หลังPreRender (ฝั่ง client)เอนจินจัดการ

รายละเอียดที่ต้องรู้

  • Unity ถ้าเฟรมเร็วกว่า fixed rate บางเฟรมไม่มี FixedUpdate เลย ถ้าช้ากว่า บางเฟรมมี 2-3 ครั้ง ตรงกับตารางถังด้านบน
  • Godot การเคลื่อนที่ต้องอยู่ใน _physics_process ถ้าเปิด physics interpolation ย้ายวัตถุนอก physics tick จะกระตุก ตอน teleport ให้เรียก reset_physics_interpolation()
  • Roblox Stepped กับ RenderStepped มี migration note ให้ใช้ PreSimulation กับ PreRender แทนในงานใหม่ ส่วน Heartbeat ทำงานจุดเดียวกับ PostSimulation (หลังฟิสิกส์) ทุก event ส่ง delta time มา ต้องคูณเสมอ และมี RunService:BindToSimulation() ที่เรียกฟังก์ชันด้วยความถี่คงที่ไม่ผูก frame rate (ค่าเริ่มต้น 30 Hz) แต่ใช้ได้เมื่อเปิด Workspace.UseFixedSimulation เท่านั้น
-- Roblox: ขยับ Part ที่ Anchored ด้วยความเร็วคงที่ทุกจอ
local RunService = game:GetService("RunService")
local part = workspace.MovingPlatform
local SPEED = 8 -- studs ต่อวินาที

RunService.PreSimulation:Connect(function(dt)
	part.CFrame += Vector3.new(SPEED * dt, 0, 0)
end)

ถ้ากำลังหัดทำเกม Roblox EP5 ลาวาขึ้น ใช้หลักเดียวกันกับลาวาที่สูงขึ้นตามเวลา

ข้อผิดพลาดที่เจอบ่อย

  • ขยับต่อเฟรม ไม่คูณ dt บั๊กแรกสุด และหาไม่เจอถ้าทีมใช้จอ 60Hz ทั้งหมด ทดสอบบนจอรีเฟรชสูงหรือจำกัดเฟรมเป็น 30 ดูบ้าง
  • อ่าน input ใน fixed step ปุ่มที่กดแค่เฟรมเดียวอาจตกช่องที่ไม่มี step เลย (เฟรม 1, 2, 4 ในตาราง) ให้เก็บ input ทุกเฟรมแล้วให้ step ถัดไปมาอ่าน
  • ย้ายวัตถุฟิสิกส์ใน Update / _process ผลคือฟิสิกส์กับภาพสู้กัน กระตุก หรือชนผิด
  • ไม่มีเพดาน เฟรมสะดุดครั้งเดียว (โหลด asset, สลับแท็บ) พาเข้า spiral of death
  • ตั้ง physics rate สูงไว้ก่อน 240 Hz คือรัน step มากกว่า 60 Hz 4 เท่า เริ่มที่ 60 แล้วขึ้นเมื่อเจอปัญหาทะลุจริง และถ้าขึ้นมาก ๆ ใน Godot ต้องขึ้น max_physics_steps_per_frame ตาม

แหล่งอ้างอิง

คำถามที่พบบ่อย

ใช้ Unity, Godot หรือ Roblox ต้องเขียน accumulator เองไหม

ไม่ต้อง เอนจินทำให้แล้ว หน้าที่เราคือวางโค้ดให้ถูกที่ ฟิสิกส์และการเคลื่อนที่ไว้ใน FixedUpdate (Unity) หรือ _physics_process (Godot) ส่วน Roblox ให้คูณ deltaTime ที่ event ส่งมาทุกครั้ง

physics step ควรเป็นกี่ Hz

60 Hz เป็นจุดเริ่มที่ดี Godot ตั้ง 60 เป็นค่าเริ่มต้น เกมที่มีของเร็วมาก ๆ อาจต้องสูงขึ้นเพื่อไม่ให้ทะลุกำแพง แต่ทุก Hz ที่เพิ่มคือ CPU ที่เพิ่ม และถ้าตั้งสูงขึ้นมากต้องเพิ่มเพดานจำนวน step ต่อเฟรมด้วย

interpolation ทำให้ภาพช้ากว่าความจริงไหม

ช้ากว่าไม่เกินหนึ่ง step เพราะวาดระหว่างสถานะก่อนหน้ากับสถานะล่าสุด ที่ 60 Hz คือไม่เกิน 16.7 ms เกมส่วนใหญ่รู้สึกไม่ออก แลกกับภาพที่ไม่กระตุก

คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ

อ่านต่อ

#เกมกลาง

Godot 4.6 ใช้ Jolt เป็นฟิสิกส์ 3D ค่าเริ่มต้น: โปรเจกต์ เก่าต้องย้ายเองไหม

Godot 4.6 ตั้ง Jolt Physics ให้โปรเจกต์ 3D ใหม่ แต่โปรเจกต์เดิมยังใช้ GodotPhysics3D อยู่ บทความนี้อธิบายพฤติกรรมที่ต่าง ค่าที่ต้องรู้ และวิธีย้ายพร้อมทดสอบ

อ่าน 8 นาที
#เกมกลาง

Roblox Server Authority: กันโกงด้วย client prediction และ rollback

Roblox เปิด Server Authority ให้ทุกเกมแล้ว เซิร์ฟเวอร์เป็นเจ้าของสถานะเกม เครื่องผู้เล่นทายผลล่วงหน้าแล้วย้อนแก้เมื่อทายผิด วิธีเปิด วิธีเขียนโค้ด และข้อจำกัด

อ่าน 10 นาที
#เกมขั้นสูง

WebGPU สำหรับเกมบนเว็บ ปี 2026: compute shader ทำ particle และ fallback ไป WebGL

WebGPU ใช้ได้ใน Chrome, Edge, Firefox และ Safari แล้ว แต่ยังมีช่องว่างบน Linux และ Android บทความนี้เขียน particle ด้วย compute shader และวางทางถอยไป WebGL 2

อ่าน 12 นาที