Bagaimana cara menggunakan lajang dengan benar dalam pemrograman mesin C ++?

16

Saya tahu bahwa lajang itu jahat, mesin gim lama saya menggunakan objek 'Gim' tunggal yang menangani semuanya, mulai dari memegang semua data hingga perulangan gim yang sebenarnya. Sekarang saya membuat yang baru.

Masalahnya adalah, untuk menggambar sesuatu di SFML yang Anda gunakan di window.draw(sprite)mana jendela adalah sf::RenderWindow. Ada 2 opsi yang saya lihat di sini:

  1. Buat objek Game tunggal yang diambil setiap entitas dalam game (apa yang saya gunakan sebelumnya)
  2. Jadikan ini sebagai konstruktor untuk entitas: Entity(x, y, window, view, ...etc)(ini hanya konyol dan menjengkelkan)

Apa cara yang tepat untuk melakukan ini sambil mempertahankan konstruktor Entity hanya x dan y?

Saya bisa mencoba dan melacak semua yang saya buat di loop game utama, dan secara manual menggambar sprite mereka di loop game, tapi itu juga tampak berantakan, dan saya juga ingin kontrol penuh mutlak atas seluruh fungsi draw untuk entitas.

Aki
sumber
1
Anda bisa melewati jendela sebagai argumen fungsi 'render'.
dari
25
Lajang tidak buruk! mereka bisa bermanfaat dan terkadang perlu (tentu saja bisa diperdebatkan).
ExOfDe
3
Jangan ragu untuk mengganti lajang dengan global polos. Tidak ada gunanya menciptakan sumber daya yang dibutuhkan secara global "sesuai permintaan", tidak ada gunanya membagikannya. Untuk entitas, Anda dapat menggunakan kelas "level", untuk menampung hal-hal tertentu yang relevan dengan semuanya.
snake5
Saya mendeklarasikan jendela saya dan dependensi lain di main saya, dan kemudian saya memiliki pointer di kelas saya yang lain.
KaareZ
1
@ JAB Mudah diperbaiki dengan inisialisasi manual dari main (). Inisialisasi malas membuatnya terjadi pada saat yang tidak diketahui, yang bukan merupakan ide bagus untuk sistem inti.
snake5

Jawaban:

3

Hanya menyimpan data yang diperlukan untuk merender sprite di dalam setiap entitas, lalu mengambilnya dari entitas dan meneruskannya ke jendela untuk rendering. Tidak perlu menyimpan jendela apa pun atau melihat data di dalam entitas.

Anda bisa memiliki kelas Game atau Engine tingkat atas yang memegang kelas Level (menampung semua entitas yang sedang digunakan), dan kelas Renderer (berisi jendela, tampilan, dan apa pun lainnya untuk dirender).

Jadi lingkaran pembaruan game di kelas tingkat atas Anda dapat terlihat seperti:

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());
}
Wisaya Biru
sumber
3
Tidak ada yang ideal tentang seorang lajang. Mengapa membuat implementasi internal untuk publik ketika Anda tidak perlu? Mengapa menulis, Logger::getInstance().Log(...)bukan hanya Log(...)? Mengapa menginisialisasi kelas secara acak ketika ditanya apakah Anda dapat melakukannya secara manual hanya sekali? Fungsi global yang mereferensikan global statis jauh lebih sederhana untuk dibuat dan digunakan.
snake5
@ snake5 Membenarkan lajang di Stack Exchange seperti bersimpati dengan Hitler.
Willy Goat
30

Pendekatan sederhana adalah dengan hanya membuat hal yang dulu Singleton<T> global T. Global juga memiliki masalah, tetapi mereka tidak mewakili banyak kerja ekstra dan kode boilerplate untuk menegakkan batasan sepele. Ini pada dasarnya satu-satunya solusi yang tidak akan melibatkan (berpotensi) menyentuh konstruktor entitas.

Pendekatan yang lebih sulit, tetapi mungkin lebih baik adalah meneruskan ketergantungan Anda ke tempat Anda membutuhkannya . Ya, ini mungkin melibatkan pengalihan Window *ke banyak objek (seperti entitas Anda) dengan cara yang terlihat kotor. Fakta bahwa itu terlihat kotor harus memberi tahu Anda sesuatu: desain Anda mungkin kotor.

Alasan ini lebih sulit (di luar melibatkan lebih banyak mengetik) adalah bahwa ini sering mengarah pada refactoring antarmuka Anda sehingga hal yang Anda "perlu" lewati diperlukan oleh lebih sedikit kelas level daun. Hal ini membuat banyak keburukan yang melekat dalam meneruskan renderer Anda ke semuanya hilang, dan itu juga meningkatkan pemeliharaan umum kode Anda dengan mengurangi jumlah dependensi dan penggandengan, sejauh mana Anda membuatnya sangat jelas dengan mengambil dependensi sebagai parameter . Ketika dependensinya adalah lajang atau global, itu kurang jelas bagaimana sistem Anda saling berhubungan.

Tapi ini berpotensi menjadi usaha besar . Melakukannya pada suatu sistem setelah fakta bisa sangat menyakitkan. Mungkin jauh lebih pragmatis bagi Anda untuk hanya meninggalkan sistem Anda sendirian, dengan singleton, untuk saat ini (terutama jika Anda mencoba untuk benar-benar mengirimkan permainan yang berfungsi dengan baik; pemain umumnya tidak akan peduli jika Anda memiliki satu atau empat di sana).

Jika Anda ingin mencoba melakukan ini dengan desain yang ada, Anda mungkin perlu memposting lebih banyak detail tentang implementasi Anda saat ini karena sebenarnya tidak ada daftar periksa umum untuk melakukan perubahan ini. Atau datang mendiskusikannya di obrolan .

Dari apa yang telah Anda posting, saya pikir langkah besar dalam arah "tanpa singleton" adalah untuk menghindari kebutuhan entitas Anda untuk memiliki akses ke jendela atau tampilan. Ini menunjukkan bahwa mereka menggambar diri mereka sendiri, dan Anda tidak perlu memiliki entitas menggambar diri mereka sendiri . Anda dapat mengadopsi metodologi di mana entitas hanya berisi informasi yang memungkinkanmereka harus ditarik oleh beberapa sistem eksternal (yang memiliki jendela dan melihat referensi). Entitas hanya memperlihatkan posisinya, dan sprite itu harus digunakan (atau semacam referensi untuk sprite tersebut, jika Anda ingin menembolokkan sprite aktual dalam renderer itu sendiri untuk menghindari duplikat instance). Penyaji hanya diminta untuk menggambar daftar entitas tertentu, yang dilaluinya, membaca data dari, dan menggunakan objek jendela yang dipegang secara internal untuk memanggil drawdengan sprite mencari entitas tersebut.

Komunitas
sumber
3
Saya tidak terbiasa dengan C ++, tetapi tidakkah ada kerangka kerja injeksi ketergantungan yang nyaman untuk bahasa ini?
bgusach
1
Saya tidak akan menggambarkan salah satu dari mereka sebagai "nyaman," dan saya tidak menemukan mereka sangat berguna secara umum, tetapi yang lain mungkin memiliki pengalaman yang berbeda dengan mereka sehingga itu adalah poin yang baik untuk membahasnya.
1
Metode yang ia gambarkan sebagai membuatnya sehingga entitas tidak menggambarnya sendiri tetapi memegang informasi dan sistem tunggal menangani menggambar semua entitas banyak digunakan di mesin game paling populer saat ini.
Patrick W. McMahon
1
+1 untuk "Fakta bahwa tampilannya terlihat kotor harus memberi tahu Anda sesuatu: desain Anda mungkin kotor."
Shadow503
+1 untuk memberikan kasus ideal dan jawaban pragmatis.
6

Mewarisi dari sf :: RenderWindow

SFML sebenarnya mendorong Anda untuk mewarisi dari kelasnya.

class GameWindow: public sf::RenderWindow{};

Dari sini, Anda membuat fungsi menggambar anggota untuk menggambar entitas.

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

Sekarang Anda bisa melakukan ini:

GameWindow window;
Entity entity;

window.draw(entity);

Anda bahkan dapat mengambil ini selangkah lebih jauh jika Entitas Anda akan menyimpan sprite unik mereka sendiri dengan membuat Entity mewarisi dari sf :: Sprite.

class Entity: public sf::Sprite{};

Sekarang sf::RenderWindowbisa menggambar Entitas, dan entitas sekarang memiliki fungsi seperti setTexture()dan setColor(). Entity bahkan dapat menggunakan posisi sprite sebagai posisinya sendiri, memungkinkan Anda untuk menggunakan setPosition()fungsi untuk memindahkan Entity DAN sprite-nya.


Pada akhirnya , cukup menyenangkan jika Anda hanya memiliki:

window.draw(game);

Di bawah ini adalah beberapa implementasi contoh cepat

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

ATAU

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

Anda menghindari lajang dalam pengembangan game dengan cara yang sama Anda menghindarinya dalam setiap jenis pengembangan perangkat lunak lain: Anda melewati dependensi .

Dengan itu keluar dari jalan, Anda dapat memilih untuk lulus dependensi langsung sebagai jenis telanjang (seperti int, Window*, dll) atau Anda dapat memilih untuk melewati mereka dalam satu atau lebih kustom pembungkus jenis (seperti EntityInitializationOptions).

Cara pertama dapat mengganggu (seperti yang telah Anda ketahui), sedangkan yang terakhir akan memungkinkan Anda untuk melewatkan semuanya dalam satu objek dan memodifikasi bidang (dan bahkan mengkhususkan jenis opsi) tanpa berkeliling dan mengubah setiap konstruktor entitas. Saya pikir cara yang terakhir lebih baik.

TC
sumber
3

Lajang tidak buruk. Sebaliknya mereka mudah disalahgunakan. Di sisi lain, global bahkan lebih mudah disalahgunakan dan memiliki banyak masalah.

Satu-satunya alasan yang sah untuk mengganti singleton dengan global adalah untuk menenangkan pembenci singleton religius.

Masalahnya adalah memiliki desain yang mencakup kelas-kelas yang hanya ada satu contoh global yang pernah ada, dan yang perlu diakses dari mana-mana. Ini pecah segera setelah Anda akhirnya memiliki beberapa contoh dari singleton, misalnya dalam permainan ketika Anda menerapkan layar split, atau dalam aplikasi perusahaan yang cukup besar ketika Anda melihat satu logger tidak selalu merupakan ide yang hebat setelah semua .

Intinya, jika Anda benar-benar memiliki kelas di mana Anda memiliki instance global tunggal yang tidak dapat Anda bagikan dengan referensi , singleton sering kali merupakan salah satu solusi yang lebih baik dalam kumpulan solusi suboptimal.

Peter - Unban Robert Harvey
sumber
1
Saya pembenci agama yang tunggal dan saya juga tidak menganggap solusi global. : S
Dan Pantry
1

Menyuntikkan dependensi. Manfaat melakukan ini sekarang adalah Anda dapat membuat berbagai jenis dependensi ini melalui pabrik. Sayangnya, mencabut lajang dari kelas yang menggunakannya seperti menarik kucing dengan kaki belakangnya melintasi karpet. Tetapi jika Anda menyuntikkannya, Anda dapat menukar implementasi, mungkin dengan cepat.

RenderSystem(IWindow* window);

Sekarang Anda bisa menyuntikkan berbagai jenis windows. Ini memungkinkan Anda untuk menulis tes terhadap RenderSystem dengan berbagai jenis windows sehingga Anda dapat melihat bagaimana RenderSystem Anda akan pecah, atau berkinerja. Ini tidak mungkin, atau lebih sulit, jika Anda menggunakan lajang langsung di dalam "RenderSystem".

Sekarang lebih dapat diuji, modular, dan juga dipisahkan dari implementasi tertentu. Itu hanya tergantung pada antarmuka, bukan implementasi konkret.

Todd
sumber