Trạng thái trò chơi 'Chồng'?


52

Tôi đã suy nghĩ về cách thực hiện các trạng thái trò chơi vào trò chơi của mình. Những điều chính tôi muốn cho nó là:

  • Các trạng thái hàng đầu bán trong suốt - có thể xem qua menu tạm dừng cho trò chơi phía sau

  • Một cái gì đó OO-Tôi thấy điều này dễ sử dụng hơn và hiểu lý thuyết đằng sau, cũng như giữ tối ưu hóa và bổ sung thêm.



Tôi đã lên kế hoạch sử dụng một danh sách được liên kết và coi nó như một chồng. Điều này có nghĩa là tôi có thể truy cập vào trạng thái bên dưới để bán trong suốt.
Gói: Có ngăn xếp trạng thái là một danh sách liên kết các con trỏ tới IGameStates. Trạng thái trên cùng xử lý các lệnh cập nhật và đầu vào của chính nó, sau đó có một thành viên isTransparent để quyết định xem có nên rút trạng thái bên dưới không.
Sau đó tôi có thể làm:

states.push_back(new MainMenuState());
states.push_back(new OptionsMenuState());
states.pop_front();

Để đại diện cho trình phát đang tải, sau đó chuyển đến tùy chọn và sau đó là menu chính.
Đây là một ý tưởng tốt, hay ...? Tôi có nên nhìn vào cái gì khác?

Cảm ơn.


Bạn có muốn xem MainMothyState đằng sau OptionsMothyState không? Hay chỉ là màn hình trò chơi đằng sau OptionsMothyState?
Skizz

Kế hoạch là các tiểu bang sẽ có giá trị / cờ opacity / isTransparent. Tôi sẽ kiểm tra và xem liệu trạng thái hàng đầu có đúng như vậy không, và nếu có thì nó có giá trị gì. Sau đó kết xuất nó với độ mờ nhiều so với trạng thái khác. Trong trường hợp này, không tôi sẽ không.
Vịt Cộng sản

Tôi biết rằng vào cuối ngày, nhưng với những độc giả tương lai: không sử dụng newtheo cách hiển thị trong mã mẫu, nó chỉ yêu cầu rò rỉ bộ nhớ hoặc các lỗi nghiêm trọng khác.
Pharap

Câu trả lời:


44

Tôi đã làm việc trên cùng một động cơ như coderanger. Tôi có một quan điểm khác nhau. :)

Đầu tiên, chúng tôi không có một đống FSM - chúng tôi có một chồng trạng thái. Một chồng các trạng thái tạo ra một FSM duy nhất. Tôi không biết một đống FSM sẽ trông như thế nào. Có lẽ quá phức tạp để làm bất cứ điều gì thực tế với.

Vấn đề lớn nhất của tôi với Bộ máy Nhà nước Toàn cầu của chúng tôi là đó là một tập hợp các trạng thái chứ không phải là một tập hợp các trạng thái. Điều này có nghĩa là, ví dụ, ... / MainMothy / Đang tải khác với ... / Đang tải / MainMothy, tùy thuộc vào việc bạn có bật menu chính trước hay sau màn hình tải không (trò chơi không đồng bộ và việc tải chủ yếu do máy chủ điều khiển ).

Như hai ví dụ về những điều này làm cho xấu xí:

  • Nó dẫn đến trạng thái LoadingGameplay, do đó bạn có Base / Loading và Base / Gameplay / LoadingGameplay để tải trong trạng thái Gameplay, phải lặp lại nhiều mã ở trạng thái tải bình thường (nhưng không phải tất cả, và thêm một số nữa ).
  • Chúng tôi đã có một số chức năng như "nếu trong trình tạo nhân vật đi đến chơi trò chơi nút vẫn hoạt động.

Mặc dù tên, nó không phải là "toàn cầu". Hầu hết các hệ thống trò chơi nội bộ đã không sử dụng nó để theo dõi trạng thái nội bộ của họ, vì họ không muốn trạng thái của họ bị chế nhạo với các hệ thống khác. Những người khác, ví dụ hệ thống UI, có thể sử dụng nó nhưng chỉ để sao chép trạng thái vào hệ thống trạng thái cục bộ của chính họ. (Tôi đặc biệt thận trọng đối với hệ thống đối với các trạng thái UI. Trạng thái UI không phải là một ngăn xếp, nó thực sự là một DAG và cố gắng ép buộc bất kỳ cấu trúc nào khác trên đó sẽ chỉ tạo ra các UI gây khó chịu khi sử dụng.)

Điều tốt cho việc cô lập các tác vụ để tích hợp mã từ các lập trình viên cơ sở hạ tầng là những người không biết cách thức dòng chảy thực sự được cấu trúc, vì vậy bạn có thể nói với anh chàng viết bản vá "đặt mã của bạn vào Client_Patch_Update" và anh chàng viết đồ họa đang tải "đặt mã của bạn vào Client_MapTransfer_On Entry" và chúng tôi có thể trao đổi một số luồng logic nhất định xung quanh mà không gặp nhiều rắc rối.

Trong một dự án phụ, tôi đã may mắn hơn với một bộ trạng thái chứ không phải là một ngăn xếp , không ngại tạo ra nhiều máy cho các hệ thống không liên quan và từ chối để mình rơi vào cái bẫy có "trạng thái toàn cầu", đó thực sự là chỉ là một cách phức tạp để đồng bộ hóa mọi thứ thông qua các biến toàn cục - Chắc chắn, bạn sẽ kết thúc việc đó gần thời hạn, nhưng đừng thiết kế với mục tiêu đó . Về cơ bản, trạng thái trong trò chơi không phải là một chồng và các trạng thái trong trò chơi không liên quan đến nhau.

GSM cũng vậy, vì các con trỏ chức năng và hành vi phi cục bộ có xu hướng làm, khiến việc gỡ lỗi trở nên khó khăn hơn, mặc dù việc gỡ lỗi các loại chuyển đổi trạng thái lớn đó cũng không thú vị trước khi chúng ta có nó. Các bộ trạng thái thay vì ngăn xếp trạng thái không thực sự giúp ích cho việc này, nhưng bạn nên biết về nó. Các hàm ảo thay vì các con trỏ hàm có thể làm giảm bớt phần nào điều đó.


Câu trả lời tuyệt vời, cảm ơn! Tôi nghĩ rằng tôi có thể lấy rất nhiều từ bài viết của bạn và kinh nghiệm trong quá khứ của bạn. : D + 1 / Đánh dấu.
Vịt Cộng sản

Điều hay ho về hệ thống phân cấp là bạn có thể xây dựng các trạng thái tiện ích vừa được đẩy lên hàng đầu và không phải lo lắng về những gì khác đang chạy.
coderanger

Tôi không thấy đó là một đối số cho một hệ thống phân cấp chứ không phải là tập hợp. Thay vào đó, một hệ thống phân cấp làm cho tất cả các giao tiếp giữa các tiểu bang trở nên phức tạp hơn, bởi vì bạn không biết chúng bị đẩy vào đâu.

Điểm mà các UI thực sự là DAG được thực hiện tốt, nhưng tôi không đồng ý ở chỗ nó chắc chắn có thể được biểu diễn trong một ngăn xếp. Bất kỳ biểu đồ chu kỳ có hướng nào được kết nối (và tôi không thể nghĩ về trường hợp nó không phải là DAG được kết nối) có thể được hiển thị dưới dạng cây và ngăn xếp về cơ bản là một cây.
Ed Ropple

2
Ngăn xếp là một tập hợp con của cây, là tập con của DAG, là tập con của tất cả các biểu đồ. Tất cả các ngăn xếp đều là cây, tất cả các cây đều là DAG, nhưng hầu hết các DAG không phải là cây và hầu hết các cây không phải là ngăn xếp. Các DAG có một trật tự tôpô sẽ cho phép bạn lưu trữ chúng trong một ngăn xếp (đối với độ phân giải phụ thuộc), nhưng một khi bạn nhồi nhét chúng vào ngăn xếp, bạn đã mất thông tin có giá trị. Trong trường hợp này, khả năng điều hướng giữa một màn hình và cha mẹ của nó nếu nó có anh chị em trước.

11

Dưới đây là một ví dụ về triển khai ngăn xếp trò chơi mà tôi thấy rất hữu ích: http://creators.xna.com/en-US/samples/gamestateman Quản lý

Nó được viết bằng C # và để biên dịch nó, bạn cần khung XNA, tuy nhiên bạn chỉ cần kiểm tra mã, tài liệu và video để có ý tưởng.

Nó có thể hỗ trợ chuyển trạng thái, trạng thái trong suốt (như hộp thông báo phương thức) và trạng thái tải (quản lý việc dỡ tải trạng thái hiện có và tải trạng thái tiếp theo).

Bây giờ tôi sử dụng các khái niệm tương tự trong các dự án sở thích (không phải C #) của tôi (được cấp, nó có thể không phù hợp với các dự án lớn hơn) và cho các dự án nhỏ / sở thích, tôi chắc chắn có thể đề xuất phương pháp này.


5

Điều này tương tự với những gì chúng ta sử dụng, một chồng các FSM. Về cơ bản chỉ cần cung cấp cho mỗi trạng thái một chức năng nhập, thoát và đánh dấu và gọi chúng theo thứ tự. Hoạt động rất độc đáo để xử lý những thứ như tải quá.


3

Một trong những tập "Đá quý lập trình trò chơi" có triển khai máy trạng thái dành cho các trạng thái trò chơi; http://emipes.net/Global/Document/textbook/Ch CHƯƠNG1_GameAppFramework.pdf có một ví dụ về cách sử dụng nó cho một trò chơi nhỏ và không nên quá đặc trưng cho Gamebryo.


Phần đầu tiên của "Lập trình trò chơi nhập vai với DirectX" cũng thực hiện một hệ thống trạng thái (và một hệ thống quy trình - sự khác biệt rất thú vị).
Ricket

Đó là một tài liệu tuyệt vời và giải thích nó gần như chính xác cách tôi đã triển khai nó trong quá khứ, thiếu hệ thống phân cấp đối tượng không cần thiết mà họ sử dụng trong các ví dụ.
dash-tom-bang

3

Chỉ cần thêm một chút tiêu chuẩn hóa vào cuộc thảo luận, thuật ngữ CS cổ điển cho loại cấu trúc dữ liệu này là một máy tự động đẩy xuống .


Tôi không chắc rằng bất kỳ triển khai thực tế nào trong các ngăn xếp trạng thái gần như tương đương với máy tự động đẩy xuống. Như đã đề cập trong các câu trả lời khác, việc triển khai thực tế luôn kết thúc bằng các lệnh như "pop hai trạng thái", "hoán đổi các trạng thái này" hoặc "chuyển dữ liệu này sang trạng thái tiếp theo bên ngoài ngăn xếp". Và máy tự động là máy tự động - máy tính - không phải cấu trúc dữ liệu. Cả ngăn xếp trạng thái và automata đẩy xuống đều sử dụng ngăn xếp làm cấu trúc dữ liệu.

1
"Tôi không chắc chắn bất kỳ triển khai thực tế nào trong các ngăn xếp nhà nước gần như tương đương với một máy tự động đẩy xuống." Có gì khác biệt? Cả hai đều có một tập hợp các trạng thái hữu hạn, một lịch sử của các trạng thái và các hoạt động nguyên thủy để đẩy và các trạng thái pop. Không có hoạt động nào khác mà bạn đề cập là khác biệt về mặt tài chính. "Pop hai bang" chỉ xuất hiện hai lần. "hoán đổi" là một pop và đẩy. Truyền dữ liệu nằm ngoài ý tưởng cốt lõi, nhưng mọi trò chơi sử dụng "FSM" cũng xử lý dữ liệu bổ sung mà không cảm thấy tên đó không còn được áp dụng.
khoan hồng

Trong một máy tự động đẩy xuống, trạng thái duy nhất có thể ảnh hưởng đến quá trình chuyển đổi của bạn là trạng thái trên cùng. Trao đổi hai trạng thái ở giữa không được phép; thậm chí nhìn vào các tiểu bang ở giữa không được phép. Tôi cảm thấy việc mở rộng ngữ nghĩa của thuật ngữ "FSM" là hợp lý và có lợi ích (và chúng tôi vẫn có thuật ngữ "DFA" và "NFA" theo nghĩa hạn chế nhất), nhưng "đẩy xuống tự động" hoàn toàn là một thuật ngữ khoa học máy tính và chỉ có sự nhầm lẫn chờ đợi nếu chúng ta áp dụng nó cho mọi hệ thống dựa trên ngăn xếp đơn lẻ ngoài kia.

Tôi thích những triển khai đó trong đó trạng thái duy nhất có thể ảnh hưởng đến bất cứ điều gì là trạng thái trên cùng, mặc dù trong một số trường hợp, có thể lọc đầu vào trạng thái và chuyển xử lý sang trạng thái "thấp hơn". (Ví dụ: xử lý đầu vào của bộ điều khiển ánh xạ tới phương thức này, trạng thái trên cùng sẽ lấy các bit mà nó quan tâm và có thể xóa chúng sau đó chuyển điều khiển sang trạng thái tiếp theo trên ngăn xếp.)
dash-tom-bang

1
Điểm tốt, cố định!
khoan hồng

1

Tôi không chắc chắn một ngăn xếp là hoàn toàn cần thiết cũng như giới hạn chức năng của hệ thống nhà nước. Sử dụng ngăn xếp, bạn không thể 'thoát' trạng thái sang một trong nhiều khả năng. Giả sử bạn bắt đầu trong "Menu chính", sau đó chuyển đến "Tải trò chơi", bạn có thể muốn chuyển sang trạng thái "Tạm dừng" sau khi tải thành công trò chơi đã lưu và quay lại "Menu chính" nếu người dùng hủy tải.

Tôi sẽ chỉ có nhà nước chỉ định trạng thái để theo dõi khi nó thoát.

Đối với những trường hợp bạn muốn quay lại trạng thái trước trạng thái hiện tại, ví dụ "Menu chính-> Tùy chọn-> Menu chính" và "Tạm dừng-> Tùy chọn-> Tạm dừng", chỉ cần chuyển dưới dạng tham số khởi động sang trạng thái nhà nước để trở lại.


Có lẽ tôi đã hiểu nhầm câu hỏi?
Skizz

Không, bạn không làm. Tôi nghĩ rằng các cử tri đã làm.
Vịt Cộng sản

Sử dụng một ngăn xếp không loại trừ việc sử dụng các chuyển đổi trạng thái rõ ràng.
dash-tom-bang

1

Một giải pháp khác cho quá trình chuyển đổi và những thứ khác là cung cấp trạng thái nguồn và trạng thái nguồn, cùng với máy trạng thái, có thể được liên kết với "động cơ", bất cứ điều gì có thể. Sự thật là hầu hết các máy trạng thái có lẽ sẽ cần phải được điều chỉnh cho dự án trong tầm tay. Một giải pháp có thể có lợi cho trò chơi này hoặc trò chơi đó, các giải pháp khác có thể cản trở nó.

class StateMachine
{
public:
    StateMachine(Engine *);
    void Push(State *);
    State *Pop();
    void Update();
    Engine *GetEngine();

private:
    std::stack<State *> _states;
    Engine *_engine;
};

Các trạng thái được đẩy với trạng thái hiện tại và máy làm tham số.

void StateMachine::Push(State *state)
{
    State *from = 0;
    if (!_states.empty()) from = _states.top();
    _states.push(state);
    state->Enter(this, from);
}

Hoa được bật theo cùng một kiểu. Cho dù bạn gọi Enter()ở phía dướiState là một câu hỏi thực hiện.

State *StateMachine::Pop()
{
    _ASSERT(!_states.empty());
    State *state = _states.top();
    State *to = 0;
    _states.pop();
    if (!_states.empty()) to = _states.top();
    state->Exit(this, to);
    return state;
}

Khi nhập, cập nhật hoặc thoát, Statenhận được tất cả thông tin cần thiết.

void SomeGameState::Enter(StateMachine *sm, State *from)
{
    Engine *eng = sm->GetEngine();
    eng->GetKeyboard()->KeyDown.Bind(this, &SomeGameState::KeyDown);
    LoadLevelState *state = new LoadLevelState();
    state->SetLevel(eng->GetSaveGame()->GetLevelName());
    state->Load.Bind(this, &SomeGameState::OnLevelLoaded);
    sm->Push(state);
}

void SomeGameState::Update(StateMachine *sm)
{
    Engine *eng = sm->GetEngine();
    float time = eng->GetFrameTime();
    if (shouldExit)
        sm->Pop();
}

void SomeGameState::Exit(StateMachine *sm, State *from)
{
    Engine *eng = sm->GetEngine();
    eng->GetKeyboard()->KeyDown.UnsubscribeAll(this);
}

0

Tôi đã sử dụng một hệ thống rất giống nhau trên một số trò chơi và thấy rằng với một vài ngoại lệ, nó hoạt động như một mô hình UI tuyệt vời.

Vấn đề duy nhất chúng tôi gặp phải là các trường hợp mong muốn trong một số trường hợp mong muốn bật lại nhiều trạng thái trước khi đẩy trạng thái mới (chúng tôi đã chuyển lại giao diện người dùng để xóa yêu cầu, vì đó thường là dấu hiệu của giao diện người dùng xấu) và tạo kiểu thuật sĩ dòng tuyến tính (được giải quyết dễ dàng bằng cách chuyển dữ liệu sang trạng thái tiếp theo).

Việc triển khai chúng tôi đã sử dụng thực sự bao bọc ngăn xếp và xử lý logic để cập nhật và kết xuất, cũng như các thao tác trên ngăn xếp. Mỗi hoạt động trên ngăn xếp kích hoạt các sự kiện trên các trạng thái để thông báo cho chúng về hoạt động xảy ra.

Một số chức năng của trình trợ giúp cũng được thêm vào để đơn giản hóa các tác vụ phổ biến, chẳng hạn như Hoán đổi (Pop & Push, cho các luồng tuyến tính) và Đặt lại (để quay lại menu chính hoặc kết thúc một luồng).


Là một mô hình UI điều này có ý nghĩa. Tôi ngần ngại gọi chúng là các trạng thái, vì trong đầu tôi sẽ liên kết rằng với các phần bên trong của công cụ trò chơi chính, trong khi "Menu chính", "Menu tùy chọn", "Màn hình trò chơi" và "Màn hình tạm dừng" ở cấp độ cao hơn, và thường chỉ không có tương tác với trạng thái bên trong của trò chơi cốt lõi và chỉ cần gửi lệnh đến công cụ cốt lõi ở dạng "Tạm dừng", "Tạm dừng", "Tải cấp 1", "Cấp độ bắt đầu", "Cấp độ khởi động lại", "Lưu" và "Khôi phục", "đặt Mức âm lượng 57", v.v ... Rõ ràng mặc dù điều này có thể thay đổi đáng kể theo trò chơi.
Kevin Cathcart

0

Đây là cách tiếp cận tôi thực hiện cho gần như tất cả các dự án của mình, bởi vì nó hoạt động cực kỳ tốt và cực kỳ đơn giản.

Dự án gần đây nhất của tôi, Sharplike , xử lý luồng điều khiển theo cách chính xác này. Các trạng thái của chúng ta đều được kết nối với một tập hợp các hàm sự kiện được gọi khi các trạng thái thay đổi và nó có khái niệm "ngăn xếp có tên" trong đó bạn có thể có nhiều ngăn trạng thái trong cùng một máy trạng thái và một nhánh trong số chúng - một khái niệm công cụ, và không cần thiết, nhưng tiện dụng để có.

Tôi sẽ thận trọng với mô hình "nói với bộ điều khiển trạng thái nào nên theo trạng thái này khi kết thúc" mô hình được đề xuất bởi Skizz: nó không có cấu trúc âm thanh và nó tạo ra các công cụ như hộp thoại (trong mô hình trạng thái ngăn xếp tiêu chuẩn chỉ liên quan đến việc tạo mới phân lớp trạng thái với các thành viên mới, sau đó đọc nó khi bạn trở về trạng thái gọi) khó hơn nhiều so với nó.


0

Về cơ bản, tôi đã sử dụng hệ thống chính xác này trong một số hệ thống; Chẳng hạn, trạng thái menu frontend và trong trò chơi (còn gọi là "tạm dừng") có các ngăn xếp trạng thái riêng. Giao diện người dùng trong trò chơi cũng sử dụng một cái gì đó như thế này mặc dù nó có các khía cạnh "toàn cầu" (như thanh sức khỏe và bản đồ / radar) mà chuyển đổi trạng thái có thể nhuốm màu nhưng được cập nhật theo cách phổ biến giữa các tiểu bang.

Menu trong trò chơi có thể "tốt hơn" được đại diện bởi DAG, nhưng với một máy trạng thái ẩn (mỗi tùy chọn menu đi đến một màn hình khác sẽ biết cách đi đến đó và nhấn nút quay lại luôn bật trạng thái trên cùng) giống hệt nhau.

Một số hệ thống khác cũng có chức năng "thay thế trạng thái hàng đầu", nhưng điều đó thường được triển khai như StatePop()sau StatePush(x);.

Việc xử lý thẻ nhớ cũng tương tự vì tôi thực sự đã đẩy hàng tấn "thao tác" vào hàng đợi hoạt động (có chức năng thực hiện tương tự như ngăn xếp, giống như FIFO thay vì LIFO); một khi bạn bắt đầu sử dụng loại cấu trúc này ("có một điều xảy ra ngay bây giờ và khi nó hoàn thành, nó sẽ tự bật"), nó bắt đầu lây nhiễm vào mọi khu vực của mã. Ngay cả AI cũng bắt đầu sử dụng thứ gì đó như thế này; AI "không biết gì" sau đó chuyển sang "cảnh giác" khi người chơi phát ra tiếng động nhưng không thấy, và cuối cùng nâng lên thành "hoạt động" khi họ nhìn thấy người chơi (và không giống như các trò chơi ít thời gian hơn, bạn không thể che giấu trong một hộp các tông và khiến kẻ thù quên bạn! Không phải tôi cay đắng ...).

GameState.h:

enum GameState
{
   k_frontend,
   k_gameplay,
   k_inGameMenu,
   k_moviePlayback,
   k_numStates
};

void GameStatePush(GameState);
void GameStatePop();
void GameStateUpdate();

GameState.cpp:

// k_maxNumStates could be bigger, but we don't need more than
// one of each state on the stack.
static const int k_maxNumStates = k_numStates;
static GameState s_states[k_maxNumStates] = { k_frontEnd };
static int s_numStates = 1;

static void (*s_startupFunctions)()[] =
   { FrontEndStart, GameplayStart, InGameMenuStart, MovieStart };
static void (*s_shutdownFunctions)()[] =
   { FrontEndStop, GameplayStop, InGameMenuStop, MovieStop };
static void (*s_updateFunctions)()[] =
   { FrontEndUpdate, GameplayUpdate, InGameMenuUpdate, MovieUpdate };

static void GameStateStart(GameState);
static void GameStateStop(GameState);

void GameStatePush(GameState gs)
{
   Assert(s_numStates < k_maxNumStates);
   GameStateStop(s_states[s_numStates - 1])
   s_states[s_numStates] = gs;
   s_numStates++;
   GameStateStart(gs);
}

void GameStatePop()
{
   Assert(s_numStates > 1);  // can't pop last state
   s_numStates--;
   GameStateStop(s_states[s_numStates]);
   GameStateStart(s_states[s_numStates - 1]);
}

void GameStateUpdate()
{
   GameState current = s_states[s_numStates - 1];
   s_updateFunctions[current]();
}

void GameStateStart(GameState gs)
{
   s_startupFunctions[gs]();
}

void GameStateStop(GameState gs)
{
   s_shutdownFunctions[gs]();
}
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.