UDN
Search public documentation:

DevelopmentKitGemsAddingOnScreenIndicatorsKR
English Translation
日本語訳
中国翻译

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

UE3 홈 > UDK 젬 > 화면위 표지 추가하기

화면위 표지 추가하기


문서 변경내역: James Tan 작성. 홍성진 번역.
UDK 2011년 5월 버전으로 최종 테스팅, PC 호환

개요


디폴트로 언리얼 토너먼트 3 에는 카메라 앞에 플레이어의 이름을 표시하는 간단한 화면위 표지(on screen indicator)가 있습니다. 이 UDK 젬에서는 화면위에 타겟 위치를 가리키는 화면위 표지 만드는 법을 보여 드리겠습니다. 타겟이 카메라 뒤에 있어 화면을 벗어나도 화면위 표지는 화면 밑에 남아있을 것입니다. 타겟이 카메라 프러스텀 밖에 있어 화면을 벗어나도 화면의 한 쪽 가장자리에 남아 타겟 방향을 가리킵니다. 타겟이 현재 화면위에 렌더되지 않는다면 화면위 표지도 서서히 사라집니다. 마지막으로 타겟의 팀에 따라 화면위 표지색도 바뀝니다.

  1. 이 폰들은 화면 위에 보이며, 파란 팀입니다. 화면위 표지는 물론 그 화살표도 그에 맞게 왼쪽이나 오른쪽으로 약간 비껴 있습니다.
  2. 이 폰은 다리 뒤에 숨어 있기에, 화면위 표지는 투명해 집니다.
  3. 이 팀원은 카메라 프러스텀 시야에서 벗어나 있으며 보이지 않습니다.
  4. 이 폰들은 플레이어 뒤에 있으며 보이지 않습니다.

OnScreenRadarExample.jpg

표지 머티리얼


화면위 표지 머티리얼은 로테이션, 불투명도, 팀 색 등 몇 가지를 담당합니다. 화면위 표지에 사용된 텍스처는 이렇습니다. 아래 이미지는 텍스처 개별 채널을 나타냅니다.

  1. 화면위 표지에 사용된 그레이스케일 디퓨즈입니다. 깨끗한 공간이 많이 있는데, 그렇지 않으면 텍스처가 회전할 때 텍스처 래핑(wrapping) 때문에 부작용이 생길 수 있습니다.
  2. 화면위 표지 중앙의 아이콘 불투명도를 설정하는 데 사용되는 마스크입니다.
  3. 화면위 표지에 대한 그림자를 만드는 데 사용되는 마스크입니다.
  4. 화면위 표지에 대한 불투명도로 사용되는 마스크입니다.

그림자의 세기를 별도로 조절할 수 있도록 하기 위해 그림자 마스크와 불투명 마스크를 분리시켰습니다만, 끝에 추가하지는 않았습니다. 필요하면 직접 추가하셔도 됩니다.

OnScreenIndicatorThumbnail.jpg

화면위 표지는 투명한 언릿(라이팅되지 않은) 머티리얼입니다. HUD 위에 렌더하려면 언릿 머티리얼이어야 합니다. 이미시브 채널만 사용되기도 합니다.

이미시브 채널은 두 갈래로 이루어져 있습니다. 첫 갈래는 화면위 표지 자체를 다룹니다. 화면위 표지 텍스처 샘플러는 Rotation 이라 불리는 스칼라 파라미터로 회전시킵니다. 그리고서 그 빨강(#1) 채널에 TeamColor 라는 벡터 파라미터를 곱합니다. 그리고 또 여기에 사인 곡선으로 만들어진 꿈틀대는 회색으로 곱해줍니다. 둘째 갈래는 가운데 들어가는 화면위 표지 아이콘을 다룹니다. Portrait 라는 텍스처 파라미터는 텍스처를 내놓습니다. 그것을 줄이고 가운데로 밀어주어, 적당한 양의 텍스처가 보이게 되는 것입니다. 그 위에 흐르는(panning) 텍스처를 곱해주어 TV 화면과 같은 효과를 냈습니다. 그런 다음 화면위 표지의 녹색(#2) 채널을 곱해줍니다. 두 갈래의 결과를 더한 다음 화면위 표지 텍스처의 알파(#4) 채널로 곱해줍니다.

불투명 채널은 단지 파랑(#3) 채널과 알파(#4) 채널을 더합니다. 그런 다음 그 결과에 Opacity 라는 스칼라 파라미터로 곱해줍니다.

OnScreenIndicatorMaterialThumbnail.jpg

언리얼스크립트


SRadarInfo 구조체

각각의 화면위 표지에 대한 정보를 담는 데 구조체가 사용되었습니다.

  • UTPawn - 이 화면위 표지가 가리키는 폰으로의 참조입니다.
  • MaterialInstanceConstant - 화면위 표지가 사용하는 머티리얼의 인스턴스입니다.
  • DeleteMe - 삭제에 쓰이는 불리언입니다.
  • Offset - 애니메이션에 쓰이는 화면위 표지의 오프셋입니다.
  • Opacity - 화면위 표지의 현재 불투명도입니다.

YourHUD.uc
  struct SRadarInfo
  {
    var UTPawn UTPawn;
    var MaterialInstanceConstant MaterialInstanceConstant;
    var bool DeleteMe;
    var Vector2D Offset;
    var float Opacity;
  };
  

AddPostRenderedActor 함수

디폴트로 언리얼 토너먼트 3 는 기존의 이름 표지를 렌더하기 위해 포스트 렌더(나중에 렌더되는) 액터를 사용합니다. 이 함수는 딱 UTPawn 에 대해서만 그러한 행위를 제거합니다.

YourHUD.uc
  function AddPostRenderedActor(Actor A)
  {
    // 이름표를 원하지 않으니 UTPawns 에 대한 포스트 렌더 콜 제거
    if (UTPawn(A) != None)
    {
      return;
    }
  
    Super.AddPostRenderedActor(A);
  }
  

PostRender 이벤트

언리얼스크립트를 통해 텍스처나 머티리얼이나 텍스트를 화면 위에 렌더할 수 있도록 하기 위해, 언리얼스크립트에서 포스트 렌더 이벤트를 엔진이 호출합니다. 바로 이 곳이 UDK 젬의 로직 보석이 다량 함유되어 있는 곳으로, 좀 더 잘게 나눠 보도록 하겠습니다.

업데이트 패스

이 패스에서, 먼저 DeleteMe 를 참으로 설정하여 레이더 정보를 모두 삭제 검사합니다.

그 다음 ForEach 를 사용하여 모든 UT Pawns 를 반복처리(iterate)합니다. 플레이어가 소유하지 않는 폰에 대해서는, 먼저 레이더 정보가 참조하고 있는지를, RadarInfo 배열에서 유효한 인덱스를 찾아보아 검사합니다. 인덱스도 유효하고 폰에 체력도 있으면 DeleteMe 를 거짓으로 설정하여 삭제 검사를 통과시킵니다. 인덱스는 무효하나 폰에 체력이 있으면, 레이더 인포를 새로 만듭니다. 새 레이더 인포가 생성되면, 그 레이더 인포에 대한 머티리얼 인스턴스도 새로 생성됩니다. 팀 컬러 역시 설정되며, DeleteMe 도 거짓으로 설정하여 삭제 검사를 통과시킵니다.

YourHUD.uc
    // 찾지 못하면 모든 레이더 인포를 삭제하도록 설정
    for (i = 0; i < RadarInfo.Length; ++i)
    {
      RadarInfo[i].DeleteMe = true;
    }
  
    // 레이더 인포를 업데이트한 후 더하거나 뺄 것이 있는지 확인
    ForEach DynamicActors(class'UTPawn', UTPawn)
    {
      if (UTPawn != PlayerOwner.Pawn)
      {
        Index = RadarInfo.Find('UTPawn', UTPawn);
        // 이 폰은 레이더 인포에서 찾지 못했으니 추가
        if (Index == INDEX_NONE && UTPawn.Health > 0)
        {
          i = RadarInfo.Length;
          RadarInfo.Length = RadarInfo.Length + 1;
          RadarInfo[i].UTPawn = UTPawn;
          RadarInfo[i].MaterialInstanceConstant = new () class'MaterialInstanceConstant';
  
          if (RadarInfo[i].MaterialInstanceConstant != None)
          {
            RadarInfo[i].MaterialInstanceConstant.SetParent(Material'GemOnscreenRadarContent.PointerMaterial');
  
            if (UTPawn.PlayerReplicationInfo != None && UTPawn.PlayerReplicationInfo.Team != None)
            {
              TeamLinearColor = (UTPawn.PlayerReplicationInfo.Team.TeamIndex == 0) ? Default.RedLinearColor : Default.BlueLinearColor;
              RadarInfo[i].MaterialInstanceConstant.SetVectorParameterValue('TeamColor', TeamLinearColor);
            }
            else
            {
              RadarInfo[i].MaterialInstanceConstant.SetVectorParameterValue('TeamColor', Default.DMLinearColor);
            }
          }
  
          RadarInfo[i].DeleteMe = false;
        }
        else if (UTPawn.Health > 0)
        {
          RadarInfo[Index].DeleteMe = false;
        }
      }
    }
  

렌더링 패스

패스 계산 전, 화면위 표지 크기를 먼저 계산한 다음 PointerSize 에 저장합니다. 여기 구현에서 이 작업은 화면 해상도 폭에 상대적입니다. 카메라가 보는 방향 역시 카메라 위치와 로테이션에서 구합니다. 카메라가 보는 방향은 폰이 플레이어 뒤에 있는지 아닌지를 알아내는 데 쓰입니다.

이 패스에서 DeleteMe 가 거짓이라면 레이더 인포가 렌더됩니다. 그렇지 않으면 레이더 인포 배열에서 제거됩니다.

화면위 표지가 렌더될 때, 최근 0.1초 이내에 연결된 폰이 렌더되었었는지를 먼저 검사합니다. 렌더되지 않았다면 그 불투명도를 40% 까지 선형 보간시켜 내립니다. 그 외의 경우는 100% 까지 선형 보간하여 올립니다. 불투명도가 계산되면, 머티리얼 인스턴스 콘스턴트에 설정됩니다.

플레이어의 폰과 폰 사이의 방향이 계산됩니다. 카메라 방향과 플레이어 폰에서 폰까지의 방향을 도트 곱(dot product)하여 플레이어가 현재 폰을 보고 있는지 아닌지를 결정합니다. 아래 이미지에서 빨강 화살표는 카메라 방향을 나타냅니다. 초록 화살표는 플레이어 폰이 폰을 보는 방향을 나타냅니다. 도트 곱을 하면 1.f 에서 0.f 사이의 값이 나올 테니, 카메라 앞에 있는 것입니다. 파랑 화살표는 플레이어 폰에서 다른 폰의 방향을 나타냅니다. 도트 곱은 0.f 와 -1.f 사이의 값이 될 테니, 카메라 뒤에 있는 것입니다.

DotProduct.jpg

YourHUD.uc
    // 모든 레이더 인포 렌더링을 처리
    PointerSize = Canvas.ClipX * 0.083f;
    PlayerOwner.GetPlayerViewPoint(CameraLocation, CameraRotation);
    CameraViewDirection = Vector(CameraRotation);
  
    for (i = 0; i < RadarInfo.Length; ++i)
    {
      if (!RadarInfo[i].DeleteMe)
      {
        if (RadarInfo[i].UTPawn != None && RadarInfo[i].MaterialInstanceConstant != None)
        {
          // 포인터의 불투명도 처리. 플레이어가 이 폰을 볼 수 없다면,
          // 절반 페이드 아웃. 볼 수 있다면, 페이드 인.
          if (WorldInfo.TimeSeconds - RadarInfo[i].UTPawn.LastRenderTime > 0.1f)
          {
            // 플레이어가 이 폰을 최근 0.1 초 동안 보지 못했음
            RadarInfo[i].Opacity = Lerp(RadarInfo[i].Opacity, 0.4f, RenderDelta * 4.f);
          }
          else
          {
            // 플레이어가 이 폰을 최근 0.1 초 내에 본 적이 있음
            RadarInfo[i].Opacity = Lerp(RadarInfo[i].Opacity, 1.f, RenderDelta * 4.f);
          }
          // 불투명도 적용
          RadarInfo[i].MaterialInstanceConstant.SetScalarParameterValue('Opacity', RadarInfo[i].Opacity);
  
          // 플레이어 폰에서 폰으로의 방향 구하기
          PawnDirection = Normal(RadarInfo[i].UTPawn.Location - PlayerOwner.Pawn.Location);
  
          // 폰이 앞에 있는지 검사
          if (PawnDirection dot CameraViewDirection >= 0.f)
          {
            // 액터가 카메라 앞에 있을 때 화면위 표지 렌더링 처리
          }
          else
          {
            // 액터가 카메라 뒤에 있을 때 화면위 표지 렌더링 처리
          }
        }
      }
      else
      {
        // 가비지 컬렉션이 가능하도록 기존 저장 변수 Null
        RadarInfo[i].UTPawn = None;
        RadarInfo[i].MaterialInstanceConstant = None;
        // 레이더 인포 배열에서 제거
        RadarInfo.Remove(i, 1);
        // 스텝을 한 단계 낮춰 fot 루프 유지
        --i;
      }
    }
  

카메라 앞 렌더링 패스

먼저 폰의 위치에다 콜리전 높이를 오프셋으로 더해서 WorldHUDLocation (월드 HUD 위치)를 계산합니다. 그런 다음 화면 좌표속으로 쏩(project)니다. 화면 좌표가 화면 왼편에 있으면 오프셋을 왼쪽으로, 오른편에 있으면 오프셋도 오른쪽으로 보간시켜, 화면위 표지에 약간의 애니메이션을 줍니다. 오프셋에도 제한(clamp)을 가하여 한 쪽으로 너무 크게 치우지지 않도록 합니다. 그런 다음 머티리얼 인스턴스의 로테이션을 계산한 다음 설정합니다. 마지막으로 화면위 표지를 화면 위에다 렌더합니다.

YourHUD.uc
            // 월드 HUD 위치, 폰의 머리 바로 위의 위치를 구합니다.
            WorldHUDLocation = RadarInfo[i].UTPawn.Location + (RadarInfo[i].UTPawn.GetCollisionHeight() * Vect(0.f, 0.f, 1.f));
            // 월드 HUD 위치를 화면 HUD 위치속으로 쏩니다.
            ScreenHUDLocation = Canvas.Project(WorldHUDLocation);
  
            // 화면 HUD 위치가 조금 더 오른쪽이면, 왼쪽으로 돌립니다.
            if (ScreenHUDLocation.X > (Canvas.ClipX * 0.5f))
            {
              RadarInfo[i].Offset.X -= PointerSize * RenderDelta * 4.f;
            }
            else
            {
              // 화면 HUD 위치가 조금 더 왼쪽이면, 오른쪽으로 돌립니다.
              RadarInfo[i].Offset.X += PointerSize * RenderDelta * 4.f;
            }
            RadarInfo[i].Offset.X = FClamp(RadarInfo[i].Offset.X, PointerSize * -0.5f, PointerSize * 0.5f);
  
            // 머티리얼 아이콘의 로테이션을 설정합니다.
            ActualPointerLocation.X = Clamp(ScreenHUDLocation.X, 8, Canvas.ClipX - 8) + RadarInfo[i].Offset.X;
            ActualPointerLocation.Y = Clamp(ScreenHUDLocation.Y - PointerSize + RadarInfo[i].Offset.Y, 8, Canvas.ClipY - 8 - PointerSize) + (PointerSize * 0.5f);
            RadarInfo[i].MaterialInstanceConstant.SetScalarParameterValue('Rotation', GetAngle(ActualPointerLocation, ScreenHUDLocation));
  
            // 머티리얼 포인터를 그립니다.
            Canvas.SetPos(ActualPointerLocation.X - (PointerSize * 0.5f), ActualPointerLocation.Y - (PointerSize * 0.5f));
            Canvas.DrawMaterialTile(RadarInfo[i].MaterialInstanceConstant, PointerSize, PointerSize, 0.f, 0.f, 1.f, 1.f);
  

카메라 뒤 렌더링 패스

카메라 뒤에 있는 액터의 경우, 화면위 표지는 항상 화면 하단에 위치합니다. project 함수를 그대로 사용할 수는 있지만, 수정이 필요한 결과를 반환할 것입니다. 쏘아준 가로 좌표를 반전시키는 이유가 바로 그렇습니다. 가로 화면 좌표가 왼쪽 가장자리에 있으면 가로 오프셋은 오른쪽으로 밀어주고, 그 반대도 마찬가지입니다. 그렇게 하여 화면위 표지는 항상 화면위에 있는 것입니다. 비슷한 로테이션 계산을 하여 머티리얼 인스턴스에도 적용합니다. 마지막으로 화면위 표지를 화면 위에 렌더합니다.

YourHUD.uc
            // 폰의 위치를 쏩니다.
            ScreenHUDLocation = Canvas.Project(RadarInfo[i].UTPawn.Location);
  
            // 화면 HUD 위치를 반전시킵니다.
            ScreenHUDLocation.X = Canvas.ClipX - ScreenHUDLocation.X;
  
            // 화면 HUD 위치가 오른쪽 가장자리에 있으면, 왼쪽으로 돌립니다.
            if (ScreenHUDLocation.X > (Canvas.ClipX - 8))
            {
              RadarInfo[i].Offset.X -= PointerSize * RenderDelta * 4.f;
              RadarInfo[i].Offset.X = FClamp(RadarInfo[i].Offset.X, PointerSize * -0.5f, PointerSize * 0.5f);
            }
            else if (ScreenHUDLocation.X < 8)
            {
              // 화면 HUD 위치가 왼쪽 가장자리에 있으면, 오른쪽으로 돌립니다.
              RadarInfo[i].Offset.X += PointerSize * RenderDelta * 4.f;
              RadarInfo[i].Offset.X = FClamp(RadarInfo[i].Offset.X, PointerSize * -0.5f, PointerSize * 0.5f);
            }
            else
            {
              // 화면 HUD 위치가 가운데 어디쯤에 있으면, 쭉 폅니다.
              RadarInfo[i].Offset.X = Lerp(RadarInfo[i].Offset.X, 0.f, 4.f * RenderDelta);
            }
  
            // 화면 HUD 위치를 설정합니다.
            ScreenHUDLocation.X = Clamp(ScreenHUDLocation.X, 8, Canvas.ClipX - 8);
            ScreenHUDLocation.Y = Canvas.ClipY - 8;
  
            // 실제 포인터 위치를 설정합니다.
            ActualPointerLocation.X = ScreenHUDLocation.X + RadarInfo[i].Offset.X;
            ActualPointerLocation.Y = ScreenHUDLocation.Y - (PointerSize * 0.5f);
  
            // 머티리얼 아이콘의 로테이션을 설정합니다.
            RadarInfo[i].MaterialInstanceConstant.SetScalarParameterValue('Rotation', GetAngle(ActualPointerLocation, ScreenHUDLocation));
  
            // 머티리얼 포인터를 그립니다.
            Canvas.SetPos(ActualPointerLocation.X - (PointerSize * 0.5f), ActualPointerLocation.Y - (PointerSize * 0.5f));
            Canvas.DrawMaterialTile(RadarInfo[i].MaterialInstanceConstant, PointerSize, PointerSize, 0.f, 0.f, 1.f, 1.f);
  

각도 계산

이 각도 계산 메서드에서, 검정 화살표는 기준(reference) 벡터입니다. 두 벡터는 빨강선 아니면 녹색선을 이룹니다. 각 계산은 파랑 호로 표시됩니다. 머티리얼이 로테이션 값을 라디안으로 받기 때문에, 계산 역시도 라디안으로 합니다. 초기 검사를 약간 하는데, 0.f (0 도), Pi (180 도), Pi * 1.5f (270 도), Pi * 0.5f (90 도)와 같이 간단한 결과를 반환하도록 하기 위해서입니다. 이 메서드는 (SOH CAH TOA 를 사용하는) 4분면 검사 메서드나 다른 acos 메서드보다 빠릅니다.

AngleCalculation.jpg

YourHUD.uc
  function float GetAngle(Vector PointB, Vector PointC)
  {
    // 위나 아래라서 각을 쉽게 결정할 수 있을지 검사
    if (PointB.X == PointC.X)
    {
      return (PointB.Y < PointC.Y) ? Pi : 0.f;
    }
  
    // 왼쪽이나 오른쪽이라서 각을 쉽게 결정할 수 있을지 검사
    if (PointB.Y == PointC.Y)
    {
      return (PointB.X < PointC.X) ? (Pi * 1.5f) : (Pi * 0.5f);
    }
  
    return (2.f * Pi) - atan2(PointB.X - PointC.X, PointB.Y - PointC.Y);
  }
  

완성된 언리얼스크립트 클래스

완성된 언리얼스크립트 클래스는 이러하며, 확실히 보여드리기 위해 모아놔 봤습니다.

YourHUD.uc
  class UTRadarHUD extends UTTeamHUD;
  
  struct SRadarInfo
  {
    var UTPawn UTPawn;
    var MaterialInstanceConstant MaterialInstanceConstant;
    var bool DeleteMe;
    var Vector2D Offset;
    var float Opacity;
  };
  
  var array<SRadarInfo> RadarInfo;
  
  function AddPostRenderedActor(Actor A)
  {
    // 이름표를 보이게 하고 싶지 않으니 UTPawns 에 대한 포스트 렌더 콜 제거
    if (UTPawn(A) != None)
    {
      return;
    }
  
    Super.AddPostRenderedActor(A);
  }
  
  event PostRender()
  {
    local int i, Index;
    local Vector WorldHUDLocation, ScreenHUDLocation, ActualPointerLocation, CameraViewDirection, PawnDirection, CameraLocation;
    local Rotator CameraRotation;
    local UTPawn UTPawn;
    local LinearColor TeamLinearColor;
    local float PointerSize;
  
    if (PlayerOwner == None || PlayerOwner.Pawn == None)
    {
      return;
    }
  
    // 렌더 델타 셋업
    RenderDelta = WorldInfo.TimeSeconds - LastHUDRenderTime;
  
    // 찾지 못한 레이더 인포를 모두 삭제 설정
    for (i = 0; i < RadarInfo.Length; ++i)
    {
      RadarInfo[i].DeleteMe = true;
    }
  
    // 레이더 인포를 업데이트하고 더하거나 뺄 것이 있는지 확인
    ForEach DynamicActors(class'UTPawn', UTPawn)
    {
      if (UTPawn != PlayerOwner.Pawn)
      {
        Index = RadarInfo.Find('UTPawn', UTPawn);
        // 레이더 인포에서 이 폰을 찾지 못했으니 추가
        if (Index == INDEX_NONE && UTPawn.Health > 0)
        {
          i = RadarInfo.Length;
          RadarInfo.Length = RadarInfo.Length + 1;
          RadarInfo[i].UTPawn = UTPawn;
          RadarInfo[i].MaterialInstanceConstant = new () class'MaterialInstanceConstant';
  
          if (RadarInfo[i].MaterialInstanceConstant != None)
          {
            RadarInfo[i].MaterialInstanceConstant.SetParent(Material'GemOnscreenRadarContent.PointerMaterial');
  
            if (UTPawn.PlayerReplicationInfo != None && UTPawn.PlayerReplicationInfo.Team != None)
            {
              TeamLinearColor = (UTPawn.PlayerReplicationInfo.Team.TeamIndex == 0) ? Default.RedLinearColor : Default.BlueLinearColor;
              RadarInfo[i].MaterialInstanceConstant.SetVectorParameterValue('TeamColor', TeamLinearColor);
            }
            else
            {
              RadarInfo[i].MaterialInstanceConstant.SetVectorParameterValue('TeamColor', Default.DMLinearColor);
            }
          }
  
          RadarInfo[i].DeleteMe = false;
        }
        else if (UTPawn.Health > 0)
        {
          RadarInfo[Index].DeleteMe = false;
        }
      }
    }
  
    // 모든 레이더 인포 렌더링 처리
    PointerSize = Canvas.ClipX * 0.083f;
    PlayerOwner.GetPlayerViewPoint(CameraLocation, CameraRotation);
    CameraViewDirection = Vector(CameraRotation);
  
    for (i = 0; i < RadarInfo.Length; ++i)
    {
      if (!RadarInfo[i].DeleteMe)
      {
        if (RadarInfo[i].UTPawn != None && RadarInfo[i].MaterialInstanceConstant != None)
        {
          // 포인터의 불투명도 처리. 플레이어가 이 폰을 볼 수 없다면,
          // 절반 페이드 아웃, 볼 수 있다면 페이드 인.
          if (WorldInfo.TimeSeconds - RadarInfo[i].UTPawn.LastRenderTime > 0.1f)
          {
            // 플레이어가 0.1 초 안에 이 폰을 본 적이 없음
            RadarInfo[i].Opacity = Lerp(RadarInfo[i].Opacity, 0.4f, RenderDelta * 4.f);
          }
          else
          {
            // 플레이어가 0.1 초 안에 이 폰을 본 적이 있음
            RadarInfo[i].Opacity = Lerp(RadarInfo[i].Opacity, 1.f, RenderDelta * 4.f);
          }
          // 불투명도 적용
          RadarInfo[i].MaterialInstanceConstant.SetScalarParameterValue('Opacity', RadarInfo[i].Opacity);
  
          // 플레이어의 폰에서 폰으로의 방향 구하기
          PawnDirection = Normal(RadarInfo[i].UTPawn.Location - PlayerOwner.Pawn.Location);
  
          // 폰이 내 앞에 있는지 검사
          if (PawnDirection dot CameraViewDirection >= 0.f)
          {
            // 월드 HUD 위치, 폰의 머리 바로 위를 구함
            WorldHUDLocation = RadarInfo[i].UTPawn.Location + (RadarInfo[i].UTPawn.GetCollisionHeight() * Vect(0.f, 0.f, 1.f));
            // 월드 HUD 위치를 화면 HUD 위치 속으로 쏨
            ScreenHUDLocation = Canvas.Project(WorldHUDLocation);
  
            // 화면 HUD 위치가 조금 더 오른쪽이면, 왼쪽으로 돌림
            if (ScreenHUDLocation.X > (Canvas.ClipX * 0.5f))
            {
              RadarInfo[i].Offset.X -= PointerSize * RenderDelta * 4.f;
            }
            else
            {
              // 화면 HUD 위치가 조금 더 왼쪽이면, 오른쪽으로 돌림
              RadarInfo[i].Offset.X += PointerSize * RenderDelta * 4.f;
            }
            RadarInfo[i].Offset.X = FClamp(RadarInfo[i].Offset.X, PointerSize * -0.5f, PointerSize * 0.5f);
  
            // 머티리얼 아이콘의 로테이션 설정
            ActualPointerLocation.X = Clamp(ScreenHUDLocation.X, 8, Canvas.ClipX - 8) + RadarInfo[i].Offset.X;
            ActualPointerLocation.Y = Clamp(ScreenHUDLocation.Y - PointerSize + RadarInfo[i].Offset.Y, 8, Canvas.ClipY - 8 - PointerSize) + (PointerSize * 0.5f);
            RadarInfo[i].MaterialInstanceConstant.SetScalarParameterValue('Rotation', GetAngle(ActualPointerLocation, ScreenHUDLocation));
  
            // 머티리얼 포인터 그림
            Canvas.SetPos(ActualPointerLocation.X - (PointerSize * 0.5f), ActualPointerLocation.Y - (PointerSize * 0.5f));
            Canvas.DrawMaterialTile(RadarInfo[i].MaterialInstanceConstant, PointerSize, PointerSize, 0.f, 0.f, 1.f, 1.f);
          }
          else
          {
            // 액터가 카메라 뒤에 있을 때 화면위 표지 렌더링 처리
            // 폰의 위치 쏘기
            ScreenHUDLocation = Canvas.Project(RadarInfo[i].UTPawn.Location);
  
            // 화면 HUD 위치 반전
            ScreenHUDLocation.X = Canvas.ClipX - ScreenHUDLocation.X;
  
            // 화면 HUD 위치가 오른쪽 가장자리에 있으면, 왼쪽으로 돌림
            if (ScreenHUDLocation.X > (Canvas.ClipX - 8))
            {
              RadarInfo[i].Offset.X -= PointerSize * RenderDelta * 4.f;
              RadarInfo[i].Offset.X = FClamp(RadarInfo[i].Offset.X, PointerSize * -0.5f, PointerSize * 0.5f);
            }
            else if (ScreenHUDLocation.X < 8)
            {
              // 화면 HUD 위치가 왼쪽 가장자리에 있으면, 오른쪽으로 돌림
              RadarInfo[i].Offset.X += PointerSize * RenderDelta * 4.f;
              RadarInfo[i].Offset.X = FClamp(RadarInfo[i].Offset.X, PointerSize * -0.5f, PointerSize * 0.5f);
            }
            else
            {
              // 화면 HUD 위치가 가운데 어디쯤 있으면, 쭉 폄
              RadarInfo[i].Offset.X = Lerp(RadarInfo[i].Offset.X, 0.f, 4.f * RenderDelta);
            }
  
            // 화면 HUD 위치 설정
            ScreenHUDLocation.X = Clamp(ScreenHUDLocation.X, 8, Canvas.ClipX - 8);
            ScreenHUDLocation.Y = Canvas.ClipY - 8;
  
            // 실제 포인터 위치 설정
            ActualPointerLocation.X = ScreenHUDLocation.X + RadarInfo[i].Offset.X;
            ActualPointerLocation.Y = ScreenHUDLocation.Y - (PointerSize * 0.5f);
  
            // 머티리얼 아이콘의 로테이션 설정
            RadarInfo[i].MaterialInstanceConstant.SetScalarParameterValue('Rotation', GetAngle(ActualPointerLocation, ScreenHUDLocation));
  
            // 머티리얼 포인터 그림
            Canvas.SetPos(ActualPointerLocation.X - (PointerSize * 0.5f), ActualPointerLocation.Y - (PointerSize * 0.5f));
            Canvas.DrawMaterialTile(RadarInfo[i].MaterialInstanceConstant, PointerSize, PointerSize, 0.f, 0.f, 1.f, 1.f);
          }
        }
      }
      else
      {
        // 가비지 컬렉팅 되도록 예전에 저장된 변수 Null
        RadarInfo[i].UTPawn = None;
        RadarInfo[i].MaterialInstanceConstant = None;
        // 레이더 인포 배열에서 제거
        RadarInfo.Remove(i, 1);
        // 스텝을 하나 낮추고, for 루프 유지
        --i;
      }
    }
  
    // 레이더 델타 셋업
    LastHUDRenderTime = WorldInfo.TimeSeconds;
    Super.PostRender();
  }
  
  function float GetAngle(Vector PointB, Vector PointC)
  {
    // 위나 아래여서 각을 쉽게 결정할 수 있을지 검사
    if (PointB.X == PointC.X)
    {
      return (PointB.Y < PointC.Y) ? Pi : 0.f;
    }
  
    // 왼쪽이나 오른쪽이어서 각을 쉽게 결정할 수 있을지 검사
    if (PointB.Y == PointC.Y)
    {
      return (PointB.X < PointC.X) ? (Pi * 1.5f) : (Pi * 0.5f);
    }
  
    return (2.f * Pi) - atan2(PointB.X - PointC.X, PointB.Y - PointC.Y);
  }
  
  defaultproperties
  {
  }
  

내려받기