WebAssembly 소개
이 절에서는 완성된 에뮬레이터를 WebAssembly라는 비교적 새로운 기술을 통해 웹 브라우저에서 실행되도록 구성하는 방법을 다룹니다. WebAssembly에 대해 더 읽어 보시기를 권합니다. WebAssembly는 프로그램을 바이너리 실행 파일로 컴파일하기 위한 형식으로, 그 범위는 .exe와 비슷하지만 웹 브라우저 안에서 실행되도록 만들어졌습니다. 모든 주요 웹 브라우저가 이를 지원하며, 이들 브라우저 개발사들이 함께 개발하고 있는 기업 간 공동 표준입니다. 즉, 웹 코드를 JavaScript나 다른 웹 중심 언어로 작성할 필요 없이 .wasm 파일로의 컴파일을 지원하는 어떤 언어로든 작성할 수 있으며, 그러면서도 브라우저에서 실행할 수 있습니다. 이 글을 쓰는 시점에서 이를 지원하는 주요 언어는 C, C++, Rust이며, 다행히 Rust도 여기에 포함됩니다.
설정하기
WebAssembly용 Rust 타깃으로 직접 크로스 컴파일할 수도 있지만, 적절한 타깃과 의존성을 수동으로 추가하지 않고도 WebAssembly로 쉽게 컴파일할 수 있도록 wasm-pack이라는 유용한 도구 모음이 개발되어 있습니다. 다음 명령으로 이를 설치해야 합니다.
$ cargo install wasm-pack
Windows를 사용하는 경우 openssl-sys 크레이트에서 설치가 실패할 수 있으며 https://github.com/rustwasm/wasm-pack/issues/1108 이때는 https://rustwasm.github.io/wasm-pack/ 에서 직접 내려받아야 합니다.
또한 앞서 언급했듯이, chip8_core 모듈이 wasm 타깃으로 올바르게 컴파일되도록 약간 수정해야 합니다. Rust는 wasm-bindgen이라는 시스템을 사용하여 WebAssembly와 함께 동작하는 훅을 만듭니다. 우리가 사용하는 std 코드는 모두 이미 문제가 없지만, 백엔드에서는 rand 크레이트도 사용하고 있으며, 이 크레이트는 현재 올바르게 동작하도록 설정되어 있지 않습니다. 다행히 이 크레이트는 해당 기능을 지원하므로 활성화하기만 하면 됩니다. chip8_core/Cargo.toml에서 다음 부분을
[dependencies]
rand = "^0.7.3"
다음과 같이 바꾸어야 합니다.
[dependencies]
rand = { version = "^0.7.3", features = ["wasm-bindgen"] }
이 변경은 컴파일 시 rand가 wasm-bindgen 기능을 포함하도록 요구한다는 점을 지정할 뿐이며, 이를 통해 rand가 WebAssembly 바이너리에서 올바르게 동작하게 됩니다.
참고: 이 튜토리얼의 코드를 작성한 뒤 글을 마무리하기까지의 기간에 rand 크레이트가 0.8 버전으로 업데이트되었습니다. 여러 변경 사항 중 하나로 wasm-bindgen 기능이 제거되었습니다. 최신 rand 크레이트를 사용하려는 경우, WebAssembly 지원이 별도의 독립된 크레이트로 분리된 것으로 보입니다. 우리는 가장 기본적인 난수 함수만 사용하므로 0.8로 업그레이드할 필요성을 느끼지 못했지만, 업그레이드하려면 추가적인 통합 작업이 필요할 것으로 보입니다.
chip8_core 모듈을 수정하는 것은 이번이 마지막이며, 나머지 작업은 모두 새 프런트엔드에서 진행합니다. 이제 프런트엔드를 설정해 봅시다. 먼저 다음 명령으로 또 다른 Rust 모듈(크레이트)을 생성합시다:
$ cargo init wasm --lib
이 명령은 익숙해 보일 수 있으며, wasm이라는 새로운 Rust 라이브러리를 하나 더 만듭니다. desktop과 마찬가지로 wasm/Cargo.toml을 수정하여 chip8_core가 있는 위치를 가리키도록 해야 합니다.
[dependencies]
chip8_core = { path = "../chip8_core" }
그런데 desktop과 새로 만든 wasm의 큰 차이점은 desktop이 실행 가능한 프로젝트였다는 것으로, 컴파일하고 실행할 main.rs가 있었습니다. wasm에는 그런 것이 없으며, 웹 페이지에 불러올 .wasm 파일로 컴파일되도록 만들어졌습니다. 프런트엔드 역할을 하는 것은 웹 페이지이므로, 시작을 위해 기본적인 HTML 보일러플레이트를 추가합시다. 웹 페이지 전용 코드를 담을 web이라는 새 폴더를 만든 다음, web/index.html을 만들고 기본적인 HTML 보일러플레이트를 추가하십시오.
<!DOCTYPE html>
<html>
<head>
<title>Chip-8 Emulator</title>
<meta charset="utf-8">
</head>
<body>
<h1>My Chip-8 Emulator</h1>
</body>
</html>
나중에 더 추가하겠지만, 지금은 이것으로 충분합니다. 웹 브라우저에서 파일을 그냥 열기만 해서는 웹 프로그램이 실행되지 않으며, 먼저 웹 서버를 시작해야 합니다. 최신 Mac과 많은 Linux 배포판에는 Python 3가 설치되어 있는데, Python 3가 설치되어 있다면 다음과 같이 간단히 웹 서버를 시작할 수 있습니다.
$ python3 -m http.server
웹 브라우저에서 localhost로 이동하십시오. web 디렉터리에서 이 명령을 실행했다면 index.html 페이지가 표시될 것입니다. Windows에서 로컬 웹 서버를 시작하는 간단한 내장 방법을 찾아보았지만, 마땅한 방법을 찾지 못했습니다. 저는 개인적으로 Python 3를 사용하지만, npm이나 일부 Visual Studio Code 확장 같은 다른 유사한 서비스를 사용하셔도 좋습니다. 로컬 웹 페이지를 호스팅할 수만 있다면 어느 것을 사용해도 상관없습니다.
WebAssembly API 정의하기
chip8_core는 이미 만들어 두었지만, 지금은 desktop에 추가했던 기능이 모두 빠져 있습니다. 파일 불러오기, 키 입력 처리, 틱을 실행할 시점 알려 주기 등이 그렇습니다. 한편으로는 JavaScript를 실행할 웹 페이지가 있으며, 이 웹 페이지는 사용자의 입력을 처리하고 항목을 표시해야 합니다. 그 사이에 들어가는 것이 바로 wasm 크레이트입니다. 이 크레이트는 JavaScript로부터 입력을 받아 chip8_core에 필요한 데이터 타입으로 변환합니다.
무엇보다도, 어떻게든 chip8_core::Emu 객체를 생성하고 웹 페이지가 유지되는 동안 계속 스코프 안에 두어야 합니다.
먼저 Rust가 JavaScript와 연동할 수 있도록 하는 데 필요한 외부 크레이트 몇 개를 포함합시다. wasm/Cargo.toml을 열고 다음 의존성을 추가하십시오.
[dependencies]
chip8_core = { path = "../chip8_core" }
js-sys = "^0.3.46"
wasm-bindgen = "^0.2.69"
[dependencies.web-sys]
version = "^0.3.46"
features = []
web-sys를 다른 의존성과 다르게 다루고 있다는 점을 눈치채셨을 것입니다. 이 크레이트는 Cargo.toml에 포함하기만 하면 그 안의 모든 것을 사용할 수 있는 구조가 아니라, 크레이트와 함께 제공되지만 기본적으로는 사용할 수 없는 추가 “기능(features)“을 별도로 지정해야 하는 구조로 되어 있습니다. 곧 web_sys 기능을 추가할 것이므로 이 파일을 열어 두십시오.
이 크레이트는 다른 언어와 연동할 것이므로, 두 언어가 어떻게 통신할지 지정해야 합니다. 세부 사항까지 깊이 들어가지는 않겠지만, Rust는 C 언어의 ABI를 사용하여 이를 지원하는 다른 언어와 쉽게 통신할 수 있으며, 그렇게 하면 wasm 바이너리가 크게 단순해집니다. 따라서 cargo에 이를 사용하도록 알려 주어야 합니다. 다음 내용도 wasm/Cargo.toml에 추가하십시오.
[lib]
crate-type = ["cdylib"]
훌륭합니다. 이제 wasm/src/lib.rs로 넘어갑시다. Emu 객체와 함께, JavaScript와 연동하고 동작하는 데 필요한 모든 프런트엔드 함수를 담을 구조체를 만듭시다. 또한 chip8_core의 모든 공개 항목도 포함해야 합니다.
#![allow(unused)]
fn main() {
use chip8_core::*;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct EmuWasm {
chip8: Emu,
}
}
#[wasm_bindgen] 태그에 주목하십시오. 이 태그는 이 구조체를 WebAssembly용으로 구성해야 한다는 것을 컴파일러에 알려 줍니다. JavaScript 내부에서 호출될 함수나 구조체에는 모두 이 태그가 있어야 합니다. 생성자도 정의합시다.
#![allow(unused)]
fn main() {
use chip8_core::*;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct EmuWasm {
chip8: Emu,
}
#[wasm_bindgen]
impl EmuWasm {
#[wasm_bindgen(constructor)]
pub fn new() -> EmuWasm {
EmuWasm {
chip8: Emu::new(),
}
}
}
}
꽤 간단합니다. 가장 주목할 점은 컴파일러가 우리가 하려는 일을 알 수 있도록 new 메서드에 특별한 constructor를 포함해야 한다는 것입니다.
이제 chip8 에뮬레이션 코어 객체를 담은 구조체가 생겼습니다. 여기서는 키 입력/해제를 코어에 전달하기, 파일 불러오기, 틱 진행하기 등 desktop 프런트엔드에서 필요했던 것과 같은 메서드들을 구현하겠습니다. 가장 쉬운 CPU와 타이머 틱부터 시작합시다.
#![allow(unused)]
fn main() {
#[wasm_bindgen]
impl EmuWasm {
// -- Unchanged code omitted --
#[wasm_bindgen]
pub fn tick(&mut self) {
self.chip8.tick();
}
#[wasm_bindgen]
pub fn tick_timers(&mut self) {
self.chip8.tick_timers();
}
}
}
이것으로 끝입니다. 이 함수들은 chip8_core의 해당 함수를 호출하는 얇은 래퍼일 뿐입니다. 이 함수들은 아무 입력도 받지 않으므로, 틱을 실행하는 것 외에는 특별히 할 일이 없습니다.
chip8_core에서 만들어 놓고 한 번도 사용하지 않았던 reset 함수를 기억하십니까? 이제 드디어 그 함수를 사용할 차례입니다. 이것도 앞의 두 함수와 마찬가지로 래퍼가 됩니다.
#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub fn reset(&mut self) {
self.chip8.reset();
}
}
키 누름은 이 함수들 중에서 desktop에서 했던 것과 달라지는 첫 번째 함수입니다. 이것은 desktop에서 했던 것과 비슷한 방식으로 동작하지만, SDL 키 입력을 받는 대신 JavaScript에서 오는 키 입력을 받아야 합니다. web-sys 기능을 몇 가지 추가하겠다고 약속했으니, 지금 추가합시다. wasm/Cargo.toml로 돌아가 KeyboardEvent 기능을 추가하십시오
[dependencies.web-sys]
version = "^0.3.46"
features = [
"KeyboardEvent"
]
#![allow(unused)]
fn main() {
use web_sys::KeyboardEvent;
// -- Unchanged code omitted --
impl EmuWasm {
// -- Unchanged code omitted --
#[wasm_bindgen]
pub fn keypress(&mut self, evt: KeyboardEvent, pressed: bool) {
let key = evt.key();
if let Some(k) = key2btn(&key) {
self.chip8.keypress(k, pressed);
}
}
}
fn key2btn(key: &str) -> Option<usize> {
match key {
"1" => Some(0x1),
"2" => Some(0x2),
"3" => Some(0x3),
"4" => Some(0xC),
"q" => Some(0x4),
"w" => Some(0x5),
"e" => Some(0x6),
"r" => Some(0xD),
"a" => Some(0x7),
"s" => Some(0x8),
"d" => Some(0x9),
"f" => Some(0xE),
"z" => Some(0xA),
"x" => Some(0x0),
"c" => Some(0xB),
"v" => Some(0xF),
_ => None,
}
}
}
이것은 desktop용 구현과 매우 비슷하지만, JavaScript KeyboardEvent를 받는다는 점이 다르며, 이로부터 파싱할 문자열을 얻게 됩니다. 키 문자열은 대소문자를 구분하므로, 플레이어가 Shift 키를 계속 누르고 있기를 바라는 것이 아니라면 모두 소문자로 유지하십시오.
게임을 불러올 때도 비슷한 상황입니다. 비슷한 방식을 따르지만, JavaScript 객체를 받아서 처리해야 한다는 점이 다릅니다.
#![allow(unused)]
fn main() {
use js_sys::Uint8Array;
// -- Unchanged code omitted --
impl EmuWasm {
// -- Unchanged code omitted --
#[wasm_bindgen]
pub fn load_game(&mut self, data: Uint8Array) {
self.chip8.load(&data.to_vec());
}
}
}
남은 것은 실제로 화면에 렌더링하는 함수뿐입니다. 여기서는 빈 함수를 만들어 두고 구현은 잠시 미루겠습니다. 대신 웹 페이지로 다시 눈을 돌려 반대 방향에서부터 작업을 시작하겠습니다.
#![allow(unused)]
fn main() {
impl EmuWasm {
// -- Unchanged code omitted --
#[wasm_bindgen]
pub fn draw_screen(&mut self, scale: usize) {
// TODO
}
}
}
JavaScript 설정을 마치고 정확히 어떻게 그릴지 정해지면 여기로 다시 돌아오겠습니다.
프런트엔드 기능 만들기
이제 JavaScript를 직접 다뤄 볼 차례입니다. 먼저 아주 밋밋한 웹 페이지에 몇 가지 요소를 추가합시다. PC에서 실행되는 에뮬레이터를 만들 때는 SDL을 사용해 그림을 그릴 창을 만들었습니다. 웹 페이지에서는 HTML5가 제공하는 캔버스라는 요소를 사용하겠습니다. 또한 웹 페이지가 (아직 존재하지 않는) JS 스크립트를 가리키도록 미리 설정해 두겠습니다.
<!DOCTYPE html>
<html>
<head>
<title>Chip-8 Emulator</title>
<meta charset="utf-8">
</head>
<body>
<h1>My Chip-8 Emulator</h1>
<label for="fileinput">Upload a Chip-8 game: </label>
<input type="file" id="fileinput" autocomplete="off"/>
<br/>
<canvas id="canvas">If you see this message, then your browser doesn't support HTML5</canvas>
</body>
<script type="module" src="index.js"></script>
</html>
여기서는 세 가지를 추가했습니다. 첫째, 클릭하면 사용자가 실행할 Chip-8 게임을 선택할 수 있는 버튼입니다. 둘째, canvas 요소로, 오래된 브라우저를 사용하는 불운한 사용자를 위한 짧은 메시지를 포함합니다. 마지막으로, 웹 페이지가 곧 만들 index.js 스크립트도 불러오도록 지정했습니다. 이 글을 쓰는 시점에는 JavaScript로 .wasm 파일을 불러오려면 module 타입임을 명시해야 한다는 점에 유의하십시오.
이제 index.js를 만들고 필요한 항목 몇 가지를 정의합시다. 먼저 JavaScript가 WebAssembly 함수를 불러오도록 해야 합니다. 다만 여기서 직접 불러오지는 않을 것입니다. wasm-pack으로 컴파일하면 .wasm 파일뿐만 아니라 자동 생성된 JavaScript “글루” 코드도 함께 생성되는데, 이 코드는 우리가 정의한 각 함수를 여기서 사용할 수 있는 JavaScript 함수로 감싸 줍니다.
import init, * as wasm from "./wasm.js"
이것은 모든 함수와 함께, wasm의 어떤 것이든 사용하기 전에 먼저 호출해야 하는 특별한 init 함수를 가져옵니다.
이제 몇 가지 상수를 정의하고 기본적인 설정을 해 봅시다.
import init, * as wasm from "./wasm.js"
const WIDTH = 64
const HEIGHT = 32
const SCALE = 15
const TICKS_PER_FRAME = 10
let anim_frame = 0
const canvas = document.getElementById("canvas")
canvas.width = WIDTH * SCALE
canvas.height = HEIGHT * SCALE
const ctx = canvas.getContext("2d")
ctx.fillStyle = "black"
ctx.fillRect(0, 0, WIDTH * SCALE, HEIGHT * SCALE)
const input = document.getElementById("fileinput")
이 모든 내용은 desktop 빌드에서 본 것과 비슷해 보일 것입니다. HTML 캔버스를 가져와 그 크기를 Chip-8 화면의 크기에 맞추고, 조금 더 확대합니다(원하는 대로 자유롭게 조정하십시오).
EmuWasm 객체를 불러오고 주요 에뮬레이션을 처리할 메인 run 함수를 만들어 봅시다.
async function run() {
await init()
let chip8 = new wasm.EmuWasm()
document.addEventListener("keydown", function(evt) {
chip8.keypress(evt, true)
})
document.addEventListener("keyup", function(evt) {
chip8.keypress(evt, false)
})
input.addEventListener("change", function(evt) {
// Handle file loading
}, false)
}
run().catch(console.error)
여기서는 필수인 init 함수를 호출했는데, 이 함수는 WebAssembly 바이너리를 사용하기 전에 초기화하라고 브라우저에 알려 줍니다. 그런 다음 새 EmuWasm 객체를 만들어 에뮬레이터 백엔드를 생성합니다.
이제 버튼이 눌렸을 때 파일을 불러오는 부분을 처리하겠습니다.
input.addEventListener("change", function(evt) {
// Stop previous game from rendering, if one exists
if (anim_frame != 0) {
window.cancelAnimationFrame(anim_frame)
}
let file = evt.target.files[0]
if (!file) {
alert("Failed to read file")
return
}
// Load in game as Uint8Array, send to .wasm, start main loop
let fr = new FileReader()
fr.onload = function(e) {
let buffer = fr.result
const rom = new Uint8Array(buffer)
chip8.reset()
chip8.load_game(rom)
mainloop(chip8)
}
fr.readAsArrayBuffer(file)
}, false)
function mainloop(chip8) {
}
이 함수는 input 버튼에 이벤트 리스너를 추가하며, 이 리스너는 버튼이 클릭될 때마다 실행됩니다. desktop 프런트엔드에서는 SDL을 사용하여 창에 그리는 일뿐만 아니라 60 FPS로 실행되도록 보장하는 일도 관리했습니다. 캔버스에서 이에 해당하는 기능은 “애니메이션 프레임(Animation Frames)“입니다. 캔버스에 무언가를 렌더링하고 싶을 때마다 창에 프레임을 애니메이션하도록 요청하면, 창은 60 FPS 성능을 보장하기 위해 적절한 시간이 지날 때까지 기다립니다. 이것이 어떻게 동작하는지는 잠시 후에 살펴보겠지만, 지금은 새 게임을 불러오는 경우 이전 애니메이션을 중지해야 한다는 것을 프로그램에 알려 주어야 합니다. 또한 웹 페이지를 새로 고칠 필요 없이 모든 것이 처음 시작할 때와 똑같은 상태가 되도록, ROM을 불러오기 전에 에뮬레이터를 초기화합니다.
그다음에는 사용자가 지정한 파일을 확인합니다. 그것이 실제로 Chip-8 프로그램인지 확인할 필요는 없지만, 어떤 종류든 파일이기는 한지 확인해야 합니다. 그런 다음 파일을 읽어 들여 EmuWasm 객체를 통해 백엔드에 전달합니다. 게임을 불러오고 나면 메인 에뮬레이션 루프로 들어갈 수 있습니다!
function mainloop(chip8) {
// Only draw every few ticks
for (let i = 0; i < TICKS_PER_FRAME; i++) {
chip8.tick()
}
chip8.tick_timers()
// Clear the canvas before drawing
ctx.fillStyle = "black"
ctx.fillRect(0, 0, WIDTH * SCALE, HEIGHT * SCALE)
// Set the draw color back to white before we render our frame
ctx.fillStyle = "white"
chip8.draw_screen(SCALE)
anim_frame = window.requestAnimationFrame(() => {
mainloop(chip8)
})
}
이 부분은 desktop 프런트엔드에서 했던 것과 매우 비슷해 보일 것입니다. 캔버스를 지우고 EmuWasm 객체에 현재 프레임을 캔버스에 그리라고 지시하기 전에 여러 번 틱을 실행합니다. 여기서 창에 프레임을 렌더링하고 싶다고 알리며, 위에서 취소해야 할 경우를 대비해 그 ID도 저장해 둡니다. requestAnimationFrame은 60 FPS 성능을 보장하기 위해 기다린 다음, 때가 되면 mainloop를 다시 시작하여 전체 과정을 처음부터 되풀이합니다.
WebAssembly 바이너리 컴파일하기
더 나아가기 전에, 이제 Rust 코드를 빌드해 보고 웹 페이지에서 문제없이 불러올 수 있는지 확인해 봅시다. wasm-pack이 .wasm 바이너리의 컴파일을 처리하지만, npm 같은 웹 패키징 시스템을 사용하지 않겠다는 것도 지정해야 합니다. 빌드하려면 wasm 폴더로 디렉터리를 이동한 뒤 다음을 실행하십시오.
$ wasm-pack build --target web
완료되면 결과물이 새로운 pkg 디렉터리에 빌드됩니다. 이 안에는 여러 항목이 있지만, 필요한 것은 wasm_bg.wasm과 wasm.js뿐입니다. wasm_bg.wasm은 wasm과 chip8_core Rust 크레이트를 하나로 합쳐 컴파일한 것이고, wasm.js는 앞에서 포함했던 JavaScript “글루(glue)” 코드입니다. 이것은 주로 wasm에서 정의한 API를 감싸는 래퍼와 약간의 초기화 코드로 이루어져 있습니다. 실제로 꽤 읽기 쉬우므로 어떤 일을 하는지 한번 살펴볼 만합니다.
로컬 웹 서버에서 페이지를 실행하면 브라우저 콘솔에 아무런 경고도 나타나지 않은 채 게임을 선택하고 불러올 수 있을 것입니다. 하지만 아직 화면 렌더링 함수를 작성하지 않았으므로, 게임이 실제로 실행되는 모습을 볼 수 있도록 이 부분을 마무리합시다.
캔버스에 그리기
이제 마지막 단계인 화면 렌더링입니다. EmuWasm 객체에 빈 draw_screen 함수를 만들고 적절한 시점에 호출하고 있지만, 현재 이 함수는 아무 일도 하지 않습니다. 이를 처리하는 방법은 두 가지가 있습니다. 프레임 버퍼를 JavaScript로 넘겨 렌더링할 수도 있고, EmuWasm 바이너리에서 캔버스를 얻어 Rust에서 렌더링할 수도 있습니다. 어느 방법이든 잘 동작하지만, 개인적으로는 Rust에서 렌더링을 처리하는 편이 더 쉬웠습니다.
지금까지 web_sys 크레이트를 사용해 Rust에서 JavaScript KeyboardEvents를 처리했지만, 이 크레이트는 훨씬 더 많은 JavaScript 요소를 다룰 수 있는 기능을 갖추고 있습니다. 이번에도 사용하려는 요소는 wasm/Cargo.toml에 기능(feature)으로 정의해야 합니다.
[dependencies.web-sys]
version = "^0.3.46"
features = [
"CanvasRenderingContext2d",
"Document",
"Element",
"HtmlCanvasElement",
"ImageData",
"KeyboardEvent",
"Window"
]
다음 단계를 간략히 살펴보겠습니다. HTML5 캔버스에 렌더링하려면 캔버스 객체와 그 컨텍스트를 얻어야 합니다. 컨텍스트는 그리기 함수가 호출되는 대상 객체입니다. WebAssembly 바이너리는 웹 페이지가 불러온 것이므로, JS 스크립트와 마찬가지로 페이지의 모든 요소에 접근할 수 있습니다. JavaScript에서 하는 것과 거의 같은 방식으로 현재 창, 캔버스, 컨텍스트를 가져오도록 new 생성자를 변경하겠습니다.
#![allow(unused)]
fn main() {
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, KeyboardEvent};
#[wasm_bindgen]
pub struct EmuWasm {
chip8: Emu,
ctx: CanvasRenderingContext2d,
}
#[wasm_bindgen]
impl EmuWasm {
#[wasm_bindgen(constructor)]
pub fn new() -> Result<EmuWasm, JsValue> {
let chip8 = Emu::new();
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document.get_element_by_id("canvas").unwrap();
let canvas: HtmlCanvasElement = canvas
.dyn_into::<HtmlCanvasElement>()
.map_err(|_| ())
.unwrap();
let ctx = canvas.get_context("2d")
.unwrap().unwrap()
.dyn_into::<CanvasRenderingContext2d>()
.unwrap();
Ok(EmuWasm{chip8, ctx})
}
// -- Unchanged code omitted --
}
}
JavaScript 프로그래밍을 해 본 분이라면 꽤 익숙해 보일 것입니다. 현재 창의 캔버스를 가져와 그 2D 컨텍스트를 얻고, 이를 EmuWasm 구조체의 멤버 변수로 저장합니다. 이제 렌더링할 실제 컨텍스트가 생겼으므로, draw_screen 함수가 이 컨텍스트에 그리도록 수정할 수 있습니다.
#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub fn draw_screen(&mut self, scale: usize) {
let disp = self.chip8.get_display();
for i in 0..(SCREEN_WIDTH * SCREEN_HEIGHT) {
if disp[i] {
let x = i % SCREEN_WIDTH;
let y = i / SCREEN_WIDTH;
self.ctx.fill_rect(
(x * scale) as f64,
(y * scale) as f64,
scale as f64,
scale as f64
);
}
}
}
}
chip8_core에서 디스플레이 버퍼를 가져와 모든 픽셀을 순회합니다. 픽셀이 켜져 있으면 프런트엔드에서 전달한 값만큼 확대하여 그립니다. draw_screen을 호출하기 전에 이미 캔버스를 검은색으로 지우고 그리기 색을 흰색으로 설정했으므로, 여기서는 그 작업을 할 필요가 없다는 점을 잊지 마십시오.
이것으로 끝입니다! 구현이 완료되었습니다. 이제 남은 일은 빌드하고 직접 실행해 보는 것뿐입니다.
wasm 디렉터리로 이동한 뒤 다음 명령을 실행하여 다시 빌드하십시오.
$ wasm-pack build --target web
$ mv pkg/wasm_bg.wasm ../web
$ mv pkg/wasm.js ../web
이제 웹 서버를 시작하고 게임을 고르십시오. 모든 것이 잘 되었다면, 데스크톱에서와 마찬가지로 브라우저에서도 Chip-8 게임을 플레이할 수 있을 것입니다!