Command Design Pattern

Bu yazıma başlamadan önce, eğer okumadıysanız acilen okumanız gereken bir kaynak önermek istiyorum; “Robert Nystrom – Game Programming Patterns“. Bu kitapta, oyun dünyasındaki genel problemleri çözmek için kullanılan design pattern’lardan oldukça detaylı olarak bahsediliyor.

Senaryo

20×20 birimlik bir tabanın üzerinde, her seferinde bir birim olmak üzere aşağı, yukarı, sağa ve sola hareket edebilen bir küp düşünün. Bu kübün her hareketi, geri alınabilir olsun. Yani küp “yukarı, yukarı, sol, sol” hareketini tamamladıktan sonra, her “geri al” komutu verildiğinde, “sağ, sağ, aşağı, aşağı” komutlarını tekrarlayarak, başlangıç noktasına dönsün.

Adım 1 – Command Design Pattern’a Giriş

Command Design Pattern’da, isteklerimizi komut nesneleri olarak tutarız. Her bir komut nesnesinin, bir execute metodu bulunmak zorundadır. Bu execute methodu içine bir actor nesnesi yollanır. Böylece actor nesnesine ulaşılmış ve ona aksiyon aldırılmış olur. Örnek olarak aşağıdaki kod parçasını inceleyelim.

 

Örneğimizde, Execute metodu içine yollanan Actor nesnesini Jump metodunu çağıran bir komut nesnesi var.

Bu design pattern’a göre, komutları yaratan bir Creator nesnesi ve komutları başlatan bir Invoker nesnesi bulunmalıdır. Invoker ve Creator arasındaki ilişki konusunda bir çok söylem okudum ama, bana en mantıklısı ikisinin de birbirinden haberdar olmadan işleri gerçekleştirebildiği ilişkidir. Yani, Creator, komut nesnesini yarattağında bir EventManager’a haber vermeli, bu EventManager’ı dinleyen Invoker da kendi kurallarına göre bu yaratılan komutu çalıştırmalıdır.

Adım 2 – Sistemi Tasarlayalım

Uygulamamızdaki akışın tamamen bizim kontrolümüzde olması gerektiğini savunuyorum. Kontrolün bizde olmasıysa, Awake, Start ve Update gibi oyun motoruyla iletişim kurmamızı sağlayan ve ne zaman çağrılacakları gene Unity3D tarafından belirlenen metodların, AppManager gibi bir yönetici nesne tarafından oyunumuzun diğer ana bileşenlerine dağıtılmasıyla olacaktır. Bence bu yöntem, karmaşayı yönetmek penceresinden bakıldığında, en temiz yöntem.

Uygulamamızda motor tarafından çağrılan Awake, Start ve Update fonksiyonlarının, uygulamamızdaki MonoBehaviour’dan türemiş diğer bütün nesnelere dağıtılabilmesi için bir içinde yalnızca bu üç metodu barındıran bir BaseObject’ye ihtiyacımız var.

BaseObject içinde sadece AwakeObject, StartObject ve UpdateObject metodlarını bulunduran bir sınıf. Bundan türeyen bütün nesnelerin bu üç metodu dışardan bir yönetici, yani AppManager tarafından çağrılabilir.

BaseObject

Uygulamamızdaki akışı yönetecek olan AppManager sınıfını yazalım.

AppManager MonoBehaviour’dan türeyen bir sınıf. Yani sahnemizde bir nesneye ataç edilmiş olarak duruyor. Oyun başladığında, oyun motoru önce Awake, sonra Start fonksiyonlarını çağırıyor. Update fonksiyonu ise her frame’de çağrılıyor.

Uygulamamızda GameManager ve InputHandler isimli iki ana bileşen bulunuyor. Bu bişelenlerden GameManager, komut nesnelerimizi Invoke(başlatmak) edecek. Yani Invoker’ımız bu nesne. Input Handler ise komutlarımızı yaratacak, yani Creator’ımız da Input Handler.

Uygulamamızdaki event’ların dinlenebilmesi ve yollanabilmesi için bir ana bileşene daha ihtiyacımız var. O da AppEventManager… Bu örneğimizde AppEventManager’ın tek görevi, GameManager ve InputHandler’ın nasıl haberleşecekleri sorununa çözüm olmak.

AppEventManager bir Singleton nesne. Yani bu projede bu nesneden sadece bir tane bulunabilir. Singleton olması bu isterin sağlandığını gösteriyor. (Single’ton’ı neden böyle uyguladığımı serinin ilerleyen yazılarından Singleton Design Pattern’da anlatacağım.) Ancak merak edenler için John Skeet’in bu yazısını tavsiye ederim.

AppEventManager’ın CommandCreated metodu çağrıldığında, OnCommandCreated event’ını dinleyen bütün dinleyicilere bir komutun yaratıldığı bilgisi gönderilir.

AppEventManager’ın SendUndoReques metodu çağrıldığında, UndoButtonClicked event’ini dinleyen bütün dinleyicilere Undo butonuna tıklandığı bilgisi gönderilir.

Adım 3 – Command Nesnesi

Bu adımda da uygulayacağımız design pattern’ın ana bileşenlerinden olan Command nesnesini oluşturalım.

Command

Command sınıfı abstract bir sınıf. İçinde Abstract olan Execute metodu bulunuyor. Ve bu Execute metodu bir Actor nesnesi alıyor. Execute metodunun abstract olması, Command sınıfından türeyen bütün nesnelerin bu metodu barındıyor olması zorunluluğunu getiriyor. Undo metodu ise virtual bir metod. Virtual olmasının sebebi ise, Jump gibi geri alınamayan komutların da sistem tarafından kısıtlanmadan kolayca yaratılabilmesi.

Şimdi de bütün komutlarımızı oluşturalım; “MoveLeft, MoveRight, MoveUp, MoveDown ve Jump”.

MoveLeftCommand

MoveRightCommand

MoveUpCommand

MoveDownCommand

JumpCommand

 

Adım 4 – Inputları Alalım

Kullanıcıdan inputları InputHandler aracılığıyla alacağız.

InputHandler

InputHandler’ımızın contructor’ında uygulamamızda tanımlı komutlar oluşturuluyor.

InputHandler doğası gereği her frame’de kullanıcıdan input dinlemek zorunda. Yani InputHandler’ın HandleInput metodu her frame’de çağrılmalı. Bu işlevi de AppManager yerine getiriyor. InputHandler’ı oluşturuyor ve onun HandleInput metodunu her frame’de çağırıyor.

HandleInput metodu içinde dinlenen klavye tuşlarından birine basıldığında, ilgili komut AppEventManager’a bildiriliyor. AppEventManager’dan da onun dinleyicilerine bu bilgi ulaştırılıyor.

Adım 5 – GameManager

GameManager’ımızın bu uygulamadaki görevinin Command Design Pattern’daki Invoker olduğunu belirtmiştik. GameManager AppEventManager’daki OnCommandCreated ve UndoButtonClicked event’larını dinleyecek ve buna göre işlem yapacak.

GameManager

Komutları bir stack’te tutacağız. Çünkü stack kullanırsak Undo yani geri al işlevini kazandırmamız bir hayli kolaylaşacak.

AwakeObject metodu çağrıldığında GameManager AppEventManager’ın OnCommandCreated ve UndoButtonClicked eventlarına kaydolacak. Bu eventlar tetiklendiğinde, kendi içindeki CommandGetted ve Undo metodlarını çağıracak.

Nesnemizin OnDestroy metodu çağrıldığında, yani nesnemiz yıkıldığında, AppEventManager’da bulunan olay dinleme kayıtlarımızı sileceğiz.

StartObject metodu içinde Actor’ümüzün, yani Player’ımızın StartObject metodunu çağırıyor ve onu başlatıyoruz.

CommandGetted fonksiyonu tetiklendiğinde, yeni yaratılan komutu komut stack’ine ekliyoruz ve InvokeLastCommand metodumuzu çağırıyoruz. InvokeLastCommand metodumuz stack’e eklenen son command nesnesini içine bir actor nesnesi vererek çalıştırıyor.

Undo metodu tetiklendiğinde ise stack’teki geri alınacak ilk komutu stack’ten çıkarıyor ve içine actor’ümüzü vererek çalıştırıyoruz.

Adım 6 – Actor

Command’lerimizin vazgeçilmezi actor nesnemizi de oluşturduğumuza göre, çalışmamıza son verebiliriz.

Design Pattern ile ilgili olan çalışmalarımın Github sayfasına buradan ulaşabilirsiniz.

Şurası da şöyle olsa daha iyi olurdu demekten, bunu yorum olarak belirtmekten lütfen çekinmeyin. Bana cosgun.halil@gmail.com adresinden ulaşmak konusunda da rahat hissetmenizi rica ediyorum.

Eğer bu çalışmam işinize yaradıysa ve daha fazla çalışma yapabilmem için bana destek olmak isterseniz, bir kahvenizi içerim. 🙂

Saygılar.

Referanslar

gameprogrammingpatterns.com

csharpindepth.com

sourcemaking.com