[유니티] #6 수박 게임 만들기 (코루틴, 랜덤 확률, Line Renderer)
초기화 문제 해결 (코루틴)
싱글톤 인스턴스를 캐싱해두기 위해 작업을 하던 중 NullReference 오류가 발생했다.
찾아본 결과 이유는 초기화 순서에 있었다.
유니티에서 게임 오브젝트의 흐름은 다음과 같은 순서로 실행된다.
- Awake() → OnEnable() → Start()
OnEnable()에서 랜덤한 값을 업데이트하는 로직을 작성했는데,
Start()에서 싱글톤 인스턴스를 초기화하는 코드를 작성했기에,
싱글톤이 아직 초기화되지 않는 상태에서 접근하려 하여 NullReference 오류가 발생한 것이다.
또한 Awake 메서드에서 매니저들을 초기화하면 싱글톤으로 인해 오류가 발생한다.
초기화 시점은 Awake 다음이어야 한다.
private void OnEnable()
{
StartCoroutine(WaitForManagers());
}
private IEnumerator WaitForManagers()
{
while (_saveManager == null || _fruitManager == null || _npcManager == null)
{
_saveManager = SaveManager.Instance;
_fruitManager = FruitManager.Instance;
_npcManager = NPCManager.Instance;
yield return null; // 1프레임 후
}
// 매니저 인스턴스가 초기화된 후에 과일 요청을 설정
SetupRequest();
}
- OnEnable()에서 함수를 바로 호출하는 대신, WaitForManagers() 코루틴을 시작한다.
- WaitForManagers()는 while 문을 사용하여 매니저가 null이 아닐 때까지 기다린다.
- yield return null; 을 사용하여 한 프레임 대기 후 다시 확인한다.
- 모든 매니저가 초기화되면 SetupRequest()를 호출한다.
이렇게 하니 매니저들이 초기화된 후에 세팅 함수가 호출될 수 있었다.
랜덤 확률
랜덤 확률로 보상을 다르게 주는 것을 어떻게 만들지 고민했다.
답은 Random.Value였다.
private Reward CalculateReward()
{
int goldAmount = _npcManager.completeCount;
int expAmount = _npcManager.completeCount;
if (isLuckyGoldUnlocked)
{
float randomChance = Random.value; // 0부터 1 사이의 값 반환
if (randomChance <= 0.01f) // 1% 확률로 10배 골드
{
goldAmount *= 10;
}
else if (randomChance <= 0.1f) // 10% 확률로 2배 골드
{
goldAmount *= 2;
}
}
return new Reward(goldAmount, expAmount);
}
Random.value는 0 ~ 1 사이의 값을 반환한다.
0.01f면 1%, 0.1f면 10% 확률을 의미한다.
이걸 이용해 업그레이드가 적용되었을 때만 보너스 보상을 받도록 설정했다.
추후에 골드 2배, 10배 이벤트 UI나 사운드를 추가하면 더 좋을 듯 하다
튜플 vs 구조체
보상 데이터 (goldAmount, expAmount)를 반환하는 함수가 필요했는데,
반환값이 2개라서 튜플이나 out을 사용할까 했다.
private (int, int) CalculateReward()
{
return (_npcManager.completeCount, _npcManager.completeCount);
}
튜플은 값을 반환받으면 reward.Item1, reward.Item2로 접근해야 해서 가독성이 떨어진다.
대신 구조체를 써봤다.
public class NPCRequest : MonoBehaviour
{
private readonly struct Reward
{
public int Gold { get; }
public int Exp { get; }
public Reward(int gold, int exp)
{
Gold = gold;
Exp = exp;
}
}
public void ClickRequest()
{
if (HasEnoughFruits(out var removeList))
{
...
// 골드, 경험치 증가
Reward reward = CalculateReward();
_saveManager.AddReward(reward.Gold, reward.Exp);
}
}
}
CalculateReward 함수에서 Reward 타입 변수를 반환하면,
클릭 이벤트 함수에서 Reward 변수로 받는 것이다.
Line Renderer
Line Renderer는 라인을 그리는 컴포넌트다.
과일이 떨어지는 경로를 시각적으로 보여주기 위해 사용했다.
점선 텍스처는 점선 이미지에서 선 하나만 잘라서 사용하니 자꾸 실선이 되어버려
공백을 추가해서 50x100 사이즈로 가져왔다.
텍스처의 Wrap Mode를 Repeat로 하여 점선이 반복되도록 설정하고,
Filter Mode를 Point로 해서 더 선명하게 보이도록 했다.
마지막으로 머티리얼에 이 텍스처를 넣고 Shader를 UI/Unlit/Transparent로 지정했다.
이렇게 텍스처를 머티리얼로 만들어야 Line Renderer에서 사용할 수 있다.
이제 오브젝트에 Line Renderer 컴포넌트를 추가했다.
Size는 2로 하고, Width는 얇게 0.1로 줄였다.
Alignment는 View로 하여 어느 시점에서도 똑같이 보이도록 했고,
Textur Mode를 Tile로 하여 점선의 점이 계속 반복되도록 했다.
Texture Scale도 X값을 1.5로 조정하여 점선이 좀 더 촘촘히 이어지도록 했고, Use World Space에 체크했다.
머티리얼은 아까 텍스쳐로 만들어둔 머티리얼을 넣으면 완성이다.
두 점의 위치(Position) 값은 Update 메서드에서 계속 변경된다.
[Header("GuideLine")]
private int fruitLayerMask;
[SerializeField] private Transform lineEndLocation;
[SerializeField] private GameObject guideLine;
private LineRenderer guideLineRenderer;
private void Start()
{
fruitLayerMask = LayerMask.GetMask("Fruit");
guideLineRenderer = guideLine.GetComponent<LineRenderer>();
guideLine.SetActive(false);
}
private void Update()
{
...
UpdateGuideLine(_fruitSpanwer.CurrentFruit.transform.position);
}
private void UpdateGuideLine(Vector3 startPos)
{
// 과일 스폰 지점에서 아래로 광선 발사
RaycastHit2D hit = Physics2D.Raycast(startPos, Vector2.down, Mathf.Infinity, fruitLayerMask);
Vector3 endPos;
if (hit.collider != null)
{
// 충돌 지점까지만 표시
endPos = new Vector3(startPos.x, hit.point.y, startPos.z);
}
else
{
// 충돌하는 과일이 없으면 원래대로 유지
endPos = new Vector3(startPos.x, lineEndLocation.position.y, startPos.z);
}
guideLineRenderer.SetPosition(0, startPos); // 시작점
guideLineRenderer.SetPosition(1, endPos); // 끝점
}
1. RaycastHit2D를 사용하여 아래 방향으로 광선을 발사한다.
2. 과일 레이어에 충돌하면 해당 지점까지 가이드 라인을 표시한다.
3. 충돌하는 과일이 없으면 미리 지정한 위치(EndPosition)까지 표시한다.
과일과 충돌하는 위치까지만 표시하는 선이 완성됐다.