随着项目越来越大,资源越来越多,有一套资源导入自动化设置很有必要,它不但可以减少你的工作量,也能更好的统一管理资源,保证资源的导入设置最优,还不会出错。
在Unity中除了引擎自己创建的资源,更多的是外部导入进来的,像贴图纹理、模型动画、音效视频,这些导入的外部资源都会经过Unity引擎转换成一种Unity内部格式的资源,存储在Library下。
AssetPostProcessor是一个编辑器类,用来管理资源导入,当资源导入之前和之后都会发送通知,可以根据不同的资源类型,在导入之前和之后做不同的处理,来修改Untiy内部格式资源。一般我们通过这个类中OnPreprocess和OnPostprocess消息处理函数来修改资源数据和设置,这两者的区别可以简单理解为:
AssetPostprocessor 常用API介绍:
using UnityEngine;
using UnityEditor;
public class MyAssetPostprocessor : AssetPostprocessor
{
//模型导入之前调用
public void OnPreprocessModel() { }
//模型导入之后调用
public void OnPostprocessModel(GameObject go) { }
//Texture导入之前调用,针对Texture进行设置
public void OnPreprocessTexture() { }
//Texture导入之后调用,针对Texture进行设置
public void OnPostprocessTexture(Texture2D tex) { }
//导入Audio后操作
public void OnPostprocessAudio(AudioClip clip) { }
//导入Audio前操作
public void OnPreprocessAudio() { }
//所有的资源的导入,删除,移动,都会调用此方法,注意,这个方法是static的
public static void OnPostprocessAllAssets(string[] importedAsset, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
foreach (string filePath in importedAsset)
{
Debug.Log("importedAsset:" + filePath);
}
foreach (string filePath in deletedAssets)
{
Debug.Log("deletedAssets:" + filePath);
}
foreach (string filePath in movedAssets)
{
Debug.Log("movedAssets:" + filePath);
}
foreach (string filePath in movedFromAssetPaths)
{
Debug.Log("movedFromAssetPaths:" + filePath);
}
}
}
纹理导入设置示例:
public void OnPreprocessTexture()
{
//获得importer实例
TextureImporter textureImporter = assetImporter as TextureImporter;
Debug.Log("OnPreprocessTexture: " + textureImporter.assetPath);
//设置Read/Write Enabled开关,不勾选
textureImporter.isReadable = false;
if (textureImporter.assetPath.StartsWith("Assets/UI"))
{
//设置UI纹理Generate Mipmaps
textureImporter.mipmapEnabled = false;
//设置UI纹理WrapMode
textureImporter.wrapMode = TextureWrapMode.Clamp;
}
}
也可以使用OnPostprocessAllAssets,导入音效设置示例:
public static void OnPostprocessAllAssets(string[] importedAsset, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
foreach (string filePath in importedAsset)
{
if (filePath.EndsWith(".wav") || filePath.EndsWith(".mp3"))
{
var Importer = AssetImporter.GetAtPath(filePath);
if (Importer != null && Importer.GetType() == typeof(AudioImporter))
{
var mImporter = Importer as AudioImporter;
mImporter.forceToMono = true;
var set = mImporter.defaultSampleSettings;
if (set.sampleRateOverride != 22050)
{
set.sampleRateSetting = AudioSampleRateSetting.OverrideSampleRate;
set.sampleRateOverride = 22050;
mImporter.defaultSampleSettings = set;
}
}
}
}
}
除了AssetPostprocessor,Unity还提供了Preset Manager,也可以对Unity的资源设置进行统一管理。Preset Manager通过创建Preset(预设),保存对象的属性信息,以此为模板可应用到新创建的组件或新导入的资源中,使它们有相同的属性设置。
Preset Manager: 是允许管理在将组件添加到游戏对象或将新资源添加到项目时创建的自定义预设以指定默认属性。您定义的默认预设会覆盖Unity的默认设置。 注意:不能为项目设置、首选项设置或原生资源(Materials, Animations, or SpriteAtlas)设置默认属性。 除了在创建component和导入资源时使用默认Presets外,在Inspecrot窗口中使用 Reset 按钮时,Unity还会应用默认Presets设置.
以音效为例,当你有一个音效文件,导入Unity并设置好你理想的属性,在Inspector栏点击保存预设的按钮,然后点击SaveCurrentTo,保存为一个新的Preset。
你可以创建三种类型的Preset,导入器、组件、ScriptableObject
创建好Preset后,在Preset Manager窗口点击Add Default Preset,则可为默认设置指定预设。
你可以添加各种各样的Preset,同一种组件,同一种导入器还可以有多个Preset,可以使用Filter(过滤器)规则来定义何时应用预设,可以按文件名、目录和文件扩展名进行过滤。
Preset Manager 虽然方便,但灵活性有所欠缺。以音效为例,如果要区分长音效短音效应用不同的设置,Preset Manager则无法满足,但AssetPostprocessor则可以。
纹理可以说是Unity中最常用的资源了,也是资源占比最多的,纹理使用不当将给项目带来严重的性能问题。
纹理压缩是一种图像压缩,用于在保持视觉质量的同时减小纹理数据大小。我们平时看到的纹理都是png、jpg、tga等格式,但在Unity中所说的纹理压缩和这些格式几乎没有关系,纹理导入Unity后,会生成Unity能识别的Texture类型资源。
Unity 支持许多常见的图像格式作为导入纹理的源文件(例如 JPG、PNG、PSD 和 TGA)。但是,3D 图形硬件(例如显卡或移动设备)在实时渲染期间不会使用这些格式。这种硬件要求纹理以专门格式进行压缩,这些格式针对快速纹理采样进行了优化。不同的平台和设备分别都有自己的专有格式。
主要考虑内存+带宽,尤其在移动设备上,内存带宽有限。假如我们直接使用RGB 16、RGBA 32 这些未经压缩的纹理格式,如一张RGBA32格式512*512的纹理贴图,一个像素占4Byte,512x512x4B=1048576B=1M,但如果使用ETC2,只有256KB,相差4倍。内存是一方面,另一个重要的是数据传输时的带宽,带宽是发热的元凶,特别在移动设备上。
因此我们需要一种内存占用既小又能被GPU读取的格式——压缩纹理格式。纹理压缩对应的算法是以某种形式的固定速率有损向量量化将固定大小的像素块编码进固定大小的字节块中。
尽管png、jpg、tga的压缩率很高,但并不适合纹理,主要问题是不支持像素的随机访问,这对GPU相当不友好,GPU渲染时只使用需要的纹理部分,我们总不能为了访问某个像素去解码整张纹理吧?不知道顺序,也不确定相邻的三角形是否在纹理上采样也相邻,很难有优化。这类格式更适合下载传输以及减少磁盘空间而使用,平时我们能在图像软件浏览这些格式的图片,是经过CPU解压后,再传给GPU渲染的。
ASTC是在OpenGL ES3.0出现后在2012年中产生的一种业界领先的纹理压缩格式,它的压缩分块从4x4到12x12最终可以压缩到每个像素占用1bit以下,压缩比例有多种可选。ASTC格式支持RGBA,且适用于2的幂次方长宽等比尺寸和无尺寸要求的NPOT(非2的幂次方)纹理。
ASTC在Android、IOS、WebGL都支持,在压缩质量和容量上有很大的优势。如一张512*512的纹理贴图,ASTC 6x6 block压缩格式,内存占用为115.6KB,只有ETC2的一半,但质量和ETC2相差无几。
不管Android、IOS都首推ASTC,都2023年了,国产Android手机基本都支持了,IOS在iphone6以上的设备都支持。
如果你的项目考虑到兼容老旧设备,或者海外手机的情况下,Android RGBA Compressed ETC2是首选
Crunch 压缩是一种基于 DXT 或 ETC 纹理压缩的有损压缩格式(压缩过程中会丢失部分数据)。在运行时,纹理在 CPU 上解压缩为 DXT 或 ETC,然后上传到 GPU。Crunch 压缩有助于纹理在磁盘上使用尽可能少的空间,但对于运行时内存使用量没有影响。Crunch 纹理可能需要很长时间进行压缩,但在运行时的解压缩速度非常快。
RGBA Crunched ETC2,虽然质量方面有些丢失,但包体小很多,对包体有要求或者网络下载有要求都可考虑。
如果你的项目考虑到兼容iphone5s以及之前的设备,只能选择RGBA Compressed PVRTC 4了,PVRTC压缩质量一般会比较差,可以考虑拆分贴图,另外PVRTC要求纹理的长宽相等并且要是2的整次幂。
选择纹理压缩格式需要在文件大小、质量和压缩时间之间取得平衡。
Unity提供了三种过滤模式:
一般的纹理使用双线性过滤(Bilinear)即可,减少Trilinear的使用,Trilinear模式对表现效果的提升,是以GPU的额外开销为代价的。同等条件下,三线性过滤的GPU占用是最高的。
优化UGUI应从哪些方面入手?
可以从CPU和GPU两方面考虑,CPU方面,避免触发或减少Canvas的Rebuild和Rebatch,减少Drawcall,减少CPU处理顶点的时间;GPU方面,降低Overdraw,缩小纹理大小。
Batch 构建过程是指Canvas通过结合网格绘制它所承载的UI元素,生成适当的渲染命令发送给Unity图形流水线。Batch的结果被缓存复用,直到这个Canvas被标为dirty,当Canvas中某一个构成的网格改变的时候就会标记为dirty,这个Dirty就会触发Rebatch。
Rebatch不仅有批处理排序,还有网格合并之类的。Canvas的网格从那些Canvas下的CnavasRenderer组件中获取,但不包含任何子Canvas。
所以对于UGUI的性能分析,要分开两点
这两点,都会影响性能。但是Rebatch是有多线程的加持的,而Rebuild是在主线程的。
Rebuild 过程是指Layout和UGUI的C#的Graphic组件的网格被重新计算,这是在CanvasUpdateRegistry类中执行的。这是一个C#类,打开UI的源码,它里面的一个函数,叫做PerformUpdate,当一个Canvas组件触发它的WillRenderCanvases事件时,这个方法就会被执行。这个事件每帧调用一次,这也是为什么我们看Profile的时候,出现性能高峰的总是会看到WillRenderCanvases的原因了。
PerformUpdate的运行过程分3步:
Rebuild分为 Layout Rebuild 和 Graphic Rebuild
要重新计算一个或多个Layout组件中包含的组件的适当位置(和可能的大小),必须按其适当的层次结构顺序应用Layouts。在GameObject层次结构中靠近根部的布局可能会更改嵌套在其中的任何布局的位置和大小,因此必须首先进行计算。
为此,UGUI根据层次结构中的深度对dirty的Layout组件列表进行排序。层次结构中较高的Layout(即,父节点较少)将被移到列表的前面。
然后,排序好的Layout组件的列表将被rebuild,在这个步骤Layout组件控制的UI元素的位置和大小将被实际改变。
当Graphic组件被rebuild的时候,UGUI会将控制权传递给ICanvasElement接口的Rebuild方法。Graphic执行了这一步,并在rebuild过程中的PreRender阶段运行了两个不同的rebuild步骤:
Graphic的Rebuild不会按照Graphic组件的特殊顺序进行,并且不需要任何排序操作。
触发Rebatch的条件:
触发Rebuild的条件:
基于以上Canvas的Rebuild和Rebatch原理,我们可以做以下优化:
UGUI 合批原理
UGUI的合批规则是进行重叠检测,然后分层合并。
基于UGUI合批原理,我们可以做以下优化:
可在Scene界面以Overdraw模式查看,颜色越亮(橙色)Overdraw越大。
Mask依赖Image组件,占用两个Batch,多一倍Overdraw,可以裁剪任意形状。
RectMask2D不依赖Image组件,不占用Batch,没有Overdraw,只能裁剪规则形状。
因此,一般情况下,规则的裁剪尽量用RectMask2D代替Mask,特别是在使用ScrollRect时。
RectMask2D一定比Mask好吗?并不是,Mask间是可以合批的,而RectMask2D间不行,因此当要使用多Mask时,如背包界面中的道具格子,每个格子有裁剪需求时,尽量用Mask,Mask可合批,而RectMask2D会导致合批被打断。
因此:
射线检测遍历所有将'Raycast Target'设置为true的Graphic组件。每一个Raycast Target都会被进行测试。如果一个Raycast Target通过了所有的测试,那么它就会被添加到“被命中”列表中。
每个Graphic Raycaster都将遍历 Transform层次结构一直到根,此操作的成本与层次结构的深度成比例线性增长。因此进行射线检测的元素越多,层级越深,消耗越高。
鉴于所有射线检测目标都必须由Graphic Raycaster进行测试,因此最好的做法是仅在必须接收点击事件的UI组件上启用'Raycast Target'设置。检测目标列表越少,遍历的层级越浅,每次射线检测的速度越快。
音效的优化一般可从三方面考虑:
Unity支持很多音频格式,WAV、MP3、OGG等,建议原始音频资源尽量采用未压缩的WAV格式,背景音乐也可考虑MP3格式。
因此建议,简短音效导入后小于200kb,采用Decompress on Load模式,对于复杂音效,大小大于200kb,长度超过5秒的音效采用Compressed In Memory模式,对于长度较长的音效或背景音乐则采用Streaming模式,虽然会有CPU额外开销,但节省内存并且加载不卡顿。
因此建议,移动平台大多数声音尽量采用Vorbis压缩设置,IOS平台或不打算循环的声音可以选择MP3格式,对于简短、常用的音效,可以采用解码速度快的ADPCM格式(PCM为未压缩格式)
一般移动平台音频采样率经验数据建议设置为22050Hz,较高的采样率听不出区别,只会徒增文件大小和内存占用。
另外,当实现静音功能时,不要简单的将音量设置为0,应销毁音频(AudioSource)组件,将音频从内存中卸载。
Unity在移动平台上音频播放有延迟,特别在Android上,感觉很明显。这是正常的,可在ProjectSettings的Audio中的DSP Buffer Size设置为Best latency。
也可以尝试使用一些三方插件,Wwise是一款不错的音频引擎,不少大厂都在用,https://github.com/monkrythree/Unity_Wwise
现在的移动设备五花八门,性能高低不同,如想让不同机型的设备都能流畅的运行我们的app,就得针对性地优化。一种通用的方式就是区分高中低机型,根据级别使用不同的画质、贴图质量、阴影、模型特效等。
要区分设备的高中低级别,可根据设备的内存、CPU、GPU等相关数据为标准进行划分。
如下是android手机的代码实现:
package com.unity3d.player;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class Themis {
/**
* 根据内存和CUP级别,获取设备高中低级别
* @param context - Context object.
* @return
*/
public static int judgeDeviceLevel(Context context) {
int level = 0;
int ramLevel = judgeMemory(context);
int cpuLevel = judgeCPU();
level = Math.min(ramLevel, cpuLevel);
Log.i("Themis", "ramLevel:" + ramLevel + ", cpuLevel:" + cpuLevel + ", level:" + level);
return level;
}
/**
* 评定内存的等级.
* @return
*/
private static int judgeMemory(Context context) {
long ramMB = getTotalMemory(context) / (1024 * 1024);
Log.i("Themis", "total memory:" + ramMB);
int level = 0;
if (ramMB <= 2000) { //2G或以下的最低档
level = 1;
} else if (ramMB <= 3000) { //2-3G
level = 2;
} else if (ramMB <= 4000) { //4G档 2018主流中端机
level = 3;
} else if (ramMB <= 6000) { //6G档 高端机
level = 4;
} else { //6G以上 旗舰机配置
level = 5;
}
return level;
}
/**
* 评定CPU等级.(按频率和厂商型号综合判断)
* @return
*/
private static int judgeCPU() {
int level = 0;
int freqMHz = getCPUMaxFreqKHz() / 1000;
Log.i("Themis", "cup freqMHz:" + freqMHz);
if (freqMHz <= 1600) { //1.5G 低端
level = 1;
} else if (freqMHz <= 2000) { //2GHz 低中端
level = 2;
} else if (freqMHz <= 2500) { //2.2 2.3g 中高端
level = 3;
} else if (freqMHz <= 3000) { //3g 高端
level = 4;
} else { //旗舰机配置
level = 5;
}
return level;
}
/**
* The default return value of any method in this class when an
* error occurs or when processing fails (Currently set to -1). Use this to check if
* the information about the device in question was successfully obtained.
*/
public static final int DEVICEINFO_UNKNOWN = -1;
private static final FileFilter CPU_FILTER = new FileFilter() {
@Override
public boolean accept(File pathname) {
String path = pathname.getName();
//regex is slow, so checking char by char.
if (path.startsWith("cpu")) {
for (int i = 3; i < path.length(); i++) {
if (!Character.isDigit(path.charAt(i))) {
return false;
}
}
return true;
}
return false;
}
};
/**
* Calculates the total RAM of the device through Android API or /proc/meminfo.
*
* @param c - Context object for current running activity.
* @return Total RAM that the device has, or DEVICEINFO_UNKNOWN = -1 in the event of an error.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public static long getTotalMemory(Context c) {
// memInfo.totalMem not supported in pre-Jelly Bean APIs.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
ActivityManager am = (ActivityManager) c.getSystemService(Context.ACTIVITY_SERVICE);
am.getMemoryInfo(memInfo);
if (memInfo != null) {
return memInfo.totalMem;
} else {
return DEVICEINFO_UNKNOWN;
}
} else {
long totalMem = DEVICEINFO_UNKNOWN;
try {
FileInputStream stream = new FileInputStream("/proc/meminfo");
try {
totalMem = parseFileForValue("MemTotal", stream);
totalMem *= 1024;
} finally {
stream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
return totalMem;
}
}
/**
* Method for reading the clock speed of a CPU core on the device. Will read from either
* {@code /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq} or {@code /proc/cpuinfo}.
*
* @return Clock speed of a core on the device, or -1 in the event of an error.
*/
public static int getCPUMaxFreqKHz() {
int maxFreq = DEVICEINFO_UNKNOWN;
try {
for (int i = 0; i < getNumberOfCPUCores(); i++) {
String filename =
"/sys/devices/system/cpu/cpu" + i + "/cpufreq/cpuinfo_max_freq";
File cpuInfoMaxFreqFile = new File(filename);
if (cpuInfoMaxFreqFile.exists() && cpuInfoMaxFreqFile.canRead()) {
byte[] buffer = new byte[128];
FileInputStream stream = new FileInputStream(cpuInfoMaxFreqFile);
try {
stream.read(buffer);
int endIndex = 0;
//Trim the first number out of the byte buffer.
while (Character.isDigit(buffer[endIndex]) && endIndex < buffer.length) {
endIndex++;
}
String str = new String(buffer, 0, endIndex);
Integer freqBound = Integer.parseInt(str);
if (freqBound > maxFreq) {
maxFreq = freqBound;
}
} catch (NumberFormatException e) {
//Fall through and use /proc/cpuinfo.
} finally {
stream.close();
}
}
}
if (maxFreq == DEVICEINFO_UNKNOWN) {
FileInputStream stream = new FileInputStream("/proc/cpuinfo");
try {
int freqBound = parseFileForValue("cpu MHz", stream);
freqBound *= 1000; //MHz -> kHz
if (freqBound > maxFreq) maxFreq = freqBound;
} finally {
stream.close();
}
}
} catch (IOException e) {
maxFreq = DEVICEINFO_UNKNOWN; //Fall through and return unknown.
}
return maxFreq;
}
/**
* Reads the number of CPU cores from the first available information from
* {@code /sys/devices/system/cpu/possible}, {@code /sys/devices/system/cpu/present},
* then {@code /sys/devices/system/cpu/}.
*
* @return Number of CPU cores in the phone, or DEVICEINFO_UKNOWN = -1 in the event of an error.
*/
public static int getNumberOfCPUCores() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
// Gingerbread doesn't support giving a single application access to both cores, but a
// handful of devices (Atrix 4G and Droid X2 for example) were released with a dual-core
// chipset and Gingerbread; that can let an app in the background run without impacting
// the foreground application. But for our purposes, it makes them single core.
return 1;
}
int cores;
try {
cores = getCoresFromFileInfo("/sys/devices/system/cpu/possible");
if (cores == DEVICEINFO_UNKNOWN) {
cores = getCoresFromFileInfo("/sys/devices/system/cpu/present");
}
if (cores == DEVICEINFO_UNKNOWN) {
cores = new File("/sys/devices/system/cpu/").listFiles(CPU_FILTER).length;;
}
} catch (SecurityException e) {
cores = DEVICEINFO_UNKNOWN;
} catch (NullPointerException e) {
cores = DEVICEINFO_UNKNOWN;
}
return cores;
}
/**
* Tries to read file contents from the file location to determine the number of cores on device.
* @param fileLocation The location of the file with CPU information
* @return Number of CPU cores in the phone, or DEVICEINFO_UKNOWN = -1 in the event of an error.
*/
private static int getCoresFromFileInfo(String fileLocation) {
InputStream is = null;
try {
is = new FileInputStream(fileLocation);
BufferedReader buf = new BufferedReader(new InputStreamReader(is));
String fileContents = buf.readLine();
buf.close();
return getCoresFromFileString(fileContents);
} catch (IOException e) {
return DEVICEINFO_UNKNOWN;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Do nothing.
}
}
}
}
/**
* Converts from a CPU core information format to number of cores.
* @param str The CPU core information string, in the format of "0-N"
* @return The number of cores represented by this string
*/
private static int getCoresFromFileString(String str) {
if (str == null || !str.matches("0-[\\d]+$")) {
return DEVICEINFO_UNKNOWN;
}
return Integer.valueOf(str.substring(2)) + 1;
}
/**
* Helper method for reading values from system files, using a minimised buffer.
*
* @param textToMatch - Text in the system files to read for.
* @param stream - FileInputStream of the system file being read from.
* @return A numerical value following textToMatch in specified the system file.
* -1 in the event of a failure.
*/
private static int parseFileForValue(String textToMatch, FileInputStream stream) {
byte[] buffer = new byte[1024];
try {
int length = stream.read(buffer);
for (int i = 0; i < length; i++) {
if (buffer[i] == '\n' || i == 0) {
if (buffer[i] == '\n') i++;
for (int j = i; j < length; j++) {
int textIndex = j - i;
//Text doesn't match query at some point.
if (buffer[j] != textToMatch.charAt(textIndex)) {
break;
}
//Text matches query here.
if (textIndex == textToMatch.length() - 1) {
return extractValue(buffer, j);
}
}
}
}
} catch (IOException e) {
//Ignore any exceptions and fall through to return unknown value.
} catch (NumberFormatException e) {
}
return DEVICEINFO_UNKNOWN;
}
/**
* Helper method used by {@link #parseFileForValue(String, FileInputStream) parseFileForValue}. Parses
* the next available number after the match in the file being read and returns it as an integer.
* @param index - The index in the buffer array to begin looking.
* @return The next number on that line in the buffer, returned as an int. Returns
* DEVICEINFO_UNKNOWN = -1 in the event that no more numbers exist on the same line.
*/
private static int extractValue(byte[] buffer, int index) {
while (index < buffer.length && buffer[index] != '\n') {
if (Character.isDigit(buffer[index])) {
int start = index;
index++;
while (index < buffer.length && Character.isDigit(buffer[index])) {
index++;
}
String str = new String(buffer, 0, start, index - start);
return Integer.parseInt(str);
}
index++;
}
return DEVICEINFO_UNKNOWN;
}
}
judgeDeviceLevel,划分为5个级别,对应Unity Quality的级别,可根据设备的高中低级别,设置 QualitySettings.SetQualityLevel 。
ios的机型有限,可直接根据机型来划分。
通常直接修改屏幕的分辨率,可以有效地优化游戏的性能,当然要在画质和性能之间平衡好关系,可根据设备高中低级别修改屏幕的分辨率。
代码示例如下:
// 设置屏幕分辨率缩放, 根据设备配置级别(1:低,2:中低,3:中,4:高,5:极高)
public void SetScreenResolution(int deviceLevel)
{
float scale = 1.0f;
if (deviceLevel == 1)
{
scale = 0.8f;
}
else if(deviceLevel == 2)
{
scale = 0.9f;
}
else
{
int deviceHeight = Mathf.Min(Screen.width, Screen.height);
// 针对中低级别的大屏设备
if (deviceHeight > 1200)
{
scale = Math.Min(scale, 0.8f);
}
else if (deviceHeight > 1080)
{
scale = Math.Min(scale, 0.9f);
}
}
if (scale < 1)
{
Screen.SetResolution((int)(Screen.width * scale), (int)(Screen.height * scale), true);
}
}
由于Screen.SetResolution是直接修改整个屏幕的分辨率,可能造成UI的画质模糊,可调整缩放系数scale,更理想的方式是3D场景与UI界面区分开来,只降3D相机,不降UI相机。