Microsoft MVP성태의 닷넷 이야기
닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신 [링크 복사], [링크+제목 복사],
조회: 1915
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 3개 있습니다.)
개발 환경 구성: 707. 빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
; https://www.sysnet.pe.kr/2/0/13581

개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법
; https://www.sysnet.pe.kr/2/0/13584

닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신
; https://www.sysnet.pe.kr/2/0/13588




C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신

Unity를 닷넷 응용 프로그램에 내장했다면,

Unity3D - C# Windows Forms / WPF Application에 통합하는 방법
; https://www.sysnet.pe.kr/2/0/13584

아마도 같은 닷넷이니, 직접 닷넷 타입끼리의 연동을 하고 싶을 것입니다.

하지만, 아쉽게도 Unity는 Mono 런타임을 사용하므로 우리가 만든 Windows Forms/WPF 측의 호스트가 동작하는 .NET Framework/.NET Core/5+ 환경과 직접적인 연동을 할 수는 없습니다.

설령 연동하는 방법을 방법을 제공해 확장 인터페이스를 마련해뒀다고 해도 각각의 런타임이 다르기 때문에 이후 동작 시 필히 문제가 발생합니다. 예를 들어 .NET 8 런타임에서 생성한 참조 개체를 Unity의 Mono 런타임으로 전달하는 경우는 어떨까요? .NET 8 런타임에서 해당 개체를 더 이상 참조하지 않아 GC가 되는 경우 Mono 런타임에 넘겨준 그 개체의 root 참조 유무를 알 수 없어 그냥 제거하게 될 것입니다. 당연히 그럼 Mono 런타임에서는 이미 해제된 참조 개체의 메서드를 호출하는 순간 문제가 발생할 수밖에 없습니다. (나아가, 닷넷 런타임에서 생성한 참조 개체를 Mono 런타임으로 넘겼을 때, 그 참조 개체의 필드에 Mono 런타임에서 생성한 참조를 담는다면 root 참조 문제는 더욱 꼬이게 됩니다.)

사실 이전의 .NET Framework CLR조차도 (다중 AppDomain 간에 전달한 개체가 있는 경우) AppDomain이 다르면 MarshalByRefObject를 이용해 통신해야 했는데, 하물며 런타임이 다른 상황이라면 뭔가 더욱 특별한 방법을 제공해야만 할 것입니다.

그렇다면 이제 차선책으로 생각해 볼 수 있는 것이, .NET 수준의 연동이 아니라 COM 인터페이스와 같은 Native 수준의 연동을 기대할 수 있는데, 아쉽게도 Unity는 이에 대해 열어 놓은 것이 없습니다. 현재 유일한 접점으로 볼 수 있는 UnitMain은 뭔가 건네주는 인자가 많은 듯해도,

[DllImport("UnityPlayer.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "UnityMain")]
public static extern int UnityMain(IntPtr hInstance, IntPtr hPrevInstance, string lpCmdLine, int nShowCmd)

저 인자들 모두 어떤 확장을 위해 제공되는 것이 아니고 단순히 WinMain 진입점과 자연스럽게 연결하기 위한 외부 함수에 지나지 않습니다.

int __clrcall WinMain(
  [in]           HINSTANCE hInstance,
  [in, optional] HINSTANCE hPrevInstance,
  [in]           LPSTR     lpCmdLine,
  [in]           int       nShowCmd
);

결국, 어떡해서든 자연스럽게 연동할 수 있는 방법은 없다고 보면 되겠습니다. ^^




어쩔 수 없습니다. 이렇게 된 이상 같은 프로세스임에도 불구하고 IPC(Inter-Process Communication) 호출에 기대야 합니다. 가령 Socket 통신이 대표적인데요, 단지 소켓은 포트 관리 등의 번거로움이 있으므로 기왕이면 Named Pipe 통신이 제어용으로는 나쁘지 않습니다. 아래의 Q&A가 바로 이에 대한 상황을 설명합니다.

Calling Functions on Unity-Application embedded in Winforms-Application [duplicate]
; https://stackoverflow.com/questions/48269904/calling-functions-on-unity-application-embedded-in-winforms-application

예를 들어, 지난번 작성한 코드의 WPF 측에 Named Pipe를 열어두는 코드를 다음과 같이 추가할 수 있습니다.

{
    // ...[생략]...
    private NamedPipeServerStream? _namedPipeServerStream;
    Thread? _unityThread;
    Thread? _commThread;

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        if (_commThread != null)
        {
            return;
        }

        _commThread = new Thread(ProxyUnity);
        _commThread.Start();
    }

    void ProxyUnity()
    {
        _namedPipeServerStream = new NamedPipeServerStream("UnityPipe", PipeDirection.InOut, 1 /*, PipeTransmissionMode.Byte, PipeOptions.Asynchronous */);

        _namedPipeServerStream.WaitForConnection();
        StreamString serverStream = new StreamString(_namedPipeServerStream);

        while (true)
        {
            string response = serverStream.ReadString();
            if (string.IsNullOrEmpty(response))
            {
                break;
            }

            System.Diagnostics.Trace.WriteLine(response); // Unity 측에서 데이터 전송을 하고 있는지 체크하기 위한 디버깅 메시지 출력
        }

        _namedPipeServerStream.Close();
    }
}

위의 소스코드에서 사용한 StreamString 도우미 클래스는 다음과 같은데요,

using System;
using System.IO;
using System.Text;

namespace Assets
{
    /// <summary>
    /// Simple Wrapper to write / read Data to / from a Named Pipe Stream.
    /// 
    /// Code based on:
    /// https://stackoverflow.com/questions/43062782/send-message-from-one-program-to-another-in-unity
    /// </summary>
    public class StreamString
    {
        private Stream ioStream;
        private UnicodeEncoding streamEncoding;

        public StreamString(Stream ioStream)
        {
            this.ioStream = ioStream;
            streamEncoding = new UnicodeEncoding();
        }

        public string ReadString()
        {
            int len = 0;

            len = ioStream.ReadByte() * 256;
            len += ioStream.ReadByte();
            byte[] inBuffer = new byte[len];
            ioStream.Read(inBuffer, 0, len);

            return streamEncoding.GetString(inBuffer);
        }

        public int WriteString(string outString)
        {
            byte[] outBuffer = streamEncoding.GetBytes(outString);
            int len = outBuffer.Length;
            if (len > UInt16.MaxValue)
            {
                len = (int)UInt16.MaxValue;
            }
            ioStream.WriteByte((byte)(len / 256));
            ioStream.WriteByte((byte)(len & 255));
            ioStream.Write(outBuffer, 0, len);
            ioStream.Flush();

            return outBuffer.Length + 2;
        }
    }
}

Unity 프로젝트에서도 저 클래스를 포함하고 WPF 측으로 Named Pipe 연결을 하는 코드를 추가하면 됩니다. 아래는 테스트를 위해 Camera 개체에 스크립트 Component를 연결한 다음 Named Pipe로 WPF 측에 데이터를 주기적으로 쓰는 작업을 합니다.

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Text;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    System.Threading.Thread _task;

    void Start()
    {
        if (_task != null)
        {
            return;
        }

        _task = new System.Threading.Thread(ThreadFunc);
        _task.IsBackground = true;
        _task.Start();
    }

    void ThreadFunc(object arg)
    {
        NamedPipeClientStream client = new NamedPipeClientStream(".", "UnityPipe", PipeDirection.InOut,
            PipeOptions.None, System.Security.Principal.TokenImpersonationLevel.None);
        client.Connect();
        StreamString clientStream = new StreamString(client);

        while (true)
        {
            clientStream.WriteString("Hello from UNITY!");
            System.Threading.Thread.Sleep(1000);
        }
    }

    // Update is called once per frame
    void Update()
    {

    }
}

따라서 WPF 프로젝트를 디버깅 모드로 실행하면,

cs_interop_with_unity_1.png

잘 동작하는군요. ^^ 이후, 양방향 제어는 필요에 따라 코드를 보완하면 끝!




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 3/28/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2024-04-23 10시26분
안녕하세요 좋은글 감사합니다.
 현재 제가 wpf로 관제 모니터링 시스템을 만들고 있는 초보 개발자입니다. 너무 밋밋해서 3d를 생각하다 unity 엔진을 사용해서 디지털 트윈 형식으로 만들어 볼까 생각 중인데요,
연동이 자연스럽지 않다고 하셨는데 혹시 제가 생각한 방법으로 시스템을 만들기 부적합할까요? 데이터 통신은 약 200/msec 간격으로 데이터를 주고받을 생각입니다.
공진영
2024-04-23 10시33분
만드실 수 있습니다. 단지, Unity 엔진 내의 스크립트와 WPF 내에서의 코드가 별도의 IPC 통신을 맺어서 처리해야 하는 번거로움이 있을 뿐입니다.

어찌 보면, 경우에 따라 응용 프로그램이 커졌을 때 프로세스를 나눠 서로 클라이언트/서버 통신하는 경우도 있을 테니, 그런 것을 감안해 보면 '번거로움'이라기보다는 그냥 필요한 작업이라고 여길 수도 있을 것입니다. ^^;

(로컬 PC 내에서의 통신에서 200/msec는 문제가 되지 않습니다.)
정성태

... 16  17  18  19  20  21  22  23  24  25  26  27  [28]  29  30  ...
NoWriterDateCnt.TitleFile(s)
12950정성태2/1/20226723.NET Framework: 1146. .NET 6에 추가되지 않은 Generic Math (예: INumber<T>)
12949정성태2/1/20226574.NET Framework: 1145. C# - ffmpeg(FFmpeg.AutoGen) - Codec 정보 열람 및 사용 준비파일 다운로드1
12948정성태1/30/20226716.NET Framework: 1144. C# - ffmpeg(FFmpeg.AutoGen) AVFormatContext를 이용해 ffprobe처럼 정보 출력파일 다운로드1
12947정성태1/30/20227887개발 환경 구성: 634. ffmpeg.exe - 기존 동영상 컨테이너에 다중 스트림을 추가하는 방법
12946정성태1/28/20226372오류 유형: 792. .NET Core - 로컬 개발 중에 docker 호스팅으로 바꾸는 경우 SQL 서버 접근 방법
12945정성태1/28/20226683오류 유형: 791. SQL 서버 로그인 시 localhost는 되고, 127.0.0.1로는 안 되는 문제
12944정성태1/28/20229139.NET Framework: 1143. C# - Entity Framework Core 6 개요
12943정성태1/27/20227954.NET Framework: 1142. .NET 5+로 포팅 시 플랫폼 호환성 경고 메시지(SYSLIB0006, SYSLIB0011, CA1416)파일 다운로드1
12942정성태1/27/20228210.NET Framework: 1141. XmlSerializer와 Dictionary 타입파일 다운로드1
12941정성태1/26/20229640오류 유형: 790. AKS/k8s - pod 상태가 Pending으로 지속되는 경우
12940정성태1/26/20226957오류 유형: 789. AKS에서 hpa에 따른 autoscale 기능이 동작하지 않는다면?
12939정성태1/25/20227738.NET Framework: 1140. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP3 오디오 파일 인코딩/디코딩하는 예제파일 다운로드1
12938정성태1/24/202210076개발 환경 구성: 633. Docker Desktop + k8s 환경에서 local 이미지를 사용하는 방법
12937정성태1/24/20227914.NET Framework: 1139. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 오디오(mp2) 인코딩하는 예제(encode_audio.c) [2]파일 다운로드1
12936정성태1/22/20227855.NET Framework: 1138. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 멀티미디어 파일의 메타데이터를 보여주는 예제(metadata.c)파일 다운로드1
12935정성태1/22/20228085.NET Framework: 1137. ffmpeg의 파일 해시 예제(ffhash.c)를 C#으로 포팅파일 다운로드1
12934정성태1/22/20227653오류 유형: 788. Warning C6262 Function uses '65564' bytes of stack: exceeds /analyze:stacksize '16384'. Consider moving some data to heap. [2]
12933정성태1/21/20228148.NET Framework: 1136. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP2 오디오 파일 디코딩 예제(decode_audio.c)파일 다운로드1
12932정성태1/20/20228614.NET Framework: 1135. C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c) [2]파일 다운로드1
12931정성태1/20/20226628개발 환경 구성: 632. ASP.NET Core 프로젝트를 AKS/k8s에 올리는 과정
12930정성태1/19/20227385개발 환경 구성: 631. AKS/k8s의 Volume에 파일 복사하는 방법
12929정성태1/19/20227187개발 환경 구성: 630. AKS/k8s의 Pod에 Volume 연결하는 방법
12928정성태1/18/20227274개발 환경 구성: 629. AKS/Kubernetes에서 호스팅 중인 pod에 shell(/bin/bash)로 진입하는 방법
12927정성태1/18/20227033개발 환경 구성: 628. AKS 환경에 응용 프로그램 배포 방법
12926정성태1/17/20227585오류 유형: 787. AKS - pod 배포 시 ErrImagePull/ImagePullBackOff 오류
12925정성태1/17/20227606개발 환경 구성: 627. AKS의 준비 단계 - ACR(Azure Container Registry)에 docker 이미지 배포
... 16  17  18  19  20  21  22  23  24  25  26  27  [28]  29  30  ...