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.
1 2 3 4 5 6 7 8 |
public interface IHero { public int X { get; set; } public int Y { get; set; } public IPositon Positon { get; set; } public Point RoomSize { get; set; } public void MoveHero(string moveList); } |
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.
1 2 3 4 5 6 7 8 |
public interface IPositon { public void Move(); public void Left(); public void Right(); public string CurrentPositon(); public IHero Hero { get; set; } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
using System; public class Program { public static void Main() { Hero hero = new Hero(); hero.X = 0; hero.Y = 0; hero.Position = "E"; hero.Move(); hero.Move(); hero.Position = "N"; hero.Move(); Console.WriteLine("Hero X:{0} Y:{1}",hero.X,hero.Y); } } public class Hero { public int X { get; set; } public int Y { get; set; } public string Position { get; set; } public void Move() { if (this.Position == "N") { this.Y += 1; } else if (this.Position == "S") { this.Y -= 1; } else if (this.Position == "E") { this.X += 1; } else if (this.Position == "W") { this.X -= 1; } } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class BasePositon { public IHero hero; public BasePositon(IHero _hero) { hero = _hero; } public PositonEnum GetCurrentPosition() { return hero.Positon switch { WestPositon => PositonEnum.W, NorthPositon => PositonEnum.N, EastPositon => PositonEnum.E, SouthPositon => PositonEnum.S, }; } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public enum PositonEnum { N = 1, E = 2, W = 3, S = 4 } public enum MoveEnum { M = 1, L = 2, R = 3 } public static class StringExtension { public static PositonEnum GetPositonEnum(this string positon) { return (PositonEnum)System.Enum.Parse(typeof(PositonEnum), positon); } public static MoveEnum GetMoveEnum(this char positon) { return (MoveEnum)System.Enum.Parse(typeof(MoveEnum), positon.ToString()); } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class NorthPositon : BasePositon, IPositon { public NorthPositon(IHero hero) : base(hero) { } public string CurrentPositon() { return this.GetCurrentPosition().ToString(); } public IHero Hero { get { return hero; } set { hero = value; } } public void Left() { base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.W); } public void Move() { base.hero.Y = hero.RoomSize.Y >= hero.Y + 1 ? hero.Y + 1 : hero.Y; } public void Right() { base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.E); } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class EastPositon : BasePositon, IPositon { public EastPositon(IHero hero) : base(hero) { } public string CurrentPositon() { return this.GetCurrentPosition().ToString(); } public IHero Hero { get { return hero; } set { hero = value; } } public void Left() { base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.N); } public void Move() { base.hero.X = hero.RoomSize.X >= hero.X + 1 ? hero.X + 1 : hero.X; } public void Right() { base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.S); } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class WestPositon : BasePositon, IPositon { public WestPositon(IHero hero) : base(hero) { } public string CurrentPositon() { return this.GetCurrentPosition().ToString(); } public IHero Hero { get { return hero; } set { hero = value; } } public void Left() { base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.S); } public void Move() { base.hero.X = hero.X - 1 >= 0 ? hero.X - 1 : hero.X; } public void Right() { base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.N); } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class SouthPositon : BasePositon, IPositon { public SouthPositon(IHero hero) : base(hero) { } public string CurrentPositon() { return this.GetCurrentPosition().ToString(); } public IHero Hero { get { return hero; } set { hero = value; } } public void Left() { base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.E); } public void Move() { base.hero.Y = hero.Y - 1 >= 0 ? hero.Y - 1 : hero.Y; } public void Right() { base.hero.Positon = PostionFactory.GeneratePositon(hero, PositonEnum.W); } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public static class PostionFactory { private static Dictionary<string, IPositon> PostionStorage = new Dictionary<string, IPositon>(); public static IPositon GeneratePositon(IHero hero, PositonEnum postionType) { if (PostionStorage.TryGetValue(postionType.ToString(), out var positon)) { positon.Hero = hero; return positon; } else { var newPositon = CreatePositon(postionType, hero); PostionStorage.Add(postionType.ToString(), newPositon); return newPositon; } } public static IPositon CreatePositon(PositonEnum postion, IHero hero) { return postion switch { PositonEnum.W => new WestPositon(hero), PositonEnum.N => new NorthPositon(hero), PositonEnum.E => new EastPositon(hero), PositonEnum.S => new SouthPositon(hero), }; } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
internal class Hero : IHero { private int x; private int y; private Point roomSize; private IPositon positon; public Hero(int _x, int _y, Point _roomSize) { X = _x; Y = _y; roomSize = _roomSize; } public int X { get => x; set => x = value; } public int Y { get => y; set => y = value; } public IPositon Positon { get => positon; set => positon = value; } public Point RoomSize { get => roomSize; set => roomSize = value; } public void MoveHero(string moveList) { moveList.ToList().ForEach(p => { switch (p.GetMoveEnum()) { case MoveEnum.M: Positon.Move(); break; case MoveEnum.R: Positon.Right(); break; case MoveEnum.L: Positon.Left(); break; default: throw new ArgumentException("Invalid enum value for command", p.ToString()); break; }; }); } |
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.
1 2 3 4 5 6 7 8 9 10 11 |
using AloneInTheRoom; using AloneInTheRoom.Positions; using System.Drawing; Point roomSize = new(5, 5); (int x, int y, Point roomSize) heroProperties = (1, 2, roomSize); var hero = new Hero(heroProperties.x, heroProperties.y, heroProperties.roomSize); hero.Positon = PostionFactory.CreatePositon(PositonEnum.N, hero); string moveList = "LMLMLMLMM"; hero.MoveHero(moveList); Console.WriteLine($"{hero.X},{hero.Y},{hero.Positon.CurrentPositon()}"); |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class HeroFactory { private int x; private int y; private Point roomSize; private PositonEnum positon; public HeroFactory() { } public IHero CreateRobot(Point _roomSize, int _x, int _y, PositonEnum _positon) { x = _x; y = _y; positon = _positon; roomSize = _roomSize; IHero hero = new Hero(x, y, roomSize); hero.Positon = PostionFactory.GeneratePositon(hero, positon); return hero; } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using AloneInTheRoom; using AloneInTheRoom.Positions; using System.Drawing; /*Point roomSize = new(5, 5); (int x, int y, Point roomSize) heroProperties = (1, 2, roomSize); var hero = new Hero(heroProperties.x, heroProperties.y, heroProperties.roomSize); hero.Positon = PostionFactory.CreatePositon(PositonEnum.N, hero); */ HeroFactory hf = new(); (int width, int height) roomSize = (5, 5); (int x, int y, string Positon) robotProperties = (1, 2, "N"); IHero hero = hf.CreateRobot(new(roomSize.width, roomSize.height), robotProperties.x, robotProperties.y, robotProperties.Positon.GetPositonEnum()); string moveList = "LMLMLMLMM"; hero.MoveHero(moveList); Console.WriteLine($"{hero.X},{hero.Y},{hero.Positon.CurrentPositon()}"); |
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.
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
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 :)
Ben tesekkur ederim Emre..