탑건: 매버릭 미션 개발하기 #9 : 대사 (2)

Lunetis·2022년 8월 13일
0

탑건: 매버릭

목록 보기
10/14
post-thumbnail

적군 대사 출력

이 부분은 2페이즈에서 등장하는 적군이 말할 대사입니다. 지난 포스트에서 다루지 않았었는데, 이 대사를 상황에 맞게 출력하도록 만드는 코드를 이전 프로젝트에서 구현해놓지 않았기 때문입니다.

_1, _2로 끝나는 대사는 게임 도중에 랜덤으로 출력되고, _X는 격추될 때 출력되는 대사입니다.


AITranscript

[RequireComponent(typeof(TargetObject))]
public class AITranscript : MonoBehaviour
{
    public float activateDistance;

    [Tooltip("Unit: Seconds, the random delay will be (delay * 0.5 - delay).")]
    public int maxScriptDelay = 60;
    public List<string> randomScriptList;

    public List<string> scriptOnDestroy;

    bool hasInvoked = false;


    // Start is called before the first frame update
    void Start()
    {
        if(activateDistance == 0)
        {
            hasInvoked = true;
            InvokePlayScriptRandomly();
        }
    }

    // Update is called once per frame
    void Update()
    {
        if(activateDistance == 0 || hasInvoked == true)
            return;

        if(Vector3.Distance(GameManager.PlayerAircraft.transform.position, transform.position)
           < activateDistance)
        {
            hasInvoked = true;
            InvokePlayScriptRandomly();
        }
    }

    public void PlayScriptOnDestroy()
    {
        CancelInvoke();
        GameManager.ScriptManager.AddScript(scriptOnDestroy);
    }

    void InvokePlayScriptRandomly()
    {
        float delay = Random.Range(maxScriptDelay * 0.5f, maxScriptDelay);
        Invoke("PlayScriptRandomly", delay);
    }

    void PlayScriptRandomly()
    {
        int index = UnityEngine.Random.Range(0, randomScriptList.Count);
        GameManager.ScriptManager.AddScript(randomScriptList[index]);
        randomScriptList.RemoveAt(index);

        if(randomScriptList.Count != 0)
        {
            InvokePlayScriptRandomly();
        }
    }
}

각 오브젝트마다 출력할 수 있는 대사의 개수는 여러 개가 있을 수 있습니다.

그런데 시작 즉시 모든 오브젝트들이 할당된 대사를 출력하는 건 이상하니까 딜레이를 주려고 합니다.
randomScriptList에 있는 대사는 미리 설정한 주기인 maxScriptDelay의 0.5배 - 1배 사이의 시간이 지날 때마다 하나씩 출력됩니다.


그리고 첫 대사를 출력하기까지의 조건으로 "플레이어가 일정 거리 미만으로 가까이 있는가?"가 있습니다.

이 맵은 엄청나게 넓습니다. 현재 전속력으로 플레이어가 날아가도 맵의 한 쪽 끝에서 다른 쪽 끝까지 가기에는 최소 2분이 걸립니다.
그런데 맵의 다른 쪽 끝에 있는 적군이 있는데, "넌 죽은 목숨이야!"라고 대사를 치는 상황을 생각해봅시다. 만나기까지 2분은 걸릴 녀석인데 벌써 대사를 말하기에는 너무 이른 타이밍 아닐까요?

그래서 일정 거리만큼 가까워져야만 대사를 출력하도록 Update() 내부에서 거리를 확인합니다. 만약 거리 상관없이 그냥 시작부터 대사를 말하기를 원한다면 거리 변수 값을 0으로 설정하면 됩니다.


scriptOnDestroy에 있는 대사는 문자 그대로 파괴될 때 출력됩니다. 그리고 Invoke()로 실행이 예약되어 있는 대사는 CancelInvoke()로 해제합니다.
이 부분은 Lifecycle 중 OnDestroy()에서 실행시키기는 너무 늦을 수 있으니, 피격되어서 파괴 판정이 날 때 실행시키려고 합니다.

[RequireComponent(typeof(TargetObject))]를 사용해서 이 스크립트가 붙는 객체는 꼭 TargetObject 또는 그 자식 컴포넌트를 가지도록 강제한 후, TargetObject에 있는 파괴 함수에서 호출하도록 만들겠습니다.


TargetObject

AITranscript aiTranscript;

protected virtual void Start()
{
    aiTranscript = GetComponent<AITranscript>();
    ....
}

protected void CommonDestroyFunction()
{
    ...
    
    aiTranscript?.PlayScriptOnDestroy();
    destroyActions.Invoke();
}

TargetObject에서는 AITranscript 컴포넌트를 미리 받아오고, 파괴될 때 실행되는 CommonDestroyFunction()에서는 파괴 시 대사를 출력하는 함수를 호출합니다.
사실 밑에 있는 destroyActions.Invoke()PlayScriptOnDestroy()를 끌어다 놓아도 됩니다. 그렇지만 일일이 끌어다 놓기는 또 귀찮잖아요. 알아서 컴포넌트 있는지 파악하고 실행시킵시다.


여기서 조금 더 나아가면 미사일 발사 시, 미사일 피격 시 등등 온갖 대사 데이터를 집어넣을 수 있습니다만, 일단은 여기까지만 구현하고 테스트를 진행하겠습니다.

적 비행기에 컴포넌트를 추가하고 대사 키값을 입력합니다.


자동으로 출력되어야 하는 대사도 잘 나오고 있고,

파괴 시 대사도 출력되고 있습니다.



적 무기 발사 시 음성 추가

여러 번 테스트를 거친 결과, 장기전으로 가면 할 말을 모두 소진해버리는 상황이 발생합니다.


어쩔 수 없죠. 발사할 때 외치는 대사도 넣읍시다.
영화같은 데에서 발사할 때마다 "Fox two!", "Fow one!"이라고 외치는 대사 있잖아요.

Fox는 "Foxtrot"의 줄임말로, 화기(Firearms)의 앞글자인 'F'의 포네틱 코드에서 따왔습니다. NATO 소속 파일럿이 공대공 미사일을 발사할 때 말하는 신호로, 아군 오사 방지를 위해 자신이 미사일을 발사한다는 것을 주변 파일럿에게 알려주기 위해 말합니다.

  • Fox one: 반능동형 레이더 유도(Semi-active Radar Guided) 미사일을 발사할 때 사용합니다.
  • Fox two: 적외선 미사일을 발사할 때 사용합니다. 가장 보편적인 공대공 추적 방식이며, 이 게임에서 사용되는 AIM-9 사이드와인더도 여기에 해당합니다.
  • Fox three: 수동 레이더(Active Radar Guided) 유도 미사일을 발사할 때 사용합니다.

AITranscript

[Range(0.2f, 1)]
public float scriptOnFirePlayProb = 0.2f; 
public List<string> scriptOnFire;

public void PlayScriptOnFire()
{
    if(scriptOnFire == null || scriptOnFire.Count == 0)
        return;
            
    float value = Random.Range(0f, 1f);
    if(value < scriptOnFirePlayProb)
    {
        GameManager.ScriptManager.AddScriptRandomly(scriptOnFire);
    }
}

발사할 때마다 진짜로 외쳐대다가는 귀가 아프니까, 대사를 외칠 확률을 설정합시다. 0.2에서 1 사이로 조정할 수 있고, 대사 외치기에 당첨되면 등록된 대사 중 하나를 랜덤으로 출력합니다.

EnemyWeaponController

AITranscript aiTranscript;

void Start()
{
    ...
    aiTranscript = GetComponent<AITranscript>();
}

void LaunchMissile()
{
    ...
    aiTranscript?.PlayScriptOnFire();
}

호출은 EnemyWeaponController에서 담당합니다. 미사일을 발사할 때마다 해당 함수를 호출합니다.


어... 그런데 이걸 추가하려면 관련 데이터들도 모조리 추가해야 되네요...
그렇지만 귀가 즐거운 게 더 중요합니다.


음성 파일과 대사와 자막 데이터를 모두 추가했습니다.

똑같은 단어나 문장만 나올까봐 같은 문장에도 일부러 번역을 다르게 했습니다.

이제 데이터를 모두 넣고 조정해준 다음 실행해봅니다.


말이 너무 많네요.
확률을 0.2로 내려도 여전히 말이 많았습니다. 특단의 조치를 취해야겠네요.

ScriptManager

public bool IsPrintingScript
{
    get { return isPrintingScript; }
}

ScriptManager에는 현재 무언가가 출력 중인 대사가 있거나, 출력 대기중인 대사가 있는지에 대한 여부를 가지는 isPrintingScript라는 변수가 있습니다. 이걸 외부에서 참조 가능하도록 만들고,

AITranscript

public void PlayScriptOnFire()
{
    if(GameManager.ScriptManager.IsPrintingScript == true)
        return;
    ...
}

IsPrintingScript가 true라면 발사 관련 대사는 출력하지 않도록 만들어줍시다.

이제 좀 조용하네요. 기존에 출력하던 대사랑도 적당히 엮이고요.


조금 더 세심하게 만들면, 한 번 출력한 발사 대사는 다음 발사 시에는 출력시키지 않게 한다든가, 남아있는 적군이 줄어들수록 더 자주 대사를 출력한다든가 할 수 있겠습니다.

하지만 중요한 게 여전히 많이 남았으니 여기까지만 다룹시다.


잡담 출력하기

페이즈 1에서 협곡 안으로 들어갔을 때 약간의 잡담을 넣어주려고 합니다.
잡담 시점을 랜덤으로 출력하기 위해 스크립트를 조금 손보겠습니다.

MissionMaverick

[SerializeField]
List<string> scriptsOnCanyon;

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

    if(canyonStatus == CanyonStatus.NOT_ENTERED && canyonEnterPositionZ < playerPos.z)
    {
        ...

        Invoke("PlayScriptsOnCanyon", Random.Range(20, 100));
    }
    ...
}

void PlayScriptsOnCanyon()
{
    AddScript(scriptsOnCanyon);
}

협곡에 최초 진입 시 호출되는 코드가 있는데, 여기다가 잡담을 끼워넣는 스크립트를 추가하겠습니다.

협곡 진입 후 20초 - 100초 사이 언젠가 출력될 겁니다.

<< 시간 없으니까 수동으로 하자고. 3, 4, 5단계는 넘어가. >>
<< 진정해. 여길 파괴하기에는 충분하지 않을 테니까. >>

네, 언젠가는 나옵니다.



폭탄 투하 시 대사 출력

플레이어가 레이저 유도 폭탄을 투하할 때의 대사도 준비했습니다. 이것도 출력시킵시다.


LaserGuidanceBomb

[SerializeField]
List<string> scriptsOnLaunch;

public override void Launch(Vector3 guidedPosition, float launchSpeed, int layer, GameObject launcher)
{
    ...
    GameManager.ScriptManager.AddScriptRandomly(scriptsOnLaunch);
}

폭탄이 투하될 때 호출되는 함수에 코드를 끼워넣습니다.
준비된 폭탄은 하나밖에 없기 때문에, 이 대사는 게임 도중 한 번만 출력될 겁니다.


이 컴포넌트는 폭탄 프리팹에 달려 있었죠. 여기다 작성해둡니다.


폭탄 투하 시에 정해진 대사 중 하나를 출력하는 모습입니다.



미션 갱신 효과 추가

협곡에서 빠져나온 후, 등장하는 적기를 모두 격추하라는 새로운 명령이 하달됩니다.
게임 도중 미션 목표가 바뀔 때 사용하는 "MISSION UPDATED"라는 UI가 있지만, 아직 사용하고 있지 않습니다.
특정 대사가 끝나면 이 UI를 보여주도록 만들려 합니다.


미션 대사 데이터를 담는 json 파일에서 작성할 수 있는 속성 중 "invokeMethodName"이라는 게 있습니다. 여기에 미션 스크립트 내의 함수 이름을 등록하면, 이 대사가 출력될 때 지정된 함수를 실행할 수 있습니다. 추가로 "invokeMethodDelay"라는 속성을 활용해서 함수 호출에 딜레이도 걸 수 있고요.

특정 대사 출력과 동시에 실행되어야 하는 함수들을 위해서 이전 프로젝트에서 만들어놓은 기능입니다. 저에게는 다 계획이 있습니다.


MissionMaverick

void UpdateMission()
{
    GameManager.UIController.SetLabel(AlertUIController.LabelEnum.MissionUpdated);
}

페이즈 2 6번째 대사가 출력될 때 동시에 UpdateMission()이라는 함수를 호출시킬 겁니다. 이 함수는 그냥 UI를 띄워주는 역할만 합니다.

아주 간단하게 UI를 추가했습니다.



게임 종료 시 페이드 아웃

추락/격추 또는 조건 미달성으로 인한 미션 실패, 그리고 미션 성공 시 대사가 출력된 이후 화면이 페이드 아웃됩니다. 추락/격추 시에는 미리 설정된 페이드 아웃 딜레이가 있는데, 그 외에는 대사가 끝난 후에 페이드 아웃 되어야 합니다.

여기까지 구현이 완료되면 대사 관련 작업은 끝나게 됩니다.


GameManager

public void GameOverFadeOut()
{
    CancelInvoke();

    fadeController.OnFadeOutComplete.AddListener(ShowGameOverUI);
    fadeController.FadeOut(FadeController.FadeInReserveType.FadeIn);
}

public void MissionAccomplishedFadeOut()
{
    CancelInvoke();

    fadeController.OnFadeOutComplete.AddListener(ShowResultScene);
    fadeController.FadeOut(FadeController.FadeInReserveType.FadeIn);
}

게임 오버 시에 페이드아웃 시키는 GameOverFadeOut() 함수는 이미 있었는데 public이 아니었고, 미션 성공 시에 페이드아웃 시키는 함수는 아예 존재하지 않았었습니다. 이건 이전 프로젝트에서 컷씬 후에 바로 암전되고 결과 화면으로 보여줬기 때문에 사용할 일이 없긴 했습니다.

아무튼, 제대로 만들어줍시다.

MissionMaverick

void FadeOut()
{
    if(GameManager.Instance.IsGameOver == true)
    {
        GameManager.Instance.GameOverFadeOut();
    }
    else
    {
        GameManager.Instance.MissionAccomplishedFadeOut();
    }
}

void GameOver()
{
    GameManager.Instance.GameOver(false, false, false);
}

미션 스크립트에서 호출할 수 있는 FadeOut() 함수는 GameManager에서 들고 있는 게임 오버 여부 값에 따라서 두 페이드 아웃 함수 중 하나를 실행합니다.

그냥 GameManager에서 위처럼 바꾸지 왜 함수를 더 만들고 분기를 여기서 태우냐면, 이전 프로젝트와의 호환을 위해서입니다. 지금 여기서 신나게 바꿔도 이전에 만든 프로젝트는 아직 남아있는 상태라서, 그것도 잘 돌아가야 하거든요.

그리고 게임 오버 라벨을 출력해주기 위한 함수도 따로 빼내겠습니다.


다시 json 파일을 수정합니다.

눈치가 극도로 빠른 분들은 눈치를 채셨을 수도 있는데, subtitleKey에 X가 들어가는 대사 데이터는 미션 실패 시에 출력되는 대사들입니다. 여러 대사가 출력될 때는 마지막으로 출력되는 대사가 끝나면 페이드 아웃 되도록invokeMethodName에 "FadeOut"을 쓰고, 이 대사가 출력되는 것과 동시에 페이드 아웃되면 안 되니 음성 파일 길이만큼 기다리도록 invokeMethodDelay를 추가합니다.

M_AC_n은 미션 성공 시 출력되는 대사입니다. 이 때도 추가하죠.


...이제 모든 실패 경우에 대해서 테스트해봐야 합니다.

  1. 추락 또는 격추

  1. 제한고도 이상으로 상승

  1. 제한시간 경과

  1. 폭격 실패

  1. 미션 성공



플레이 영상

대사까지 구현된 모습의 플레이 영상입니다.

배경음악을 뭘 넣을지 결정하지 못해서 아예 안 넣었는데 많이 심심하네요.


버그를 수정하고 적당히 갈고 닦은 후에 제대로 넣어볼까 합니다.

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

구색을 어느 정도 갖췄으니 이제 저장소를 private에서 public으로 전환해도 될 것 같습니다.
마음껏 뜯어보세요.

0개의 댓글