왜.... 오셨나요?

부끄러워요

유니티개발 TIL

5주차 2일 TIL_유니티 입문(유사 2D메타버스 만들기_플래피버드, UI작업)

와피했는데 2025. 7. 29. 09:50

들어가기 앞서

두번째 추가될 게임은 플래피 버드이다 떨어지는 플레이어를 점프로 살려서 장애물에 안부딪히고 멀리가면 되는게임다

그리고 게임마다 UI가 필요하다 각 게임마다 UI를 소개하겠다


프로젝트 구현 단계


<플래피 버드>

● Rigidbody2d.gravityScale을 주어 지속적인 중력 이용

점프의 갯수 제한없음

일정 속도로 앞으로가는 로직 필요

 

=>플레이어 점프 컨트롤

  [Header("플레이어 Flap 세팅")]
  [SerializeField] float jumpPower;
  [SerializeField] float flapForce;
  [SerializeField] float forwardSpeed;

  private Rigidbody2D rd;
  bool isFlap = false;
  bool isDead = false;
  public bool playerDead  { get => isDead; set => isDead = value; } 

  private void Awake()
  {
      rd = GetComponent<Rigidbody2D>();
  }

  private void FixedUpdate()
  {
      if (isDead == true) return;
      MoveFowerd();
  }

  void OnJump()
  {
      Jump(jumpPower);
      isFlap = true;
  }


  void MoveFowerd()
  {
          Vector3 vector3 = rd.velocity;
          vector3.x = forwardSpeed;
          if (isFlap == true)
          {
              vector3.y = flapForce;
              isFlap = false;
          }
          rd.velocity = vector3;
  }

  void Jump(float jumpPower)
  {
      if (isDead == true) 
          return;
      else
      rd.AddForce(new Vector3(0.0f, jumpPower, 0.0f), ForceMode2D.Impulse);
  }

● 일정속도 하면 역시 .velocity 만한게 없다

 

=>장애물

public class Obstacle : MonoBehaviour
{
    [SerializeField] float highPosY = 1f;
    [SerializeField] float lowPosY = -1f;

    [SerializeField] float holeSizeMin = 1f;
    [SerializeField] float holeSizeMax = 3f;

    [SerializeField] Transform topOdj;
    [SerializeField] Transform bottomOdj;

    [SerializeField] float widthPadding = 4f;

    public Vector3 SetRandomPlace(Vector3 lastPosition, int obstaclCount)
    {
        float holeSize = Random.Range(holeSizeMin, holeSizeMax);
        float halfHoleSize = holeSize / 2;

        topOdj.localPosition = new Vector3(0, halfHoleSize);
        bottomOdj.localPosition = new Vector3(0, -halfHoleSize);

        Vector3 placePosition = lastPosition + new Vector3(widthPadding, 0);
        placePosition.y = Random.Range(lowPosY, highPosY);

        transform.position = placePosition;

        return placePosition;
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            GameManager.Data.BirdAddScore(1);
        }
    }
}
구조 설명
1. Y축으로 정해둔 상하양선으로 움직임
2. 플레이어 감지하는 Tirgger Collider가 감지하면 점수 +1

 

태그는 사진처럼 따로 설정해둘 필요가 있음

 

맵의 배경, 위아래 벽 또한 별도로 태그 설정을 해둘 필요가 있음

 

=> 플래피버드 핵심 루프기능

카메라에 자식오브젝트는 배경과 장애물을 캐릭터 뒤로가면 다시 카메라 넘어서 캐릭터 앞에 배치해주는 기능이 있음

// 해당 클래스가 켜지면 장애물 세팅
private void OnEnable()
 {
     obstacleLastPosition = Vector3.zero;
     Obstacle[] obstacles = GameObject.FindObjectsOfType<Obstacle>();
     obstacleLastPosition = obstacles[0].transform.position;
     obstacleCount = obstacles.Length;

     for (int i = 0; i < obstacleCount; i++)
     {
         obstacleLastPosition = obstacles[i].SetRandomPlace(obstacleLastPosition, obstacleCount);
     }
 }
 
 // 뒤로간 배경과 장애물 앞으로 다시 배치
     private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("BackGround"))
        {
            float widthOfBgObject = ((BoxCollider2D)collision).size.x;
            Vector3 pos = collision.transform.position;

            pos.x += widthOfBgObject+25;
            collision.transform.position = pos;
            return;
        }

        Obstacle obstacles = collision.gameObject.GetComponent<Obstacle>();
        if (obstacles)
        {
            obstacleLastPosition = obstacles.SetRandomPlace(obstacleLastPosition, obstacleCount);
        }
    }


<UI>

● 전체 UI관리하는 매니저 필요

UI 용도별 분류하여 부모클래스 생성

UI의 이벤트속성은 코드변환 오브젝트 재생성시 대입된 값이 Missing되는 사고가 있어 Prefab으로 만들어줘야함
(Resource매니저 이용)

 

=>UI Prefab화

 

=>UIManager

private Canvas windowCanvas;
     //아래 다른 UI 추가

 private void Awake()
 {
     popUpCanvas = GameManager.Resource.Instantiate<Canvas>("Prefabs/UI/Canvas");
     popUpCanvas.gameObject.name = "PopUpCanvas";
     popUpCanvas.sortingOrder = 100;
     //아래 다른 UI 추가
    	
}

  public void Recreated()
  {
      popUpCanvas = GameManager.Resource.Instantiate<Canvas>("Prefabs/UI/Canvas");
      popUpCanvas.gameObject.name = "PopUpCanvas";
      popUpCanvas.sortingOrder = 100;
  }
  
    public void Clear()
  {
      popUpStack.Clear();
      GameManager.Resource.Destroy(windowCanvas);
  }
  
// 함수 오버로딩으로 다양한 방식으로 불러오도록함
    public T ShowWindowUI<T>(T windowUI) where T : WindowUI
  {
      T ui = GameManager.Pool.GetUI(windowUI);
      ui.transform.SetParent(windowCanvas.transform, false);
      return ui;
  }

  public T ShowWindowUI<T>(string path) where T : WindowUI
  {
      T ui = GameManager.Resource.Load<T>(path);
      return ShowWindowUI(ui);
  }

// UI 닫기
  public void CloseWindowUI(WindowUI windowUI)
  {
      GameManager.Pool.ReleaseUI(windowUI.gameObject);
  }

 

=>UI부모 클래스

public class BaseUI : MonoBehaviour
{
    // UI 컴포넌트들 불러오기 
    protected Dictionary<string, RectTransform> transforms;
    protected Dictionary<string, Button> buttons;
    protected Dictionary<string, TMP_Text> texts;
    protected virtual void Awake()
    {
        BindChildren();
    }
    
	// 자식오브젝트 확인
    private void BindChildren()
    {
        transforms = new Dictionary<string, RectTransform>();
        buttons = new Dictionary<string, Button>();
        texts = new Dictionary<string, TMP_Text>();

        RectTransform[] children = GetComponentsInChildren<RectTransform>();
        foreach (RectTransform child in children)
        {
            string key = child.gameObject.name;

            if (transforms.ContainsKey(key))
                continue;

            transforms.Add(key, child);


            Button button = child.GetComponent<Button>();
            if (button != null)
                buttons.Add(key, button);

            TMP_Text text = child.GetComponent<TMP_Text>();
            if (text != null)
                texts.Add(key, text);
        }
    }
}

 

=>UI자식 클래스

// 분류용 클래스(사용하면 안됨)
public class WindowUI : BaseUI
{
    protected override void Awake()
    {
        base.Awake();
    }
}
public class FlappyBirdGameInfo : WindowUI
{
    GameObject player;
    GameObject jumpStartPoint;
    
    // 스트링값으로 자식오브젝트에 있는 버튼오브젝트 찾음
    protected override void Awake()
    {
        base.Awake();
        buttons["StartButton"].onClick.AddListener(() => { FlappyBirdSelect(); });
        buttons["CancelButton"].onClick.AddListener(() => { Back(); });
    }

    private void Start()
    {
        player = GameObject.FindGameObjectWithTag("Player");
        jumpStartPoint = GameObject.FindGameObjectWithTag("JumpStartPoint");
    }

//================[실행함수]==================
    public void FlappyBirdSelect()
    {
        Debug.Log("플래피 버드게임 시작");
        StartCoroutine(FlappyBirdSelectLodingRountine());
    }

    public void Back()
    {
        Debug.Log("뒤로가기");
        StartCoroutine(UILodingRountine());
    }
    
   //=============[기능]=============
    IEnumerator UILodingRountine()
    {
        yield return new WaitForSeconds(0.5f);
        player.GetComponent<PlayerControl>()._activeSpeed = 5f;
        GameManager.UI.CloseWindowUI(this);
    }

    IEnumerator FlappyBirdSelectLodingRountine()
    {
        yield return new WaitForSeconds(0.5f);
        player.GetComponent<PlayerControl>().enabled = false;
        player.transform.position = jumpStartPoint.transform.position;
        GameManager.UI.ShowPopUpUI<PopUpJumpScoreUI>("Prefabs/UI/JumpScoreUI");
        GameManager.UI.CloseWindowUI(this);
    }
}
코루틴을 사용한 이유
UI의 버튼 눌림 같은 작은 애니메이션 없이 바로 이동, 닫힘 등 즉시 액션이 일어나 플레이어 입장에서는 확인 또는 준비 없이 바로 무언가 일어나 당황할수있음 그래서 아무 잠깐의 텀을 주어 일종의 로딩같은 역할
*잘못 남발할경우 프레임 드랍 같은 렉으로 보일수있음

 


마치며.


 

UI 작업은 반복작업, 재사용을 위한 구조설계가 주가 된다 자칫 UI에 기능을 넣게되면 UI본연의 역할을 상실하고 스파게티 코드가 될수있으니 조심하면서 작업하도록 하자.... 하지만 버튼누르면 이동 되는 형식일경우 여러므로 세팅을 해줘야 하는게 있으니

이럴경우 상태매니저 같은걸 만들어서 전체적인 관리를 한 클래스에서 이뤄지게 하는게 좋을듯 하다