728x90
SMALL

게임 진행 UI 구현

게임을 시작하면 지금 몇 스테이지를 플레이하고 있는지, 몇 명의 손님을 태웠는지, 몇 번째 손님을 태우고 있는지 등의 정보를 출력하는 UI를 구현하였습니다.


게임 진행상황을 UI에 전달할 수 있도록 GameLogic 클래스에 게임시작, 종료, 손님승차/하차 이벤트 추가하였습니다.

public event EventHandler GamePlayEvent;
public event EventHandler<int> CustomerTakeInEvent;
public event EventHandler<int> CustomerTakeOutEvent;
public event EventHandler<bool> GameEndedEvent;

 

StageProgressViewUI 클래스를 추가하여 위 그림과 같은 기능을 가진 UI를 구현하였습니다.

public class StageProgressViewUI : MonoBehaviour
{
    [Serializable]
    public class Part
    {
        [SerializeField]
        GameObject container;
        [SerializeField]
        Image stateImage;
        [SerializeField]
        Sprite nonSprite;
        [SerializeField]
        Sprite takeInSprite;
        [SerializeField]
        Sprite passSprite;

        public void SetVisible(bool visible) =>
            container.SetActive(visible);

        public void SetState(int state)
        {
            if (state == 1)
                stateImage.sprite = takeInSprite;
            else if (state == 2)
                stateImage.sprite = passSprite;
            else
                stateImage.sprite = nonSprite;
        }
    }

    [SerializeField]
    LayoutGroup layoutGroup;
    [SerializeField]
    TMP_Text currentStageText;
    [SerializeField]
    Part[] parts;
    [SerializeField]
    Image nextStageImage;
    [SerializeField]
    Sprite nextStageNonSprite;
    [SerializeField]
    Sprite nextStagePassSprite;
    [SerializeField]
    TMP_Text nextStageText;

    void OnEnable()
    {
        StartCoroutine(UpdateLayoutGroup());
    }

    public void Init()
    {
        var stage = GameLogic.StageIndex + 1;
        var stageCount = ClientManager
            .Instance
            .TemplateService
            .StageTemplates
            .Count;
        var customerCount = GameLogic.Instance.CustomerCount;

        currentStageText.text = stage.ToString();

        for (int i = 0; i < parts.Length; i++)
        {
            parts[i].SetVisible(i < customerCount);
            parts[i].SetState(0);
        }
        nextStageText.text = Math.Min(stage + 1, stageCount).ToString();

        if (gameObject.activeInHierarchy)
            StartCoroutine(UpdateLayoutGroup());

        GameLogic.Instance.CustomerTakeInEvent += (sender, customerNumber) =>
        {
            parts[customerNumber - 1].SetState(1);
        };
        GameLogic.Instance.CustomerTakeOutEvent += (sender, customerNumber) =>
        {
            parts[customerNumber - 1].SetState(2);
        };
        GameLogic.Instance.GameEndedEvent += (sender, isGoal) =>
        {
            if (isGoal)
                nextStageImage.sprite = nextStagePassSprite;
        };
    }

    IEnumerator UpdateLayoutGroup()
    {
        layoutGroup.enabled = true;
        yield return new WaitForEndOfFrame();
        layoutGroup.enabled = false;
    }
}

 

결과창 구현

 

결과창을 구현하기 전, 제가 참고하는 프로젝트에서 게임을 실패하여도 손님을 태운 만큼 보상을 받지만, 저는 실패하면 보상을 받지 않도록 구현하였습니다.
그래서 서버와 클라이언트에 있는 EndStage 메소드를 각각 수정하였습니다.

[HttpPut("EndStage")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult> EndStage([FromBody] EndStageRequest body)
{
    var userId = ClaimHelper.FindNameIdentifier(User);
    if (string.IsNullOrEmpty(userId))
        return Unauthorized();

    var user = await userRepository.Get(userId);
    if (user == null)
        return Forbid();

    var stageTemps = await templateService.GetStages();
    var stageCount = stageTemps?.Count ?? 0;
    var stageIndex = body.StageIndex;
    // 템플릿에 등록되지 않은 스테이지
    if (stageIndex < 0 || body.StageIndex >= stageCount)
        return BadRequest();

    // 현재 스테이지도 아직 클리어하지 않았을 경우
    if (stageIndex > user.CurrentStageIndex)
        return Forbid();

    // 클라이언트에서 계산한 요금이 서버에서 계산한 요금보다 더 큰 경우
    var maxCoin = stageTemps?[stageIndex]?.MaxCoin ?? 0;
    if (body.Coin >= maxCoin)
        return Forbid();

    if (body.IsGoal)
    {
        // 다음 스테이지 개방
        if (stageIndex == user.CurrentStageIndex && stageIndex < stageCount + 1)
            user.CurrentStageIndex = stageIndex + 1;
    }
    user.Coin += body.Coin;
    await userRepository.Update(user.Id!, user);

    return NoContent();
}
public async UniTask<HttpStatusCode> EndStage(int stageIndex, bool isGoal, long coin)
{
    var currStageIndex = User.CurrentStage.Index;
    var stageCount = templateService.StageTemplates.Count;

    if (stageIndex < 0 || stageIndex >= stageCount)
    {
        Debug.LogError($"End stage failed. Because {stageIndex} is out of range.");
        return HttpStatusCode.BadRequest;
    }

    if (stageIndex > currStageIndex)
    {
        Debug.LogWarning($"End stage({stageIndex}) failed. Because {User.CurrentStage.Index} was not clear.");
        return HttpStatusCode.Forbidden;
    }

    if (coin >= templateService.StageTemplates[stageIndex].MaxCoin)
    {
        Debug.LogError($"End stage failed. Because {coin} is too much.");
        return HttpStatusCode.Forbidden;
    }

    if (isGoal)
    {
        if (stageIndex == currStageIndex && stageIndex < stageCount - 1)
            User.CurrentStage = templateService.StageTemplates[stageIndex + 1];
    }
    User.Coin += coin;

    var res = await http.Put($"User/EndStage", new EndStageRequest
    {
        StageIndex = stageIndex,
        IsGoal = isGoal,
        Coin = coin
    });
    if (!res.IsSuccess())
    {
        Debug.LogWarning($"End stage failed. - {res}");
        UserUpdateFailed?.Invoke(this, EventArgs.Empty);
    }
    return res;
}

 

결과창은 돈을 얼마나 벌었는지, 게임을 성공하였다면 다음 스테이지로 이동하고, 실패하였다면 재시작하는 버튼 1개만 추가하여 간단하게 끝냈습니다.

 

ResultViewUI 클래스를 추가하여 기능을 구현하였습니다.

public class ReadyViewUI : MonoBehaviour
{
    [SerializeField]
    TMP_Text coinText;
    [SerializeField]
    Button playButton;
    [SerializeField]
    Button carListButton;

    void Awake()
    {
        UpdateCointext();
    }

    void Start()
    {
        playButton.onClick.AddListener(() =>
        {
            GameUI.Instance.ShowPlayView();
            GameLogic.Instance.PlayGame();
            gameObject.SetActive(false);
        });
        carListButton.onClick.AddListener(() =>
        {
            GameUI.Instance.ShowCarList();
        });
    }

    public void UpdateCointext()
    {
        coinText.text = ClientManager.Instance.UserService.User.Coin.ToString();
    }
}

 

가이드 UI 구현

 

플레이 버튼을 누르면 바로 게임을 시작하지 않고 게임 조작법을 먼저 출력하고, 화면을 터치하면 조작법 UI가 사라지면서 게임을 시작하도록 수정하였습니다.
제가 참고 하였던 프로젝트는 조작법을 출력하는 동안에 조작을 안 해도 자동차가 최소 속도로 이동하였습니다.
하지만 저는 개인적으로 버그처럼 보여서 저는 플레이 버튼을 누른 후 아무 조작도 하지 않으면 자동차가 출발하지 않도록 구현하였습니다.

 

2초 간격으로 텍스트를 강조하는 효과는 GuideViewUI 클래스를 추가하여 구현하였고, 손가락이 움직이는 효과는 직접 애니메이션을 만들었습니다.

public class GuideViewUI : MonoBehaviour, IPointerDownHandler
{
    [SerializeField]
    List<TMP_Text> texts;
    [SerializeField]
    Material normalMaterial;
    [SerializeField]
    Material highlightedMaterial;


    void OnEnable()
    {
        if (texts.Count == 0)
        {
            Debug.LogError("Not exist any texts.");
            return;
        }
        StartCoroutine(HighlightText(0));
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        gameObject.SetActive(false);
    }

    IEnumerator HighlightText(int index)
    {
        for (int i = 0; i < texts.Count; i++)
        {
            if (i == index)
            {
                texts[i].textStyle = texts[i].styleSheet.GetStyle("Guide Highlighted");
                texts[i].fontMaterial = highlightedMaterial;
            }
            else
            {
                texts[i].textStyle = texts[i].styleSheet.GetStyle("Normal");
                texts[i].fontMaterial = normalMaterial;
            }
        }
        yield return new WaitForSecondsRealtime(2f);
        if (gameObject.activeInHierarchy)
            StartCoroutine(HighlightText((index + 1) % texts.Count));
    }
}

 

버그 수정

 

UI를 만들고 게임을 플레이하였을 때 4번 맵에서 자동차가 거꾸로 생성되어 추락하는 버그를 발견하였습니다.

 

이 버그를 해결하기 위해 각도 등의 수치를 조절하거나 도로의 기울기를 조절하는 포인트를 이동시켜 보거나 처음부터 다시 이동경로를 배치해봤지만 해결하지 못 했습니다.
그러다 별 생각 없이 직선 도로에 포인트를 1개 더 추가하여 문제를 해결하였기 때문에 원인은 알 수 없습니다.

 

이동경로 문제를 해결하고, 다시 게임 테스트를 해보니 이번엔 일부 커브 구간에서 큰 자동차와 충돌하여 의도치 않게 게임오버되는 버그도 발견하였습니다.

 

이 버그는 자동차들의 사이즈를 줄여서 해결하였습니다.

 

원래 게임 플레이 중 출력되는 UI를 모두 구현한 후에 글을 게시하려고 하였지만 위에 언급한 버그 외에 TextMeshPro에서 제공하는 스타일 시트를 사용해보려다 원하는 결과가 안 나와 삽질을 하는데 시간을 소비하여 다 구현하지 않고 작업을 마무리 하였습니다.
다음에 손님을 타고, 내릴 때 메시지가 출력 되도록 구현한 후 플레이 UI 작업은 완료하겠습니다.

 

 

구현 결과

깃 허브 저장소 : taxi-game-3d-unity

728x90
LIST

+ Recent posts