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.

浙公网安备 33010602011771号