<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title><![CDATA[Cinevva Eğitimler]]></title>
        <link>https://app.cinevva.com</link>
        <description><![CDATA[Web oyunu geliştirenler için pratik eğitimler.]]></description>
        <lastBuildDate>Sun, 16 Aug 2026 06:38:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>scripts/build-rss.mjs</generator>
        <copyright>Copyright 2024-present Cinevva</copyright>
        <item>
            <title><![CDATA[Ajan tabanlı yapay zekâ kod araçları]]></title>
            <link>https://app.cinevva.com/tr/tutorials/agentic-code-tools</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/agentic-code-tools</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Claude Code, Cursor ve GitHub Copilot'ta uzmanlaşın: ajan tabanlı iş akışlarını, en iyi uygulamaları ve en yüksek verimlilik için hangi aracın ne zaman kullanılacağını öğrenin.]]></description>
            <content:encoded><![CDATA[<h1>Ajan tabanlı yapay zekâ kod araçları</h1>
<p>Yapay zekâ kodlama asistanları, basit otomatik tamamlamadan plan yapabilen, birden fazla dosyayı düzenleyebilen, testleri çalıştırabilen ve pull request oluşturabilen otonom ajanlara dönüştü. Bu eğitimde üç önemli aracı ele alacağız: Claude Code, Cursor ve GitHub Copilot.</p>
<h2>1) Bir aracı &quot;ajan tabanlı&quot; yapan nedir?</h2>
<p>Geleneksel yapay zekâ kod tamamlama araçları, sonraki birkaç satırı önerir. Ajan tabanlı araçlar ise daha ileri gider:</p>
<table>
<thead>
<tr>
<th>Yetenek</th>
<th>Otomatik tamamlama</th>
<th>Ajan tabanlı</th>
</tr>
</thead>
<tbody>
<tr>
<td>Kapsam</td>
<td>Geçerli satır/fonksiyon</td>
<td>Tüm kod tabanı</td>
</tr>
<tr>
<td>Planlama</td>
<td>Yok</td>
<td>Çok adımlı akıl yürütme</td>
</tr>
<tr>
<td>Yürütme</td>
<td>Yalnızca öneriler</td>
<td>Dosyaları düzenleyebilir, komutları çalıştırabilir</td>
</tr>
<tr>
<td>Bağlam</td>
<td>Geçerli dosya</td>
<td>Deponun tamamını anlama</td>
</tr>
<tr>
<td>İş akışı</td>
<td>Tepkisel</td>
<td>Görevleri otonom olarak tamamlama</td>
</tr>
</tbody>
</table>
<p>Ajan tabanlı araçlar projenizin tüm yapısını anlayabilir, birden fazla dosyaya yayılan değişiklikleri planlayabilir, bu değişiklikleri uygulayabilir, testleri çalıştırabilir ve görev tamamlanana kadar yineleme yapabilir.</p>
<h2>2) Hızlı karşılaştırma: Claude Code, Cursor ve Copilot</h2>
<table>
<thead>
<tr>
<th>Özellik</th>
<th>Claude Code</th>
<th>Cursor</th>
<th>GitHub Copilot</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Arayüz</strong></td>
<td>CLI, web, IDE eklentileri</td>
<td>Tam IDE (VS Code çatalı)</td>
<td>IDE uzantıları</td>
</tr>
<tr>
<td><strong>Bağlam penceresi</strong></td>
<td>Büyük (uzun bağlamlı Opus)</td>
<td>Tüm depo indeksleme</td>
<td>Daha küçük, gelişiyor</td>
</tr>
<tr>
<td><strong>Çoklu dosya düzenleme</strong></td>
<td>Güçlü</td>
<td>Mükemmel</td>
<td>Gelişiyor</td>
</tr>
<tr>
<td><strong>Plan modu</strong></td>
<td>Var</td>
<td>Var</td>
<td>Var (daha yeni)</td>
</tr>
<tr>
<td><strong>Fiyatlandırma</strong></td>
<td>Pro/Max katmanları</td>
<td>Krediler + abonelik</td>
<td>Abonelik</td>
</tr>
<tr>
<td><strong>En uygun kullanım</strong></td>
<td>Mimari, büyük yeniden düzenlemeler</td>
<td>Etkileşimli geliştirme</td>
<td>Satır içi kodlama</td>
</tr>
</tbody>
</table>
<h2>3) Claude Code'un temelleri</h2>
<p>Claude Code, geniş bağlam pencerelerine sahip Opus 4.8 (güncel varsayılan modeli) üzerinde çalışır. Kurulum ve kimlik doğrulama:</p>
<pre><code class="language-bash"># Claude Code'u yükleyin (yerel yükleyici, önerilen)
curl -fsSL https://claude.ai/install.sh | bash

# Ya da npm aracılığıyla (Node 18+ gerekir)
npm install -g @anthropic-ai/claude-code

# Kimlik doğrulama: claude'u çalıştırın ve tarayıcıdaki istemi izleyin
claude

# Projenizde başlatın
cd your-project
claude
</code></pre>
<h3>Temel komutlar</h3>
<pre><code class="language-bash"># Kod tabanınız hakkında soru sorun
claude &quot;Kimlik doğrulama burada nasıl uygulanmış?&quot;

# Kod oluşturun
claude &quot;Bir hız sınırlayıcı ara yazılımı ekle&quot;

# Çoklu dosya yeniden düzenlemesi
claude &quot;Kod tabanının tamamında UserService adını AuthService olarak değiştir&quot;

# PR oluşturun
claude &quot;Tüm API uç noktalarına girdi doğrulaması ekleyen bir PR oluştur&quot;
</code></pre>
<h3>Proje yapılandırması</h3>
<p>Proje bağlamını deponuzun kökündeki bir <code>CLAUDE.md</code> dosyasına yerleştirin. Claude Code, her oturumun başında bu dosyayı otomatik olarak okur:</p>
<pre><code class="language-markdown"># Proje notları

- Dil: TypeScript, Prettier ile biçimlendirilir
- Testler: Vitest, tests/ içinde
- node_modules/ veya dist/ dizinlerine dokunma
</code></pre>
<p>Makine tarafından okunabilen ayarlar (araç izinleri, ortam değişkenleri, kancalar) <code>.claude/settings.json</code> dosyasına yazılır. Örneğin, yıkıcı araçlar çalıştırılmadan önce onay istemek için izinleri burada yapılandırın.</p>
<h3>Bellek ve bağlam</h3>
<p>Kalıcı proje bağlamı, Claude Code'un her oturumun başında yüklediği <a href="http://CLAUDE.md">CLAUDE.md</a> dosyalarında ve düzeltmeleriniz ile tercihlerinize göre Claude'un kendisinin güncellediği otomatik bellek deposunda tutulur. Kalıcı bağlam sağlamak için tek seferlik bir &quot;Hatırla:&quot; istemi vermek yerine <a href="http://CLAUDE.md">CLAUDE.md</a> dosyasını düzenleyin:</p>
<pre><code class="language-markdown"># CLAUDE.md
Bu bir monorepo'dur. Paylaşılan kod packages/ içinde bulunur.
</code></pre>
<p>Çalışma bağlamını sıfırlamak için oturum içinde <code>/clear</code> komutunu kullanın.</p>
<h2>4) Cursor IDE'nin ajan tabanlı özellikleri</h2>
<p>Cursor, ajan tabanlı iş akışları etrafında oluşturulmuştur. Temel özellikler:</p>
<h3>Ajan modu</h3>
<p>Composer'ı Ajan modunda açmak için <code>Cmd+Shift+I</code> (Mac) veya <code>Ctrl+Shift+I</code> (Windows/Linux) tuşlarına basın:</p>
<pre><code>Ajan modu iş akışı:
1. Görevinizi açıklayın
2. Ajan bir plan oluşturur
3. Onaylar veya değiştirirsiniz
4. Ajan değişiklikleri uygular
5. İnceleyip yineleme yaparsınız
</code></pre>
<h3>Plan modu</h3>
<p>Karmaşık görevlerde Plan modunu açıkça kullanın:</p>
<pre><code>@plan Şunlarla kullanıcı kimlik doğrulaması ekle:
- JWT belirteçleri
- Yenileme belirteci rotasyonu
- bcrypt ile parola karma işlemi
- Oturum açma denemelerinde hız sınırlama
</code></pre>
<p>Ajan şunları yapar:</p>
<ol>
<li>Mevcut kodunuzu inceler</li>
<li>Ayrıntılı bir uygulama planı oluşturur</li>
<li>Onayınızı bekler</li>
<li>Planı adım adım uygular</li>
</ol>
<h3>Bağlam ekleme</h3>
<p>Belirli bir bağlam eklemek için <code>@</code> kullanın:</p>
<pre><code>@file src/api/auth.ts
@folder src/middleware
@docs https://jwt.io/introduction

Bu kalıpları izleyen JWT doğrulama ara yazılımını uygula
</code></pre>
<h3>Paralel ajanlar</h3>
<p>Farklı dallarda birden fazla ajanı eşzamanlı olarak çalıştırın:</p>
<pre><code># Cursor'da:
1. Komut Paleti'ni açın
2. &quot;Cursor: New Agent in Worktree&quot;
3. Her ajan yalıtılmış olarak çalışır
4. Sonuçları yan yana karşılaştırın
</code></pre>
<p>Cursor 3, yerel çalışma kopyaları, git worktree'leri ve bulut ortamlarında birden fazla ajanı paralel olarak çalıştıran bir Agents Window ekledi (açmak için Cmd+Shift+P tuşlarına basıp &quot;Agents Window&quot; yazın). /worktree komutu her ajanı kendine ait yalıtılmış bir git worktree'sinde başlatır; /best-of-n ise aynı görevi birkaç modelde çalıştırarak çıktıları yan yana karşılaştırmanızı sağlar.</p>
<h3>Tutarlılık kuralları</h3>
<p><code>.cursor/rules/project.mdc</code> dosyasını oluşturun:</p>
<pre><code class="language-markdown"># Proje Kuralları

## Kod Stili
- TypeScript katı modunu kullan
- Fonksiyonel bileşenleri tercih et
- Varsayılan dışa aktarımlar yerine adlandırılmış dışa aktarımları kullan

## Test
- Her özellik için test gerekir
- describe/it bloklarını kullan
- Harici servisleri taklit et

## Güvenlik
- Gizli bilgileri asla commit etme
- Tüm kullanıcı girdilerini doğrula
- Parametreli sorgular kullan
</code></pre>
<h2>5) GitHub Copilot'ın ajan tabanlı özellikleri</h2>
<p>Copilot, otomatik tamamlamanın ötesine geçti:</p>
<h3>Copilot Chat</h3>
<p>Sohbet panelini açın ve eğik çizgi komutlarını kullanın:</p>
<pre><code>/explain - Seçili kodu açıkla
/fix - Seçimdeki sorunları düzelt
/tests - Testler oluştur
/doc - Dokümantasyon ekle
</code></pre>
<h3>Kodlama ajanı (issue'dan pull request'e)</h3>
<pre><code># GitHub'da:
1. Bir GitHub issue'su açın
2. Atanan kişi olarak &quot;Copilot&quot;ı seçin
3. Kodlama ajanı arka planda çalışır:
   issue'yu analiz eder, dosyaları düzenler, testleri çalıştırır
4. İncelemeniz için bir pull request açar
</code></pre>
<p>Bu özellik, VS Code veya JetBrains içinde sizinle eşzamanlı çalışan Copilot'ın IDE içi ajan modundan farklıdır.</p>
<p>Copilot artık otonom çalışmasını ikiye ayırıyor: ajan modu IDE'nizin (VS Code ve JetBrains) içinde eşzamanlı çalışarak hangi dosyaların düzenleneceğine karar verir, terminal komutlarını çalıştırır ve hatalar üzerinde yineleme yapar; kodlama ajanı ise bulutta eşzamansız çalışarak kendisine atanan bir issue'yu ele alır ve siz başka bir şey üzerinde çalışırken bir pull request teslim eder.</p>
<h3>Bağlama dayalı satır içi öneriler</h3>
<p>Copilot kod tabanınızı okur. Yorumlarla ona yardımcı olun:</p>
<pre><code class="language-typescript">// Şunları yapan kimlik doğrulama ara yazılımı:
// - Authorization başlığından JWT'yi çıkarır
// - Belirteç imzasını doğrular
// - Kullanıcıyı isteğe ekler
// - Geçersizse 401 döndürür
export function authMiddleware(req, res, next) {
  // Copilot uygulamayı oluşturur
}
</code></pre>
<h2>6) Tüm araçlar için en iyi uygulamalar</h2>
<h3>Açık istemler yazın</h3>
<pre><code>❌ Kötü: &quot;Hatayı düzelt&quot;

✅ İyi: &quot;Kullanıcı mevcut olmadığında UserService.getById içindeki 
null işaretçi istisnasını düzelt. İstisna fırlatmak yerine null döndür.&quot;

❌ Kötü: &quot;Test ekle&quot;

✅ İyi: &quot;CartService için şu durumları kapsayan birim testleri ekle:
- Boş sepete ürün ekleme
- Ürünleri kaldırma
- İndirimlerle toplamları hesaplama
- Uç durum: negatif miktarlar&quot;
</code></pre>
<h3>Karmaşık görevlerde plan modunu kullanın</h3>
<pre><code>3 veya daha fazla dosyaya dokunan her görevde daima:

1. Önce bir plan isteyin
2. Planı inceleyin
3. Onaylayın veya değiştirin
4. Ajanın uygulamasına izin verin
5. Commit etmeden önce değişiklikleri inceleyin
</code></pre>
<h3>Mimari bağlam sağlayın</h3>
<pre><code>Büyük bir çalışmadan önce ajanı bilgilendirin:

&quot;Bu bir mikroservis mimarisidir:
- api-gateway: İstekleri yönlendirir
- auth-service: JWT, oturumlar
- user-service: Kullanıcı CRUD işlemleri
- notification-service: E-posta, anlık bildirim

Servisler Redis pub/sub aracılığıyla iletişim kurar.
Veritabanı, Prisma ORM kullanan PostgreSQL'dir.&quot;
</code></pre>
<h3>Ajanlarla test güdümlü geliştirme</h3>
<pre><code>1. Önce başarısız olacak testi yazın:
   &quot;E-posta doğrulaması bekleyen bir kullanıcı kaydı testi yaz&quot;

2. Ajanın başarısız testi görmesini sağlayın:
   &quot;Bu testi geçecek şekilde kayıt işlemini uygula&quot;

3. Tüm testler geçene kadar yineleyin:
   &quot;Kalan test hatalarını düzelt&quot;
</code></pre>
<h2>7) Güvenlik değerlendirmeleri</h2>
<p>Ajan tabanlı araçların gerçek yetkileri vardır. Sınırlar belirleyin:</p>
<h3>Onay geçitleri</h3>
<pre><code class="language-json">// .claude/config.json
{
  &quot;safety&quot;: {
    &quot;requireApproval&quot;: [
      &quot;rm&quot;,
      &quot;git push&quot;,
      &quot;npm publish&quot;,
      &quot;database migrations&quot;,
      &quot;env file changes&quot;
    ]
  }
}
</code></pre>
<h3>Korumalı alan kullanımı</h3>
<pre><code class="language-bash"># Ajanları yalıtılmış ortamlarda çalıştırın
docker run -v $(pwd):/workspace -it claude-sandbox

# Ya da yalıtım için git worktree'lerini kullanın
git worktree add ../feature-branch feature-branch
</code></pre>
<h3>Hassas dosyalar</h3>
<pre><code># .cursorignore / .claudeignore
.env
.env.*
secrets/
credentials/
*.pem
*.key
</code></pre>
<h3>Tüm değişiklikleri inceleyin</h3>
<pre><code class="language-bash"># Ajan değişikliklerini kabul etmeden önce:
git diff --stat
git diff

# Etkileşimli hazırlama kullanın
git add -p

# İnceleme olmadan asla otomatik birleştirme yapmayın
</code></pre>
<h2>8) İşe yarayan iş akışı kalıpları</h2>
<h3>Kalıp 1: Keşfet, Planla, Uygula</h3>
<pre><code>Oturum 1: &quot;Ödeme sistemi nasıl çalışıyor?&quot;
          → Ajan araştırır, siz öğrenirsiniz

Oturum 2: &quot;Stripe eklemek için neler gerekir?&quot;
          → Ajan plan oluşturur

Oturum 3: &quot;Stripe entegrasyonunu uygula&quot;
          → Ajan planı uygular
</code></pre>
<h3>Kalıp 2: Paralel keşif</h3>
<pre><code># Paralel ajanlarla Cursor'da:

Ajan 1: &quot;Redis ile önbelleğe alma uygula&quot;
Ajan 2: &quot;Memcached ile önbelleğe alma uygula&quot;
Ajan 3: &quot;Bellek içi LRU ile önbelleğe alma uygula&quot;

→ Uygulamaları karşılaştırın, en iyi yaklaşımı seçin
</code></pre>
<h3>Kalıp 3: Aşamalı geçiş</h3>
<pre><code>Adım 1: &quot;Kullanımdan kaldırılmış API'nin tüm kullanımlarını belirle&quot;
Adım 2: &quot;Yeni API için bağdaştırıcı oluştur&quot;
Adım 3: &quot;A modülünü bağdaştırıcıyı kullanacak şekilde taşı&quot;
Adım 4: &quot;B modülünü taşı...&quot;
...
Adım N: &quot;Bağdaştırıcıyı kaldır, yeni API'yi doğrudan kullan&quot;
</code></pre>
<h3>Kalıp 4: Önce test yaklaşımıyla özellik geliştirme</h3>
<pre><code>1. &quot;Kullanıcı profili düzenleme için kabul testleri yaz&quot;
2. &quot;Profil doğrulaması için birim testleri yaz&quot;
3. &quot;Testleri geçecek şekilde uygula&quot;
4. &quot;Entegrasyon testleri ekle&quot;
5. &quot;Anlaşılırlık için yeniden düzenle&quot;
</code></pre>
<h2>9) Hangi araç ne zaman kullanılmalı?</h2>
<table>
<thead>
<tr>
<th>Durum</th>
<th>Önerilen Araç</th>
</tr>
</thead>
<tbody>
<tr>
<td>Gerçek zamanlı kodlama, satır içi yardım</td>
<td>GitHub Copilot</td>
</tr>
<tr>
<td>Büyük yeniden düzenlemeler, mimari</td>
<td>Claude Code</td>
</tr>
<tr>
<td>Tam geliştirme oturumu</td>
<td>Cursor</td>
</tr>
<tr>
<td>Kod hakkında hızlı sorular</td>
<td>Herhangi bir sohbet arayüzü</td>
</tr>
<tr>
<td>Issue'lardan PR oluşturma</td>
<td>Copilot Workspace</td>
</tr>
<tr>
<td>Karmaşık çoklu dosya değişiklikleri</td>
<td>Cursor Agent</td>
</tr>
<tr>
<td>API tasarımı, dokümantasyon</td>
<td>Claude Code</td>
</tr>
<tr>
<td>Yeni bir kod tabanını öğrenme</td>
<td>Cursor veya Claude Code</td>
</tr>
</tbody>
</table>
<h2>10) Etkinliği ölçme</h2>
<p>Ajan tabanlı araçları değerlendirmek için şu metrikleri izleyin:</p>
<pre><code class="language-javascript">const metrics = {
  // Verimlilik
  suggestionsAccepted: 'sakladığınız önerilerin yüzdesi',
  timeToFeature: 'başlangıçtan birleştirilmiş PR aşamasına kadar geçen saat',
  linesPerHour: 'kod üretim hızı (dikkatli kullanın)',
  
  // Kalite
  bugsIntroduced: 'yapay zekâ tarafından oluşturulan koda kadar izlenen kusurlar',
  testCoverage: 'yapay zekâ tarafından oluşturulan kodun kapsamı',
  reviewChanges: 'yapay zekâ önerilerini ne ölçüde değiştirdiğiniz',
  
  // Maliyet
  apiCosts: 'yapay zekâ araçlarına yapılan aylık harcama',
  creditsUsed: 'tüketilen Cursor kredileri',
  
  // Öğrenme
  newPatternsLearned: 'yapay zekâdan öğrendiğiniz teknikler',
  codebaseUnderstanding: 'yapay zekânın öğrenmenize ne kadar yardımcı olduğu',
}
</code></pre>
<h2>11) Yaygın hatalar</h2>
<h3>Ajana aşırı güvenmek</h3>
<pre><code>❌ Büyük değişiklikleri incelemeden kabul etmek
❌ Ajanların yıkıcı komutlar çalıştırmasına izin vermek
❌ Yayınlamadan önce kodu anlamamak

✅ Her değişikliği inceleyin
✅ Yayınladığınız şeyi anlayın
✅ Ajanları yerinize geçen araçlar değil, iş arkadaşları olarak kullanın
</code></pre>
<h3>Bağlam kirliliği</h3>
<pre><code>❌ Alakasız bağlam içeren devasa istemler
❌ Birbiriyle ilgisiz görevler arasında bağlamı temizlememek
❌ Oluşturulan dosyaları bağlama dahil etmek

✅ İstemleri odaklı tutun
✅ Belirli @file referansları kullanın
✅ Derleme çıktılarını hariç tutun
</code></pre>
<h3>İstem belirsizliği</h3>
<pre><code>❌ &quot;Daha iyi yap&quot;
❌ &quot;Sorunları düzelt&quot;
❌ &quot;Birkaç test ekle&quot;

✅ &quot;Zaman karmaşıklığını O(n²)'den O(n log n)'ye düşür&quot;
✅ &quot;45. satırdaki null durumunu erken dönüş yaparak ele al&quot;
✅ &quot;Uç durumlar için testler ekle: boş girdi, maksimum uzunluk, unicode&quot;
</code></pre>
<h2>12) Başlangıç kontrol listesi</h2>
<pre><code class="language-markdown">## Kurulum Kontrol Listesi

### Claude Code
- [ ] CLI'yi yükleyin: `npm install -g @anthropic/claude-code`
- [ ] Kimlik doğrulaması yapın: `claude login`
- [ ] `.claude/config.json` oluşturun
- [ ] Hassas dosyalar için `.claudeignore` ekleyin

### Cursor
- [ ] cursor.com adresinden indirin
- [ ] VS Code ayarlarını içe aktarın
- [ ] `.cursor/rules/` dizinini yapılandırın
- [ ] Ajan modu için `Cmd+Shift+I` kısayolunu öğrenin

### GitHub Copilot
- [ ] Uzantıyı IDE'nize yükleyin
- [ ] Copilot Chat'i etkinleştirin
- [ ] Issue'lar için Copilot Workspace'i deneyin
- [ ] `.github/copilot-instructions.md` dosyasını yapılandırın

### Tüm Araçlar
- [ ] Yıkıcı işlemler için onay geçitleri ayarlayın
- [ ] Gizli bilgiler için yok sayma dosyaları oluşturun
- [ ] İnceleme iş akışı oluşturun
- [ ] İlk günden itibaren metrikleri izleyin
</code></pre>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlayın</a></li>
<li><a href="/tr/tutorials/web-workers-game-logic">Oyun mantığı için Web Worker'lar</a></li>
<li><a href="/tr/guides/game-jams-hackathons">Game Jam'ler ve Hackathon'lar</a> — Yapay zekâ kod araçları, 48 saatlik bir jam'de yapılabilecekleri değiştiriyor</li>
<li><a href="/tr/guides/web-game-engines-comparison">Web Oyunu Motorları Karşılaştırması</a> — yapay zekâ destekli iş akışlarıyla iyi çalışan bir motor seçme</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Web oyunları için analiz ve telemetri]]></title>
            <link>https://app.cinevva.com/tr/tutorials/analytics-web-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/analytics-web-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Oyununuzda önemli olan verileri izleyin: oyuncu davranışı, performans, hatalar ve dönüşümler. Veriye dayalı kararlar alın.]]></description>
            <content:encoded><![CDATA[<h1>Web oyunları için analiz ve telemetri</h1>
<p>Analizler, oyuncuların neler yaptığını, nerelerde zorlandığını ve onları oyuna bağlı tutan unsurları anlamanıza yardımcı olur. Bu eğitimde neleri ve nasıl izlemeniz gerektiğini öğreneceksiniz.</p>
<h2>1) Neler izlenmeli?</h2>
<p><strong>Etkileşim metrikleri:</strong></p>
<ul>
<li>Oturum başlangıçları ve süreleri</li>
<li>Başlatılan ve tamamlanan seviyeler</li>
<li>Kullanılan özellikler</li>
<li>Oyuncu tutma oranı (geri dönüş ziyaretleri)</li>
</ul>
<p><strong>Performans metrikleri:</strong></p>
<ul>
<li>Yükleme süresi</li>
<li>Kare hızı</li>
<li>Bellek kullanımı</li>
<li>Hatalar ve çökmeler</li>
</ul>
<p><strong>Dönüşüm metrikleri:</strong></p>
<ul>
<li>Eğitimi tamamlama</li>
<li>İlk satın alma</li>
<li>Sosyal medya paylaşımları</li>
</ul>
<h2>2) Basit etkinlik izleme</h2>
<pre><code class="language-js">class Analytics {
  constructor(endpoint) {
    this.endpoint = endpoint
    this.sessionId = crypto.randomUUID()
    this.queue = []
    this.flushInterval = 30000 // 30 seconds
    
    setInterval(() =&gt; this.flush(), this.flushInterval)
    // Flush when the page is being hidden, not on beforeunload.
    // beforeunload/unload are unreliable (often don't fire on mobile,
    // and block the back/forward cache). visibilitychange + pagehide
    // are the recommended events for end-of-session sends.
    document.addEventListener('visibilitychange', () =&gt; {
      if (document.visibilityState === 'hidden') this.flush()
    })
    window.addEventListener('pagehide', () =&gt; this.flush())
  }
  
  track(event, data = {}) {
    this.queue.push({
      event,
      data,
      sessionId: this.sessionId,
      timestamp: Date.now(),
      url: location.href,
    })
    
    // Flush immediately for important events
    if (event === 'error' || event === 'purchase') {
      this.flush()
    }
  }
  
  async flush() {
    if (this.queue.length === 0) return
    
    const events = [...this.queue]
    this.queue = []
    
    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ events }),
        keepalive: true, // Important for beforeunload
      })
    } catch {
      // Put events back in queue
      this.queue.unshift(...events)
    }
  }
}

const analytics = new Analytics('/api/analytics')
</code></pre>
<h2>3) Oturum izleme</h2>
<pre><code class="language-js">// Track session start
analytics.track('session_start', {
  referrer: document.referrer,
  screen: `${screen.width}x${screen.height}`,
  devicePixelRatio: window.devicePixelRatio,
  userAgent: navigator.userAgent,
})

// Track session end
let sessionStart = Date.now()

// Send session_end when the page is hidden, not on beforeunload.
// beforeunload is unreliable on mobile and breaks the bfcache.
function recordSessionEnd() {
  analytics.track('session_end', {
    duration: Date.now() - sessionStart,
  })
}
document.addEventListener('visibilitychange', () =&gt; {
  if (document.visibilityState === 'hidden') recordSessionEnd()
})
window.addEventListener('pagehide', recordSessionEnd)

// Track visibility changes
document.addEventListener('visibilitychange', () =&gt; {
  if (document.hidden) {
    analytics.track('tab_hidden')
  } else {
    analytics.track('tab_visible')
  }
})
</code></pre>
<h2>4) Oyuna özgü etkinlikler</h2>
<pre><code class="language-js">// Level tracking
function onLevelStart(levelId) {
  analytics.track('level_start', { levelId })
}

function onLevelComplete(levelId, score, time) {
  analytics.track('level_complete', {
    levelId,
    score,
    timeSeconds: time,
  })
}

function onLevelFail(levelId, reason) {
  analytics.track('level_fail', {
    levelId,
    reason, // 'death', 'timeout', 'quit'
  })
}

// Achievement tracking
function onAchievementUnlocked(achievementId) {
  analytics.track('achievement', { achievementId })
}

// Tutorial tracking
function onTutorialStep(step, skipped = false) {
  analytics.track('tutorial', { step, skipped })
}
</code></pre>
<h2>5) Performans izleme</h2>
<pre><code class="language-js">class PerformanceMonitor {
  constructor(analytics) {
    this.analytics = analytics
    this.frameTimes = []
    this.lastFrame = performance.now()
  }
  
  recordFrame() {
    const now = performance.now()
    this.frameTimes.push(now - this.lastFrame)
    this.lastFrame = now
    
    // Keep last 60 frames
    if (this.frameTimes.length &gt; 60) {
      this.frameTimes.shift()
    }
  }
  
  getAverageFPS() {
    if (this.frameTimes.length === 0) return 0
    const avgFrameTime = this.frameTimes.reduce((a, b) =&gt; a + b) / this.frameTimes.length
    return 1000 / avgFrameTime
  }
  
  reportPerformance() {
    const fps = this.getAverageFPS()
    const memory = performance.memory?.usedJSHeapSize
    
    this.analytics.track('performance', {
      avgFPS: Math.round(fps),
      memoryMB: memory ? Math.round(memory / 1024 / 1024) : null,
    })
  }
}

// Report every minute
const perfMonitor = new PerformanceMonitor(analytics)
setInterval(() =&gt; perfMonitor.reportPerformance(), 60000)
</code></pre>
<h3>Takılmaların nedenini Long Animation Frames ile belirleyin</h3>
<p>Yukarıdaki FPS sayacı, karelerin <em>ne zaman</em> düştüğünü gösterir ancak <em>neden</em> düştüğünü göstermez. Chrome ve Edge 123 ile kullanıma sunulan Long Animation Frames API (LoAF), bu boşluğu doldurur. 50 ms'den uzun süren tüm kareleri işaretler ve gecikmeye hangi betiklerin neden olduğunu ayrıntılı olarak gösterir; böylece tahmin yürütmek yerine takılmaları belirli kodlarla ilişkilendirebilirsiniz.</p>
<pre><code class="language-js">if (PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame')) {
  const observer = new PerformanceObserver((list) =&gt; {
    for (const entry of list.getEntries()) {
      analytics.track('long_frame', {
        durationMs: Math.round(entry.duration),
        blockingMs: Math.round(entry.blockingDuration),
        // scripts[] names the source URLs that ate the frame
        scripts: entry.scripts?.map((s) =&gt; s.sourceURL),
      })
    }
  })
  observer.observe({ type: 'long-animation-frame', buffered: true })
}
</code></pre>
<p>LoAF şimdilik yalnızca Chromium tabanlı tarayıcılarda kullanılabilir (Firefox veya Safari'de yoktur). Bu nedenle kullanmadan önce özellik algılaması yapın ve tarayıcılar arası temel ölçümünüz olarak FPS sayacını kullanmaya devam edin.</p>
<h2>6) Hata izleme</h2>
<pre><code class="language-js">window.addEventListener('error', (event) =&gt; {
  analytics.track('error', {
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    stack: event.error?.stack,
  })
})

window.addEventListener('unhandledrejection', (event) =&gt; {
  analytics.track('error', {
    message: event.reason?.message || String(event.reason),
    type: 'unhandledrejection',
    stack: event.reason?.stack,
  })
})

// Custom error tracking
function trackGameError(context, error) {
  analytics.track('game_error', {
    context,
    message: error.message,
    stack: error.stack,
  })
}
</code></pre>
<h2>7) Yükleme süresini izleme</h2>
<pre><code class="language-js">// Track initial load
window.addEventListener('load', () =&gt; {
  const timing = performance.timing
  const loadTime = timing.loadEventEnd - timing.navigationStart
  const domReady = timing.domContentLoadedEventEnd - timing.navigationStart
  
  analytics.track('page_load', {
    totalMs: loadTime,
    domReadyMs: domReady,
  })
})

// Track game-specific load phases
async function loadGame() {
  const start = performance.now()
  
  await loadCriticalAssets()
  const criticalTime = performance.now() - start
  
  analytics.track('load_critical', { ms: Math.round(criticalTime) })
  
  await loadGameAssets()
  const totalTime = performance.now() - start
  
  analytics.track('load_complete', { ms: Math.round(totalTime) })
}
</code></pre>
<h2>8) Huni izleme</h2>
<p>Oyuncuların önemli akışlardaki ilerlemesini izleyin:</p>
<pre><code class="language-js">class FunnelTracker {
  constructor(analytics, funnelName) {
    this.analytics = analytics
    this.funnelName = funnelName
    this.startTime = Date.now()
  }
  
  step(stepName) {
    this.analytics.track('funnel_step', {
      funnel: this.funnelName,
      step: stepName,
      elapsedMs: Date.now() - this.startTime,
    })
  }
  
  complete() {
    this.analytics.track('funnel_complete', {
      funnel: this.funnelName,
      totalMs: Date.now() - this.startTime,
    })
  }
  
  abandon(reason) {
    this.analytics.track('funnel_abandon', {
      funnel: this.funnelName,
      reason,
      elapsedMs: Date.now() - this.startTime,
    })
  }
}

// Usage
const onboarding = new FunnelTracker(analytics, 'onboarding')
onboarding.step('welcome_shown')
// ... player clicks continue
onboarding.step('name_entered')
// ... player completes tutorial
onboarding.complete()
</code></pre>
<h2>9) A/B testi desteği</h2>
<pre><code class="language-js">class ABTest {
  constructor(testName, variants) {
    this.testName = testName
    this.variants = variants
    
    // Get or assign variant
    const stored = localStorage.getItem(`ab_${testName}`)
    if (stored &amp;&amp; variants.includes(stored)) {
      this.variant = stored
    } else {
      this.variant = variants[Math.floor(Math.random() * variants.length)]
      localStorage.setItem(`ab_${testName}`, this.variant)
    }
    
    // Track assignment
    analytics.track('ab_assignment', {
      test: testName,
      variant: this.variant,
    })
  }
  
  getVariant() {
    return this.variant
  }
  
  trackConversion(metric) {
    analytics.track('ab_conversion', {
      test: this.testName,
      variant: this.variant,
      metric,
    })
  }
}

// Usage
const difficultyTest = new ABTest('difficulty', ['easy', 'normal', 'hard'])
game.difficulty = difficultyTest.getVariant()

// When player completes level
difficultyTest.trackConversion('level_complete')
</code></pre>
<h2>10) Gizlilikle ilgili hususlar</h2>
<pre><code class="language-js">class PrivacyAwareAnalytics extends Analytics {
  constructor(endpoint) {
    super(endpoint)
    this.enabled = this.checkConsent()
  }
  
  checkConsent() {
    return localStorage.getItem('analytics_consent') === 'true'
  }
  
  setConsent(enabled) {
    localStorage.setItem('analytics_consent', enabled ? 'true' : 'false')
    this.enabled = enabled
    
    if (enabled) {
      this.track('consent_granted')
    }
  }
  
  track(event, data = {}) {
    if (!this.enabled) return
    
    // Strip PII
    const sanitized = { ...data }
    delete sanitized.email
    delete sanitized.name
    delete sanitized.ip
    
    super.track(event, sanitized)
  }
}

// Show consent dialog
function showConsentDialog() {
  const dialog = document.createElement('div')
  dialog.innerHTML = `
    &lt;p&gt;Oyunu geliştirmek için analiz verileri kullanıyoruz. Bunu kabul ediyor musunuz?&lt;/p&gt;
    &lt;button id=&quot;accept&quot;&gt;Kabul Et&lt;/button&gt;
    &lt;button id=&quot;decline&quot;&gt;Reddet&lt;/button&gt;
  `
  document.body.appendChild(dialog)
  
  dialog.querySelector('#accept').onclick = () =&gt; {
    analytics.setConsent(true)
    dialog.remove()
  }
  
  dialog.querySelector('#decline').onclick = () =&gt; {
    analytics.setConsent(false)
    dialog.remove()
  }
}
</code></pre>
<h2>Üçüncü taraf alternatifleri</h2>
<p>Kendi sisteminizi geliştirmek istemiyorsanız:</p>
<ul>
<li><strong>Plausible</strong> — Gizlilik odaklı ve basit</li>
<li><strong>Amplitude</strong> — Ürün analizi ve huniler</li>
<li><strong>Mixpanel</strong> — Etkinlik izleme ve kullanıcı yolculukları</li>
<li><strong>Sentry</strong> — Özellikle hata izleme</li>
</ul>
<pre><code class="language-js">// Example: Plausible
const script = document.createElement('script')
script.defer = true
script.dataset.domain = 'yourgame.com'
script.src = 'https://plausible.io/js/plausible.js'
document.head.appendChild(script)

// Track custom events
window.plausible('level_complete', { props: { level: '1' } })
</code></pre>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlayın</a></li>
<li><a href="/tr/tutorials/indexeddb-game-saves">IndexedDB ile oyun kayıtları</a></li>
<li><a href="/tr/creators">İçerik üreticileri için</a></li>
<li><a href="/tr/guides/itch-io-launch-guide">Oyununuzu itch.io'da nasıl yayınlarsınız?</a> — <a href="http://itch.io">itch.io</a> analizleri ve indirme takibi</li>
<li><a href="/tr/guides/steam-next-fest-strategy">Steam Next Fest stratejisi</a> — istek listelerini ve demo dönüşüm oranlarını ölçme</li>
</ul>
<h2>Harici kaynaklar</h2>
<ul>
<li><a href="https://plausible.io/">Plausible Analytics</a> — gizlilik dostu, hafif bir analiz aracı</li>
<li><a href="https://posthog.com/">PostHog</a> — etkinlik izleme özellikli açık kaynaklı ürün analizi</li>
<li><a href="https://sentry.io/">Sentry</a> — web uygulamaları için hata izleme ve çökme raporlama</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Canvas 2D Oyun Döngüsünün Temelleri]]></title>
            <link>https://app.cinevva.com/tr/tutorials/canvas-2d-game-loop</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/canvas-2d-game-loop</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Canvas 2D ile sağlam bir oyun döngüsü oluşturun. Zamanlama, sabit zaman adımı, girdi, görüntü oluşturma ve geliştiricilerin sıkça takıldığı hataları ele alır.]]></description>
            <content:encoded><![CDATA[<h1>Canvas 2D Oyun Döngüsünün Temelleri</h1>
<p>Oyun döngüsü, oyununuzun kalbidir. Sürekli çalışarak oyun durumunu günceller ve tarayıcının izin verdiği hızda kareleri çizer. Bunu doğru kurarsanız geri kalan her şey kolaylaşır. Yanlış kurarsanız yalnızca belirli cihazlarda ortaya çıkan hataların peşinden koşarsınız.</p>
<p><strong>Deneyin (ok tuşlarını veya WASD'yi kullanın):</strong></p>
<GameLoopDemo />
<h2>En Basit Sürüm</h2>
<p>İşte yalnızca temel unsurlara indirgenmiş bir oyun döngüsü:</p>
<pre><code class="language-js">const canvas = document.getElementById('game')
const ctx = canvas.getContext('2d')

function gameLoop() {
  update()
  render()
  requestAnimationFrame(gameLoop)
}

function update() {
  // Oyun mantığı buraya
}

function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  // Çizim işlemleri buraya
}

gameLoop()
</code></pre>
<p>Bu çalışır, ancak bir sorunu vardır. <code>update</code> fonksiyonu, son kareden bu yana ne kadar zaman geçtiğini bilmez. 144 Hz'lik bir monitörde, 60 Hz'lik bir monitöre göre iki kat hızlı çalışır. Oyun fiziğiniz farklı cihazlarda farklı davranır.</p>
<h2>Delta Time Kare Hızı Sorunlarını Giderir</h2>
<p>Oyununuzun her yerde aynı hızda çalışmasını sağlamak için kareler arasında ne kadar zaman geçtiğini takip etmeniz gerekir. Buna delta time denir ve genellikle <code>dt</code> olarak kısaltılır.</p>
<pre><code class="language-js">let lastTime = 0

function gameLoop(currentTime) {
  const dt = (currentTime - lastTime) / 1000 // saniyeye dönüştür
  lastTime = currentTime
  
  update(dt)
  render()
  
  requestAnimationFrame(gameLoop)
}

function update(dt) {
  player.x += player.speed * dt // artık her kare hızında çalışır
}

requestAnimationFrame(gameLoop)
</code></pre>
<p><code>requestAnimationFrame</code> geri çağırma fonksiyonu, milisaniye cinsinden geçerli zaman damgasını alır. Matematiği kolaylaştırdığı için bunu 1000'e bölerek saniyeye dönüştürüyorum. 200 hız değeri, saniyede 200 piksel anlamına gelir; bu da anlaşılması kolaydır.</p>
<p>Dikkat edilmesi gereken bir nokta var: İlk karede <code>lastTime</code> 0 olduğundan <code>dt</code> çok büyük olur. Bazı geliştiriciler döngüyü başlatmadan önce <code>lastTime</code> değerini <code>performance.now()</code> ile ayarlar, bazılarıysa ilk karenin güncellemesini atlar.</p>
<h2>Neden setInterval Yerine requestAnimationFrame?</h2>
<p>Neden <code>setInterval</code> yerine <code>requestAnimationFrame</code> kullandığımızı merak edebilirsiniz. Bunun birkaç nedeni var.</p>
<p>İlk olarak <code>requestAnimationFrame</code>, ekranın yenileme hızıyla eşzamanlanır; böylece yırtılma olmadan akıcı animasyon elde edersiniz. İkinci olarak sekme gizlendiğinde otomatik biçimde duraklar, bu da pil ve işlemci kullanımını azaltır. Üçüncü olarak tarayıcı bunu daha iyi optimize edebilir.</p>
<p>Bu duraklama davranışı genellikle istediğiniz şeydir, ancak kullanıcı başka bir sekmeye geçtiğinde oyununuzun donduğu anlamına gelir. Çok oyunculu bir oyun veya çalışmaya devam etmesi gereken başka bir şey geliştiriyorsanız farklı bir yaklaşıma ihtiyacınız olur.</p>
<h2>Fizik İçin Sabit Zaman Adımı</h2>
<p>Değişken delta time basit hareketlerde işe yarar, ancak fizik simülasyonlarında sorunlara yol açar. Bir karenin işlenmesi çok uzun sürerse çarpışma kontrolü temas anını kaçırdığı için karakteriniz bir duvarın içinden geçebilir.</p>
<p>Çözüm, sabit zaman adımıdır. Geçen zamanı biriktirir ve karelerin ne kadar hızlı oluşturulduğundan bağımsız olarak fizik güncellemelerini sabit bir hızda çalıştırırsınız.</p>
<pre><code class="language-js">const FIXED_DT = 1 / 60 // saniyede 60 fizik güncellemesi
let accumulator = 0
let lastTime = 0

function gameLoop(currentTime) {
  const frameTime = Math.min((currentTime - lastTime) / 1000, 0.25)
  lastTime = currentTime
  
  accumulator += frameTime
  
  while (accumulator &gt;= FIXED_DT) {
    update(FIXED_DT)
    accumulator -= FIXED_DT
  }
  
  render()
  requestAnimationFrame(gameLoop)
}
</code></pre>
<p>Buradaki <code>Math.min(..., 0.25)</code> önemlidir. Sekme gizlenip yeniden açıldığında <code>frameTime</code> çok büyük olabilir. Bu sınır olmadan while döngüsü arayı kapatmak için yüzlerce güncelleme çalıştırarak oyunu dondurabilir. Değeri 0,25 saniyeyle sınırlamak, kare başına hiçbir zaman 15'ten fazla fizik güncellemesi çalıştırmayacağınız anlamına gelir.</p>
<h2>Oyun Durumunu Düzenleme</h2>
<p>Oyununuz büyüdükçe tüm verileri koyabileceğiniz bir yere ihtiyaç duyarsınız. Ben her şeyi barındıran tek bir nesne kullanmayı tercih ediyorum:</p>
<pre><code class="language-js">const game = {
  state: 'playing', // 'menu', 'playing', 'paused', 'gameover'
  score: 0,
  player: {
    x: 100,
    y: 100,
    vx: 0,
    vy: 0,
    speed: 200,
    width: 32,
    height: 32,
  },
  entities: [],
}
</code></pre>
<p>Bu yaklaşım oyun durumunu kaydedip yüklemeyi, oyuncu öldüğünde sıfırlamayı ve <code>game</code> nesnesini konsola yazarak hata ayıklamayı kolaylaştırır. Bazı geliştiriciler sınıfları tercih eder, ancak sade bir nesne küçük ve orta ölçekli oyunlarda gayet iyi çalışır.</p>
<h2>Hepsini Bir Araya Getirme</h2>
<pre><code class="language-js">const canvas = document.getElementById('game')
const ctx = canvas.getContext('2d')

const FIXED_DT = 1 / 60
let accumulator = 0
let lastTime = 0

const input = { left: false, right: false, up: false, down: false }

const game = {
  player: { x: 100, y: 100, speed: 200 },
}

function update(dt) {
  const p = game.player
  
  if (input.left) p.x -= p.speed * dt
  if (input.right) p.x += p.speed * dt
  if (input.up) p.y -= p.speed * dt
  if (input.down) p.y += p.speed * dt
  
  // Sınırlar içinde tut
  p.x = Math.max(0, Math.min(canvas.width - 32, p.x))
  p.y = Math.max(0, Math.min(canvas.height - 32, p.y))
}

function render() {
  ctx.fillStyle = '#1a1a2e'
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  
  ctx.fillStyle = '#4ade80'
  ctx.fillRect(game.player.x, game.player.y, 32, 32)
}

function gameLoop(currentTime) {
  const frameTime = Math.min((currentTime - lastTime) / 1000, 0.25)
  lastTime = currentTime
  
  accumulator += frameTime
  
  while (accumulator &gt;= FIXED_DT) {
    update(FIXED_DT)
    accumulator -= FIXED_DT
  }
  
  render()
  requestAnimationFrame(gameLoop)
}

// Girdi işleme
window.addEventListener('keydown', (e) =&gt; {
  if (e.code === 'ArrowLeft' || e.code === 'KeyA') input.left = true
  if (e.code === 'ArrowRight' || e.code === 'KeyD') input.right = true
  if (e.code === 'ArrowUp' || e.code === 'KeyW') input.up = true
  if (e.code === 'ArrowDown' || e.code === 'KeyS') input.down = true
})

window.addEventListener('keyup', (e) =&gt; {
  if (e.code === 'ArrowLeft' || e.code === 'KeyA') input.left = false
  if (e.code === 'ArrowRight' || e.code === 'KeyD') input.right = false
  if (e.code === 'ArrowUp' || e.code === 'KeyW') input.up = false
  if (e.code === 'ArrowDown' || e.code === 'KeyS') input.down = false
})

requestAnimationFrame(gameLoop)
</code></pre>
<p>Bu, eksiksiz ve çalıştırılabilir bir oyun döngüsüdür. Oyuncu ok tuşları veya WASD ile hareket eder, sınırlar içinde kalır ve kare hızından bağımsız olarak her şey tutarlı bir hızda çalışır. Bunu herhangi bir Canvas 2D oyunu için başlangıç noktası olarak kopyalayabilirsiniz.</p>
<p>Girdi işlemenin oyun döngüsünden ayrı olduğuna dikkat edin. Olay dinleyicileri yalnızca bayrakları ayarlar, güncelleme fonksiyonuysa bu bayrakları okur. Bu ayrım önemlidir çünkü klavye olayları öngörülemeyen zamanlarda tetiklenir; oyun mantığınızın ise öngörülebilir bir hızda çalışmasını istersiniz.</p>
<h2>Sprite Yükleme ve Çizme</h2>
<pre><code class="language-js">const sprites = {}

async function loadSprite(name, url) {
  return new Promise((resolve) =&gt; {
    const img = new Image()
    img.onload = () =&gt; {
      sprites[name] = img
      resolve(img)
    }
    img.src = url
  })
}

function drawSprite(name, x, y, width, height) {
  const sprite = sprites[name]
  if (sprite) {
    ctx.drawImage(sprite, x, y, width || sprite.width, height || sprite.height)
  }
}
</code></pre>
<p>Görseller eşzamansız yüklendiği için oyununuzu başlatmadan önce yüklenmelerini beklemeniz gerekir. Yaygın bir yaklaşım, tüm sprite'ları bir Promise döndüren <code>loadAssets</code> fonksiyonunda yüklemek ve oyun döngüsünü yalnızca bu Promise tamamlandıktan sonra başlatmaktır.</p>
<p><code>drawSprite</code> fonksiyonu, çizmeden önce sprite'ın var olup olmadığını kontrol eder. Bu, henüz yüklenmemiş bir şeyi çizmeye çalıştığınızda hata oluşmasını önler.</p>
<h2>Animasyonlu Sprite'lar</h2>
<pre><code class="language-js">class AnimatedSprite {
  constructor(image, frameWidth, frameHeight, frameCount, fps = 10) {
    this.image = image
    this.frameWidth = frameWidth
    this.frameHeight = frameHeight
    this.frameCount = frameCount
    this.frameDuration = 1 / fps
    this.currentFrame = 0
    this.elapsed = 0
  }
  
  update(dt) {
    this.elapsed += dt
    if (this.elapsed &gt;= this.frameDuration) {
      this.currentFrame = (this.currentFrame + 1) % this.frameCount
      this.elapsed = 0
    }
  }
  
  draw(ctx, x, y) {
    ctx.drawImage(
      this.image,
      this.currentFrame * this.frameWidth, 0,
      this.frameWidth, this.frameHeight,
      x, y,
      this.frameWidth, this.frameHeight
    )
  }
}
</code></pre>
<p>Bu sınıf, sprite sayfanızdaki karelerin tek bir satırda yatay olarak dizildiğini varsayar. Her kare aynı boyuttadır. <code>update</code> metodu geçen zamana göre kareler arasında ilerler; <code>draw</code> ise yalnızca geçerli kareyi kesip almak için <code>drawImage</code> fonksiyonunun dokuz argümanlı sürümünü kullanır.</p>
<p>Şöyle kullanabilirsiniz:</p>
<pre><code class="language-js">const playerWalk = new AnimatedSprite(walkImage, 32, 32, 4, 12)

// update içinde:
playerWalk.update(dt)

// render içinde:
playerWalk.draw(ctx, player.x, player.y)
</code></pre>
<h2>Çarpışma Algılama</h2>
<pre><code class="language-js">function rectCollision(a, b) {
  return (
    a.x &lt; b.x + b.width &amp;&amp;
    a.x + a.width &gt; b.x &amp;&amp;
    a.y &lt; b.y + b.height &amp;&amp;
    a.y + a.height &gt; b.y
  )
}

function circleCollision(a, b) {
  const dx = a.x - b.x
  const dy = a.y - b.y
  const distance = Math.sqrt(dx * dx + dy * dy)
  return distance &lt; a.radius + b.radius
}

// update içinde:
for (const enemy of game.enemies) {
  if (rectCollision(game.player, enemy)) {
    handleCollision(game.player, enemy)
  }
}
</code></pre>
<p>Dikdörtgen çarpışması (eksene hizalı sınırlayıcı kutu anlamına gelen AABB olarak da bilinir) en basit ve en hızlı seçenektir. Çoğu şey için bunu kullanın. Daire çarpışması, yuvarlak nesneler veya çarpışmaların daha toleranslı hissettirmesini istediğiniz durumlar için daha uygundur.</p>
<p>Az sayıda varlık için her çifti kontrol etmek yeterlidir. Yüzlerce varlığınız varsa uzamsal bölümleme (dörtlü ağaçlar veya bir ızgara) kullanmanız gerekir, ancak bu başka bir eğitimin konusu.</p>
<h2>Basit Bir Kamera</h2>
<pre><code class="language-js">const camera = {
  x: 0,
  y: 0,
  
  follow(target) {
    this.x = target.x - canvas.width / 2
    this.y = target.y - canvas.height / 2
  }
}

function render() {
  ctx.save()
  ctx.translate(-camera.x, -camera.y)
  
  // Dünyayı çiz
  drawWorld()
  drawEntities()
  
  ctx.restore()
  
  // Arayüzü çiz (kameradan etkilenmez)
  drawUI()
}
</code></pre>
<p>İşin püf noktası <code>ctx.save()</code> ve <code>ctx.restore()</code> kullanmaktır. Geçerli dönüşüm durumunu kaydeder, <code>translate</code> ile kamera kaydırmasını uygular, dünya uzayındaki her şeyi çizer ve ardından arayüz öğelerinin kameradan etkilenmemesi için durumu geri yüklersiniz.</p>
<p>Daha yumuşak kamera hareketi için doğrudan hedefe sıçramak yerine hedefe doğru doğrusal enterpolasyon uygulayabilirsiniz. <code>this.x += (targetX - this.x) * 0.1</code> gibi bir ifade, hoş bir takip gecikmesi efekti sağlar.</p>
<h2>Oyunu Duraklatma</h2>
<pre><code class="language-js">let paused = false

window.addEventListener('keydown', (e) =&gt; {
  if (e.code === 'Escape') {
    paused = !paused
  }
})

function gameLoop(currentTime) {
  const frameTime = Math.min((currentTime - lastTime) / 1000, 0.25)
  lastTime = currentTime
  
  if (!paused) {
    accumulator += frameTime
    while (accumulator &gt;= FIXED_DT) {
      update(FIXED_DT)
      accumulator -= FIXED_DT
    }
  }
  
  render()
  
  if (paused) {
    renderPauseOverlay()
  }
  
  requestAnimationFrame(gameLoop)
}

function renderPauseOverlay() {
  ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  
  ctx.fillStyle = '#fff'
  ctx.font = '32px sans-serif'
  ctx.textAlign = 'center'
  ctx.fillText('DURAKLATILDI', canvas.width / 2, canvas.height / 2)
}
</code></pre>
<p>Temel fikir, oyun duraklatıldığında zaman biriktirmeyi bırakmanız ancak görüntü oluşturmaya devam etmenizdir. Böylece duraklatma katmanı hemen görünür ve oyun, devam ettirme girdisine yanıt vermeyi sürdürür.</p>
<p>Bu kalıbı menüler, ara sahneler ve oyun sonu ekranları gibi birden fazla oyun durumunu yönetmek üzere genişletebilirsiniz. Yalnızca geçerli durumu kontrol edin ve işlemi farklı güncelleme/görüntü oluşturma fonksiyonlarına yönlendirin.</p>
<h2>Yaygın Hatalar</h2>
<p>Geliştiricilerin sıkça takıldığını gördüğüm birkaç nokta:</p>
<p><strong>Canvas'ı temizlemeyi unutmak.</strong> Görüntü oluşturmanın başında <code>clearRect</code> çağırmazsanız önceki karenin üzerine çizim yapar ve iz bırakma efektiyle karşılaşırsınız.</p>
<p><strong>Delta time değerini sınırlamamak.</strong> Biri başka bir sekmeye geçip geri döndüğünde çok büyük bir delta time, fizik sisteminizi bozabilir veya oyunun saniyeler boyunca &quot;arayı kapatmaya&quot; çalışmasına neden olabilir.</p>
<p><strong>Görseller yüklenmeden çizim yapmak.</strong> Canvas, henüz yüklenmemiş görsellerle yapılan <code>drawImage</code> çağrılarını sessizce yok sayar. Sprite'larınız herhangi bir hata mesajı olmadan görünmez.</p>
<p><strong>Döngü için <code>setInterval</code> kullanmak.</strong> Ekranla eşzamanlanmaz ve sekme gizlendiğinde duraklamaz. <code>requestAnimationFrame</code> kullanın.</p>
<p><strong>Dünya ve ekran koordinatlarını karıştırmak.</strong> Kamera eklediğinizde bir konumun dünya uzayında mı yoksa ekran uzayında mı olduğunu takip edin. Fare tıklamaları ekran uzayında gelir ve dönüştürülmeleri gerekir.</p>
<h2>Sırada Ne Var?</h2>
<p>Bu eğitim temel konuları kapsar. Bu kalıplara alıştıktan sonra şunları incelemek isteyebilirsiniz:</p>
<p>Farklı ekran boyutlarını ve yüksek DPI'lı ekranları yönetmek için <a href="/tr/tutorials/responsive-game-canvas">Duyarlı oyun canvas'ı</a>.</p>
<p>Dokunmatik kontroller, oyun kumandaları ve daha gelişmiş girdi sistemleri için <a href="/tr/tutorials/game-input-handling">Oyun girdisi işleme</a>.</p>
<p>Bulanıklık olmadan net piksel grafikleri elde etmek için <a href="/tr/tutorials/pixel-art-rendering">Piksel sanatı görüntü oluşturma</a>.</p>
<p>Oyun döngünüze katı cisim fiziği, çarpışmalar ve eklemler eklemek için <a href="/tr/tutorials/game-physics-libraries">Oyun fiziği kütüphaneleri</a>.</p>
<p>Projeniz için Canvas 2D, WebGL ve WebGPU arasında karar veriyorsanız <a href="/tr/guides/web-games-stack-2026">2026'da Web Oyunları Teknoloji Yığını</a> rehberi, seçeneklerin avantaj ve dezavantajlarını açıklar. <a href="/tr/guides/web-game-engines-comparison">Web Oyun Motorları Karşılaştırması</a> ise oyun döngüsünü sizin yerinize yöneten framework'leri ele alır.</p>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame">MDN: requestAnimationFrame</a> — tarayıcı API referansı</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API">MDN: Canvas API</a> — eksiksiz Canvas 2D dokümantasyonu</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent">MDN: KeyboardEvent</a> — tuş kodları ve olay özellikleri</li>
<li><a href="https://gafferongames.com/post/fix_your_timestep/">Zaman Adımınızı Düzeltin!</a> — Glenn Fiedler'ın sabit zaman adımlı oyun döngüleri üzerine klasik makalesi</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[COOP/COEP ile Wasm iş parçacıklarını (SharedArrayBuffer) etkinleştirin]]></title>
            <link>https://app.cinevva.com/tr/tutorials/coop-coep-sharedarraybuffer</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/coop-coep-sharedarraybuffer</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Kaynaklar arası yalıtım için pratik, kopyalayıp yapıştırılabilir bir rehber: gerekli başlıklar, yaygın hatalar ve SharedArrayBuffer / WebAssembly iş parçacıkları için sunucu yapılandırma örnekleri. Herhangi bir URL’nin kaynaklar arası yalıtılmış olup olmadığını test etmek için ücretsiz bir denetleyici içerir.]]></description>
            <content:encoded><![CDATA[<h1>COOP/COEP ile Wasm iş parçacıklarını (SharedArrayBuffer) etkinleştirin</h1>
<p>Web oyununuz (veya oyun motoru çıktınız) <strong>WebAssembly iş parçacıkları</strong> kullanıyorsa çoğunlukla <code>SharedArrayBuffer</code> özelliğine ihtiyaç duyar. Modern tarayıcılar bunun için <strong>kaynaklar arası yalıtım</strong> gerektirir.</p>
<p><img src="/img/cross-origin-isolation.svg" alt="Şema: kaynaklar arası yalıtım başlıkları"></p>
<h2>1) Hızlı kontrol: Yalıtılmış durumda mısınız?</h2>
<p>Geliştirici Araçları’nı açın ve şunu çalıştırın:</p>
<pre><code class="language-js">self.crossOriginIsolated
</code></pre>
<p>Sonuç <code>true</code> ise <code>SharedArrayBuffer</code> için doğru güvenlik durumundasınız.</p>
<p>Resmî referans:</p>
<ul>
<li><code>https://developer.mozilla.org/docs/Web/API/Window/crossOriginIsolated</code></li>
</ul>
<h2>2) İhtiyacınız olan iki başlık</h2>
<p>HTML yanıtınızda (ve genellikle uygulamanızın tamamında) şu yanıt başlıklarını sunun:</p>
<pre><code class="language-http">Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
</code></pre>
<p>Resmî referanslar:</p>
<ul>
<li><code>https://web.dev/articles/why-coop-coep</code></li>
<li><code>https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer</code></li>
<li><code>https://developer.chrome.com/blog/enabling-shared-array-buffer/</code></li>
</ul>
<h2>3) “Hâlâ çalışmamasının” bir numaralı nedeni: alt kaynaklar</h2>
<p><code>COEP: require-corp</code> kullanıldığında sayfanız artık farklı kaynaklardan gelen içerikleri serbestçe gömemez.</p>
<p>Farklı bir kaynaktan betik, görsel, worker, video vb. yüklüyorsanız bu kaynağın aşağıdakilerden biri aracılığıyla açıkça izin vermesi gerekir:</p>
<ul>
<li>CORS (<code>Access-Control-Allow-Origin</code>) <strong>veya</strong></li>
<li>CORP (<code>Cross-Origin-Resource-Policy</code>)</li>
</ul>
<p>Resmî referans:</p>
<ul>
<li><code>https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cross-Origin_Resource_Policy</code></li>
</ul>
<h2>4) Kopyala/yapıştır yapılandırmaları</h2>
<h3>Nginx</h3>
<pre><code class="language-nginx">add_header Cross-Origin-Opener-Policy &quot;same-origin&quot; always;
add_header Cross-Origin-Embedder-Policy &quot;require-corp&quot; always;
</code></pre>
<h3>Node / Express</h3>
<pre><code class="language-js">app.use((req, res, next) =&gt; {
  res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
  res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
  next()
})
</code></pre>
<h3>Cloudflare Pages (<code>_headers</code>)</h3>
<pre><code class="language-txt">/*
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Embedder-Policy: require-corp
</code></pre>
<h2>5) Hata ayıklama kontrol listesi</h2>
<ul>
<li>Ağ panelindeki <strong>belge</strong> yanıtında başlıkların bulunduğunu doğrulayın.</li>
<li><code>self.crossOriginIsolated</code> değerini kontrol edin.</li>
<li>Engellenen istekleri arayın (Geliştirici Araçları’ndaki yaygın mesajlardan biri “blocked … by COEP” ifadesini içerir).</li>
<li>CORS/CORP desteği sunmayan üçüncü taraf betiklerini kaldırın veya kendi sunucunuzda barındırın.</li>
</ul>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlayın</a></li>
<li><a href="/tr/guides/web-games-stack-2026">2026’da web oyunları için teknoloji yığını</a></li>
<li><a href="/tr/tutorials/web-workers-game-logic">Oyun mantığı için Web Worker’lar</a> — SharedArrayBuffer’dan yararlanan Worker’ları kullanma</li>
<li><a href="/tr/tutorials/websocket-multiplayer-basics">WebSocket ile çok oyunculu oyunların temelleri</a> — kaynaklar arası yalıtıma ihtiyaç duyan çok oyunculu oyunlar</li>
<li><a href="/tr/tutorials/iframe-embedding-games">Oyunları iframe ile gömme</a> — COOP/COEP başlıklarının iframe ile gömülen içerikleri nasıl etkilediği</li>
</ul>
<h2>Harici kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy">MDN: Cross-Origin-Opener-Policy</a> — COOP başlığı referansı</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy">MDN: Cross-Origin-Embedder-Policy</a> — COEP başlığı referansı</li>
<li><a href="https://web.dev/articles/cross-origin-isolation-guide">web.dev: Web sitenizi kaynaklar arası yalıtılmış hâle getirme</a> — pratik dağıtım rehberi</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Oyun girdilerini işleme (klavye, fare, dokunma, oyun kumandası)]]></title>
            <link>https://app.cinevva.com/tr/tutorials/game-input-handling</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/game-input-handling</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Web oyunları için birleşik girdi işleme: pratik kod kalıplarıyla klavye, fare, dokunma ve oyun kumandası desteği.]]></description>
            <content:encoded><![CDATA[<h1>Oyun Girdilerini İşleme</h1>
<p>İyi bir girdi sistemi, bir oyunu başarıya da başarısızlığa da götürebilir. Gecikmeli kontroller, algılanmayan girdiler veya kafa karıştırıcı düğme eşlemeleri, oyuncular daha oyununuzun içeriğini görmeden onları bezdirir. Bu eğitimde klavye, fare, dokunma ve oyun kumandası girdilerini birleşik bir şekilde nasıl işleyeceğinizi öğreneceksiniz.</p>
<p><strong>Önce deneyin:</strong></p>
<InputDemo />
<h2>Girdi Durumu Kalıbı</h2>
<p>Basit yaklaşım, olaylara doğrudan tepki vermektir: kullanıcı bir tuşa bastığında bir şey yap. Bu yaklaşım sorunlara yol açar. Olaylar öngörülemeyen zamanlarda tetiklenirken oyun döngünüz sabit bir hızda çalışır. Oyuncuyu <code>keydown</code> işleyicisinde hareket ettirirseniz hareket hızı, işletim sistemine göre değişen tuş tekrarlama hızına bağlı olur.</p>
<p>Bunun yerine, oyun döngünüzün okuyacağı bir girdi durumu nesnesi tutun:</p>
<pre><code class="language-js">const input = {
  left: false,
  right: false,
  up: false,
  down: false,
  action: false,
  pointer: { x: 0, y: 0, down: false },
}
</code></pre>
<p>Olay işleyicileri bu bayrakları ayarlar. Oyun döngüsü de bunları okur. Bu ayrıştırma her şeyi kolaylaştırır: güncelleme mantığınız, girdinin klavyeden, dokunmadan veya oyun kumandasından gelip gelmediğini önemsemez.</p>
<h2>Klavye</h2>
<p>Fiziksel tuş konumunu belirttiği için <code>e.key</code> yerine <code>e.code</code> kullanın. Böylece WASD, QWERTY dışındaki klavye düzenlerinde de doğru çalışır.</p>
<pre><code class="language-js">const keyMap = {
  ArrowLeft: 'left',
  ArrowRight: 'right',
  ArrowUp: 'up',
  ArrowDown: 'down',
  KeyA: 'left',
  KeyD: 'right',
  KeyW: 'up',
  KeyS: 'down',
  Space: 'action',
}

window.addEventListener('keydown', (e) =&gt; {
  const action = keyMap[e.code]
  if (action) {
    input[action] = true
    e.preventDefault()
  }
})

window.addEventListener('keyup', (e) =&gt; {
  const action = keyMap[e.code]
  if (action) input[action] = false
})
</code></pre>
<p><code>e.preventDefault()</code>, ok tuşlarının sayfayı kaydırmasını engeller. Bunu yalnızca işlediğiniz tuşlar için çağırın.</p>
<h2>Fare</h2>
<p>Fare koordinatları ekran uzayında gelir. Bunları tuval uzayına dönüştürmeniz gerekir:</p>
<pre><code class="language-js">const canvas = document.getElementById('game')

canvas.addEventListener('mousemove', (e) =&gt; {
  const rect = canvas.getBoundingClientRect()
  input.pointer.x = e.clientX - rect.left
  input.pointer.y = e.clientY - rect.top
})

canvas.addEventListener('mousedown', () =&gt; {
  input.pointer.down = true
  input.action = true
})

canvas.addEventListener('mouseup', () =&gt; {
  input.pointer.down = false
  input.action = false
})
</code></pre>
<p>Tuvaliniz ölçeklendirilmişse (duyarlı tuval eğitimine bakın), oyun koordinatlarını elde etmek için ölçek faktörüne bölmeniz gerekir.</p>
<h2>Dokunma</h2>
<p>Dokunma, fareye benzer şekilde çalışır ancak tarayıcının kaydırma veya yakınlaştırma yapmasını engellemek için <code>e.preventDefault()</code> kullanmanız gerekir:</p>
<pre><code class="language-js">canvas.addEventListener('touchstart', (e) =&gt; {
  e.preventDefault()
  const touch = e.touches[0]
  const rect = canvas.getBoundingClientRect()
  input.pointer.x = touch.clientX - rect.left
  input.pointer.y = touch.clientY - rect.top
  input.pointer.down = true
  input.action = true
}, { passive: false })

canvas.addEventListener('touchmove', (e) =&gt; {
  e.preventDefault()
  const touch = e.touches[0]
  const rect = canvas.getBoundingClientRect()
  input.pointer.x = touch.clientX - rect.left
  input.pointer.y = touch.clientY - rect.top
}, { passive: false })

canvas.addEventListener('touchend', () =&gt; {
  input.pointer.down = false
  input.action = false
})
</code></pre>
<p>Modern tarayıcılar performans amacıyla varsayılan olarak pasif dokunma dinleyicileri kullandığından <code>{ passive: false }</code> gereklidir. Bu ayar olmadan <code>preventDefault</code> çalışmaz.</p>
<h2>Mobil Cihazlar İçin Sanal Yön Tuşları</h2>
<p>Mobil cihazlarda yön kontrollerine ihtiyaç duyan oyunlar için ekrana düğmeler ekleyin:</p>
<pre><code class="language-js">function createVirtualDpad(container) {
  const dpad = document.createElement('div')
  dpad.className = 'virtual-dpad'
  dpad.innerHTML = `
    &lt;button data-dir=&quot;up&quot;&gt;↑&lt;/button&gt;
    &lt;button data-dir=&quot;left&quot;&gt;←&lt;/button&gt;
    &lt;button data-dir=&quot;right&quot;&gt;→&lt;/button&gt;
    &lt;button data-dir=&quot;down&quot;&gt;↓&lt;/button&gt;
  `
  
  dpad.querySelectorAll('button').forEach(btn =&gt; {
    const dir = btn.dataset.dir
    btn.addEventListener('touchstart', (e) =&gt; {
      e.preventDefault()
      input[dir] = true
    })
    btn.addEventListener('touchend', () =&gt; input[dir] = false)
  })
  
  container.appendChild(dpad)
}
</code></pre>
<p>Bu düğmeleri başparmakla kullanılabilecek kadar büyük (en az 44x44 piksel) olacak şekilde biçimlendirin ve alt köşelere yerleştirin.</p>
<h2>Oyun Kumandası</h2>
<p>Klavye ve farenin aksine oyun kumandası durumu olay tabanlı değildir. Her karede sorgulamanız gerekir:</p>
<pre><code class="language-js">function pollGamepads() {
  const gamepads = navigator.getGamepads()
  const gp = gamepads[0]
  
  if (gp) {
    // D-pad or left stick
    input.left = gp.buttons[14]?.pressed || gp.axes[0] &lt; -0.5
    input.right = gp.buttons[15]?.pressed || gp.axes[0] &gt; 0.5
    input.up = gp.buttons[12]?.pressed || gp.axes[1] &lt; -0.5
    input.down = gp.buttons[13]?.pressed || gp.axes[1] &gt; 0.5
    
    // A button (standard mapping)
    input.action = gp.buttons[0]?.pressed
  }
}
</code></pre>
<p>Güncelleme fonksiyonunuzun başında <code>pollGamepads()</code> çağrısı yapın. Analog çubuklardaki 0,5 eşiği, küçük çubuk hareketlerinin girdi olarak algılanmaması için bir ölü bölge oluşturur.</p>
<p>Oyun kumandalarının ne zaman bağlandığını ve bağlantısının kesildiğini algılayabilirsiniz:</p>
<pre><code class="language-js">window.addEventListener('gamepadconnected', (e) =&gt; {
  console.log('Gamepad connected:', e.gamepad.id)
})

window.addEventListener('gamepaddisconnected', (e) =&gt; {
  console.log('Gamepad disconnected:', e.gamepad.id)
})
</code></pre>
<h2>Oyun Döngünüzde Girdi Kullanımı</h2>
<p>Artık güncelleme fonksiyonunuz yalnızca girdi durumunu okur:</p>
<pre><code class="language-js">function update(dt) {
  pollGamepads()
  
  if (input.left) player.x -= player.speed * dt
  if (input.right) player.x += player.speed * dt
  if (input.up) player.y -= player.speed * dt
  if (input.down) player.y += player.speed * dt
  
  if (input.action &amp;&amp; player.canShoot) {
    player.shoot()
  }
}
</code></pre>
<p>Bu, girdinin klavyeden, dokunmadan veya oyun kumandasından gelmesinden bağımsız olarak çalışır. Oyun mantığının bunu bilmesi gerekmez.</p>
<h2>Yeni Basıldı ve Basılı Tutuluyor Ayrımı</h2>
<p>Bazen bir düğmenin o anda basılı tutulup tutulmadığını değil, yalnızca bu karede yeni basılıp basılmadığını algılamak istersiniz. Zıplama buna iyi bir örnektir: düğme basılı tutulduğu sürece sürekli zıplamak yerine, her basışta bir kez zıplanmasını istersiniz.</p>
<pre><code class="language-js">const inputPrev = { ...input }

function wasJustPressed(action) {
  return input[action] &amp;&amp; !inputPrev[action]
}

function endFrame() {
  Object.assign(inputPrev, input)
}
</code></pre>
<p>Güncelleme döngünüzün sonunda <code>endFrame()</code> çağrısı yapın. Ardından tek seferlik eylemler için <code>input.action</code> yerine <code>wasJustPressed('action')</code> kullanın.</p>
<h2>Odak Kaybı</h2>
<p>Tarayıcı penceresi odağı kaybettiğinde klavye olayları tetiklenmeyi bırakır. Oyuncu bir tuşu basılı tutuyorsa tuş basılı kalmış gibi davranır:</p>
<pre><code class="language-js">window.addEventListener('blur', () =&gt; {
  input.left = input.right = input.up = input.down = input.action = false
})
</code></pre>
<p>Bu olmadan oyuncu, kullanıcı Alt+Tab ile başka bir pencereye geçtiğinde hareket etmeye devam eder.</p>
<h2>En İyi Uygulamalar</h2>
<p>Oyuncuların farklı tercihleri olduğundan hem WASD hem de ok tuşlarını destekleyin. Tuşların yeniden atanmasına izin verin ve yapılandırmayı localStorage içinde saklayın. Mobil cihazlarda dokunmatik kontroller sağlayın. Xbox, PlayStation ve genel amaçlı oyun kumandalarının her biri biraz farklı davrandığından gerçek kumandalarla test yapın. Ayrıca odak kaybını her zaman sorunsuz biçimde yönetin.</p>
<h2>Diğer Kaynaklar</h2>
<p><a href="/tr/tutorials/gamepad-api">Gamepad API'ye ayrıntılı bakış</a>, titreşim ve birden fazla oyuncu gibi gelişmiş oyun kumandası özelliklerini ele alır.</p>
<p><a href="/tr/tutorials/mobile-friendly-web-games">Mobil uyumlu web oyunları</a>, dokunmatik girdiyi daha ayrıntılı biçimde ele alır.</p>
<p><a href="/tr/tutorials/pointer-lock-fps">FPS oyunları için Pointer Lock</a>, birinci şahıs kontrolleri için fareyi nasıl yakalayacağınızı gösterir.</p>
<p><a href="/tr/tutorials/canvas-2d-game-loop">Canvas 2D oyun döngüsü</a>, eksiksiz bir oyun döngüsüne entegre edilmiş girdi durumu kalıbını gösterir.</p>
<p><a href="/tr/tutorials/websocket-multiplayer-basics">WebSocket ile çok oyunculu oyunların temelleri</a>, ağ üzerinden bağlı oyuncular arasında girdilerin eşitlenmesini ele alır.</p>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code">MDN: KeyboardEvent.code</a> — girdi eşleme için fiziksel tuş değerleri</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events">MDN: İşaretçi olayları</a> — birleşik fare ve dokunma olayları</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API">MDN: Gamepad API</a> — tarayıcıda oyun kumandası desteği</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Touch_events">MDN: Dokunma olayları</a> — mobil cihazlarda çoklu dokunma yönetimi</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Web oyunları için oyun fiziği]]></title>
            <link>https://app.cinevva.com/tr/tutorials/game-physics-libraries</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/game-physics-libraries</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Web oyunlarına yönelik fizik motorlarını karşılaştırın: Rapier, Cannon-es, Ammo.js, Matter.js, Planck.js. Performans testleri, kod örnekleri ve her birinin ne zaman kullanılacağı.]]></description>
            <content:encoded><![CDATA[<h1>Web oyunları için oyun fiziği</h1>
<p>Fizik simülasyonu; düşen nesneler, zıplayan toplar, ragdoll karakterler, araçlar ve yok edilebilir ortamlarla oyunlara hayat verir. Bu rehber, web oyunlarında kullanılabilen başlıca fizik kütüphanelerini gerçek kod örnekleri ve dürüst artı/eksi değerlendirmeleriyle ele alıyor.</p>
<h2>Genel bakış</h2>
<table>
<thead>
<tr>
<th>Motor</th>
<th style="text-align:center">Boyut</th>
<th style="text-align:center">Performans</th>
<th style="text-align:center">Boyut</th>
<th style="text-align:center">Zorluk</th>
<th style="text-align:center">Yumuşak Cisimler</th>
<th style="text-align:center">Araçlar</th>
<th style="text-align:center">CCD</th>
<th style="text-align:center">Belirlenimlilik</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Rapier</strong></td>
<td style="text-align:center">2D/3D</td>
<td style="text-align:center">⭐⭐⭐⭐⭐</td>
<td style="text-align:center">1.4 MB</td>
<td style="text-align:center">Orta</td>
<td style="text-align:center">—</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
</tr>
<tr>
<td><strong>Cannon-es</strong></td>
<td style="text-align:center">3D</td>
<td style="text-align:center">⭐⭐⭐</td>
<td style="text-align:center">150 KB</td>
<td style="text-align:center">Kolay</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
</tr>
<tr>
<td><strong>Ammo.js</strong></td>
<td style="text-align:center">3D</td>
<td style="text-align:center">⭐⭐⭐⭐⭐</td>
<td style="text-align:center">1-2 MB</td>
<td style="text-align:center">Zor</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">⚠️</td>
</tr>
<tr>
<td><strong>Jolt</strong></td>
<td style="text-align:center">3D</td>
<td style="text-align:center">⭐⭐⭐⭐⭐</td>
<td style="text-align:center">—</td>
<td style="text-align:center">Zor</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
</tr>
<tr>
<td><strong>Oimo.js</strong></td>
<td style="text-align:center">3D</td>
<td style="text-align:center">⭐⭐⭐</td>
<td style="text-align:center">100 KB</td>
<td style="text-align:center">Kolay</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
</tr>
<tr>
<td><strong>Matter.js</strong></td>
<td style="text-align:center">2D</td>
<td style="text-align:center">⭐⭐⭐</td>
<td style="text-align:center">80 KB</td>
<td style="text-align:center">Kolay</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">⚠️</td>
</tr>
<tr>
<td><strong>Planck.js</strong></td>
<td style="text-align:center">2D</td>
<td style="text-align:center">⭐⭐⭐⭐</td>
<td style="text-align:center">120 KB</td>
<td style="text-align:center">Orta</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">⚠️</td>
</tr>
<tr>
<td><strong>p2-es</strong></td>
<td style="text-align:center">2D</td>
<td style="text-align:center">⭐⭐⭐</td>
<td style="text-align:center">100 KB</td>
<td style="text-align:center">Orta</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
</tr>
<tr>
<td><strong>Box2D WASM</strong></td>
<td style="text-align:center">2D</td>
<td style="text-align:center">⭐⭐⭐⭐⭐</td>
<td style="text-align:center">300 KB</td>
<td style="text-align:center">Zor</td>
<td style="text-align:center">—</td>
<td style="text-align:center">—</td>
<td style="text-align:center">✅</td>
<td style="text-align:center">✅</td>
</tr>
</tbody>
</table>
<p><strong>Açıklamalar:</strong></p>
<ul>
<li>CCD = Sürekli Çarpışma Algılama (hızlı nesnelerin duvarların içinden geçmesini önler)</li>
<li>⚠️ Belirlenimlilik = Sabit zaman adımıyla mümkün olsa da platformlar arasında garanti edilmez</li>
</ul>
<h3>Hızlı öneriler</h3>
<table>
<thead>
<tr>
<th>Durumunuz</th>
<th>En iyi seçim</th>
</tr>
</thead>
<tbody>
<tr>
<td>Yayına hazır 3D oyun</td>
<td><strong>Rapier</strong> — en iyi performans, modern API, aktif geliştirme</td>
</tr>
<tr>
<td>Öğrenme / prototip</td>
<td><strong>Cannon-es</strong> (3D) veya <strong>Matter.js</strong> (2D) — basit API'ler, kolay hata ayıklama</td>
</tr>
<tr>
<td>Araç fiziği</td>
<td><strong>Rapier</strong> veya <strong>Ammo.js</strong> — ikisinde de ışın izlemeli araç denetleyicileri bulunur</td>
</tr>
<tr>
<td>Yumuşak cisimler, kumaşlar, halatlar</td>
<td><strong>Ammo.js</strong> veya <strong>Jolt (JoltPhysics.js)</strong> — ikisi de kumaşları ve yumuşak cisimleri destekler; Jolt'un WASM portu daha aktif biçimde geliştirilmektedir</td>
</tr>
<tr>
<td>Hassas 2D platform oyunu</td>
<td><strong>Planck.js</strong> — Box2D algoritmaları, sabit zaman adımıyla belirlenimlilik</td>
</tr>
<tr>
<td>En yüksek 2D performansı</td>
<td><strong>Box2D WASM</strong> — tarayıcıda yerel kod hızı</td>
</tr>
<tr>
<td>En küçük paket boyutu</td>
<td><strong>Oimo.js</strong> (3D) veya <strong>Matter.js</strong> (2D)</td>
</tr>
</tbody>
</table>
<hr>
<h2>Ayrıntılı karşılaştırma</h2>
<h3>3D Fizik Motorları</h3>
<table>
<thead>
<tr>
<th>Motor</th>
<th>Dil</th>
<th>Boyut</th>
<th>Performans</th>
<th>En uygun kullanım</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Rapier</strong></td>
<td>Rust/WASM</td>
<td>~1.4 MB</td>
<td>Mükemmel</td>
<td>Yayına hazır oyunlar, karmaşık simülasyonlar</td>
</tr>
<tr>
<td><strong>Cannon-es</strong></td>
<td>JavaScript</td>
<td>~150 KB</td>
<td>İyi</td>
<td>Prototipler, basit oyunlar</td>
</tr>
<tr>
<td><strong>Ammo.js</strong></td>
<td>C++/WASM</td>
<td>~1-2 MB</td>
<td>Mükemmel</td>
<td>AAA özellikleri, yumuşak cisimler, araçlar</td>
</tr>
<tr>
<td><strong>Jolt</strong></td>
<td>C++/WASM</td>
<td>WASM</td>
<td>Mükemmel</td>
<td>Aktif bakımla sunulan AAA özellikleri</td>
</tr>
<tr>
<td><strong>Oimo.js</strong></td>
<td>JavaScript</td>
<td>~100 KB</td>
<td>İyi</td>
<td>Basit oyunlar, hızlı prototipler</td>
</tr>
</tbody>
</table>
<h3>2D Fizik Motorları</h3>
<table>
<thead>
<tr>
<th>Motor</th>
<th>Dil</th>
<th>Boyut</th>
<th>Performans</th>
<th>En uygun kullanım</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Matter.js</strong></td>
<td>JavaScript</td>
<td>~80 KB</td>
<td>İyi</td>
<td>Görsel oyunlar, prototipler</td>
</tr>
<tr>
<td><strong>Planck.js</strong></td>
<td>JavaScript</td>
<td>~120 KB</td>
<td>İyi</td>
<td>Platform oyunları, hassas fizik</td>
</tr>
<tr>
<td><strong>p2-es</strong></td>
<td>JavaScript</td>
<td>~100 KB</td>
<td>İyi</td>
<td>Kısıtlamalar, mekanizmalar</td>
</tr>
<tr>
<td><strong>Box2D WASM</strong></td>
<td>C++/WASM</td>
<td>~300 KB</td>
<td>Mükemmel</td>
<td>Çok sayıda fizik cismi</td>
</tr>
</tbody>
</table>
<hr>
<h2>3D Fizik Motorları</h2>
<h3>Rapier — Modern seçim</h3>
<p>Rapier, JavaScript/WASM bağlayıcılarına sahip bir Rust fizik motorudur. 2024 sürümüne kıyasla 2-5 kat hız artışı sunarak 2025-2026 döneminde web oyunları için en yüksek performanslı seçenek hâline gelmiştir.</p>
<p><strong>Kurulum:</strong></p>
<pre><code class="language-bash">npm install @dimforge/rapier3d
# Or with SIMD (faster, requires modern browsers):
npm install @dimforge/rapier3d-simd
</code></pre>
<p><strong>Temel kurulum:</strong></p>
<pre><code class="language-js">import RAPIER from '@dimforge/rapier3d'

// Initialize (async required for WASM)
await RAPIER.init()

// Create world with gravity
const gravity = { x: 0, y: -9.81, z: 0 }
const world = new RAPIER.World(gravity)

// Create ground (static body)
const groundDesc = RAPIER.RigidBodyDesc.fixed()
const groundBody = world.createRigidBody(groundDesc)
const groundCollider = RAPIER.ColliderDesc.cuboid(50, 0.1, 50)
world.createCollider(groundCollider, groundBody)

// Create falling box (dynamic body)
const boxDesc = RAPIER.RigidBodyDesc.dynamic()
  .setTranslation(0, 10, 0)
const boxBody = world.createRigidBody(boxDesc)
const boxCollider = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5)
  .setDensity(1.0)
  .setRestitution(0.5)
world.createCollider(boxCollider, boxBody)
</code></pre>
<p><strong>Oyun döngüsü entegrasyonu:</strong></p>
<pre><code class="language-js">const FIXED_TIMESTEP = 1 / 60

function physicsStep() {
  world.step()
}

function gameLoop() {
  physicsStep()
  
  // Sync render objects with physics
  const position = boxBody.translation()
  const rotation = boxBody.rotation()
  
  // Update your Three.js/Babylon mesh
  mesh.position.set(position.x, position.y, position.z)
  mesh.quaternion.set(rotation.x, rotation.y, rotation.z, rotation.w)
  
  requestAnimationFrame(gameLoop)
}
</code></pre>
<p><strong>Çarpışma algılama:</strong></p>
<pre><code class="language-js">// Event-based collision detection
world.contactPairsWith(boxCollider, (otherCollider) =&gt; {
  console.log('Box is touching:', otherCollider)
})

// Ray casting
const ray = new RAPIER.Ray({ x: 0, y: 10, z: 0 }, { x: 0, y: -1, z: 0 })
const hit = world.castRay(ray, 100, true)
if (hit) {
  const hitPoint = ray.pointAt(hit.timeOfImpact)
  console.log('Hit at:', hitPoint)
}
</code></pre>
<p><strong>Eklemler:</strong></p>
<pre><code class="language-js">// Create a hinge joint (door)
const jointData = RAPIER.JointData.revolute(
  { x: 0, y: 0, z: 0 },  // Anchor on body1
  { x: -1, y: 0, z: 0 }, // Anchor on body2
  { x: 0, y: 1, z: 0 }   // Rotation axis
)
world.createImpulseJoint(jointData, body1, body2, true)
</code></pre>
<p><strong>Araç denetleyicisi:</strong></p>
<pre><code class="language-js">// Create chassis rigid body
const chassisDesc = RAPIER.RigidBodyDesc.dynamic()
  .setTranslation(0, 2, 0)
const chassis = world.createRigidBody(chassisDesc)
const chassisCollider = RAPIER.ColliderDesc.cuboid(1, 0.5, 2)
  .setDensity(100)
world.createCollider(chassisCollider, chassis)

// Create vehicle controller
const vehicle = world.createVehicleController(chassis)

// Add wheels (front-left, front-right, rear-left, rear-right)
const suspensionRestLength = 0.3
const wheelRadius = 0.4

// Front wheels (steering)
vehicle.addWheel(
  { x: -0.8, y: 0, z: 1.2 },   // Connection point
  { x: 0, y: -1, z: 0 },       // Suspension direction
  { x: -1, y: 0, z: 0 },       // Axle direction
  suspensionRestLength,
  wheelRadius
)
vehicle.addWheel(
  { x: 0.8, y: 0, z: 1.2 },
  { x: 0, y: -1, z: 0 },
  { x: -1, y: 0, z: 0 },
  suspensionRestLength,
  wheelRadius
)

// Rear wheels (drive)
vehicle.addWheel(
  { x: -0.8, y: 0, z: -1.2 },
  { x: 0, y: -1, z: 0 },
  { x: -1, y: 0, z: 0 },
  suspensionRestLength,
  wheelRadius
)
vehicle.addWheel(
  { x: 0.8, y: 0, z: -1.2 },
  { x: 0, y: -1, z: 0 },
  { x: -1, y: 0, z: 0 },
  suspensionRestLength,
  wheelRadius
)

// Configure suspension for all wheels
for (let i = 0; i &lt; 4; i++) {
  vehicle.setWheelSuspensionStiffness(i, 30)
  vehicle.setWheelSuspensionCompression(i, 4.4)
  vehicle.setWheelSuspensionRelaxation(i, 2.3)
  vehicle.setWheelMaxSuspensionTravel(i, 0.5)
  vehicle.setWheelFrictionSlip(i, 2)
}

// In game loop
function updateVehicle(steering, engineForce, brakeForce) {
  // Steering (front wheels only)
  vehicle.setWheelSteering(0, steering)
  vehicle.setWheelSteering(1, steering)
  
  // Engine (rear wheels)
  vehicle.setWheelEngineForce(2, engineForce)
  vehicle.setWheelEngineForce(3, engineForce)
  
  // Brakes (all wheels)
  for (let i = 0; i &lt; 4; i++) {
    vehicle.setWheelBrake(i, brakeForce)
  }
  
  // Update vehicle physics
  vehicle.updateVehicle(world.timestep)
}
</code></pre>
<p><strong>Artıları:</strong></p>
<ul>
<li>En iyi performans (WASM + SIMD)</li>
<li>Mükemmel çarpışma algılama doğruluğu</li>
<li>Platformlar arası belirlenimlilik (aynı girdiler = aynı çıktılar)</li>
<li>Aktif geliştirme, modern API</li>
<li>Sürekli çarpışma algılama (nesneler yüzeylerin içinden geçmez)</li>
<li>Yerleşik karakter ve araç denetleyicileri</li>
</ul>
<p><strong>Eksileri:</strong></p>
<ul>
<li>Daha büyük paket boyutu (~1.4 MB)</li>
<li>Eşzamansız başlatma gerektirir</li>
<li>Saf JS alternatiflerinden daha karmaşık API</li>
<li>WASM'de hata ayıklamak zor olabilir</li>
</ul>
<hr>
<h3>Cannon-es — Basit ve etkili</h3>
<p>Cannon-es, Cannon.js'in bakımı sürdürülen çatallanmış sürümüdür. Saf JavaScript ile yazılmıştır; anlaşılması kolaydır ve öğrenme ile prototip geliştirme için idealdir.</p>
<p><strong>Kurulum:</strong></p>
<pre><code class="language-bash">npm install cannon-es
</code></pre>
<p><strong>Temel kurulum:</strong></p>
<pre><code class="language-js">import * as CANNON from 'cannon-es'

// Create world
const world = new CANNON.World({
  gravity: new CANNON.Vec3(0, -9.81, 0)
})

// Ground
const groundBody = new CANNON.Body({
  type: CANNON.Body.STATIC,
  shape: new CANNON.Plane()
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
world.addBody(groundBody)

// Falling sphere
const sphereBody = new CANNON.Body({
  mass: 1,
  shape: new CANNON.Sphere(0.5),
  position: new CANNON.Vec3(0, 10, 0)
})
sphereBody.linearDamping = 0.1
world.addBody(sphereBody)
</code></pre>
<p><strong>Oyun döngüsü:</strong></p>
<pre><code class="language-js">const TIMESTEP = 1 / 60

function animate() {
  world.step(TIMESTEP)
  
  // Sync with Three.js
  mesh.position.copy(sphereBody.position)
  mesh.quaternion.copy(sphereBody.quaternion)
  
  requestAnimationFrame(animate)
}
</code></pre>
<p><strong>Çarpışma olayları:</strong></p>
<pre><code class="language-js">sphereBody.addEventListener('collide', (event) =&gt; {
  const contact = event.contact
  const impactVelocity = contact.getImpactVelocityAlongNormal()
  
  if (Math.abs(impactVelocity) &gt; 5) {
    console.log('Hard impact!')
  }
})
</code></pre>
<p><strong>Kısıtlamalar:</strong></p>
<pre><code class="language-js">// Distance constraint (rope-like)
const constraint = new CANNON.DistanceConstraint(
  bodyA, bodyB, 
  2 // distance
)
world.addConstraint(constraint)

// Hinge constraint
const hinge = new CANNON.HingeConstraint(bodyA, bodyB, {
  pivotA: new CANNON.Vec3(1, 0, 0),
  axisA: new CANNON.Vec3(0, 1, 0),
  pivotB: new CANNON.Vec3(-1, 0, 0),
  axisB: new CANNON.Vec3(0, 1, 0)
})
world.addConstraint(hinge)
</code></pre>
<p><strong>Artıları:</strong></p>
<ul>
<li>Saf JavaScript, WASM karmaşıklığı yok</li>
<li>Küçük paket boyutu (~150 KB)</li>
<li>Öğrenmesi ve hata ayıklaması kolay</li>
<li>Her yerde çalışır</li>
<li>Güçlü Three.js entegrasyonu</li>
<li>İyi dokümantasyon</li>
</ul>
<p><strong>Eksileri:</strong></p>
<ul>
<li>WASM alternatiflerinden daha yavaş</li>
<li>Çok sayıda cisimle (&gt;100) zorlanır</li>
<li>Sınırlı üçgen örgü desteği</li>
<li>Yerleşik CCD yoktur (nesneler yüzeylerin içinden geçebilir)</li>
<li>Geliştirme etkinliği daha düşük</li>
</ul>
<hr>
<h3>Ammo.js — Bullet Physics'in tüm gücü</h3>
<p>Ammo.js, WebAssembly'ye derlenmiş Bullet Physics motorudur. Yumuşak cisimler, araçlar ve gelişmiş kısıtlamalar dâhil en kapsamlı özellik setini sunar.</p>
<p><strong>Kurulum:</strong></p>
<pre><code class="language-bash">npm install ammo.js
# Or use from CDN
</code></pre>
<p><strong>Temel kurulum:</strong></p>
<pre><code class="language-js">import Ammo from 'ammo.js'

let physicsWorld

async function initPhysics() {
  await Ammo()
  
  const collisionConfig = new Ammo.btDefaultCollisionConfiguration()
  const dispatcher = new Ammo.btCollisionDispatcher(collisionConfig)
  const broadphase = new Ammo.btDbvtBroadphase()
  const solver = new Ammo.btSequentialImpulseConstraintSolver()
  
  physicsWorld = new Ammo.btDiscreteDynamicsWorld(
    dispatcher, broadphase, solver, collisionConfig
  )
  physicsWorld.setGravity(new Ammo.btVector3(0, -9.81, 0))
}

function createBox(mass, width, height, depth, x, y, z) {
  const transform = new Ammo.btTransform()
  transform.setIdentity()
  transform.setOrigin(new Ammo.btVector3(x, y, z))
  
  const motionState = new Ammo.btDefaultMotionState(transform)
  const shape = new Ammo.btBoxShape(
    new Ammo.btVector3(width / 2, height / 2, depth / 2)
  )
  
  const localInertia = new Ammo.btVector3(0, 0, 0)
  if (mass &gt; 0) {
    shape.calculateLocalInertia(mass, localInertia)
  }
  
  const rbInfo = new Ammo.btRigidBodyConstructionInfo(
    mass, motionState, shape, localInertia
  )
  const body = new Ammo.btRigidBody(rbInfo)
  
  physicsWorld.addRigidBody(body)
  return body
}
</code></pre>
<p><strong>Yumuşak cisimler (kumaş):</strong></p>
<pre><code class="language-js">function createCloth(width, height, segments) {
  const softBodyHelpers = new Ammo.btSoftBodyHelpers()
  
  const corner00 = new Ammo.btVector3(-width/2, height, 0)
  const corner10 = new Ammo.btVector3(width/2, height, 0)
  const corner01 = new Ammo.btVector3(-width/2, 0, 0)
  const corner11 = new Ammo.btVector3(width/2, 0, 0)
  
  const softBody = softBodyHelpers.CreatePatch(
    physicsWorld.getWorldInfo(),
    corner00, corner10, corner01, corner11,
    segments, segments,
    0, true
  )
  
  const sbConfig = softBody.get_m_cfg()
  sbConfig.set_viterations(10)
  sbConfig.set_piterations(10)
  
  softBody.setTotalMass(0.9, false)
  Ammo.castObject(softBody, Ammo.btCollisionObject)
    .getCollisionShape().setMargin(0.05)
  
  physicsWorld.addSoftBody(softBody, 1, -1)
  return softBody
}
</code></pre>
<p><strong>Araç fiziği:</strong></p>
<pre><code class="language-js">function createVehicle(chassisBody) {
  const tuning = new Ammo.btVehicleTuning()
  const rayCaster = new Ammo.btDefaultVehicleRaycaster(physicsWorld)
  const vehicle = new Ammo.btRaycastVehicle(tuning, chassisBody, rayCaster)
  
  vehicle.setCoordinateSystem(0, 1, 2)
  physicsWorld.addAction(vehicle)
  
  // Add wheels
  const wheelRadius = 0.4
  const wheelWidth = 0.3
  const suspensionRestLength = 0.3
  
  const wheelDirectionCS = new Ammo.btVector3(0, -1, 0)
  const wheelAxleCS = new Ammo.btVector3(-1, 0, 0)
  
  function addWheel(isFront, pos) {
    const wheelInfo = vehicle.addWheel(
      pos, wheelDirectionCS, wheelAxleCS,
      suspensionRestLength, wheelRadius, tuning, isFront
    )
    wheelInfo.set_m_suspensionStiffness(20)
    wheelInfo.set_m_wheelsDampingRelaxation(2.3)
    wheelInfo.set_m_wheelsDampingCompression(4.4)
    wheelInfo.set_m_frictionSlip(1000)
    wheelInfo.set_m_rollInfluence(0.1)
  }
  
  addWheel(true, new Ammo.btVector3(1, 0, 1.5))   // Front left
  addWheel(true, new Ammo.btVector3(-1, 0, 1.5))  // Front right
  addWheel(false, new Ammo.btVector3(1, 0, -1.5)) // Rear left
  addWheel(false, new Ammo.btVector3(-1, 0, -1.5))// Rear right
  
  return vehicle
}
</code></pre>
<p><strong>Artıları:</strong></p>
<ul>
<li>En kapsamlı özellik seti</li>
<li>Yumuşak cisimler, kumaşlar, halatlar</li>
<li>Gelişmiş araç fiziği</li>
<li>Savaş koşullarında kendini kanıtlamış teknoloji (birçok AAA oyunda kullanılır)</li>
<li>Son derece özelleştirilebilir
<strong>Eksileri:</strong></li>
<li>Karmaşık ve ayrıntılı API</li>
<li>Büyük paket boyutu</li>
<li>Bellek yönetimi gerektirir (nesneleri yok etme)</li>
<li>Zorlu öğrenme süreci</li>
<li>Dağınık dokümantasyon</li>
<li>Platformlar arasında deterministik değildir (dikkatli yapılandırmayla yalnızca aynı cihazda)</li>
</ul>
<hr>
<h3>Jolt — modern, AAA kalitesinde 3D fizik</h3>
<p>Jolt Physics, Horizon Forbidden West gibi oyunların arkasındaki motordur; JoltPhysics.js ise onu bir WASM portu olarak tarayıcıya getirir. Rapier ile Ammo.js arasında güçlü bir orta yoldur: Rapier'den daha fazla özellik (yumuşak cisimler, kumaş, tekerlekli araç kontrolcüsü) sunarken Ammo.js'de bulunmayan aktif bakıma sahiptir. npm paketi <code>jolt-physics</code>'tir; React Three Fiber (<code>@react-three/jolt</code>) ve Babylon.js için hazır entegrasyonlar da bulunur.</p>
<pre><code class="language-bash">npm install jolt-physics
</code></pre>
<p>Rapier gibi, C++ arayüzünü yansıtan ayrıntılı ama eksiksiz bir API'ye sahip, eşzamansız başlatılan bir WASM çözümüdür. Bullet sınıfı özellikler (yumuşak cisimler, araçlar) istediğiniz ancak Ammo.js yerine bakımı sürdürülen modern bir kod tabanı tercih ettiğiniz durumlarda Jolt'u kullanın.</p>
<hr>
<h3>Oimo.js — Hafif ve hızlı</h3>
<p>Oimo.js hafif bir 3D fizik motorudur. Gelişmiş özelliklere ihtiyaç duymadığınız basit oyunlar için idealdir.</p>
<p><strong>Kurulum:</strong></p>
<pre><code class="language-bash">npm install oimo
</code></pre>
<p><strong>Temel kurulum:</strong></p>
<pre><code class="language-js">import * as OIMO from 'oimo'

const world = new OIMO.World({
  timestep: 1/60,
  iterations: 8,
  broadphase: 2, // 1: kaba kuvvet, 2: süpür ve buda, 3: hacim ağacı
  worldscale: 1,
  random: true,
  gravity: [0, -9.8, 0]
})

// Zemini oluştur
world.add({
  type: 'box',
  size: [100, 1, 100],
  pos: [0, -0.5, 0],
  move: false
})

// Düşen küreyi oluştur
const sphere = world.add({
  type: 'sphere',
  size: [1],
  pos: [0, 10, 0],
  move: true,
  density: 1,
  friction: 0.4,
  restitution: 0.2
})
</code></pre>
<p><strong>Oyun döngüsü:</strong></p>
<pre><code class="language-js">function animate() {
  world.step()
  
  // Konumu/dönüşü al
  const pos = sphere.getPosition()
  const rot = sphere.getQuaternion()
  
  mesh.position.set(pos.x, pos.y, pos.z)
  mesh.quaternion.set(rot.x, rot.y, rot.z, rot.w)
  
  requestAnimationFrame(animate)
}
</code></pre>
<p><strong>Artıları:</strong></p>
<ul>
<li>Çok küçük (~100 KB)</li>
<li>Basit API</li>
<li>Temel sahnelerde iyi performans</li>
<li>Yerleşik Babylon.js desteği</li>
</ul>
<p><strong>Eksileri:</strong></p>
<ul>
<li>Sınırlı şekiller (yalnızca temel geometriler)</li>
<li>Yumuşak cisim desteği yok</li>
<li>Yetersiz dokümantasyon</li>
<li>Daha az aktif geliştirme</li>
<li>Sınırlı eklem seçenekleri</li>
</ul>
<hr>
<h2>2D Fizik Motorları</h2>
<h3>Matter.js — Güzel ve sezgisel</h3>
<p>Matter.js, mükemmel render ve hata ayıklama araçlarına sahip, özellik bakımından zengin bir 2D fizik motorudur.</p>
<p><strong>Kurulum:</strong></p>
<pre><code class="language-bash">npm install matter-js
</code></pre>
<p><strong>Temel kurulum:</strong></p>
<pre><code class="language-js">import Matter from 'matter-js'

const { Engine, Render, World, Bodies, Runner } = Matter

// Motoru oluştur
const engine = Engine.create()

// Render aracını oluştur (isteğe bağlı, hata ayıklama için harika)
const render = Render.create({
  element: document.body,
  engine: engine,
  options: {
    width: 800,
    height: 600,
    wireframes: false
  }
})

// Cisimleri oluştur
const ground = Bodies.rectangle(400, 580, 810, 60, { 
  isStatic: true 
})

const box = Bodies.rectangle(400, 200, 80, 80, {
  restitution: 0.8,
  friction: 0.5
})

const circle = Bodies.circle(300, 100, 40, {
  restitution: 0.9
})

// Dünyaya ekle
World.add(engine.world, [ground, box, circle])

// Çalıştır
Render.run(render)
Runner.run(Runner.create(), engine)
</code></pre>
<p><strong>Çarpışma olayları:</strong></p>
<pre><code class="language-js">Matter.Events.on(engine, 'collisionStart', (event) =&gt; {
  event.pairs.forEach(pair =&gt; {
    console.log('Şunlar arasında çarpışma:', pair.bodyA.label, pair.bodyB.label)
  })
})
</code></pre>
<p><strong>Kısıtlamalar:</strong></p>
<pre><code class="language-js">// Sabitleme kısıtlaması
const pin = Matter.Constraint.create({
  pointA: { x: 400, y: 100 },
  bodyB: box,
  stiffness: 0.9
})

// Cisimler arasında yay
const spring = Matter.Constraint.create({
  bodyA: box,
  bodyB: circle,
  stiffness: 0.01,
  length: 100
})

World.add(engine.world, [pin, spring])
</code></pre>
<p><strong>Fare etkileşimi:</strong></p>
<pre><code class="language-js">const mouse = Matter.Mouse.create(render.canvas)
const mouseConstraint = Matter.MouseConstraint.create(engine, {
  mouse: mouse,
  constraint: {
    stiffness: 0.2,
    render: { visible: false }
  }
})
World.add(engine.world, mouseConstraint)
</code></pre>
<p><strong>Artıları:</strong></p>
<ul>
<li>Güzel varsayılan render</li>
<li>Öğrenme/prototipleme için harika</li>
<li>Sezgisel API</li>
<li>İyi dokümantasyon</li>
<li>Aktif topluluk</li>
<li>Varsayılan olarak deterministik — Matter.Runner artık sabit ve deterministik bir zaman adımı kullanıyor (sabit olmayan zaman adımı v0.20.0'da kaldırıldı)</li>
</ul>
<p><strong>Eksileri:</strong></p>
<ul>
<li>CCD yok (hızlı nesneler diğer cisimlerin içinden geçebilir; bunu azaltmak için alt adımlama kullanın)</li>
<li>Çok sayıda cisimde performans sorunları</li>
<li>WASM seçeneği yok</li>
<li>Karmaşık simülasyonlarda sınırlı hassasiyet</li>
</ul>
<hr>
<h3>Planck.js — JavaScript için Box2D</h3>
<p>Planck.js, Box2D'nin JavaScript ile baştan yazılmış eksiksiz bir sürümüdür. Savaşta sınanmış fizik sunar ve sabit zaman adımı kullanıldığında deterministiktir.</p>
<p><strong>Kurulum:</strong></p>
<pre><code class="language-bash">npm install planck
</code></pre>
<p><strong>Temel kurulum:</strong></p>
<pre><code class="language-js">import { World, Vec2, Box, Circle, Edge } from 'planck'

// Dünyayı oluştur
const world = new World({
  gravity: Vec2(0, -10)
})

// Zemini oluştur
const ground = world.createBody()
ground.createFixture({
  shape: Edge(Vec2(-40, 0), Vec2(40, 0))
})

// Dinamik kutuyu oluştur
const box = world.createBody({
  type: 'dynamic',
  position: Vec2(0, 10)
})
box.createFixture({
  shape: Box(1, 1),
  density: 1,
  friction: 0.3,
  restitution: 0.5
})
</code></pre>
<p><strong>Sabit zaman adımlı oyun döngüsü:</strong></p>
<pre><code class="language-js">const TIMESTEP = 1 / 60
const VELOCITY_ITERATIONS = 8
const POSITION_ITERATIONS = 3

function gameLoop() {
  world.step(TIMESTEP, VELOCITY_ITERATIONS, POSITION_ITERATIONS)
  
  // Tüm cisimleri dolaş
  for (let body = world.getBodyList(); body; body = body.getNext()) {
    const pos = body.getPosition()
    const angle = body.getAngle()
    // Sprite'larınızı güncelleyin...
  }
  
  requestAnimationFrame(gameLoop)
}
</code></pre>
<p><strong>Çarpışma geri çağrıları:</strong></p>
<pre><code class="language-js">world.on('begin-contact', (contact) =&gt; {
  const fixtureA = contact.getFixtureA()
  const fixtureB = contact.getFixtureB()
  console.log('Temas başladı')
})

world.on('end-contact', (contact) =&gt; {
  console.log('Temas sona erdi')
})

world.on('pre-solve', (contact, oldManifold) =&gt; {
  // Temas burada devre dışı bırakılabilir
  // contact.setEnabled(false)
})
</code></pre>
<p><strong>Eklemler:</strong></p>
<pre><code class="language-js">import { RevoluteJoint, DistanceJoint, PrismaticJoint } from 'planck'

// Döner eklem (menteşe)
const joint = world.createJoint(RevoluteJoint({
  bodyA: ground,
  bodyB: box,
  localAnchorA: Vec2(0, 5),
  localAnchorB: Vec2(-1, 0),
  enableMotor: true,
  maxMotorTorque: 1000,
  motorSpeed: 2
}))

// Mesafe eklemi (yay)
world.createJoint(DistanceJoint({
  bodyA: boxA,
  bodyB: boxB,
  localAnchorA: Vec2(0, 0),
  localAnchorB: Vec2(0, 0),
  length: 5,
  stiffness: 10,
  damping: 0.5
}))
</code></pre>
<p><strong>Artıları:</strong></p>
<ul>
<li>Sabit zaman adımıyla deterministik</li>
<li>İyi belgelenmiş (Box2D dokümanları geçerlidir)</li>
<li>İyi performans</li>
<li>TypeScript desteği</li>
<li>Küçük paket boyutu</li>
</ul>
<p><strong>Eksileri:</strong></p>
<ul>
<li>CCD eklemleri işlemez; bu nedenle eklemlerle bağlanan hızlı nesneler esneyebilir</li>
<li>Matter.js'e göre daha zorlu öğrenme süreci</li>
<li>Yerleşik render aracı yok</li>
<li>Box2D'ye özgü ayrıntılar (birim ölçeği önemlidir)</li>
</ul>
<hr>
<h3>p2-es — Esnek 2D fizik</h3>
<p>p2-es, p2.js'in bakımı sürdürülen çatallanmış sürümüdür. Karmaşık kısıtlamalara ve mekanizmalara ihtiyaç duyan oyunlar için harikadır.</p>
<p><strong>Kurulum:</strong></p>
<pre><code class="language-bash">npm install p2-es
</code></pre>
<p><strong>Temel kurulum:</strong></p>
<pre><code class="language-js">import * as p2 from 'p2-es'

const world = new p2.World({
  gravity: [0, -9.81]
})

// Zemin
const groundBody = new p2.Body({
  mass: 0, // statik
  position: [0, -1]
})
groundBody.addShape(new p2.Plane())
world.addBody(groundBody)

// Dinamik daire
const circleBody = new p2.Body({
  mass: 1,
  position: [0, 5]
})
circleBody.addShape(new p2.Circle({ radius: 0.5 }))
world.addBody(circleBody)
</code></pre>
<p><strong>Gelişmiş kısıtlamalar:</strong></p>
<pre><code class="language-js">// Dişli kısıtlaması
const gear = new p2.GearConstraint(bodyA, bodyB, {
  ratio: 2 // bodyB iki kat hızlı döner
})
world.addConstraint(gear)

// Prizmatik kısıtlama (kaydırıcı)
const prismatic = new p2.PrismaticConstraint(bodyA, bodyB, {
  localAnchorA: [0, 0],
  localAnchorB: [0, 0],
  localAxisA: [1, 0],
  disableRotationalLock: false
})
world.addConstraint(prismatic)

// Kilit kısıtlaması (kaynak)
const lock = new p2.LockConstraint(bodyA, bodyB)
world.addConstraint(lock)
</code></pre>
<p><strong>Temas malzemeleri:</strong></p>
<pre><code class="language-js">const ice = new p2.Material()
const rubber = new p2.Material()

const iceRubber = new p2.ContactMaterial(ice, rubber, {
  friction: 0.1,
  restitution: 0.9
})
world.addContactMaterial(iceRubber)

// Şekillere uygula
iceBody.shapes[0].material = ice
rubberBody.shapes[0].material = rubber
</code></pre>
<p><strong>Artıları:</strong></p>
<ul>
<li>Zengin kısıtlama sistemi</li>
<li>Temas malzemeleri</li>
<li>Mekanizmalar/makineler için uygun</li>
<li>Uyuyan cisimler (performans)</li>
<li>ES modülleri, tree-shaking desteği</li>
</ul>
<p><strong>Eksileri:</strong></p>
<ul>
<li>Yerleşik render aracı yok</li>
<li>Bazı şekil çiftleri desteklenmiyor</li>
<li>Alternatiflerden daha az aktif</li>
<li>Dokümantasyon eksiklikleri</li>
</ul>
<hr>
<h2>Performans ipuçları</h2>
<h3>1. Sabit zaman adımı kullanın</h3>
<pre><code class="language-js">const TIMESTEP = 1 / 60
let accumulator = 0

function gameLoop(deltaTime) {
  accumulator += deltaTime
  
  while (accumulator &gt;= TIMESTEP) {
    world.step(TIMESTEP)
    accumulator -= TIMESTEP
  }
  
  // Akıcı render için enterpolasyon yap
  const alpha = accumulator / TIMESTEP
  // lerp(previousState, currentState, alpha)
}
</code></pre>
<h3>2. Etkin olmayan cisimleri uyutun</h3>
<p>Çoğu motor uyku özelliğini destekler. Etkinleştirin:</p>
<pre><code class="language-js">// Cannon-es
world.allowSleep = true
body.allowSleep = true
body.sleepSpeedLimit = 0.1
body.sleepTimeLimit = 1

// Rapier
bodyDesc.setCanSleep(true)
</code></pre>
<h3>3. Basit çarpışma şekilleri kullanın</h3>
<pre><code class="language-js">// İYİ: Basit temel geometriler
const sphere = new CANNON.Sphere(1)
const box = new CANNON.Box(new CANNON.Vec3(1, 1, 1))

// KAÇININ: Dinamik cisimler için karmaşık üçgen ağ
const trimesh = new CANNON.Trimesh(vertices, indices) // Yavaş!
</code></pre>
<h3>4. Çözücü yinelemelerini azaltın (dikkatlice)</h3>
<pre><code class="language-js">// Cannon-es
world.solver.iterations = 5 // Varsayılan 10

// Rapier - dünya oluşturulurken yapılandırılır
</code></pre>
<h3>5. Fiziği bir Web Worker içinde çalıştırın</h3>
<pre><code class="language-js">// main.js
const worker = new Worker('physics-worker.js')

worker.postMessage({ type: 'step', deltaTime: 1/60 })
worker.onmessage = (e) =&gt; {
  const { positions, rotations } = e.data
  // Render nesnelerini güncelle
}

// physics-worker.js
importScripts('cannon-es.js')
const world = new CANNON.World()

onmessage = (e) =&gt; {
  if (e.data.type === 'step') {
    world.step(e.data.deltaTime)
    postMessage({
      positions: bodies.map(b =&gt; b.position.toArray()),
      rotations: bodies.map(b =&gt; b.quaternion.toArray())
    })
  }
}
</code></pre>
<hr>
<h2>Hangi durumda hangisini kullanmalı?</h2>
<table>
<thead>
<tr>
<th>Senaryo</th>
<th>Önerilen</th>
<th>Neden</th>
</tr>
</thead>
<tbody>
<tr>
<td>Üretime hazır 3D oyun</td>
<td><strong>Rapier</strong></td>
<td>En iyi performans, modern API, araçlar</td>
</tr>
<tr>
<td>3D oyun prototipi</td>
<td><strong>Cannon-es</strong></td>
<td>Basit, hata ayıklaması kolay</td>
</tr>
<tr>
<td>Yumuşak cisimler, kumaş, halatlar</td>
<td><strong>Ammo.js</strong> veya <strong>Jolt</strong></td>
<td>Her ikisi de kumaş/yumuşak cisimleri destekler; Jolt'un WASM portunun (JoltPhysics.js) bakımı daha aktif biçimde sürdürülür</td>
</tr>
<tr>
<td>Basit 3D tarayıcı oyunu</td>
<td><strong>Oimo.js</strong></td>
<td>Küçük, yeterli</td>
</tr>
<tr>
<td>2D game jam</td>
<td><strong>Matter.js</strong></td>
<td>Hızlı kurulum, yerleşik render</td>
</tr>
<tr>
<td>Hassas platform oyunu</td>
<td><strong>Planck.js</strong></td>
<td>Sabit zaman adımı determinizmi, iyi dokümantasyon</td>
</tr>
<tr>
<td>Karmaşık 2D mekanizmalar</td>
<td><strong>p2-es</strong></td>
<td>Zengin kısıtlama sistemi</td>
</tr>
<tr>
<td>Performans açısından kritik 2D</td>
<td><strong>Box2D WASM</strong></td>
<td>En hızlı 2D seçeneği, gerçek CCD</td>
</tr>
</tbody>
</table>
<hr>
<h2>Render araçlarıyla entegrasyon</h2>
<h3>Three.js + Rapier</h3>
<pre><code class="language-js">import * as THREE from 'three'
import RAPIER from '@dimforge/rapier3d'

await RAPIER.init()

const scene = new THREE.Scene()
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 })

const bodies = new Map() // fizik cismi -&gt; mesh

function createPhysicsBox(x, y, z) {
  // Fizik
  const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y, z)
  const body = world.createRigidBody(bodyDesc)
  const collider = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5)
  world.createCollider(collider, body)
  
  // Render
  const mesh = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshStandardMaterial({ color: 0x00ff00 })
  )
  scene.add(mesh)
  
  bodies.set(body, mesh)
  return body
}

function syncPhysics() {
  bodies.forEach((mesh, body) =&gt; {
    const pos = body.translation()
    const rot = body.rotation()
    mesh.position.set(pos.x, pos.y, pos.z)
    mesh.quaternion.set(rot.x, rot.y, rot.z, rot.w)
  })
}
</code></pre>
<h3>PixiJS + Matter.js</h3>
<pre><code class="language-js">import * as PIXI from 'pixi.js'
import Matter from 'matter-js'

const app = new PIXI.Application({ width: 800, height: 600 })
document.body.appendChild(app.view)

const engine = Matter.Engine.create()
const bodies = new Map()

function createPhysicsSprite(texture, x, y, width, height) {
  // Fizik
  const body = Matter.Bodies.rectangle(x, y, width, height)
  Matter.World.add(engine.world, body)
  
  // Render
  const sprite = new PIXI.Sprite(texture)
  sprite.anchor.set(0.5)
  app.stage.addChild(sprite)
  
  bodies.set(body, sprite)
  return body
}

app.ticker.add(() =&gt; {
  Matter.Engine.update(engine, 1000 / 60)
  
  bodies.forEach((sprite, body) =&gt; {
    sprite.position.set(body.position.x, body.position.y)
    sprite.rotation = body.angle
  })
})
</code></pre>
<hr>
<h2>Yaygın tuzaklar</h2>
<h3>Ölçek önemlidir</h3>
<p>Fizik motorları gerçek dünya ölçeğiyle (1 birim = 1 metre) en iyi şekilde çalışır. Piksel koordinatlarını doğrudan kullanmayın.</p>
<pre><code class="language-js">// KÖTÜ: Piksel konumlarını kullanmak
const body = Bodies.circle(400, 300, 50) // 50 piksel yarıçap mı?

// İYİ: Bir ölçek faktörü kullanın
const SCALE = 50 // metre başına 50 piksel
const body = Bodies.circle(8, 6, 1) // 1 metre yarıçap
// Ardından render sırasında SCALE ile çarpın
</code></pre>
<h3>WASM motorlarında bellek sızıntıları</h3>
<p>Cisimleri kaldırırken her zaman temizleyin:</p>
<pre><code class="language-js">// Rapier
world.removeRigidBody(body)

// Ammo.js
physicsWorld.removeRigidBody(body)
Ammo.destroy(body)
Ammo.destroy(shape)
Ammo.destroy(motionState)
</code></pre>
<h3>Tünelleme (nesnelerin birbirinin içinden geçmesi)</h3>
<p>Hızlı hareket eden nesneler ince duvarların içinden geçebilir. Çözümler:</p>
<pre><code class="language-js">// Rapier: CCD'yi etkinleştir
const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setCcdEnabled(true)

// Cannon-es: Daha küçük zaman adımları veya daha kalın duvarlar kullan
world.step(1/120) // 60 yerine 120 Hz
</code></pre>
<hr>
<h2>İlgili</h2>
<ul>
<li><a href="/tr/tutorials/webgl-fundamentals-for-games">Oyun geliştiricileri için WebGL temelleri</a></li>
<li><a href="/tr/tutorials/web-workers-game-logic">Oyun mantığı için Web Worker'lar</a> — fizik hesaplamalarını bir Worker iş parçacığına aktarın</li>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlayın</a></li>
<li><a href="/tr/tutorials/canvas-2d-game-loop">Canvas 2D oyun döngüsü</a> — her fizik motorunun kullandığı sabit zaman adımı kalıbı</li>
<li><a href="/tr/guides/web-games-stack-2026">2026'da Web Oyunları Teknoloji Yığını</a> — fizik motorlarının genel teknoloji yığınındaki yeri</li>
<li><a href="/tr/guides/web-game-engines-comparison">Web Oyun Motorları Karşılaştırması</a> — yerleşik fizik desteğine sahip motorlar</li>
</ul>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://rapier.rs/docs/">Rapier belgeleri</a> — resmi Rapier fizik belgeleri ve örnekleri</li>
<li><a href="https://pmndrs.github.io/cannon-es/docs/">Cannon-es belgeleri</a> — API referansı ve kılavuzlar</li>
<li><a href="https://brm.io/matter-js/docs/">Matter.js belgeleri</a> — eksiksiz API referansı</li>
<li><a href="https://piqnt.com/planck.js/">Planck.js belgeleri</a> — JavaScript için Box2D</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Web oyunları için Gamepad API]]></title>
            <link>https://app.cinevva.com/tr/tutorials/gamepad-api</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/gamepad-api</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Eksiksiz gamepad desteği: algılama, düğme eşleme, analog çubuklar, titreşim ve birden fazla kontrolcüyü yönetme. Web'de konsol kalitesinde giriş.]]></description>
            <content:encoded><![CDATA[<h1>Web oyunları için Gamepad API</h1>
<p>Gamepad API, web oyunlarına konsol kalitesinde kontrolcü desteği getirir. Xbox, PlayStation, Switch Pro ve genel amaçlı kontrolcüleri destekleyin.</p>
<h2>1) Gamepad'leri algılama</h2>
<pre><code class="language-js">window.addEventListener('gamepadconnected', (e) =&gt; {
  console.log('Gamepad bağlandı:', e.gamepad.id)
  console.log('Dizin:', e.gamepad.index)
  console.log('Düğmeler:', e.gamepad.buttons.length)
  console.log('Eksenler:', e.gamepad.axes.length)
})

window.addEventListener('gamepaddisconnected', (e) =&gt; {
  console.log('Gamepad bağlantısı kesildi:', e.gamepad.id)
})
</code></pre>
<h2>2) Gamepad durumunu yoklama</h2>
<p>Gamepad durumu yoklanmalıdır (olay tabanlı değildir):</p>
<pre><code class="language-js">function pollGamepads() {
  const gamepads = navigator.getGamepads()
  
  for (const gamepad of gamepads) {
    if (gamepad) {
      processGamepad(gamepad)
    }
  }
}

// Oyun döngünüzde çağırın
function gameLoop() {
  pollGamepads()
  update()
  render()
  requestAnimationFrame(gameLoop)
}
</code></pre>
<h2>3) Standart düğme eşleme</h2>
<p>&quot;Standart&quot; eşleme (çoğu Xbox/PlayStation kontrolcüsü):</p>
<pre><code class="language-js">const BUTTONS = {
  A: 0,           // Çarpı (PS) / A (Xbox)
  B: 1,           // Daire (PS) / B (Xbox)
  X: 2,           // Kare (PS) / X (Xbox)
  Y: 3,           // Üçgen (PS) / Y (Xbox)
  LB: 4,          // L1 (PS) / LB (Xbox)
  RB: 5,          // R1 (PS) / RB (Xbox)
  LT: 6,          // L2 (PS) / LT (Xbox)
  RT: 7,          // R2 (PS) / RT (Xbox)
  SELECT: 8,      // Share (PS) / Back (Xbox)
  START: 9,       // Options (PS) / Start (Xbox)
  L3: 10,         // Sol çubuğa basma
  R3: 11,         // Sağ çubuğa basma
  DPAD_UP: 12,
  DPAD_DOWN: 13,
  DPAD_LEFT: 14,
  DPAD_RIGHT: 15,
  HOME: 16,       // PS düğmesi / Xbox düğmesi
}

const AXES = {
  LEFT_X: 0,
  LEFT_Y: 1,
  RIGHT_X: 2,
  RIGHT_Y: 3,
}
</code></pre>
<h2>4) Düğmeleri ve çubukları okuma</h2>
<pre><code class="language-js">function processGamepad(gamepad) {
  // Düğmeler (dijital veya analog)
  const jump = gamepad.buttons[BUTTONS.A].pressed
  const shoot = gamepad.buttons[BUTTONS.X].pressed
  
  // Tetikler (analog 0-1)
  const leftTrigger = gamepad.buttons[BUTTONS.LT].value
  const rightTrigger = gamepad.buttons[BUTTONS.RT].value
  
  // Çubuklar (analog -1 ile 1 arası)
  const leftX = gamepad.axes[AXES.LEFT_X]
  const leftY = gamepad.axes[AXES.LEFT_Y]
  const rightX = gamepad.axes[AXES.RIGHT_X]
  const rightY = gamepad.axes[AXES.RIGHT_Y]
  
  return { jump, shoot, leftTrigger, rightTrigger, leftX, leftY, rightX, rightY }
}
</code></pre>
<h2>5) Ölü bölge yönetimi</h2>
<p>Çubuklar boşta nadiren tam olarak 0 değerinde kalır:</p>
<pre><code class="language-js">function applyDeadzone(value, deadzone = 0.15) {
  if (Math.abs(value) &lt; deadzone) return 0
  
  // Kalan aralığı 0-1 ölçeğine yeniden ayarla
  const sign = Math.sign(value)
  const adjusted = (Math.abs(value) - deadzone) / (1 - deadzone)
  return sign * adjusted
}

function getStickInput(gamepad) {
  return {
    leftX: applyDeadzone(gamepad.axes[AXES.LEFT_X]),
    leftY: applyDeadzone(gamepad.axes[AXES.LEFT_Y]),
    rightX: applyDeadzone(gamepad.axes[AXES.RIGHT_X]),
    rightY: applyDeadzone(gamepad.axes[AXES.RIGHT_Y]),
  }
}
</code></pre>
<h2>6) Radyal ölü bölge (2D hareket için daha iyi)</h2>
<pre><code class="language-js">function applyRadialDeadzone(x, y, deadzone = 0.15) {
  const magnitude = Math.sqrt(x * x + y * y)
  
  if (magnitude &lt; deadzone) {
    return { x: 0, y: 0 }
  }
  
  const normalized = {
    x: x / magnitude,
    y: y / magnitude,
  }
  
  const adjusted = (magnitude - deadzone) / (1 - deadzone)
  
  return {
    x: normalized.x * adjusted,
    y: normalized.y * adjusted,
  }
}

// Kullanım
const stick = applyRadialDeadzone(
  gamepad.axes[AXES.LEFT_X],
  gamepad.axes[AXES.LEFT_Y]
)
player.vx = stick.x * player.speed
player.vy = stick.y * player.speed
</code></pre>
<h2>7) Eksiksiz gamepad yöneticisi</h2>
<pre><code class="language-js">class GamepadManager {
  constructor() {
    this.gamepads = new Map()
    this.prevState = new Map()
    
    window.addEventListener('gamepadconnected', (e) =&gt; {
      this.gamepads.set(e.gamepad.index, e.gamepad)
      console.log('Bağlandı:', e.gamepad.id)
    })
    
    window.addEventListener('gamepaddisconnected', (e) =&gt; {
      this.gamepads.delete(e.gamepad.index)
      this.prevState.delete(e.gamepad.index)
    })
  }
  
  poll() {
    const gamepads = navigator.getGamepads()
    for (const gp of gamepads) {
      if (gp) this.gamepads.set(gp.index, gp)
    }
  }
  
  getState(index = 0) {
    const gp = this.gamepads.get(index)
    if (!gp) return null
    
    const stick = applyRadialDeadzone(gp.axes[0], gp.axes[1])
    const rightStick = applyRadialDeadzone(gp.axes[2], gp.axes[3])
    
    return {
      connected: true,
      leftStick: stick,
      rightStick: rightStick,
      
      // Ön yüz düğmeleri
      a: gp.buttons[0].pressed,
      b: gp.buttons[1].pressed,
      x: gp.buttons[2].pressed,
      y: gp.buttons[3].pressed,
      
      // Omuz düğmeleri
      lb: gp.buttons[4].pressed,
      rb: gp.buttons[5].pressed,
      
      // Tetikler (analog)
      lt: gp.buttons[6].value,
      rt: gp.buttons[7].value,
      
      // Yön düğmeleri
      dpadUp: gp.buttons[12].pressed,
      dpadDown: gp.buttons[13].pressed,
      dpadLeft: gp.buttons[14].pressed,
      dpadRight: gp.buttons[15].pressed,
      
      // Meta
      select: gp.buttons[8].pressed,
      start: gp.buttons[9].pressed,
    }
  }
  
  isPressed(index, button) {
    const gp = this.gamepads.get(index)
    return gp?.buttons[button]?.pressed || false
  }
  
  wasJustPressed(index, button) {
    const current = this.isPressed(index, button)
    const prev = this.prevState.get(index)?.[button] || false
    return current &amp;&amp; !prev
  }
  
  endFrame() {
    for (const [index, gp] of this.gamepads) {
      const state = {}
      for (let i = 0; i &lt; gp.buttons.length; i++) {
        state[i] = gp.buttons[i].pressed
      }
      this.prevState.set(index, state)
    }
  }
}

// Kullanım
const gamepadManager = new GamepadManager()

function update() {
  gamepadManager.poll()
  
  const state = gamepadManager.getState(0)
  if (state) {
    player.vx = state.leftStick.x * player.speed
    player.vy = state.leftStick.y * player.speed
    
    if (gamepadManager.wasJustPressed(0, BUTTONS.A)) {
      player.jump()
    }
  }
  
  gamepadManager.endFrame()
}
</code></pre>
<h2>8) Titreşim / dokunsal geri bildirim</h2>
<pre><code class="language-js">function vibrate(gamepad, duration = 200, weakMagnitude = 0.5, strongMagnitude = 0.5) {
  if (gamepad.vibrationActuator) {
    gamepad.vibrationActuator.playEffect('dual-rumble', {
      startDelay: 0,
      duration,
      weakMagnitude,   // Yüksek frekanslı motor
      strongMagnitude, // Düşük frekanslı motor
    })
  }
}

// Kullanım
function onPlayerHit() {
  const gp = navigator.getGamepads()[0]
  if (gp) vibrate(gp, 150, 0.3, 0.6)
}

function onExplosion() {
  const gp = navigator.getGamepads()[0]
  if (gp) vibrate(gp, 300, 0.8, 1.0)
}
</code></pre>
<h2>9) Birden fazla oyuncu</h2>
<pre><code class="language-js">class LocalMultiplayer {
  constructor(maxPlayers = 4) {
    this.maxPlayers = maxPlayers
    this.players = []
  }
  
  getActivePlayers() {
    const gamepads = navigator.getGamepads()
    const active = []
    
    for (let i = 0; i &lt; this.maxPlayers; i++) {
      if (gamepads[i]) {
        active.push({
          index: i,
          gamepad: gamepads[i],
          state: this.getState(gamepads[i])
        })
      }
    }
    
    return active
  }
  
  getState(gp) {
    // ... öncekiyle aynı
  }
}

// Oyun döngüsünde
const multiplayer = new LocalMultiplayer(4)

function update() {
  const players = multiplayer.getActivePlayers()
  
  for (const { index, state } of players) {
    updatePlayer(index, state)
  }
}
</code></pre>
<h2>10) Yedek ve birleşik giriş</h2>
<p>Hem klavyeyi hem de gamepad'i destekleyin:</p>
<pre><code class="language-js">class InputManager {
  constructor() {
    this.keyboard = new KeyboardInput()
    this.gamepad = new GamepadManager()
  }
  
  poll() {
    this.gamepad.poll()
  }
  
  getInput(playerIndex = 0) {
    const gpState = this.gamepad.getState(playerIndex)
    const kbState = this.keyboard.getState()
    
    // Girişleri birleştir (analog girişlerde gamepad önceliklidir)
    return {
      moveX: gpState?.leftStick.x || (kbState.left ? -1 : kbState.right ? 1 : 0),
      moveY: gpState?.leftStick.y || (kbState.up ? -1 : kbState.down ? 1 : 0),
      jump: gpState?.a || kbState.space,
      shoot: gpState?.x || kbState.z,
      pause: gpState?.start || kbState.escape,
    }
  }
  
  endFrame() {
    this.gamepad.endFrame()
    this.keyboard.endFrame()
  }
}
</code></pre>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/game-input-handling">Oyun girişi yönetimi</a></li>
<li><a href="/tr/tutorials/mobile-friendly-web-games">Mobil uyumlu web oyunları</a></li>
<li><a href="/tr/tutorials/canvas-2d-game-loop">Canvas 2D oyun döngüsü</a></li>
<li><a href="/tr/tutorials/pointer-lock-fps">FPS oyunları için Pointer Lock</a> — gamepad'i fareyle bakış kontrolüyle birleştirme</li>
<li><a href="/tr/guides/co-op-game-design">Eşli Oyun Tasarımı</a> — yerel eşli oyunda birden fazla kontrolcü girişine göre tasarım yapma</li>
</ul>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API">MDN: Gamepad API</a> — eksiksiz API referansı</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API">MDN: Gamepad API'yi Kullanma</a> — adım adım rehber</li>
<li><a href="https://gamepad-tester.com/">Gamepad Test Aracı</a> — kontrolcü eşlemelerinizi tarayıcıda test edin</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Oyunları iframe ile yerleştirmek için en iyi uygulamalar]]></title>
            <link>https://app.cinevva.com/tr/tutorials/iframe-embedding-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/iframe-embedding-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Web oyunlarını iframe'lere doğru şekilde yerleştirin: güvenlik, iletişim, tam ekran ve platform gereksinimleri. Oyununuzu her yerde yerleştirilebilir hâle getirin.]]></description>
            <content:encoded><![CDATA[<h1>Oyunları iframe ile yerleştirmek için en iyi uygulamalar</h1>
<p>Birçok platform (<a href="http://Itch.io">Itch.io</a>, Newgrounds, Cinevva ve kendi siteniz) oyunları iframe'ler içinde yerleştirir. Bu eğitim, oyununuzun iframe içinde sorunsuz çalışmasını nasıl sağlayacağınızı ele alır.</p>
<h2>1) Temel yerleştirilebilir yapı</h2>
<p>Oyununuz, sayfanın tamamını kontrol etmeyi gerektirmeden çalışmalıdır:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
  &lt;style&gt;
    * { margin: 0; padding: 0; }
    html, body { width: 100%; height: 100%; overflow: hidden; }
    canvas { display: block; width: 100%; height: 100%; }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;canvas id=&quot;game&quot;&gt;&lt;/canvas&gt;
  &lt;script src=&quot;game.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h2>2) Iframe bağlamını algılama</h2>
<pre><code class="language-js">const isEmbedded = window.self !== window.top

if (isEmbedded) {
  // Davranışı yerleştirilmiş bağlama göre ayarla
  hideExternalLinks()
  adjustUIForSmallSize()
}

function hideExternalLinks() {
  document.querySelectorAll('a[target=&quot;_blank&quot;]').forEach(link =&gt; {
    link.style.display = 'none'
  })
}
</code></pre>
<h2>3) Gerekli iframe izinlerini sağlama</h2>
<p>Üst sayfa, iframe'inizin neler yapabileceğini kontrol eder. Yaygın izinler:</p>
<pre><code class="language-html">&lt;iframe 
  src=&quot;https://game.example.com&quot;
  allow=&quot;fullscreen; autoplay; gamepad; pointer-lock&quot;
  sandbox=&quot;allow-scripts allow-same-origin allow-pointer-lock allow-popups&quot;
&gt;&lt;/iframe&gt;
</code></pre>
<p>Oyununuz, eksik izinleri sorunsuz biçimde ele almalıdır:</p>
<pre><code class="language-js">// Tam ekran özelliğinin kullanılabilir olup olmadığını kontrol et
const canFullscreen = document.fullscreenEnabled || document.webkitFullscreenEnabled

if (!canFullscreen) {
  document.getElementById('fullscreen-btn').style.display = 'none'
}
</code></pre>
<h2>4) Iframe içinden tam ekrana geçme</h2>
<p>Tam ekran için <code>allow=&quot;fullscreen&quot;</code> özniteliği gerekir:</p>
<pre><code class="language-js">async function requestFullscreen() {
  const elem = document.documentElement
  
  try {
    if (elem.requestFullscreen) {
      await elem.requestFullscreen()
    } else if (elem.webkitRequestFullscreen) {
      await elem.webkitRequestFullscreen()
    }
  } catch (err) {
    // Tam ekrana izin verilmiyor; mesaj göster
    showMessage('Yerleştirilmiş durumdayken tam ekran kullanılamıyor')
  }
}
</code></pre>
<h2>5) Üst sayfayla iletişim kurma</h2>
<p>Farklı kaynaklar arasında güvenli iletişim için <code>postMessage</code> kullanın:</p>
<p><strong>Oyununuzda:</strong></p>
<pre><code class="language-js">// Üst sayfaya mesaj gönder
function notifyParent(type, data) {
  if (window.parent !== window) {
    window.parent.postMessage({ type, data, source: 'game' }, '*')
  }
}

// Üst sayfadan gelen mesajları al
window.addEventListener('message', (event) =&gt; {
  // Gerekirse kaynağı doğrula
  // if (event.origin !== 'https://trusted-host.com') return
  
  const { type, data } = event.data
  
  switch (type) {
    case 'pause':
      pauseGame()
      break
    case 'resume':
      resumeGame()
      break
    case 'setVolume':
      setVolume(data.volume)
      break
  }
})

// Oyun hazır olduğunda bildir
window.addEventListener('load', () =&gt; {
  notifyParent('ready', { width: 800, height: 600 })
})

// Oyun olaylarını bildir
function onGameOver(score) {
  notifyParent('gameover', { score })
}
</code></pre>
<p><strong>Üst sayfada:</strong></p>
<pre><code class="language-js">const iframe = document.getElementById('game-iframe')

iframe.addEventListener('load', () =&gt; {
  // Oyundan gelen mesajları dinle
  window.addEventListener('message', (event) =&gt; {
    if (event.source !== iframe.contentWindow) return
    if (event.data.source !== 'game') return
    
    const { type, data } = event.data
    
    if (type === 'ready') {
      console.log('Oyun hazır:', data)
    }
    if (type === 'gameover') {
      showScoreModal(data.score)
    }
  })
})

// Oyuna komut gönder
function pauseGame() {
  iframe.contentWindow.postMessage({ type: 'pause' }, '*')
}
</code></pre>
<h2>6) Odağı yönetme</h2>
<p>Iframe'ler odağı kaybederek klavye girişinin çalışmamasına neden olabilir:</p>
<pre><code class="language-js">// Tıklandığında canvas'a otomatik olarak odaklan
canvas.addEventListener('click', () =&gt; {
  canvas.focus()
})

// Canvas'ı odaklanabilir hâle getir
canvas.tabIndex = 1

// Odak kaybını işle
window.addEventListener('blur', () =&gt; {
  // Basılı tutulan tuşları sıfırla
  input.left = input.right = input.up = input.down = false
  
  if (isEmbedded) {
    // İsteğe bağlı olarak duraklat
    // pauseGame()
  }
})

// Üst sayfadan odak iste
function requestFocus() {
  notifyParent('requestFocus', {})
}
</code></pre>
<h2>7) Duyarlı yerleştirme boyutu</h2>
<p>Farklı yerleştirme boyutlarını destekleyin:</p>
<pre><code class="language-js">function handleResize() {
  const width = window.innerWidth
  const height = window.innerHeight
  
  // Kullanıcı arayüzünü boyuta göre ayarla
  if (width &lt; 400 || height &lt; 300) {
    enableCompactUI()
  } else {
    enableFullUI()
  }
  
  // Oyunu uygun şekilde ölçeklendir
  resizeCanvas(width, height)
}

window.addEventListener('resize', handleResize)
handleResize()
</code></pre>
<h2>8) Yerleştirmeler için yükleme göstergesi</h2>
<p>Hemen bir şeyler gösterin:</p>
<pre><code class="language-js">// Anında görüntülemek için HTML içine yerleştir
const loadingHTML = `
  &lt;div id=&quot;loading&quot; style=&quot;
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #1a1a2e;
    color: #fff;
    font-family: sans-serif;
  &quot;&gt;
    &lt;div&gt;
      &lt;div class=&quot;spinner&quot;&gt;&lt;/div&gt;
      &lt;p&gt;Yükleniyor...&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
`

// Oyun hazır olduğunda kaldır
function hideLoading() {
  const loading = document.getElementById('loading')
  if (loading) {
    loading.style.opacity = '0'
    loading.style.transition = 'opacity 0.3s'
    setTimeout(() =&gt; loading.remove(), 300)
  }
}
</code></pre>
<h2>9) Platforma özgü SDK'lar</h2>
<p>Bazı platformların kendi API'leri vardır:</p>
<p><strong><a href="http://Itch.io">Itch.io</a>:</strong></p>
<pre><code class="language-js">// SDK gerekmez ancak başarımlar için postMessage kullanılabilir
window.parent.postMessage({ type: 'itch-achievement', data: { id: 'first-win' }}, '*')
</code></pre>
<p><strong>Newgrounds:</strong></p>
<pre><code class="language-js">// Newgrounds.io SDK'sını ekle
const ngio = new Newgrounds.io.core('APP_ID', 'AES_KEY')
ngio.callComponent('Medal.unlock', { id: 12345 })
</code></pre>
<p><strong>Cinevva:</strong></p>
<pre><code class="language-js">// Oyunun hazır olduğunu bildir
window.parent.postMessage({ type: 'cinevva:ready' }, '*')

// Oynanabilir duruma geldiğini bildir
window.parent.postMessage({ type: 'cinevva:playable' }, '*')
</code></pre>
<h2>10) Güvenlik hususları</h2>
<pre><code class="language-js">// Hassas işlemlerde mesaj kaynaklarını doğrula
window.addEventListener('message', (event) =&gt; {
  const trustedOrigins = [
    'https://itch.io',
    'https://cinevva.com',
    'https://yoursite.com'
  ]
  
  if (!trustedOrigins.includes(event.origin)) {
    return // Güvenilmeyen mesajları yok say
  }
  
  // Mesajı işle...
})

// Hassas işlemleri postMessage üzerinden kullanıma açma
// Yalnızca izin verilenler listesindeki komutları kabul et
const allowedCommands = ['pause', 'resume', 'setVolume', 'mute']

window.addEventListener('message', (event) =&gt; {
  const { type } = event.data
  if (!allowedCommands.includes(type)) return
  
  // Komutu işle...
})
</code></pre>
<h2>Test kontrol listesi</h2>
<ul>
<li><strong>Yerel test</strong>: <code>file://</code> yerine basit bir HTTP sunucusu kullanın</li>
<li><strong>Farklı kaynaklar</strong>: Gerçek bir iframe yerleştirmesiyle test edin</li>
<li><strong>İzinler</strong>: Kısıtlı bir sandbox ile test edin</li>
<li><strong>Odak</strong>: Iframe'in dışına tıkladıktan sonra klavyeyi test edin</li>
<li><strong>Yeniden boyutlandırma</strong>: Farklı yerleştirme boyutlarında test edin</li>
<li><strong>Mobil</strong>: Yerleştirilmiş bağlamda dokunmatik kontrolleri test edin</li>
</ul>
<pre><code class="language-html">&lt;!-- Yerleştirme test sayfası --&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;body style=&quot;background: #333; padding: 20px;&quot;&gt;
  &lt;h1 style=&quot;color: #fff;&quot;&gt;Yerleştirme Testi&lt;/h1&gt;
  &lt;iframe 
    src=&quot;http://localhost:8000&quot; 
    width=&quot;800&quot; 
    height=&quot;600&quot;
    allow=&quot;fullscreen; autoplay; gamepad&quot;
  &gt;&lt;/iframe&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlayın</a></li>
<li><a href="/tr/tutorials/mobile-friendly-web-games">Mobil uyumlu web oyunları</a></li>
<li><a href="/tr/creators">İçerik üreticileri için</a></li>
<li><a href="/tr/tutorials/coop-coep-sharedarraybuffer">COOP/COEP ve SharedArrayBuffer</a> — iframe yerleştirmesini etkileyen farklı kaynak üstbilgileri</li>
<li><a href="/tr/guides/itch-io-launch-guide">Oyununuzu itch.io'da nasıl yayımlarsınız?</a> — <a href="http://itch.io">itch.io</a>, tarayıcı oyunlarını yerel olarak iframe'lere yerleştirir</li>
</ul>
<h2>Harici kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe">MDN: iframe öğesi</a> — iframe öznitelikleri için eksiksiz başvuru</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy">MDN: İzin Politikası</a> — iframe'lerin hangi özellikleri kullanabileceğini kontrol etme</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage">MDN: postMessage API'si</a> — iframe ile üst sayfa arasında iletişim kurma</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[IndexedDB ile oyun durumunu kaydetme ve yükleme]]></title>
            <link>https://app.cinevva.com/tr/tutorials/indexeddb-game-saves</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/indexeddb-game-saves</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[IndexedDB ile kalıcı oyun kayıtları: ilerlemeyi, ayarları ve büyük verileri saklama. Oyununuzu engellemeyen asenkron kalıplar.]]></description>
            <content:encoded><![CDATA[<h1>IndexedDB ile oyun durumunu kaydetme ve yükleme</h1>
<p>IndexedDB, tarayıcıda kalıcı oyun verileri için en iyi seçenektir. Büyük miktarda veriyi işler, çevrimdışı çalışır ve ana iş parçacığını engellemez.</p>
<h2>1) Neden localStorage yerine IndexedDB?</h2>
<table>
<thead>
<tr>
<th>Özellik</th>
<th>localStorage</th>
<th>IndexedDB</th>
</tr>
</thead>
<tbody>
<tr>
<td>Depolama sınırı</td>
<td>~5-10 MB</td>
<td>50+ MB (çoğu zaman GB)</td>
</tr>
<tr>
<td>Veri türleri</td>
<td>Yalnızca dizeler</td>
<td>Nesneler, blob'lar, diziler</td>
</tr>
<tr>
<td>Asenkron</td>
<td>Hayır (engeller)</td>
<td>Evet</td>
</tr>
<tr>
<td>İndeksli sorgular</td>
<td>Hayır</td>
<td>Evet</td>
</tr>
</tbody>
</table>
<p>Oyunlar için IndexedDB neredeyse her zaman doğru seçimdir.</p>
<h2>2) Veritabanı açma</h2>
<pre><code class="language-js">function openGameDB() {
  return new Promise((resolve, reject) =&gt; {
    const request = indexedDB.open('MyGame', 1)
    
    request.onerror = () =&gt; reject(request.error)
    request.onsuccess = () =&gt; resolve(request.result)
    
    request.onupgradeneeded = (event) =&gt; {
      const db = event.target.result
      
      // Create stores
      if (!db.objectStoreNames.contains('saves')) {
        db.createObjectStore('saves', { keyPath: 'slot' })
      }
      if (!db.objectStoreNames.contains('settings')) {
        db.createObjectStore('settings', { keyPath: 'key' })
      }
    }
  })
}
</code></pre>
<h2>3) Oyun durumunu kaydetme</h2>
<pre><code class="language-js">async function saveGame(slot, gameState) {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) =&gt; {
    const tx = db.transaction('saves', 'readwrite')
    const store = tx.objectStore('saves')
    
    const saveData = {
      slot,
      state: gameState,
      timestamp: Date.now(),
    }
    
    const request = store.put(saveData)
    request.onsuccess = () =&gt; resolve()
    request.onerror = () =&gt; reject(request.error)
  })
}
</code></pre>
<h2>4) Oyun durumunu yükleme</h2>
<pre><code class="language-js">async function loadGame(slot) {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) =&gt; {
    const tx = db.transaction('saves', 'readonly')
    const store = tx.objectStore('saves')
    
    const request = store.get(slot)
    request.onsuccess = () =&gt; resolve(request.result?.state || null)
    request.onerror = () =&gt; reject(request.error)
  })
}
</code></pre>
<h2>5) Tüm kayıtları listeleme</h2>
<pre><code class="language-js">async function listSaves() {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) =&gt; {
    const tx = db.transaction('saves', 'readonly')
    const store = tx.objectStore('saves')
    
    const request = store.getAll()
    request.onsuccess = () =&gt; resolve(request.result)
    request.onerror = () =&gt; reject(request.error)
  })
}
</code></pre>
<h2>6) Bir kaydı silme</h2>
<pre><code class="language-js">async function deleteSave(slot) {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) =&gt; {
    const tx = db.transaction('saves', 'readwrite')
    const store = tx.objectStore('saves')
    
    const request = store.delete(slot)
    request.onsuccess = () =&gt; resolve()
    request.onerror = () =&gt; reject(request.error)
  })
}
</code></pre>
<h2>7) Eksiksiz bir GameStorage sınıfı</h2>
<pre><code class="language-js">class GameStorage {
  constructor(dbName = 'GameData', version = 1) {
    this.dbName = dbName
    this.version = version
    this.db = null
  }
  
  async init() {
    this.db = await this.openDB()
  }
  
  openDB() {
    return new Promise((resolve, reject) =&gt; {
      const request = indexedDB.open(this.dbName, this.version)
      request.onerror = () =&gt; reject(request.error)
      request.onsuccess = () =&gt; resolve(request.result)
      request.onupgradeneeded = (e) =&gt; {
        const db = e.target.result
        if (!db.objectStoreNames.contains('saves')) {
          db.createObjectStore('saves', { keyPath: 'slot' })
        }
        if (!db.objectStoreNames.contains('settings')) {
          db.createObjectStore('settings', { keyPath: 'key' })
        }
        if (!db.objectStoreNames.contains('assets')) {
          db.createObjectStore('assets', { keyPath: 'url' })
        }
      }
    })
  }
  
  async save(slot, data) {
    const tx = this.db.transaction('saves', 'readwrite')
    tx.objectStore('saves').put({ slot, data, timestamp: Date.now() })
    return tx.complete
  }
  
  async load(slot) {
    const tx = this.db.transaction('saves', 'readonly')
    const result = await this.promisify(tx.objectStore('saves').get(slot))
    return result?.data || null
  }
  
  async setSetting(key, value) {
    const tx = this.db.transaction('settings', 'readwrite')
    tx.objectStore('settings').put({ key, value })
  }
  
  async getSetting(key, defaultValue = null) {
    const tx = this.db.transaction('settings', 'readonly')
    const result = await this.promisify(tx.objectStore('settings').get(key))
    return result?.value ?? defaultValue
  }
  
  promisify(request) {
    return new Promise((resolve, reject) =&gt; {
      request.onsuccess = () =&gt; resolve(request.result)
      request.onerror = () =&gt; reject(request.error)
    })
  }
}
</code></pre>
<h2>8) İkili verileri (dokular, sesler) depolama</h2>
<p>IndexedDB, Blob'ları ve ArrayBuffer'ları destekler:</p>
<pre><code class="language-js">async function cacheAsset(url, blob) {
  const tx = db.transaction('assets', 'readwrite')
  tx.objectStore('assets').put({ url, blob, cached: Date.now() })
}

async function getCachedAsset(url) {
  const tx = db.transaction('assets', 'readonly')
  const result = await promisify(tx.objectStore('assets').get(url))
  return result?.blob || null
}
</code></pre>
<h2>9) Otomatik kayıt kalıbı</h2>
<pre><code class="language-js">class AutoSave {
  constructor(storage, interval = 60000) {
    this.storage = storage
    this.interval = interval
    this.timer = null
    this.dirty = false
  }
  
  markDirty() {
    this.dirty = true
  }
  
  start(getState) {
    this.timer = setInterval(async () =&gt; {
      if (this.dirty) {
        await this.storage.save('autosave', getState())
        this.dirty = false
        console.log('Autosaved')
      }
    }, this.interval)
  }
  
  stop() {
    clearInterval(this.timer)
  }
}
</code></pre>
<h2>10) Hata yönetimi ve geri dönüş seçenekleri</h2>
<pre><code class="language-js">async function safeLoad(slot, defaultState) {
  try {
    const saved = await loadGame(slot)
    if (saved) {
      // Validate/migrate old saves if needed
      return migrateSave(saved)
    }
  } catch (err) {
    console.warn('Failed to load save:', err)
  }
  return defaultState
}

function migrateSave(save) {
  // Handle old save formats
  if (!save.version) {
    save.version = 1
    save.settings = save.settings || {}
  }
  return save
}
</code></pre>
<h2>11) Kayıtları otomatik silinmeye karşı koruma</h2>
<p>IndexedDB verilerinin kalıcı olacağı garanti edilmez. Bir kaynak varsayılan olarak &quot;mümkün olan en iyi&quot; depolamayı kullanır ve disk dolduğunda ya da Safari/WebKit'te kullanıcı sitenizle uzun süre etkileşim kurmadığında tarayıcı bu verileri silebilir. Oyun kayıtları ise tam olarak kaybetmek istemeyeceğiniz verilerdir.</p>
<p>Tarayıcının açık bir kullanıcı işlemi olmadan verilerinizi temizlememesi için kalıcı depolama isteyin:</p>
<pre><code class="language-js">async function makeStoragePersistent() {
  if (navigator.storage &amp;&amp; navigator.storage.persist) {
    const persisted = await navigator.storage.persist()
    console.log(persisted ? 'Saves are protected from eviction' : 'Saves may be evicted under storage pressure')
    return persisted
  }
  return false
}
</code></pre>
<p>Tarayıcılar bu izni etkileşim sinyallerine (kullanıcının sitenizle ne kadar etkileşim kurduğu, sitenin PWA olarak yüklenip yüklenmediği) göre verir; bu nedenle isteğin her zaman başarılı olacağını varsaymayın. Büyük kayıtları veya önbelleğe alınmış varlıkları yazmadan önce ne kadar alanınız kaldığını da kontrol edebilirsiniz:</p>
<pre><code class="language-js">async function checkStorage() {
  if (navigator.storage &amp;&amp; navigator.storage.estimate) {
    const { usage, quota } = await navigator.storage.estimate()
    console.log(`Using ${usage} of ${quota} bytes`)
  }
}
</code></pre>
<p>Her iki API de güvenli bir bağlam (HTTPS veya localhost) gerektirir.</p>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/pwa-offline-games">Çevrimdışı oyunlar için PWA</a></li>
<li><a href="/tr/tutorials/service-workers-caching">Oyun önbelleğe alma için service worker'lar</a></li>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlama</a></li>
<li><a href="/tr/tutorials/streaming-asset-loading">Akış tabanlı varlık yükleme</a> — IndexedDB'yi varlık önbelleği olarak kullanma</li>
<li><a href="/tr/tutorials/analytics-web-games">Web oyunları için analiz</a> — oyuncu davranışını anlamak için kaydetme/yükleme kalıplarını izleme</li>
</ul>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">MDN: IndexedDB API</a> — eksiksiz API başvurusu</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB">MDN: IndexedDB'yi kullanma</a> — adım adım kılavuz</li>
<li><a href="https://github.com/jakearchibald/idb">idb kütüphanesi</a> — Promise tabanlı, küçük bir IndexedDB sarmalayıcısı</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Mobil uyumlu web oyunları]]></title>
            <link>https://app.cinevva.com/tr/tutorials/mobile-friendly-web-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/mobile-friendly-web-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Web oyunlarının mobil cihazlarda çalışmasını sağlayın: dokunmatik kontroller, performans, görüntü alanı, pil ve duyarlı tasarım. En büyük oyun platformuna ulaşın.]]></description>
            <content:encoded><![CDATA[<h1>Mobil uyumlu web oyunları</h1>
<p>Mobil cihazlar en büyük oyun platformudur. Birkaç düzenlemeyle web oyununuzun telefonlarda ve tabletlerde harika çalışmasını sağlayabilirsiniz.</p>
<h2>1) Görüntü alanı ayarları</h2>
<p>Yakınlaştırmayı önleyin ve doğru ölçeklendirmeyi sağlayın:</p>
<pre><code class="language-html">&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no&quot;&gt;
</code></pre>
<h2>2) Tam ekran canvas</h2>
<pre><code class="language-css">* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #000;
  touch-action: none; /* Prevent browser gestures */
}

#game {
  display: block;
  width: 100%;
  height: 100%;
}
</code></pre>
<p>Arayüzü <code>height: 100%</code> yerine görüntü alanı yüksekliğine göre boyutlandırıyorsanız <code>vh</code> yerine dinamik görüntü alanı birimleri olan <code>dvh</code>, <code>svh</code> ve <code>lvh</code> birimlerini tercih edin. Bu birimler, mobil tarayıcının daralıp genişleyen araç çubuğunu hesaba katar; <code>100vh</code> ise bunu göz ardı eder (iOS'te <code>100vh</code>, araç çubuğunun gizli olduğu yüksekliği temsil ettiğinden düzeninizin alt kısmı kırpılır). Bu birimler Baseline Widely Available durumuna ulaşmıştır ve Chrome/Edge 108+, Firefox 101+ ile Safari/iOS Safari 15.4+ tarafından desteklenmektedir; bu da kullanıcıların yaklaşık %92'sine karşılık gelir.</p>
<h2>3) Dokunma olaylarını işleme</h2>
<pre><code class="language-js">const canvas = document.getElementById('game')

// Prevent default touch behaviors
canvas.addEventListener('touchstart', (e) =&gt; e.preventDefault(), { passive: false })
canvas.addEventListener('touchmove', (e) =&gt; e.preventDefault(), { passive: false })

// Track touches
const touches = new Map()

canvas.addEventListener('touchstart', (e) =&gt; {
  for (const touch of e.changedTouches) {
    touches.set(touch.identifier, {
      x: touch.clientX,
      y: touch.clientY,
      startX: touch.clientX,
      startY: touch.clientY,
    })
  }
})

canvas.addEventListener('touchmove', (e) =&gt; {
  for (const touch of e.changedTouches) {
    const t = touches.get(touch.identifier)
    if (t) {
      t.x = touch.clientX
      t.y = touch.clientY
    }
  }
})

canvas.addEventListener('touchend', (e) =&gt; {
  for (const touch of e.changedTouches) {
    touches.delete(touch.identifier)
  }
})
</code></pre>
<p>Oyununuzun fare veya ekran kalemiyle de çalışması gerekiyorsa dokunma olayları yerine Pointer Events (<code>pointerdown</code>/<code>pointermove</code>/<code>pointerup</code>) kullanmayı değerlendirin. MDN artık bunları cihazdan bağımsız tek giriş modeli olarak öneriyor: tek bir kod yolu dokunma, fare ve kalem girişlerini işlerken çoklu dokunma da <code>pointerId</code> üzerinden çalışmaya devam eder (yukarıda kullanılan <code>touch.identifier</code> karşılığıdır). Yalnızca dokunmatik giriş kullanan oyunlar için dokunma olayları hâlâ tamamen uygundur.</p>
<h2>4) Sanal joystick</h2>
<pre><code class="language-js">class VirtualJoystick {
  constructor(x, y, radius) {
    this.baseX = x
    this.baseY = y
    this.radius = radius
    this.knobRadius = radius * 0.4
    this.active = false
    this.touchId = null
    this.dx = 0
    this.dy = 0
  }
  
  handleTouchStart(touch) {
    const dist = Math.hypot(touch.clientX - this.baseX, touch.clientY - this.baseY)
    if (dist &lt; this.radius) {
      this.active = true
      this.touchId = touch.identifier
      this.updatePosition(touch.clientX, touch.clientY)
    }
  }
  
  handleTouchMove(touch) {
    if (touch.identifier === this.touchId) {
      this.updatePosition(touch.clientX, touch.clientY)
    }
  }
  
  handleTouchEnd(touch) {
    if (touch.identifier === this.touchId) {
      this.active = false
      this.touchId = null
      this.dx = 0
      this.dy = 0
    }
  }
  
  updatePosition(x, y) {
    let dx = x - this.baseX
    let dy = y - this.baseY
    const dist = Math.hypot(dx, dy)
    
    if (dist &gt; this.radius) {
      dx = (dx / dist) * this.radius
      dy = (dy / dist) * this.radius
    }
    
    this.dx = dx / this.radius
    this.dy = dy / this.radius
  }
  
  draw(ctx) {
    // Base circle
    ctx.beginPath()
    ctx.arc(this.baseX, this.baseY, this.radius, 0, Math.PI * 2)
    ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'
    ctx.fill()
    
    // Knob
    const knobX = this.baseX + this.dx * this.radius
    const knobY = this.baseY + this.dy * this.radius
    ctx.beginPath()
    ctx.arc(knobX, knobY, this.knobRadius, 0, Math.PI * 2)
    ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'
    ctx.fill()
  }
  
  getInput() {
    return { x: this.dx, y: this.dy }
  }
}
</code></pre>
<h2>5) Dokunmatik düğmeler</h2>
<pre><code class="language-js">class TouchButton {
  constructor(x, y, radius, label) {
    this.x = x
    this.y = y
    this.radius = radius
    this.label = label
    this.pressed = false
    this.touchId = null
  }
  
  contains(px, py) {
    return Math.hypot(px - this.x, py - this.y) &lt; this.radius
  }
  
  handleTouchStart(touch) {
    if (this.contains(touch.clientX, touch.clientY)) {
      this.pressed = true
      this.touchId = touch.identifier
    }
  }
  
  handleTouchEnd(touch) {
    if (touch.identifier === this.touchId) {
      this.pressed = false
      this.touchId = null
    }
  }
  
  draw(ctx) {
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
    ctx.fillStyle = this.pressed ? 'rgba(255, 255, 255, 0.5)' : 'rgba(255, 255, 255, 0.2)'
    ctx.fill()
    
    ctx.font = 'bold 24px sans-serif'
    ctx.fillStyle = '#fff'
    ctx.textAlign = 'center'
    ctx.textBaseline = 'middle'
    ctx.fillText(this.label, this.x, this.y)
  }
}
</code></pre>
<h2>6) Ekran yönünü işleme</h2>
<pre><code class="language-js">function checkOrientation() {
  const isLandscape = window.innerWidth &gt; window.innerHeight
  
  if (!isLandscape) {
    showRotatePrompt()
  } else {
    hideRotatePrompt()
    resizeCanvas()
  }
}

window.addEventListener('resize', checkOrientation)
window.addEventListener('orientationchange', checkOrientation)

function showRotatePrompt() {
  document.getElementById('rotate-prompt').style.display = 'flex'
}

function hideRotatePrompt() {
  document.getElementById('rotate-prompt').style.display = 'none'
}
</code></pre>
<h2>7) Performans optimizasyonu</h2>
<pre><code class="language-js">// Reduce resolution on low-end devices
function getDeviceScale() {
  const dpr = window.devicePixelRatio || 1
  const isLowEnd = navigator.hardwareConcurrency &lt;= 2
  return isLowEnd ? Math.min(dpr, 1.5) : Math.min(dpr, 2)
}

// Throttle expensive operations
let lastHeavyUpdate = 0
function maybeDoHeavyWork(timestamp) {
  if (timestamp - lastHeavyUpdate &gt; 100) { // 10 fps for heavy stuff
    doExpensiveCalculations()
    lastHeavyUpdate = timestamp
  }
}

// Use lower quality assets
function getAssetQuality() {
  const memory = navigator.deviceMemory || 4
  if (memory &lt;= 2) return 'low'
  if (memory &lt;= 4) return 'medium'
  return 'high'
}
</code></pre>
<h2>8) Pil kullanımına ilişkin hususlar</h2>
<pre><code class="language-js">// Check battery and reduce effects
async function checkBattery() {
  if ('getBattery' in navigator) {
    const battery = await navigator.getBattery()
    
    if (battery.level &lt; 0.2 &amp;&amp; !battery.charging) {
      enableBatterySaver()
    }
    
    battery.addEventListener('levelchange', () =&gt; {
      if (battery.level &lt; 0.2 &amp;&amp; !battery.charging) {
        enableBatterySaver()
      }
    })
  }
}

function enableBatterySaver() {
  game.particleCount = Math.floor(game.particleCount * 0.5)
  game.targetFps = 30
  console.log('Battery saver enabled')
}
</code></pre>
<h2>9) Fullscreen API</h2>
<pre><code class="language-js">async function requestFullscreen() {
  const elem = document.documentElement
  
  try {
    if (elem.requestFullscreen) {
      await elem.requestFullscreen()
    } else if (elem.webkitRequestFullscreen) {
      await elem.webkitRequestFullscreen()
    }
    
    // Lock orientation
    if (screen.orientation?.lock) {
      await screen.orientation.lock('landscape')
    }
  } catch (err) {
    console.warn('Fullscreen failed:', err)
  }
}

// Call on user interaction
document.getElementById('play-btn').addEventListener('click', () =&gt; {
  requestFullscreen()
  startGame()
})
</code></pre>
<p>Önemli bir sınırlama vardır: Fullscreen API, iPhone'daki genel öğeler için çalışmaz. iOS Safari tam ekran özelliğini yalnızca yerel video öğeleri için standart dışı <code>webkitEnterFullscreen</code> aracılığıyla sunar; bu nedenle <code>documentElement.requestFullscreen()</code> iPhone'da reddedilir. Yukarıdaki <code>try/catch</code> bunu sorunsuz biçimde işler. Bu nedenle oyununuzu iPhone'da gerçek tam ekran olmadan oynanabilecek şekilde tasarlayın ve tam ekranı, desteklendiği iPad, Android ve masaüstü cihazlarda ek bir iyileştirme olarak değerlendirin. <code>screen.orientation.lock()</code> çağrısı da iPhone'da aynı sınırlamaya sahiptir, dolayısıyla buna bağımlı olmayın.</p>
<h2>10) Test kontrol listesi</h2>
<p>Yayınlamadan önce:</p>
<ul>
<li><strong>Dokunma tepkiselliği</strong> — Yalnızca emülatörlerde değil, gerçek cihazlarda test edin</li>
<li><strong>Çeşitli ekran boyutları</strong> — Telefon, tablet, iPad Pro</li>
<li><strong>Her iki ekran yönü</strong> — Ya da tek bir yöne kilitleyin</li>
<li><strong>Düşük donanımlı cihazlar</strong> — 2-3 yıllık telefonlarda test edin</li>
<li><strong>Yavaş ağlar</strong> — 3G simülasyonunda test edin</li>
<li><strong>iOS Safari</strong> — Ses ve tam ekranla ilgili kendine özgü davranışları vardır</li>
<li><strong>Android Chrome</strong> — En yaygın tarayıcıdır</li>
<li><strong>Kesintiler</strong> — Telefon aramaları, bildirimler</li>
<li><strong>Bellek baskısı</strong> — Arka plan sekmesinden döndükten sonra toparlanıyor mu?</li>
</ul>
<pre><code class="language-js">// Debug touch visualization
function drawTouchDebug(ctx) {
  for (const [id, touch] of touches) {
    ctx.beginPath()
    ctx.arc(touch.x, touch.y, 30, 0, Math.PI * 2)
    ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'
    ctx.fill()
    ctx.fillStyle = '#fff'
    ctx.fillText(id.toString(), touch.x, touch.y)
  }
}
</code></pre>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/game-input-handling">Oyun girişlerini işleme</a></li>
<li><a href="/tr/tutorials/responsive-game-canvas">Duyarlı oyun canvas'ı</a></li>
<li><a href="/tr/tutorials/pwa-offline-games">Çevrimdışı oyunlar için PWA</a></li>
<li><a href="/tr/tutorials/web-audio-api-games">Oyunlar için Web Audio API</a> — iOS ses kısıtlamalarını işleme</li>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlama</a> — mobil bağlantılar için yükleme sürelerini optimize etme</li>
<li><a href="/tr/tutorials/iframe-embedding-games">Oyunları iframe ile yerleştirme</a> — mobil cihazlara özgü yerleştirme hususları</li>
</ul>
<h2>Harici kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Touch_events">MDN: Dokunma olayları</a> — çoklu dokunmayı işleme</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Screen_Orientation_API">MDN: Screen Orientation API</a> — ekran yönünü kilitleme</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API">MDN: Fullscreen API</a> — mobil cihazlarda tam ekrana geçme</li>
<li><a href="https://web.dev/learn/design/">web.dev: Duyarlı tasarım</a> — oyun arayüzlerine uygulanabilen duyarlı tasarım kalıpları</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Web oyunlarında piksel sanatı işleme]]></title>
            <link>https://app.cinevva.com/tr/tutorials/pixel-art-rendering</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/pixel-art-rendering</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Piksel sanatını doğru şekilde işleyin: net ölçeklendirme, bulanıklık olmadan görüntüleme, renk paletleri ve animasyon. Retro oyununuzun bilinçli tasarlanmış görünmesini sağlayın.]]></description>
            <content:encoded><![CDATA[<h1>Web oyunlarında piksel sanatı işleme</h1>
<p>Piksel sanatı doğru işlendiğinde harika, bulanık olduğundaysa berbat görünür. Bu eğitimde net ve özgün piksel grafikler oluşturma tekniklerini ele alacağız.</p>
<h2>1) Bulanıklık sorunu</h2>
<p>Tarayıcılar, varsayılan olarak ölçeklendirme sırasında görselleri yumuşatır. Bu, piksel sanatı için yanlıştır:</p>
<pre><code class="language-css">/* CSS solution */
canvas {
  image-rendering: pixelated;
  image-rendering: crisp-edges; /* Firefox fallback */
}
</code></pre>
<h2>2) Canvas bağlamı ayarları</h2>
<pre><code class="language-js">const canvas = document.getElementById('game')
const ctx = canvas.getContext('2d')

// Disable smoothing for crisp pixels
ctx.imageSmoothingEnabled = false
</code></pre>
<p>Bu ayar, canvas her yeniden boyutlandırıldığında veya bağlam her değiştiğinde yeniden yapılmalıdır.</p>
<h2>3) Tam sayı ölçeklendirme</h2>
<p>Piksel sanatı en iyi tam sayı ölçeklerinde (1x, 2x, 3x, 4x) görünür:</p>
<pre><code class="language-js">const GAME_WIDTH = 160
const GAME_HEIGHT = 144 // Game Boy resolution

function resize() {
  const maxScale = Math.floor(Math.min(
    window.innerWidth / GAME_WIDTH,
    window.innerHeight / GAME_HEIGHT
  ))
  const scale = Math.max(1, maxScale)
  
  canvas.width = GAME_WIDTH
  canvas.height = GAME_HEIGHT
  canvas.style.width = GAME_WIDTH * scale + 'px'
  canvas.style.height = GAME_HEIGHT * scale + 'px'
  
  ctx.imageSmoothingEnabled = false
}
</code></pre>
<h2>4) Sprite'ları tam sayı konumlarında çizme</h2>
<p>Alt piksel konumları bulanıklığa neden olur:</p>
<pre><code class="language-js">// Bad: causes blur
ctx.drawImage(sprite, 10.5, 20.3)

// Good: crisp pixels
ctx.drawImage(sprite, Math.floor(10.5), Math.floor(20.3))

// Or round to nearest
ctx.drawImage(sprite, Math.round(player.x), Math.round(player.y))
</code></pre>
<h2>5) Sprite sayfası animasyonu</h2>
<pre><code class="language-js">class PixelSprite {
  constructor(image, frameWidth, frameHeight) {
    this.image = image
    this.frameWidth = frameWidth
    this.frameHeight = frameHeight
    this.animations = {}
    this.currentAnim = null
    this.frame = 0
    this.timer = 0
  }
  
  addAnimation(name, frames, fps = 8) {
    this.animations[name] = { frames, duration: 1 / fps }
  }
  
  play(name) {
    if (this.currentAnim !== name) {
      this.currentAnim = name
      this.frame = 0
      this.timer = 0
    }
  }
  
  update(dt) {
    if (!this.currentAnim) return
    
    const anim = this.animations[this.currentAnim]
    this.timer += dt
    
    if (this.timer &gt;= anim.duration) {
      this.timer = 0
      this.frame = (this.frame + 1) % anim.frames.length
    }
  }
  
  draw(ctx, x, y, flipX = false) {
    if (!this.currentAnim) return
    
    const anim = this.animations[this.currentAnim]
    const frameIndex = anim.frames[this.frame]
    const sx = (frameIndex % 16) * this.frameWidth
    const sy = Math.floor(frameIndex / 16) * this.frameHeight
    
    ctx.save()
    if (flipX) {
      ctx.scale(-1, 1)
      x = -x - this.frameWidth
    }
    
    ctx.drawImage(
      this.image,
      sx, sy, this.frameWidth, this.frameHeight,
      Math.round(x), Math.round(y), this.frameWidth, this.frameHeight
    )
    ctx.restore()
  }
}

// Usage
const player = new PixelSprite(spriteSheet, 16, 16)
player.addAnimation('idle', [0, 1], 2)
player.addAnimation('walk', [2, 3, 4, 5], 8)
player.addAnimation('jump', [6], 1)
</code></pre>
<h2>6) Palet değiştirme</h2>
<p>Sprite'ları yeniden renklendirmek için kullanılan klasik bir teknik:</p>
<pre><code class="language-js">function createPaletteSwap(image, oldColors, newColors) {
  const canvas = document.createElement('canvas')
  canvas.width = image.width
  canvas.height = image.height
  const ctx = canvas.getContext('2d')
  
  ctx.drawImage(image, 0, 0)
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  const data = imageData.data
  
  for (let i = 0; i &lt; data.length; i += 4) {
    const r = data[i], g = data[i + 1], b = data[i + 2]
    
    for (let j = 0; j &lt; oldColors.length; j++) {
      const old = oldColors[j]
      if (r === old.r &amp;&amp; g === old.g &amp;&amp; b === old.b) {
        const newC = newColors[j]
        data[i] = newC.r
        data[i + 1] = newC.g
        data[i + 2] = newC.b
        break
      }
    }
  }
  
  ctx.putImageData(imageData, 0, 0)
  return canvas
}

// Create enemy variant
const blueEnemy = createPaletteSwap(
  enemySprite,
  [{ r: 255, g: 0, b: 0 }],    // Red
  [{ r: 0, g: 100, b: 255 }]   // Blue
)
</code></pre>
<h2>7) Döşeme haritası işleme</h2>
<pre><code class="language-js">class Tilemap {
  constructor(tileSize, tilesetImage) {
    this.tileSize = tileSize
    this.tileset = tilesetImage
    this.tilesPerRow = Math.floor(tilesetImage.width / tileSize)
    this.data = []
  }
  
  setData(width, height, tiles) {
    this.width = width
    this.height = height
    this.data = tiles
  }
  
  draw(ctx, cameraX = 0, cameraY = 0) {
    const startCol = Math.floor(cameraX / this.tileSize)
    const startRow = Math.floor(cameraY / this.tileSize)
    const endCol = startCol + Math.ceil(ctx.canvas.width / this.tileSize) + 1
    const endRow = startRow + Math.ceil(ctx.canvas.height / this.tileSize) + 1
    
    for (let row = startRow; row &lt; endRow; row++) {
      for (let col = startCol; col &lt; endCol; col++) {
        if (row &lt; 0 || row &gt;= this.height || col &lt; 0 || col &gt;= this.width) continue
        
        const tileIndex = this.data[row * this.width + col]
        if (tileIndex &lt; 0) continue
        
        const sx = (tileIndex % this.tilesPerRow) * this.tileSize
        const sy = Math.floor(tileIndex / this.tilesPerRow) * this.tileSize
        const dx = Math.round(col * this.tileSize - cameraX)
        const dy = Math.round(row * this.tileSize - cameraY)
        
        ctx.drawImage(
          this.tileset,
          sx, sy, this.tileSize, this.tileSize,
          dx, dy, this.tileSize, this.tileSize
        )
      }
    }
  }
}
</code></pre>
<h2>8) Dış çizgi efektleri</h2>
<p>Görünürlüğü artırmak için sprite'lara dış çizgiler ekleyin:</p>
<pre><code class="language-js">function drawWithOutline(ctx, image, x, y, outlineColor = '#000') {
  const offsets = [[-1, 0], [1, 0], [0, -1], [0, 1]]
  
  // Draw outline
  ctx.globalCompositeOperation = 'source-over'
  for (const [ox, oy] of offsets) {
    ctx.drawImage(image, Math.round(x + ox), Math.round(y + oy))
  }
  
  // Apply color to outline
  ctx.globalCompositeOperation = 'source-in'
  ctx.fillStyle = outlineColor
  ctx.fillRect(x - 1, y - 1, image.width + 2, image.height + 2)
  
  // Draw sprite on top
  ctx.globalCompositeOperation = 'source-over'
  ctx.drawImage(image, Math.round(x), Math.round(y))
}
</code></pre>
<h2>9) Ekran sarsıntısı</h2>
<p>Klasik bir oyun hissi efekti:</p>
<pre><code class="language-js">const camera = {
  x: 0,
  y: 0,
  shakeIntensity: 0,
  shakeDuration: 0,
  
  shake(intensity, duration) {
    this.shakeIntensity = intensity
    this.shakeDuration = duration
  },
  
  update(dt) {
    if (this.shakeDuration &gt; 0) {
      this.shakeDuration -= dt
    }
  },
  
  getOffset() {
    if (this.shakeDuration &lt;= 0) return { x: 0, y: 0 }
    
    return {
      x: Math.round((Math.random() - 0.5) * 2 * this.shakeIntensity),
      y: Math.round((Math.random() - 0.5) * 2 * this.shakeIntensity),
    }
  }
}

// In render:
function render() {
  const shake = camera.getOffset()
  ctx.save()
  ctx.translate(shake.x, shake.y)
  // Draw everything...
  ctx.restore()
}

// On hit:
camera.shake(4, 0.2) // 4 pixel intensity, 0.2 seconds
</code></pre>
<h2>10) Renk paleti kısıtlamaları</h2>
<p>Özgün piksel sanatında sınırlı paletler kullanılır:</p>
<pre><code class="language-js">const PALETTES = {
  gameboy: ['#0f380f', '#306230', '#8bac0f', '#9bbc0f'],
  nes: ['#000000', '#fcfcfc', '#f8f8f8', '#bcbcbc', /* ... */],
  pico8: [
    '#000000', '#1d2b53', '#7e2553', '#008751',
    '#ab5236', '#5f574f', '#c2c3c7', '#fff1e8',
    '#ff004d', '#ffa300', '#ffec27', '#00e436',
    '#29adff', '#83769c', '#ff77a8', '#ffccaa'
  ],
}

function quantizeColor(r, g, b, palette) {
  let minDist = Infinity
  let closest = palette[0]
  
  for (const color of palette) {
    const pr = parseInt(color.slice(1, 3), 16)
    const pg = parseInt(color.slice(3, 5), 16)
    const pb = parseInt(color.slice(5, 7), 16)
    
    const dist = (r - pr) ** 2 + (g - pg) ** 2 + (b - pb) ** 2
    if (dist &lt; minDist) {
      minDist = dist
      closest = color
    }
  }
  
  return closest
}
</code></pre>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/canvas-2d-game-loop">Canvas 2D oyun döngüsü</a></li>
<li><a href="/tr/tutorials/responsive-game-canvas">Duyarlı oyun canvas'ı</a></li>
<li><a href="/tr/tutorials/streaming-asset-loading">Akışla oyun varlığı yükleme</a></li>
<li><a href="/tr/guides/game-assets-guide">Ücretsiz oyun varlıkları nerede bulunur?</a> — sprite sayfaları, piksel sanatı paketleri ve 2D varlık kaynakları</li>
<li><a href="/tr/tutorials/text-rendering-games">Oyunlarda metin işleme</a> — piksel sanatı tarzınızla uyumlu bitmap yazı tipleri</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[FPS oyunları için Pointer Lock]]></title>
            <link>https://app.cinevva.com/tr/tutorials/pointer-lock-fps</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/pointer-lock-fps</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Pointer Lock API ile fareyle bakış özelliğini uygulayın: imleci yakalama, hareketi işleme ve web oyunları için FPS kontrolleri oluşturma.]]></description>
            <content:encoded><![CDATA[<h1>FPS oyunları için Pointer Lock</h1>
<p>Pointer Lock API, fare imlecini yakalayarak akıcı FPS tarzı kamera kontrollerine olanak tanır. Birinci ve üçüncü şahıs nişancı oyunları için vazgeçilmezdir.</p>
<h2>1) Pointer lock isteme</h2>
<p>Pointer lock için bir kullanıcı etkileşimi gerekir:</p>
<pre><code class="language-js">const canvas = document.getElementById('game')

canvas.addEventListener('click', () =&gt; {
  canvas.requestPointerLock()
})
</code></pre>
<h2>Ham fare girdisi (ivmeyi devre dışı bırakma)</h2>
<p>Tarayıcı varsayılan olarak işletim sisteminin fare ivmesini <code>movementX</code>/<code>movementY</code> değerlerine uygular; bu nedenle aynı mesafede yapılan hızlı bir hareket ile yavaş bir sürükleme, kamerayı farklı miktarlarda döndürür. FPS nişan alma için genellikle bunun tersini istersiniz: aynı fiziksel mesafe, kamerayı her zaman aynı miktarda döndürmelidir. <code>requestPointerLock()</code> işlevine <code>unadjustedMovement: true</code> geçirerek ham ve ivmesiz hareket isteyin:</p>
<pre><code class="language-js">canvas.addEventListener('click', async () =&gt; {
  try {
    await canvas.requestPointerLock({ unadjustedMovement: true })
  } catch (err) {
    // unadjustedMovement unsupported here; fall back to OS-adjusted deltas
    await canvas.requestPointerLock()
  }
})
</code></pre>
<p>Modern <code>requestPointerLock()</code>, başarı durumunda çözümlenen ve başarısızlık durumunda reddedilen bir Promise döndürür; bu nedenle yukarıdaki çağrı <code>try/catch</code> içine alınmıştır. Chrome ve Edge, 88. sürümden beri <code>unadjustedMovement</code> seçeneğini destekler; Safari ise <code>requestPointerLock</code> işlevinin Promise döndürmesini da düzelten 18.4 sürümünden (Mart 2025) beri desteklemektedir. Promise döndürmeyen eski tarayıcılar da burada çalışır; çünkü <code>await</code>, Promise olmayan bir değeri kabul eder ve bilinmeyen seçenek yok sayılır. Bu nedenle sade <code>requestPointerLock()</code> yedeğini her zaman koruyun.</p>
<h2>2) Kilit durumunu algılama</h2>
<pre><code class="language-js">document.addEventListener('pointerlockchange', () =&gt; {
  if (document.pointerLockElement === canvas) {
    console.log('Pointer locked')
    game.mouseLocked = true
  } else {
    console.log('Pointer unlocked')
    game.mouseLocked = false
  }
})

document.addEventListener('pointerlockerror', () =&gt; {
  console.error('Pointer lock failed')
})
</code></pre>
<h2>3) Fare hareketini okuma</h2>
<p>Kilitliyken <code>movementX</code> ve <code>movementY</code> değerlerini kullanın:</p>
<pre><code class="language-js">document.addEventListener('mousemove', (e) =&gt; {
  if (document.pointerLockElement !== canvas) return
  
  const sensitivity = 0.002
  camera.yaw -= e.movementX * sensitivity
  camera.pitch -= e.movementY * sensitivity
  
  // Clamp pitch to prevent flipping
  camera.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.pitch))
})
</code></pre>
<h2>4) FPS kamera sınıfı</h2>
<pre><code class="language-js">class FPSCamera {
  constructor() {
    this.position = { x: 0, y: 1.7, z: 0 } // Eye height
    this.yaw = 0      // Left/right rotation
    this.pitch = 0    // Up/down rotation
    this.sensitivity = 0.002
  }
  
  handleMouseMove(e) {
    this.yaw -= e.movementX * this.sensitivity
    this.pitch -= e.movementY * this.sensitivity
    this.pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.pitch))
  }
  
  getForward() {
    return {
      x: Math.sin(this.yaw) * Math.cos(this.pitch),
      y: Math.sin(this.pitch),
      z: Math.cos(this.yaw) * Math.cos(this.pitch),
    }
  }
  
  getRight() {
    return {
      x: Math.cos(this.yaw),
      y: 0,
      z: -Math.sin(this.yaw),
    }
  }
  
  move(forward, right, dt, speed = 5) {
    const fwd = this.getForward()
    const rgt = this.getRight()
    
    // Move on XZ plane only
    this.position.x += (fwd.x * forward + rgt.x * right) * speed * dt
    this.position.z += (fwd.z * forward + rgt.z * right) * speed * dt
  }
  
  getViewMatrix() {
    const forward = this.getForward()
    const target = {
      x: this.position.x + forward.x,
      y: this.position.y + forward.y,
      z: this.position.z + forward.z,
    }
    return lookAt(this.position, target, { x: 0, y: 1, z: 0 })
  }
}
</code></pre>
<h2>5) WASD hareketi</h2>
<pre><code class="language-js">const input = {
  forward: false,
  backward: false,
  left: false,
  right: false,
  jump: false,
}

window.addEventListener('keydown', (e) =&gt; {
  switch (e.code) {
    case 'KeyW': input.forward = true; break
    case 'KeyS': input.backward = true; break
    case 'KeyA': input.left = true; break
    case 'KeyD': input.right = true; break
    case 'Space': input.jump = true; break
  }
})

window.addEventListener('keyup', (e) =&gt; {
  switch (e.code) {
    case 'KeyW': input.forward = false; break
    case 'KeyS': input.backward = false; break
    case 'KeyA': input.left = false; break
    case 'KeyD': input.right = false; break
    case 'Space': input.jump = false; break
  }
})

function update(dt) {
  const moveForward = (input.forward ? 1 : 0) - (input.backward ? 1 : 0)
  const moveRight = (input.right ? 1 : 0) - (input.left ? 1 : 0)
  
  camera.move(moveForward, moveRight, dt)
}
</code></pre>
<h2>6) Tam FPS kontrolcüsü</h2>
<pre><code class="language-js">class FPSController {
  constructor(canvas) {
    this.canvas = canvas
    this.camera = new FPSCamera()
    this.locked = false
    this.speed = 5
    this.sprintMultiplier = 1.5
    
    this.input = {
      forward: false, backward: false,
      left: false, right: false,
      jump: false, sprint: false,
    }
    
    this.velocity = { x: 0, y: 0, z: 0 }
    this.onGround = true
    this.gravity = -20
    this.jumpSpeed = 8
    
    this.bindEvents()
  }
  
  bindEvents() {
    this.canvas.addEventListener('click', () =&gt; {
      this.canvas.requestPointerLock()
    })
    
    document.addEventListener('pointerlockchange', () =&gt; {
      this.locked = document.pointerLockElement === this.canvas
    })
    
    document.addEventListener('mousemove', (e) =&gt; {
      if (this.locked) {
        this.camera.handleMouseMove(e)
      }
    })
    
    window.addEventListener('keydown', (e) =&gt; this.handleKey(e.code, true))
    window.addEventListener('keyup', (e) =&gt; this.handleKey(e.code, false))
  }
  
  handleKey(code, pressed) {
    switch (code) {
      case 'KeyW': this.input.forward = pressed; break
      case 'KeyS': this.input.backward = pressed; break
      case 'KeyA': this.input.left = pressed; break
      case 'KeyD': this.input.right = pressed; break
      case 'Space': this.input.jump = pressed; break
      case 'ShiftLeft': this.input.sprint = pressed; break
    }
  }
  
  update(dt) {
    if (!this.locked) return
    
    const speed = this.speed * (this.input.sprint ? this.sprintMultiplier : 1)
    
    // Horizontal movement
    const moveForward = (this.input.forward ? 1 : 0) - (this.input.backward ? 1 : 0)
    const moveRight = (this.input.right ? 1 : 0) - (this.input.left ? 1 : 0)
    
    const forward = this.camera.getForward()
    const right = this.camera.getRight()
    
    this.velocity.x = (forward.x * moveForward + right.x * moveRight) * speed
    this.velocity.z = (forward.z * moveForward + right.z * moveRight) * speed
    
    // Jumping
    if (this.input.jump &amp;&amp; this.onGround) {
      this.velocity.y = this.jumpSpeed
      this.onGround = false
    }
    
    // Gravity
    if (!this.onGround) {
      this.velocity.y += this.gravity * dt
    }
    
    // Apply velocity
    this.camera.position.x += this.velocity.x * dt
    this.camera.position.y += this.velocity.y * dt
    this.camera.position.z += this.velocity.z * dt
    
    // Simple ground collision
    if (this.camera.position.y &lt; 1.7) {
      this.camera.position.y = 1.7
      this.velocity.y = 0
      this.onGround = true
    }
  }
}
</code></pre>
<h2>7) Nişangâh</h2>
<pre><code class="language-js">function drawCrosshair(ctx) {
  const cx = ctx.canvas.width / 2
  const cy = ctx.canvas.height / 2
  const size = 10
  const gap = 4
  
  ctx.strokeStyle = '#fff'
  ctx.lineWidth = 2
  
  // Top
  ctx.beginPath()
  ctx.moveTo(cx, cy - gap)
  ctx.lineTo(cx, cy - gap - size)
  ctx.stroke()
  
  // Bottom
  ctx.beginPath()
  ctx.moveTo(cx, cy + gap)
  ctx.lineTo(cx, cy + gap + size)
  ctx.stroke()
  
  // Left
  ctx.beginPath()
  ctx.moveTo(cx - gap, cy)
  ctx.lineTo(cx - gap - size, cy)
  ctx.stroke()
  
  // Right
  ctx.beginPath()
  ctx.moveTo(cx + gap, cy)
  ctx.lineTo(cx + gap + size, cy)
  ctx.stroke()
}
</code></pre>
<h2>8) Fare düğmeleriyle ateş etme</h2>
<pre><code class="language-js">document.addEventListener('mousedown', (e) =&gt; {
  if (!document.pointerLockElement) return
  
  if (e.button === 0) {
    // Left click - primary fire
    weapon.fire()
  } else if (e.button === 2) {
    // Right click - aim down sights
    weapon.aimDownSights(true)
  }
})

document.addEventListener('mouseup', (e) =&gt; {
  if (e.button === 2) {
    weapon.aimDownSights(false)
  }
})

// Prevent context menu
canvas.addEventListener('contextmenu', (e) =&gt; e.preventDefault())
</code></pre>
<h2>9) Hassasiyet ayarları</h2>
<pre><code class="language-js">class Settings {
  constructor() {
    this.mouseSensitivity = parseFloat(localStorage.getItem('sensitivity') || '1.0')
    this.invertY = localStorage.getItem('invertY') === 'true'
  }
  
  save() {
    localStorage.setItem('sensitivity', this.mouseSensitivity.toString())
    localStorage.setItem('invertY', this.invertY.toString())
  }
}

// Apply in mouse handler
document.addEventListener('mousemove', (e) =&gt; {
  if (!locked) return
  
  const sens = settings.mouseSensitivity * 0.002
  camera.yaw -= e.movementX * sens
  camera.pitch -= e.movementY * sens * (settings.invertY ? -1 : 1)
})
</code></pre>
<h2>10) Çıkış istemi</h2>
<p>Pointer lock'tan çıkma talimatlarını gösterin:</p>
<pre><code class="language-js">function showLockUI() {
  const ui = document.getElementById('lock-ui')
  
  if (document.pointerLockElement) {
    ui.innerHTML = '&lt;p&gt;Farenin kilidini açmak için ESC tuşuna basın&lt;/p&gt;'
    ui.style.opacity = '0.5'
    setTimeout(() =&gt; ui.style.opacity = '0', 2000)
  } else {
    ui.innerHTML = '&lt;p&gt;Oynamak için tıklayın&lt;/p&gt;'
    ui.style.opacity = '1'
  }
}

document.addEventListener('pointerlockchange', showLockUI)
</code></pre>
<p>Çıkış istemi hakkında bilinmesi gereken bir nokta vardır: Oyuncu pointer lock'tan çıkmak için ESC tuşuna bastığında tarayıcı, yeniden kilitleme yapılabilmesi için yeni bir kullanıcı etkileşimi (tıklama) ister. “Oynamak için tıklayın” katmanının bulunma nedeni budur. Ancak kendi kodunuz <code>document.exitPointerLock()</code> işlevini çağırırsa (örneğin oyun içi bir menü açmak için), sonrasında yeniden kilitleme yapmak için yeni bir etkileşim gerekmez. Böylece menü kapandığında kilidi program aracılığıyla yeniden etkinleştirebilirsiniz. ESC tuşuna art arda basılması da tarayıcının daha bilinçli bir kullanıcı eylemi gerçekleşene kadar yeniden kilitlemeyi reddetmesine yol açabilir. Bu nedenle <code>pointerlockchange</code> sırasında otomatik olarak yeniden kilitlemeye çalışmayın.</p>
<h2>Iframe ile ilgili hususlar</h2>
<p>Iframe'lerde pointer lock için <code>allow=&quot;pointer-lock&quot;</code> özniteliği gerekir:</p>
<pre><code class="language-html">&lt;iframe 
  src=&quot;game.html&quot; 
  allow=&quot;pointer-lock; fullscreen&quot;
&gt;&lt;/iframe&gt;
</code></pre>
<p>Pointer lock özelliğinin kullanılabilir olup olmadığını kontrol edin:</p>
<pre><code class="language-js">if (!document.pointerLockElement &amp;&amp; !('requestPointerLock' in canvas)) {
  showMessage('Fare yakalama kullanılamıyor')
}
</code></pre>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/game-input-handling">Oyun girdilerini işleme</a></li>
<li><a href="/tr/tutorials/gamepad-api">Gamepad API</a></li>
<li><a href="/tr/tutorials/webgl-fundamentals-for-games">WebGL'in temelleri</a></li>
<li><a href="/tr/tutorials/webxr-basics">WebXR'ın temelleri</a> — birinci şahıs deneyimleri için VR kontrolleri</li>
<li><a href="/tr/tutorials/webgpu-getting-started">WebGPU'ya başlangıç</a> — FPS oyunları için modern GPU görüntü işleme</li>
</ul>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API">MDN: Pointer Lock API</a> — eksiksiz API başvurusu</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX">MDN: MouseEvent.movementX</a> — fare hareket farklarını okuma</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Çevrimdışı web oyunları için PWA]]></title>
            <link>https://app.cinevva.com/tr/tutorials/pwa-offline-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/pwa-offline-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Oyununuzu PWA ile çevrimdışı çalıştırın: manifest, service worker, önbelleğe alma stratejileri ve yükleme istemleri. Her yerde, her zaman oynayın.]]></description>
            <content:encoded><![CDATA[<h1>Çevrimdışı web oyunları için PWA</h1>
<p>Progresif Web Uygulamaları, oyuncuların oyununuzu yüklemesine ve çevrimdışı oynamasına olanak tanır. Bu eğitim, bir web oyununa PWA desteğinin nasıl ekleneceğini gösterir.</p>
<h2>1) Web Uygulaması Manifesti</h2>
<p><code>manifest.json</code> dosyasını oluşturun:</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;Harika Oyunum&quot;,
  &quot;short_name&quot;: &quot;HarikaOyun&quot;,
  &quot;description&quot;: &quot;Çevrimdışı oynayabileceğiniz harika bir oyun&quot;,
  &quot;start_url&quot;: &quot;/&quot;,
  &quot;display&quot;: &quot;fullscreen&quot;,
  &quot;orientation&quot;: &quot;landscape&quot;,
  &quot;background_color&quot;: &quot;#1a1a2e&quot;,
  &quot;theme_color&quot;: &quot;#4ade80&quot;,
  &quot;icons&quot;: [
    {
      &quot;src&quot;: &quot;/icons/icon-192.png&quot;,
      &quot;sizes&quot;: &quot;192x192&quot;,
      &quot;type&quot;: &quot;image/png&quot;
    },
    {
      &quot;src&quot;: &quot;/icons/icon-512.png&quot;,
      &quot;sizes&quot;: &quot;512x512&quot;,
      &quot;type&quot;: &quot;image/png&quot;
    },
    {
      &quot;src&quot;: &quot;/icons/icon-maskable.png&quot;,
      &quot;sizes&quot;: &quot;512x512&quot;,
      &quot;type&quot;: &quot;image/png&quot;,
      &quot;purpose&quot;: &quot;maskable&quot;
    }
  ]
}
</code></pre>
<p>HTML dosyanızda buna bağlantı verin:</p>
<pre><code class="language-html">&lt;link rel=&quot;manifest&quot; href=&quot;/manifest.json&quot;&gt;
&lt;meta name=&quot;theme-color&quot; content=&quot;#4ade80&quot;&gt;
&lt;link rel=&quot;apple-touch-icon&quot; href=&quot;/icons/icon-192.png&quot;&gt;
</code></pre>
<h2>2) Temel service worker</h2>
<p><code>sw.js</code> dosyasını oluşturun:</p>
<pre><code class="language-js">const CACHE_NAME = 'game-v1'

const ASSETS = [
  '/',
  '/index.html',
  '/game.js',
  '/style.css',
  '/assets/sprites.png',
  '/assets/sounds/jump.mp3',
  '/assets/sounds/music.mp3',
]

// Install: cache assets
self.addEventListener('install', (event) =&gt; {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) =&gt; {
      return cache.addAll(ASSETS)
    })
  )
})

// Activate: clean old caches
self.addEventListener('activate', (event) =&gt; {
  event.waitUntil(
    caches.keys().then((keys) =&gt; {
      return Promise.all(
        keys.filter(key =&gt; key !== CACHE_NAME)
            .map(key =&gt; caches.delete(key))
      )
    })
  )
})

// Fetch: serve from cache, fall back to network
self.addEventListener('fetch', (event) =&gt; {
  event.respondWith(
    caches.match(event.request).then((cached) =&gt; {
      return cached || fetch(event.request)
    })
  )
})
</code></pre>
<h2>3) Service worker'ı kaydetme</h2>
<pre><code class="language-js">if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () =&gt; {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js')
      console.log('SW registered:', registration.scope)
    } catch (err) {
      console.error('SW registration failed:', err)
    }
  })
}
</code></pre>
<h2>4) Ağ üzerinden güncellemeli önbellek öncelikli strateji</h2>
<p>Oyunlar için daha iyidir: arka planda güncellemeler yapılırken hızlı yükleme sağlar.</p>
<pre><code class="language-js">self.addEventListener('fetch', (event) =&gt; {
  event.respondWith(
    caches.open(CACHE_NAME).then(async (cache) =&gt; {
      const cached = await cache.match(event.request)
      
      // Start network fetch in background
      const fetchPromise = fetch(event.request).then((response) =&gt; {
        if (response.ok) {
          cache.put(event.request, response.clone())
        }
        return response
      }).catch(() =&gt; null)
      
      // Return cached immediately, or wait for network
      return cached || fetchPromise
    })
  )
})
</code></pre>
<h2>5) Güncellemeler için sürümlendirilmiş önbellek</h2>
<pre><code class="language-js">const CACHE_VERSION = 'v2'
const STATIC_CACHE = `static-${CACHE_VERSION}`
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`

const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/game.js',
  // ... core assets that rarely change
]

self.addEventListener('install', (event) =&gt; {
  self.skipWaiting() // Activate immediately
  
  event.waitUntil(
    caches.open(STATIC_CACHE).then(cache =&gt; cache.addAll(STATIC_ASSETS))
  )
})

self.addEventListener('activate', (event) =&gt; {
  event.waitUntil(
    caches.keys().then(keys =&gt; {
      return Promise.all(
        keys.filter(key =&gt; !key.includes(CACHE_VERSION))
            .map(key =&gt; caches.delete(key))
      )
    })
  )
  
  clients.claim() // Take control immediately
})
</code></pre>
<h2>6) Oyun güncellemelerini yönetme</h2>
<p>Bir güncelleme kullanıma sunulduğunda oyuncuları bilgilendirin:</p>
<pre><code class="language-js">// In main app
let refreshing = false

navigator.serviceWorker.addEventListener('controllerchange', () =&gt; {
  if (!refreshing) {
    refreshing = true
    showUpdateNotification()
  }
})

function showUpdateNotification() {
  const banner = document.createElement('div')
  banner.innerHTML = `
    &lt;p&gt;Oyun güncellendi! En son sürümü edinmek için yenileyin.&lt;/p&gt;
    &lt;button onclick=&quot;location.reload()&quot;&gt;Yenile&lt;/button&gt;
  `
  banner.className = 'update-banner'
  document.body.appendChild(banner)
}
</code></pre>
<h2>7) Çevrimdışı durumu algılama</h2>
<pre><code class="language-js">function updateOnlineStatus() {
  if (navigator.onLine) {
    hideOfflineBanner()
    syncGameData()
  } else {
    showOfflineBanner()
  }
}

window.addEventListener('online', updateOnlineStatus)
window.addEventListener('offline', updateOnlineStatus)

function showOfflineBanner() {
  document.getElementById('offline-banner').style.display = 'block'
}

function hideOfflineBanner() {
  document.getElementById('offline-banner').style.display = 'none'
}
</code></pre>
<h2>8) Yükleme istemi</h2>
<pre><code class="language-js">let deferredPrompt = null

window.addEventListener('beforeinstallprompt', (e) =&gt; {
  e.preventDefault()
  deferredPrompt = e
  showInstallButton()
})

function showInstallButton() {
  const btn = document.getElementById('install-btn')
  btn.style.display = 'block'
  btn.addEventListener('click', installApp)
}

async function installApp() {
  if (!deferredPrompt) return
  
  deferredPrompt.prompt()
  const { outcome } = await deferredPrompt.userChoice
  
  console.log('Install prompt outcome:', outcome)
  deferredPrompt = null
  document.getElementById('install-btn').style.display = 'none'
}

window.addEventListener('appinstalled', () =&gt; {
  console.log('App installed!')
  deferredPrompt = null
})
</code></pre>
<p>iOS ve iPadOS'te <code>beforeinstallprompt</code> olayı hiçbir zaman tetiklenmez; bu nedenle yukarıdaki yükleme düğmesi yalnızca Chromium tarayıcılarında görünür. iPhone ve iPad kullanıcıları, Paylaş'a ve ardından &quot;Ana Ekrana Ekle&quot; seçeneğine dokunarak yükleme yapar. 2026'da değişen ve bilinmesi gereken bir nokta var: Safari 26'dan (iOS 26 / iPadOS 26) itibaren Ana Ekran'a eklenen her site, varsayılan olarak etkin olan &quot;Web Uygulaması Olarak Aç&quot; seçeneğiyle birlikte varsayılan olarak bir web uygulaması şeklinde açılır. iOS'te artık yüklenebilirlik şartı yoktur; ancak manifest ve service worker sunmak yine de çok daha iyi bir çevrimdışı deneyim sağlar.</p>
<h2>9) Liderlik tabloları için arka plan senkronizasyonu</h2>
<p>Destek konusunda önemli bir not: Background Sync API yalnızca Chromium tarayıcılarında (Chrome, Edge, Opera, Samsung Internet) uygulanmıştır. Firefox ve Safari (iOS dâhil) bunu desteklemez; bu nedenle her zaman özellik algılama kullanın ve skorları uygulamanın bir sonraki normal yüklenişinde gönderen bir yedek yöntem bulundurun.</p>
<pre><code class="language-js">// In service worker
self.addEventListener('sync', (event) =&gt; {
  if (event.tag === 'sync-scores') {
    event.waitUntil(syncScores())
  }
})

async function syncScores() {
  const db = await openDB('game', 1)
  const pendingScores = await db.getAll('pending-scores')
  
  for (const score of pendingScores) {
    try {
      await fetch('/api/scores', {
        method: 'POST',
        body: JSON.stringify(score),
        headers: { 'Content-Type': 'application/json' }
      })
      await db.delete('pending-scores', score.id)
    } catch (err) {
      // Will retry on next sync
      break
    }
  }
}

// In main app
async function submitScore(score) {
  try {
    await fetch('/api/scores', { method: 'POST', body: JSON.stringify(score) })
  } catch {
    // Save for later sync
    const db = await openDB('game', 1)
    await db.add('pending-scores', { ...score, id: Date.now() })
    
    if ('serviceWorker' in navigator &amp;&amp; 'sync' in window.registration) {
      await navigator.serviceWorker.ready
      await registration.sync.register('sync-scores')
    }
  }
}
</code></pre>
<h2>10) PWA'yı test etme</h2>
<p><strong>Chrome DevTools:</strong></p>
<ul>
<li>Application &gt; Service Workers</li>
<li>Application &gt; Manifest</li>
<li>Application &gt; Cache Storage</li>
<li>Network &gt; Offline onay kutusu</li>
</ul>
<p><strong>Lighthouse denetimi:</strong></p>
<ul>
<li>PWA denetimini çalıştırın</li>
<li>Yüklenebilirliği kontrol edin</li>
<li>Çevrimdışı çalışma özelliğini kontrol edin</li>
</ul>
<p><strong>Gerçek cihazda test:</strong></p>
<ul>
<li>Telefonun ana ekranına yükleyin</li>
<li>Uçak modunu etkinleştirin</li>
<li>Tüm özellikleri çevrimdışı test edin</li>
</ul>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/service-workers-caching">Oyunları önbelleğe almak için service worker'lar</a></li>
<li><a href="/tr/tutorials/indexeddb-game-saves">IndexedDB ile oyun kayıtları</a></li>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlama</a></li>
<li><a href="/tr/tutorials/mobile-friendly-web-games">Mobil uyumlu web oyunları</a> — PWA oyunları için dokunmatik kontroller ve viewport yönetimi</li>
<li><a href="/tr/tutorials/streaming-asset-loading">Akışlı varlık yükleme</a> — service worker önbelleğiyle çalışan yükleme stratejileri</li>
</ul>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps">MDN: Progresif Web Uygulamaları</a> — kapsamlı PWA belgeleri</li>
<li><a href="https://web.dev/learn/pwa/">web.dev: PWA'yı Öğrenin</a> — Google'ın PWA öğrenme yolu</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Manifest">MDN: Web Uygulaması Manifesti</a> — manifest dosyası referansı</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Doğru şekilde ölçeklenen duyarlı oyun tuvali]]></title>
            <link>https://app.cinevva.com/tr/tutorials/responsive-game-canvas</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/responsive-game-canvas</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Oyun tuvalinizi duyarlı hâle getirin: piksel hassasiyetinde ölçekleme, letterbox ve yeniden boyutlandırma olaylarını takılma olmadan yönetme.]]></description>
            <content:encoded><![CDATA[<h1>Duyarlı Oyun Tuvali</h1>
<p>Doğru şekilde ölçeklenmeyen bir tuval, farklı ekran boyutlarında bulanık görünür veya bozulur. Bu sorunu sayamayacağım kadar çok kez ayıkladım; sizi bu zahmetten kurtarayım.</p>
<p><strong>Farklı ölçekleme modlarını deneyin:</strong></p>
<ResponsiveDemo />
<h2>Temel Sorun</h2>
<p>Canvas'ın kolayca karıştırılabilen iki boyutu vardır. CSS boyutu, ekranda ne kadar büyük görüneceğini belirler. Dahili çözünürlük ise gerçekte kaç piksel çizdiğinizi belirler. Bunlar eşleşmezse bulanıklık veya bozulma oluşur.</p>
<p>Retina bir ekranda CSS değeriniz 800x600 olabilir, ancak ekranın fiziksel çözünürlüğü aslında 1600x1200 pikseldir. Canvas'ın dahili çözünürlüğü yalnızca 800x600 ise tarayıcı bunu büyütür ve her şey bulanık görünür.</p>
<h2>Temel Duyarlı Kurulum</h2>
<p>Bir canvas'ın pencereyi doldurmasını ve net görünmesini şu şekilde sağlayabilirsiniz:</p>
<pre><code class="language-js">const canvas = document.getElementById('game')
const ctx = canvas.getContext('2d')

function resize() {
  const dpr = window.devicePixelRatio || 1
  const width = window.innerWidth
  const height = window.innerHeight
  
  // Dahili çözünürlüğü fiziksel piksellerle eşleşecek şekilde ayarla
  canvas.width = width * dpr
  canvas.height = height * dpr
  
  // CSS boyutunu mantıksal piksellerle eşleşecek şekilde ayarla
  canvas.style.width = width + 'px'
  canvas.style.height = height + 'px'
  
  // Mantıksal piksellerle çizim yapabilmek için bağlamı ölçekle
  ctx.scale(dpr, dpr)
}

window.addEventListener('resize', resize)
resize()
</code></pre>
<p>İşin püf noktası <code>ctx.scale(dpr, dpr)</code> satırıdır. Bundan sonra (100, 100) konumuna çizim yapabilirsiniz ve DPR değerinden bağımsız olarak doğru yerde görünür. Canvas, 2x bir ekranda dahili olarak 200x200 çözünürlükte görüntü oluşturur, ancak kodunuzun bunu bilmesi gerekmez.</p>
<p>Dikkat edilmesi gereken bir nokta: <code>ctx.scale</code> birikimlidir. Yeniden boyutlandırma işlevini birden çok kez çağırırsanız ölçek giderek büyür. Ölçeklemeden önce <code>ctx.setTransform(1, 0, 0, 1, 0, 0)</code> ile dönüşümü sıfırlayın veya yalnızca bir kez ölçekleyin.</p>
<h2>Letterbox ile Sabit En-Boy Oranı</h2>
<p>Birçok oyun belirli bir en-boy oranına ihtiyaç duyar. 16:9 oyununuzun 21:9 ultra geniş bir monitörü doldurmak için esnetilmesini istemezsiniz. Letterbox, oranı korumak için siyah şeritler ekler.</p>
<pre><code class="language-js">const GAME_WIDTH = 1920
const GAME_HEIGHT = 1080
const ASPECT = GAME_WIDTH / GAME_HEIGHT

function resizeWithLetterbox() {
  const windowWidth = window.innerWidth
  const windowHeight = window.innerHeight
  const windowAspect = windowWidth / windowHeight
  
  let width, height
  
  if (windowAspect &gt; ASPECT) {
    // Pencere oyundan daha genişse yanlara şerit ekle
    height = windowHeight
    width = height * ASPECT
  } else {
    // Pencere oyundan daha uzunsa üste ve alta şerit ekle
    width = windowWidth
    height = width / ASPECT
  }
  
  canvas.style.width = width + 'px'
  canvas.style.height = height + 'px'
  canvas.style.position = 'absolute'
  canvas.style.left = (windowWidth - width) / 2 + 'px'
  canvas.style.top = (windowHeight - height) / 2 + 'px'
  
  // Dahili çözünürlük sabit kalır
  canvas.width = GAME_WIDTH
  canvas.height = GAME_HEIGHT
}
</code></pre>
<p>Letterbox şeritlerinin görünmesi için gövde arka planını siyah yapın:</p>
<pre><code class="language-css">body {
  margin: 0;
  background: #000;
  overflow: hidden;
}
</code></pre>
<h2>Piksel Sanatı İçin Tam Sayılı Ölçekleme</h2>
<p>Piksel sanatı tam sayılı ölçeklerde (1x, 2x, 3x) net, 2.7x gibi kesirli ölçeklerde ise bulanık görünür. <code>image-rendering: pixelated</code> kullanılsa bile 2.5x ölçek, bazı piksellerin 2 ekran pikseli, diğerlerinin ise 3 ekran pikseli genişliğinde olması anlamına gelir.</p>
<pre><code class="language-js">const GAME_WIDTH = 320
const GAME_HEIGHT = 180

function resizePixelPerfect() {
  const maxWidth = window.innerWidth
  const maxHeight = window.innerHeight
  
  // Sığabilecek en büyük tam sayılı ölçeği bul
  const scaleX = Math.floor(maxWidth / GAME_WIDTH)
  const scaleY = Math.floor(maxHeight / GAME_HEIGHT)
  const scale = Math.max(1, Math.min(scaleX, scaleY))
  
  const width = GAME_WIDTH * scale
  const height = GAME_HEIGHT * scale
  
  canvas.width = GAME_WIDTH
  canvas.height = GAME_HEIGHT
  canvas.style.width = width + 'px'
  canvas.style.height = height + 'px'
  canvas.style.imageRendering = 'pixelated'
}
</code></pre>
<p>Bunun karşılığında oyununuz ekranı tam olarak doldurmaz. 320x180 boyutundaki bir oyun 1920x1080 ekranda 5x ölçeklenir (1600x900) ve kenarlarda siyah şeritler oluşur. Bazı oyuncular bunu tercih ederken diğerleri, biraz bulanık olsa bile oyunun ekranı doldurmasını ister.</p>
<h2>WebGL Canvas Ölçekleme</h2>
<p>WebGL için canvas boyutuna ek olarak görünüm alanını da güncellemeniz gerekir:</p>
<pre><code class="language-js">function resizeGL(gl, canvas) {
  const dpr = window.devicePixelRatio || 1
  const width = canvas.clientWidth * dpr
  const height = canvas.clientHeight * dpr
  
  if (canvas.width !== width || canvas.height !== height) {
    canvas.width = width
    canvas.height = height
    gl.viewport(0, 0, width, height)
    return true // Yeniden boyutlandırıldı; projeksiyon matrislerini güncellemeniz gerekebilir
  }
  return false
}
</code></pre>
<p>Bunu yalnızca yeniden boyutlandırma olaylarında değil, görüntü oluşturma döngünüzün başında çağırın. Bazı tarayıcılar bir pencereyi monitörler arasında sürüklediğinizde DPR değerini değiştirir; bunu da yakalamak istersiniz.</p>
<h2>Yeniden Boyutlandırma Olaylarını Seyrekleştirme</h2>
<p>Kullanıcı pencerenin kenarını sürüklerken yeniden boyutlandırma olayları hızla tetiklenir. Yeniden boyutlandırma işleviniz maliyetliyse takılmalar yaşanır.</p>
<pre><code class="language-js">let resizeTimeout
window.addEventListener('resize', () =&gt; {
  clearTimeout(resizeTimeout)
  resizeTimeout = setTimeout(resize, 100)
})
</code></pre>
<p>100 ms'lik gecikme, yeniden boyutlandırmanın yalnızca kullanıcı sürüklemeyi bıraktıktan sonra gerçekleşmesini sağlar. Sürükleme sırasında daha akıcı bir yönetim için bunun yerine ResizeObserver kullanın:</p>
<pre><code class="language-js">const observer = new ResizeObserver(entries =&gt; {
  resize()
})
observer.observe(document.body)
</code></pre>
<p>ResizeObserver daha seyrek tetiklenir ve tarayıcı tarafından daha iyi optimize edilir.</p>
<h2>Tam Ekran</h2>
<p>Fullscreen API, ekranın tamamını kullanmanızı sağlar:</p>
<pre><code class="language-js">async function toggleFullscreen() {
  if (!document.fullscreenElement) {
    await canvas.requestFullscreen()
  } else {
    await document.exitFullscreen()
  }
}

document.addEventListener('fullscreenchange', resize)
</code></pre>
<p><code>requestFullscreen</code> işlevini tıklama gibi bir kullanıcı etkileşimine yanıt olarak çağırmalısınız. Aksi takdirde tarayıcılar bunu engeller. Ayrıca kullanılabilir alan değişeceği için tam ekrana girerken ve tam ekrandan çıkarken yeniden boyutlandırmayı unutmayın.</p>
<p>Platformla ilgili bir sınırlama: 2026 ortası itibarıyla Fullscreen API, iPhone Safari'de öğeleri tam ekrana alma özelliğini hâlâ kapsamıyor. requestFullscreen masaüstü tarayıcılarda ve iPadOS'te çalışır, ancak iPhone'da canvas gibi rastgele öğeler için desteklenmez (yalnızca yerel video tam ekranı çalışır). Desteği <code>if (canvas.requestFullscreen)</code> ile algılayın ve desteklenmediğinde canvas'ı görünür görünüm alanını dolduracak şekilde boyutlandırmak veya kullanıcılardan oyunu PWA olarak ana ekranlarına eklemelerini istemek gibi uygun bir alternatif sunun.</p>
<h2>Her Şeyi Bir Araya Getirme</h2>
<p>Yaygın durumların tümünü yöneten bir sınıf:</p>
<pre><code class="language-js">class ResponsiveCanvas {
  constructor(canvas, options = {}) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.gameWidth = options.width || 1280
    this.gameHeight = options.height || 720
    this.pixelArt = options.pixelArt || false
    this.letterbox = options.letterbox !== false
    
    this.bind()
    this.resize()
  }
  
  bind() {
    window.addEventListener('resize', () =&gt; this.resize())
    document.addEventListener('fullscreenchange', () =&gt; this.resize())
  }
  
  resize() {
    const aspect = this.gameWidth / this.gameHeight
    let width = window.innerWidth
    let height = window.innerHeight
    
    if (this.letterbox) {
      if (width / height &gt; aspect) {
        width = height * aspect
      } else {
        height = width / aspect
      }
    }
    
    if (this.pixelArt) {
      const scale = Math.max(1, Math.floor(Math.min(
        window.innerWidth / this.gameWidth,
        window.innerHeight / this.gameHeight
      )))
      width = this.gameWidth * scale
      height = this.gameHeight * scale
    }
    
    this.canvas.width = this.gameWidth
    this.canvas.height = this.gameHeight
    this.canvas.style.width = width + 'px'
    this.canvas.style.height = height + 'px'
    this.canvas.style.position = 'absolute'
    this.canvas.style.left = (window.innerWidth - width) / 2 + 'px'
    this.canvas.style.top = (window.innerHeight - height) / 2 + 'px'
    
    if (this.pixelArt) {
      this.canvas.style.imageRendering = 'pixelated'
    }
  }
  
  get width() { return this.gameWidth }
  get height() { return this.gameHeight }
}
</code></pre>
<p>Şu şekilde kullanın:</p>
<pre><code class="language-js">const responsive = new ResponsiveCanvas(document.getElementById('game'), {
  width: 320,
  height: 180,
  pixelArt: true,
  letterbox: true
})

// Oyun kodunuzda responsive.width ve responsive.height kullanın
</code></pre>
<h2>Sık Karşılaşılan Sorunlar</h2>
<p>İnsanların sıkça gözden kaçırdığı birkaç nokta:</p>
<p><strong>Canvas yeniden boyutlandırıldığında temizlenir.</strong> <code>canvas.width</code> veya <code>canvas.height</code> değerini ayarladığınızda canvas temizlenir. Her karede yeniden çizim yapmıyorsanız yeniden boyutlandırmadan sonra tekrar çizmeniz gerekir.</p>
<p><strong>DPR değişebilir.</strong> Kullanıcı tarayıcı penceresini Retina bir ekrandan Retina olmayan bir ekrana sürüklerse DPR değişir. Bunu yalnızca yeniden boyutlandırma sırasında değil, görüntü oluşturma döngünüzde de kontrol edin.</p>
<p><strong>iOS Safari görünüm alanı tuhaflıkları.</strong> Adres çubuğu gösterildiğinde veya gizlendiğinde görünüm alanının yüksekliği değişir. Mobil cihazlarda daha doğru boyutlandırma için <code>window.visualViewport</code> kullanın.</p>
<p><strong>Bağlam durumu sıfırlanır.</strong> Canvas boyutlarını ayarlamak; dönüşümler, dolgu stilleri ve <code>imageSmoothingEnabled</code> dâhil olmak üzere bağlam durumunu sıfırlar. Yeniden boyutlandırmadan sonra bunları tekrar uygulayın.</p>
<h2>Daha Fazla Kaynak</h2>
<p><a href="/tr/tutorials/canvas-2d-game-loop">Canvas 2D oyun döngüsü</a>, güncelleme ve görüntü oluşturma döngülerinin temellerini ele alır.</p>
<p><a href="/tr/tutorials/pixel-art-rendering">Piksel sanatı görüntüleme</a>, net piksel grafiklerini daha ayrıntılı olarak inceler.</p>
<p><a href="/tr/tutorials/mobile-friendly-web-games">Mobil uyumlu web oyunları</a>, dokunmatik girişi ve görünüm alanı sorunlarını ele alır.</p>
<p><a href="/tr/tutorials/webgl-fundamentals-for-games">WebGL temelleri</a>, WebGL bağlamları için görünüm alanı ve canvas kurulumunu ele alır.</p>
<p><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlama</a>, performans için canvas boyutunu optimize etmeyi ele alır.</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Oyun varlıklarını önbelleğe almak için service worker'lar]]></title>
            <link>https://app.cinevva.com/tr/tutorials/service-workers-caching</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/service-workers-caching</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Oyun varlıklarını service worker'larla önbelleğe alın: stratejiler, sürümleme ve güncellemeler. Oyununuzun tekrar ziyaretlerde anında yüklenmesini sağlayın.]]></description>
            <content:encoded><![CDATA[<h1>Oyun varlıklarını önbelleğe almak için service worker'lar</h1>
<p>Service worker'lar, varlıkların nasıl önbelleğe alınacağını ve sunulacağını kontrol etmenizi sağlar. Oyunlar için bu, tekrar ziyaretlerde anında yükleme ve çevrimdışı destek anlamına gelir.</p>
<h2>1) Service worker temelleri</h2>
<p>Kök dizininizde <code>sw.js</code> dosyasını oluşturun:</p>
<pre><code class="language-js">// sw.js
self.addEventListener('install', (event) =&gt; {
  console.log('Service worker installing')
})

self.addEventListener('activate', (event) =&gt; {
  console.log('Service worker activated')
})

self.addEventListener('fetch', (event) =&gt; {
  // Intercept all network requests
})
</code></pre>
<p>Kaydedin:</p>
<pre><code class="language-js">if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg =&gt; console.log('SW registered'))
    .catch(err =&gt; console.error('SW registration failed:', err))
}
</code></pre>
<h2>2) Önce önbellek stratejisi (oyun varlıkları için en iyisi)</h2>
<p>Önce önbellekten yükleyin, yalnızca önbellekte yoksa getirin:</p>
<pre><code class="language-js">const CACHE_NAME = 'game-assets-v1'

self.addEventListener('fetch', (event) =&gt; {
  event.respondWith(
    caches.match(event.request).then((cached) =&gt; {
      if (cached) {
        return cached
      }
      
      return fetch(event.request).then((response) =&gt; {
        // Don't cache non-ok responses or non-GET requests
        if (!response.ok || event.request.method !== 'GET') {
          return response
        }
        
        // Clone response for caching
        const clone = response.clone()
        caches.open(CACHE_NAME).then((cache) =&gt; {
          cache.put(event.request, clone)
        })
        
        return response
      })
    })
  )
})
</code></pre>
<h2>3) Kritik varlıkları önceden önbelleğe alma</h2>
<p>Install olayı, önceden önbelleğe alma için idealdir:</p>
<pre><code class="language-js">const CACHE_NAME = 'game-v1'

const PRECACHE = [
  '/',
  '/index.html',
  '/game.js',
  '/style.css',
  '/assets/sprites.png',
  '/assets/tileset.png',
  '/assets/player.png',
]

self.addEventListener('install', (event) =&gt; {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache =&gt; cache.addAll(PRECACHE))
      .then(() =&gt; self.skipWaiting())
  )
})
</code></pre>
<h2>4) Dinamik içerik için önce ağ stratejisi</h2>
<p>Değişen içerikler (liderlik tabloları, oyuncu verileri) için önce ağ stratejisini kullanın:</p>
<pre><code class="language-js">self.addEventListener('fetch', (event) =&gt; {
  const url = new URL(event.request.url)
  
  // API calls: network first
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(event.request))
    return
  }
  
  // Assets: cache first
  event.respondWith(cacheFirst(event.request))
})

async function networkFirst(request) {
  try {
    const response = await fetch(request)
    const cache = await caches.open('api-cache')
    cache.put(request, response.clone())
    return response
  } catch {
    return caches.match(request)
  }
}

async function cacheFirst(request) {
  const cached = await caches.match(request)
  if (cached) return cached
  
  const response = await fetch(request)
  const cache = await caches.open(CACHE_NAME)
  cache.put(request, response.clone())
  return response
}
</code></pre>
<h2>5) Eski içeriği sunup arka planda yenileme</h2>
<p>Önbellekteki içeriği hemen döndürün, arka planda güncelleyin:</p>
<pre><code class="language-js">async function staleWhileRevalidate(request) {
  const cache = await caches.open(CACHE_NAME)
  const cached = await cache.match(request)
  
  const fetchPromise = fetch(request).then((response) =&gt; {
    if (response.ok) {
      cache.put(request, response.clone())
    }
    return response
  })
  
  return cached || fetchPromise
}
</code></pre>
<h2>6) Önbellek sürümleme ve temizleme</h2>
<p>Activate sırasında eski önbellekleri temizleyin:</p>
<pre><code class="language-js">const CACHE_VERSION = 'v2'
const CACHE_NAME = `game-${CACHE_VERSION}`

self.addEventListener('activate', (event) =&gt; {
  event.waitUntil(
    caches.keys().then((keys) =&gt; {
      return Promise.all(
        keys
          .filter(key =&gt; key.startsWith('game-') &amp;&amp; key !== CACHE_NAME)
          .map(key =&gt; {
            console.log('Deleting old cache:', key)
            return caches.delete(key)
          })
      )
    }).then(() =&gt; self.clients.claim())
  )
})
</code></pre>
<h2>7) Önbellekleri türe göre ayırma</h2>
<p>Daha iyi yönetim için önbellekleri düzenleyin:</p>
<pre><code class="language-js">const CACHES = {
  static: 'static-v1',    // HTML, JS, CSS
  images: 'images-v1',    // Sprites, textures
  audio: 'audio-v1',      // Music, SFX
  api: 'api-v1',          // API responses
}

self.addEventListener('fetch', (event) =&gt; {
  const url = new URL(event.request.url)
  
  if (url.pathname.match(/\.(png|jpg|webp)$/)) {
    event.respondWith(cacheFirst(event.request, CACHES.images))
  } else if (url.pathname.match(/\.(mp3|ogg|wav)$/)) {
    event.respondWith(cacheFirst(event.request, CACHES.audio))
  } else if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(event.request, CACHES.api))
  } else {
    event.respondWith(cacheFirst(event.request, CACHES.static))
  }
})
</code></pre>
<h2>8) Ses/video için aralık istekleri</h2>
<p>Baştan bilmeniz gereken bir nokta var: Cache API, kısmi yanıtları saklamaz. Bir 206 Partial Content yanıtı <code>cache.put()</code> işlevine ulaşırsa <code>TypeError: Failed to execute 'put' on 'Cache': Partial response (status code 206) is unsupported</code> hatasını verir. Bu nedenle önbelleğe alınmış medya için kalıp her zaman aynıdır: dosyanın tamamını normal bir 200 yanıtı olarak önceden önbelleğe alın (veya getirin), ardından Range başlığı içeren bir istek geldiğinde önbellekteki baytları dilimleyerek 206 yanıtını kendiniz oluşturun. Bunu kendiniz uygulamak istemiyorsanız Workbox'ın range-requests eklentisi, CacheFirst stratejisiyle eşleştirildiğinde bu dilimleme işlemini sizin için yapar.</p>
<p>Aralık isteklerini doğru şekilde işleyin (ses içinde ileri veya geri sarma için gereklidir):</p>
<pre><code class="language-js">self.addEventListener('fetch', (event) =&gt; {
  if (event.request.headers.has('range')) {
    event.respondWith(handleRangeRequest(event.request))
    return
  }
  // ... normal handling
})

async function handleRangeRequest(request) {
  const cache = await caches.open(CACHES.audio)
  // Match the full resource, ignoring the Range header (which would never match a cached full file)
  let cached = await cache.match(request.url)

  if (!cached) {
    // Fetch the FULL file (string URL drops the Range header) and cache it as a 200
    cached = await fetch(request.url)
    if (cached.ok) cache.put(request.url, cached.clone()) // cache.put rejects 206, so only store full 200s
  }

  // Build a 206 Partial Content response from the cached full body
  const rangeHeader = request.headers.get('range') // e.g. &quot;bytes=200-1000&quot;
  const buffer = await cached.arrayBuffer()
  const total = buffer.byteLength
  const [startStr, endStr] = rangeHeader.replace(/bytes=/, '').split('-')
  const start = Number(startStr)
  const end = endStr ? Number(endStr) : total - 1
  const slice = buffer.slice(start, end + 1)

  return new Response(slice, {
    status: 206,
    statusText: 'Partial Content',
    headers: {
      'Content-Type': cached.headers.get('Content-Type') || 'application/octet-stream',
      'Content-Range': `bytes ${start}-${end}/${total}`,
      'Content-Length': String(slice.byteLength),
      'Accept-Ranges': 'bytes',
    },
  })
}
</code></pre>
<h2>9) Güncelleme bildirimi</h2>
<p>Yeni bir sürüm hazır olduğunda oyunculara bildirin:</p>
<pre><code class="language-js">// In main app
let refreshing = false

navigator.serviceWorker.addEventListener('controllerchange', () =&gt; {
  if (refreshing) return
  refreshing = true
  
  showUpdateBanner()
})

function showUpdateBanner() {
  const banner = document.createElement('div')
  banner.className = 'update-banner'
  banner.innerHTML = `
    &lt;span&gt;Yeni sürüm hazır!&lt;/span&gt;
    &lt;button onclick=&quot;location.reload()&quot;&gt;Güncelle&lt;/button&gt;
  `
  document.body.appendChild(banner)
}
</code></pre>
<h2>10) Eksiksiz oyun service worker'ı</h2>
<pre><code class="language-js">const VERSION = 'v1'
const STATIC_CACHE = `static-${VERSION}`
const ASSET_CACHE = `assets-${VERSION}`

const PRECACHE_URLS = [
  '/',
  '/index.html',
  '/game.js',
  '/style.css',
]

// Install: precache critical resources
self.addEventListener('install', (event) =&gt; {
  event.waitUntil(
    caches.open(STATIC_CACHE)
      .then(cache =&gt; cache.addAll(PRECACHE_URLS))
      .then(() =&gt; self.skipWaiting())
  )
})

// Activate: clean old caches
self.addEventListener('activate', (event) =&gt; {
  event.waitUntil(
    caches.keys().then(keys =&gt; Promise.all(
      keys
        .filter(key =&gt; !key.endsWith(VERSION))
        .map(key =&gt; caches.delete(key))
    )).then(() =&gt; self.clients.claim())
  )
})

// Fetch: route requests to appropriate strategy
self.addEventListener('fetch', (event) =&gt; {
  const url = new URL(event.request.url)
  
  // Same-origin only
  if (url.origin !== location.origin) {
    return
  }
  
  // API: network first with cache fallback
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(event.request, 'api-cache'))
    return
  }
  
  // Assets: cache first
  if (url.pathname.startsWith('/assets/')) {
    event.respondWith(cacheFirst(event.request, ASSET_CACHE))
    return
  }
  
  // HTML/JS/CSS: stale while revalidate
  event.respondWith(staleWhileRevalidate(event.request, STATIC_CACHE))
})

async function cacheFirst(request, cacheName) {
  const cached = await caches.match(request)
  if (cached) return cached
  
  try {
    const response = await fetch(request)
    if (response.ok) {
      const cache = await caches.open(cacheName)
      cache.put(request, response.clone())
    }
    return response
  } catch {
    return new Response('Çevrimdışı', { status: 503 })
  }
}

async function networkFirst(request, cacheName) {
  try {
    const response = await fetch(request)
    if (response.ok) {
      const cache = await caches.open(cacheName)
      cache.put(request, response.clone())
    }
    return response
  } catch {
    return caches.match(request) || new Response('Çevrimdışı', { status: 503 })
  }
}

async function staleWhileRevalidate(request, cacheName) {
  const cache = await caches.open(cacheName)
  const cached = await cache.match(request)
  
  const fetchPromise = fetch(request).then(response =&gt; {
    if (response.ok) {
      cache.put(request, response.clone())
    }
    return response
  }).catch(() =&gt; cached)
  
  return cached || fetchPromise
}
</code></pre>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/pwa-offline-games">Çevrimdışı oyunlar için PWA</a></li>
<li><a href="/tr/tutorials/streaming-asset-loading">Varlıkları akışla yükleme</a></li>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlama</a></li>
<li><a href="/tr/tutorials/web-audio-api-games">Oyunlar için Web Audio API</a> — çevrimdışı oyun için ses varlıklarını önbelleğe alma</li>
<li><a href="/tr/tutorials/indexeddb-game-saves">IndexedDB ile oyun kayıtları</a> — oyun durumunu önbelleğe alınmış varlıklarla birlikte kalıcı olarak saklama</li>
</ul>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">MDN: Service Worker API</a> — eksiksiz API referansı</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Cache">MDN: Cache API</a> — Cache arayüzü referansı</li>
<li><a href="https://web.dev/learn/pwa/service-workers/">web.dev: Service worker'lar</a> — önbelleğe alma stratejileri ve kalıpları</li>
<li><a href="https://developer.chrome.com/docs/workbox/">Workbox</a> — Google'ın service worker araçları için geliştirdiği kütüphane</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Hızlı yüklenen bir web oyunu yayınlayın (pratik kontrol listesi)]]></title>
            <link>https://app.cinevva.com/tr/tutorials/ship-web-game-fast</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/ship-web-game-fast</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Web oyunlarının hızla yüklenmesini sağlamak için pragmatik, adım adım bir kontrol listesi: bütçeler, sıkıştırma, önbelleğe alma, varlık biçimleri ve performans ölçümleri.]]></description>
            <content:encoded><![CDATA[<h1>Hızlı yüklenen bir web oyunu yayınlayın (pratik kontrol listesi)</h1>
<p>Oyuncular <strong>saniyeler içinde oynamaya başlayamazsa</strong> oyundan çıkar. Bu eğitim, neredeyse her web oyunu teknoloji yığınına (Canvas/WebGL/WebGPU, Pixi/Three/PlayCanvas, özel motor vb.) uygulayabileceğiniz pratik bir kontrol listesidir.</p>
<h2>1) Önce bütçeleri belirleyin (aksi hâlde optimizasyon yapamazsınız)</h2>
<p>Hedef cihazınıza uygun bütçeler seçin. “Anında oyna” deneyimi için iyi bir varsayılan:</p>
<ul>
<li><strong>Gönderilen JS+CSS</strong>: gzip ile ~300–500 KB'ın altında (ne kadar küçükse o kadar iyi)</li>
<li><strong>İlk girdiden önce gereken kritik varlıklar</strong>: sıkıştırılmış olarak ~2–5 MB'ın altında</li>
<li><strong>İlk girdiye kadar geçen süre</strong>: orta sınıf mobil cihazlarda ~3–5 saniyenin altında</li>
</ul>
<p>Bunları yazılı hâle getirin ve API sözleşmeleri gibi ele alın.</p>
<h2>2) Doğru şeyleri ölçün</h2>
<p>Chrome DevTools'ta:</p>
<ul>
<li>Bir oturumu kaydetmek ve uzun görevleri incelemek için <strong>Performance</strong>'ı kullanın.</li>
<li>Şunları kontrol etmek için <strong>Network</strong>'ü kullanın:
<ul>
<li>aktarım boyutu ile açılmış boyut</li>
<li>önbelleğe alma üstbilgileri</li>
<li>varlıkların aşamalı olarak akışla mı geldiği, yoksa tek bir dev veri bloğu olarak mı ulaştığı</li>
</ul>
</li>
</ul>
<p>Oyun açısından şunları takip edin:</p>
<ul>
<li>“<strong>Oyuncu hareket edebiliyor mu?</strong>” süresi</li>
<li>“<strong>İlk anlamlı kare</strong>” süresi</li>
<li>“<strong>İlk girdi gecikmesi</strong>” (girdi → kare)</li>
</ul>
<p>“İlk girdi gecikmesi” yaklaşımınız artık standart bir metrikle örtüşüyor: <strong>Interaction to Next Paint (INP)</strong>, Mart 2024'te Core Web Vital olarak First Input Delay'in yerini aldı ve yalnızca ilk etkileşimi değil, tüm etkileşimlerde girdiden sonraki çizime kadar geçen sürenin tamamını ölçüyor. Zaten takip ettiğiniz tepki hızı değerleri için yararlı bir harici referans olan 75. yüzdelik dilimde 200 ms'nin altında INP'yi hedefleyin.</p>
<h2>3) Sıkıştırabildiğiniz her şeyi sıkıştırın</h2>
<p>Barındırma hizmetinizin şunları sunduğundan emin olun:</p>
<ul>
<li>Kullanılabilir olduğunda metin varlıkları (JS/CSS/HTML/WASM) için <strong>Brotli</strong> (<code>br</code>)</li>
<li>Yedek seçenek olarak <strong>gzip</strong></li>
</ul>
<p>Barındırma hizmetiniz ve CDN'iniz destekliyorsa <strong>Zstandard</strong> (<code>zstd</code>) artık <code>Content-Encoding</code> için üçüncü bir seçenektir. Chrome ve Edge 123+, Firefox 126+ ve Safari 26.3+ sürümlerinde (kullanıcıların yaklaşık %80'inde) desteklenir ve daha düşük sunucu CPU maliyetiyle Brotli seviyesinde sıkıştırma oranlarına ulaşabilir. Bu da büyük WASM ve JSON yükleri için faydalıdır. Tarayıcılar <code>Accept-Encoding: zstd</code> desteğini yalnızca HTTPS üzerinden bildirir; bu nedenle diğer herkes için Brotli ve gzip'i yedek seçenekler olarak yapılandırılmış hâlde tutun.</p>
<p>WebAssembly derlemelerinde sıkıştırma, çoğu zaman “anında açılıyor” ile “hiç yüklenmiyor” arasındaki farktır.</p>
<h2>4) Tüm dünyayı en başta indirmeyin</h2>
<p>Oyununuzu şu bölümlere ayırın:</p>
<ul>
<li><strong>Başlatma</strong>: minimum yükleyici + girdi + ilk sahne</li>
<li><strong>Çekirdek</strong>: ana oynanış sistemleri</li>
<li><strong>İsteğe bağlı</strong>: ek bölümler, kozmetik öğeler, yüksek çözünürlüklü dokular, bonus sesler vb.</li>
</ul>
<p>“İsteğe bağlı” içeriği yalnızca şu durumlarda yükleyin:</p>
<ul>
<li>oyuncu zaten oynayabiliyorsa ve/veya</li>
<li>bu içeriği istediğini biliyorsanız (ör. bir menüye/bölüme ulaştıysa)</li>
</ul>
<h2>5) Doğru varlık biçimlerini seçin</h2>
<p>Çoğu web oyunu için:</p>
<ul>
<li><strong>Görseller</strong>: Arayüzler/arka planlar için <strong>WebP</strong>'yi (veya daha yavaş kodlamayı tolere edebiliyorsanız <strong>AVIF</strong>'i) tercih edin.</li>
<li><strong>Ses</strong>: Genel kullanım için <strong>Ogg Vorbis</strong>'i tercih edin; Apple ekosistemleri için <strong>AAC</strong>'yi test edin.</li>
<li><strong>Video</strong>: Uyumluluk için <strong>MP4/H.264</strong>'ü, desteklendiğinde ve daha küçük olduğunda <strong>WebM</strong>'yi tercih edin.</li>
</ul>
<p>Kod çözme maliyetini göz önünde bulundurun: “daha küçük indirme”, yine de “daha yavaş kod çözme” anlamına gelebilir.</p>
<h2>6) İlk sahnenizi küçük ve deterministik tutun</h2>
<p>İlk etkileşimli sahnenizde şunlardan kaçının:</p>
<ul>
<li>ani ve yoğun gölgelendirici derlemeleri</li>
<li>çok büyük doku yüklemeleri</li>
<li>ana iş parçacığını engelleyen prosedürel üretim</li>
<li>megabaytlarca veri bloğunun senkron JSON ayrıştırması</li>
</ul>
<p>Özellikle dokular için PNG veya JPEG yerine <strong>Basis Universal ile KTX2</strong> süper sıkıştırmasını kullanın. Yüklemeden sonra GPU üzerinde sıkıştırılmış hâlde kalır (yükleme sırasında masaüstünde BC7'ye veya mobilde ASTC'ye dönüştürülür); böylece VRAM kullanımı 4 ila 8 kat azalır ve yüklemeler, tam boyutlu bir PNG'nin kodunu çözüp ham pikselleri GPU'ya aktarmaktan çok daha düşük maliyetli olur.</p>
<p>Ağır işlemler yapmanız gerekiyorsa bunları:</p>
<ul>
<li>karelere yayarak artımlı biçimde veya</li>
<li>mümkün olduğunda bir Worker içinde gerçekleştirin.</li>
</ul>
<h2>7) Agresif ama doğru biçimde önbelleğe alın</h2>
<p>İçerik adresli varlıklar için uzun süreli önbelleğe alma kullanın:</p>
<ul>
<li><code>Cache-Control: public, max-age=31536000, immutable</code></li>
</ul>
<p>Güncellemeleri güvenle dağıtabilmek için HTML giriş noktalarının önbellek süresini kısa tutun.</p>
<h2>8) “Hatanın” hızlı ve anlaşılır olmasını sağlayın</h2>
<p>Bir sorun oluşursa şunları gösterin:</p>
<ul>
<li>açık bir hata mesajı</li>
<li>yeniden deneme düğmesi</li>
<li>sorunu bildirmek için bir bağlantı</li>
</ul>
<p>Sessiz hatalar güveni yok eder.</p>
<h2>9) Wasm iş parçacıkları kullanıyorsanız COOP/COEP'i anlayın</h2>
<p>Derlemeniz <code>SharedArrayBuffer</code>'a bağlıysa (bazı Wasm iş parçacığı kurulumlarında yaygındır), büyük olasılıkla <strong>çapraz kaynak yalıtımı</strong> üstbilgilerine ihtiyacınız olacaktır:</p>
<ul>
<li><code>Cross-Origin-Opener-Policy: same-origin</code></li>
<li><code>Cross-Origin-Embedder-Policy: require-corp</code></li>
</ul>
<p>COEP üstbilgisi için iki seçeneğiniz vardır. <code>require-corp</code> katı seçenektir ancak kaynaklar arası her varlığın (CDN dokuları, üçüncü taraf betikleri) bir <code>Cross-Origin-Resource-Policy</code> üstbilgisi göndermesi gerekir; aksi takdirde varlık engellenir. <code>Cross-Origin-Embedder-Policy: credentialless</code> da <code>SharedArrayBuffer</code> ve çapraz kaynak yalıtımını etkinleştirir ancak kaynaklar arası varlıkların CORP ile açıkça izin vermesini zorunlu tutmak yerine bunları kimlik bilgileri olmadan yükler. Kontrolünüzde olmayan bir CDN'den varlık alıyorsanız <code>credentialless</code> genellikle daha az zahmetli seçenektir.</p>
<p>Üstbilgiler ve üçüncü taraf yerleştirmeleri sorunlara yol açabileceğinden gerçek barındırma ortamınızda erkenden test yapın.</p>
<h2>10) SDK olmadan oynanabilir hâle getirin</h2>
<p>Cinevva'nın içerik üreticisi iş akışını hedefliyorsanız şunları amaçlayın:</p>
<ul>
<li>güvenilir biçimde açılan tek bir URL</li>
<li>demoyu başlatmak için zorunlu giriş olmaması</li>
<li>öngörülebilir girdi kontrolleri</li>
</ul>
<p>Böylece ek SDK'lar entegre etmeden dağıtıma hazır olursunuz.</p>
<p>İlgili içerikler:</p>
<ul>
<li><a href="/tr/creators">Oyun geliştiricileri için</a></li>
<li><a href="/tr/ai-content">Yapay zekâyla üretilen içerik politikası</a></li>
<li><a href="/tr/tutorials/streaming-asset-loading">Akışla varlık yükleme</a> — büyük oyunlar için aşamalı yükleme kalıpları</li>
<li><a href="/tr/tutorials/service-workers-caching">Oyunları önbelleğe almak için service worker'lar</a> — sonraki ziyaretlerin anında açılması için varlıkları önbelleğe alın</li>
<li><a href="/tr/guides/web-game-engines-comparison">Web Oyunu Motorları Karşılaştırması</a> — motorlar arasında derleme boyutları ve yükleme süreleri</li>
<li><a href="/tr/guides/itch-io-launch-guide">Oyununuzu itch.io'da Nasıl Yayınlarsınız?</a> — tarayıcı oyunlarını <a href="http://itch.io">itch.io</a> yerleştirmeleri için optimize etme</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Web oyunları için akışlı varlık yükleme]]></title>
            <link>https://app.cinevva.com/tr/tutorials/streaming-asset-loading</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/streaming-asset-loading</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Oyun varlıklarını aşamalı olarak yükleyin: önceliklendirme, akış, önbelleğe alma ve ilerleme gösterimi. Yükleme sırasında oyuncuların ilgisini koruyun.]]></description>
            <content:encoded><![CDATA[<h1>Web oyunları için akışlı varlık yükleme</h1>
<p>Oyuncular 100 MB'lık bir indirmeyi beklemez. Varlıkları aşamalı olarak yükleyin; böylece geri kalanı arka planda yüklenirken oyuncular oynamaya başlayabilir.</p>
<h2>1) Yükleme stratejisi</h2>
<p>Varlıkları katmanlara ayırın:</p>
<ol>
<li><strong>Kritik</strong> — İlk ekranı göstermek için gerekli (&lt; 1 MB)</li>
<li><strong>Oynanış</strong> — Oynamak için gerekli (&lt; 10 MB)</li>
<li><strong>İyileştirme</strong> — Olması faydalı olanlar (arka planda yüklenir)</li>
</ol>
<h2>2) Temel varlık yükleyici</h2>
<pre><code class="language-js">class AssetLoader {
  constructor() {
    this.cache = new Map()
    this.loading = new Map()
  }
  
  async loadImage(url) {
    if (this.cache.has(url)) return this.cache.get(url)
    if (this.loading.has(url)) return this.loading.get(url)
    
    const promise = new Promise((resolve, reject) =&gt; {
      const img = new Image()
      img.onload = () =&gt; {
        this.cache.set(url, img)
        this.loading.delete(url)
        resolve(img)
      }
      img.onerror = reject
      img.src = url
    })
    
    this.loading.set(url, promise)
    return promise
  }
  
  async loadJSON(url) {
    if (this.cache.has(url)) return this.cache.get(url)
    
    const response = await fetch(url)
    const data = await response.json()
    this.cache.set(url, data)
    return data
  }
  
  async loadAudio(url, audioCtx) {
    if (this.cache.has(url)) return this.cache.get(url)
    
    const response = await fetch(url)
    const buffer = await response.arrayBuffer()
    const audioBuffer = await audioCtx.decodeAudioData(buffer)
    this.cache.set(url, audioBuffer)
    return audioBuffer
  }
}
</code></pre>
<h2>3) İlerleme göstergesiyle yükleme</h2>
<pre><code class="language-js">async function loadWithProgress(url, onProgress) {
  const response = await fetch(url)
  const contentLength = response.headers.get('Content-Length')
  const total = parseInt(contentLength, 10)
  
  const reader = response.body.getReader()
  const chunks = []
  let received = 0
  
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    
    chunks.push(value)
    received += value.length
    onProgress(received / total)
  }
  
  const blob = new Blob(chunks)
  return blob
}
</code></pre>
<p>Dikkat: Sunucu yanıtı sıkıştırdığında (çoğu CDN'in varsayılan olarak yaptığı gibi gzip veya brotli ile), <code>Content-Length</code> sıkıştırılmış boyutu belirtirken okuduğunuz parçaların sıkıştırması zaten açılmış olur. Bu nedenle <code>received / total</code> değeri %100'ü aşar. Sıkıştırılmış yanıtlarda bayt düzeyinde doğru bir ilerleme çubuğu elde etmek için varlığı sıkıştırmadan sunun, sıkıştırılmamış boyutu belirten kendi üstbilginizi gönderin veya gerçek aktarım ilerlemesini bildiren <code>XMLHttpRequest</code>'in <code>onprogress</code> olayını kullanın.</p>
<h2>4) Genel ilerleme göstergesiyle toplu yükleme</h2>
<pre><code class="language-js">async function loadAssets(manifest, onProgress) {
  const total = manifest.length
  let completed = 0
  const results = {}
  
  const promises = manifest.map(async (item) =&gt; {
    const asset = await loadAsset(item.url, item.type)
    results[item.name] = asset
    completed++
    onProgress(completed / total, item.name)
  })
  
  await Promise.all(promises)
  return results
}

// Usage
const manifest = [
  { name: 'player', url: 'player.png', type: 'image' },
  { name: 'level1', url: 'level1.json', type: 'json' },
  { name: 'music', url: 'music.mp3', type: 'audio' },
]

const assets = await loadAssets(manifest, (progress, name) =&gt; {
  console.log(`Loading: ${Math.round(progress * 100)}% (${name})`)
})
</code></pre>
<h2>5) Öncelik kuyruklu yükleyici</h2>
<pre><code class="language-js">class PriorityLoader {
  constructor(concurrency = 4) {
    this.queue = []
    this.active = 0
    this.concurrency = concurrency
  }
  
  add(url, priority = 0) {
    return new Promise((resolve, reject) =&gt; {
      this.queue.push({ url, priority, resolve, reject })
      this.queue.sort((a, b) =&gt; b.priority - a.priority)
      this.process()
    })
  }
  
  async process() {
    if (this.active &gt;= this.concurrency || this.queue.length === 0) return
    
    this.active++
    const { url, resolve, reject } = this.queue.shift()
    
    try {
      const response = await fetch(url)
      const blob = await response.blob()
      resolve(blob)
    } catch (err) {
      reject(err)
    }
    
    this.active--
    this.process()
  }
}

// Usage
const loader = new PriorityLoader()
loader.add('critical.png', 10)  // Load first
loader.add('optional.png', 1)   // Load later
</code></pre>
<h2>6) Bölümler için tembel yükleme</h2>
<pre><code class="language-js">class LevelManager {
  constructor(loader) {
    this.loader = loader
    this.levels = new Map()
  }
  
  async preload(levelId) {
    if (this.levels.has(levelId)) return
    
    const manifest = await this.loader.loadJSON(`levels/${levelId}/manifest.json`)
    const assets = await this.loadLevelAssets(manifest)
    this.levels.set(levelId, { manifest, assets })
  }
  
  async loadLevelAssets(manifest) {
    // Load only what's needed for this level
    const assets = {}
    
    for (const texture of manifest.textures) {
      assets[texture.name] = await this.loader.loadImage(texture.url)
    }
    
    return assets
  }
  
  unload(levelId) {
    this.levels.delete(levelId)
    // Assets can be garbage collected
  }
}
</code></pre>
<h2>7) Büyük dosyaları akışla yükleme</h2>
<p>Büyük dosyaları (3D modeller, sesler) akışla yükleyip aşamalı olarak işleyin:</p>
<pre><code class="language-js">async function streamLargeFile(url, onChunk) {
  const response = await fetch(url)
  const reader = response.body.getReader()
  
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    onChunk(value)
  }
}

// For audio: use Media Source Extensions
// For 3D: process mesh data as it arrives
</code></pre>
<h2>8) Yüklenen varlıkları önbelleğe alma</h2>
<p>Kalıcı önbellekleme için IndexedDB ile birleştirin:</p>
<pre><code class="language-js">class CachedLoader {
  constructor() {
    this.memCache = new Map()
    this.dbName = 'AssetCache'
  }
  
  async loadImage(url) {
    // Check memory
    if (this.memCache.has(url)) return this.memCache.get(url)
    
    // Check IndexedDB
    const cached = await this.getFromDB(url)
    if (cached) {
      const img = await this.blobToImage(cached)
      this.memCache.set(url, img)
      return img
    }
    
    // Fetch and cache
    const response = await fetch(url)
    const blob = await response.blob()
    await this.saveToDB(url, blob)
    
    const img = await this.blobToImage(blob)
    this.memCache.set(url, img)
    return img
  }
  
  blobToImage(blob) {
    return new Promise((resolve) =&gt; {
      const img = new Image()
      img.onload = () =&gt; {
        URL.revokeObjectURL(img.src)
        resolve(img)
      }
      img.src = URL.createObjectURL(blob)
    })
  }
  
  // IndexedDB methods...
}
</code></pre>
<h2>9) Yükleme ekranı kalıbı</h2>
<pre><code class="language-js">class LoadingScreen {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.progress = 0
    this.message = 'Yükleniyor...'
  }
  
  update(progress, message) {
    this.progress = progress
    this.message = message || this.message
    this.render()
  }
  
  render() {
    const { ctx, canvas } = this
    ctx.fillStyle = '#1a1a2e'
    ctx.fillRect(0, 0, canvas.width, canvas.height)
    
    // Progress bar
    const barWidth = canvas.width * 0.6
    const barHeight = 20
    const x = (canvas.width - barWidth) / 2
    const y = canvas.height / 2
    
    ctx.fillStyle = '#333'
    ctx.fillRect(x, y, barWidth, barHeight)
    
    ctx.fillStyle = '#4ade80'
    ctx.fillRect(x, y, barWidth * this.progress, barHeight)
    
    // Text
    ctx.fillStyle = '#fff'
    ctx.font = '16px sans-serif'
    ctx.textAlign = 'center'
    ctx.fillText(this.message, canvas.width / 2, y - 20)
    ctx.fillText(`${Math.round(this.progress * 100)}%`, canvas.width / 2, y + 50)
  }
}
</code></pre>
<h2>10) En iyi uygulamalar</h2>
<ul>
<li><strong>Hemen bir şey gösterin</strong> — statik bir görsel bile olabilir</li>
<li><strong>Önce görünür olanları yükleyin</strong> — mevcut görünümün dokuları</li>
<li><strong>Yer tutucular kullanın</strong> — sonradan yüksek çözünürlüklü sürümleriyle değiştirilen düşük çözünürlüklü görseller</li>
<li><strong>Sonraki bölümü önceden getirin</strong> — oyuncu hâlâ oynarken</li>
<li><strong>Hataları sorunsuz yönetin</strong> — yeniden deneme mantığı ve yedek seçenekler</li>
</ul>
<pre><code class="language-js">async function loadWithRetry(url, maxRetries = 3) {
  for (let i = 0; i &lt; maxRetries; i++) {
    try {
      return await fetch(url)
    } catch (err) {
      if (i === maxRetries - 1) throw err
      await new Promise(r =&gt; setTimeout(r, 1000 * (i + 1)))
    }
  }
}
</code></pre>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlayın</a></li>
<li><a href="/tr/tutorials/service-workers-caching">Oyun önbelleklemesi için service worker'lar</a></li>
<li><a href="/tr/tutorials/indexeddb-game-saves">IndexedDB ile oyun kayıtları</a></li>
<li><a href="/tr/tutorials/web-workers-game-logic">Oyun mantığı için Web Worker'lar</a> — varlıkların kodunu ana iş parçacığı dışında çözüp işleyin</li>
<li><a href="/tr/guides/game-assets-guide">Ücretsiz oyun varlıkları nerede bulunur?</a> — akışla yükleyebileceğiniz 3D modeller, dokular ve sesler için kaynaklar</li>
</ul>
<h2>Harici kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">MDN: Fetch API</a> — yanıtları akışla alma ve ReadableStream</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream">MDN: ReadableStream</a> — akışla gelen veri parçalarını işleme</li>
<li><a href="https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html">glTF 2.0 teknik şartnamesi</a> — 3D varlıkları akışla yüklemek için standart biçim</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Web oyunlarında metin oluşturma]]></title>
            <link>https://app.cinevva.com/tr/tutorials/text-rendering-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/text-rendering-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Oyunlarda metin oluşturma: Canvas metni, bitmap yazı tipleri, SDF yazı tipleri ve performans. Basit etiketlerden diyalog sistemlerine.]]></description>
            <content:encoded><![CDATA[<h1>Web oyunlarında metin oluşturma</h1>
<p>Metinler oyunların her yerindedir: skorlar, diyaloglar, menüler ve hasar sayıları. Bu eğitimde farklı yaklaşımları ve her birinin ne zaman kullanılacağını ele alacağız.</p>
<h2>1) Canvas metninin temelleri</h2>
<p>En basit yaklaşım:</p>
<pre><code class="language-js">ctx.font = '24px Arial'
ctx.fillStyle = '#ffffff'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText('Score: 1000', canvas.width / 2, 50)
</code></pre>
<p>Başlıklar ve HUD etiketleri için daha dar veya geniş aralıklara ihtiyacınız varsa Canvas bağlamı artık <code>letterSpacing</code> ve <code>wordSpacing</code> özelliklerini doğrudan destekliyor. Her ikisi de CSS uzunluk dizeleri kabul eder ve 2025'te tarayıcılar arası Baseline desteğine ulaştı. Bu nedenle karakter aralığını taklit etmek için glifleri artık tek tek çizmeniz gerekmez:</p>
<pre><code class="language-js">ctx.font = '24px sans-serif'
ctx.letterSpacing = '2px'
ctx.wordSpacing = '6px'
ctx.fillText('GAME OVER', 100, 100)
</code></pre>
<p>Bu değerler <code>font</code> ve <code>fillStyle</code> gibi bağlamda kalıcı olduğundan işiniz bittiğinde ikisini de <code>'0px'</code> değerine sıfırlayın.</p>
<h2>2) Konturlu metin</h2>
<pre><code class="language-js">function drawTextWithOutline(ctx, text, x, y, fillColor, outlineColor, outlineWidth = 2) {
  ctx.font = '24px Arial'
  ctx.textAlign = 'center'
  ctx.textBaseline = 'middle'
  
  ctx.strokeStyle = outlineColor
  ctx.lineWidth = outlineWidth
  ctx.strokeText(text, x, y)
  
  ctx.fillStyle = fillColor
  ctx.fillText(text, x, y)
}

drawTextWithOutline(ctx, 'GAME OVER', 400, 300, '#fff', '#000', 3)
</code></pre>
<h2>3) Gölgeli metin</h2>
<pre><code class="language-js">function drawTextWithShadow(ctx, text, x, y, color, shadowOffset = 2) {
  ctx.font = '32px sans-serif'
  ctx.textAlign = 'left'
  ctx.textBaseline = 'top'
  
  // Shadow
  ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
  ctx.fillText(text, x + shadowOffset, y + shadowOffset)
  
  // Main text
  ctx.fillStyle = color
  ctx.fillText(text, x, y)
}
</code></pre>
<h2>4) Bitmap yazı tipi oluşturma</h2>
<p>Piksel sanatlı oyunlar veya platformlar arasında tutarlı bir görünüm için:</p>
<pre><code class="language-js">class BitmapFont {
  constructor(image, charWidth, charHeight, chars) {
    this.image = image
    this.charWidth = charWidth
    this.charHeight = charHeight
    this.chars = chars
  }
  
  draw(ctx, text, x, y, scale = 1) {
    const w = this.charWidth * scale
    const h = this.charHeight * scale
    
    for (let i = 0; i &lt; text.length; i++) {
      const charIndex = this.chars.indexOf(text[i].toUpperCase())
      if (charIndex === -1) continue
      
      const sx = (charIndex % 16) * this.charWidth
      const sy = Math.floor(charIndex / 16) * this.charHeight
      
      ctx.drawImage(
        this.image,
        sx, sy, this.charWidth, this.charHeight,
        Math.round(x + i * w), Math.round(y), w, h
      )
    }
  }
  
  measureText(text, scale = 1) {
    return text.length * this.charWidth * scale
  }
}

// Usage
const font = new BitmapFont(fontImage, 8, 8, 
  'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+:; '
)
font.draw(ctx, 'SCORE: 1000', 10, 10, 2)
</code></pre>
<h2>5) Performans için önceden oluşturulmuş metin</h2>
<p>Sabit metinleri bir kez oluşturup yeniden kullanın:</p>
<pre><code class="language-js">function createTextImage(text, font, color) {
  const tempCanvas = document.createElement('canvas')
  const tempCtx = tempCanvas.getContext('2d')
  
  tempCtx.font = font
  const metrics = tempCtx.measureText(text)
  
  tempCanvas.width = Math.ceil(metrics.width)
  tempCanvas.height = parseInt(font) * 1.5
  
  tempCtx.font = font
  tempCtx.fillStyle = color
  tempCtx.textBaseline = 'top'
  tempCtx.fillText(text, 0, 0)
  
  return tempCanvas
}

// Create once
const titleImage = createTextImage('MY GAME', '48px Impact', '#fff')

// Draw many times (fast)
ctx.drawImage(titleImage, x, y)
</code></pre>
<h2>6) Daktilo efekti</h2>
<pre><code class="language-js">class TypewriterText {
  constructor(text, x, y, charsPerSecond = 20) {
    this.fullText = text
    this.x = x
    this.y = y
    this.speed = 1 / charsPerSecond
    this.timer = 0
    this.charIndex = 0
  }
  
  update(dt) {
    this.timer += dt
    while (this.timer &gt;= this.speed &amp;&amp; this.charIndex &lt; this.fullText.length) {
      this.charIndex++
      this.timer -= this.speed
    }
  }
  
  draw(ctx) {
    ctx.font = '18px monospace'
    ctx.fillStyle = '#fff'
    ctx.fillText(this.fullText.slice(0, this.charIndex), this.x, this.y)
  }
  
  isComplete() {
    return this.charIndex &gt;= this.fullText.length
  }
  
  skip() {
    this.charIndex = this.fullText.length
  }
}
</code></pre>
<h2>7) Sözcük kaydırmalı metin</h2>
<pre><code class="language-js">function wrapText(ctx, text, maxWidth) {
  const words = text.split(' ')
  const lines = []
  let currentLine = ''
  
  for (const word of words) {
    const testLine = currentLine ? currentLine + ' ' + word : word
    const metrics = ctx.measureText(testLine)
    
    if (metrics.width &gt; maxWidth &amp;&amp; currentLine) {
      lines.push(currentLine)
      currentLine = word
    } else {
      currentLine = testLine
    }
  }
  
  if (currentLine) lines.push(currentLine)
  return lines
}

function drawWrappedText(ctx, text, x, y, maxWidth, lineHeight) {
  const lines = wrapText(ctx, text, maxWidth)
  for (let i = 0; i &lt; lines.length; i++) {
    ctx.fillText(lines[i], x, y + i * lineHeight)
  }
}
</code></pre>
<h2>8) Diyalog kutusu sistemi</h2>
<pre><code class="language-js">class DialogueBox {
  constructor(x, y, width, height) {
    this.x = x
    this.y = y
    this.width = width
    this.height = height
    this.padding = 16
    this.lineHeight = 24
    this.text = ''
    this.displayedChars = 0
    this.charsPerSecond = 30
    this.timer = 0
  }
  
  show(text) {
    this.text = text
    this.displayedChars = 0
    this.timer = 0
  }
  
  update(dt) {
    if (this.displayedChars &lt; this.text.length) {
      this.timer += dt
      const charsToAdd = Math.floor(this.timer * this.charsPerSecond)
      this.displayedChars = Math.min(this.displayedChars + charsToAdd, this.text.length)
      this.timer = this.timer % (1 / this.charsPerSecond)
    }
  }
  
  skip() {
    this.displayedChars = this.text.length
  }
  
  isComplete() {
    return this.displayedChars &gt;= this.text.length
  }
  
  draw(ctx) {
    // Box background
    ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'
    ctx.fillRect(this.x, this.y, this.width, this.height)
    
    // Border
    ctx.strokeStyle = '#fff'
    ctx.lineWidth = 2
    ctx.strokeRect(this.x, this.y, this.width, this.height)
    
    // Text
    ctx.font = '18px sans-serif'
    ctx.fillStyle = '#fff'
    ctx.textBaseline = 'top'
    
    const displayText = this.text.slice(0, this.displayedChars)
    const maxWidth = this.width - this.padding * 2
    const lines = wrapText(ctx, displayText, maxWidth)
    
    for (let i = 0; i &lt; lines.length; i++) {
      ctx.fillText(
        lines[i],
        this.x + this.padding,
        this.y + this.padding + i * this.lineHeight
      )
    }
    
    // Continue indicator
    if (this.isComplete()) {
      ctx.fillText('▼', this.x + this.width - 24, this.y + this.height - 24)
    }
  }
}
</code></pre>
<h2>9) Havada süzülen hasar sayıları</h2>
<pre><code class="language-js">class FloatingText {
  constructor(text, x, y, color = '#fff') {
    this.text = text
    this.x = x
    this.y = y
    this.color = color
    this.vy = -60
    this.lifetime = 1
    this.age = 0
  }
  
  update(dt) {
    this.y += this.vy * dt
    this.vy *= 0.95
    this.age += dt
  }
  
  draw(ctx) {
    const alpha = 1 - (this.age / this.lifetime)
    ctx.font = 'bold 20px sans-serif'
    ctx.textAlign = 'center'
    ctx.fillStyle = this.color
    ctx.globalAlpha = alpha
    ctx.fillText(this.text, Math.round(this.x), Math.round(this.y))
    ctx.globalAlpha = 1
  }
  
  isExpired() {
    return this.age &gt;= this.lifetime
  }
}

// Manager
const floatingTexts = []

function spawnDamageNumber(damage, x, y) {
  const color = damage &gt; 50 ? '#ff4444' : '#ffff44'
  floatingTexts.push(new FloatingText(damage.toString(), x, y, color))
}

function updateFloatingTexts(dt) {
  for (let i = floatingTexts.length - 1; i &gt;= 0; i--) {
    floatingTexts[i].update(dt)
    if (floatingTexts[i].isExpired()) {
      floatingTexts.splice(i, 1)
    }
  }
}
</code></pre>
<h2>10) WebGL metni (SDF yazı tipleri)</h2>
<p>WebGL oyunlarında Signed Distance Field yazı tipleri ölçeklendirildiğinde netliğini korur:</p>
<pre><code class="language-js">// SDF shader fragment
const sdfFragmentShader = `
  precision mediump float;
  
  uniform sampler2D u_texture;
  uniform vec4 u_color;
  varying vec2 v_texCoord;
  
  void main() {
    float distance = texture2D(u_texture, v_texCoord).a;
    float alpha = smoothstep(0.4, 0.6, distance);
    gl_FragColor = vec4(u_color.rgb, u_color.a * alpha);
  }
`

// Libraries like msdf-bmfont-xml can generate SDF font atlases
</code></pre>
<p>Özellikle three.js sahneleri için <code>troika-three-text</code> iyi bir seçenektir. Önceden bir atlas hazırlamak yerine <code>.ttf</code>/<code>.otf</code>/<code>.woff</code> dosyalarını ayrıştırır, SDF gliflerini bir web worker içinde anında oluşturur ve karakter aralığını, bitişik harfleri ve sağdan sola yazılan alfabeleri sizin yerinize işler.</p>
<p>Çoğu 2D Canvas oyunu için bitmap yazı tipleri veya önceden oluşturulmuş Canvas metinleri iyi sonuç verir.</p>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/canvas-2d-game-loop">Canvas 2D oyun döngüsü</a></li>
<li><a href="/tr/tutorials/pixel-art-rendering">Piksel sanat oluşturma</a></li>
<li><a href="/tr/tutorials/webgl-fundamentals-for-games">WebGL'in temelleri</a></li>
<li><a href="/tr/tutorials/responsive-game-canvas">Duyarlı oyun canvas'ı</a> — metni canvas çözünürlüğü ve DPI ile ölçeklendirme</li>
<li><a href="/tr/guides/game-assets-guide">Ücretsiz oyun varlıkları nerede bulunur?</a> — yazı tipi ve kullanıcı arayüzü varlık kaynakları</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Oyun sesleri için Tone.js]]></title>
            <link>https://app.cinevva.com/tr/tutorials/tone-js-game-audio</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/tone-js-game-audio</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Tone.js ile tatmin edici oyun sesleri oluşturun: sentezleyici tabanlı ses efektleri, dinamik müzik ve oynanışa tepki veren reaktif sesler.]]></description>
            <content:encoded><![CDATA[<h1>Oyun Sesleri için Tone.js</h1>
<p>Tone.js, Web Audio API üzerine kuruludur ve sentezlenmiş sesleri pratik hâle getirir. Ses dosyaları yüklemek yerine sesleri programlı olarak üretirsiniz. Bu da sıfır yükleme süresi, sonsuz çeşitlilik ve oynanışa gerçek zamanlı tepki verebilen sesler anlamına gelir.</p>
<p><strong>Deneyin (sentezlenmiş oyun seslerini duymak için düğmelere tıklayın):</strong></p>
<ToneDemo />
<h2>Neden Ses Dosyaları Yerine Tone.js?</h2>
<p>Ses dosyaları müzik ve karmaşık sesler için gayet iyi çalışır. Ancak sentezleme, oyun ses efektleri için gerçek avantajlar sunar. Seslerin tekrara düşmemesi için her çalınışta perdeyi ve zamanlamayı değiştirebilirsiniz. Oyun durumuna göre anında sesler üretebilirsiniz. Üstelik ağ isteklerini tamamen ortadan kaldırırsınız.</p>
<p>Dezavantajı, sentezlenmiş seslerin bir dosyayı çalmaktan daha fazla işlemci gücü kullanmasıdır. Basit oyunlarda bu sorun değildir. Karmaşık oyunlarda ise iki yaklaşımı birlikte kullanabilirsiniz.</p>
<h2>Kurulum</h2>
<p>Tone.js'i bir CDN veya npm üzerinden yükleyin:</p>
<pre><code class="language-html">&lt;script src=&quot;https://unpkg.com/tone@15/build/Tone.js&quot;&gt;&lt;/script&gt;
</code></pre>
<p>Ya da npm ile:</p>
<pre><code class="language-bash">npm install tone
</code></pre>
<pre><code class="language-js">import * as Tone from 'tone'
</code></pre>
<h2>Otomatik Oynatma Sorunu</h2>
<p>Tarayıcılar, kullanıcı etkileşimi gerçekleşene kadar sesi engeller. Tone.js bunu yönetir, ancak bir kullanıcı hareketi sırasında <code>Tone.start()</code> çağrısı yapmanız gerekir:</p>
<pre><code class="language-js">document.addEventListener('click', async () =&gt; {
  await Tone.start()
  console.log('Audio ready')
}, { once: true })
</code></pre>
<p>Bunu yapmazsanız AudioContext askıda kalır ve hiçbir ses çalmaz.</p>
<h2>İlk Sentezleyiciniz</h2>
<p>Sentezleyici ses üretir. En basit seçenek <code>Tone.Synth</code>'tir:</p>
<pre><code class="language-js">const synth = new Tone.Synth().toDestination()

// Play a note
synth.triggerAttackRelease('C4', '8n')
</code></pre>
<p><code>triggerAttackRelease</code>, bir notayı belirli bir süre boyunca çalar. <code>'C4'</code> orta Do notasıdır. <code>'8n'</code>, mevcut tempoda bir sekizlik notadır.</p>
<p>Sentezleyicide bir osilatör (ses kaynağı) ve bir zarf (sesin nasıl girip çıktığı) bulunur. Her ikisini de özelleştirebilirsiniz:</p>
<pre><code class="language-js">const synth = new Tone.Synth({
  oscillator: { type: 'triangle' },
  envelope: {
    attack: 0.02,
    decay: 0.2,
    sustain: 0.3,
    release: 0.4,
  },
}).toDestination()
</code></pre>
<p>Osilatör türleri arasında <code>sine</code> (saf, yumuşak), <code>triangle</code> (yumuşak), <code>sawtooth</code> (vızıltılı) ve <code>square</code> (boş tınılı, retro) bulunur.</p>
<h2>Ses Efekti Sistemi Oluşturma</h2>
<p>Bir oyunda, farklı ses türleri için birden fazla sentezleyici kullanmak istersiniz. Aşağıdaki yapı bu iş için oldukça uygundur:</p>
<pre><code class="language-js">let isInitialized = false
let softSynth = null
let subSynth = null
let noiseSynth = null
let sfxGain = null

function initSFX() {
  if (isInitialized) return
  
  // Master volume control
  sfxGain = new Tone.Gain(0.7).toDestination()
  
  // Soft synth for melodic sounds
  softSynth = new Tone.PolySynth(Tone.Synth, {
    maxPolyphony: 6,
    oscillator: { type: 'sine' },
    envelope: {
      attack: 0.02,
      decay: 0.2,
      sustain: 0.3,
      release: 0.4,
    },
    volume: -8,
  }).connect(sfxGain)
  
  // Sub bass for deep impacts
  subSynth = new Tone.Synth({
    oscillator: { type: 'sine' },
    envelope: {
      attack: 0.02,
      decay: 0.3,
      sustain: 0.1,
      release: 0.3,
    },
    volume: -8,
  }).connect(sfxGain)
  
  // Noise for texture
  noiseSynth = new Tone.NoiseSynth({
    noise: { type: 'pink' },
    envelope: {
      attack: 0.02,
      decay: 0.15,
      sustain: 0,
      release: 0.15,
    },
    volume: -20,
  }).connect(sfxGain)
  
  isInitialized = true
}
</code></pre>
<p><code>PolySynth</code>, aynı anda birden fazla nota çalmanızı sağlar. Normal <code>Synth</code> ise monofoniktir.</p>
<h2>Oyun Sesi Örnekleri</h2>
<p>Yaygın oyun seslerinden bazılarını şu şekilde oluşturabilirsiniz:</p>
<p><strong>Jeton toplama</strong> - yükselen notalar ödüllendirici hissettirir:</p>
<pre><code class="language-js">function coinCollect() {
  const now = Tone.now()
  softSynth.triggerAttackRelease('C4', '8n', now, 0.3)
  softSynth.triggerAttackRelease('E4', '8n', now + 0.1, 0.35)
  softSynth.triggerAttackRelease('G4', '4n', now + 0.2, 0.4)
}
</code></pre>
<p><strong>Zıplama</strong> - hızlı bir yukarı perde kayması:</p>
<pre><code class="language-js">function jump() {
  const synth = new Tone.Synth({
    oscillator: { type: 'square' },
    envelope: { attack: 0.01, decay: 0.1, sustain: 0, release: 0.1 },
  }).toDestination()
  
  const now = Tone.now()
  synth.frequency.setValueAtTime(150, now)
  synth.frequency.exponentialRampToValueAtTime(400, now + 0.1)
  synth.triggerAttackRelease('C4', '8n', now, 0.2)
}
</code></pre>
<p><strong>Darbe/hasar</strong> - gürültü eşliğinde alçalan ses:</p>
<pre><code class="language-js">function hit() {
  const now = Tone.now()
  subSynth.triggerAttackRelease('E2', '4n', now, 0.5)
  noiseSynth.triggerAttackRelease('4n', now, 0.25)
}
</code></pre>
<p><strong>Ölüm</strong> - hüzünlü, alçalan notalar:</p>
<pre><code class="language-js">function death() {
  const now = Tone.now()
  softSynth.triggerAttackRelease('E4', '4n', now, 0.4)
  softSynth.triggerAttackRelease('C4', '4n', now + 0.2, 0.35)
  softSynth.triggerAttackRelease('A3', '4n', now + 0.4, 0.3)
}
</code></pre>
<h2>Efekt Ekleme</h2>
<p>Efektler sesleri daha zengin hâle getirir. Bunları sentezleyici ile hedef arasına zincirleyin:</p>
<pre><code class="language-js">const filter = new Tone.Filter({
  frequency: 3000,
  type: 'lowpass',
  rolloff: -12,
})

const reverb = new Tone.Reverb({
  decay: 1.2,
  wet: 0.25,
})

const gain = new Tone.Gain(0.7)

// Chain: synth -&gt; filter -&gt; reverb -&gt; gain -&gt; destination
filter.connect(reverb)
reverb.connect(gain)
gain.toDestination()

const synth = new Tone.Synth().connect(filter)
</code></pre>
<p>Alçak geçiren filtre, rahatsız edici yüksek frekansları keser. Yankı, sese mekân hissi katar. Kazanç ise ses seviyesini kontrol eder.</p>
<h2>Hız Sınırlama</h2>
<p>Oyununuz kısa sürede çok sayıda sesi tetiklerse bu sesler üst üste binerek karmaşa yaratabilir. Hız sınırlaması ekleyin:</p>
<pre><code class="language-js">const MIN_INTERVAL_MS = 35
let lastNoteTime = 0

function canPlayNote() {
  const now = performance.now()
  if (now - lastNoteTime &lt; MIN_INTERVAL_MS) {
    return false
  }
  lastNoteTime = now
  return true
}

function playSound() {
  if (!canPlayNote()) return
  synth.triggerAttackRelease('C4', '8n')
}
</code></pre>
<h2>Dinamik Arka Plan Müziği</h2>
<p>Oynanışa tepki veren müzik için vuruş tabanlı bir sistem kullanın:</p>
<pre><code class="language-js">const BPM = 140
const BEAT_MS = (60 / BPM) * 1000

let bassSynth = null
let kickDrum = null
let beatInterval = null
let beatCount = 0
let energy = 0.3

const PROGRESSION = [
  { root: 'E2', chord: ['E3', 'G3', 'B3'] },
  { root: 'C2', chord: ['C3', 'E3', 'G3'] },
  { root: 'D2', chord: ['D3', 'F#3', 'A3'] },
  { root: 'A1', chord: ['A2', 'C3', 'E3'] },
]

function playBeat() {
  const beatInMeasure = beatCount % 8
  const chordIndex = Math.floor(beatCount / 8) % PROGRESSION.length
  const chord = PROGRESSION[chordIndex]
  const now = Tone.now()
  
  // Bass on beat 1
  if (beatInMeasure === 0) {
    bassSynth.triggerAttackRelease(chord.root, '4n', now, 0.6)
  }
  
  // Kick drum - only when energy is high
  if (beatInMeasure === 0 &amp;&amp; energy &gt; 0.3) {
    kickDrum.triggerAttackRelease('C1', '8n', now, 0.4)
  }
  
  beatCount++
}

function startMusic() {
  beatInterval = setInterval(playBeat, BEAT_MS)
}

function stopMusic() {
  clearInterval(beatInterval)
}

// Game events affect energy
function onEnemyKill() {
  energy = Math.min(1, energy + 0.1)
}
</code></pre>
<p>Müziğin yoğunluğu, oyunda olup bitenlere göre değişir.</p>
<h2>MembraneSynth ve NoiseSynth ile Davullar</h2>
<p>Vurmalı çalgılar için özel sentezleyiciler kullanın:</p>
<pre><code class="language-js">// Kick drum
const kick = new Tone.MembraneSynth({
  pitchDecay: 0.05,
  octaves: 4,
  envelope: {
    attack: 0.001,
    decay: 0.2,
    sustain: 0,
    release: 0.1,
  },
  volume: -14,
}).toDestination()

kick.triggerAttackRelease('C1', '8n')

// Snare
const snare = new Tone.NoiseSynth({
  noise: { type: 'white' },
  envelope: {
    attack: 0.002,
    decay: 0.15,
    sustain: 0,
    release: 0.1,
  },
  volume: -12,
}).toDestination()

snare.triggerAttackRelease('8n')

// Hi-hat
const hihat = new Tone.NoiseSynth({
  noise: { type: 'white' },
  envelope: {
    attack: 0.001,
    decay: 0.04,
    sustain: 0,
    release: 0.03,
  },
  volume: -22,
}).toDestination()

hihat.triggerAttackRelease('32n')
</code></pre>
<h2>Temizleme</h2>
<p>Kaynakları serbest bırakmak için işiniz bittiğinde sentezleyicileri temizleyin:</p>
<pre><code class="language-js">function cleanup() {
  if (softSynth) softSynth.dispose()
  if (subSynth) subSynth.dispose()
  if (noiseSynth) noiseSynth.dispose()
  if (sfxGain) sfxGain.dispose()
}
</code></pre>
<h2>Yaygın Hatalar</h2>
<p><strong>Tone.start() çağrısını beklememek</strong> - Kullanıcı etkileşimiyle bağlam başlatılana kadar ses çalmaz.</p>
<p><strong>Her ses için yeni sentezleyici oluşturmak</strong> - Sentezleyicileri başlangıçta bir kez oluşturup yeniden kullanın. Her seferinde yenilerini oluşturmak bellek sızıntısına yol açar.</p>
<p><strong>Çok fazla sesin üst üste binmesi</strong> - Ses karmaşasını önlemek için hız sınırlaması ve yinelenenleri kaldırma yöntemlerini kullanın.</p>
<p><strong>Sert frekanslar</strong> - Oyuncuları rahatsız etmeyecek sesler için alçak geçiren filtreler ve daha yumuşak osilatör türleri (sinüs, üçgen) kullanın.</p>
<h2>Daha Fazla Kaynak</h2>
<p><a href="/tr/tutorials/web-audio-api-games">Oyunlar için Web Audio API</a>, temel API'yi ve ses dosyalarının nasıl yükleneceğini ele alır.</p>
<p><a href="https://tonejs.github.io/">Tone.js belgeleri</a> eksiksiz API referansını içerir.</p>
<p><a href="/tr/guides/game-assets-guide">Ücretsiz Oyun Varlıkları Nerede Bulunur?</a>, Freesound gibi ses ve ses efekti sağlayıcılarını içerir.</p>
<p><a href="/tr/tutorials/mobile-friendly-web-games">Mobil uyumlu web oyunları</a>, iOS ses kısıtlamalarını ve dokunarak ses kilidini açma yöntemlerini ele alır.</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Oyunlar için Web Audio API]]></title>
            <link>https://app.cinevva.com/tr/tutorials/web-audio-api-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/web-audio-api-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Web Audio API ile oyun sesleri uygulayın: ses efektleri, müzik, uzamsal ses ve miksaj. İşe yarayan pratik kalıplar.]]></description>
            <content:encoded><![CDATA[<h1>Oyunlar için Web Audio API</h1>
<p>İyi sesler bir oyunu dönüştürür. Sessiz bir oyun cansız hissettirir. Web Audio API; düşük gecikmeli ses efektleri, müzik çalma ve hatta 3B uzamsal ses özelliklerini doğrudan tarayıcıda kullanmanızı sağlar.</p>
<p><strong>Deneyin (sentezlenmiş sesleri duymak için bir düğmeye tıklayın):</strong></p>
<AudioDemo />
<h2>Otomatik Oynatma Sorunu</h2>
<p>Her şeyden önce otomatik oynatma kısıtlamalarını anlamanız gerekir. Tarayıcılar, kullanıcı sayfanızla etkileşime geçene kadar sesi engeller. Bu iyi bir şeydir (kimse sayfa yüklenir yüklenmez ses yayınına başlayan siteler istemez), ancak sesi bir tıklama, dokunma veya tuşa basma işlemine yanıt olarak başlatmanız gerektiği anlamına gelir.</p>
<pre><code class="language-js">let audioCtx = null

function initAudio() {
  if (!audioCtx) {
    audioCtx = new AudioContext()
  }
  if (audioCtx.state === 'suspended') {
    audioCtx.resume()
  }
  return audioCtx
}

// Initialize on first interaction
document.addEventListener('click', () =&gt; initAudio(), { once: true })
document.addEventListener('keydown', () =&gt; initAudio(), { once: true })
</code></pre>
<p>Bundan önce ses çalmayı denerseniz işlem hiçbir uyarı vermeden başarısız olur veya AudioContext askıda kalır. Bu durum birçok geliştiriciyi hazırlıksız yakalar.</p>
<h2>Sesleri Yükleme</h2>
<p>Ses dosyalarının oynatılmadan önce kodlarının çözülmesi gerekir. Dosyayı alın, kodunu çözün ve yeniden kullanmak üzere arabelleği saklayın:</p>
<pre><code class="language-js">async function loadSound(url) {
  const response = await fetch(url)
  const arrayBuffer = await response.arrayBuffer()
  const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer)
  return audioBuffer
}
</code></pre>
<p>Kod çözme zaman alır; bu nedenle tüm seslerinizi çalmanız gerektiği anda değil, yükleme ekranı sırasında yükleyin.</p>
<h2>Sesleri Çalma</h2>
<p>Bir sesi çalmak için bir arabellek kaynağı oluşturun, ses seviyesini kontrol etmek üzere bunu bir kazanç düğümüne bağlayın ve başlatın:</p>
<pre><code class="language-js">function playSound(buffer, volume = 1) {
  const source = audioCtx.createBufferSource()
  source.buffer = buffer
  
  const gainNode = audioCtx.createGain()
  gainNode.gain.value = volume
  
  source.connect(gainNode)
  gainNode.connect(audioCtx.destination)
  
  source.start(0)
  return source
}
</code></pre>
<p>Arabellek kaynakları tek kullanımlıktır. Bir kaynak oynatmayı bitirdikten sonra onu yeniden başlatamazsınız. Her oynatmada yeni bir tane oluşturun. Bu, kaynak israfı gibi görünse de API bu şekilde çalışır ve tarayıcılar buna göre optimize edilmiştir.</p>
<h2>Ses Yöneticisi</h2>
<p>Gerçek bir oyun için her şeyi bir yönetici sınıfında toplayın:</p>
<pre><code class="language-js">class SoundManager {
  constructor() {
    this.ctx = null
    this.sounds = new Map()
    this.masterGain = null
  }
  
  init() {
    this.ctx = new AudioContext()
    this.masterGain = this.ctx.createGain()
    this.masterGain.connect(this.ctx.destination)
  }
  
  async load(name, url) {
    const response = await fetch(url)
    const buffer = await this.ctx.decodeAudioData(await response.arrayBuffer())
    this.sounds.set(name, buffer)
  }
  
  play(name, volume = 1) {
    const buffer = this.sounds.get(name)
    if (!buffer) return
    
    const source = this.ctx.createBufferSource()
    source.buffer = buffer
    
    const gain = this.ctx.createGain()
    gain.gain.value = volume
    
    source.connect(gain)
    gain.connect(this.masterGain)
    source.start(0)
    
    return source
  }
  
  setMasterVolume(v) {
    this.masterGain.gain.value = v
  }
}
</code></pre>
<p>Ana kazanç düğümü, genel bir ses seviyesi kaydırıcısı uygulamanızı sağlar. Tüm sesler bu düğüm üzerinden yönlendirilir.</p>
<h2>Arka Plan Müziği</h2>
<p>Müzik için döngüye alma ve durdurma ya da kademeli olarak kısma özellikleri gerekir:</p>
<pre><code class="language-js">class MusicPlayer {
  constructor(ctx, masterGain) {
    this.ctx = ctx
    this.masterGain = masterGain
    this.currentTrack = null
    this.gain = ctx.createGain()
    this.gain.connect(masterGain)
  }
  
  play(buffer, volume = 0.5) {
    this.stop()
    
    this.currentTrack = this.ctx.createBufferSource()
    this.currentTrack.buffer = buffer
    this.currentTrack.loop = true
    this.currentTrack.connect(this.gain)
    this.gain.gain.value = volume
    this.currentTrack.start(0)
  }
  
  stop() {
    if (this.currentTrack) {
      this.currentTrack.stop()
      this.currentTrack = null
    }
  }
  
  fadeOut(duration = 1) {
    this.gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + duration)
  }
}
</code></pre>
<p><code>linearRampToValueAtTime</code> yöntemi, ani kesintiler yerine yumuşak geçişler sağlar.</p>
<h2>Uzamsal Ses</h2>
<p>3B oyunlarda sesleri uzayda konumlandırabilirsiniz. Sesler dinleyiciden uzaklaştıkça daha kısık hâle gelir:</p>
<pre><code class="language-js">function playSpatialSound(buffer, x, y, z) {
  const source = audioCtx.createBufferSource()
  source.buffer = buffer
  
  const panner = audioCtx.createPanner()
  panner.panningModel = 'HRTF'
  panner.distanceModel = 'inverse'
  panner.refDistance = 1
  panner.maxDistance = 100
  panner.positionX.value = x
  panner.positionY.value = y
  panner.positionZ.value = z
  
  source.connect(panner)
  panner.connect(audioCtx.destination)
  source.start(0)
}

function updateListener(x, y, z, fx, fy, fz) {
  const listener = audioCtx.listener
  listener.positionX.value = x
  listener.positionY.value = y
  listener.positionZ.value = z
  listener.forwardX.value = fx
  listener.forwardY.value = fy
  listener.forwardZ.value = fz
  listener.upX.value = 0
  listener.upY.value = 1
  listener.upZ.value = 0
}
</code></pre>
<p>Her karede kamera veya oyuncu konumuyla <code>updateListener</code> işlevini çağırın. Kaydırıcı, mesafe ve yöne göre ses seviyesini ve stereo kaydırmayı otomatik olarak ayarlar.</p>
<h2>Hızla Tekrarlanan Sesler</h2>
<p>Saniyede birçok kez çalınan seslerde (silah sesleri, ayak sesleri) üst üste binen örneklerin birikmesini istemezsiniz. Basit bir havuz kalıbı yardımcı olur:</p>
<pre><code class="language-js">class SoundPool {
  constructor(ctx, buffer, size = 8) {
    this.sources = []
    this.index = 0
    this.ctx = ctx
    this.buffer = buffer
    this.size = size
  }
  
  play(volume = 1) {
    const source = this.ctx.createBufferSource()
    source.buffer = this.buffer
    
    const gain = this.ctx.createGain()
    gain.gain.value = volume
    
    source.connect(gain)
    gain.connect(this.ctx.destination)
    source.start(0)
    
    this.index = (this.index + 1) % this.size
  }
}
</code></pre>
<p>Havuz, aynı sesin aynı anda kaç örneğinin çalınabileceğini sınırlar. Sınıra ulaştığınızda en eski örnek değiştirilir.</p>
<h2>Ses Biçimleri</h2>
<p>Farklı biçimler, farklı durumlarda daha iyi sonuç verir:</p>
<p>MP3 her yerde çalışır ve müzik için uygundur. OGG Vorbis, aynı dosya boyutunda daha iyi kalite sunar. Safari uzun süre bu biçimi desteklemeyen tek büyük tarayıcıydı ancak Safari 18.4 ile yerel Ogg Vorbis oynatma desteği eklendi; dolayısıyla güncel tüm büyük tarayıcılar artık bu biçimi destekliyor. Eski Safari sürümlerini önemsiyorsanız yine de yedek olarak MP3 sunmalısınız. AAC/M4A, Apple cihazlarında iyi çalışır. WAV sıkıştırılmamıştır ve büyüktür ancak kodu anında çözülür; bu da dosya boyutunu karşılayabildiğiniz kısa ses efektleri için uygundur. WebM/Opus en iyi kalite-boyut oranını sunar ancak yalnızca modern tarayıcılarda çalışır.</p>
<p>En geniş uyumluluk için yedek biçimler sunun:</p>
<pre><code class="language-js">const audioUrl = canPlayOgg() ? 'sound.ogg' : 'sound.mp3'
</code></pre>
<h2>Kullanışlı İpuçları</h2>
<p><strong>Perde değişimi</strong>, tekrarlanan seslerin daha az robotik duyulmasını sağlar:</p>
<pre><code class="language-js">function playWithPitchVariation(buffer, variance = 0.1) {
  const source = audioCtx.createBufferSource()
  source.buffer = buffer
  source.playbackRate.value = 1 + (Math.random() - 0.5) * variance
  source.connect(audioCtx.destination)
  source.start(0)
}
</code></pre>
<p>Böylece her silah veya ayak sesi biraz farklı duyulur.</p>
<p><strong>Ses bastırma</strong>, diyaloglar veya önemli sesler sırasında müziğin seviyesini düşürür:</p>
<pre><code class="language-js">function duckMusic(musicGain, duration = 0.3) {
  musicGain.gain.linearRampToValueAtTime(0.2, audioCtx.currentTime + duration)
}

function unduckMusic(musicGain, duration = 0.3) {
  musicGain.gain.linearRampToValueAtTime(1, audioCtx.currentTime + duration)
}
</code></pre>
<h2>iOS'taki Dikkat Edilmesi Gerekenler</h2>
<p>iOS, diğer platformlara göre daha sıkı ses kısıtlamalarına sahiptir. AudioContext bir kullanıcı hareketi sırasında oluşturulmalı ve sürdürülmelidir. Bazı iOS sürümleri ayrıca yalnızca bağlamı oluşturmanızı değil, kullanıcı hareketi sırasında bir ses (sessiz bile olsa) çalmanızı gerektirir. Ses masaüstünde çalışıyor ancak iOS'ta çalışmıyorsa nedeni muhtemelen budur.</p>
<h2>Diğer Kaynaklar</h2>
<p><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlayın</a>, ses dâhil varlık sıkıştırmayı ele alır.</p>
<p><a href="/tr/tutorials/mobile-friendly-web-games">Mobil uyumlu web oyunları</a>, iOS'a özgü sorunlar hakkında daha fazla bilgi içerir.</p>
<p><a href="/tr/tutorials/pwa-offline-games">Çevrimdışı oyunlar için PWA</a>, çevrimdışı oynatma için ses dosyalarının nasıl önbelleğe alınacağını gösterir.</p>
<p><a href="/tr/tutorials/tone-js-game-audio">Oyun sesleri için Tone.js</a>, daha üst düzey bir kütüphane kullanarak prosedürel ses üretimini ve müzik sistemlerini ele alır.</p>
<p><a href="/tr/guides/game-assets-guide">Ücretsiz Oyun Varlıkları Nerede Bulunur?</a>, Freesound ve Poly Haven gibi ücretsiz ses efekti ve müzik kaynaklarını listeler.</p>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API">MDN: Web Audio API</a> — eksiksiz API referansı</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/AudioContext">MDN: AudioContext</a> — ses bağlamlarını oluşturma ve yönetme</li>
<li><a href="https://www.w3.org/TR/webaudio/">Web Audio API belirtimi</a> — W3C belirtimi</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Oyun mantığı için Web Workers]]></title>
            <link>https://app.cinevva.com/tr/tutorials/web-workers-game-logic</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/web-workers-game-logic</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Ana iş parçacığını engellemeden fizik, yol bulma, yapay zekâ ve prosedürel üretim gibi ağır hesaplamaları Web Workers'a aktarın.]]></description>
            <content:encoded><![CDATA[<h1>Oyun mantığı için Web Workers</h1>
<p>Web Workers, JavaScript'i bir arka plan iş parçacığında çalıştırmanızı sağlar. Oyunlarda bu, fizik, yapay zekâ ve prosedürel üretimin kare kaybına yol açmadan çalışabileceği anlamına gelir.</p>
<h2>1) Workers ne zaman kullanılmalı?</h2>
<p>Uygun kullanım alanları:</p>
<ul>
<li>Fizik simülasyonu</li>
<li>Yol bulma (A*, navmesh)</li>
<li>Yapay zekâ karar verme</li>
<li>Prosedürel üretim</li>
<li>Varlık işleme (görüntü düzenleme, sıkıştırma)</li>
<li>Karmaşık matematiksel işlemler (FFT, çarpışma algılama)</li>
</ul>
<p>Şunlar için ideal değildir:</p>
<ul>
<li>Render işlemleri (Workers, DOM/Canvas'a doğrudan erişemez)</li>
<li>Çok küçük görevler (iş parçacığı ek yüküne değmez)</li>
</ul>
<h2>2) Temel bir Worker oluşturma</h2>
<p><strong>worker.js:</strong></p>
<pre><code class="language-js">self.onmessage = (e) =&gt; {
  const { type, data } = e.data
  
  if (type === 'calculate') {
    const result = heavyCalculation(data)
    self.postMessage({ type: 'result', data: result })
  }
}

function heavyCalculation(input) {
  // Yoğun işlem burada
  return input * 2
}
</code></pre>
<p><strong>main.js:</strong></p>
<pre><code class="language-js">const worker = new Worker('worker.js')

worker.onmessage = (e) =&gt; {
  const { type, data } = e.data
  if (type === 'result') {
    console.log('Sonuç alındı:', data)
  }
}

worker.postMessage({ type: 'calculate', data: 42 })
</code></pre>
<h2>3) Satır içi Workers (ayrı dosya olmadan)</h2>
<pre><code class="language-js">function createInlineWorker(fn) {
  const blob = new Blob([`(${fn.toString()})()`], { type: 'text/javascript' })
  return new Worker(URL.createObjectURL(blob))
}

const worker = createInlineWorker(() =&gt; {
  self.onmessage = (e) =&gt; {
    const result = e.data * 2
    self.postMessage(result)
  }
})
</code></pre>
<h2>4) ES modülü Workers</h2>
<p>Worker kodunu bir ES modülü olarak yazabilir ve <code>importScripts</code> yerine içinde <code>import</code> kullanabilirsiniz. Oluşturucuya <code>{ type: 'module' }</code> iletin:</p>
<pre><code class="language-js">const worker = new Worker('physics-worker.js', { type: 'module' })
</code></pre>
<p>Ardından Worker içinde paylaşılan matematik, çarpışma veya yapay zekâ yardımcılarını ana iş parçacığında olduğu gibi <code>import</code> edebilirsiniz; böylece oyun mantığı yinelenmez. Modül Workers, Chrome ve Edge 80+, Safari 15+ ve Firefox 114+ sürümlerinde desteklenir.</p>
<h2>5) Fizik Worker örneği</h2>
<p><strong>physics-worker.js:</strong></p>
<pre><code class="language-js">const bodies = []
const FIXED_DT = 1 / 60

self.onmessage = (e) =&gt; {
  const { type, data } = e.data
  
  switch (type) {
    case 'init':
      initWorld(data)
      break
    case 'step':
      step()
      break
    case 'addBody':
      bodies.push(data)
      break
  }
}

function initWorld(config) {
  // Fizik dünyasını başlat
}

function step() {
  // Fiziği güncelle
  for (const body of bodies) {
    body.vy += 9.8 * FIXED_DT // Yer çekimi
    body.x += body.vx * FIXED_DT
    body.y += body.vy * FIXED_DT
  }
  
  // Konumları geri gönder
  self.postMessage({
    type: 'positions',
    data: bodies.map(b =&gt; ({ id: b.id, x: b.x, y: b.y, rotation: b.rotation }))
  })
}
</code></pre>
<h2>6) Performans için aktarılabilir nesneler</h2>
<p>Büyük veriler kopyalanmadan aktarılabilir:</p>
<pre><code class="language-js">// Ana iş parçacığı
const positions = new Float32Array(1000)
worker.postMessage(positions, [positions.buffer])
// positions artık burada kullanılamaz (aktarıldı)

// Worker
self.onmessage = (e) =&gt; {
  const positions = e.data
  // Konumlarla çalış
  self.postMessage(positions, [positions.buffer])
}
</code></pre>
<h2>7) Yol bulma Worker'ı</h2>
<p><strong>pathfinding-worker.js:</strong></p>
<pre><code class="language-js">let grid = null

self.onmessage = (e) =&gt; {
  const { type, data } = e.data
  
  if (type === 'setGrid') {
    grid = data
  }
  
  if (type === 'findPath') {
    const path = aStar(data.start, data.end, grid)
    self.postMessage({ type: 'path', id: data.id, path })
  }
}

function aStar(start, end, grid) {
  // A* uygulaması
  const openSet = [start]
  const cameFrom = new Map()
  const gScore = new Map()
  gScore.set(key(start), 0)
  
  while (openSet.length &gt; 0) {
    // ... A* mantığı
  }
  
  return reconstructPath(cameFrom, end)
}

function key(pos) {
  return `${pos.x},${pos.y}`
}
</code></pre>
<h2>8) Paralel görevler için Worker havuzu</h2>
<pre><code class="language-js">class WorkerPool {
  constructor(workerUrl, size = navigator.hardwareConcurrency || 4) {
    this.workers = []
    this.queue = []
    this.available = []
    
    for (let i = 0; i &lt; size; i++) {
      const worker = new Worker(workerUrl)
      worker.onmessage = (e) =&gt; this.handleResult(worker, e)
      this.workers.push(worker)
      this.available.push(worker)
    }
  }
  
  run(data) {
    return new Promise((resolve) =&gt; {
      const task = { data, resolve }
      
      if (this.available.length &gt; 0) {
        this.dispatch(this.available.pop(), task)
      } else {
        this.queue.push(task)
      }
    })
  }
  
  dispatch(worker, task) {
    worker._currentTask = task
    worker.postMessage(task.data)
  }
  
  handleResult(worker, e) {
    const task = worker._currentTask
    task.resolve(e.data)
    
    if (this.queue.length &gt; 0) {
      this.dispatch(worker, this.queue.shift())
    } else {
      this.available.push(worker)
    }
  }
  
  terminate() {
    this.workers.forEach(w =&gt; w.terminate())
  }
}
</code></pre>
<h2>9) Gerçek zamanlı senkronizasyon için SharedArrayBuffer</h2>
<p>Kaynaklar arası yalıtım etkinleştirildiğinde belleği paylaşabilirsiniz:</p>
<pre><code class="language-js">// Ana iş parçacığı
const shared = new SharedArrayBuffer(1024)
const positions = new Float32Array(shared)

worker.postMessage({ type: 'init', buffer: shared })

// Worker, paylaşılan belleği doğrudan okur/yazar
// Konum güncellemelerinde postMessage ek yükü yoktur
</code></pre>
<h2>10) Workers için OffscreenCanvas</h2>
<p>Render işlemlerini bir Worker içinde gerçekleştirin. OffscreenCanvas artık Chrome, Edge, Firefox ve Safari genelinde Baseline Widely Available durumundadır (Safari desteği macOS ve iOS'ta 17.0 sürümüyle eklendi):</p>
<pre><code class="language-js">// Ana iş parçacığı
const canvas = document.getElementById('game')
const offscreen = canvas.transferControlToOffscreen()
worker.postMessage({ canvas: offscreen }, [offscreen])

// Worker
self.onmessage = (e) =&gt; {
  const canvas = e.data.canvas
  const ctx = canvas.getContext('2d')
  
  function render() {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    // Çiz...
    requestAnimationFrame(render)
  }
  render()
}
</code></pre>
<h2>11) Hata yönetimi</h2>
<pre><code class="language-js">worker.onerror = (e) =&gt; {
  console.error('Worker hatası:', e.message, e.filename, e.lineno)
}

// Worker içinde
self.onerror = (e) =&gt; {
  self.postMessage({ type: 'error', message: e.message })
}
</code></pre>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlayın</a></li>
<li><a href="/tr/tutorials/coop-coep-sharedarraybuffer">Wasm iş parçacıklarını etkinleştirin (SharedArrayBuffer)</a></li>
<li><a href="/tr/tutorials/canvas-2d-game-loop">Canvas 2D oyun döngüsü</a></li>
<li><a href="/tr/tutorials/game-physics-libraries">Oyun fiziği kütüphaneleri</a> — Rapier veya Cannon-es'i bir Worker içinde çalıştırma</li>
<li><a href="/tr/tutorials/streaming-asset-loading">Akışla varlık yükleme</a> — varlıkları ana iş parçacığı dışında çözme</li>
<li><a href="/tr/guides/web-games-stack-2026">2026'da Web Oyunları Teknoloji Yığını</a> — Workers'ın WebGL/WebGPU/Wasm yığınına nasıl uyduğu</li>
</ul>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">MDN: Web Workers API</a> — eksiksiz API referansı</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers">MDN: Web Workers Kullanımı</a> — adım adım kılavuz</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects">MDN: Aktarılabilir nesneler</a> — sıfır kopyalı veri aktarımı</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer">MDN: SharedArrayBuffer</a> — iş parçacıkları arasında paylaşılan bellek</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Oyun geliştiricileri için WebGL temelleri]]></title>
            <link>https://app.cinevva.com/tr/tutorials/webgl-fundamentals-for-games</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/webgl-fundamentals-for-games</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Oyunlar için WebGL temellerini öğrenin: shader'lar, buffer'lar, dokular ve render döngüsü. Kopyalayıp geliştirebileceğiniz pratik kodlar.]]></description>
            <content:encoded><![CDATA[<h1>Oyun geliştiricileri için WebGL temelleri</h1>
<p>WebGL, tarayıcıda GPU'ya doğrudan erişim sağlar. 3D ve yüksek performanslı 2D web oyunlarının temelini oluşturur. Bu eğitimde temel konuları ele alacağız.</p>
<h2>1) WebGL bağlamı oluşturma</h2>
<pre><code class="language-js">const canvas = document.getElementById('game')
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl')

if (!gl) {
  // Fallback or error message
  console.error('WebGL not supported')
}
</code></pre>
<p>Kullanılabildiğinde <code>webgl2</code>yi tercih edin; yaygın biçimde desteklenir ve daha gelişmiş özellikler sunar.</p>
<h2>2) Render hattı (basitleştirilmiş)</h2>
<ol>
<li><strong>Vertex shader</strong>, her vertex için bir kez çalışır. Geometrinizi konumlandırır.</li>
<li><strong>Fragment shader</strong>, her piksel için bir kez çalışır. Çıktıyı renklendirir.</li>
<li>Buffer'lar verileri (konumlar, UV'ler, renkler) tutar.</li>
<li>Dokular, shader'ların örneklediği görsellerdir.</li>
</ol>
<h2>3) Minimal bir shader çifti</h2>
<p><strong>Vertex shader:</strong></p>
<pre><code class="language-glsl">#version 300 es
in vec2 a_position;
void main() {
  gl_Position = vec4(a_position, 0.0, 1.0);
}
</code></pre>
<p><strong>Fragment shader:</strong></p>
<pre><code class="language-glsl">#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
  fragColor = vec4(1.0, 0.5, 0.2, 1.0); // orange
}
</code></pre>
<h2>4) Shader'ları derleme ve bağlama</h2>
<pre><code class="language-js">function createShader(gl, type, source) {
  const shader = gl.createShader(type)
  gl.shaderSource(shader, source)
  gl.compileShader(shader)
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error(gl.getShaderInfoLog(shader))
    gl.deleteShader(shader)
    return null
  }
  return shader
}

function createProgram(gl, vs, fs) {
  const program = gl.createProgram()
  gl.attachShader(program, vs)
  gl.attachShader(program, fs)
  gl.linkProgram(program)
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error(gl.getProgramInfoLog(program))
    return null
  }
  return program
}
</code></pre>
<h2>5) Buffer oluşturma</h2>
<pre><code class="language-js">const positions = new Float32Array([
  -0.5, -0.5,
   0.5, -0.5,
   0.0,  0.5,
])

const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW)
</code></pre>
<h2>6) Buffer'ları shader'lara bağlama</h2>
<pre><code class="language-js">const positionLoc = gl.getAttribLocation(program, 'a_position')
gl.enableVertexAttribArray(positionLoc)
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0)
</code></pre>
<h2>7) Oyun döngüsü</h2>
<pre><code class="language-js">function render() {
  gl.viewport(0, 0, canvas.width, canvas.height)
  gl.clearColor(0, 0, 0, 1)
  gl.clear(gl.COLOR_BUFFER_BIT)

  gl.useProgram(program)
  gl.drawArrays(gl.TRIANGLES, 0, 3)

  requestAnimationFrame(render)
}
render()
</code></pre>
<h2>8) Doku yükleme</h2>
<pre><code class="language-js">function loadTexture(gl, url) {
  const texture = gl.createTexture()
  gl.bindTexture(gl.TEXTURE_2D, texture)

  // Placeholder pixel until image loads
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
    new Uint8Array([255, 0, 255, 255]))

  const img = new Image()
  img.onload = () =&gt; {
    gl.bindTexture(gl.TEXTURE_2D, texture)
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img)
    gl.generateMipmap(gl.TEXTURE_2D)
  }
  img.src = url
  return texture
}
</code></pre>
<h2>9) Sık karşılaşılan sorunlar</h2>
<ul>
<li><strong>İkinin kuvveti boyutundaki dokular</strong> WebGL 2'de kesinlikle gerekli değildir ancak mipmap'ler bunlarla daha iyi çalışır.</li>
<li><strong>Bağlam kaybı</strong>: <code>webglcontextlost</code> olaylarını yönetin; mobil tarayıcılar bellek baskısı altında bağlamları bellekten çıkarabilir.</li>
<li><strong>Float dokular</strong> için uzantı kontrolleri (<code>EXT_color_buffer_float</code>) gerekir.</li>
</ul>
<h2>10) Ne zaman bir kütüphane kullanılmalı?</h2>
<p>Saf WebGL ile kod yazmak çok ayrıntılıdır. Çoğu oyun için şunları değerlendirin:</p>
<ul>
<li><strong>Three.js</strong> — tam özellikli 3D</li>
<li><strong>PixiJS</strong> — hızlı 2D render</li>
<li><strong>PlayCanvas</strong> — editörlü oyun motoru</li>
</ul>
<p>Saf WebGL'yi şu durumlarda kullanın:</p>
<ul>
<li>maksimum kontrole ihtiyacınız varsa</li>
<li>özel bir render sistemi geliştiriyorsanız</li>
<li>sistemin gerçekte nasıl çalıştığını öğrenmek istiyorsanız</li>
</ul>
<p>2026 için bir not: WebGPU artık tüm büyük tarayıcılarda varsayılan olarak sunuluyor (Chrome ve Edge'de 113. sürümden, Firefox'ta Windows için 141. ve macOS için 145. sürümden itibaren; ayrıca iOS 26, iPadOS 26 ve macOS Tahoe 26'daki Safari'de). Three.js gibi kütüphaneler WebGPU render sistemlerini otomatik WebGL 2 geri dönüşüyle çalıştırdığından, buradaki temeller hâlâ geçerlidir ve WebGL 2 güvenilir temel olmaya devam eder. Bugün sıfırdan yeni bir render sistemi geliştirmeye başlıyorsanız bir API'ye karar vermeden önce WebGPU eğitimini de bununla birlikte okuyun.</p>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/webgpu-getting-started">Oyunlar için WebGPU</a></li>
<li><a href="/tr/tutorials/canvas-2d-game-loop">Canvas 2D oyun döngüsü</a></li>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlama</a></li>
<li><a href="/tr/guides/web-games-stack-2026">2026'da Web Oyunları Teknoloji Yığını</a> — WebGL, WebGPU ve Wasm arasında ne zaman seçim yapılmalı?</li>
<li><a href="/tr/tutorials/game-physics-libraries">Oyun fiziği kütüphaneleri</a> — WebGL üzerinden render yapan fizik motorları</li>
<li><a href="/tr/guides/web-game-engines-comparison">Web Oyun Motorları Karşılaştırması</a> — Three.js, PixiJS, PlayCanvas ve diğerleri</li>
</ul>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API">MDN: WebGL API'si</a> — eksiksiz API referansı</li>
<li><a href="https://webgl2fundamentals.org/">WebGL2 Temelleri</a> — ayrıntılı WebGL2 eğitim serisi</li>
<li><a href="https://thebookofshaders.com/">Shader Kitabı</a> — GLSL fragment shader'ları için etkileşimli rehber</li>
<li><a href="https://threejs.org/docs/">Three.js dokümantasyonu</a> — en popüler WebGL çatısı</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Oyun geliştiriciler için WebGPU'ya başlangıç]]></title>
            <link>https://app.cinevva.com/tr/tutorials/webgpu-getting-started</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/webgpu-getting-started</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[WebGPU'ya başlayın: cihaz kurulumu, işlem hatları, gölgelendiriciler ve işleme. Yeni nesil web oyunları için modern GPU API'si.]]></description>
            <content:encoded><![CDATA[<h1>Oyun geliştiriciler için WebGPU'ya başlangıç</h1>
<p>WebGPU, WebGL'nin halefidir. Daha düşük seviyeli, daha açık ve modern GPU'lar için tasarlanmış bir API'dir. 2026'da yeni bir 3D projeye başlıyorsanız WebGPU'yu öğrenmeye değer.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0">
<iframe src="https://www.youtube.com/embed/KTFFdZSDiTU" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0" allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture" allowfullscreen></iframe>
</div>
<p style="font-size:0.9rem;color:var(--vp-c-text-2);margin-top:-0.5rem">WebGPU Eğitimi: Kurulumdan aydınlatmalı ve animasyonlu 3D işlemeye kadar uzanan 10 proje</p>
<h2>1) Tarayıcı desteğini kontrol edin</h2>
<pre><code class="language-js">if (!navigator.gpu) {
  console.error('WebGPU not supported')
}
</code></pre>
<p>2026 itibarıyla WebGPU; Chrome, Edge, Firefox ve Safari'de kullanılabilir.</p>
<h2>2) Bir cihaz edinin</h2>
<pre><code class="language-js">const adapter = await navigator.gpu.requestAdapter()
const device = await adapter.requestDevice()
</code></pre>
<p>Bağdaştırıcı, fiziksel GPU'yu temsil eder. Cihaz ise onunla iletişim kurmanızı sağlayan arayüzdür.</p>
<p>Her iki çağrı da başarısız olabilir. Sistemde kullanılabilir bir GPU olmadığında <code>requestAdapter()</code> değeri <code>null</code> döndürür ve <code>requestDevice()</code> çağrısı reddedilebilir; bu nedenle devam etmeden önce bunlara karşı önlem alın:</p>
<pre><code class="language-js">const adapter = await navigator.gpu.requestAdapter()
if (!adapter) throw new Error('No suitable GPU adapter found')
const device = await adapter.requestDevice()
</code></pre>
<p>Bir bağdaştırıcıdan cihaz oluşturduğunuzda bağdaştırıcı da tüketilir; bu nedenle başka bir cihaza ihtiyacınız varsa yeni bir bağdaştırıcı isteyin.</p>
<h2>3) Tuvali yapılandırın</h2>
<pre><code class="language-js">const canvas = document.getElementById('game')
const context = canvas.getContext('webgpu')
const format = navigator.gpu.getPreferredCanvasFormat()

context.configure({
  device,
  format,
  alphaMode: 'premultiplied',
})
</code></pre>
<h2>4) Gölgelendiricileri WGSL ile yazın</h2>
<p>WebGPU, GLSL yerine WGSL'yi (WebGPU Gölgelendirme Dili) kullanır.</p>
<pre><code class="language-wgsl">@vertex
fn vs_main(@builtin(vertex_index) idx: u32) -&gt; @builtin(position) vec4f {
  var pos = array&lt;vec2f, 3&gt;(
    vec2f( 0.0,  0.5),
    vec2f(-0.5, -0.5),
    vec2f( 0.5, -0.5),
  );
  return vec4f(pos[idx], 0.0, 1.0);
}

@fragment
fn fs_main() -&gt; @location(0) vec4f {
  return vec4f(1.0, 0.5, 0.2, 1.0); // orange
}
</code></pre>
<h2>5) Bir gölgelendirici modülü oluşturun</h2>
<pre><code class="language-js">const shaderModule = device.createShaderModule({
  code: wgslSource,
})
</code></pre>
<h2>6) Bir işleme işlem hattı oluşturun</h2>
<pre><code class="language-js">const pipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: {
    module: shaderModule,
    entryPoint: 'vs_main',
  },
  fragment: {
    module: shaderModule,
    entryPoint: 'fs_main',
    targets: [{ format }],
  },
})
</code></pre>
<h2>7) Bir kare işleyin</h2>
<pre><code class="language-js">function render() {
  const commandEncoder = device.createCommandEncoder()
  const textureView = context.getCurrentTexture().createView()

  const renderPass = commandEncoder.beginRenderPass({
    colorAttachments: [{
      view: textureView,
      clearValue: { r: 0, g: 0, b: 0, a: 1 },
      loadOp: 'clear',
      storeOp: 'store',
    }],
  })

  renderPass.setPipeline(pipeline)
  renderPass.draw(3)
  renderPass.end()

  device.queue.submit([commandEncoder.finish()])
  requestAnimationFrame(render)
}
render()
</code></pre>
<h2>8) WebGPU'da tamponlar</h2>
<pre><code class="language-js">const vertices = new Float32Array([
  -0.5, -0.5,
   0.5, -0.5,
   0.0,  0.5,
])

const vertexBuffer = device.createBuffer({
  size: vertices.byteLength,
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
})

device.queue.writeBuffer(vertexBuffer, 0, vertices)
</code></pre>
<h2>9) Tekbiçimli değişkenler için bağlama grupları</h2>
<pre><code class="language-js">const uniformBuffer = device.createBuffer({
  size: 64,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})

const bindGroup = device.createBindGroup({
  layout: pipeline.getBindGroupLayout(0),
  entries: [{
    binding: 0,
    resource: { buffer: uniformBuffer },
  }],
})
</code></pre>
<h2>10) WebGPU ve WebGL karşılaştırması</h2>
<table>
<thead>
<tr>
<th>Özellik</th>
<th>WebGL</th>
<th>WebGPU</th>
</tr>
</thead>
<tbody>
<tr>
<td>API tarzı</td>
<td>OpenGL benzeri, durum bilgili</td>
<td>Modern, açık</td>
</tr>
<tr>
<td>Gölgelendiriciler</td>
<td>GLSL</td>
<td>WGSL</td>
</tr>
<tr>
<td>Hesaplama</td>
<td>Sınırlı (uzantılar)</td>
<td>Birinci sınıf destek</td>
</tr>
<tr>
<td>Çoklu iş parçacığı</td>
<td>Zor</td>
<td>Bunun için tasarlandı</td>
</tr>
<tr>
<td>Tarayıcı desteği</td>
<td>Evrensel</td>
<td>Tüm büyük tarayıcılar (Chrome, Edge, Safari 26, Firefox)</td>
</tr>
</tbody>
</table>
<h2>WebGPU ne zaman kullanılmalı?</h2>
<ul>
<li>Modern tarayıcıları hedefleyen yeni projelerde</li>
<li>Yoğun hesaplama gerektiren iş yüklerinde (fizik, yapay zekâ, prosedürel üretim)</li>
<li>GPU kaynakları üzerinde ayrıntılı denetime ihtiyaç duyduğunuzda</li>
</ul>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/webgl-fundamentals-for-games">WebGL'nin temelleri</a></li>
<li><a href="/tr/tutorials/web-workers-game-logic">Oyun mantığı için Web Workers</a></li>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlayın</a></li>
<li><a href="/tr/guides/web-games-stack-2026">2026'da Web Oyunları Teknoloji Yığını</a> — WebGL, WebGPU ve Wasm arasında seçim yapmak için karar çerçevesi</li>
<li><a href="/tr/tutorials/coop-coep-sharedarraybuffer">COOP/COEP ve SharedArrayBuffer</a> — WebGPU hesaplamayla Wasm iş parçacığı kullanımı için gerekli üst bilgiler</li>
</ul>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://www.w3.org/TR/webgpu/">WebGPU belirtimi</a> — W3C belirtimi</li>
<li><a href="https://www.w3.org/TR/WGSL/">WGSL belirtimi</a> — WebGPU Gölgelendirme Dili belirtimi</li>
<li><a href="https://webgpufundamentals.org/">WebGPU'nun Temelleri</a> — ayrıntılı eğitim serisi</li>
<li><a href="https://codelabs.developers.google.com/your-first-webgpu-app">İlk WebGPU uygulamanız</a> — adım adım Google Codelab rehberi</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[WebSocket ile çok oyunculu oyunların temelleri]]></title>
            <link>https://app.cinevva.com/tr/tutorials/websocket-multiplayer-basics</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/websocket-multiplayer-basics</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[WebSocket kullanarak gerçek zamanlı çok oyunculu oyunlar geliştirin: web oyunları için bağlantı yönetimi, mesaj protokolleri ve senkronizasyon kalıpları.]]></description>
            <content:encoded><![CDATA[<h1>WebSocket ile çok oyunculu oyunların temelleri</h1>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;margin:1.5rem 0">
<iframe src="https://www.youtube.com/embed/cXxEiWudIUY" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0" allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture" allowfullscreen></iframe>
</div>
<p style="font-size:0.9rem;color:var(--vp-c-text-2);margin-top:-0.5rem">WebSocket ile çok oyunculu oyun geliştirme: sunucu mimarisi, oyun odaları ve durum senkronizasyonu</p>
<p>WebSocket'ler, çok oyunculu oyunlarda gerçek zamanlı iletişimi mümkün kılar. Bu eğitim, istemci tarafındaki temel konuları ele alır.</p>
<h2>1) Bir sunucuya bağlanma</h2>
<pre><code class="language-js">const ws = new WebSocket('wss://game-server.example.com')

ws.onopen = () =&gt; {
  console.log('Bağlandı')
  ws.send(JSON.stringify({ type: 'join', name: 'Player1' }))
}

ws.onmessage = (event) =&gt; {
  const msg = JSON.parse(event.data)
  handleMessage(msg)
}

ws.onclose = () =&gt; {
  console.log('Bağlantı kesildi')
}

ws.onerror = (err) =&gt; {
  console.error('WebSocket hatası:', err)
}
</code></pre>
<h2>2) Mesaj protokolü tasarımı</h2>
<p>Basit bir protokol tanımlayın:</p>
<pre><code class="language-js">// İstemci -&gt; Sunucu
{ type: 'join', name: 'Player1' }
{ type: 'input', keys: { left: true, jump: true } }
{ type: 'chat', message: 'Merhaba!' }

// Sunucu -&gt; İstemci
{ type: 'state', players: [...], entities: [...] }
{ type: 'playerJoined', id: 'abc', name: 'Player2' }
{ type: 'playerLeft', id: 'abc' }
</code></pre>
<h2>3) Bağlantı yöneticisi sınıfı</h2>
<pre><code class="language-js">class GameConnection {
  constructor(url) {
    this.url = url
    this.ws = null
    this.handlers = new Map()
    this.reconnectDelay = 1000
  }
  
  connect() {
    this.ws = new WebSocket(this.url)
    
    this.ws.onopen = () =&gt; {
      this.reconnectDelay = 1000
      this.emit('connected')
    }
    
    this.ws.onmessage = (e) =&gt; {
      const msg = JSON.parse(e.data)
      this.emit(msg.type, msg)
    }
    
    this.ws.onclose = () =&gt; {
      this.emit('disconnected')
      this.scheduleReconnect()
    }
    
    this.ws.onerror = () =&gt; {
      this.ws.close()
    }
  }
  
  scheduleReconnect() {
    setTimeout(() =&gt; {
      this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000)
      this.connect()
    }, this.reconnectDelay)
  }
  
  send(type, data = {}) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, ...data }))
    }
  }
  
  on(type, handler) {
    if (!this.handlers.has(type)) {
      this.handlers.set(type, [])
    }
    this.handlers.get(type).push(handler)
  }
  
  emit(type, data) {
    const handlers = this.handlers.get(type) || []
    handlers.forEach(h =&gt; h(data))
  }
  
  disconnect() {
    this.ws?.close()
  }
}
</code></pre>
<h2>4) Oyuncu girdilerini gönderme</h2>
<p>Konumları değil, girdileri gönderin. Yetkili taraf sunucu olsun:</p>
<pre><code class="language-js">const connection = new GameConnection('wss://server.example.com')

function sendInput(input) {
  connection.send('input', {
    left: input.left,
    right: input.right,
    jump: input.jump,
    shoot: input.shoot,
    seq: inputSequence++,
  })
}

// Girdiyi sabit bir hızda gönder (ör. saniyede 20 kez)
setInterval(() =&gt; {
  sendInput(currentInput)
}, 50)
</code></pre>
<h2>5) Oyun durumunu alma</h2>
<pre><code class="language-js">let gameState = { players: [], entities: [] }

connection.on('state', (msg) =&gt; {
  gameState = msg
})

connection.on('playerJoined', (msg) =&gt; {
  gameState.players.push({ id: msg.id, name: msg.name })
})

connection.on('playerLeft', (msg) =&gt; {
  gameState.players = gameState.players.filter(p =&gt; p.id !== msg.id)
})
</code></pre>
<h2>6) Akıcı hareket için enterpolasyon</h2>
<p>Sunucu anlık durum görüntüleri gönderir. Bunların arasında enterpolasyon yapın:</p>
<pre><code class="language-js">class EntityInterpolator {
  constructor() {
    this.buffer = [] // { zaman damgası, durum }
    this.renderDelay = 100 // sunucunun ms cinsinden gerisinde
  }
  
  addSnapshot(timestamp, state) {
    this.buffer.push({ timestamp, state })
    
    // Yalnızca son anlık durum görüntülerini tut
    while (this.buffer.length &gt; 10) {
      this.buffer.shift()
    }
  }
  
  getState(currentTime) {
    const renderTime = currentTime - this.renderDelay
    
    // Çevreleyen anlık durum görüntülerini bul
    let before = null
    let after = null
    
    for (let i = 0; i &lt; this.buffer.length - 1; i++) {
      if (this.buffer[i].timestamp &lt;= renderTime &amp;&amp; 
          this.buffer[i + 1].timestamp &gt;= renderTime) {
        before = this.buffer[i]
        after = this.buffer[i + 1]
        break
      }
    }
    
    if (!before || !after) {
      return this.buffer[this.buffer.length - 1]?.state
    }
    
    // Enterpolasyon yap
    const t = (renderTime - before.timestamp) / (after.timestamp - before.timestamp)
    return this.lerp(before.state, after.state, t)
  }
  
  lerp(a, b, t) {
    return {
      x: a.x + (b.x - a.x) * t,
      y: a.y + (b.y - a.y) * t,
    }
  }
}
</code></pre>
<h2>7) İstemci taraflı tahmin</h2>
<p>Kontrollerin hızlı tepki vermesi için yerel olarak tahmin yapın ve sunucuyla uzlaştırın:</p>
<pre><code class="language-js">class PredictedPlayer {
  constructor() {
    this.position = { x: 0, y: 0 }
    this.pendingInputs = []
  }
  
  applyInput(input) {
    // Girdiyi yerel olarak uygula
    if (input.left) this.position.x -= 5
    if (input.right) this.position.x += 5
    
    // Uzlaştırma için sakla
    this.pendingInputs.push({ seq: input.seq, input })
  }
  
  reconcile(serverState, lastProcessedSeq) {
    // Sunucu tarafından doğrulanan konum
    this.position = { ...serverState.position }
    
    // Doğrulanan girdileri kaldır
    this.pendingInputs = this.pendingInputs.filter(i =&gt; i.seq &gt; lastProcessedSeq)
    
    // Doğrulanmamış girdileri yeniden uygula
    for (const pending of this.pendingInputs) {
      if (pending.input.left) this.position.x -= 5
      if (pending.input.right) this.position.x += 5
    }
  }
}
</code></pre>
<h2>8) Gecikme göstergesini yönetme</h2>
<pre><code class="language-js">class LatencyMonitor {
  constructor(connection) {
    this.connection = connection
    this.pingStart = 0
    this.latency = 0
    
    connection.on('pong', () =&gt; {
      this.latency = Date.now() - this.pingStart
    })
    
    setInterval(() =&gt; this.ping(), 1000)
  }
  
  ping() {
    this.pingStart = Date.now()
    this.connection.send('ping')
  }
  
  getLatency() {
    return this.latency
  }
}
</code></pre>
<h2>9) Verimlilik için ikili mesajlar</h2>
<p>Yüksek frekanslı güncellemelerde ikili verileri kullanın:</p>
<pre><code class="language-js">// İkili veri gönderme
const buffer = new ArrayBuffer(12)
const view = new DataView(buffer)
view.setFloat32(0, player.x)
view.setFloat32(4, player.y)
view.setUint32(8, inputFlags)
ws.send(buffer)

// İkili veri alma
ws.binaryType = 'arraybuffer'
ws.onmessage = (e) =&gt; {
  if (e.data instanceof ArrayBuffer) {
    const view = new DataView(e.data)
    const x = view.getFloat32(0)
    const y = view.getFloat32(4)
    // ...
  }
}
</code></pre>
<h2>10) Temel sunucu örneği (Node.js)</h2>
<pre><code class="language-js">import { WebSocketServer } from 'ws'

const wss = new WebSocketServer({ port: 8080 })
const players = new Map()

wss.on('connection', (ws) =&gt; {
  const id = crypto.randomUUID()
  players.set(id, { id, x: 0, y: 0, ws })
  
  ws.on('message', (data) =&gt; {
    const msg = JSON.parse(data)
    
    if (msg.type === 'input') {
      const player = players.get(id)
      if (msg.left) player.x -= 5
      if (msg.right) player.x += 5
    }
  })
  
  ws.on('close', () =&gt; {
    players.delete(id)
    broadcast({ type: 'playerLeft', id })
  })
})

// Durumu saniyede 20 kez yayınla
setInterval(() =&gt; {
  const state = {
    type: 'state',
    players: Array.from(players.values()).map(p =&gt; ({
      id: p.id, x: p.x, y: p.y
    }))
  }
  broadcast(state)
}, 50)

function broadcast(msg) {
  const data = JSON.stringify(msg)
  for (const player of players.values()) {
    player.ws.send(data)
  }
}
</code></pre>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/ship-web-game-fast">Hızlı yüklenen bir web oyunu yayınlayın</a></li>
<li><a href="/tr/tutorials/game-input-handling">Oyun girdilerini yönetme</a></li>
<li><a href="/tr/tutorials/web-workers-game-logic">Oyun mantığı için Web Workers</a></li>
<li><a href="/tr/tutorials/coop-coep-sharedarraybuffer">COOP/COEP ve SharedArrayBuffer</a> — çok oyunculu bağlamlarda SharedArrayBuffer için gerekli başlıklar</li>
<li><a href="/tr/guides/co-op-game-design">2026'da eşli oyun tasarımı</a> — &quot;friend slop&quot; eşli oyunların neden hızla yayıldığı ve tarayıcı tabanlı çok oyunculu oyunların bu alandaki yeri</li>
<li><a href="/tr/guides/game-jams-hackathons">Game Jam'ler ve Hackathon'lar</a> — çok oyunculu jam oyunları daha fazla ilgi görür</li>
</ul>
<h2>Harici kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API">MDN: WebSocket API'si</a> — tarayıcı WebSocket referansı</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API">MDN: WebRTC API'si</a> — daha düşük gecikme için eşler arası bağlantılar</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebTransport_API">MDN: WebTransport API'si</a> — Safari 26.4'ün destek sunmasıyla Mart 2026'da tarayıcılar arası Baseline düzeyine ulaşan daha yeni bir HTTP/3 aktarım teknolojisi (Chrome 97+, Edge ve Firefox 114+ sürümlerinde zaten destekleniyordu). TCP tabanlı tek bir WebSocket bağlantısının aksine WebTransport, QUIC üzerinden çalışır; güvenilir olmayan datagramların yanı sıra birbirinden bağımsız birden fazla akış sunar. Böylece düşen tek bir paket, diğer tüm verileri sıra başı engellemesine maruz bırakmaz. Yaygın bir yaklaşım, yüksek frekanslı konum güncellemelerini datagramlar üzerinden gönderirken sohbeti veya skor tablolarını güvenilir bir akışta tutmaktır. WebSocket'ler hâlâ en uyumlu seçenektir ve yukarıdaki her şey geçerliliğini korur; ancak gecikmeye duyarlı oyunlarda artık WebTransport da değerlendirilmeye değer.</li>
<li><a href="https://socket.io/docs/v4/">Socket.IO belgeleri</a> — popüler WebSocket sarmalayıcı kütüphanesi</li>
<li><a href="https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking">Valve Source Çok Oyunculu Ağ İletişimi</a> — yayınlanmış oyunlarda kullanılan yetkili ağ iletişimi kalıpları</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[VR oyunları için WebXR temelleri]]></title>
            <link>https://app.cinevva.com/tr/tutorials/webxr-basics</link>
            <guid isPermaLink="true">https://app.cinevva.com/tr/tutorials/webxr-basics</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[WebXR'ı kullanmaya başlayın: oturum kurulumu, görüntü oluşturma, kontrolcüler ve etkileşimler. Tarayıcıda çalışan VR deneyimleri oluşturun.]]></description>
            <content:encoded><![CDATA[<h1>VR oyunları için WebXR temelleri</h1>
<p>WebXR, VR ve AR'ı tarayıcıya getirir. Oyuncular hiçbir şey indirmeden oyununuzu bir başlıkla deneyimleyebilir.</p>
<h2>1) WebXR desteğini kontrol etme</h2>
<pre><code class="language-js">async function checkXRSupport() {
  if (!navigator.xr) {
    return { vr: false, ar: false }
  }
  
  const vr = await navigator.xr.isSessionSupported('immersive-vr')
  const ar = await navigator.xr.isSessionSupported('immersive-ar')
  
  return { vr, ar }
}

// Kullanım
const support = await checkXRSupport()
if (support.vr) {
  showVRButton()
}
</code></pre>
<h2>2) VR oturumu başlatma</h2>
<pre><code class="language-js">let xrSession = null
let xrRefSpace = null

async function startVR() {
  try {
    xrSession = await navigator.xr.requestSession('immersive-vr', {
      requiredFeatures: ['local-floor'],
      optionalFeatures: ['hand-tracking'],
    })
    
    xrSession.addEventListener('end', onSessionEnd)
    
    // Görüntü oluşturmayı ayarla
    const gl = canvas.getContext('webgl2', { xrCompatible: true })
    await xrSession.updateRenderState({
      baseLayer: new XRWebGLLayer(xrSession, gl),
    })
    
    // Referans uzayını al
    xrRefSpace = await xrSession.requestReferenceSpace('local-floor')
    
    // Görüntü oluşturma döngüsünü başlat
    xrSession.requestAnimationFrame(onXRFrame)
  } catch (err) {
    console.error('VR başlatılamadı:', err)
  }
}

function onSessionEnd() {
  xrSession = null
  xrRefSpace = null
}
</code></pre>
<h2>3) XR görüntü oluşturma döngüsü</h2>
<pre><code class="language-js">function onXRFrame(time, frame) {
  const session = frame.session
  session.requestAnimationFrame(onXRFrame)
  
  const pose = frame.getViewerPose(xrRefSpace)
  if (!pose) return
  
  const glLayer = session.renderState.baseLayer
  gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer)
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  
  // Her göz için görüntü oluştur
  for (const view of pose.views) {
    const viewport = glLayer.getViewport(view)
    gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height)
    
    // Kamera matrislerini al
    const viewMatrix = view.transform.inverse.matrix
    const projectionMatrix = view.projectionMatrix
    
    // Sahneyi bu matrislerle oluştur
    renderScene(viewMatrix, projectionMatrix)
  }
}
</code></pre>
<h2>4) Giriş kaynakları (kontrolcüler)</h2>
<pre><code class="language-js">xrSession.addEventListener('inputsourceschange', (e) =&gt; {
  for (const source of e.added) {
    console.log('Kontrolcü eklendi:', source.handedness) // 'left' veya 'right'
  }
  for (const source of e.removed) {
    console.log('Kontrolcü kaldırıldı:', source.handedness)
  }
})

function processInput(frame) {
  for (const source of xrSession.inputSources) {
    if (!source.gamepad) continue
    
    const gamepad = source.gamepad
    
    // Tetik (genellikle indeks 0)
    const triggerPressed = gamepad.buttons[0]?.pressed
    const triggerValue = gamepad.buttons[0]?.value || 0
    
    // Kavrama (genellikle indeks 1)
    const gripPressed = gamepad.buttons[1]?.pressed
    
    // Başparmak çubuğu
    const thumbstickX = gamepad.axes[2] || 0
    const thumbstickY = gamepad.axes[3] || 0
    
    // A/B düğmeleri (indeks 4, 5)
    const buttonA = gamepad.buttons[4]?.pressed
    const buttonB = gamepad.buttons[5]?.pressed
  }
}
</code></pre>
<h3>Her cihazda gamepad bulunduğunu varsaymayın</h3>
<p>Yukarıdaki kod, tetikler ve başparmak çubukları için <code>source.gamepad</code> değerini okur. Bu, Meta Quest gibi kontrolcülü başlıklarda çalışır ancak birincil girişin bakış ve parmak kıstırma ya da el takibi olduğu Apple Vision Pro'da (WebXR, visionOS 2 Safari'de varsayılan olarak etkindir), Samsung Galaxy XR'da ve diğer Android XR cihazlarında hiçbir şey yapmaz. Bu başlıklarda <code>source.gamepad</code> null değerindedir ve kullanıcı gerçekten parmaklarını kıstırana kadar <code>session.inputSources</code> boş kalır.</p>
<p>Her cihazın garanti ettiği tek etkileşim olan “birincil eylem” için düğmeleri sürekli denetlemek yerine <code>select</code> olaylarını dinleyin. WebXR; bir kontrolcü tetiği, el hareketiyle parmak kıstırma ve Vision Pro'daki bakış ve parmak kıstırma için aynı şekilde <code>selectstart</code>, <code>select</code> ve <code>selectend</code> olaylarını gönderir. Böylece tek bir işleyici bunların tümünü kapsar:</p>
<pre><code class="language-js">xrSession.addEventListener('selectstart', (e) =&gt; {
  // e.inputSource kontrolcü, el veya geçici parmak kıstırma girişidir
  // e.frame, targetRaySpace pozunu okumanızı sağlar
})
xrSession.addEventListener('select', (e) =&gt; {
  // birincil eylem tamamlandı (tetik çekildi, hedef üzerindeki parmak kıstırma bırakıldı)
})
xrSession.addEventListener('selectend', (e) =&gt; {})
</code></pre>
<p>Kontrolcülü başlıklarda başparmak çubuğuyla hareket ve ek düğmeler için gamepad kodunu koruyun ancak bunu <code>if (source.gamepad)</code> koşulunun arkasına alın. Temel “seç/kavra/etkinleştir” etkileşiminizi bu olaylar üzerinden yönlendirin; böylece oyun el ve bakışla kontrol edilen cihazlarda da çalışır.</p>
<h2>5) Kontrolcü konumları</h2>
<pre><code class="language-js">function getControllerPose(frame, source) {
  if (!source.gripSpace) return null
  
  const pose = frame.getPose(source.gripSpace, xrRefSpace)
  if (!pose) return null
  
  return {
    position: pose.transform.position,
    orientation: pose.transform.orientation,
    matrix: pose.transform.matrix,
  }
}

function renderControllers(frame) {
  for (const source of xrSession.inputSources) {
    const pose = getControllerPose(frame, source)
    if (pose) {
      renderControllerModel(pose, source.handedness)
    }
  }
}
</code></pre>
<h2>6) İşaretleme için ışın gönderimi</h2>
<pre><code class="language-js">function getControllerRay(frame, source) {
  if (!source.targetRaySpace) return null
  
  const pose = frame.getPose(source.targetRaySpace, xrRefSpace)
  if (!pose) return null
  
  const origin = pose.transform.position
  const direction = {
    x: -pose.transform.matrix[8],
    y: -pose.transform.matrix[9],
    z: -pose.transform.matrix[10],
  }
  
  return { origin, direction }
}

function checkRayIntersection(ray, objects) {
  let closest = null
  let closestDist = Infinity
  
  for (const obj of objects) {
    const dist = rayIntersectBox(ray, obj.boundingBox)
    if (dist !== null &amp;&amp; dist &lt; closestDist) {
      closest = obj
      closestDist = dist
    }
  }
  
  return closest
}
</code></pre>
<h2>7) Işınlanarak hareket</h2>
<pre><code class="language-js">class TeleportSystem {
  constructor() {
    this.targetPosition = null
    this.isAiming = false
  }
  
  update(frame, inputSources) {
    for (const source of inputSources) {
      if (source.handedness !== 'left') continue
      
      const gamepad = source.gamepad
      const thumbstickY = gamepad?.axes[3] || 0
      
      if (thumbstickY &lt; -0.5) {
        // Nişan alma
        this.isAiming = true
        const ray = getControllerRay(frame, source)
        this.targetPosition = this.findTeleportTarget(ray)
      } else if (this.isAiming) {
        // Bırak — ışınlan
        if (this.targetPosition) {
          player.position.x = this.targetPosition.x
          player.position.z = this.targetPosition.z
        }
        this.isAiming = false
        this.targetPosition = null
      }
    }
  }
  
  findTeleportTarget(ray) {
    // Zemin düzlemiyle kesiştir
    if (ray.direction.y &gt;= 0) return null
    
    const t = -ray.origin.y / ray.direction.y
    if (t &lt; 0 || t &gt; 10) return null
    
    return {
      x: ray.origin.x + ray.direction.x * t,
      y: 0,
      z: ray.origin.z + ray.direction.z * t,
    }
  }
  
  render() {
    if (this.isAiming &amp;&amp; this.targetPosition) {
      renderTeleportMarker(this.targetPosition)
    }
  }
}
</code></pre>
<h2>8) Akıcı hareket</h2>
<pre><code class="language-js">function updateSmoothLocomotion(frame, inputSources, dt) {
  for (const source of inputSources) {
    if (source.handedness !== 'left') continue
    
    const gamepad = source.gamepad
    if (!gamepad) continue
    
    const moveX = gamepad.axes[2] || 0
    const moveY = gamepad.axes[3] || 0
    
    // Ölü bölge uygula
    if (Math.abs(moveX) &lt; 0.1) moveX = 0
    if (Math.abs(moveY) &lt; 0.1) moveY = 0
    
    // Hareket için baş yönünü al
    const pose = frame.getViewerPose(xrRefSpace)
    if (!pose) continue
    
    const headMatrix = pose.transform.matrix
    const forward = { x: -headMatrix[8], z: -headMatrix[10] }
    const right = { x: headMatrix[0], z: headMatrix[2] }
    
    // XZ düzlemine göre normalleştir
    const len = Math.sqrt(forward.x ** 2 + forward.z ** 2)
    forward.x /= len
    forward.z /= len
    
    const speed = 3 * dt
    player.position.x += (forward.x * -moveY + right.x * moveX) * speed
    player.position.z += (forward.z * -moveY + right.z * moveX) * speed
  }
}
</code></pre>
<h2>9) VR konfor yönergeleri</h2>
<pre><code class="language-js">// Hareket hastalığını azaltmak için hareket sırasında vinyet
function renderComfortVignette(movementSpeed) {
  const intensity = Math.min(movementSpeed / 5, 0.5)
  if (intensity &lt; 0.1) return
  
  // Hızla birlikte koyulaşan kenarları oluştur
  renderVignette(intensity)
}

// Kademeli dönüş
let snapTurnCooldown = 0

function handleSnapTurn(gamepad, dt) {
  snapTurnCooldown -= dt
  
  const thumbstickX = gamepad.axes[2] || 0
  
  if (Math.abs(thumbstickX) &gt; 0.7 &amp;&amp; snapTurnCooldown &lt;= 0) {
    const angle = Math.sign(thumbstickX) * (Math.PI / 4) // 45 derece
    player.rotation += angle
    snapTurnCooldown = 0.3 // Hızlı dönüşleri önle
  }
}
</code></pre>
<h2>10) Eksiksiz WebXR kurulumu</h2>
<pre><code class="language-js">class VRGame {
  constructor(canvas) {
    this.canvas = canvas
    this.gl = canvas.getContext('webgl2', { xrCompatible: true })
    this.session = null
    this.refSpace = null
    this.teleport = new TeleportSystem()
  }
  
  async checkSupport() {
    if (!navigator.xr) return false
    return await navigator.xr.isSessionSupported('immersive-vr')
  }
  
  async start() {
    this.session = await navigator.xr.requestSession('immersive-vr', {
      requiredFeatures: ['local-floor'],
    })
    
    await this.session.updateRenderState({
      baseLayer: new XRWebGLLayer(this.session, this.gl),
    })
    
    this.refSpace = await this.session.requestReferenceSpace('local-floor')
    this.session.addEventListener('end', () =&gt; this.onEnd())
    this.session.requestAnimationFrame((t, f) =&gt; this.onFrame(t, f))
  }
  
  onFrame(time, frame) {
    this.session.requestAnimationFrame((t, f) =&gt; this.onFrame(t, f))
    
    const dt = (time - this.lastTime) / 1000
    this.lastTime = time
    
    // Oyun mantığını güncelle
    this.teleport.update(frame, this.session.inputSources)
    
    // Görüntüyü oluştur
    const pose = frame.getViewerPose(this.refSpace)
    if (!pose) return
    
    const glLayer = this.session.renderState.baseLayer
    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, glLayer.framebuffer)
    this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT)
    
    for (const view of pose.views) {
      const vp = glLayer.getViewport(view)
      this.gl.viewport(vp.x, vp.y, vp.width, vp.height)
      this.render(view.transform.inverse.matrix, view.projectionMatrix)
    }
    
    this.teleport.render()
  }
  
  render(viewMatrix, projectionMatrix) {
    // Görüntü oluşturma kodunuz buraya gelir
  }
  
  onEnd() {
    this.session = null
  }
}
</code></pre>
<h2>İlgili içerikler</h2>
<ul>
<li><a href="/tr/tutorials/webgl-fundamentals-for-games">WebGL temelleri</a></li>
<li><a href="/tr/tutorials/webgpu-getting-started">WebGPU'ya başlangıç</a></li>
<li><a href="/tr/tutorials/game-input-handling">Oyun girişlerini işleme</a></li>
<li><a href="/tr/guides/web-games-stack-2026">2026'da Web Oyunları Teknoloji Yığını</a> — WebXR'ın daha geniş teknoloji dünyasındaki yeri</li>
<li><a href="/tr/guides/threejs-usdc-tech-report">Tarayıcıda Three.js + USDC</a> — XR sahneleri için 3D varlıkları yükleme</li>
</ul>
<h2>Harici Kaynaklar</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API">MDN: WebXR Device API</a> — eksiksiz API referansı</li>
<li><a href="https://immersiveweb.dev/">Immersive Web</a> — WebXR örnekleri, araçları ve tarayıcı destek tabloları</li>
<li><a href="https://threejs.org/docs/#manual/en/introduction/How-to-create-VR-content">Three.js WebXR rehberi</a> — Three.js ile VR içerikleri</li>
</ul>
]]></content:encoded>
        </item>
    </channel>
</rss>
