<aside> 👀 프로젝트 소개
다양한 종류의 AI탱크를 처지하고 점령지를 탈환하자!
항목 | 내용 |
---|---|
장르 | 밀리터리 TPS 게임 |
플랫폼 | PC |
맵 컨셉 | 랜덤하게 스폰되는 AI탱크를 모두 처치하거나 지정된 점령지를 탈환 시 승리 (게임 종료) |
개발 내용 | Off-Screen Indicator UI, 미니맵UI 를 포함해 전투 경험을 강화하는 UI 개발, AI탱크 개발 |
</aside> |
<aside> 📹
프로젝트 시연 영상
</aside>
Unreal Engine
, C++
, Blueprint
GitHub
, Discord
Adobe Photoshop
<aside> 💬 기능 개요
ProjectWorldToScreen()
를 이용해 UI화면에 투영FMath::Atan2
로 점령지가 Off-Screen일 경우 위치와 각도를 정확히 표시하는 Off-Screen Indicator UI 를 구현</aside>
✅ 구현 방식
점령지를 캐싱한 후, 현재 뷰포트 사이즈와 스케일값을 저장한다.
const FVector2D CurrentViewportSize = UWidgetLayoutLibrary::GetViewportSize(this);
const float CurrentViewportScale = FMath::Max( UWidgetLayoutLibrary::GetViewportScale(this), 0.001f );
ProjectWorldToScreen()
를 이용해 월드 좌표를 스크린 픽셀 좌표로 변환한다.
ProjectWorldToScreen()
를 이용해서 언리얼엔진의 카메라 행렬을 통해 월드 좌표를 스크린 좌표로 변환한 후, ViewportScale
로 나누어 DPI에 무관한 좌표(픽셀 좌표)를 획득함
// 월드 좌표를 스크린 좌표로 변환
FVector2D ScreenPosLogical = FVector2D::ZeroVector;
const bool bOk = UGameplayStatics::ProjectWorldToScreen(PC, WorldPos, ScreenPosLogical, true);
// ProjectWorldToScreen은 DPI 스케일이 적용된 좌표를 반환한다
const float ViewportScale = FMath::Max( UWidgetLayoutLibrary::GetViewportScale(this), 0.001f );
// ViewportScale나누면 해상도나 DPI에 무관하게 일정한 기준 좌표계에서 위치 계산 가능!
OutPx = ScreenPosLogical / ViewportScale;
return bOk;
이 좌표를 통해 Target이 화면 내에 있는지 없는 지를 판단해 투영 가능 여부를 bool 변수로 반환함
Target이 화면 내에 없다면 UI를 숨김처리
카메라를 기준 방위각을 이용한 Target의 화면 방향 벡터를 구한다.
Target이 화면 내에 있다면, 카메라를 원점처럼 사용해 Target까지의 벡터를 구한후 정규화
FVector Dir3D = (WorldTarget - CamLoc).GetSafeNormal();
이 벡터를 XY평면에 투영한 후, X축과 투영 벡터 사이의 방위각을 얻음
(이때 방위각을 구하는 기준 (그림에서의 x축) 은 카메라의 회전 값, 즉 카메라의 Yaw이다.)
그 각도를 이용하면 Target이 카메라의 앞/뒤/좌/우 어디에 있는지를 반환
이를 화면 기준으로 치환하면, 화면에 표시되는 방향을 구할 수 있음
R(=ΔYaw) | Dir2D | 화면 방향(=카메라 기준) |
---|---|---|
0° | (0, −1) | 앞(위쪽) |
+90° | (1, 0) | 오른쪽 |
±180° | (0, +1) | 뒤(아래) |
−90° | (−1, 0) | 왼쪽 |
화면 중앙에서 → 2번에서 얻은 방향 벡터로 쏜 2D레이를 경계까지 진행, 교차점을 구한다.
화면 중점에서 2번에서 얻은 방향을 향해 Ray를 쏘면 화면 경계와의 교차점이 생성
// 축별 충돌 시점 t 계산
auto ComputeHitT = [](float StartCoord, float DirAxis, float BoxMin, float BoxMax)
{
// 진행 방향이 거의 0이라면 (해당 축으로 움직이지 않음) → 충돌 없음
if (FMath::IsNearlyZero(DirAxis)) return TNumericLimits<float>::Max();
// 양수 방향이면 경계의 Max, 음수 방향이면 Min과 만남
const float TargetBoundary = (DirAxis > 0.f) ? BoxMax : BoxMin;
// (경계 - 시작좌표) / 진행방향 = 해당 축에서 충돌하는 시점 t
return (TargetBoundary - StartCoord) / DirAxis;
};
const float tHitX = ComputeHitT(StartFrom.X, DirNorm.X, ClampMin.X, ClampMax.X);
const float tHitY = ComputeHitT(StartFrom.Y, DirNorm.Y, ClampMin.Y, ClampMax.Y);
float tHit = FMath::Min(tHitX, tHitY);
이 교차점을 이용해 UI의 위치와 **UI내부 화살표의 방향(각도)**를 결정
UI의 위치 및 화살표 방향 업데이트
3번의 교차점을 통해 UI의 위치를 지정
화면 중심에서 교차점으로 향하는 2D 방향 벡터를 이용해 FMath::Atan2
로 목적지의 위치를 표시하는 각도를 얻음
if (OffScreenArrow)
{
const FVector2D V = IndicatorScreenLoc - ScreenCenterPx;
const float Deg = FMath::RadiansToDegrees( FMath::Atan2(V.Y, V.X) );
OffScreenArrow->SetRenderTransformAngle(Deg + ArrowSpriteCorrect);
}
이걸 SetRenderTransformAngle()
에 넣으면 화살표가 정확한 방향을 가리키게 됨
<aside> 💬
기능 개요
SceneCaptureComponent
를 이용해 지형을 한번 렌더링, GetActorLocation()
과 GetActorRotation()
를 이용해 미니맵위의 마커를 갱신함</aside>