:: BASIC_14_two_circles_moving_repetition

BamgasiJM·2025년 9월 28일

Nannou <BASIC>

목록 보기
27/41
post-thumbnail

📝 Rust Code

use nannou::prelude::*;

const RADIUS: f32 = 300.0;

fn main() {
    nannou::app(model).update(update).run();
}

struct Model {
    dots: Vec<Dot>,
}

struct Dot {
    position: Point2,
    velocity: Vec2,
}

fn model(app: &App) -> Model {
    app.new_window()
        .size(800, 800)
        .view(view)
        .build()
        .unwrap();

    let mut dots = Vec::new();
    dots.push(create_dot(app));
    
    Model { dots }
}

fn create_dot(app: &App) -> Dot {
    let win = app.window_rect();
    Dot {
        position: pt2(win.left() - RADIUS, win.bottom() - RADIUS), // 완전히 화면 밖에서 시작
        velocity: vec2(2.0, 2.0), // 우상향 속도
    }
}

fn update(app: &App, model: &mut Model, _update: Update) {
    let win = app.window_rect();

    // 모든 원 업데이트
    for dot in &mut model.dots {
        dot.position += dot.velocity;
    }

    // 현재 원이 우상단 경계에 도달했는지 확인
    if let Some(current_dot) = model.dots.first() {
        if current_dot.position.x >= win.right() - RADIUS && 
           current_dot.position.y >= win.top() - RADIUS &&
           model.dots.len() == 1 {
            // 새로운 원 추가 (왼쪽 아래에서 시작)
            model.dots.push(create_dot(app));
        }
    }

    // 화면 밖으로 완전히 나간 원 제거
    model.dots.retain(|dot| {
        !(dot.position.x > win.right() + RADIUS && dot.position.y > win.top() + RADIUS)
    });
}

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();

    draw.background().color(hsl(0.0, 0.0, 0.01));
    
    for dot in &model.dots {
        draw.ellipse()
            .xy(dot.position)
            .radius(RADIUS)
            .color(rgba(0.8, 0.4, 0.4, 1.0));
    }

    draw.to_frame(app, &frame).unwrap();
}


📝 Rust Code + Comment

// nannou 라이브러리의 기본 기능들을 가져옵니다 (prelude는 자주 사용하는 모듈들을 한번에 가져오는 역할)
use nannou::prelude::*;

// 원의 반지름을 상수로 정의 (상수는 컴파일 타임에 값이 결정되며 변경 불가)
const RADIUS: f32 = 300.0;

// 프로그램의 진입점 (main 함수)
fn main() {
    // nannou 앱을 설정하고 실행
    // model 함수로 초기 모델 생성, update 함수로 매 프레임 업데이트, run으로 앱 실행
    nannou::app(model).update(update).run();
}

// 앱의 상태를 나타내는 메인 모델 구조체
struct Model {
    dots: Vec<Dot>,    // 여러 개의 원을 관리하기 위한 벡터 (가변 길이 배열)
}

// 개별 원의 정보를 저장하는 구조체
struct Dot {
    position: Point2, // 2D 좌표 (x, y) - 원의 중심 위치
    velocity: Vec2,   // 2D 벡터 (x, y) - 원의 이동 속도와 방향
}

// 앱 초기화 함수 - 윈도우 생성과 초기 모델 설정
fn model(app: &App) -> Model {
    // 새로운 윈도우 생성
    app.new_window()
        .size(800, 800)    // 윈도우 크기 800x800 픽셀
        .view(view)        // view 함수를 렌더링 콜백으로 지정
        .build()           // 윈도우 빌드
        .unwrap();         // Result 타입을 unwrap으로 안전하게 처리 (에러 시 패닉)

    // 빈 벡터 생성
    let mut dots = Vec::new();
    // create_dot 함수로 첫 번째 원 생성하여 벡터에 추가
    dots.push(create_dot(app));
    
    // Model 구조체 인스턴스 생성 후 반환
    Model { dots }
}

// 새로운 원을 생성하는 도우미 함수
fn create_dot(app: &App) -> Dot {
    // 현재 윈도우의 사각형 영역 정보 가져오기 (left, right, top, bottom 등)
    let win = app.window_rect();
    
    // Dot 구조체 인스턴스 생성
    Dot {
        // 위치: 화면 왼쪽 아래 모서리 밖에서 시작
        // pt2()는 Point2 생성 함수 (x, y 좌표)
        position: pt2(win.left() - RADIUS, win.bottom() - RADIUS),
        // 속도: 우상향으로 이동 (x: 오른쪽, y: 위쪽)
        // vec2()는 Vec2 생성 함수
        velocity: vec2(2.0, 2.0),
    }
}

// 매 프레임마다 호출되는 업데이트 함수
// app: 앱 참조, model: 가변 참조로 모델 수정, _update: 사용하지 않는 매개변수
fn update(app: &App, model: &mut Model, _update: Update) {
    // 현재 윈도우의 사각형 영역 정보 가져오기
    let win = app.window_rect();

    // 모든 원들의 위치 업데이트
    // &mut model.dots: 가변 참조로 원들에 접근하여 수정
    for dot in &mut model.dots {
        // 현재 위치에 속도를 더해서 새로운 위치 계산
        // Vec2와 Point2의 덧셈 연산이 가능하도록 구현되어 있음
        dot.position += dot.velocity;
    }

    // 현재 첫 번째 원이 존재하는지 확인 (if let 패턴 매칭)
    if let Some(current_dot) = model.dots.first() {
        // 첫 번째 원이 우상단 경계에 도달했는지 AND 원이 1개만 있는지 확인
        // 조건 1: x 좌표가 오른쪽 경계 - 반지름에 도달 (원이 오른쪽 경계에 닿기 시작)
        // 조건 2: y 좌표가 위쪽 경계 - 반지름에 도달 (원이 위쪽 경계에 닿기 시작)
        // 조건 3: 현재 원이 1개만 있을 때 (중복 생성 방지)
        if current_dot.position.x >= win.right() - RADIUS && 
           current_dot.position.y >= win.top() - RADIUS &&
           model.dots.len() == 1 {
            // 새로운 원을 생성하여 벡터에 추가
            // 이때 첫 번째 원은 아직 화면에 있음
            model.dots.push(create_dot(app));
        }
    }

    // 화면 밖으로 완전히 나간 원들 제거
    // retain() 메서드: 클로저 조건이 true인 요소만 남기고 제거
    model.dots.retain(|dot| {
        // 조건: 원이 화면을 완전히 벗어나지 않았는지 확인
        // !(NOT) 연산자: 조건이 false일 때만 retain (제거하지 않음)
        // 조건: x 좌표가 오른쪽 경계 + 반지름보다 크고 AND y 좌표가 위쪽 경계 + 반지름보다 큼
        !(dot.position.x > win.right() + RADIUS && dot.position.y > win.top() + RADIUS)
    });
}

// 매 프레임마다 호출되는 렌더링 함수
// app: 앱 참조, model: 불변 참조로 모델 읽기, frame: 현재 프레임
fn view(app: &App, model: &Model, frame: Frame) {
    // 그림을 그리기 위한 Draw 인스턴스 생성
    let draw = app.draw();

    // 배경색 설정 - hsl(색상, 채도, 명도) 색상 모델 사용
    // 검은색에 가까운 어두운 배경 (명도 1%)
    draw.background().color(hsl(0.0, 0.0, 0.01));
    
    // 모든 원들에 대해 반복
    for dot in &model.dots {
        // 원(타원) 그리기
        draw.ellipse()
            .xy(dot.position)                 // 원의 중심 위치 설정
            .radius(RADIUS)                   // 원의 반지름 설정
            .color(rgba(0.8, 0.4, 0.4, 1.0)); // 원의 색상 설정
    }

    // 그려진 내용을 프레임 버퍼에 기록
    draw.to_frame(app, &frame).unwrap();
}

📝 Some의 의미

Some은 Rust의 Option 열거형(enum)에서 값이 존재함을 나타내는 variant(배리언트)입니다.

Option 열거형 구조

enum Option<T> {
    Some(T),  // 값이 있음 (T 타입의 값을 감싸고 있음)
    None,     // 값이 없음
}

if let Some(current_dot) = model.dots.first() 의 동작 원리

1. model.dots.first()의 반환값

  • first() 메서드는 Option<&Dot> 타입을 반환합니다
  • 벡터가 비어있으면 None을 반환
  • 벡터에 요소가 있으면 Some(&첫번째_요소)를 반환

2. if let 패턴 매칭

if let Some(current_dot) = model.dots.first() {
    // Some인 경우 실행 (값이 존재할 때)
    // current_dot 변수에 &Dot 타입의 참조가 바인딩됨
}

3. 실제 동작 예시

let dots = vec![Dot { position: pt2(0, 0), velocity: vec2(1, 1) }];

// case 1: 벡터에 요소가 있을 때
if let Some(dot) = dots.first() {
    // 이 블록이 실행됨
    // dot 변수에 &Dot 참조가 바인딩됨
    println!("원의 위치: {:?}", dot.position);
}
  
// case 2: 빈 벡터일 때
let empty_dots: Vec<Dot> = Vec::new();
if let Some(dot) = empty_dots.first() {
    // 이 블록은 실행되지 않음 (None이므로)
}

Some이 필요한가?

1. Null 안전성

  • Rust에는 null 개념이 없음
  • 대신 Option을 사용하여 값의 존재/부재를 명시적으로 처리
  • 컴파일 타임에 null 포인터 예방

2. 명시적 에러 처리

// ❌ 이렇게 직접 접근하면 벡터가 비었을 때 패닉 발생
// let dot = model.dots.first().unwrap(); // 위험!

// ✅ 안전한 접근
if let Some(dot) = model.dots.first() {
    // 값이 있을 때만 안전하게 사용
    dot.position;
}

3. 대안 비교

// 방법 1: if let (권장 - 간결함)
if let Some(dot) = model.dots.first() {
    // dot 사용
}

// 방법 2: match (더 장황함)
match model.dots.first() {
    Some(dot) => {
        // dot 사용
    }
    None => {
        // 값이 없을 때 처리
    }
}

// 방법 3: is_some() 체크 (덜 우아함)
if model.dots.first().is_some() {
    let dot = model.dots.first().unwrap(); // 두 번 호출해야 함
}

우리 코드에서의 역할

if let Some(current_dot) = model.dots.first() {
    // model.dots가 비어있지 않을 때만 이 블록 실행
    // current_dot은 &Dot 타입으로, 첫 번째 원의 불변 참조
    if current_dot.position.x >= win.right() - RADIUS { 
        // 조건 체크
    }
}
// model.dots가 비어있으면 이 블록 건너뜀

즉, Some은 "값이 있어!"라고 알려주는 Rust의 안전한 방식이며, if let Some() 패턴은 그 값을 안전하게 추출하여 사용하는 관용적 방법입니다.


📝 dots: Vec<Dot> 의 의미

struct Model {
  dots: Vec<Dot>,  // Dot 타입의 벡터를 저장하는 필드 선언
}

이 코드는 "Model구조체는 dots라는 필드를 가지며, 이 필드는 Dot 타입의 요소들을 저장하는 Vec(벡터)이다" 라고 선언하고 있습니다.

실제 인스턴스 생성 부분

fn model(app: &App) -> Model {
    let mut dots = Vec::new();  // 빈 벡터 생성 (아직 타입이 추론되지 않음)
    dots.push(create_dot(app)); // Dot 타입의 값을 push → 벡터 타입이 Vec<Dot>으로 추론됨
    
    Model { dots }  // 구조체 인스턴스 생성
}

단계별 설명

1. 빈 벡터 생성 시점

let mut dots = Vec::new();
  • 이때 dots의 타입은 아직 결정되지 않음
  • Rust 컴파일러는 이후 사용을 통해 타입을 추론

2. 타입 추론 발생

dots.push(create_dot(app));  // create_dot()은 Dot 타입을 반환
  • create_dot() 함수가 Dot 타입을 반환함
  • Dot 타입을 벡터에 push하면 컴파일러가 "아, 이 벡터는 Vec<Dot> 타입이구나!"라고 추론

3. 구조체 인스턴스 생성

Model { dots }  // 필드 초기화 shorthand (dots: dots와 동일)

타입 추론의 중요성

Rust는 강타입 언어이지만 타입 추론을 지원합니다:

// 명시적 타입 선언 (가능하지만 불필요)
let mut dots: Vec<Dot> = Vec::new();

// 타입 추론 활용 (일반적인 방식)
let mut dots = Vec::new();
dots.push(Dot { ... });  // 여기서 타입이 Vec<Dot>으로 추론됨

전체적인 메모리 구조

Model {
    dots: Vec<Dot> ─────→ [Heap 메모리]
        │
        ↓
        [Dot { position, velocity }, Dot { position, velocity }, ...]
}
  • Model 구조체는 스택에 저장
  • Vec<Dot>Heap에 동적 배열로 저장
  • Dot 인스턴스도 Heap에 저장

왜 이 방식이 안전한가?

// ❌ 이것은 컴파일 에러!
let mut dots = Vec::new();
dots.push("문자열");  // 에러: Model의 dots 필드는 Vec<Dot> 타입이어야 함
dots.push(123);       // 에러: Dot 타입이 아님

// ✅ 이것만 가능
dots.push(Dot { ... });  // Dot 타입만 push 가능

Rust의 타입 시스템이 컴파일 타임에 타입 안정성을 보장해줍니다!


✅ ⚙️🛠️📄📚🎨🖼️🧩🧠🧮🕹️🧿⏳🎛️🔮🌌🪩🗂️📝🧰🔒

아보카도
Sample
Important
Highlighting
비색
적황색
purple
hot pink
yellow
살구색
orange
green
blue

profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0개의 댓글