전체 글 (114)

03
29

중요 개념 제네릭

 

 

코딩하다가 Getcomponent<T>(); 이런식으로 제네릭을 많이 보는데 맨날 개념을 까먹는다.

이런걸 매일 보는데도 헷갈린다.

이번에 간단하게 개념을 정리해보자.


#1 제네릭(Generic)이란?

 

제네릭(Generic)메서드(함수)나 클래스를 작성할 때 데이터 형식(Type)을 지정하지 않는다.

그렇기에 코드로 작성한 후, 실제 사용하는 시점에서 데이터 형식을 지정할 수 있도록 하는 기능이다. 

제네릭은 재사용성이 높고 컴파일 시점에서 타입 체크를 하기 때문에 안정적이다.

덕분에 제네릭을 쓰면 코드의 가독성과 유지보수성을 향상시킬 수 있다.

제네릭이 생긴게 <T> 이렇게 T에다가 데이터 타입을 암거나 넣을 수 있다.

데이터 타입은 말 그대로 자료형(Data Type)이다.

아래와 같은 자료형들을 얘기한다.

 

값 형식: int, float, double, bool, struct 등
참조 형식: string, object, class 등
사용자 정의 형식: enum, delegate, interface, struct 등

 

이것들 외에도 그냥 자료형이면 인식이 된다.

이제 제네릭 메서드클래스를 보면서 이해해보자.

 

1. 제네릭 메서드(함수)

 

우선 제네릭으로 함수 작성하는 방법을 보자.

T Generic<T>(T a) 로 선언하고 

public T Generic<T>(T a)
{
    return a;
}

 

사용할 때 T(Type)에 해당하는 타입에 int를 넣는다면 나머지 Tint로 정해진다.

 

예를 들어, 위의 제네릭 함수는 return a;Type int로 반환하고 싶다면

제네릭 함수를 선언할 때 Generic<T>T에다가 int를 넣으면 된다.

그러면 뒤에 따라오는 매개변수 a, b도 int로 인식이 되는걸 볼 수 있다.

var num은 말그대로 int말고 다른 타입이 와도 인식하기 위한 거니 int num이랑 똑같다.

 

실제로도 int 대신 Transform 넣으면 리턴 값도 Transform이여야하는 것을 볼 수 있다.

 

2. ref 키워드

 

다른 곳에서 자료를 찾다보니 제네릭 메서드에 ref 키워드를 항상 쓰는 느낌이다.

ref 키워드 메서드(함수)의 인수로 전달되는 값의 참조를 전달하기 위해 사용한다.

일반적으로 값형식인 변수의 값을 변경하거나 외부에게 객체의 참조를 전달하는 식으로 사용한다.

그래서 제네릭을 사용할 때 ref 키워드가 무조건 필요한건 아니다.

 

사용방법은 간단하다.

public T Generic<T>(ref T a)
{
    return a;
}

위의 제네릭 메서드랑 똑같이 하고 매개변수 부분에 ref 키워드 넣어주면된다.

 

3. 제네릭 클래스

 

사실 기본적으로 제네릭 하면 제네릭 클래스를 더 많이 볼 수 있다.

제네릭 클래스의 기본적인 형태는 다음과 같다.

접근제한자 class 클래스이름<T>

그리고 기본적인 선언 형태는 다음과 같다. (클래스에 특별한게 없다면)

클래스이름 <T> genericClass = new 클래스이름<T>();

근데 오른쪽에서 new 클래스이름 <T>(); 여기에 괄호()가 있다. 이건 타입 매개변수(T)다.

이는 생성자제네릭 클래스매개변수로 전달 가능하다. 예제를 보자.

 

public class GenericClass<T>
{
    private T data;

    public GenericClass(T data)
    {
        this.data = data;
    }

    public T GetData()
    {
        return data;
    }
}

private의 멤버 변수 하나 선언하고 생성자에서 매개변수로 입력받아 저장되게 만들었다.

이제 선언하고 우리가 원하는 자료형을 넣어서 사용해보자.

// GenericClass를 사용하여 int 형식의 데이터 저장하기
GenericClass<int> intClass = new GenericClass<int>(1);
Debug.Log(intClass.GetData()); // 1 출력

// GenericClass를 사용하여 string 형식의 데이터 저장하기
GenericClass<string> stringClass = new GenericClass<string>("hello");
Debug.Log(stringClass.GetData()); // "hello" 출력

 

보다시피 <T>에 해당하는 우리가 원하는 자료형을 넣은 뒤 괄호 사이에 데이터를 저장했다.

이런식으로 타입 매개 변수(T)의 응용이 가능하다.

 

4. where 키워드, 제약조건이란?

이제 또 보이는게 where 키워드인데, 이건 타입 매개 변수(T)만족해야 할 조건을 명시할 수 있다.

모양은 대충 이렇게 생겼다.


접근제한자 class 클래스이름 where T : 제약조건

public class GenericClass<T> where T : new()
{
}

제약 조건에는 new() 말고도 다양하게 올 수 있다.

 

where T : struct는 값 타입인 형식에 대한 제약 조건이다.
where T : class는 참조 타입인 형식에 대한 제약 조건이다.
where T : new()는 매개 변수가 없는 기본 생성자를 가지는 형식에 대한 제약 조건이다.
where T : ParentClass는 파생된 형식에 대한 제약 조건이다. 무조건 ParentClass를 상속해야한다.
where T : ISomeInterface는 ISomeInterface 인터페이스를 구현하는 형식에 대한 제약 조건이다.
where T : struct, ISomeInterface는 값 타입 형식 + 인터페이스 형식에 대한 제약 조건이다.

암튼 이런식으로 제한이 가능하다.

 

COMMENT
 
03
26

 

게임을 만들다 보면 콘솔에다가 디버깅하는건 일상이다.

 

근데 맨날 Debug.log()해서 디버깅하기엔 우리가 알아야할 정보들이 너무 많다.

 

Quantum Console은 강력하고 사용하기 쉬운 게임 내 명령 콘솔이다.

 

코드에 [Command]를 추가하기만 하면 자신만의 명령과 개발자 치트를 콘솔에 추가할 수 있다고한다.

 

이 콘솔 에셋은 멀티 게임 만들 때 더 빛을 발휘한다.

 

런타임 중에 네트워크 상태를 모니터링하고 다양한 디버깅 정보를 볼 수 있다. 

 

예를 들어 네트워크 패킷, RPC 호출, 연결 및 끊기 이벤트, 옵젝 생성 및 파괴 이벤트 등을 모니터링 가능하다.

 

에셋 스토어 링크 ↓

https://assetstore.unity.com/packages/tools/utilities/quantum-console-211046?aid=1101l96nj&pubref=rev_quantum&utm_campaign=unity_affiliate&utm_medium=affiliate&utm_source=partnerize-linkmaker 

 

 


#1 사용법

 

이제부터 사용법을 간단하게 정리할텐데 에셋에 들어있는 설명pdf랑 똑같다 ↓

readme.pdf
0.13MB

 

 

우선 사용하기 위해선 TextMeshPro 패키지가 기본적으로 있어야한다.

 

콘솔 에셋을 나타낼 UI들이 다 TextMeshPro인것같다.

 

그리고 기본적인 Canvas를 생성할 때 EventSystem가 같이 생성되는데 만약에 없다면

 

유니티 에디터 상단 메뉴바에 GameObject -> UI -> EventSystem을 눌러 생성해주자

 

이제 에셋을 설치한 풀더, 즉, Assets/Plugins/QFSW/Quantum Console/Source/Prefabs

 

Plugins 풀더에 하위 디렉토리에 있는 Prefabs 풀더에 가보면 

 

이런식으로 두개의 프리팹이 있는걸 볼 수 있다.

 

Quantum Console (SRP)는 SRP 파이프라인을 지원하고

Quantum Console은 built-in 파이프라인을 지원한다.

 

테스트중인 프로젝트가 URP라 SRP 프리팹을 선택해서 월드에 끌어다 주자 

(URP는 SRP의 일종이라 호환가능)

 

프리팹을 끌어다주면 화면에 이렇게 콘솔창이 생긴걸 볼 수 있다.

 

이제 여기다가 명령어로 우리가 원하는걸 구현 가능하다.

 

이 상태에서 help라고 명령어를 쳐서 Submit 눌러보면 간략한 소개를 해준다.

 

1. 명령어 추가

 

우선 콘솔창에 커스텀 명령어를 추가해주기 위해서는 네임스페이스를 먼저 선언해야한다.

 

using QFSW.QC;

 

그리고 함수 이름을 명령어로 사용가능한데 스크립트에서 함수 위에 [Command] 키워드만 붙혀주면 된다.

 

[Command]
private void StopTime()
{
    Time.timeScale = 0f;
}
[Command]
private void RunTime()
{
    Time.timeScale = 1f;
}

 

이런식으로 선언해주면 아래와 같이 된다.

 

보다시피 콘솔창에서 함수명을 입력해 바로 호출가능하다.

 

참고로 마크 명령어 같은 느낌이라 stop까지만 입력하고 Tab을 눌러주면 자동완성도 지원해준다.

 

그리고 명령어는 굳이 함수명이 아니어도 이름을 다르게 지어줄 수 있다.

 

방법은 간단하다. 아까 [Command] 키워드에서 [Command ("원하는 이름")]  이런식으로 지어주면 된다.

 

[Command ("STOP")]
private void StopTime()
{
    Time.timeScale = 0f;
}
[Command ("RUN")]
private void RunTime()
{
    Time.timeScale = 1f;
}

 

이런식으로 STOPRUN이 명령어로 잘 인식이 된다.

 

그리고 개인적으로 한글로도 이름 지을 수 있지않을까 싶었는데

 

역시나 한글은 지원안하는건지 TextMeshPro폰트가 깨진다.

 

그러면 내가 자음모음 모든 한글 입력가능한 TextMeshPro 폰트를 만들어 넣으면 되는법

위에서 프리팹 풀더에 보면 Default Theme (SRP) 부분에서 테마를 바꿀 수 있다.

 

눌러서 인스펙터를 확인해보면 Font부분이 있는데 여기에 내가 만든 폰트를 넣었다.

 

그렇게 테스트를 해보았더니

아주 잘 되는 모습이다.

 

실제 게임에서 유저들이 사용할 수 있게 재밌는 한국어 명령어들을 넣으면 재밌을 듯하다.

 

2. 응용

그리고 에셋에서 time-scale [값] 입력하면 바로 timescale값을 설정 가능하도록 지원이 되어있다.

 

그렇기 때문에 기본적으로 에셋에서 지원하는 명령어들을 익혀두면 좋다.

 

모든 명령어를 한번에 볼 수 있는 명령어는 all-commands가 있다

이런식으로 명령어를 다 볼 수 있다.

 

각 명령어에 대한 설명은 man [명령어 이름] 식으로 적어주면 설명도 볼 수 있다.

 

참고로 우리가 추가한 명령어들도 설명을 적어주는게 가능하다.

 

방법은 간단하다. 함수 위에 [CommandDescription ("명령어 설명")]  이런식으로 선언해주면 된다.

[Command("STOP")]
[CommandDescription("It makes timescale to 0")]
private void StopTime()
{
    Time.timeScale = 0f;
}
[Command("RUN")]
[CommandDescription("It makes timescale to 1")]
private void RunTime()
{
    Time.timeScale = 1f;
}

이런식으로 우리가 적은 설명이 아주 잘 나오는 모습이다.

 

 

3. 코루틴 활용

그리고 명령어를 입력하고 나서 되묻거나 선택지를 제공하는것도 코루틴으로 구현 가능하다.

 

다만 QFSW.QC.Actions 네임스페이스를 선언해줘야만 한다.

 

선택지 선택 ↓

using QFSW.QC;
using QFSW.QC.Actions;
using System.Collections.Generic;
...

[Command("read-key")]
public static IEnumerator<ICommandAction> ReadKey()
{
    KeyCode key = default;
    yield return new GetKey(k => key = k);
    yield return new Value(key);
}

 

되묻고 결과 알려주기 ↓

using QFSW.QC;
using QFSW.QC.Actions;
using System.Collections.Generic;
...

[Command("choice")]
public static IEnumerator<ICommandAction> Choice()
{
    string[] consoles = {"PS4", "Xbox", "Switch"};
    string selectedConsole = default;

    yield return new Value("Pick a console");
    yield return new Choice<string>(consoles, s => selectedConsole = s);

    yield return new Typewriter($"You picked {selectedConsole}.");
}

 

 


#2 주의 점

Company Name과 Product Name은 꼭 입력이 되어있어야 나중에 로그파일 찾을 때 문제가 생기지않는다.

 

그리고 Player탭에서 스크롤 내려서 밑에 설정에서도

Fullscreen Mode가 Windowed로 되어있어야 테스트할 때 편리하다.

 

밑에 Resizeable Window 항목을 체크해줘도 좋다.

 

더 자세한 API 문서는 아래의 링크에 있다

https://qfsw.co.uk/docs/QC/api/QFSW.QC.QuantumConsole.html

 

 

COMMENT
 
03
19

체력이나 이름 UI 같은 월드상에 있고 카메라를 바라보는 UI를 구현할 때 많이 쓴다.

 

이 글에서 사용한 유니티 버전은 2021.3.20f1이다.


#1 Canvas World Space 렌더 모드 셋팅

 

우선 Canvas 하나를 만들어준다.

Hierarchy -> 우클릭 -> UI -> Canvas

하이어라키에 우클릭해서 UI탭의 Canvas를 눌러 하나 생성해준다.

 

생성해준 Canvas를 선택해주면

 

오른쪽 인스펙터의 Canvas 컴포넌트 부분에 Render Mode를 선택할 수 있다.

 

여기서 World Space를 눌러 선택해주면 월드 캔버스 하나 생성 끝

 

 


#2 World Space 캔버스 크기 설정

 

이제 우리가 만든 월드 캔버스의 RectTransform크기를 설정해줘야한다.

 

이때 scale은 웬만해서 건들지않고 크기를 조절해주자

 

개인적으로 이 캔버스에 스탯을 표기할 예정이기에 적당하게 2x2 크기로 해두었다.

 

그리고 자식에다가 대충 스탯을 나타낼 UI를 만들어 두었다.

 

결과물 ↓

COMMENT
 
03
13

생각보다 강한 구조체...

C언어의 사용자 정의 자료형에는 구조체 struct, 공용체 union, 열겨형 enum이 있지만

 

C#에선 셋중 공용체가 없어서 구조체 struct와 열거형 enum만 있다.

 

물론 공용체를  쓰고 싶으면 아래의 글에서 편법으로 쓸 수 있다고 한다. ↓

C#에서 Union 구현하기

 

오늘은 구조체만 한번 다뤄보자

 


#1 구조체 struct

 

구조체란 하나 이상의 변수들을 묶어서 그룹으로 만드는 사용자 정의 자료형이다.

 

사용 목적 자체는 변수들을 그룹화하고 값형식이라 값으로써 사용하기 위한거다.

 

게임을 만들 때 플레이어의 스탯을 그룹으로 만든다면 참조하여 쓰기 편리하다.

 

이제 구조체를 정의해볼건데 구조체 정의한다는건 구조체 이름, 구조체 멤버 변수를 만든다는 뜻이다.

//* 접근제한자 struct 구조체 이름
public struct Player
{
    public int Health {get; set;} //* 프로퍼티 get set
    public float Mana; //* 멤버 변수
    public void Dead() //* 멤버 메서드(함수)
    {
        if(Health <= 0)
        {
            bool isDead = true;
        }
    }
}

접근제한자 뒤에 struct 키워드를 쓰고 구조체 이름을 쓰면 된다.

 

구조체 안에는 생성자, 프로퍼티, 멤버 변수, 멤버 메서드(함수), 이벤트가 올 수 있다.

 

이렇게 정의한 구조체의 사용 방법은 간단하다.

 

private void Start()
{
    Player player;
    player.Health = 0;
    player.Mana = 0;
    player.Dead();
}

 

클래스 내에 함수에서 구조체를 선언해서 사용하면 되는데 실제로 위에처럼 하면 에러가 뜰 것이다.

 

이유는 Dead()와 Health가 각각 메서드(함수)와 프로퍼티인데 이것들이 할당을 안했다고 에러가 계속 뜬다.

 

이럴 때 구조체 고유의 인스턴스를 만들고 다시 할당하면 된다. 즉 재할당을 해주면 된다.

 

private void Start()
{
    Player player;
    player = new Player(); //인스턴스 생성
    player.Health = 0;
    player.Mana = 0;
    player.Dead();
}

이런식으로 하면 된다.

 

위에서 메서드와 프로퍼티만 에러 뜬다고 했는데 값형식 다 인스턴스 생성해 재할당 해줘야한다.

 

멤버 변수는 변수가 참조타입(클래스)같은거만 아니라면 꼭 재할당 안해도 되는거같다.

 

근데 Player player을 Start함수가 아닌 더 위에서 선언하면 에러가 안뜨던데 왜그런진 모르겠다.

 

그리고 위에서 말한거처럼 구조체안에는 이벤트도 올 수 있다.

//* 접근제한자 struct 구조체 이름
public struct Player
{
    public int Health {get; set;} //* 프로퍼티 get set
    public float Mana; //* 멤버 변수
    public void Dead() //* 멤버 메서드(함수)
    {
        if(Health <= 0)
        {
            bool isDead = true;
        }
    }
    public event EventHandler OnEvent; //* 이벤트 핸들러

    public void InvokeEvent() //* 이벤트 실행을 위한 멤버 메서드(함수)
    {
        if (OnEvent != null)
        {
            OnEvent(this, EventArgs.Empty);
        }
    }
}
void Start()
{
    Player player = new Player(); //* 인스턴스 생성
    customEventStruct.OnEvent += CustomEventHandler; //* 이벤트 구독

    customEventStruct.InvokeEvent(); //* 이벤트 실행 메서드 호출
}

구조체안의 이벤트는 어디다  써야할지 감이 안온다.

 

아무튼 구조체상속이 불가능한데 Interface 구현은 가능하다.

 

그럼에도 구조체를 사용하는 이유는 위에서 말한 것처럼

 

일반적으로 클래스보다 가볍고 속도가 빠르고 데이터를 저장하고 전달하는데 효과적이기 때문이다.

 

그 이유는 아래 사진에서 스택 부분에 저장되기 때문이다.

 

 

구조체는 값 형식이기 때문에 스택 영역에 저장되어 메모리 할당과 해제가 간단하고 빠르다.

 

또한 구조체는 스택 메모리에 저장되므로 클래스와는 달리

 

힙 메모리를 사용하지 않아 가비지 컬렉션 부하가 발생하지 않는다.

 

근데 이게 오히려 독이 될 수도 있는데

 

구조체의 크기가 너무 커지거나 구조체 인스턴스를 불필요하게 많이 생성하면 

 

스택 메모리가 과도하게 사용될 수 있는데 그러면 스택 오버플로 발생할 수 있다.

 

그냥 적당하게 상황에 따라서 최대한 적게 쓰면 될듯하다.

 

 

위에서 구조체값 형식이라고 했는데 그러면 클래스는 무슨 형식일까?

 

클래스참조형식으로써 데이터의 실제 값이 메모리 공간을 가지지 않는다.

 

참조 형식의 인스턴스(클래스)는 힙 메모리에 저장된 데이터를 참조만한다.

 

때문에 메모리 할당과 해제가 복잡하고 가비지 컬렉션에 의해 메모리 관리가 이루어진다.

 

그리고 구조체와 다르게 상속, 다형성 등을 지원한다.

 

아무튼 구조체를 알아보는 김에 클래스도 다시 알게 된 것 같다.

 

구조체도 상황에 따라서 유연하게 쓰도록 하자

 


#2 구조체 struct 배웠으니까 활용법도

 

이제 유니티에서 구조체를 사용할만한 예시를 몇가지 보자

 

1. 색 구조체

우선 색 구조체 하나를 만들어보자 ↓

public struct Color {
    public float r, g, b, a;

    public Color(float r, float g, float b, float a) {
        this.r = r;
        this.g = g;
        this.b = b;
        this.a = a;
    }
}

그 다음 나만의 색을 저장할 구조체를 선언하자 ↓

Color red = new Color(1, 0, 0, 1);
Color green = new Color(0, 1, 0, 1);

 

위에서 구조체를 설명할 때 값 형식이라고 했기 때문에 이런식으로 값 저장식으로 활용하면 좋다.

 

개인적으로 저렇게 저장해둔 컬러 값처럼 카메라 필터 값을 저장해 필요한 상황에 쓰는것도 좋겠다고 생각함 

 

2. 캐릭터 스탯 구조체

struct CharacterAttributes 
{
    int health;
    int attackPower;
    int defensePower;
}

CharacterAttributes myCharacterAttributes = new CharacterAttributes();
myCharacterAttributes.health = 100;
myCharacterAttributes.attackPower = 50;
myCharacterAttributes.defensePower = 20;

일반적으로 캐릭터 스탯을 이렇게 저장하는듯하다.

 

번외) ChatGPT가 알려준 애니메이션  정보 저장 구조체

struct AnimationInfo 
{
    string animationName;
    float speed;
    bool loop;
}

AnimationInfo myAnimationInfo = new AnimationInfo();
myAnimationInfo.animationName = "Run";
myAnimationInfo.speed = 1.5f;
myAnimationInfo.loop = true;

이렇게 말고도 활용할 부분은 굉장히 많을듯하다.

 

구조체..생각보다 강한거같다

COMMENT