반응형
🎮 구현 목표
게임 엔진에서 BMP 픽셀 충돌 맵(윈도우 계층)과 액터 Transform(엔진 계층) 간의 좌표계 불일치 문제를 해결하여 정확한 픽셀 기반 지형 충돌을 구현합니다.
🚨 문제 상황
엔진 계층 구조로 인한 좌표계 불일치
게임 엔진을 Windows에 종속된 Platform 계층과 게임 컨텐츠 제작에 필요한 기능을 지원하는 Core 계층으로 분리한 것이 문제의 시작이었습니다.
[윈도우 계층 - EnginePlatform]
- BMP 이미지 (픽셀 충돌용)
- UEngineWinImage (Transform 없음)
- 스크린 좌표 시스템
</br>
[엔진 계층 - EngineCore]
- PNG 텍스처 (렌더링용)
- Transform (월드 좌표)
- 데카르트 좌표 시스템
문제의 핵심:
- BMP 파일은 윈도우 계층에 속하며 Transform 정보를 가질 수 없음
- 액터는 엔진 계층의 월드 좌표로 움직이지만, 픽셀 충돌은 윈도우 계층의 BMP를 참조
- 두 계층의 좌표 기준점이 다름
대안
방법 1: 엔진 구조 변경
// UEngineWinImage에 Transform 추가?
class UEngineWinImage
{
FTransform Transform; // ❌ 계층 규칙 위반
};
- 계층 분리 원칙 위반 (Platform 계층이 Core 계층 의존)
- 엔진 전체 구조 수정 필요
방법 2: WinImage를 엔진 계층으로 이동
// UEngineWinImage를 EngineCore로?
#include <EngineCore/EngineWinImage.h> // ❌ 의존성 역전
💭 해결 방안
핵심 아이디어: 픽셀 충돌 검사는 데카르트 좌표계에서 윈도우 좌표계로 변환
현재 배경 이미지(png) 파일의 좌상단(Left Top) 좌표를 스크린 좌표의 원점(0, 0)으로 변환하고, y축을 반전시켜 액터의 픽셀 충돌 검사는 스크린 좌표계 기준으로 변환합니다.
플레이어가 속하지 않은 맵 리소스는 모두 비활성화시켜 항상 플레이어가 입장한 맵에서만 좌표계를 변환한 충돌 검사를 수행합니다.
월드 좌표 (액터) → BMP 로컬 좌표 → 픽셀 색상 검사
설계 원칙:
- BMP 파일은 변경하지 않는다 (윈도우 계층 유지)
- 액터 좌표를 BMP의 좌표계로 변환한다
- 맵이 바뀌면 변환 기준점만 갱신한다
Room 기반 좌표 변환 시스템
// Room.h - 좌표 변환의 중심
class ARoom : public AActor
{
private:
UEngineWinImage PixelCollisionImage; // BMP 충돌 맵
FVector LeftTopPos; // BMP의 좌표 기준점 (월드 좌표)
FVector Size; // 방 크기
};
Room이 좌표 변환을 담당하는 이유:
- Room은 엔진 계층에 속하므로 Transform 사용 가능
- 각 Room은 독립적인 BMP 충돌 맵 보유
- Room 전환 시 자동으로 좌표계 변환 기준점 갱신
좌표 변환 구현
// Room.cpp
FVector ARoom::GetPixelCollisionPoint(AActor* _Actor, FVector _Offset)
{
float DeltaTime = UEngineCore::GetDeltaTime();
// 액터의 월드 좌표 획득
FVector CollisionPos = Knight->GetPixelCollision()->GetWorldLocation();
FVector GravityForce = Knight->GetGravityForce();
// 1단계: 월드 좌표 → Room 로컬 좌표
CollisionPos -= LeftTopPos;
// 2단계: 다음 프레임 예측 위치 계산
FVector NextPos = GravityForce * DeltaTime;
FVector CollisionPoint = {
CollisionPos.X + NextPos.X + _Offset.X,
CollisionPos.Y + NextPos.Y + _Offset.Y
};
return CollisionPoint;
}
변환 과정:
[월드 좌표]
Knight: (9000, -6000)
Room LeftTopPos: (6150, -3550)
[1단계] 월드 → Room 로컬
CollisionPos = (9000, -6000) - (6150, -3550)
= (2850, -2450)
[2단계] BMP 좌표 (Y축 반전)
BMP GetColor({ 2850, 2450 }) // Y축 부호 반전
Y축 좌표계 통일
// Room.cpp
bool ARoom::IsOnGround(FVector _Pos)
{
FVector CollisionPoint = _Pos;
CollisionPoint.RoundVector();
// Y축 반전: 엔진(Y Down) → BMP(Y Up)
UColor CollisionColor = PixelCollisionImage.GetColor(
{ CollisionPoint.X, -CollisionPoint.Y }
);
return (CollisionColor == UColor::BLACK);
}
좌표계 차이:
| 시스템 | 원점 | Y축 방향 |
|---|---|---|
| 엔진 월드 좌표 | 임의 | 아래가 음수 |
| BMP 좌표 | 좌상단 | 아래가 양수 |
🔧 구현 세부사항
Room 초기화 시 좌표 기준점 설정
// RoomManager.cpp - 맵 생성
Crossroads1->SetRoomLocation({ 6150, -3550 });
// Room.cpp
void ARoom::SetRoomLocation(FVector _Pos)
{
float ZOrder = static_cast<float>(EZOrder::BACKGROUND);
SetActorLocation({ GetActorLocation().X + _Pos.X,
GetActorLocation().Y + _Pos.Y,
ZOrder });
LeftTopPos = _Pos; // BMP 좌표 변환 기준점 저장
}
픽셀 충돌 검사 - 중력
// Room.cpp
void ARoom::CheckPixelCollisionWithGravity(AActor* _Actor, FVector _Offset)
{
float DeltaTime = UEngineCore::GetDeltaTime();
FVector TwoPixelUp = FVector::UP * 2.0f;
// 현재 위치의 픽셀 색상 검사
FVector CollisionPoint = GetPixelCollisionPoint(_Actor, _Offset);
FVector CollisionPoint2PixelUp = GetPixelCollisionPoint(_Actor, _Offset + TwoPixelUp);
if (true == IsOnGround(CollisionPoint))
{
Knight->SetOnGround(true);
// 지면에 박히지 않도록 위로 조정
while (true == IsOnGround(CollisionPoint2PixelUp))
{
Knight->AddRelativeLocation(TwoPixelUp);
CollisionPoint2PixelUp = GetPixelCollisionPoint(_Actor, _Offset + TwoPixelUp);
}
}
else
{
Knight->SetOnGround(false);
}
Force(_Actor, DeltaTime);
}
동작 원리:
- 액터의 월드 좌표를 BMP 로컬 좌표로 변환
- 해당 좌표의 픽셀 색상을
GetColor()로 검사 - 검은색(지면)이면 중력 중단, 아니면 중력 적용
픽셀 충돌 검사 - 벽
// Room.cpp
void ARoom::CheckPixelCollisionWithWall(AActor* _Actor, float _Speed, bool _Left, FVector _Offset)
{
FVector CollisionPos = Knight->GetPixelCollision()->GetWorldLocation();
FVector CollisionHalfScale = Knight->GetPixelCollision()->GetWorldScale3D().Half();
float DeltaTime = UEngineCore::GetDeltaTime();
float NextPos = _Speed * DeltaTime;
// 월드 좌표 → Room 로컬 좌표
CollisionPos -= LeftTopPos;
FVector CollisionPoint = { CollisionPos.X + NextPos, CollisionPos.Y + 10.0f };
// 좌우 방향에 따라 충돌 체크 위치 조정
if (true == _Left)
{
CollisionPoint.X -= CollisionHalfScale.X;
}
else
{
CollisionPoint.X += CollisionHalfScale.X;
}
CollisionPoint.RoundVector();
// Y축 반전하여 BMP 좌표로 검사
UColor CollisionColor = PixelCollisionImage.GetColor(
{ CollisionPoint.X, -CollisionPoint.Y }
);
// 노란색(벽) 또는 검은색(지면)이면 벽으로 인식
if (CollisionColor == UColor::YELLOW || CollisionColor == UColor::BLACK)
{
Knight->SetWallHere(true);
}
else
{
Knight->SetWallHere(false);
}
}
색상 기반 충돌 판정:
| 픽셀 색상 | 의미 | 충돌 처리 |
|---|---|---|
| BLACK | 지면 | 중력 중단, 벽 충돌 |
| YELLOW | 벽 | 벽 충돌 |
| RED | 천장 | 천장 충돌 |
| WHITE | 빈 공간 | 통과 가능 |
Actor에서 픽셀 충돌 활성화
// Knight.cpp
void AKnight::ActivatePixelCollsion()
{
if (true == NoneGravity) // 디버그 모드
{
return;
}
ARoom* Room = ARoom::GetCurRoom();
if (nullptr != Room)
{
// Room에게 좌표 변환 및 충돌 검사 위임
Room->CheckPixelCollisionWithWall(this, Stat.GetVelocity(), bIsLeft, FVector::ZERO);
Room->CheckPixelCollisionWithCeil(this, BodyRenderer.get(),
Stat.GetVelocity(), bIsLeft, FVector::ZERO);
}
}
// Knight.cpp
void AKnight::Tick(float _DeltaTime)
{
AActor::Tick(_DeltaTime);
ActivatePixelCollsion(); // 매 프레임 픽셀 충돌 검사
// ...
}
✅ 결과
계층 분리 원칙 준수
[EnginePlatform]
└─ UEngineWinImage
└─ GetColor() 만 제공
[EngineCore]
└─ ARoom (좌표 변환 담당)
├─ LeftTopPos (기준점)
└─ GetPixelCollisionPoint() (좌표 변환)
- UEngineWinImage는 순수하게 픽셀 색상만 반환
- ARoom이 모든 좌표 변환 로직 관리
- 계층 간 의존성 규칙 유지
맵 전환 시 자동 좌표계 갱신
// Door를 통한 Room 전환
void ADoor::WarpRoom()
{
ARoom::SetCurRoom(TargetRoom); // 현재 Room 변경
Knight->SetActorLocation(TargetPos);
// 새로운 Room의 LeftTopPos를 기준으로 자동 변환
// 추가 코드 불필요 - Room이 알아서 처리
}
- Room이 바뀌면 해당 Room의 LeftTopPos 자동 적용
- 개발자는 월드 좌표만 신경 쓰면 됨
- 좌표 변환은 Room이 투명하게 처리
정확한 지형 충돌 구현
변환 전 (좌표 불일치):
액터 월드 좌표: (9000, -6000)
BMP 검사 좌표: (9000, 6000) ❌ 엉뚱한 위치
변환 후 (정확한 매핑):
액터 월드 좌표: (9000, -6000)
Room LeftTopPos: (6150, -3550)
BMP 검사 좌표: (2850, 2450) ✅ 정확한 위치
확장성 확보
// 새로운 Room 추가 시
std::shared_ptr<ARoom> NewRoom = CreateRoom("NewRoom", "NewRoom_back.png", "
NewRoom_pixel.bmp", { 5000, 3000 });
NewRoom->SetRoomLocation({ 20000, -10000 });
// 좌표 변환 로직은 자동으로 동작
// 추가 코드 작성 불필요반응형
'프로젝트 회고' 카테고리의 다른 글
| [UE5 액션] 루트 모션 기반 자연스러운 대시 구현 Motion Warping (0) | 2025.12.02 |
|---|---|
| [UE5 액션] 유연한 콤보 시스템: 이벤트 기반 Perfect/Mercy 구간을 통한 공격 후딜레이 캔슬 및 콤보 연계 (0) | 2025.12.02 |
| [UE5 액션] 애니메이션 라이프 사이클 기반 비동기 상태 관리 (0) | 2025.12.02 |
| [UE5 액션] 상태 관리: StateComponent와 GameplayTag (0) | 2025.12.02 |
| [DirectX 11] Enum의 한계 → FSM Component (0) | 2025.12.02 |
