프로젝트 개요
bracket-lib 라이브러리를 활용한 간단한 플래피 버드 게임 구현 프로젝트이다. 스페이스바를 통해,玩家를 조작하며,P 키로 게임 일시정지 및 재개가 가능하다.
프로젝트 구조
.
├── Cargo.lock
├── Cargo.toml
├── src/
│ ├── main.rs
│ ├── player/
│ │ ├── player.rs
│ │ └── mod.rs
│ ├── game/
│ │ ├── game.rs
│ │ └── mod.rs
│ └── barriers/
│ ├── barrier.rs
│ └── mod.rs
game.rs는 게임 로직과 조작을 담당하고, player.rs는 플레이어 캐릭터의 동작을 구현하며, barrier.rs는 장애물의 생성과 이동을 처리한다.
모듈 정의 파일들
// game/mod.rs
pub mod game;
// player/mod.rs
pub mod player;
// barriers/mod.rs
pub mod barrier;
main.rs 구현
use bracket_lib::prelude::{main_loop, BError, BTermBuilder};
pub mod game;
pub mod player;
pub mod barriers;
fn main() -> BError {
let ctx = BTermBuilder::simple80x50()
.with_title("Flappy Bird")
.build()
.unwrap();
let game_state = game::game::GameScreen::new();
main_loop(ctx, game_state)
}
game.rs 구현
use crate::player::player;
use crate::barriers::barrier;
use bracket_lib::{
prelude::{BTerm, GameState},
terminal::{VirtualKeyCode, LIGHT_CYAN},
};
use std::collections::LinkedList;
const SCREEN_WIDTH: u32 = 80;
const SCREEN_HEIGHT: u32 = 50;
const GAME_CYCLE: f32 = 120.0;
enum PlayMode {
Active,
Finished,
Paused,
StartScreen,
}
pub struct GameScreen {
current_mode: PlayMode,
points: u32,
player_entity: player::Player,
frame_accumulator: f32,
barrier_collection: LinkedList<barrier::Barrier>,
}
impl GameState for GameScreen {
fn tick(&mut self, ctx: &mut BTerm) {
match self.current_mode {
PlayMode::Active => self.handle_active(ctx),
PlayMode::Finished => self.handle_finished(ctx),
PlayMode::StartScreen => self.handle_menu(ctx),
PlayMode::Paused => self.handle_paused(ctx),
}
}
}
impl GameScreen {
pub fn new() -> Self {
let mut barriers = LinkedList::new();
barriers.push_back(barrier::Barrier::new(0, 35, SCREEN_HEIGHT));
barriers.push_back(barrier::Barrier::new(0, 50, SCREEN_HEIGHT));
barriers.push_back(barrier::Barrier::new(0, 65, SCREEN_HEIGHT));
barriers.push_back(barrier::Barrier::new(0, 80, SCREEN_HEIGHT));
Self {
current_mode: PlayMode::StartScreen,
points: 0,
player_entity: player::Player::new(SCREEN_WIDTH, SCREEN_HEIGHT),
frame_accumulator: 0.0,
barrier_collection: barriers,
}
}
fn restart(&mut self) {
self.current_mode = PlayMode::Active;
self.frame_accumulator = 0.0;
self.player_entity = player::Player::new(SCREEN_WIDTH, SCREEN_HEIGHT);
self.points = 0;
let mut barriers = LinkedList::new();
barriers.push_back(barrier::Boundary::new(0, 35, SCREEN_HEIGHT));
barriers.push_back(barrier::Barrier::new(0, 50, SCREEN_HEIGHT));
barriers.push_back(barrier::Barrier::new(0, 65, SCREEN_HEIGHT));
barriers.push_back(barrier::Barrier::new(0, 80, SCREEN_HEIGHT));
self.barrier_collection = barriers;
}
fn handle_active(&mut self, ctx: &mut BTerm) {
ctx.cls();
ctx.set_bg(SCREEN_WIDTH, SCREEN_HEIGHT, LIGHT_CYAN);
self.frame_accumulator += ctx.frame_time_ms;
if self.frame_accumulator >= GAME_CYCLE {
self.player_entity.apply_gravity();
self.player_entity.update_position();
self.update_barriers();
self.check_passage();
self.frame_accumulator = 0.0;
}
if let Some(key) = ctx.key {
match key {
VirtualKeyCode::P => self.current_mode = PlayMode::Paused,
VirtualKeyCode::Space => {
self.player_entity.jump();
self.frame_accumulator = 0.0;
}
VirtualKeyCode::Q | VirtualKeyCode::Escape => ctx.quit(),
_ => (),
}
}
self.player_entity.render(ctx);
for barrier in &self.barrier_collection {
barrier.render(SCREEN_HEIGHT, ctx);
}
ctx.print(0, 0, format!("점수: {}", self.points));
ctx.print(0, 1, "SPACE: 점프");
ctx.print(0, 2, "P: 일시정지 Q/ESC: 종료");
if self.player_entity.is_out_of_bounds(SCREEN_HEIGHT) {
self.current_mode = PlayMode::Finished;
}
}
fn update_barriers(&mut self) {
for barrier in &mut self.barrier_collection {
barrier.slide(1);
}
}
fn check_passage(&mut self) {
let front_barrier = self.barrier_collection.front().unwrap();
if front_barrier.get_x() as u32 <= self.player_entity.get_x() as u32 {
let half_gap = front_barrier.get_gap_size() / 2;
let top_limit = front_barrier.get_gap_y() - half_gap;
let bottom_limit = front_barrier.get_gap_y() + half_gap;
let player_y = self.player_entity.get_y() as u32;
if player_y > top_limit && player_y < bottom_limit {
self.points += 1;
} else {
self.current_mode = PlayMode::Finished;
}
self.barrier_collection.pop_front();
self.barrier_collection
.push_back(barrier::Barrier::new(self.points, 80, SCREEN_HEIGHT));
}
}
fn handle_finished(&mut self, ctx: &mut BTerm) {
ctx.cls();
ctx.print_centered(6, format!("게임 오버! 점수: {}", self.points));
ctx.print_centered(8, "R: 다시하기");
ctx.print_centered(9, "Q/ESC: 종료");
if let Some(key) = ctx.key {
match key {
VirtualKeyCode::R => {
self.restart();
self.current_mode = PlayMode::Active;
}
VirtualKeyCode::Q | VirtualKeyCode::Escape => ctx.quitting = true,
_ => (),
}
}
}
fn handle_menu(&mut self, ctx: &mut BTerm) {
ctx.cls();
ctx.print_centered(6, "플래피 버드에 오신 것을 환영합니다!");
ctx.print_centered(8, "S: 게임 시작");
ctx.print_centered(9, "Q/ESC: 종료");
if let Some(key) = ctx.key {
match key {
VirtualKeyCode::S => self.current_mode = PlayMode::Active,
VirtualKeyCode::Q | VirtualKeyCode::Escape => ctx.quit(),
_ => (),
}
}
}
fn handle_paused(&mut self, ctx: &mut BTerm) {
ctx.print_centered(0, format!("현재 점수: {}", self.points));
ctx.print_centered(7, "일시정지 상태 - P 키를 눌러 계속하기");
self.player_entity.render(ctx);
if let Some(key) = ctx.key {
match key {
VirtualKeyCode::P => self.current_mode = PlayMode::Active,
VirtualKeyCode::Q | VirtualKeyCode::Escape => ctx.quit(),
_ => (),
}
}
}
}
player.rs 구현
use bracket_lib::prelude::BTerm;
use bracket_lib::terminal::{to_cp437, BLACK, GOLD};
pub struct Player {
position_x: f64,
position_y: f64,
fall_speed: f64,
}
impl Player {
pub fn new(screen_width: u32, screen_height: u32) -> Self {
Self {
position_x: (screen_width / 5) as f64,
position_y: (screen_height / 2) as f64,
fall_speed: 0.1,
}
}
pub fn render(&self, ctx: &mut BTerm) {
ctx.set(
self.position_x as u32,
self.position_y as u32,
GOLD,
BLACK,
to_cp437('@'),
);
}
pub fn get_x(&self) -> f64 {
self.position_x
}
pub fn get_y(&self) -> f64 {
self.position_y
}
pub fn apply_gravity(&mut self) {
self.fall_speed += 0.1;
self.position_y += self.get_velocity();
}
pub fn jump(&mut self) {
self.position_y -= 2.0;
self.fall_speed = 0.1;
}
pub fn update_position(&mut self) {}
pub fn is_out_of_bounds(&self, max_y: u32) -> bool {
self.position_y as u32 > max_y || self.position_y < 0.0
}
fn get_velocity(&mut self) -> f64 {
if self.fall_speed > 2.0 {
self.fall_speed = 2.0;
}
self.fall_speed
}
}
barrier.rs 구현
use bracket_lib::{
prelude::BTerm,
random::RandomNumberGenerator,
terminal::{to_cp437, BLACK, CRIMSON},
};
pub struct Barrier {
x_position: u32,
gap_height: u32,
gap_center: u32,
}
impl Barrier {
pub fn new(current_score: u32, initial_x: u32, height: u32) -> Self {
let mut rng = RandomNumberGenerator::new();
let gap_size;
if current_score > 60 {
gap_size = 2;
} else {
gap_size = u32::max(2, 20 - current_score / 3);
}
let half_gap = gap_size / 2 + 1;
Self {
x_position: initial_x,
gap_height: gap_size,
gap_center: rng.range(half_gap, height - half_gap),
}
}
pub fn slide(&mut self, distance: u32) {
self.x_position -= distance;
}
pub fn get_x(&self) -> u32 {
self.x_position
}
pub fn get_gap_size(&self) -> u32 {
self.gap_height
}
pub fn get_gap_y(&self) -> u32 {
self.gap_center
}
pub fn render(&self, screen_height: u32, ctx: &mut BTerm) {
let render_x = self.x_position;
let half_gap = self.gap_height / 2;
for y in 0..self.gap_center - half_gap {
ctx.set(render_x, y, CRIMSON, BLACK, to_cp437('█'));
}
for y in self.gap_center + half_gap..screen_height {
ctx.set(render_x, y, CRIMSON, BLACK, to_cp437('█'));
}
}
}
Cargo.toml 설정
[package]
name = "flappy_bird"
version = "0.1.0"
edition = "2021"
[dependencies]
bracket-lib = "0.8.7"
이 프로젝트는 Rust의 터미널 라이브러리인 bracket-lib를 활용하여 구현되었다. 게임 루프는 bracket-lib의 main_loop 함수를 사용하며, 각 프레임마다 GameState 트레이트의 tick 메서드가 호출되어 게임 상태를 업데이트한다. 플레이어 캐릭터는 중력의 영향을 받아下落하며, 스페이스바 입력 시 점프 동작을 수행한다. 장애물은 LinkedList로 관리되어 왼쪽으로 이동하면서 새로운 장애물이 오른쪽에서 생성된다.