Status Game 'Stack'?

52

Saya sedang berpikir tentang bagaimana menerapkan status game ke dalam game saya. Hal utama yang saya inginkan adalah:

  • Keadaan semi-transparan top-mampu melihat melalui menu jeda ke permainan di belakang

  • Sesuatu OO-Saya menemukan ini lebih mudah untuk digunakan dan memahami teori di belakang, serta menjaga orgranised dan menambahkan lebih banyak.



Saya berencana menggunakan daftar tertaut, dan memperlakukannya sebagai tumpukan. Ini berarti saya dapat mengakses keadaan di bawah ini untuk semi transparansi.
Paket: Buat tumpukan negara menjadi daftar pointer yang terhubung ke IGameStates. Negara bagian atas menangani perintah pembaruan dan inputnya sendiri, dan kemudian memiliki anggota isTransparent untuk memutuskan apakah keadaan di bawahnya harus dibuat.
Maka saya bisa melakukan:

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

Untuk mewakili pemuatan pemain, lalu pergi ke opsi, dan kemudian menu utama.
Apakah ini ide yang bagus, atau ...? Haruskah saya melihat sesuatu yang lain?

Terima kasih.

Bebek Komunis
sumber
Apakah Anda ingin melihat MainMenuState di belakang OptionsMenuState? Atau hanya layar game di belakang OptionsMenuState?
Mendesing
Rencananya adalah negara akan memiliki nilai / bendera opacity / isTransparent. Saya akan memeriksa dan melihat apakah keadaan teratas memiliki ini benar, dan jika demikian apa nilainya. Lalu render dengan opacity sebanyak itu di negara lain. Dalam hal ini, tidak, saya tidak akan.
Bebek Komunis
Saya tahu ini sudah terlambat, tetapi untuk pembaca di masa depan: jangan gunakan newdengan cara yang ditunjukkan dalam kode sampel, itu hanya meminta kebocoran memori atau kesalahan lain yang lebih serius.
Pharap

Jawaban:

44

Saya bekerja pada mesin yang sama dengan coderanger. Saya memiliki sudut pandang yang berbeda. :)

Pertama, kami tidak memiliki setumpuk FSM - kami memiliki setumpuk status. Setumpuk negara membuat FSM tunggal. Saya tidak tahu akan seperti apa tumpukan FSM. Mungkin terlalu rumit untuk melakukan sesuatu yang praktis.

Masalah terbesar saya dengan Mesin Negara Global kami adalah bahwa itu adalah tumpukan negara, dan bukan satu set negara. Ini berarti, misalnya, ... / MainMenu / Memuat berbeda dari ... / Memuat / MainMenu, tergantung pada apakah Anda mendapatkan menu utama sebelum atau setelah layar pemuatan (permainan asinkron dan memuat sebagian besar didorong oleh server ).

Sebagai dua contoh hal ini menjadikannya jelek:

  • Ini menyebabkan misalnya status LoadingGameplay, jadi Anda memiliki Base / Loading, dan Base / Gameplay / LoadingGameplay untuk memuat dalam keadaan Gameplay, yang harus mengulang banyak kode dalam keadaan loading normal (tetapi tidak semua, dan menambahkan lebih banyak lagi) ).
  • Kami memiliki beberapa fungsi seperti "jika dalam pencipta karakter pergi ke gameplay; jika dalam gameplay pergi ke pilih karakter; jika dalam karakter pilih kembali untuk masuk", karena kami ingin menunjukkan jendela antarmuka yang sama di berbagai negara tetapi membuat Kembali / Maju tombol masih berfungsi.

Meskipun namanya, itu tidak terlalu "global". Sebagian besar sistem permainan internal tidak menggunakannya untuk melacak keadaan internal mereka, karena mereka tidak ingin keadaan mereka bercanda dengan sistem lain. Lainnya, misalnya sistem UI, dapat menggunakannya tetapi hanya untuk menyalin negara ke sistem negara setempat mereka sendiri. (Saya akan sangat berhati-hati terhadap sistem untuk negara UI. Keadaan UI bukan tumpukan, itu benar-benar DAG, dan mencoba untuk memaksa struktur lain di atasnya hanya akan membuat UI yang frustasi untuk digunakan.)

Apa yang baik untuk itu adalah mengisolasi tugas untuk mengintegrasikan kode dari pemrogram infrastruktur yang tidak tahu bagaimana alur permainan sebenarnya terstruktur, sehingga Anda bisa memberi tahu orang yang menulis patcher "masukkan kode Anda di Client_Patch_Update", dan orang yang menulis grafik memuat "masukkan kode Anda di Client_MapTransfer_OnEnter", dan kami dapat menukar logika tertentu tanpa masalah.

Pada proyek sampingan, saya lebih beruntung dengan set keadaan daripada tumpukan , tidak takut untuk membuat beberapa mesin untuk sistem yang tidak terkait, dan menolak untuk membiarkan diri saya jatuh ke dalam perangkap memiliki "negara global", yang sebenarnya hanya cara rumit untuk menyinkronkan hal-hal melalui variabel global - Tentu, Anda akhirnya akan melakukannya di dekat tenggat waktu, tetapi jangan merancang dengan itu sebagai tujuan Anda . Pada dasarnya, status dalam game bukan tumpukan, dan status dalam game tidak semuanya terkait.

GSM juga, seperti fungsi pointer dan perilaku non-lokal cenderung, membuat hal-hal debugging lebih sulit, meskipun debug semacam transisi negara besar itu tidak terlalu menyenangkan sebelum kita memilikinya juga. Set negara bukannya tumpukan negara tidak benar-benar membantu ini, tetapi Anda harus menyadarinya. Fungsi virtual daripada fungsi pointer dapat mengurangi itu.


sumber
Jawaban yang bagus, terima kasih! Saya pikir saya bisa mengambil banyak dari pos Anda dan pengalaman masa lalu Anda. : D + 1 / Centang.
Bebek Komunis
Yang menyenangkan tentang hierarki adalah Anda dapat membangun status utilitas yang hanya didorong ke atas dan tidak perlu khawatir tentang apa yang sedang berjalan.
coderanger
Saya tidak melihat bagaimana itu argumen untuk hierarki daripada set. Sebaliknya, hierarki membuat semua komunikasi antar negara lebih rumit, karena Anda tidak tahu ke mana mereka didorong.
Poin bahwa UI sebenarnya adalah DAG diambil dengan baik, tapi saya tidak setuju bahwa itu pasti dapat diwakili dalam tumpukan. Grafik asiklik terarah yang terhubung (dan saya tidak bisa memikirkan kasus di mana itu tidak akan menjadi DAG terhubung) dapat ditampilkan sebagai pohon, dan tumpukan pada dasarnya adalah pohon.
Ed Ropple
2
Tumpukan adalah bagian dari pohon, yang merupakan bagian dari DAG, yang merupakan bagian dari semua grafik. Semua tumpukan adalah pohon, semua pohon adalah DAG, tetapi sebagian besar DAG bukan pohon, dan sebagian besar pohon bukan tumpukan. DAG memiliki urutan topologi yang akan memungkinkan Anda untuk menyimpannya di stack (untuk traversal misalnya resolusi dependensi), tetapi begitu Anda menjejalkannya ke dalam stack, Anda telah kehilangan informasi berharga. Dalam hal ini, kemampuan menavigasi antara layar dan induknya jika memiliki saudara kandung sebelumnya.
11

Berikut ini contoh implementasi tumpukan gamestate yang menurut saya sangat berguna: http://creators.xna.com/en-US/samples/gamestatemanagement

Itu ditulis dalam C # dan untuk mengkompilasinya Anda memerlukan kerangka kerja XNA, namun Anda bisa melihat kode, dokumentasi dan video untuk mendapatkan ide.

Ini dapat mendukung transisi status, status transparan (seperti kotak pesan modal) dan status pemuatan (yang mengelola pembongkaran status yang ada dan memuat status berikutnya).

Saya menggunakan konsep yang sama dalam proyek hobi saya (non-C #) sekarang (memang, mungkin tidak cocok untuk proyek yang lebih besar) dan untuk proyek kecil / hobi saya pasti dapat merekomendasikan pendekatan ini.

Janis Kirsteins
sumber
5

Ini mirip dengan apa yang kami gunakan, setumpuk FSM. Pada dasarnya hanya memberi setiap negara fungsi masuk, keluar, dan centang dan memanggil mereka secara berurutan. Bekerja sangat baik untuk menangani hal-hal seperti memuat juga.

pembuat kode
sumber
3

Salah satu volume "Permata Pemrograman Game" memiliki implementasi mesin negara di dalamnya yang dimaksudkan untuk status permainan; http://emergent.net/Global/Documents/textbook/Chapter1_GameAppFramework.pdf memiliki contoh cara menggunakannya untuk gim kecil, dan tidak boleh terlalu spesifik Gamebryo untuk dapat dibaca.

Tom Hudson
sumber
Bagian pertama dari "Pemrograman Peran Bermain Game dengan DirectX" juga menerapkan sistem negara (dan sistem proses - perbedaan yang sangat menarik).
Ricket
Itu adalah dokumen yang bagus dan menjelaskan hampir persis bagaimana saya menerapkannya di masa lalu, singkat dari hirarki objek yang tidak perlu yang mereka gunakan dalam contoh.
dash-tom-bang
3

Hanya untuk menambahkan sedikit standardisasi ke dalam diskusi, istilah CS klasik untuk struktur data semacam ini adalah otomat pushdown .

banyak sekali
sumber
Saya tidak yakin implementasi dunia nyata dari tumpukan negara hampir setara dengan pushdown automaton. Seperti yang disebutkan dalam jawaban lain, implementasi praktis selalu berakhir dengan perintah seperti "pop two state", "tukar negara ini", atau "kirimkan data ini ke keadaan berikutnya di luar tumpukan". Dan automaton adalah otomat - komputer - bukan struktur data. Baik state stack dan pushdown automata menggunakan stack sebagai struktur data.
1
"Saya tidak yakin implementasi tumpukan negara di dunia nyata hampir setara dengan robot pushdown." Apa bedanya? Keduanya memiliki seperangkat negara bagian yang terbatas, sejarah negara bagian, dan operasi primitif untuk mendorong dan mendorong negara bagian. Tak satu pun dari operasi lain yang Anda sebutkan secara fundamental berbeda dari itu. "Pop two state" hanya muncul dua kali. "swap" adalah sembulan dan dorongan. Melewati data berada di luar ide inti, tetapi setiap game yang menggunakan "FSM" juga mengolah data tambahan tanpa merasa seperti nama itu tidak berlaku lagi.
murah hati
Dalam automat pushdown, satu-satunya negara yang dapat mempengaruhi transisi Anda adalah negara di atas. Bertukar dua status di tengah tidak diperbolehkan; bahkan melihat keadaan di tengah tidak diperbolehkan. Saya merasa ekspansi semantik dari istilah "FSM" masuk akal dan memiliki manfaat (dan kami masih memiliki istilah "DFA" dan "NFA" untuk arti yang paling terbatas), tetapi "pushdown otomaton" hanyalah istilah ilmu komputer dan hanya ada kebingungan menunggu jika kita menerapkannya pada setiap sistem berbasis stack tunggal di luar sana.
Saya lebih suka implementasi di mana satu-satunya negara yang dapat mempengaruhi apa pun adalah keadaan yang di atas, meskipun dalam beberapa kasus berguna untuk dapat menyaring masukan negara dan melewati pemrosesan ke keadaan "lebih rendah". (Misalnya peta pengontrol input pengolah ke metode ini, kondisi teratas mengambil bit yang dipedulikannya dan mungkin menghapusnya, lalu meneruskan kontrol ke status berikutnya di stack.)
dash-tom-bang
1
Poin bagus, tetap!
murah hati
1

Saya tidak yakin tumpukan sepenuhnya diperlukan serta membatasi fungsi sistem negara. Menggunakan tumpukan, Anda tidak dapat 'keluar' dari satu ke beberapa kemungkinan. Katakanlah Anda memulai di "Menu Utama" kemudian pergi ke "Load Game", Anda mungkin ingin pergi ke "Jeda" setelah berhasil memuat game yang disimpan dan kembali ke "Menu Utama" jika pengguna membatalkan beban.

Saya hanya akan meminta negara menentukan negara untuk diikuti ketika keluar.

Untuk kasus-kasus di mana Anda ingin kembali ke keadaan sebelum keadaan saat ini, misalnya "Menu Utama-> Pilihan-> Menu Utama" dan "Jeda-> Pilihan-> Jeda", cukup masukkan sebagai parameter startup ke status negara untuk kembali ke.

Mendesis
sumber
Mungkin saya salah paham pertanyaannya?
Mendesing
Tidak, kamu tidak. Saya pikir pemilih-bawah melakukannya.
The Communist Duck
Menggunakan tumpukan tidak menghalangi penggunaan transisi status eksplisit.
dash-tom-bang
1

Solusi lain untuk transisi dan hal-hal lain seperti itu adalah menyediakan negara tujuan dan sumber, bersama dengan mesin negara, yang dapat dikaitkan dengan "mesin", apa pun itu. Yang benar adalah bahwa sebagian besar mesin negara mungkin perlu disesuaikan dengan proyek yang ada. Satu solusi mungkin menguntungkan game ini atau itu, solusi lain mungkin menghambatnya.

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

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

Status didorong dengan kondisi saat ini dan mesin sebagai parameter.

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

Negara muncul dengan cara yang sama. Apakah Anda memanggil Enter()yang lebih rendah Stateadalah pertanyaan implementasi.

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;
}

Saat memasukkan, memperbarui atau keluar, Statemendapatkan semua informasi yang dibutuhkan.

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);
}
Nick Bedford
sumber
0

Saya telah menggunakan sistem yang sangat mirip di beberapa permainan dan menemukan bahwa dengan beberapa pengecualian, ini berfungsi sebagai model UI yang sangat baik.

Satu-satunya masalah yang kami temui adalah kasus yang diinginkan dalam kasus tertentu untuk mengembalikan beberapa negara sebelum mendorong negara baru (kami mengubah ulang UI untuk menghapus persyaratan, karena biasanya itu merupakan tanda UI yang buruk) dan membuat gaya penyihir aliran linier (diselesaikan dengan mudah dengan meneruskan data ke negara berikutnya).

Implementasi yang kami gunakan sebenarnya membungkus stack dan menangani logika untuk memperbarui dan rendering, serta operasi pada stack. Setiap operasi di stack memicu peristiwa di negara bagian untuk memberi tahu mereka tentang operasi yang terjadi.

Beberapa fungsi pembantu ditambahkan juga untuk menyederhanakan tugas-tugas umum, seperti Swap (Pop & Push, untuk aliran linier) dan Reset (untuk kembali ke menu utama, atau mengakhiri aliran).

Jason Kozak
sumber
Sebagai model UI, ini masuk akal. Saya akan ragu untuk memanggil mereka menyatakan, karena di kepala saya, saya akan mengaitkannya dengan internal mesin gim utama, sementara "Menu Utama", "Menu Opsi", "Layar Game", dan "Layar jeda" adalah level yang lebih tinggi, dan sering hanya tidak memiliki interaksi dengan keadaan internal dari game inti, dan cukup mengirim perintah ke mesin inti dari bentuk "Jeda", "Unpause", "Load level 1", "Start Level", "Restart Level", "Simpan", dan "Kembalikan", "atur Tingkat volume 57", dll. Jelas hal ini bisa sangat berbeda menurut permainan.
Kevin Cathcart
0

Ini adalah pendekatan yang saya ambil untuk hampir semua proyek saya, karena ia bekerja dengan sangat baik dan sangat sederhana.

Proyek terbaru saya, Sharplike , menangani aliran kontrol dengan cara yang tepat ini. Status kita semua terhubung dengan serangkaian fungsi acara yang dipanggil ketika status berubah, dan fitur konsep "bernama tumpukan" di mana Anda dapat memiliki banyak tumpukan negara dalam mesin negara yang sama dan bercabang di antaranya - sebuah konsep alat, dan tidak perlu, tetapi berguna untuk dimiliki.

Saya akan memperingatkan terhadap paradigma "beri tahu controller keadaan apa yang harus diikuti ketika ini berakhir" yang disarankan oleh Skizz: paradigma itu tidak terdengar secara struktural, dan itu membuat hal-hal seperti kotak dialog (yang dalam paradigma stack-state standar hanya melibatkan pembuatan baru sebutkan subclass dengan anggota baru, lalu bacalah saat Anda kembali ke status aktif) jauh lebih sulit daripada yang seharusnya.

Ed Ropple
sumber
0

Pada dasarnya saya menggunakan sistem yang tepat ini di beberapa sistem secara orthogonal; menu frontend dan dalam game (alias "jeda"), misalnya, memiliki tumpukan status mereka sendiri. UI dalam game juga menggunakan sesuatu seperti ini walaupun memiliki aspek "global" (seperti bilah kesehatan dan peta / radar) yang mungkin diganti oleh peralihan negara tetapi diperbarui secara umum di seluruh negara bagian.

Menu dalam gim mungkin "lebih baik" diwakili oleh DAG, tetapi dengan mesin keadaan implisit (setiap opsi menu yang menuju ke layar lain tahu cara menuju ke sana, dan menekan tombol kembali selalu muncul di bagian atas) efeknya adalah persis sama.

Beberapa dari sistem lain ini juga memiliki fungsionalitas "ganti keadaan teratas", tetapi itu biasanya diterapkan sebagai StatePop()diikuti oleh StatePush(x);.

Penanganan kartu memori serupa karena saya benar-benar mendorong satu ton "operasi" ke dalam antrian operasi (yang secara fungsional melakukan hal yang sama dengan tumpukan, seperti halnya FIFO daripada LIFO); begitu Anda mulai menggunakan struktur semacam ini ("ada satu hal yang terjadi sekarang, dan ketika selesai muncul sendiri") ia mulai menginfeksi setiap area kode. Bahkan AI mulai menggunakan sesuatu seperti ini; AI "tidak mengerti" kemudian beralih ke "waspada" ketika pemain membuat suara tetapi tidak terlihat, dan akhirnya diangkat menjadi "aktif" ketika mereka melihat pemain (dan tidak seperti permainan yang lebih kecil saat itu, Anda tidak bisa menyembunyikan dalam kotak kardus dan buat musuh melupakanmu! Bukannya aku pahit ...).

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]();
}
dash-tom-bang
sumber