Ace Combat Zero: 유니티로 구현하기 #4 : 미사일

Lunetis·2021년 3월 21일
3

Ace Combat Zero

목록 보기
5/27
post-thumbnail




이제 비행기와 카메라 조작은 어느정도 가능한 상태가 되었으니,
슬슬 에이스 컴뱃의 기틀을 다질 때입니다.

이 게임은 "비행 슈팅 게임"입니다.


미사일을 쏴야죠.

근데 그 전에 비행기 모델링을 약간 손보려고 합니다.



비행기 양 날개와 기체 밑에 양쪽으로 깎은 몽당연필처럼 생긴 부분이 보이시죠?
이 부분은 연료 탱크입니다.

조금 더 정확한 명칭으로는 Conformal Fuel Tank (CFT) 라고 불립니다.

비행기 외부에 여분의 연료를 싣는 부품으로, 비행기는 이 외부에 있는 연료를 먼저 소모하고, 모두 소모한 이후에는 비행기 내부의 연료를 사용합니다.


미사일을 추가하기 전에, 모델링에 붙어있는 연료 탱크들을 떼어내는 작업을 먼저 하겠습니다.

왜냐하면... 게임에서 이 연료 탱크 모델링을 따로 구현하지 않았기 때문이죠.



에이스 컴뱃 7 공식 소개 영상입니다.
썸네일에 보이는 건 연료탱크가 아니고 레이저 무기입니다. (0:26 참고)


따로 원작자에게 불만이 있는 건 아닙니다.

솔직히 말하면 그냥 굳이 저 부분이 있어야 하나 싶어서 빼는 거에요.

(https://skfb.ly/6VnHW)

고마워요 Usman1684님!


아무튼, 모델링에 있는 연료 탱크 부분을 떼어내기 위해 3D 작업을 해야겠네요.


모델링에서 연료탱크 떼어내기

무료 3D 모델링 소프트웨어 Blender를 가져왔습니다.

fbx 파일로 된 비행기 모델링을 Import했더니 저 화살표 비스무리한 게 붙어있습니다.
위치를 보아하니 뭔가 애니메이션이 붙었다는 뜻 같은데...

잘 모르겠군요.

상단의 Modeling 탭에 들어가서 연료탱크 부분에 있는 Vertex들을 모두 삭제하겠습니다.

몽당연필을 모두 날렸습니다.

이제 수정한 fbx 파일을 Export한 다음...


아니, 그러고보니 유니티는 blend 파일을 지원했었죠.

그냥 프로젝트에 넣읍시다.

중요: 3D 모델링 파일을 .blend 파일로 프로젝트에 넣는 경우, 프로젝트를 여는 컴퓨터에 Blender가 설치되어있지 않으면 해당 3D 모델링이 게임 내에서 보이지 않습니다.
.obj, .stl 등 기타 3D 포맷은 관련된 3D 모델링 프로그램이 설치되어있지 않아도 프로젝트 열람 및 실행에 문제가 없지만, .blend 파일은 Blender가 설치된 환경에서만 제대로 열람이 가능합니다.

비행기 모델링을 새로운 모델링으로 교체해줬습니다.

이제 여기에 새 미사일을 달아줘야 합니다.
공짜 미사일 모델링을 찾아보죠.


미사일 모델링 찾기

아무리 비행기 한 대에 미사일 100개 이상을 탑재하는 말도 안 되는 게임이라고는 하지만,
탑재하는 미사일만큼은 고증을 맞춰줍시다.

F-15C에 탑재되는 미사일 정보를 알아보러 갑시다.

https://www.boeing.com/defense/f-15/

F-15C 공대공 미사일은 3가지를 탑재한다고 하는군요.

  • AIM-7 Sparrow: 중거리 반능동 유도 미사일, 발사 코드: Fox One
  • AIM-9 Sidewinder: 단거리 적외선 유도 미사일, 발사 코드: Fox Two
  • AIM-120 AMRAAM: 중거리 능동형 레이더 유도 미사일, 발사 코드: Fox Three

게임을 하는 동안 일반 미사일을 발사할 때 AWACS가 외치는 말은 Fox Two입니다.
대충 AIM-9 사이드와인더 미사일을 사용하고 있다는 뜻이겠지요.



에이스 컴뱃 시리즈에서는 기본 미사일 외에 특수무기를 장착할 수 있습니다.
최신작인 7, 지금 만들려는 0편에서는 3가지 특수무기 중 한 가지만 장착할 수 있습니다.

MSSL은 기본 미사일이고, 나머지 3개 중에 하나를 골라야 합니다.


보스전에서는 보통 진리의 QAAM (Quick Maneuver Air-to-Air Missile) 을 사용합니다.
어메이징한 기동성을 자랑하며 개판으로 움직이는 물체도 잡아주시는 킹갓 미사일입니다.

UGBL (Unguided Bomb (Large), 무유도 폭탄) 을 사용하고 싶으시다고요?


지금 당장 에이스 컴뱃 7을 구매하세요.

(세일할 때 사세요. 정가 주고 사기에는 조금 아깝습니다.)

다시 본론으로 돌아가서,

에이스 컴뱃 제로나 에이스 컴뱃 7이나 QAAM은 (미국산 전투기의 경우) AIM-9X 사이드와인더 미사일 모델링을 사용한다고 합니다.
기본 미사일도 미국산 전투기는 AIM-9M 미사일을 사용한다고 하네요.

X든 M이든 일단 무료로 쓸 수 있는 AIM-9 미사일 모델링이 있는지 찾아보겠습니다.

(https://www.turbosquid.com/3d-models/free-3ds-mode-missile-aim-9-sidewinder/924208)

AIM-120은 많이 보이는데 AIM-9는 퀄리티 좋은 무료 모델링이 흔하지는 않았습니다.

그래도 이 정도면 괜찮아보이네요.

블렌더에서 텍스쳐를 씌워봅시다...

텍스쳐가 90도 돌아간 것 같은데요...

텍스쳐를 선택하고 옆에 있는 속성에서 Rotation을 바꿔주면 되는군요.


이제 저장하고 유니티에 모델링 파일과 텍스쳐 파일을 넣어보겠습니다.





...?





적용이 안 되네요?






꼼수를 씁시다. 그냥 90도 돌린 텍스쳐를 만들고 붙여버리죠.

모든 텍스쳐를 만들어서 붙인 모습입니다.

아무튼 이렇게 AIM-9 미사일 모델링을 구했습니다.


미사일은 게임 도중 생성/삭제되면서 재사용하는 객체니 Prefab으로 만들어줍시다.



미사일 스크립트

먼저 간단한 미사일 코드부터 작성하겠습니다.

Missile.cs

public class Missile : MonoBehaviour
{
    Transform target;
    float turningForce;

    public float maxSpeed;
    public float accelAmount;
    public float lifetime;
    float speed;

    public void Launch(Transform target, float launchSpeed)
    {
        this.target = target;
        speed = launchSpeed;
    }

    void Start()
    {
        Destroy(gameObject, lifetime);
    }
    
    // Update is called once per frame
    void Update()
    {
        if(speed < maxSpeed)
        {
            speed += accelAmount * Time.deltaTime;
        }

        transform.Translate(new Vector3(0, 0, speed * Time.deltaTime));
    }
}

간략한 미사일 스크립트입니다.
최대 속도까지 가속하고 이동하는 기능만 가지고 있고, 유도 기능은 아직 없습니다.

Launch(...) 함수는 유도될 대상(target)과 초기 속도(launchSpeed)를 설정합니다.

비행기에서 미사일을 발사할 때, 그 미사일의 속력은 비행기의 속력과 동일한 상태에서 가속하게 됩니다.
700km/h인 비행기와 2000km/h인 비행기에서 발사한 미사일의 초기 속력이 같을리가 없죠.

따라서 발사될 당시의 비행기의 속력값을 넘겨줘야 합니다.

Start()에서는 lifetime초가 지나면 삭제되도록 Destroy(...)를 추가했습니다.

미사일이나 총알처럼 게임 도중 여러 번 생성/삭제되는 오브젝트는 오브젝트 풀링을 이용해 재사용하는 편이 좋습니다.
하지만 일단 빠르게 만들기 위해서 InstantiateDestroy를 사용하겠습니다.



#3에서 카메라 기능을 기존 코드와 분리해서 따로 작성한 것처럼,
무기 관련 기능을 기존 코드와 분리해서 작성하겠습니다.

WeaponController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class WeaponController : MonoBehaviour
{
    // Weapon Inputs
    bool useSpecialWeapon;
    bool isGunFiring;
    
    // Weapon Callbacks
    public void Fire(InputAction.CallbackContext context)
    {
        if(context.action.phase == InputActionPhase.Performed)
        {
            Debug.Log("Fire");
        }
    }

    public void GunFire(InputAction.CallbackContext context)
    {
        switch(context.action.phase)
        {
            case InputActionPhase.Performed:
                isGunFiring = true;
                break;

            case InputActionPhase.Canceled:
                isGunFiring = false;
                break;
        }
    }


    public void SwitchWeapon(InputAction.CallbackContext context)
    {
        if(context.action.phase == InputActionPhase.Performed)
        {
            useSpecialWeapon = !useSpecialWeapon;
        }
    }
}

WeaponController를 만들고, AircraftController에 있는 무기 관련 코드를 모두 옮겼습니다.

여기에 미사일을 발사하는 코드를 추가해보겠습니다.


WeaponController.cs

public GameObject missilePrefab;

// Weapon Callbacks
public void Fire(InputAction.CallbackContext context)
{
    if(context.action.phase == InputActionPhase.Performed)
    {
        Debug.Log("Fire");
        LaunchMissile();
    }
}

void LaunchMissile()
{
    GameObject missile = Instantiate(missilePrefab, transform.position, transform.rotation);
    Missile missileScript = missile.GetComponent<Missile>();
    missileScript.Launch(null, ...)
}

이벤트 콜백인 Fire(...) 함수 내부에서 LaunchMissile() 을 실행하게끔 코드를 추가했습니다.

미사일을 발사하는 LaunchMissile() 함수는 미사일 객체를 Instantiate로 생성합니다.

LaunchMissile()에서는 미사일 프리팹을 현재 위치와 회전값에 맞게 생성하고,
Missile.csLaunch()를 호출해서 유도 대상과 속도를 넘겨줍니다.

오브젝트 풀링에서는 Instantiate 대신 비활성화된 객체를 활성화하거나, 풀이 가득찼을 경우 추가 생성 작업을 하게 됩니다.


근데 속도가 없네요.
AircraftController에서는 있었지만 WeaponController는 없습니다.

얻어올 창구가 필요합니다.


AircraftController.cs

float speed;
public float Speed
{
    get
    {
        return speed;
    }
}

AircraftControllerspeed 값을 얻어올 수 있도록 public float Speed를 만들겠습니다.
get은 그대로 public으로 두고, set은 private로 두겠습니다.


그리고 WeaponController에서는 AircraftController에서 속력을 얻어와야 합니다.
무조건 AircraftController가 필요하므로, [RequireComponent(typeof(AircraftController))]를 클래스 밖에 추가해주겠습니다.


[RequireComponent(typeof(AircraftController))]	// 이 줄은 클래스 밖에 있습니다.

void Start()
{
    aircraftController = GetComponent<AircraftController>();
}

void LaunchMissile()
{
    GameObject missile = Instantiate(missilePrefab, transform.position, transform.rotation);
    Missile missileScript = missile.GetComponent<Missile>();
    missileScript.Launch(null, aircraftController.Speed + 15);
}

Start()에서 AircraftController 컴포넌트를 받아온 다음, 미사일을 발사할 때 Speed 값을 가져와서 넘겨주겠습니다.

*미사일의 초기 가속을 반영하기 위해 +15를 추가했습니다.

구현이 끝났으면 PlayerInput에 있는 무기 관련 이벤트들을 새로 연결해줍니다.
원래 AircraftController에 있었던 코드들이 사라졌기 때문에 Missing(...)으로 표시되고 있었을 것입니다.

마지막으로 미사일 프리팹Missile 스크립트를 추가하고, 초기값을 추가합니다.

WeaponController.LaunchMissile()에서 미사일을 생성할 때, 미사일 프리팹에 Missile 스크립트가 있을 것이라고 가정하고 코드를 작성했기 때문입니다.
없으면 NullReferenceException이 발생하죠.


근데 미사일 최대 속도가 얼마나 되죠?

마하 2.5 이상이라고 합니다.

그냥 마하 2.5로 잡아서 3,087km/h라고 한다면 F-15C의 최대 속도 (3,017km/h)와 비슷한 수준이네요.

가속력은 얼마나 될 지는 모르겠지만 대충 넣어봅시다.

이제 발사 버튼을 누를 때마다 미사일이 날아가는 것을 확인할 수 있습니다.

근데 이펙트같은 게 없으니 제대로 안 보이네요.


유튜브를 보면서 5분컷으로 임시로 쓸 이펙트를 추가했습니다.

https://youtu.be/__y100uwVdM


...아 맞다, 비행운.


세부적인 미사일 기능 구현하기

양쪽에서 번갈아가며 발사하기

에이스 컴뱃에서는 미사일을 양쪽 날개 밑에 달고 다닙니다.
보통 가운데에 달고 다니는 건 대형 미사일이나 폭탄 종류죠.

지금은 가운데(정확히 말하면 그냥 비행기 중앙)에서 미사일이 나가고 있습니다.
이 위치를 양쪽 날개 밑으로 옮기고, 2개를 연속으로 쏠 수 있도록 합시다.

그리고 왼쪽 날개의 미사일과 오른쪽 날개의 미사일을 번갈아서 사용하도록 만들죠.


WeaponController.cs


public int missileCnt;
public Transform leftMissileTransform;
public Transform rightMissileTransform;

void LaunchMissile()
{
    if(missileCnt <= 0)
        return;

    Vector3 missilePosition = (missileCnt % 2 == 1) ? rightMissileTransform.position : leftMissileTransform.position;
    GameObject missile = (GameObject)Instantiate(missilePrefab, missilePosition, transform.rotation);
    Missile missileScript = missile.GetComponent<Missile>();
    missileScript.Launch(null, aircraftController.Speed);
    
    missileCnt--;
}

미사일 탑재량을 나타내는 missileCnt를 추가하고,
남은 미사일이 홀수면 오른쪽, 짝수면 왼쪽 미사일을 발사하도록 구현했습니다.

bool형 변수같은 거 추가할 필요도 없고, 아주 직관적이죠.

그 다음 미사일을 생성할 위치를 잡아줍시다.

미사일 프리팹을 생성해서 비행기에 붙어있어야 할 위치를 잡아보고,

그 자리에 빈 GameObject를 생성한 다음 비행기 오브젝트에 붙여줍니다.

위치를 모두 잡았으면 미사일을 삭제하고, Transform을 등록해줍니다.
미사일 탑재량도 손본 다음에 플레이를 해봅니다.

양쪽 날개에서 미사일이 잘 날아갑니다.


미사일 쿨타임

(탑재량은 방금 위에서 추가했습니다.)

미사일 발사하는 데에는 쿨타임이 있습니다.
이 쿨타임을 구현하는 방법도 여러가지가 있겠죠.

WaitForSeconds()를 사용하는 방법도 있고,
Update()에서 Time.deltaTime만큼 계속 빼준다던가 하는 방법도 있고,
아무튼 다양한 방법이 존재합니다.


제가 쓸 방식은 Time.deltaTime을 빼주는 방식입니다.

그냥 딜레이를 주는 함수를 만들수도 있지만,
나중에 UI를 만들 때 고려해야 하는 사항이 있습니다.

미사일 쿨타임을 UI에 표현해줘야 합니다.

단순하게 bool 변수 하나를 만들고 몇 초 후에 true/false를 바꾸는 식으로는 이 쿨타임을 표현할 수 없습니다.
진행도를 표현하기 위해서는 숫자로 된 변수가 필요합니다.


public float missileCooldownTime;

float rightMslCooldown;
float leftMslCooldown;

void LaunchMissile()
{
    if(missileCnt <= 0)
    {
        // Ammunition Zero!
        return;
    }
    if(leftMslCooldown > 0 && rightMslCooldown > 0)
    {
        // Beep sound
        return;
    }

    Vector3 missilePosition;
    
    if(missileCnt % 2 == 1)
    {
        missilePosition = rightMissileTransform.position;
        rightMslCooldown = missileCooldownTime;
    }
    else
    {
        missilePosition = leftMissileTransform.position;
        leftMslCooldown = missileCooldownTime;
    }
    GameObject missile = (GameObject)Instantiate(missilePrefab, missilePosition, transform.rotation);

    Missile missileScript = missile.GetComponent<Missile>();
    missileScript.Launch(null, aircraftController.Speed + 15);
    
    missileCnt--;
}

미사일 발사 코드를 수정합니다.
먼저 public float missileCooldownTime은 미사일 발사 쿨타임을 나타냅니다.
float rightMslCooldown, leftMslCooldown은 날개 양쪽에 달린 미사일의 쿨타임입니다. 이 값이 0이면 발사할 수 있다는 뜻입니다.

미사일을 발사할 때 남은 미사일이 있는지, 쿨타임은 다 돌았는지를 먼저 검사합니다.

미사일 발사가 가능한 상태면 현재 탑재된 미사일의 홀짝에 따라 왼쪽/오른쪽을 선택하고,
해당하는 부분에 쿨타임을 부여합니다.

void MissileCooldown(ref float cooldown)
{
    if(cooldown > 0)
    {
        cooldown -= Time.deltaTime;
        if(cooldown < 0) cooldown = 0;
    }
    else return;
}

void Update()
{
    MissileCooldown(ref rightMslCooldown);
    MissileCooldown(ref leftMslCooldown);
}

쿨타임은 Update()에서 MissileCooldown(...)을 호출하여 조절합니다.
쿨타임이 남아있으면 매 프레임마다 Time.deltaTime만큼 빼고, 0 이하가 되면 0으로 설정합니다.

쿨타임은 참조형으로 보내줘서 양쪽이 같은 함수를 사용할 수 있도록 구현했습니다.

이제 쿨타임 값을 주고 실행해봅시다.

옆의 콘솔에 있는 Cooldown 값이 0이 되기 전까지는 미사일 발사가 되지 않고,
0이 되면 미사일의 개수에 따라 한 쪽에서 미사일이 발사되는 것을 볼 수 있습니다.



유도 시스템

락온(lock-on)을 구현하기 전에 미사일을 유도하는 것부터 먼저 만듭시다.

아까 미사일 스크립트를 만들 때 target 변수도 추가했었지만 당장 쓰지는 않았었죠.
target을 향해 유도되는 미사일을 만들어보겠습니다.

일단 타겟이 되는 물체를 추가합시다.

우리가 만드는 미션의 유일한 타겟인 픽시입니다.

원래 보스에 맞는 특별한 비행기 (ADFX-02 Morgan)를 타고 다니지만,
모델링이 없는 관계로 얘도 F-15C입니다.

WeaponController.cs


public Transform target;

void LaunchMissile()
{
    ...
    
    missileScript.Launch(target, aircraftController.Speed + 15);
    
    missileCnt--;
}

원래는 public이 아니어야 하지만, 일단 Inspector View에서 할당해줄 수 있도록 이번에만 설정해줍니다.
그리고 Launch()에 아까 null로 보내줬던 첫 번째 매개변수 target 값을 넣어줍시다.

Inspector View에서 등록하는 것도 잊지 마세요.



이제 미사일 코드를 건드려보죠.

transform.LookAt(target);

이 함수는 쓰지 않습니다. 실행 즉시 방향을 바꿔버리기 때문이죠.
LookAt()을 썼다가는 아무도 피하지 못하는 개사기급 미사일이 만들어지게 됩니다.


상상이 안 가신다면, 아래 예시를 보시면 됩니다.





과해보이지만 이 정도는 그럴 수 있다고 칩시다.

에이스 컴뱃에서도 이 정도의 미사일 기동은 나오니까요.






하지만 이건 좀 심하잖아요?






게임 밸런스와 난이도 조절을 위해, 미사일의 방향이 서서히 틀어지도록 만들어야 합니다.


Missile.cs

Transform target;
public float turningForce;

void LookAtTarget()
{
    Quaternion lookRotation = Quaternion.LookRotation(target.position - transform.position);
    transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, turningForce * Time.deltaTime);
}

// Update is called once per frame
void Update()
{
    if(speed < maxSpeed)
    {
        speed += accelAmount * Time.deltaTime;
    }

    transform.Translate(new Vector3(0, 0, speed * Time.deltaTime));
    LookAtTarget();
}

LookAtTarget() 함수를 추가하고, Update()에서 호출합니다.

LookAtTarget() 함수는 타겟과 자신의 위치의 차이를 이용해서 바라보는 각도를 구합니다.
그리고 현재 각도에서 바라보는 각도로 서서히 변화하도록 Slerp를 이용합니다.


서서히 변화하는 정도는 turningForce(선회력)으로 조절합니다.

이 값이 높을수록 미사일의 선회력이 증가하게 되고, 미칠듯한 기동을 하는 적도 손쉽게 잡아낼 수 있게 됩니다.
하늘을 뚫을 정도로 높은 값을 주면 LookAt()과 같은 역할을 하게 될 것입니다.

이전에 언급했던 특수무기 중 하나인 QAAM(Quick Maneuver Air-to-Air Missile)은 높은 선회력이 특징인 미사일입니다.
기본 미사일과 같은 코드를 공유하면서 turningForce 값을 높이면 간단하게 구현할 수 있겠죠.

이제 미사일 프리팹으로 들어가서 선회력을 조절합니다.
글쎄요, 1 정도면 될까요? (온전히 돌아가는 데에 1초 소모)


나쁘지 않아보이는데...

움짤 끝에 왜 미사일이 다른쪽에서 날라오고 있는 것 같을까요?




미사일 궤도 추적을 위해 Trail Renderer를 달아보았습니다.

그리고 대형 비행기를 세워놓으면 궤도 추적에 방해가 될 수 있어서,
타겟도 그냥 구(Sphere)로 바꿨습니다.

아, 통과되고 계속 도는 거였군요.


그러고보니 진짜로 중요한 기능을 하나 빼먹었습니다.

물체랑 닿으면 미사일이 터져야 하는데, 그 기능이 없었네요.



미사일 뒷처리

폭파시키기

유니티 엔진을 다뤄보신 분이라면 한 번쯤 해보셨을 충돌 감지 되겠습니다.

구현은 굉장히 간단하죠. 그냥 OnCollisionEnter나 OnTriggerEnter를 써주면 되니까요.
지금 만들고 있는 프로젝트는 rigidbody를 거의 사용하지 않으니, OnTriggerEnter를 사용하면 되겠네요.


Missile.cs

void OnTriggerEnter(Collider other)
{
    Explode();
    Destroy(gameObject);
}

void Explode()
{
    Instantiate(explosionPrefab, transform.position, Quaternion.identity);
}

닿은 것이 뭐가 되었든 간에 그냥 폭파시켜버립시다.

폭발 이펙트 프리팹을 생성하고, 자기 자신을 삭제합니다.

중요: OnTriggerEnter를 사용하여 충돌을 감지할 때, 충돌되는 물체 둘 중 하나는 반드시 rigidbody가 있어야 합니다.
사실 모든 오브젝트에 있어야 하는 게 맞겠지만, 최소한으로 넣기 위해서 미사일과 지면에만 rigidbody를 추가하겠습니다.


폭발 이펙트는...

https://assetstore.unity.com/packages/essentials/tutorial-projects/unity-particle-pack-127325

공짜로 유니티에서 뿌려주는 폭발 이펙트를 사용합시다.

실제로 적용하면 이렇게 보입니다.

그나저나 이펙트 프리팹이 계속 루프를 돌고 있네요.

프리팹에 들어가서 모든 Particle System들의 Looping을 비활성화시킵시다.


자, 폭파 준비도 완료되었습니다. 실행해볼까요?



어...

저희 비행기도 충돌체가 있었죠... 참...
약간 겹쳤나봅니다.

근데 이펙트에 디버그 UI가 나오는 건 어떻게 설명해야 할지 모르겠네요.




이펙트에 UI가 왜 묻어나와??

UI가 이펙트에 반사되어 보이는 현상부터 수정하겠습니다.

Canvas의 Sorting Layer를 Default가 아닌 다른 레이어로 두었습니다.
저는 UI라는 레이어를 새로 만들어서 적용시켰습니다.



Before

After


UI가 반사되던 현상이 사라졌습니다.




정밀한 충돌 처리

그 다음은 충돌 처리입니다.

지금 플레이어가 발사한 미사일에 플레이어가 맞고 있습니다.
근접신관 성능이 너무 죽여줘서 피아식별도 하지 않는 상황이라고나 할까요.

"그냥 충돌체가 플레이어면 무시해도 되지 않을까?"

라고 생각하기에는 조금 문제가 복잡합니다.

  • 플레이어가 발사한 미사일은 플레이어가 맞으면 안 됩니다.
  • 마찬가지로, 적이 발사한 미사일에 적이 맞으면 안 됩니다. 팀킬도 없다고 칩시다.
  • 미사일은 요격이 가능합니다. 다만 같은 편에서 쏜 미사일끼리는 요격되지 않습니다.

좀 더 깊이 파고들어가면 비행기끼리의 충돌도 고려해야 하지만, 이 미션은 1:1 대전이므로 무시하도록 하겠습니다.

#3. 카메라 조작하기에서, 비행기만 보이지 않는 카메라를 만들기 위해 레이어 설정을 건드렸었죠.

이 충돌 처리도 레이어 기반으로 구현할 수 있습니다.


레이어는 간단하게, 플레이어플레이어가 발사한 미사일Player,
적이 발사한 미사일Object로 처리하겠습니다.

기타 중립적인 물체Ground로 두죠. 이 값은 이미 프로젝트 기본값으로 추가되어 있습니다.


먼저 Tags & Layers에서 Player, Object 레이어를 추가해줍니다.


유니티 프로그램 상단의 Edit > Project Settings... 창의 Physics 항목에 들어갑니다.

하단에 Layer Collsion Matrix라는 선택 항목들이 있습니다.
여기서 체크된 부분은 서로의 충돌을 감지하고, 체크 해제된 부분은 충돌을 감지하지 않게 됩니다.

Player끼리, Object끼리는 충돌을 무시하도록 설정해줍시다.



이제 미사일에 레이어를 적용시켜야 합니다.

근데 미사일은 적이나 아군이나 똑같은 프리팹을 쓰게끔 하고 싶은데요.


Missile.cs

public void Launch(Transform target, float launchSpeed, int layer)
{
    this.target = target;
    speed = launchSpeed;
    gameObject.layer = layer;
}

발사할 때 미사일에 적용할 레이어도 같이 넘겨주도록 코드를 작성해봅시다.
세 번째 매개변수로 int layer를 받아서 미사일 오브젝트에 바로 적용시킵니다.


WeaponController.cs

void LaunchMissile()
{
    ...
    
    missileScript.Launch(target, aircraftController.Speed + 15, gameObject.layer);
    
    ...
}

그러면 이 함수를 호출하는 쪽도 매개변수를 추가해줘야죠.
발사하는 gameObject의 레이어 값을 넘겨줍니다.

이제 레이어를 수정할 시간입니다.

플레이어가 조종할 기체 오브젝트 레이어를 변경해줍니다.

아뇨, 이번에 자식 오브젝트는 변경하지 않습니다.


기체 모델링은 오브젝트 자식에 있으며, 충돌체는 모델링이 아닌 부모 오브젝트에 적용되어 있습니다.
지금 레이어를 변경한 오브젝트는 부모 오브젝트입니다.

지금은 가만히 있는 보스몹, 임시로 만든 타겟에는 Object 레이어를 적용시켜줍니다.




ParticleAutoDestroy.cs

public class ParticleAutoDestroy : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        float duration = GetComponent<ParticleSystem>().main.duration;
        Destroy(gameObject, duration);
    }
}

그리고 지금 파티클 재생이 끝날 때 사라지지 않고 있습니다.
재생 시간이 끝나면 사라지도록 코드를 작성했습니다.

이것도 오브젝트 풀링을 하면 좋겠지만, 일단 빠르게 구현해보기 위해 Destroy를 예약거는 방식으로 만들었습니다.



이제 미사일이 '적당히' 유도가 되는 것을 볼 수 있습니다.

목표물과 너무 가까이 있는 상태에서 미사일을 쏘면,
유도에 충분한 거리가 확보되지 못하게 되어서 빗나가게 됩니다.



현재 이 미사일의 선회력은 1.5입니다. 한 번 선회력을 높여보죠.

선회력을 2.5로 주었을 때, 가까이에 있는 물체도 유도가 되어서 맞게 됩니다.


미사일 추적 제한

지금 빗나간 미사일이 계속 돌아가는 현상이 있습니다.

현재 개발된 미사일이 얼마나 넓은 범위를 탐지하고 추적하는지는 저도 잘 모르지만,
대부분의 미사일은 탐지 범위가 있습니다. 그 범위를 벗어나면 추적이 끊기겠죠.


요즘은 빗나간 미사일도 다시 추적해서 맞출 수 있는 기능이 있다고는 하나,
고증을 너무 자세하게 따라가다가 게임이 재미없게 될 수 있으므로 적당히만 구현해봅시다.

상상해보세요. 여러분이 미사일을 피했는데 다시 돌아와서 날아오고 있습니다.


미사일 경보가 안 꺼진다니까요?



AIM-9X 미사일을 기준으로, 좌우 90도만큼의 탐지 범위를 가진다고 합니다.

특정 각도를 넘어서는 순간 target을 지워버리는 코드를 추가해봅시다.

target을 지우면 진행하던 방향으로 계속 나아갑니다.



Missile.cs

public float boresightAngle;

void LookAtTarget()
{
    if(target == null)
        return;

    Vector3 targetDir = target.position - transform.position;
    float angle = Vector3.Angle(targetDir, transform.forward);

    if(angle > boresightAngle)
    {
        target = null;
        return;
    }

    Quaternion lookRotation = Quaternion.LookRotation(targetDir);
    transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, turningForce * Time.deltaTime);
}

미사일 코드의 LookAtTarget() 함수에 각도를 계산하는 기능을 추가했습니다.

일단 target == null이면 더 이상 미사일을 회전시키지 않도록 제한을 걸어뒀습니다.

Vector3.Angle을 이용해서 각도를 계산하고,
그 각도가 boresightAngle보다 크면, targetnull로 두고 회전을 중지합니다.


다시 미사일 프리팹으로 들어가서 Boresight Angle 값을 추가합니다.



이제 목표물이 미사일의 탐지 범위를 벗어나게 되면 미사일은 더 이상 유도되지 않습니다.



이렇게 탐지 범위를 벗어나서 target을 상실하게 될 때마다 UI에 이렇게 띄워주면 됩니다.




마무리

오브젝트 풀링도 안 되어있고, 이펙트도 아직 미흡한 등 고쳐야 할 부분이 많습니다.

언젠가 대규모로 수정해야 할 일이 있을 것 같네요.


다음에는 기총을 만들러 갑니다.
유도기능 같은 것도 없으니 분량이 짧았으면 좋겠네요.

뭐야 이거 스크롤이 왜 이렇게 길어



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

1개의 댓글

comment-user-thumbnail
2021년 4월 16일

wow 이것이 게임개발..

답글 달기