2차 프로젝트 일지 - 3일차
3일차: 개발 시작
재밌는 게임
크레이지아케이드 베이스에서, 그리고 지난 1차 프로젝트 베이스에서 사람들이 내 게임을 할 때 재미를 느끼게 할 수 있는 것들은 뭐가 있을까?
- 이동 컨트롤
- 물풍선으로 상호작용
- 적과의 전투
이동 컨트롤
지금의 이동 컨트롤은:
- 설정한 frameRate 분의 1초마다 이동 입력을 받고 화면을 업데이트한다
- 기본값이 60프레임이므로 1/60초마다 화면이 업데이트되는 셈이며 이는 인지하기에 매우 부드러운 반복주기를 가진다
- 그럼에도 불구하고 내 게임이 '뻑뻑'하고 계단식으로 느껴지는 가장 큰 원인은 '화면의 표현 한계'에 있다
- 아무리 프레임마다 캐릭터의 위치를 정밀하게 계산해도, 현재 환경에서는 '글자 단위'의 타일로만 표현할 수 있다. 즉 죽었다깨나도 0.5같은건 표현되지 않는다는 것이다 Copilot의 진단:
게임 좌표 (Vector2 int x,y) ↓ 1:1 매핑콘솔 셀 (CHAR_INFO[width × height]) ↓ WriteConsoleOutputW화면 출력- Vector2가 정수이고, 그 정수가 곧 콘솔 셀 인덱스입니다. 콘솔 셀 하나는 대략 8×16 픽셀 크기이므로, 80×40 콘솔이면 사실상 80×40 해상도로 게임을 하는 것과 같습니다. 프레임레이트를 아무리 올려도 한 프레임에 이동할 수 있는 최소 단위가 "셀 1칸"이라 부드러운 움직임 자체가 불가능합니다.
Q. 그럼 좌표를 정수로 유지하되 화면 스케일을 키우면 결국 유저가 느끼기엔 더 부드러워졌다고 느끼지 않을까?
[현재: 1셀 = 1게임유닛]5 → 6 이동 = 화면에서 1칸 점프중간 단계: 0개 (7프레임 정지 → 1칸 점프)[화면 스케일 3배: 1셀 = 3게임유닛]15 → 18 이동 = 화면에서 3칸 거리프레임 0: Lerp(15, 18, 0.00) = 15프레임 2: Lerp(15, 18, 0.27) = 15프레임 3: Lerp(15, 18, 0.40) = 16프레임 5: Lerp(15, 18, 0.67) = 17프레임 7: Lerp(15, 18, 1.00) = 18중간 단계: 2개- Vector2를 float으로 형변환하는 것보다 화면 스케일(해상도)을 키우는게 낫겠다
Half-block Unicode

There are two block elements, the half block characters ("▀" and "▄"), that allow us to use the terminal as an actual canvas, with pixels that can be set individually. ... Those characters allow us to divide a cell into two vertically, by making smart use of foreground and background colors.
문자가 1byte이므로 8bit, half block unicode는 4bit짜리
Before
Actor::Draw() │ ▼Renderer::Submit(L"P", pos(5,3), Green, 8) │ ▼ 렌더큐에 저장RenderCommand { text=L"P", pos=(5,3), color=Green } │ ▼ Draw()에서 CHAR_INFO에 직접 기록CHAR_INFO[3 * 80 + 5] = { UnicodeChar=L'P', Attributes=0x0002(Green) } │ ▼WriteConsoleOutputW → 콘솔 셀 (5,3)에 초록색 'P' 출력After
Actor::Draw() │ ├─ [기존 경로 - HUD/텍스트용] Submit(L"Stage: 1", ...) ← 변경 없음 │ └─ [새 경로 - 픽셀용] SubmitPixel(x, y, Green, 8) │ ▼ 픽셀 버퍼에 기록 pixelBuffer[y * width + x] = Green pixelSortingOrder[y * width + x] = 8 │ ▼ Draw()에서 변환: 픽셀 2개 → CHAR_INFO 1개 픽셀 (5, 6) = Red ──┐ ┌─ CHAR_INFO[3 * 80 + 5] 픽셀 (5, 7) = Blue ──┘ → │ UnicodeChar = L'▀' │ Attributes = 0x0014 │ ~~~~ │ bits 0-3: Red(fg) = 0x04 ← 윗 픽셀 │ bits 4-7: Blue(bg) = 0x10 ← 아랫 픽셀 └─ │ ▼ WriteConsoleOutputW → 셀 (5,3)의 윗절반 빨강, 아랫절반 파랑
WA! 드디어 글자가 아닌 타일이 표시된다 신나서 tileSize를 늘렸더니

해상도가 높아지는게 아니라 셀 자체가 늘어났다. 왜?
[현재: 1단계]게임 좌표 ──────────────────────→ 화면 픽셀 tileSize가 둘 다 결정[필요한 구조: 2단계]게임 좌표 ──→ 가상 픽셀 버퍼 ──→ 화면 픽셀 tileSize가 결정 축소(downscale)해서 (내부 정밀도) 콘솔에 맞춤가상 버퍼 (고해상도) 화면 버퍼 (콘솔 크기)┌─────────────────┐ ┌─────────┐│ 272 × 224 px │ ──축소──→ │ 51 × 42 │ ──half-block──→ 콘솔│ (17타일 × 16) │ │ 픽셀 │└─────────────────┘ └─────────┘가상 좌표 (0~271) → 화면 좌표 (0~50)비율: 272/51 ≈ 5.3 가상 픽셀 → 1 화면 픽셀downScale하는 과정이 추가되어야 하는구나
가상 해상도(Virtual Resolution)
게임 로직이 기준 삼는 좌표계, 실제 콘솔 화면 크기와 별개로 게임이 항상 같은 '월드 크기'로 동작하게 하는 개념.
- virtualWidth, virtualHeight: 게임 코드가 쓰는 해상도
// 가상 해상도: 화면 픽셀 × (tileSize / displayTileSize)virtualWidth = pixelWidth * tileSize / displayTileSize;virtualHeight = pixelHeight * tileSize / displayTileSize; - pixelWidth, pixelHeight: 실제 렌더링에 쓰는 픽셀 버퍼 크기
- tileSize, displayTileSize: 논리 좌표를 실제 크기로 얼마나 확대/축소할지 결정하는 스케일
- 게임 코드에서 가상 좌표로 그리기(Submit, SubmitRect, SubmitPixel)
- 렌더러가 그 좌표를 내부 픽셀 버퍼에 반영
- 프레임의 CHAR_INFO로 변환해서 화면 출력
Sprite 시스템
[현재] [에셋이 그려지려면]┌──────┐ ┌──────┐│██████│ 단색 Green │ ▓██▓ │ 픽셀마다 다른 색│██████│ │██▓▓██││██████│ │▓▓██▓▓│└──────┘ └──────┘이제 내가 원하는 에셋으로 그리려면 스프라이트 데이터가 필요하다
SubmitSprite(x, y, Color* pixels, w, h, sortingOrder)SpriteAsset(2차원같은 1차원 배열),SpriteLoader추가(그래픽스 특강이 도움이 되네)Actor::Sprite→wchar_t*에서Color[][]로 확장
Behavior tree
Selector (root)├── Sequence ─ 위험 회피 (최우선)│ ├── IsDangerNearbyCondition│ └── FleeFromDangerAction│├── Sequence ─ 근접 공격│ ├── IsPlayerAdjacentCondition│ └── BTCooldown(1.5s) → PlaceBubbleAction│├── Sequence ─ 경로가 열려있으면 추적│ └── BTCooldown(0.4s) → BFSMoveAction ← 경로 없으면 Failure│└── Sequence ─ 경로 막혔으면 박스 제거 ├── HasBubbleAmmoCondition └── BTCooldown(3s) → PlaceBubbleNearBoxAction