【unity官方】Unity项目常见问题

【unity官方】Unity项目常见问题,第1张

Unity技术支持团队经常会对有需求的客户公司项目进行游戏项目性能审查与优化,在我们碰到过的各种项目相关的问题中也有很多比较共同的方面,这里我们罗列了一些常见的问题并进行了归类,开发者朋友们可以参考下。

(1)资源导入

1纹理没有压缩

在很多情况下,美术会觉得纹理压缩后效果不理想。我们建议的是: 可以把原图的分辨率长宽都扩大一倍,保持原有压缩格式。这样压缩过后的文件还是比不压缩的文件要小,并且视觉效果可以得到较大的改善。

2纹理导入设置中的 Read/Write Enabled 处于勾选状态

开启纹理导入设置中 Read/Write Enabled,纹理在传到GPU之后,CPU端的数据也会一直保留在内存中。因为在移动端显存共享内存,会导致内存占用加倍。 因此需要注意是否有需要在CPU端访问的纹理 ,比如:需要通过脚本获取纹理像素的情况下,就要开启纹理导入设置中的 Read/Write Enabled。

3模型文件导入设置中 Read/Write Enabled 处于勾选状态

除了需要脚本中访问的网格,作为网格碰撞器中的网格,脚本中用StaticBatchingUtilityCombine静态合批的网格,以及粒子系统发射的网格之外,其它模型建议不要勾选此项, 否则会在内存也保留一份网格实例占用内存。

4模型导入设置[Rig]选项页中Optimize GameObject没有勾选

建议开启Optimize GameObject ,这个选项可以把SceneManager中用于skinning的节点都去除,节省了场景节点树更新以及查询的CPU消耗,对于需要做挂点的节点可以添加到例外列表中。

5使用第三方音频插件时没有禁用Unity内置音频

不需要使用Unity内置音频模块的时候,建议Editor中通过勾选Edit->Project Settings->Audio->Disable Unity Audio来完全禁用FMOD模块 ,避免不必要的CPU消耗。

(2)CPU常见性能问题

1频繁调用的Cameramain

建议脚本做好Main Camera的Cache 。Cameramain实际为GameObjectFindGameObjectsWithTag(“MainCamera”)调用,主要因为引擎无法得知用户通过脚本设置的MainCamera,CPU消耗较高。

2脚本中大量UnityEngineObject的判等 *** 作

建议改为用InstanceID来判断 ,即Object GetInstanceID,运行期间保证唯一。 因为Object的判等还有额外的耗时 *** 作,而Int类型的判等就非常快速了。同理,使用Object作为key的数据结构也建议改用InstanceID做key。

3用于查询 *** 作的数据使用list数据结构

List线性结构Contains的耗时非常高,建议改为hashset,hashtable之类的查询 *** 作效率高的数据结构

4加载资源时每帧从Assetbundle加载的Asset数量没有限制

在场景内每帧从Assetbundle加载的Asset数建议限制在2到5个 ,数量高时耗时过长容易造成卡顿。

(3)内存常见问题

1加载场景的时候有 一段内存尖峰

常见的情况是 有无索引的资源被加载进来,然后因为UnloadUnusedAssets被卸载掉 。内存尖峰基本都是对游戏本身无用的内存,但是可能会因此造成游戏在内存紧张的机器上被强制关闭。

2静态索引导致的内存泄漏

一些内存占用较大的资源如纹理, 因为有静态索引而无法在切换场景或者调用UnloadUnusedAssets时被卸载掉,因此内存的泄漏量会随着用户切换场景的次数而增加。

这个值越大说明有越多的不必要内存池扩展 ,比如说在同一帧内有加载大量资源,实例化大量对象等,可能让内存池瞬间膨胀的 *** 作。

4GC分配量较大

项目Review过程中,除了CPU时间占用和内存分配量,我们还会留意脚本函数的GC分配。 GC分配越频繁,量越大,由于Mono内存池可用内存不足导致的GCCollect(造成卡顿原因之一)调用就越频繁,并且可能引起mono内存池不必要的扩展,因此脚本函数的GC分配量是既影响CPU也影响内存的重要参数

对于GC分配量。我们建议的参考数值为

对于基本每帧都会分配GC的函数,GC分配量大于2KB的建议都确认下是否有可能把临时变量抽取出来。对于偶尔分配GC的函数,GC分配量大于10KB的建议都确认下分配的数据结构是否有优化空间。

(4)GPU常见性能问题

1 特效渲染的Pass数量较多

一些特效的渲染可以合并到同一个Pass以节省GPU开销,另外RenderTexture在可以共用的情况下尽量共用

2 同屏面数过多

同屏面数建议在20W以下,较优情况是控制在10W以内

3 UI元素在需要隐藏的时候使用了设置Alpha为0的方式

实际上GPU依然需要对UI mesh进行渲染,建议不要通过设置Alpha为0的方式来隐藏UI。

4 当使用网格作为地形时,适当切分地形网格

在网格顶点数很高情况下需要依靠硬件裁剪来剔除顶点,比较消耗GPU性能,建议按照大概的同屏可见范围来切分地形网格。

5 UI元素过多依赖多层元素的混合来达到美术效果

这样会造成较多的Overdraw,建议尽量通过预制纹理来做到想要的效果。

转自:unity官方中文论坛

感觉你的问题应该有更直接的办法,一是不太懂你的意思,比如你具体是要实现怎样的功能?为什么要根据XY计算,难道同样的装备随机次数不同,power计算也不同? 第一、第二个装备又是什么意思?理论上应该限定总的可装备上限,比如一共就可以装备三个东西,那么你自然需要定义一个三个元素的数组了。如果每个装备名称对应的xy是固定的话,计算power似乎没意义了,还不如把power参数和装备写在一起,而你只要随机xy来取不同装备就行了吧。

以下顺带讲一下写入数据的办法。

(不好意思我用的是JS,语法上转换一下就行了。)

不用playerprefs的方法是:

新建一个类,这个类里面定义你要储存在磁盘上的变量,最后把这个类写成dat文件(binary文件),这样这个类里的数据就写在磁盘上了,以后你就可以随时调取、更新所存储的数据了。

(1)编程时你要用到几个基本的包:

import System;

import SystemRuntimeSerializationFormattersBinary; //用来写binary文件

import SystemIO; //基本的输入输出

详细的你还可以去查net 的MSDN 参考。

(2)你要自定义一个类用来规定数据,比如:

Class GameData {

var itemID:int;

var power:float;

}

(3)你还需要一个实例化的脚本(比如命名成,GameDataManager ),把这个脚本放在一个场景中GameObject上就可以了,这个脚本用来实际 *** 作读取和写入。把这个类做成一个Singleton,就是说仅在整个游戏刚启动时初始化一个静态的实例,而且在此后的场景退出时都不要清除,这样可以避免反复覆盖读取和存储数据的风险。比如:

static var instance:GameDataManager;

Awake() {

if(instance == null){ //当前场景中没有其他实例化的脚本,

DontDestroyOnLoad(gameObject); //那么说现在本脚本是唯一的实例,所以不要销毁

instance = this; //把唯一的静态指针指向自己。

}else if(instance != this){

Destroy(gameObject); //当前场景中已经有了其它实例!说本脚本是重复的实例,销毁!

}

}

(4)接下来要判断是否已经存在先前的存档binary文件,如果没有,就需要初始化一个GameData类。

var myGameData:GameData;

function Start () {

myGameData= Load(); //此处Load()是脚本后面定义的一个读取binary文件Dat的方法

if(myGameData== null){ // 如果没有读取到文件,就初始化一个新的数据类

myGameData= new GameData();

myGameDatapower= 999; // 数据初始化,这里你可以自定义更复杂的方法或算法

Save(); //写入数据,此处Save()也是后面定义的一个存储binary文件Dat的方法

}

}

(5)具体完成Load() 和 Save()方法:

function Save (){

var bFile:BinaryFormatter;

var file:FileStream;

bFile = new BinaryFormatter();

file = FileCreate(ApplicationpersistentDataPath + "/GameDatadat"); //在系统默认应用程序路径创建Dat文件

bFileSerialize(file, currentGameData); // 写入数据

fileClose(); //完成文件

}

function Load ():GameData{

var bFile:BinaryFormatter;

var file:FileStream;

var loadData:GameData;

if(FileExists(ApplicationpersistentDataPath + "/GameDatadat")){//判断dat文件是否存在

bFile = new BinaryFormatter();

file = FileOpen(ApplicationpersistentDataPath +"/GameDatadat", FileModeOpen);//打开系统默认路径中的Dat文件

loadData = bFileDeserialize(file) as GameData; //获取读取到的数据

fileClose();//关闭文件

}

return loadData; //返回获取到的数据类

}

最后,如果你英文过的去,unity的官方网站上有全套视频,其中一个章节就是讲解如何存储数据的!不过前提是你得会 夫安 七一昂,否则视频可能看不了。今年封的更严了,国情你懂的,

应用。appid为unity生成的应用在微信开放平台的appid,appid是应用编号,由App和ID组合而形成的名称,APP是指应用,ID是指编号,unity是面向开发人员的3D或者2D游戏引擎和强大的跨平台IDE。

由于数据要序列化,所以要保存类中的字段是可在Unity可序列化的,例如:Type 和 MethodInfo 以及 object 就不行

object不能序列化就导致反射函数的参数需要转换,带来很多麻烦

一、定义保存数据的类

[Serializable]

public class SerializableClass

{

public byte bytedata;

public short shortdata;

public int intdata;

public string stringdata = "";

public float floatdata;

public double doubledata;

public bool booldata;

public Color colordata;

public Vector2 Vector2data;

public Vector3 Vector3data;

public Vector4 Vector4data;

public Vector2Int Vector2Intdata;

public Vector3Int Vector3Intdata;

public AnimationCurve animationCurvedata = new AnimationCurve();

public Gradient gradientdata = new Gradient();

public Ease animationType = EaseLinear;

public UnityEngineObject data = null;

public CustomType type = CustomTypeInt16;

public SerializableClass()

{

}

public SerializableClass(object value)

{

type = (CustomType)EnumParse(typeof(CustomType), valueGetType()Name);

FieldInfo[] fieldinfo = typeof(SerializableClass)GetFields();

for (int i = 0; i < fieldinfoLength; i++)

{

if(fieldinfo[i]FieldType == valueGetType() || valueGetType()IsInstanceOfType(fieldinfo[i]FieldType))

{

fieldinfo[i]SetValue(this, value);

}

}

}

public object GetData()

{

FieldInfo[] fieldinfo = typeof(SerializableClass)GetFields();

for (int i = 0; i < fieldinfoLength; i++)

{

if (fieldinfo[i]FieldTypeName == typeToString())

{

return fieldinfo[i]GetValue(this);

}

}

return data;

}

public void SetValue(object value )

{

if (value == null) return;

if (typeof(UnityEngineObject)IsAssignableFrom(valueGetType()))

type = (CustomType)EnumParse(typeof(CustomType), typeof(UnityEngineObject)Name);

else

type = (CustomType)EnumParse(typeof(CustomType), valueGetType()Name);

FieldInfo[] fieldinfo = typeof(SerializableClass)GetFields();

for (int i = 0; i < fieldinfoLength; i++)

{

if (fieldinfo[i]FieldType == valueGetType() || fieldinfo[i]FieldTypeIsAssignableFrom(valueGetType()))

{

fieldinfo[i]SetValue(this, value);

}

}

}

}

public enum CustomType

{

Byte,

Int16,

Int32,

String,

Single,

Double,

Boolean,

Color,

Vector2,

Vector3,

Vector4,

Vector2Int,

Vector3Int,

AnimationCurve,

Gradient,

Object,

Ease

}

登录后复制

首先把数据分成3种,一种是基础的数据类型例如int、float等,第二种是在Unity种定义的没有继承Object的类型,一般是Unity种的结构体,第三种是Unity中定义的继承与Object的类型,例如继承MonoBehaviour的类。

因为C#中的Type不能序列化,所以自定义CustomType,这个通过对比类型名和CustomType中的字段,区分属于哪种类型。

二、计算

public enum MathParameterEnum

{

[CanUsedType(null, "GreaterThan")]

[TipsName("大于")]

Greater = 0,

[CanUsedType(null, "LessThan")]

[TipsName("小于")]

Less,

[CanUsedType(null, "")]

[TipsName("等于")]

Equal,

[CanUsedType(null, "")]

[TipsName("不等于")]

NotEqual,

[CanUsedType(null, "Add")]

[TipsName("加法")]

Addition,

[CanUsedType(null, "Subtract")]

[TipsName("减法")]

Subtraction,

[CanUsedType(null, "Multiply")]

[TipsName("乘法")]

Multiply,

[CanUsedType(null, "Divide")]

[TipsName("除法")]

Division,

[TipsName("绝对值")]

[CanUsedType(typeof(Mathf) , "Abs")]

Abs = 8,

[TipsName("正切")]

[CanUsedType(typeof(Mathf), "Tan")]

Tan,

[TipsName("余弦")]

[CanUsedType(typeof(Mathf), "Cos")]

Cos,

[TipsName("正弦")]

[CanUsedType(typeof(Mathf), "Sin")]

Sin,

[TipsName("长度归一")]

[CanUsedType(null , "Normalize")]

Normalized = 12,

[TipsName("角度" )]

[CanUsedType(null, "Angle")]

Angle,

[TipsName("距离")]

[CanUsedType(null, "Distance")]

Distance,

}

登录后复制

首先通过自定义特性来简化计算的遍历。这里有三种计算类型,一种是基础类型的加减乘除运算,一种是Mathf类或者其他计算类中的函数,还有一种是数据类型自定义的计算。

其中大部分运算都可以通过反射实现,但是这里有两个问题。

1、隐式转换

例如Vector3 可以和float 相乘,也可以和Int相乘,但是Vector3的自定义运算中并没有和int相乘的函数。因为int可以隐式转换乘float,然后再调用float的运算重载。

因此在计算判断的时候,需要考虑类型能否隐式转换为可计算的类型。

//判断类型是否有隐式转换

public static bool HasImplicitConversion(Type baseType, Type targetType)

{

if (IsNumber(baseType, targetType))

{

if(typeof(double) == targetType && (typeof(long) == targetType || typeof(float) == baseType || typeof(int) == baseType|| typeof(short) == baseType|| typeof(byte) == baseType))

{

return true;

}

if (typeof(float) == targetType && (typeof(long) == targetType || typeof(int) == baseType || typeof(short) == baseType || typeof(byte) == baseType))

{

return true;

}

if (typeof(long) == targetType && (typeof(int) == baseType || typeof(short) == baseType || typeof(byte) == baseType))

{

return true;

}

if (typeof(int) == targetType && (typeof(short) == baseType || typeof(byte) == baseType))

{

return true;

}

if (typeof(short) == targetType && (typeof(byte) == baseType))

{

return true;

}

}

return baseTypeGetMethods(BindingFlagsPublic | BindingFlagsStatic)

Where(mi => miName == "op_Implicit" && miReturnType == targetType)

Any(mi => {

ParameterInfo pi = miGetParameters()FirstOrDefault();

return pi != null && piParameterType == baseType;

});

}

登录后复制

因为C#的基础数据类型,并没有靠op_Implicit去写隐式转换,所以只能手写。

2基本数据的计算

Vector3这种类型的加减乘除可以通过发射寻找计算函数,但是float,int,double等的加减运算就不行了。

public static BinaryExpression CanMath(ParameterExpression type1, ParameterExpression type2, string methodName)

{

try

{

BinaryExpression obj = (BinaryExpression)typeof(Expression)InvokeMember(methodName,

SystemReflectionBindingFlagsInvokeMethod | SystemReflectionBindingFlagsStatic

| SystemReflectionBindingFlagsPublic, null, null,

new object[] { type1, type2 });

return obj;

}

catch (Exception e)

{

return null;

}

}

登录后复制

public static object MathNumber(List<object> allobj, MathParameterEnum mathtype)

{

Type baseType = null;

for (int i = 0; i < allobjCount; i++)

{

Type nowtype = allobj[i]GetType();

if (baseType == null)

baseType = nowtype;

else

{

if (nowtype != baseType)

{

if (!HasImplicitConversion(nowtype, baseType))

{

if (HasImplicitConversion(baseType, nowtype))

{

baseType = nowtype;

}

else

return null;

}

}

}

}

CanUsedType obsAttr = GetEnumMathTip(mathtypeGetType()GetField(mathtypeToString()));

ParameterExpression _ParaA = ExpressionParameter(baseType, "a");

ParameterExpression _ParaB = ExpressionParameter(baseType, "b");

BinaryExpression _BinaAdd = CanMath(_ParaA, _ParaB, obsAttrmethodName);

if (_BinaAdd == null) return null;

Expression<Func<double, double, double>> doubleLamb;

Expression<Func<float, float, float>> floatLamb;

Expression<Func<long, long, long>> longLamb;

Expression<Func<int, int, int>> intLamb;

Expression<Func<short, short, short>> shortLamb;

if (baseType == typeof(double))

{

doubleLamb = ExpressionLambda<Func<double, double, double>>(_BinaAdd, new ParameterExpression[] { _ParaA, _ParaB });

double data = ConvertToDouble(allobj[0]);

for (int i = 1; i < allobjCount; i++)

{

data += doubleLambCompile()(data, ConvertToDouble(allobj[i]));

}

return data;

}

else if (baseType == typeof(float))

{

floatLamb = ExpressionLambda<Func<float, float, float>>(_BinaAdd , new ParameterExpression[] { _ParaA, _ParaB });

float data = ConvertToSingle(allobj[0]);

for (int i = 1; i < allobjCount; i ++)

{

data = floatLambCompile()(data , ConvertToSingle(allobj[i]));

}

return data;

}

else if (baseType == typeof(long))

{

longLamb = ExpressionLambda<Func<long, long, long>>(_BinaAdd, new ParameterExpression[] { _ParaA, _ParaB });

long data = ConvertToInt64(allobj[0]);

for (int i = 1; i < allobjCount; i++)

{

data = longLambCompile()(data, ConvertToInt64(allobj[i]));

}

return data;

}

else if (baseType == typeof(int))

{

intLamb = ExpressionLambda<Func<int, int, int>>(_BinaAdd, new ParameterExpression[] { _ParaA, _ParaB });

int data = ConvertToInt32(allobj[0]);

for (int i = 1; i < allobjCount; i++)

{

data = intLambCompile()(data, ConvertToInt32(allobj[i]));

}

return data;

}

else if (baseType == typeof(short))

{

shortLamb = ExpressionLambda<Func<short, short, short>>(_BinaAdd, new ParameterExpression[] { _ParaA, _ParaB });

short data = ConvertToInt16(allobj[0]);

for (int i = 1; i < allobjCount; i++)

{

data = shortLambCompile()(data, ConvertToInt16(allobj[i]));

}

return data;

}

return null;

}

登录后复制

这种方式虽然还是很麻烦,但是比用if else 简单了很多很多。

(T)ConvertChangeType(dataValue, typeof(T)); 应用为类型转换为范型

我们先生成一个最简单的prefab,看看unity是如何存储数据的

生成的yaml数据如下图:

注意图中的2个ID

Node节点的Component组件包含了一个ID为 799 的组件

而这个 799 的组件是一个transform,并且transformm_GameObjectfileID指向了node的ID

不难看出,通过id建立了之间的索引关系

如果我copy自己点,这个ID会重新生成吗?

很明显,即时我们通过复制一个节点,新的节点id也发生了变化

我们给节点添加一个脚本,任何在copy后,发现id也是被重新生成了

可以看出,unity采用的方案是围绕着id进行关系关联

using SystemCollections;

using SystemText;

using System;

using SystemRuntimeInteropServices;

public class GetMemoryClass : MonoBehaviour 

{

private long avaliableMb;

void Start () 

{

//获取当前系统

//SystemInfooperatingSystem;

}

void Update()

{

#region 检测内存是否溢出

GetMemoryStatus();

#endregion

}

[StructLayout(LayoutKindSequential)]

public struct MEMORY_INFO

{

public uint dwLength;

public uint dwMemoryLoad;

//系统内存总量

public ulong dwTotalPhys;

//系统可用内存

public ulong dwAvailPhys;

public ulong dwTotalPageFile;

public ulong dwAvailPageFile;

public ulong dwTotalVirtual;

public ulong dwAvailVirtual;

}

[DllImport("kernel32")]

public static extern void GlobalMemoryStatus(ref MEMORY_INFO meminfo);

// [DllImport("User32")]

// public static extern void GetWindowThreadProgressld (IntPtr hwnd,out int id);

private void GetMemoryStatus()

{

MEMORY_INFO MemInfo;

MemInfo = new MEMORY_INFO();

GlobalMemoryStatus(ref MemInfo);

avaliableMb = ConvertToInt64( MemInfodwAvailPhysToString())/1024/1024;

print("FreeMemory:" + ConvertToString(avaliableMb) +" MB");

if (avaliableMb<200) 

{

DebugLog ("内存不足!");

//d出内存警告

else

{

DebugLog ("可以使用");

//自动取消内存警告

DebugLog(EnvironmentWorkingSetToString());

}

Author :JerryYang

Create By 20201019

环境:

Unity:201942f1

MacOS:10156

Xcode: 1201

我发现很多做Unity的童鞋都没打过IOS包,这里分享一个教程,希望能帮到大家。

首先需要准备一台Mac电脑(搭载MacOS系统的电脑),一部苹果手机,和一个苹果开发者账号;

软件需要用到Unity Edit 和Xcode;

最后就是你的unity项目啦。

这里的注意事项:

<1> Icon不用切圆角;

<2> 一定不能包含透明通道,否则提交不了。

<1>Bundle Identifier和App Store后台申请的Bundle ID要对应上;

<2>Version也要和后台相对应,我推荐使用semver格式管理版本;

<3>Build每次提交AppStore的时候必须改变(version 相同的情况下+1,不同的情况可以从0开始);

<4>TeamID可以暂时不写,也可以写开发者账号对应的TeamID。

当我们项目开发完要上线的时候可以看接下来这部分。

如需上架Google Play请看 Unity项目上架Google Play 。

暂时写这么多,有不懂的或者需要补充的请私信我。

以上就是关于【unity官方】Unity项目常见问题全部的内容,包括:【unity官方】Unity项目常见问题、unity中游戏装备数据怎么 建立,存储和读取、unityappid是什么等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/9597380.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-30
下一篇2023-04-30

发表评论

登录后才能评论

评论列表(0条)

    保存