Rust 게임 개발: Bevy, Macroquad 등
게임을 만들고 싶어서 Rust를 배우는 사람이 정말 많아요. 아주 좋은 이유예요! 게임에는 속도, 신중한 메모리 사용, 그리고 보스전 한가운데서 갑자기 죽지 않는 코드가 필요한데, 바로 Rust가 잘하는 것들이거든요.
이 페이지에서는 솔직한 그림을 보여 드릴게요. 사람들이 이미 Rust로 무엇을 만들고 있는지, 어떤 엔진이나 라이브러리를 고르면 좋은지, 그리고 거의 모든 게임이 쓰는 두 가지 개념인 게임 루프와 ECS를 알아봐요. 그다음 Bevy와 Macroquad로 만든 첫 프로그램을 볼 거예요.
어떤 예제를 어디서 실행할 수 있나요? 게임 루프, 작은 ECS 월드, 연습 문제는 순수 Rust라서 Rust Playground로 가는 “Try it Yourself” 버튼이 있어요. Bevy와 Macroquad는 창을 열기 때문에, 그 예제들은 여러분의 컴퓨터에서 cargo run으로 실행해야 해요. Macroquad 예제는 이 페이지를 쓰면서 실제로 컴파일해 봤고(웹용으로도요), Bevy 예제는 Bevy의 공식 예제를 따랐어요.
Rust 게임 개발의 솔직한 현재 상황
섹션 제목: “Rust 게임 개발의 솔직한 현재 상황”커뮤니티 사이트 Are We Game Yet?(영어)은 Rust 게임 생태계를 추적해요. 사이트 이름의 질문(“이제 게임 만들 수 있나요?”)에 대한 스스로의 답은 이래요. “거의요. 블록은 다 있으니, 접착제는 직접 가져오세요(Almost. We have the blocks, bring your own glue).” 꽤 정확한 요약이에요.
지금 좋은 점:
- 실제로 출시된 게임이 있어요. Steam에 나온 아늑한 성 쌓기 게임 Tiny Glade와, 오픈 소스 복셀 RPG인 Veloren이 둘 다 Are We Game Yet에 Rust 게임으로 올라가 있어요.
- 강력한 엔진이 있어요. Bevy는 크고 활발한 커뮤니티가 있고, 몇 달마다 새 버전이 나와요.
- 부품이 많아요. Are We Game Yet에는 물리, 오디오, 입력, 네트워킹, UI 등을 위한 크레이트가 정리되어 있어요.
- 게임을 웹에서도 돌릴 수 있어요. Rust는 WebAssembly로 컴파일되기 때문에, 많은 Rust 게임이 브라우저에서도 실행돼요.
아직 어려운 점:
- 아직 “Rust의 Unity”는 없어요. Rust 엔진 대부분은 코드 중심이에요. 비주얼 에디터도 있지만(Fyrox에 하나 있고, Godot에서는 Rust를 쓸 수 있어요), 큰 상용 도구들보다는 젊어요.
- 자주 바뀌어요. Bevy는 아직 1.0 전이고, 릴리스마다 호환성이 깨지는 변경이 있어요. 작년 튜토리얼이 더 이상 컴파일되지 않을 수도 있어요.
- 컴파일 시간. 큰 엔진은 첫 빌드에 몇 분이 걸려요. 이걸 줄이는 요령은 아래에서 배울 거예요.
아티스트들과 팀을 이뤄 내년에 상용 3D 게임을 출시하는 게 목표라면, Unity, Unreal, Godot이 더 안전해요. 하지만 배우고 싶거나, 2D 게임, 게임 잼, 도구, 시뮬레이션을 만들고 싶거나, 그냥 Rust가 좋다면 정말 즐거운 곳이에요. 🦀
어떤 엔진이나 라이브러리를 고를까요?
섹션 제목: “어떤 엔진이나 라이브러리를 고를까요?”도구는 두 종류가 있어요. 엔진은 게임 오브젝트, 씬, 에셋이 어떻게 동작하는지까지 전체 구조를 줘요. 라이브러리(또는 프레임워크)는 창, 그리기, 소리, 입력을 주고, 나머지는 여러분이 직접 정리해요.
| 크레이트 | 무엇인가요 | 이럴 때 좋아요 | 알아 둘 점 |
|---|---|---|---|
bevy (0.20) |
ECS를 중심으로 만든 완전한 2D·3D 엔진 | 더 큰 프로젝트, 3D, 현대 엔진의 동작 방식 배우기 | 가장 유명한 Rust 엔진이에요. 릴리스마다 호환성이 깨지고, 첫 빌드가 오래 걸려요 |
macroquad (0.4) |
raylib에서 영감을 받은 간단한 2D 라이브러리 | 첫 게임, 게임 잼, 웹 게임 | 의존성이 아주 적어서 빌드가 빨라요. 반복문 안에서 그리기 함수를 호출해요 |
ggez (0.10) |
LÖVE에서 영감을 받은 가벼운 2D 프레임워크 | 구조가 분명한 2D 게임 | update와 draw가 있는 EventHandler 트레이트를 구현해요 |
fyrox (1.0) |
씬 에디터가 있는 2D·3D 엔진 | Godot이나 Unity처럼 에디터를 원하는 사람 | 2026년에 1.0이 됐어요. 커뮤니티는 Bevy보다 작아요 |
raylib (6.0) |
C 라이브러리 raylib의 Rust 바인딩 | C에서 raylib를 써 본 사람 | C 라이브러리를 직접 빌드하기 때문에 CMake와 C 빌드 도구가 필요해요(FFI 참고) |
godot (0.5, “gdext”) |
Godot 4 엔진용 Rust 바인딩 | Godot의 에디터를 쓰면서 게임 로직은 Rust로 짜기 | Godot이 GDExtension으로 불러오는 Rust 클래스를 작성해요 |
간단한 규칙: 첫 2D 게임은 Macroquad로 시작하세요. 완전한 엔진을 원하고 ECS를 배울 마음이 있다면 Bevy를 고르세요. 에디터에서 눈으로 보며 배치하고 싶다면 Godot과 gdext나 Fyrox를 고르세요.
게임 루프
섹션 제목: “게임 루프”거의 모든 게임은 그만둘 때까지 계속 반복되는 하나의 큰 반복문이에요.
- 입력 읽기(키보드, 마우스, 컨트롤러)
- 게임 업데이트: 물체를 움직이고, 충돌을 확인하고, 점수를 세요.
- 화면에 새 그림 그리기
반복 한 번을 프레임(frame)이라고 해요. 초당 60프레임(FPS)이면 프레임마다 약 16밀리초가 주어져요. 그런데 프레임마다 걸리는 시간이 다 같지는 않아요. 컴퓨터가 다른 일로 바쁠 수도 있고, 어떤 프레임은 파일을 불러와야 할 수도 있어요.
게임이 플레이어를 “프레임마다 2칸씩” 움직이면, 빠른 컴퓨터에서는 빨라지고 느린 컴퓨터에서는 느려져요. 흔히 쓰는 해결책은 고정 타임스텝(fixed timestep)이에요. 게임 로직은 항상 같은 크기의 걸음(여기서는 10 ms)으로 나아가고, 각 프레임은 지나간 시간 안에 들어가는 만큼 걸음을 실행해요. 남은 시간은 다음 프레임까지 누산기(accumulator)에 모아 둬요.
const STEP_MS: u32 = 10; // 게임 로직은 항상 10 ms씩 나아가요const SPEED: i32 = 2; // 플레이어는 업데이트마다 2칸씩 움직여요
fn main() { // 각 프레임이 실제로 걸린 시간. 진짜 게임에서는 시계로 재요. let frame_times = [16, 16, 45, 5, 16, 33];
let mut accumulator = 0; // 지나갔지만 아직 시뮬레이션하지 않은 시간 let mut position = 0;
for (frame, elapsed) in frame_times.into_iter().enumerate() { accumulator += elapsed;
// 남은 시간 안에 들어가는 만큼 고정 걸음을 실행해요 let mut updates = 0; while accumulator >= STEP_MS { position += SPEED; // update(): 게임 로직 accumulator -= STEP_MS; updates += 1; }
// render(): 무슨 일이 있었든 프레임마다 한 번 그려요 println!("frame {frame}: {elapsed:>2} ms, {updates} update(s), player at {position:>2}, {accumulator} ms left over"); }}실행 결과:
frame 0: 16 ms, 1 update(s), player at 2, 6 ms left overframe 1: 16 ms, 2 update(s), player at 6, 2 ms left overframe 2: 45 ms, 4 update(s), player at 14, 7 ms left overframe 3: 5 ms, 1 update(s), player at 16, 2 ms left overframe 4: 16 ms, 1 update(s), player at 18, 8 ms left overframe 5: 33 ms, 4 update(s), player at 26, 1 ms left overframe 2를 보세요. 이 프레임은 느렸기 때문에(45 ms) 따라잡으려고 업데이트를 4번 실행했어요. 전체로 보면 131 ms가 지났고, 게임은 10 ms짜리 업데이트를 13번 실행했어요. 시간이 프레임으로 어떻게 나뉘든 플레이어는 26에서 끝나요. 그래서 물리가 안정적이고 어떤 컴퓨터에서나 똑같이 동작해요. 이건 리플레이와 온라인 게임에서도 중요해요. 위험한 점이 하나 있어요. 프레임 하나가 아주 느리면 따라잡으려고 업데이트를 많이 실행하게 되고, 그러면 다음 프레임도 느려져요. 그래서 실제 루프는 프레임당 업데이트 횟수에 상한도 둬요(예를 들어 Bevy의 게임 시계는 기본적으로 프레임당 최대 250 ms까지만 나아가요). 출력 문자열은 {:>2}로 칸을 맞추기 때문에 영어로 두었어요.
또 하나 흔한 방법은 델타 타임(delta time)이에요. 지난 프레임 이후 지난 초를 재서 속도에 곱해요(x += speed * dt). 더 간단하고, 아래의 Bevy와 Macroquad 예제도 이 방법을 써요. 많은 엔진이 둘 다 써요. 애니메이션에는 델타 타임을, 물리에는 고정 걸음을 쓰는 식이에요.
ECS란 무엇일까요: 엔티티, 컴포넌트, 시스템
섹션 제목: “ECS란 무엇일까요: 엔티티, 컴포넌트, 시스템”다른 많은 언어에서는 Player 클래스를 만들고, Character를 상속하는 Enemy 클래스를 만드는 식으로 짜요. 이건 금방 복잡해져요. 주워서 들 수도 있는 적은 어떻게 하죠? 그래서 대부분의 Rust 게임 엔진은 대신 ECS(Entity Component System)를 써요.
- 엔티티(entity)는 그냥 숫자 같은 ID예요. 자기만의 데이터도 동작도 없어요.
- 컴포넌트(component)는 엔티티에 붙이는 평범한 데이터 조각이에요.
Position,Velocity,Health같은 거요. Rust에서는 평범한 구조체나 열거형이에요. - 시스템(system)은 특정 컴포넌트를 가진 모든 엔티티에 대해 실행되는 함수예요. 예를 들어 이동 시스템은
Position과Velocity를 둘 다 가진 모든 것에 동작해요.
나무에는 Position은 있지만 Velocity는 없으니, 이동 시스템이 나무를 건너뛰어요. 나무를 움직이고 싶다고요? Velocity를 붙여 주면 돼요. 클래스를 고칠 필요가 없어요. 데이터와 동작이 따로 떨어져 있고, 이 방식은 Rust의 대여 규칙과도 잘 맞아요.
여기 순수 Rust로 만든 아주 작은 ECS 월드가 있어요. 컴포넌트 타입마다 자기 벡터가 있고, 엔티티 ID가 곧 인덱스예요. None은 “이 엔티티는 그 컴포넌트가 없다”는 뜻이에요(Option 참고).
struct Position { x: i32, y: i32 }struct Velocity { dx: i32, dy: i32 }
// 엔티티는 그냥 ID예요: 모든 컴포넌트 목록에 쓰는 인덱스type Entity = usize;
#[derive(Default)]struct World { names: Vec<&'static str>, positions: Vec<Option<Position>>, velocities: Vec<Option<Velocity>>,}
impl World { fn spawn(&mut self, name: &'static str, pos: Option<Position>, vel: Option<Velocity>) -> Entity { self.names.push(name); self.positions.push(pos); self.velocities.push(vel); self.names.len() - 1 }}
// 시스템: Position과 Velocity를 둘 다 가진 모든 엔티티에 동작해요fn movement_system(world: &mut World) { for (pos, vel) in world.positions.iter_mut().zip(&world.velocities) { if let (Some(pos), Some(vel)) = (pos, vel) { pos.x += vel.dx; pos.y += vel.dy; } }}
// 또 다른 시스템: Position을 가진 모든 엔티티를 출력해요fn print_system(world: &World, tick: u32) { println!("tick {tick}:"); for (id, pos) in world.positions.iter().enumerate() { if let Some(pos) = pos { println!(" #{id} {:<6} at ({}, {})", world.names[id], pos.x, pos.y); } }}
fn main() { let mut world = World::default(); world.spawn("player", Some(Position { x: 0, y: 0 }), Some(Velocity { dx: 1, dy: 0 })); world.spawn("tree", Some(Position { x: 5, y: 2 }), None); // Velocity가 없어서 절대 움직이지 않아요 world.spawn("arrow", Some(Position { x: 0, y: 3 }), Some(Velocity { dx: 3, dy: -1 }));
for tick in 1..=2 { movement_system(&mut world); print_system(&world, tick); }}실행 결과:
tick 1: #0 player at (1, 0) #1 tree at (5, 2) #2 arrow at (3, 2)tick 2: #0 player at (2, 0) #1 tree at (5, 2) #2 arrow at (6, 1)나무는 Velocity가 없어서 (5, 2)에 그대로 있어요. 아무도 “나무는 움직이지 않는다”고 코드에 쓰지 않았어요. 데이터에서 저절로 그렇게 된 거예요. 이름은 {:<6}으로 칸을 맞추기 때문에 영어로 두었어요. 실제 ECS 라이브러리는 이것보다 훨씬 빠르고 똑똑하지만(CPU가 빠르게 읽을 수 있도록 컴포넌트를 저장하고, 시스템을 병렬로 실행해요), 기본 아이디어는 같아요.
첫 Bevy 앱
섹션 제목: “첫 Bevy 앱”Bevy(영어)는 가장 유명한 Rust 엔진이고, 그 안의 모든 것이 ECS예요. 0.20 버전이 2026년 10월에 나왔어요. Bevy는 최신 stable Rust가 필요하니, 먼저 업데이트한 다음 프로젝트를 만드세요.
rustup updatecargo new my_bevy_gamecd my_bevy_gamecargo add bevyLinux에서는 시스템 라이브러리(소리, 입력, 그래픽)도 몇 개 필요해요. Bevy의 설정 가이드(영어)에 배포판별로 정리되어 있어요. Windows에서는 Visual Studio C++ Build Tools가, macOS에서는 Xcode 명령줄 도구가 필요해요.
이 프로그램은 Bevy의 공식 2d/move_sprite 예제를 바탕으로 했어요. 주황색 사각형이 왼쪽과 오른쪽으로 미끄러지듯 움직여요. (공식 예제는 그림을 불러오지만, 여기서는 이미지 파일이 필요 없도록 단색 사각형을 썼어요.)
use bevy::prelude::*;
fn main() { App::new() .add_plugins(DefaultPlugins) // 창, 렌더링, 입력, 오디오... .add_systems(Startup, setup) // 시작할 때 한 번 실행돼요 .add_systems(Update, sprite_movement) // 프레임마다 실행돼요 .run();}
// 컴포넌트: 이 엔티티가 어느 쪽으로 움직이는지#[derive(Component)]enum Direction { Left, Right,}
fn setup(mut commands: Commands) { commands.spawn(Camera2d);
// 컴포넌트 세 개를 가진 엔티티 하나를 만들어요 commands.spawn(( Sprite::from_color(Color::srgb(1.0, 0.5, 0.2), Vec2::new(50.0, 50.0)), Transform::from_xyz(0.0, 0.0, 0.0), Direction::Right, ));}
// 시스템: Direction과 Transform을 둘 다 가진 모든 엔티티에 대해 실행돼요fn sprite_movement(time: Res<Time>, mut sprite_position: Query<(&mut Direction, &mut Transform)>) { for (mut logo, mut transform) in &mut sprite_position { match *logo { Direction::Right => transform.translation.x += 150. * time.delta_secs(), Direction::Left => transform.translation.x -= 150. * time.delta_secs(), }
if transform.translation.x > 200. { *logo = Direction::Left; } else if transform.translation.x < -200. { *logo = Direction::Right; } }}위의 작은 ECS 월드와 비교해 보세요. #[derive(Component)]는 평범한 열거형을 컴포넌트로 만들어요. commands.spawn((...))은 컴포넌트 튜플로 엔티티를 만들어요. 그리고 Query<(&mut Direction, &mut Transform)>는 Bevy에게 “둘 다 가진 모든 엔티티”를 달라고 해요. 우리 movement_system이 zip으로 했던 것과 똑같아요. time.delta_secs()는 게임 루프 섹션에서 본 델타 타임이라서, 어떤 컴퓨터에서든 사각형이 1초에 150픽셀씩 움직여요.
cargo run으로 실행하세요. 첫 빌드는 크레이트 수백 개를 컴파일하느라 몇 분이 걸릴 수 있으니 느긋하게 기다려 주세요. 그다음부터는 다시 빌드하는 게 훨씬 빨라요.
Bevy를 더 빨리 컴파일(하고 실행)하기
섹션 제목: “Bevy를 더 빨리 컴파일(하고 실행)하기”Bevy의 설정 가이드는 두 가지를 추천해요. 첫째, Cargo.toml에 다음을 추가하세요. Bevy와 다른 의존성은 최적화하면서, 여러분의 코드는 빠르게 컴파일되도록 해 줘요.
[dependencies]bevy = "0.20"
# Enable a small amount of optimization in the dev profile.[profile.dev]opt-level = 1
# Enable a large amount of optimization in the dev profile for dependencies.[profile.dev.package."*"]opt-level = 3(주석은 Bevy 가이드 그대로예요. 위쪽은 “dev 프로필에 약간의 최적화를 켠다”, 아래쪽은 “dev 프로필에서 의존성에 강한 최적화를 켠다”는 뜻이에요.) 이 설정이 없으면 게임의 디버그 빌드가 아주 느릴 수 있어요. 프레임 레이트가 낮고 로딩이 오래 걸려요. 의존성은 한 번만 컴파일되니까, 최적화 비용은 첫 빌드에서만 치르면 돼요. (프로필은 성능 페이지에서 설명해요.)
둘째, 개발하는 동안에는 dynamic_linking 피처를 켜세요. 그러면 Bevy가 별도의 공유 라이브러리로 빌드되어서, 코드를 바꿀 때마다 Cargo가 Bevy 전체를 여러분의 프로그램에 다시 링크하지 않아도 돼요.
cargo run --features bevy/dynamic_linkingBevy 가이드의 경고 두 가지가 있어요. Windows에서는 dynamic_linking이 위의 opt-level 설정과 함께일 때만 동작해요. 그리고 이 피처를 켠 채로 게임을 출시하지 마세요. 게임 옆에 라이브러리 파일이 하나 더 있어야 하거든요. 릴리스는 이 피처 없이 빌드하세요. 가이드에는 더 빠른 링커(lld나 mold)를 쓰는 방법도 나와 있는데, Linux에서 특히 큰 도움이 돼요.
Macroquad 창
섹션 제목: “Macroquad 창”Macroquad(영어)는 느낌이 아주 달라요. ECS도 앱 빌더도 없어요. 게임 루프를 직접 쓰고, draw_circle 같은 간단한 함수로 그려요. 의존성이 아주 적어서 처음부터 빌드해도 몇 분이 아니라 몇 초면 끝나요.
cargo new move_the_ballcd move_the_ballcargo add macroquad[dependencies]macroquad = "0.4"이 작은 게임은 화살표 키로 움직이는 공을 보여 줘요.
use macroquad::prelude::*;
#[macroquad::main("Move the Ball")]async fn main() { let mut player = vec2(screen_width() / 2.0, screen_height() / 2.0); let speed = 250.0; // 초당 픽셀 수
loop { // 1. 입력: 화살표 키로 플레이어를 움직여요 let dt = get_frame_time(); // 지난 프레임 이후 지난 초 if is_key_down(KeyCode::Right) { player.x += speed * dt; } if is_key_down(KeyCode::Left) { player.x -= speed * dt; } if is_key_down(KeyCode::Down) { player.y += speed * dt; } if is_key_down(KeyCode::Up) { player.y -= speed * dt; } if is_key_pressed(KeyCode::Escape) { break; }
// 2. 플레이어가 화면 밖으로 나가지 않게 해요 player.x = player.x.clamp(0.0, screen_width()); player.y = player.y.clamp(0.0, screen_height());
// 3. 이번 프레임의 모든 것을 그려요 clear_background(DARKBLUE); draw_circle(player.x, player.y, 20.0, ORANGE); draw_text("Arrow keys to move, Esc to quit", 20.0, 30.0, 28.0, WHITE);
next_frame().await }}게임 루프의 세 단계, 입력, 업데이트, 그리기가 보이죠. #[macroquad::main("Move the Ball")]은 그 제목으로 창을 열어요. main이 async이지만, Tokio가 필요하거나 async를 알아야 하는 건 아니에요. next_frame().await는 그냥 “이 프레임을 보여 주고 다음 프레임을 기다려라”라는 뜻이에요. Linux에서는 Macroquad의 README에 먼저 설치할 시스템 패키지 몇 개가 나와 있어요(Ubuntu에서는 pkg-config libx11-dev libxi-dev libgl1-mesa-dev libasound2-dev).
웹에서 돌아가는 게임(WebAssembly)
섹션 제목: “웹에서 돌아가는 게임(WebAssembly)”Macroquad와 Bevy 둘 다 게임을 WebAssembly로 빌드해서 브라우저에서 돌릴 수 있어요. Wasm이 처음이라면 Rust와 WebAssembly를 먼저 읽어 보세요. 시작은 항상 Wasm 타깃을 추가하는 거예요.
rustup target add wasm32-unknown-unknownMacroquad는 이걸 아주 쉽게 해 줘요. 명령 하나로 .wasm 파일이 만들어져요.
cargo build --release --target wasm32-unknown-unknown파일은 target/wasm32-unknown-unknown/release/move_the_ball.wasm에 생겨요(위 예제로 만들었을 때 약 600 KB였어요). Macroquad의 README에는 작은 JavaScript 파일 mq_js_bundle.js로 이 파일을 불러오는 짧은 index.html이 나와 있어요. 두 파일을 한 폴더에 넣고 아무 로컬 웹 서버로나 제공하세요. 예를 들면 basic-http-server .이에요(cargo install basic-http-server로 설치해요). HTML 파일을 더블 클릭하는 것만으로는 보통 안 돼요. 브라우저가 디스크에서 바로 .wasm 파일을 불러오는 걸 막기 때문이에요.
Bevy는 wasm-bindgen으로 JavaScript 연결 코드를 만들어요. 다음 단계는 Bevy의 examples README를 위의 my_bevy_game 프로젝트에 맞게 고친 거예요.
cargo install wasm-bindgen-clicargo build --release --target wasm32-unknown-unknownwasm-bindgen --out-dir web --target web target/wasm32-unknown-unknown/release/my_bevy_game.wasm그러면 web 폴더에 .js 파일과 _bg.wasm 파일이 생겨요. 작은 HTML 페이지가 .js 파일을 불러와서 게임을 시작해요. Bevy의 설정 가이드는 다운로드 크기를 줄이기 위해 opt-level = "s"를 쓰는 wasm-release 프로필도 제안해요. 웹 빌드는 게임 잼 출품작을 공유하기에 아주 좋아요. 설치할 것 없이 클릭 한 번이면 플레이할 수 있거든요.
자주 하는 실수
섹션 제목: “자주 하는 실수”프레임마다 고정된 양만큼 움직이기. 프레임마다 x += 5.0을 하면, 120 FPS에서는 60 FPS일 때보다 게임이 두 배 빨라져요. 델타 타임을 곱하거나(x += speed * dt) 고정 타임스텝을 쓰세요.
오래된 Bevy 튜토리얼 따라 하기. Bevy는 릴리스마다 API가 바뀌어서, 0.12용 블로그 글은 0.20에서 컴파일되지 않는 경우가 많아요. 튜토리얼이 어느 버전을 쓰는지 확인하고, 가능하면 내 버전의 공식 예제를 보세요. 릴리스마다 Bevy 웹사이트에 마이그레이션 가이드도 있어요.
느린 디버그 빌드. Bevy 게임이 초당 몇 프레임밖에 안 나온다면, 아마 Cargo.toml의 opt-level 설정을 잊었을 거예요.
너무 크게 시작하기. 오픈 월드 RPG는 어떤 언어로든 첫 프로젝트로는 어려워요. Pong, Snake, Breakout, Flappy Bird로 시작하세요. 끝까지 완성하고, 다음 게임은 조금 더 크게 만들어 보세요.
연습 문제
섹션 제목: “연습 문제”1. 열거형으로 게임 상태 만들기
섹션 제목: “1. 열거형으로 게임 상태 만들기”모든 게임에는 화면이 있어요. 메뉴, 게임 본편, 일시 정지 화면, “게임 오버” 같은 거요. Menu, Playing, Paused, GameOver를 가진 열거형 GameState와, Enter, Escape, PlayerDied를 가진 열거형 Input을 만드세요. 그리고 fn next_state(state: GameState, input: Input) -> GameState를 작성하세요. 메뉴에서 Enter를 누르면 게임이 시작되고, Escape는 일시 정지와 재개를 하고, 플레이어가 죽으면 게임이 끝나고, “게임 오버”에서 Enter를 누르면 메뉴로 돌아가요. 그 밖의 입력은 아무것도 바꾸지 않아요. 정해진 입력 목록을 넣고 바뀔 때마다 출력하세요. 힌트: 튜플 (state, input)에 match를 쓰고, 마지막을 (same, _) => same으로 끝내세요.
정답 보기
#[derive(Debug, Clone, Copy)]enum GameState { Menu, Playing, Paused, GameOver,}
#[derive(Debug, Clone, Copy)]enum Input { Enter, Escape, PlayerDied,}
fn next_state(state: GameState, input: Input) -> GameState { match (state, input) { (GameState::Menu, Input::Enter) => GameState::Playing, (GameState::Playing, Input::Escape) => GameState::Paused, (GameState::Paused, Input::Escape) => GameState::Playing, (GameState::Playing, Input::PlayerDied) => GameState::GameOver, (GameState::GameOver, Input::Enter) => GameState::Menu, // 나머지는 전부: 입력을 무시하고 지금 상태에 머물러요 (same, _) => same, }}
fn main() { use Input::*; // Input::Enter 대신 Enter라고 쓸 수 있게 해 줘요 let inputs = [Escape, Enter, Escape, PlayerDied, Escape, PlayerDied, Enter];
let mut state = GameState::Menu; for input in inputs { let new_state = next_state(state, input); println!("{state:?} + {input:?} -> {new_state:?}"); state = new_state; }}실행 결과:
Menu + Escape -> MenuMenu + Enter -> PlayingPlaying + Escape -> PausedPaused + PlayerDied -> PausedPaused + Escape -> PlayingPlaying + PlayerDied -> GameOverGameOver + Enter -> Menu튜플에 대해 매칭하면 허용되는 모든 상태 변화가 한곳에 정리되고, 마지막 갈래 덕분에 나머지 입력은 모두 아무 해가 없어요. 일시 정지한 플레이어는 죽을 수 없어요. (Paused, PlayerDied)가 (same, _)에 걸리기 때문이에요. Bevy에도 States라는 이름으로 같은 개념이 들어 있어요.
- Rust는 게임 개발의 진지한 선택지예요. 아직 “거의 다 온” 단계지만, 강력한 엔진(Bevy), 간단한 라이브러리(Macroquad, ggez), 에디터 중심의 선택지(Fyrox, gdext를 쓰는 Godot), 그리고 많은 부품이 있어요.
- 모든 게임은 입력, 업데이트, 그리기를 반복하는 루프를 돌아요. 델타 타임이나 고정 타임스텝을 써서 속도가 프레임 레이트에 좌우되지 않게 하세요.
- ECS는 게임을 엔티티(ID), 컴포넌트(평범한 데이터), 시스템(특정 컴포넌트를 가진 엔티티에 대해 실행되는 함수)으로 나눠요.
- Bevy는 처음부터 끝까지 ECS예요.
opt-level프로필 설정을 추가하고,dynamic_linking은 개발하는 동안에만 쓰세요. - Macroquad는 2D를 가장 빨리 시작하는 방법이에요. 루프를 직접 쓰고 간단한 함수로 그려요.
- 둘 다
wasm32-unknown-unknown타깃으로 브라우저용 빌드를 만들 수 있어요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. ECS에서 엔티티란 무엇일까요?
2. 고정 타임스텝을 쓰면 왜 게임이 어떤 컴퓨터에서나 똑같이 동작할까요?
3. 완성한 게임을 출시할 때 켜 두면 안 되는 Bevy 설정은 무엇일까요?
4. 빌드 시간이 아주 짧은 작은 2D 게임을 처음으로 만들고 싶어요. 이 페이지가 추천하는 크레이트는 무엇일까요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”- Are We Game Yet? (영어): Rust 게임 엔진, 물리·오디오·네트워킹 라이브러리, 그리고 Rust로 만든 게임을 정리한 커뮤니티 사이트예요.
- Bevy Quick Start: Setup (영어): Bevy 설치, 빠른 컴파일 설정, 릴리스와 Wasm 프로필을 다룬 공식 가이드예요.
- Game development in Rust with Macroquad (영어): Macroquad로 완전한 우주 슈팅 게임을 한 단계씩 만들어 가는 무료 온라인 책이에요.
- The godot-rust book (영어): gdext로 Godot 4의 게임 로직을 Rust로 작성하는 방법을 설명해요.
- Game Loop — Game Programming Patterns (영어): 게임 루프, 델타 타임, 고정 타임스텝을 친절하고 자세하게 설명해요.