ฉันจะใช้ซิงเกิลตันอย่างถูกต้องในการเขียนโปรแกรมเอ็นจิน C ++ ได้อย่างไร?


16

ฉันรู้ว่าซิงเกิลตันไม่ดีเอ็นจิ้นเกมเก่าของฉันใช้วัตถุ 'เกม' ซิงเกิลตันที่จัดการทุกอย่างตั้งแต่การเก็บข้อมูลทั้งหมดจนถึงลูปเกมจริง ตอนนี้ฉันกำลังสร้างใหม่

ปัญหาคือการวาดอะไรบางอย่างใน SFML คุณใช้ที่เป็นหน้าต่างwindow.draw(sprite) sf::RenderWindowมี 2 ​​ตัวเลือกที่ฉันเห็นอยู่ที่นี่:

  1. สร้างวัตถุเกม Singleton ที่ทุกหน่วยงานในเกมดึง (สิ่งที่ฉันใช้มาก่อน)
  2. ทำให้นี่เป็นตัวสร้างสำหรับเอนทิตี: Entity(x, y, window, view, ...etc)(นี่เป็นเพียงเรื่องไร้สาระและน่ารำคาญ)

อะไรจะเป็นวิธีที่เหมาะสมในการทำเช่นนี้ในขณะที่รักษานวกรรมิกของ Entity ให้เป็นแค่ x และ y

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


1
คุณสามารถส่งหน้าต่างเป็นอาร์กิวเมนต์ของฟังก์ชัน 'render'
dari

25
ซิงเกิลไม่เลว! มันอาจมีประโยชน์และบางครั้งก็จำเป็น (แน่นอนว่ามันเป็นปัญหา)
ExOfDe

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

ฉันประกาศหน้าต่างของฉันและการอ้างอิงอื่น ๆ ในหลักของฉันแล้วฉันมีตัวชี้ในชั้นเรียนอื่น ๆ ของฉัน
KaareZ

1
@JAB แก้ไขได้อย่างง่ายดายด้วยการเริ่มต้นด้วยตนเองจาก main () การเริ่มต้น Lazy ทำให้มันเกิดขึ้นในช่วงเวลาที่ไม่รู้จักซึ่งไม่ใช่ความคิดที่ดีสำหรับระบบหลักเลยทีเดียว
snake5

คำตอบ:


3

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

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

ดังนั้นการอัพเดตเกมในคลาสระดับบนของคุณอาจมีลักษณะดังนี้:

EntityList entities = mCurrentLevel.getEntities();
for(auto& i : entities){
  // Run game logic...
  i->update(...);
}
// Render all the entities
for(auto& i : entities){
  mRenderer->draw(i->getSprite());
}

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

@ snake5 Justifying singletons บน Stack Exchange เป็นเหมือนการเห็นอกเห็นใจกับ Hitler
Willy Goat

30

วิธีการง่าย ๆ คือทำสิ่งที่เคยSingleton<T>เป็นโลกTแทน Globals มีปัญหาด้วยเช่นกัน แต่พวกเขาไม่ได้เป็นตัวแทนของงานพิเศษและรหัสสำเร็จรูปเพื่อบังคับใช้ข้อ จำกัด เล็กน้อย นี่เป็นเพียงแนวทางเดียวที่จะไม่เกี่ยวข้องกับการสัมผัสกับตัวสร้างเอนทิตี

วิธีที่ยากกว่า แต่อาจดีกว่าคือการ ผ่านการอ้างอิงของคุณไปยังที่ที่คุณต้องการพวกเขา ใช่สิ่งนี้อาจเกี่ยวข้องกับการส่งผ่านWindow *ไปยังวัตถุจำนวนมาก (เช่นเอนทิตีของคุณ) ในแบบที่ดูแย่ ความจริงที่ว่ามันดูไม่ดีควรบอกคุณบางอย่าง: การออกแบบของคุณอาจจะแย่

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

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

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

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


3
ฉันไม่คุ้นเคยกับ C ++ แต่ไม่มีกรอบการฉีดพึ่งพาที่สะดวกสบายสำหรับภาษานี้หรือไม่?
bgusach

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

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

1
+1 สำหรับ "ความจริงที่ว่าดูน่าเกรงขามน่าจะบอกอะไรคุณได้บ้าง: การออกแบบของคุณอาจดูแย่"
Shadow503

+1 สำหรับการให้ทั้งกรณีและอุดมคติของคำตอบอย่างจริงจัง

6

รับช่วงจาก sf :: RenderWindow

SFML สนับสนุนให้คุณสืบทอดจากคลาสของจริง

class GameWindow: public sf::RenderWindow{};

จากที่นี่คุณสร้างฟังก์ชั่นการวาดสมาชิกสำหรับเอนทิตีการวาด

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity);
};

ตอนนี้คุณสามารถทำได้:

GameWindow window;
Entity entity;

window.draw(entity);

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

class Entity: public sf::Sprite{};

ตอนนี้ sf::RenderWindowก็สามารถดึงหน่วยงานและหน่วยงานในขณะนี้มีการทำงานเช่นการและsetTexture() setColor()เอนทิตีสามารถใช้ตำแหน่งของสไปรท์เป็นตำแหน่งของตัวเองทำให้คุณสามารถใช้setPosition()ฟังก์ชั่นสำหรับทั้งการเคลื่อนย้ายเอนทิตีและสไปรท์


ในท้ายที่สุดมันก็ค่อนข้างดีถ้าคุณมี:

window.draw(game);

ด้านล่างคือตัวอย่างการใช้งานอย่างรวดเร็ว

class GameWindow: public sf::RenderWindow{
 sf::Sprite entitySprite; //assuming your Entities don't need unique sprites.
public:
 void draw(const Entity& entity){
  entitySprite.setPosition(entity.getPosition());
  sf::RenderWindow::draw(entitySprite);
 }
};

หรือ

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity){
  sf::RenderWindow::draw(entity.getSprite()); //assuming Entities hold their own sprite.
 }
};

3

คุณหลีกเลี่ยงซิงเกิลตันในการพัฒนาเกมเช่นเดียวกับที่คุณหลีกเลี่ยงในการพัฒนาซอฟต์แวร์ทุกประเภท: คุณผ่านการอ้างอิง

ด้วยการออกมาจากวิธีการที่คุณสามารถเลือกที่จะผ่านการอ้างอิงโดยตรงเป็นชนิดเปลือย (เช่นint,Window*ฯลฯ ) หรือคุณสามารถเลือกที่จะผ่านพวกเขาในหนึ่งหรือที่กำหนดเองเสื้อคลุมประเภท (ชอบEntityInitializationOptions)

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


3

Singletons ไม่เลว แทนที่จะเป็นเรื่องง่ายที่จะละเมิด ในทางกลับกัน globals ง่ายยิ่งขึ้นที่จะละเมิดและมีปัญหามากขึ้น

เหตุผลที่ถูกต้องเพียงอย่างเดียวในการแทนที่ซิงเกิลตันด้วยระดับโลกคือการทำให้ผู้เกลียดชังซิงเกิลทางศาสนาสงบลง

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

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


1
ฉันเป็นคนเกลียดศาสนาเดี่ยวและฉันก็ไม่คิดว่าจะเป็นทางออกระดับโลกเช่นกัน : S
Dan Pantry

1

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

RenderSystem(IWindow* window);

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

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

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