นี่คือขั้นตอนที่จำเป็นสำหรับการพัฒนาลูปจำลองฟิสิกส์ของคุณ
1. การประทับเวลา
ปัญหาหลักที่ฉันเห็นด้วยรหัสของคุณก็คือมันไม่ได้คำนึงถึงขั้นตอนทางฟิสิกส์ ควรชัดเจนว่ามีสิ่งผิดปกติเกิดขึ้นPosition += Velocity;เนื่องจากหน่วยไม่ตรงกัน ไม่ว่าVelocityจะเป็นความเร็วหรือสิ่งที่ขาดหายไป
แม้ว่าค่าความเร็วและแรงโน้มถ่วงของคุณจะถูกปรับอัตราส่วนให้แต่ละเฟรมเกิดขึ้นที่หน่วยเวลา1(หมายความว่าเช่น Velocityหมายถึงระยะทางที่เดินทางในหนึ่งวินาที) เวลาจะต้องปรากฏที่ใดที่หนึ่งในรหัสของคุณโดยปริยาย (โดยการแก้ไขตัวแปรเพื่อให้ ชื่อของพวกเขาสะท้อนให้เห็นถึงสิ่งที่พวกเขาเก็บจริง ๆ ) หรืออย่างชัดเจน (โดยการแนะนำการประทับเวลา) ฉันเชื่อว่าสิ่งที่ง่ายที่สุดที่จะทำคือการประกาศหน่วยเวลา:
float TimeStep = 1.0;
และใช้ค่านั้นในทุกที่ที่ต้องการ:
Velocity += Physics.Gravity.Force * TimeStep;
Position += Velocity * TimeStep;
...
โปรดทราบว่าคอมไพเลอร์ที่เหมาะสมจะลดความซับซ้อนของการคูณด้วย1.0ดังนั้นส่วนนั้นจะไม่ทำให้ช้าลง
ตอนนี้Position += Velocity * TimeStepยังไม่แน่นอน (ดูคำถามนี้เพื่อทำความเข้าใจว่าทำไม) แต่ตอนนี้อาจจะทำได้
นอกจากนี้ต้องคำนึงถึงเวลาด้วย:
Velocity *= Physics.Air.Resistance;
มันค่อนข้างยากที่จะแก้ไข วิธีหนึ่งที่เป็นไปได้คือ:
Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, TimeStep),
                    Math.Pow(Physics.Air.Resistance.Y, TimeStep))
          * Velocity;
2. อัพเดตสองเท่า
ตอนนี้ตรวจสอบสิ่งที่คุณทำเมื่อตีกลับ (แสดงเฉพาะรหัสที่เกี่ยวข้อง):
Position += Velocity * TimeStep;
if (Position.Y < 0)
{
    Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
    Position.Y = Position.Y + Velocity.Y * TimeStep;
}
คุณจะเห็นว่าTimeStepมีการใช้สองครั้งระหว่างการเด้ง นี่เป็นการทำให้ลูกบอลมีเวลามากขึ้นสองเท่าในการอัพเดทตัวเอง นี่คือสิ่งที่ควรเกิดขึ้นแทน:
Position += Velocity * TimeStep;
if (Position.Y < 0)
{
    /* First, stop at Y = 0 and count how much time is left */
    float RemainingTime = -Position.Y / Velocity.Y;
    Position.Y = 0;
    /* Then, start from Y = 0 and only use how much time was left */
    Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
    Position.Y = Velocity.Y * RemainingTime;
}
3. แรงโน้มถ่วง
ตรวจสอบส่วนนี้ของรหัสตอนนี้:
if(Position.Y < GraphicsViewport.Height - Texture.Height)
{
    Velocity += Physics.Gravity.Force * TimeStep;
}            
คุณเพิ่มแรงโน้มถ่วงตลอดระยะเวลาของเฟรม แต่ถ้าลูกบอลตีกลับระหว่างกรอบจริงล่ะ จากนั้นความเร็วจะกลับด้าน แต่แรงโน้มถ่วงที่เพิ่มเข้ามาจะทำให้ลูกบอลเร่งความเร็วจากพื้นดิน! ดังนั้นแรงโน้มถ่วงส่วนเกินจะต้องถูกลบออกเมื่อตีกลับจากนั้นเพิ่มอีกครั้งในทิศทางที่ถูกต้อง
อาจเกิดขึ้นได้แม้การเพิ่มแรงโน้มถ่วงในทิศทางที่ถูกต้องจะทำให้ความเร็วในการเร่งความเร็วมากเกินไป เพื่อหลีกเลี่ยงปัญหานี้คุณสามารถข้ามการเพิ่มแรงโน้มถ่วง (หลังจากทั้งหมดมันไม่มากและเพียงแค่เฟรม) หรือยึดความเร็วเป็นศูนย์
4. รหัสคงที่
และนี่คือรหัสที่ได้รับการอัปเดตอย่างสมบูรณ์:
public void Update()
{
    float TimeStep = 1.0;
    Update(TimeStep);
}
public void Update(float TimeStep)
{
    float RemainingTime;
    // Apply gravity if we're not already on the ground
    if(Position.Y < GraphicsViewport.Height - Texture.Height)
    {
        Velocity += Physics.Gravity.Force * TimeStep;
    }
    Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, RemainingTime),
                        Math.Pow(Physics.Air.Resistance.Y, RemainingTime))
              * Velocity;
    Position += Velocity * TimeStep;
    if (Position.X < 0 || Position.X > GraphicsViewport.Width - Texture.Width)
    {
        // We've hit a vertical (side) boundary
        if (Position.X < 0)
        {
            RemainingTime = -Position.X / Velocity.X;
            Position.X = 0;
        }
        else
        {
            RemainingTime = (Position.X - (GraphicsViewport.Width - Texture.Width)) / Velocity.X;
            Position.X = GraphicsViewport.Width - Texture.Width;
        }
        // Apply friction
        Velocity -= Vector2(Math.Pow(Physics.Surfaces.Concrete.X, RemainingTime),
                            Math.Pow(Physics.Surfaces.Concrete.Y, RemainingTime))
                  * Velocity;
        // Invert velocity
        Velocity.X = -Velocity.X;
        Position.X = Position.X + Velocity.X * RemainingTime;
    }
    if (Position.Y < 0 || Position.Y > GraphicsViewport.Height - Texture.Height)
    {
        // We've hit a horizontal boundary
        if (Position.Y < 0)
        {
            RemainingTime = -Position.Y / Velocity.Y;
            Position.Y = 0;
        }
        else
        {
            RemainingTime = (Position.Y - (GraphicsViewport.Height - Texture.Height)) / Velocity.Y;
            Position.Y = GraphicsViewport.Height - Texture.Height;
        }
        // Remove excess gravity
        Velocity.Y -= RemainingTime * Physics.Gravity.Force;
        // Apply friction
        Velocity -= Vector2(Math.Pow(Physics.Surfaces.Grass.X, RemainingTime),
                            Math.Pow(Physics.Surfaces.Grass.Y, RemainingTime))
                  * Velocity;
        // Invert velocity
        Velocity.Y = -Velocity.Y;
        // Re-add excess gravity
        float OldVelocityY = Velocity.Y;
        Velocity.Y += RemainingTime * Physics.Gravity.Force;
        // If velocity changed sign again, clamp it to zero
        if (Velocity.Y * OldVelocityY <= 0)
            Velocity.Y = 0;
        Position.Y = Position.Y + Velocity.Y * RemainingTime;
    }
}
5. การเพิ่มเติมเพิ่มเติม
เพื่อความเสถียรของการจำลองที่ได้รับการปรับปรุงคุณอาจตัดสินใจใช้การจำลองทางฟิสิกส์ด้วยความถี่ที่สูงขึ้น นี้จะทำเล็ก ๆ น้อย ๆจากการเปลี่ยนแปลงดังกล่าวข้างต้นเกี่ยวข้องTimeStepเพราะคุณเพียงแค่ต้องแยกกรอบของคุณในขณะที่ชิ้นมากที่สุดเท่าที่คุณต้องการ ตัวอย่างเช่น
public void Update()
{
    float TimeStep = 1.0;
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
}