게임 진행 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
'개발노트 > Taxi Game 3D' 카테고리의 다른 글
Devlog) Taxi Game 3D) 18) 서버 구현 3 (2) | 2024.01.17 |
---|---|
Devlog) Taxi Game 3D) 17) 손님 대화 구현 (0) | 2024.01.09 |
Devlog) Taxi Game 3D) 15) UI 최초 구현 (2) | 2023.12.29 |
Devlog) Taxi Game 3D) 14) ClientManager 적용 (2) | 2023.12.26 |
Devlog) Taxi Game 3D) 13) 서버, 클라이언트 통신 구현 (0) | 2023.12.22 |