2010-11-06 37 views
11

Là một bài tập nhỏ, tôi đang cố gắng viết một công cụ trò chơi rất đơn giản, chỉ xử lý các thực thể (di chuyển, AI cơ bản, v.v.)Xử lý các thực thể trong một trò chơi

Như vậy, tôi đang cố gắng suy nghĩ về cách thức một trò chơi xử lý các bản cập nhật cho tất cả các thực thể, và tôi nhận được một chút bối rối (Có lẽ vì tôi đang đi về nó một cách sai)

Vì vậy, tôi quyết định đăng câu hỏi này ở đây để cho bạn thấy cách hiện tại của tôi suy nghĩ về nó, và để xem liệu có ai có thể gợi ý cho tôi cách làm tốt hơn không.

Hiện nay, tôi có một lớp CEngine mà mất con trỏ đến các lớp học khác mà nó cần (Ví dụ một lớp CWindow, CEntityManager lớp vv)

Tôi có một vòng lặp trò chơi mà trong mã giả sẽ đi như thế này (trong lớp CEngine)

while(isRunning) { 
    Window->clear_screen(); 

    EntityManager->draw(); 

    Window->flip_screen(); 

    // Cap FPS 
} 

lớp CEntityManager tôi trông như thế này:

enum { 
    PLAYER, 
    ENEMY, 
    ALLY 
}; 

class CEntityManager { 
    public: 
     void create_entity(int entityType); // PLAYER, ENEMY, ALLY etc. 
     void delete_entity(int entityID); 

    private: 
     std::vector<CEntity*> entityVector; 
     std::vector<CEntity*> entityVectorIter; 
}; 

Và lớp CEntity của tôi trông như thế này:

class CEntity() { 
    public: 
     virtual void draw() = 0; 
     void set_id(int nextEntityID); 
     int get_id(); 
     int get_type(); 

    private: 
     static nextEntityID; 
     int entityID; 
     int entityType; 
}; 

Sau đó, tôi sẽ tạo ra lớp ví dụ, đối với một kẻ thù, và cung cấp cho nó một tờ ma, chức năng riêng của mình, vv

Ví dụ:

class CEnemy : public CEntity { 
    public: 
     void draw(); // Implement draw(); 
     void do_ai_stuff(); 

}; 

class CPlayer : public CEntity { 
    public: 
     void draw(); // Implement draw(); 
     void handle_input(); 
}; 

Tất cả điều này làm việc tốt cho chỉ vẽ sprites vào màn hình.

Nhưng sau đó tôi đã đến vấn đề sử dụng các hàm tồn tại trong một thực thể, nhưng không phải trong một thực thể khác.

Trong ví dụ mã giả ở trên, do_ai_stuff(); và handle_input();

Như bạn thấy từ vòng lặp trò chơi của tôi, có một cuộc gọi đến EntityManager-> draw(); Điều này chỉ lặp lại thông qua entityVector và gọi là draw(); chức năng cho mỗi thực thể - Mà làm việc tốt nhìn thấy như tất cả các thực thể có một draw(); chức năng.

Nhưng sau đó tôi nghĩ, nếu đó là một thực thể người chơi cần xử lý đầu vào thì sao? Cách thức hoạt động?

Tôi chưa thử nhưng tôi giả sử rằng tôi không thể lặp lại như đã làm với hàm draw(), vì các thực thể như kẻ thù sẽ không có hàm handle_input().

tôi có thể sử dụng một câu lệnh if để kiểm tra entityType, như vậy:

for(entityVectorIter = entityVector.begin(); entityVectorIter != entityVector.end(); entityVectorIter++) { 
    if((*entityVectorIter)->get_type() == PLAYER) { 
     (*entityVectorIter)->handle_input(); 
    } 
} 

Nhưng tôi không biết làm thế nào mọi người thường đi về cách viết công cụ này vì vậy tôi không chắc chắn về cách tốt nhất để làm đi.

tôi đã viết rất nhiều ở đây và tôi không yêu cầu bất kỳ câu hỏi cụ thể, vì vậy tôi sẽ làm rõ những gì tôi đang tìm kiếm ở đây:

  • có phải là cách tôi đã đặt ra/thiết kế mã của tôi ok, và nó có thực tế không?
  • Có cách nào hiệu quả hơn để tôi cập nhật các thực thể và chức năng gọi của mình mà các thực thể khác có thể không có?
  • Đang sử dụng enum để theo dõi một thực thể có phải là cách hay để xác định các thực thể không?

Trả lời

10

Bạn đang nhận được khá gần với cách mà hầu hết trò chơi thực sự làm điều đó (mặc dù các chuyên gia hiệu suất keo cú Mike Acton often gripes about that).

Thông thường bạn sẽ thấy một cái gì đó như thế này

class CEntity { 
    public: 
    virtual void draw() {}; // default implementations do nothing 
    virtual void update() {} ; 
    virtual void handleinput(const inputdata &input) {}; 
} 

class CEnemy : public CEntity { 
    public: 
    virtual void draw(); // implemented... 
    virtual void update() { do_ai_stuff(); } 
     // use the default null impl of handleinput because enemies don't care... 
} 

class CPlayer : public CEntity { 
    public: 
    virtual void draw(); 
    virtual void update(); 
    virtual void handleinput(const inputdata &input) {}; // handle input here 
} 

và sau đó người quản lý thực thể đi qua và kêu gọi cập nhật(), handleinput(), và vẽ() trên từng đối tượng trên thế giới.

Tất nhiên, có rất nhiều chức năng này, hầu hết trong số đó không làm gì khi bạn gọi chúng, có thể khá lãng phí, đặc biệt đối với các chức năng ảo. Vì vậy, tôi đã nhìn thấy một số cách tiếp cận khác quá.

Một là lưu trữ ví dụ: dữ liệu đầu vào trong toàn cầu (hoặc là thành viên của giao diện chung hoặc đơn lẻ, v.v.). Sau đó ghi đè lên hàm update() của kẻ thù để chúng do_ai_stuff(). và bản cập nhật() của người chơi để xử lý đầu vào bằng cách bỏ phiếu toàn cầu.

Cách khác là sử dụng một số biến thể trên Listener pattern, để mọi thứ quan tâm đến đầu vào kế thừa từ một lớp người nghe thông thường và bạn đăng ký tất cả những người nghe đó bằng Trình quản lý nhập liệu. Sau đó, người quản lý dữ liệu gọi mỗi người nghe theo từng khung hình:

class CInputManager 
{ 
    AddListener(IInputListener *pListener); 
    RemoveListener(IInputListener *pListener); 

    vector<IInputListener *>m_listeners; 
    void PerFrame(inputdata *input) 
    { 
    for (i = 0 ; i < m_listeners.count() ; ++i) 
    { 
     m_listeners[i]->handleinput(input); 
    } 
    } 
}; 
CInputManager g_InputManager; // or a singleton, etc 

class IInputListener 
{ 
    virtual void handleinput(inputdata *input) = 0; 
    IInputListener() { g_InputManager.AddListener(this); } 
    ~IInputListener() { g_InputManager.RemoveListener(this); } 
} 

class CPlayer : public IInputListener 
{ 
    virtual void handleinput(inputdata *input); // implement this.. 
} 

Và còn có những cách khác, phức tạp hơn. Nhưng tất cả những công việc và tôi đã nhìn thấy mỗi người trong số họ trong một cái gì đó thực sự vận chuyển và bán.

+4

Người đàn ông, anh chàng trong liên kết đó thực sự khủng khiếp. – Puppy

+4

Có lẽ, nhưng anh ấy cũng lý do tại sao Ratchet & Clank không bao giờ thấp hơn 60fps. – Crashworks

+1

@DeadMG: tốt, ông chỉ ra một số vấn đề thực tế, về hiệu suất và thiết kế. Anh ta cũng có vẻ không thích hợp với C++, và vắt vào một số bản sao hoàn toàn vô nghĩa (giống như cái chống lại các tham chiếu). Tuy nhiên, lọc ra sự nhảm nhí, và lời khuyên hiệu suất là khá vững chắc. – jalf

6

Bạn có thể nhận ra chức năng này bằng cách sử dụng chức năng ảo cũng như:

class CEntity() { 
    public: 
     virtual void do_stuff() = 0; 
     virtual void draw() = 0; 
     // ... 
}; 

class CEnemy : public CEntity { 
    public: 
     void do_stuff() { do_ai_stuff(); } 
     void draw(); // Implement draw(); 
     void do_ai_stuff(); 

}; 

class CPlayer : public CEntity { 
    public: 
     void do_stuff() { handle_input(); } 
     void draw(); // Implement draw(); 
     void handle_input(); 
}; 
+1

Tôi muốn "cập nhật()" làm tên so với "do_stuff()", nhưng tôi đồng ý với quan điểm của bạn! – Philipp

+0

Tôi sẽ không bao giờ đặt tên cho hàm/phương thức "do_stuff" hoặc tương tự. Tôi chỉ cần thông qua việc đặt tên "do_ai_stuff" vào một tên chung chung hơn về nhanh. Vì vậy, tôi đồng ý với bạn, quá! Có nhiều tiềm năng hơn nữa để cải tiến hơn nữa thiết kế. ;) – Flinsch

8

Bạn nên tìm trong các thành phần, thay vì thừa kế cho điều này. Ví dụ, trong động cơ của tôi, tôi đã (giản thể):

class GameObject 
{ 
private: 
    std::map<int, GameComponent*> m_Components; 
}; // eo class GameObject 

tôi có các thành phần khác nhau mà làm những việc khác nhau:

class GameComponent 
{ 
}; // eo class GameComponent 

class LightComponent : public GameComponent // represents a light 
class CameraComponent : public GameComponent // represents a camera 
class SceneNodeComponent : public GameComponent // represents a scene node 
class MeshComponent : public GameComponent // represents a mesh and material 
class SoundComponent : public GameComponent // can emit sound 
class PhysicsComponent : public GameComponent // applies physics 
class ScriptComponent : public GameComponent // allows scripting 

Các thành phần này có thể được thêm vào một đối tượng trò chơi để tạo ra hành vi. Họ có thể giao tiếp thông qua một hệ thống nhắn tin, và những thứ yêu cầu cập nhật trong vòng lặp chính đăng ký một người nghe khung. Chúng có thể hoạt động độc lập và được thêm/xóa một cách an toàn khi chạy. Tôi thấy đây là một hệ thống rất mở rộng.

EDIT: Xin lỗi, tôi sẽ xác thịt này ra một chút, nhưng tôi ở giữa một cái gì đó ngay bây giờ :)

1

Một điều nhỏ - tại sao bạn sẽ thay đổi ID của một thực thể?Thông thường, điều này là không đổi và khởi tạo trong quá trình thi, và đó là nó:

class CEntity 
{ 
    const int m_id; 
    public: 
    CEntity(int id) : m_id(id) {} 
} 

Đối với những điều khác, có những cách tiếp cận khác nhau, việc lựa chọn phụ thuộc vào có bao nhiêu loại cụ thể chức năng đang có (và như thế nào bạn có thể hủy bỏ chúng).


Thêm vào tất cả

Phương pháp đơn giản nhất là chỉ cần thêm tất cả các phương pháp để giao diện cơ sở và thực hiện chúng như không-op trong lớp học mà không hỗ trợ nó. Điều đó có vẻ giống như lời khuyên xấu, nhưng là một sự không chuẩn hóa acceptabel, nếu có rất ít phương pháp không áp dụng, và bạn có thể giả định tập hợp các phương thức sẽ không phát triển đáng kể với các yêu cầu trong tương lai.

Thậm chí, bạn thậm chí không thể triển khai loại cơ chế "khám phá" cơ bản, ví dụ:

class CEntity 
{ 
    public: 
    ... 
    virtual bool CanMove() = 0; 
    virtual void Move(CPoint target) = 0; 
} 

Đừng lạm dụng! Thật dễ dàng để bắt đầu theo cách này, và sau đó dính vào nó ngay cả khi nó tạo ra một mớ hỗn độn lớn của mã của bạn. Nó có thể được phủ như "cố ý không chuẩn hóa hệ thống phân cấp loại" - nhưng cuối cùng nó chỉ là một hack cho phép bạn giải quyết một vài vấn đề một cách nhanh chóng, nhưng nhanh chóng gây tổn hại khi ứng dụng phát triển.


True Type khám phá

sử dụng và dynamic_cast, bạn có thể đúc một cách an toàn đối tượng của bạn CEntity-CFastCat. Nếu thực thể thực sự là một CReallyUnmovableBoulder, kết quả sẽ là một con trỏ rỗng. Bằng cách đó bạn có thể thăm dò một đối tượng cho loại thực tế của nó, và phản ứng với nó cho phù hợp.

CFastCat * fastCat = dynamic_cast<CFastCat *>(entity) ; 
if (fastCat != 0) 
    fastCat->Meow(); 

Cơ chế đó hoạt động tốt nếu chỉ có ít logic gắn với các phương pháp loại cụ thể. Đó là không một giải pháp tốt nếu bạn kết thúc với chuỗi nơi bạn thăm dò đối với nhiều chủng loại, và hành động phù hợp:

// -----BAD BAD BAD BAD Code ----- 
CFastCat * fastCat = dynamic_cast<CFastCat *>(entity) ; 
if (fastCat != 0) 
    fastCat->Meow(); 

CBigDog * bigDog = dynamic_cast<CBigDog *>(entity) ; 
if (bigDog != 0) 
    bigDog->Bark(); 

CPebble * pebble = dynamic_cast<CPebble *>(entity) ; 
if (pebble != 0) 
    pebble->UhmWhatNoiseDoesAPebbleMake(); 

Đó thường là các phương thức ảo của bạn không được chọn lựa cẩn thận.


Giao diện

Trên có thể được mở rộng đến các giao diện, khi các chức năng loại cụ thể không phải là phương pháp duy nhất, nhưng nhóm của phương pháp. Họ không hỗ trợ rất tốt trong C++, nhưng nó có thể chịu đựng được. Ví dụ. đối tượng của bạn có tính năng khác nhau:

class IMovable 
{ 
    virtual void SetSpeed() = 0; 
    virtual void SetTarget(CPoint target) = 0; 
    virtual CPoint GetPosition() = 0; 
    virtual ~IMovable() {} 
} 

class IAttacker 
{ 
    virtual int GetStrength() = 0; 
    virtual void Attack(IAttackable * target) = 0; 
    virtual void SetAnger(int anger) = 0; 
    virtual ~IAttacker() {} 
} 

đối tượng khác nhau của bạn kế thừa từ lớp cơ sở và một hoặc nhiều giao diện:

class CHero : public CEntity, public IMovable, public IAttacker 

Và một lần nữa, bạn có thể sử dụng dynamic_cast để thăm dò cho các giao diện trên bất kỳ thực thể.

Điều đó khá mở rộng và thường là cách an toàn nhất để đi khi bạn không chắc chắn.Đó là một chút mroe tiết hơn so với các giải pháp trên, nhưng có thể đối phó khá tốt với những thay đổi bất ngờ trong tương lai. Tính năng bao thanh toán vào giao diện là không phải là dễ dàng, phải mất một số kinh nghiệm để có được một cảm giác cho nó.


Visitor pattern

Các visitor pattern đòi hỏi rất nhiều đánh máy, nhưng nó cho phép bạn thêm chức năng cho các lớp học mà không sửa đổi những lớp học.

Trong ngữ cảnh của bạn, điều đó có nghĩa là bạn có thể xây dựng cấu trúc tổ chức của mình nhưng thực hiện riêng các hoạt động của họ. Điều này thường được sử dụng khi bạn có các hoạt động rất riêng biệt trên các thực thể của bạn, bạn không thể tự do sửa đổi các lớp, hoặc thêm chức năng vào các lớp sẽ vi phạm mạnh mẽ nguyên tắc duy nhất-trách nhiệm.

Điều này có thể đáp ứng hầu như mọi yêu cầu thay đổi (miễn là các thực thể của bạn cũng được tính toán tốt).

(tôi chỉ liên kết đến nó, bởi vì nó có hầu hết mọi người một thời gian để quấn quanh đầu của họ xung quanh nó, và tôi sẽ không khuyên bạn nên sử dụng nó, trừ khi bạn đã có kinh nghiệm những hạn chế của các phương pháp khác)

+0

dynamic_cast <> s có thể có vấn đề vì chúng có xu hướng rất, rất chậm (một micro giây trở lên). Trong trò chơi, nơi bạn có hàng nghìn thực thể và 16,6 mili giây để chạy một khung hình, nó sẽ tăng lên. – Crashworks

+0

@Crashworks: Cảm ơn bạn đã chỉ ra - nhưng tôi sẽ kiểm tra trình biên dịch của mình trước khi đưa chúng ra một cách nhanh chóng. Nó không phải là một vấn đề cho "khám phá kiểu đúng" ở trên (trừ khi trình biên dịch của bạn bi quan về trường hợp đó - trong trường hợp đó cơ chế dễ thực hiện). Đối với một hệ thống phân cấp rất phức tạp, một vài nghìn chu kỳ có thể là có thể, nhưng với một cơ cấu giới hạn các thực thể hạn chế, việc triển khai tùy chỉnh nhanh hơn nhiều là có thể. (Yeah, nó sucks ....) Dù sao, có một lý do có những phương pháp khác nhau. – peterchen

+0

Tôi đang sử dụng Crashworks. Tôi đã không bao giờ đi qua dynamic_cast được sử dụng trong giao diện điều khiển runtimes vì ​​nó đòi hỏi RTTI phải được kích hoạt và thường có chi phí hiệu suất quá cao. Hầu hết các nhà phát triển bảng điều khiển cuộn hệ thống phản ánh/phản ánh tùy chỉnh C++ của riêng mình – zebrabox

1

Trong nói chung, mã của bạn là khá ok, như những người khác đã chỉ ra.

Để trả lời câu hỏi thứ ba của bạn: Trong mã bạn đã cho chúng tôi thấy, bạn không sử dụng loại enum ngoại trừ việc tạo. Có vẻ như ok (mặc dù tôi tự hỏi nếu một "createPlayer()", "createEnemy()" phương pháp và như vậy woudn't được dễ dàng hơn để đọc). Nhưng ngay sau khi bạn có mã sử dụng nếu hoặc thậm chí chuyển sang làm những việc khác nhau dựa trên loại, thì bạn đang vi phạm một số nguyên tắc OO. Sau đó, bạn nên sử dụng sức mạnh của các phương pháp ảo để đảm bảo họ làm những gì họ có. Nếu bạn phải "tìm" đối tượng của một loại nhất định, bạn cũng có thể lưu trữ một con trỏ tới đối tượng trình phát đặc biệt của mình ngay khi bạn tạo nó.

Bạn cũng có thể xem xét thay thế ID bằng con trỏ thô nếu bạn chỉ cần một ID duy nhất.

Vui lòng xem đây là gợi ý rằng MIGHT phù hợp tùy thuộc vào những gì bạn thực sự cần.

+0

Ngoài sự quan tâm, tại sao sự khác biệt dựa trên loại vi phạm nguyên tắc OO? – Ell

+0

trong OO, bạn cố gắng đóng gói hành vi trong các đối tượng. Vì vậy, thay vì có if's và switch's, bạn thay vì gọi một phương thức ảo và vì nó nằm trong một đối tượng của kiểu hiện đang hoạt động, phương thức này có thể làm những gì nên được thực hiện. Điều này linh hoạt hơn và an toàn hơn (xem xét thêm kiểu mới trong cấu trúc phân cấp, bạn nhận được sự an toàn biên dịch theo thời gian) – Philipp

+0

Tôi nghĩ việc sử dụng các phương thức ảo, nhưng tại sao cho phép các đối tượng khác biết loại vi phạm đóng gói? Có một đối tượng lưu trữ loại của nó (hoặc sử dụng dynamic_cast) chỉ làm cho nó dễ dàng hơn để lưu trữ một mảng các đối tượng với một tổ tiên chung, hoặc là thiết kế chỉ là xấu? – Ell

Các vấn đề liên quan