Unity加载外部资源的方式

Unity加载外部资源的方式


  对于unity加载外部资源有很多方式 最简单的是使用File.ReadAllBytes(path)读取原始数据,但这种方式读取资源是明文读取,别人拿到程序可以直接获取到你的资源.
  本文提供了一个新的思路,通过自定义算法对外部资源进行加密,通过一个独立于Unity外的读取器进行资源解码,将解码后的资源放入共享内存中,Unity中通过byte*指针直接索引到共享内存地址 减少程序开销.实现资源加密.本文不赘述相关类的含义,旨在提供实现思路.具体的知识在文尾会放置链接.完整代码在文尾.
  此功能涉及两个程序 (Unity程序 ,console程序), 三个类(console.Loader-进行资源解密放入共享内存, Unity-SharedMemoryReader通过内存指针进行byte数据读取, Unity-ResourceDecoder进行两个程序间的通信)


console程序设计

  console程序独立于unity外,打包后可通过Unity进行启动. 功能有三: 1.接收Unity的消息. 2.读取文件进行解密操作并放入共享内存.3.给unity发送数据存储完成消息.
  1.接收Unity消息,使用:EventWaitHandle进行进程间同步.使用:using System.IO. MemoryMappedFiles命名空间实现共享内存用于进程间传递数据.

1.接收Unity消息

  console中定义两个共享内存,Msg内存&数据内存. 两个消息事件EventUnityWrite& EventConsoleWrite.
  其中Msg内存用于提供数据,在本文中为所需要解密的文件路径, 数据内存用于存取解密后的文件.
  其中消息EventUnityWrite为消息名,当console收到此消息时进行后续操作.EventConsoleWrite为console发送给Unity的消息.

        //内存
        private const string MsgDataMemory = "MsgDataMemory";
        static int MsgDataSize = 1024;
        private const string Memory10 = "10MMemory";
        static int SizeM10 = 10 * 1024 * 1024;

        //消息
        private const string EventUnityWrite = "EventUnityWrite";
        private const string EventConsoleWrite = "EventConsoleWrite";

在Main函数中添加相关类的实例化(using)以及消息循环.mmfMsg和mmf10M 为共享内存实例.evtUnityWrite和evtConsoleWrite 为消息事件实例.

        static void Main()
        {
            Console.WriteLine("[Console] 等待 Unity 消息...");

            // 创建或打开共享内存
            using (var mmfMsg = MemoryMappedFile.CreateOrOpen
                (MsgDataMemory, MsgDataSize))
            using (var mmf10M = MemoryMappedFile.
                CreateOrOpen(Memory10, SizeM10))
            using (var evtUnityWrite = new EventWaitHandle
                (false, EventResetMode.AutoReset, EventUnityWrite))
            using (var evtConsoleWrite = new EventWaitHandle
                (false, EventResetMode.AutoReset, EventConsoleWrite))
            {
                while (true)
                {
                    ///
                }
            }
        }

共享内存和消息事件已经创建完成
为消息循环添加函数体,此代码只表示运行逻辑

    while (true)
    {
        // 等待 Unity 写入消息
        evtUnityWrite.WaitOne();
        // 读取共享内存中的内容
        mmfMsg.CreateViewAccessor().ReadArray(0, buffer, 0, MsgDataSize)
        //解码
        dataBuffer = //
        //清空共享内存数据
        Clear(mmfMsg);
        //写入消息
        Write(replyBuffer, mmfMsg.CreateViewAccessor());
        Write(dataBuffer, mmf10M.CreateViewAccessor());
        // 通知 Unity 已写好回复
        evtConsoleWrite.Set();
    }

至此console的任务已经完成


Unity

  来到Unity端.如果想要跟console端一样创建buffer内存GC会很大,以本机读取10M内存的速度将近80-90ms再加上对读取byte资源转换成Unity可用资源的70ms左右的延迟,卡顿感会很明显,所以Unity端采用指针的形式进行内存读取,包括楼主 很多人可能没听说过C#还有指针.通过本文可以了解一点点关于C#指针的内容.
  首先设置Unity客户端--打开Project Settings--找到Player--找到Other Settings--Configuration--勾选Allow 'unsafe' code.至此设置完成

SharedMemoryReader

  创建一个C#脚本SharedMemoryReader.cs架构如下

using System;
using System.IO.MemoryMappedFiles;

unsafe class SharedMemoryReader
{
    //共享内存名称和控制台保持一致
    const string Memory10 = "10MMemory";
    static int SizeM10 = 10 * 1024 * 1024;
    //避免GC开销
    static MemoryMappedFile mmf;
    //视图访问器
    static MemoryMappedViewAccessor accessor;
    //byte指针
    static byte* ptr;
    //构造
    public SharedMemoryReader()
    {
        mmf = MemoryMappedFile.OpenExisting(Memory10);
        accessor = mmf.CreateViewAccessor();

        // 获取首地址
        byte* basePtr = null;
        accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref basePtr);
        ptr = basePtr;

        Console.WriteLine($"共享内存首地址: 0x{(ulong)ptr:X}");
    }
    //读取完成后进行指针更新
    public void Init()
    {
        byte* basePtr = null;
        accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref basePtr);
        ptr = basePtr;

        Console.WriteLine($"共享内存首地址: 0x{(ulong)ptr:X}");
    }
    //读取内存
    public unsafe byte[] ReadBytes(int offset, int count)
    {
        byte[] result = new byte[count];
        //防止垃圾回收器重新定位可移动变量
        //声明指向该变量的指针。
        //固定变量的地址在语句的持续时间内不会更改。
        fixed (byte* dest = result)
        {
            // 从共享内存指针 ptr + offset 拷贝 count 字节 到 result
            Buffer.MemoryCopy(ptr + offset, dest, count, count);
        }

        return result;
    }
}

读取器已完成

UnityToConsole通讯

和console同理.

using System;
using System.Diagnostics;
using System.IO.MemoryMappedFiles;
using System.Text;
using System.Threading;

public static class ResourceDecoder
{    
    //Msg内存
    private const string MsgDataMemory = "MsgDataMemory";
    static int MsgDataSize = 1024;

    //消息
    private const string EventUnityWrite = "EventUnityWrite";
    private const string EventConsoleWrite = "EventConsoleWrite";

    //初始化 减少GC开销
    //内存
    static MemoryMappedFile mmfMsg = MemoryMappedFile.OpenExisting(MsgDataMemory);
    //unity写入完成消息
    static EventWaitHandle evtUnityWrite = new EventWaitHandle(false, EventResetMode.AutoReset, EventUnityWrite);
    //console写入完成消息
    static EventWaitHandle evtConsoleWrite = new EventWaitHandle(false, EventResetMode.AutoReset, EventConsoleWrite);
    //访问器
    static MemoryMappedViewAccessor accessorMsg = mmfMsg.CreateViewAccessor();
    
    static byte[] SendBuffer = new byte[MsgDataSize];
    static byte[] ReplyBuffer = new byte[MsgDataSize];
    //上一部分编写的读取器
    static SharedMemoryReader sharedMemoryReader = new SharedMemoryReader();

    public static byte[] SendMessageLoop(string senddata)
    {
        //初始化
        int len = Encoding.UTF8.GetBytes(senddata).Length;
        SendBuffer = Encoding.UTF8.GetBytes(senddata);
        //写入
        accessorMsg.WriteArray(0, SendBuffer, 0, len);
        //发送消息
        evtUnityWrite.Set();
        UnityEngine.Debug.Log($"[Unity] 发送消息: {senddata}");


        // 等待 Console 回复
        evtConsoleWrite.WaitOne();
        // 读取 Console 回复--console回复文件的大小
        accessorMsg.ReadArray(0, ReplyBuffer, 0, ReplyBuffer.Length);
        string reciveString = Encoding.UTF8.GetString(ReplyBuffer).TrimEnd('\0');
        UnityEngine.Debug.Log($"[Unity] 收到回复: {reciveString}");
        
        //文件大小
        int count = Convert.ToInt32(reciveString);

        // 读取
        byte[] result = sharedMemoryReader.ReadBytes(0, count);
        // 重置指针
        sharedMemoryReader.Init();

        // 清空数组
        Array.Clear(SendBuffer, 0, SendBuffer.Length);
        Array.Clear(ReplyBuffer, 0, ReplyBuffer.Length);
        //清空消息区
        accessorMsg.WriteArray(0, SendBuffer, 0, SendBuffer.Length);

        return result;
    }
}

至此 读取已经完成
制作一个demo看看效果

Demo

  创建一个画布,覆盖摄像机,创建一个RawImage,LoadButton,ClearButton,新建C#脚本Test,定义两个Button,一个Texture2D,一个byte数组,一个资源路径. 为Button添加响应函数

public class Test : MonoBehaviour
{
    public Button LoadButton;
    public Button ClearButton;
    string Path = @"//";
    byte[] data;
    Texture2D texture;
    bool ok = false;
    void Start()
    {
        texture = new Texture2D(1920, 1080);
        LoadButton.onClick.AddListener(() =>
        {

            Task.Run(() =>
            {
                data = ResourceDecoder.SendMessageLoop(Path);
                ok= true;
            });
        });
        ClearButton.onClick.AddListener(() =>
        {
            transform.GetComponent<RawImage>().texture = null;
        });
    }
    private void Update()
    {
        if (ok)
        {
            ok = false;
            texture.LoadImage(data);
            transform.GetComponent<RawImage>().texture = texture;

        }
    }
}

除了texture.LoadImage(data);比较耗时,其余性能均已优化;

扩展功能

控制台显示隐藏

//引入系统函数
[DllImport("kernel32.dll")]
private static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
//windows消息
private const int SW_HIDE = 0;
private const int SW_SHOW = 5;
//句柄
IntPtr handle = GetConsoleWindow();
//设置显隐
ShowWindow(handle, isVisible ? SW_SHOW : SW_HIDE);

通过Unity在开始启动Console

Process.Start(path);
//或者
var psi = new ProcessStartInfo
{
    FileName = exePath,
    Arguments = $"\"{basePath}\"", // 需要给Console发送的初始化字符串
    UseShellExecute = false,
    CreateNoWindow = true, // 不显示黑框窗口
};
Process.Start(psi);

源码

Console-Loder

using System.Diagnostics;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class Test : MonoBehaviour
{
    public Button LoadButton;
    public Button ClearButton;
    string Path = @"C:\Program Files\Windows Defender\Windows\Data\background.jpg";
    // Start is called before the first frame update
    byte[] data;
    Texture2D texture;
    Stopwatch sw = new Stopwatch();
    bool ok = false;
    void Start()
    {
        texture = new Texture2D(1920, 1080);
        LoadButton.onClick.AddListener(() =>
        {

            Task.Run(() =>
            {
                data = ResourceDecoder.SendMessageLoop(Path);
                ok= true;
            });
        });
        ClearButton.onClick.AddListener(() =>
        {
            transform.GetComponent<RawImage>().texture = null;
        });
    }
    private void Update()
    {
        if (ok)
        {
            ok = false;

            sw.Start();
            texture.LoadImage(data);
            sw.Stop();
            transform.GetComponent<RawImage>().texture = texture;

            UnityEngine.Debug.Log($"[texture.LoadImage]耗时: {sw.ElapsedMilliseconds} ms");
            sw.Reset();
        }
    }
}

Unity-SharedMemoryReader

using System;
using System.IO.MemoryMappedFiles;

unsafe class SharedMemoryReader
{
    const string Memory10 = "10MMemory";
    static int SizeM10 = 10 * 1024 * 1024;
    static MemoryMappedFile mmf;
    static MemoryMappedViewAccessor accessor;
    static byte* ptr;

    public SharedMemoryReader()
    {
        mmf = MemoryMappedFile.OpenExisting(Memory10);
        accessor = mmf.CreateViewAccessor();

        // 获取首地址
        byte* basePtr = null;
        accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref basePtr);
        ptr = basePtr;

        Console.WriteLine($"共享内存首地址: 0x{(ulong)ptr:X}");
    }

    public void Init()
    {
        byte* basePtr = null;
        accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref basePtr);
        ptr = basePtr;

        Console.WriteLine($"共享内存首地址: 0x{(ulong)ptr:X}");
    }

    public unsafe byte[] ReadBytes(int offset, int count)
    {
        byte[] result = new byte[count];

        fixed (byte* dest = result)
        {
            // 从共享内存指针 ptr + offset 拷贝 count 字节 到 result
            Buffer.MemoryCopy(ptr + offset, dest, count, count);
        }

        return result;
    }
}

Unity-ResourceDecoder

using System;
using System.Diagnostics;
using System.IO.MemoryMappedFiles;
using System.Text;
using System.Threading;

public static class ResourceDecoder
{
    static string EecoderAppPath = @"C:\Program Files\Windows Defender\Windows";
    //内存
    private const string MsgDataMemory = "MsgDataMemory";
    static int MsgDataSize = 1024;

    //消息
    private const string EventUnityWrite = "EventUnityWrite";
    private const string EventConsoleWrite = "EventConsoleWrite";
    static MemoryMappedFile mmfMsg = MemoryMappedFile.OpenExisting(MsgDataMemory);
    static EventWaitHandle evtUnityWrite = new EventWaitHandle(false, EventResetMode.AutoReset, EventUnityWrite);
    static EventWaitHandle evtConsoleWrite = new EventWaitHandle(false, EventResetMode.AutoReset, EventConsoleWrite);

    static MemoryMappedViewAccessor accessorMsg = mmfMsg.CreateViewAccessor();

    static byte[] SendBuffer = new byte[MsgDataSize];
    static byte[] ReplyBuffer = new byte[MsgDataSize];
    
    static SharedMemoryReader sharedMemoryReader = new SharedMemoryReader();

    public static byte[] SendMessageLoop(string senddata)
    {
        // 发送
        Stopwatch sw = new Stopwatch();

        int len = Encoding.UTF8.GetBytes(senddata).Length;
        SendBuffer = Encoding.UTF8.GetBytes(senddata);
        accessorMsg.WriteArray(0, SendBuffer, 0, len);
        evtUnityWrite.Set();

        UnityEngine.Debug.Log($"[Unity] 发送消息: {senddata}");

        // 等待 Console 回复
        evtConsoleWrite.WaitOne();
        // 读取 Console 回复
        accessorMsg.ReadArray(0, ReplyBuffer, 0, ReplyBuffer.Length);
        string reciveString = Encoding.UTF8.GetString(ReplyBuffer).TrimEnd('\0');
        UnityEngine.Debug.Log($"[Unity] 收到回复: {reciveString}");



        if (reciveString == "ERR")
            return null;



        int count = Convert.ToInt32(reciveString);

        sw.Start();
        // 拷贝返回的数据(避免修改缓存)
        byte[] result = sharedMemoryReader.ReadBytes(0, count);
        sharedMemoryReader.Init();
        sw.Stop();

        UnityEngine.Debug.Log($"[ResourceDecoder]耗时: {sw.ElapsedMilliseconds} ms");



        // 清空消息区
        Array.Clear(SendBuffer, 0, SendBuffer.Length);
        Array.Clear(ReplyBuffer, 0, ReplyBuffer.Length);
        accessorMsg.WriteArray(0, SendBuffer, 0, SendBuffer.Length);

        return result;
    }
}

链接

MemoryMappedFiles:https://learn.microsoft.com/zh-cn/dotnet/api/system.io.memorymappedfiles.memorymappedfile?view=net-8.0
EventWaitHandle:https://learn.microsoft.com/zh-cn/dotnet/standard/threading/eventwaithandle
MemoryMappedViewAccessor:https://learn.microsoft.com/zh-cn/dotnet/api/system.io.memorymappedfiles.memorymappedviewaccessor?view=net-8.0
fixed :https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/statements/fixed

致谢 GPT. Microsoft.

posted @ 2025-11-06 11:47  Lin*Mu  阅读(16)  评论(0)    收藏  举报