Unity 物理引擎 学习

刚体:

刚体是模拟物理效果的对象。添加了刚体组件的游戏物体,会受重力影响,可以被玩家四处推动,或者直接用脚本添加力给对象来让他四处移动。通常情况下,对同一物体,要么通过刚体操纵,要么通过变换(transform)操纵。但添加刚体组件之后直接通过Transform组件更改物体位置,它和其他对象的碰撞可能出问题(除非Is Kinematic 选项被选中,选中该选项,则没法通过物理模拟来改变物体运动状态)。应避免同时使用两种方式。正确地方法应该是通过施加力或者扭矩来移动刚体。

属性:

Mass 质量:物体的质量(任意单位)。建议一个物体的质量不要多于或少于其他单位的100倍。
Drag 阻力:移动时物体受到的空气阻力。0表示没有,极大时物体立即停止运动。
Angular Drag:角阻力 当受扭力旋转时物体受到的空气阻力。0表示没有,极大时物体立即停止旋转。
Use Gravity 是否使用重力
Is Kinematic 是否是运动学:若激活,该物体不再受物理引擎驱动,而只能通过transform来操作。适用于模拟运动的平台或者模拟与铰链关节连接的刚体。
Interpolate 插值
当你发现刚体运动时抖动,可以尝试下面的选项。
None 无 不应用插值。
Interpolate 内插值 基于上一帧的变换来平滑本帧变换。
Extrapolate 外插值 基于下一帧的预估变换来平滑本帧变换。

Collision Detection 碰撞检测模式。用于避免高速物体穿过其他物体,却未触发碰撞。
Discrete 离散

离散碰撞检测。使用离散碰撞检测模式来与场景中其他碰撞器进行碰撞检测。其他物体与它的碰撞检测,也会应用这种模式。适用于普通碰撞(这是默认的模式)。
Continuous 连续
连续碰撞检测。使用离散碰撞检测来检测与动态碰撞器(刚体)的碰撞,使用连续碰撞检测来检测与静态网格(非刚体)的碰撞检测。采用连续动态碰撞检测模式的刚体碰见这类物体也将采用连续碰撞检测模式。而与其他刚体将采用离散碰撞检测模式。这种模式适用于那些采用动态连续碰撞模式的物体碰撞的物体。(这对物理表现有很大的影响,如果你不关心与高速物体的碰撞,那么就让其为默认的离散模式。)
Continuous Dynamic 动态连续
连续动态碰撞检测。使用连续动态碰撞检测模式来检测与连续模式和连续动态模式的物体间的碰撞。也适用于与静态网格(非刚体)的碰撞检测。而与之碰撞的其他模式的物体,采用的是离散动态碰撞检测模式。适用于高速物体。
Constraints 约束 对刚体运动的约束。 限制刚体在某些维度上的移动或|和旋转。

Freeze Position 冻结位置,刚体在世界中沿所选X,Y,Z轴的移动,将无效。
Freeze Rotation 冻结旋转,刚体在世界中沿所选的X,Y,Z轴的旋转,将无效。
velocity:刚体的速度向量,最好不要直接修改它,可以通过它的大小判断撞击的强度。

方法:
Rigibody.AddForce(Vector3),在物体的中心点位置添加一个Vector3的力,使物体朝该Vector3方向运动,运动速度为该向量的模。
Rigibody.AddTorque(Vector3),在物体中心添加一个力矩Vector3,力矩可以使物体旋转,刚体将绕着torque轴旋转。
Rigibody.AddForceAtPosition(Vector3,Vector3),在物体某个位置添加一个力,对于真实的效果,位置应大约在刚体表面的范围。
Rigibody.AddExplosionForce(float forceStrength,Vector3 postion,float range),在某个位置添加一个大小为float,半径为range的爆炸力(向爆炸范围内各个方向的对象施加一个力),用来模拟爆炸效果。爆炸力将随着到刚体的距离线形衰减。。

提示:两个刚体 (Rigidbody) 的相对质量 (Mass) 确定它们在互相碰撞时的反应方式。
使一个刚体 (Rigidbody) 的 质量 (Mass) 大于另一个刚体 (Rigidbody) 不会使其在自由落体过程中下落得更快。请使用阻力 (Drag) 实现此目的。
较小阻力 (Drag)值会使对象看起来较重。较高值会使对象看起来较轻。阻力 (Drag) 的典型值介于 0.001(实心金属块)与 10(羽毛)之间。
如果直接处理对象的变换 (Transform) 组件 (Component) ,但是仍需要物理,请附加刚体 (Rigidbody) 并将其设为运动学 (Kinematic)。
如果通过其变换 (Transform) 组件 (Component) 移动某个游戏对象 (GameObject),但是希望接收碰撞体 (Collider)/触发器 (Trigger) 消息,则必须将刚体 (Rigidbody) 附加到移动的对象。
不能仅仅通过将其“角阻力”(Angular Drag) 设置为无穷大使对象停止旋转。

碰撞体、触发器
发生碰撞条件:两个物体都必须带有碰撞器(Collider),其中一个物体还必须带有Rigidbody刚体,没有碰撞体的刚体会彼此相互穿过。发生碰撞时,当两个物体都是碰撞器(Is Trigger都未勾选)时触发OnCollisionXXX,否则触发OnTriggerxxx。

属性:
Is Trigger 是否触发器,勾选后会触发OnTriggerxxx方法,会穿过刚体
Material 材质,后面再讲
Center 中心点,用于调整碰撞盒的位置
Size 大小 在X、Y、Z三维上的碰撞器尺寸。
Convex 凸起的 如激活,该网格碰撞器将会和其他网格碰撞器碰撞。凸起的网格碰撞器限制在255个三角形面内。网格碰撞器之间通常不相互碰撞,但如果一个网格碰撞器被标记为凸起的(Convex ),那么它就可以与其他网格碰撞器碰撞。典型的解决方案是,对移动的对象使用基本碰撞器,而对静态环境对象使用网格碰撞器。
碰撞方法:
OnTriggerEnter( Collider other )当进入触发器
OnTriggerExit( Collider other )当退出触发器
OnTriggerStay( Collider other )当逗留触发器
OnCollisionEnter( Collision collisionInfo ) 当进入碰撞器
OnCollisionExit( Collision collisionInfo ) 当退出碰撞器
OnCollisionStay( Collision collisionInfo ) 当逗留碰撞器

Static Colliders 静态碰撞器,包含碰撞器但不含刚体的游戏对象。你不要逐帧移动 一个静态碰撞器,移动静态碰撞器将导致PhysX引擎的内部重置,非常耗费资源,而且会造成性能的极大下降。
基于层的碰撞检测:Edit->Project Settings->Physics.,打开物理引用界面. 在碰撞矩阵中选择要与其他层相互作用的层,勾选即可.(前提是先设置好层)

物理材质 Physic Material
物理材质用来调节碰撞物体的摩擦力和弹力效果。要创建物理材质从菜单栏选择Assets->Create->Physic Material。然后从项目视图拖拽物理材质到场景的一个碰撞器上。

属性:
Dynamic Friction 动力摩擦力,对象在运动时的摩擦力,取值范围01,该值越大,物体受到阻力越大,越快停止运动。
Static Friction 静态摩擦力,对象被放置在表面时的摩擦力,取值范围0
1,该值越大,物体受到阻力越大,开始运动所需力也越大,取值接近0时可模拟冰的效果。
Bouncyness 弹力 0值将不反弹。1值反弹将没有任何能量损失。
Friction Combine Mode 摩擦力结合模式 ,两个碰撞物体的摩擦力是如何结合起来。
Average 使用两个摩擦力的平均值
Min 使用两个摩擦力的最小值
Max 使用两个摩擦力的最大值
Multiply 使用两个摩擦力的乘积

AssetBundle加载到内存和释放研究

AssetBundle运行时加载:
img
来自文件就用CreateFromFile(注意这种方法只能用于standalone程序:PC/MAC端)这是最快的加载方法
也可以来自Memory,用CreateFromMemory(byte[]),这个byte[]可以来自文件读取的缓冲,www的下载或者其他可能的方式。
其实WWW的assetBundle就是内部数据读取完后自动创建了一个assetBundle而已
Create完以后,等于把硬盘或者网络的一个文件读到内存一个区域,这时候只是个AssetBundle内存镜像数据块,还没有Assets的概念。
Assets加载:
用AssetBundle.Load(同Resources.Load) 这才会从AssetBundle的内存镜像里读取并创建一个Asset对象,创建Asset对象同时也会分配相应内存用于存放(反序列化)
异步读取用AssetBundle.LoadAsync
也可以一次读取多个用AssetBundle.LoadAll,加载完后立即AssetBundle.Unload(false),释放AssetBundle文件本身的内存镜像,但不销毁加载的Asset对象。(这样你不用保存AssetBundle的引用并且可以立即释放一部分内存)
AssetBundle.CreateFrom…..:创建一个AssetBundle内存镜像,注意同一个assetBundle文件在没有Unload之前不能再次被使用
WWW.AssetBundle:同上,当然要先new一个再 yield return 然后才能使用
AssetBundle.Load(name):从AssetBundle读取一个指定名称的Asset并生成Asset内存对象,如果多次Load同名对象,除第一次外都只会返回已经生成的Asset对象,也就是说多次Load一个Asset并不会生成多个副本(singleton)。
Resources.Load(path;name):同上,只是从默认的位置加载。
Instantiate(object):Clone一个object的完整结构,包括其所有Component和子物体(详见官方文档),浅Copy,并不复制所有引用类型。有个特别用法,虽然很少这样用,其实可以用Instantiate来完整的拷贝一个引用类型的Asset,比如Texture等,要拷贝的Texture必须类型设置为Read/Write able。

释放:
AssetBundle的释放:
AssetBundle.Unload(flase)是释放AssetBundle文件的内存镜像,不包含Load创建的Asset内存对象。
AssetBundle.Unload(true)是释放那个AssetBundle文件内存镜像和并销毁所有用Load创建的Asset内存对象。

一个Prefab从assetBundle里Load出来 里面可能包括:Gameobject transform mesh texture material shader script和各种其他Assets。
你Instantiate一个Prefab,是一个对Assets进行Clone(复制)+引用结合的过程,GameObject transform 是Clone是新生成的。其他mesh / texture / material / shader 等,这其中有些是纯引用的关系的,包括:Texture和TerrainData,还有引用和复制同时存在的,包括:Mesh/material/PhysicMaterial。引用的Asset对象不会被复制,只是一个简单的指针指向已经Load的Asset对象。所以你Load出来的Assets其实就是个数据源,用于生成新对象或者被引用,生成的过程可能是复制(clone)也可能是引用(指针)
当你Destroy一个实例时,只是释放那些Clone对象,并不会释放引用对象和Clone的数据源对象,Destroy并不知道是否还有别的object在引用那些对象。
等到没有任何游戏场景物体在用这些Assets以后,这些assets就成了没有引用的游离数据块了,是UnusedAssets了,这时候就可以通过Resources.UnloadUnusedAssets来释放,Destroy不能完成这个任务,AssetBundle.Unload(false)也不行,AssetBundle.Unload(true)可以但不安全,除非你很清楚没有任何对象在用这些Assets了。
Destroy:主要用于销毁克隆对象,也可以用于场景内的静态物体,不会自动释放该对象的所有引用。虽然也可以用于Asset,但是概念不一样要小心,如果用于销毁从文件加载的Asset对象会销毁相应的资源文件!但是如果销毁的Asset是Copy的或者用脚本动态生成的,只会销毁内存对象。
AssetBundle.Unload(false):释放AssetBundle文件内存镜像
AssetBundle.Unload(true):释放AssetBundle文件内存镜像同时销毁所有已经Load的Assets内存对象
Reources.UnloadAsset(Object):显式的释放已加载的Asset对象,只能卸载磁盘文件加载的Asset对象
Resources.UnloadUnusedAssets:用于释放所有没有引用的Asset对象
GC.Collect()强制垃圾收集器立即释放内存 Unity的GC功能不算好,没把握的时候就强制调用一下
静态引用和Resources.Load引用对象texture是在instantiate时加载,用到时才会加载,会导致第一次加载卡顿,而assetBundle.Load会把perfab的全部assets都加载,instantiate时只是生成Clone。Unity系统在加载新场景时,所有的内存对象都会被自动销毁,包括你用AssetBundle.Load加载的对象和Instaniate克隆的。
但是不包括AssetBundle文件自身的内存镜像,那个必须要用Unload来释放,用.net的术语,这种数据缓存是非托管的。
既然加载场景不会释放AssetBundle文件自身的内存镜像,那我们就手动释放。
Destroy:主要用于销毁克隆对象,也可以用于场景内的静态物体,不会自动释放该对象的所有引用。虽然也可以用于Asset,但是概念不一样要小心,如果用于销毁从文件加载的Asset对象会销毁相应的资源文件!但是如果销毁的Asset是Copy的或者用脚本动态生成的,只会销毁内存对象。
AssetBundle.Unload(false):释放AssetBundle文件内存镜像
AssetBundle.Unload(true):释放AssetBundle文件内存镜像同时销毁所有已经Load的Assets内存对象
Reources.UnloadAsset(Object):显式的释放已加载的Asset对象,只能卸载磁盘文件加载的Asset对象
Resources.UnloadUnusedAssets:用于释放所有没有引用的Asset对象
GC.Collect()强制垃圾收集器立即释放内存 Unity的GC功能不算好,没把握的时候就强制调用一下
例如:场景A切换到场景B,使用同步加载Application.LoadLevel(sceneName)或者异步加载Application.LoadLevelAsync(sceneName)都可以。
我们可以在场景A和场景B之间插入一个清理内存的场景X,场景X就是一个空场景,它的主要作用是承上启下,把场景A留下的资源清理,然在切换到场景B。
具体就是在Awake方法里清空内存,Start方法里切换下一个场景,如下:  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

void Awake()
{
object[] objAry = Resources.FindObjectsOfTypeAll();

for (int i = 0; i < objAry.Length; i++)
{
objAry[i] = null;//解除资源引用
}

//卸载没有被引用的资源
Resources.UnloadUnusedAssets();

//进行垃圾回收
GC.Collect();
GC.waitForPendingFinalizers();//
GC.Collect();
}
void Start()
{
StartCoroutine("AsyncLoadScene", nextSceneName);
}
IEnumerator AsyncLoadScene(string sceneName)
{
async = Application.LoadLevelAsync(sceneName);
yield return async;
}

Lerp线性插值 用法

在Unity3D中经常用线性插值函数Lerp()来在两者之间插值,两者之间可以是两个材质之间、两个向量之间、两个浮点数之间、两个颜色之间,其函数原型如下:
1.Material.Lerp 插值
function Lerp(start : Material, end : Material, t : float) : void
在两个材质之间插值

2.Vector2.Lerp 插值
static functionLerp (from : Vector2, to : Vector2, t : float) : Vector2
两个向量之间的线性插值。按照数字t在form到to之间插值。
t是夹在0到1之间。当t=0时,返回from。当t=1时,返回to。当t=0.5时放回from和to之间的平均数。

3.Vector3.Lerp 插值
static functionLerp (from : Vector3, to :Vector3, t : float) :Vector3
两个向量之间的线性插值。按照数字t在from到to之间插值。

4.Vector4.Lerp 插值
static functionLerp (from : Vector4, to : Vector4, t : float) : Vector4
两个向量之间的线形插值。按照数字t在from到to之间插值。t是夹在[0…1]之间的值。,当t = 0时,返回from。当t = 1时,返回to。当t = 0.5 返回from和to的平均数。

5.Mathf.Lerp 插值
static functionLerp (from : float, to : float, t : float) : float
基于浮点数t返回a到b之间的插值,t限制在0~1之间。当t = 0返回from,当t = 1 返回to。当t = 0.5 返回from和to的平均值。

6.Color.Lerp 插值
static functionLerp (a : Color, b : Color, t : float) : Color
通过t在颜色a和b之间插值。
“t”是夹在0到1之间的值。当t是0时返回颜色a。当t是1时返回颜色b。

插值,从字面意思上看,就是在其间插入一个数值,其原理为a=from + (to - from) * t

例如Mathf.Lerp(100.0f, 200.0f,0.2f) = 100 + (200 - 100)*0.2 = 100 + 100 * 0.2 = 120

应用例子:

1.几秒内移动到目标点,其中smooth为移动时间

1
2
3
4
5
6
public Transform target;
public float smooth = 5.0F;
void Update()
{
transform.position =Vector3.Lerp(transform.position, target.position, Time.deltaTime * smooth);
}
  2.我们以前所玩的游戏中,主人公身上依附着一只宠物如鹰,主人公移动时,鹰会跟随着飞动,主人公移动得快它就飞行跟动得快,始终不会离开主人公,使用Lerp插值函数就可实现。

unity Assetbundle 使用

本文的主要内容是分析5.X版本的AssetBundle机制(包括创建资源包、压缩资源包、加载资源包和从资源包中加载/卸载资源等几个方面)及其关键的API使用方式。
AssetBundle是Unity推荐的资源管理方式,官方列举了诸如热更新,压缩,灵活等等优点。

使用步骤
一、打包
1.通过Editor中的UI为AssetBundle标记资源。而且一个资源和对应的AssetBundle的映射将会在资源数据库(AssetDatabase)中被创建。

2.BuildPipeline.BuildAssetBundles():我们只需要提供一个输出AssetBundle的地址即可。引擎将自动根据资源的assetbundleName属性(即在上文中UI中设置的值)批量打包,自动建立Bundle以及资源之间的依赖关系。
Unity3D为我们提供了唯一的API用来打AssetBundle包。即:BuildPipeline.BuildAssetBundles
在脚本中调用BuildPipeline.BuildAssetBundles,U3D将自动根据资源的assetbundleName属性批量打包,自动建立Bundle和资源之间的依赖关系。在资源的Inpector界面最下方可设置该资源的assetbundleName,每个assetbundleName对应一个Bundle,即assetbundleName相同的资源会打在一个Bundle中。如果所依赖的资源设置了不同的assetbundleName,则会自动与之建立依赖关系,避免出现冗余,从而减小Bundle包的大小。当然,除了可以指定assetbundleName,我们还可以在Inpector中设置另一个名字,即variant。在打包时,variant会作为后缀添加在assetbundleName之后。相同assetbundleName,不同variant的Bundle是可以相互替换的。

设置好之后,我们只需要创建一个新的脚本,通过编辑器拓展调用BuildPipeline.BuildAssetBundles方法即可:

BuildPipeline.BuildAssetBundles方法的参数
outputPath:输出目录,一般我们设置为 Application.streamingAssetsPath + Bundle的目标路径和Bundle名称
targetPlatform:目标平台,在安卓,IOS,PC下,我们需要传入不同的平台标识,以打出不同平台适用的包,注意,Windows平台下打出来的包,不能用于IOS
BuildAssetBundleOptions:BuildAssetBundleOptions.CollectDependencies会去查找依赖,BuildAssetBundleOptions.CompleteAssets会强制包含整个资源,BuildAssetBundleOptions.DeterministicAssetBundle会确保生成唯一ID,在打包依赖时会有用到

Unity3D引擎为我们提供了三种压缩策略来处理AssetBundle的压缩,即:LZMA格式、LZ4格式、不压缩。
LZMA格式:在默认情况下,打包生成的AssetBundle都会被压缩。在U3D中,AssetBundle的标准压缩格式便是LZMA(LZMA是一种序列化流文件),因此在默认情况下,打出的AssetBundle包处于LZMA格式的压缩状态,在使用AssetBundle前需要先解压缩。
使用LZMA格式压缩的AssetBundle的包体积最小(高压缩比),但是相应的会增加解压缩时的时间。
LZ4格式: Unity 5.3之后的版本增加了LZ4格式压缩,由于LZ4的压缩比一般,因此经过压缩后的AssetBundle包体的体积较大(该算法基于chunk)。但是,使用LZ4格式的好处在于解压缩的时间相对要短。若要使用LZ4格式压缩,只需要在打包的时候开启BuildAssetBundleOptions.ChunkBasedCompression即可。
BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath,
BuildAssetBundleOptions.ChunkBasedCompression);
不压缩:我们也可以不对AssetBundle进行压缩。没有经过压缩的包体积最大,但是访问速度最快。若要使用不压缩的策略,只需要在打包的时候开启BuildAssetBundleOptions.UncompressedAssetBundle即可。
BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath,
BuildAssetBundleOptions.UncompressedAssetBundle);

在打包的时候,我们需要对包的大小和数量进行一个平衡,所有资源打成一个包,一个资源打一个包,都是比较极端的做法,他们的问题也很明显,更多情况下我们需要灵活地将他们组合起来
打成一个包的缺点是加载了这个包,我们不需要的东西也会被加载进来,占用额外内存,而且不利于热更新 打成多个包的缺点是,容易造成冗余,首先影响包的读取速度,然后包之间的内容可能会有重复,且太多的包不利于资源管理哪些模块打成一个包,哪些模块打成多个包,需要根据实际情况来,例如游戏中每个怪物都需要打成一个包,因为每个怪物之间是独立的,例如游戏的基础UI,可以打成一个包,因为他们在各个界面都会出现
PS.想打包进AssetBundle中的二进制文件,文件名的后缀必须为“.bytes”

二、加载
首先获取AssetBundle对象,第二从AssetBundle中加载目标资源。在运行时加载AssetBundle对象主要可以分为两大类途径:
1.先获取WWW对象,再通过WWW.assetBundle加载AssetBundle对象:
public WWW(string url),直接调用WWW类的构造函数,目标AssetBundle所在的路径作为其参数,构造WWW对象的过程中会加载Bundle文件并返回一个WWW对象,完成后会在内存中创建较大的WebStream(解压后的内容,通常为原Bundle文件的4~5倍大小,纹理资源比例可能更大),因此后续的AssetBundle.LoadAsset可以直接在内存中进行。
public static WWW LoadFromCacheOrDownload(string url, int version, uint crc = 0),WWW类的一个静态方法,调用该方法同样会加载Bundle文件同时返回一个WWW对象,和上一个直接调用WWW的构造函数的区别在于该方法会将解压形式的Bundle内容存入磁盘中作为缓存(如果该Bundle已在缓存中,则省去这一步),完成后只会在内存中创建较小的SerializedFile,而后续的AssetBundle.LoadAsset需要通过IO从磁盘中的缓存获取。LoadFromCacheOrDownload会记录所有Bundle的使用情况,并在适当的时候删除最近很少使用的资源包,它允许存在两个版本号不同但名字一样的资源包,这意味着你更新这个资源包之后,如果没有更新代码中的版本号,你可能取到的会是旧版本的资源包,从而产生其他的一些BUG。另外,当你的磁盘空间不足的时候(硬盘爆了),LoadFromCacheOrDownload只是一个普通的new WWW
2.直接加载AssetBundle对象:
public static AssetBundle LoadFromFile(string path, uint crc = 0):新的从文件创建加载AssetBundle方法,loadFromFile方法支持上一节中提到的几个压缩格式,针对LZ压缩格式和未压缩的磁盘上的bundle文件,该方法会直接加载。针对使用默认的LZMA压缩格式压缩的bundle文件,该方法会在幕后先将bundle文件解压后再加载。这是最快的加载AssetBundle的方式。该方法是同步版本,还有异步版本:LoadFromFileAsync。
public static AssetBundle LoadFromMemory(byte[] binary, uint crc = 0):从内存中获取Bundle的二进制数据,同步地创建AssetBundle对象。该方法一般用在经过加密的数据上,经过加密的流数据经过解密之后我们可以调用该方法动态的创建AssetBundle对象。该方法是同步版本,还有异步版本:LoadFromMemoryAsync。
三、资源加载
LoadAsset:从资源包中加载指定的资源
LoadAllAsset:加载当前资源包中所有的资源.
LoadAssetAsync:从资源包中异步加载资源
在Bundle中加载的Prefab是不能直接使用的,它需要被实例化之后,才能使用,而对于这种Prefab,实例化之后,这个Bundle就可以被释放了
GameObject obj = GameObject.Instantiate(bundle.Load(“MyPrefab”)) as GameObject;
bundle.Load(“MyPrefab”, typeof(GameObject))加载的时候最好传入资源的类型

四、针对项目的建议
由于以上分析的几种加载手段各有各的使用情景和特点。因此建议在我们的项目中按照以下情景使用这些方法:
1.随游戏一同发布的AssetBundle(一般位于StreamingAssets文件夹中):在打AssetBundle包时,使用LZ4压缩格式进行打包(开启BuildAssetBundleOptions.ChunkBasedCompression即可)。
2.在运行时需要加载AssetBundle对象时,使用LoadFromFile方法进行加载。
这样做的好处是:即可以将AssetBundle文件压缩,又可以兼顾加载速度,且节约内存。
3.在打AssetBundle包时,使用默认的LZMA格式压缩。
4.使用WWW.LoadFromCacheOrDownload方法下载并缓存AssetBundle包文件。

UGUI适配

随着游戏设备越来越多,屏幕的分辨率也越来越多。而针对不同的屏幕分辨率,制作不同的素材是不现实的,所以就需要我们提供一套分辨率自适应的机制,来适配不同屏幕分辨率的设备。这篇博客就来介绍一下UGUI提供的分辨率自适应的机制。
游戏中的分辨率自适应主要包括两部分:一是在不同尺寸的屏幕下,整体缩放比例的计算方式;二是在不同比例(宽高比)的屏幕下,UI控件所处的位置关系,也成为布局。

整体缩放比例
UGUI中Canvas Scaler组件是负责整体缩放机制的,缩放模式提供了三种:
Constant Pixel Size:固定像素尺寸,即按素材的“固定像素”渲染,缩放屏幕不缩放图片,不适配屏幕
Scale Factor:缩放比例,在素材原尺寸上的缩放比例,默认值是1,缩放图片
Reference Pixels Per Unit:每个unity单位对应的像素数

Scale With Screen Size:根据屏幕尺寸缩放,图片跟着缩放,适配
Reference Resolution:标准分辨率,这是我们提供给美术做图的标准分辨率,所有的UI素材都应该按这个分辨率去做
Screen Match Mode:

Shrink 保持缩放比例,裁切

Expand 缩放不裁切

Match Width Or Height 以宽高权重匹配
Match:宽高所占权重,默认值是0,相当于以“标准分辨率的宽”和“实际屏幕的宽”的比例作为缩放比例。同理,如果值是1,相当于以“标准分辨率的高”和“实际屏幕的高”的比例作为缩放比例。如果值是0.5,则相当于宽和高的比例权重相等,最终的缩放比=宽缩放比宽权重+高缩放比高权重
Reference Pixels Per Unit:每个unity单位对应的像素数

Constant Physical Size:固定物理尺寸
Physical Unit:物理单位,包括点,英寸,厘米,毫米等
Fallback Screen DPI:对应物理单位的像素密度
Default Sprite DPI:默认精灵的像素密度
Reference Pixels Per Unit:每个unity单位对应的像素数
目前手机设备分辨率宽高比都在1.5:11.8:1范围,pad的分辨率在1.3:11.5:1范围,所以一般情况下,我们会根据要覆盖的机型,得到其大概的宽高比范围,从中间选择一个合适的宽高比,例如1.7:1(因为手机设备比较多,所以更接近1.8:1的比例)。有了宽高比,接下来我们要选择目标分辨率了,通常情况下以1024作为宽,以1024/1.7=602作为高比较合适,因为一般的压缩格式会要求宽高是2的次幂或者4的倍数。
在确定宽高比和目标分辨率之后,我们还需要设置适配规则,比较推荐的做法是做一张较大的背景图,背景图中上下左右边的内容允许被裁剪。当适配到1.8:1的设备上时,由于背景图内容宽度小于设备宽度,此时需要使用“宽”适配,这时背景图的上下边将被裁剪掉;反之适配到1.5:1的设备上时,需要使用“高”适配,这时背景图的左右边将被裁减掉。
布局
在适配分辨率方面,除了等比缩放外,还有一方面是对UI控件位置的适配,例如角色头像一般处在屏幕的左上角,虚拟摇杆处在屏幕的左下角,要想在不同的分辨率下都处在比较合适的位置,就需要理解“锚点”的作用了。
UGUI中控件的锚点都是相对于父控件的,包括 左上,中上,右上,左中,中心,右中,左下,中下,右下,默认值是中心。
当我们在标准分辨率下定位好控件后,设置好合适的锚点,不论在那种分辨率下,控件都会处于一个合适的位置。
以左上角头像为例,我们将其锚点(anchor)设置为“左上”,轴(pivot)设置为“左上角”。
PosX和PosY即UI控件相对于父控件(Canvas)的相对位置,由于锚点设置为“左上”,轴设置为“左上角”,所以位置PosX和PosY都为0

三消游戏的一些算法总结

•	"如何表示消除游戏的棋盘?"
•	"怎样检测相邻的相同元素?"

•	"如何实现消除后自动补全棋盘效果?
•	"如何实现消除后自动生成新元素效果?"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 消除游戏棋盘表示
int[,] board = new int[8, 8]; // 0表示空位,1-6表示不同颜色

// 检测相邻相同元素
bool HasMatch(int[,] grid, int x, int y) {
int color = grid[x, y];
// 检查水平方向
if (x > 0 && grid[x-1, y] == color && x < grid.GetLength(0)-1 && grid[x+1, y] == color)
return true;
// 检查垂直方向
if (y > 0 && grid[x, y-1] == color && y < grid.GetLength(1)-1 && grid[x, y+1] == color)
return true;
return false;
}
• "如何实现三消游戏中的特殊连锁反应效果?"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

// 连锁反应节点
public class ChainNode {
public Vector2Int Position;
public ChainNode Next;
}

// 处理连锁反应
void ProcessChain(ChainNode head) {
while (head != null) {
RemoveTile(head.Position);
ApplySpecialEffect(head.Position);
head = head.Next;
yield return new WaitForSeconds(0.3f); // 动画间隔
}
}

“如何检测棋盘上的所有可消除组合?”

“怎样实现特殊消除效果(如L型/T型消除)?”
// 检测所有匹配项
List FindAllMatches(int[,] grid) {
var matches = new List();
int width = grid.GetLength(0);
int height = grid.GetLength(1);

// 水平检测
for (int y = 0; y < height; y++) {
    for (int x = 0; x < width - 2; x++) {
        if (grid[x, y] > 0 && 
            grid[x, y] == grid[x+1, y] && 
            grid[x, y] == grid[x+2, y]) {
            matches.Add(new Vector2Int(x, y));
            matches.Add(new Vector2Int(x+1, y));
            matches.Add(new Vector2Int(x+2, y));
        }
    }
}

// 垂直检测(类似逻辑)
// ...

return matches.Distinct().ToList();

}

问题:”如何实现随着玩家水平变化的难度系统?”

public class DifficultyAdjuster {
private float playerSuccessRate;
private int gamesPlayed;

public int GetAdjustedLevel(int baseLevel) {
    float difficultyFactor = Mathf.Clamp(1.5f - playerSuccessRate, 0.5f, 2f);
    return Mathf.RoundToInt(baseLevel * difficultyFactor);
}

public void UpdateSuccess(bool levelPassed) {
    gamesPlayed++;
    playerSuccessRate = (playerSuccessRate * (gamesPlayed - 1) + (levelPassed ? 1 : 0)) / gamesPlayed;
}

}

文件的读写存储操作

当程序运行时,变量时保存数据的好方法,如果希望程序结束了数据仍能保持,就需要将数据存储到文件中(序列化),你可以认为文件的内容是一个字符串值,那如何使用python 在硬盘上创建,读取和保存呢。

文件关键属性:

- 文件名
- 文件路径
- 文件扩展名
- 文件权限
- 文件显示隐藏属性
- 文件大小

在不同的平台路径分隔符不一致:在window上是反斜杠,在OSX 和linux上是斜杠,附加卷诸如DVD或USB在不同的操作系统上显示也不同,在window上显示的新的带字符的跟驱动器,在osx表示位/Volumes下的新文件夹,在linux上显示的是/mnt下的新文件夹

import os
os.listdir() #列出当前目录的所有文件
os.getcwd() #获取当前目录
os.chdir(‘path’) #切好当前工作目录
os.makedirs(‘path’) #创建文件夹
os.path.join(‘path’,’file’) #目录和文件拼接

绝对路径和相对路径
绝对路径总是从根文件夹开始‘/’
相对路径相对的是程序当前的工作目录
‘.’ 当前目录
‘..’ 当前目录的父目录
os.path.abspath(‘path’) #将目录转为绝对目录
os.path.isabs(‘path’) #判断目录是否是绝对路径
os.path.relpath(path1,path2) #传人参数是俩个绝对路径,返回path1 相对 path2的相对路径
os.path.dirname(‘path’) #返回path 参数重最后一个斜杆之前的所有内容, (文件夹路径)
os.path.basename(‘path’) #返回path参数最后一个斜杆之后的内容。(文件或者文件夹名)
os.path.split(‘path’) #返回(os.path.dirname(),os.path.basename())

检查路径有效性
os.path.exists(‘path’) #路径是否存在或者正确
os.path.isdir(‘path’) #路径是否是一个目录
os.path.isfile(‘path’) #路径是否是一个文件

文件的读写
open(‘path’,’r/w/a/) #打开文件读,写,追加,返回一个File对象
close(‘path’) #关闭文件

用shelve 模块保存变量
import shelve
shelveFile = shelve.open(‘mydata’)
cats =[‘zoophix’,’bbb’,’ccc’]
shelveFile[‘cats’]=cats
shelveFile.close()

list(shelveFile.keys)
list(shelveFile.values)

用pprint.pformat()函数保存变量

1
2
3
4
5
6
7
8
9
10
11
12
import pprint
cats =[{'name':'hello','desc':'world'},{'name:'ouyang','desc':'haha'}]
pprint.pformat(cats)
fileobj = open('myCats.py','w')
fileobj.write('cats=' + pprint.pformat(cats) + '\n')
fileobj.close()


import myCats
myCats.cats
myCats.cats[0]
myCats.cats[0][name]

创建一个.py文件而不是利用shelve模块保存变量的好处在于他是一个文本文件,所以任何人都可以用一个简单的文本编辑器读取和修改该文件的内容,但是对于大多数应用,利用shelve模块来保存数据,是将变量保存到文本的最佳方式,因为只有基本数据类型,诸如整形,浮点型,字符串,列表和字典,可以作为简单文本写入一个文件。而对象就不能编码为简单文本,比如File对象。

从剪切板取得文本
将文本张贴到剪切板

1
2
3
4
5
6
7
8
9
10
11
import pyperclip,re
text = str(pyperclip.paste())
matchs =[]
for groups in phoneRx.findall(text):
phonenum = '-'.join([group[1],group[3],group[5])
matchs.append(phonenum

if len(matchs)>0:
pyperclip.copy('\n'.join(matchs))
print(Copied to clipboard:)
print('\n'.join(matchs))

python 正则表达式练习

知道【正则表达式】可能意味着用3步解决一个问题,而不是用3000步,如果你是一个技术怪侠,别忘了你用几次快捷键就能解决的问题,其他人需要数天的繁琐工作才能解决,而且他们还容易犯错。
—- Cory Doctorow

正则表达式总结:

  • ()添加小括号将在正则表达式中创建分组,
  • ?匹配[0,1]次前面的分组
  • *匹配[0,多]次前面的分组
  • +匹配[1,多]次前面的分组
  • {}匹配特定次数或者特定范围次数的前面的分组
  • {N} 匹配 N 次前面的分组
  • {N,}匹配[N, 多]次前面的分组
  • {,N}匹配[0,N]次前面的分组
  • {N,M}匹配[N,M]次前面的分组进行贪心(最大)匹配
  • {N,M}?匹配[N,M]次前面的分组进行非贪心(最小)匹配
  • *?或+?对前面的分组进行最小匹配。
  • ^span匹配的字符串必须以span开头
  • span$匹配的字符串必须以span结束
  • . 匹配所有字符(一个字符),换行符除外
  • \d,\w和\s分别匹配数字(0-9),单词(任意字母,数字或下划线),和空格(空格,制表符,换行符),
  • \D,\W,\S分别匹配出除数字,单词(任意字母,数字或下划线),空格(空格,制表符,换行符)以外的所有字符
  • [abca-zA-Z0-9]匹配方括号中的任意字符
  • [^abc]匹配不在方括号内的任意字符

python中使用正则表达式
import re #导入模块
x = re.compile(r’(\d\d\d)-(\d\d\d-\d\d\d\d) ‘) # 创建一个匹配对象 ,python 的转义字符要加
mo = x.search(‘My number is 415-555-4242. ‘) # 搜索匹配正则
mo.group() #匹配的结果

使用group()匹配对象的方法从一个分组中获取匹配的文本,查找的是第一个匹配对象
group()和group(0)返回完整的匹配
group(1),group(2) 返回对应第几个括号中的匹配值
groups() 返回多个匹配值的元组
mo.groups()
(‘415’,’555-4242’)
areacode,mainNumber = mo.groups()
areacode = 415
mainNumber = 5555-4242

用管道匹配多个分组(|)
字符‘|’称为管道,希望匹配多个值时使用r’a|b|c’将匹配a或b或c

使用findall() 可以找出所有的匹配结果
只有正则表达式中没有分组,返回的是一个字符串列表。
如果有分组,findall将返回一个匹配的元组的列表。

用问号?实现可选匹配
re.compile(r’[aeiouAEIOU]’)匹配单词中的元音字母
re.compile(r’[^aeiouAEIOU]’)匹配单词中的非元音字母
在[]中的普通的正则表达式符合不会被解释,所以不要在前面(.,*,?,(,),)加转义的反斜杠(\)

点-星(.*)能匹配除换行外的所有字符,通过对re.comile()的第二个参数传人re.DOTALL,可以让句点字符匹配包括换行字符。

用sub()方法替换字符串

管理复杂的正则表达式
比较复杂的需要长的费解的正则表达式,可以传人re.VERBOSE 作为re.compile()的第二个参数,可以忽略正则表达式中的空白符和注释。从而缓解复杂

1
2
3
4
5
6
7
8
9
10
eg: phoneRx = re.compile(r'((\d{3}|\(\d3\))?(\s|-|\.)?\d{3}(\s|-|\.)\d{4}(\s*(ext|x|ext.)\s*\d{2,5})?)')

phoneRx = re.compile(r'''(
(\d{3}|\(d{3}\))? #区号
(\s|-|\.)? #分隔
\d{3} #前三
(\s|-\.) #分隔
\d{4} #后四
(\s*(ext|x|ext.)\s*\d{2,5})? #扩展
)''',re.VERBOSE)

使用re.IGNORECASE 忽略大小写
someRx = re.compile(‘foo’,re.IGNORECASE|re.DOTALL|re.VERBOSE) #按位或来组合使用

不要急于学习编程语言,先学会如何解决问题

这是一篇关于虚度青春的短篇回忆录。
多年来,我一直在学习编程语言,然后用它们来构建更好的投资工具。在建立预测和风险管理模型之余,编程方面的工作堆积如山。
但实际上,我并没有花太多时间在真正需要解决的问题上。
我把大部分注意力放在了算法和编程语言上。多年来,我每周都要花几个小时学习计算机基础知识,却没有花太多时间应用这些知识来让现实世界变得更美好。
而我的一个朋友却基于微软 Office 开发出了一个完整的产品,它使用 Access 保存数据,使用 Excel 分析数据,然后使用 Word 输出报告。
当他把产品卖给一家大公司时,我为他感到高兴,同时也有些嫉妒。我比他更了解编程,但却没能推出可以解决实际问题的产品。
实际上,为什么要开发产品比如何开发产品更加重要。我花时间学习如何编程,而我的朋友把时间花在解决实际的问题上。
他从使用非常简单的工具开始,一步一步往前走,直到找到一个完整的解决方案。在这个过程中,他自学了如何将其他工具整合在一起。这仍然需要做大量的工作,但他做到了,尽管资源非常有限。
1
神奇之处在于“为什么”
后来,我加入了一家初创公司,并最终汲取了这个教训。
一个团队能否取得重大突破从来都不取决于如何使用代码完成某些功能,而在于知道要做什么,以及为什么要这样做。
在创造性的旅程中,没有什么比在不必要的东西上投入太多更令人心碎的了。
如果你想要创造出一些有用的东西,首先要知道“为什么”要这样做。从一开始你就要问自己:谁会用这些东西,它们将如何改善他们的生活?
2
专注于解决真正的问题可以加快学习速度
当你专注于真正需要解决的问题时,大脑中的信息留存率会更高。我们的大脑会优先考虑需要保存的信息,并忽略大部分其他东西。这样可以节约能量,但不利于学习。
知道自己为什么要学习,就相当于触发大脑要优先考虑新信息。
追求创造性的活动会让你学得更快,而这并不要求你一定要学会一门编程语言。
3
下面这些事情比学习编程语言更重要
找到真正的问题。
把问题写下来,它现在是什么样的,它应该是什么样的,并尽可能详细。例如,如果你的问题与使用 Office 文件格式保存数据有关,那么就详细说明这方面的问题,这样有助于找到最好的解决方案。
当你对问题本身以及你想如何解决问题有了很好的描述,你就已经成功了一半。
然后你要努力去澄清问题,朝着可以找到解决方案的方向迈进。
如果你在这个时候纠结于编程语言,那么你可能不是一个好程序员。你要不断培养描述问题和解决方案的能力。
当你对问题有了很好的描述,并且确切地知道想要什么,然后再去学习一门编程语言来解决这个问题就变得很容易了。

每天看一遍,不久你会变(转)

要月入5000看3遍,要月入1万看10遍,要月入100万以上把以下内容背下来!
是什么限制了你的能力?
1:否定性思想
比如:不可能、没办法、怎么会?没想过、不知道等等,这些词汇会让你的大脑停止思考,不会再为结果找答案。没方法等于没能力。
2:推卸责任
比如:没看见、不知道、不是我的错、因为···所以···,这些词汇会让一个人心安理得的让这件事情和自己没关系,损失了很多次成长的机会。
3:金钱
金钱会锁定一个人的能力,不给钱就不干活,钱给的少就不去做,久而久之自己丧失了赚钱的能力。
4:职责局限
这个不归我管、那个不归我管,不在其位不谋其政,他忘了机会永远留给有准备的人。

5:抱怨
自己永远是一个受害者,发生事情永远是别人的原因,整天怨天尤人,慢慢的失去了解决问题的能力,幸福快乐都离他而去。
6:自以为是
谁的意见都听不进去,总是觉得自己是对的,慢慢的谁都不再给他提意见,再也听不到真话的同时,只能自己慢慢成长。

7:不自信
你不相信的东西永远不会全力以赴的去争取。一切随缘,顺其自然。
8:怕犯错
怕犯错的人不敢去做更多的事,出了错第一时间先给自己找理由找借口,失去了很多次尝试的机会,没有结果意识,经常为了对和错争论的面红耳赤。
9:懒惰
不想干,也不愿意去想,安于现状、与世无争、承受不了压力,只想不劳而获,每天做着美梦,温水煮青蛙,这种人是舒服死的。提醒自己全力以赴。

宁可去碰壁,也不能在家里面壁;
是狼就要练好牙,是羊就要练好腿;是人就要练好脑子。

什么是奋斗?
奋斗就是每一天都很难,
可一年一年却越来越容易!
不奋斗就是每一天都很容易,
可一年一年却越来越难!
拼一个春夏秋冬,
赢一个无悔人生,
我可以接受失败,
不能接受从来没有奋斗过! 


跟谁混很重要
“沙子是废物,水泥也是废物,但他们混在一起是混凝土,就是
精品;大米是精品,汽油也是精品,但他们混在一起就是废物。
是精品还是废物不重要,跟谁混,很重要! 
"
朋友不是乱交的,每天在一起喝酒的不一定是真朋友,每天在一起混的也不一定是永远的朋友。

人生要交的四位朋友:
1、交一个欣赏你的朋友,即便在你穷困潦倒的时候反而会更安慰你、帮助你。
2、交一个有正能量的朋友,在你情绪低落的时候陪伴你、鼓励你。
3、交一个为你领路的朋友,自愿做你的垫脚石,带你走过泥泞、拨开迷雾。
4、交一个肯指点你的朋友,时刻提醒你、监督你,让你时刻发现自己的不足。财富不是永远的朋友,朋友却是永远的财富!

抽出三分钟给自己洗个脑
30年前说下海能赚钱的人,被认为是骗子。
20年前说炒股能赚钱的人,被认为是骗子。
15年前说保险能帮到大家的,被认为是骗子。
10年前马云说互联网能改变人们的生活,也被认为是骗子。
那些说别人是骗子的人,生活一成不変,生活质量一天比一天差!而那些当年所谓的“骗子”却成了时代的标志!

“每一次新的机遇的到来,都会造就一批富翁!”
每一批富翁都是:
当别人不明白的时候,他明白他在做什么;
当别人不理解的时候,他理解他在做什么;
当别人明白了,他富有了;当别人理解了,他成功了。
任何一次机遇的到来,都必将经历四个阶段:“看不见“、“看不起“、“看不懂”、“来不及” 
;
任何一次财富的缔造必将经历一个过程:“先知先觉经营者;后知后觉跟随者;不知不觉消费者 
! ”反省一下,你有错过吗?人生比努力更重要的是选择!与时俱进是财富的源泉哦!

下面一席话,发人深省!
当摩托罗拉还沉醉在V8088的时候,不知道诺基亚已迎头赶上。
当诺基亚还注重低端机市场时,乔布斯的苹果已经潜入。
当苹果成为街机的时候,三星已经傲视天下。
当中国移动沾沾自喜为中国最大的通讯商时,浑然不觉微信客户已突破4个亿。
当中国银行业赚的盆满钵满高歌猛进时,阿里巴巴已经推出网络虚拟信用卡。
当很多人还在想租个门面房开个小生意时,光棍节一天中国互联网上创造天价成交额。
不要说停止学习,就是慢一点都有可能被淘汰出局。

未来十年拼什么?
四个字:整~借~学~变
一:整;资源整合!
你能整合多少资源,多少渠道,你将来就会得到多少财富!
二:借;造船过河不如借船过河。
趋势,无法阻挡;
抉择,要有智慧!
三:学;今天的企业家,赢在学习,胜在改变!
案例:
一;1991年,跨国巨头柯达在技术上领先同行十年,但却在2012年1月申请破产。
二;昔日手机霸主,诺基亚风雨飘摇濒临倒闭。
三;自2003年,便赋闲不再学习的李宁,于去年7月10日,出售了香港豪宅,被迫二度岀山。不知高龄的李宁能否力挽狂澜,拯救岌岌可危的”李宁”?
古人云:富不学富不长,穷不学穷不尽!
四:变;要想改变口袋,先要改变脑袋!
这个社会一直在淘汰有学历的人,但是不会淘汰有学习力愿意改变的人!
读万卷书不如行万里路,
行万里路不如阅人无数,
阅人无数不如名师指路,
名师指路不如重叠成功人的脚步!

【不要等到明天】
1.改变一种行为不要拖到明天,否则它会变成你的习惯。
2.拒绝一份诱惑不要拖到明天,否则它会造成你的伤害。
3.抓住一次机会不要拖到明天,否则失去了就不会再来。
4.不要让今天的行动拖到明天,否则它无法带来精彩。
5.不要把今天的幸福拖到明天,否则它将一去不复返。
6.不要把机会拖到明天,因机会是唯一的你还要等到明天吗?