โครงสร้างข้อมูลสำหรับการแก้ไขและเธรด?


20

ฉันได้รับการจัดการกับปัญหาการกระเพื่อมอัตราเฟรมกับเกมของฉันเมื่อเร็ว ๆ นี้และดูเหมือนว่าทางออกที่ดีที่สุดจะเป็นที่แนะนำโดย Glenn Fiedler (Gaffer ในเกม) ในคลาสสิกFix Your Timestep! บทความ.

ตอนนี้ - ฉันใช้ขั้นตอนเวลาที่แน่นอนสำหรับการอัปเดตของฉันแล้ว ปัญหาคือฉันไม่ได้ทำการแก้ไขที่แนะนำสำหรับการเรนเดอร์ ผลที่สุดคือฉันได้รับสองเท่าหรือข้ามเฟรมหากอัตราการแสดงผลของฉันไม่ตรงกับอัตราการอัปเดตของฉัน สิ่งเหล่านี้สามารถสังเกตเห็นได้ชัดเจน

ดังนั้นฉันจึงต้องการเพิ่มการแก้ไขในเกมของฉัน - และฉันสนใจที่จะรู้ว่าคนอื่นมีโครงสร้างข้อมูลและรหัสของพวกเขาเพื่อสนับสนุนสิ่งนี้อย่างไร

เห็นได้ชัดว่าฉันจะต้องจัดเก็บ (ที่ไหน / อย่างไร?) ข้อมูลสถานะเกมสองชุดที่เกี่ยวข้องกับโหมดแสดงภาพของฉันเพื่อให้สามารถแก้ไขระหว่างพวกเขาได้

นอกจากนี้ - นี่เป็นสถานที่ที่เหมาะสำหรับเพิ่มเธรด ฉันคิดว่าเธรดการอัปเดตสามารถทำงานในสถานะเกมที่สามได้โดยปล่อยให้อีกสองสำเนาเป็นแบบอ่านอย่างเดียวสำหรับเธรดการแสดงผล (นี่เป็นความคิดที่ดีใช่ไหม)

มันดูเหมือนว่าจะมีสองหรือสามรุ่นของรัฐของเกมสามารถนำประสิทธิภาพการทำงานและ - ไกลที่สำคัญกว่า - ความน่าเชื่อถือและผลิตพัฒนาปัญหาเมื่อเทียบกับมีเพียงรุ่นเดียว ดังนั้นฉันจึงสนใจเป็นพิเศษในวิธีการลดปัญหาเหล่านั้น

โดยเฉพาะอย่างยิ่งผมคิดว่าเป็นปัญหาของวิธีจัดการกับการเพิ่มและลบวัตถุออกจากสถานะเกม

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

คำตอบ:


4

อย่าพยายามจำลองสถานะเกมทั้งหมด การแก้ไขมันจะเป็นฝันร้าย เพียงแยกชิ้นส่วนที่เป็นตัวแปรและต้องการโดยการเรนเดอร์ (ให้เราเรียกสิ่งนี้ว่า "Visual State")

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

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

ตัวอย่าง

การออกแบบแบบดั้งเดิม

class Actor
{
  Matrix4x3 position;
  float fuel;
  float armor;
  float stamina;
  float age;

  void Simulate(float deltaT)
  {
    age += deltaT;
    armor -= HitByAWeapon();
  }
}

ใช้สถานะภาพ

class IVisualState
{
  public:
  virtual void Interpolate(const IVisualState &newVS, float f) {}
};
class Actor
{
  struct VisualState: public IVisualState
  {
    Matrix4x3 position;
    float fuel;
    float armor;
    float stamina;
    float age;

    virtual auto_ptr<IVisualState> Interpolate(const IVisualState &newVS, float f)
    {
      const VisualState &newState = static_cast<const VisualState &>(newVS);
      IVisualState *ret = new VisualState;
      ret->age = lerp(this->age,newState.age);
      // ... interpolate other properties as well, using any suitable interpolation method
      // liner, spline, slerp, whatever works best for the given property
      return ret;
    };
  };

  auto_ptr<VisualState> state_;

  void Simulate(float deltaT)
  {
    state_->age += deltaT;
    state_->armor -= HitByAWeapon();
  }
}

1
ตัวอย่างของคุณจะอ่านง่ายขึ้นหากคุณไม่ได้ใช้ "new" (คำสงวนใน C ++) เป็นชื่อพารามิเตอร์
Steve S

3

โซลูชันของฉันมีความหรูหรา / ซับซ้อนน้อยกว่ามาก ฉันใช้Box2Dเป็นเครื่องมือฟิสิกส์ของฉันเพื่อให้การรักษามากกว่าหนึ่งสำเนาของรัฐระบบไม่สามารถจัดการได้ (โคลนระบบฟิสิกส์แล้วพยายามที่จะให้พวกเขาในการซิงค์อาจจะมีวิธีที่ดีกว่า แต่ฉันไม่สามารถเกิดขึ้นกับ หนึ่ง).

แต่ฉันเก็บเคาน์เตอร์ทำงานของรุ่นฟิสิกส์ การอัปเดตแต่ละครั้งจะเพิ่มการสร้างฟิสิกส์เมื่อระบบฟิสิกส์อัปเดตสองครั้งตัวนับการสร้างจะอัปเดตคู่เช่นกัน

ระบบการเรนเดอร์ติดตามการสร้างเรนเดอร์ล่าสุดและเดลต้านับตั้งแต่การสร้าง เมื่อแสดงวัตถุที่ต้องการสอดแทรกตำแหน่งของพวกเขาสามารถใช้ค่าเหล่านี้พร้อมกับตำแหน่งและความเร็วในการเดาตำแหน่งที่ควรแสดงวัตถุ

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

เมื่อฉันเขียนการประมาณค่าฉันใช้กราฟิกที่ 60Hz และฟิสิกส์ที่ 30Hz ปรากฎว่า Box2D มีเสถียรภาพมากขึ้นเมื่อทำงานที่ 120Hz ด้วยเหตุนี้รหัสการแก้ไขของฉันจึงใช้น้อยมาก โดยการเพิ่มเป้าหมายเป็นสองเท่าทำให้เฟรมฟิสิกส์มีการอัพเดทโดยเฉลี่ยสองครั้งต่อเฟรม ด้วยกระวนกระวายใจที่อาจเป็น 1 หรือ 3 ครั้งเช่นกัน แต่แทบจะไม่เคยเป็น 0 หรือ 4+ อัตราฟิสิกส์ที่สูงขึ้นจะช่วยแก้ไขปัญหาการแก้ไขด้วยตัวเอง เมื่อใช้ทั้งฟิสิกส์และเฟรมที่ 60hz คุณอาจได้รับการอัพเดต 0-2 ต่อเฟรม ความแตกต่างของภาพระหว่าง 0 และ 2 นั้นมากเมื่อเทียบกับ 1 และ 3


3
ฉันก็พบสิ่งนี้เช่นกัน วงฟิสิกส์ 120Hz พร้อมการอัพเดทเฟรมใกล้ 60Hz ทำให้การแก้ไขมีค่าเกือบไร้ค่า น่าเสียดายที่นี่ใช้งานได้กับชุดของเกมที่สามารถจ่าย 120Hz ฟิสิกส์ลูปเท่านั้น

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

อีกทั้ง: ฉันไม่เข้าใจคำอธิบายของคุณเกี่ยวกับระบบการแก้ไขของคุณ ฟังดูคล้ายกับการคาดการณ์ใช่ไหม?
Andrew Russell

โทรดีมาก ฉันอธิบายระบบการคาดการณ์ เมื่อพิจารณาถึงตำแหน่งความเร็วและระยะเวลานับตั้งแต่การอัพเดทฟิสิกส์ครั้งล่าสุดฉันคาดการณ์ว่าวัตถุจะเป็นอย่างไรหากเอนจินฟิสิกส์ไม่หยุดทำงาน
deft_code

2

ฉันได้ยินมาว่าวิธีการประทับเวลาแนะนำค่อนข้างบ่อย แต่ใน 10 ปีในเกมฉันไม่เคยทำงานในโครงการโลกแห่งความจริงที่อาศัยการประทับเวลาและการแก้ไขที่แน่นอน

ดูเหมือนว่าโดยทั่วไปแล้วความพยายามมากกว่าระบบการตั้งเวลาผันแปร (สมมติว่าเป็นช่วงที่เหมาะสมของเฟรมเมเรเตอร์ในช่วงเรียง 25Hz-100Hz)

ฉันลองใช้วิธีการแก้ไขการประทับเวลา + การแก้ไขครั้งเดียวสำหรับต้นแบบที่มีขนาดเล็กมาก - ไม่มีการเธรด แต่เป็นการอัพเดตตรรกะการประทับเวลาคงที่และการเรนเดอร์ที่เร็วที่สุดเท่าที่จะทำได้เมื่อไม่อัปเดต วิธีการของฉันคือการมีคลาสไม่กี่คลาสเช่น CInterpolatedVector และ CInterpolatedMatrix ซึ่งเก็บค่าก่อนหน้า / ปัจจุบันและมี accessor ที่ใช้จากรหัสการแสดงผลเพื่อดึงค่าสำหรับเวลาการเรนเดอร์ปัจจุบัน เวลาปัจจุบัน)

เมื่อสิ้นสุดการอัปเดตวัตถุแต่ละเกมจะตั้งค่าสถานะปัจจุบันเป็นชุดของเวกเตอร์ / เมทริกซ์ที่สามารถแก้ไขได้เหล่านี้ สิ่งนี้สามารถขยายเพื่อรองรับเธรดคุณต้องมีค่าอย่างน้อย 3 ชุด - ค่าที่อัปเดตและอย่างน้อย 2 ค่าก่อนหน้าเพื่อแก้ไขระหว่าง ...

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

IMHO เป็นการดีที่สุดที่จะไปตั้งเวลาผันแปร - เว้นแต่ว่าคุณกำลังสร้าง RTS หรือเกมอื่น ๆ ที่คุณมีวัตถุจำนวนมากและต้องทำการจำลองแบบอิสระ 2 แบบให้ตรงกันสำหรับเกมบนเครือข่าย (ส่งคำสั่ง / คำสั่งเหนือ เครือข่ายมากกว่าตำแหน่งของวัตถุ) ในสถานการณ์นั้นการประทับเวลาคงที่เป็นตัวเลือกเท่านั้น


1
ดูเหมือนว่าอย่างน้อย Quake 3 กำลังใช้วิธีนี้โดยค่าเริ่มต้น "ขีด" คือ 20 fps (50 ms)
Suma

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

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

2

เห็นได้ชัดว่าฉันจะต้องจัดเก็บ (ที่ไหน / อย่างไร?) ข้อมูลสถานะเกมสองชุดที่เกี่ยวข้องกับโหมดแสดงภาพของฉันเพื่อให้สามารถแก้ไขระหว่างพวกเขาได้

ใช่ขอบคุณที่สำคัญที่นี่คือ "เกี่ยวข้องกับ renderer ของฉัน" นี่อาจจะไม่เป็นการเพิ่มตำแหน่งเก่าและเวลาประทับลงในส่วนผสม ด้วยตำแหน่ง 2 ตำแหน่งคุณสามารถสอดแทรกตำแหน่งระหว่างตำแหน่งเหล่านั้นและหากคุณมีระบบภาพเคลื่อนไหว 3 มิติคุณสามารถขอโพสท่าที่แม่นยำในเวลานั้นได้

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

นอกจากนี้ - นี่เป็นสถานที่ที่เหมาะสำหรับเพิ่มเธรด ฉันคิดว่าเธรดการอัปเดตสามารถทำงานในสถานะเกมที่สามได้โดยปล่อยให้อีกสองสำเนาเป็นแบบอ่านอย่างเดียวสำหรับเธรดการแสดงผล (นี่เป็นความคิดที่ดีใช่ไหม)

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


1

หมายเหตุฉันไม่ได้มองการแก้ไขดังนั้นคำตอบนี้ไม่ได้ตอบ ฉันเกี่ยวข้องกับการมีสถานะเกมหนึ่งสำเนาสำหรับเธรดการแสดงผลและอีกอันสำหรับเธรดการอัปเดต ดังนั้นฉันจึงไม่สามารถออกความเห็นเกี่ยวกับปัญหาของการแก้ไขแม้ว่าคุณสามารถแก้ไขวิธีการต่อไปนี้เพื่อแก้ไข

ฉันสงสัยเกี่ยวกับสิ่งนี้ในขณะที่ฉันกำลังออกแบบและคิดถึงเครื่องยนต์แบบมัลติเธรด ดังนั้นผมจึงถามคำถามเกี่ยวกับกองมากเกินเกี่ยวกับวิธีการดำเนินการเรียงลำดับของ "บันทึก" หรือ "การทำธุรกรรม" รูปแบบการออกแบบบาง ฉันได้รับคำตอบที่ดีและคำตอบที่ยอมรับทำให้ฉันคิดอย่างนั้น

มันยากที่จะสร้างวัตถุที่ไม่เปลี่ยนรูปแบบเพราะเด็กทุกคนจะต้องไม่เปลี่ยนรูปแบบและคุณต้องระวังอย่างมากว่าทุกอย่างไม่เปลี่ยนรูป แต่ถ้าคุณระมัดระวังคุณสามารถสร้างซูGameStateเปอร์คลาสที่มีข้อมูลทั้งหมด (และข้อมูลย่อยและอื่น ๆ ) ในเกมของคุณ ส่วน "Model" ของสไตล์องค์กร Model-View-Controller

จากนั้นตามที่ Jeffrey กล่าวว่าอินสแตนซ์ของวัตถุ GameState ของคุณนั้นรวดเร็วหน่วยความจำที่มีประสิทธิภาพและเธรดที่ปลอดภัย ข้อเสียที่สำคัญคือเพื่อที่จะเปลี่ยนแปลงอะไรเกี่ยวกับแบบจำลองคุณจำเป็นต้องสร้างโมเดลขึ้นใหม่ดังนั้นคุณต้องระวังจริงๆว่าโค้ดของคุณจะไม่กลายเป็นระเบียบขนาดใหญ่ การตั้งค่าตัวแปรภายในวัตถุ GameState ให้เป็นค่าใหม่นั้นมีส่วนเกี่ยวข้องมากกว่าเพียงแค่var = val;ในแง่ของบรรทัดของโค้ด

ฉันรู้สึกทึ่งกับมันมาก คุณไม่จำเป็นต้องคัดลอกโครงสร้างข้อมูลทั้งหมดของคุณทุกเฟรม คุณเพียงแค่คัดลอกตัวชี้ไปยังโครงสร้างที่ไม่เปลี่ยนรูป โดยตัวของมันเองนั้นน่าประทับใจมากคุณไม่เห็นด้วยไหม


มันเป็นโครงสร้างที่น่าสนใจอย่างแน่นอน อย่างไรก็ตามฉันไม่แน่ใจว่ามันจะทำงานได้ดีสำหรับเกม - เนื่องจากกรณีทั่วไปเป็นต้นไม้ที่ค่อนข้างแบนซึ่งแต่ละการเปลี่ยนแปลงมีผลเพียงครั้งเดียวต่อเฟรม นอกจากนี้เนื่องจากการจัดสรรหน่วยความจำแบบไดนามิกนั้นไม่ใหญ่มาก
Andrew Russell

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

... นั่นไม่ใช่การจัดสรรแบบไดนามิกเพียงแค่ใช้หน่วยความจำที่จัดสรรล่วงหน้าแบบไดนามิกเท่านั้น)
Kaj

1

ฉันเริ่มต้นด้วยการมีสามสถานะเกมของแต่ละโหนดในกราฟฉากของฉัน หนึ่งจะถูกเขียนโดยเธรดกราฟฉากหนึ่งคือกำลังถูกอ่านโดย renderer และที่สามคือการอ่าน / เขียนทันทีที่หนึ่งในนั้นต้องสลับ สิ่งนี้ใช้ได้ดี แต่มีความซับซ้อนมากกว่า

จากนั้นฉันก็รู้ว่าฉันจะต้องรักษาสถานะของสิ่งที่จะแสดงผลสามสถานะเท่านั้น ตอนนี้เธรดที่อัปเดตของฉันเติมบัฟเฟอร์ขนาดเล็กลงหนึ่งในสามของ "RenderCommands" และ Renderer อ่านจากบัฟเฟอร์ใหม่ล่าสุดที่ไม่ได้ถูกเขียนลงไปในขณะนี้ซึ่งจะป้องกันไม่ให้เธรดรออีกต่อไป

ในการตั้งค่าของฉันแต่ละ RenderCommand มีรูปทรงเรขาคณิต / วัสดุ 3 มิติ, เมทริกซ์การแปลงรูปและรายการไฟที่ส่งผลกระทบต่อ (ยังคงแสดงผลไปข้างหน้า)

เธรดการเรนเดอร์ของฉันไม่จำเป็นต้องทำการคำนวณใด ๆ ที่คัดสรรหรือระยะทางที่ไกลอีกต่อไป

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.