Cách tránh các đối tượng trò chơi vô tình xóa mình trong C ++


20

Giả sử trò chơi của tôi có một con quái vật có thể phát nổ trên người chơi. Hãy chọn một tên cho quái vật này một cách ngẫu nhiên: một cây leo. Vì vậy, Creeperlớp có một phương thức trông giống như thế này:

void Creeper::kamikaze() {
    EventSystem::postEvent(ENTITY_DEATH, this);

    Explosion* e = new Explosion;
    e->setLocation(this->location());
    this->world->addEntity(e);
}

Các sự kiện không được xếp hàng, chúng được gửi đi ngay lập tức. Điều này làm cho Creeperđối tượng bị xóa ở đâu đó bên trong cuộc gọi đến postEvent. Một cái gì đó như thế này:

void World::handleEvent(int type, void* context) {
    if(type == ENTITY_DEATH){
        Entity* ent = dynamic_cast<Entity*>(context);
        removeEntity(ent);
        delete ent;
    }
}

Bởi vì Creeperđối tượng bị xóa trong khi kamikazephương thức vẫn đang chạy, nó sẽ bị sập khi nó cố truy cập this->location().

Một giải pháp là xếp hàng các sự kiện vào một bộ đệm và gửi chúng sau. Đó có phải là giải pháp phổ biến trong các trò chơi C ++? Cảm giác giống như một chút hack, nhưng điều đó có thể là do kinh nghiệm của tôi với các ngôn ngữ khác với các hoạt động quản lý bộ nhớ khác nhau.

Trong C ++, có một giải pháp chung tốt hơn cho vấn đề này khi một đối tượng vô tình xóa chính nó từ bên trong một trong các phương thức của nó?


6
uh, làm thế nào về việc bạn gọi postEvent tại END của phương thức kamikaze thay vì lúc bắt đầu?
Hackworth

@Hackworth sẽ làm việc cho ví dụ cụ thể này, nhưng tôi đang tìm kiếm một giải pháp tổng quát hơn. Tôi muốn có thể đăng các sự kiện từ bất cứ đâu và không sợ gây ra sự cố.
Tom Dalling

Bạn cũng có thể xem qua việc triển khai autoreleasetrong Objective-C, trong đó việc xóa được thực hiện cho đến khi "chỉ một chút".
Chris Burt-Brown

Câu trả lời:


40

Đừng xóa this

Thậm chí là ngầm.

- Không bao giờ -

Xóa một đối tượng trong khi một trong các chức năng thành viên của nó vẫn còn trên ngăn xếp đang cầu xin sự cố. Bất kỳ kiến ​​trúc mã nào dẫn đến điều đó xảy ra ("vô tình" hay không) đều là khách quan xấu , nguy hiểm và cần được tái cấu trúc ngay lập tức . Trong trường hợp này, nếu quái vật của bạn sẽ được phép gọi 'World :: handleEvent', thì trong mọi trường hợp, đừng xóa quái vật bên trong chức năng đó!

(Trong tình huống cụ thể này, cách tiếp cận thông thường của tôi là để con quái vật tự đặt cờ 'chết' và để đối tượng Thế giới - hoặc một cái gì đó giống như nó - kiểm tra cờ 'chết' đó một lần trên mỗi khung, loại bỏ các đối tượng đó từ danh sách các vật thể trên thế giới và xóa nó hoặc đưa nó trở lại bể quái vật hoặc bất cứ thứ gì phù hợp. Vào thời điểm này, thế giới cũng gửi thông báo về việc xóa, vì vậy các vật thể khác trên thế giới biết rằng quái vật đã đã ngừng tồn tại và có thể thả bất kỳ con trỏ nào vào đó mà chúng có thể đang giữ. Thế giới thực hiện điều này vào thời điểm an toàn, khi nó biết rằng không có đối tượng nào đang xử lý, vì vậy bạn không phải lo lắng về việc xếp chồng lên một điểm trong đó con trỏ 'this' chỉ vào bộ nhớ đã giải phóng.)


6
Nửa sau của câu trả lời của bạn trong ngoặc đơn là hữu ích. Bỏ phiếu cho một lá cờ là một giải pháp tốt. Nhưng, tôi đã hỏi làm thế nào để tránh làm điều đó, không phải là điều tốt hay xấu. Nếu một câu hỏi hỏi " Làm thế nào tôi có thể tránh làm X một cách tình cờ? " Và câu trả lời của bạn là " Không bao giờ làm X bao giờ, thậm chí vô tình " in đậm, điều đó không thực sự trả lời câu hỏi.
Tom Dalling

7
Tôi đứng đằng sau những bình luận tôi đã đưa ra trong nửa đầu câu trả lời của mình, và tôi cảm thấy rằng họ trả lời đầy đủ câu hỏi như câu gốc. Điểm quan trọng mà tôi sẽ nhắc lại ở đây là một đối tượng không tự xóa. Bao giờ . Nó không gọi người khác để xóa nó. Bao giờ . Thay vào đó, bạn cần phải có một cái gì đó khác, bên ngoài đối tượng, người sở hữu đối tượng và chịu trách nhiệm thông báo khi đối tượng cần phải tiêu diệt. Đây không phải là một điều "chỉ khi một con quái vật chết"; cái này dành cho tất cả các mã C ++ luôn luôn, ở mọi nơi, mọi lúc. Không có ngoại lệ.
Trevor Powell

3
@TrevorPowell Tôi không nói rằng bạn sai. Trong thực tế, tôi đồng ý với bạn. Tôi chỉ nói rằng nó không thực sự trả lời câu hỏi đã được hỏi. Giống như nếu bạn hỏi tôi " Làm thế nào để tôi đưa âm thanh vào trò chơi của mình? " Và câu trả lời của tôi là " Tôi không thể tin rằng bạn không có âm thanh. Đặt âm thanh vào trò chơi của bạn ngay bây giờ. " đặt " (Bạn có thể sử dụng FMOD) ", đó là một câu trả lời thực tế.
Tom Dalling

6
@TrevorPowell Đây là nơi bạn sai. Đó không phải là "chỉ kỷ luật" nếu tôi không biết bất kỳ sự thay thế nào. Mã ví dụ tôi đưa ra hoàn toàn là lý thuyết. Tôi đã biết đó là một thiết kế tồi, nhưng C ++ của tôi bị gỉ, vì vậy tôi nghĩ tôi sẽ hỏi về các thiết kế tốt hơn ở đây trước khi tôi thực sự viết mã những gì tôi muốn. Vì vậy, tôi đã đến để hỏi về thiết kế thay thế. " Thêm một cờ xóa " là một thay thế. " Không bao giờ làm điều đó " không phải là một thay thế. Nó chỉ cho tôi biết những gì tôi đã biết. Cảm giác như thể bạn đã viết câu trả lời mà không đọc đúng câu hỏi.
Tom Dalling

4
@ BOB Câu hỏi là "Làm thế nào để KHÔNG làm X". Đơn giản chỉ cần nói "KHÔNG làm X" là một câu trả lời vô giá trị. NẾU câu hỏi là "Tôi đã làm X" hoặc "Tôi đã nghĩ đến việc làm X" hoặc bất kỳ biến thể nào của câu hỏi thì nó sẽ đáp ứng các tham số của cuộc thảo luận meta, nhưng không phải ở dạng hiện tại.
Joshua Drake

21

Thay vì xếp hàng các sự kiện trong bộ đệm, hãy xếp hàng xóa trong bộ đệm. Trì hoãn-xóa có khả năng đơn giản hóa ồ ạt logic; bạn thực sự có thể giải phóng bộ nhớ ở cuối hoặc đầu khung khi bạn biết không có gì thú vị đang xảy ra với các đối tượng của mình và xóa khỏi mọi nơi.


1
Thật buồn cười khi bạn đề cập đến điều này, bởi vì tôi đã nghĩ rằng một chiếc NSAutoreleasePoolObjective-C sẽ đẹp như thế nào trong tình huống này. Có thể phải thực hiện DeletionPoolvới các mẫu C ++ hoặc một cái gì đó.
Tom Dalling

@TomDalling Một điều cần cẩn thận nếu bạn đặt bộ đệm bên ngoài vào đối tượng là một đối tượng có thể muốn bị xóa vì nhiều lý do trên một khung hình và có thể cố gắng xóa nó nhiều lần.
John Calsbeek

Rất đúng. Tôi sẽ phải giữ các con trỏ trong một std :: set.
Tom Dalling

5
Thay vì một bộ đệm của các đối tượng cần xóa, bạn cũng có thể chỉ cần đặt một cờ trong đối tượng. Khi bạn bắt đầu nhận ra mức độ bạn muốn tránh gọi mới hoặc xóa trong thời gian chạy và di chuyển đến nhóm đối tượng, điều đó sẽ đơn giản hơn và nhanh hơn.
Sean Middleditch

4

Thay vì để cả thế giới xử lý việc xóa, bạn có thể để một thể hiện của lớp khác đóng vai trò là một cái xô để lưu trữ tất cả các thực thể bị xóa. Trường hợp cụ thể này nên lắng nghe ENTITY_DEATHcác sự kiện và xử lý chúng sao cho nó xếp hàng chúng. Sau Worldđó, có thể lặp lại các trường hợp này và thực hiện các thao tác sau khi chết sau khi khung được hiển thị và 'xóa' nhóm này, do đó sẽ thực hiện xóa thực tế các thực thể.

Một ví dụ về một lớp như vậy sẽ như thế này: http://ideone.com/7Upza


+1, đó là một cách thay thế để gắn cờ trực tiếp các thực thể. Trực tiếp hơn, chỉ cần có một danh sách sống và một danh sách các thực thể trực tiếp trong Worldlớp.
Laurent Couvidou

2

Tôi sẽ đề nghị triển khai một nhà máy được sử dụng cho tất cả các phân bổ đối tượng trò chơi trong trò chơi. Vì vậy, thay vì tự gọi mới, bạn sẽ nói với nhà máy tạo ra thứ gì đó cho bạn.

Ví dụ

Object* o = gFactory->Create("Explosion");

Bất cứ khi nào bạn muốn xóa một đối tượng, nhà máy sẽ đẩy đối tượng trong bộ đệm bị xóa khung tiếp theo. Sự hủy diệt bị trì hoãn là rất quan trọng trong hầu hết các kịch bản.

Cũng xem xét gửi tất cả các tin nhắn có độ trễ của một khung. Chỉ có một vài trường hợp ngoại lệ mà bạn cần gửi ngay lập tức, đó là phần lớn các trường hợp, tuy nhiên chỉ là


2

Bạn có thể tự thực hiện bộ nhớ được quản lý trong C ++, để khi ENTITY_DEATHđược gọi, tất cả những gì xảy ra là số lượng tham chiếu của nó bị giảm đi một.

Sau đó, như @John đã đề xuất lúc cầu xin mọi khung hình, bạn có thể kiểm tra những thực thể nào là vô dụng (những thực thể không có tham chiếu) và xóa chúng. Ví dụ: bạn có thể sử dụng boost::shared_ptr<T>( tài liệu ở đây ) hoặc nếu bạn đang sử dụng C ++ 11 (VC2010)std::tr1::shared_ptr<T>


Chỉ là std::shared_ptr<T>, không phải báo cáo kỹ thuật! - Bạn sẽ phải chỉ định một deleter tùy chỉnh, nếu không nó cũng sẽ xóa đối tượng ngay lập tức khi số tham chiếu đạt đến không.
leftaroundabout

1
@leftaroundabout nó thực sự phụ thuộc, ít nhất tôi cần sử dụng tr1 trong gcc. nhưng trong VC không cần điều đó.
Ali1S232

2

Sử dụng gộp và không thực sự xóa các đối tượng. Thay vào đó, thay đổi cấu trúc dữ liệu mà họ đã đăng ký. Ví dụ để kết xuất các đối tượng có một đối tượng cảnh và tất cả các thực thể bằng cách nào đó đã đăng ký với nó để kết xuất, phát hiện va chạm, v.v. Thay vì xóa đối tượng, hãy tách nó ra khỏi cảnh và chèn vào nhóm đối tượng chết. Phương pháp này sẽ không chỉ ngăn ngừa các vấn đề về bộ nhớ (chẳng hạn như một đối tượng tự xóa) mà còn có thể tăng tốc trò chơi của bạn nếu bạn sử dụng nhóm chính xác.


1

Những gì chúng tôi đã làm trong một trò chơi là sử dụng vị trí mới

SomeEvent* obj = new(eventPool.alloc()) new SomeEvent();

eventPool chỉ là một mảng lớn bộ nhớ được khắc lên và con trỏ tới từng phân đoạn được lưu trữ. Vì vậy, alloc () sẽ trả về địa chỉ của một khối bộ nhớ miễn phí. Trong sự kiện của chúng tôi, bộ nhớ được coi là một ngăn xếp, vì vậy sau khi tất cả các sự kiện đã được gửi, chúng tôi chỉ cần đặt lại con trỏ ngăn xếp trở lại bắt đầu của mảng.

Do cách hệ thống sự kiện của chúng tôi hoạt động, chúng tôi không cần phải gọi những kẻ hủy diệt trên các evetns. Vì vậy, pool chỉ đơn giản là đăng ký khối bộ nhớ là miễn phí và sẽ phân bổ nó.

Điều này đã cho chúng tôi một tốc độ rất lớn lên.

Ngoài ra ... Chúng tôi thực sự đã sử dụng các nhóm bộ nhớ cho tất cả các đối tượng được phân bổ động trong quá trình phát triển, vì đó là một cách tuyệt vời để tìm rò rỉ bộ nhớ, nếu có bất kỳ đối tượng nào còn lại trong các nhóm khi trò chơi thoát (bình thường) thì có khả năng là có một rò rỉ mem.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.