มีสองสิ่งสำคัญที่จะทำให้การเคลื่อนไหวราบรื่นขึ้นอย่างแรกคือสิ่งที่คุณต้องการเพื่อให้ตรงกับสถานะที่คาดไว้ ณ เวลาที่เฟรมถูกนำเสนอให้กับผู้ใช้ข้อที่สองคือคุณต้องนำเสนอเฟรมให้กับผู้ใช้ ในช่วงเวลาที่ค่อนข้างคงที่ การนำเสนอเฟรมที่ T + 10ms จากนั้นอีกเฟรมที่ T + 30ms และอีกเฟรมที่ T + 40ms จะปรากฏต่อผู้ใช้ในการตัดสินแม้ว่าสิ่งที่แสดงจริงสำหรับช่วงเวลาเหล่านั้นจะถูกต้องตามการจำลอง
ลูปหลักของคุณดูเหมือนจะไม่มีกลไกการ gating ใด ๆ เพื่อให้แน่ใจว่าคุณจะแสดงผลเป็นระยะเท่านั้น ดังนั้นบางครั้งคุณอาจทำการอัพเดท 3 ครั้งระหว่างการเรนเดอร์บางครั้งคุณอาจทำ 4 โดยทั่วไปลูปของคุณจะแสดงผลบ่อยที่สุดเท่าที่จะเป็นไปได้ทันทีที่คุณจำลองเวลาเพียงพอที่จะผลักดันสถานะการจำลองหน้าเวลาปัจจุบัน จากนั้นแสดงสถานะนั้น แต่ความแปรปรวนในระยะเวลาที่ใช้ในการอัปเดตหรือเรนเดอร์และช่วงเวลาระหว่างเฟรมจะแตกต่างกันเช่นกัน คุณมีการประทับเวลาคงที่สำหรับการจำลอง แต่การประทับเวลาผันแปรสำหรับการเรนเดอร์ของคุณ
สิ่งที่คุณอาจต้องการคือการรอก่อนเรนเดอร์ของคุณเพื่อให้แน่ใจว่าคุณจะเริ่มเรนเดอร์เมื่อเริ่มช่วงเรนเดอร์เท่านั้น เป็นการดีที่ควรปรับตัว: หากคุณใช้เวลานานเกินไปในการอัปเดต / แสดงผลและการเริ่มต้นของช่วงเวลาได้ผ่านไปแล้วคุณควรแสดงผลทันที แต่ยังเพิ่มความยาวช่วงเวลาจนกว่าคุณจะสามารถแสดงผลและอัปเดตอย่างสม่ำเสมอและยัง การเรนเดอร์ถัดไปก่อนที่ช่วงเวลาจะเสร็จสิ้น หากคุณมีเวลาเหลือเฟือคุณสามารถลดช่วงเวลาช้าลง (เช่นเพิ่มอัตราเฟรม) เพื่อให้แสดงผลเร็วขึ้นอีกครั้ง
แต่และนี่คือนักเตะถ้าคุณไม่แสดงเฟรมทันทีหลังจากตรวจพบว่าสถานะการจำลองได้รับการอัปเดตเป็น "ตอนนี้" คุณจะแนะนำนามแฝงชั่วคราว เฟรมที่นำเสนอแก่ผู้ใช้นั้นถูกนำเสนอในเวลาที่ผิดเล็กน้อยและในตัวเองจะรู้สึกเหมือนพูดติดอ่าง
นี่คือเหตุผลสำหรับ "การตั้งเวลาบางส่วน" คุณจะเห็นกล่าวถึงในบทความที่คุณอ่าน มันมีอยู่ในนั้นด้วยเหตุผลที่ดีและนั่นเป็นเพราะถ้าคุณไม่แก้ไขการประทับเวลาฟิสิกส์ของคุณให้เป็นอินทิกรัลหลายค่าของการประทับเวลาการแสดงผลคงที่ของคุณคุณก็ไม่สามารถแสดงเฟรมในเวลาที่เหมาะสม คุณจบด้วยการนำเสนอพวกเขาเร็วเกินไปหรือช้าไป วิธีเดียวที่จะได้รับอัตราการเรนเดอร์คงที่และยังคงนำเสนอบางสิ่งที่ถูกต้องทางร่างกายคือการยอมรับว่าในช่วงเวลาที่เรนเดอร์เรนเดอร์มาถึงคุณจะเป็นกึ่งกลางระหว่าง แต่นั่นไม่ได้หมายความว่าวัตถุจะถูกปรับเปลี่ยนในระหว่างการเรนเดอร์ เพียงแค่การเรนเดอร์ต้องสร้างตำแหน่งของออบเจกต์ชั่วคราวเพื่อให้สามารถเรนเดอร์ที่ใดที่หนึ่งก่อนและหลังการอัพเดท สิ่งสำคัญ - อย่าเปลี่ยนสถานะโลกสำหรับการเรนเดอร์การอัพเดตเท่านั้นควรเปลี่ยนสถานะโลก
ดังนั้นเมื่อต้องการใส่ลงในลูป pseudocode ฉันคิดว่าคุณต้องการอะไรมากกว่านี้
InitialiseWorldState();
previousTime = currentTime = 0.0;
renderInterval = 1.0 / 60.0; //A nice high starting interval
subFrameProportion = 1.0; //100% currentFrame, 0% previousFrame
while (true)
{
frameStart = ActualTime();
//Render the world state as if it was some proportion
// between previousTime and currentTime
// E.g. if subFrameProportion is 0.5, previousTime is 0.1 and
// currentTime is 0.2, then we actually want to render the state
// as it would be at time 0.15. We'd do that by interpolating
// between movingObject.previousPosition and movingObject.currentPosition
// with a lerp parameter of 0.5
Render(subFrameProportion);
//Check we've not taken too long and missed our render interval
frameTime = ActualTime() - frameStart;
if (frameTime > renderInterval)
{
renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
}
expectedFrameEnd = frameStart + renderInterval;
//Loop until it's time to render the next frame
while (ActualTime() < expectedFrameEnd)
{
//step the simulation forward until it has moved just beyond the frame end
if (previousTime < expectedFrameEnd) &&
currentTime >= expectedFrameEnd)
{
previousTime = currentTime;
Update();
currentTime += fixedTimeStep;
//After the update, all objects will be in the position they should be for
// currentTime, **but** they also need to remember where they were before,
// so that the rendering can draw them somewhere between previousTime and
// currentTime
//Check again we've not taken too long and missed our render interval
frameTime = ActualTime() - frameStart;
if (frameTime > renderInterval)
{
renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
expectedFrameEnd = frameStart + renderInterval
}
}
else
{
//We've brought the simulation to just after the next time
// we expect to render, so we just want to wait.
// Ideally sleep or spin in a tight loop while waiting.
timeTillFrameEnd = expectedFrameEnd - ActualTime();
sleep(timeTillFrameEnd);
}
}
//How far between update timesteps (i.e. previousTime and currentTime)
// will we be at the end of the frame when we start the next render?
subFrameProportion = (expectedFrameEnd - previousTime) / (currentTime - previousTime);
}
สำหรับการทำงานของวัตถุทั้งหมดที่ได้รับการปรับปรุงจำเป็นต้องรักษาความรู้ว่าพวกเขาอยู่ที่ไหนมาก่อนและตอนนี้พวกเขาอยู่ที่ไหนเพื่อให้การแสดงผลสามารถใช้มันเป็นความรู้เกี่ยวกับที่ของวัตถุ
class MovingObject
{
Vector velocity;
Vector previousPosition;
Vector currentPosition;
Initialise(startPosition, startVelocity)
{
currentPosition = startPosition; // position at time 0
velocity = startVelocity;
//ignore previousPosition because we should never render before time 0
}
Update()
{
previousPosition = currentPosition;
currentPosition += velocity * fixedTimeStep;
}
Render(subFrameProportion)
{
Vector actualPosition =
Lerp(previousPosition, currentPosition, subFrameProportion);
RenderAt(actualPosition);
}
}
และวางโครงร่างไทม์ไลน์เป็นมิลลิวินาทีโดยบอกว่าการเรนเดอร์ใช้เวลา 3 มิลลิวินาทีในการดำเนินการอัปเดตใช้เวลา 1 มิลลิวินาทีขั้นตอนการอัปเดตของคุณจะถูกกำหนดไว้ที่ 5 มิลลิวินาทีและเรนเดอร์
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
R0 U5 U10 U15 U20 W16 R16 U25 U30 U35 W32 R32
- ครั้งแรกที่เราเริ่มต้นที่เวลา 0 (ดังนั้น currentTime = 0)
- เราแสดงสัดส่วน 1.0 (เวลาปัจจุบัน 100%) ซึ่งจะดึงดูดโลกในเวลา 0
- เมื่อเสร็จสิ้นแล้วเวลาจริงคือ 3 และเราไม่คาดหวังว่าเฟรมจะสิ้นสุดจนถึง 16 ดังนั้นเราจึงจำเป็นต้องเรียกใช้การอัปเดตบางอย่าง
- T + 3: เราอัปเดตตั้งแต่ 0 ถึง 5 (ดังนั้นหลังจากนั้น currentTime = 5, previousTime = 0)
- T + 4: ยังก่อนเฟรมจะสิ้นสุดดังนั้นเราอัปเดตจาก 5 เป็น 10
- T + 5: ยังก่อนเฟรมจะสิ้นสุดดังนั้นเราอัปเดตตั้งแต่ 10 ถึง 15
- T + 6: ยังก่อนเฟรมจะสิ้นสุดดังนั้นเราอัปเดตจาก 15 เป็น 20
- T + 7: ยังอยู่ก่อนเฟรมสิ้นสุด แต่ปัจจุบันเวลาอยู่เหนือเฟรมสิ้นสุด เราไม่ต้องการจำลองอีกต่อไปเพราะการทำเช่นนั้นจะผลักดันเราเกินกว่าเวลาที่เราต้องการแสดงผลต่อไป เรารออย่างเงียบ ๆ แทนช่วงเวลาเรนเดอร์ถัดไป (16)
- T + 16: ถึงเวลาที่จะเรนเดอร์อีกครั้ง ก่อนหน้านี้เวลา 15, เวลาปัจจุบันคือ 20 ดังนั้นหากเราต้องการเรนเดอร์ที่ T + 16 เราจะผ่าน 1 มิลลิวินาทีผ่านการประทับเวลานาน 5ms ดังนั้นเราจึง 20% ของทางผ่านเฟรม (สัดส่วน = 0.2) เมื่อเราเรนเดอร์เราวาดวัตถุ 20% ของวิธีระหว่างตำแหน่งก่อนหน้าและตำแหน่งปัจจุบัน
- วนกลับไปที่ 3 และทำต่อไปเรื่อย ๆ
มีความแตกต่างกันเล็กน้อยที่นี่เกี่ยวกับการจำลองล่วงหน้าก่อนเวลาซึ่งหมายความว่าอินพุตของผู้ใช้อาจถูกละเว้นแม้ว่าจะเกิดขึ้นก่อนที่เฟรมจะแสดงผลจริง แต่ไม่ต้องกังวลเกี่ยวกับเรื่องนั้นจนกว่าคุณจะมั่นใจว่าลูปจำลองได้อย่างราบรื่น