Canvas Rebuild 문제: 캔버스의 일부 요소가 변경되면 전체 캔버스가 다시 렌더링됩니다.
캔버스를 여러 개 사용하는 것이 적합한 경우:
하나의 캔버스를 사용하는 것이 적합한 경우:
Destroy()로 오브젝트가 삭제될 경우
SetActive(false)로 비활성화된 경우
반투명한 회색 배경이 깔린 게임 시작 화면에서
Press Any Key to Start 떠있고 아무 키나 누르면 게임 시작
파리 잡아먹으면 점수가 올라감
시간이 다 지나면 게임 종료 창이 뜨고
역대 순위와 이번 판 점수, 게임 재시작 버튼이 뜬다.
역대 순위에 이번 판 점수가 올라갔다면 강조처리한다.
싱글톤을 굳이 해야 하는건가?
캡슐화 사용 가능하고 유일성 보장되고 그런 건 알겠는데
굳이 이렇게 간단한 게임에서 필요한가? 라는 생각이 듬.
배우는 입장이니까 일단 간단하게 해보다가 이래서 싱글톤 써야되는구나~ 깨달으면 될듯?
그리고 일단 만드는게 급하니까 다 만든 후에 싱글톤에 대해 더 알아볼 것.
static 쓰자마자 ide에서 갈! 시전함.
public static class GameManager : MonoBehaviour
이거 이러면 안됨?
아쉽지만, static
클래스와 Unity의 MonoBehaviour
는 서로 호환되지 않습니다. Unity에서 MonoBehaviour
를 상속받는 클래스는 인스턴스화가 필수이기 때문에, static
으로 정의된 클래스와 충돌이 발생합니다.
MonoBehaviour
는 Unity의 컴포넌트 시스템:
MonoBehaviour
는 씬에 배치된 GameObject에 붙여져야 동작합니다.MonoBehaviour
클래스의 인스턴스를 관리하며, 이를 통해 Start()
, Update()
등 Unity 이벤트 루프를 호출합니다.static
클래스는 인스턴스를 생성할 수 없으므로 MonoBehaviour와 호환되지 않습니다.static
클래스는 인스턴스화 불가능:
static
클래스는 설계상 인스턴스를 만들 수 없는 클래스입니다.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);
}
}
이런걸 게임 매니저라고 할 수 있는지 모르겠지만 아무튼 만들었다
각 오브젝트가 게임의 상태를 대표함.
근데 이렇게 하면 굳이 메모리에 올렸다가 내렸다가 하는 과정이 생겨서 좀 비효율적인듯.
만들고 나서 리팩토링 생각해보자.
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
https://discussions.unity.com/t/why-does-a-collider-with-0-scale-still-take-collisions/912856/2
검색했더니 바로 답을 찾음.
근데 뭔가 해결책이 있는 건 아니고 원래 그런거라고 함.
콜라이더나 혀를 아예 비활성화시켜야할듯?
콜라이더 비활성화 시키는게 정확하게 내가 원하는거니까
괜히 혀 자체를 건드려서 코드 꼬일 가능성 만들지는 말자
아니 겨우 코드 한 줄 바꾼건데 왜 갑자기 할당 안됐다고 뭐라 하는거임
그것은 초기화 코드를 내가 지워버렸기 때문이었고요
이제 혀 안 내밀면 부딪히지 않는다
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);
}
}
대충 이런 느낌
문제
버그는 금방 고쳤고, 난이도는 내일 폴리싱할 때 마저 생각해보면 될듯.
이제 랭킹 시스템만 구현하면 게임의 구색은 갖추게 됨.
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는 브라우저의 로컬 스토리지에 저장된다고 함
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한테 예시 읊어보라고 하고 그 중에 마음에 드는거 씀
스크립트를 다른 스크립트에서 어떤 오브젝트에 동적으로 붙이는 방법
// 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 썼다
내일 무슨 코드인지 확인하고 이해해보기
일단 만족