การซิงโครไนซ์ระหว่างเธรดเกมตรรกะและเธรดการแสดงผล


16

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

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

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

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


1
ฉันเกลียดที่จะสแปมลิงก์ แต่ฉันคิดว่านี่เป็นการอ่านที่ดีมากและควรตอบคำถามของคุณทั้งหมด: altdevblogaday.com/2011/07/03/threading-and-your-game-loop
Roy T.


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

ฉันคิดว่าลิงก์ค่อนข้างชัดเจนว่ามีการคัดลอกสถานะเป็นจำนวนเท่าใดในแต่ละกรณี เช่น. (จากลิงค์ที่ 1) "ชุดข้อมูลที่จำเป็นสำหรับการวาดเฟรม แต่ไม่มีสถานะเกมอื่น ๆ " หรือ (จากลิงค์ที่ 2) "ข้อมูลยังคงต้องแชร์ แต่ตอนนี้แทนที่จะแต่ละระบบเข้าถึงตำแหน่งข้อมูลทั่วไปเพื่อรับข้อมูลตำแหน่งหรือข้อมูลปฐมนิเทศแต่ละระบบมีสำเนาของตัวเอง" (ดู 3.2.2 โดยเฉพาะอย่างยิ่งรัฐ - ผู้จัดการ)
DMGregory

ใครก็ตามที่เขียนว่าบทความของ Intel ดูเหมือนจะไม่รู้ว่าการทำเกลียวในระดับบนเป็นความคิดที่แย่มาก ไม่มีใครทำสิ่งที่โง่ ทันใดนั้นแอปพลิเคชั่นทั้งหมดจะต้องสื่อสารผ่านช่องทางเฉพาะและมีการล็อคและ / หรือการแลกเปลี่ยนสถานะขนาดใหญ่ที่ประสานงานกันทุกที่ ไม่ต้องพูดถึงว่าจะไม่มีการบอกเวลาที่ข้อมูลจะถูกประมวลผลดังนั้นจึงยากอย่างยิ่งที่จะให้เหตุผลว่ารหัสทำอะไร มันง่ายกว่ามากในการคัดลอกข้อมูลฉากที่เกี่ยวข้อง (ไม่เปลี่ยนรูปเป็นตัวชี้ ref.count, ไม่แน่นอน - ตามค่า) ในจุดเดียวและปล่อยให้ระบบย่อยเรียงลำดับตามที่ต้องการ
snake5

คำตอบ:


1

ฉันทำงานในสิ่งเดียวกัน ข้อกังวลเพิ่มเติมคือ OpenGL (และสำหรับความรู้ของฉัน, OpenAL) และอินเทอร์เฟซฮาร์ดแวร์อื่น ๆ อีกจำนวนหนึ่งเป็นเครื่องที่มีประสิทธิภาพซึ่งไม่เข้ากันได้ดีกับการถูกเรียกโดยเธรดหลายตัว ฉันไม่คิดว่าพฤติกรรมของพวกเขาจะได้รับการกำหนดและสำหรับ LWJGL (อาจเป็น JOGL) มันมักจะมีข้อยกเว้น

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

ตามกฎแล้วการเรียก OpenGL ทั้งหมดจะต้องผ่านเธรดกราฟิก OpenAL ทั้งหมดผ่านเธรดเสียงอินพุตทั้งหมดผ่านเธรดอินพุตและสิ่งที่เธรดควบคุมการจัดระเบียบทั้งหมดต้องกังวลคือการจัดการเธรด สถานะของเกมถูกจัดขึ้นในคลาส GameState ซึ่งพวกเขาสามารถดูได้ตามที่พวกเขาต้องการ หากฉันเคยตัดสินใจเช่นนั้นบอกว่า JOAL ลงวันที่แล้วและฉันต้องการใช้ JavaSound รุ่นใหม่แทนฉันเพิ่งใช้เธรดอื่นสำหรับ Audio

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


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

1

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

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

ขึ้นอยู่กับแพลตฟอร์มที่คุณใช้ ตัวอย่างเช่น:

  • ถ้าคุณทำในแพลตฟอร์มที่เกี่ยวข้องกับ Open GL ส่วนใหญ่ ( GLUT สำหรับ C / C ++ , JOLG สำหรับ Java , การกระทำที่เกี่ยวข้องกับ OpenGL ES ของ Android ) พวกเขามักจะให้วิธีการ / ฟังก์ชั่นซึ่งเรียกว่าเธรดการแสดงผลเป็นระยะ สามารถรวมเข้ากับลูปเกมของคุณ (โดยไม่ต้องทำการวนซ้ำของ gameloop ขึ้นอยู่กับเมื่อเรียกใช้เมธอดนั้น) สำหรับ GLUT โดยใช้ C คุณทำสิ่งนี้:

    glutDisplayFunc (myFunctionForGraphicsDrawing);

    glutIdleFunc (myFunctionForUpdatingState);

  • ใน JavaScript คุณสามารถใช้Web Workers เนื่องจากไม่มีมัลติเธรด (ที่คุณสามารถเข้าถึงโปรแกรมได้ตามหลักวิชาการ)คุณยังสามารถใช้กลไก "requestAnimationFrame" เพื่อรับการแจ้งเตือนเมื่อมีการกำหนดการแสดงกราฟิกใหม่และทำสถานะเกมของคุณให้สอดคล้อง .

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


0

ใน Java มีคำหลัก "ซิงโครไนซ์" ซึ่งล็อคตัวแปรที่คุณส่งผ่านเพื่อให้เป็นเธรดที่ปลอดภัย ใน C ++ คุณสามารถบรรลุสิ่งเดียวกันโดยใช้ Mutex เช่น:

Java:

synchronized(a){
    //code using a
}

C ++:

mutex a_mutex;

void f(){
    a_mutex.lock();
    //code using a
    a_mutex.unlock();
}

การล็อกตัวแปรทำให้แน่ใจว่าพวกเขาจะไม่เปลี่ยนแปลงในขณะที่รันโค้ดตามมาดังนั้นตัวแปรจะไม่ได้รับการเปลี่ยนแปลงจากเธรดการอัพเดทของคุณในขณะที่คุณกำลังแสดงผล (อันที่จริงพวกมันทำการเปลี่ยนแปลง แต่จากจุดยืนของเธรดการเรนเดอร์ t) คุณต้องระวังด้วยคีย์เวิร์ดที่ซิงโครไนซ์ใน Java เนื่องจากต้องแน่ใจว่าตัวชี้ไปยังตัวแปร / วัตถุไม่เปลี่ยนแปลง แอ็ตทริบิวต์ยังสามารถเปลี่ยนแปลงได้โดยไม่เปลี่ยนพอยน์เตอร์ ในการพิจารณาเรื่องนี้คุณสามารถคัดลอกวัตถุด้วยตัวเองหรือโทรไปซิงโครไนซ์กับคุณสมบัติทั้งหมดของวัตถุที่คุณไม่ต้องการเปลี่ยน


1
Mutexes ไม่จำเป็นต้องเป็นคำตอบที่นี่เพราะ OP ไม่เพียง แต่ต้องการ decouple game logic และการเรนเดอร์เท่านั้น แต่พวกเขาต้องการหลีกเลี่ยงการถ่วงของความสามารถของเธรดหนึ่งที่จะดำเนินการต่อไปโดยไม่คำนึงว่าเธรดอื่น ๆ ห่วง
Naros

0

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

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

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


0

นี่คือโพสต์เก่า แต่ก็ยังปรากฏขึ้นดังนั้นต้องการเพิ่ม 2 เซนต์ของฉันที่นี่

ข้อมูลรายการแรกที่ควรเก็บไว้ใน UI / display thread vs logic thread ในเธรด UI คุณอาจรวมถึง 3d mesh, พื้นผิว, ข้อมูลแสงและสำเนาข้อมูลตำแหน่ง / การหมุน / ทิศทาง

ในเธรดเกมลอจิกคุณอาจต้องใช้ขนาดวัตถุของเกมในแบบ 3 มิติ, ขอบเขตดั้งเดิม (ทรงกลม, ลูกบาศก์), ข้อมูล 3d mesh แบบง่าย (สำหรับการชนโดยละเอียดเช่น), คุณลักษณะทั้งหมดที่มีผลต่อการเคลื่อนไหว / พฤติกรรมเช่นความเร็ววัตถุ และข้อมูลตำแหน่ง / การหมุน / ทิศทาง

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

วิธีที่คุณทำขึ้นอยู่กับภาษาที่คุณใช้ด้วย ใน Scala คุณสามารถใช้ Software Transactional Memory ใน Java / C ++ การล็อค / การซิงโครไนซ์บางชนิด ฉันชอบข้อมูลที่ไม่เปลี่ยนรูปแบบดังนั้นฉันมักจะกลับวัตถุที่เปลี่ยนรูปแบบไม่ได้ใหม่สำหรับการปรับปรุงทุกครั้ง นี่เป็นบิตของหน่วยความจำเสีย แต่ด้วยคอมพิวเตอร์ที่ทันสมัยมันไม่ใช่เรื่องใหญ่อะไร อย่างไรก็ตามหากคุณต้องการล็อคโครงสร้างข้อมูลที่ใช้ร่วมกันคุณสามารถทำได้ ตรวจสอบคลาส Exchanger ใน Java โดยใช้บัฟเฟอร์อย่างน้อยสองรายการสามารถเร่งความเร็วของสิ่งต่างๆ

ก่อนที่คุณจะเข้าสู่การแชร์ข้อมูลระหว่างเธรดจะต้องคำนวณจำนวนข้อมูลที่คุณต้องการส่งผ่าน หากคุณมีการแบ่งพาร์ติชันสามมิติในพื้นที่สามมิติและคุณสามารถเห็นวัตถุเกม 5 รายการจากทั้งหมด 10 วัตถุแม้ว่าตรรกะของคุณจะต้องอัปเดตทั้ง 10 รายการคุณต้องวาดเฉพาะ 5 รายการที่คุณเห็น สำหรับการอ่านเพิ่มเติมลองดูที่บล็อกนี้: http://gameprogrammingpatterns.com/game-loop.html สิ่งนี้ไม่เกี่ยวกับการซิงโครไนซ์ แต่มันแสดงให้เห็นว่าตรรกะของเกมถูกแยกออกจากการแสดงผลอย่างไรและความท้าทายใดที่คุณต้องเอาชนะ (FPS) หวังว่าจะช่วยได้

เครื่องหมาย

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