Rust: kompilasi ke WASM untuk membuat game berbasis browser menggunakan kanvas

27 Februari 2026 [Programming, Rust, Tech, Videos]

Saya suka menulis Rust, dan saya suka menulis game sederhana, jadi mari gabungkan keduanya dengan menunjukkan cara membuat kerangka game kecil dengan properti berikut:

  • 99% kodenya adalah Rust, dikompilasi ke WASM
  • permainan ini berjalan di browser Web non-kuno apa pun
  • gambar layarnya dibuat pada kanvas yang diperbesar sehingga kita dapat membuat game retro berpiksel

Mendirikan

Sebelum kita mulai, kita perlu menginstal:

Catatan: ada beberapa kebingungan seputar dokumentasi alat WASM untuk Rust, dengan alat terbaru terkadang tertaut ke dokumentasi yang sudah ketinggalan zaman. Untuk keperluan postingan ini, Anda cukup menginstal wasm-pack melalui link di atas.

Ingin melihat saya melakukan ini secara langsung? Ada videonya…

Ikuti terus

Anda dapat melihat saya membuat pengaturan ini secara langsung di sini:

dan Anda dapat menemukan kode sumber lengkapnya di: codeberg.org/andybalaam/wasm-game.

Ikuti saya di mastodon: @andybalaam@mastodon.social

Hal pertama yang perlu kita lakukan adalah membuat proyek Rust+WASM.

Membuat proyek

wasm-pack memberi kita alat sederhana untuk membuat dan membuat kode Rust ke dalam WASM. Ini membungkus standar cargo perintah yang mengatur segala sesuatunya sesuai kebutuhan.

Untuk membuat proyek, ketik:

wasm-pack new wasm-game
cd wasm-game

Ini menciptakan proyek dengan pengaturan default yang tepat:

$ tree
.
├── Cargo.toml
├── src
│   ├── lib.rs
│   └── utils.rs
└── tests
    └── web.rs
...

Silakan lihat Cargo.toml. Ini memiliki info proyek normal tetapi juga menambah ketergantungan wasm-bindgen yang memungkinkan kita memanggil fungsi Rust dari JavaScript, dan fungsi JavaScript dari Rust. Itu memberi kita
console_error_panic_hook yang memungkinkan kita mencetak pesan panik dari Rust di konsol browser, dan memberitahu compiler untuk mengoptimalkan ukuran WASM kita, yang biasanya merupakan pilihan yang tepat.

Sekarang mari kita lihat src/lib.rs:

mod utils;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet() {
    alert("Hello, wasm-game!");
}

Dua baris yang disorot berisi #[wasm_bindgen] melakukan dua hal berbeda: yang pertama memberi tahu kompiler bahwa ada fungsi eksternal: fungsi JavaScript, meskipun demikian extern "C" bagian, yang ingin kita panggil dari dalam kode Rust kita. Dengan menambahkan ini, kami memungkinkan untuk menelepon alert dari Rust, seperti yang kita lakukan di dalam greet fungsi.

Yang kedua #[wasm_bindgen] baris melakukan yang sebaliknya: dikatakan kita menginginkannya greet
fungsi, yang ditulis dalam Rust, agar dapat dipanggil dari kode JavaScript.

Sekarang kita sudah mempunyai beberapa kode, mari kita kompilasi ke WASM agar kita bisa menjalankannya di browser.

Kompilasi ke WASM

Sekarang kita bisa mengkompilasi semua Rust ini ke dalam WASM seperti ini:

wasm-pack build --target=web

Ini menciptakan beberapa file di dalam pkg direktori:

$ ls -1 pkg
package.json
wasm_game_bg.wasm
wasm_game_bg.wasm.d.ts
wasm_game.d.ts
wasm_game.js
...

package.json tidak relevan bagi kami: akan membantu jika kami ingin membuat paket npm, tetapi kami tidak memerlukannya karena kami sedang menulis kode untuk browser.

Itu .ts file adalah informasi tipe yang dapat digunakan orang jika mereka ingin memanggil kode kita dari TypeScript. Kami tidak melakukan itu jadi kami tidak terlalu tertarik.

Yang kami minati adalah wasm_game_bg.wasmyang merupakan hasil kompilasi kode Rust kita ke WASM, dan wasm_game.jsyang menyediakan modul sederhana yang dapat kita impor dari JavaScript untuk menjalankan kode kita. Jika Anda membuka wasm_game.js
ada banyak hal di sana, tetapi yang penting adalah ada fungsi yang diekspor yang disebut greetyang dihasilkan karena kita menulis fungsi Rust yang dipanggil greet dan membubuhi keterangannya #[wasm_bindgen].

Jadi kita punya beberapa WASM dan beberapa JavaScript, tapi bagaimana kita menjalankannya?

Berjalan di browser

Kita perlu menulis beberapa HTML. Mari kita membuat direktori bernama www untuk semua HTML, CSS, dan JavaScript tulisan tangan kami, dan buat file di dalamnya bernama
index.html:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <script type="module">
            import init, { greet } from "./wasm_game.js";
            await init();
            greet();
        </script>
    </body>
</html>

File HTML ini sebagian besar sebenarnya adalah JavaScript di dalam a script tag, dan yang dilakukan kode ini adalah mengimpor greet berfungsi dari wasm_game.jsbersama dengan
inityang harus selalu kita panggil di awal program, lalu program itu akan memanggil greet.

Kami ingin HTML dan WASM yang kami hasilkan serta file yang menyertainya berada di direktori yang sama, jadi mari salin ke dalam satu direktori bernama public.

mkdir -p public
cp www/* pkg/*.js pkg/*.ts pkg/*.wasm public/

Sekarang kita membutuhkan server web yang menyajikan semua file di dalamnya public. Saya biasanya menggunakan yang disertakan dengan Python tetapi Anda bisa menggunakan yang mana pun yang Anda suka:

cd public
python3 -m http.server

Ini memulai server web, mendengarkan pada port 8000, yang menyajikan file-file di public direktori, yang meliputi index.html, wasm_game.js dan kode WASM kompilasi kami di wasm_game_bg.wasm.

Jadi jika kita membuka browser web dan pergi ke kita melihat pop up peringatan yang mengatakan “Halo, wasm-game”.

Kode Rust kami telah dikompilasi ke WASM dan dijalankan di browser! Minumlah secangkir teh.

Membuat kanvas

Sekarang, untuk membuat game sebenarnya, kita harus bisa menggambar grafis. Cara yang bagus untuk melakukannya, terutama untuk game retro berpiksel, adalah dengan menggunakan kanvas. Mari tambahkan satu ke index.html:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <link rel="stylesheet" href=" />
    </head>
    <body>
        <canvas id="canvas" width="22" height="22"></canvas>

        <script type="module">
            import init, { greet } from "./wasm_game.js";
            await init();
            greet();
        </script>
    </body>
</html>

Kami telah menambahkan a <canvas> tag, dengan id canvasdan lebar serta tinggi dalam piksel – meskipun kita merentangkan kanvas untuk memenuhi layar menggunakan CSS, itu hanya akan menampilkan piksel besar berukuran 22×22, yang sangat cocok untuk kita. Berbicara tentang CSS, sebaiknya kita menulis secukupnya untuk memusatkan dan menskalakan kanvas agar menutupi sebagian besar layar. Kami menciptakan style.css di dalam www seperti ini:

* {
    padding: 0px;
    margin: 0px;
}

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

#canvas {
    width: 90vmin;
    image-rendering: pixelated;
}

Ini membuat tampilan badan halaman menggunakan flex layout, yang berarti kita dapat memusatkan kanvas di dalamnya, dan width: 90vmin menginstruksikan kanvas menjadi 90% dari ukuran dimensi terpendek layar. Itu image-rendering Bagian ini memberi tahu kanvas untuk tidak memburamkan piksel besar apa pun agar grafik terlihat lebih halus, namun menggambar pikselnya.

Jika server web Anda masih berjalan, salin semua file ke dalamnya public lagi dan segarkan halaman browser. Layar akan tetap kosong, namun jika Anda menggunakan alat pengembangan (tekan F12) Anda seharusnya dapat melihat kanvas di dalam halaman menggunakan tab Inspektur.

Saya kira sebaiknya kita menggambar sesuatu di kanvas!

Menggambar ke kanvas

Pertama, mari kita ganti namanya greet dalam kode Rust kami ke start. Kami juga perlu memperbarui tempat di dalamnya index.html yang mengacu pada greet:

Di dalam src/lib.rs:

...

#[wasm_bindgen]
pub fn start() {
    set_panic_hook();

    alert("Hello, wasm-game!");
}

Di dalam www/index.html:

...
        <script type="module">
            import init, { start } from "./wasm_game.js";
            await init();
            start();
        </script>
...

Perhatikan bahwa kami juga menambahkan panggilan ke set_panic_hook di bagian atas start – kita harus mengimpornya dari kita utils modul yang dibuat oleh wasm-pack new. Menetapkan kait panik di sini berarti jika kode Rust kita panik, pesan panik akan dicetak ke konsol browser, yang sangat berguna.

Buat kode dan salin semuanya ke dalamnya public lagi:

wasm-pack build --target=web
cp www/* pkg/*.js pkg/*.ts pkg/*.wasm public/

Pastikan server web masih berjalan, dan segarkan browser. Ini akan tetap mengganggu Anda dengan pesan “Halo” yang sama.

Sekarang kita perlu menulis beberapa kode non-sepele di dalamnya start fungsi.

Pertama, kita perlu meminta kemampuan untuk memanggil banyak fungsi asli browser dari Rust. Untuk melakukan itu, kami akan menambahkan web-sys peti sebagai ketergantungan.

Di Cargo.toml, tepat di atas [dev-dependencies] mari tambahkan bagian ini:

[dependencies.web-sys]
features = [
    "CanvasRenderingContext2d",
    "Document",
    "Element",
    "Event",
    "HtmlCanvasElement",
    "KeyboardEvent",
    "Window",
    "console",
]
version = "0.3.88"

Jika ini terlihat aneh, itu sama dengan menambahkan satu baris di dalamnya
[dependencies] bagian yang terlihat seperti itu
web-sys = { versi = "0.3.88", fitur = [... but it wraps much better onto
multiple lines. (Re-arranging it this way is a feature of the TOML configuration
language rather than cargo specifically.)

web-sys provides browser functions that we can call from Rust, but it
separates everything into features, so if you want to use a particular browser
feature, e.g. the Window object, you need to specify it in features.
Most of the features, like Window, are very directly named after the object
that you want to use. We’ve added all the features we will want immediately, to
avoid messing around later.

I promised you some Rust code, so let’s update our start function to draw
something on the canvas:

#[wasm_bindgen]
pub fn mulai() { set_panic_hook(); biarkan jendela = jendela().unwrap(); biarkan dokumen = window.document().unwrap(); biarkan kanvas: HtmlCanvasElement = dokumen .get_element_by_id("kanvas") .unwrap() .dyn_into() .unwrap(); biarkan konteks: CanvasRenderingContext2d = kanvas
        .get_context("2d")
        .unwrap() .unwrap() .dyn_into() .unwrap(); konteks.isi_benar(3.0, 2.0, 1.0, 1.0); konteks.isi_benar(4.0, 3.0, 1.0, 1.0); }

Perhatikan bahwa Anda perlu menambahkan beberapa impor di bagian atas agar dapat dikompilasi – sebagian besar berasal dari web-sys peti yang baru saja kita tambahkan.

Kode ini mengambil referensi ke jendela browser, lalu menggunakannya untuk mendapatkan referensi ke dokumen yang sedang ditampilkan. Dokumen tersebut memiliki a
get_element_by_id metode yang dapat kita gunakan untuk menemukan kanvas kita. Jika Anda terbiasa menulis JavaScript di browser, Anda akan terbiasa mengejanya
getElementById. Semua nama diterjemahkan dengan cara ini menjadi hal-hal yang terasa lebih familiar bagi programmer Rust.

Setelah kita memiliki kanvas, untuk benar-benar menggambar di atasnya kita memerlukan konteks (tipe “2d”). Setelah kita memiliki konteksnya, kita sebenarnya dapat melakukan beberapa operasi menggambar.

Jika Anda mengetikkan kode ini dan melakukan rutinitas kompilasi dan penyalinan seperti biasa, lalu segarkan browser, Anda akan melihat beberapa kotak hitam digambar di kanvas Anda!

Kita sudah melakukan cukup banyak hal untuk membuat sebuah game: bagian yang paling rumit masih akan datang, tapi setidaknya sebagian besar adalah kode Rust!

Mendengarkan penekanan tombol

Mari kita dengarkan pengguna yang menekan tombol panah, dan menyimpan arah terakhir yang mereka tekan di a Game struktur.

Pertama kita akan membuat Gamedan enum pendukung dipanggil Dir. Anda bisa memasukkan semua ini ke dalam lib.rs atau gunakan modul terpisah jika Anda mau:

#[derive(Debug)]
pub enum Dir {
    Left,
    Up,
    Right,
    Down,
}

pub struct Game {
    dir: Dir,
}

impl Game {
    fn new() -> Self {
        Self { dir: Dir::Left }
    }

    fn on_keydown(&mut self, event: KeyboardEvent) {
        match event.key_code() {
            37 => self.dir = Dir::Left,
            38 => self.dir = Dir::Up,
            39 => self.dir = Dir::Right,
            40 => self.dir = Dir::Down,
            _ => {}
        }
        console::log_1(&format!("{:?}", self.dir).into());
    }
}

Yang menarik di sini adalah on_keydown metode yang diharapkan untuk menerima a
KeyboardEvent (jenis browser lain yang disediakan oleh web-sys) dan memperbarui kami
Game untuk menyimpan arah yang benar berdasarkan tombol yang ditekan pengguna. Kode kunci yang kami gunakan adalah untuk tombol panah.

Sekarang kita memiliki Game yang siap menerima event keyboard, mari hapus semua kode kanvas itu start dan menambahkan pendengar acara untuk menyediakannya (kami akan segera menambahkan kembali kode kanvas, di tempat lain):

#[wasm_bindgen]
pub fn start() {
    set_panic_hook();

    let game = Arc::new(Mutex::new(Game::new()));

    let window = window().unwrap();

    let game_clone = Arc::clone(&game);
    let on_keydown: Closure = Closure::new(
        move |event: Event| {
            game_clone
                .lock()
                .unwrap()
                .on_keydown(event.dyn_into().unwrap())
        }
    );

    window
        .add_event_listener_with_callback(
            "keydown",
            on_keydown.as_ref().unchecked_ref()
        ).unwrap();

    on_keydown.forget();
}

Pertama kita hebat yang baru Game contoh dan membungkusnya dalam sebuah Arc dari a Mutex
karena ini akan digunakan oleh banyak kode yang berpotensi bersamaan seperti event handler, jadi kita harus bisa memanggil lock menunggu giliran untuk mendapatkan akses ke sana.

Kemudian kita membuat a Closureyang merupakan cara untuk membungkus penutupan Rust sehingga dapat dipanggil dengan kode JavaScript. Penutupan yang kita bungkus panggilan Game‘S
on_keydown metode dan melewati acara yang diterimanya. Setelah kita berhasil Closurekita dapat mengubahnya menjadi a Function ketik (dengan menelepon
.as_ref().unchecked_ref()), yang merupakan hal yang perlu kita sampaikan
add_event_listener_with_callback. Kami menanyakan hal itu setiap kali a keydown
peristiwa terjadi, penutupan kami harus dipanggil.

Akhirnya, kami menelepon forget pada objek Closure, karena jika tidak maka akan di-drop oleh Rust saat kita keluar dari fungsi ini, sehingga tidak akan ada lagi saat dipanggil sebagai callback.

Jika Anda mengkompilasi-dan-menyalin dan menyegarkan browser, lalu klik di jendela utama dan menekan beberapa tombol panah, Anda akan melihat beberapa pesan debug dicetak ke tab Konsol alat pengembang, yang menyatakan arah mana yang Anda hadapi!

Sekarang, jika kita ingin membuat game, kita memerlukan game loop.

Lingkaran permainan dengan requestAnimationFrame

Cara terbaik untuk membuat game loop di browser adalah dengan fungsi bawaan yang disebut requestAnimationFrameyang mencoba memanggil kode Anda sekitar 60 kali per detik. Untuk menggunakannya, Anda harus menelepon ulang requestAnimationFrame dari dalam panggilan balik yang dipanggil oleh frame terakhir. Melakukannya di dalam penutupan Rust sedikit rumit, tapi inilah resep yang bisa Anda tempelkan ke dalamnya start berfungsi setelah apa yang sudah ada:

    let on_frame_refcell: Rc<RefCell<Option<Closure<dyn Fn(BigInt)>>>> =
        Rc::new(RefCell::new(None));

    let window_clone = window.clone();
    let on_frame_refcell_clone = Rc::clone(&on_frame_refcell);
    let callback = move |ts: BigInt| {
        game.lock().unwrap().on_frame(ts.unchecked_into_f64());

        window_clone
            .request_animation_frame(
                on_frame_refcell_clone
                    .borrow()
                    .as_ref()
                    .unwrap()
                    .as_ref()
                    .unchecked_ref(),
            )
            .unwrap();
    };
    let cb: Closure<dyn Fn(BigInt)> = Closure::new(callback);

    on_frame_refcell.replace(Some(cb));

    window
        .request_animation_frame(
            on_frame_refcell
                .borrow()
                .as_ref()
                .unwrap()
                .as_ref()
                .unchecked_ref(),
        )
        .unwrap();

Jika Anda ingin detail tentang cara kerjanya, saya sarankan menonton video di bagian atas halaman ini. Untuk saat ini, cukuplah dikatakan bahwa kita harus mengadakan penutupan di a Rc<RefCell<_>> sehingga kita bisa merujuknya dari dalam dirinya sendiri.

Agar ini berfungsi, kita perlu menambahkan beberapa impor, dan juga menambahkan satu metode lagi Game:

    pub fn on_frame(&mut self, ts: f64) {
        console::log_1(&"on_frame called".into());
    }

Jika kita melakukan rutin kompilasi-dan-salinan dan menyegarkan browser, kita akan melihat pesan “on_frame dipanggil” muncul di bagian Konsol dengan nomor di sebelahnya yang muncul saat dipanggil 60 kali per detik.

Kami hampir punya cukup uang untuk membuat game. Mari segera tambahkan kembali beberapa gambar ke kanvas, dan kurangi sedikit kecepatan bingkai.

Menggambar setiap frame, dan frame rate yang lebih rendah

Di dalam Game‘S on_frame metodenya, kita akan mengurangi kecepatan frame, hanya melakukan apa pun setiap 400 md atau lebih, dan kita akan menggambar beberapa hal di kanvas. Mudah-mudahan ini akan menunjukkan bagaimana sebuah permainan nyata dapat bekerja: di dalam yang baru
step metodenya, kami berharap kami akan menjalankan beberapa logika permainan (yang mungkin akan memperbarui beberapa properti yang ada di dalamnya Game), lalu gambar hasilnya ke kanvas.

Pertama, kita perlu menyediakan kanvas ke Gamejadi di dalam startayo ambil kanvasnya dan sebarkan ke dalamnya new:

#[wasm_bindgen]
pub fn start() {
    // ...

    let canvas: HtmlCanvasElement = document
        .get_element_by_id("canvas")
        .unwrap()
        .dyn_into()
        .unwrap();

    let game = Arc::new(Mutex::new(Game::new(canvas)));

    // ...

dan mari kita beradaptasi Game untuk menahannya:

pub struct Game {
    canvas: HtmlCanvasElement,
    dir: Dir,
    next_frame_ts: f64,
}

impl Game {
    pub fn new(canvas: HtmlCanvasElement) -> Self {
        Self {
            canvas,
            dir: Dir::Up,
            next_frame_ts: 0.0,
        }
    }
    // ...

Perhatikan bahwa ketika kami berada di sana, bersama dengan canvaskami juga menambahkan
next_frame_ts. Kita akan menggunakannya untuk mendeteksi kapan 400 ms telah berlalu, jadi kita harus memproses frame lainnya. Ini secara efektif memperlambat kecepatan bingkai dari 60 FPS menjadi sekitar 2,5 FPS:

    pub fn on_frame(&mut self, ts: f64) {
        if ts > self.next_frame_ts {
            self.step();
            self.next_frame_ts = ts + 400.0;
        }
    }

Itu ts argumen meningkat seiring berjalannya waktu, jadi kita menunggu hingga tiba waktunya untuk frame berikutnya, dan menelepon step kapan itu terjadi. Sebaiknya kita menambahkan a step metode:

    fn step(&mut self) {
        console::log_1(&format!("Inside step dir={:?}", self.dir).into());

        let context: CanvasRenderingContext2d = self
            .canvas
            .get_context("2d")
            .unwrap()
            .unwrap()
            .dyn_into()
            .unwrap();

        context.fill_rect(3.0, 2.0, 1.0, 1.0);
        context.fill_rect(4.0, 3.0, 1.0, 1.0);
    }

Jika kita mengkompilasi-dan-menyalin dan menyegarkan browser, kita dapat melihat bahwa ini mencetak entri log di tab Konsol beberapa kali per detik, dan juga menggambar di kanvas.

Ini seharusnya cukup untuk membuat Anda mulai menulis permainan kecil di Rust yang berjalan di dalam kanvas browser Anda. Tonton video saya selanjutnya untuk melihat game apa yang saya buat!

Bisakah kamu membuatnya menggambar sesuatu yang berbeda setiap saat step dipanggil?

Berita Terkini

Berita Terbaru

Daftar Terbaru

News

Jasa Impor China

Berita Terbaru

Flash News

RuangJP

Pemilu

Berita Terkini

Prediksi Bola

Technology

Otomotif

Berita Terbaru

Teknologi

Berita terkini

Berita Pemilu

Berita Teknologi

Hiburan

master Slote

Berita Terkini

Pendidikan

Resep

Jasa Backlink

Slot gacor terpercaya

Anime Batch