WebAssembly ile 2D Oyun Motoru Oluşturma

Oyun geliştirme dünyası artık sadece masaüstü veya mobil platformlarla sınırlı değil. Tarayıcı üzerinde çalışan, native performansa yakın hız sunan oyunlar artık bir hayal değil. WebAssembly’nin bize verdiği güç sayesinde, C++ ile yazdığımız bir 2D oyun motoru doğrudan tarayıcıda koşabiliyor. Ben de bir sistem yöneticisi olarak bu konuya biraz farklı açıdan bakıyorum: altyapı, deployment pipeline’ı ve performans optimizasyonu. Bu yazıda sıfırdan bir 2D oyun motoru inşa edip, bunu WebAssembly ile tarayıcıya taşıyacağız.

Neden WebAssembly ile Oyun Motoru?

JavaScript ile oyun yazmak mümkün ama gerçek zamanlı fizik hesaplamaları, sprite rendering ve collision detection gibi yoğun işlemler söz konusu olduğunda JS’in tek thread yapısı ve garbage collector gecikmesi ciddi darboğazlar yaratıyor. WebAssembly ise derlenmiş binary format kullandığı için bu hesaplamaları çok daha verimli yapıyor.

Gerçek dünyadan bir örnek verelim: Bir indie oyun stüdyosunun “Steam’e ek olarak tarayıcıda da çalışsın” dediği durumu düşünün. Emscripten toolchain ile C++ kodunu WASM’a derleyip, aynı kod tabanını hem masaüstü hem web için kullanabilirsiniz. Bu hem maliyet hem de maintainability açısından çok mantıklı.

Geliştirme Ortamını Hazırlama

Önce toolchain’i kuralım. Emscripten SDK olmadan bu iş yürümez.

# Emscripten SDK kurulumu
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk

# En son versiyonu çek ve aktif et
./emsdk install latest
./emsdk activate latest

# Environment variable'ları ayarla
source ./emsdk_env.sh

# Kurulumu doğrula
emcc --version

# SDL2 bağımlılıklarını kontrol et
emcc -s USE_SDL=2 --version

Sisteminizde ayrıca CMake ve Python3 olması gerekiyor. Ubuntu tabanlı sistemlerde:

sudo apt update && sudo apt install -y 
    cmake 
    python3 
    python3-pip 
    build-essential 
    libsdl2-dev 
    git

# Node.js ile local test sunucusu
npm install -g serve

Proje Yapısını Oluşturma

İyi bir proje yapısı, ileride başınızı ağrıtmaz. Özellikle hem native hem WASM hedefi varsa build sistemi kritik önem taşıyor.

mkdir -p wasm-engine/{src,assets,web,build-native,build-wasm}
cd wasm-engine

# Dizin yapısı
tree .
# ├── src/
# │   ├── engine/
# │   │   ├── renderer.cpp
# │   │   ├── physics.cpp
# │   │   ├── input.cpp
# │   │   └── game_loop.cpp
# │   ├── game/
# │   │   └── main.cpp
# │   └── CMakeLists.txt
# ├── assets/
# │   ├── sprites/
# │   └── sounds/
# └── web/
#     └── index.html

CMakeLists.txt dosyasını oluşturalım:

cat > src/CMakeLists.txt << 'EOF'
cmake_minimum_required(VERSION 3.16)
project(WasmEngine)

set(CMAKE_CXX_STANDARD 17)

# Platform tespiti
if(EMSCRIPTEN)
    message(STATUS "Building for WebAssembly target")
    set(CMAKE_EXECUTABLE_SUFFIX ".js")
    
    set(EMCC_FLAGS
        "-s USE_SDL=2"
        "-s USE_SDL_IMAGE=2"
        "-s SDL2_IMAGE_FORMATS='["png","jpg"]'"
        "-s WASM=1"
        "-s ALLOW_MEMORY_GROWTH=1"
        "-s MAXIMUM_MEMORY=256MB"
        "-s EXPORTED_FUNCTIONS=['_main','_update_game_state']"
        "-s EXPORTED_RUNTIME_METHODS=['ccall','cwrap']"
        "--preload-file ../assets@/assets"
    )
    
    string(REPLACE ";" " " EMCC_FLAGS_STR "${EMCC_FLAGS}")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${EMCC_FLAGS_STR}")
else()
    find_package(SDL2 REQUIRED)
endif()

file(GLOB_RECURSE SOURCES "*.cpp")

add_executable(wasm_engine ${SOURCES})

if(EMSCRIPTEN)
    target_compile_options(wasm_engine PRIVATE -O3)
else()
    target_link_libraries(wasm_engine SDL2::SDL2)
endif()
EOF

Oyun Motorunun Çekirdeği

Şimdi asıl işe gelelim. Game loop yapısı her oyun motorunun kalbidir. Fixed timestep kullanmak, farklı cihazlarda tutarlı davranış sağlar.

// src/engine/game_loop.cpp
#include <SDL2/SDL.h>
#include <functional>
#include <chrono>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif

class GameLoop {
private:
    static const int TARGET_FPS = 60;
    static const int FIXED_TIMESTEP_MS = 1000 / TARGET_FPS;
    
    bool running;
    Uint32 lastTime;
    Uint32 accumulator;
    
    std::function<void(float)> updateCallback;
    std::function<void()> renderCallback;

public:
    GameLoop() : running(false), lastTime(0), accumulator(0) {}
    
    void setUpdateCallback(std::function<void(float)> cb) {
        updateCallback = cb;
    }
    
    void setRenderCallback(std::function<void()> cb) {
        renderCallback = cb;
    }
    
    static void mainLoopCallback(void* arg) {
        GameLoop* loop = static_cast<GameLoop*>(arg);
        loop->tick();
    }
    
    void tick() {
        Uint32 currentTime = SDL_GetTicks();
        Uint32 deltaTime = currentTime - lastTime;
        lastTime = currentTime;
        
        // Delta time'i cap'le, debug breakpoint durumlarinda
        // buyuk spike'lari onlemek icin
        if (deltaTime > 250) deltaTime = 250;
        
        accumulator += deltaTime;
        
        while (accumulator >= FIXED_TIMESTEP_MS) {
            if (updateCallback) {
                updateCallback(FIXED_TIMESTEP_MS / 1000.0f);
            }
            accumulator -= FIXED_TIMESTEP_MS;
        }
        
        if (renderCallback) {
            renderCallback();
        }
    }
    
    void start() {
        running = true;
        lastTime = SDL_GetTicks();
        
#ifdef __EMSCRIPTEN__
        // WASM'da blocking loop kullanamayiz
        // requestAnimationFrame benzeri calisir
        emscripten_set_main_loop_arg(mainLoopCallback, this, 0, 1);
#else
        while (running) {
            tick();
            SDL_Delay(1); // CPU'yu bosalt
        }
#endif
    }
    
    void stop() { running = false; }
};

Renderer Sistemi

2D renderer için SDL2’nin texture API’sini kullanacağız. Sprite batching önemli bir optimizasyon tekniğidir.

// src/engine/renderer.cpp
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <unordered_map>
#include <string>
#include <vector>

struct Sprite {
    SDL_Texture* texture;
    int width;
    int height;
};

struct DrawCommand {
    SDL_Texture* texture;
    SDL_Rect src;
    SDL_Rect dst;
    double angle;
    Uint8 alpha;
};

class Renderer {
private:
    SDL_Window* window;
    SDL_Renderer* sdlRenderer;
    std::unordered_map<std::string, Sprite> textureCache;
    std::vector<DrawCommand> drawQueue;
    
    // Render istatistikleri
    int drawCallCount;
    int cachedTextureCount;

public:
    Renderer(const char* title, int width, int height) 
        : drawCallCount(0), cachedTextureCount(0) {
        
        window = SDL_CreateWindow(
            title,
            SDL_WINDOWPOS_CENTERED,
            SDL_WINDOWPOS_CENTERED,
            width, height,
            SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE
        );
        
        sdlRenderer = SDL_CreateRenderer(
            window, -1,
            SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
        );
        
        // Blend mode varsayilan ayar
        SDL_SetRenderDrawBlendMode(sdlRenderer, SDL_BLENDMODE_BLEND);
        
        IMG_Init(IMG_INIT_PNG | IMG_INIT_JPG);
    }
    
    Sprite* loadSprite(const std::string& path) {
        // Cache hit kontrolu
        auto it = textureCache.find(path);
        if (it != textureCache.end()) {
            return &it->second;
        }
        
        SDL_Surface* surface = IMG_Load(path.c_str());
        if (!surface) {
            SDL_Log("Sprite yuklenemedi: %s", path.c_str());
            return nullptr;
        }
        
        Sprite sprite;
        sprite.texture = SDL_CreateTextureFromSurface(sdlRenderer, surface);
        sprite.width = surface->w;
        sprite.height = surface->h;
        SDL_FreeSurface(surface);
        
        textureCache[path] = sprite;
        cachedTextureCount++;
        
        return &textureCache[path];
    }
    
    void queueDraw(Sprite* sprite, int x, int y, 
                   float scaleX = 1.0f, float scaleY = 1.0f,
                   double angle = 0.0, Uint8 alpha = 255) {
        
        DrawCommand cmd;
        cmd.texture = sprite->texture;
        cmd.src = {0, 0, sprite->width, sprite->height};
        cmd.dst = {
            x, y,
            static_cast<int>(sprite->width * scaleX),
            static_cast<int>(sprite->height * scaleY)
        };
        cmd.angle = angle;
        cmd.alpha = alpha;
        
        drawQueue.push_back(cmd);
    }
    
    void render() {
        SDL_SetRenderDrawColor(sdlRenderer, 20, 20, 30, 255);
        SDL_RenderClear(sdlRenderer);
        
        // Queue'daki tum draw command'lari isle
        drawCallCount = 0;
        for (const auto& cmd : drawQueue) {
            SDL_SetTextureAlphaMod(cmd.texture, cmd.alpha);
            SDL_RenderCopyEx(
                sdlRenderer, cmd.texture,
                &cmd.src, &cmd.dst,
                cmd.angle, nullptr,
                SDL_FLIP_NONE
            );
            drawCallCount++;
        }
        
        drawQueue.clear();
        SDL_RenderPresent(sdlRenderer);
    }
    
    int getDrawCallCount() const { return drawCallCount; }
    
    ~Renderer() {
        for (auto& pair : textureCache) {
            SDL_DestroyTexture(pair.second.texture);
        }
        SDL_DestroyRenderer(sdlRenderer);
        SDL_DestroyWindow(window);
        IMG_Quit();
    }
};

Fizik ve Collision Sistemi

Basit AABB (Axis-Aligned Bounding Box) collision sistemi kuralım. Platformer oyunlar için yeterince güçlü.

// src/engine/physics.cpp
#include <vector>
#include <functional>
#include <cmath>

struct Vec2 {
    float x, y;
    Vec2(float x = 0, float y = 0) : x(x), y(y) {}
    Vec2 operator+(const Vec2& o) const { return {x + o.x, y + o.y}; }
    Vec2 operator*(float s) const { return {x * s, y * s}; }
};

struct AABB {
    float x, y, w, h;
    
    bool intersects(const AABB& other) const {
        return x < other.x + other.w &&
               x + w > other.x &&
               y < other.y + other.h &&
               y + h > other.y;
    }
    
    // Penetrasyon vektorunu hesapla
    Vec2 getPenetration(const AABB& other) const {
        float overlapX = std::min(x + w, other.x + other.w) - 
                         std::max(x, other.x);
        float overlapY = std::min(y + h, other.y + other.h) - 
                         std::max(y, other.y);
        
        if (overlapX < overlapY)
            return {overlapX * (x < other.x ? -1.0f : 1.0f), 0};
        else
            return {0, overlapY * (y < other.y ? -1.0f : 1.0f)};
    }
};

class PhysicsBody {
public:
    Vec2 position;
    Vec2 velocity;
    Vec2 acceleration;
    AABB bounds;
    bool isStatic;
    bool isGrounded;
    float mass;
    float friction;
    
    static const float GRAVITY;
    
    PhysicsBody(float x, float y, float w, float h, bool isStatic = false)
        : position(x, y), velocity(0, 0), acceleration(0, 0),
          isStatic(isStatic), isGrounded(false), mass(1.0f), friction(0.85f) {
        bounds = {x, y, w, h};
    }
    
    void update(float dt) {
        if (isStatic) return;
        
        // Yerçekimi uygula
        acceleration.y += GRAVITY;
        
        // Euler entegrasyonu
        velocity = velocity + acceleration * dt;
        
        // Yatay friction
        velocity.x *= friction;
        
        // Velocity limit
        const float MAX_VELOCITY = 800.0f;
        if (std::abs(velocity.y) > MAX_VELOCITY)
            velocity.y = velocity.y > 0 ? MAX_VELOCITY : -MAX_VELOCITY;
        
        position = position + velocity * dt;
        
        // Bounds'u pozisyonla güncelle
        bounds.x = position.x;
        bounds.y = position.y;
        
        // Accelerasyonu sifirla (kuvvetler her frame uygulanir)
        acceleration = {0, 0};
        isGrounded = false;
    }
    
    void applyForce(Vec2 force) {
        if (!isStatic) {
            acceleration.x += force.x / mass;
            acceleration.y += force.y / mass;
        }
    }
};

const float PhysicsBody::GRAVITY = 980.0f; // pixels/s^2

Build Pipeline ve Deployment

İşte sysadmin tarafı burada devreye giriyor. CI/CD pipeline ile otomatik build ve deployment.

#!/bin/bash
# build.sh - Hem native hem WASM icin build scripti

set -euo pipefail

PROJECT_ROOT=$(dirname "$0")
BUILD_TYPE="${1:-wasm}"  # native veya wasm

echo "=== WasmEngine Build Pipeline ==="
echo "Build hedefi: $BUILD_TYPE"

build_wasm() {
    echo "[WASM] Emscripten environment yukleniyor..."
    source "$HOME/emsdk/emsdk_env.sh" 2>/dev/null
    
    mkdir -p "$PROJECT_ROOT/build-wasm"
    cd "$PROJECT_ROOT/build-wasm"
    
    emcmake cmake ../src 
        -DCMAKE_BUILD_TYPE=Release 
        -DCMAKE_CXX_FLAGS="-O3 -flto"
    
    emmake make -j$(nproc)
    
    # Output dosyalarini web dizinine kopyala
    cp wasm_engine.js wasm_engine.wasm "$PROJECT_ROOT/web/"
    
    # Asset'leri gzip ile sikistir
    find "$PROJECT_ROOT/web" -name "*.wasm" -exec gzip -9 -k {} ;
    
    echo "[WASM] Build tamamlandi!"
    ls -lh "$PROJECT_ROOT/web/"
}

build_native() {
    echo "[NATIVE] Native build basliyor..."
    
    mkdir -p "$PROJECT_ROOT/build-native"
    cd "$PROJECT_ROOT/build-native"
    
    cmake ../src -DCMAKE_BUILD_TYPE=Release
    make -j$(nproc)
    
    echo "[NATIVE] Build tamamlandi: $PROJECT_ROOT/build-native/wasm_engine"
}

# Asset optimizasyonu
optimize_assets() {
    echo "[ASSETS] Sprite optimizasyonu..."
    
    find "$PROJECT_ROOT/assets" -name "*.png" | while read -r img; do
        if command -v pngquant &>/dev/null; then
            pngquant --force --quality=80-95 --output "$img" "$img"
        fi
    done
    
    echo "[ASSETS] Optimizasyon tamamlandi"
}

case "$BUILD_TYPE" in
    wasm)   optimize_assets && build_wasm ;;
    native) build_native ;;
    all)    optimize_assets && build_native && build_wasm ;;
    *)      echo "Gecersiz hedef: $BUILD_TYPE"; exit 1 ;;
esac

echo "=== Build Basarili ==="

HTML Shell ve JavaScript Entegrasyonu

WASM modülünü tarayıcıda ayağa kaldırmak için HTML shell gerekiyor. Performans metrikleri göstermek de işimize yarayacak.

<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WASM 2D Engine</title>
    <style>
        body { 
            margin: 0; 
            background: #141420; 
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            font-family: monospace;
            color: #ccc;
        }
        #canvas { 
            border: 2px solid #333;
            display: block;
        }
        #hud {
            padding: 8px 16px;
            background: rgba(0,0,0,0.7);
            border-radius: 4px;
            margin-top: 8px;
            font-size: 12px;
        }
        #loading {
            color: #7af;
            font-size: 18px;
        }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
    <div id="hud">FPS: <span id="fps">0</span> | Draw Calls: <span id="drawcalls">0</span> | Memory: <span id="memory">0</span> MB</div>
    <p id="loading">Engine yukleniyor...</p>

    <script>
        // Emscripten Module konfigurasyonu
        var Module = {
            canvas: (function() {
                var canvas = document.getElementById('canvas');
                canvas.addEventListener("webglcontextlost", function(e) { 
                    alert('WebGL context kayboldu!'); 
                    e.preventDefault(); 
                }, false);
                return canvas;
            })(),
            
            onRuntimeInitialized: function() {
                document.getElementById('loading').style.display = 'none';
                console.log('WASM Engine hazir!');
                
                // C++ tarafindaki fonksiyona JS'den erisim
                const getEngineStats = Module.cwrap(
                    'get_engine_stats', 'string', []
                );
                
                // HUD guncelleme
                setInterval(function() {
                    try {
                        const stats = JSON.parse(getEngineStats());
                        document.getElementById('fps').textContent = stats.fps;
                        document.getElementById('drawcalls').textContent = stats.drawCalls;
                        
                        // WASM bellek kullanimi
                        const memMB = (Module.HEAP8.byteLength / 1024 / 1024).toFixed(1);
                        document.getElementById('memory').textContent = memMB;
                    } catch(e) {}
                }, 500);
            },
            
            print: function(text) { console.log('[ENGINE]', text); },
            printErr: function(text) { console.error('[ENGINE ERROR]', text); },
            
            // WASM dosyasi yuklenirken progress goster
            setStatus: function(text) {
                if (text) document.getElementById('loading').textContent = text;
            },
            
            monitorRunDependencies: function(left) {
                if (left == 0) {
                    document.getElementById('loading').textContent = 
                        'Hazir! Baslatiyor...';
                }
            }
        };
        
        // Klavye olaylarini WASM'a ilet
        document.addEventListener('keydown', function(e) {
            if (Module._handle_keydown) {
                Module._handle_keydown(e.keyCode);
            }
            // Ok tuslari icin scroll'u engelle
            if ([32, 37, 38, 39, 40].includes(e.keyCode)) {
                e.preventDefault();
            }
        });
        
        document.addEventListener('keyup', function(e) {
            if (Module._handle_keyup) {
                Module._handle_keyup(e.keyCode);
            }
        });
    </script>
    <script src="wasm_engine.js"></script>
</body>
</html>

Nginx ile Production Deployment

WASM dosyalarının doğru MIME type ve header’larla servis edilmesi gerekiyor. Aksi halde streaming compilation çalışmaz.

# /etc/nginx/sites-available/wasm-game.conf

server {
    listen 80;
    listen [::]:80;
    server_name oyun.example.com;
    
    # HTTPS'e yonlendir
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name oyun.example.com;
    
    root /var/www/wasm-game/web;
    index index.html;
    
    # SSL sertifikasi
    ssl_certificate /etc/letsencrypt/live/oyun.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/oyun.example.com/privkey.pem;
    
    # WASM icin kritik header'lar
    # SharedArrayBuffer icin COOP/COEP gerekli
    add_header Cross-Origin-Opener-Policy "same-origin" always;
    add_header Cross-Origin-Embedder-Policy "require-corp" always;
    add_header Cross-Origin-Resource-Policy "same-site" always;
    
    # Gzip WASM icin
    gzip on;
    gzip_types application/wasm application/javascript;
    gzip_min_length 1024;
    
    # WASM dosyalari icin ozel ayarlar
    location ~* .wasm$ {
        add_header Content-Type "application/wasm";
        add_header Cross-Origin-Opener-Policy "same-origin" always;
        add_header Cross-Origin-Embedder-Policy "require-corp" always;
        
        # Pre-compressed varsa kullan
        gzip_static on;
        
        # Uzun sure cache - hash ile versiyonlama yapiliyorsa
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
    
    # Asset dosyalari
    location /assets/ {
        expires 7d;
        add_header Cache-Control "public";
    }
    
    # JS dosyalari - versiyonlama yoksa kisa cache
    location ~* .js$ {
        expires 1h;
        add_header Cache-Control "public";
    }
    
    location / {
        try_files $uri $uri/ =404;
    }
    
    # Loglama
    access_log /var/log/nginx/wasm-game-access.log;
    error_log /var/log/nginx/wasm-game-error.log warn;
}

Performans Profiling ve Izleme

Production’da oyunun nasıl davrandığını izlemek için basit bir profiler entegrasyonu:

#!/bin/bash
# monitor-wasm.sh - WASM oyun performans izleme

NGINX_LOG="/var/log/nginx/wasm-game-access.log"
REPORT_FILE="/tmp/wasm-perf-report-$(date +%Y%m%d-%H%M).txt"

echo "=== WASM Game Performans Raporu ===" > "$REPORT_FILE"
echo "Tarih: $(date)" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"

# WASM dosya indirme sureleri
echo "--- WASM Dosya Yüklenme Süreleri ---" >> "$REPORT_FILE"
grep ".wasm" "$NGINX_LOG" | awk '{
    # Request time son alan
    split($NF, t, ".");
    total += $NF;
    count++;
    if ($NF > max) max = $NF;
}
END {
    if (count > 0) {
        printf "Toplam istek: %dn", count;
        printf "Ortalama süre: %.3f snn", total/count;
        printf "Maksimum süre: %.3f snn", max;
    }
}' >> "$REPORT_FILE"

# Bant genisligi kullanimi
echo "" >> "$REPORT_FILE"
echo "--- Bant Genisligi (Son 1 saat) ---" >> "$REPORT_FILE"
awk -v cutoff="$(date -d '1 hour ago' '+%d/%b/%Y:%H:%M:%S')" '
    $4 > "["cutoff {total += $10}
    END {printf "Toplam: %.2f MBn", total/1024/1024}
' "$NGINX_LOG" >> "$REPORT_FILE"

# En cok yuklenen asset'ler
echo "" >> "$REPORT_FILE"
echo "--- En Cok Istenen Kaynaklar ---" >> "$REPORT_FILE"
grep -o '"GET [^"]*"' "$NGINX_LOG" | sort | uniq -c | sort -rn | head -10 >> "$REPORT_FILE"

cat "$REPORT_FILE"
echo ""
echo "Rapor kaydedildi: $REPORT_FILE"

Gerçek Dünya Deneyimlerinden Notlar

Birkaç projede bu pipeline’ı kullandım ve karşılaştığım sorunları paylaşayım.

Memory sızıntısı: C++ tarafında SDL_FreeSurface çağrısını unuttuğumda, WASM heap hızla doluyordu. ALLOW_MEMORY_GROWTH=1 kurtarıcı ama sonsuz değil. Chrome DevTools’un Memory sekmesi WASM heap’i de gösteriyor, bunu kullanın.

CORS sorunları: SharedArrayBuffer kullanmak istiyorsanız mutlaka COOP ve COEP header’larını ayarlayın. Bu olmadan multi-thread WASM çalışmıyor. Firebase Hosting veya Netlify’da bu header’ları eklemek için platform bazlı konfigürasyon dosyası gerekiyor.

Asset yükleme sırası: Emscripten’in preload sistemi iyi çalışıyor ama büyük asset paketlerinde ilk yüklenme uzuyor. Bu yüzden asset’leri kategorilere bölün, önce kritik olanları yükleyin.

Mobile tarayıcılar: iOS Safari’de WebAssembly desteği var ama bazı eski versiyonlarda SharedArrayBuffer yok. Graceful degradation için feature detection yapın.

  • Dosya boyutu: WASM binary’si başlangıçta büyük görünebilir. -O3 -flto flag’lerini kullanın ve --closure 1 ile JS tarafını da minify edin
  • Debug modu: ASSERTIONS=2 ve -g4 flag’leriyle source map üretebilirsiniz, gerçekten çok işe yarıyor
  • Async loading: emscripten_async_wget kullanarak level asset’lerini lazy load edebilirsiniz

Sonuç

WebAssembly ile 2D oyun motoru geliştirmek başta korkutucu görünüyor ama aslında sysadmin bakış açısıyla değerlendirdiğinizde oldukça sistematik bir süreç. C++ kod tabanınızı koruyorsunuz, Emscripten toolchain derlemeyi hallediyor, Nginx ise doğru header’larla servis ediyor.

Bu yaklaşımın en güçlü yanı platform bağımsızlığı. Aynı oyun motoru hem Windows/Linux masaüstünde hem de Chrome’da çalışıyor. Steam’e yayınlayıp aynı anda bir web demosu sunmak artık çok büyük bir overhead gerektirmiyor.

Performans tarafında ise JavaScript tabanlı çözümlere kıyasla özellikle fizik hesaplamalarında ve büyük sprite sayılarında ciddi fark görüyorsunuz. 60 FPS’i korumak için game loop yapısına ve draw call optimizasyonuna dikkat etmek yeterli.

Sonraki adım olarak audio sistemi, WebSocket ile multiplayer desteği veya WebGPU entegrasyonunu deneyebilirsiniz. WebGPU özellikle 2024 sonu itibarıyla browser desteği genişlediğinden, shader tabanlı efektler için ciddi bir seçenek haline geliyor.

Bir yanıt yazın

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