Rust ve WebAssembly ile Basit Oyun Geliştirme Rehberi
Sunucu tarafında yıllarca Rust ile performans kritik servisler yazdıktan sonra, aynı dilin tarayıcı içinde de bu kadar iyi çalışabileceğini gördüğümde açıkçası biraz şaşırmıştım. WebAssembly ile Rust kombinasyonu, özellikle oyun geliştirme gibi hesaplama yoğun senaryolarda JavaScript’in sınırlarını zorlayan işler için gerçekten güçlü bir alternatif sunuyor. Bu rehberde sıfırdan başlayarak tarayıcıda çalışan basit ama tam işlevsel bir oyun geliştireceğiz.
Neden Rust + WASM ile Oyun Geliştirmeli?
JavaScript oyun geliştirme için yeterli, bu doğru. Ama “yeterli” ile “optimal” arasında ciddi farklar var. Rust’ın WASM’a derlenmesi size birkaç önemli avantaj getiriyor.
Performans tarafında garbage collector yok, yani beklenmedik duraklamalar yok. Bir oyunda frame rate tutarlılığı hayati önem taşıyor ve GC pause’ları bunu doğrudan etkiliyor. Bellek güvenliği açısından ise Rust’ın ownership sistemi runtime’da bellek hatalarını imkansız kılıyor, bu da özellikle uzun süreli oyun sessionlarında önem kazanıyor. Binary boyutu konusunda optimize edilmiş Rust WASM binary’leri oldukça küçük tutuluyor. Tip güvenliği ile compile time’da yakalanan hatalar runtime sürprizleri minimize ediyor.
Sysadmin perspektifinden bakarsak, bu stack production’a almak oldukça basit. Statik dosyalar olduğu için Nginx, Caddy ya da bir edge CDN arkasında kolayca servis edebiliyorsunuz.
Geliştirme Ortamını Hazırlamak
Önce gerekli araçları kuralım. Rust toolchain’i zaten kurulu olduğunu varsayıyorum ama WASM hedefini eklemek gerekiyor.
# Rust WASM hedefini ekle
rustup target add wasm32-unknown-unknown
# wasm-pack kur - bu araç hayatı kolaylaştırıyor
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# cargo-generate ile proje şablonu kullan
cargo install cargo-generate
# wasm-bindgen-cli kur
cargo install wasm-bindgen-cli
# Gerekli sistem araçlarını kur (Ubuntu/Debian için)
sudo apt-get install -y pkg-config libssl-dev
# Node.js gerekecek (paket yönetimi için)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# Kurulumları doğrula
rustup target list --installed | grep wasm
wasm-pack --version
node --version
Şimdi proje yapısını oluşturalım. Basit bir Snake oyunu yapacağız çünkü oyun mekaniği anlaşılır, ama WASM’ın avantajlarını göstermek için yeterince hesaplama içeriyor.
# Yeni proje oluştur
cargo new --lib rust-snake-wasm
cd rust-snake-wasm
# Dizin yapısını hazırla
mkdir -p www/src
touch www/index.html www/src/index.js www/package.json
# Proje yapısına bak
tree .
# .
# ├── Cargo.toml
# ├── src/
# │ └── lib.rs
# └── www/
# ├── index.html
# ├── package.json
# └── src/
# └── index.js
Cargo.toml Yapılandırması
cat > Cargo.toml << 'EOF'
[package]
name = "rust-snake-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
# WASM boyutunu minimize et
opt-level = "s"
lto = true
codegen-units = 1
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
getrandom = { version = "0.2", features = ["js"] }
[dependencies.web-sys]
version = "0.3"
features = [
"console",
"Window",
"Document",
"HtmlCanvasElement",
"CanvasRenderingContext2d",
"KeyboardEvent",
]
EOF
Oyun Mantığını Yazmak
Şimdi asıl işe geliyoruz. Rust kodunu yazmaya başlayalım. Önce temel veri yapıları ve oyun mantığı:
cat > src/lib.rs << 'EOF'
use wasm_bindgen::prelude::*;
use js_sys::Math;
// Web-sys console için macro
macro_rules! log {
( $( $t:tt )* ) => {
web_sys::console::log_1(&format!( $( $t )* ).into());
}
}
// Yön enum'u - Copy trait ekliyoruz çünkü küçük bir değer
#[wasm_bindgen]
#[derive(Clone, Copy, PartialEq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
// Koordinat yapısı
#[derive(Clone, Copy, PartialEq)]
pub struct Point {
pub x: i32,
pub y: i32,
}
impl Point {
fn new(x: i32, y: i32) -> Self {
Point { x, y }
}
}
// Ana oyun yapısı - wasm_bindgen ile JavaScript'e expose ediliyor
#[wasm_bindgen]
pub struct SnakeGame {
width: i32,
height: i32,
snake: Vec<Point>,
direction: Direction,
next_direction: Direction,
food: Point,
score: u32,
game_over: bool,
speed: u32,
tick_count: u32,
}
#[wasm_bindgen]
impl SnakeGame {
#[wasm_bindgen(constructor)]
pub fn new(width: i32, height: i32) -> SnakeGame {
// Panic handler'ı WASM için ayarla
console_error_panic_hook::set_once();
let start_x = width / 2;
let start_y = height / 2;
// Yılan başlangıçta 3 segmentli
let snake = vec![
Point::new(start_x, start_y),
Point::new(start_x - 1, start_y),
Point::new(start_x - 2, start_y),
];
let food = SnakeGame::generate_food(width, height, &snake);
log!("Oyun başlatıldı: {}x{} grid", width, height);
SnakeGame {
width,
height,
snake,
direction: Direction::Right,
next_direction: Direction::Right,
food,
score: 0,
game_over: false,
speed: 10,
tick_count: 0,
}
}
// Rastgele yem pozisyonu üret - yılanın üzerinde olmayan bir yer
fn generate_food(width: i32, height: i32, snake: &[Point]) -> Point {
loop {
let x = (Math::random() * width as f64) as i32;
let y = (Math::random() * height as f64) as i32;
let candidate = Point::new(x, y);
if !snake.iter().any(|s| s.x == candidate.x && s.y == candidate.y) {
return candidate;
}
}
}
// Her frame'de çağrılacak güncelleme fonksiyonu
pub fn tick(&mut self) -> bool {
if self.game_over {
return false;
}
self.tick_count += 1;
// Speed kontrolü - her N tick'te bir hareket et
// Bu sayede JavaScript tarafında hız kontrolü kolaylaşıyor
if self.tick_count % (20 - self.speed.min(19)) != 0 {
return true;
}
// Yönü güncelle
self.direction = self.next_direction;
// Yeni baş pozisyonunu hesapla
let head = self.snake[0];
let new_head = match self.direction {
Direction::Up => Point::new(head.x, head.y - 1),
Direction::Down => Point::new(head.x, head.y + 1),
Direction::Left => Point::new(head.x - 1, head.y),
Direction::Right => Point::new(head.x + 1, head.y),
};
// Duvar çarpışması kontrolü
if new_head.x < 0 || new_head.x >= self.width
|| new_head.y < 0 || new_head.y >= self.height {
self.game_over = true;
log!("Oyun bitti: Duvara çarpıldı! Skor: {}", self.score);
return false;
}
// Kendine çarpışma kontrolü
if self.snake.iter().any(|s| s.x == new_head.x && s.y == new_head.y) {
self.game_over = true;
log!("Oyun bitti: Kendine çarptı! Skor: {}", self.score);
return false;
}
// Yılanı hareket ettir
self.snake.insert(0, new_head);
// Yem yendi mi?
if new_head.x == self.food.x && new_head.y == self.food.y {
self.score += 10;
// Her 50 puanda hız artır
if self.score % 50 == 0 {
self.speed = (self.speed + 1).min(18);
log!("Hız arttı: {}", self.speed);
}
self.food = SnakeGame::generate_food(self.width, self.height, &self.snake);
} else {
// Kuyruk segment'ini kaldır
self.snake.pop();
}
true
}
// JavaScript'ten yön değişikliği al
pub fn change_direction(&mut self, dir: Direction) {
// Ters yöne dönmeyi engelle
let invalid = match dir {
Direction::Up => self.direction == Direction::Down,
Direction::Down => self.direction == Direction::Up,
Direction::Left => self.direction == Direction::Right,
Direction::Right => self.direction == Direction::Left,
};
if !invalid {
self.next_direction = dir;
}
}
// Render için gerekli verileri JavaScript'e aktar
// Vec<u8> kullanarak bellek kopyasını minimize et
pub fn get_snake_cells(&self) -> Vec<i32> {
let mut cells = Vec::with_capacity(self.snake.len() * 2);
for point in &self.snake {
cells.push(point.x);
cells.push(point.y);
}
cells
}
pub fn get_food_x(&self) -> i32 { self.food.x }
pub fn get_food_y(&self) -> i32 { self.food.y }
pub fn get_score(&self) -> u32 { self.score }
pub fn is_game_over(&self) -> bool { self.game_over }
pub fn get_width(&self) -> i32 { self.width }
pub fn get_height(&self) -> i32 { self.height }
// Oyunu yeniden başlat
pub fn reset(&mut self) {
let start_x = self.width / 2;
let start_y = self.height / 2;
self.snake = vec![
Point::new(start_x, start_y),
Point::new(start_x - 1, start_y),
Point::new(start_x - 2, start_y),
];
self.direction = Direction::Right;
self.next_direction = Direction::Right;
self.food = SnakeGame::generate_food(self.width, self.height, &self.snake);
self.score = 0;
self.game_over = false;
self.speed = 10;
self.tick_count = 0;
log!("Oyun sıfırlandı");
}
}
EOF
Panic handler için bağımlılığı da ekleyelim:
# Cargo.toml'a ekle
cat >> Cargo.toml << 'EOF'
[dependencies]
console_error_panic_hook = "0.1"
EOF
# WASM binary'sini derle
wasm-pack build --target web --out-dir www/pkg
# Çıktıyı kontrol et
ls -lh www/pkg/
# Şunu görmemiz gerekiyor:
# rust_snake_wasm_bg.wasm (binary)
# rust_snake_wasm.js (bindgen wrapper)
# rust_snake_wasm.d.ts (TypeScript tanımları)
Frontend Katmanını Yazmak
Artık JavaScript tarafını hazırlayalım. Canvas üzerine render yapacağız:
cat > www/index.html << 'EOF'
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rust WASM Snake</title>
<style>
body {
margin: 0;
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
font-family: 'Courier New', monospace;
color: #eee;
}
#scoreboard {
font-size: 1.5rem;
margin-bottom: 16px;
color: #00ff88;
text-shadow: 0 0 10px #00ff88;
}
canvas {
border: 2px solid #00ff88;
box-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
}
#game-over {
display: none;
position: absolute;
background: rgba(0,0,0,0.85);
padding: 30px 50px;
border: 2px solid #ff4444;
text-align: center;
border-radius: 8px;
}
#game-over h2 { color: #ff4444; margin: 0 0 10px; }
#restart-btn {
margin-top: 15px;
padding: 10px 25px;
background: #00ff88;
color: #1a1a2e;
border: none;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
border-radius: 4px;
}
#restart-btn:hover { background: #00cc66; }
#info { margin-top: 12px; font-size: 0.8rem; color: #888; }
</style>
</head>
<body>
<div id="scoreboard">Skor: <span id="score">0</span></div>
<canvas id="canvas" width="400" height="400"></canvas>
<div id="game-over">
<h2>OYUN BİTTİ</h2>
<p>Final Skoru: <span id="final-score">0</span></p>
<button id="restart-btn" onclick="restartGame()">Tekrar Oyna</button>
</div>
<div id="info">Ok tuşları veya WASD ile oyna | Rust WASM ile çalışıyor</div>
<script type="module" src="src/index.js"></script>
</body>
</html>
EOF
cat > www/src/index.js << 'EOF'
import init, { SnakeGame, Direction } from '../pkg/rust_snake_wasm.js';
const CELL_SIZE = 20;
const GRID_SIZE = 20;
let game = null;
let animationId = null;
let lastFrameTime = 0;
async function run() {
// WASM modülünü başlat
await init();
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
game = new SnakeGame(GRID_SIZE, GRID_SIZE);
// Klavye kontrollerini ayarla
document.addEventListener('keydown', (e) => {
if (!game) return;
const keyMap = {
'ArrowUp': Direction.Up, 'KeyW': Direction.Up,
'ArrowDown': Direction.Down, 'KeyS': Direction.Down,
'ArrowLeft': Direction.Left, 'KeyA': Direction.Left,
'ArrowRight': Direction.Right, 'KeyD': Direction.Right,
};
const dir = keyMap[e.code];
if (dir !== undefined) {
e.preventDefault();
game.change_direction(dir);
}
});
// Game loop - requestAnimationFrame ile
function gameLoop(timestamp) {
const delta = timestamp - lastFrameTime;
// 60fps hedefle ama WASM tick'i kendi hızında çalıştır
if (delta > 16) {
lastFrameTime = timestamp;
const alive = game.tick();
render(ctx, game);
if (!alive || game.is_game_over()) {
showGameOver(game.get_score());
return;
}
}
animationId = requestAnimationFrame(gameLoop);
}
animationId = requestAnimationFrame(gameLoop);
}
function render(ctx, game) {
const w = game.get_width();
const h = game.get_height();
// Arka planı temizle
ctx.fillStyle = '#0d0d1a';
ctx.fillRect(0, 0, w * CELL_SIZE, h * CELL_SIZE);
// Grid çiz (hafif görünürlük)
ctx.strokeStyle = 'rgba(255,255,255,0.03)';
ctx.lineWidth = 0.5;
for (let x = 0; x <= w; x++) {
ctx.beginPath();
ctx.moveTo(x * CELL_SIZE, 0);
ctx.lineTo(x * CELL_SIZE, h * CELL_SIZE);
ctx.stroke();
}
// Yılanı çiz
const cells = game.get_snake_cells();
for (let i = 0; i < cells.length; i += 2) {
const x = cells[i];
const y = cells[i + 1];
const isHead = i === 0;
// Gradient efekti - baş daha parlak
const alpha = isHead ? 1.0 : 0.5 + 0.5 * (1 - i / cells.length);
ctx.fillStyle = isHead
? `rgba(0, 255, 136, ${alpha})`
: `rgba(0, 200, 100, ${alpha})`;
ctx.fillRect(
x * CELL_SIZE + 1,
y * CELL_SIZE + 1,
CELL_SIZE - 2,
CELL_SIZE - 2
);
// Baş için göz efekti
if (isHead) {
ctx.fillStyle = '#1a1a2e';
ctx.beginPath();
ctx.arc(
x * CELL_SIZE + CELL_SIZE / 2,
y * CELL_SIZE + CELL_SIZE / 2,
3, 0, Math.PI * 2
);
ctx.fill();
}
}
// Yemi çiz
const fx = game.get_food_x();
const fy = game.get_food_y();
ctx.fillStyle = '#ff4444';
ctx.shadowColor = '#ff4444';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.arc(
fx * CELL_SIZE + CELL_SIZE / 2,
fy * CELL_SIZE + CELL_SIZE / 2,
CELL_SIZE / 2 - 2,
0, Math.PI * 2
);
ctx.fill();
ctx.shadowBlur = 0;
// Skoru güncelle
document.getElementById('score').textContent = game.get_score();
}
function showGameOver(score) {
document.getElementById('final-score').textContent = score;
document.getElementById('game-over').style.display = 'block';
}
window.restartGame = function() {
document.getElementById('game-over').style.display = 'none';
if (animationId) cancelAnimationFrame(animationId);
game.reset();
lastFrameTime = 0;
animationId = requestAnimationFrame(function loop(ts) {
lastFrameTime = ts;
const alive = game.tick();
const ctx = document.getElementById('canvas').getContext('2d');
render(ctx, game);
if (!alive) { showGameOver(game.get_score()); return; }
animationId = requestAnimationFrame(loop);
});
};
run();
EOF
Geliştirme Sunucusu ve Build Pipeline
cat > www/package.json << 'EOF'
{
"name": "rust-snake-wasm",
"version": "1.0.0",
"description": "Rust WASM Snake Oyunu",
"scripts": {
"start": "npx serve . -l 8080",
"build": "cd .. && wasm-pack build --target web --out-dir www/pkg --release",
"dev": "concurrently "cargo watch -i www -s 'wasm-pack build --target web --out-dir www/pkg'" "npx serve www -l 8080""
},
"devDependencies": {
"serve": "^14.0.0",
"concurrently": "^8.0.0",
"cargo-watch": "^8.0.0"
}
}
EOF
cd www && npm install
# WASM'ı derle ve sunucuyu başlat
npm run build
npm start
# Tarayıcıda aç
echo "http://localhost:8080 adresini tarayıcında aç"
Production Deployment
Sysadmin olarak asıl önem verdiğimiz kısım burası. Nginx ile nasıl servis ederiz:
# Nginx konfigürasyonu
cat > /etc/nginx/sites-available/rust-wasm-game << 'EOF'
server {
listen 80;
server_name oyun.example.com;
# WASM için doğru MIME type zorunlu
include /etc/nginx/mime.types;
types {
application/wasm wasm;
}
root /var/www/rust-snake-wasm/www;
index index.html;
# WASM dosyaları için özel header
location ~* .wasm$ {
add_header Content-Type application/wasm;
# WASM'ı tarayıcı cache'lesin
add_header Cache-Control "public, max-age=31536000, immutable";
# Streaming compilation için gerekli headerlar
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";
gzip off; # WASM zaten binary, sıkıştırma anlamsız
}
# JS dosyaları
location ~* .(js|mjs)$ {
add_header Cache-Control "public, max-age=3600";
}
# Statik dosyalar
location / {
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";
}
# Güvenlik headerları
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "SAMEORIGIN";
}
EOF
# Siteyi etkinleştir
ln -s /etc/nginx/sites-available/rust-wasm-game /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
# WASM binary boyutunu kontrol et
ls -lh /var/www/rust-snake-wasm/www/pkg/*.wasm
WASM Boyutunu Optimize Etmek
# wasm-opt ile ek optimizasyon (binaryen paketi gerekir)
sudo apt-get install -y binaryen
# Mevcut boyutu not et
ls -lh www/pkg/rust_snake_wasm_bg.wasm
# Optimizasyon uygula
wasm-opt -Os www/pkg/rust_snake_wasm_bg.wasm
-o www/pkg/rust_snake_wasm_bg_opt.wasm
# Boyut farkını gör
ls -lh www/pkg/rust_snake_wasm_bg*.wasm
# Genellikle %20-40 arası küçülme görürsün
# Örnek çıktı:
# rust_snake_wasm_bg.wasm -> 84K
# rust_snake_wasm_bg_opt.wasm -> 52K
# İsmi geri döndür
mv www/pkg/rust_snake_wasm_bg_opt.wasm www/pkg/rust_snake_wasm_bg.wasm
# Ek olarak Brotli sıkıştırması ile serve et (Nginx)
# sudo apt-get install nginx-module-brotli
echo "WASM dosyası optimize edildi"
Performans İzleme
Production’da oyunun nasıl çalıştığını izlemek önemli:
# Basit bir health check scripti
cat > /usr/local/bin/check-wasm-game.sh << 'EOF'
#!/bin/bash
URL="http://localhost/pkg/rust_snake_wasm_bg.wasm"
RESPONSE=$(curl -s -o /dev/null -w "%{http_code} %{size_download} %{time_total}" "$URL")
HTTP_CODE=$(echo $RESPONSE | awk '{print $1}')
SIZE=$(echo $RESPONSE | awk '{print $2}')
TIME=$(echo $RESPONSE | awk '{print $3}')
echo "$(date): HTTP=$HTTP_CODE, Boyut=${SIZE}B, Süre=${TIME}s"
if [ "$HTTP_CODE" != "200" ]; then
echo "HATA: WASM dosyası erişilemiyor!" |
mail -s "WASM Oyun Alerti" [email protected]
fi
EOF
chmod +x /usr/local/bin/check-wasm-game.sh
# Crontab'a ekle - her 5 dakikada kontrol
echo "*/5 * * * * /usr/local/bin/check-wasm-game.sh >> /var/log/wasm-game.log 2>&1" | crontab -
Yaygın Sorunlar ve Çözümleri
Geliştirme sürecinde sıkça karşılaşılan problemleri ve çözümleri paylaşayım.
SharedArrayBuffer hatası: Tarayıcıda SharedArrayBuffer is not defined hatası alıyorsanız, Nginx konfigürasyonundaki Cross-Origin-Opener-Policy ve Cross-Origin-Embedder-Policy headerlarının doğru ayarlandığından emin olun. Bu headerlar olmadan çoklu thread özelliklerini kullanan WASM’lar çalışmıyor.
MIME type sorunu: Failed to execute 'compile' on 'WebAssembly' hatası genellikle yanlış MIME type’tan kaynaklanıyor. Nginx’te application/wasm MIME type’ını manuel olarak tanımlamak şart.
Büyük binary boyutları: Debug modunda derleme yapıyorsanız binary çok büyük olacak. Her zaman --release flag’i kullanın ve wasm-opt ile optimize edin.
Rust panic’leri: WASM’da Rust panic’leri varsayılan olarak sessiz kalıyor. console_error_panic_hook kütüphanesini kullanarak panic mesajlarını browser console’da görünür hale getirin.
Bellek sızıntıları: JavaScript ve WASM arasında veri transferi yaparken dönen Vec değerleri JavaScript tarafında free() çağrılmadan bırakılırsa bellek sızıntısı oluşuyor. wasm-bindgen‘in ürettiği wrapper kodları bunu çoğunlukla hallediyor ama dikkatli olmakta fayda var.
Sonuç
Bu rehberde sıfırdan başlayarak Rust ile WASM üzerinde çalışan bir Snake oyunu geliştirdik. Geliştirme ortamının kurulumundan production Nginx konfigürasyonuna, WASM binary optimizasyonundan monitoring scriptlerine kadar gerçek dünyada işe yarayacak her şeyi ele aldık.
Rust WASM stack’inin en güzel yanı deployment basitliği. Ürettiğiniz şey sonuç olarak statik dosyalar. CDN arkasına koyuyorsunuz, dünya genelinde edge noktalarından servis edilebiliyor. Herhangi bir uygulama sunucusu, container orkestrasyon ya da karmaşık deployment pipeline’ı gerektirmiyor.
Oyun geliştirme bu teknoloji kombinasyonunu öğrenmek için harika bir başlangıç noktası çünkü performans farklılıklarını hemen hissediyorsunuz ve sonuçları görsel olarak test edebiliyorsunuz. Buradan gerçek zamanlı simülasyonlara, veri görselleştirme araçlarına ya da hesaplama yoğun web uygulamalarına geçiş yapmak artık çok daha kolay.
Rust ownership sistemi başlangıçta biraz can sıkıcı gelebilir, özellikle JavaScript geliştiricisi geçmişinden geliyorsanız. Ama bu kısıtlamalar aynı zamanda WASM’ın neden bu kadar güvenilir çalıştığının da açıklaması. Compile time’da yakalanan hatalar production’da sizi uyandırmıyor.
