Makale Özeti

XNA Game Studio 4 kütüphanelerinden faydalanarak Texture2D nesnesini ve bu nesnelerimize Windows Phone 7 cihazımızın ekranında hareket kabiliyeti kazandırmak için de Vektor2D nesnesini kullanmıştık. Önceki makalemizde olduğu gibi konuyu temel düzeyde ele alarak kullandığımız Texture nesnelerinin çarpışmasını (Collision Detection) 2D Rectangle Collision ve 2D Per-Pixel Collision olarak ele alacağız.

Makale

Bu makalemizde Windows Phone 7 'de XNA başlıklı yazdığımız makalemize kaldığımız yerden devam ediyoruz. XNA Game Studio 4 kütüphanelerinden faydalanarak Texture2D nesnesini ve bu nesnelerimize Windows Phone 7 cihazımızın ekranında hareket kabiliyeti kazandırmak için de Vektor2D nesnesini kullanmıştık. Önceki makalemizde olduğu gibi konuyu temel düzeyde ele alarak kullandığımız Texture nesnelerinin çarpışmasını (Collision Detection) 2D Rectangle Collision ve 2D Per-Pixel Collision olarak ele alacağız.

Oyunlarda nesnelerin çarpışması (ya da kesişmesi diyebiliriz) kritik öneme sahiptir. Özellikle yarış oyunlarında oyuncuların çarpışmamak, ya da yoldan çıkmamak için ne ölçüde çaba harcadıklarını, hırslandıklarını gözlemleyebiliriz. Böyle bir oyunda elbette çarpışmanın kalitesi ve gerçeğe yakın olması da oyuncuyu daha az çileden çıkaracaktır.

Bu makalemizde de örnek bir senaryo üzerinden devam edeceğiz. Ekran üzerinde bulunacak 2 adet kırmızı topun ekranın farklı yerlerinden rastgele çıkarak aracımızın üzerine doğru farklı hızlarda hareket etmesini sağlayacağız. Aracımızı (/Makaleler/Resimler/1000002642_car.PNG) ekranın üzerinde parmağımızla hareket ettirerek bu toplardan kaçırmaya çalışacağız. Toplar ile aracın çarpışması durumunda oyundan 1 hakkımız eksilecek. Toplam 3 hakkımız olacak ve 3 hakkımızı da kaybedersek oyun sonlanmış olacak.

2D Rectangle Collision

Adından da anlaşılacağı üzere 2 boyutlu düzlemde 2 dikdörtgenin kesişmesi durumudur. Nesnelerimizin görüntüsünün dikdörgen olmamasının bir önemi yoktur. Burada önemli olan imaj nesnesinden dikdörtgen veya dikdörtgenler elde edilmesidir. Aşağıdaki resimlerde 2 boyutlu düzlemde dikdörtgenlerin kesişme durumlarına örnekler görebiliriz ya da resimde gösterdiğimiz uçak imajında olduğu gibi bir imajı birden fazla dikdörtgene bölebiliriz.

Aşağıdaki araç ve kırmızı top resimlerinde açıkça görüldüğü gibi dikdörtgenlerin kesişmesinde, asıl kesişim noktaları nesnelerin ekranda görünen yanlarından farklı olarak imajın dikdörtgen olarak belirlenen kısımlarının kesişmesidir.

Araç için /Makaleler/Resimler/1000002642_car.PNG, toplar için /Makaleler/Resimler/1000002642_Ball-red-48.png ve yol kenar çizgileri içinde /Makaleler/Resimler/1000002642_blackLine.png imajlarını kullanacağız. Aşağıdaki dosyaları sağ tıklayarak bilgisayarınıza kaydedebilirsiniz.

Windows Phone 7'de XNA konulu ilk makalemizde olduğu gibi nesnelerimizi solution explorer altında oluşturulan content projesi içerisine ekliyoruz. Ayrıca Imaj dosyaları dışında bir de spritefont dosyasını content projemize ekleyelim. Bu dosyanın nasıl ekleneceğini de Windows Phone 7'de XNA konulu ilk makalemizde belirtmiştik.

Şimdi C# ile ön hazırlığımızı yaparak, oyunumuz için gerekli olacak kodlarımızı yazmaya başlayalım. Öncelikle private olarak Game1.cs sınıfımızda kullanacağımız Texture2D , Vektor2D ve SpriteFont nesnelerimizi oluşturalım.

 GraphicsDeviceManager graphics;
 SpriteBatch spriteBatch;
 
 // Araba objesi için kullanılacak Texture ve Vektörü
 Texture2D carTexture;
 Vector2 carPosition;
 
 //Top 1 objesi için kullanacağımız Texture ve Vektörü
 Texture2D ball_1Texture;
 Vector2 ball_1Position;
 
 //Top 2 objesi için kullanacağımız Texture ve Vektörü
 Texture2D ball_2Texture;
 Vector2 ball_2Position;
 
 //Ekrana ekleyeceğimiz text bilgi için SpriteFont ve Vektörü
 SpriteFont textFont;
 Vector2 textPosition;
 const string TEXT_TEMPLATE = "Gokhan Manduz Car Game - {0} ";
 string TEXT = "";
 
 //Yolu belirtmek için kullanılacak Texture ve Vektörler
 Texture2D line1Texture;
 Vector2 line1Position;
 Texture2D line2Texture;
 Vector2 line2Position;
 
 bool collisionDetected;
 short game = 3;
 Random random = new Random();
 int ballSpeed = 10;

Game1 constractor içerisinde herhangi bir değişiklik yapmıyoruz. Default olarak belirtilen TargetElapsedTime zaman aralığı ile update ve draw methodlarımız 30 fps zaman aralığı ile sürekli çalışacaktır.

 public Game1()
 {
     graphics = new GraphicsDeviceManager(this);
     Content.RootDirectory = "Content";
     TargetElapsedTime = TimeSpan.FromTicks(333333);
 }

Şimdide Content projemize eklediğimiz nesnelerimizi LoadContent override methodumuz içerisinde oyunumuzda kullanmak üzere hazırlayarlayalım oyun içerisinde bu nesnelerin hangi pozisyonda (koordinatta) başlayacağını belirleyelim.

 protected override void LoadContent()
 {
     spriteBatch = new SpriteBatch(GraphicsDevice);
 
     carTexture = Content.Load<Texture2D>("car");
     Viewport viewPort = graphics.GraphicsDevice.Viewport;
     carPosition = new Vector2( (viewPort.Width - carTexture.Width) / 2,  (viewPort.Height - carTexture.Height) / 2 );
 
     ball_2Texture = ball_1Texture = Content.Load<Texture2D>("Ball-red-48");
     ball_1Position = new Vector2(-150, 30);
     ball_2Position = new Vector2(-150, 260);
 
     textFont = this.Content.Load<SpriteFont>("CarGameFont");
     textPosition = new Vector2(2, 25);
 
     line2Texture = line1Texture = Content.Load<Texture2D>("blackLine");
     line1Position = new Vector2(-800, 0);
     line2Position = new Vector2(-800, 470);
 }

Tabiki oyunumuzun kilit kodları Update override methodu içerisinde yer alacaktır. Update methodu her çalıştırıldığında nesnelerimizin yeni pozisyonunu, yer değiştirmelerden sonra herhangi bir çarpışma olup olmadığını ve bir takım hesaplamalarımızı buraya yazacağız. Öncelikle Update methodumuz içerisinde 2D Rectangle ile çarpışma olup olmadığını kontrol edelim. Eğer çarpışma meydana gelmiş ise ekranımızın arka plan rengini de kırmızıya boyayalım. 3 çarpışmadan sonra oyunumuz sonlanacaktır. Kırmızı toplarımız X koordinatında soldan sağa doğru farklı hızlarda ve her defasında farklı Y koordinatından gelerek aracımıza doğru ilerleyecektir. Aracımıza bir yol üzerinde gidiyormuş izlenimi kazandımak için de yol kenarlarında bulunan çizgilerimizi sürekli olarak X koordinatında soldan sağa doğru hareket ettireceğiz. Update methodu içerisindeki kod satırlarını comment satırlarına dikkat ederek inceleyelim.

 protected override void Update(GameTime gameTime)
 {
     if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
         this.Exit();
 
     switch (game)
     {
         case 3:
             //-- 3 oyun hakkımız var.
             TEXT = string.Format(TEXT_TEMPLATE, "|||");
             break;
         case 2:
             //-- 2 oyun hakkımız kaldı.
             TEXT = string.Format(TEXT_TEMPLATE, "||");
             break;
         case 1:
             //-- 1 oyun hakkımız kaldı.
             TEXT = string.Format(TEXT_TEMPLATE, "|");
             break;
         case 0:
             //-- 0 oyun hakkı. Oyundan çıkılacak :)
             this.Exit();
             break;
     }
            
     //-- 13 ve 20 birim arasında rastgele topların hızlarını değiştirelim.
     //   (X koordinatında)
     ballSpeed = random.Next(13, 20);
     ball_1Position.X += ballSpeed;
     ball_2Position.X += (ballSpeed - 3);
     //-- 
 
 
     //-- X koordinatındaki pozisyon 900'den fazla ise toplar X'de tekrar başa dönsün.
     //   ve Y ekseninde toplar rasgele bir pozisyonda başlasın.
     if (ball_1Position.X > 900) {
         ball_1Position.X = -60;
         int ball1_Y = random.Next(ball_1Texture.Height, (480 - ball_1Texture.Height));
         ball_1Position.Y = ball1_Y;
     }
     if (ball_2Position.X > 900)
     {
         ball_2Position.X = -60;
         int ball2_Y = random.Next(ball_2Texture.Height, (480 - ball_1Texture.Height));
         ball_2Position.Y = ball2_Y;
     }
     line1Position.X += 10;
     line2Position.X += 10;
     if (line1Position.X > -2)
     {
         line1Position.X = -800;
         line2Position.X = -800;
     }
          
     //-- Ekrana dokunulduysa hangi pozisyona dokunulduğu bilgisi alınır.
     //   ve aracı o yönde ve mesafeye göre hızı ayarlanarak aracın X,Y değerleri arttırılır.
     TouchCollection touchCollection = TouchPanel.GetState();
     if (touchCollection.Count > 0) {
         TouchLocation t1 = touchCollection[0];
         double x = t1.Position.X - (carPosition.X + (carTexture.Width / 2));
         double y = t1.Position.Y - (carPosition.Y + (carTexture.Height / 2));
         double speed = Math.Sqrt(x * x + y * y) / 10;
         double angle = (float)Math.Atan2(y, x);
         carPosition.X += (float)(speed * Math.Cos(angle));
         carPosition.Y += (float)(speed * Math.Sin(angle));
     }
 
 
     //-- Collision Detaction kod satırları
     Rectangle carRectangle = new Rectangle((int)carPosition.X, (int)carPosition.Y, carTexture.Width, carTexture.Height);
     Rectangle ball1Rectangle = new Rectangle((int)ball_1Position.X, (int)ball_1Position.Y, ball_1Texture.Width, ball_1Texture.Height);
     Rectangle ball2Rectangle = new Rectangle((int)ball_2Position.X, (int)ball_2Position.Y, ball_2Texture.Width, ball_2Texture.Height);
     if (carRectangle.Intersects(ball1Rectangle) || carRectangle.Intersects(ball2Rectangle))
     {
         collisionDetected = true;
     }
     else {
         if (collisionDetected) {
             //Oyun hakkımızı 1 azaltıyoruz
             game--;
         }
         collisionDetected = false;
     }
     //--
 
     base.Update(gameTime);
 }

Yukarıdaki kod satırlarına dikkat edecek olursak çarpışma olup olmadığını rectangle nesnemizin Intersects methodunu kullanarak gerçekleştiriyoruz.

Şimdi de aynı örneğimiz üzerinden yalnızca Collision Detection kod satırlarımızı düzenleyerek 2D Per-Pixel Collision Detaction ile çarpışma olup olmadığını kontrol edeceğiz.

2D Per-Pixel Collision

2 boyutlu düzlemde yer alan nesnelerimizin piksel piksel kontrol edilmesiyle çarpışma olup olmadığının anlaşıldığı yöntemdir. Bu yöntem Rectangle Collision yöntemine göre çok daha sağlıklı sonuçlar verecektir. Hatta tam olarak istediğimiz şekillerin çarpışmasını en ince detaya kadar kontrol edebiliriz. Bu yöntemde imaj içerisinde yer alan nesnelerimizi piksel bazında üst üste gelecek renk değişimini kontrol ederek çarpışma oldup olmadığını belirleyeceğiz. Aşağıdaki resimlerde imaj dosyalarımızın içeriğini piksel bazında detaylı olarak görebiliriz. (VS 2010 content projemiz içerisindeki imaj dosyalarına çift tıklayarak hangi renge sahip piksellerden oluştuğunu görebiliyoruz.)

Piksel piksel bölünmüş olan nesnelerimizin aslında bazı piksel renklerinin transparent, bazılarının ise transparent dan farlı olarak bir renge sahip olduğunu görebiliyoruz. Per-Pixel Collision da nesnelerimizin kesişmesini piksellerindeki renk farklılıklarını kontrol ederek yakalayacağız. Imajlar kesiştiğinde kesişen bölgelerdeki renklerin transparent ya da saydam olması muhtemeldir. Bu sebepten dolayı imajların kesişiminde transparent renkten farklı olan renklerin aynı piksel üzerinde yer almasını dikkate alacağız. C# kodlarımızı bu özelliğe uygun şekilde hazırlayalım.

Öncelikle private olan değişkenlerimizin olduğu kısıma Color array olarak carTextureData, ball1TextureData ve ball2TextureData isimli nesnelerimizi tanımlıyoruz. Hemen ardından LoadContent override methodumuz içerisinde de color array nesnelerimizi aşağıdaki gibi oluşturalım.

private değişkenler;

 Color[] carTextureData;
 Color[] ball1TextureData;
 Color[] ball2TextureData;

LoadContent methoduna eklememiz gereken c# kod satırlarımız.

 carTextureData = new Color[carTexture.Width * carTexture.Height];
ball2TextureData = ball1TextureData = new Color[ball_1Texture.Width * ball_1Texture.Height];
 
carTexture.GetData(carTextureData);
ball_1Texture.GetData(ball1TextureData);
ball_2Texture.GetData(ball2TextureData);

Ve Update override methodumuzun son hali ile static bool IntersectPixel (Piksel çakışması) methodumuzu aşağıdaki gibi hazırlayalım.

protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
        this.Exit();
 
    switch (game)
    {
        case 3:
            //-- 3 oyun hakkımız var.
            TEXT = string.Format(TEXT_TEMPLATE, "|||");
            break;
        case 2:
            //-- 2 oyun hakkımız kaldı.
            TEXT = string.Format(TEXT_TEMPLATE, "||");
            break;
        case 1:
            //-- 1 oyun hakkımız kaldı.
            TEXT = string.Format(TEXT_TEMPLATE, "|");
            break;
        case 0:
            //-- 0 oyun hakkı. Oyundan çıkılacak :)
            this.Exit();
            break;
     }
            
    //-- 13 ve 20 birim arasında rastgele topların hızlarını değiştirelim.
    //   (X koordinatında)
    ballSpeed = random.Next(13, 20);
    ball_1Position.X += ballSpeed;
    ball_2Position.X += (ballSpeed - 3);
    //-- 
 
 
    //-- X koordinatındaki pozisyon 900'den fazla ise toplar X'de tekrar başa dönsün.
    //   ve Y ekseninde toplar rasgele bir pozisyonda başlasın.
    if (ball_1Position.X > 900) {
        ball_1Position.X = -60;
        int ball1_Y = random.Next(ball_1Texture.Height, (480 - ball_1Texture.Height));
        ball_1Position.Y = ball1_Y;
    }
    if (ball_2Position.X > 900)
    {
        ball_2Position.X = -60;
        int ball2_Y = random.Next(ball_2Texture.Height, (480 - ball_1Texture.Height));
        ball_2Position.Y = ball2_Y;
    }
 
    line1Position.X += 10;
    line2Position.X += 10;
    if (line1Position.X > -2)
    {
        line1Position.X = -800;
        line2Position.X = -800;
    }
        
    //-- Ekrana dokunulduysa hangi pozisyona dokunulduğu bilgisi alınır.
    //   ve aracı o yönde ve mesafeye göre hızı ayarlanarak aracın X,Y değerleri arttırılır.
    TouchCollection touchCollection = TouchPanel.GetState();
    if (touchCollection.Count > 0) {
        TouchLocation t1 = touchCollection[0];
        double x = t1.Position.X - (carPosition.X + (carTexture.Width / 2));
        double y = t1.Position.Y - (carPosition.Y + (carTexture.Height / 2));
        double speed = Math.Sqrt(x * x + y * y) / 10;
        double angle = (float)Math.Atan2(y, x);
        carPosition.X += (float)(speed * Math.Cos(angle));
        carPosition.Y += (float)(speed * Math.Sin(angle));
    }
 
 
    //-- Per-Pixel Collision Detaction kod satırları
    Rectangle carRectangle = new Rectangle((int)carPosition.X, (int)carPosition.Y, carTexture.Width, carTexture.Height);
    Rectangle ball1Rectangle = new Rectangle((int)ball_1Position.X, (int)ball_1Position.Y, ball_1Texture.Width, ball_1Texture.Height);
    Rectangle ball2Rectangle = new Rectangle((int)ball_2Position.X, (int)ball_2Position.Y, ball_2Texture.Width, ball_2Texture.Height);
    if (IntersectPixels(carRectangle, carTextureData, ball1Rectangle, ball1TextureData) || IntersectPixels(carRectangle, carTextureData,ball2Rectangle, ball2TextureData))
    {
        collisionDetected = true;
    }
    else {
        if (collisionDetected) {
            //Oyun hakkımızı 1 azaltıyoruz
            game--;
        }
        collisionDetected = false;
    }
    //--
 
    base.Update(gameTime);
}
 
static bool IntersectPixels(Rectangle rectangleCar, Color[] dataCar,
                           Rectangle rectangleBall, Color[] dataBall)
{
    int top = Math.Max(rectangleCar.Top, rectangleBall.Top);
    int bottom = Math.Min(rectangleCar.Bottom, rectangleBall.Bottom);
    int left = Math.Max(rectangleCar.Left, rectangleBall.Left);
    int right = Math.Min(rectangleCar.Right, rectangleBall.Right);
 
    for (int y = top; y < bottom; y++)
    {
        for (int x = left; x < right; x++)
        {
            Color colorA = dataCar[(x - rectangleCar.Left) +
                                 (y - rectangleCar.Top) * rectangleCar.Width];
            Color colorB = dataBall[(x - rectangleBall.Left) +
                                 (y - rectangleBall.Top) * rectangleBall.Width];
 
            // Eğer her 2 piksel rengide transparent dan farklı ise kesişme var
            if (colorA.A != 0 && colorB.A != 0)
            {
                return true;
            }
        }
    }
    // Kesişme bulunamadı ise false
    return false;
}

Oyunumuzda aracımızı hareket ettirirken TouchCollection nesnesinden faydalandık. Eğer istersek Windows Phone 7 cihazlarında bulunan Akselerometreden faydalanarak da aracımızı hareket ettirebiliriz. Akselerometre ile ilgili detaylı bilgiye buradan erişilebilir.

Son olarak Draw override methodumuzda nesnelerimizi ekrana çizdirecek olan C# kodlarımızı yazarak makalemizi sonlandıralım. Eğer kodları ilgili methodlarımızın içerisine yazıp F5 ile Windows Phone 7 emulator üzerinde çalıştıracak olursak aşağıdaki ekran görüntüsünü elde edeceğiz.

protected override void Draw(GameTime gameTime)
{
    if(collisionDetected)
        GraphicsDevice.Clear(Color.Red);
    else
       GraphicsDevice.Clear(Color.White);
 
    spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
    spriteBatch.Draw(carTexture, carPosition, collisionDetected ? Color.Red : Color.White);
    spriteBatch.End();
 
    spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
    spriteBatch.Draw(ball_1Texture, ball_1Position, Color.White);
    spriteBatch.End();
 
    spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
    spriteBatch.Draw(ball_2Texture, ball_2Position, Color.White);
    spriteBatch.End();
 
    spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
    spriteBatch.DrawString(textFont, TEXT, textPosition, Color.Blue);
    spriteBatch.End();
 
    spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
    spriteBatch.Draw(line1Texture, line1Position, Color.White);
    spriteBatch.End();
 
    spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
    spriteBatch.Draw(line2Texture, line2Position, Color.White);
    spriteBatch.End();
 
    base.Draw(gameTime);
}

Windows Phone 7 de keyifli oyunlar... Kolay gelsin.

gokhanmanduz.blogspot.com
gokhanmanduz@hotmail.com
gmanduz@gmail.com