JavaScript ile Pong Oyunu Yapımı: Adım Adım Canvas Rehberi
Canvas ve JavaScript ile tarayıcıda Pong oyunu yaz: oyun döngüsü, klavye kontrolü, çarpışma, skor ve bilgisayar rakip. Sonunda tek dosyalık tam kod var.
Pong, iki raket ve bir toptan oluşan, kuralları bir dakikada anlaşılan bir oyun. Küçük bir programın içinde oyun döngüsü, klavye kontrolü, çarpışma ve skor gibi her oyunda karşına çıkacak fikirlerin hepsi var; bu yüzden ilk oyun projesi için çok uygun.
Bu rehberin sonunda tarayıcıda çalışan, bilgisayara karşı oynanan bir Pong oyunun olacak. Daha önemlisi, oyunun neden çalıştığını anlayacaksın: topun nasıl hareket ettiğini, raketin topu nasıl “gördüğünü” ve skorun nasıl tutulduğunu. Rehberin en sonunda tek dosyalık tam kodu bulacaksın; onu doğrudan /Editor'e yapıştırıp oynayabilirsin. Yine de adım adım ilerlemeni öneririz; her adımda ekranda yeni bir şeyin canlandığını görmek öğrenmenin en keyifli kısmı.
if ve fonksiyon kavramlarını biliyorsan yeterli. Emin değilsen önce Değişken, koşul, döngü rehberine ve JavaScript derslerine göz atabilirsin.1. Adım: Canvas'a ilk çizim
Tarayıcıda oyun çizmek için HTML'deki <canvas> etiketini kullanırız. Canvas, üzerine JavaScript ile piksel piksel çizim yapabildiğin boş bir tuval gibidir. Çizim yapmak için önce tuvalin “fırçasını” alırız: getContext("2d"). Aşağıdaki sayfa iki raketi ve topu yerinde, hareketsiz olarak çizer:
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<title>İlk çizim</title>
</head>
<body style="background: #222;">
<canvas id="oyun" width="600" height="400" style="background: #000;"></canvas>
<script>
var canvas = document.getElementById("oyun");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#fff";
ctx.fillRect(20, 160, 10, 80); // sol raket: x, y, genişlik, yükseklik
ctx.fillRect(570, 160, 10, 80); // sağ raket
ctx.beginPath();
ctx.arc(300, 200, 8, 0, Math.PI * 2); // top: merkez x, merkez y, yarıçap
ctx.fill();
</script>
</body>
</html>
Burada iki komut öğrendin: fillRect(x, y, genişlik, yükseklik) dolu bir dikdörtgen çizer, arc(...) ile fill() ise dolu bir daire çizer. Renk, çizimden önce fillStyle ile seçilir.
Koordinatlar matematik dersinden farklı
Canvas'ta (0, 0) noktası sol üst köşedir. x sağa, y ise aşağı doğru büyür; 600×400'lük tuvalde sağ alt köşe (600, 400) olur. Bu yüzden raketi yukarı taşımak için y'yi azaltırız.
2. Adım: Oyun döngüsü ile topu hareket ettir
Oyunlar bir film gibi çalışır: saniyede onlarca kez “her şeyi biraz ilerlet, ekranı sil, yeniden çiz” yapılır. Buna oyun döngüsü denir. Tarayıcıda bunun için requestAnimationFrame kullanırız: “bir sonraki ekran yenilemesinde bu fonksiyonu çağır” demektir. Fonksiyon her seferinde kendini yeniden sıraya koyduğu için döngü hiç durmaz.
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<title>Zıplayan top</title>
</head>
<body style="background: #222;">
<canvas id="oyun" width="600" height="400"></canvas>
<script>
var canvas = document.getElementById("oyun");
var ctx = canvas.getContext("2d");
var t = { x: 300, y: 200, r: 8, hx: 4, hy: 3 }; // hx, hy: her karedeki adım
function dongu() {
// 1) GÜNCELLE
t.x += t.hx;
t.y += t.hy;
if (t.x - t.r < 0 || t.x + t.r > canvas.width) t.hx = -t.hx;
if (t.y - t.r < 0 || t.y + t.r > canvas.height) t.hy = -t.hy;
// 2) ÇİZ (önce eski kareyi sil)
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(t.x, t.y, t.r, 0, Math.PI * 2);
ctx.fill();
// 3) TEKRARLA
requestAnimationFrame(dongu);
}
dongu();
</script>
</body>
</html>
Topun iki yeni özelliği var: hx ve hy. Bunlar her karede x ve y'ye eklenen miktarlar, yani topun hızı ve yönü. Top bir duvara değdiğinde ilgili hızın işaretini çeviririz: 4 olan hx, −4 olur ve top geri döner. Bütün “sekme” fikri bu kadar.
fillRect satırını geçici olarak kaldırıp dene: top, arkasında iz bırakan bir fırçaya dönüşür. Ekranı her karede silmenin neden gerekli olduğunu en iyi böyle görürsün.3. Adım: Raketler ve klavye
İlk akla gelen yol, her tuşa basışta raketi biraz kaydırmaktır. Ama tuşu basılı tutunca klavye tekrarı gecikmeyle başladığı için raket kesik kesik ilerler. Daha akıcı yol: hangi tuşların şu anda basılı olduğunu bir nesnede tutup raketi oyun döngüsünde hareket ettirmek.
var tuslar = {};
document.addEventListener("keydown", function (e) { tuslar[e.key] = true; });
document.addEventListener("keyup", function (e) { tuslar[e.key] = false; });
// Oyun döngüsünün içinde:
if (tuslar["w"] || tuslar["ArrowUp"]) solRaket.y -= 6;
if (tuslar["s"] || tuslar["ArrowDown"]) solRaket.y += 6;
Raketin ekrandan taşmaması için tam oyundaki raketiSinirla fonksiyonu her karede y değerini 0 ile yükseklik - raket.h arasında tutar. Tam oyunda ok tuşlarına ve boşluk tuşuna basıldığında e.preventDefault() da çağrılıyor. Bu satır olmazsa, oyun sayfanın içindeyken ok tuşları bütün sayfayı yukarı aşağı kaydırır.
4. Adım: Çarpışma, yani top rakete değdi mi?
Oyunun en önemli sorusu bu. Topu bir kare gibi düşünürsek (merkezden her yöne r kadar uzanan bir kutu), soru iki dikdörtgenin üst üste binip binmediğine dönüşür. İki kutu ancak dört koşulun hepsi doğruysa çakışır: topun sol kenarı raketin sağ kenarından solda, topun sağ kenarı raketin sol kenarından sağda, topun alt kenarı raketin üstünden aşağıda ve topun üst kenarı raketin altından yukarıda. Bu yönteme oyun programcılığında “kutu çakışması” (AABB) denir ve çok yaygındır.
// Top (daire) ile raket (dikdörtgen) üst üste biniyor mu?
function raketeCarptiMi(t, raket) {
return t.x - t.r < raket.x + raket.w &&
t.x + t.r > raket.x &&
t.y + t.r > raket.y &&
t.y - t.r < raket.y + raket.h;
}
Sekişi ilginç yapmak
Top her zaman aynı açıyla dönerse oyun çabuk sıkıcı olur. Gerçek Pong'daki gibi, topun raketin neresine çarptığına bakarız: ortaya çarparsa düz döner, uçlara yaklaştıkça daha dik bir açıyla gider. Ayrıca her vuruşta top biraz hızlanır, ama bir üst sınırı vardır.
// Raketten sek: yatay yön döner, çarpma noktasına göre açı değişir
function raketeSek(t, raket, solRaketMi) {
var merkez = raket.y + raket.h / 2;
var fark = (t.y - merkez) / (raket.h / 2); // -1 (üst uç) ... 1 (alt uç)
var hiz = Math.min(Math.abs(t.hx) * 1.05, 12); // her vuruşta biraz hızlan
if (solRaketMi) {
t.hx = hiz;
t.x = raket.x + raket.w + t.r; // topu raketin dışına it
} else {
t.hx = -hiz;
t.x = raket.x - t.r;
}
t.hy = fark * 5;
}
Dikkat: Sekerken topu raketin dışına itiyoruz (t.x = ... satırları). Bunu yapmazsak top bir sonraki karede hâlâ raketin içinde olabilir, yönü tekrar döner ve top raketin içinde titreyip kalır. Tam oyunda ayrıca sol raketi yalnızca top sola giderken (hx < 0), sağ raketi de yalnızca top sağa giderken kontrol ediyoruz; bu da aynı sorunu ikinci kez önler.
Duvarlar için de aynı dikkat geçerli. 2. adımdaki hy = -hy yöntemi basit, ama top duvarın biraz içine girmişse yön her karede yeniden dönebilir. Bu yüzden tam oyundaki duvardanSek fonksiyonu yönü Math.abs ile kesin belirler: üst duvarda top mutlaka aşağı, alt duvarda mutlaka yukarı gider.
5. Adım: Skor, kazanan ve bilgisayar rakip
Top sol kenardan tamamen çıkarsa sayıyı sağdaki oyuncu, sağdan çıkarsa soldaki oyuncu alır. Fonksiyon yalnızca kimin kazandığını söyler; skoru artırmak ve topu ortaya koymak oyun döngüsünün işidir. Böylece her fonksiyon tek bir işi yapar.
// Top ekrandan çıktıysa sayıyı kim aldı? ("sol", "sag" ya da null)
function sayiKimin(t, genislik) {
if (t.x + t.r < 0) return "sag";
if (t.x - t.r > genislik) return "sol";
return null;
}
Oyunun bir de durum değişkeni var: "bekliyor", "oynaniyor" ya da "bitti". Boşluk tuşu oyunu başlatır; bir taraf 5 sayıya ulaşınca durum "bitti" olur ve ekranda kazanan yazılır.
Sağ raketi bilgisayar yönetir. Fikir çok basit: raketin ortası topun altındaysa yukarı, üstündeyse aşağı git. Ama bilgisayar bir karede en fazla maksHiz kadar hareket edebilir. Sınır koymazsak raket her topa anında yetişir ve onu yenmek imkânsız olur. Tam oyundaki bilgisayarOynasin fonksiyonunun son parametresi olan bu sayıyı değiştirerek zorluğu ayarlayabilirsin.
6. Adım: Mantığı ekran olmadan test et
Çarpışma ve skor fonksiyonları canvas'a hiç dokunmuyor; sadece sayı alıp sonuç döndürüyorlar. Bu sayede onları oyunu oynamadan, tek tek deneyebilirsin. Aşağıdaki kod tarayıcı gerektirmez; /Editor'de JavaScript olarak ya da Node.js ile çalıştırabilirsin.
function raketeCarptiMi(t, raket) {
return t.x - t.r < raket.x + raket.w &&
t.x + t.r > raket.x &&
t.y + t.r > raket.y &&
t.y - t.r < raket.y + raket.h;
}
function sayiKimin(t, genislik) {
if (t.x + t.r < 0) return "sag";
if (t.x - t.r > genislik) return "sol";
return null;
}
var raket = { x: 20, y: 160, w: 10, h: 80 };
console.log(raketeCarptiMi({ x: 35, y: 200, r: 8 }, raket)); // true: raketin tam önünde
console.log(raketeCarptiMi({ x: 35, y: 100, r: 8 }, raket)); // false: raketin üstünden geçiyor
console.log(raketeCarptiMi({ x: 30, y: 155, r: 8 }, raket)); // true: üst köşeyi sıyırdı
console.log(sayiKimin({ x: -10, y: 200, r: 8 }, 600)); // "sag": top soldan çıktı
console.log(sayiKimin({ x: 300, y: 200, r: 8 }, 600)); // null: oyun sürüyor
Çıktılar satır sonlarındaki yorumlarla aynı olmalı. Oyunda garip bir şey olduğunda önce böyle küçük bir test yazmak, hatayı bulmanın en hızlı yoludur.
Sık yapılan hatalar
- Tuşlar hiçbir şey yapmıyor. Oyun bir editörün önizleme penceresinde çalışıyorsa, klavye o pencereye gitmiyor olabilir. Önce oyun alanına bir kez tıkla. Hâlâ çalışmıyorsa tuş adını kontrol et:
"ArrowUp"büyük-küçük harfe duyarlıdır,"arrowup"çalışmaz. - Top iz bırakıyor. Her karenin başında ekranı silmeyi unuttun.
ciz()fonksiyonu tuvalin tamamını boyayan birfillRectile başlamalı. - Top raketin içinde titriyor ya da duvara yapışıyor. Yönü çevirmek yetmez; topu engelin dışına da it ya da yönü
Math.absile kesin belirle (4. adım). - Raket ters yöne gidiyor. Canvas'ta y aşağı doğru büyür. Yukarı gitmek için y'yi azaltmalısın.
Cannot read properties of null (reading 'getContext')hatası.getElementById("oyun")canvas'ı bulamadı. Yaidyazımı farklı ya da<script>canvas'tan önce geliyor. Script'i</body>'den hemen önceye koy.- Top raketin içinden geçip gidiyor. Hız çok artınca top bir karede raketi atlayabilir. Bu yüzden
raketeSekhızı 12 ile sınırlıyor; bu, raket genişliği (10) ile top çapının (16) toplamından küçüktür.
Kendin dene
- Renkleri değiştir (kolay). Topu sarı, raketleri farklı renklerde yap. İpucu:
ciz()içinde her çizimden öncefillStyleayarla. - Zorluk ayarı (kolay).
bilgisayarOynasin(sagRaket, oyunTopu, 4)satırındaki 4'ü 2 ve 7 yaparak dene. Hangi değer sana göre adil? - Duraklat (orta). P tuşuna basınca oyun dursun, tekrar basınca devam etsin. İpucu: yeni bir
"duraklatildi"durumu ekle veguncelle()içinde bu durumda hiçbir şey yapma. - İki kişilik mod (orta). Bilgisayarı kaldır; sol oyuncu W/S, sağ oyuncu ok tuşlarıyla oynasın. Önce sol raketin ok tuşu kontrolünü kaldırman gerekecek.
- Ses ve süs (zor). Top rakete her çarptığında kısa bir ses çal (
new Audio(...)) ya da raket kısa bir süre renk değiştirsin. İpucu: çarpmanın gerçekleştiği yerraketeSekçağrısıdır.
Sık sorulan sorular
Bu oyunu yazmak için ne kadar JavaScript bilmem gerekiyor?
Değişken, if koşulu, fonksiyon ve nesne (süslü parantezli { x: 10 } yapısı) kavramlarına aşina olman yeterli. Canvas komutlarının hepsi rehberde adım adım anlatılıyor.
Kodu nereye yazıp çalıştırmalıyım?
Tam kodu /Editor'e yapıştırıp çalıştırabilirsin. İstersen bir metin düzenleyiciyle pong.html adıyla kaydedip dosyaya çift tıklayarak tarayıcıda da açabilirsin; kurulum gerekmez.
requestAnimationFrame yerine setInterval kullanabilir miyim?
Kullanabilirsin, oyun yine çalışır. Ancak requestAnimationFrame çizimi ekranın yenilenmesiyle aynı anda yapar ve sekme arka plandayken döngüyü durdurur; bu yüzden akıcı animasyonlar için daha uygundur.
Oyun neden bazı bilgisayarlarda daha hızlı çalışıyor?
requestAnimationFrame genellikle ekranın yenileme hızında çağrılır. 60 Hz bir ekranda saniyede yaklaşık 60, 120 Hz bir ekranda yaklaşık 120 kare çizilir; hızlar kare başına yazıldığı için top da daha hızlı gider. Çözüm, iki kare arasında geçen süreyi ölçüp hareketi bu süreyle çarpmaktır.
Bu oyunu telefonda oynayabilir miyim?
Bu haliyle klavye gerektirir. Telefonda oynamak için canvas'a touchmove olayı ekleyip parmağın y konumunu sol raketin y değerine aktarman gerekir; bu, iyi bir geliştirme görevidir.
Oyunun tam kodu
Aşağıdaki kodun tamamını kopyala ve /Editor'e yapıştır ya da bilgisayarında pong.html adıyla kaydedip çift tıklayarak tarayıcıda aç. Boşluk tuşuyla başla; sen soldaki raketsin. Kodun üstünde ekrana dokunmayan oyun mantığı, altında klavye, çizim ve oyun döngüsü var.
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pong</title>
<style>
body { background: #111; color: #eee; font-family: sans-serif; text-align: center; }
canvas { background: #000; border: 2px solid #555; display: block; margin: 10px auto; max-width: 100%; }
</style>
</head>
<body>
<canvas id="oyun" width="600" height="400"></canvas>
<p>W / S ya da yukarı / aşağı ok tuşlarıyla raketini oynat. İlk 5 sayıyı alan kazanır.</p>
<script>
// ===== OYUN MANTIĞI (ekrana hiç dokunmaz) =====
// Top üst ya da alt duvara değince dikey yönü çevir
function duvardanSek(t, yukseklik) {
if (t.y - t.r < 0) {
t.y = t.r;
t.hy = Math.abs(t.hy);
} else if (t.y + t.r > yukseklik) {
t.y = yukseklik - t.r;
t.hy = -Math.abs(t.hy);
}
}
// Top (daire) ile raket (dikdörtgen) üst üste biniyor mu?
function raketeCarptiMi(t, raket) {
return t.x - t.r < raket.x + raket.w &&
t.x + t.r > raket.x &&
t.y + t.r > raket.y &&
t.y - t.r < raket.y + raket.h;
}
// Raketten sek: yatay yön döner, çarpma noktasına göre açı değişir
function raketeSek(t, raket, solRaketMi) {
var merkez = raket.y + raket.h / 2;
var fark = (t.y - merkez) / (raket.h / 2); // -1 (üst uç) ... 1 (alt uç)
var hiz = Math.min(Math.abs(t.hx) * 1.05, 12); // her vuruşta biraz hızlan
if (solRaketMi) {
t.hx = hiz;
t.x = raket.x + raket.w + t.r; // topu raketin dışına it
} else {
t.hx = -hiz;
t.x = raket.x - t.r;
}
t.hy = fark * 5;
}
// Top ekrandan çıktıysa sayıyı kim aldı? ("sol", "sag" ya da null)
function sayiKimin(t, genislik) {
if (t.x + t.r < 0) return "sag";
if (t.x - t.r > genislik) return "sol";
return null;
}
// Raket ekranın dışına taşmasın
function raketiSinirla(raket, yukseklik) {
if (raket.y < 0) raket.y = 0;
if (raket.y + raket.h > yukseklik) raket.y = yukseklik - raket.h;
}
// Bilgisayar raketi topu takip eder ama en fazla maksHiz kadar
function bilgisayarOynasin(raket, t, maksHiz) {
var fark = t.y - (raket.y + raket.h / 2);
if (fark > maksHiz) fark = maksHiz;
if (fark < -maksHiz) fark = -maksHiz;
raket.y += fark;
}
// Topu ortaya koy ve yon (-1 sola, 1 sağa) tarafına gönder
function topuOrtala(t, genislik, yukseklik, yon) {
t.x = genislik / 2;
t.y = yukseklik / 2;
t.hx = 4 * yon;
t.hy = Math.random() * 4 - 2;
}
// ===== OYUN DURUMU =====
var canvas = document.getElementById("oyun");
var ctx = canvas.getContext("2d");
var GENISLIK = canvas.width;
var YUKSEKLIK = canvas.height;
var KAZANMA_SKORU = 5;
var oyunTopu = { x: 0, y: 0, r: 8, hx: 0, hy: 0 };
var solRaket = { x: 20, y: 160, w: 10, h: 80 };
var sagRaket = { x: GENISLIK - 30, y: 160, w: 10, h: 80 };
var skorSol = 0;
var skorSag = 0;
var durum = "bekliyor"; // "bekliyor", "oynaniyor" ya da "bitti"
var tuslar = {};
// ===== KLAVYE =====
document.addEventListener("keydown", function (e) {
tuslar[e.key] = true;
if (e.key === " " || e.key === "ArrowUp" || e.key === "ArrowDown") {
e.preventDefault(); // sayfa kaymasın
}
if (e.key === " " && durum !== "oynaniyor") {
if (durum === "bitti") {
skorSol = 0;
skorSag = 0;
}
topuOrtala(oyunTopu, GENISLIK, YUKSEKLIK, Math.random() < 0.5 ? -1 : 1);
durum = "oynaniyor";
}
});
document.addEventListener("keyup", function (e) {
tuslar[e.key] = false;
});
// ===== GÜNCELLE: her karede konumları hesapla =====
function guncelle() {
if (tuslar["w"] || tuslar["W"] || tuslar["ArrowUp"]) solRaket.y -= 6;
if (tuslar["s"] || tuslar["S"] || tuslar["ArrowDown"]) solRaket.y += 6;
raketiSinirla(solRaket, YUKSEKLIK);
if (durum !== "oynaniyor") return;
bilgisayarOynasin(sagRaket, oyunTopu, 4);
raketiSinirla(sagRaket, YUKSEKLIK);
oyunTopu.x += oyunTopu.hx;
oyunTopu.y += oyunTopu.hy;
duvardanSek(oyunTopu, YUKSEKLIK);
if (oyunTopu.hx < 0 && raketeCarptiMi(oyunTopu, solRaket)) {
raketeSek(oyunTopu, solRaket, true);
}
if (oyunTopu.hx > 0 && raketeCarptiMi(oyunTopu, sagRaket)) {
raketeSek(oyunTopu, sagRaket, false);
}
var kazanan = sayiKimin(oyunTopu, GENISLIK);
if (kazanan === "sol") skorSol++;
if (kazanan === "sag") skorSag++;
if (kazanan) {
if (skorSol >= KAZANMA_SKORU || skorSag >= KAZANMA_SKORU) {
durum = "bitti";
} else {
topuOrtala(oyunTopu, GENISLIK, YUKSEKLIK, kazanan === "sol" ? 1 : -1);
}
}
}
// ===== ÇİZ: her karede ekranı baştan boya =====
function ciz() {
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, GENISLIK, YUKSEKLIK);
ctx.fillStyle = "#444"; // kesikli orta çizgi
for (var y = 0; y < YUKSEKLIK; y += 30) {
ctx.fillRect(GENISLIK / 2 - 2, y, 4, 18);
}
ctx.fillStyle = "#fff";
ctx.fillRect(solRaket.x, solRaket.y, solRaket.w, solRaket.h);
ctx.fillRect(sagRaket.x, sagRaket.y, sagRaket.w, sagRaket.h);
if (durum === "oynaniyor") {
ctx.beginPath();
ctx.arc(oyunTopu.x, oyunTopu.y, oyunTopu.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.textAlign = "center";
ctx.font = "40px monospace";
ctx.fillText(skorSol, GENISLIK / 4, 50);
ctx.fillText(skorSag, GENISLIK * 3 / 4, 50);
ctx.font = "22px sans-serif";
if (durum === "bekliyor") {
ctx.fillText("Başlamak için Boşluk tuşuna bas", GENISLIK / 2, YUKSEKLIK / 2 + 60);
}
if (durum === "bitti") {
var mesaj = skorSol > skorSag ? "Kazandın!" : "Bilgisayar kazandı.";
ctx.fillText(mesaj + " Tekrar için Boşluk", GENISLIK / 2, YUKSEKLIK / 2 + 60);
}
}
// ===== OYUN DÖNGÜSÜ =====
function dongu() {
guncelle();
ciz();
requestAnimationFrame(dongu);
}
dongu();
</script>
</body>
</html>
Oyun çalıştıysa tebrikler! Şimdi “Kendin dene” görevleriyle onu kendi oyunun haline getir. Yeni projeler için derslere ve diller sayfasına bakabilirsin.