Unity と Python はソケットを介して相互に通信し、カスタム データ パケットを送信します。これは、Unity を使用してシーンを構築し、Python を通じてデータ処理を実行する方法であり、2 つの異なる言語の利点を効果的に活用できます。
対応する操作を対応するモジュール SocketTools.cs にカプセル化しました。最初に特定のコードの使用法を見てみましょう (このコードは、Unity のメイン カメラによってレンダリングされた画像をエンコードし、ソケット経由で Python に送信する単純なコードです)そして、Python が画像結果を処理して表示するコードを受け取ります):
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using System.Text;
using System.Runtime.InteropServices;
using System.Linq;
using UnityEngine.UI;
public class HttpClient : MonoBehaviour
{
public Camera cam;
//关于相机的参数
public RawImage rawImage;
private float timeOld = 0;
private RenderTexture cameraView = null;
private Texture2D screenShot = null;
private Texture2D texture = null;
private byte[] image_recv_bytes = new byte[0];
SocketTools tools = new SocketTools();
private bool renderState = false;
// Start is called before the first frame update
void Start()
{
cameraView = new RenderTexture(Screen.width, Screen.height, 8);
cameraView.enableRandomWrite = true;
texture = new Texture2D(500, 400);
Rigidbody rigidbody = GetComponent<Rigidbody>();
rigidbody.angularVelocity = new Vector3(0,0.2f,0);
timeOld = Time.time;
tools.SocketInit();
tools.SetCallBackFun(CallBackFun);
tools.ConnectServer("127.0.0.1", 4444);
}
public void CallBackFun(SocketStatus socketStatus, System.Object obj)
{
switch (socketStatus)
{
case SocketStatus.eSocketConnecting:
Debug.Log("正在连接服务器...");
break;
case SocketStatus.eSocketConnected:
Debug.Log("服务器连接成功...");
break;
case SocketStatus.eSocketReceived:
{
//Debug.Log("接收到消息:" + System.Text.Encoding.Default.GetString((byte[])obj));
image_recv_bytes = (byte[])obj;
}
break;
// case SocketStatus.eSocketSending:
// Debug.Log("正在发送消息...");
// break;
// case SocketStatus.eSocketSent:
// Debug.Log("消息已发送...");
// break;
case SocketStatus.eSocketClosed:
Debug.Log("服务器已断开...");
break;
default:
break;
}
}
// Update is called once per frame
void Update()
{
if (tools.Connected && renderState)
{
renderState = false;
if (null == screenShot)
{
screenShot = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
}
// 读取屏幕像素进行渲染
cam.targetTexture = cameraView;
cam.Render();
RenderTexture.active = cameraView;
screenShot.ReadPixels(new Rect(0, 0, cameraView.width, cameraView.height), 0, 0);
screenShot.Apply();
byte[] bytes = screenShot.EncodeToJPG();
// //实例化一个文件流--->与写入文件相关联
// FileStream fs = new FileStream("test_unity.jpg", FileMode.Create);
// //开始写入
// fs.Write(bytes, 0, bytes.Length);
// //清空缓冲区、关闭流
// fs.Flush();
// fs.Close();
// return;
cam.targetTexture = null;
try
{
timeOld = Time.time;
tools.SendToServer(tools.PackData(bytes, 1));
}
catch
{
if (!tools.Connected)
{
tools.SocketClose();
Debug.Log("the server has been disconnected. ");
}
}
}
if (image_recv_bytes != null)
{
Debug.Log("图片大小:" + image_recv_bytes.Length + "bytes, 帧间延时:" + Time.deltaTime * 1000 + "ms, 网络时间延迟:" + ((Time.time - timeOld) * 1000).ToString() + "ms");
texture.LoadImage(image_recv_bytes);
image_recv_bytes = null;
rawImage.texture = texture;
renderState = true;
}
if (Input.GetKeyDown(KeyCode.Space))
{
if (tools.Connected)
{
tools.SocketClose();
Debug.Log("socket closed. ");
}
else
{
tools.ConnectServer("127.0.0.1", 4444);
}
}
}
}
対応するコード効果は次のとおりです。
ユニティ+Python
いくつかの技術的な詳細については、以下で詳しく説明します。この実装の詳細に興味がない場合は、私が完成したプロジェクト コードを直接ダウンロードすることもできます。
Unity と Python はカスタム ソケット プロトコルを介して非同期通信リソースを実装します - CSDN ライブラリ
次に、詳細な技術的実装プロセスですが、一般的に、主な技術的手段は次の 3 つの部分に分かれています。
1. PythonとUnityでの非同期ソケットの実装
2. PythonとUnityでのカスタムデータパケットの分割とマージの実装
3. PythonとUnity間のソケット通信におけるパケット認証の実装
4. Unity データ処理の非同期カプセル化ロジック
1. PythonとUnityでの非同期ソケットの実装
a. Python 非同期ソケット
ここでの Python は主にサーバーとして使用されるため、ここでの Python コードは主に Python をソケット TCP サーバーとして使用して実装されます。
最初は初期化サーバーのコードです
# 初始化服务器
def socket_init(self, host, port):
# 1 创建服务端套接字对象
self.tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# signal_update.emit(0.2, 1, 1, 'socket object created...')
# 设置端口复用,使程序退出后端口马上释放
self.tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 2 绑定端口
self.tcp_server.bind((host, port))
# signal_update.emit(0.7, 1, 1, "socket bind successfully...")
print('server port bind successfully, server host: {}, server port: {}...'.format(host, port))
次に、サーバー監視コードを開始します。まず、TCP サーバーのステータスを監視する新しいスレッドを開始し、次に、次のように、対応するコードを start_thread_fun スレッド関数に実装します。
# 开始的线程函数,外部最好调用 start() 函数,不要调用此函数
# 否则会阻塞
def start_thread_fun(self):
print("server_thread started. ")
# 3 设置监听
self.tcp_server.listen(self.max_connections)
print('start to listen connections from client, max client count: {}'.format(
self.max_connections))
# 4 循环等待客户端连接请求(也就是最多可以同时有128个用户连接到服务器进行通信)
while True:
tcp_client_1, tcp_client_address = self.tcp_server.accept()
self.tcp_clients.append(tcp_client_1)
# 创建多线程对象
thd = threading.Thread(target=self.client_process,
args=(tcp_client_1, tcp_client_address), daemon=True)
# 设置守护主线程 即如果主线程结束了 那子线程中也都销毁了 防止主线程无法退出
thd.setDaemon(True)
# 启动子线程对象
thd.start()
print("new client connected, client address: {}, total client count: {}".format(tcp_client_address, 1))
# 启动服务器
def start(self):
self.start_thread = threading.Thread(target=self.start_thread_fun, daemon=True)
self.start_thread.start()
print("starting server_thread...")
注: ここでメイン スレッドを終了することはできません。そうしないと、すべてのサブスレッドが停止します。time.sleep(100) をメイン スレッドに追加して、メイン スレッドの実行時間を手動で遅らせることができます。
b. Unity 非同期ソケット
Unity の非同期ソケットは、非同期ステータスを毎回コードで手動でリセットおよび調整する必要があるため、それに比べて少し面倒ですが、そのプロセスについては後で詳しく説明します。
全体のプロセスに関しては、フローチャートを使用する方が適切だと思います。コードの簡単なフローチャートは次のとおりです。
それぞれの場所のコード内容は次のとおりです。
public void SocketInit()
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
public void ConnectServer(string host, int port)
{
try
{
SocketAsyncEventArgs args = new SocketAsyncEventArgs();//创建连接参数对象
this.endPoint = new IPEndPoint(IPAddress.Parse(host), port);
args.RemoteEndPoint = this.endPoint;
args.Completed += OnConnectedCompleted;//添加连接创建成功监听
socket.ConnectAsync(args); //异步创建连接
}
catch (Exception e)
{
Debug.Log("服务器连接异常:" + e);
}
}
private void OnConnectedCompleted(object sender, SocketAsyncEventArgs args)
{
try
{ ///连接创建成功监听处理
if (args.SocketError == SocketError.Success)
{
StartReceiveMessage(); //启动接收消息
}
}
catch (Exception e)
{
Debug.Log("开启接收数据异常" + e);
}
}
private void StartReceiveMessage(int bufferSize = 40960)
{
//启动接收消息
SocketAsyncEventArgs receiveArgs = new SocketAsyncEventArgs();
//设置接收消息的缓存大小,正式项目中可以放在配置 文件中
byte[] buffer = new byte[bufferSize];
//设置接收缓存
receiveArgs.SetBuffer(buffer, 0, buffer.Length);
receiveArgs.RemoteEndPoint = this.endPoint;
receiveArgs.Completed += OnReceiveCompleted; //接收成功
socket.ReceiveAsync(receiveArgs);//开始异步接收监听
}
public void OnReceiveCompleted(object sender, SocketAsyncEventArgs args)
{
try
{
//Debug.Log("网络接收成功线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
if (args.SocketError == SocketError.Success && args.BytesTransferred > 0)
{
//创建读取数据的缓存
byte[] bytes = new byte[args.BytesTransferred];
//将数据复制到缓存中
Buffer.BlockCopy(args.Buffer, 0, bytes, 0, args.BytesTransferred);
Debug.Log(bytes);
//再次启动接收数据监听,接收下次的数据。
StartReceiveMessage();
}
}
catch (Exception e)
{
Debug.Log("接收数据异常:" + e);
}
}
public void SendToServer(byte[] data)
{
try
{
//创建发送参数
SocketAsyncEventArgs sendEventArgs = new SocketAsyncEventArgs();
sendEventArgs.RemoteEndPoint = endPoint;
//设置要发送的数据
sendEventArgs.SetBuffer(data, 0, data.Length);
sendEventArgs.Completed += OnSendCompleted;
//异步发送数据
socket.SendAsync(sendEventArgs);
}
catch (Exception e)
{
Debug.Log("发送数据异常:" + e);
}
}
public void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
Debug.Log("send ok");
}
}
public void SocketClose()
{
socket.Close();
}
このコードには、最も単純な非同期 C# ソケット コードの一部が含まれています。
2. PythonとUnityでのカスタムデータパケットの分割とマージの実装
しかし、TCP がストリーミング伝送プロトコルであることは誰もが知っています。特に画像などの大きなデータ パケットの場合、フレーム間でうまく分割することができません。プロトコルのデータ パケット ヘッダーとデータ パケット本体をカスタマイズする必要があります。可変長データを実現するには、送信については、Python と C# でバイト レベルでデータを分割および結合する方法について話しましょう。
a.パイソン
Python は主に struct.pack 関数と struct.unpack 関数を使用してデータを分割および結合します。具体的なコードを以下に示します。
# 判断当前的数据头对不对,如果正确返回解析结果
def process_protocol(self, header):
header_unpack = struct.unpack(self.header_format, header)
if header_unpack[0] == self.header_bytes:
return True, header_unpack
else:
return False, None
def pack_data(self, data, data_type):
if type(data) is str:
data = data.encode()
data_pack = struct.pack(self.header_format, self.header_bytes, data_type, len(data))
# print("datalen:{}".format(len(data)))
return data_pack+data
# 部分相关变量定义
self.header_bytes = b'\x0a\x0b' # 占用两个字节
self.header_format = "2ssi"
b. 団結
UnityのC#では、対応するデータフレームヘッダーの内容を解析しやすくするために、対応するbyte[]型の変数を直接構造体に変換する方法がとられており、両者を相互に変換する対応コードは以下の通りです。以下に続きます:
public static object BytesToStruct(byte[] buf, int len, Type type)
{
object rtn;
IntPtr buffer = Marshal.AllocHGlobal(len);
Marshal.Copy(buf, 0, buffer, len);
rtn = Marshal.PtrToStructure(buffer, type);
Marshal.FreeHGlobal(buffer);
return rtn;
}
public static byte[] StructToBytes(object structObj)
{
//得到结构体的大小
int size = Marshal.SizeOf(structObj);
//创建byte数组
byte[] bytes = new byte[size];
//分配结构体大小的内存空间
IntPtr structPtr = Marshal.AllocHGlobal(size);
//将结构体拷到分配好的内存空间
Marshal.StructureToPtr(structObj, structPtr, false);
//从内存空间拷到byte数组
Marshal.Copy(structPtr, bytes, 0, size);
//释放内存空间
Marshal.FreeHGlobal(structPtr);
//返回byte数组
return bytes;
}
2 番目は、いくつかの単純なメモリ操作です。C# のカプセル化により、C++ のようなポインタを介してメモリ データに直接アクセスできません。Buffer クラスを使用する必要があります。具体的な使用方法は次のとおりです。
//用于同时操作内存块中的数据
public static void BlockCopy (Array src, int srcOffset, Array dst, int dstOffset, int count);
src 開始位置オフセット srcOffset のデータ傍受カウント ビットを、dst 開始位置オフセット dstOffset より後ろのカウント長の内側の太い領域にコピーできます。
byte[] 型変数の指定範囲のデータをインターセプトする場合は、次の関数を使用する必要があります。
bytes.Skip(recvLenReal).Take(bytes.Length - recvLenReal).ToArray();
recvLenReal の長さを先頭からスキップし、bytes.Length - recvLenReal の長いデータをインターセプトして、byte[] 型の変数に変換します。
3. データフレームのカプセル化ロジック
カスタム データ パケットの実装は、主にデータ ヘッダーとデータ本体の 2 つの部分に分かれています。
パケットヘッダーは次のように定義されます。
データヘッダー | データの種類 | データ主体の長さ | ||||
2 バイトのデータ ヘッダーはプロトコルを区別するために使用されます。1 バイトのデータ タイプは、後続のデータ本体のデータ タイプ (写真、テキスト、その他のコンテンツなど) を識別するために使用されます。4 バイトのデータ本体の長さは一緒にこれらは int 型変数を形成し、後続のデータの合計長を示すことができます。
UnityとPythonでのこのデータヘッダーの実装方法は以下の図のようになります。
public struct DataHeader
{
public byte header1;
public byte header2;
public byte data_type;
public int data_len;
}
コードの Python 部分は上に示されているため、ここでは繰り返しません。
4. PythonとUnity間のソケット通信におけるパケット認証の実装
データ パケット認証コードに関しては、どちらも同じロジックを使用しています。ここでは、対応するロジック処理のアノテーションのみを示しますが、コードの実装には若干の違いがあります。
def process_raw_data(self, recv_data):
'''
关于操作:
本函数应该具有递归功能,否则无法处理复杂任务
关于消息接收的逻辑处理:
1. 首先判断当前是否已经接收过帧头 (self.frame_info.data_type is not None)
接收过:
根据帧头的数据长度接收对应的数据体内容
没接收过:
判断当前接收的数据长度是否满足帧头的长度
满足:尝试解析
解析失败:正常传输数据
解析成功:如果有其他的数据,继续接收处理后续的数据
不满足:将本次数据输出,丢弃此次的数据 !!!
'''
# 如果已经接收过数据头,直接继续接收内容
if self.frame_info.data_type is not None:
# 首先计算剩余的数据包长度
recv_len_left = self.frame_info.data_all_length - self.frame_info.data_recv_length
# 然后计算本次可以接收的数据长度,选取数据长度和剩余接收长度的最小值
recv_len_real = min(recv_len_left, len(recv_data))
self.frame_info.data_buffer += recv_data[:recv_len_real]
# 更新对应的接收的数据长度
self.frame_info.data_recv_length = len(self.frame_info.data_buffer)
# 判断当前是否已经接受完本帧的内容
if self.frame_info.data_recv_length >= self.frame_info.data_all_length:
# 根据回调函数返回对应的内容
if self.callback_fun is not None:
self.callback_fun(self.frame_info.data_buffer)
# 从剩余的数据中尝试检索出对应的数据头
# 首先更新 recv_data 的数据的内容
# print(self.frame_info.data_buffer)
self.frame_info.reset()
recv_data = recv_data[recv_len_real:len(recv_data)]
if len(recv_data) != 0:
self.process_raw_data(recv_data)
else:
return
# 从剩余的数据中尝试解析数据头
else:
if len(recv_data) >= self.header_length:
ret = self.process_protocol(recv_data[:self.header_length])
if ret[0]:
# 打印出协议对应的内容
# print(ret[1])
self.frame_info.set(ret[1][1], ret[1][2])
# 此处还得继续判断当前是否转换完了,如果没有的话需要继续转换接收到的内容
recv_data = recv_data[self.header_length:len(recv_data)]
if len(recv_data) != 0:
self.process_raw_data(recv_data)
else:
print(recv_data)
else:
print(recv_data)
private void ProcessRawData(ref byte[] bytes)
{
// 关于操作:
// 本函数应该具有递归功能,否则无法处理复杂任务
// 关于消息接收的逻辑处理:
// 1.首先判断当前是否已经接收过帧头(self.frame_info.data_type is not None)
// 接收过:
// 根据帧头的数据长度接收对应的数据体内容
// 没接收过:
// 判断当前接收的数据长度是否满足帧头的长度
// 满足:尝试解析
// 解析失败:正常传输数据
// 解析成功:如果有其他的数据,继续接收处理后续的数据
// 不满足:将本次数据输出,丢弃此次的数据!!!
if (frameInfo.sPyDataTest.data_type != 0)
{
int recvLenLeft = frameInfo.sPyDataTest.data_len - frameInfo.data_recv_len;
int recvLenReal = Math.Min(recvLenLeft, bytes.Length);
frameInfo.AddBufferData(bytes.Skip(0).Take(recvLenReal).ToArray());
frameInfo.data_recv_len = frameInfo.GetBufferLength();
if (frameInfo.data_recv_len >= frameInfo.sPyDataTest.data_len)
{
//Debug.Log("接收成功!");
socketStatus = SocketStatus.eSocketReceived;
if (callBackFun != null)
{
callBackFun(socketStatus, frameInfo.data_buffer);
}
frameInfo.Reset();
bytes = bytes.Skip(recvLenReal).Take(bytes.Length - recvLenReal).ToArray();
if (bytes.Length != 0)
{
ProcessRawData(ref bytes);
}
}
else
{
return;
}
}
else
{
if (recvBuffer != null)
{
byte[] newBytes = new byte[recvBuffer.Length + bytes.Length];
Buffer.BlockCopy(recvBuffer, 0, newBytes, 0, recvBuffer.Length);
Buffer.BlockCopy(bytes, 0, newBytes, recvBuffer.Length, bytes.Length);
bytes = newBytes;
recvBuffer = null;
}
if (bytes.Length >= frameInfo.StructLength)
{
bool ret = true;
ret = frameInfo.IsHeaderMatch(bytes.Take(frameInfo.StructLength).ToArray());
if (ret == true)
{
//Debug.Log(frameInfo.sPyDataTest);
frameInfo.Set(frameInfo.sPyDataTest.data_type, frameInfo.sPyDataTest.data_len);
bytes = bytes.Skip(frameInfo.StructLength).Take(bytes.Length - frameInfo.StructLength).ToArray();
if (bytes.Length != 0)
{
ProcessRawData(ref bytes);
}
}
else
{
Debug.Log(bytes);
}
}
else
{
recvBuffer = bytes;
Debug.Log(bytes);
}
}
}
要約する
ここでは、コードの実装方法と、対応する主要な手順の原則の説明のみを提供します。間違いがある場合は、修正してください。