Merhaba, geçen Pazar öğlen saatlerinde canım sıkıldı ve bir program yazmak istedim. “Ne yazsam?” diye düşünürken, aklıma birden yılan oyunu geldi. Şimdiye kadar hiç oyun yazmayı denememiştim. Fikir ilginç geldi ve hemen yazmaya başladım. Yaklaşık 2 saat gibi bir sürede oyunu yazmayı tamamladım. Bu makalemde, C# ile yılan oyunu nasıl yazılır onu anlatacağım. Kodları anlamak istiyorsanız yazıyı okuyun. Yok ben kodu görünce ne yapıldığını anlarım diyorsanız da yazının sonundaki kaynak kodları indirip inceleyebilirsiniz.

Projenin oluşturulması

Ortam olarak Visual Studio 2013 kullandım. Siz 2010, 2012 veya 2013 kullanabilirsiniz, elinizde hangisi varsa iş görür. Yalnız eğer kaynak kodları indirip projeyi açmaya çalışırsanız 2010’da açılmayabilir. Visual Studio’yu açtıktan sonra File > New > Project menüsünü kullanarak yeni bir proje oluşturuyoruz.

save-project

Yeni projeyi oluşturduktan sonra, Solution Explorer üzerinde Form1.cs isimli formun ismini SnakeGame.cs olarak değiştiriyoruz ve çıkan uyarıya Yes (Evet) diyoruz. Her şey hazır. Artık formumuzu oluşturup kodlarımızı yazabiliriz.

Formun oluşturulması

Formumuzun görünümü şu şekilde olacak:

work-area

Ekrandaki kontroller ve isimleri şöyle:

  • 1 adet panel (gameArea)
  • 2 adet buton (resetButton, startButton)
  • 4 adet label (scoreHeaderLabel, scoreLabel, baitHeaderLabel, baitLabel)
  • 1 adet combobox (speedSelection)
  • 1 adet timer (gameTimer)

Combobox içerisindeki değerler ise şu şekilde:

  • 1 – Çok Kolay
  • 2 – Kolay
  • 3 – Orta
  • 4 – Zor
  • 5 – Çok Zor
  • 6 – İmkansız

Kontrollere verdiğim bazı özellikler ise aşağıdaki gibi:

Combobox
  • DropDownStyle: DropDownList
Panel
  • BorderStyle: FixedSingle
  • Size: 770×517
Form
  • FormBorderStyle: Fixed3D
  • MaximizeBox: False
  • StartPosition: CenterScreen
  • Size: 814×614

Burada en önemli nokta, panelin boyutlarını birebir aynı yapmanız gerekiyor. Yoksa yılanın duvar ile olan olaylarında hatalı sonuç elde edersiniz. Formumuzu da oluşturduk, şimdi gelelim kodlara.

Enum oluşturulması

Öncelikle yılanın yönünü daha kolay ifade edebilmemiz için bir enum oluşturuyoruz. Enum tanımını SnakeGame sınıfından sonra yapıyoruz. Yani aşağıdaki gibi bir kod görünümü olmalı:

C#
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;

namespace Snake
{
    public partial class SnakeGame : Form
    {
    }

    public enum DirectionEnum
    {
        Undefined,
        Up,
        Right,
        Down,
        Left
    }
}

Böylelikle DirectionEnum isminde yönleri belirten bir enum yazmış olduk.

Global değişkenlerin tanımlanması

SnakeGame sınıfımızın içerisinde aşağıdaki değişkenleri tanımlayalım. Ardından hangisi ne işe yarar açıklayalım.

C#
private const Keys up = Keys.Up;
private const Keys right = Keys.Right;
private const Keys down = Keys.Down;
private const Keys left = Keys.Left;

private int posX;
private int posY;
private const int xMax = 69;
private const int xMin = 0;
private const int yMax = 46;
private const int yMin = 0;

private bool lastKeyProcessed = true;
private int multiplier = 11;
private int gamePoint = 0;
private DirectionEnum direction;
private Point bait;
private List<Point> snakePosition = new List<Point>();

İlk bölümdeki 4 değişken, yön tuşlarını algılamak için tanımlandı. Basılan tuşun ne olduğunu daha kolay anlamak için bu değişkenleri kullanacağız.

İkinci bölümdeki 6 değişkenin ilk ikisi, yılanın bir sonraki pozisyonunu saklamak için kullanacağımız değişkenler. Kalan 4 tanesi ise, formun yani panelin sınırlarını belirleyen sayılar. Yani örneğin, posX değeri maksimum xMax değerine eşit olabilir. xMax’tan 1 tane fazla olursa yılan formun dışına çıkmış demektir. Buradaki xMax ve yMax değerleri formu oluştururken panel’e verdiğiniz boyutlar ile ilişkilidir.

Üçüncü bölümdeki değişkenler ise şöyle:

  • lastKeyProcessed: herhangi bir tuşa basıldıktan sonra o yöne en az bir defa gidilip gidilmediğini tutuyor.
  • multiplier: ikinci bölümdeki posX, xMax gibi değişkenleri piksel cinsinden ifade edebilmek için kullandığımız çarpan.
  • gamePoint: oyun puanının tutulduğu değişken.
  • direction: yılanın o anda hangi yöne gittiğinin tutulduğu değişken.
  • bait: yemin bulunduğu pozisyon.
  • snakePosition: yılanın hangi karelerde olduğunu tutan liste.

Değişkenlerimizi de tanımladık. Şimdi ufak bir kod ekleyeceğiz. Formun Load olayına aşağıdaki kodu yazalım ki, yılanın hızı varsayılan olarak 3 olsun. Forma Load olayı tanımlamak için: Formu seçin ve Properties penceresindeki yıldırım şeklindeki simgeye tıklayın. Listede Load yazan satırı bulun ve yan tarafına çift tıklayın. Kod kısmında bir metod oluşacaktır. Bu metodun içine aşağıdaki kodu ekleyin. Herhangi bir kontrole herhangi bir olay tanımlamak için bu yöntemi kullanabilirsiniz.

events
C#
speedSelection.SelectedIndex = 2;

Metotların yazılması

Oyun için toplamda 16 metot yazdım. Bunlardan 3 tanesi çizim metotları. Önce bu çizim metotlarını yazalım. Metotlar aşağıdaki gibi:

C#
private void drawSnake()
{
    gameArea.Refresh();
    drawBait();
    foreach (Point item in snakePosition)
    {
        int xVal = item.X * multiplier;
        int yVal = item.Y * multiplier;

        drawPoint(xVal, yVal);
    }
}

private void drawPoint(int x, int y, bool isBlack = true)
{
    using (Graphics g = this.gameArea.CreateGraphics())
    {
        Color penColor = isBlack ? Color.Black : Color.Red;
        Pen pen = new Pen(penColor, 5);
        g.DrawRectangle(pen, x, y, 5, 5);
        pen.Dispose();
    }
}

private void drawBait()
{
    drawPoint(bait.X, bait.Y, false);
}

Eğer İngilizce biliyorsanız, metotların ne iş yaptığını hemen anlamışsınızdır. Bilmeyen arkadaşlar için açıklama yapalım. drawSnake metodu oyunun temel metotlarından birisi aslında. Yaptığı iş ekrana yılan çizmek. Daha doğrusu yılanı çizdirmek. İlk satırdaki Refresh komutu, panelin temizlenmesini sağlıyor. Yani ekranı temizliyor. Ardından drawBait metodu çağrılıyor ve bu metot da drawPoint metodu yardımıyla ekrana yem çiziyor. Ardından snakePosition isimli değişkenimiz vardı hatırlarsanız, yılanın bulunduğu konumları tutacak demiştik. Bu değişken içerisinde yılanın bulunduğu her bir karenin değerleri mevcut. Foreach ile bu değerleri dönüp, öncelikle multiplier ile çarparak gerçek piksel değerlerini elde ediyoruz ve drawPoint ile bu pikseller üzerinde bir kutu çizdiriyoruz. snakePosition içerisindeki tüm değerler için bu işi yaptığımızda ekranda yılan görünür oluyor.

drawPoint metodu ise aslında ekrana sadece bir kutu çiziyor. using satırında panelin (gameArea) kullanılması gerektiği belirtiliyor. penColor, kutunun hangi renk olacağına karar veriyor (yem için kırmızı, yılan için siyah). Ardından 5px kalınlığında ve penColor renginde bir adet kalem nesnesi (pen) oluşturuyor. Daha sonra da DrawRectangle metoduyla gameArea üzerine x-y koordinatlarında eni ve boyu 5px olan bir kutu çiziyor.

Çizim metotları bunlar. Şimdi gelelim diğer metotlara. Bu diğer metotlar da kendi içerisinde ikiye ayrılıyor aslında, ana metotlar ve yardımcı metotlar olarak. Ana metotlar, olayların çağırdığı metotlar; yardımcı metotlar ise ana metotlar içerisinden çağrılan metotlardır. Önce yardımcı metotlardan başlayalım. Aşağıdaki metotlar yardımcı metotlardır:

C#
private void setGameSpeed()
{
    switch (speedSelection.SelectedIndex)
    {
        case 0:
            gameTimer.Interval = 100;
            break;
        case 1:
            gameTimer.Interval = 75;
            break;
        case 2:
        default:
            gameTimer.Interval = 50;
            break;
        case 3:
            gameTimer.Interval = 40;
            break;
        case 4:
            gameTimer.Interval = 25;
            break;
        case 5:
            gameTimer.Interval = 10;
            break;
    }
}

private void resetVariables()
{
    posX = 12;
    posY = 20;
    gamePoint = 0;
    direction = DirectionEnum.Right;
    printStat();
}

private void createNewSnake()
{
    snakePosition.Clear();
    snakePosition.Add(new Point(12, 20));
    snakePosition.Add(new Point(11, 20));
    snakePosition.Add(new Point(10, 20));
}

private void setPositionValues()
{
    switch (direction)
    {
        case DirectionEnum.Down:
            posY++;
            break;
        case DirectionEnum.Up:
            posY--;
            break;
        case DirectionEnum.Left:
            posX--;
            break;
        case DirectionEnum.Right:
        default:
           posX++;
           break;
    }
}

private bool isGameOver()
{
    //Check limits
    //if (posX &gt; xMax || posX &lt; xMin || posY &gt; yMax || posY &lt; yMin)
    //{
        // return true;
    //}

    //Eat itself
    if (snakePosition.Any(t =&gt; t.X == posX &amp;&amp; t.Y == posY))
    {
        return true;
    }

    return false;
}

private void createBait()
{
    Random random = new Random(DateTime.Now.TimeOfDay.Milliseconds);
    int x = 0;
    int y = 0;
    bool contains = true;

    while (contains)
    {
        x = random.Next(xMin, xMax + 1) * multiplier;
        y = random.Next(yMin, yMax + 1) * multiplier;

        contains = snakePosition.Any(t =&gt; t.X == x &amp;&amp; t.Y == y);
    }

    bait = new Point(x, y);
}

private void eatBait()
{
    Point lastPoint = snakePosition[snakePosition.Count - 1];
    snakePosition.Add(new Point(lastPoint.X, lastPoint.Y));
    gamePoint += (speedSelection.SelectedIndex + 1) * 10;
    printStat();
    createBait();
}

private void printStat()
{
    scoreLabel.Text = gamePoint.ToString();
    baitLabel.Text = (snakePosition.Count - 3).ToString();
}

private void determineDirection(Keys keyData)
{
    switch (keyData)
    {
        case up:
            if (direction != DirectionEnum.Down)
                direction = DirectionEnum.Up;
            break;
        case down:
            if (direction != DirectionEnum.Up)
                direction = DirectionEnum.Down;
            break;
        case left:
            if (direction != DirectionEnum.Right)
                direction = DirectionEnum.Left;
            break;
        case right:
        default:
            if (direction != DirectionEnum.Left)
                direction = DirectionEnum.Right;
            break;
    }
}

İlk metottan anlatmaya başlayalım:

  • setGameSpeed: bu metot, combobox içerisindeki değere göre timer’ın interval değerini setliyor. Yani kısaca yılanın ne kadar hızlı hareket edeceğini belirliyor. Değer ne kadar küçük olursa yılan o kadar hızlı hareket eder demektir.
  • resetVariables: oyuna yeniden başlanmak istendiğinde, bu metot yardımıyla değerler sıfırlanıyor.
  • createNewSnake: yine oyuna yeniden başlanmak istediğinde 3 kutulu başlangıç yılanını oluşturan metot.
  • setPositionValues: bu metot, yılanın gideceği yöndeki pozisyon değerini setliyor. Örneğin, yılan aşağı doğru bir kare gitmesi gerekiyorsa, posY değeri 1 arttırılıyor. Eğer yılan sola doğru bir kare gitmesi gerekiyorsa posX değeri bir azaltılıyor.
  • isGameOver: oyunun bitip bitmediğini kontrol eden metot. Yoruma alınmış if metodu yorumdan çıkarılırsa, yılan duvara çarptığında oyun bitecektir. Eğer yorumda kalırsa, yılan duvardan geçebilecektir. Alttaki if ise, yılanın kendi kuyruğuna çarpıp çarpmadığını kontrol ediyor. Bunu da şöyle yapıyor, yılanın bir sonraki noktası olan posX,posY koordinatları yılanın bulunduğu koordinatları içeren snakePosition değişkeni içerisinde var mı yok mu diye kontrol ediyor.
  • createBait: yeni yem oluşturan metot. Random sınıfı yardımıyla iki adet nokta belirleniyor (x,y). Eğer bu iki nokta yılanın bulunduğu koordinatlara denk geliyorsa başka iki nokta belirleniyor. Bu metodu biraz geliştirmek gerekebilir. Çünkü ekranın birçok kısmı yılan ile dolu olduğunda Random sınıfı boş bir nokta bulmak için çok zorlanacaktır ve program sonsuz döngüye girebilir. Ekranımız çok büyük olduğu için şimdilik bu durumu göz ardı edebiliriz.
  • eatBait: yılanın yemi yediğine karar verildiğinde çağrılan metot. Bu metot içerisinde yılanın en son noktsından yılana bir tane daha ekleniyor. Yani yılanın boyu bir uzatılıyor. Ardından puan arttırılıyor (yılan ne kadar hızlı ise o kadar çok artıyor puan). Sonra ekrandaki istatistikler güncelleniyor ve yeni yem oluşturuluyor.
  • printStat: ekrana istatistikleri yazmak için kullanılan metot. Oyun puanı zaten değişkende tutulduğu için direkt olarak yazdırabiliyoruz. Yenilen yem sayısını da, yılanın şimdiki boyutundan başlangıçtaki boyutunu (3) çıkararak bulabiliyoruz.
  • determineDirection: bu metot ile de yılanın yönüne karar veriliyor. case’ler içerisindeki if blokları ise tersine gidiş olmasın diye konuldu. Yani eğer yılan aşağı doğru giderken yukarı butonuna basarsanız bu tuş yoksayılacaktır. Buradaki if bloklarını kaldırdığınızda ise yılan aşağı giderken yukarı bastığınızda, yılan ters dönecektir (beklenmeyen durumlara yol açabilir).

Gelelim son metotlarımıza, yani ana metotlarımıza. Ana metotlarımız 4 adet: oyunu başlat, oyna, durdur ve sıfırla. Metot içerikleri ise şu şekilde:

C#
private void startGame()
{
    speedSelection.Enabled = false;
    startButton.Enabled = false;
    setGameSpeed();
    gameTimer.Enabled = true;
}

private void playGame()
{
    setPositionValues();
    bool isGameEnded = isGameOver();

    if (isGameEnded)
    {
        gameTimer.Enabled = false;
        MessageBox.Show(String.Format("Oyun Bitti! Puanınız: {0}", gamePoint),
"Game Over", MessageBoxButtons.OK, MessageBoxIcon.Information);
        return;
    }

    if (posY &gt; yMax)
        posY = yMin;
    else if (posY &lt; yMin)
        posY = yMax;

    if (posX &gt; xMax)
        posX = xMin;
    else if (posX &lt; xMin)
        posX = xMax;

    snakePosition.Insert(0, new Point(posX, posY));
    snakePosition.RemoveAt(snakePosition.Count - 1);

    if (bait.X == posX * multiplier &amp;&amp; bait.Y == posY * multiplier)
    {
        eatBait();
    }

    drawSnake();
    lastKeyProcessed = true;
}

private void resetGame()
{
    gameTimer.Enabled = false;
    startButton.Enabled = true;
    speedSelection.Enabled = true;

    createNewSnake();
    resetVariables();
    createBait();
    drawSnake();
}

private void pauseGame(Keys keyData)
{
    if (keyData == Keys.P)
    {
        gameTimer.Enabled = !gameTimer.Enabled;
    }
}

Ana metotların açıklamaları ise şöyle:

  • startGame: oyunu başlatan metottur. Hız seçimi kutusunu disable ediyor, Başlat butonunu disable ediyor, oyun hızını setliyor ve Timer’ı başlatıyor. Timer aktif olduğu sürece playGame metodu çalışacak ve yılan sürekli hareket edecektir.
  • resetGame: oyuna yeniden başlamak için kullanılan metottur. Timer durdurulur, Başlat butonu aktifleştirilir, Hız seçim kutusu aktifleştirilir. Ardından reset metotları çalıştırılır. Yani, başlangıç yılanı oluşturulur, değişkenler sıfırlanır, yeni yem oluşturulur ve yılan ile yem ekrana çizilir.
  • pauseGame: basılan tuş P ise Timer durdurulur veya devam ettirilir. Dolayısıyla oyun durur veya devam eder.
  • playGame: oyunun asıl metodu budur. Timer’ın her tick olayında bu metot çağrılır. Yılana hareketini veren metottur. İlk adımda yılanın yeni pozisyonu setlenir (posX ve posY değerleri güncelleniyor). Ardından oyunun bitip bitmediği kontrol edilir. Eğer oyun bitmişse Timer kapatılır ve ekrana uyarı basılır ve return ile metottan çıkılır. Eğer oyun bitmemişse, yılanın duvar sınırlarına ulaşıp ulaşmadığı kontrol edilir. İki adet if-else if bloğu eğer yılan sınırdan çıkmışsa, ekranın diğer tarafından görünmesi için posX ve posY değerlerini yeniden ayarlar. Sonraki insert ve removeAt metotları ise yılanı yürüten metotlar. Mantığı ise çok basit, yılanın yeni noktası olan posX ve posY noktaları yılanın baş kısmına eklenir, removeAt ile de en sondaki nokta yılandan silinir. Ardından gelen if bloğu ise yılanın yeni koordinatlarının yem ile çakışıp çakışmadığına bakar. Eğer yem ile çakışıyorsa yem yeme metodu çağrılır. Bu işlemlerden sonra yılan ve yem ekrana çizilsin diye drawSnake metodu çağrılır. Son olarak ilgili yönde bir tuşa basıldığını belli etmek için lastKeyProcessed değişkeni true olarak setlenir.

Olayların tanımlanması

Form üzerinde iki buton ve bir de timer kontrolümüz var ve bunlara birer olay (event) tanımlamamız gerekli. Butonlar için click olayı, timer için tick olayı tanımlamamız gerekiyor. Yazdığımız kodun temiz görünmesi için bu olaylar içerisinde sadece metot çağrımı yapacağız. Bahsettiğim üç kontrolün olaylarını aşağıdaki gibi yazabilirsiniz.

C#
private void startButton_Click(object sender, EventArgs e)
{
    startGame();
}

private void gameTimer_Tick(object sender, EventArgs e)
{
    playGame();
}

private void resetButton_Click(object sender, EventArgs e)
{
    resetGame();
}

Bu üç olayın yanı sıra, form açıkken basılan tuşun algılanabilmesi için formun bir metodunu override etmemiz gerekiyor. Aşağıdaki kodu yazmanız yeterli:

C#
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
    if (gameTimer.Enabled &amp;&amp; lastKeyProcessed &amp;&amp; keyData != Keys.P)
    {
        lastKeyProcessed = false;
        determineDirection(keyData);
    }

    pauseGame(keyData);
    return base.ProcessCmdKey(ref msg, keyData);
}

Açıklamasına gelirsek: if kontrolünde yılanın şu anda hareket edip etmediği, son basılan tuş için en az bir hareket gerçekleştiğinin ve basılan tuşun P olmadığının kontrolü yapılıyor. Eğer yılan hareket ediyorsa, son basılan tuş için en az bir hareket yapılmışsa ve basılan tuş P değilse, yeni hareket için öncelikle lastKeyProcessed değişkeni false yapılıyor (yeni bir tuşa basıldı anlamında), ardından da determineDirection metodu çağrılarak yılanın yeni pozisyonu belirleniyor. pauseGame metodu ise P tuşuna basılmışsa oyunu durdurmaya/devam ettirmeye yarıyor. Son satırdaki kod ise, metodu override ettiğimiz için, asıl kod bloğunun çalışmasını sağlıyor.

Oyunun oynanışı

Projeyi build edip çalıştırdığınızda, ilk önce bir hız seçiyorsunuz ve ardından Sıfırla butonuna basıyorsunuz. Ekrana yılan ve yem çiziliyor. Başla butonuna basarak da oyunu başlatıyorsunuz. Oyunda istediğiniz bir anda P tuşuna basrak oyunu durdurabilir veya devam ettirebilirsiniz. Bu da oyuna ait ekran görüntüsü:

game

Evet, ilk oyunum olan yılan oyununu böyle yazdım. Elbetteki eksiklikleri yanlışları vardır. Onları da kendiniz bulup düzeltirsiniz artık. Oyunun kaynak kodlarına aşağıdaki linkten ulaşabilirsiniz. Oyunun .exe dosyası ise tahmin edeceğiniz gibi bin/Debug klasöründe. Bir sonraki oyun olarak, tetris yazmayı düşünüyorum; ancak ne zaman yazarım bilemiyorum. Bir sonraki makalemde görüşmek ümidiyle…

C# Yılan Oyunu
C# Yılan Oyunu
Size: 16 KB

Benzer Makaleler

2 thoughts on “C# ile Yılan Oyunu

    1. Merhaba, paylaşmış olduğum kodlar örnek niteliğinde ve basit bir oyun olarak çalışıyor. Eğer bu konuya, yani oyun yazma konusuna ilginiz varsa farklı eğitim videoları veya dersleri izleyebilirsiniz. Agar.io gibi bir oyun yazmak ise uzun zaman alan ve ekip gerektiren bir iş, dolayısıyla benim yazmam pek mantıklı ve mümkün değil maalesef.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

*

warning
www.kemalkefeli.com.tr üzerindeki herhangi bir yazının veya kodun izinsiz olarak başka bir yerde kullanılması yasaktır.