Raylib 기반 게임 개발에서 직선형 투사체(포탄)를 구현할 때, 특정 방향으로 이동하는 투사체의 궤적에 검은색 점 간격이 발생하는 문제가 발견되었습니다. 이는 주로 픽셀 기반 라인 그리기 알고리즘의 조건 처리 미흡으로 인해 발생하며, 본 문서에서는 이 문제를 분석하고 해결한 과정을 설명합니다.
개발 환경 및 배경
본 프로젝트는 C++와 Raylib 4.5를 사용하여 2D 그리드 기반의 게임 환경을 구축합니다. 플레이어 이동, 카메라 제어(확대/축소 및 패닝), AABB(Axis-Aligned Bounding Box) 충돌 감지, 그리고 직선형 투사체 발사 및 궤적 그리기 기능이 포함되어 있습니다. 투사체는 각 프레임마다 한 칸씩 목표 지점까지 이동하며 궤적을 그리는데, 이 과정에서 Bresenham(브레젠햄)과 유사한 라인 그리기 알고리즘이 사용됩니다.
문제 정의: 궤적 간격 오류
문제는 특히 북서 방향(Y축 증가, X축 감소)으로 이동하는 투사체에서 명확하게 나타났습니다. 투사체의 이동 경로를 계산하고 그리는 로직 중, 다음과 같은 조건문에서 오류가 발생했습니다:
// 기존 (문제 발생): 북쪽으로 이동하며 북서 방향으로 기울어진 경우
if (drawi - playeri > 0 && drawi - playeri > -(drawj - playerj) && drawj - playerj <= 0) {
// ... 투사체 궤적 계산 및 그리기 로직 ...
}
여기서 drawi - playeri > 0은 투사체가 북쪽(맵 그리드 Y축 증가)으로 이동함을 의미하고, drawj - playerj <= 0은 투사체가 서쪽(맵 그리드 X축 감소)으로 이동하거나 수직으로 이동함(`drawj - playerj == 0`)을 의미합니다. 문제는 drawj - playerj == 0, 즉 완벽하게 수직으로 북쪽으로만 이동하는 경우가 이 '북서 대각선' 처리 로직에 포함되면서 발생했습니다. 대각선 처리에 특화된 로직이 순수 수직 이동을 처리하려 할 때, 픽셀 계산의 불일치로 인해 궤적에 간격이 생기는 현상이 나타났습니다.
해결책
문제의 핵심은 북서 방향 처리 조건에서 순수 수직 이동(`drawj - playerj == 0`) 케이스를 명확하게 제외하는 것이었습니다. `drawj - playerj <= 0` 조건을 `drawj - playerj < 0`으로 변경하여, 완벽한 수직 이동은 해당 대각선 처리 로직에서 제외하고, 다른 (아마도 더 일반적인 수직선 처리) 로직이 담당하도록 유도했습니다.
// 수정 후 (오류 해결): 북쪽으로 이동하며 북서 방향으로 기울어진 경우 (X 변화량이 0인 수직선 제외)
if (drawi - playeri > 0 && drawi - playeri > -(drawj - playerj) && drawj - playerj < 0) {
// ... 투사체 궤적 계산 및 그리기 로직 ...
}
이 변경을 통해, 순수 수직선 경로의 경우 불필요한 대각선 로직 진입을 막아 궤적 계산의 정확성을 높였고, 결과적으로 궤적의 검은색 점 간격 오류가 해결되었습니다.
투사체 데이터 구조 및 이동 로직 예시
투사체를 관리하기 위한 기본 데이터 구조는 다음과 같습니다. 각 필드는 투사체의 상태와 위치 정보를 저장합니다.
struct GameProjectile {
int originX; // 발사 시작 그리드 X 좌표
int originY; // 발사 시작 그리드 Y 좌표
int targetX; // 목표 그리드 X 좌표
int targetY; // 목표 그리드 Y 좌표
int prevRenderX; // 이전 프레임에 그려진 X 좌표 (궤적 지우기 위함)
int prevRenderY; // 이전 프레임에 그려진 Y 좌표
bool isActive; // 투사체 활성 상태
int currentStep; // 현재 이동 진행 단계 (시간 또는 길이)
bool isExploding; // 폭발 중 여부
};
다음은 각 프레임마다 단일 투사체의 위치를 업데이트하는 핵심 로직 중, 문제 해결과 관련된 '북서 대각선' 부분에 초점을 맞춘 간소화된 예시 코드입니다. 실제 게임에서는 다양한 방향과 상황을 처리하는 더 많은 조건문이 포함됩니다.
void UpdateProjectilePosition(GameProjectile& projectile, int** gameMap, int mapRows, int mapCols) {
if (!projectile.isActive) return;
// 이전 위치를 배경 타일로 되돌려 궤적 효과 생성 (예시: 224는 특정 배경 타일 ID)
if (projectile.currentStep > 0 && projectile.prevRenderX >= 0 && projectile.prevRenderY >= 0 &&
projectile.prevRenderY < mapRows && projectile.prevRenderX < mapCols) {
gameMap[projectile.prevRenderY][projectile.prevRenderX] = 224;
}
int diffY = projectile.targetY - projectile.originY; // Y축 변화량
int diffX = projectile.targetX - projectile.originX; // X축 변화량
// 투사체가 목표에 도달했는지 확인 (각 방향별로 정확한 조건이 필요하나, 여기서는 간소화)
if (projectile.currentStep >= std::max(std::abs(diffX), std::abs(diffY))) {
projectile.isExploding = true;
// 목표 지점 주변에 폭발 효과 타일 그리기 (예시: 1224는 폭발 타일 ID)
for (int i = 0; i < 3; ++i) { // 폭발 범위
for (int j = 0; j < 10; ++j) {
int expY = projectile.targetY - i;
int expX = projectile.targetX + j;
if (expY >= 0 && expY < mapRows && expX >= 0 && expX < mapCols) {
gameMap[expY][expX] = 1224;
}
}
}
return; // 폭발했으므로 이동 처리 중단
}
int newPlotX = projectile.originX;
int newPlotY = projectile.originY;
float slope;
// Y축이 증가하는(북쪽) 방향의 투사체 처리
if (diffY > 0) {
// ... (북동 대각선, 순수 수직 북쪽 등 다른 방향 조건 생략) ...
// 북서 대각선 방향 (Y 증가, X 감소) 처리 로직
// 여기에 버그 수정이 적용된 조건이 있습니다: 'diffX <= 0' -> 'diffX < 0'
// 이 변경으로 'diffX == 0'인 순수 수직선이 이 대각선 로직으로 잘못 진입하는 것을 방지합니다.
if (diffY > -diffX && diffX < 0) { // Y 변화량이 X 변화량의 절댓값보다 크고, X는 감소
slope = (float)-diffX / diffY; // Y가 1 증가할 때 X 변화량 계산
newPlotY = projectile.originY + projectile.currentStep;
newPlotX = projectile.originX + static_cast<int>(std::round(slope * projectile.currentStep));
}
// ... (Y 증가 방향의 다른 조건들) ...
}
// ... (Y 감소(남쪽), X 증가(동쪽), X 감소(서쪽) 방향의 조건들 생략) ...
// 위 특정 방향 로직에 해당되지 않는 경우(예: 순수 수평/수직선 처리 폴백)를 위한 일반화된 계산
// 실제 원본 코드에서는 모든 8방향에 대해 개별적인 Bresenham 루프가 있었습니다.
if (std::abs(diffY) >= std::abs(diffX)) { // Y축이 지배적
float xInc = (float)diffX / diffY;
int currentYStep = (diffY > 0) ? projectile.currentStep : -projectile.currentStep;
newPlotY = projectile.originY + currentYStep;
newPlotX = projectile.originX + static_cast<int>(std::round(xInc * currentYStep));
} else { // X축이 지배적
float yInc = (float)diffY / diffX;
int currentXStep = (diffX > 0) ? projectile.currentStep : -projectile.currentStep;
newPlotX = projectile.originX + currentXStep;
newPlotY = projectile.originY + static_cast<int>(std::round(yInc * currentXStep));
}
// 새롭게 계산된 위치를 맵에 그리기 (예시: 1949는 투사체 타일 ID)
if (newPlotX >= 0 && newPlotX < mapCols && newPlotY >= 0 && newPlotY < mapRows) {
gameMap[newPlotY][newPlotX] = 1949;
projectile.prevRenderX = newPlotX;
projectile.prevRenderY = newPlotY;
} else {
projectile.isExploding = true; // 맵 밖으로 나가면 폭발 처리
}
projectile.currentStep++; // 진행 단계 증가
}
충돌 감지 및 렌더링
게임 내에서는 AABB 충돌 감지 로직을 사용하여 투사체와 적 객체 간의 충돌을 효율적으로 확인합니다. 충돌이 발생하면, 충돌 영역을 특정 색상으로 표시하여 시각적인 피드백을 제공합니다. 렌더링 측면에서는 Raylib의 RenderTexture2D를 활용하여 전체 게임 월드 중 현재 카메라 시야에 해당하는 부분을 오프스크린 렌더링 버퍼에 그립니다. 이 버퍼는 최종적으로 화면에 확대/축소 및 패닝되어 표시됩니다. 마우스 휠을 이용한 카메라 확대/축소(camerasize)와 마우스 우클릭 드래그를 이용한 화면 패닝(draftx, drafty)이 가능하여 플레이어가 넓은 게임 맵을 자유롭게 탐색할 수 있습니다.