Bir Video Oyununda Odada Duran Bir Karakterin Hareketlerini Patternler ile Kodlama

Selamlar,

Bu makalede, bir bilgisayar oyununda odada bulunan bir karakterin hareketlerini ve buna bağlı pozisyonunu clean kod yazarak belirleyeceğiz.

Öncelikle, karakterin bir konumu, bir yönü ve odanın da sınırları bulunmaktadır. Karakterin bu örnekteki tek kabiliyeti, harket etme sadece (İleri Gitme) ve sağ, sola dönmesi olacaktır.

IHero.cs: Karakterin, fonksiyonları bir interface ile belirlenir.

  • X,Y: Robotun odadaki konumu.
  • IPosition: Robotun posizyonu, yön değiştirme özelliğinden dolayı, complex bir objectdir. Bu da onun Interface’i.
  • RoomSize: Odanın sınırları. Sonuçda, karakter odanın dışına çıkmamalıdır.
  • MoveHero(): Karakterin hareket edip, kordinatlarını değiştirdiği methoddur. Sadece ileri doğru hareket edecektir.

IPosition: Hero’nun güncel pozisyonunu gösterimi, değişen yönleri ile değişen pozisyon ve ileri doğru hareketi ile yönlere göre farklılık gösteren X,Y kordinat değişimi, burada setlenecektir.

  • Move(): İleri hareket.
  • Left(): Sola dönüş.
  • Right(): Sağ dönüş.
  • CurrentPosition(): Enum string bulunduğu konum.
  • IHero: Karakterin kendisidir. Değişen pozisyonlar ve kordinatlar referans ile tekrardan Hero’nun kendisine atanmaktadır.

Yönlere Göre Kordinatların Değişimi:

Şimdi şunun düşünülmesi gerekmektedir. Örneğin karakter Doğuya(E)’ya doğru yönlenmiş ise, Move() durumunda ‘X+1’ degeri artmaktadır. Yönü Kuzey (N) dönük ise, Move() durumunda ‘Y+1’ değeri artmaktadır.

Bu da bize, karakterin bulunduğu yöne göre hareketinin, farklı kordinat değişimlerine sebebiyet verdiğini göstermektedir. Bu noktada kodlar istendiğinde, geleneksel alt alta ‘If‘ ya da ‘Switch‘ koşullamaları ile aşağıdaki gibi yazılabilir.

Aşağıdaki kod tamamen Dummy olarak yazılmıştır. Kahramanın bulunduğu yöne doğru, ileri doğru hareketi neticesinde kordinatları değişmektedir. Örneğin ilerde, KuzeyDoğu veya GüneyBatı diye farklı yönler gelse, tekrardan bu if koşullarının içine girilip, yeni koşulların eklenmesi gerekmektedir.

Sonuç: “Hero X:2 Y:1

Simdi bunun için Strategy Pattern, gayet güzel bir çözümdür. Her yön için farklı classların yaratılması ve ortak özellikler için bir BaseClass’ın oluşturulması ilk yapılacak işlerdendir.

BasePosition: Aşağıda, her yön sınıfı için geçerli olan Hero’nun kendisinin referansı, Dependecy Injection ile Constructor’da alınmıştır. Ayrıca geçerli yönün, Sınıf Tiplerine göre Enum karşılığı, dönülen ‘GetCurrentPosition()’ methodu ile tanımlanmıştır. Kısaca her gidilebilecek yön için, ayrı bir sınıf tanımlanmıştır. BasePosition’ın esas görevi, Constructor’da kendisinin yani ‘hero‘ değişkenini alınmasıdır. Böylece, yön veya kordinat değiştiği zaman, bunu referance parametresi olarak kullanıp, doğrudan kendisine atayabilmektedir.

PositionEnum.cs: Aşağıda görüldüğü gibi, yönler ve hareketler için enumlar tanımlanmıştır. Ayrıca StringExtension() ile, string bir veriden Position ve Move Enum değerleri dönen, static methodlar tanımlanmıştır.

Ben bu makalede yönlerden Doğu, Batı, Kuzey  ve Güney tanımlanacaktır. Ara yönler (KuzeyDoğu-GüneyBatı) gibi size ödev olsun. Tüm yönler için ayrı sınıfların yazılmasındaki esas amaç, hepsinin kendine göre bussineslarının olması ve ‘if’, ‘else’ gibi koşullardan kurtulmaktır.

NorthPosition: Aşağıda görüldüğü gibi, BasePosition sınıfı ve IPosition interface’inden türemiştir. Aşağıdaki örnekde Right(), Left(), Move() methodlarının Kuzey yönü durumundaki bussinesları yazılmıştır. Constructor’da hero yani kendisini, referance parameter olarak alınmaktadır.

  • Move(): ‘base.hero.Y = hero.RoomSize.Y >= hero.Y + 1 ? hero.Y + 1 : hero.Y‘: Bu satırda, Hero kuzeye dönük olduğu için, ileri hareketinde Y ekseninde +1 artım olmaktadır. Bu işlem yapılırken, Odanın  boyutlarına bakılmış ve odanın kordinatlarından dışarıya çıkılması engellenmiştir.
  • Left(): ‘base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.W)‘: Hero kuzeye bakarken sola dönüşte, artık kahramanın yeni yönü Batı(W)’dır. PositionFactory(), makalenin devamında detaylıca anlatılacaktır. Yeni yön değeri, hero yani kendi Postion parametresine atanmaktadır.
  • Right(): ‘base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.E)‘: Hero kuzeye bakarken sağ dönüşte, artık kahramanın yeni yönü Doğu(E)’dur.

EastPosition: EastPosition da diğer yönler gibi, BasePosition sınıfı ve IPosition interface’inden türemiştir.

  • Move(): ‘base.hero.X = hero.RoomSize.X >= hero.X + 1 ? hero.X + 1 : hero.X‘ : İleri doğru hareketde, X ekseninde +1 artım olmaktadır. Bu işlem yapılırken, Odanın  boyutlarına bakılmış ve odanın kordinatlarından dışarıya çıkılması engellenmiştir.
  • Left(): ‘base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.N)‘ : Hero doğuya bakarken sola dönüşte, artık kahramanın yeni yönü Kuzeye(N)’dır.Yeni yön değeri, ‘hero‘ yani kendi Postion parametresine atanmaktadır.
  • Right(): ‘base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.S)‘: Hero doğuya bakarken sağ dönüşte, artık kahramanın yeni yönü Güney(S)’dir.

WestPosition: WestPositin’da diğer yönler gibi BasePosition sınıfı ve IPosition interface’inden türemiştir.

  • Move(): ‘base.hero.X = hero.X – 1 >= 0 ? hero.X – 1 : hero.X‘ : İleri doğru hareketde, X ekseninde -1 olarak azalmaktadır. Bu işlem yapılırken, X kordinatının 0’dan küçük olması engellenmiş. Böylece, Hero’nun odadan dışarıya çıkmasına izin verilmemiştir.
  • Left(): ‘base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.S)‘ : Hero batıya bakarken sola dönüşte, artık kahramanın yeni yönü Güney(S)’dir.Yeni yön değeri, hero yani kendi Postion parametresine atanmaktadır.
  • Right(): ‘base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.N)‘: Hero batıya bakarken sağ dönüşte, artık kahramanın yeni yönü Kuzey(N)’dir.

SouthPosition: SouthPositin’da diğer yönler gibi BasePosition sınıfı ve IPosition interface’inden türemiştir.

  • Move(): ‘base.hero.Y = hero.Y – 1 >= 0 ? hero.Y – 1 : hero.Y‘ : İleri doğru hareketde, Y ekseninde -1 olarak azalmaktadır. Bu işlem yapılırken, Y kordinatının 0’dan küçük olması engellenmiş, böylece Hero’nun odadan dışarıya çıkması engellenmiştir.
  • Left(): ‘base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.E)‘ : Hero güneye bakarken sola dönüşte, artık kahramanın yeni yönü Doğu(E)’dir.Yeni yön değeri, hero yani kendi Postion parametresine atanmaktadır.
  • Right(): ‘base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.W)‘: Hero günye bakarken sağ dönüşte, artık kahramanın yeni yönü Batı(W)’dir.

Buraya kadar Strategy pattern kullanarak, Pozisyonlara göre tüm hareketlerin(Move, Left, Right) bussineslarını yazdık.

PostionFactory: Bir süre sonra fark edeceksiniz ki, Left() veya Right() hareketinde yön değişmekde ve her seferinde yeni bir Position sınıfı türetilmek de, bu da Memory Allocationa büyük bir yük olmaktadır.

  • GeneratePositon(): Peki ne yapalım ? Her seferinde yeni bir sınıf üretmektense Singleton Pattern kullanarak yeni üretilen bir Position Sınıfı, static bir dictionary’e koyalım. Tekrardan ihtiyacımız olduğunda, yeni yaratmak yerine buradan istenen sınıfı verelim.
  • CreatePositon():  Bu sefer de Enum olarak gelen yönleri, IPositon tipinde Dictioanry içinde yok ise, yaratıp geri dönüyoruz.

Hero: İlgili hero sınıfı aşağıda görüldüğü gibi, odadaki lokasyonu Kordinat(X,Y) bilgisi, Position yani hangi yöne baktığı, odanın sınırları RoomSize propertyleri tanımlanmıştır.

Son olarak MoveHero() methodu ile gelen string komuta göre bulunduğu pozisyondaki Hareket Methodu( Move(), Left(), Right() ) çağrılmıştır. Hareketten sonra, gerekir ise yönü ve odadaki konumu yani kordinatları değişmektedir.

Program.cs: Aşağıda görüldüğü gibi, kahramının hareket listesi (“LMLMLMLMM”) toplu olarak verilmiş ve hiçbir if – else koşulu kullanılmadan bulunduğu kordinat ve en son yönü ekrana basılmıştır. Aslında burada, herbir koşul için ayrı bir sınıf yaratılmıştır. Bu durumda, kod bir parça uzatılmış olsa da, örneğin yeni bir yön eklendiği zaman, (Kuzey-Batı), bu durumda bu case’in koşullara yeni bir sınıf olarak eklenmesi, son derece kolay ve hızlı olacaktır. Ayrıca hareket tiplerine örneğin ‘Turn180Left()’ veya ‘Turn180Right()’ gibi algoritmaları tüm yönlere eklemek gene çok kolay ve hızlı olacaktır. Bu da bize kod okunaklığını arttıracak, test süreçlerini kolaylaştıracak, debug ve hatanın bulunmasını hızlandıracak ve son olarak devops süreçlerini dağıtık yapı ile destekleyecektir.

Sonuc Ekrani:

Peki diyelim ki oyunda birden fazla karakter olacak ya da, bizi kovalayan birden fazla düşman olacak. Onların da aynı hareketlere sahip olduğu farz edilir ise, bu durumda bu karakterlerin oluşturulmasi için bir factorye ihtiyaç duyulacaktır. Tüm karakterlerin propertylerinin tek tek manuel setlenmesi hem vakit alacak, hem de kod okunaklığını bozacaktır.

HeroFactory: Aşağıda görüldüğü gibi HeroFactory sınıfının Constructor’ında, oda boyutu, bulunduğu kordinatlar ve yönü parametre olarak alınmıştır. Daha sonra ilgili paramtereler yaratılacak hero için set edilip geriye dönülmüştür.

Program.cs(New): Aşağıda görüldüğü gibi Hero, manuel yaratılması yerine HeroFactory sınıfı kullanılarak yaratılmıştır. Esas amaç, birden fazla karakterin yaratılması gerektiğinde, kod kalabılığından kurtulunmuş ve işleri daha kısa bir yöntem ile halledilmiştir.

Aslına bakılırsa, birbirine benzer özellikde Herolar ya da düşmanlar yaratılacak ise, Prototype Design Pattern kullanılıp, Hero’dan bir tane yaratılıp diğerlerinin ondan Clone’u alınarak yaratılıp, kendine özgü özelliklerinin güncellenmesi sağlanabilir.

chain

Yukaridaki koda dikkat ederseniz, “LMLMLMLMM” komut katarinda, Hero’nun yönü birçok kere değişmektedir. Her yön değişimi demek, farklı bir bussines kuralı demektir. Bu bussines geçişleri aynı “The Chain of Responsibility Pattern“‘de olduğu gibi kendinden sonra çalışacak sınıfın, bir property ile atanması ile yapılmaktadır. Hero’nun Position property’sine, değişen yöne göre farklı sınıflar atanmakta, böylece ‘if-switch’ gibi koşullardan kurtulunup atanan sınıfın bussinise duruma göre çalıştırılması sağlanmaktadır. Position property’sinin beklediği sınıfın, ‘IPositon‘ interface’inden türemesinin beklenmesi, tüm yön sınıflarının istenen bu IPosition interfaceden türemesi ve hepsinin ortak ‘Move(), Left(), Right()‘ methodlarına sahip olması, akla hemen strategy design pattern’i getirmektedir.

Dikkat ederseniz, bir sorunu çözmek için birden fazla Design Pattern bir araya getirilip, kod okunaklığını arttırmak, genişlemeye açık değişikliğe kapalı, test edilebilir ve yönetilebilir bir yapı oluşturmak amacı ile kullanılmış ve böylece OOP ilkelerine uyulmaya çalışılmıştır.

Geldik bir makalenin daha sonuna, yeni bir makalede görüşmek üzere hepinize hoşçakalın.

Github Source: https://github.com/borakasmer/AloneInTheRoom

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

2 Cevaplar

  1. emre dedi ki:

    Yeni başlayanlar için fikir verici ama keşke gerçekte bu kadar basit olsa :)

    *Daha çok behaviour tree yaklaşımları gerekiyor. Move için bile öncelikte hareket vektörü yönünde ışın (ray) atılıp herhangi bir engel ile collision var mı yok mu tespit edilmeli.
    *Dünyanın merkezine doğru ışın parçası gönderilip karakter havada olup olmadığı tespit edilmeli.
    *Eğerki daha gerçekçi simülasyon yapılıyorsa zemin ile arasındaki sürtünme kuvveti hesaplanıp, rigidbody üzerinde hareket yönüne ters kuvvet etki ettirilmeli.
    *Tüm bunlar yapıldıktan sonra animator controller ile idle konumundan move state geçirtilip animasyon oynamaya başlamalı, durduğunda ise move durumundan idle durumuna geçmeli. Tabi daha doğal olması için karakterin ivme vektörü hesaplanılıp easing (yumuşatma) uygulanmalı.
    *Yürümeye başladığıyla ilgili gerekli mesaj message broker üzerinde yayınlamalı ki, audio controller bu mesajı dinleyip uygun sesi çalsın. Veya bu mesaj ile ilgili kim ne yapacaksa artık.

    Temelde aklıma gelen ve sürekli yapılması gerekenler bunlar. Keyifli bir yazıydı teşekkürler Bora hocam :)

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.