unity + python socket通信,自定义数据包

这篇具有很好参考价值的文章主要介绍了unity + python socket通信,自定义数据包。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

unity和python相互之间通过socket通信来发送自定义数据包是一个利用unity构建场景和通过python来做数据处理的方式,能够有效的利用两种不同语言的优势。

我已经将对应的操作封装为对应的一个模块,SocketTools.cs,我们先来看一下具体的代码用法(这段代码是一个简单的将unity主相机渲染的图像经过编码处理后通过socket发送给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);
            }
        }

    }

}

下面是对应的代码效果:

unity+python

下面将会针对一些技术细节做详细的说明,如果您对此实现的细节不感兴趣,您也可以直接下载我已经完成的项目代码:

unity和python通过自定义socket协议实现异步通信资源-CSDN文库

接下来是详细的技术实现过程,总的来讲主要技术手段分为三个部分:

1. python和unity异步socket的实现

2. python和unity处理自定义数据包的拆分和合并的实现

3. python和unity两者socket通信的数据包认证实现

4. unity数据处理异步封装逻辑

1. python和unity异步socket的实现

a. python异步socket

由于此处python主要是作为服务端的,所以此处pytho代码主要是讲python作为socket tcp server来实现的。

首先是初始化服务器代码

# 初始化服务器
    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))

然后是启动服务器监听代码,首先 start 开启一个新的线程用于监听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 异步socket

unity的异步socket相较而言会比较麻烦一点,因为它的异步状态每次都需要手动的通过代码去复位调整,下面将针对其过程做具体的说明。

关于整个流程我觉得用流程图会比较合适,下面是代码的一个简单的流程图:

unity python,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#socket的代码。

2. python和unity处理自定义数据包的拆分和合并的实现

但是大家都知道,tcp是流式传输协议,特别是针对图像这类大的数据包是不能够很好的实现帧之间的拆分的,我们需要自定义协议的数据包头和数据包体来实现不定长的数据传输,接下来讲一下如何在python和c#中实现字节层面上的数据拆分和组合。

a. python

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

在unity的c#中,为了便于解析对应的数据帧头的内容,采用的是将对应的byte[]类型的变量直接转换为struct的方式,对应的两者相互转换的代码如下:

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;
    }

其次就是一些简单的内存操作,由于c#封装的缘故,我们并不能像c++那样直接通过指针的方式访问内存数据,我们需要借助Buffer类,具体的用法如下所示:

//用于同时操作内存块中的数据
public static void BlockCopy (Array src, int srcOffset, Array dst, int dstOffset, int count);

它可以将src起始位置偏移srcOffset的数据截取count位复制到dst起始位置偏移dstOffset的位置后面count长度的内粗区域中。

如果要截取一个byte[]类型的变量指定范围的数据,需要用到如下的函数:

bytes.Skip(recvLenReal).Take(bytes.Length - recvLenReal).ToArray();

从头跳过recvLenReal的长度然后截取bytes.Length - recvLenReal长的数据并将其转换为byte[]类型的变量。

3. 数据帧封装逻辑

关于自定义数据包的实现主要分为两部分:数据头和数据主体。

数据包帧头的定义方式如下:

数据头 数据类型 数据主体长度
             

两个字节的数据头用于进行协议的区分辨认;一个字节的数据类型用于标识后续的数据主体的数据类型,例如:图片或者文字或者其他的内容;四个字节的数据主体长度共同组成一个int类型的变量,可以指示后续的数据总长度。

其中该数据头在unity和python的实现方法如下图所示:

public struct DataHeader
{
    public byte header1;
    public byte header2;
    public byte data_type;
    public int data_len;
    
}

python部分的代码上面有展示,此处不再重复。

4. python和unity两者socket通信的数据包认证实现

关于数据包认证的代码,两者采用的是同一个逻辑此处仅提供对应的逻辑处理注释内容,只是在代码的实现上稍有不同。

    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);
            }
        }
        
    }

总结

此处仅仅提供对应的关键步骤的代码实现手段和原理讲解,如有错误欢迎大家指正!文章来源地址https://www.toymoban.com/news/detail-719571.html

到了这里,关于unity + python socket通信,自定义数据包的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【UE5】UE5与Python Socket通信中文数据接收不全

    最近在使用UE的Socket模块与Python服务器进行通信时遇到了一些坑,特此记录一下。 先来复现一下问题,这里只截取关键代码。 UE端: Python端: 运行结果: UE端发送的数据: Python端接收到数据: 可以看到数据容量并没有超出缓存上限,且Python端接收的数据都有做utf-8的编码转

    2024年02月14日
    浏览(41)
  • WinForm內嵌Unity(Socket通信)

    最近有需求要实现WinForm和Unity交互,也就是通信,查过几个方案有用UnityWebPlayer Control组件的(由于版本问题所以我没尝试),也有把Winform打包成dll动态链接库然后unity内引入的,还有打包Unity.exe然后Winform内嵌入的,后面两种都可以。 一.Winform打包成dll动态链接库然后unity内引

    2024年02月13日
    浏览(33)
  • 【Unity】Socket网络通信(TCP) - 最基础的客户端通信流程

    这篇文章主要内容是客户端与服务器通信的内容,服务端代码可以看我的这一篇文章【Unity】Socket网络通信(TCP) - 最基础的C#服务端通信流程 客户端与服务器的整个流程比较相似,客户端会更加简单一些: 创建socket 连接服务器 收发消息 释放socket,关闭连接 和服务端创建

    2024年02月03日
    浏览(46)
  • 【Unity】Socket网络通信(TCP) - 最基础的C#服务端通信流程

    我这里新建了一个C#控制台项目来写服务端代码。 下面是基于C# Socket的最基础服务端通信流程: 创建服务端Socket对象 绑定IP地址和端口 设置最大监听客户端数量 等待客户端连接 收发消息 释放连接 基于上面流程就能实现一个最简单并且能和客户端通信的服务器程序,每个步

    2023年04月16日
    浏览(61)
  • 【Unity】Socket网络通信(TCP) - 实现简单的多人聊天功能

    多客户端连接服务器其原理是在服务端保存客户端连入后与客户端通信的socket,由于等待客户端连接会阻塞主线程,所以结合多线程就能实现多客户端连入功能。多人聊天只需要将A客户端发来的消息,转发给除A客户端外的其他客户端,即可实现。如果你还不怎么熟悉服务端

    2024年02月03日
    浏览(62)
  • 十八、Unity游戏引擎入门

    1、下载     首先需要下载Unity Hub,下载网址:https://unity.com/cn。     然后在其中下载Unity编辑器并安装,可选择最新版本。     接着需要选择适合的开发环境,例如Android Studio或Xcode,以便进行手机游戏开发。在安装完Unity后,需要根据项目需求下载对应的模块和插件,例

    2024年02月16日
    浏览(73)
  • Unity3D 网络游戏框架(二、同步Socket) 参考连接:Socket 类 (System.Net.Sockets) | Microsoft Learn

    1、Socket.Connect() 2、Socket.Send() 3、Socket.Receive() 在了解完Socket通讯流程图和相关API之后我们来开发客户端代码:  在Unity中添加 两个Button、一个InputField和有Text ,Connect 方法 绑定连接的按钮, Send 方法绑定发送按钮。 Connect :客户端点击连接后会和服务端进行连接,这里面127

    2024年02月16日
    浏览(44)
  • 使用团结引擎开发Unity 3D射击游戏

           本案例是初级案例,意在引导想使用unity的初级开发者能较快的入门,体验unity开发的方便性和简易性能。       本次我们将使用团结引擎进行开发,帮助想体验团结引擎的入门开发者进行较快的环境熟悉。      本游戏是一个俯视角度的射击游戏。主角始终位于屏幕

    2024年01月19日
    浏览(72)
  • Python物联网开发-Python_Socket通信开发-Python与Tcp协议物联网设备通信-Socket客户端

            Python在物联网开发中的重要愈来愈重,因此,掌握Python语言与物联网设备之间的通信就显得尤为重要,可以通过编写Python程序实现获取物联网设备的传感器数值并可以更改物联网设备上的执行器状态。         首先,当使用Python进行Socket通信时,需要导入Python的so

    2024年02月17日
    浏览(67)
  • Unity、UE、Cocos游戏开发引擎的区别

    Unity、Unreal Engine(UE)和Cocos引擎是三个常用的游戏开发引擎,它们在功能和特性上有一些区别。以下是它们之间的主要区别: 编程语言:Unity使用C#作为主要的编程语言,开发者可以使用C#脚本进行游戏逻辑编写。Unreal Engine主要使用C++作为编程语言,但也支持蓝图系统,允许

    2024年02月22日
    浏览(62)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包