Panduan Praktis: Membuat Game Snake dengan Rust & WebAssembly

Pelajari cara membuat game Snake yang menarik dengan Rust & WebAssembly! Temukan panduan praktis dan tips yang tidak boleh Anda lewatkan!

By WGS INDONESIA
4.9/4.9
Indonesia
Rp 43,750.00 GRATIS
E-COURSE banner with text and icons representing Artificial Intelligence and video learning

Detail Pembelajaran

Panduan Praktis: Membuat Game Snake dengan Rust & WebAssembly
  • Pengembangan Game, Rust, WebAssembly, Tutorial, Panduan Praktis

Baca Online

Panduan Praktis: Membuat Game Snake dengan Rust & WebAssembly

Daftar Isi

  1. Pengantar
  2. Persiapan Lingkungan Pengembangan
  3. Struktur Proyek Rust & WebAssembly
  4. Membuat Game Snake: Step by Step
  5. Source Code Lengkap
  6. Referensi & Channel Pembelajaran Lainnya

1. Pengantar

Game Snake adalah salah satu game klasik yang sederhana namun menyenangkan. Dalam panduan ini, kita akan belajar bagaimana membuat game Snake menggunakan bahasa pemrograman Rust dan mengompilasinya ke WebAssembly (Wasm) agar dapat dijalankan di browser dengan performa tinggi.

Rust adalah bahasa pemrograman yang cepat dan aman, sedangkan WebAssembly memungkinkan kode yang ditulis dalam bahasa lain selain JavaScript untuk berjalan di browser. Dengan menggabungkan keduanya, kita dapat membuat game yang ringan, cepat, dan modern.

Ilustrasi game Snake klasik berjalan di browser dengan latar belakang grid hijau dan ular berwarna hijau tua

2. Persiapan Lingkungan Pengembangan

Sebelum mulai membuat game, kita perlu menyiapkan beberapa alat dan lingkungan pengembangan:

  • Rust: Pastikan Rust sudah terpasang di komputer Anda. Jika belum, kunjungi https://rustup.rs untuk instalasi.
  • wasm-pack: Alat untuk membangun paket WebAssembly dari kode Rust. Instal dengan perintah:
    cargo install wasm-pack
  • Node.js & npm: Untuk menjalankan server lokal dan mengelola paket JavaScript. Unduh dari https://nodejs.org .
  • Editor Kode: Gunakan editor favorit Anda, seperti Visual Studio Code.

Setelah semua terpasang, kita siap membuat proyek Rust yang akan dikompilasi ke WebAssembly.

3. Struktur Proyek Rust & WebAssembly

Struktur proyek yang akan kita buat kira-kira seperti ini:

snake-game/
├── pkg/                  # Folder hasil build wasm-pack
├── src/
│   └── lib.rs            # Kode Rust utama
├── index.html            # Halaman web untuk menjalankan game
├── package.json          # Konfigurasi npm
└── README.md             # Dokumentasi proyek
    

Kita akan fokus pada src/lib.rs untuk logika game, dan index.html untuk menampilkan game di browser.

4. Membuat Game Snake: Step by Step

4.1 Membuat Proyek Rust Baru

Jalankan perintah berikut di terminal untuk membuat proyek Rust baru dengan tipe library:

cargo new --lib snake-game

Masuk ke folder proyek:

cd snake-game

4.2 Menambahkan Dependensi untuk WebAssembly

Buka file Cargo.toml dan tambahkan dependensi berikut:

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["Window", "Document", "HtmlCanvasElement", "CanvasRenderingContext2d", "KeyboardEvent", "console"] }
    

Ini memungkinkan kita mengakses API browser dan menghubungkan Rust dengan JavaScript.

4.3 Membuat Logika Game di src/lib.rs

Berikut contoh kode Rust untuk game Snake sederhana yang bisa Anda gunakan:

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, KeyboardEvent};
use std::collections::VecDeque;

#[wasm_bindgen]
pub struct Game {
    width: u32,
    height: u32,
    snake: VecDeque<(u32, u32)>,
    dir: Direction,
    food: (u32, u32),
    game_over: bool,
    context: CanvasRenderingContext2d,
}

#[wasm_bindgen]
impl Game {
    #[wasm_bindgen(constructor)]
    pub fn new(canvas_id: &str) -> Game {
        let window = web_sys::window().expect("no global `window` exists");
        let document = window.document().expect("should have a document on window");
        let canvas = document.get_element_by_id(canvas_id)
            .unwrap()
            .dyn_into::()
            .unwrap();
        let context = canvas
            .get_context("2d").unwrap()
            .unwrap()
            .dyn_into::()
            .unwrap();

        let width = canvas.width();
        let height = canvas.height();

        let mut snake = VecDeque::new();
        snake.push_back((width / 2, height / 2));

        let food = (width / 4, height / 4);

        Game {
            width,
            height,
            snake,
            dir: Direction::Right,
            food,
            game_over: false,
            context,
        }
    }

    pub fn change_direction(&mut self, key: &str) {
        self.dir = match key {
            "ArrowUp" if self.dir != Direction::Down => Direction::Up,
            "ArrowDown" if self.dir != Direction::Up => Direction::Down,
            "ArrowLeft" if self.dir != Direction::Right => Direction::Left,
            "ArrowRight" if self.dir != Direction::Left => Direction::Right,
            _ => self.dir,
        };
    }

    pub fn update(&mut self) {
        if self.game_over {
            return;
        }

        let (head_x, head_y) = self.snake.front().unwrap();
        let new_head = match self.dir {
            Direction::Up => (*head_x, head_y.saturating_sub(10)),
            Direction::Down => (*head_x, head_y.saturating_add(10)),
            Direction::Left => (head_x.saturating_sub(10), *head_y),
            Direction::Right => (head_x.saturating_add(10), *head_y),
        };

        // Cek tabrakan dengan dinding
        if new_head.0 >= self.width || new_head.1 >= self.height {
            self.game_over = true;
            return;
        }

        // Cek tabrakan dengan tubuh sendiri
        if self.snake.contains(&new_head) {
            self.game_over = true;
            return;
        }

        self.snake.push_front(new_head);

        // Cek makan makanan
        if new_head == self.food {
            self.spawn_food();
        } else {
            self.snake.pop_back();
        }
    }

    pub fn draw(&self) {
        // Bersihkan canvas
        self.context.set_fill_style(&JsValue::from_str("#f0f0f0"));
        self.context.fill_rect(0.0, 0.0, self.width as f64, self.height as f64);

        // Gambar makanan
        self.context.set_fill_style(&JsValue::from_str("#e53e3e"));
        self.context.fill_rect(self.food.0 as f64, self.food.1 as f64, 10.0, 10.0);

        // Gambar ular
        self.context.set_fill_style(&JsValue::from_str("#38a169"));
        for &(x, y) in &self.snake {
            self.context.fill_rect(x as f64, y as f64, 10.0, 10.0);
        }

        if self.game_over {
            self.context.set_fill_style(&JsValue::from_str("rgba(0,0,0,0.7)"));
            self.context.fill_rect(0.0, 0.0, self.width as f64, self.height as f64);
            self.context.set_fill_style(&JsValue::from_str("#fff"));
            self.context.set_font("48px sans-serif");
            self.context.fill_text("Game Over", (self.width / 4) as f64, (self.height / 2) as f64).unwrap();
        }
    }

    fn spawn_food(&mut self) {
        use rand::Rng;
        let mut rng = rand::thread_rng();

        loop {
            let x = (rng.gen_range(0..self.width / 10)) * 10;
            let y = (rng.gen_range(0..self.height / 10)) * 10;
            if !self.snake.contains(&(x, y)) {
                self.food = (x, y);
                break;
            }
        }
    }
}

#[derive(PartialEq, Copy, Clone)]
enum Direction {
    Up,
    Down,
    Left,
    Right,
}
      

Catatan: Untuk menggunakan rand di WebAssembly, Anda perlu menambahkan dependensi rand = "0.8" di Cargo.toml dan mengaktifkan fitur yang kompatibel dengan Wasm. Namun, untuk kesederhanaan, Anda bisa mengganti logika spawn makanan dengan metode lain jika mengalami masalah.

4.4 Membuat Halaman HTML untuk Menjalankan Game

Buat file index.html di root proyek dengan isi berikut:

<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Game Snake Rust & Wasm</title>
  <script type="module">
    import init, { Game } from './pkg/snake_game.js';

    async function run() {
      await init();
      const game = new Game('game-canvas');

      window.addEventListener('keydown', e => {
        game.change_direction(e.key);
      });

      function gameLoop() {
        game.update();
        game.draw();
        if (!game.game_over) {
          setTimeout(gameLoop, 100);
        }
      }

      gameLoop();
    }

    run();
  </script>
  <style>
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      margin: 0;
      background-color: #f0f0f0;
    }
    canvas {
      border: 2px solid #38a169;
      background-color: #ffffff;
    }
  </style>
</head>
<body>
  <canvas id="game-canvas" width="400" height="400"></canvas>
</body>
</html>
      

Halaman ini memuat modul WebAssembly yang sudah dibangun dan menjalankan game Snake di canvas HTML.

4.5 Membangun Proyek dan Menjalankan

Jalankan perintah berikut untuk membangun proyek menjadi WebAssembly:

wasm-pack build --target web

Setelah selesai, jalankan server lokal untuk melihat hasilnya. Jika Anda memiliki serve dari npm, jalankan:

npx serve .

Buka browser dan akses http://localhost:5000 (atau port yang ditampilkan) untuk memainkan game Snake yang Anda buat.

5. Source Code Lengkap

Berikut adalah source code lengkap src/lib.rs yang sudah dirapikan dan siap digunakan:

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
use std::collections::VecDeque;

#[wasm_bindgen]
pub struct Game {
    width: u32,
    height: u32,
    snake: VecDeque<(u32, u32)>,
    dir: Direction,
    food: (u32, u32),
    game_over: bool,
    context: CanvasRenderingContext2d,
}

#[wasm_bindgen]
impl Game {
    #[wasm_bindgen(constructor)]
    pub fn new(canvas_id: &str) -> Game {
        let window = web_sys::window().expect("no global `window` exists");
        let document = window.document().expect("should have a document on window");
        let canvas = document.get_element_by_id(canvas_id)
            .unwrap()
            .dyn_into::()
            .unwrap();
        let context = canvas
            .get_context("2d").unwrap()
            .unwrap()
            .dyn_into::()
            .unwrap();

        let width = canvas.width();
        let height = canvas.height();

        let mut snake = VecDeque::new();
        snake.push_back((width / 2, height / 2));

        let food = (width / 4, height / 4);

        Game {
            width,
            height,
            snake,
            dir: Direction::Right,
            food,
            game_over: false,
            context,
        }
    }

    pub fn change_direction(&mut self, key: &str) {
        self.dir = match key {
            "ArrowUp" if self.dir != Direction::Down => Direction::Up,
            "ArrowDown" if self.dir != Direction::Up => Direction::Down,
            "ArrowLeft" if self.dir != Direction::Right => Direction::Left,
            "ArrowRight" if self.dir != Direction::Left => Direction::Right,
            _ => self.dir,
        };
    }

    pub fn update(&mut self) {
        if self.game_over {
            return;
        }

        let (head_x, head_y) = self.snake.front().unwrap();
        let new_head = match self.dir {
            Direction::Up => (*head_x, head_y.saturating_sub(10)),
            Direction::Down => (*head_x, head_y.saturating_add(10)),
            Direction::Left => (head_x.saturating_sub(10), *head_y),
            Direction::Right => (head_x.saturating_add(10), *head_y),
        };

        if new_head.0 >= self.width || new_head.1 >= self.height {
            self.game_over = true;
            return;
        }

        if self.snake.contains(&new_head) {
            self.game_over = true;
            return;
        }

        self.snake.push_front(new_head);

        if new_head == self.food {
            self.spawn_food();
        } else {
            self.snake.pop_back();
        }
    }

    pub fn draw(&self) {
        self.context.set_fill_style(&JsValue::from_str("#f0f0f0"));
        self.context.fill_rect(0.0, 0.0, self.width as f64, self.height as f64);

        self.context.set_fill_style(&JsValue::from_str("#e53e3e"));
        self.context.fill_rect(self.food.0 as f64, self.food.1 as f64, 10.0, 10.0);

        self.context.set_fill_style(&JsValue::from_str("#38a169"));
        for &(x, y) in &self.snake {
            self.context.fill_rect(x as f64, y as f64, 10.0, 10.0);
        }

        if self.game_over {
            self.context.set_fill_style(&JsValue::from_str("rgba(0,0,0,0.7)"));
            self.context.fill_rect(0.0, 0.0, self.width as f64, self.height as f64);
            self.context.set_fill_style(&JsValue::from_str("#fff"));
            self.context.set_font("48px sans-serif");
            self.context.fill_text("Game Over", (self.width / 4) as f64, (self.height / 2) as f64).unwrap();
        }
    }

    fn spawn_food(&mut self) {
        // Cara sederhana spawn makanan tanpa rand crate
        let mut x = (self.food.0 + 30) % self.width;
        let mut y = (self.food.1 + 30) % self.height;

        // Pastikan makanan tidak muncul di tubuh ular
        while self.snake.contains(&(x, y)) {
            x = (x + 10) % self.width;
            y = (y + 10) % self.height;
        }
        self.food = (x, y);
    }
}

#[derive(PartialEq, Copy, Clone)]
enum Direction {
    Up,
    Down,
    Left,
    Right,
}
    

6. Referensi & Channel Pembelajaran Lainnya

Edukasi Terkait