Menerapkan Pola Desain Perintah di Unity

gambar

Pernahkah Anda bertanya-tanya bagaimana dalam game seperti Super Meat Boy fungsi replay diimplementasikan? Salah satu cara untuk mengimplementasikannya adalah dengan melakukan input dengan cara yang sama seperti yang dilakukan pemain, yang, pada gilirannya, berarti bahwa input tersebut perlu disimpan. Anda dapat menggunakan pola Command untuk ini dan banyak lagi.

Templat Perintah juga berguna untuk membuat fungsi Batalkan dan Ulangi dalam permainan strategi.

Dalam tutorial ini, kami mengimplementasikan Templat perintah di C # dan menggunakannya untuk memandu karakter bot melalui labirin tiga dimensi. Dari tutorial Anda akan belajar:

  • Dasar-dasar pola Perintah.
  • Bagaimana menerapkan pola Perintah
  • Cara membuat antrian perintah input dan menunda eksekusi.

Catatan : diasumsikan bahwa Anda sudah terbiasa dengan Unity dan memiliki pengetahuan rata-rata tentang C #. Dalam tutorial ini kita akan bekerja dengan Unity 2019.1 dan C # 7 .

Mulai bekerja


Untuk memulai, unduh materi proyek . Buka zip file dan buka proyek Starter di Unity.

Pergi ke RW / Adegan dan buka adegan Utama . Adegan ini terdiri dari bot dan labirin, serta UI terminal yang menampilkan instruksi. Desain level dibuat dalam bentuk kisi, yang berguna ketika kita memindahkan bot secara visual melalui labirin.


Jika Anda mengklik Play , kita akan melihat bahwa instruksi tidak berfungsi. Ini normal karena kami akan menambahkan fungsionalitas ini ke tutorial.

Bagian paling menarik dari adegan ini adalah GameObject Bot . Pilih di jendela Hierarki dengan mengkliknya.


Di Inspektur, Anda dapat melihat bahwa ia memiliki komponen Bot . Kami akan menggunakan komponen ini dengan mengeluarkan perintah input.


Kami memahami logika bot


Pergi ke RW / Skrip dan buka skrip Bot di editor kode. Anda tidak perlu tahu apa yang terjadi dalam skrip Bot . Tapi lihat dua metode: Move dan Shoot . Sekali lagi, Anda tidak perlu mencari tahu apa yang terjadi di dalam metode ini, tetapi Anda perlu memahami cara menggunakannya.

Perhatikan bahwa metode Move menerima parameter input CardinalDirection . CardinalDirection adalah enumerasi. Elemen enumerasi dari tipe CardinalDirection bisa Up , Down , Right atau Left . Tergantung pada CardinalDirection dipilih CardinalDirection bot bergerak tepat satu kotak di sepanjang kisi dalam arah yang sesuai.


Metode Shoot memaksa bot untuk menembakkan peluru yang menghancurkan dinding kuning , tetapi tidak berguna terhadap dinding lain.


Akhirnya, lihat metode ResetToLastCheckpoint ; untuk memahami apa yang dia lakukan, lihat labirin. Ada titik-titik di labirin yang disebut pos pemeriksaan . Untuk melewati labirin, bot harus sampai ke titik kontrol hijau .


Ketika bot menginjak titik kontrol baru, maka itu menjadi yang terakhir baginya. ResetToLastCheckpoint me-reset posisi bot, memindahkannya ke titik kontrol terakhir.


Meskipun kami tidak dapat menggunakan metode ini, kami akan segera memperbaikinya. Untuk memulai, Anda perlu belajar tentang pola desain Perintah .

Apa itu Pola Desain Perintah?


Pola Perintah adalah salah satu dari 23 pola desain yang dijelaskan dalam Pola Desain: Elemen-elemen buku Perangkat Lunak Berorientasi Objek yang Dapat Digunakan Kembali yang ditulis oleh Geng Empat oleh Erich Gamma, Richard Helm, Ralph Johnson dan John Vlissides (GoF, Geng Empat).

Para penulis melaporkan bahwa "pola Perintah merangkum permintaan sebagai objek, sehingga memungkinkan kami untuk membuat parameter objek lain dengan berbagai permintaan, antrian atau permintaan log, dan mendukung operasi yang dapat dibalik."

Wow! Bagaimana itu?

Saya mengerti definisi ini tidak terlalu sederhana, jadi mari kita menganalisisnya.

Enkapsulasi berarti pemanggilan metode dapat dienkapsulasi sebagai objek.


Metode enkapsulasi dapat mempengaruhi banyak objek tergantung pada parameter input. Ini disebut parameterisasi objek lain.

"Perintah" yang dihasilkan dapat disimpan bersama dengan tim lain sampai mereka dieksekusi. Ini adalah antrian permintaan.


Antrian Tim

Akhirnya, reversibilitas berarti operasi dapat dikembalikan menggunakan fungsi Undo.

OK, tapi bagaimana ini tercermin dalam kode?

Kelas Command akan memiliki metode Execute , yang menerima sebagai input parameter objek (dengan mana perintah dieksekusi) yang disebut Receiver . Faktanya, metode Execute dienkapsulasi oleh kelas Command.

Banyak instance dari kelas Command dapat dilewatkan sebagai objek biasa, yaitu, mereka dapat disimpan dalam struktur data seperti antrian, tumpukan, dll.

Untuk menjalankan perintah, Anda harus memanggil metode Execute-nya. Kelas yang memulai eksekusi disebut Invoker .

Proyek saat ini berisi kelas kosong yang disebut BotCommand . Di bagian selanjutnya, kami akan mengimplementasikan implementasi di atas untuk memungkinkan bot melakukan tindakan menggunakan templat Perintah.

Pindahkan bot


Implementasi pola perintah


Di bagian ini, kami menerapkan pola Perintah. Ada banyak cara untuk mengimplementasikannya. Dalam tutorial ini kita akan membahas salah satunya.

Untuk memulai, buka RW / Skrip dan dan buka skrip BotCommand di editor. Kelas BotCommand masih kosong, tetapi tidak lama.

Masukkan kode berikut ke dalam kelas:

  //1 private readonly string commandName; //2 public BotCommand(ExecuteCallback executeMethod, string name) { Execute = executeMethod; commandName = name; } //3 public delegate void ExecuteCallback(Bot bot); //4 public ExecuteCallback Execute { get; private set; } //5 public override string ToString() { return commandName; } 

Apa yang sedang terjadi di sini?

  1. Variabel commandName digunakan hanya untuk menyimpan nama perintah yang dapat dibaca manusia. Tidak perlu menggunakannya dalam template, tetapi kita akan membutuhkannya nanti dalam tutorial.
  2. Konstruktor BotCommand menerima fungsi dan string. Ini akan membantu kita mengatur metode Execute objek Command dan name .
  3. Delegasi ExecuteCallback mendefinisikan jenis metode yang dienkapsulasi. Metode enkapsulasi akan mengembalikan batal dan menerima sebagai parameter input objek bertipe Bot (komponen Bot ).
  4. Properti Execute akan merujuk ke metode enkapsulasi. Kami akan menggunakannya untuk memanggil metode enkapsulasi.
  5. Metode ToString diganti untuk mengembalikan string commandName . Ini nyaman, misalnya, untuk digunakan di UI.

Simpan perubahan dan hanya itu! Kami telah berhasil menerapkan pola Perintah.

Masih menggunakannya.

Membangun tim


Buka BotInputHandler di folder RW / Scripts .

Di sini kita akan membuat lima contoh BotCommand . Contoh-contoh ini akan merangkum metode untuk memindahkan GameObject Bot ke atas, bawah, kiri, dan kanan, serta untuk pemotretan.

Untuk mengimplementasikan ini, masukkan berikut ini ke dalam kelas ini:

  //1 private static readonly BotCommand MoveUp = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Up); }, "moveUp"); //2 private static readonly BotCommand MoveDown = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Down); }, "moveDown"); //3 private static readonly BotCommand MoveLeft = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Left); }, "moveLeft"); //4 private static readonly BotCommand MoveRight = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Right); }, "moveRight"); //5 private static readonly BotCommand Shoot = new BotCommand(delegate (Bot bot) { bot.Shoot(); }, "shoot"); 

Dalam setiap contoh ini, metode anonim diteruskan ke konstruktor. Metode anonim ini akan diringkas di dalam objek perintah yang sesuai. Seperti yang Anda lihat, tanda tangan dari masing-masing metode anonim memenuhi persyaratan yang ditentukan oleh delegasi ExecuteCallback .

Selain itu, parameter kedua ke konstruktor adalah string yang menunjukkan nama perintah. Nama ini akan dikembalikan oleh metode ToString dari instance perintah. Nanti kita akan menerapkannya untuk UI.

Dalam empat contoh pertama, metode anonim memanggil metode Move pada objek bot . Namun, parameter inputnya berbeda.

Perintah MoveUp , MoveDown , MoveLeft dan MoveRight melewati parameter Move CardinalDirection.Up , CardinalDirection.Down , CardinalDirection.Left dan CardinalDirection.Right . Seperti yang disebutkan di bagian What is Command Design Pattern , mereka menunjukkan arah yang berbeda untuk Bot GameObject untuk dipindahkan.

Dalam contoh kelima, metode anonim memanggil metode Shoot untuk objek bot . Berkat ini, bot akan menembakkan shell selama eksekusi perintah.

Sekarang kita telah membuat perintah, kita perlu mengaksesnya entah bagaimana ketika pengguna membuat input.

Untuk melakukan ini, BotInputHandler kode berikut dalam BotInputHandler , segera setelah perintah instances:

  public static BotCommand HandleInput() { if (Input.GetKeyDown(KeyCode.W)) { return MoveUp; } else if (Input.GetKeyDown(KeyCode.S)) { return MoveDown; } else if (Input.GetKeyDown(KeyCode.D)) { return MoveRight; } else if (Input.GetKeyDown(KeyCode.A)) { return MoveLeft; } else if (Input.GetKeyDown(KeyCode.F)) { return Shoot; } return null; } 

Metode HandleInput mengembalikan satu instance dari perintah tergantung pada tombol yang ditekan oleh pengguna. Simpan perubahan Anda sebelum melanjutkan.

Menerapkan perintah


Luar biasa, sekarang saatnya menggunakan tim yang kami buat. Buka RW / Script lagi dan buka skrip SceneManager di editor. Di kelas ini, Anda akan melihat tautan ke variabel uiManager dari tipe UIManager .

Kelas UIManager menyediakan metode pembantu yang berguna untuk UI terminal yang kami gunakan dalam adegan ini. Jika metode dari UIManager digunakan, maka tutorial akan menjelaskan apa yang dilakukannya, tetapi secara umum untuk tujuan kita, tidak perlu mengetahui struktur internalnya.

Selain itu, variabel bot mengacu pada komponen bot yang terpasang pada GameObject Bot .

Sekarang tambahkan kode berikut ke kelas SceneManager , ganti dengan komentar //1 :

  //1 private List<BotCommand> botCommands = new List<BotCommand>(); private Coroutine executeRoutine; //2 private void Update() { if (Input.GetKeyDown(KeyCode.Return)) { ExecuteCommands(); } else { CheckForBotCommands(); } } //3 private void CheckForBotCommands() { var botCommand = BotInputHandler.HandleInput(); if (botCommand != null && executeRoutine == null) { AddToCommands(botCommand); } } //4 private void AddToCommands(BotCommand botCommand) { botCommands.Add(botCommand); //5 uiManager.InsertNewText(botCommand.ToString()); } //6 private void ExecuteCommands() { if (executeRoutine != null) { return; } executeRoutine = StartCoroutine(ExecuteCommandsRoutine()); } private IEnumerator ExecuteCommandsRoutine() { Debug.Log("Executing..."); //7 uiManager.ResetScrollToTop(); //8 for (int i = 0, count = botCommands.Count; i < count; i++) { var command = botCommands[i]; command.Execute(bot); //9 uiManager.RemoveFirstTextLine(); yield return new WaitForSeconds(CommandPauseTime); } //10 botCommands.Clear(); bot.ResetToLastCheckpoint(); executeRoutine = null; } 

Wow, berapa banyak kodenya! Tapi jangan khawatir; kami akhirnya siap untuk peluncuran nyata pertama proyek di jendela Game.

Saya akan menjelaskan kodenya nanti. Ingatlah untuk menyimpan perubahan.

Menjalankan game untuk menguji templat Perintah


Jadi sekarang saatnya untuk membangun; Klik Mainkan di editor Persatuan.

Anda harus bisa memasukkan perintah pindah menggunakan tombol WASD . Untuk memasukkan perintah pemotretan, tekan tombol F. Untuk menjalankan perintah, tekan Enter .

Catatan : sampai proses eksekusi selesai, memasukkan perintah baru tidak mungkin.



Perhatikan bahwa garis ditambahkan ke UI terminal. Tim di UI ditandai dengan namanya. Ini dimungkinkan berkat variabel commandName .

Juga, perhatikan bagaimana UI bergulir sebelum eksekusi dan bagaimana garis dihapus selama eksekusi.

Kami mempelajari tim lebih dekat


Saatnya mempelajari kode yang kami tambahkan di bagian "Menerapkan Perintah":

  1. Daftar botCommands menyimpan tautan ke instance dari BotCommand . Ingat bahwa untuk menghemat memori, kita hanya dapat membuat lima instance perintah, tetapi mungkin ada beberapa referensi untuk satu perintah. Selain itu, variabel executeCoroutine merujuk ke ExecuteCommandsRoutine , yang mengontrol eksekusi perintah.
  2. Update memeriksa apakah pengguna telah menekan tombol Enter; jika demikian, ia memanggil ExecuteCommands , atau ExecuteCommands CheckForBotCommands .
  3. CheckForBotCommands menggunakan metode HandleInput statis dari BotInputHandler untuk memeriksa apakah pengguna telah menyelesaikan input, dan jika demikian, perintah dikembalikan . Perintah yang dikembalikan dilewatkan ke AddToCommands . Namun, jika perintah dijalankan, mis. jika executeRoutine bukan nol, maka itu akan kembali tanpa melewati apa pun ke AddToCommands . Artinya, pengguna harus menunggu sampai selesai.
  4. AddToCommands menambahkan tautan baru ke instance perintah yang botCommands di botCommands .
  5. Metode InsertNewText dari kelas InsertNewText menambahkan baris teks baru ke UI terminal. String teks adalah string yang diteruskan sebagai parameter input. Dalam hal ini, kami memberikan commandName ke commandName .
  6. Metode ExecuteCommandsRoutine meluncurkan ExecuteCommandsRoutine .
  7. ResetScrollToTop dari UIManager menggulirkan terminal UI ke atas. Ini dilakukan sesaat sebelum dimulainya eksekusi.
  8. ExecuteCommandsRoutine berisi perulangan for yang mengulangi perintah di dalam daftar botCommands dan mengeksekusi mereka satu per satu, meneruskan objek bot ke metode yang dikembalikan oleh properti Execute . Setelah setiap eksekusi, jeda ditambahkan dalam CommandPauseTime detik.
  9. Metode RemoveFirstTextLine dari UIManager menghapus baris teks pertama di UI terminal, jika ada. Yaitu, ketika sebuah perintah dieksekusi, namanya dihapus dari UI.
  10. Setelah semua perintah botCommands dihapus dan bot diatur ulang ke breakpoint terakhir menggunakan ResetToLastCheckpoint . Pada akhirnya, executeRoutine null dan pengguna dapat terus memasukkan perintah.

Menerapkan Fitur Undo dan Redo


Jalankan lagi pemandangan itu dan cobalah untuk sampai ke titik kontrol hijau.

Anda akan melihat bahwa sementara kami tidak dapat membatalkan perintah yang dimasukkan. Ini berarti bahwa jika Anda membuat kesalahan, Anda tidak dapat kembali sampai Anda menyelesaikan semua perintah yang Anda masukkan. Anda dapat memperbaikinya dengan menambahkan fitur Undo dan Redo .

Kembali ke SceneManager.cs dan tambahkan deklarasi variabel berikut segera setelah deklarasi Daftar untuk botCommands :

  private Stack<BotCommand> undoStack = new Stack<BotCommand>(); 

Variabel undoStack adalah tumpukan (dari keluarga Koleksi) yang akan menyimpan semua referensi ke perintah yang dapat dibatalkan.

Sekarang kita menambahkan dua metode UndoCommandEntry dan RedoCommandEntry yang akan mengeksekusi Undo dan Redo. Di kelas SceneManager , SceneManager kode berikut setelah ExecuteCommandsRoutine :

  private void UndoCommandEntry() { //1 if (executeRoutine != null || botCommands.Count == 0) { return; } undoStack.Push(botCommands[botCommands.Count - 1]); botCommands.RemoveAt(botCommands.Count - 1); //2 uiManager.RemoveLastTextLine(); } private void RedoCommandEntry() { //3 if (undoStack.Count == 0) { return; } var botCommand = undoStack.Pop(); AddToCommands(botCommand); } 

Mari kita menganalisis kodenya:

  1. Jika perintah dieksekusi atau daftar botCommands kosong, metode UndoCommandEntry apa UndoCommandEntry apa. Kalau tidak, itu menulis tautan ke perintah terakhir yang dimasukkan pada tumpukan undoStack . Ini juga menghapus tautan ke perintah dari daftar botCommands .
  2. Metode RemoveLastTextLine dari UIManager menghapus baris teks terakhir dari UI terminal sehingga UI cocok dengan konten botCommands .
  3. Jika tumpukan undoStack kosong, maka RedoCommandEntry tidak melakukan apa pun. Jika tidak, ia mengekstrak perintah terakhir dari atas undoStack dan menambahkannya kembali ke daftar AddToCommands menggunakan AddToCommands .

Sekarang kita akan menambahkan input keyboard untuk menggunakan fungsi-fungsi ini. Di dalam kelas SceneManager ganti tubuh metode Update dengan kode berikut:

  if (Input.GetKeyDown(KeyCode.Return)) { ExecuteCommands(); } else if (Input.GetKeyDown(KeyCode.U)) //1 { UndoCommandEntry(); } else if (Input.GetKeyDown(KeyCode.R)) //2 { RedoCommandEntry(); } else { CheckForBotCommands(); } 

  1. Ketika Anda menekan tombol U , metode UndoCommandEntry .
  2. Ketika Anda menekan tombol R , metode RedoCommandEntry .

Penanganan Case Tepi


Bagus, kita hampir selesai! Tetapi pertama-tama, kita perlu melakukan hal berikut:

  1. Saat memasukkan perintah baru, tumpukan undoStack harus dihapus.
  2. Sebelum menjalankan perintah, tumpukan undoStack harus dibersihkan.

Untuk mengimplementasikan ini, pertama-tama kita perlu menambahkan metode baru ke SceneManager . Masukkan metode berikut setelah CheckForBotCommands :

  private void AddNewCommand(BotCommand botCommand) { undoStack.Clear(); AddToCommands(botCommand); } 

Metode ini menghapus undoStack dan kemudian memanggil metode AddToCommands .

Sekarang ganti panggilan ke AddToCommands di dalam AddToCommands dengan kode berikut:

  AddNewCommand(botCommand); 

Kemudian masukkan baris berikut setelah if di dalam metode ExecuteCommands untuk menghapus sebelum menjalankan perintah undoStack :

  undoStack.Clear(); 

Dan akhirnya kita selesai!

Simpan pekerjaan Anda. Bangun proyek dan klik di editor Play . Masukkan perintah seperti sebelumnya. Tekan U untuk membatalkan perintah. Tekan R untuk mengulangi perintah yang dibatalkan.


Cobalah untuk sampai ke pos pemeriksaan hijau.

Ke mana harus pergi selanjutnya?


Untuk mempelajari lebih lanjut tentang pola desain yang digunakan dalam pemrograman game, saya sarankan Anda mempelajari Pola Pemrograman Game Robert Nystrom.

Untuk mempelajari lebih lanjut tentang teknik C # lanjutan, ambil kursus C # Collections, Lambdas, dan LINQ .

Tugas


Sebagai tugas, cobalah untuk sampai ke titik kontrol hijau di ujung labirin. Saya menyembunyikan salah satu solusi di bawah spoiler.

Solusi
  • pindah × 2
  • moveRight × 3
  • pindah × 2
  • moveLeft
  • tembak
  • moveLeft × 2
  • pindah × 2
  • moveLeft × 2
  • moveDown × 5
  • moveLeft
  • tembak
  • moveLeft
  • pindah × 3
  • menembak × 2
  • pindah × 5
  • moveRight × 3

Source: https://habr.com/ru/post/id463953/


All Articles