탑건: 매버릭 미션 개발하기 #7 : 미션 스크립팅

Lunetis·2022년 8월 9일
0

탑건: 매버릭

목록 보기
8/14
post-thumbnail



경고: 영화 내용에 대한 스포일러가 존재합니다.




이제 영화의 미션을 직접 만들어볼 시간입니다.
게임적 허용을 위해 실제 영화 내용과는 약간 다릅니다...

  • 1대의 F/A-18
  • 협곡 사이를 저공비행, 비행기를 수직으로 세워서 통과해야 하는 다리 2개 존재
  • 제한 고도를 넘길 경우 미션 실패
  • 협곡 진입 후 폭격까지의 제한 시간 존재, 경과할 경우 미션 실패
  • 수동으로 레이저 유도 폭탄을 조준
  • 폭격 후 상승해서 빠져나오면 지대공미사일과 적 전투기 공격이 시작됨
  • 승리 조건: 폭격 성공, 적 전투기 섬멸

지금 당장 생각해놓은 미션 구성은 위와 같습니다.

여러분은 F/A-18 전투기를 몰고 협곡을 저공비행한 다음 저고도 폭격을 시도하고 빠져나와야 합니다.

  • 제한 고도를 넘길 경우 적에게 감지되었다는 이유로 미션 실패 처리합니다.
  • 제한 시간이 경과할 경우 폭격에 대한 적의 대비가 끝났다고 둘러대면서 미션 실패 처리합니다.

목표물 만들기

영화에서 나오는 목표물은 환풍구같이 생긴 구조물이고, 그 안쪽으로 폭탄을 투하했었습니다.

일반적인 환풍구처럼 위에서 아래로 뚫려 있었다면 아마 고고도 폭격으로 미친듯이 때려부었으면 되었을 테지만, 그렇지 않고 약간 비스듬하게 만들어져 있었기 때문에 폭격을 하기 위해서 충분히 내려가야 했었습니다.


일단 우리도 이거랑 비슷한 걸 만들어봅시다.


지난 시간에 다리를 만들 때 썼던 ProBuilder로 이렇게 생긴 구조물을 만들겠습니다.

안쪽에 있는 주황색 큐브는 Trigger 용으로 사용하려고 합니다.

레이저 유도 폭탄이 무언가에 부딪혀서 폭발하기 전에 저 주황색 큐브를 통과했다면 (OnTriggerEnter가 호출되었다면), 목표물에 명중한 것으로 판정하기 위해 사용할 예정입니다.
주황색 큐브를 통과하려면 무조건 저 구멍 속으로 들어가야 하기 때문에 아마 적절한 판정 기준이 되지 않을까 싶습니다.

BombingTarget

public class BombingTarget : MonoBehaviour
{
    private void OnTriggerEnter(Collider other) {
        LaserGuidedBomb lgb = other.GetComponent<LaserGuidedBomb>();

        if(lgb != null)
        {
            lgb.hit = true;
        }
    }
}

주황색 상자 오브젝트에 붙여질 스크립트입니다.

상자의 트리거 범위 내에 무언가 들어가면, 그것이 레이저 유도 폭탄인지 확인합니다.
맞다면 lgb.hittrue로 놓습니다.


Missile

public bool IsHit
{
    set { isHit = value; }
}

LaserGuidedBomb

protected override void DisableMissile()
{
    // Send Message to object that it is no more locked on
    if(isHit == false)
    {
        ShowMissedLabel();
    }
    else
    {
        if(gameObject.layer == LayerMask.NameToLayer("Player"))
        {
            GameManager.UIController.SetLabel(AlertUIController.LabelEnum.Destroyed);
        }
    }
    ...
}

미사일과 레이저 유도 폭탄의 스크립트를 조금 수정합니다.

Missile에는 미사일이 Target(목표물)에 적중했는지에 대한 여부를 가지는 isHit이라는 변수가 있습니다. 이 값은 LaserGuidedBomb에서도 사용할 수 있도록 protected로 선언되어 있기 때문에, public bool IsHit을 만들어서 BombingTarget에서 접근할 수 있도록 만들었습니다.

LaserGuidedBomb에서는 폭탄이 폭발하여 비활성화될 때 isHit 값에 따라 Missed / Destroyed 라벨을 보여주는 코드를 추가합니다. 이 코드는 Missile에도 비슷한 형태로 있지만, 실제로는 Missed 라벨만 미사일 측에서 보여주게 되어 있습니다.

미사일 명중 시 목표물의 남은 체력에 따라 Hit 또는 Destroyed 라벨을 보여줘야 하기 때문이며, 이 라벨을 표시하는 코드는 미사일이 아닌 목표물 코드(TargetObject)에 붙어있습니다.

하지만 레이저 유도 폭탄은 한 발만 투하하는데다가 명중 여부만 따지면 되기 때문에, Hit 없이 그냥 Missed / Destroyed 라벨을 폭탄 측 코드에서 출력하게끔 만들었습니다.

안쪽으로 들어가서 폭발하면 Destroyed,

그렇지 않으면 Missed가 출력됩니다.

레이저 유도 폭탄은 한 발밖에 없기 때문에, 빗나가면 미션 실패 처리됩니다.
이 부분은 조금 있다가 만들어보죠.


그나저나, 미사일도 아니고 엄연히 폭격용 폭탄인데 폭발 크기가 너무 작습니다.

상황에 따라 키워주는 코드를 추가해보겠습니다.

Missile

protected void Explode(float explosionScale = 1)
{
    // Instantiate in world space
    GameObject effect = GameManager.Instance.explosionEffectObjectPool.GetPooledObject();
    ...
    effect.transform.localScale *= explosionScale;
    ...
}

폭발 이펙트를 만드는 Explode() 함수에 폭발 이펙트 크기를 설정해주는 매개변수를 하나 추가합니다.

LaserGuidedBomb

void OnCollisionEnter(Collision other)
{
    Explode(20);
    DisableMissile();
}

20배로 키워봅시다.

좀 낫네요.



페이즈 1

시간 제한, 고도 제한 기능 추가

협곡에 진입한 이후로는 폭격 성공까지의 제한시간이 주어집니다.

주어진 시간 내에 폭탄이 목표물에 명중하지 않으면 미션 실패 처리됩니다.


먼저 플레이어가 협곡에 진입하는 것을 감지하고, 타이머를 작동시키는 코드를 작성합니다.

MissionMaverick

public class MissionMaverick : MissionManager
{
    enum CanyonStatus
    {
        NOT_ENTERED,
        ENTERED,
        LEAVED
    }
    
    public float canyonEnterPositionZ;
    public float canyonLeavePositionZ;
    public float warningAltitude;
    public float failAltitude;

    
    [SerializeField]
    AlertUIController alertUIController;

    [SerializeField]
    RedTimer redTimer;

    [SerializeField]
    int timeLimit;

    CanyonStatus canyonStatus;

    protected override void Start()
    {
        phase = 1;
        isInCanyon = false;
        base.Start();
    }

    void CheckAltitude(Vector3 playerPos)
    {
        if(playerPos.y > warningAltitude)
        {
            if(playerPos.y < failAltitude)
            {
                // Caution
                alertUIController.SetCautionUI(true);
            }
            else
            {
                // Fail
                GameManager.Instance.GameOver(false);
            }
        }
        else
        {
            alertUIController.SetCautionUI(false);
        }
    }


    void CheckPhase1()
    {
        Vector3 playerPos = GameManager.PlayerAircraft.transform.position;

        if(canyonStatus == CanyonStatus.NOT_ENTERED && canyonEnterPositionZ < playerPos.z)
        {
            canyonStatus = CanyonStatus.ENTERED;
            redTimer.RemainTime_RedTimerOnly = timeLimit;
            redTimer.gameObject.SetActive(true);
        }

        if(canyonStatus == CanyonStatus.ENTERED)
        {
            // Altitude Restriction
            if(playerPos.z < canyonLeavePositionZ)
            {
                CheckAltitude(playerPos);
            }
            else
            {
                canyonStatus = CanyonStatus.LEAVED;
                alertUIController.SetCautionUI(false);
                phase = 2;
            }
        }
    }

    void CheckPhase2()
    {
        redTimer.enabled = false;
    }


    void Update()
    {
        switch(phase)
        {
            case 1:
                CheckPhase1();
                break;

            case 2:
                CheckPhase2();
                break;
        }
    }
}

미션과 관련된 내용을 가지고 있는 MissionManager라는 클래스를 상속받는 MissionMaverick이라는 클래스를 생성합니다. 미션에 종속된 데이터 및 코드는 여기에 대부분 들어가게 될 것입니다.

시작 시에 phase는 1로 두고, (1페이즈) 협곡 입구에 해당하는 Z좌표를 플레이어가 넘어가게 되면 타이머를 작동시킵니다. 협곡 입구와 출구에 해당하는 Z좌표를 플레이어가 넘어가게 되면 CanyonStatus가 바뀌게 됩니다.

RedTimer는 이전에 만들어두었던 타이머 코드인데, RemainTime_RedTimerOnly라는 public setter를 이번에 새로 추가했습니다. 이전에 있던 RemainTime은 미션의 남은 시간까지 바꿔버리기 때문에, 그 시간은 내버려두고 협곡 돌파 및 폭격에 필요한 시간만 따로 조정할 수 있도록 변경했습니다.


CheckAltitude()는 협곡 내에 있을 때의 고도를 확인하는 함수입니다. warningAltitude를 넘어가면 경고, failAltitude를 넘어가면 미션 실패 처리됩니다.

AlertUIController

public void SetCautionUI(bool enable)
{
    if(caution.activeSelf == enable) return;
    
    caution.SetActive(enable);
    if(enable == true)
    {
        InvokeRepeating("BlinkAttackAlertUI", 0, warningBlinkTime);
        InvokeRepeating("PlayCautionVoiceAudio", 0, voiceAlertRepeatTime);
    }
    else
    {
        CancelInvoke("BlinkAttackAlertUI");
        CancelInvoke("PlayCautionVoiceAudio");
    }
}

void PlayCautionVoiceAudio()
{
    alertAudioSource.PlayOneShot(cautionVoiceAlertClip);
}

이전에는 존재하지 않았던 경고 표시 기능을 추가합니다.

경고가 활성화되면 경고 UI를 깜빡이는 것과 경고 음성을 출력하는 것을 반복하고, 비활성화되면 반복을 멈춥니다.


만들었던 미션 컴포넌트는 MissionManager에 붙여주고, GameManager에 이 컴포넌트를 등록합니다.

미션 스크립트에는 MissionInfo 타입의 ScriptableObject가 필요합니다.
이전 프로젝트에서 이 오브젝트를 찍어낼 수 있는 기능을 구현했으므로, 바로 하나 만들어줍니다.


원래 타이머를 멈추는 기준은 폭격이 성공할 때이지만, 협곡을 빠져나오는 것을 제대로 인식하는지 확인하기 위해 협곡에서 빠져나올 때 페이즈 2로 진입시켜서 타이머를 멈춰보겠습니다.

(페이즈 2는 폭격이 성공한 이후를 나타낼 예정입니다.)

협곡에 진입하면 (일정 Z좌표를 넘어가면) 타이머가 작동합니다.

고도가 600을 넘어가면 경고 UI가 깜빡이고 "Descend"라는 경고음이 반복적으로 출력됩니다.

  • "Descend"는 에이스 컴뱃 7에서 더미 데이터로 있었던 음성입니다.

고도가 700을 넘어가면 미션 실패 처리됩니다.

협곡을 빠져나오면 이렇게 타이머가 멈추고, 이렇게 고도를 올려도 미션 실패 처리되지 않습니다.


참, 제한 시간이 지나면 미션 실패입니다.



폭격 성공/실패 시 스크립트 추가

협곡에서 빠져나올 때 페이즈 2로 진입시키지 않고, 폭격에 성공했을 때 페이즈 2로 진입시키도록 수정하겠습니다. 그리고 폭격에 실패하면 미션 실패 처리를 시켜보죠.


LaserGuidedBomb

protected override void DisableMissile()
{
    SetMissionStatus(isHit);
    
    ...
}

void SetMissionStatus(bool hit)
{
    MissionMaverick mission = (MissionMaverick)GameManager.MissionManager;
    mission.CheckBombing(hit);
}

미션 스크립트에 접근해서 폭격 성공 여부를 알려주는 함수를 하나 작성합니다. 이 함수는 미사일이 폭발해서 비활성화될 때 호출됩니다.

MissionMaverick

public void CheckBombing(bool isHit)
{
    StopTimer();

    if(isHit == false)
    {
        GameManager.Instance.GameOver(false);
    }
    else
    {
    	phase = 2;
	}
}

void StopTimer()
{
    redTimer.enabled = false;

    Invoke("RemoveTimer", 3);
}

void RemoveTimer()
{
    redTimer.gameObject.SetActive(false);
}

미션 스크립트에서는 폭격에 성공하면 페이즈 2로 넘어가고, 실패하면 게임 오버를 출력시키도록 하겠습니다. 이전에 있었던 phase = 2; 코드를 삭제하고 폭격에 성공한 경우에만 페이즈 값을 올리도록 바꿔줍니다.

폭격에 성공하든 실패하든 일단 폭탄이 터지면 타이머를 멈추게 합니다. 그리고 타이머가 멈추고 3초 후에는 타이머 UI를 비활성화합니다.

이제는 협곡 부분에서 나와도 시간은 계속 흘러갑니다.

폭격에 성공하면 타이머가 멈춥니다.

폭격에 실패하거나,

이상한 곳에 폭탄은 투하해서 아무튼 목표물에 맞지 않을 때는 타이머가 멈추고 미션 실패 처리됩니다.



협곡 내 지대공 미사일 배치

제한 고도를 넘는 순간 미션 실패와 함께 수많은 미사일을 플레이어에게 날려보내려고 했었는데, 생각해보니 미션 실패 처리되는 순간 모든 타겟이 사라집니다. 플레이어도 적도 모두 공격을 하지 않는 상태가 되어버리도록 만들어놓았죠.

그리고 구현된 모습을 생각해봤을 때, 임무 실패와 관련된 대사를 읊는 것에 집중해야 할 텐데 그 동안 미사일 경보음이 모든 소리를 덮어버리는 것도 플레이어 입장에서 좋은 경험은 아닌 것 같습니다.

배치를 해놓긴 하겠지만, 협곡을 통과하는 과정에서 고도를 넘겨서 게임 오버되는 상황에서도 미사일이 발사되지는 않을 것입니다.

안 쓸텐데 그럼 왜 배치를 하냐고요?

폭격하고 돌아올 때 미사일들을 날려주려고요.



페이즈 2

지대공 미사일 활성화

폭격을 마친 후 기지에서 빠져나오는 순간 무수한 미사일 세례가 플레이어를 반겨줘야 합니다.

일단 꼭대기 쪽에 미사일 포대를 여러 대 배치해주겠습니다.

그런데 이 미사일 포대들은 플레이어가 폭격하러 내려가는 동안 발사되면 안 되니까, 처음에는 비활성화시키겠습니다.

MissionMaverick

[SerializeField]
GameObject[] SAMs;

List<EnemyWeaponController> SAMControllers;
List<SAM> SAMScripts;


protected override void Start()
{
    phase = 1;
    canyonStatus = CanyonStatus.NOT_ENTERED;

    SAMControllers = new List<EnemyWeaponController>(SAMs.Length);
    SAMScripts = new List<SAM>(SAMs.Length);

    foreach(var SAM in SAMs)
    {
        SAMControllers.Add(SAM.GetComponent<EnemyWeaponController>());
        SAMScripts.Add(SAM.GetComponent<SAM>());
    }

    SetSAMsActive(false);

    base.Start();
}

void SetSAMsActive(bool active)
{
    hasSAMsActivated = active;
    foreach(var controller in SAMControllers)
    {
        controller.enabled = active;
    }
    foreach(var script in SAMScripts)
    {
        script.enabled = active;
    }
}

void CheckPhase2()
{
    if(hasSAMsActivated == false && GameManager.PlayerAircraft.transform.position.y > phase2BombingLeaveAltitude)
    {
        SetSAMsActive(true);
    }
}

비활성화 조치가 필요한 지대공 미사일 포대들을 따로 들고 있게 한 다음에, 시작 시에 비활성화하고 나중에 활성화할 수 있도록 함수를 작성합니다.

폭격에 성공해서 페이즈 2에 진입하고, 분지를 빠져나오는 고도 이상으로 플레이어가 상승하면 다시 활성화시키겠습니다.

컴포넌트에 해당 지대공 미사일들을 등록합니다.

그리고 이 지대공 미사일들은 맵에서 가장 높은 위치에 있기 때문에, 플레이어가 지대공 미사일 포대보다 낮은 위치에 있어도 발사가 가능해야 합니다.

이 속성을 체크 해제하겠습니다.

일단 협곡을 빠져나와서 폭격하러 올라가는 동안에는 미사일을 발사하지 않습니다.

문제는 폭격한 다음이죠.

꼭대기에 있는 모든 지대공 미사일 포대가 공격하는 것은 물론,

협곡에 있었던 미사일 포대도 이제 플레이어에게 미사일을 날리러 옵니다.


근데 그 와중에 미사일 코드에 버그가 있는 것을 이제야 확인했습니다.

미사일의 목표물 탐지 각도를 계산할 때 항상 현재 목표물의 위치에 기반해서 계산해야 하는데,
미래의 위치를 예측해서 따라가는 미사일의 경우 미래 위치를 가지고 계산하느라 각도가 이상하게 계산되고 있었고, 그로 인해 발사 즉시 탐지 각도에서 벗어났다고 판단하여 목표물을 향해 날아가지 않는 경우가 있었습니다.

이전 프로젝트도 똑같은 코드를 사용하고 있는데다가 큰 이슈라서 이전 프로젝트에도 커밋을 해줬습니다.


적기 출현

폭격을 하고 빠져나오면 적기가 플레이어를 요격하러 올 것입니다.

적기의 비행 AI가 조금 나사빠져있긴 한데, 일단은 생성부터 시켜봅시다.

적기 출현과 위치 자체는 확정적이기 때문에, Instantiate로 생성하기보다는 비활성화된 채로 미리 만들어놓고 활성화시키는 방식이 나을 것 같다는 생각이 들었습니다.

이렇게 5기의 Su-57를 가져다 놓습니다.


MissionMaverick

[SerializeField]
GameObject[] enemyAircrafts;

int remainingEnemyAircraftCnt;

[SerializeField]
Transform firstEnemyWaypoint;

protected override void Start()
{
    ...
    
    SetEnemyAircraftsActive(false);
    remainingEnemyAircraftCnt = enemyAircrafts.Length;
}

void CheckPhase2()
{
    if(hasSAMsActivated == false && GameManager.PlayerAircraft.transform.position.y > phase2BombingLeaveAltitude)
    {
        SetSAMsActive(true);
        SetEnemyAircraftsActive(true);
        hasSAMsActivated = true;
    }
}

void SetEnemyAircraftsActive(bool active)
{
    foreach(var enemy in enemyAircrafts)
    {
        enemy.SetActive(active);
        enemy.GetComponent<EnemyAircraft>().ForceChangeWaypoint(firstEnemyWaypoint.position);
    }
}

public void DecreaseEnemyAircraftCnt()
{
    remainingEnemyAircraftCnt--;

    // Some additional voice comms can be added

    if(remainingEnemyAircraftCnt == 0)
    {
        GameManager.Instance.MissionAccomplish();
    }
}

게임 시작 시 지대공 미사일을 비활성화시킬 때 적기도 같이 비활성화시키고,
CheckPhase2()에서 지대공 미사일을 활성화시키는 부분에서 적기도 같이 활성화시킵니다.

적기를 격추할 때마다 DecreaseEnemyAircraftCnt()를 호출시키고, 0이 되면 미션 성공 함수를 호출합니다.

생성한 적기들을 여기에 등록시킵니다.


GameManager

public void MissionAccomplish()
{
    UIController.SetLabel(AlertUIController.LabelEnum.MissionAccomplished);

    audioController.TargetBGMVolume = AudioController.MIN_VOLUME;

    foreach(TargetObject obj in objects)
    {
        obj.DeleteMinimapSprite();
    }

    executeOnGameOver.Invoke();

    targetController.RemoveAllTargetUI();
    objects.Clear();
    weaponController.ChangeTarget();

    foreach(GameObject obj in disableOnGameOver)
    {
        obj.SetActive(false);
    }

    scriptManager.ClearScriptQueue(true);

    ResultData.elapsedTime += GameManager.UIController.StopCountAndGetElapsedTime();
    
    float gameOverFadeOutDelay = 5.0f;
    Invoke("GameOverFadeOut", gameOverFadeOutDelay);
}

근데 이제 보니 미션 성공 함수가 없었더군요.
게임 오버 함수를 참고해서 새로 만들었습니다.


TargetObject

using UnityEngine.Events;

public UnityEvent destroyActions;

protected void CommonDestroyFunction()
{
    ...
    
    destroyActions.Invoke();
}

목표물 공통 컴포넌트인 TargetObject에는 파괴 시 호출할 콜백 함수를 따로 등록하는 기능이 없었는데, 이번 기회에 추가합니다.

추가한 적기의 DestroyActions에 앞서 구현한 콜백을 등록하고 실행해봅시다.


이제 분지를 벗어나면 적기가 출현하게 됩니다.


반갑다고 미사일을 서로 날려주는 모습입니다.



어... 음...

그리고 예상한 상황이 벌어지고 있습니다.
전에 만들어둔 AI가 너무 멍청해서 땅에 처박히고 있어요.

어쨌든, 다 잡으면 "Mission Accomplished"가 뜨긴 합니다.



번외: 땅에 부딪히지 않는 비행 인공지능

비행기가 등장하는 게임에서 인공지능이 땅에 부딪히는 거 보신 적 있나요?

절대 있어서는 안 될 일인데 지금 일어나고 있다는 게 문제죠.
그 어떤 버그보다도 먼저 고쳐야 하는 이슈입니다.

이전 프로젝트의 맵은 비교적 평평해서 최소 비행 고도 제한을 적당히 걸어두면 문제가 생기지 않았지만, 이 맵은 지형의 고저차가 심하기 때문에 단순 고도 제한으로는 해결할 수 없습니다.


지금의 비행기 AI는 지면보다 위에 있는 공간 또는 플레이어를 향해 날아가도록 되어 있습니다.
설정되는 목적지 자체는 문제가 없습니다.

그렇지만 위와 같이 아무 생각없이 무턱대고 경로를 향해 비행하다가 땅에 충돌할 수 있는 상황이 발생할 수 있다는 것이죠.


그래서, 다음과 같은 기능을 하는 코드를 추가했습니다.

비행기가 목표 지점을 향해 비행하면서 비행기 정면에 땅이 있는지 확인합니다.
비행기의 앞쪽을 기준으로 200m 이내에 땅이 있으면, 비행기의 위쪽으로 목표 지점을 강제로 변경하고 선회력을 최대로 설정합니다.

지점이 강제로 변경된 이후에도 충돌 확인 과정은 계속됩니다. 200m 이내에 땅이 감지되지 않을 때까지 비행기의 위쪽으로 목표 지점이 강제로 설정됩니다.


AircraftAI

int layerMask;

protected override void Start()
{
    layerMask = 1 << LayerMask.NameToLayer("Ground");
    
    ...
}

protected virtual void Update()
{
    CheckGroundCollision();
    
    ...
}

void CheckGroundCollision()
{
    RaycastHit hit;
    Physics.Raycast(transform.position, transform.forward, out hit, 200, layerMask);
    if(0 < hit.distance)
    {
        ForceChangeWaypoint(transform.position + Vector3.up * 50);
        
        // Quick Turn
        currentTurningForce = 2.5f;
        turningTime = 1 / currentTurningForce;
        currentTurningTime = turningTime;
    }
}

CheckGroundCollision()에서는 매 프레임마다 비행기의 앞쪽으로 길이 200의 ray를 발사합니다. Ground 레이어에 해당하는 물체만 감지하며, ray에서 감지된 물체가 있다면 200만큼의 거리 이내에 충돌할 수 있는 지면이나 물체가 있다는 뜻이므로 위쪽으로 목적지를 변경합니다.

ForceChangeWaypoint()로 현재 위치로부터 50만큼 위쪽에 있는 위치로 목적지를 설정합니다. 그 밑에 있는 코드는 선회력을 강제로 설정하는 코드이며, 여기서 2.5f는 아무튼 큰 값이라고 알아두시면 됩니다.

비행기 인공지능 테스트 씬을 다시 꺼내서, 이번에 만든 맵으로 바꾼 후 협곡 내부에 AI를 놓고 실험해보겠습니다.

새로 작성한 지면 충돌 회피 기능을 비활성화시켰을 때는,

이렇게 땅에 냅다 꽂히는 상황이 발생합니다.

이제 기능을 활성화시켜보죠.


땅에 가까워지면 급속히 방향을 틀어버리는 모습을 확인할 수 있습니다.
지금 카메라를 강제로 고정시켜놓아서 이상하게 보일 수 있는데, 자유 시점 카메라에서 보면,

이렇게 비행하고 있습니다.

인공지능 테스트 기능 중 비행할 수 있는 지역 범위를 제한할 수 있는데, 이렇게 협곡 안쪽으로만 비행하도록 해보겠습니다.

그리고 지면으로부터의 목적지 높이를 50에서 200 사이로 설정해서 저공비행만 시켜보죠.

아무 문제 없이 비행하고 있습니다.

10배속으로 돌려도 여전히 비행하고 있고요.


이제 다시 원래 씬으로 돌아가서, 공중전을 하면서 적 비행기가 땅에 부딪히지 않는지 확인해봅시다.


5기를 모두 격추하는 동안 한 대의 전투기도 땅에 박히지 않았습니다.



마침내 미션의 뼈대가 만들어졌습니다.
다음에 구현할 것은 대사입니다. 다음 포스트에서는 조기경보기와 적군, 기타 인물들의 대사로 이 게임의 오디오를 채우겠습니다.

참고: 플레이어는 대사를 칠 수 없습니다.

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

1개의 댓글

comment-user-thumbnail
2022년 8월 11일

멋있네요:)
완성되면 플레이 영상 풀버전으로라도 한 번 보고싶네요 기대하겠습니다.

답글 달기