วิธีปรับปรุงประสิทธิภาพสำหรับฟังก์ชั่นราคาแพงในตัวสร้างเมือง 2 มิติ


9

ฉันค้นหาคำตอบแล้ว แต่ฉันไม่สามารถหาวิธีที่ดีที่สุดในการจัดการฟังก์ชั่น / การคำนวณราคาแพง

ในเกมปัจจุบันของฉัน (อาคารเมืองแบบกระเบื้อง 2d) ผู้ใช้สามารถวางอาคารสร้างถนน ฯลฯ อาคารทุกหลังจำเป็นต้องเชื่อมต่อกับทางแยกที่ผู้ใช้ต้องวางไว้ที่ชายแดนของแผนที่ หากอาคารไม่ได้เชื่อมต่อกับทางแยกนี้สัญญาณ "ไม่ได้เชื่อมต่อกับถนน" จะปรากฏขึ้นเหนือสิ่งปลูกสร้างที่ได้รับผลกระทบ อาคารส่วนใหญ่มีรัศมีและอาจเกี่ยวข้องกันเช่นกัน (เช่นแผนกดับเพลิงสามารถช่วยบ้านทั้งหมดภายในรัศมี 30 แผ่น) นั่นคือสิ่งที่ฉันต้องปรับปรุง / ตรวจสอบเมื่อการเชื่อมต่อถนนเปลี่ยนไป

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

  1. หากอาคารยังคงเชื่อมต่อกับทางแยกในกรณีที่มีการถอดกระเบื้องถนน (ฉันทำเช่นนั้นเฉพาะอาคารที่ได้รับผลกระทบจากถนนนั้น) (อาจเป็นปัญหาที่เล็กกว่าในสถานการณ์นี้)
  2. รายการของกระเบื้องรัศมีและได้รับอาคารภายในรัศมี(ลูปซ้อนกัน - ปัญหาใหญ่!)

    // Go through all buildings affected by erasing this road tile.
    foreach(var affectedBuilding in affectedBuildings) {
        // Get buildings within radius.
        foreach(var radiusTile in affectedBuilding.RadiusTiles) {
            // Get all buildings on Map within this radius (which is technially another foreach).
            var buildingsInRadius = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);  
    
            // Do stuff.
        }
    }

ทั้งหมดนี้แบ่ง FPS ของฉันจาก 60 เป็นเกือบ 10 เป็นเวลาหนึ่งวินาที

ดังนั้นฉันจะทำ ความคิดของฉันจะเป็น:

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

ใช้วิธีการสุดท้ายฉันสามารถลบหนึ่งซ้อนในรูปแบบ foreach นี้แทน:

// Go through all buildings affected by erasing this road tile.
foreach(var affectedBuilding in affectedBuildings) {
    // Go through buildings within radius.
    foreach(var buildingInRadius in affectedBuilding.BuildingsInRadius) {
        // Do stuff.
    }
}

แต่ฉันไม่รู้ว่านี่เพียงพอหรือไม่ เกมอย่าง Cities Skylines จะต้องจัดการกับสิ่งก่อสร้างมากมายหากผู้เล่นมีแผนที่ขนาดใหญ่ พวกเขาจัดการกับสิ่งเหล่านั้นได้อย่างไร! อาจมีคิวการอัปเดตเนื่องจากสิ่งปลูกสร้างทั้งหมดจะไม่อัพเดตในเวลาเดียวกัน

ฉันรอคอยที่จะความคิดและความคิดเห็นของคุณ!

ขอบคุณมาก!


2
การใช้ตัวสร้างโปรไฟล์ควรช่วยระบุว่าส่วนใดของรหัสที่มีปัญหา อาจเป็นวิธีที่คุณค้นหาอาคารที่ได้รับผลกระทบหรืออาจเป็น // สิ่งที่ต้องทำ ในฐานะที่เป็นบันทึกย่อของเกมใหญ่ ๆ อย่าง City Skylines จัดการกับปัญหาเหล่านี้โดยใช้โครงสร้างข้อมูลเชิงพื้นที่เช่น quad-trees ดังนั้นข้อความค้นหาเชิงพื้นที่ทั้งหมดนั้นเร็วกว่าการไปหาอาร์เรย์ด้วยลูป ในกรณีของคุณคุณสามารถมีกราฟการพึ่งพาของสิ่งปลูกสร้างทั้งหมดและโดยการทำตามกราฟนั้นสามารถรู้ได้ทันทีว่าอะไรส่งผลกระทบต่อสิ่งที่ไม่มีการวนซ้ำ
Exaila

ขอบคุณสำหรับข้อมูลรายละเอียด ฉันชอบความคิดในการพึ่งพา! ฉันจะดูที่นั่น!
Yheeky

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

ฉันดีใจที่คุณพบว่ามีประโยชน์: D
Exaila

คำตอบ:


3

การครอบคลุมอาคารแคช

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

นี่คือการแลกเปลี่ยนแบบคลาสสิกของรอบ CPU สำหรับหน่วยความจำ

การจัดการข้อมูลความครอบคลุมตามภูมิภาค

หากปรากฎว่าคุณใช้หน่วยความจำมากเกินไปในการติดตามข้อมูลนี้ดูว่าคุณสามารถจัดการข้อมูลดังกล่าวตามภูมิภาคของแผนที่ได้หรือไม่ แบ่งแผนที่ของคุณออกเป็นสี่เหลี่ยมจตุรัสของn *nกระเบื้อง หากพื้นที่ครอบคลุมโดยแผนกดับเพลิงอาคารทั้งหมดในภูมิภาคนั้นจะได้รับการคุ้มครองเช่นกัน ดังนั้นคุณจะต้องเก็บข้อมูลความคุ้มครองตามภูมิภาคไม่ใช่ตามแต่ละอาคาร หากพื้นที่ครอบคลุมเพียงบางส่วนคุณจะต้องถอยกลับไปที่การจัดการการเชื่อมต่อโดยการสร้างในภูมิภาคนั้น ดังนั้นฟังก์ชั่นการอัปเดตสำหรับสิ่งปลูกสร้างของคุณจะตรวจสอบก่อน "ภูมิภาคนี้อาคารนี้มีแผนกดับเพลิงอยู่หรือไม่" และถ้าไม่ใช่ "อาคารนี้มีแผนกดับเพลิงแยกต่างหากหรือไม่" สิ่งนี้ยังเพิ่มความเร็วในการอัปเดตด้วยเนื่องจากเมื่อแผนกดับเพลิงถูกนำออกคุณไม่จำเป็นต้องอัปเดตสถานะครอบคลุมของอาคาร 2,000 แห่งอีกต่อไปคุณจะต้องอัปเดตอาคาร 100 แห่งและ 25 ภูมิภาค

การอัปเดตล่าช้า

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

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

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

เกี่ยวกับมัลติเธรด

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


สิ่งนี้อธิบายได้ว่าทำไม SimCity บน SNES ใช้เวลาสักครู่เพื่อให้พลังงานเชื่อมต่อ / เชื่อมต่ออีกครั้งฉันคิดว่ามันเกิดขึ้นกับเอฟเฟกต์อื่น ๆ
lozzajp

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

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

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

1
@Yheeky อย่างที่ฉันบอกว่าไม่มีวิธีแก้ปัญหาที่ใช้ในระดับสากล สิ่งที่ฉันมักจะลอง (ตามลำดับ): 1. ดูว่าคุณสามารถเพิ่มประสิทธิภาพการคำนวณนั้นโดยใช้อัลกอริทึมและ / หรือโครงสร้างข้อมูลที่เหมาะสมยิ่งขึ้นหรือไม่ 2. ดูว่าคุณสามารถแบ่งออกเป็นงานย่อยที่คุณสามารถล่าช้าเป็นรายบุคคล 3. ดูว่าคุณสามารถทำสิ่งนี้ในภัยคุกคามที่แยกต่างหาก 4. กำจัดช่างเกมที่ต้องการการคำนวณนั้นและดูว่าคุณสามารถแทนที่มันด้วยสิ่งที่คำนวณได้น้อยลงหรือไม่
ฟิลิปป์

3

1. งานซ้ำซ้อนการทำงานซ้ำ

คุณaffectedBuildingsอยู่ใกล้ ๆ กันดังนั้นรัศมีที่แตกต่างกันจะทับซ้อนกัน ติดธงสิ่งปลูกสร้างที่จำเป็นต้องได้รับการปรับปรุงแล้วอัปเดตสิ่งปลูกสร้าง

var toBeUpdated = new HashSet<Tiles>();
foreach(var affectedBuilding in affectedBuildings) {
    foreach(var radiusTile in affectedBuilding.RadiusTiles) {
         toBeUpdated.Add(radiusTile);

}
foreach (var tile in toBeUpdated)
{
    var buildingsInTile = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);
    // Do stuff.
}

2. โครงสร้างข้อมูลที่ไม่เหมาะสม

var buildingsInTile = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);

ควรจะชัดเจน

var buildingsInRadius = tile.Buildings;

โดยที่ Buildings จะต้องIEnumerableมีการวนซ้ำตลอดเวลา (เช่น a List<Building>)


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