탑건: 매버릭 미션 개발하기 #3 : 레이저 유도 폭탄 UI

Lunetis·2022년 7월 29일
0

탑건: 매버릭

목록 보기
4/14
post-thumbnail

아무리 생각해도 유도 방법이 어색함

만들고 나서 보니 레이저 유도 폭탄을 사용하는 과정이 조금 어색하다고 생각이 들었습니다.

유도를 멈추기 위해서 특수무기 대신 기본 미사일로 전환해야 한다는 부분이 아무리 생각해도 직관적이지 않고 플레이어 입장에서는 이해하기도 힘들 것 같습니다.

그래서 대안을 하나 생각했는데,
미사일 발사/폭탄 투하 버튼을 누르고 있는 동안만 유도 기능을 활성화시키는 것입니다.
그러면 기본 미사일로 전환할 필요가 없겠죠.


원래 에이스 컴뱃 시리즈에서는 발사 버튼을 누르고 있는 동안에는 날아가는 미사일이나 폭탄을 보여줍니다.
하지만 저는 이전 프로젝트에서 이 기능을 구현하지 않았죠.

언젠가 미사일에 대해서 이 기능을 추가할 수 있겠지만,
최소한 레이저 유도 폭탄에 대해서는 용도를 바꾸겠습니다.



폭탄 유도 방법 바꾸기

전술한 대로, 발사 버튼을 누르고 있는 동안에만 유도 기능이 활성화되도록 바꾸겠습니다.

그나저나 Input System을 어떻게 사용했었는지 까먹어버렸습니다. 다시 파악을 해봅시다.


이 프로젝트는 Input.GetKey(...)를 사용하는 방식이 아니라 Player Input이라는 컴포넌트를 사용해서 입력을 처리하는 방식을 사용합니다.

컴포넌트의 Events에 수동으로 등록하기도 하고, 스크립트로 등록하기도 했었죠.

아마 두 방식을 모두 다룬 것으로 기억하고 있는데, 여기서는 스크립트를 위 스크린샷처럼 수동으로 등록하는 방식이 아닌, 스크립트로 등록하는 방식을 사용하겠습니다.

왜냐하면 LaserGuidedBomb는 이 미션에서만 사용되는 스크립트이기 때문이죠. 공통으로 사용되는 스크립트가 아니기 때문에 개별적으로 등록하겠습니다.


LaserGuidanceController

using UnityEngine.InputSystem;

bool enableGuidance;

void Start()
{
    ...
    enableGuidance = true;

    InputAction fireAction = GameManager.PlayerInput.actions.FindAction("Fire");

    fireAction.started += (InputAction.CallbackContext context) => { enableGuidance = true; };
    fireAction.canceled += (InputAction.CallbackContext context) => { enableGuidance = false; };
}

// Update is called once per frame
void Update()
{
    if(lgb == null || enableGuidance == false)
        return;
    
    ...
}

현재 Scene의 PlayerInput을 받아오는 방법으로 GameManager를 싱글톤으로 만들어놓은 다음 바로 PlayerInput을 가져올 수 있게 만들어놓았었는데, 시간이 지나서도 활용할 일이 생기네요.

PlayerInput.actions.FindAction("Fire")로 미사일 발사 버튼 바인딩을 가져온 다음,
started(버튼 누름)에 enableGuidance = true, canceled(버튼 뗌)에 enableGuidance = false 코드를 실행하도록 작은 람다식을 만들어서 넣어줍시다.

바인딩에 대한 액션을 등록하기 위해서는 매개변수로 InputAction.CallbackContext를 받아와야 하므로 () 안에 넣어주기만 하고 내부에는 아무것도 건드리지 맙시다.


제가 버튼을 누르는 모습을 보실 수는 없습니다만, 발사 버튼을 누르고 있는 동안 유도 기능이 켜지는 모습을 표현한 gif입니다.


문제가 하나 생겼는데, 무기 재고가 0이 되면 "Ammunition Zero"라는 경고음이 납니다.

그리고 미션 상에서 레이저 유도 폭탄은 단 하나만 들고 있죠.
그래서 발사한 후에는 발사 버튼을 누를 때마다 경고음이 나고 있습니다.
발사하려는 게 아니라 유도하려는 건데.


최소한 현재 레이저 유도 폭탄이 날아가고 있는 동안에는 경고음이 나지 않도록 바꾸겠습니다.


WeaponController

void LaunchMissile(ref int weaponCnt, ref ObjectPool objectPool, ref WeaponSlot[] weaponSlots)
{
    ...
    
    // Ammunition Zero!
    if(weaponCnt <= 0)
    {
        int activeChildCnt = 0;
        foreach(Transform child in objectPool.transform)
        {
            if(child.gameObject.activeSelf == true)
            {
                activeChildCnt++;
            }
        }

        if(voiceAudioSource.isPlaying == false && activeChildCnt == 0) 
        {
            voiceAudioSource.PlayOneShot(ammunitionZeroClip);
        }
        return;
    }
    
    ...
}

모든 미사일, 폭탄, 기총 등의 무장은 오브젝트 풀에서 관리되고 있고,
목표물을 향해 날아가는 중인 발사체들은 active 상태가 됩니다. 그 발사체가 무언가에 맞았거나 지속시간이 다 되면 비활성화되고요.
따라서 아직 목표물을 향해 날아가고 있는 레이저 유도 폭탄이 존재하는지 확인하는 방법은, 해당 오브젝트 풀에서 active 상태인 자식 오브젝트가 1개 이상 존재하는가를 확인하면 됩니다.

경고음을 출력하는 부분에 오브젝트 풀 내의 active 상태인 자식 오브젝트 개수를 세는 코드를 추가하고,
그러한 오브젝트 개수가 0이라면, 즉 아직 날아가고 있는 미사일이나 폭탄이 하나도 없다면 경고음을 출력하도록 변경했습니다.



UI 추가

동그라미 그리기

에이스 컴뱃 7의 SAAM과 비슷한 UI를 사용해보겠습니다.

플레이어의 전투기가 향하는 방향에 점선으로 그려진 동그라미가 그려지고, 원 안에 적 전투기가 들어가도록 방향을 맞추면 미사일이 유도됩니다.


지금 만드는 레이저 유도 폭탄과는 '유도'라는 점 외에는 공통점이 하나도 없지만,
"특수무기가 선택된 상태임"을 플레이어가 확실하게 알 수 있도록 이 UI를 사용하려 합니다.

점선으로 된 동그라미를 그려서 하나 넣어주었습니다.
스크린샷에는 상하좌우에 짧은 선이 있지만, 딱히 중요하진 않으므로 생략합니다.


비행기에서 앞쪽으로 일정 거리 떨어진 거리에 빈 오브젝트를 하나 놓고,

UI를 배치한 다음,

이전에 만들어뒀던 FollowTransformUI라는 컴포넌트를 추가하고, 아까 비행기 앞쪽에 추가했던 빈 오브젝트를 Target Transform에, 이 UI가 보여질 카메라는 UI Camera를 등록합니다.

이 컴포넌트는 3D 공간에 있는 물체의 위치에 UI가 놓이도록 하는 기능을 가집니다.
점선으로 된 동그라미의 중앙은 항상 비행기 앞에 놓인 빈 오브젝트의 위치에 고정시키는 용도로 사용합니다.

이렇게 만들어주기 위해서죠.

저 가운데에 있는 조준점에 고정시키면 되지 않냐는 생각을 하실 수도 있는데,

사실 저 조준점은 비행기 기동에 따라 약간씩 움직여야 하고, 동그라미는 움직이면 안 되기 때문에 별개의 오브젝트로 만들어주었습니다.

조준점 개발 과정은 여기에서 볼 수 있습니다.

그리고 이 동그라미는 특수무기가 선택된 상태에서만 보여져야 합니다.

LaserGuidanceController

...
public GameObject scanUI;

void Awake()
{
    scanUI.SetActive(false);
}

void OnEnable()
{
    scanUI?.SetActive(true);
}

void OnDisable()
{
    scanUI?.SetActive(false);
}

기본적으로는 비활성화시키고, 이 스크립트는 특수무기가 선택될 때만 활성화되므로 OnEnable()OnDisable()에 UI를 키고 끄는 코드를 추가합니다.


모드를 바꿀 때마다 동그라미가 나타나거나 사라집니다.



조준점 그리기

이제 빨간 구를 치워버리고 제대로 된 UI를 사용하겠습니다.

특수무기를 사용할 때는 이렇게 생긴 락온 표시를 사용합니다.
유도되는 위치에 이 UI를 띄워주겠습니다.

UI를 새로 생성하고 FollowTransformUI를 추가해줬는데,

생각해보니 '어떤 물체'에 붙어가는 UI라서 Vector3 좌표만으로는 UI 위치를 지정해줄 수 없군요. 결국 Transform이 하나 필요합니다.

자꾸 미리 생성해놓아서 씬에 배치하기는 그러니까, 발사되고 폭발하는 사이에만 사용되는 빈 GameObject를 하나 생성하고 삭제하는 방식으로 하겠습니다.

LaserGuidanceController

public FollowTransformUI guidanceUI;
GameObject guideObject;

public void ShowGuidanceUI()
{
    guideObject = new GameObject();
    guidanceUI.gameObject.SetActive(true);
    guidanceUI.targetTransform = guideObject.transform;
}

public void HideGuidanceUI()
{
    lgb = null;
    guidanceUI.gameObject.SetActive(false);
    Destroy(guideObject);
}

void Update()
{
    ...
    
    guideObject.transform.position = lgb.guidedPosition;
}

컨트롤러에는 UI를 활성화/비활성화하는 스크립트를 추가합니다.
ShowGuidanceUI()는 UI에 넘겨줄 빈 GameObject를 생성하고 UI를 활성화합니다.
HideGuidanceUI()는 UI를 비활성화하고 만들었던 GameObject를 삭제합니다.

그리고 매 프레임마다 UI가 가리키는 위치를 설정해줍니다.

정확히 말하면 정말 매 프레임마다는 아니고, enableGuidance == true일 때만 위치를 설정해주기 때문에 발사 버튼을 누르고 있는 동안만 실행될 것입니다.


LaserGuidedBomb

LaserGuidanceController laserGuidanceController;

public override void Launch(Vector3 guidedPosition, float launchSpeed, int layer, GameObject launcher)
{
    ...

    laserGuidanceController = launcher.GetComponent<LaserGuidanceController>();
    laserGuidanceController.lgb = this;

    laserGuidanceController.ShowGuidanceUI();
}

protected override void DisableMissile()
{
    ...
    
    laserGuidanceController.HideGuidanceUI();
}

폭탄이 LaserGuidanceController 스크립트를 들고 있어야 할 것 같습니다.
컨트롤러가 가지고 있는 폭탄 정보를 자기 자신으로 초기화하고, ShowGuidanceUI()를 발사 시에, HideGuidanceUI()를 비활성화 시에 호출합니다.

일단 원하는대로 작동은 하고 있습니다.

하지만 조금 마음에 들지 않네요. 뭔가 밋밋합니다.

그리고 조준점이랑 UI가 비슷해서 중앙에 있으면 구분이 잘 안 되기도 하고요.
조준점은 나중에 생각해보고, 밋밋함을 해결하기 위한 무언가를 하나 추가해보겠습니다.



유도 경로 그리기

이런 식으로 현재 폭탄의 위치와 유도 지점 사이에 점선을 그려주면 한결 나을 것 같습니다.

유니티에서는 GL이라는 라이브러리로 OpenGL의 로우레벨 함수를 실행할 수 있습니다.

이것 역시 이전 프로젝트에서 다룬 적이 있습니다.

타겟 화살표 그릴 때에요.

전에 사용했던 코드를 참고하면서, 두 지점 사이에 점선을 그을 수 있도록 코드를 작성해보겠습니다.

직선 그리기

일단 점선 대신 직선부터 그어보죠.


LaserGuidanceController

[Header("Line Properties")]
[SerializeField]
Material lineMaterial;

Vector3[] vertexPositions;

void Start()
{
    ...

    vertexPositions = new Vector3[2];
    Camera.onPostRender += OnPostRenderCallback;
}

void OnPostRenderCallback(Camera cam)
{
    if(lgb == null)
        return;

    if (vertexPositions == null || vertexPositions.Length < 2)
        return;
        
    vertexPositions[0] = guideObject.transform.position;
    vertexPositions[1] = lgb.gameObject.transform.position;

    int end = vertexPositions.Length - 1;
    lineMaterial.SetPass(0);

    GL.Begin(GL.LINES);
    for (int i = 0; i < end; ++i)
    {
        GL.Vertex(vertexPositions[i]);
        GL.Vertex(vertexPositions[i + 1]);
    }
    GL.End();
}

GL을 이용한 그리기는 OnPostRender()라는 함수에서 실행하거나
위 예시처럼 콜백 함수를 만들어서 그 안에서 그리기를 실행하고, Camera.onPostRender에 콜백 함수를 넘겨주어야 합니다.

또한 선을 그리기 위해서는 선에 적용되는 Material을 지정해줘야 합니다.

guideObject와 lgb의 위치 사이를 이어주도록 코드를 작성했습니다.

참고로 GL.LINES를 사용할 때 그려지는 직선의 두께는 1입니다. 이 두께는 조정할 수 없습니다.

이렇게 유도 위치와 미사일 사이에 줄이 그어지는 것을 확인할 수 있습니다.


점선으로 바꾸기

이제 점선으로 바꿔봅시다.


public float dashedLineLength = 10;

void OnPostRenderCallback(Camera cam)
{
    if(lgb == null)
        return;

    if (vertexPositions == null || vertexPositions.Length < 2)
        return;
    
    // Init
    vertexPositions[0] = guideObject.transform.position;
    vertexPositions[1] = lgb.gameObject.transform.position;
    Vector3 directionVector = vertexPositions[1] - vertexPositions[0];

    float distance = Vector3.Distance(vertexPositions[0], vertexPositions[1]);
    directionVector = directionVector.normalized * dashedLineLength;

    int end = vertexPositions.Length - 1;
    lineMaterial.SetPass(0);

    // Draw
    GL.Begin(GL.LINES);
    while(distance > 0)
    {
        distance -= dashedLineLength * 2;
        if(distance > 0)
        {
            vertexPositions[1] = vertexPositions[0] + directionVector;
        }
        else
        {
            vertexPositions[1] = lgb.gameObject.transform.position;
        }
        
        GL.Vertex(vertexPositions[0]);
        GL.Vertex(vertexPositions[1]);

        vertexPositions[0] += directionVector * 2;
    }
 }

두 점 사이의 거리를 구하고, 두 점 사이의 방향 벡터를 구합니다.
그리고 두 점 사이는 dashedLineLength마다 선을 그리기와 그리지 않기를 반복합니다.


점선까지 그리는 것을 완료했습니다.





풀리지 않는 문제

사실은 점선의 두께를 설정하는 코드가 있는데, 화면 밖에 유도 위치나 폭탄이 있는 경우 제대로 좌표 계산이 안 되고 있어서 주석 처리해놓았습니다.

선이 너무 얇아서 잘 보이지 않기 때문에 두께를 적당히 키워줄 필요가 있습니다.
그러나 지금 당장 해결책이 생각이 나지 않기 때문에, 나중에 해결하거나... 제 스스로 이슈에 등록해야겠습니다.

이 프로젝트의 작업 결과물은 Github에 업로드되고 있습니다.
https://github.com/lunetis/OperationMaverick

0개의 댓글