Thread 

스레드 생성 시 반복문의 인덱스를 매개변수로 받을 때


스레드(Thread)를 생성할 때, 반복문으로 여러 개의 스레드를 생성하면서 그 반복문의 인덱스를 매개변수로 전달하는 방법을 쓸 때가 있다.


class ThreadTestProgram

{

    public static int DeviceNum = 10;


    public static void Main(string[] args)

    {

        for (int i = 0; i < DeviceNum; i++)

        {

            new Thread(() => Run(i)).Start();

        }

    }


    public static void Run(int idx)

    {

        // 디바이스 인덱스에 따라서 스레드 별로 각 디바이스와 연결하는 작업...

        Console.WriteLine(idx);

    }

}

 

위의 예시 코드가 바로 그것이다. 여러 개의 디바이스에 연결해서 스레드로 작업을 처리해야 할 때의 코드인데, 스레드 함수에서는 반복문에서 디바이스의 인덱스를 전달받아서 연결하도록 설계된 코드이다.


물론 스레드이기 때문에 실행 순서 자체는 보장할 수 없지만, 적어도 각 스레드가 매개변수의 값으로 0, 1, 2, 3, 4, 5, 6, 7, 8, 9를 전달받는 것을 기대하고 설계된 코드라고 볼 수 있다.


 

하지만 실행결과를 보면 각 스레드가 전달받은 매개변수 값은 1, 2, 3, 4, 5, 5, 6, 8, 8, 10으로 0, 7, 9를 전달받은 스레드는 없고 5와 8을 전달받은 스레드는 두 개씩 있는 엉망진창인 상태인 것을 볼 수 있다.


이 상황이 의미하는 것은 스레드의 매개변수로 넣은 반복문의 인덱스 값이 스레드가 시작되기 전에 변경되면 스레드의 매개변수 값 역시 영향을 받는다는 것이다.


// int i = 0 -> 반복문에 사용될 인덱스 값 설정

for(int i = 0; i < DeviceNum; i++)

{// i < DeviceNum -> 인덱스 값이 반복문 내의 코드 블럭을 실행하기에 유효한지 검사

    new Thread(() => Run(i)).Start(); // 스레드 생성 

    // i 값이 증가하기 전에 스레드가 시작되면 원래 값이 들어간다.

}// i++ 값 증가 // i 값이 증가한 이후에 스레드가 시작되면 i + 1 값이 들어간다.


각 코드 진행 상황에 대한 해설을 달자면 위와 같다. i값이 증가한 이후에 스레드가 시작되는 것이 문제로 스레드가 시작되기 전까지 전달되는 값이 변하지 않을 것에 대한 보장이 필요한 상태이다.


이를 위해서 코드를 다음과 같이 변경해보자.


class ThreadTestProgram

{

    public static int DeviceNum = 10;


    public static void Main(string[] args)

    {

        for (int i = 0; i < DeviceNum; i++)

        {

            int idx = i; // i 값이 바뀌어도 상관없도록 임시 변수에 값을 전달하여 스레드의 매개 변수로 사용

            new Thread(() => Run(idx)).Start();

        }

    }


    public static void Run(int idx)

    {

        // 디바이스 인덱스에 따라서 스레드 별로 각 디바이스와 연결하는 작업...

        Console.WriteLine(idx);

    }

}

 

위의 임시 코드처럼 i의 값을 임시 변수에 전달해서 스레드에 매개변수로 전달하면 i값이 증가해도 idx의 값은 증가하지 않기 때문에 스레드가 실행될 때까지 값이 변조되지 않을 것이다.


 

실제로 코드를 컴파일해보면 실행순서는 섞여있지만 각 스레드가 디바이스 인덱스로 0, 1, 2, 3, 4, 5, 6, 7, 8, 9를 받은 것을 확인할 수 있다.

반응형

Thread 

여러 작업을 동시 처리하기


일반적으로 우리가 사용하는 운영체제(Operation System, OS)은 멀티 태스크를 지원한다. 그 덕분에 우리는 구글에서 자료를 찾으면서, 유튜브에서 강좌를 듣고, 동시에 비주얼 스튜디오에서 작업을 할 수 있으며 그와 동시에 오디오 재생 프로그램을 통해서 음악을 들을 수 있다. 이때 구글과 유튜브에 접속할 수 있게 해주는 브라우저, 코드 작업을 하는 비주얼 스튜디오, 음악을 재생한느 오디오 재생 프로그램이 각각 하나의 프로세스(Process)이다.


또 여기서 이 프로세스는 하나 이상의 스레드(Thread)로 이루어진다. 스레드는 프로세스를 여러 개의 조각으로 나눈 것으로, 한 OS에서 여러 프로세스가 작업하는 것처럼, 한 프로세스에서 여러 스레드가 동시에 작업을 처리할 수 있게 해준다. 방금 앞에서 든 예시 중에 오디오 재생 프로그램을 예시로 들자면, 오디오 프로그램은 하나의 프로세스으로, 그 안에서 여러 스레드로 나뉘어서 한 스레드는 음악을 재생하고, 또 다른 스레드는 가사를 보여주면서 음악 재생 시간에 맞춰서 싱크를 맞추는 등의 방식으로 동시에 여러 가지 작업을 동시에 처리하는 것이다.



스레드 생성/시작하기


그럼 이 스레드를 사용하기 위한 방법을 차근차근 배워보자.


using System.Threading;


스레드에 관련된 기능들은 System.Threading 네임스페이스에 포함되어 있다. System.Threading.* 처럼 일일이 네임스페이스를 입력해서 코드를 작성해줄 수도 있지만 가독성 문제와 작업 효율성을 위해서 using 선언을 해주자.


using System;

using System.Threading;


namespace ThreadTest

{

    class ThreadTestProgram

    {

        public static void Main(string[] args)

        {

            Run(0);

            Run(1);

        }


        public static void Run(int idx)

        {

            Console.WriteLine(string.Format("Run {0} Start"idx));

            for (int i = 0; i < 10; i++)

            {

                Console.WriteLine(string.Format("Run {0} :: {1}"idx, i));

            }

            Console.WriteLine(string.Format("Run {0} End"idx));

        }

    }

}


우선 스레드를 사용하지 않는 경우의 코드를 먼저 확인해보자. 위의 코드는 스레드를 전혀 사용하지 않고 Run() 함수가 두 번 연속 호출된다. 


 

이렇게 스레드를 사용하지 않고 Run() 함수를 두 번 호출하면 모두가 알다시피 코드는 순차적으로 진행해서 첫 번째 Run(0) 함수가 완전히 끝난 후에야 두 번째 Run(1) 함수가 동작한다.


using System;

using System.Threading;


namespace ThreadTest

{

    class ThreadTestProgram

    {

        public static void Main(string[] args)

        {

            Thread thread = new Thread(() => Run(0));

            thread.Start();

            Run(1);

        }


        public static void Run(int idx)

        {

            Console.WriteLine(string.Format("Run {0} Start"idx));

            for (int i = 0; i < 100; i++)

            {

                Console.WriteLine(string.Format("Run {0} :: {1}"idx, i));

            }

            Console.WriteLine(string.Format("Run {0} End"idx));

        }

    }

}


이번에는 스레드를 생성해서 첫 번째 Run(0) 함수를 스레드로 호출하게 했다. 그리고 반복문 10회로는 동시 실행을 판별하기 어려워서 반복 횟수를 100회로 늘렸다.


 

스레드를 사용한 후의 실행결과는 어느 함수가 끝나기 전에 두 함수가 동시에 진행되고 있음을 충분히 알 수 있다.


Thread thread = new Thread(() => Run(0));

thread.Start();

 

스레드를 사용하는 방법은 간단하게 Thread 객체를 생성하고 생성자의 매개변수로 스레드로 돌리고자 하는 함수를 넣어준 뒤 Start() 함수를 호출하면 된다. 스레드를 생성하기만 하고 Start() 함수를 호출하지 않으면 그 스레드는 동작하지 않는다.



스레드 양보하기


위의 스레드 실행 예시 이미지를 보면 스레드가 몇 번의 연산을 처리하고 잠시 다른 스레드에 처리 시간을 넘겨주고 다시 돌려받는 것을 알 수 있다. 스레드 프로그래밍에서는 이런 CPU 점유 상태를 다른 스레드에 언제 얼마동안 양보할 지를 알리는 함수가 있는데 이것이 바로 Thread.Sleep() 함수다.


Thread.Sleep(10);


Thread.Sleep() 함수는 해당 함수를 호출한 스레드가 매개변수의 시간만큼 쉬면서 다른 스레드에 처리 우선권을 양보하게 만든다. 매개변수의 시간 단위는 밀리세컨드(Milisecond)로 1000분의 1초에 해당한다. 즉 위 코드에 적힌 시간으로는 0.001초 동안 다른 스레드에 처리 우선권을 양보한다는 의미이다.


using System;

using System.Threading;


namespace ThreadTest

{

    class ThreadTestProgram

    {

        public static void Main(string[] args)

        {

            Thread thread0 = new Thread(() => Run(0));

            thread0.Start();

            Thread thread1 = new Thread(() => Run(1));

            thread1.Start();

        }


        public static void Run(int idx)

        {

            Console.WriteLine(string.Format("Run {0} Start"idx));

            for (int i = 0; i < 100; i++)

            {

                Console.WriteLine(string.Format("Run {0} :: {1}"idx, i));

                Thread.Sleep(10);

            }

            Console.WriteLine(string.Format("Run {0} End"idx));

        }

    }

}

 

이번에는 Run(0)와 Run(1) 함수를 모두 스레드로 호출했으며 반복문 중간에 Sleep() 함수를 추가했다.


 

이번 실행결과를 보면 Sleep() 함수를 사용하지 않을 때와는 다르게 허용된 시간에 최대한 몰아서 처리하지 않고 필요한 계산만 처리한 뒤에 바로 다른 스레드에게 처리 우선권을 넘기는 것을 확인할 수 있다.





스레드 중단하기


thread.Abort();

thread.Join();


작동 중인 스레드를 중지하는 방법은 두 가지가 있는데 Abort() 함수와 Join() 함수가 그것이다. 이 두 함수의 차이는 다음과 같다.


Abort() :: 함수의 종료를 보장하지 않고 어느 시점이던지 상관 없이 도중에 강제로 중단시킨다.

Join() :: 함수의 종료를 보장하며 스레드가 동작시키는 중인 함수의 끝에 도달하기를 기다린 다음에 스레드를 닫는다.


using System;

using System.Threading;


namespace ThreadTest

{

    class ThreadTestProgram

    {

        public static void Main(string[] args)

        {

            Thread thread0 = new Thread(() => Run(0));

            thread0.Start();

            Thread.Sleep(100);

            thread0.Abort();


            Thread thread1 = new Thread(() => Run(1));

            thread1.Start();

            Thread.Sleep(100);

            thread1.Join();

        }


        public static void Run(int idx)

        {

            Console.WriteLine(string.Format("Run {0} Start"idx));

            for (int i = 0; i < 100; i++)

            {

                Console.WriteLine(string.Format("Run {0} :: {1}"idx, i));

                Thread.Sleep(10);

            }

            Console.WriteLine(string.Format("Run {0} End"idx));

        }

    }

}


thread0은 Abort() 시키고 thread1은 Join() 시키는 코드를 작성한다음 컴파일 해보자.


 

Run(0)는 반복문이 동작하던 도중에 중단되고, Run(1)은 End까지 무사히 호출되고 종료된 것을 확인할 수 있다.


위듸 예시를 통해 알 수 있듯이 Abort() 함수의 경우에는 스레드를 작동 도중에 강제로 종료하기 때문에 스레드 강제 종료가 시스템에 심각한 영향을 끼치지 않는다는 보장이 있을 때만 사용하는 것이 좋다.


class ThreadTestProgram

{

    public static void Main(string[] args)

    {

        Thread thread0 = new Thread(() => Run(0));

        thread0.Start();

        Thread.Sleep(100);

        thread0.Abort();

    }


    public static void Run(int idx)

    {

        try

        {

            int runIdx = idx;

            Console.WriteLine(string.Format("Run {0} Start", runIdx));

            for (int i = 0; i < 100; i++)

            {

                Console.WriteLine(string.Format("Run {0} :: {1}", runIdx, i));

                Thread.Sleep(10);

            }

            Console.WriteLine(string.Format("Run {0} End", runIdx));

        }

        catch (Exception e)

        {

            Console.WriteLine(e);

        }

    }

}


스레드를 Abort() 함수로 강제 종료할 때 해당 스레드 함수에서는 System.Threading.ThreadAbortException이라는 예외를 발생시킨다. 만약 스레드를 Abort() 시켰을 때, 리소스 정리 등의 뒤처리 작업이 필요한 경우라면 반드시 해당 스레드 함수에서 발생하는 ThreadAbortException 예외를 받아서 정리 작업을 진행하는 것이 좋다.



스레드 동기화(Thread Synchronization)


여러 개의 스레드를 두고 작동하는 프로그램의 경우에, 여러 스레드가 자원이나 변수 등을 공유하는 경우가 많다. 다음의 예시를 보자.


class ThreadTestProgram

{

    public class Villige

    {

        public int population = 1000

            

        public void AddVillager()

        {

            population++;


            for(int i = 0; i < population; i++)

            {

                for(int j = 0; j < population; j++)

                {


                }

            }

            // 추가된 주민에게 주민번호 주기

            Console.WriteLine(string.Format("새 주민의 주민번호 :: {0}", population));

        }

    }


    public static void Main(string[] args)

    {

        Villige manager = new Villige();

        for(int i = 0; i < 10; i++)

        {

            new Thread(new ThreadStart(manager.AddVillager)).Start();

        }

    }

}


작은 마을을 키우는 게임을 만든다고 가정했을 때, 마을에 새로운 마을 주민이 태어나거나 새로 들어오면 인구 수를 늘려주고 몇 가지 처리를 한 뒤에 주민번호를 매겨주는 AddVillager() 함수를 구현했다. 그리고 주민번호는 고유한 번호이기 때문에 각 주민 마다 번호가 중복되어서는 안된다고 가정해보자. 이 때 마을 주민이 동시에 추가될 수도 있기 때문에 스레드 처리를 한다.


그런데 플레이 도중에 마을에 10명의 주민이 동시에 추가되었다고 해보자. 그러면 현재까지 1000명의 주민이 있었으니 그 뒤에 추가되는 주민들의 번호는 1001, 1002, 1003, ..., 1009, 1010이 되기를 기대할 것이다.


 

하지만 실행결과는 새 주민들의 주민번호가 중복되어서 발급되어 버렸다. 이러한 문제를 스레드 세이프 하지 않다(Not thread-safe)라고 하는데 이 문제를 해결하기 위해서 필요한 것이 바로 스레드 동기화이다. 스레드 동기화는 하나의 공용된 자원이나 변수에 여러 개의 스레드가 접근할 때, 스레드들이 순서를 지켜서 사용하고 다른 스레드가 사용 중일 때는 사용하지 못하게 만드는 것이다.


class ThreadTestProgram

{

    public class Vilige

    {

        public int population = 1000;


        public object populationLock = new object();


        public void AddHuman()

        {

            lock (populationLock)

            {

                population++;


                for (int i = 0; i < population; i++)

                {

                    for (int j = 0; j < population; j++)

                    {


                    }

                }

                // 추가된 주민에게 주민번호 주기

                Console.WriteLine(string.Format("새 주민의 주민번호 :: {0}", population));

            }

        }

    }


    public static void Main(string[] args)

    {

        Vilige manager = new Vilige();

        for(int i = 0; i < 10; i++)

        {

            new Thread(new ThreadStart(manager.AddHuman)).Start();

        }

    }

}


스레드를 동기화하는 방법은 lock을 사용사는 것이다. 스레드 락을 하기 위한 객체를 하나 만들어서 lock()을 해주면 lock() { } 으로 묶어준 블럭이 한 스레드에서 실행되는 동안에는 같은 객체의 lock으로 묶인 스레드는 멈춘 상태로 해당 코드를 진행하지 못하게 된다.


 

스레드를 lock() 함수로 동기화하여 실행하면 새로 들어온 주민들의 주민번호가 겹치지 않고 정상적으로 매겨지게 된다.


이런 스레드 동기화에도 단점은 있는데 스레드 동기화되는 부분은 동시 처리가 안되고 한 스레드씩 작업을 진행하기 때문에 프로그램의 속도가 느려질 수 있다.


 

그리고 스레드의 동기화 구조가 복잡한 경우라면, 위의 이미지처럼 두 개의 스레드가 두 자원을 사용하려고 할 때, 스레드 1이 자원 1을 사용하며 자원 2가 풀리기를 기다리고 있고 스레드 2가 자원 2를 사용하며 자원 1이 풀리기를 기다려서 두 스레드가 멈춰버리는 데드락(Dead lock, 교착상태)이 발생할 수도 있다.


이렇게 스레드는 동시 처리를 하기에 유용한 방법이지만, 호출 순서를 보장할 수 없고 디버깅이 어려운 구조이기 때문에 잘못 사용할 경우 해결하기 어려운 문제를 발생시키기 쉽다. 그러므로 스레드를 사용할 때는 조심해서 사용해야만 한다.

반응형
  1. 질문충 2020.09.26 22:40

    4스레드짜리 시피유로 24스레드까지 만들어도 돌아가는데 기계적인 부분과는 무관한건가요?

    • wergia 2020.10.20 00:05 신고

      네, CPU 사양으로 표시되는 코어나 스레드 수와는 무관하게 메모리가 허용하는 양만큼 스레드를 만들 수 있다고 하네요.

static 

정적 변수와 정적 함수 그리고 정적 클래스


static 키워드는 변수나 함수, 클래스에 정적 속성을 부여하는 것으로 클래스로부터 객체를 생성하지 않고 변수나 함수를 호출할 수 있도록 해주는 것이다.



정적 변수


public class StaticTestClass

{

    public static int score;

}


정적 변수를 선언하기 위해서는 위의 예시 코드와 같이 static 키워드를 붙여서 변수를 정의하면 된다. 이렇게 선언한 정적 변수는 클래스로부터 객체를 생성하지 않아도 [클래스명.변수이름]의 형식으로 곧바로 사용할 수 있게 된다. 


public class MainClass

{

    public void Main()

    {

        StaticTestClass.score = 10;

    }

}


클래스의 일반 멤버 변수는 클래스의 객체가 생성될 때, 각 객체마다 따로 생기지만, 정적 변수는 해당 클래스가 처음으로 사용되는 때에 한 번만 초기화되어 계속 동일한 메모리를 사용하게 된다.

 

 

도식으로 보면 위의 그림과 같다. 정적 변수를 포함한 클래스 A의 객체를 두 개를 생성하여 각 이름을 object1, object2라고 했을 때, 각 인스턴스에는 정적 변수가 포함되지 않으며, 일반 멤버 변수만 포함된다. 클래스 A의 정적 변수는 클래스 A가 처음 사용되는 시점에 별도의 메모리 공간에 할당된다.


 

생성된 객체에 정적 변수가 포함되지 않는 것은 실제로 객체를 생성해서 멤버 변수를 찾았을 때, 목록에 나오지 않는 것을 보면 확인할 수 있다.



정적 함수


public class StaticTestClass

{

    public static int score;


    public int memberInt;


    public static void StaticFunction()

    {

        score = 10 // static 변수는 호출할 수 있다.

        memberInt = 10// static 함수 내에서 멤버변수는 호출할 수 없다.

    }

}


public class MainClass

{

    public void Main()

    {

        StaticTestClass.score = 10;

        StaticTestClass.StaticFunction();

    }

}


함수를 선언할 때, static 키워드를 붙여서 함수를 정의하면 정적 함수를 만들 수 있다. 이 정적 함수 역시 [클래스명.함수이름]의 형식으로 객체를 생성하지 않고 곧바로 호출할 수 있다.


단, 정적 함수는 객체가 생성되기 전에 호출이 가능하기 때문에, 정적 함수 내에서는 정적 변수가 아닌 일반 멤버 변수를 호출할 수 없다.



정적 클래스


public static class StaticTestClass

{

    public static int score;


    static StaticTestClass()

    {

        score = 10;

    }


    public static void StaticFunction()

    {

        score = 20;

    }

}


정적 클래스는 모든 멤버가 정적 변수 혹은 정적 함수로 이루어진 것으로 객체를 생성할 수 없는 클래스이다. 모든 정적 멤버 변수 및 정적 멤버 함수는 [클래스명.변수이름] 혹은 [클래스명.함수이름]으로 호출된다.


정적 클래스는 정적 생성자를 가질 수 있는데 이 정적 생성자는 public, protected, private 등의 액세스 한정자를 사용할 수 없으며, 매개변수 역시 가질 수 없다.

반응형

C++ 코드 생성자에서 콘텐츠 브라우저의 클래스와 리소스 불러오기

 

작성 기준 버전 :: 4.21.1

 

게임을 제작하는 과정에서 객체를 초기화할 때, 프로젝트에 포함된 다른 클래스나 오브젝트, 리소스를 가져와야하는 경우가 종종 생긴다.

 

 

그런 경우 블루프린트의 이벤트 그래프에서 작업하는 경우라면 위의 이미지와 같이 콘텐츠 브라우저에 있는 리소스나 블루프린트 클래스 등을 곧바로 선택할 수 있지만, C++ 코드에서는 직접 경로를 지정해서 코드를 작성해야 한다.

 

단, C++ 코드에서 직접 경로를 지정해서 리소스나 블루프린트 클래스를 가져올 때, 주의할 점은 리소스나 블루프린트 클래스의 경로나 파일명이 자주 바뀌는 상황을 피하는 게 좋다. 경로를 지정한 이후에 경로가 바뀌지 않을 것이 확실하다면 C++ 코드로 경로를 지정해서 가져오는게 낫겠지만 자주 바뀌는 상황이라면 바뀐 리소스를 불러오는 모든 코드를 일일이 찾아서 수정하고 컴파일하는 문제가 발생한다.

 

그렇기 때문에, 경로나 리소스의 파일명이 자주 바뀔 상황이라면 위의 이미지처럼 블루프린트를 이용해서 초기화를 진행하거나, 별도의 기능을 만들어서 일일이 경로를 지정하고 바꾸는 작업을 자동화시키는 것이 좋다.

 

우선 C++ 코드에서 콘텐츠 브라우저의 리소스나 블루프린트 클래스를 가져오기 위해서는 다음의 헤더를 전처리기로 포함시켜주어야 한다.

 

#include "UObject/ConstructorHelpers.h"

 

ConstructorHelpers는 생성자에 도움을 주는 클래스로 생성자에서 콘텐츠 브라우저의 리소스나 블루프린트 클래스를 불러오는 작업을 도와주는 기능들을 가지고 있다. ConstructorHelpers는 생성자에서 사용되는 기능이기 때문에 생성자 이외의 장소에서 ConstructorHelpers를 사용하려고 시도하면 컴파일 에러가 발생하게 된다.

 

 

C++ 코드에서 블루프린트 클래스 가져오기

 

콘텐츠 브라우저 패널에 Blueprints 폴더 안에 TestBlueprintClass라는 이름의 APawn 클래스를 상속받은 블루프린트 클래스가 있다고 가정할 때, 그것을 C++ 코드에 가져오기 위해서는 다음 예시와 같이 코드를 작성하면 된다.

 

static ConstructorHelpers::FClassFinder<APawn> BPClass(TEXT("/Game/Blueprints/TestBlueprintClass"));
if (BPClass.Succeeded() && BPClass.Class != NULL)
{
    // 가져온 BPClass.Class를 통한 작업
}

 

FString 경로를 통해서 불러오는 것이니 만큼, 오타나 변경된 경로나 파일명으로 인해서, 클래스가 제대로 불러와지지 않는 경우가 발생할 수 있기 때문에, Succeeded() 함수와 Class의 NULL 체크를 통해서 성공적으로 클래스가 불러와졌는지 체크하고 사용해야 한다.

 

클래스 탐색자(Class Finder)는 성공적으로 블루프린트 클래스를 가져온 경우, Class 멤버 변수 안에 TSubclassOf<T> 타입으로 해당 클래스를 가지고 있게 된다. 이것을 이용해서 필요한 작업을 진행하면 된다.

 

 

C++ 코드에서 리소스 가져오기

 

이번에 알아볼 것은 C++ 코드에서 콘텐츠 브라우저 패널의 리소스를 가져오는 과정이다. 리소스의 종류는 여러가지가 될 수 있는데 대표적인 것으로는 스태틱 메시나 텍스처를 예로 들 수 있다.

 

아래의 예시코드는 드롭된 아이템의 메시가 아이템의 종류에 따라서 달라진다는 가정하에 만들어졌다. 콘텐츠 브라우저의 Item/StaticMesh 폴더 안에 SM_Helmet 이라는 이름을 가진 헬멧 모양의 스태틱 메시가 있을 때, FObjectFinder를 통해서 가져올 수 있다.

 

DropItemStaticMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("DropItemMesh"));
RootComponent = DropItemStaticMeshComponent;

 

static ConstructorHelpers::FObjectFinder<UStaticMesh> HelmetStaticMesh(TEXT("/Game/Item/StaticMesh/SM_Helmet"));
if (HelmetStaticMesh.Succeeded() && HelmetStaticMesh.Object != nullptr)
{
    DropItemStaticMeshComponent->SetStaticMesh(HelmetStaticMesh.Object);
}

 

FObjectFinder를 통해서 가져온 오브젝트 역시 Succeeded() 함수와 Object 변수의 null 체크를 통해서 리소스가 제대로 불러와졌는지 체크를 한 뒤 사용해야 한다.

 

 

 

 

C++ 코드에서 C++ 클래스 가져오기

 

C++ 코드에서 블루프린트 클래스가 아닌 직접 작성한 C++ 클래스를 가져와서 사용하고 싶을 수도 있다. 예를 들어 게임 모드 클래스에서 기본 폰이나 기본 플레이어 컨트롤러를 설정하려고 할 때, C++로 작성한 폰 클래스나 플레이어 컨트롤러 클래스를 기반으로 블루프린트 클래스를 생성해서 넣어주는게 아니라 C++ 클래스를 곧바로 코드에서 넣어주고자 한다면 다음 예시 코드와 같이 작성하면 된다.

 

AYourProjectGameMode::AYourProjectGameMode()
{
    PlayerControllerClass = AYourCustomPlayerController::StaticClass();
}

 

StaticClass() 함수를 이용하면 런타임 중에 해당 클래스를 나타내는 UClass를 얻어낼 수 있다.

반응형

데이터 테이블(Data Table) 사용하기


작성 기준 버전 :: 4.21.1


게임을 제작할 때 레벨업에 필요한 경험치량이나 스킬의 계수 등 추후에 밸런스 수정 작업이 필요한 값들은 함부로 코드에 상수로 넣어서는 안된다. 이런 부분은 기획자가 손쉽게 접근이 가능해야 하기 때문에, 기획자들이 주로 사용하는 엑셀이나 스프레드시트의 데이터를 언리얼 엔진으로 임포트해서 사용하는 방식을 지원한다. 이것을 데이터 주도형 접근법이라고 한다.


언리얼 엔진에서는 기획자들이 주로 사용하는 엑셀이나 스프레드시트에서 손쉽게 만들어낼 수 있는 .CSV 파일이나 서버 프로그램에서 주로 사용되는 JSON 파일을 손쉽게 임포트하는 기능을 제공한다.



데이터 테이블 임포트

데이터 테이블은 유용한 방식으로 짜여진 표를 의미한다. .CSV 파일을 임포트하기 위해서는 우선 프로그래머가 데이터를 엔진이 인식할 수 있게 Row 컨테이너를 만들어서 엔진에 데이터 해석 방식을 알려줘야 한다.


우리가 예시로 사용할 .CSV 파일은 다음 레벨업까지 필요한 경험치의 양에 대한 것이고 그 내용은 다음과 같다.


Name,ExpToNextLevel,TotalExp
1,0,0
2,100,100
3,200,300
4,300,500
5,400,700
6,500,900
7,600,1100
8,700,1300
9,800,1500
10,1600,2400


LevelUpTable.csv


이런 컨테이너를 만드는 방법은 두 가지가 있는데 블루프린트를 이용하는 방식과 C++ 코드를 통해 만드는 방식이 있다.



블루프린트


데이터 테이블 로우를 만들기 위해서는 구조체를 생성해야 한다. 구조체의 이름은 BP_LevelUpTableRow로 한다.


 

블루프린트 구조체가 생성되면 더블클릭해서 블루프린트 구조체 에디터를 열고 변수를 추가한다. 추가하는 변수의 이름은 ExpToNextLevel과 TotalExp로 각 열의 이름과 순서가 일치해야 한다. 제일 첫 열인 Name은 게임 내에게 각 행에 접근하는 이름이 되는 것으로 따로 변수를 추가하지 않아도 된다.



변수를 모두 추가한 뒤에는 구조체를 저장하고 에디터를 닫는다. 그리고 콘텐츠 브라우저 패널에서 파일 창에 우클릭하여 /Game에 임포트... 를 선택한다.



CSV 파일을 임포트한다.


 

데이터 테이블 옵션 창이 뜨면 데이터 테이블 행 유형 선택을 방금 추가한 구조체로 설정하고 확인을 누른다.


 

추가된 데이터 테이블을 열어보면 .CSV 파일의 내용이 훌륭하게 임포트된 것을 확인할 수 있다.




C++ 코드


행 컨테이너를 블루프린트 구조체로 만들 경우, C++ 코드에서는 사용할 수 없다는 단점이 있다. C++ 코드에서 사용하기 위해서는 USTRUCT로 만들어야 되는데 언리얼 구조에 대한 설명은 C++ / USTRUCT 사용자 정의 구조체 만들기 문서에서 참고할 수 있다.


우선 Actor 클래스를 상속받아서 CustomDataTables라는 더미 클래스를 생성한다.



클래스가 생성되면 전처리기와 클래스 선언 사이에 구조체를 선언하는 코드를 추가해준다. 행 컨테이너로 사용되는 구조체는 FTableRowBase를 상속받아야만 한다.


// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Engine/DataTable.h"
#include "CustomDataTables.generated.h"

USTRUCT(BlueprintType)
struct FLevelUpTableRow : public FTableRowBase
{
    GENERATED_BODY()

public:

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LevelUp")
        int32 ExpToNextLevel;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LevelUp")
        int32 TotalExp;
};

UCLASS()
class DATATABLETEST_API ACustomDataTables : public AActor
{
    GENERATED_BODY()
   
};


코드를 완성하고 프로젝트를 빌드한 뒤, 에디터로 돌아가서 .CSV 파일을 임포트해서 데이터 테이블 옵션의 데이터 테이블 행 유형 선택 드롭다운 메뉴를 열어보면 우리가 방금 추가한 LevelUpTableRow가 있는 것을 확인할 수 있다.



이를 통해서 .CSV 파일을 임포트하면 아까 블루프린트 구조체를 통해서 임포트했을 때와 동일하게 DataTable이 생성되는 것을 볼 수 있다.






데이터 테이블 사용하기


이번 파트에서는 작성한 데이터 테이블을 사용하는 방법에 대해서 알아보자.


블루프린트에서 데이터 테이블 사용하기


블루프린트에서 데이터 테이블을 사용하기 위해서는 블루프린트 그래프의 빈 자리에 우클릭해서 컨텍스트 메뉴를 열고 "데이터 테이블 행 구하기"를 검색해서 이 노드를 배치하면 된다.



GameModeBase를 상속받는 블루프린트 클래스를 하나 생성한다. 이벤트 그래프의 BeginPlay 이벤트로부터 다음과 같이 블루프린트 그래프를 구성하자.



위 그래프는 LevelUpTable에서 각 행을 가져와서 Total Exp 값을 화면에 출력하는 역할을 한다.


블루프린트를 저장하고 에디터로 가서 월드 세팅의 Game Mode를 방금 추가한 게임 모드로 바꿔준다.



그 다음 에디터에서 플레이 버튼을 눌러보면 화면에 각 레벨의 Total Exp가 연속으로 출력되는 것을 볼 수 있다.




C++ 코드에서 데이터 테이블 사용하기


C++ 코드에서 데이터 테이블을 사용하는 과정은 블루프린트에서 노드 하나만 생성하면 되는 것에 비해서는 조금 복잡하다.


우선 프로젝트에 DataTableTestGameModeBase라는 이름으로 새 게임 모드 클래스를 만들고 헤더에 다음 멤버 변수와 함수를 추가한다.


public:
    ADataTableTestGameModeBase();

    virtual void BeginPlay() override;

private:
    class UDataTable* LevelUpDataTable;


그리고 DataTableTestGameModeBase라는 .cpp로 가서 다음 전처리기를 추가한다.


#include "CustomDataTables.h"
#include "UObject/ConstructorHelpers.h"


ADataTableTestGameModeBase::ADataTableTestGameModeBase() 생성자 함수와 BeginPlay() 함수를 다음과 같이 구현한다.


ADataTableTestGameModeBase::ADataTableTestGameModeBase()
{
    static ConstructorHelpers::FObjectFinder<UDataTable> DataTable(TEXT("/Game/LevelUpTable"));
    if (DataTable.Succeeded())
    {
        LevelUpDataTable = DataTable.Object;
    }
}

void ADataTableTestGameModeBase::BeginPlay()
{
    Super::BeginPlay();

    if (LevelUpDataTable != nullptr)
    {
        for (int32 i = 1; i <= 10; i++)
        {
            FLevelUpTableRow* LevelUpTableRow = LevelUpDataTable->FindRow<FLevelUpTableRow>(FName(*(FString::FormatAsNumber(i))), FString(""));
            UE_LOG(LogTemp, Log, TEXT("Lv.%d :: ExpToNextLevel(%d) TotalExp(%d)"), i, (*LevelUpTableRow).ExpToNextLevel, (*LevelUpTableRow).TotalExp);
        }
    }
}


코드 작성이 완료되면 프로젝트를 빌드하고 에디터로 넘어간다.


그 다음 월드 세팅에서 게임 모드를 방금 만든 것으로 교체한다.


 

플레이 버튼을 눌러보면 게임이 시작되면서 데이터 테이블의 값들을 가져와서 로그를 출력하는 것을 확인할 수 있다.



반응형
  1. Game 2021.02.17 13:38

    감사합니다

  2. 익명 2022.05.11 07:32

    비밀댓글입니다

    • 익명 2022.05.18 08:51

      비밀댓글입니다

캐스팅(Casting)

 

 

작성 기준 버전 :: 4.21 - 4.26

 

프로그래밍 작업을 할 때 캐스팅, 즉 형 변환은 상당히 중요하다. 특히 여러 클래스가 상속으로 엮여있는 상황이라면 더더욱 중요해진다. 언리얼 엔진에서는 대부분의 클래스가 AActor 클래스를 상속받고 있고, 개발자가 만들어내는 클래스 역시 상당수는 AActor를 상속받게 된다.

 

C++

 

그렇기 때문에 몇몇 함수들은 이렇게 메인이 되는 부모 클래스를 매개변수로 받거나 돌려준다. 간단한 예를 들자면 다음 함수가 있다.

 

void AActor::NotifyActorBeginOverlap(AActor* OtherActor)
{

}

 

NotifyActorBeginOverlap() 함수는 액터의 콜리전에 콜리전을 가진 다른 액터가 들어오기 시작했을 때 호출되는 함수로, 매개변수를 통해서 자신의 콜리전과 접촉한 액터를 알려준다. 언리얼 엔진에서는 레벨에 배치되는 모든 오브젝트는 AActor 클래스를 상속받기 때문에, 콜리전과 접촉한 액터가 어떤 클래스던지 상관없이 무조건 AActor 클래스로 보내주는 것이다.

 

만약 콜리전 체크를 하는 액터가 겹침 이벤트가 발생할때마다 데미지를 입는 클래스인데 데미지를 입힐 수 있는 클래스가 AProjectile 클래스라고 가정했을 때, 위의 예시 코드처럼 별도의 검사를 하지 않는다면, 액터가 아무 물체에나 스칠 때마다 데미지를 입어버릴 것이다.

 

그래서 필요한 것이 바로 캐스팅이다. 언리얼 엔진에서는 Cast<T>() 라는 함수로 기본적인 캐스팅을 제공한다.

 

void AActor::NotifyActorBeginOverlap(AActor* OtherActor)
{
    AProjectile* Projectile = Cast<AProjectile>(OtherActor);
    if (Projectile)
    {
        // Damage Process
    }
}

 

바로 위의 예시 코드처럼 캐스팅을 진행하면 된다. 만약 콜리전에 검출된 액터가 AProjectile 클래스가 아니라면 캐스팅에 실패할 것이고 Projectile 변수의 값을 nullptr이 되기 때문에 if문 안으로 진행하지 못해서 Damage Process가 진행되지 않는다.

 

블루프린트

 

블루프린트 작업에서도 캐스팅이 가능하다.

 

 

 

블루프린트 컨텍스트 메뉴에서 "형변환"이나, 캐스팅하고자 하는 타입의 클래스 명을 검색하면 해당 클래스로 형변환할 수 있는 노드를 추가할 수 있다.

반응형
  1. 시우 2019.05.28 08:51

    좋은 자료 감사합니다

  2. scarecrow1992 2020.08.10 19:46 신고

    감사합니다~

RPG :: 캐릭터를 가리는 벽 투명하게 만들기


작성 기준 버전 :: 4.21.1


탑/다운에 가까운 쿼터뷰에서 내려보는 RPG에서는 건물이나 벽, 기둥 같은 오브젝트에 캐릭터가 가려지는 경우가 많다.



이런 경우 플레이어는 자신의 캐릭터의 위치를 찾기가 어려워지고 플레이에 매우 큰 지장을 준다. 그렇기 때문에 개발자들은 이런 문제를 해소하기 위해서 여러 가지 테크닉을 사용하는데 그 중 대표적인 것이 캐릭터를 가리고 있는 벽을 투명하게 만드는 것이다.


이 섹션에서는 캐릭터를 가리고 있는 벽을 투명하게 만드는 방법에 대해서 배워볼 것이다.


본격적인 내용에 들어가기에 앞서, 이 섹션은 진행하기 위해서는 다음과 같은 선행 지식이 필요하다.


머티리얼 인스턴싱 :: 머티리얼 파라미터와 머티리얼 인스턴스

C++ 코드에서 머티리얼 인스턴스 다이내믹 생성하고 다루기

콜리전과 콜리전 이벤트


이번 섹션은 지난 섹션 중 RPG :: 마우스 입력 이동 구현하기 섹션에 이어서 진행되는 섹션이다.



설계


이 기능을 구현하기 위한 설계는 다음과 같다.


1. 벽이나 기둥 같은 오브젝트가 가리는 위치에 캐릭터가 존재하고 있는지 확인하기 위한 콜리전을 깔아줄 SeeingThroughCollision 클래스


2. SeeingThroughCollision에게 신호를 받아서 투명하게 만들어질 SeeingThroughActor



이렇게 바닥에 콜리전을 까는 방법 이외에도 캐릭터와 카메라 사이에 콜리전을 두고 여기에 벽이나 기둥같은 오브젝트가 닿으면 투명하게 만드는 방법도 생각해볼 수 있다.



기능 구현


SeeingThroughActor


SeeingThroughActor 구현


새 C++ 클래스를 하나 추가하자. Actor 클래스를 상속방아서 SeeingThroughActor라는 이름으로 클래스를 생성한다.


 

이 클래스는 콜리전으로부터 통지를 받아서 자신의 머티리얼을 투명하게 하는 역할을 한다.


SeeingThroughActor.h에 다음 멤버 변수들을 추가한다.


private:
    UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"))
    class UStaticMeshComponent* SeeingTroughMesh;

    UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"))
    float SeeingThroughTime;

    bool bSeeingThrough;

    float RunningTimer;


먼저 SeeingThroughMesh는 메시가 가진 머티리얼을 다이내믹 인스턴스로 만들어서 투명화 작업을 하기 위한 변수이다.


SeeingThroughTime은 메시가 투명해지는데까지 걸리는 시간에 대한 변수이다. 이 변수는 외부에 공개해서 다른 개발자가 수정할 수 있게 하였다.


bSeeingThrough는 지금 메시가 투명해지는 중인지 아니면 불투명해지는 중인지에 대한 변수이다.


RunningTimer는 진행도를 위한 변수이다.


그 다음에는 SeeingThroughActor.cpp로 가서 스태틱 메시 컴포넌트의 기능을 사용하기 위해서 다음 전처리기를 추가한다.


#include "Engine/Classes/Components/StaticMeshComponent.h"


그리고 ASeeingThroughActor::ASeeingThroughActor() 생성자 함수로 가서 멤버 변수들을 초기화하는 코드를 추가한다.


SeeingTroughMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SeeingThroughMesh"));
RootComponent = SeeingTroughMesh;
SeeingTroughMesh->CreateDynamicMaterialInstance(0);

SeeingTroughMesh->SetCollisionProfileName(TEXT("InvisibleWall"));

SeeingThroughTime = 0.3f;
RunningTimer = 0.0f;
bSeeingThrough = false;


SeeingThroughMesh를 생성한 뒤, RootComponent로 설정해주고 다이내믹 머티리얼 인스턴스를 생성한다. 그리고 콜리전 프로필을 InvisibleWall로 설정해주는데, 이것은 벽이 물체는 가로막고 마우스를 클릭했을때 발생하는 트레이스는 통과시키기 위함이다.


그리고 투명해지는데 걸리는 시간은 기본으로 0.3초로 설정한다.


BeginPlay() 함수에 다음 코드를 추가한다.


PrimaryActorTick.SetTickFunctionEnable(false);


게임 시작했을때 Tick() 함수를 작동하지 않도록 만들어서 성능 낭비가 없도록 한다.


그 다음 Tick() 함수를 다음과 같이 구현한다.


void ASeeingThroughActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    RunningTimer += DeltaTime;

    float Opacity = 0.0f;
    if (bSeeingThrough)
    {
        Opacity = FMath::Lerp(1.0f, 0.0f, RunningTimer * (1.0f / SeeingThroughTime));
    }
    else
    {
        Opacity = FMath::Lerp(0.0f, 1.0f, RunningTimer * (1.0f / SeeingThroughTime));
    }

    SeeingTroughMesh->SetScalarParameterValueOnMaterials(TEXT("Opacity"), Opacity);

    if (RunningTimer > SeeingThroughTime)
    {
        RunningTimer = 0.0f;
        PrimaryActorTick.SetTickFunctionEnable(false);
    }
}


위의 코드는 틱이 작동하는 동안 bSeeingThrough의 상태에 따라서 다이내믹 머티리얼 인스턴스의 머티리얼 파라미터 "Opacity"를 0에서 1로 만들거나 1에서 0으로 만들고 작동이 끝나면 Tick() 함수의 작동을 멈추게 한다. 참고로 머티리얼 파라미터 "Opacity"는 이후 작업에서 추가한다.


다음 함수의 선언을 헤더에 추가한다.


public:

    void SetShowSeeingThrough(bool bThroughShow);


그리고 SeeingThroughActor.cpp로 가서 함수 구현을 완료한다.


void ASeeingThroughActor::SetShowSeeingThrough(bool bThroughShow)
{
    bSeeingThrough = bThroughShow;
    if (RunningTimer != 0.0f)
    {
        RunningTimer = SeeingThroughTime - RunningTimer;
    }
    PrimaryActorTick.SetTickFunctionEnable(true);
}


SetShowSeeingThrough() 함수는 bShow 변수를 받아서 액터가 투명해지기 시작하는지 불투명해지기 시작하는지 결정한 뒤 Tick() 함수를 작동시킨다.


코드 작업이 끝났다면 솔루션 탐색기에서 프로젝트를 빌드한 뒤, 에디터로 돌아간다.


투명해지는 만들기


이번에는 SeeingThroughActor가 사용할 머티리얼을 만들 차례이다. Props 폴더 안에 Materials 폴더를 만든 다음, 콘텐츠 브라우저 패널의 파일 창에 우클릭해서 머티리얼을 선택한다. 그리고 생성된 머티리얼의 이름을 M_SeeingThrough로 한다.



머티리얼을 더블클릭해서 머티리얼 에디터를 열고 디테일 패널에서 Material 카테고리의 Blend Mode를 Translucent로 설정한다.



그리고 TextureSamleParameter2D와 ScalarParameter를 추가하고 각각 이름을 Texture와 Opacity로 한다.



그리고 Opacity 파라미터 노드를 선택한 뒤, 디테일 패널에서 Default Value를 1로 설정한다.


그 다음, 적용과 저장을 하고 머티리얼 에디터를 닫는다.


언리얼 에디터로 돌아와서, 콘텐츠 브라우저 패널에서 방금 만든 머티리얼을 우클릭하고 머티리얼 인스턴스 생성을 선택한다.



머티리얼 인스턴스가 만들어지면 더블클릭해서 머티리얼 인스턴스 에디터를 열고 디테일 패널에서 Opacity를 체크해주고 머티리얼 인스턴스를 저장한 뒤, 머티리얼 인스턴스 에디터를 닫는다.



SeeingThroughActor 배치


이제 SeeingThroughActor를 배치할 차례이다. 콘텐츠 브라우저 패널에서 SeeingTroughActor를 찾아서 벽 토대 위에 배치한다.



SeeingThroughActor의 SeeingThroughMesh를 선택하고 Static Mesh에 Box를 할당한다.



그 다음, Material에 방금 전에 만든 M_SeeingThrough_Inst를 할당한다.


 

SeeingThroughActor의 스케일을 {1.0, 12.0, 5.0}으로 설정한다.



액터를 복사해서 다음과 같이 만든다.






SeeingThroughCollision


SeeingThroughCollision 구현


Actor 클래스를 상속받아서 SeeingThroughCollision 이라는 이름으로 클래스를 생성한다.


 

SeeingThroughCollision.h에 다음 멤버 변수 선언을 추가한다.


private:
    UPROPERTY()
    class UBoxComponent* SeeingThroughCollision;

    UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"))
    TArray<class ASeeingThroughActor*> SeeingThroughActors;


SeeingThroughActor를 배열로 선언한 이유는 SeeingThroughCollision과 SeeingThroughActor를 1:1 매치를 시킬 수도 있지만, 1:N의 매치도 가능하게 유연성을 주기 위한 것이다.


SeeingThroughCollision.cpp로 가서 박스 컴포넌트와 SeeingThroughActor의 기능을 쓰기 위해서 다음 전처리기들을 추가한다.


#include "Engine/Classes/Components/BoxComponent.h"

#include "SeeingThroughActor.h"


ASeeingThroughCollision::ASeeingThroughCollision() 생성자 함수에 초기화 코드를 추가한다.


SeeingThroughCollision = CreateDefaultSubobject<UBoxComponent>(TEXT("SeeingThroughCollision"));
RootComponent = SeeingThroughCollision;
SeeingThroughCollision->SetCollisionProfileName(TEXT("OverlapOnlyPawn"));


콜리전의 프로필 네임을 OverlapOnlyPawn으로 설정해서 폰이 오버랩되었을 때만 반응하도록 만든다.


다시 SeeingThroughCollision.h로 가서 겹침 이벤트를 받아서 처리할 함수를 덮어씌우는 코드를 추가한다.


protected:
    virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;

    virtual void NotifyActorEndOverlap(AActor* OtherActor) override;


그리고 SeeingThroughCollision.cpp로 가서 두 함수를 구현한다.


void ASeeingThroughCollision::NotifyActorBeginOverlap(AActor * OtherActor)
{
    ARpgCharacter* Character = Cast<ARpgCharacter>(OtherActor);
    if (Character)
    {
        for (auto SeeingThroughActor : SeeingThroughActors)
        {
            SeeingThroughActor->SetShowSeeingThrough(true);
        }
    }
}

void ASeeingThroughCollision::NotifyActorEndOverlap(AActor * OtherActor)
{
    ARpgCharacter* Character = Cast<ARpgCharacter>(OtherActor);
    if (Character)
    {
        for (auto SeeingThroughActor : SeeingThroughActors)
        {
            SeeingThroughActor->SetShowSeeingThrough(false);
        }
    }
}


코드 작업이 끝나면 솔루션 탐색기에서 프로젝트를 빌드하고 언리얼 에디터로 넘어간다.


SeeingThroughCollision 배치


콘텐츠 브라우저 패널에서 SeeingThroughCollision을 드래그해서 레벨에 배치한다.



배치한 SeeingThroughCollision의 위치를 {-420.0 -790.0 0.0}으로, 스케일을 {4.0, 22.0, 1.0}으로 수정한다. 그렇게 하면 벽 너머에 캐릭터가 들어갔을 때 가려지는 영역에 콜리전이 위치하게 된다.



그 다음, SeeingThroughActors 배열에 + 버튼을 눌러서 엘리먼트를 추가해준 다음, 투명해져야 하는 벽을 할당한다.



 

테스트


플레이 버튼을 누르고 캐릭터를 벽 뒤로 이동시켜보면 벽이 투명해지고 그 지역을 벗어나면 다시 벽이 불투명해지는 것을 확인할 수 있다.



나머지 기둥과 벽으로 가려지는 부분에도 적당하게 SeeingThroughCollision을 배치하고 SeeingThroughActor와 매칭시켜서 벽이 투명해지도록 만들어보자.

반응형

TSubclassOf<T>


TSubclassOf 클래스는 UClass 타입 안정성을 제공하는 템플릿 클래스이다.


예를 들어, 데미지 타입을 지정할 수 있는 발사체 클래스를 만든다고 가정했을 때, 다음 코드처럼 UClass 타입의 UPROPERTY를 만들어서 에디터에 노출시킨 뒤, 에디터 작업자에게 이 프로퍼티에 UDamageType의 파생 클래스만 할당해 달라고 한다면 어떻게 될까?


UPROPERTY(EditDefaultsOnly, Category = Damage)
UClass* DamageType;


블루프린트 에디터의 디테일 패널에서 Damage Type 프로퍼티에 클래스를 할당하기 위해서 드롭다운 메뉴를 확장해보면 클래스 타입에 상관없이 모든 클래스가 표시되고 있음을 볼 수 있다. 이런 상황에서 UDamageType의 파생 클래스만 할당해달라고 한다면, 낮은 확률으로라도 언젠가는 잘못된 클래스를 할당하는 일이 분명 생길 수 밖에 없다.


 

이러한 문제를 예방하기 위해서 존재하는 것이 바로 TSubclssOf 클래스이다. 다음 코드와 같이 TSubclassOf<UDamageType>으로 UPROPERTY를 만든다.


UPROPERTY(EditDefaultsOnly, Category = Damage)
TSubclassOf<UDamageType> DamageType;


그렇게 하면, 블루프린트 창의 디테일 패널에서 Damage Type의 드롭다운 메뉴에서는 UDamageType의 파생 클래스만 표시된다. 이렇게 되면 개발자가 가끔 잘못된 데미지 타입을 골라서 넣는 실수는 할 수 있겠지만, 애초에 잘못된 클래스를 선택하는 문제는 발생하지 않을 것이다.



또한 TSubclassOf 클래스는 이런 UPROPERTY에 대한 안정성 이외에 C++ 수준의 타입 안정성 역시 제공하기 때문에 서로 호환되지 않는 TSubclassOf 타입을 서로 할당하려고 하면 컴파일 오류가 발생하고, UClass 타입을 할당하려고 하면 할당을 수행할 수 있는지 런타임중에 검사한다. 런타임 검사에 실패하면 결과값은 nullptr이 된다.


UClass* ClassA = UDamageType::StaticClass();

TSubclassOf<UDamageType> ClassB;

ClassB = ClassA; // 런타임 중에 체크.

TSubclassOf<UFireDamageType> ClassC;

ClassB = ClassC; // 컴파일 타임 중에 체크


반응형
  1. KhuHyeon 2020.09.14 02:18

    좋은 설명 감사합니다!!

+ Recent posts