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, 교착상태)이 발생할 수도 있다.

 

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

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

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

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

    • wergia 2020.10.20 00:05 신고

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

네트워크 애니메이터(Network Animator)

 

네트워크 게임에서는 게임이나 유닛, 캐릭터 등의 상태를 동기화하는 것도 중요하지만 눈에 보이는 캐릭터들의 움직임, 즉 애니메이션 역시 동기화가 필요하다. 아무리 다른 동기화가 잘 되고 있다고 하더라도, 애니메이션 동기화가 진행되지 않아서 가만히 서있는 자세로 이동하거나 공격한다면 문제가 많을 것이다.

 

유니티 네트워크 시스템에서는 이러한 애니메이션 동기화를 위한 기본적인 기능을 제공하는데 그것이 바로 네트워크 애니메이터(Network Animator)다.

 

이번 섹션을 진행하기 위해서는 기본적인 유니티의 애니메이션 시스템과 애니메이터 컨트롤러에 대한 지식이 필요하다. 기반 지식이 부족하다면 유니티의 애니메이션 문서를 참조하여 공부를 해두는 것이 좋다.

 

그럼 이제부터 네트워크 애니메이터를 사용하는 방법을 알아보도록 하자.

 

 

네트워크 애니메이터 컴포넌트의 기본적인 요구사항

 

 

 

네트워크 애니메이터를 사용하기 위해서는 네트워크 애니메이터 컴포넌트가 있는 오브젝트와 같은 오브젝트에 에니메이터 컨트롤러와 네트워크 아이덴티티 컴포넌트가 있어야 한다.

 

 

네트워크 애니메이터의 작동 방식

 

 

네트워크 애니메이터의 작동 방식은 기본 애니메이터 컨트롤러의 파라메터가 변경되면 그 변경된 파라메터의 값을 네트워크 애니메이터를 통해서 클라이언트의 해당 네트워크 애니메이터로 전송하고 받은 측의 네트워크 애니메이터가 자신이 소유한 애니메이터 컨트롤러에 변경된 파라메터와 값을 알려서 애니메이션을 동작하게 한다.

 

 

 

 

 

네트워크 애니메이터를 사용하기 위한 준비

 

앞에서 네트워크 애니메이터의 기본적인 요구 사항과 작동 방식을 알아보았으니 이제 네트워크 애니메이터를 추가하고 사용하는 방법에 대해서 알아보도록 하자.

 

 

 

애니메이션 섹션의 단골인 박스맨이 이번 네트워크 애니메이터 섹션에서도 도움을 줄 것이다.

 

 

 

위의 이미지에 맞춰서 애니메이션 스테이트를 구성해보자. 박스맨 캐릭터는 대기(Idle) - 이동(Walk) - 공격(Attack), 세 가지 상태를 가지며, IsMove 파라메터의 상태에 따라서 대기와 이동 상태를 오가며, Attack 트리거를 받으면 공격 애니메이션을 재생하고 대기 상태로 돌아가는 아주 간단한 형태다.

 

 

 

위와 같이 게임 오브젝트와 컴포넌트를 세팅해주면 준비는 끝이다.

 

 

 

 

네트워크 애니메이터 추가하기

 

 

네트워크 애니메이터를 추가하는 방법은 간단하다. 애니메이션을 동기화하고자 하는 오브젝트에(애니메이터 컨트롤러 컴포넌트를 가지고 있어야 한다) 네트워크 애니메이터를 Add Component 해주면 된다. 그러면 네트워크 애니메이터와 함께 네트워크 통신에 필요한 Network Identity 컴포넌트가 자동으로 함께 추가된다.

 

 

 

네트워크 애니메이터를 추가한 후에는 네트워크 애니메이터 컴포넌트의 애니메이터 프로퍼티에 동기화되어야할 애니메이터를 추가해주면 된다.

 

 

그렇게하면 네트워크 애니메이터 컴포넌트에 동기화될 애니메이터의 파라메터들이 표시된다. 이 다음에는 네트워크 애니메이터를 컨트롤할 스크립트를 작성해야 한다.

 

using UnityEngine;
using UnityEngine.Networking;

public class PlayerCharacter : MonoBehaviour
{
    private NetworkAnimator netAnimator;

    void Start ()
    {
        netAnimator = GetComponent<NetworkAnimator>();
    }
   
    void Update ()
    {
        if (Input.GetKeyDown(KeyCode.W))
        {
            netAnimator.animator.SetBool("IsMove", true);
        }
        else if (Input.GetKeyUp(KeyCode.W))
        {
            netAnimator.animator.SetBool("IsMove", false);
        }

        if (Input.GetMouseButtonDown(0))
        {

            netAnimator.animator.SetTrigger("Attack");
            netAnimator.SetTrigger("Attack");
        }
    }
}

 

위와 같이 스크립트를 작성한 이후에 빌드하고 실행해서 한 쪽에서 서버를 열고 다른 쪽에서 클라이언트로 접속한 뒤 서버 측에서 W를 누르거나 마우스를 클릭했을때 애니메이션이 동기화됨을 확인할 수 있다.

 

 

위의 예시 코드에서 몇 가지 의문점이 있을 수 있는데, SetBool을 호출할 때는 networkAnimator.animator를 통해서 호출하고 SetTrigger를 호출할 때는 networkAnimator.SetTrigger()로 바로 호출하는가와 트리거를 사용하는 부분에서 왜 networkAnimator.animator.SetTrigger()와 networkAnimator.SetTrigger()를 중복해서 사용했는지가 그것이다.

 

첫 번째 의문점의 경우에는 Trigger는 NetworkAnimtor 클래스에서 호출할 수 있는 SetTrigger() 함수가 있지만, 다른 애니메이터의 파라메터(Int, Float, Bool)는 NetworkAnimator 클래스에서 바로 호출해서 사용하는 메서드가 따로 없고, NetworkAnimator의 멤버 변수인 animator를 통해서 SetBool(), SetInt(), SetFloat() 함수를 호출하도록 만들어져 있기 때문이다.

 

두 번째 문제는 트리거를 사용하는 부분에서 왜 networkAnimator.animator.SetTrigger()와 networkAnimator.SetTrigger()를 중복해서 사용했는지 인데, 이것은 networkAnimator.animator.SetTrigger() 함수를 통해서 동작하는 애니메이션은 서버에서만 재생되고, networkAnimator.SetTrigger() 함수를 통해서 동작하는 애니메이션은 클라이언트에서만 재생되기 때문이다. 즉, 서버에서도 애니메이션을 재생하고 클라이언트에서도 애니메이션을 재생하기를 원한다면 위의 예시 코드와 같이 작성되어야 한다. 특히 서버 측에서 애니메이션 이벤트를 통해서 애니메이션이 진행되는 도중에 특정한 동작이 발생되도록 로직이 짜여있다면 서버에서도 애니메이션이 동작되어야 하기 때문에 반드시 위의 코드처럼 작성되어야만 한다. 하지만 이후에도 설명하겠지만, 애니메이션을 재생하는 처리는 상당히 무거운 작업에 속하기 때문에, 클라이언트가 서버의 역할을 함께하는 P2P 방식이 아닌 순수한 서버라면 애니메이션 재생 도중 호출되는 애니메이션 이벤트는 반드시 배제해야 한다. 그리고 서버에서는 애니메이션이 완전히 동작하지 않도록 하는 것이 좋다.

 

 

 

 

추가로 : 네트워크 애니메이터에 대한 중요한 사실

 

유니티에서 기본적으로 제공하는 네트워크 애니메이터가 있기 때문에 우선은 소개하고 사용법에 대해서 알려주지만, 사실 기본 네트워크 애니메이터를 사용하는 것은 추천하지 않는다. 오히려 직접 커스텀 네트워크 애니메이터를 따로 구현해서 사용하도록 권장하고 싶다. 지금은 주요 작업을 2017.3.03f 버전의 유니티로 하고 있기 때문에 이후의 버전에서는 더 좋게 바뀌었는지 모르겠지만, 이 버전과 이전 버전에서는 기본 네트워크 애니메이터에 상당한 문제가 있기 때문이다.

 

첫 번째 문제는, 기본 네트워크 애니메이터가 소모하는 데이터량이 너무 많다. 커스텀 애니메이터를 잘 구현한다면 기본 네트워크 애니메이터를 사용할 때보다 훨씬 데이터 소모량을 많이 줄일 수 있다.

 

두 번째 문제는, 꽤나 심각한 문제인데, 기본 네트워크 애니메이터가 굉장히 많은 가비지(Garbage)를 발생시켜서 GC로 인한 프레임드랍이 심각하다고 여겨질 만큼 발생한다는 것이다. 이것에 대한 이슈는 구글링해보면 해외 개발자들도 상당히 심각하게 느끼도 있다는 것을 알 수 있다. 실제 개발에서 네트워크 애니메이터로 애니메이션을 동기화 했다가 이 문제 때문에 네트워크 애니메이터를 모두 제거하고 커스텀 네트워크 애니메이터를 구현해서 사용해야 했었다.

 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형
  1. modernator 2018.09.30 01:21

    잘보고 갑니다. UNet의 NetworkAnimator는 잘모르겠지만, 예전에 Photon Unity Networking의 NetworkAnimator 코드를 분석한 적이 있었는데요, 동기화를 위해서 어마어마한 작업들을 하더군요. 간단한 데모 구현이나 가벼운 게임이 아니라면 말씀하신대로 직접 스크립트로 필요한 경우에만 동기화하는게 제일 좋은것 같습니다.

    • wergia 2018.10.01 11:08 신고

      유넷이 기본 제공하는 네트워크 애니메이터는 사실상 프로토타이핑용인 것 같습니다.

  2. mimus 2018.11.15 23:07

    안녕하세요 unet 관련 설명을 잘봤습니다 ㅎㅎ..
    다름이 아니라 이번에 혼자서 unet으로 VR 게임을 제작 하는데 문제가 생겨서 질문을 좀드립니다ㅠㅠ..
    멀티플레이 방식을 internt 방식을 사용해서 제작하니 방을 만든 사용자의 움직임은 방에 접속한 사람에게 보이는데
    방에 접속한 사람의 움직임은 방을 만든 사용자에게 보이지가 않습니다..
    동기화 방식은 networkTransform 을 사용했습니다..

    혼자서 아무리 생각을 해봐도 이유를 모르겠습니다 ㅠㅠ

    아그리고 추가 로 질문을 하면 혹시 internet 방식으로 멀티플레이를 하게되면 방을 만든 사람 만 local player로 인식 되는건가요?..
    답변 기다리겠습니다 ㅠ

    • wergia 2018.11.16 11:13 신고

      움직임이 보이지 않는다는 것은 애니메이션이 보이지 않는다는 의미인가요? 아니면 위치 동기화가 되지 않는다는 의미인가요?

      1. 만약 애니메이션이 보이지 않는다는 뜻이라면 방을 만들때 StartServer로 하셨나요?
      방을 만드는 사람이 플레이어로 참여하려면 StartServer로 방을 만들기보다 StartHost로 방을 만들어야 합니다. Server의 경우 기본적으로 서버로서의 계산만을 하기 때문에 기본 NetworkAnimator의 경우 애니메이션 재생을 배재하기때문에 애니메이션 동기화가 되지 않을 수 있습니다.

      2. 만약 위치 동기화가 되지 않는다면 authority 문제일 확률이 제일 높습니다. 일반적으로 네트워크 게임에서 동기화하는 권한은 서버 역할을 하는 호스트에게 있어야만 합니다. 그런데 만약 권한이 로컬 플레이어가 가지게 된다면 호스트 역할을 하는 플레이어의 경우 서버역을 겸하기 때문에 방에 접속한 다른 사용자에게 위치 동기화가 되지만, 클라이언트로서 접속한 플레이어의 위치 동기화가 이루어지지 않는 문제라고 생각됩니다.
      플레이어 프리팹 오브젝트의 NetworkIdentity 컴포넌트에서 Local Player Authority를 확인해보세요.

    • mius 2018.11.16 15:12

      감사합니다! 덕분에 해결 했습니다!

  3. myouzing 2019.08.05 23:09

    안녕하세요 unity를 공부하고 있는 학생입니다.
    저는 이제까지 single fps 게임을 개발하였습니다. 그 후에 multi player로 발전시키는 과정에서 문제가 생겨 여쭤보고 싶습니다. ㅠㅠ
    우선, 문제점은 제가 single에서 넣어놓은 적 애니메이션은 player와 관련이 있었습니다.
    player가 존재하면 걷고, 가까워지면 player를 공격하고 하는 등등의 애니메이션을 적용한 것입니다
    그러나, 제가 player에게 networkmanager를 적용시켰을 때 player를 인식하지 못하는 문제점이 있는 것 같습니다.
    이 문제에 대해 혹시 조언을 구할 수 있을까요..?
    답변 주시면 정말 감사하겠습니다 !

    • wergia 2019.08.19 17:53 신고

      상황을 정확히 알 수 없어서 자세한 답변은 어렵지만 대강 짐작가는 부분에 대한 답변을 드립니다.

      https://wergia.tistory.com/176

UNet Tutorial (6) - SyncVar와 Hook


지난 Unet Tutorial 섹션에서는 Command와 ClientRpc를 이용한 클라이언트와 서버 간의 액션 통신 방법에 대해서 알아보았다. 이것은 함수를 통한 동작 수행을 처리하는 것이었다. 하지만 이번 섹션에서 알아볼 SyncVar와 Hook은 NetworkBehaviour를 상속받는 클래스의 멤버 변수를 동기화하기 위한 것이다.


지난 섹션에서 배운 Command 와 ClientRpc를 이용해서도 멤버변수에 대한 동기화를 구현할 수는 있다. Command와 ClientRpc를 통한 멤버 변수 동기화를 구현한 코드는 다음과 같다.


using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{
    // 플레이어의 체력
    public int hp;

    void Start()
    {
        // 서버에서 플레이어가 생성되면 hp를 100으로 만들고 Rpc를 통해 클라이언트에 알려준다.
        if (isServer)
        {
            hp = 100;
            RpcSyncHp(hp);
        }
    }
   
    [ClientRpc]
    // 클라이언트는 Rpc를 통해 현재 hp를 받으면 자신의 객체에 적용한다.
    public void RpcSyncHp(int hp)
    {
        this.hp = hp;
    }

    [Command]
    // 서버는 Cmd로 플레이어가 받아야될 데미지를 받으면 현재 hp에서 데미지를 빼고 남은 hp를 Rpc로 클라이언트에 알려준다.
    public void CmdDamaged(int dmg)
    {
        hp -= dmg;
        RpcSyncHp(hp);
    }

    void Update()
    {
        // 클라이언트에서 스페이스 버튼을 누르면 Cmd를 호출해서 서버에 플레이어가 입어야될 데미지를 알려준다.
        if (isClient && isLocalPlayer)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                CmdDamaged(10);
            }
        }
    }
}

위의 예시에서는 CmdDamaged를 호출할 때는 클라이언트에서 데미지 값을 넣도록 했지만 실제로 구현할 때는 저렇게 하지 않도록 하자. 클라이언트에서 중요한 값을 넣을 수 있도록하면 해킹에 취약해진다.


Command와 ClientRpc를 이용하여 멤버 변수 동기화 구현은 가능하지만 약간은 복잡하다.





SyncVar를 통한 멤버 변수 동기화


SyncVar를 통해 멤버 변수 동기화를 이용하면 아래의 예시 코드처럼 조금 더 간단한 코드가 작성 가능해진다.


using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{
    [SyncVar]
    // 플레이어의 체력
    public int hp;

    void Start()
    {
        // 서버에서 플레이어가 생성되면 hp를 100으로 만든다.
        if (isServer)
        {
            hp = 100;
        }
    }

    [Command]
    // 서버는 Cmd로 플레이어가 받아야될 데미지를 받으면 현재 hp에서 데미지를 뺀다.
    public void CmdDamaged(int dmg)
    {
        hp -= dmg;
    }

    void Update()
    {
        // 클라이언트에서 스페이스 버튼을 누르면 Cmd를 호출해서 서버에 플레이어가 입어야될 데미지를 알려준다.
        if (isClient && isLocalPlayer)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                CmdDamaged(10);
            }
        }
    }
}


위의 예시 코드처럼 동기화하고자 하는 멤버 변수에 SyncVar 어트리뷰트를 붙여주면 서버에서 hp의 값이 변경되면 클라이언트의 해당 객체에 변경된 값을 알려주고 동기화하게 된다. 이러한 SyncVar의 동기화에서 알고있어야 할 점은 이 멤버 변수 동기화는 서버에서 클라이언트의 방향으로만 이루어진다는 점이다. 즉, 서버에서 변수가 수정되면 모든 클라이언트로 동기화되고, 클라이언트에서는 이 변수의 값을 아무리 수정해도 다른 클라이언트나 서버에서는 변경이 되지 않는다는 점이다.





Hook


멤버 변수가 변경된 것을 클라이언트에서 동기화 받고 난 뒤에 클라이언트에서 처리해야할 작업이 더 있는 경우가 더러 있다. 위의 코드를 예를 들자면 hp가 동기화된 이후에 클라이언트 측에서는 변경된 hp의 양에 맞춰서 hp ui를 변경해 주어야할 것이다. 그런 것을 처리하는 역할을 할 수 있는 것이 바로 hook이다. hook의 구현은 다음 예시 코드와 같이할 수 있다.


using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{

    public delegate void OnChangeHp(int hp);

    public OnChangeHp onChangeHp;


    [SyncVar(hook = "ChangeHookHp")]
    // 플레이어의 체력
    public int hp;
    void ChangeHookHp(int hp)
    {

        // hook을 이용하는 경우에는 동기화해야할 값이 매개 변수로 넘어오고 그것을 직접 대입해주어야 한다.
        this.hp = hp;

        // 만약 delegate event에 hp ui의 함수를 등록해두었다면 hp ui가 갱신될 것이다.

        onChangeHp(this.hp);
    }

    void Start()
    {
        // 서버에서 플레이어가 생성되면 hp를 100으로 만들고 Rpc를 통해 클라이언트에 알려준다.
        if (isServer)
        {
            hp = 100;
        }
    }

    [Command]
    // 서버는 Cmd로 플레이어가 받아야될 데미지를 받으면 현재 hp에서 데미지를 빼고 남은 hp를 Rpc로 클라이언트에 알려준다.
    public void CmdDamaged(int dmg)
    {
        hp -= dmg;
    }

    void Update()
    {
        // 클라이언트에서 스페이스 버튼을 누르면 Cmd를 호출해서 서버에 플레이어가 입어야될 데미지를 알려준다.
        if (isClient && isLocalPlayer)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                CmdDamaged(10);
            }
        }
    }
}


위의 코드처럼 SyncVar의 어트리뷰트에 hook을 등록해두면 서버에서 hp값이 수정되고 그것이 클라이언트에 통지되었을 때, 자동으로 hook으로 등록해둔 함수가 호출된다. 즉, hp 값이 변경되었을 때, 클라이언트에서 추가적으로 처리해야 하는 일들을 처리할 수 있게 되는 것이다.


단, 이 hook을 사용할 때 알아두어야 할 것이 있다. hook은 서버에서 변경된 값이 클라이언트에 통지되었을때 동작하는 것임으로 클라이언트에서만 동작한다는 것과, hook을 사용한 경우에는 클라이언트의 멤버 변수에 동기화되어야하는 값이 바로 적용되지 않는다는 점이다. 멤버 변수에 바로 동기화되지 않고, hook으로 등록한 함수의 매개 변수를 통해서 전달되기 때문에, 클라이언트 측의 멤버 변수에 매개 변수의 값을 직접 대입해주어야 한다.

반응형
  1. HJ 2018.05.04 03:06

    hook 사용시 왜 클라이언트에 값이 바로 동기화 되지않는지 헤메고 있었는데 그 이유를 드디어 찾았네요!
    감사합니다 ^^!

+ Recent posts