JavaScript ile Yılan Oyunu Yapımı: Adım Adım Snake Rehberi
Canvas ve JavaScript ile yılan (Snake) oyunu yaz: ızgara, diziyle yılan gövdesi, yem, büyüme, çarpışma ve oyun bitti ekranı. Sonunda tek dosyalık tam kod var.
Yılan oyununu muhtemelen biliyorsun: yılan sürekli ilerler, sen yönünü değiştirirsin, yem yedikçe uzar, duvara ya da kendi gövdesine çarparsa oyun biter. Kuralları bu kadar basit olan bir oyunun içinde aslında programlamanın çok güçlü bir fikri saklı: diziler. Yılanın gövdesi bir dizidir ve bütün hareket, bu diziye başından eleman ekleyip sonundan eleman silmekten ibarettir.
Bu rehberin sonunda tarayıcıda çalışan, skor tutan ve “oyun bitti” ekranı olan bir yılan oyunu yazmış olacaksın. Izgara mantığını, dizilerle hareketi, zamanlayıcıyı ve çarpışma kontrolünü adım adım kuracağız. En sonda tek dosyalık tam kod var; onu /Editor'e yapıştırıp hemen oynayabilirsin.
1. Adım: Ekranı ızgaraya böl
Pong'da top her karede birkaç piksel kayıyordu. Yılan oyununda ise her şey kareli defter gibi bir ızgaranın üzerinde yaşar: yılan bir seferde tam bir hücre ilerler, yem her zaman bir hücrenin içindedir. 400×400 piksellik tuvali 20×20 hücreye bölersek her hücre 20 piksel olur.
Buradaki kilit fikir: Oyunun içinde konumları hücre numarasıyla tutarız (örneğin x: 10, y: 10), piksele yalnızca çizerken çeviririz. Çeviri tek bir çarpmadır: piksel = hücre × HUCRE. Böylece oyun mantığı piksellerle hiç uğraşmaz.
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<title>Izgara ve yılan</title>
</head>
<body style="background: #222;">
<canvas id="oyun" width="400" height="400"></canvas>
<script>
var canvas = document.getElementById("oyun");
var ctx = canvas.getContext("2d");
var HUCRE = 20; // bir hücre 20x20 piksel, 400 / 20 = 20 hücre
var yilan = [{ x: 10, y: 10 }, { x: 9, y: 10 }, { x: 8, y: 10 }];
var yem = { x: 15, y: 5 };
ctx.fillStyle = "#1b1f24";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// İnce ızgara çizgileri (sadece görmek için)
ctx.strokeStyle = "#2c333b";
for (var i = 0; i <= 20; i++) {
ctx.beginPath();
ctx.moveTo(i * HUCRE, 0);
ctx.lineTo(i * HUCRE, 400);
ctx.moveTo(0, i * HUCRE);
ctx.lineTo(400, i * HUCRE);
ctx.stroke();
}
// Hücre koordinatını piksele çevir: piksel = hücre * HUCRE
ctx.fillStyle = "#e5533d";
ctx.fillRect(yem.x * HUCRE, yem.y * HUCRE, HUCRE, HUCRE);
ctx.fillStyle = "#4caf50";
for (var j = 0; j < yilan.length; j++) {
ctx.fillRect(yilan[j].x * HUCRE, yilan[j].y * HUCRE, HUCRE, HUCRE);
}
</script>
</body>
</html>
Bu sayfayı çalıştırdığında ızgaranın ortasında üç hücrelik yeşil bir yılan ve sağ üstte kırmızı bir yem göreceksin. Henüz hiçbir şey hareket etmiyor, ama oyunun bütün verisi zaten burada: bir dizi ve bir nokta.
2. Adım: Yılanı bir dizi olarak düşün
Yılan, hücrelerden oluşan bir dizidir. Dizinin ilk elemanı baş, son elemanı kuyruktur. Yılanı bir adım ilerletmek için her parçayı tek tek kaydırmak gerekmez. Şu iki işlem yeter:
- Başın gideceği yeni hücreyi hesapla ve dizinin başına ekle (
unshift). - Dizinin sonundaki kuyruğu sil (
pop).
Ortadaki parçalar hiç değişmez, ama ekranda yılan bir hücre ilerlemiş görünür. Peki yılan nasıl büyür? Çok kolay: yem yediği adımda kuyruğu silmeyiz. Aşağıdaki kodu tarayıcı olmadan, /Editor'de JavaScript olarak ya da Node.js ile çalıştırıp diziyi kendi gözünle izle:
var yilan = [{ x: 3, y: 1 }, { x: 2, y: 1 }, { x: 1, y: 1 }];
var yon = { x: 1, y: 0 }; // sağa
function yaz(etiket) {
var parcalar = yilan.map(function (h) { return "(" + h.x + "," + h.y + ")"; });
console.log(etiket + ": " + parcalar.join(" "));
}
yaz("Başlangıç");
// Normal adım: başa ekle, kuyruktan sil
yilan.unshift({ x: yilan[0].x + yon.x, y: yilan[0].y + yon.y });
yilan.pop();
yaz("1 adım ");
// Yem yedi: başa ekle ama kuyruğu SİLME
yilan.unshift({ x: yilan[0].x + yon.x, y: yilan[0].y + yon.y });
yaz("Yem yedi ");
Çıktı şöyle olmalı:
// Başlangıç: (3,1) (2,1) (1,1)
// 1 adım : (4,1) (3,1) (2,1)
// Yem yedi : (5,1) (4,1) (3,1) (2,1)
Yönü de bir nokta gibi tutuyoruz: sağ için { x: 1, y: 0 }, yukarı için { x: 0, y: -1 }. Canvas'ta y aşağı doğru büyüdüğü için yukarı gitmek y'yi azaltmak demektir. Yeni başın yeri her zaman eski baş + yön olur.
3. Adım: Klavye ve “ters dönememe” kuralı
Ok tuşlarının her biri bir yöne karşılık gelir. Bunu bir nesnede tutarsak klavye kodu çok kısalır:
var YONLER = {
ArrowUp: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 }
};
Yılan oyununun önemli bir kuralı var: Sağa giden yılan bir anda sola dönemez, yoksa başı ikinci parçasına çarpar. İki yönün ters olup olmadığını anlamanın kısa bir yolu, toplamlarına bakmaktır: sağ (1, 0) ile sol (−1, 0) toplanınca (0, 0) olur. Toplam sıfırsa yön terstir ve isteği görmezden geliriz.
// Yılan kendi üstüne, tam ters yöne dönemez
function yonDegistir(sonYon, istenenYon) {
if (sonYon.x + istenenYon.x === 0 && sonYon.y + istenenYon.y === 0) {
return sonYon;
}
return istenenYon;
}
Gizli tuzak: Çok hızlı iki tuş
Yılan sağa giderken, bir adım süresi içinde hızlıca önce yukarı, sonra sola bastığını düşün. Her tuşu geldiği anda yönle karşılaştırırsan “yukarı” kabul edilir, ardından “sol” da yukarıya göre ters olmadığı için kabul edilir. Yılan hiç yukarı gitmeden sola döner ve kendi üstüne çarpar. Oyuncu bunu haksızlık olarak hisseder.
Çözüm: Tuşa basıldığında yönü hemen değiştirmeyiz, sadece istenenYon olarak not ederiz. Kontrol, yılan gerçekten adım atarken, en son gerçekten kullanılan yöne göre yapılır. Tam oyunda bu iş adimAt fonksiyonunun ilk satırında oluyor.
4. Adım: Zamanlayıcı ile sabit hızda ilerle
Pong'da requestAnimationFrame kullandık ve top saniyede onlarca kez az az kaydı. Yılan ise hücre hücre, belli aralıklarla ilerler. Bunun için setInterval daha uygundur: “her 120 milisaniyede bir oyunAdimi fonksiyonunu çalıştır” deriz. Sayı küçüldükçe yılan hızlanır.
function baslat() {
basladi = true;
clearInterval(zamanlayici); // eski zamanlayıcı kalmasın
zamanlayici = setInterval(oyunAdimi, ADIM_SURESI);
}
clearInterval satırı küçük ama önemli. Oyun her yeniden başladığında yeni bir zamanlayıcı kurulur. Eskisini durdurmazsan iki zamanlayıcı birlikte çalışır ve yılan her yeni oyunda bir öncekinin iki katı hızla koşmaya başlar.
5. Adım: Yem, büyüme ve oyun bitti
Yemi doğru yere koymak
Yemi rastgele bir hücreye koymak kolay, ama ya o hücrede yılanın gövdesi varsa? Yem gövdenin altında kaybolur. Bunun yerine önce bütün boş hücrelerin listesini çıkarıp bunlardan birini rastgele seçeriz. Yılan bütün alanı doldurduysa boş hücre kalmaz; bu da oyunu kazanmak demektir.
// Yemi yalnızca yılanın OLMADIĞI bir hücreye koy
function rastgeleYem(yilan, hucreSayisi) {
var bosHucreler = [];
for (var y = 0; y < hucreSayisi; y++) {
for (var x = 0; x < hucreSayisi; x++) {
var hucre = { x: x, y: y };
if (!govdeyeCarptiMi(hucre, yilan)) bosHucreler.push(hucre);
}
}
if (bosHucreler.length === 0) return null; // alan doldu
return bosHucreler[Math.floor(Math.random() * bosHucreler.length)];
}
Oyunun kalbi: adimAt
Şimdi her şeyi bir araya getiriyoruz. adimAt fonksiyonu yılanı bir hücre ilerletir ve sırasıyla şunları yapar: yönü güncelle, yeni başı hesapla, yem yenip yenmediğine bak, duvar ya da gövde çarpışmasını kontrol et, sonra diziyi güncelle.
// Yılanı bir hücre ilerlet: oyunun kalbi
function adimAt(d) {
d.yon = yonDegistir(d.yon, d.istenenYon);
var bas = { x: d.yilan[0].x + d.yon.x, y: d.yilan[0].y + d.yon.y };
var yediMi = d.yem !== null && ayniHucre(bas, d.yem);
// Yem yemediyse kuyruk bu adımda kalkacak; o hücreye girmek serbest
var kontrolEdilecek = yediMi ? d.yilan : d.yilan.slice(0, -1);
if (duvaraCarptiMi(bas, d.hucreSayisi) || govdeyeCarptiMi(bas, kontrolEdilecek)) {
d.bitti = true;
return d;
}
d.yilan.unshift(bas); // başa yeni hücre ekle
if (yediMi) {
d.skor++; // kuyruğu silme: yılan büyüdü
d.yem = rastgeleYem(d.yilan, d.hucreSayisi);
if (d.yem === null) {
d.bitti = true;
d.kazandi = true;
}
} else {
d.yilan.pop(); // kuyruğu sil: boy aynı kaldı
}
return d;
}
Bir inceliğe dikkat et: Gövde çarpışmasını kontrol ederken, yem yenmediyse kuyruğu listeden çıkarıyoruz (slice(0, -1)). Çünkü kuyruk bu adımda zaten yerinden ayrılacak. Yılan kendi kuyruğunu kovalarken başın, kuyruğun az önce boşalttığı hücreye girmesi kurallara uygundur. Bu satırı yazmazsak oyun bazen haksız yere biter.
Duvar kontrolü ise sadece sınırlara bakar: Hücre numarası 0'dan küçük ya da hücre sayısına eşit veya büyükse yılan alanın dışındadır.
function duvaraCarptiMi(bas, hucreSayisi) {
return bas.x < 0 || bas.y < 0 || bas.x >= hucreSayisi || bas.y >= hucreSayisi;
}
6. Adım: Oyunu oynamadan test et
Bu oyunun mantık fonksiyonları canvas'a hiç dokunmaz; sadece veriyi değiştirir. Bu sayede oyunu oynamadan, ne olacağını bildiğimiz durumlar kurup sonucu kontrol edebiliriz. Aşağıdaki satırları tam koddaki “OYUN MANTIĞI” bölümünün (ilk <script> satırından “EKRAN VE DURUM” yorumuna kadar olan kısım) hemen altına yapıştırıp /Editor'de JavaScript olarak ya da Node.js ile çalıştır:
var d = yeniOyun(20);
d.yem = { x: 12, y: 10 }; // yemi başın iki hücre sağına koy
adimAt(d);
console.log(d.yilan.length, d.skor); // 3 0 -> boy aynı
adimAt(d);
console.log(d.yilan.length, d.skor); // 4 1 -> yedi ve büyüdü
d.istenenYon = { x: -1, y: 0 }; // sağa giderken sola basmak
adimAt(d);
console.log(d.yon.x, d.bitti); // 1 false -> ters dönüş engellendi
for (var i = 0; i < 20; i++) adimAt(d);
console.log(d.bitti); // true -> sağ duvara çarptı
Her satırın yanındaki yorum beklenen çıktıyı gösteriyor. Çıktın farklıysa, hangi fonksiyonda sorun olduğunu artık tam olarak biliyorsun.
Sık yapılan hatalar
- Yılan uzamıyor ya da hiç durmadan uzuyor.
pop()satırının yeri yanlış. Kuyruk yalnızca yem yenmediğinde silinmeli. - Yılan çizilmiyor ya da çok küçük görünüyor. Hücre koordinatını piksele çevirmeyi unuttun.
fillRect(x, y, ...)yerinefillRect(x * HUCRE, y * HUCRE, ...)yazmalısın. - Yılan her yeni oyunda daha hızlı. Yeni
setIntervalkurmadan önce eskisiniclearIntervalile durdurmadın (4. adım). - Hızlı tuşlamada yılan kendi üstüne dönüyor. Yön kontrolünü tuşa basıldığı anda değil, adım atılırken yap (3. adım).
- Diziyi kopyaladığını sanmak.
var yedek = yilan;yazınca yeni bir dizi oluşmaz, iki isim aynı diziyi gösterir. Birinde yapılan değişiklik ötekinde de görünür. Gerçek kopya içinyilan.slice()kullan. - Ok tuşları sayfayı kaydırıyor. Klavye fonksiyonunda
e.preventDefault()çağır. Tuşlar hiç tepki vermiyorsa önce oyun alanına bir kez tıkla.
Kendin dene
- Hızı ayarla (kolay).
ADIM_SURESIdeğerini 80 ve 200 yaparak dene. Sonra yılanın başını farklı bir renk yap. - Daha büyük ızgara (kolay).
HUCRE_SAYISIdeğerini 25 yap ve canvas boyutunu buna göre değiştir. İpucu: canvas genişliği hücre sayısının tam katı olmalı. - Yem yedikçe hızlan (orta). Her yemde
ADIM_SURESIdeğerini biraz azalt ve zamanlayıcıyı yeniden kur. Bir alt sınır koymayı unutma. - Duvarsız mod (orta). Yılan sağdan çıkınca soldan girsin. İpucu:
adimAtiçinde yeni başı hesapladıktan sonra(bas.x + hucreSayisi) % hucreSayisikullan ve bu modda duvar kontrolünü atla. - En yüksek skor (zor). En iyi skoru
localStorageile sakla ve ekranın sağ üstünde göster. Sayfa yenilense de rekor kaybolmasın.
Sık sorulan sorular
Yılan oyunu Pong'dan daha mı zor?
Çizim kısmı daha kolaydır, çünkü her şey ızgara hücrelerinde durur. Zorluk, yılan gövdesini dizi olarak yönetmekte ve kendine çarpma kontrolündedir. Diziler sana yeniyse önce rehberin 2. adımındaki küçük örneği birkaç kez çalıştır.
Neden requestAnimationFrame yerine setInterval kullandık?
Yılan akıcı kaymak yerine belli aralıklarla tam bir hücre ilerler. setInterval bu sabit aralığı doğrudan verir. requestAnimationFrame ile de yapılabilir, ama o zaman geçen süreyi kendin ölçüp adım zamanını hesaplaman gerekir.
Yılan neden ters yöne dönemiyor?
Sağa giden yılan bir anda sola dönerse başı hemen arkasındaki parçaya çarpar ve oyun anında biter. Bu yüzden yonDegistir fonksiyonu tam ters yöndeki istekleri görmezden gelir.
Yemin yılanın üstünde çıkmasını nasıl önleriz?
Rastgele bir hücre seçmek yerine önce yılanın olmadığı bütün boş hücrelerin listesini çıkarır, sonra bu listeden rastgele birini seçeriz. rastgeleYem fonksiyonu tam olarak bunu yapar.
Kodu nereye yazıp çalıştırmalıyım?
Tam kodu /Editor'e yapıştırıp çalıştırabilir ya da yilan.html adıyla kaydedip tarayıcıda açabilirsin. Mantık testlerini ise /Editor'de JavaScript olarak ya da Node.js ile çalıştırabilirsin.
Oyunun tam kodu
Aşağıdaki kodun tamamını kopyala ve /Editor'e yapıştır ya da bilgisayarında yilan.html adıyla kaydedip tarayıcıda aç. Bir ok tuşuna (ya da W, A, S, D) basınca oyun başlar; oyun bitince Boşluk tuşu yeni oyun açar. Kodun üst bölümü ekrana dokunmayan oyun mantığı, alt bölümü ise ekran, klavye ve zamanlayıcıdır.
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Yılan Oyunu</title>
<style>
body { background: #111; color: #eee; font-family: sans-serif; text-align: center; }
canvas { border: 2px solid #555; display: block; margin: 10px auto; max-width: 100%; }
</style>
</head>
<body>
<canvas id="oyun" width="400" height="400"></canvas>
<p>Ok tuşları ya da W A S D ile yılanı yönlendir. Kırmızı yemleri ye, duvara ve kendine çarpma!</p>
<script>
// ===== OYUN MANTIĞI (ekrana hiç dokunmaz) =====
function ayniHucre(a, b) {
return a.x === b.x && a.y === b.y;
}
// Yılan kendi üstüne, tam ters yöne dönemez
function yonDegistir(sonYon, istenenYon) {
if (sonYon.x + istenenYon.x === 0 && sonYon.y + istenenYon.y === 0) {
return sonYon;
}
return istenenYon;
}
function duvaraCarptiMi(bas, hucreSayisi) {
return bas.x < 0 || bas.y < 0 || bas.x >= hucreSayisi || bas.y >= hucreSayisi;
}
function govdeyeCarptiMi(bas, govde) {
for (var i = 0; i < govde.length; i++) {
if (ayniHucre(bas, govde[i])) return true;
}
return false;
}
// Yemi yalnızca yılanın OLMADIĞI bir hücreye koy
function rastgeleYem(yilan, hucreSayisi) {
var bosHucreler = [];
for (var y = 0; y < hucreSayisi; y++) {
for (var x = 0; x < hucreSayisi; x++) {
var hucre = { x: x, y: y };
if (!govdeyeCarptiMi(hucre, yilan)) bosHucreler.push(hucre);
}
}
if (bosHucreler.length === 0) return null; // alan doldu
return bosHucreler[Math.floor(Math.random() * bosHucreler.length)];
}
function yeniOyun(hucreSayisi) {
var orta = Math.floor(hucreSayisi / 2);
var yilan = [
{ x: orta, y: orta }, // baş
{ x: orta - 1, y: orta },
{ x: orta - 2, y: orta } // kuyruk
];
return {
hucreSayisi: hucreSayisi,
yilan: yilan,
yon: { x: 1, y: 0 },
istenenYon: { x: 1, y: 0 },
yem: rastgeleYem(yilan, hucreSayisi),
skor: 0,
bitti: false,
kazandi: false
};
}
// Yılanı bir hücre ilerlet: oyunun kalbi
function adimAt(d) {
d.yon = yonDegistir(d.yon, d.istenenYon);
var bas = { x: d.yilan[0].x + d.yon.x, y: d.yilan[0].y + d.yon.y };
var yediMi = d.yem !== null && ayniHucre(bas, d.yem);
// Yem yemediyse kuyruk bu adımda kalkacak; o hücreye girmek serbest
var kontrolEdilecek = yediMi ? d.yilan : d.yilan.slice(0, -1);
if (duvaraCarptiMi(bas, d.hucreSayisi) || govdeyeCarptiMi(bas, kontrolEdilecek)) {
d.bitti = true;
return d;
}
d.yilan.unshift(bas); // başa yeni hücre ekle
if (yediMi) {
d.skor++; // kuyruğu silme: yılan büyüdü
d.yem = rastgeleYem(d.yilan, d.hucreSayisi);
if (d.yem === null) {
d.bitti = true;
d.kazandi = true;
}
} else {
d.yilan.pop(); // kuyruğu sil: boy aynı kaldı
}
return d;
}
// ===== EKRAN VE DURUM =====
var canvas = document.getElementById("oyun");
var ctx = canvas.getContext("2d");
var HUCRE_SAYISI = 20;
var HUCRE = canvas.width / HUCRE_SAYISI; // 400 / 20 = 20 piksel
var ADIM_SURESI = 120; // milisaniye: küçüldükçe yılan hızlanır
var oyun = yeniOyun(HUCRE_SAYISI);
var basladi = false;
var zamanlayici = null;
// ===== KLAVYE =====
var YONLER = {
ArrowUp: { x: 0, y: -1 }, w: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 }, s: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 }, a: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 }, d: { x: 1, y: 0 }
};
document.addEventListener("keydown", function (e) {
var tus = e.key.length === 1 ? e.key.toLowerCase() : e.key;
if (YONLER[tus]) {
e.preventDefault();
oyun.istenenYon = YONLER[tus]; // yön, bir sonraki adımda uygulanır
if (!basladi) baslat();
}
if (tus === " ") {
e.preventDefault();
if (oyun.bitti) {
oyun = yeniOyun(HUCRE_SAYISI);
baslat();
} else if (!basladi) {
baslat();
}
}
});
function baslat() {
basladi = true;
clearInterval(zamanlayici); // eski zamanlayıcı kalmasın
zamanlayici = setInterval(oyunAdimi, ADIM_SURESI);
}
function oyunAdimi() {
adimAt(oyun);
if (oyun.bitti) clearInterval(zamanlayici);
ciz();
}
// ===== ÇİZİM =====
function kare(x, y, renk) {
ctx.fillStyle = renk;
ctx.fillRect(x * HUCRE + 1, y * HUCRE + 1, HUCRE - 2, HUCRE - 2);
}
function ciz() {
ctx.fillStyle = "#1b1f24";
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (oyun.yem) kare(oyun.yem.x, oyun.yem.y, "#e5533d");
for (var i = oyun.yilan.length - 1; i >= 0; i--) {
kare(oyun.yilan[i].x, oyun.yilan[i].y, i === 0 ? "#9be15d" : "#4caf50");
}
ctx.fillStyle = "#fff";
ctx.font = "18px sans-serif";
ctx.textAlign = "left";
ctx.fillText("Skor: " + oyun.skor, 8, 22);
ctx.textAlign = "center";
if (!basladi) {
ctx.fillText("Başlamak için bir ok tuşuna bas", canvas.width / 2, canvas.height / 2 + 50);
}
if (oyun.bitti) {
ctx.font = "28px sans-serif";
ctx.fillText(oyun.kazandi ? "Tebrikler, alan doldu!" : "Oyun bitti!", canvas.width / 2, canvas.height / 2 - 10);
ctx.font = "18px sans-serif";
ctx.fillText("Tekrar oynamak için Boşluk", canvas.width / 2, canvas.height / 2 + 20);
}
}
ciz();
</script>
</body>
</html>
Oyunun çalıştıysa “Kendin dene” görevleriyle onu geliştirmeye başlayabilirsin. Python ile bir oyun yazmayı da denemek istersen Python sayı tahmin oyunu rehberi iyi bir sonraki adım; diğer dersler için /Learn sayfasına göz at.