
Stage는 어느 정도 구현해놨는데, Score도 올라가게 구현해보자. Score은 Knife가 Target에 OnHit 될 때 1 증가하도록 해주자.
Events에 OnHitTarget 델리게이트를 하나 새로 만들어주고 해당 이벤트가 실행될 때 기존의 OnHit과 ScoreUpdate가 실행되도록 수정해주었다.
private void OnEnable() {
if (Instance == null) {
Instance = this;
DontDestroyOnLoad(this);
}
else {
Destroy(gameObject);
}
Events.OnStartStage += OnStartStage;
Events.OnAllKnivesOnHit += OnAllKnivesOnHit;
Events.OnHitTarget += UpdateScore;
}
private void OnDisable() {
Events.OnStartStage -= OnStartStage;
Events.OnAllKnivesOnHit -= OnAllKnivesOnHit;
Events.OnHitTarget -= UpdateScore;
}
public void UpdateScore() {
scoreText.text = GameManager.Instance.ScoreNum.ToString();
}
public void Start() {
ScoreNum = 0;
stageNum = 1;
UIManager.Instance.Initialize();
StartStage();
}
private void OnTriggerEnter2D(Collider2D collision) {
gameObject.GetComponent<TrailRenderer>().enabled = false;
Events.OnTouchScreen -= FireKnife;
if (collision.gameObject.CompareTag("Target") && !hasInteracted) {
hasInteracted = true;
rb.velocity = Vector3.zero;
transform.position = new Vector3(0f, collision.gameObject.transform.position.y - knifeOffsetY, 0f);
transform.SetParent(collision.transform, true);
particle.Play();
GameManager.Instance.ScoreNum++;
Events.OnHitTarget.Invoke();
if ( GameManager.Instance.RemainKnives > 0) {
GameManager.Instance.SpawnKnife();
}
}
//생략
}
private void OnEnable() {
Events.OnAllKnivesOnHit += DestroyTarget;
Events.OnHitTarget += OnHitTarget;
}
private void OnDisable() {
Events.OnAllKnivesOnHit -= DestroyTarget;
Events.OnHitTarget -= OnHitTarget;
}
[Button("OnHitTarget")]
public void OnHitTarget() {
transform.DOPunchPosition(Vector3.right * shakeStrength, shakeDuration, vibrato, randomness);
FlashWhiteRenderer.DOFade(1f, flashDuration / 2)
.OnComplete(() => FlashWhiteRenderer.DOFade(0f, flashDuration /2));
if (GameManager.Instance.RemainKnives == 0) {
Events.OnAllKnivesOnHit.Invoke();
}
UIManager.Instance.UpdateScore();
}
중요한 점은 ScoreUpdate를 OnHitTarget 동일한 델리게이트 객체가 참조하고 있기 때문에 원래는 등록된 순서로 실행되는데, 이러면 UIManager와 GameManager의 Start 문의 실행 순서까지 봐야하기 때문에, 그냥 Invoke 하기 전에 Score를 1 증가 시켜주도록 별도로 작성해주었다.

점수와 스테이지가 잘 증가하는 것을 확인할 수 있다.
이제 게임 오버를 구현해보자. 게임 오버시에 할 일은 간단하다.
이 두가지가 전부인데 일단 UI 먼저 만들어보자. 일단 실제 인게임에서 GameOver 창을 확인해보자.

점수와 스테이지가 나오면서 최고 점수이면 NEW BEST를 추가로 보여준다. 그 다음 인게임 재화인 사과를 더 얻는 광고 버튼과, RESTART 버튼이 있다. 하단 메뉴는 기존 TitleScene에서의 하단 메뉴와 동일하므로 나중에 구현해보자.
일단 인게임 재화인 사과를 구현하지 않아서 사과를 더 받는 광고 버튼이 아니라 다시 한 번 살아날 수 있는 Continue 버튼을 추가하는 것으로 대체했다.

그럴싸해 보인다! 이제 GameOver 로직을 구현해보자.
public void OnGameOver() {
CanvasGroup canvasGroup = gameOverUI.GetComponent<CanvasGroup>();
canvasGroup.alpha = 0f;
gameoverStageText.text = GameManager.Instance.stageNum.ToString();
gameoverScoreText.text = GameManager.Instance.scoreNum.ToString();
gameOverUI.SetActive(true);
canvasGroup.DOFade(1f, gameOverFadeDuration);
}
이렇게 했는데 canvasGroup에 DOFade가 작동하지 않는다.. 원인을 찾아보자.
-> 찾앗는데, Target.cs에서 DOTween KillAll을 해버려서 UI의 DOTween도 모두 중지되어버렸다.
public void OnGameOver() {
StopCoroutine(rotateCoroutine);
rotateTween?.Kill();
DOTween.Sequence()
.AppendInterval(1f)
.OnComplete(() => {
DOTween.Kill(gameObject);
Destroy(gameObject);
});
}
그 다음에 Restart에는 간단한 다시 시작을 넣어줬다.
public void RestartButton() {
CanvasGroup canvasGroup = gameOverUI.GetComponent<CanvasGroup>();
canvasGroup.DOFade(0f, gameOverFadeDuration);
gameOverUI.SetActive(false);
Events.OnRestartButton.Invoke();
}
Continue 버튼은 추후에 Google Ads나 Unity Ads를 넣어보자.

잘 작동한다, 이제 HOME 화면을 돌아가면?

최고 스테이지, 점수도 잘 적용된 모습이다.
참고로 점수의 저장은 아래와 같이 처리했다.
public void OnGameOver() {
if (scoreNum > bestScoreNum) {
PlayerPrefs.SetInt("BestScore", scoreNum);
PlayerPrefs.SetInt("BestStage", stageNum);
Events.OnNewBestScore?.Invoke();
}
}
PlayerPrefs원래 자체적으로 간단한 데이터만 저장할 용도이기 때문에 보안성이 굉장히 취약하다. 실제 게임을 만들 때 점수나 인게임 재화는 데이터 위조나 변조를 방지하기 위해 PlayerPrefs 보다는 다른 별도의 데이터로 저장하고 관리하는게 좋다.
TitleScene에서 MainScene, MainScene에서 TitleScne으로 LoadScene할 때 뚝뚝 끊기는 느낌이 있는 것을 중간에 SceneFader라는 연결부를 만들어줘서 해결할 수 있다.
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class SceneFader : MonoBehaviour
{
public Image img;
public AnimationCurve curve;
private void Start() {
StartCoroutine(FadeIn());
}
public void FadeTo(string scene) {
StartCoroutine(FadeOut(scene));
}
IEnumerator FadeIn() {
float t = 1f;
while (t > 0f) {
t -= Time.deltaTime;
float a = curve.Evaluate(t);
img.color = new Color(0f, 0f, 0f, a);
yield return 0;
}
}
IEnumerator FadeOut(string scene) //FadeIn 반대로 설정
{
float t = 0f;
while (t < 1f)
{
t += Time.deltaTime;
float a = curve.Evaluate(t);
img.color = new Color(0f, 0f, 0f, a);
yield return 0;
}
SceneManager.LoadScene(scene);
}
}
흠... DOTween을 사용해보자.
using DG.Tweening;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class SceneFader : MonoBehaviour
{
public Image img;
private void Start() {
img.DOFade(0f, 0.5f);
}
public void FadeTo(string scene) {
img.DOFade(1f, 0.5f)
.OnComplete(() => {
DOTween.Kill(gameObject);
SceneManager.LoadScene(scene);
}
);
}
}
여기에 싱글톤을 살짝 섞으면 어디서든 FadeTo를 사용할 수 있다.
public class SceneFader : MonoBehaviour
{
public static SceneFader Instance;
private void OnEnable() {
if (Instance == null) {
Instance = this;
}
else {
Destroy(gameObject);
}
}
public Image img;
public float fadeDuration = 0.25f;
private void Start() {
img.DOFade(0f, fadeDuration);
}
public void FadeTo(string scene) {
img.DOFade(1f, 0.25f)
.OnComplete(() => {
DOTween.Kill(gameObject);
SceneManager.LoadScene(scene);
}
);
}
}
Animation Curve 넣고 싶으면 Ease를 적용시켜주면 되는데 Fade 시간도 짧은데 굳이 넣을까 싶어서 제외했다. 이제 각 Scene마다 SceneManager를 넣어주자.

중요한건 image의 Raycast Target을 체크 해제해주어야 한다.

그래야 다른 UI 버튼이나 화면 클릭이 작동한다. 이제 LoadScene 해주는 FadeTo로 Scene 전환을 해준다.
public class PlayButton : MonoBehaviour
{
public TitleAnimation titleAnimation;
public void Play() {
titleAnimation.StopTitleAnimation();
SceneFader.Instance.FadeTo("MainScene");
}
}
생각해보니깐 DontDestroyOnLoad로 해서 별도의 Start 실행이 안되서 따로 FadeIn도 실행해줘야된다. 최종적으로 완성된 SceneFader는 아래와 같다.
public class SceneFader : MonoBehaviour
{
public static SceneFader Instance;
private void OnEnable() {
if (Instance == null) {
Instance = this;
DontDestroyOnLoad(this);
}
else {
Destroy(gameObject);
}
}
public Image img;
public float fadeDuration = 0.25f;
private void Start() {
img = GetComponentInChildren<Image>();
FadeIn();
}
public void FadeIn() {
img.DOFade(0f, fadeDuration);
}
public void FadeTo(string scene) {
img.DOFade(1f, 0.25f)
.OnComplete(() => {
DOTween.Kill(gameObject);
SceneManager.LoadScene(scene);
FadeIn();
}
);
}
}
이런 방식의 SceneFader 말고 로딩창 자체는 DontDestroyOnLoad로 로딩창을 구현해놓고 맵 전환시에 해당 로딩창을 띄어놓고 다음 씬을 로드해오는 방식도 있다.
아래와 같이 완성됐다.
