Merhaba arkadaşlar,
Bu yazımızda, XNA ’de oyun sahnesine bir nesne nasıl yerleştirilir, yerleştirdiğimiz bu nesneyi nasıl hareket ettiririz ve collision dediğimiz çarpışma kavramının nasıl işlediğine bakacağız. Özellikle collision kavramı oyun geliştirme evresinde, ne kadar basit düşünmemiz gerektiğini anlamamıza yardımcı olacak güzel bir detay. Ben bunu, oyun geliştirme sürecinin HelloWorld uygulaması olarak tanımlıyorum.
Öncelikle Terimler
Konuya girmeden önce bazı terimlere değinmek istiyorum ki ilerde bunlara rastladıgımız zaman yabancılık çekmeyelim.
Sprite : 2 boyutlu olan imajlara genel olarak sprite diyoruz.
Texture : 3 boyutlu bir modeli giydirmek icin kullanılan, 2 boyutlu imajlarada texture diyoruz.
Tiles : Küçük imajların birleşip büyük bir imaj haline gelmesiyle oluşan yapı. Mesela bir çöl zemini için çok büyük bir çöl imajı hazırlamak yerine, çok daha küçük bir imaj hazırlayıp bunu yan yana ve altalta dizerek bir bütün şeklinde gösterilmesiyle oluşan yapı.Genelde zemin, bir evin duvarı veya gök yüzü gibi yapılar bu şekilde oluşturulur.
Herşey Kodların Ekrana Yansımasıdır...
Konuyu bu şekilde bir tek cümle ile özetlemekle beraber dilerseniz arka planda bu yapı nasıl işliyor bir bakalım. Herşey kodların ekranda yansıması dedik, peki bu yansımaların ekranın neresinde olacağını nasıl kontrol ediyoruz, asıl sorulması gereken soru budur. İşte burada da biraz monitörümüzü, çözünürlük ve piksel kavramlarını incelemek gerekiyor. Şimdi isterseniz bunlar hakkında ve 2D oyun dünyasıyla ilgili bazı detaylara bakalım.
2D bir dünya üzerinde çalışacagımız için bize 2 tane boyut gerekiyor, onlarda hepimizin lisede gördüğü koordinat sistemi üzerindeki x ve y’den başka bir şey değildir. Yani bizim ekran üzerinde oluşturdugumuz herşeyin pozisyonu bu x ve y ’ler ile belirlenebiliyor. Aynı zamanda nesnelerimizin boyutlarıda, yani genişliği ve yüksekliğide bu x ve y degerleriyle belirleniyor. Burada hemen bir detaydan bahsetmek gerekiyor, klasik koordinat sisteminin orjini, tam ortadadır. Yani aşağıdaki resim 1’de ki klasik koordinat sisteminin xy(0,0) noktası düzlemin tam ortasıdır. Fakat ekran düzleminde bu nokta ekranın sol üst köşesini ifade eden yerdir. Bu durumda şöyle birşey söylenebilir, "ekran düzlemin de, x veya y için negatif değerler söz konusu değildir." Bu da birazda olsa işlerin karmaşıklaşmasını engelliyor.En azından negatif sayılar ile uğraşmış olmuyoruz.
Bu düzlemler kagıt üzerinde teorik olarak sonuza kadar gidebilir. Fakat biz sonlu bir ekranda çalışıyoruz. Yani kullandıgımız ekranların belli bir genişlik ve yükseklik değerleri vardır. Örneğin benim laptopum boyutu 15.4" ’tir ve bu sabittir, hiç bir şekilde değişmez. Fakat benim ekranımın aynı zamanda desteklediği bazı çözünürlük değerleride vardır ve benim kullandıgım çözünürlük değeri 1280x800 ’dür. Yani ben bu çözünürlük değeriyle, ekranda 1280 pixel genişliğinde ve 800 pixel yüksekliğinde bir kullanım alanına sahip oluyorum. Aynı zamanda benim ekranım 800x600 değerinde bir çözünürlüğüde destekliyor. Ozaman ne oluyor, 800px genişliğe ve 600px de yüksekliğe sahip oluyorum. İşte bizim ekran üzerindeki koordinat sistemimizin sınırlarıda bu çözünürlük degerlerine baglıdır. Eğer biz oyunumuzu geliştirirken çözünürlük ayarlarını 800x600 olarak ayarlarsak, 801.pixel artık ekran dısında bir nokta olacaktır ve bu noktada olan bir nesnenin o bölümü görünmeyecektir.
Bu bilgiler ışığında, kendimize nesneler nasıl hareket ediyor diye sordugumuz zaman vereceğimiz yanıt artık cok daha anlaşılır olacaktır sanırım. Eğer nesneyi x eksini boyunca ilerletmek, yani sağa dogru hareket ettirmek istiyorsak ozaman, nesnemizin x koordinatına daha büyük bir değer vermek yeterli olacaktır. Aynı şekilde sola dogru gitmesini istiyorsak x koordinatına daha kücük bir değer vermemiz gerekiyor. Bu x ve y değerlerini nesnemizi olusturan sınıfın property’ leri olarak düşünebilirsiniz. Nitekim birazdan böyle bir örnek yapacagız.
Hello World
Nesnelerin ekranda teorik olarak nerede gösterileceğini ve nasıl hareket edeceğini gördüğümüze göre artık XNA ’de ilk uygulamamızı geliştirmeye başlayabiliriz. Bu uygulamamızda ilk olarak ekrana bir imaj yerleştirip, ardından bunu kontrolümüz dahilinde hareket ettireceğiz. Son olarakta ikinci bir imaj ekleyip bunların hareket ederken çarpışıp çarpışmadığını kontrol edeceğiz.
İlk olarak Visual Studio’u açalım ve File>NewProject menüsünden Windows Game (3.0) şablonunu seçip yeni bir proje yaratalım. Karşımıza önceki makalelerimizde incelediğimiz Game1.cs sınıfı gelecek. Dilerseniz default olarak gelen yorum satırlarını silebilirsiniz.
Şimdi isterseniz uygulamada kullanacağımız sprite’ları kolay bir şekilde yönetebilmek için Content Projesine, sprite’ları saklayabileceğimiz, Sprites adından bir klasör ekleyelim. Bunun için Content projesine sağ tıklayıp Add>NewFolder diyip adını vermemiz yeterli. Ardından da bu klasör içine uygulamada kullanacağımız imajları koyacağız. Bunu yapmak için resimleri sürükle bırak yöntemiyle Sprites klasörü içine atabilirsiniz.Ayrıca örnek resimleri aşağıdan kopyalayıp kullanabilirsiniz.
Şimdi en basit haliyle sadece bir imajı ekranda nasıl gösteririz buna değinmek istiyorum, ardından da bunu daha detaylı bir şekilde, Object Oriented tabanlı nasıl yaparız onu göstermek istiyorum.İlerleyen yazılarda zaten bu şekilde kod yazacağız. O yüzden konuyu daha kolay anlamak ve daha modüler kodlar yazabilmeniz için azda olsa bir OOP bilginiz olması gerekiyor.
Şimdi öncelikle ekrana yerleştireceğimiz şey bir imaj, diğer bir deyişle bir resim. Peki biz bu tarz 2D resim dosyalarına ne diyorduk? Texture veya Sprite. XNA ’de bu tarz dosyaları tutmak için Texture2D adında özelleştirilmiş bir sınıf geliştirilmiştir. Benimde öncelikle yapmam gerekende bu tipte bir değişken tanımlamak. Ozaman aşağıdaki kodu class içinde global olarak yazalım isterseniz.
Artık elimde 2D Texture’leri tutacak bir değişken mevcut. Bu değişkenede imajımızı nasıl yükleyeceğimizi görebilmek için hemen LoadContent methodu içine aynen aşagıdaki kodu yazıyorum.
sprite1 = Content.Load("Sprites/sprite1"); |
Burada, parametre olarak verdiğmiz değerde, neden resmin uzantısını belirtmedik diye düşünebilirsiniz. Bunun nedeni parametre verdiğimiz değerin aslında resmin adresinden oluşmuyor olmasıdır. Solution Explorer’dan resmi seçip, properties panelinden özelliklerine bakarsanız eğer aşağıdaki benzer bir görüntü ile karşılaşırsınız.
Burada önemli olan AssetName isimli özelliktir. Bunu klasik Name özelliğine benzetebilirsiniz. İşte bizim parametremizdeki değerde bu isimdir. Bir diğer önemli nokta ise ContentImporter ve ContentProcessor özellikleridir.Bu özelliklerde XNA ’in desteklediği çeşitli formatlar vardır. Biz projeye bir resim ekleyip, bunun ContentImporter özelliğini Mp3 Audio File yaparsak hata alırız tabiki. Visual Studio zaten dosya formatını ayırt edip otomatik olarak bu özelliği o dosya için uygun olacak şekilde belirliyor.Bunun dışında kendi Importer ve Processor tiplerimizide yazabiliriz. Buna ilerleyen derslerde ayrıca değinmeyi düşünüyorum.
Kodumuza geri dönersek eğer, anlaşılabilirliği arttırmak amacıyla, sprite1 adındaki değişkenimize imajımız atanmış diyebiliriz. Şu anda uygulamayı F5 ile çalıştırırsanız ekranda hiçbir şey göremezsiniz. Aslında imajımız yüklenmiştir ama henüz bunu ekrana çizdirmediğimiz için göremiyoruzdur. Peki çizim işlemlerini nerede yapıyorduk? Draw methodunda. Hatırlarsanız daha öncede SpriteBatch nesnesini tanıtırken onun hakkında, çeşitli nesnelerin ekrana çizilmesinden sorumludur demiştik. Ozaman biz bu sorumluluğu önce başlatıp, sonra imajı çizdirip, en sonunda da bu sorumluluğu sonlandırıyoruz. Bunu yapabilmek içinde aşağıdaki kodları Draw methodu içine yazmanız yeterlidir.
spriteBatch.Begin();
spriteBatch.Draw(sprite1, new Vector2(10, 10), Color.White);
spriteBatch.End(); |
Bu üç satır kodu kısaca özetlemek gerekise; bir çizim yapılacagı zaman bunu spriteBatch’e Begin methodu ile bildirmek gerekiyor. Ardından çizme işleminin gerçekleşmesi için spriteBatch’in Draw methodunu çağırmak gerekiyor. Draw methodunun çeşitli override versiyonları bulunmaktadır. Biz burada 3 parametre alacak şekilde kullandık. 1.parametre çizilecek olan texture’ ü gösteriyor, 2.parametre nesnenin ekranda nereye çizileceğini gösteriyorki burada yukardan ve soldan 10 ’ar pixel boşluk bırakarak çiziliyor, 3.parametre olarakta renk değeri veriliyor.Şu anda rengin bir geçerliliği yok. Son olarakta spriteBatch’e End methodu ile çizim işlemini sonlandırması gerektiğini söylüyoruz, işte bu kadar. Artık F5 ile uygulamayı çalıştırdıgımızda, texture’ ümüzü oyun sahnesinin en üst sol köşesinde görebiliriz.
İlk uygulamasını yapacak arkadaşların uygulamayı çalıştıramama ihtimallerine karşılık uygulamanın son halini buraya yükledim.
Şimdi bu örneğin sadece en kısa sürede somut birşeyler gösterebilmek için yapıldıgını söylemekte fayda var. Yoksa sizde takdir edersinizki binlerce nesnenin yer alacağı gerçek uygulamalarda bu tarz bir kodlama ile işin içinden çıkılması imkansızdır. Ayrıca burada sprite1’ imizin sahip oldugu herhangi bir özellik veya yetenekte yoktur. Şimdi dilerseniz yaptığımız bu işlemi bir adım daha ileri götürelim ve basitte olsa OOP ’den faydalanmaya başlayalım.
Kendi Sınıfımızı Geliştirelim
İsterseniz yazdığımız kodları silelim veya yeni bir proje acalım. Eğer yeni bir proje açtıysak, content altında bir Sprites klasörü oluşturup, resimleri buraya koymayı unutmayalım.
Şimdi bizim sahne üzerindeki nesnelerimizi temsil edecek basit bir sınıf yazacağız. Bu sınıfın bazı özellikleri olacak elbette. Bunlar ekranda hangi texture’ ün gösterileceğini belirten, imajın boyutunu ve ekrandaki pozisyonunu belirten özellikler olacaktır. Ozaman bu sınıfı oluşturmak için projemize yeni bir class ekleyelim ve adınıda myGameObject olarak verelim.
Class’ımızı oluşturduktan sonra namespacelerimizi eklememiz gerekiyor. Tek tek yazmak yerine Game1.cs dosyamızı acıp, hepsini kullanmıyor olsakta, oradaki tüm using deyimlerini kopyalamanız yeterlidir.Yani bunları;
using System;
using System.Collections.Generic;
using System.Linq; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage; |
Hemen ardından az önce belirttiğim özellikleri tutacak değişkenlerimizi tanımlayalım. Ve nesnemizi oluştururken rahat bir şekilde değer atayabilmek için default constructor’ ımızı 3 parametre alacak şekilde overload edelim.
public Texture2D image;
public Vector2 size;
public Vector2 position;
public myGameObject(Texture2D pImage, Vector2 pSize, Vector2 pPosition)
{
this.image = pImage;
this.size = pSize;
this.position = pPosition;
} |
Böylece uygulamamızda kullanacağımız sınıfı hazırladık. Şimdi game1.cs dosyamıza geri dönelim ve global olacak şekilde aşağıdaki gibi bu sınıftan bir nesne tanımlayalım.
myGameObject gameObject1; |
Sonra bu nesnemizi LoadContent methodu içinde oluşturalım.
gameObject1 = new myGameObject(Content.Load("Sprites/sprite1"),
new Vector2(70f, 80f),
new Vector2(10f, 10f)); |
Nesnemizi oluştururken, ilk parametre ile hangi texture’ün yükleneceğini belirtiyoruz, 2.parametre ile nesnemizin boyutlarını ve 3.parametre ile de nesnenin ekranda nerede görüneceğini belirtiyoruz.
Son olarak bu nesneyi Draw methodu içinde ekrana çizdiriyoruz.
spriteBatch.Begin();
spriteBatch.Draw(gameObject1.texture, gameObject1.position, Color.White);
spriteBatch.End();
|
Şimdi F5 ile uygulamayı çalıştırırsanız sonucu görebilirsiniz. Kısaca özetlemek gerekirse artık bizim bir çok imajı, nesne olarak ele alabileceğimiz, onun belli başlı 3 özelliğine erişebileceğimiz bir sınıfımız var. Bir başka deyişle, artık GameObject sınıfı sayesinde daha modüler bir programlama yapabiliriz. Şimdi isterseniz bu nesnemize nasıl hareket kazandırırız buna bir bakalım.
Ekrandaki Nesneyi Hareket Ettirmek
Artık oyun sahnemizde bir nesnemiz var. Ama bunu yapmakla işimiz bitmiyor tabiki. Şimdi bu nesneye hareket kazandırmalıyız. Önce işin arka plandaki mantığını özetlemek istiyorum. Bir cismin ekranda hareket etmesi için gereken en basit şey bir tuşa basılmasıdır. Peki biz herhangi bir tuşa basıldıgını nasıl anlayacağız, tabiki sürekli kontrol ederek. Bir işlem sürekli kontrol ediliyorsa bu kontrolü yapacak kodlar nereye yazılacaktı ? Update methodu içine tabiki. Mesela ben W tuşuna basınca ekrandaki nesnenin yukarı hareket etmesini istiyorum, ozaman ne yapıyorduk, bu nesnenin Y pozisyonunun sahip oldugu değeri arttırıyorduk. Sağa gitmesi içinde X değerini yükseltiyorduk. Şimdi somut örneği görmek için aşağıdaki kodları aynen Update methodu içine yazalım isterseniz, ardından da kısaca kodlarımızı inceleyeceğiz.
KeyboardState ks = Keyboard.GetState();
if (ks.IsKeyDown(Keys.W))
{
gameObject1.position.Y -= 5;
}
if (ks.IsKeyDown(Keys.S))
{
gameObject1.position.Y += 5;
}
if (ks.IsKeyDown(Keys.A))
{
gameObject1.position.X -= 5;
}
if (ks.IsKeyDown(Keys.D))
{
gameObject1.position.X += 5;
} |
Öncelikle KeyboardState türünden bir değişken tanımlıyoruz ve buna Keyboard sınıfının GetState() methodu ile bir değer atıyoruz. Keyboardstate o anda klavye ile ilgili bir durumu tutar. Burada yapılan işlemde şudur, GetState() ile o anki durumu ks ’ye atıyoruz ardından IF ’ler ile bu durumu kontrol ediyoruz. ks.IsKeyDown() ’a Keys tipinden bir enumerator yardımıyla parametre veriyoruz. Ve diyoruzki, tuşlardan W’ya basıldı mı, eğer basıldıysa, gameObject1’in yukarı dogru hareket etmesi için, position.Y değerini, sahip oldugu değerden 5 azaltıyoruz. Buda nesnenin 5 pixel yukarı doğru hareket etmesini sağlıyor. Diğer IF satırlarıda aynı mantıkla çalışıyor tabiki.
Gördüğünüz gibi ekrandaki bir nesneyi hareket ettirmek en basit haliyle bu şekilde yapılabilir. Şimdi ikinci bir nesne ekleyip bunların çarpışıp çarpışmadığını kontrol etmemiz gerekiyor.
Collision (Çarpışma) Kontrolü
Öncelikle aynı mantıkla, bu sefer sprite2 isimli texture’ ümüzü ekrana çizdirelim. Bunu bu sefer ekranın sağ alt köşesine koyalım ama.
Class içinde global olarak;
myGameObject gameObject2; |
LoadContent içinde;
gameObject2 = new myGameObject(Content.Load("Sprites/sprite2"),
new Vector2(70f, 80f),
new Vector2((graphics.PreferredBackBufferWidth-70f),(graphics.PreferredBackBufferHeight-80f))); |
Burada farklı bir kod göze çarpıyor.
graphics.PrefferedBackBufferHeight
graphics.PrefferedBackBufferWidth |
Bu property ’ler bize, oyun sahnesinin genişlik ve yükseklik değerlerini verir. Bunları neden kullanıyoruz diye düşünebilirsiniz. Bunun nedeni, ikinci nesnemizin ekranın sağ alt köşesinde göstermektir. Ekranın bu genişlik ve yükseklik değerlerinden, nesenin genişlik(70) ve yükseklik(80) değerlerini cıkartıp, bunu nesnemizin position özelliğine parametre olarak verirseniz istediğimiz şeyi gerçekleştirmiş oluruz. Şimdi ikinci nesnemizi ekrana çizdirmek için Draw methodu içine gerekenleri yazalım isterseniz.
Draw içinde;
spriteBatch.Begin();
spriteBatch.Draw(gameObject1.texture, gameObject1.position, Color.White);
spriteBatch.Draw(gameObject2.texture, gameObject2.position, Color.White);
spriteBatch.End();
|
Artık uygulamayı bu hali ile çalıştırırsanız aşağıdaki gibi bir çıktı alırsınız.
Şimdi hemen bu ikinci eklediğimiz nesneyede hareket özelliği kazandıralım isterseniz. Ozaman bunun için aşağıdaki kodları yukarda yazdıgımız update methodu içindeki kodların ardına yazmamız yeterlidir.Bu seferki kontrol tuşlarımız ok tuşları olacak.
if (ks.IsKeyDown(Keys.Up))
{
gameObject2.position.Y -= 5;
}
if (ks.IsKeyDown(Keys.Down))
{
gameObject2.position.Y += 5;
}
if (ks.IsKeyDown(Keys.Left))
{
gameObject2.position.X -= 5;
}
if (ks.IsKeyDown(Keys.Right))
{
gameObject2.position.X += 5;
} |
Artık elimizde 2 farklı kişi tarafından kontrol edilebilen 2 nesne var. Uygulamanın son halini buradan indirebilirsiniz.
Şimdi isterseniz çarpışma kontrolünün 2 boyutlu bir ekranda nasıl gerçekleştiğini daha rahat anlayabilmek için aşağıdaki resmi inceleyelim.
Şekilde görüldüğü gibi bu böyle bir çarpışmanın algoritması şu şekilde özetlenebilir;
beyaz cismin X pozisyonu + genişliğinin toplamı, gri cismin X pozisyonundan büyükse ve
beyaz cismin X pozisyonu, gri cismin X pozisyonu + genişliği, toplamından küçükse ve
beyaz cismin Y pozisyonu + yüksekliği, gri cismin Y pozisyonundan büyükse ve
beyaz cismin Y pozisyonu, gri cismin Y pozisyonu + yüksekliğinden küçükse eğer
bunlar çarpışmıştır.
değilse
çarpışmamıştır.
Şimdi bu durumu koda dökmek gerekiyor tabiki. Öncelike myGameObject sınıfımıza geri dönüyoruz ve CollisionControl adında, myGameObject tipinden bir paremetre alan ve geriye bool değer döndüren bir fonksiyon tanımlayıp içine aşağıdaki kodlarımızı yazıyoruz.
public bool CollisionControl(myGameObject pGameObject)
{
if (this.position.X + this.size.X > pGameObject.position.X &&
this.position.X < pGameObject.position.X + pGameObject.size.X &&
this.position.Y + this.size.Y > pGameObject.position.Y &&
this.position.Y < pGameObject.position.Y + pGameObject.size.Y)
return true;
else
return false;
} |
Bu methodla beraber artık sınıfımız içinde çarpışma kontolünü yapabileceğiz. Peki bunu oyun içinde nasıl kontrol edeceğiz. Çarpışma kontrölüde nesnelerin hareketleri gibi sürekli kontrol edilmesi gereken bir işlemdir. Ozaman bunu ben Update methodu içinde kontrol edebilirim değilmi.
Şimdi Game1.cs sınıfımıza geri dönüyoruz ve global olarak bool tipinde IsCollide adında bir değişken tanımlıyoruz. Daha sonra Update methodu içinde çarpışma durumunu kontrol ediyoruz ve eğer çarpışma varsa IsCollide değişkenimize true değerini atıyoruz. Bu IsCollide ’i birazdan Draw içinde kullanacağız.
Sınıf içinde;
Update methodu içinde;
if (gameObject1.CollisionControl(gameObject2))
{
IsCollide = true;
}
else
{
IsCollide = false;
}
|
Artık çarpışma kontrolümüz yapılmaktadır ve nesneler çarpıştığı zaman bu method geriye true döndürmektedir. Fakat biz bunu göremiyoruzdur. Bunun nedeni bu işlemi belirten bir kod blogu yazmamış olmamzıdır. Şimdi bunu göstermek için, çarpışma gerçekleştiği zaman, ekranın ortasında bir tane kırmızı çarpı işareti gösterelim. Bu resmimizede sprite 3 diyelim ve Sprites klasörümüz altına koyalım. Resmi aşağıdan kopyalayabilirsiniz.
İşte az önce tanımladıgımız IsCollide isimli global değişkeni, Draw methodu içinde şimdi kullanacağız. Aşağıdaki kod Draw methodunun son halidir.
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
if (IsCollide == true)
{
spriteBatch.Draw(Content.Load("Sprites/sprite3"),
new Vector2(((graphics.PreferredBackBufferWidth / 2)-50),
((graphics.PreferredBackBufferHeight / 2)-50)), Color.White);
}
spriteBatch.Draw(gameObject1.texture, gameObject1.position, Color.White);
spriteBatch.Draw(gameObject2.texture, gameObject2.position, Color.White);
spriteBatch.End();
base.Draw(gameTime);
} |
Bu kodlarıda yazdıktan sonra artık uygulamayı çalıştırıp nesneleri çarpıştırmayı denerseniz çarpışma anında ekranın ortasında sprite3 adındaki çarpı resmimiz görünecektir. Böylece çarpışma olup olmadıgını net bir şekilde görmüş oluyoruz.
Uygulamanın son halini buradan indirebilirsiniz.
Hepinize iyi çalışmalar dilerim.
Kaynak : APress, Beginning XNA 2.0 Game Programming: From Novice to Professional
Mehmet Aydın Ünlü
[email protected]
http://aydinunlu.blogspot.com
Makale:
XNA ile Oyun Programlama - 3 C#, Visual C# ve .NET Mehmet Aydın Ünlü
|