เมื่อหลาย ๆ คลาสจำเป็นต้องเข้าถึงข้อมูลเดียวกันข้อมูลจะถูกประกาศที่ไหน?


38

ฉันมีเกมป้องกันหอคอยพื้นฐาน 2 มิติใน C ++

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

update():
  for each creep in creeps:
    creep.update()
  for each tower in towers:
    tower.update()
  for each missile in missiles:
    missile.update()

วัตถุ (ครีพหอคอยและขีปนาวุธ) ถูกเก็บไว้ในพอยน์เตอร์พอยน์เตอร์ หอคอยต้องมีการเข้าถึง vector-of-creeps และ vector-of-missiles เพื่อสร้างขีปนาวุธใหม่และระบุเป้าหมาย

คำถามคือ: ฉันจะประกาศเวกเตอร์ได้ที่ไหน พวกเขาควรเป็นสมาชิกของคลาส Map หรือไม่และส่งผ่านเป็นอาร์กิวเมนต์ไปยังฟังก์ชัน tower.update ()? หรือประกาศทั่วโลก? หรือมีวิธีแก้ไขปัญหาอื่น ๆ ที่ฉันขาดไปทั้งหมดหรือไม่

เมื่อหลาย ๆ คลาสจำเป็นต้องเข้าถึงข้อมูลเดียวกันข้อมูลจะถูกประกาศที่ไหน?


1
สมาชิกทั่วโลกได้รับการพิจารณาว่า 'น่าเกลียด' แต่มีความรวดเร็วและทำให้การพัฒนาง่ายขึ้นหากเป็นเกมเล็ก ๆ นั่นก็ไม่ใช่ปัญหา (IMHO) คุณสามารถสร้างคลาส exteral ซึ่งจัดการตรรกะ ( ทำไมหอคอยต้องใช้เวกเตอร์เหล่านี้) และเข้าถึงเวกเตอร์ทั้งหมดได้
Jonathan Connell

-1 ถ้าสิ่งนี้เกี่ยวข้องกับการเขียนโปรแกรมเกมการกินพิซซ่าก็เช่นกัน คว้าหนังสือการออกแบบซอฟต์แวร์ที่ดีสักเล่ม
Maik Semder

9
@Maik: การออกแบบซอฟต์แวร์มีความเกี่ยวข้องกับการเขียนโปรแกรมเกมอย่างไร? เพียงเพราะมันใช้กับสาขาอื่น ๆ ของการเขียนโปรแกรมไม่ได้ทำให้มันนอกหัวข้อ
BlueRaja - Danny Pflughoeft

@BlueRaja รายการรูปแบบการออกแบบซอฟต์แวร์นั้นเหมาะสมกับ SO มากกว่านั่นคือสิ่งที่มันมีอยู่ GD.SE ใช้สำหรับการเขียนโปรแกรมเกมไม่ใช่การออกแบบซอฟต์แวร์
Maik Semder

คำตอบ:


52

เมื่อคุณต้องการเช่นเดียวของชั้นตลอดโปรแกรมของคุณที่เราเรียกชั้นว่าบริการ มีวิธีการมาตรฐานหลายประการในการใช้บริการในโปรแกรม:

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

    class MyClass
    {
    private:
        static MyClass* _instance;
        MyClass() {} //private constructor
    
    public:
        static MyClass* getInstance();
        void method();
    };
    
    ...
    
    MyClass* MyClass::_instance = NULL;
    MyClass* MyClass::getInstance()
    {
        if(_instance == NULL)
            _instance = new MyClass(); //Not thread-safe version
        return _instance;
    
        //Note that _instance is *never* deleted - 
        //it exists for the entire lifetime of the program!
    }
  • พึ่งพาการฉีด (DI) นี่หมายถึงการส่งผ่านบริการเป็นพารามิเตอร์ตัวสร้าง บริการต้องมีอยู่แล้วเพื่อส่งต่อไปยังชั้นเรียนดังนั้นจึงไม่มีวิธีใดที่บริการทั้งสองจะต้องพึ่งพาซึ่งกันและกัน ใน 98% ของกรณีนี้คือสิ่งที่คุณต้องการ (และอื่น ๆ 2% คุณก็สามารถสร้างsetWhatever()วิธีการและผ่านในการให้บริการในภายหลัง) ด้วยเหตุนี้ DI จึงไม่มีปัญหาการมีเพศสัมพันธ์เช่นเดียวกับตัวเลือกอื่น ๆ มันสามารถใช้กับมัลติเธรดเนื่องจากแต่ละเธรดสามารถมีอินสแตนซ์ของตนเองสำหรับทุกบริการ (และแชร์เฉพาะที่จำเป็นต้องใช้เท่านั้น) นอกจากนี้ยังทำให้รหัสหน่วยทดสอบได้หากคุณสนใจ

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

    //Example of dependency injection
    class Tower
    {
    private:
        MissileCreationService* _missileCreator;
        CreepLocatorService* _creepLocator;
    public:
        Tower(MissileCreationService*, CreepLocatorService*);
    }
    
    //In order to create a tower, the creating-class must also have instances of
    // MissileCreationService and CreepLocatorService; thus, if we want to 
    // add a new service to the Tower constructor, we must add it to the
    // constructor of every class which creates a Tower as well!
    //This is not a problem in languages like C# and Java, where you can use
    // a framework to create an instance and inject automatically.

    ดูหน้านี้ (จากเอกสารสำหรับ Ninject, กรอบงาน C # DI) สำหรับตัวอย่างอื่น

    การฉีดพึ่งพาเป็นวิธีแก้ปัญหาปกติสำหรับปัญหานี้และเป็นคำตอบที่คุณจะเห็น upvoted มากที่สุดสำหรับคำถามเช่นนี้ใน StackOverflow.com DI เป็นประเภทของการกลับรายการควบคุม (IoC)

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

    Service Locators เป็นอีกรูปแบบหนึ่งของ Inversion of Control (IoC) โดยปกติแล้วกรอบงานที่ทำการฉีดการพึ่งพาอัตโนมัติจะมีตัวระบุตำแหน่งบริการ

    XNA (กรอบการเขียนโปรแกรมเกม C # ของ Microsoft)มีตัวระบุตำแหน่งบริการ เพื่อเรียนรู้เพิ่มเติมเกี่ยวกับมันดูคำตอบนี้


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


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Josh

หนึ่งในคำตอบที่ดีที่สุดและชัดเจนที่สุดที่ฉันเคยอ่าน ทำได้ดี. ฉันคิดว่าควรจะมีการแบ่งปันบริการเสมอ
Nikos

5

ฉันเองจะใช้ความแตกต่างที่นี่ ทำไมต้องมีmissileเวกเตอร์เวกเตอร์และtowerเวกเตอร์creep.. เมื่อพวกเขาทั้งหมดเรียกฟังก์ชันเดียวกัน update? ทำไมไม่ได้เวกเตอร์ของตัวชี้ไปยังชั้นฐานบางอย่างEntityหรือGameObject?

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

ทางเดียวคือระบบส่งข้อความบางรูปแบบ towerอาจจะส่งข้อความไปยังmap(ซึ่งมีการเข้าถึงอาจจะมีการอ้างอิงถึงเจ้าของ?) ว่ามันตีcreepและmapแล้วบอกcreepมันรับการตี นี่สะอาดมากและแยกข้อมูล

อีกวิธีหนึ่งคือเพียงค้นหาแผนที่ในสิ่งที่ต้องการ อย่างไรก็ตามอาจมีปัญหากับคำสั่งอัปเดตที่นี่


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

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

1
ในเรื่องของประสิทธิภาพของเกม ดังนั้นเวกเตอร์ของเวลาวัตถุเดียวกันจึงมีตำแหน่งอ้างอิงที่ดีกว่า นอกจากนี้วัตถุ polymorphic ที่มีตัวชี้เสมือนมีประสิทธิภาพที่น่ากลัวเพราะไม่สามารถ inline เข้าในลูปการปรับปรุง
Zan Lynx

0

นี่เป็นกรณีที่การเขียนโปรแกรมเชิงวัตถุที่เข้มงวด (OOP) หยุดทำงาน

ตามหลักการของ OOP คุณควรจัดกลุ่มข้อมูลที่มีพฤติกรรมที่เกี่ยวข้องโดยใช้คลาส แต่คุณมีพฤติกรรม (การกำหนดเป้าหมาย) ที่ต้องการข้อมูลที่ไม่เกี่ยวข้องกัน (towers และ creeps) ในสถานการณ์นี้โปรแกรมเมอร์จำนวนมากจะพยายามเชื่อมโยงพฤติกรรมกับส่วนของข้อมูลที่ต้องการ (เช่นหอคอยจัดการกับการกำหนดเป้าหมาย แต่ไม่ทราบเกี่ยวกับครีพ) แต่มีตัวเลือกอื่น: อย่าจัดกลุ่มพฤติกรรมกับข้อมูล

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

ตัวอย่างที่รุนแรงของวิธีนี้คือสถาปัตยกรรมระบบเอนทิตี

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