Tarayıcı tabanlı sonsuz koşu oyunu geliştirmek mi istiyorsunuz? Bu adım adım TypeScript rehberiyle HTML5 Canvas, oyun döngüsü, çarpışma tespiti ve performans ipuçlarını öğrenerek kendi oyununuzu sıfırdan inşa edin.
Modern web tarayıcıları, güçlü ve etkileşimli deneyimler sunma kapasitesine sahip gelişmiş platformlar haline geldi. Eskiden yalnızca masaüstü uygulamalarla sınırlı olan oyun geliştirme süreçleri, günümüzde HTML5, CSS3 ve JavaScript gibi web teknolojileri sayesinde doğrudan tarayıcılar üzerinden erişilebilir hale geldi. Peki, neden tarayıcı tabanlı bir oyun geliştirmeyi düşünmelisiniz? Bu sorunun cevabı aslında oldukça basit: erişilebilirlik ve dağıtım kolaylığı. Bir tarayıcı tabanlı oyun, herhangi bir indirme veya kurulum gerektirmeden, sadece bir bağlantıya tıklayarak anında oynanabilir. Bu, oyununuzu geniş kitlelere ulaştırmak için benzersiz bir avantaj sunar. Ayrıca, mobil cihazlardan masaüstü bilgisayarlara kadar geniş bir cihaz yelpazesinde çalışabilme potansiyeli de cabası.
Endless runner (sonsuz koşu) oyunları, basit ama bağımlılık yapıcı mekanikleriyle özellikle mobil platformlarda büyük popülarite kazanmıştır. Temple Run, Subway Surfers gibi örnekler, bu tür oyunların ne kadar geniş bir oyuncu kitlesine ulaşabileceğini kanıtlamıştır. Bu tür oyunlar, genellikle tek dokunuş veya basit klavye kontrolleriyle oynanabilir ve sürekli artan bir zorluk seviyesi sunarak oyuncuları meşgul tutar. Bu basitlik, onları hem geliştirmesi nispeten kolay hem de oldukça eğlenceli hale getirir. Geliştirme sürecine yeni başlayanlar için harika bir öğrenme projesi sunarlar çünkü temel oyun mekaniklerini, animasyonları, çarpışma tespitini ve skor sistemlerini uygulamanızı gerektirirler.
Bu makalede, TypeScript kullanarak sıfırdan bir tarayıcı tabanlı sonsuz koşu oyunu geliştireceğiz. Neden TypeScript mi? JavaScript’in tip tanımlı bir üst kümesi olan TypeScript, özellikle büyük ölçekli ve karmaşık projelerde kod kalitesini, okunabilirliğini ve bakımını önemli ölçüde artırır. Hata ayıklamayı kolaylaştırır, derleme zamanında potansiyel hataları yakalar ve nesne yönelimli programlama (OOP) prensiplerini daha sağlam bir şekilde uygulamanıza olanak tanır. Ayrıca, modern geliştirme araçlarıyla harika bir uyum içindedir ve giderek artan bir topluluğa sahiptir. Oyun geliştirme sırasında karşılaşılan karmaşıklıkları yönetmek için TypeScript’in sunduğu yapısal avantajlar paha biçilmezdir. Dolayısıyla, sadece bir oyun değil, aynı zamanda sağlam ve ölçeklenebilir bir kod tabanı oluşturmayı da öğreneceksiniz.
Hazırsanız, web’in güçlü potansiyelini keşfetmeye ve kendi dijital koşu maceranızı yaratmaya başlayalım! Bu rehberin sonunda, sadece çalışan bir oyun değil, aynı zamanda modern web teknolojileri ve oyun geliştirme prensipleri hakkında derinlemesine bilgi sahibi olacaksınız.
Temel Konseptler: Bir Endless Runner Nasıl Çalışır?
Bir sonsuz koşu oyununun ardındaki temel mekanikler oldukça basittir, ancak bu basitlik etkili bir şekilde uygulandığında bağımlılık yaratıcı bir deneyim sunabilir. Öncelikle, her oyunun kalbinde bir “oyun döngüsü” yatar. Bu döngü, oyunun sürekli olarak güncellenmesini ve çizilmesini sağlayan merkezi bir mekanizmadır. Tipik bir oyun döngüsü iki ana aşamadan oluşur: güncellemeler (update) ve çizimler (render). Güncelleme aşamasında, oyuncunun konumu, engellerin hızı, skor gibi oyunun tüm dinamik elemanları hesaplanır ve değiştirilir. Çizim aşamasında ise, bu güncellenmiş durum ekranda görsel olarak sunulur. Tarayıcı tabanlı oyunlarda, bu döngü genellikle requestAnimationFrame API’ı kullanılarak optimize edilir, bu da tarayıcının ekran yenileme hızıyla senkronize olarak pürüzsüz animasyonlar elde etmemizi sağlar.
Oyunun temel yapı taşları “oyun nesneleri”dir. Bunlar genellikle oyuncu karakteri, engeller, arkaplan elemanları ve bazen toplanabilir öğeler (power-ups) gibi görsel ve mantıksal varlıklardır. Her oyun nesnesinin kendine ait bir konumu, boyutu, hızı ve durumu bulunur. TypeScript kullanırken, bu nesneleri temsil etmek için sınıflar (classes) oluşturmak mükemmel bir yaklaşımdır. Örneğin, bir Player sınıfı, karakterin zıplama mekaniklerini ve animasyonunu yönetirken, bir Obstacle sınıfı engellerin hareketini ve çarpışma özelliklerini kontrol edebilir. Bu nesneler, oyun döngüsü içinde sürekli olarak güncellenir ve çizilir.
Sonsuz koşu oyunlarında olmazsa olmaz mekaniklerden biri “çarpışma tespiti”dir. Bu, oyuncu karakterinin bir engele çarpıp çarpmadığını veya bir öğeyi toplayıp toplamadığını anlamak için kullanılır. En yaygın çarpışma tespit yöntemlerinden biri, iki dikdörtgenin (oyuncu ve engel) birbirine değip değmediğini kontrol etmektir (Axis-Aligned Bounding Box – AABB çarpışma tespiti). Eğer bir çarpışma tespit edilirse, oyunun “oyun bitti” (game over) durumuna geçmesi, skorun kaydedilmesi veya başka bir olayın tetiklenmesi gerekebilir.
Oyunun “skor sistemi” ise oyuncunun ne kadar süre hayatta kaldığını veya ne kadar mesafe kat ettiğini takip eder. Genellikle zamanla veya geçilen engel sayısıyla orantılı olarak artar. Bu, oyuncuyu daha yüksek bir skor elde etmek için tekrar oynamaya teşvik eden önemli bir geri bildirim mekanizmasıdır. Basit bir skor sayacı, bu tür oyunlar için başlangıçta fazlasıyla yeterli olacaktır.
Son olarak, tüm bu görsel elemanları ekranda göstermek için “HTML5 Canvas” öğesini kullanacağız. Canvas, JavaScript ile programatik olarak grafikler çizmenize olanak tanıyan bir HTML öğesidir. Dikdörtgenler, daireler, çizgiler çizebilir, resimler yükleyebilir ve metinler gösterebilirsiniz. Oyunumuzdaki her çizim işlemi, Canvas’ın 2D çizim bağlamı (CanvasRenderingContext2D) aracılığıyla gerçekleştirilecektir. TypeScript’in sunduğu güçlü tip sistemi, Canvas API’larını kullanırken hataları azaltmamıza ve kodumuzu daha güvenilir hale getirmemize yardımcı olacaktır. Bu temel konseptleri anladığımızda, kendi oyunumuzu inşa etmek için sağlam bir zeminimiz olacak.
Proje Kurulumu ve İlk Adımlar: TypeScript Ortamı Nasıl Hazırlanır?
Bir tarayıcı tabanlı sonsuz koşu oyunu geliştirmeye başlamadan önce, projemizin temelini oluşturan bir geliştirme ortamını kurmamız gerekiyor. TypeScript’in gücünden yararlanabilmek için öncelikle Node.js ve npm (Node Package Manager) veya yarn kurulu olmalıdır. Bunlar, TypeScript derleyicisini ve diğer bağımlılıkları yönetmemizi sağlayacak araçlardır. Eğer kurulu değillerse, Node.js’in resmi web sitesinden kolayca indirebilirsiniz.
- Proje Dizini Oluşturma ve Başlatma:
Öncelikle projemiz için bir klasör oluşturun ve terminal veya komut istemcisini kullanarak bu klasöre gidin. Ardından, npm’i kullanarak yeni bir proje başlatın:mkdir endless-runner cd endless-runner npm init -y
Bu komut, projenizin kök dizininde temel birpackage.jsondosyası oluşturacaktır. - TypeScript ve Gerekli Paketleri Yükleme:
Şimdi, TypeScript derleyicisini ve gelecekte işimize yarayabilecek bir canlı sunucu (live-server) paketini kuralım:npm install --save-dev typescript live-server
--save-devbayrağı, bu paketlerin sadece geliştirme ortamında kullanılacağını belirtir. - TypeScript Yapılandırması (tsconfig.json):
TypeScript'i projenizde nasıl derleyeceğinizi belirtmek için birtsconfig.jsondosyası oluşturmanız gerekir. Bu dosya, TypeScript derleyicisine (tsc) hangi dosyaları derleyeceğini, çıktı dizinini ve hangi ECMAScript sürümüne dönüştüreceğini söyler. Projenizin kök dizininde aşağıdaki komutu çalıştırarak varsayılan birtsconfig.jsondosyası oluşturabilirsiniz:npx tsc --init
Ardından,tsconfig.jsondosyasını açın ve aşağıdaki önemli ayarları güncelleyin (veya ekleyin):{ "compilerOptions": { "target": "ESNext", /* Çıkış JavaScript sürümünü ayarlar */ "module": "CommonJS", /* Modül üretme sistemini ayarlar */ "outDir": "./dist", /* Çıktı dizinini ayarlar */ "rootDir": "./src", /* Giriş dosyalarının kök dizinini ayarlar */ "strict": true, /* Tüm sıkı tip kontrol seçeneklerini etkinleştirir */ "esModuleInterop": true, /* CommonJS/ES modüllerini birlikte çalıştırır */ "skipLibCheck": true, /* Bildirim dosyalarında tip kontrolünü atlar */ "forceConsistentCasingInFileNames": true /* Aynı dosya için büyük/küçük harf kullanımında tutarlılık sağlar */ }, "include": ["src/**/*.ts"], /* Derlenecek dosyaları belirler */ "exclude": ["node_modules"] /* Hariç tutulacak dizinleri belirler */ }
Bu ayarlar, TypeScript kaynak kodunuzu (.tsdosyaları)srcklasöründen alacak, modern JavaScript'e (ESNext) derleyecek ve derlenmiş JavaScript çıktılarınıdistklasörüne yerleştirecektir. - HTML Yapısını Oluşturma (index.html):
Oyunumuzun çalışacağı temel HTML dosyasını, yaniindex.html'i projenizin kök dizinine oluşturalım. Bu dosya, oyunumuz için bir HTML5 Canvas elementi içerecek ve derlenmiş JavaScript kodumuzu yükleyecektir:TypeScript Endless Runner
Buradaelementi oyunumuzun görsel çıktısını alacağı alandır.satırı ise derlenmiş TypeScript kodumuzu (ki budist/main.jsolacak) tarayıcıya yükler. CSS kısmı, oyunun tarayıcıda ortalanmasını ve mobil cihazlarda ekran boyutuna uyum sağlamasını hedefleyen basit bir stil sağlar. Özellikle@mediasorguları, küçük ekranlarda Canvas'ın genişliğini ayarlayarak oyunun responsive olmasını sağlar. - İlk TypeScript Dosyasını Oluşturma (src/main.ts):
Şimdi,srcadında bir klasör oluşturun ve içinemain.tsadında bir dosya ekleyin. Bu, oyunumuzun ana giriş noktası olacaktır:mkdir src touch src/main.ts
src/main.tsdosyasına şimdilik basit bir "Merhaba Dünya" kodu ekleyelim:console.log("TypeScript Endless Runner'a Hoş Geldiniz!"); // Canvas elementini alalım const canvas = document.getElementById('gameCanvas') as HTMLCanvasElement; const ctx = canvas.getContext('2d'); if (ctx) { ctx.fillStyle = 'red'; ctx.fillRect(50, 50, 100, 100); // Kırmızı bir kare çiz console.log("Canvas başarıyla alındı ve çizim yapıldı."); } else { console.error("Canvas 2D bağlamı alınamadı."); } - Derleme ve Çalıştırma:
package.jsondosyanıza, TypeScript kodunu derlemek ve oyunu bir canlı sunucuyla başlatmak için bazı scriptler ekleyelim:{ // ... diğer ayarlar ... "scripts": { "build": "npx tsc", "start": "npm run build && live-server" }, // ... diğer ayarlar ... }
Şimdi terminalde aşağıdaki komutu çalıştırın:npm start
Bu komut önce TypeScript kodunuzu derleyecek (src/main.ts->dist/main.js), ardındanlive-serverile projenizi bir web sunucusunda yayınlayacaktır. Tarayıcınızda açılan sayfada, Canvas üzerinde kırmızı bir kare görmelisiniz. Konsolda ise "TypeScript Endless Runner'a Hoş Geldiniz!" ve "Canvas başarıyla alındı ve çizim yapıldı." mesajlarını göreceksiniz. Tebrikler! TypeScript oyun geliştirme ortamınızı başarıyla kurdunuz ve ilk çiziminizi yaptınız.Uzman İpucu: Projenizin boyutu büyüdükçe Webpack veya Rollup gibi modül paketleyicileri kullanmayı düşünebilirsiniz. Bu araçlar, birden fazla TypeScript dosyasını tek bir JavaScript dosyasına derleme, bağımlılıkları yönetme ve hatta kod sıkıştırma gibi gelişmiş özellikler sunar, bu da oyununuzun yükleme süresini ve performansını iyileştirebilir. Şimdiliknpx tscyeterli olsa da, gelecekteki projelerinizde bu araçları araştırmak ufkunuzu genişletecektir.
Oyun Nesneleri ve Animasyon: Karakterinizi ve Engellerinizi Hayata Geçirin
Oyunumuzun temel yapı taşlarını ve ortamını kurduktan sonra, sıra oyuna can verecek karakter ve engelleri tasarlamaya geliyor. TypeScript'in nesne yönelimli programlama yetenekleri sayesinde, bu unsurları ayrı ayrı yönetilebilir sınıflar olarak tanımlayabiliriz. Bu yaklaşım, kodumuzu daha düzenli, okunabilir ve bakımı kolay hale getirecektir. Her oyun nesnesinin kendi konumu, boyutu, hızı ve görünümü olacaktır. Özellikle bir sonsuz koşu oyununda, oyuncu sürekli hareket ederken engeller ona doğru ilerleyecek, bu da dinamik bir etkileşim alanı yaratacaktır.
Oyuncu Karakterini Tasarlama
Öncelikle oyuncu karakterimiz için bir Player sınıfı oluşturalım. Bu sınıf, karakterin konumunu (x, y koordinatları), boyutunu (genişlik, yükseklik), yerçekimi etkisini, zıplama gücünü ve mevcut hızını tutacaktır. Ayrıca, karakteri Canvas üzerine çizecek bir draw metodu ve her oyun döngüsünde durumunu güncelleyecek bir update metodu içerecektir. Zıplama mekaniği, oyuncunun y eksenindeki konumunu ve dikey hızını değiştirerek simüle edilir. Yerçekimi, karakterin zıpladıktan sonra tekrar yere inmesini sağlar.
src/Player.ts dosyasını oluşturalım:
// src/Player.ts
export class Player {
x: number;
y: number;
width: number;
height: number;
velocityY: number;
gravity: number;
isJumping: boolean;
onGround: boolean;
constructor(x: number, y: number, width: number, height: number) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.velocityY = 0;
this.gravity = 0.5; // Yerçekimi kuvveti
this.isJumping = false;
this.onGround = true;
}
jump() {
if (this.onGround) {
this.velocityY = -10; // Zıplama gücü
this.isJumping = true;
this.onGround = false;
}
}
update(canvasHeight: number) {
// Yerçekimi etkisi
this.velocityY += this.gravity;
this.y += this.velocityY;
// Zemine çarpma kontrolü
if (this.y + this.height > canvasHeight) {
this.y = canvasHeight - this.height;
this.velocityY = 0;
this.isJumping = false;
this.onGround = true;
}
}
draw(ctx: CanvasRenderingContext2D) {
ctx.fillStyle = 'blue'; // Oyuncuyu mavi bir dikdörtgen olarak çizelim
ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
Bu Player sınıfı, bir oyuncunun temel özelliklerini ve hareketlerini kapsar. update metodu, her karede karakterin pozisyonunu yerçekimi ve zıplama durumuna göre ayarlar. draw metodu ise karakteri Canvas üzerine basit bir dikdörtgen olarak çizer. Daha gelişmiş animasyonlar için, bu metodda sprite sheet'ler kullanarak kare kare animasyonlar gösterebiliriz. Örneğin, koşu animasyonu için ardışık resimler yükleyebilir ve her update döngüsünde bir sonraki kareye geçebiliriz.
Engelleri Oluşturma
Sıra geldi oyuncumuzun aşması gereken engellere. Engeller de tıpkı oyuncu gibi kendi konum, boyut ve hareket özelliklerine sahip olacaktır. Ancak engeller, oyuncuya doğru (ekranın sağından soluna) hareket eder ve ekrandan çıktıklarında geri dönüştürülmeleri veya yeni engellerin oluşturulması gerekir. Bu, oyunun sonsuzluğunu sağlayan önemli bir mekaniktir.
src/Obstacle.ts dosyasını oluşturalım:
// src/Obstacle.ts
export class Obstacle {
x: number;
y: number;
width: number;
height: number;
speed: number;
constructor(x: number, y: number, width: number, height: number, speed: number) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.speed = speed;
}
update() {
this.x -= this.speed; // Engeli sola doğru hareket ettir
}
draw(ctx: CanvasRenderingContext2D) {
ctx.fillStyle = 'green'; // Engeli yeşil bir dikdörtgen olarak çizelim
ctx.fillRect(this.x, this.y, this.width, this.height);
}
// Engelin ekran dışına çıkıp çıkmadığını kontrol etmek için
isOffscreen(): boolean {
return this.x + this.width < 0;
}
}
Bu Obstacle sınıfı, engelin başlangıç konumunu, boyutunu ve hızını tanımlar. update metodu, engeli her karede sola doğru hareket ettirir. isOffscreen metodu ise, engelin ekranın sol tarafından tamamen çıkıp çıkmadığını kontrol ederek, bu engeli yeniden kullanma veya yok etme kararı almamıza yardımcı olur. Bu engeller, oyunun zorluğunu ve çeşitliliğini artıran ana unsurlardır.
Paralaks Arkaplan ile Derinlik Katma
Bir sonsuz koşu oyununa derinlik ve hareket hissi katmanın etkili yollarından biri "paralaks arkaplan" kullanmaktır. Bu teknik, farklı hızlarda hareket eden birden fazla arkaplan katmanı oluşturarak, oyuncunun etrafındaki dünyanın hareketli olduğu izlenimini verir. Uzaktaki nesneler yavaş hareket ederken, yakın plan nesneler daha hızlı hareket eder. Bu, görsel olarak oldukça çekici bir etki yaratır ve oyunun daha dinamik hissetmesini sağlar. Arkaplan görsellerini Image objeleri olarak yükleyebilir ve her katman için farklı bir kaydırma hızı uygulayarak bu efekti Canvas üzerinde oluşturabiliriz.
Örneğin, src/Background.ts adında bir dosya oluşturup aşağıdaki gibi bir yapı kurabiliriz:
// src/Background.ts
export class Background {
image: HTMLImageElement;
x: number;
y: number;
width: number;
height: number;
speed: number;
constructor(imageSrc: string, y: number, width: number, height: number, speed: number) {
this.image = new Image();
this.image.src = imageSrc;
this.x = 0;
this.y = y;
this.width = width;
this.height = height;
this.speed = speed;
}
update() {
this.x -= this.speed;
if (this.x < -this.width) {
this.x = 0; // Arkaplanı başa sar
}
}
draw(ctx: CanvasRenderingContext2D) {
// İki kere çizerek sürekli kaydırma efekti veriyoruz
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
ctx.drawImage(this.image, this.x + this.width, this.y, this.width, this.height);
}
}
Bu Background sınıfı, bir resim dosyası alır, onu Canvas üzerine çizer ve belirli bir hızda kaydırır. update metodunda, resmin sol kenarı ekranın dışına çıktığında resmi tekrar başa sararak sonsuz bir kaydırma illüzyonu yaratırız. draw metodunda ise, resmin iki kopyasını yan yana çizerek bu sürekliliği sağlarız. Farklı hızlarda birden fazla Background objesi oluşturarak paralaks etkisini elde edebiliriz. Örneğin, bulutlar için yavaş, ağaçlar için orta, zemin için hızlı bir hız ayarlayarak daha gerçekçi bir derinlik hissiyatı yaratabiliriz.
Bu sınıfları oluşturduktan sonra, src/main.ts dosyamızda bunları import edip oyunumuzun ana döngüsünde kullanmaya başlayabiliriz. Bu nesneler, oyunun görsel ve etkileşimli deneyimini oluşturan temel bileşenlerdir. Gelecek bölümde, bu nesneleri bir araya getirerek oyunun ana mantığını ve etkileşimlerini nasıl yöneteceğimizi inceleyeceğiz.
Oyun Mantığı ve Etkileşim: Çarpışma, Skor ve Sonsuz Koşu Mekanikleri
Oyuncu ve engel nesnelerimizi tanımladıktan sonra, bu nesneleri bir araya getirip onlara hayat verecek olan oyun mantığını inşa etme zamanı geldi. Bu aşama, oyunumuzun "sonsuz" ve "koşu" kısmını tanımlayacak mekanikleri içerir: oyun döngüsü, engellerin üretimi ve geri dönüştürülmesi, çarpışma tespiti, skor takibi ve tabii ki kullanıcı girişi. Tüm bu elementler, birbiriyle uyumlu bir şekilde çalışarak akıcı ve bağımlılık yapıcı bir deneyim sunar.
Ana Oyun Döngüsünü Kurma
Daha önce bahsettiğimiz gibi, oyun döngüsü her oyunun kalbidir. Tarayıcı tabanlı oyunlarda, bu döngü genellikle requestAnimationFrame API'ı ile oluşturulur. Bu fonksiyon, tarayıcının bir sonraki ekran yenilemesinden önce belirli bir fonksiyonu çağırmasını ister, bu da animasyonların daha pürüzsüz olmasını sağlar ve CPU kullanımını optimize eder. Oyunumuzun tüm durum güncellemeleri ve çizim işlemleri bu döngü içinde gerçekleşecektir.
src/Game.ts adında bir dosya oluşturalım ve ana oyun mantığını buraya taşıyalım:
// src/Game.ts
import { Player } from './Player';
import { Obstacle } from './Obstacle';
import { Background } from './Background'; // Paralaks arkaplan için
export class Game {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
player: Player;
obstacles: Obstacle[];
backgrounds: Background[]; // Birden fazla arkaplan katmanı için
score: number;
gameOver: boolean;
gameSpeed: number;
lastObstacleTime: number;
obstacleInterval: number; // Engel üretim aralığı
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!; // ! ile null kontrolünü geçebiliriz, çünkü main.ts'de kontrol ettik
this.player = new Player(50, this.canvas.height - 70, 50, 50); // Oyuncuyu zemine yakın konumlandır
this.obstacles = [];
this.backgrounds = [];
this.score = 0;
this.gameOver = false;
this.gameSpeed = 5;
this.lastObstacleTime = 0;
this.obstacleInterval = 1500; // Her 1.5 saniyede bir engel üret
// Arkaplan katmanlarını ekle (örnek görseller kullandım)
this.backgrounds.push(new Background('https://via.placeholder.com/800x400/87CEEB/ffffff?text=Sky', 0, this.canvas.width, this.canvas.height, 0.5)); // Gökyüzü (en yavaş)
this.backgrounds.push(new Background('https://via.placeholder.com/800x400/6B8E23/ffffff?text=Hills', this.canvas.height - 200, this.canvas.width, 200, 1.5)); // Tepeler (orta)
this.backgrounds.push(new Background('https://via.placeholder.com/800x400/556B2F/ffffff?text=Ground', this.canvas.height - 50, this.canvas.width, 50, this.gameSpeed)); // Zemin (en hızlı, oyun hızıyla aynı)
this.setupEventListeners();
}
setupEventListeners() {
document.addEventListener('keydown', e => {
if (e.code === 'Space' && !this.gameOver) {
this.player.jump();
}
if (e.code === 'KeyR' && this.gameOver) {
this.restartGame();
}
});
this.canvas.addEventListener('touchstart', (e) => {
if (!this.gameOver) {
this.player.jump();
}
e.preventDefault(); // Mobil tarayıcıların varsayılan kaydırma davranışını engeller
});
// Dokunmatik cihazlar için oyun bitiminde yeniden başlatma
this.canvas.addEventListener('click', () => {
if (this.gameOver) {
this.restartGame();
}
});
}
generateObstacle(currentTime: number) {
if (currentTime - this.lastObstacleTime > this.obstacleInterval) {
const obstacleWidth = Math.random() * 30 + 30; // 30-60 arası genişlik
const obstacleHeight = Math.random() * 40 + 40; // 40-80 arası yükseklik
const obstacleX = this.canvas.width;
const obstacleY = this.canvas.height - obstacleHeight; // Zemine oturmasını sağla
this.obstacles.push(new Obstacle(obstacleX, obstacleY, obstacleWidth, obstacleHeight, this.gameSpeed));
this.lastObstacleTime = currentTime;
// Zamanla engel üretim aralığını kısaltarak zorluğu artır
this.obstacleInterval = Math.max(800, this.obstacleInterval - 50);
// Oyun hızını artır
this.gameSpeed += 0.1;
this.backgrounds.forEach(bg => {
// Sadece zemin katmanının hızını oyun hızıyla senkronize et
if (bg.image.src.includes('Ground')) { // veya başka bir tanımlayıcı kullan
bg.speed = this.gameSpeed;
}
});
}
}
checkCollision(player: Player, obstacle: Obstacle): boolean {
// AABB (Axis-Aligned Bounding Box) çarpışma tespiti
return player.x < obstacle.x + obstacle.width &&
player.x + player.width > obstacle.x &&
player.y < obstacle.y + obstacle.height &&
player.y + player.height > obstacle.y;
}
update(deltaTime: number) {
if (this.gameOver) return;
this.player.update(this.canvas.height);
// Arkaplanları güncelle
this.backgrounds.forEach(bg => bg.update());
// Engelleri güncelle ve çarpışma kontrolü yap
this.obstacles.forEach((obstacle, index) => {
obstacle.speed = this.gameSpeed; // Engellerin hızı oyun hızıyla senkronize olsun
obstacle.update();
if (this.checkCollision(this.player, obstacle)) {
this.gameOver = true;
console.log("Game Over! Skor: " + this.score);
}
// Engelleri ekran dışına çıktığında temizle
if (obstacle.isOffscreen()) {
this.obstacles.splice(index, 1);
this.score++; // Engel geçildiğinde skoru artır
}
});
this.generateObstacle(performance.now());
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // Her karede Canvas'ı temizle
// Arkaplanları çiz
this.backgrounds.forEach(bg => bg.draw(this.ctx));
this.player.draw(this.ctx);
this.obstacles.forEach(obstacle => obstacle.draw(this.ctx));
// Skoru çiz
this.ctx.fillStyle = 'black';
this.ctx.font = '24px Arial';
this.ctx.fillText(Skor: ${this.score}, 10, 30);
// Game Over ekranı
if (this.gameOver) {
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = 'white';
this.ctx.font = '48px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText('OYUN BİTTİ!', this.canvas.width / 2, this.canvas.height / 2 - 30);
this.ctx.font = '24px Arial';
this.ctx.fillText(Son Skor: ${this.score}, this.canvas.width / 2, this.canvas.height / 2 + 10);
this.ctx.fillText('Yeniden Başlamak İçin "R" Tuşuna Basın veya Tıklayın', this.canvas.width / 2, this.canvas.height / 2 + 50);
}
}
restartGame() {
this.player = new Player(50, this.canvas.height - 70, 50, 50);
this.obstacles = [];
this.score = 0;
this.gameOver = false;
this.gameSpeed = 5;
this.lastObstacleTime = 0;
this.obstacleInterval = 1500;
this.backgrounds.forEach(bg => {
if (bg.image.src.includes('Ground')) {
bg.speed = this.gameSpeed;
}
bg.x = 0; // Arkaplanı da sıfırla
});
}
}
Ana main.ts Dosyasını Güncelleme
Şimdi src/main.ts dosyasını, Game sınıfımızı kullanarak oyun döngüsünü başlatacak şekilde güncelleyelim:
// src/main.ts
import { Game } from './Game';
console.log("TypeScript Endless Runner başlatılıyor...");
const canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d');
if (ctx) {
const game = new Game(canvas);
let lastTime = 0;
function gameLoop(timestamp: number) {
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
game.update(deltaTime);
game.draw();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop); // Oyun döngüsünü başlat
console.log("Oyun döngüsü başlatıldı.");
} else {
console.error("Canvas 2D bağlamı alınamadı. Oyun başlatılamıyor.");
}
Yukarıdaki kodda, gameLoop fonksiyonu, requestAnimationFrame tarafından sürekli olarak çağrılır. Her çağrıda game.update() metodu oyunun mantığını güncellerken (oyuncu hareketi, engel ilerlemesi, çarpışma kontrolü vb.), game.draw() metodu ise tüm nesneleri Canvas üzerine çizer. Bu yapı, oyunumuzun akıcı bir şekilde çalışmasını ve sürekli güncellenmesini sağlar. Özellikle mobil cihazlarda dokunmatik kontroller için touchstart olay dinleyicisini eklemeyi unutmadık. Bu sayede oyun hem klavye hem de dokunmatik ekranlar üzerinden kontrol edilebilir hale gelir.
Nesne Havuzu (Object Pooling) ile Performans İyileştirmesi
Oyun ilerledikçe, sürekli olarak yeni engeller oluşturup eski engelleri bellekten atmak (garbage collection) performansı olumsuz etkileyebilir. Özellikle tarayıcı ortamlarında bu durum daha belirgin olabilir. Bu sorunu aşmak için "nesne havuzu" (object pooling) tekniğini kullanabiliriz. Bu yaklaşımda, belirli sayıda engeli (veya diğer oyun nesnelerini) önceden oluşturur ve bir havuzda tutarız. Bir engele ihtiyaç duyulduğunda, havuzdan mevcut bir engeli alıp konumunu ve özelliklerini sıfırlayarak kullanırız. Engel ekran dışına çıktığında ise onu bellekten atmak yerine havuza geri göndeririz. Bu, bellek ayırma ve serbest bırakma işlemlerinin maliyetini ortadan kaldırarak performansı artırır.
generateObstacle metodunu nesne havuzu kullanacak şekilde optimize edebiliriz. Örneğin, bir availableObstacles dizisi oluşturup, engel dışarı çıktığında buraya ekleyebilir, yeni engel gerektiğinde ise önce bu diziden almayı deneyebiliriz. Bu, özellikle kaynakları kısıtlı cihazlarda veya çok sayıda nesnenin olduğu oyunlarda büyük bir performans kazancı sağlayacaktır. Ancak bu rehberin kapsamı gereği, mevcut basit obstacles.push ve splice kullanımı başlangıç için yeterlidir ve konuyu karmaşıklaştırmayacaktır.
gameSpeed değişkenini düzenli aralıklarla artırarak veya skorla orantılı olarak değiştirerek oyunun zorluk seviyesini dinamik olarak ayarlayabilirsiniz. Unutmayın, iyi bir zorluk eğrisi, oyuncuların oyunu bırakmamasını sağlar. Ayrıca, mobil uyumluluk için touchstart ve click event'lerini kullanmak, oyununuzun daha geniş bir kitleye ulaşmasına yardımcı olur.
İleri Düzey İpuçları ve Performans Optimizasyonu: Oyununuzu Bir Sonraki Seviyeye Taşıyın
Temel bir sonsuz koşu oyununu başarılı bir şekilde oluşturduktan sonra, sıra onu daha cilalı, performanslı ve kullanıcı dostu hale getirmeye geliyor. Bu bölümde, oyununuzu bir sonraki seviyeye taşıyacak ileri düzey ipuçları ve performans optimizasyon tekniklerinden bahsedeceğiz. Bu teknikler, sadece deneyimli geliştiriciler için değil, aynı zamanda projesini daha profesyonel hale getirmek isteyen her seviyeden geliştirici için değerlidir.
Asset Yükleme ve Yönetimi
Oyununuzdaki görselleri (sprite sheet'ler, arkaplan resimleri), ses efektlerini ve müzikleri önceden yüklemek, oyunun başlangıcında oluşabilecek takılmaları önler ve daha pürüzsüz bir kullanıcı deneyimi sunar. Bir "Asset Yöneticisi" veya basit bir yükleyici fonksiyonu oluşturarak tüm görsel ve ses dosyalarını oyun başlamadan önce hafızaya alabilirsiniz. Bu işlem tamamlandığında, oyunu başlatan bir geri arama (callback) fonksiyonu çağırabilirsiniz. Bu, özellikle oyununuzun açılışında bir yükleme ekranı (loading screen) göstermek istediğinizde kritik öneme sahiptir.
// Basit bir asset yükleyici örneği
class AssetLoader {
images: { [key: string]: HTMLImageElement } = {};
totalAssets: number = 0;
loadedAssets: number = 0;
onComplete: (() => void) | null = null;
load(assets: { name: string; src: string }[], onComplete: () => void) {
this.totalAssets = assets.length;
this.onComplete = onComplete;
if (this.totalAssets === 0) {
this.onComplete?.();
return;
}
assets.forEach(asset => {
const img = new Image();
img.onload = () => {
this.loadedAssets++;
if (this.loadedAssets === this.totalAssets) {
this.onComplete?.();
}
};
img.onerror = () => {
console.error(Resim yüklenemedi: ${asset.src});
this.loadedAssets++; // Hata olsa bile sayacı artır
if (this.loadedAssets === this.totalAssets) {
this.onComplete?.();
}
};
img.src = asset.src;
this.images[asset.name] = img;
});
}
getImage(name: string): HTMLImageElement | undefined {
return this.images[name];
}
}
// Kullanım örneği:
// const loader = new AssetLoader();
// loader.load([
// { name: 'playerSprite', src: 'assets/player.png' },
// { name: 'obstacleSprite', src: 'assets/obstacle.png' }
// ], () => {
// console.log('Tüm assetler yüklendi, oyunu başlatabiliriz!');
// // Oyun başlatma mantığı buraya
// });
Canvas Optimizasyonları
Canvas üzerinde çizim yapmak pahalı bir işlemdir, özellikle her karede çok sayıda nesne çiziyorsanız. Performansı artırmak için bazı teknikler kullanabiliriz:
clearRectKullanımı: Her karede tüm Canvas'ı temizlemek yerine, sadece değişen bölgeleri temizlemeyi düşünebilirsiniz. Ancak çoğu durumdaclearRect(0, 0, width, height)performansa çok büyük bir etki yapmaz ve kodun daha basit kalmasını sağlar.- Offscreen Canvas (Çift Tamponlama): Karmaşık sahneleri arka plandaki görünmez bir Canvas'a çizip, sonra tek bir işlemle bu Canvas'ı ana Canvas'a kopyalamak, ekran yırtılmasını (tearing) azaltabilir ve daha pürüzsüz animasyonlar sağlayabilir. Özellikle yüksek çözünürlüklü veya çok sayıda elemanı olan oyunlarda faydalıdır.
- Pixel Snapping: Nesneleri tam piksel koordinatlarına hizalamak (
Math.floor(x)veyaMath.round(x)kullanarak), tarayıcının anti-aliasing (kenar yumuşatma) işlemlerinden kaçınmasını sağlayarak çizim performansını artırabilir. save()verestore()Yeri: Canvas bağlamının durumunu (dönüşümler, stiller vb.) sık sık kaydetmek ve geri yüklemek performans maliyeti yaratabilir. Yalnızca gerçekten gerektiğinde kullanmaya özen gösterin.
Mobil Uyumluluk ve Dokunmatik Kontroller
Oyununuzun mobil cihazlarda da sorunsuz çalışması için meta viewport etiketini kullanmak zorunludur:
user-scalable=no ile kullanıcıların yakınlaştırmasını engelleyebiliriz. Dokunmatik kontroller için ise touchstart, touchmove ve touchend olaylarını kullanmalısınız. Ayrıca, mobil tarayıcıların varsayılan kaydırma davranışlarını engellemek için olay dinleyicilerinizde e.preventDefault() çağırmayı unutmayın. Makalenin önceki bölümlerinde verdiğimiz CSS media query örnekleri, Canvas'ın farklı ekran boyutlarına uyum sağlamasına yardımcı olacaktır.
Oyun Durum Yönetimi
Oyununuz büyüdükçe, farklı durumları (ana menü, oyun içi, oyun bitti, duraklatıldı) yönetmek karmaşık hale gelebilir. Bir "durum makinesi" (state machine) deseni kullanarak bu durumlar arasındaki geçişleri daha düzenli bir şekilde yönetebilirsiniz. Her durum, kendi update ve draw mantığını içerebilir.
// Basit bir durum makinesi arayüzü
interface GameState {
enter(): void;
update(deltaTime: number): void;
draw(ctx: CanvasRenderingContext2D): void;
exit(): void;
}
// Örneğin, bir menü durumu
class MainMenuState implements GameState {
enter() { console.log('Ana Menüye Giriş'); }
update(deltaTime: number) { /* Menü mantığı */ }
draw(ctx: CanvasRenderingContext2D) {
ctx.fillStyle = 'black';
ctx.font = '30px Arial';
ctx.fillText('BAŞLAMAK İÇİN TIKLA', 400, 200);
}
exit() { console.log('Ana Menüden Çıkış'); }
}
// Game sınıfı içinde durumları yönetebiliriz
// let currentState: GameState;
// currentState = new MainMenuState();
// ...
// currentState.update(deltaTime);
// currentState.draw(ctx);
Ses Efektleri ve Müzik
Ses efektleri ve fon müziği, oyun deneyimini zenginleştiren kritik unsurlardır. HTML5 Audio API'si veya Web Audio API'si (daha gelişmiş kontrol için) kullanılarak sesler kolayca eklenebilir. Zıplama, çarpışma veya puan kazanma gibi olaylar için kısa ses efektleri, oyunun daha etkileşimli hissetmesini sağlar. Fon müziği ise oyunun atmosferini güçlendirir.
// Basit bir ses efekti çalma örneği
function playSound(src: string) {
const audio = new Audio(src);
audio.play().catch(e => console.error("Ses çalınamadı:", e));
}
// Örneğin, zıplama sesi
// playSound('assets/jump.mp3');
Bu ileri düzey ipuçları ve optimizasyon stratejileri, oyununuzu sadece işlevsel olmaktan çıkarıp, gerçekten keyifli ve akılda kalıcı bir deneyim haline getirecektir. Her birini denemek, web tabanlı oyun geliştirme becerilerinizi daha da keskinleştirecektir.
Vaka Çalışması: Popüler Endless Runner Oyunlarından İlham
Endless runner türünün popüler örnekleri olan Subway Surfers ve Temple Run, basit bir koşu mekaniğini çeşitli özelliklerle nasıl zenginleştirebileceğimizi gösteriyor. Bu oyunlar, temel döngüyü sürdürürken, oyuncunun ilgisini canlı tutacak unsurlar ekleyerek uzun süreli bir oynanabilirlik sunar.
- Subway Surfers: Bu oyunda karakter, tren rayları üzerinde sürekli koşar ve engellerin üzerinden atlar, altından geçer veya yana doğru hareket eder. Bizim basit oyunumuzda sadece zıplama mekaniği varken, Subway Surfers'ın yanal hareket ve engellerden kaçınma çeşitliliği, oyuncuya daha fazla kontrol ve stratejik düşünme alanı sunar. Örneğin, bizim oyunumuza farklı "şeritler" ekleyebilir ve oyuncunun bu şeritler arasında geçiş yapmasını sağlayabiliriz. Power-up'lar (örneğin, jetpack veya mıknatıs) ve görevler (belirli sayıda para toplama, belirli bir mesafeyi kat etme) oyuncuyu oyunda tutar. Ayrıca, farklı karakterler ve özelleştirme seçenekleri de oyuncunun oyuna bağlılığını artırır.
- Temple Run: Bu oyun ise 3D bir ortamda geçen bir sonsuz koşu oyunudur ve kamera perspektifi karakterin arkasından takip eder. Engellerden kaçınmanın yanı sıra, virajlara girmeyi ve uçurumlardan atlamayı da gerektirir. Bizim 2D Canvas tabanlı oyunumuzda doğrudan 3D perspektifi taklit etmek zor olsa da, Temple Run'ın dinamik kamera hareketleri ve hızlı karar verme mekanikleri bize ilham verebilir. Örneğin, oyun hızını zamanla artırmak, farklı engel türleri (yerdeki engeller, havadan gelen engeller) eklemek veya belirli aralıklarla görsel temayı değiştirmek (gündüz/gece döngüsü gibi) oyunun monotonluğunu kırabilir.
Bu oyunlardan ilham alarak, kendi oyunumuza ekleyebileceğimiz özellikler şunlar olabilir:
- Zorluk Eğrisi: Oyun hızı ve engel üretim aralığının zamanla kademeli olarak artırılması. Bizim örneğimizde bu mekaniği basitçe uyguladık, ancak daha karmaşık algoritmalarla (örneğin skorla orantılı artış) daha dinamik bir zorluk sağlayabiliriz.
- Engel Çeşitliliği: Sadece dikdörtgen engeller yerine, farklı boyutlarda, yüksekliklerde veya hareket paternlerine sahip engeller eklemek. Örneğin, havadan düşen engeller veya daha geniş atlama gerektiren çift engeller.
- Toplanabilir Öğeler (Collectibles): Puanı artıran veya geçici yetenekler (daha hızlı koşma, yenilmezlik) veren paralar veya güçlendirmeler eklemek. Bu, oyuncuya kaçınmanın ötesinde bir hedef verir.
- Paralaks Arkaplan Geliştirmesi: Daha fazla katman ve daha detaylı görseller kullanarak daha zengin bir görsel deneyim sunmak.
- Görsel Geri Bildirim: Oyuncu zıpladığında, çarptığında veya puan aldığında görsel efektler (parçacıklar, flaş efektleri) eklemek.
Bu vaka analizleri, temel bir oyun motorunun üzerine nasıl katmanlar inşa ederek daha zengin ve ilgi çekici bir deneyim oluşturabileceğimizi gösterir. Basit bir sonsuz koşucu, bu eklemelerle birlikte saatlerce oynanabilir bir hal alabilir.
Sonuç: Kendi Sonsuz Koşu Oyununuzu Geliştirme Yolculuğunuz
Bu kapsamlı rehber boyunca, TypeScript'in gücünü kullanarak tarayıcı tabanlı bir sonsuz koşu oyunu geliştirmenin adımlarını detaylı bir şekilde inceledik. Başlangıçta basit bir proje kurulumuyla temel altyapıyı oluşturduk, ardından oyuncu ve engel gibi ana oyun nesnelerini tanımladık. HTML5 Canvas üzerinde çizim yapmayı, oyun döngüsünü yönetmeyi, çarpışma tespiti ve skor takibi gibi temel oyun mekaniklerini uygulamayı öğrendik. Ayrıca, paralaks arkaplan gibi görsel zenginlik katan özelliklere ve mobil uyumluluk, performans optimizasyonu ve asset yönetimi gibi ileri düzey konulara da değindik. Subway Surfers ve Temple Run gibi popüler örneklerden ilham alarak kendi oyunumuzu daha da geliştirmenin yollarını keşfettik.
Bu süreç, sadece bir oyun geliştirmeyi değil, aynı zamanda modern web geliştirme prensiplerini, nesne yönelimli programlama yaklaşımlarını ve tarayıcının güçlü grafik yeteneklerini anlama fırsatı sundu. TypeScript'in sağladığı tip güvenliği ve kod düzeni, büyük projelerde karmaşıklığı yönetme konusunda paha biçilmez bir yardımcı olduğunu bir kez daha gösterdi. Artık kendi yaratıcı fikirlerinizi hayata geçirebilecek sağlam bir temel bilgi birikimine sahipsiniz.
Bu noktadan sonra, oyununuzu kişiselleştirmek ve yeni özellikler eklemek tamamen sizin elinizde. Belki farklı engel türleri, toplanabilir eşyalar, güçlendirmeler, lider tabloları veya daha karmaşık bir görsel tema eklemek isteyebilirsiniz. Unutmayın, oyun geliştirme sürekli bir öğrenme ve deneme sürecidir. Bu rehber, başlangıç noktanız oldu; şimdi sıra sizde, bu bilgiyi kullanarak kendi benzersiz dijital maceralarınızı yaratın. Başarılar dileriz!
Sıkça Sorulan Sorular (SSS)
- TypeScript'i JavaScript'e tercih etmemin nedenleri nelerdir?
- TypeScript, JavaScript'in tip tanımlı bir üst kümesidir. Yani, JavaScript kodunuz TypeScript kodu olarak geçerlidir. Ancak TypeScript, özellikle büyük ölçekli projelerde tip güvenliği, geliştirme zamanında hata tespiti, kodun okunabilirliği ve bakımı kolaylığı gibi avantajlar sunar. Bu, özellikle karmaşık oyun mantığı geliştirirken hataları azaltır ve daha sağlam bir kod tabanı oluşturmanıza yardımcı olur.
- Tarayıcı tabanlı oyunlar için başka framework'ler veya kütüphaneler var mı?
- Evet, birçok popüler oyun motoru ve kütüphane bulunmaktadır. Örneğin, Phaser, PixiJS ve Babylon.js gibi kütüphaneler, sprite animasyonları, fizik motorları, sahne yönetimi ve daha fazlası gibi hazır çözümler sunarak oyun geliştirme sürecini büyük ölçüde hızlandırabilir. Bu rehberde Canvas API'ını doğrudan kullanarak temel prensipleri öğrenmenizi sağladık, ancak daha karmaşık oyunlar için bu kütüphaneleri araştırmanızı şiddetle tavsiye ederiz.
- Oyunumu farklı ekran boyutlarına ve cihazlara nasıl uyumlu hale getirebilirim?
- Oyununuzun responsive (duyarlı) olmasını sağlamak için CSS media query'lerini kullanarak Canvas elementinin boyutunu ayarlayabilirsiniz. Ayrıca, oyun içindeki nesnelerin boyutlarını ve konumlarını, Canvas'ın mevcut boyutlarına göre dinamik olarak ölçeklendiren bir oranlama stratejisi (örneğin, tüm koordinatları ve boyutları Canvas'ın mevcut genişliğine/yüksekliğine göre oranlamak) kullanmak en etkili yöntemdir. Dokunmatik cihazlar için ise fare olayları yerine
touchstart,touchmove,touchendgibi dokunmatik olayları dinlemelisiniz. - Oyunuma ses efektleri ve fon müziği nasıl ekleyebilirim?
- Oyununuza ses efektleri ve fon müziği eklemek için HTML5 Audio API'sini kullanabilirsiniz. Basit ses dosyaları için
new Audio('path/to/sound.mp3').play()yeterlidir. Daha gelişmiş ses kontrolü (örneğin, ses seviyesi ayarı, efektler, mekansal ses) için Web Audio API'si daha güçlü bir seçenek sunar. Ses dosyalarını oyun başlamadan önce yüklemeyi ve mobil cihazlarda otomatik oynatma kısıtlamalarını dikkate almayı unutmayın (genellikle ilk kullanıcı etkileşimi sonrası ses çalınabilir). - Geliştirdiğim bu oyunu nasıl yayınlayabilirim?
- Tarayıcı tabanlı bir oyunu yayınlamak oldukça basittir. Projenizin tüm derlenmiş JavaScript dosyalarını (
.js), HTML dosyasını (index.html) ve tüm assetleri (resimler, sesler) bir web sunucusuna yüklemeniz yeterlidir. GitHub Pages, Netlify, Vercel veya basit bir hosting sağlayıcısı gibi ücretsiz veya uygun fiyatlı platformlar üzerinden kolayca yayınlayabilirsiniz. Oyununuzu bir klasöre toplayıp bu klasörü sunucuya yüklediğinizde, oyununuz internet üzerinden erişilebilir olacaktır.