크래프톤 정글 TIL : 1123

lazyArtisan·2024년 11월 23일
0

정글 TIL

목록 보기
146/147

📝 배운 것들


🏷️ Unity : Canvas 여러 개 쓸지말지

Canvas Rebuild 문제: 캔버스의 일부 요소가 변경되면 전체 캔버스가 다시 렌더링됩니다.

캔버스를 여러 개 사용하는 것이 적합한 경우:

  • UI 구조가 모듈화되어 있고, 각 모듈이 독립적으로 작동할 때.
  • 예: HUD, 메인 메뉴, 인벤토리 등.
  • 자주 업데이트되는 UI(예: 체력바, 점수 등)와 거의 변경되지 않는 UI(예: 배경 UI, 버튼 등)를 분리하고 싶을 때.
  • 성능 최적화가 중요한 경우.

하나의 캔버스를 사용하는 것이 적합한 경우:

  • UI가 상대적으로 간단하거나, 변경 빈도가 낮을 때.
  • 메모리 사용량을 줄이고 싶을 때.
  • UI 전환을 코드로 간단히 관리하고 싶을 때.

🏷️ Unity : 오브젝트 삭제 시 이벤트 리스너

Destroy()로 오브젝트가 삭제될 경우

  • 게임 오브젝트가 삭제되면, 해당 오브젝트의 모든 컴포넌트가 파괴됩니다.
  • 컴포넌트와 연결된 이벤트 리스너도 더 이상 동작하지 않습니다.
  • 하지만 이벤트를 등록한 대상이 다른 곳에서 해당 리스너를 참조하고 있다면, 메모리 릭이나 예외가 발생할 수 있습니다.

SetActive(false)로 비활성화된 경우

  • 게임 오브젝트가 비활성화되면, 컴포넌트와 관련된 MonoBehaviour 이벤트(Update, Start, etc.)는 중단되지만, 이벤트 자체는 유지됩니다.
  • UnityEvent, C# 이벤트(delegate) 등은 여전히 연결된 상태로 존재하며, 비활성화 여부와 상관없이 호출될 수 있습니다.


🎮 Frog on Lotus


기획

반투명한 회색 배경이 깔린 게임 시작 화면에서
Press Any Key to Start 떠있고 아무 키나 누르면 게임 시작
파리 잡아먹으면 점수가 올라감
시간이 다 지나면 게임 종료 창이 뜨고
역대 순위와 이번 판 점수, 게임 재시작 버튼이 뜬다.
역대 순위에 이번 판 점수가 올라갔다면 강조처리한다.

게임 매니저 만들기

싱글톤을 굳이 해야 하는건가?
캡슐화 사용 가능하고 유일성 보장되고 그런 건 알겠는데
굳이 이렇게 간단한 게임에서 필요한가? 라는 생각이 듬.
배우는 입장이니까 일단 간단하게 해보다가 이래서 싱글톤 써야되는구나~ 깨달으면 될듯?
그리고 일단 만드는게 급하니까 다 만든 후에 싱글톤에 대해 더 알아볼 것.

static 쓰자마자 ide에서 갈! 시전함.

public static class GameManager : MonoBehaviour
이거 이러면 안됨?

아쉽지만, static 클래스와 Unity의 MonoBehaviour는 서로 호환되지 않습니다. Unity에서 MonoBehaviour를 상속받는 클래스는 인스턴스화가 필수이기 때문에, static으로 정의된 클래스와 충돌이 발생합니다.

  1. MonoBehaviour는 Unity의 컴포넌트 시스템:

    • Unity의 MonoBehaviour는 씬에 배치된 GameObject에 붙여져야 동작합니다.
    • Unity 엔진은 MonoBehaviour 클래스의 인스턴스를 관리하며, 이를 통해 Start(), Update() 등 Unity 이벤트 루프를 호출합니다.
    • static 클래스는 인스턴스를 생성할 수 없으므로 MonoBehaviour와 호환되지 않습니다.
  2. static 클래스는 인스턴스화 불가능:

    • static 클래스는 설계상 인스턴스를 만들 수 없는 클래스입니다.
    • Unity는 컴포넌트를 GameObject에 추가할 때, 해당 클래스의 인스턴스를 생성합니다. 따라서 static 클래스를 MonoBehaviour로 사용할 수 없습니다.
특징싱글톤 (MonoBehaviour)정적 클래스 (static)
Unity 이벤트 사용가능 (Start(), Update(), etc.)불가능
GameObject와 연결반드시 GameObject에 붙어야 함GameObject와 독립적
인스턴스 관리씬 전환 시 유지 (DontDestroyOnLoad)클래스 자체가 전역적으로 사용됨
상태 관리캡슐화 가능 (인스턴스 변수로 관리)전역 변수로 상태 관리
추가 오버헤드약간 있음 (GameObject 생성)없음

굳이 싱글톤을 쓰는 이유가 있었다.
근데 그렇다고 하더라도 엄격하게 싱글톤을 구현하기보단
그냥 전역 상태 관리만 구현하면 되지 않을까

근데 monobehaviour 한다고 해도 싱글톤을 꼭 굳이 써야돼? 그냥 전역 접근 가능하게만 만들면 되는거 아님?

MonoBehaviour를 사용하면서도 반드시 싱글톤을 써야 하는 것은 아닙니다. 전역 접근만 필요하다면 싱글톤 없이도 충분히 구현할 수 있습니다. 하지만 싱글톤을 사용하는 이유는 유일한 인스턴스를 보장하면서 객체지향적인 설계와 캡슐화를 제공하기 위해서입니다.

using UnityEngine;

public class GameManager : MonoBehaviour
{
    public static GameManager Instance; // 전역 접근용 정적 변수

    private void Awake()
    {
        Instance = this; // 현재 인스턴스를 정적 변수에 할당
    }

    public int Score;

    public void AddScore(int value)
    {
        Score += value;
        Debug.Log("Score: " + Score);
    }
}

이렇게 만들면 될 거라고 한다.

초안

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    public static GameManager Instance; // 전역 접근용 정적 변수
    public int Score;
    [SerializeField] private GameObject InGameManager;
    [SerializeField] private GameObject EndGameManager;

    private void Awake()
    {
        Instance = this; // 현재 인스턴스를 정적 변수에 할당
    }

    public void GameStart()
    {
        Instantiate(InGameManager);
    }

    public void GameEnd()
    {
        Instantiate(EndGameManager);
    }
}

이런걸 게임 매니저라고 할 수 있는지 모르겠지만 아무튼 만들었다
각 오브젝트가 게임의 상태를 대표함.

근데 이렇게 하면 굳이 메모리에 올렸다가 내렸다가 하는 과정이 생겨서 좀 비효율적인듯.
만들고 나서 리팩토링 생각해보자.

버그 발생 : nullref

public class InGameLogic : MonoBehaviour
{
    [SerializeField] private GameObject InGameUI;
    [SerializeField] private GameObject TimeTextObj;
    private TextMeshPro TimeText;
    [SerializeField] private GameObject ScoreTextObj;
    private TextMeshPro ScoreText;
    private float time;

    // Start is called before the first frame update
    void Awake()
    {
        TimeText = TimeTextObj.GetComponent<TextMeshPro>();
        ScoreText = ScoreTextObj.GetComponent<TextMeshPro>();
        InGameUI.SetActive(false);
    }

    void OnEnable()
    {
        time = 60f;
        Debug.Log("InGameLogic :: Game Start!");
        InGameUI.SetActive(true);
        TimeText.text = "Time: " + time.ToString("F2");
        ScoreText.text = "Score: " + GameManager.Instance.Score.ToString();
    }

    void Update()
    {
        // 시간이 0이 되면 게임 종료
        time -= Time.deltaTime;
        if (time < 0)
        {
            InGameUI.SetActive(false);
            GameManager.Instance.GameEnd();
            Destroy(gameObject);
        }

        TimeText.text = "Time: " + time.ToString("F2");
        ScoreText.text = "Score: " + GameManager.Instance.Score.ToString();
    }
}

TimeText.text = "Time: " + time.ToString("F2"); 여기에서 마구마구 에러가 발생함.

알고보니 스크립트에선 프리팹을 생성하는 것을 상정하고 짰는데
등록한 건 씬에 시작부터 활성화돼있는 프리팹도 아닌 그냥 오브젝트

프리팹으로 만드니 당연하게도 씬에 있는 게임 오브젝트 할당한 것들이 풀렸다.

처음부터 다시 짜야하는 건가 패닉이 왔지만, 생각해보니 UI를 각 상태 매니저 밑으로 달아주면
프리팹 안에 있는 객체들이니까 할당 가능했다. 휴~

근데 아직도 nullref 뜸.

private TextMeshPro TimeText; 이 부분이 문제였다.
private TextMeshProUGUI myText;로 선언해야 되는데 자동완성 뜬 거 그냥 써버림.

https://higatsuryu9975.tistory.com/32

사용법 찾으면서 이런 팁도 얻음

    void Awake()
    {
        time = 60f;
        Debug.Log("InGameLogic :: Game Start!");
        TimeText.text = "Time\n" + time.ToString("F1");
        ScoreText.text = "Score\n" + GameManager.Instance.Score.ToString();
    }

근데 여전히 ScoreText.text = "Score\n" + GameManager.Instance.Score.ToString(); 여기서 오류 발생.

파리 하나 잡아먹으니까 이상한 문자 나옴.

현재 클릭 했을 때 게임 시작이 아니라 테스트를 위해 바로 시작해놓은 상태라
awake에서 GameManager 불러오면 GameManager가 초기화되기도 전에 불러오려고 함.
그래서 Awake가 아니라 Start로 바꿔줬더니 오류 해결.

완성

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    public static GameManager Instance; // 전역 접근용 정적 변수
    public int Score;
    [SerializeField] private GameObject InGameManager;
    private InGameLogic inGameLogic;
    [SerializeField] private GameObject EndGameManager;
    private EndGameLogic endGameLogic;

    private void Awake()
    {
        Instance = this; // 현재 인스턴스를 정적 변수에 할당
        Score = 0;
        GameStart();
    }

    public void GameStart()
    {
        Debug.Log("GameManager :: Game Start!");
        Score = 0;
        Instantiate(InGameManager);
    }

    public void AddScore()
    {
        Score += 1;
    }

    public void GameEnd()
    {
        Debug.Log("GameManager :: Game End!");
        Instantiate(EndGameManager);
    }
}

이 GameManager.cs가 InGameManager와 EndGameManager를 하위 객체로 갖고 있는 느낌으로 만들었다

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class InGameLogic : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI TimeText;
    [SerializeField] private TextMeshProUGUI ScoreText;
    [SerializeField] private float time;

    void Start()
    {
        Debug.Log("InGameLogic :: Game Start!");
        TimeText.text = "Time\n" + time.ToString("F1");
        ScoreText.text = "Score\n" + GameManager.Instance.Score.ToString();
    }

    void Update()
    {
        // 시간이 0이 되면 게임 종료
        time -= Time.deltaTime;
        if (time < 0)
        {
            GameManager.Instance.GameEnd();
            Destroy(gameObject);
        }

        TimeText.text = "Time\n" + time.ToString("F1");
        ScoreText.text = "Score\n" + GameManager.Instance.Score.ToString();
    }
}

InGameManager에 달려있는 InGameLogic.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class EndGameLogic : MonoBehaviour
{
    [SerializeField] private GameObject RestartButtonObj;
    [SerializeField] private GameObject ExitButtonObj;
    private Button RestartButton;
    private Button ExitButton;

    void Awake()
    {
        RestartButton = RestartButtonObj.GetComponent<Button>();
        ExitButton = ExitButtonObj.GetComponent<Button>();

        RestartButton.onClick.AddListener(Restart);
        ExitButton.onClick.AddListener(Exit);
    }

    private void Restart()
    {
        GameManager.Instance.GameStart();
        Destroy(gameObject);
    }

    private void Exit()
    {
#if UNITY_EDITOR
        UnityEditor.EditorApplication.isPlaying = false;
#else
            Application.Quit();
#endif
    }

}

EndGameManager에 달려있는 EndGameLogic.cs

버그 발생 : 크기 0인 콜라이더에 파리가 와서 죽는다

https://discussions.unity.com/t/why-does-a-collider-with-0-scale-still-take-collisions/912856/2

검색했더니 바로 답을 찾음.
근데 뭔가 해결책이 있는 건 아니고 원래 그런거라고 함.

콜라이더나 혀를 아예 비활성화시켜야할듯?
콜라이더 비활성화 시키는게 정확하게 내가 원하는거니까
괜히 혀 자체를 건드려서 코드 꼬일 가능성 만들지는 말자

아니 겨우 코드 한 줄 바꾼건데 왜 갑자기 할당 안됐다고 뭐라 하는거임

그것은 초기화 코드를 내가 지워버렸기 때문이었고요

이제 혀 안 내밀면 부딪히지 않는다

처음 시작할 때 Press Any Key

public class PressAnyKeyStart : MonoBehaviour
{
    void Update()
    {
        if (Input.anyKeyDown)
        {
            GameManager.Instance.GameStart();
            Destroy(gameObject);
        }
    }
}

StartGameUI에 이 스크립트 넣었더니 끝.

인게임 로직 만들기

난이도 따라 파리 숫자와 속도 조절

FrogAttack.cs에서 GameManager.cs의 AddScore()을 호출하면
그 안에서 InGameManager.cs의 Balancing()을 호출하고
거기서 점수가 올라갈수록 난이도 높아지게 조절

    private Vector2 GetRandomSpawnPosition()
    {
        // 뚫린 영역을 제외한 네 구역 정의
        Vector2[] corners = new Vector2[4]
        {
        new Vector2(Random.Range(outerBounds.min.x, holeBounds.min.x), Random.Range(outerBounds.min.y, outerBounds.max.y)), // 좌측
        new Vector2(Random.Range(holeBounds.max.x, outerBounds.max.x), Random.Range(outerBounds.min.y, outerBounds.max.y)), // 우측
        new Vector2(Random.Range(holeBounds.min.x, holeBounds.max.x), Random.Range(outerBounds.min.y, holeBounds.min.y)), // 아래
        new Vector2(Random.Range(holeBounds.min.x, holeBounds.max.x), Random.Range(holeBounds.max.y, outerBounds.max.y)) // 위
        };

        // 랜덤하게 하나의 구역에서 위치 선택
        return corners[Random.Range(0, corners.Length)];
    }

gpt 도움으로 아무렇지도 않게 짤 수 있는 말도 안되게 귀찮지만 성능은 좋은 함수를 사용

근데 이제 hole과 outer 영역을 알게 하는 방법이 필요
1. 좌표들을 하드코딩한다
2. Find 써버린다
3. Area들을 Tag로 정해놓고 Tag로 찾는다

어차피 파리도 프리팹화 하느라 flying area를 모르는데, 이거 등록도 할 겸
hole area랑 outer area도 자식으로 보내버림

아 파리는 원래 SpawnArea = GameObject.FindWithTag("SpawnArea"); 이걸로 퉁쳐버렸었구나
이건 그냥 두자

난이도는 GameManager에 전역 변수로 두고
시그모이드 함수로 어떻게 어떻게 해서 파리 속도 결정했는데
중앙에 오질 않음;

특단의 조치로 했던거 전부 discard 후 다시 시작
쓰레기 같은 코드들이었어서 다시 짜긴 했어야 함
차라리 잘됨

이동 로직이 잘못됐었나? 하고 다시 테스트해봤는데 정상임
밖에 있어도 중앙으로 다시 오는게 맞음
난이도 기반 수식이 뭔가 잘못됐었던 거임

일단 되게 간단하게 짜보자
값 이리 갔다가 저리 갔다가 하지 말고
필요한 곳에서 바로 계산하고
이해도 제대로 안한 시그모이드 코드 복붙하지 말고
속도는 로그 함수, 개수는 upper limit 있는 선형 함수로

아 일단 파리 새로 생성시키는 로직만 다시 만들어보고 테스트 했는데
중앙으로 가버림

flyArea을 애초에 spawnArea라는 이전 이름으로 찾고 있었는데다,
나중에 instantiate된 오브젝트의 자식으로 생성될 예정이라 생성돼있지도 않는 친구를 검색해서 그랬음

일단 flyArea는 프리팹 밖으로 뺌

    private void SetDifficulty()
    {
        difficulty = 1 + (int)Mathf.Log(Score, 2f);
        Debug.Log("GameManager :: difficulty : " + difficulty);
    }

아니 이게 뭐가 문젠데 오버 플로우 발생하는거임

0 넣으면 오류 발생한다고 해서 1 더해줌
difficulty = 1 + (int)Mathf.Log(1 + Score, 2f);

        // 중심에 대해 원운동 시키기
        fall_v = (Center - transform.position).normalized;
        moving_v = new Vector3(-fall_v.y, fall_v.x) * speed;
        fall_v *= grav;
        transform.position += (moving_v + fall_v) * Time.deltaTime;

아 아까 왜 오류 났는지 알아버림
grav는 speed의 2배가 되면 적절한 비율임을 테스트를 통해 알아냈었음
그래서 이번에 고칠 때 speed만 difficulty 기반으로 재설정해주고
grav도 2배 해주면 될 거라고 생각했는데 (말하고 보니까 더 말이 안되네)

grav는 speed의 2배가 '돼야' 하는거지
speed의 2배로 설정돼있는 값이 아니었음.
거기에 2배 하면 될거라는 건 또 무슨 생각이었는지...

아 제대로 했는데도 왜 그러나 했는데
2배가 아니라 1/2배였음
바보ㅄ

거기에 speed를 +- 랜덤 변환해주는 코드가 있었어서
grav를 그냥 speed/2로 바꾸면 안되고 절대값 씌워줘야 됨

        // 중심에 대해 원운동 시키기
        fall_v = (Center - transform.position).normalized;
        moving_v = new Vector3(-fall_v.y, fall_v.x) * speed;
        fall_v *= Mathf.Abs(speed / 2);

정상적으로 궤도 운동 시작

완성

difficulty = 1 + (int)Mathf.Log(1 + Score, 2f);

speed = 5 * GameManager.Instance.difficulty;

moving_v = new Vector3(-fall_v.y, fall_v.x) * speed;
fall_v *= Mathf.Abs(speed / 2);

    public void NewFly()
    {
        while (GameManager.Instance.flyCount < GameManager.Instance.difficulty)
        {
            GameManager.Instance.flyCount += 1;
            Instantiate(fly, GetRandomSpawnPosition(), Quaternion.identity);
        }
    }

대충 이런 느낌

문제

  • 첫 파리가 랜덤 스폰용 area를 할당 받지 못해서 0,0에서 스폰됨
  • 파리를 한 마리 잡을때마다 (log 함수의 특성상) 조금씩 난이도가 상승하는게 아니라 갑자기 확 상승
  • 시간이 지날수록 인간의 에임으로 잡는게 불가능한 속도로 움직여서 한 곳만 빠르게 클릭하는게 맞출 확률이 더 높아짐

버그는 금방 고쳤고, 난이도는 내일 폴리싱할 때 마저 생각해보면 될듯.
이제 랭킹 시스템만 구현하면 게임의 구색은 갖추게 됨.

Bug : 첫 파리 스폰 area 할당 못 받음

GameManager에서 ingamemanager.newfly() 하던걸 ingamemanager에서 newfly했더니 해결

폴리싱

랭킹 시스템 만들기 전에 폴리싱 작업

mp3는 앞뒤에 약간 공백 생겨서 루프할 때 어색하다고 함
wav로 내보내기 했더니 자연스럽게 편집했던 그대로 재생됨

개구리 공격할 때 울음소리 너무 크고 째져서 음량 줄이고 자연스럽게 만드느라 힘들었

하이 스코어

    void Awake()
    {
        RestartButton = RestartButtonObj.GetComponent<Button>();
        ExitButton = ExitButtonObj.GetComponent<Button>();

        RestartButton.onClick.AddListener(Restart);
        ExitButton.onClick.AddListener(Exit);

        YourScore.text = "Your Score : " + GameManager.Instance.Score.ToString();
        HighScore.text = "High Score : " + PlayerPrefs.GetInt("HighScore", 0).ToString();

        if (GameManager.Instance.Score > PlayerPrefs.GetInt("HighScore", 0))
        {
            PlayerPrefs.SetInt("HighScore", GameManager.Instance.Score);
            NewHighScore.SetActive(true);
            SoundManager.Instance.PlayNewHighScore();
        }
        else
        {
            GameOver.SetActive(true);
            SoundManager.Instance.PlayGameOver();
        }
    }

EndGameManager.cs에 살짝만 추가.
랭킹까지는 굳이...
넣으면 오히려 게임의 간결함을 해침.

webgl 버전으로 빌드하면 어케 되냐고 물어봤더니
PlayerPrefs.Save(); 이거 쓰면 된다고 함
그리고 playerprefs는 브라우저의 로컬 스토리지에 저장된다고 함

press any key에 효과

public class WaveEffect : MonoBehaviour
{
    public TextMeshProUGUI tmpText;
    public float scaleSpeed = 1f;
    public float scaleAmount = 1.2f;

    private Vector3 originalScale;

    private void Start()
    {
        tmpText = GetComponent<TextMeshProUGUI>();
        originalScale = tmpText.transform.localScale;
    }

    private void Update()
    {
        float scale = 1 + Mathf.Sin(Time.time * scaleSpeed) * (scaleAmount - 1);
        tmpText.transform.localScale = originalScale * scale;
    }
}

gpt한테 예시 읊어보라고 하고 그 중에 마음에 드는거 씀

폴리싱 마무리

new high score면 글자 커졌다가 작아졌다가

스크립트를 다른 스크립트에서 어떤 오브젝트에 동적으로 붙이는 방법

// MyScript를 동적으로 추가
MyScript newScript = targetObject.AddComponent<MyScript>();

스크립트가 그냥 monobehaviour만 상속받으면 static 아니어도 바로 addcomponent하면 띡 달리는거야?

네, MonoBehaviour를 상속받는 스크립트는 AddComponent<T>()를 통해 런타임에서 GameObject에 바로 추가할 수 있습니다. 이 스크립트가 반드시 static일 필요는 없으며, 일반적인 MonoBehaviour 클래스로 작성되었다면 Unity가 내부적으로 관리하여 컴포넌트를 GameObject에 붙입니다.

YourScore.gameObject.AddComponent<TextScaleEffect>();

잘 붙는다

개구리 위아래로 흔들흔들

using System.Collections;
using UnityEngine;

public class FrogMovement : MonoBehaviour
{
    private float moveAmount = 0.05f; // 위아래로 움직일 거리
    private float duration = 1f; // 움직이는 주기 (1초)
    private Vector3 initialPosition; // 초기 위치 저장

    void Start()
    {
        initialPosition = transform.position; // 초기 위치 저장
        StartCoroutine(MoveUpDown());
    }

    private IEnumerator MoveUpDown()
    {
        while (true)
        {
            // 아래로 이동
            yield return MoveToPosition(initialPosition - new Vector3(0, moveAmount, 0));

            // 위로 이동
            yield return MoveToPosition(initialPosition);
        }
    }

    private IEnumerator MoveToPosition(Vector3 targetPosition)
    {
        Vector3 startPosition = transform.position;
        float elapsed = 0;

        while (elapsed < duration)
        {
            elapsed += Time.deltaTime;
            transform.position = Vector3.Lerp(startPosition, targetPosition, elapsed / duration);
            yield return null;
        }

        transform.position = targetPosition; // 정확한 위치로 고정
    }
}

코루틴 한 번도 안 써봐서 무지성 gpt 썼다
내일 무슨 코드인지 확인하고 이해해보기

마우스 커서 바꾸기

https://velog.io/@slow_cosmos/Unity-%EB%B2%84%ED%8A%BC-%EB%A7%88%EC%9A%B0%EC%8A%A4-%EC%BB%A4%EC%84%9C-%EB%B3%80%EA%B2%BD

완성

일단 만족

0개의 댓글