반응형

유니티에서 이런 UI를 구현하고자 한다

원리는 간단하다.

탭은 각 UI를 할당하고 있고,

탭버튼을 누를때 다른 종속된 모든UI를 꺼지게하고

내가 누른 버튼에 할당된 UI만 띄우게 하면된다.

결국 각 오브젝트들이 껐다 켜지면서 전환되는 개념이다.
실제 윈도우폼에서의 탭 작동과도 원리가 유사하다

 

 

2개의 cs파일로 간단하게 끝낼 수 있다.

 

기능이 동작하는 방식을 이해하기 쉽도록 아주 간단한 코드로 작성했다

TabButton.cs - 탭 버튼

using UnityEngine.UI;
using UnityEngine;

[RequireComponent(typeof(Button))] //버튼이 없는 오브젝트라면 자동으로 추가
public class TabButton : MonoBehaviour
{
    //해당 버튼 클릭시 열어 줄 UI를 인스펙터창에서 받아온다
    [SerializeField] GameObject panel;
    Image btnImage; //버튼 이미지 
    public GameObject GetPanel => panel;

    TabController parent; //부모 컨트롤러
    private void Start()
    {
        Button btn = GetComponent<Button>();
        btn.onClick.AddListener(SwitchTab); //버튼 리스너에 함수를 할당
        parent = transform.parent.GetComponent<TabController>(); //TabController컴포넌트를 보유한 부모를 가져옴
        btnImage = btn.image; //버튼에 할당된 이미지를 가져옴
    }
    void SwitchTab() //버튼 클릭시 부모에게 내가 눌려졌음을 알림
    {
        parent.SwitchTab(this);
    }
    public void ChangeButtonImage(Sprite _sprite)
    {
        if (btnImage == null) return;
        if (btnImage.sprite != _sprite) //현재 버튼 이미지의 스프라이트와 매개변수 _sprite가 다르다면
            btnImage.sprite = _sprite; //_sprite로 버튼 이미지를 바꿈
    }
}

 

 

TabController.cs - 탭들을 관리하는 관리자 역할

using UnityEngine;

public class TabController : MonoBehaviour
{
    [SerializeField] Sprite btnNormal; //탭버튼이 정상일때
    [SerializeField] Sprite btnSelect; //탭버튼이 눌러진 상태일때

    TabButton[] tabs; //자식들인 탭버튼을 저장할 배열

    // Start is called before the first frame update
    void Start()
    {
        tabs = GetComponentsInChildren<TabButton>();
        SwitchTab(tabs[0]); //첫번째 탭을 눌러준다
    }
    public void SwitchTab(TabButton _target)
    {
        for (int i = 0; i < tabs.Length; i++)
        {
            bool _isActiveTab = _target == tabs[i]; //tabs[i]가 눌려진 버튼인지 판단
            tabs[i].GetPanel.SetActive(_isActiveTab); //모든 탭을 다 꺼준다
            //삼항연산자로 버튼이 눌러졌을때와 일반적인상태일때를 판단해 바꾸어 준다
            tabs[i].ChangeButtonImage(_isActiveTab? btnSelect : btnNormal); 

        }
    }
}

 

두개의 스크립트를 만들고 하이어라키창을 아래와 같이 구성해준다(당연히 입맛대로 설정해주면 된다)

pTabs에는 TabController.cs / 각 탭 버튼에는 TabButton.cs를 넣어주자
아마 TabButton을 넣어주면 버튼이 없을경우 자동으로 생성될 것이다.
해당 버튼이 눌러졌을때 열리게할 패널을 Panel 변수에 넣어주자.

 

모든 탭버튼에 열리게할 패널을 하나씩 각각 넣어주면 버튼쪽은 준비 끝

 

pTabs에는 TabController를 넣어주었다

버튼들의 부모가 되는 pTabs에

BtnNormal, BtnSelect 두개의 스프라이트를 채워넣자.

버튼이 일반적인 상태일때는 BtnNormal의 이미지가 보여질것이고

버튼이 눌려진 상태일때는 BtnSelect 의 이미지가 보여질것이다.

(난 대충 아무거나 넣었다.)

 

pPanels와 그 아래 panel0, panel1 자식들은 UI아무렇게 가져다 꾸몄다.

예시를 위해 바로 보이게끔 0에는 빨강 / 1에는 파랑의 이미지를 넣었다.

대충 이런모양? 아래UI는 원하는대로 필요한 것을 넣으면 된다

 

여러 UI에 탭버튼을 활용해야할때마다 언제든지 갖다 쓰면 될듯하다.

실제로 바로사용해도 무방하나, 프로젝트 상황에 맞게 입맛대로 변경하면 되지싶다.

 

 

퍼가고 맘대로 사용해도 되지만 출처는 꼭 밝히시길..

이해하는데 도움이 되셨다면 아래 하트 부탁함다.

반응형
반응형

 

게임 개발은 객체 생성과 관리가 핵심적인 역할을 합니다. 이를 효율적으로 다루기 위해 Factory 패턴은 유니티(Unity)에서 강력한 디자인 패턴 중 하나로 사용됩니다. Factory 패턴은 게임 오브젝트나 컴포넌트 등의 생성과 관리를 추상화하여 코드의 가독성, 유지보수성, 그리고 재사용성을 향상시키는 데 도움을 주는 패턴입니다.

Factory 패턴이란?

Factory 패턴은 객체 생성 로직을 별도의 클래스로 분리하는 디자인 패턴입니다. 이를 통해 개체 생성에 대한 세부 사항을 숨기고, 클라이언트 코드가 생성 프로세스를 알 필요 없이 객체를 생성할 수 있게 됩니다. Factory 패턴은 크게 두 가지 형태로 나뉩니다.

  • 단순 팩토리(Simple Factory) 객체 생성을 위한 인터페이스를 제공하고, 클라이언트에게 어떤 클래스를 생성할 것인지 선택하는 책임을 지게 합니다. 유니티에서는 이 패턴을 자주 사용합니다.
  • 추상 팩토리(Abstract Factory) 여러 종류의 관련된 객체를 생성하며, 이러한 객체들이 함께 작동할 수 있도록 보장합니다. 일반적으로 복잡한 시스템에서 사용됩니다.

 

햄버거 가게를 상상해보세요. 햄버거 가게는 다양한 종류의 햄버거를 만듭니다. 이 가게에서는 각 햄버거를 주문할 때마다 주문한 종류에 따라 조리과정이 달라집니다. Factory 패턴을 사용하여 이 가게의 동작을 설명해보겠습니다.
햄버거 공장 (Hamburger Factory) 이 공장은 다양한 종류의 햄버거를 만들어내는 곳입니다. 이 팩토리는 주문받은 햄버거의 종류에 따라 다른 햄버거를 생성합니다.
햄버거 (Hamburger) 각각의 햄버거는 고유한 레시피와 재료를 가지고 있습니다. 팩토리는 이 햄버거를 만들 때 필요한 재료와 조리 방법을 알고 있습니다.
주문 (Order) 고객이 원하는 햄버거 종류를 주문합니다. 예를 들어, "치즈버거 주세요" 라고 주문하면, 팩토리는 치즈버거를 만들어 제공합니다.
서비스 (Service) 팩토리는 주문을 받고, 그에 맞는 햄버거를 만들어내며, 최종적으로 손님에게 제공합니다.

Factory 패턴을 사용하면 고객은 어떤 햄버거가 만들어지는지, 어떤 재료와 레시피가 사용되는지 신경 쓸 필요 없이 주문만 하면 됩니다. 팩토리가 주문에 따라 올바른 햄버거를 만들어주기 때문입니다.

이런 방식으로 Factory 패턴은 객체를 생성하고 초기화하는 복잡한 작업을 추상화하며, 클라이언트가 생성 과정을 신경 쓰지 않고 필요한 객체를 얻을 수 있도록 도와줍니다.

 

Factory 패턴의 장점

  • 유연성: 객체 생성 방법을 중앙에서 관리하므로 생성 로직 변경 시 코드 수정이 최소화됩니다.
  • 유지보수성: 객체 생성 코드가 중앙 집중화되므로 유지보수가 쉬워집니다.
  • 재사용성: 객체 생성 로직을 재사용 가능한 컴포넌트로 만들어 코드의 재사용성을 향상시킵니다.

 

Factory 패턴 간단한 실 예제

게임에서 캐릭터를 생성해야 한다고 가정해봅시다. 캐릭터의 생성은 다양한 속성과 초기화 과정이 포함될 수 있습니다.

using UnityEngine;

public class CharacterFactory : MonoBehaviour
{
    public GameObject playerPrefab; // 플레이어 프리팹을 할당할 변수
    public GameObject enemyPrefab;  // 적 캐릭터 프리팹을 할당할 변수

    // 플레이어 캐릭터를 생성하는 메서드
    public GameObject CreatePlayer(Vector3 position)
    {
        GameObject player = Instantiate(playerPrefab, position, Quaternion.identity);
        return player;
    }

    // 적 캐릭터를 생성하는 메서드
    public GameObject CreateEnemy(Vector3 position)
    {
        GameObject enemy = Instantiate(enemyPrefab, position, Quaternion.identity);
        return enemy;
    }
}

 

위의 코드에서 CharacterFactory 클래스는 플레이어와 적 캐릭터를 생성하는 두 가지 메서드를 제공합니다. 클라이언트는 이 팩토리 클래스를 사용하여 필요한 캐릭터를 생성하고 초기화할 수 있습니다.

using UnityEngine;

public class CharacterSpawner : MonoBehaviour
{
    public CharacterFactory characterFactory; // 팩토리 클래스를 할당할 변수

    void Start()
    {
        // 플레이어 생성
        GameObject player = characterFactory.CreatePlayer(new Vector3(0, 0, 0));
        
        // 적 생성
        GameObject enemy = characterFactory.CreateEnemy(new Vector3(5, 0, 0));
    }
}

 

위의 CharacterSpawner 스크립트에서 CharacterFactory 클래스를 사용하여 플레이어와 적을 생성할 수 있습니다.

 

이 예제 외에도

총알을 발사하는 과정에서 총알마다 각기 다른 사거리, 속도, 데미지 등을 설정하고

필요한 상황에 맞게 어떤 총알을 생성하여 어떻게 발사할지 관리하게 만들 수도 있겠죠.

마치며

Factory 패턴을 이용하면 게임 오브젝트 생성과 관리를 효율적으로 다룰 수 있으며, 복잡한 게임에서 객체 생성과 초기화를 효과적으로 관리할 수 있습니다. Factory 패턴은 유니티 프로젝트에서 객체 생성과 관리에 있어서 필수적인 디자인 패턴 중 하나입니다.

 

이런 Factroy 패턴으로 코드를 작성한다면 생성하는 부분과 관리하는 부분을 별도로 가지고있기에 오류가 생길 확률이 적어집니다.

또한, 오브젝트 풀링과도 많은 연관이 있기에 잘 이용한다면 관리가 쉬운 코드를 짤수있습니다.

반응형
반응형
유니티 작업 시, 레이아웃을 따라 UI를 잘 꾸며 놓았는데 간혹 특정 UI의 크기를 자동으로 조절을 해야할 때가 있다.
즉, 특정 UI의 사이즈가 자식의 크기에 따라 자동으로 정렬이 되었으면 할때 필요한 기능이다.

딱 이런 경우

이런 경우 자식 오브젝트들의 사이즈에 맞게 가변형으로 바뀌게 할 수 있는 방법이다.

 

1. 사용방법

우선 패널의 인스펙터 내용이다.

간단하게 예시 Image와 HorizontalLayout Group을 추가해줬다

 

아래에서 추가 설명하겠지만,

이 글에서 설명하려는 ContentSizeFitter 컴포넌트의 경우

Layout Group이 필수로 함께 있어야한다

(Vertical  Layout Group 또는 Horizontal Layout Group)

 

나는 Horizontal Layout Group을 추가했다. 

 

하위 자식 오브젝트들은 아무거나 넣어주었고 사이즈도 대충 집어 넣었다.

 

 

 

 

 

 

 

 

 

 

 

이렇게까지만 하면 아마 여전히 아래와 같을 것이다.

 

자 그럼 다시 부모오브젝트인 panel로 돌아가서 Add Componenet ->  Content Size Fitter 추가

그럼 아래 그림과 같은 컴포넌트가 추가된다.

여기서,

가로사이즈를 가변형으로 바꾸고 싶다 하면 Horizontal Fit

세로사이즈바꾸고 싶다하면 Vertical Fit을 조절하면된다.

 

눌러보면 세 개중 하나를 고르도록 되어있다.

Unconstrained의 경우는 원래 그대로의 상태다 (쉽게 말해, 기능 off)

Min Size 혹은 Preferred Size 중 하나를 선택하면 된다.

 

Min Size는 가장 최소화된 사이즈로 맞춰준다.

Preferred Size 적당히 선호되는 사이즈로 맞춰준다.

아래는 Preferred Size를 누른 결과이다

완성

꽤 직관적으로 되어 있어서 몇 번 사용해보면 어떻게 동작하는지 금방 알 수있다.

텍스트의 길이에 따라 사이즈를 조절한다든지도 이걸로 해결할 수 있다.(텍스트는 Preferred Size 만 된다.)

 

 

2. 주의사항 두 가지

1. 경고메시지

다만, 주의 할 점은 이렇게 Layout Group을 사용하고있지 않은 오브젝트는 경고 메시지가 뜨는걸 확인할 수 있다.

ContentSizeFitter는 하위 자식오브젝트들을 감지해야하므로 Layout Group이 필수로 있어야한다

(Vertical  Layout Group 또는 Horizontal Layout Group)

당연히 하위 자식오브젝트들이 어떤 기준으로 정렬이 되어있어야 그에 맞춰 사이즈를 조절할테니..

하지만 내 경우 텍스트의 경우 이를 무시하고 사용할때가 종종있다. 문제없이 됨

 

2. 간혹 안되는 경우

간혹가다 ContentSizeFitter가 특정 상황에서 먹히지 않는 경우가 있다.

해당 부분은 특정상황과 해결방법에 대해 따로 다뤄야할 듯해서 바로 다음 글에 게시하겠다

반응형
반응형

좋은 코드는 언어나 기술에 종속되지 않는다.

 

그렇지만 코딩을 하면서 매일 드는 생각은 '그래서 이게 잘 짠 코드인가?' 이다.

 

결론부터 말하자면, 내가 생각하는 잘 짠코드의 정의는 단 두가지로 함축될 것 같다.

1. 남들이 읽기 쉬운가? a.k.a 가독성
    이 '남'의 정의를 코드를 짠 이후의 미래의 '나'도 '남'에 포함해야한다고 본다.
    시간이 지나고 다시 들여다 보면 내가 쓴 게 아닌 것 같다...
    ...그래서 보통 주석을 달곤하지만, 과연 그 주석도 잘 읽히던가?


2. 재사용이 가능한가 +모듈화
    내가 짠 코드를 다른 프로젝트에서도 그대로 가져와 쓸 수 있는지,
    그리고 타인이 사용했을 때도 간단하게 사용할 수 있는지

    간단한 것 조차도 항상 확장성을 고려해서 짜야한다

 

위의 내용만 잘 지켜도 유지 보수가 쉽고 확장 가능한 프로그래밍이라 할 수 있을듯하다.
하지만, 그 디테일 함은 여전히 누구나 어려워한다. 그래서 나도 몇가지 규칙을 두고 코딩을 하려고 노력한다.

오늘은 그 규칙들 중 4가지 정도만 설명해보고자 한다.

 

 

 

1) 주석보다 코드내용에 집중하자

주석이 달린 코드는 이후 수정이 필요할때 수정하고 난 후 주석도 같이 수정해야하지만 생각보다 번거롭다.

특히나 협업의 경우 다른 팀원이 내 코드내용을 수정한 이후 주석도 같이 수정해주는 팀원은 희귀했다.

이런문제들로 쌓인 '잘못된 주석' 때문에 리딩을 다시 해야하는 경우도 종종 생긴다

 

2) 이름은 항상 중요

아래 3)항과 같은 내용이라고도 볼수 있을지 모르겠지만

변수명이 무엇을 담고있는 변수인지 구체적으로 명시하자.

string name; //(X)
string user_name; //(O)

 

마찬가지로 함수명도 어떠한 기능을 하는지에 대해 구체적이고 명확한게 좋다.

 

하지만, 이름을 짓는게 가장 고통스럽다

프로그래머가 가장 힘들어하는 일은? = 이름 짓기...공감... 출처:나무위키

 

3) 조건은 항상 짧게

예시를 들어보면 바로 알 수 있다.

if(level > 1 && job == EJob.Designer && isWhiteUser)
{
	//내용
}

조건을 변수로 빼주고나면, 해당 조건문이 무엇을 뜻하는지 명확하게 알 수 있다.

bool isManager = level > 1 && job == EJob.Designer && isWhiteUser;
if(isManager)
{
	//내용
}

 

4) 리턴의 활용

코드를 보면 무슨 얘기를 할지 바로 이해할듯하다.

1.

public int GetUserPermission()
{
    int permission;
    if (IsWhiteUser()) //화이트유저인지체크
    {
        if (job == EJob.none) //Job이 공란일 경우
        {
            permission = 0;
        }
        else //Job이 공란이 아닐때
        {
            permission = level; 
        }
    }
    else
    {
        permission = 0;
    }
    return permission; //권한값을 리턴
}

2.

public int GetUserPermission()
{
    if (IsWhiteUser() && job != EJob.none) return level;
    return 0;
}

위 2개의 함수는 같은 기능을 한다.

그러나 1번 함수는 쓸데없이 이중 조건을 걸고 있다. (읽는데 거부감이 있다)

그리고 return에 대한 의미를 정확히 이해를 하지 못해 사족이 많이 길다.

당연하겠지만 if문에 들어가서 return이 되면 그 함수는 종료가 된다.

그럼 else는 필요가 없다.

 

//더 줄이자면 이렇게 변태같이 줄일 수도 있겠지만 좋은 예시는 아니라고 생각이 든다
public int GetUserPermission()=> (IsWhiteUser() && job != EJob.none) ? level : 0;

 

본인은 병적으로 코드최적화에 집착한다.

줄 수를 줄이면 최적화가 된다고 생각하는 사람이 많은 것 같은데, 맞는 말 같기도 틀린말 같기도 하다.

줄 수에 집착하기 보다는 정확한 플로우를 이해하는게 중요한 것같다.

즉, '실행 시점'과 '조건'을 잘 생각한다면 많은 것이 해결되더라.

 

더불어 enum의 활용과 코딩컨벤션, 상수활용 등 많은 방법이 있지만 커먼한 방법들이라 판단이되어 따로 게제하지는 않았다.

추후에 시간이 된다면 해볼지도..

반응형
반응형

UI Text에 float을 String으로 표현해야 할 때 원하는 소수점 자리까지만 표현을 하고 싶을경우가 있다.

 

단순히 ToString() 함수에 'F' 포맷을 넣어주면 된다.

float a = 1.234f;

a.ToString("F1"); // 1.2

a.ToString("F2"); // 1.23

a.ToString("F3"); // 1.234

 

또 마찬가지로 숫자를 1,000 단위로 콤마를 표현해 String으로 반환하고 싶을때가 있다.

float a = 10000.123f;

a.ToString("N1"); // 10,000.1

a.ToString("N2"); // 10,000.12

a.ToString("N3"); // 10,000.123

F대신에 N으로 바꾸어 주면 된다.(아마도 Numric 의 약자가 아닐까 싶다.)

이렇게 간단한 포맷으로 사용이 가능하다.

반응형
반응형

로컬에 데이터를 저장하는 방법은 여러 가지가 있습니다.

 

저는 저장될 내용에 따라 JSON, 바이너리, PlayerPrefs 등 혼용해서 많이 쓰는데요.

 

그중 이번에는

아마도 유니티 내에서 가장 간단한 PlayerPrefs를 활용해볼까 합니다.

 

 

PlayerPrefs

일단 알아 두셔야 할 점은,

-간편하게 관리할 수 있다는 점.

-간편하고 쉬운만큼 보안성은 개나 줘 버려서 없음. (저장 위치의 파일을 열어보면 날 것 그대로의 값이..)

 

위 같은 이유로 중요한 데이터는 웬만해서는 이 방법을 추천하지 않습니다.

보통, 게임 옵션의 세팅 정보 (BGM, Sound의 볼륨 크기, 진동 유무) 정도로 사용하시는 게 적당하지 않을까 싶네요.

(만약 정말 만약 PlayerPrefs를 활용해서 중요한 게임 데이터를 저장코자 한다면,

꼭 저장 전에 암호화한 값을 저장해주세요. 암호화는 나중에 한 번 다뤄 보도록 하겠습니다.)

 

PlayerPrefs은 기본적으로 Int / Float / String의 변수 타입을 지원합니다.

 

(Bool 변수형의 경우는 지원을 하지 않는 것으로 보이는데, 저의 경우 삼항 연산을 이용 Int로 변환해서 사용합니다.)

 

(배열을 간혹 저장해야 할 때가 있는데 배열을 String으로 변환 후 Spilt을 이용해 사용합니다.)

 

저장하기(Save)

정말 간단히 저장된다.

//기본적으로 PlayerPrefs는 ("키", 값) 형태로 사용

//Int - 정수형 변수의 저장 
PlayerPrefs.SetInt("키", int);

//String - 문자열 형태의 변수 저장
PlayerPrefs.SetString("키", string);


//Float - 실수형의 변수 저장
PlayerPrefs.SetFloat("키", float);

예를 들어

//유저 데이터 세이브

//캐릭터 이름
PlayerPrefs.SetString("CharName", "무예꼬마");

//캐릭터 레벨
PlayerPrefs.SetInt("Level", 10); 

//치명타 확률
PlayerPrefs.SetFloat("Critical", 0.5f);

불러오기(Load)

로드 역시 간단하다.

 

//기본적으로 PlayerPrefs는 ("키", 값) 형태로 사용

//Int - 정수형 변수의 저장 
PlayerPrefs.GetInt("키");

//String - 문자열 형태의 변수 저장
PlayerPrefs.GetString("키");

//Float - 실수형의 변수 저장
PlayerPrefs.GetFloat("키");

 

//유저 데이터 로드

//캐릭터 이름
string charName = PlayerPrefs.SetString("CharName"); //"무예꼬마"

//캐릭터 레벨
int level = PlayerPrefs.SetInt("Level"); //10

//치명타 확률
float criticalPoint =PlayerPrefs.SetFloat("Critical"); //0.5f

끝이다.

 

 

다만 만약에 로드를 했는데 해당 키가 없을 경우 기본 초기값을 설정해주는 방법이 있는데

//불러올때 초기값을 설정해주면, 혹시나 값이 없을때 초기값으로 값을 돌려준다.
PlayerPrefs.GetInt("키", 초기값);

//예를들어
//레벨 데이터를 가져오고, 만약 레벨 데이터가 없다면 1을 불러온다.
PlayerPrefs.GetInt("level", 1);

//String Float 모두 동일하게 사용가능하다.

 

 

아래는 추가적으로 필요한 기능들!

삭제(Delete)

더 이상 필요가 없는 저장된 키값을 지우고 싶을 때가 있다.

//지우고싶은 데이터 삭제
PlayerPrefs.DeleteKey("키");

//저장되어있는 모든 데이터 삭제
PlayerPrefs.DeleteAll();

해당 데이터가 있는지 확인

//해당 이름을 가진 데이터가 있다면 true
if(PlayerPrefs.HasKey("키"))
    Debug.Log("해당 키가 존재합니다.");

 

반응형

+ Recent posts