Unity3D学习笔记
3D的场景与操作
3D场景与2D场景:
3D场景中多了以下元素:
视图导航器Gizmo,网格,天空盒,光照,Z轴。均可以通过场景的顶部栏进行调整
快捷键:
Alt+左键:更改视位和角度;
右键:更改角度;
鼠标中键:更改视位;
方向键:视位前后左右移动;
滚轮:视位前后移动;
Alt+右键:视位前后移动;
Q键:切换成移动视位模式;
方向:
X轴正向:东方;
Y轴正向:上方;
Z轴正向:北方;
快捷定位游戏对象:
层级栏中选中游戏对象,然后鼠标移到场景栏按下F,即可将视角定位到指定游戏对象。
摄像机:
摄像机在层级栏中作为一个游戏对象存在,负责拍摄游戏最终画面,我们在左下角Game栏看到的画面就是主摄像机拍摄的画面。
做个比喻的话,场景栏是导演看到的,游戏栏是摄影师看到的。
选中摄像机,GameObject->AlignWithView,强制摄像机位置和视角移动到当前场景栏位置与视角。
摄像机属性中的Projection选项能控制摄像机显示方式,是透视模式还是正交模式,正交模式是2D情况下的默认镜头。
全局光照GlobalIllumination
观察场景中的游戏物体,创建的时候是纯白的,但是经由光照的影响,它会变成其他颜色。
Window->Rendering->LightingSetting可以生成当前场景的光照表单,点击GenerateLighting生成该场景的光照文件,在Scenes文件夹下作为SimpleScene的同名文件夹被存储。回到LightingSetting,InviromentLighting中拖动IntensityMultiplie(光照强度因数)来改变光照强度。InviromentReflection中拖动IntensityMultiplie(光照反射强度因数)来改变光照反射的强度。
3D物体的操作
原生3D物体
指Unity系统自带的一些简单3D建模(PrimitiveObject),白模,可用于简单的3D测试,它们的大小都与Unit单位长度有关。
四个基本物体:用途:度量和占位(模型未出而逻辑先行)
? 和两个水平和竖直的平面(由于另一面不需要被玩家看到,故不需要渲染)。
物体的操作
Q:移动视位 W:移动物体 E:旋转物体 R:缩放物体
多选物体情况下的操作
当一次多选了几个3D物体,移动,旋转和缩放操作便需要区分轴心(Pivot)和中心(Center)了,Pivot情况下,每个被选中的物体根据自己的轴心位置进行移动,旋转,缩放;Center情况下,所有被选中的物体根据它们共同的几何中心进行整体旋转。
增量移动操作
最简单的移动方式是计算两物体间距并手动赋值。增量移动操作可以确保手动拖拽的误差,按住Ctrl并拖动即可。
Edit->GridAndSnapSettings->IncrementSnap设定步长,可以让物体每次移动固定步长,保证两物体能严丝合缝而不缺漏或多余。
顶点对齐
顶点(Vertex)属于建模术语,不是指代一个四边形就有四个顶点,将场景栏的渲染模式切换成ShadedWireframe,可以查看模型的所有顶点
我们选中一个立方体,按住V可以按照顶点拖动它的整体,Unity不是建模软件,不要想着拖动顶点来改变物体形状。
点面重叠,这样也能保证两个物体严丝合缝。
网格与材质
网格
网格(mesh)用于定义一个物体的形状,网格是怎么定义一个物体的形状的:
1,所有物体都有自己的表面;
2,将表面分割为若干个三面体,成为一个面;
3,记录每个面的顶点(Vertex)和法线(Normal)等数据;
4,运行游戏时,由GPU渲染每个面的数据,显示为物体;
材质
材质(Material)用于规定一个物体的表面如何渲染,包括颜色,纹理,受光影响,突起/凹陷特征,一个好的材质可以让物体看起来不违和。在Assets文件夹下新建Materials文件夹,用于存放材质文件,新建材质文件(Material),这里面的参数用于调整这个材质的表现形式,例如Albedo(反照率)可以调整物体表面的颜色。给游戏对象的MeshRenderer组件赋值为新建的材质文件。
贴图
几种重要的贴图:
1,反照率贴图(Albedo):用于表现物体的表面颜色。
2,金属度贴图(Metallic):用于表现物体的哪些部位更有金属光泽。
3,法线贴图(NormalMap):用于表现物体表面的凹凸细节。
4,镜面贴图(Specular):用于表现镜面反射。
通过给Albedo赋予贴图(Texture)的方式定义物体表面显示的图片,这个图片应该是美术人员设计好的:
组件栏中的MeshFilter用于从模型文件中读取Mesh数据,绘制出物体的网格数据,得到形状。
而MeshRenderer用于将物体的形状显示出来,贴上表面材质,所以需要为它指定一个材质文件。
没有MeshRenderer的物体就没有实体,两组件缺一不可。
而且美术人员配套给出Model+Material+Texture。
我们可以发现任何一个材质的预览都是以球的形式出现的,这是因为物体的光照运算这一属性也被包含到了材质文件当中,使用球来做材质预览可以清楚的看到光对该物体每个面的影响,各种光照条件下的颜色表示(材质球)。
法线贴图用于定义一个物体的凹凸度:
贴图可以由美术人员绘制,而法线贴图是建模软件通过美术人员的雕刻自动生成的。
无法线贴图的铁块:
有法线贴图的铁块:
还可以通过调整它的金属度和光滑度来进行设置。
着色器
着色器是一种程序,一种算法,它渲染了画面上每一个物体的最终显示效果,通过材质给出的各种贴图,各种参数,加上环境光照的影响最终计算出一个物体的显示效果。如图,蓝色框选部分比较亮,而红色框选部分比较暗,这是因为着色器综合光照的强度,角度,颜色,以及物体的反照率贴图,法线贴图,金属度,光滑度从而计算出了物体表面的不同部位的不同颜色。当物体表面与光线方向垂直时,采光率最高;当物体表面与光线方向平行时,采光率最低。编写着色器语言可以达到不一样的美术效果。
除此以外,着色器还用于表示材质与顶点的对应关系,当我们将一个材质赋予一个物体的时候,着色器会根据模型的顶点参数,生成材质的显示办法。如图,正方体的面数明显小于球体的面数,但却需要6张材质来着色,而球体只用了1张。并且着色器还计算出球体的哪个面显示材质的哪一部分。
每个材质文件都会综合一个着色器算法,初始都是标准着色算法(Standard),除此以外还有其他着色算法可供选择,或者自己写一个。
模型与资源
资源的导入
我们可以将Unity项目中的各种资源打包,也可以将外部资源导入,资源的格式:*.unitypackage。
导入方式:将要导入的资源文件拖到Assets文件夹下完成导入,或右键项目栏,点击ImportPackage进行导入。
导出方式:右键项目栏,点击ExportPackage进行导出。
导出资源的标准:
Meshes/Models 模型;
Textures 贴图;
Materials 材质;
Prefabs 预制体;
Scenes 样例;
外部模型
Unity支持的模型文件:
3dsMax (*.max *.3ds)
Maya (*.ma)
Blender (*.blend)
Wavefront (*.obj)
模型被导入时转换形式变成FBX (*.fbx) 文件,是Unity的标准格式**(仅限Mesh网格;材质和贴图额外生成)**。
材质的标准保存格式:Material(*.mat)
各种贴图的标准保存格式:(*.tga)
贴图->材质->模型->3D对象
如果外部模型资源包含了预制体,则直接拖过来使用便可,如果不包含,则可以自己通过上述步骤拼接模型。
多个模型拼接而成的一个大模型被称为组合模型。
物体的运动
类似于2D中学过的,3D物体的运动也是需要脚本来控制的,方法基本相同
transform.Translate(x,y,z)让物体移动固定长度的方法3D中同样适用。
物体朝向的更改
3D情况下要想让一个物体朝向另一个,显然比2D中复杂,定义了两个对象Ammo和Target:
Transform target = transform.Find("/Target").transform;
Vector3 face = transform.forward;
Vector3 direction = (target.position - transform.position).normalized;
float angleX = Vector3.SignedAngle(face, direction, Vector3.right);
float angleY = Vector3.SignedAngle(face, direction, Vector3.up);
transform.Rotate(angleX, angleY, 0);
由于这个过程比较复杂,Unity有LookAt API来使用:
Transform target = transform.Find("/Target").transform;
transform.LookAt(target);
值得注意的是,LookAt指定朝向的默认方向是z轴。也就是说,使用LookAt的话物体的z轴朝向会正对目标。所以当物体的正面不是z轴正向的话,LookAt就不起作用。
默认情况下,三个轴的对应关系:
X轴:红轴:右right
Y轴:绿轴:上up
Z轴:蓝轴:前forward
原则上来说,模型应该在建模软件中被调整好后再导入Unity,被导入的模型应该有以下标准:
(Axis)模型默认朝向Z轴方向;
(Pivot)模型的轴心应该位于模型底部的中心(方便定位);
(Size)尺寸不应当再进行改变,模型大小应当基于真实比例大小(1Unit = 1meter)
练习:设计一枚追踪导弹
思路:目标自动向一个方向移动,子弹永远朝向目标,匀速移动
//目标
//Update内
float step = 2f * Time.deltaTime;
transform.Translate(0, 0, step);
//导弹
//Update内
Transform target = transform.Find("/Plane").transform;
transform.LookAt(target);
float step = 2.4f * Time.deltaTime;
transform.Translate(0, 0, step);
天空盒
我们在游戏栏和场景栏中看到的游戏背景是一种叫天空盒的材质,它的Shader方式为Skybox/6Sided。这个着色器下的材质需要六张正方形贴图来规定天空盒的六个面:Up,Down,Left,Right,Front,Rear。这6张图片尽量大,2048x2048 或 4096x4096都可。整个游戏都是在天空盒内部进行的。
在Window/Rendering/LightingSettings中可以将天空盒的材质拖入。
天空盒的手动创建
首先将六张贴图准备好,必须让六张图可以无缝衔接。将六张贴图的WrapMode改为Clamp(紧凑模式),新建天空盒材质并选择6Sided着色器模式。将图片拖入即可使用。除了六面型天空盒,还有其他种类的天空盒。
动画
一个物体的运动有两种实现方式,一个是代码,另一个就是动画(Animation),在Assets文件夹创建Anim文件夹存储动画文件,创建动画(Animation)。
将Inspector窗口改为**Debug(深度开发)模式,给Animation勾选Legacy,这个模式用于播放单一的动画文件。另外一种状态机动画(Animator)**适合播放复杂动画,例如人物移动,跑跳等。
将动画文件拖给游戏对象就可以实现挂载,游戏对象就可以根据动画的指示做出反应。
WrapMode中规定了动画的几个播放模式:Once,Loop,PingPong;
动画编辑
在Window/Animation中找到动画编辑器菜单,把它拖到方便的地方,选中要用编辑器操作的游戏对象,点击AddProperty,加入position,可以通过添加关键帧的方式,从时间角度控制物体的运动。
左边框信息视图,右边时间视图,使用滚轮缩放,鼠标中键拖动。
加入position,进入录制模式/编辑模式(红色圆圈),一切修改都必须在录制模式下才能自动保存。创建两个关键帧,修改当前的position信息,两个关键帧拥有不同的位置信息则动画建立完成,可以预览,退出录制模式,动画保存成功。
关键帧:在某一帧时游戏对象的状态,行为规定好后,动画在关键帧间的行动都会由插值算法自动计算,可以极大节省工作量。关键帧由菱形方块表示。
插值算法:给定两个时间点的两个值,通过不停计算两个值中间的值,得到平滑的位置曲线的算法。
动画曲线
使用Animation制作的移动动画不是匀速的,它有一个加速减速的过程,这个过程由动画曲线(Curves)来表示。曲线上的点表示所对应参数对应时间的值。
快捷键:
ctrl+滚轮:横向缩放
shift+滚轮:纵向缩放
鼠标中键:按住拖动
选中某一参数:只显示此参数
选中某一参数按下F键:放大观察这个参数的整个曲线
在曲线上双击:添加关键帧
选中这条曲线的左右顶点(关键帧),右键打开这个关键帧物体的移动方式。将前后两个关键帧的BothTargents(左右切线)改为Linear(线性模式),就可以将曲线变成平滑的。Free(拖动式),Constant(常量)阶梯式,Weighted(加权拖动式)。Free和Weighted模式下会显示贝赛尔曲线手柄,能够微调曲线。
动画的父子关系
建立一个直升机,上面的螺旋桨是它的子对象。为它创建动画,可以看到在加入组件的窗口里有它的子对象可以调控
加入子对象的rotation属性就可以编辑子对象的动画了(Legacy),将螺旋桨旋转角度的起止设置成0~359度,曲线设置成线性,螺旋桨动画制作完成。
动画的回调方法
可以规定在动画的哪一帧触发回调函数,为此我们需要准备一个脚本和一次动画。脚本内建立了一个回调函数,动画为小球掉到地上反弹。
//回调函数
public void Drop()
{
Debug.Log("PONG!", this);
}
在时间轴和动画曲线的中间位置右键可以添加动画回调函数,意思是在动画运动到此帧的时候触发对应函数,这个函数在被回调的时候可以传参(有些参数不能),并且脚本必须要被挂载到和动画相同的游戏对象上。
使用API来播放动画
进入之前使用的小球弹跳脚本:
//Update内
Animation anim = transform.GetComponent<Animation>();
if (!anim.isPlaying)
{
//anim.Play(clipName) 播放指定动画,缺省则为默认动画
anim.Play("Ball");
}
动画状态机
状态机(Mecanim)是一个计算机术语,它用来区分和控制一个事物或系统的不同状态,比如控制玩家行走与跑动之间的切换。
一个人物模型最基本有以下几种状态:
静止(Idle) 走(Walk) 跑(Run) 游泳(Swim) 攻击(Attack)
Animator
Animator用于可视化编辑动画状态机,在动画文件中新建AnimatorController,将它挂载到游戏对象上,用于控制这个游戏对象的状态机。双击AnimatorController可以进入状态机编辑器,将它调整到合适位置。
快捷键:
鼠标中键/Alt+左键:拖动窗口
鼠标滚轮:缩放窗口
F:完全显示所有状态方块
状态方块的属性:Motion规定了当前状态下物体播放的动画,Transitions规定它有哪些过渡。
右键状态方块可以新建状态过渡(Transition),用于不同状态之间的切换,Idle -> Walk 代表可以从Idle状态直接切换为Walk状态。右键灰色的状态方块SetAsLayerDefaultState可以将这个状态设置为默认状态,设置为默认会在Awake的时候进入该状态。
动画绑定:
添加jump动画,将它拖给到动画控制器里的jump状态的Motion属性内,动画中勾选LoopTime表示循环播放,不可以改用Legacy模式。
状态参数
状态机的切换宏观上调控是依照玩家的输入进行的,微观上是一个个状态参数在调控,在Animator编辑器的左侧边栏中可以建立状态参数。建立walking参数,选中Idle->Walk的过渡,将HasExitTime(定时触发)取消勾选,Conditions中添加walking参数,如果walking为true,则执行这个过渡;选中Walk->Idle的过渡,将HasExitTime取消勾选,Conditions中添加walking参数,如果walking为false,则执行这个过渡。状态参数的值可以是float,int,bool,trigger;
ExitTime
HasExitTime表示这个过渡在起始点动画执行xx秒或执行xx次后进行过渡,选中Walk->Idle的过渡,勾选HasExitTime,Settings中ExitTime为动作执行次数,执行这个动作x次以后自动进行过渡,如果Conditions有判定的话,ExitTime结束后进行Conditions的判定。
FixedDuration被勾选表示进行x秒动画后过渡,没有被勾选表示进行x轮动画后过渡。
下图代表:Walk动作执行3s后进入Conditions判定,如果此时walking为false则过渡到Idle状态,过渡时间为0.1s;
注意,如果3s后进入Conditions判定后判定没有成功,那么这个动作会永远持续下去,所以不建议同时使用HasExitTime和Conditions双重判定。
练习:方块的前进和跳跃
脚本中可以将Animator当作组件传入,也可以直接调整状态参数,创建两个动画jump和walk,两个状态参数,将它们在animator中设置好:
Idle -> Walk:walking=true时触发
Walk -> Idle:walking=false且本次walk执行完毕时触发
Idle -> Jump:jumping=true时触发
Jump -> Idle:jumping=false且本次jump执行完毕时触发
代码:
//Update内
//animator.SetBool是设置参数值方法
//animator.GetBool是获取参数值方法
//animator.GetCurrentAnimatorStateInfo(0).IsName("Name")是获取当前状态名的方法
Animator animator = transform.GetComponent<Animator>();
animator.SetBool("walking", Input.GetKey(KeyCode.W));
animator.SetBool("jumping", Input.GetKey(KeyCode.Space));
状态机行为
状态机行为(StatesBehaviour)就是在状态内才会触发的脚本。选中Animator中的一个状态方块,点击AddBehaviour为它添加一个状态机行为。
状态机行为脚本默认重载了几个状态机函数供我们使用:
OnStateEnter:进入状态时触发该函数一次
OnStateUpdate:物体处于该状态时每一帧触发该函数一次
OnStatesExit:退出该状态时触发该状态一次
模型动画
模型的动画由美术人员给出,unitypackages里包含了模型需要的动画,格式为(*.fbx)。外部导入的动画应用于模型上,配上Animator,基本上可以完整刻画出一个模型的完整形象。
学习一个模型动画设计的基本步骤:
1,查看模型的文档排版,弄清存储方式
2,将模型的预制体放出来或进入写好的场景
3,找到animator,学习状态机的设计
4,尝试修改状态参数,理解调用方式
5,学习脚本,理解内部逻辑
6,尝试自己写接口使用该模型
场景搭建
游戏推进的基底就是场景,所有游戏中进行的活动都是记录在场景中的,更高级的比如地形破坏,场景互动,昼夜交替也是场景搭建中要考虑的。
搭建场景时的推荐排版:
这样设计的目的是方便观察,对比需要的模型材料。
正交视图与透视
点击Gizmo中心的方块可以切换正交视图(Isocate)和透视视图(perspective),正交视图下三视图模式更方便场景的布置。
地形系统
Unity自带了地形编辑器,在新建3D物体有一项Terrain地形选项,可以创建一个1000x1000的地形,新建Terrain文件夹,将创建好的地形文件放入其中。
Terrain的设定栏有五个按钮,从左到右:
CreateNeighborTerrains:增加相邻地形
PaintTerrain:绘制地形
PaintTrees:绘制树
PaintDetails:详细绘制地形
TerrainSettings:地形设定
进入地形设定,从上到下:
BasicTerrain:基础设定
Tree&DetailObjects:树和细节组件
WindSettingsforGrass:控制植被摇晃的风设置
MeshResolution:网格设置
HolesSettings:山洞,坑洞设置
TextureResolution:贴图设置
Lighting:光照设置
Lightmapping:光照烘培设置(高级光照设置,减少资源消耗)
进入MeshResolution,将网格长宽改为100x100,够用。Terrain物体的pivot在它的一个角上,不在中心。
绘制地表
在Terrain的五个工具中选择PaintTerrain进入地形绘制界面,下面第一个下拉栏的六种工具从上到下:
RaiseOrLowerTerrain:升起或抬高地形
PaintHoles:绘制山洞
PaintTexture:绘制贴图
SetHeight:抬高地形到固定高度
SmoothHeight:平滑高度曲线
StampTerrain:盖章式绘制地形
在Terrains目录下新建一个TerrainLayer(地形层)资源,设定好它的反照率贴图和法线贴图,进入PaintTexture中,TerrainLayers控制地表材质,放入刚建好的TerrainLayer;可以对此材质进行详细设计。TillingSettings可以设置此贴图的面积,面积太小贴图重复度就很高,面积太大贴图会不清晰。
多添加几个TerrainLayer,以供绘制。默认情况下,地形的材质为第一个。选中想要的layer和笔刷,就可以在地形上绘制了。
植被
选中Terrain对象的PaintDetail工具,这个工具用来进行地形上的细节设计,比如花草,石头。通常来说我们应当使用模型来制作花草,但是花草太多了,如果使用模型的话会给GPU造成极大负担。在一些游戏中,花卉和石头拥有建模,而杂草使用2D贴图代替,有的游戏的花草与石头都用2D贴图。
在Details里的EditDetails可以加入一些修饰地图细节的贴图或网格,点击AddGrassTexture添加一个花草用的2D贴图,打开一个界面:用于设计这个花草的一些数值,比如最大最小高度宽度,噪波(草地颜色改变的幅度,系统以当前噪声为seed不断生成随机数的方法,我们可以人为给它一个变换率)和两个渐变的颜色。BillBoard选项用来设置这个贴图是否会跟着玩家的视角改变朝向,保证玩家永远看到草地的正面。
选好以后,Inspector中下面的settings可以改变刷子的大小,每次生成花草的系数,拉高:每次生成的花草变多。
Terrain自带了种树功能,PaintTrees功能可以种树。和PaintDetails类似,它也是将树以笔刷的形式放置在地形上,树是比较大型的游戏对象,所以不能使用2D贴图。为此PaintTrees需要获得树的预制体,然后动态创建树到地形文件中。
将树的预制体导入,然后使用笔刷种树即可。实验证明挂载到地形中的树不会触发自己的预制体所带的脚本。
造山与挖坑
PaintTerrain内有两个工具,RaiseOrLowerTerrain和SetHeight,前者用来拉高或降低地势,后者用来设定地势的高低为固定值。Unity地形中,地形的最低高度为0,如果想要挖坑,就必须先抬高整体地势。先将整个地形抬高4米,然后使用SetHeight将高度设为比4小的值就能挖坑了。
Canvas层
3D项目中的UI与2D中类似,新建一个Canvas层用来存放UI物件。与此同时也会建立一个EventSystem。将Canvas的RenderMode改为ScreenSpace-Camera,然后将主摄像机的游戏对象拖给它。PlaneDistance表示摄像机与此Canvas层的距离,默认为100Unit,当这个值过小,UI就会将游戏遮住。利用这个特点,我们可以制作不同层次的Canvas。
在制作UI时,最好将视角切换成2D显示模式。在这个模式下,将摄像机对准Z轴方向。
Text,Image,Button等2D效果在3D中依旧通用。
Image类型贴图注意:Default型的贴图是无法被当作Image的源图像的,需要进入那个图像里**将TextureType改为Sprite(2D and UI)**才能使用。
RawImage类型:拥有Image类型的功能,并附带了切割图片(UV Rect)的功能。
SortOrder:用来控制多个Canvas层条件下各个Canvas的先后顺序。
在Canvas层中,谁最后渲染,谁就在最上层:
CanvasGroup组件
可以为任何UI对象添加CanvasGroup组件,可以用来控制该组件和其子级的透明度,可互动性等,点击AddComponent添加该组件。
Alpha:不透明度
Interactable:是否可互动(按钮等)
BlocksRaycasts:是否阻挡射线
IgnoreParentGroups:是否忽略父级CanvasGroup的调控
TextMeshPro
TextMeshPro是Unity内置的3D字体生成器,需要一个SDF字体库。Window栏里选择TextMeshPro/FontAssetCreator,点击Import TMP Essentials会自动生成资源到Assets目录下。在FontAssetCreator中引入ttf字体库,名字不可有中文。将CharacterSet改为CuntomCharacters,将游戏中需要的所有字体都写进去,否则无法使用。
完成后,save到字体目录下。右键层级3D Object可以新建TextMeshPro类型字体。将刚新建好的SDF字库引入,就能显示字体了。如果字体不在SDF库中,会显示方块。Inspector中可以调整字体的各种属性。
TMP文本是游戏当中的物体,支持position,rotation,scale,material等一系列3D对象的属性,也包括文本框独有的边框,字体大小,字间距等属性。UI,也就是Canvas层中也可以使用TMP文本。
FontAsset:字库;
FontStyle:粗体/斜体/下划线;
FontSize/AutoSize:文字大小;
VertexColor/ColorGradient:颜色;
Spacing:文字间距/行间距/段落间距;
Alignment:对齐方式;
Anchors
UI对象中的组件RectTransform中的Anchor属性,与2D中一样用来规定UI的锚点。它规定了当屏幕分辨率发生变换的时候,该UI对象以屏幕的哪个地点为原点进行移动,这个值是按照百分比算的,最小值不能超过最大值。
Min X 左 Max X 右
Min Y 下 Max Y 上
AnchorPresets中可视化定点:并且还能规定对象朝哪个方向拉伸。被拉伸的图像,其宽高度也会随着屏幕自动缩放。
布局器
对于一个内容了很多UI对象的父级对象,可以为它添加布局器组件来调整。添加组件/Layout/VerticalLayout或HorizontalLayout
组件中的属性调整后都是可视化的。
Overlay模式
Canvas层有3个模式,最常用的是Overlay模式和Camera模式。Overlay模式下的UI对象永远在游戏屏幕上方,类似于视频中的水印。游戏中HUD和玩家状态都显示在Overlay的Canvas层。
HUD
HUD代表HeadUpDisplay抬头显示,就是跟随玩家或游戏对象移动的UI对象。一般是血条或等级文本。
在Canvas中添加一个Image,子对象中添加一个文本。Image轴心设为屏幕左下角。新建脚本HudForObject:
[Tooltip("要跟随的移动目标的transform")]
public Transform follow;
void Update()
{
//将目标的位置转换成屏幕位置
Vector3 sp = Camera.main.WorldToScreenPoint(follow.position);
//HUD跟随
transform.position = sp;
}
对于想要添加HUD的对象:
为它新建一个子节点,位置为它的上方,这个子节点用于定位hud的位置。将这个子对象拖给hud。
尝试拖动对象,观察它的HUD是否跟着自己移动。在实际项目中,HUD要思考的细节更多。并且一个游戏是允许多个Canvas的,要让HUD所在的Canvas的SortOrder比其他UI靠后,才能保证HUD不把UI叠加。
练习:文本显示游戏时间
新建Canvas层,在其中新建Text,使用 (分:秒) 的时间格式。挂载脚本
脚本:
using UnityEngine.UI;
public class TimeCtrl : MonoBehaviour
{
private float startTime;
private Text timingText;
void Start()
{
timingText = GetComponent<Text>();
}
void Update()
{
//在按下Space后开始刷新游戏时间
if(Input.GetKeyDown(KeyCode.Space))
{
//定义开始时间点
startTime = Time.time;
//不停调用刷新文本函数
InvokeRepeating("UpdateTime", 0.1f, 0.1f);
}
}
private void UpdateTime()
{
//时间累加器
float index = Time.time - startTime;
//转化时间为string的分秒形式
//Format用于将数据以特定格式方便的转换成string
string str = string.Format("{0:00}:{1:00.0}", index / 60, index % 60);
//更新时间
timingText.text = str;
}
}
用户输入进阶
在写代码时如果只使用Input.GetKey()和Input.GetMouse()的话,用户便无法在游戏内进行键位的更改,另外如果用户使用其他游戏设备,例如手柄,游戏就不能玩了。为此,虚拟键应运而生,以下脚本中仍然可以控制游戏对象移动。
[Tooltip("移动速度")]
public float speed = 5f;
void Update()
{
//Fire虚拟键对应鼠标左键
if (Input.GetButton("Fire1"))
{
transform.Translate(0, 0, speed * Time.deltaTime);
}
}
Input.GetButton(string) Input.GetButtonDowm(string) Input.GetButtonUp(string)
默认虚拟键映射表(部分):
虚拟键 | 键盘 | 鼠标 | 摇杆 |
---|
Fire1 | Left CTRL | 左键 | Fire1 | Fire2 | Left ALT | 右键 | Fire2 | Fire3 | Left Shift | 中键 | Fire3 | Jump | Space | | Jump |
所有虚拟键可以在Edit/ProjectSettings/InputManager中查看和修改。
另外,Unity提供了另外一套用户输入API,扩展性,兼容性更好:UnityEngine.InputSystem,需要安装插件才能使用。Input和InputSystem是二选一的,不能同时使用。
轴输入
通过观察映射表,发现操控前进和左右移动的按键Horizontal和Vertical,但是控制移动的按键最少有四个,此时需要轴输入了
void Update()
{
//获取轴输入
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
//移动
transform.Translate(h * speed * Time.deltaTime, 0, v * speed * Time.deltaTime);
}
Input.GetAxis()得到的是一个在[-1,1]区间内的值,如果使用键盘的话,这个值会在短时间内从0变成1或-1,如果使用手柄,这个值会随手柄输入的值发生变化。
InputFlag
在代码中写入的InputAPI当中的函数,归根结底是判断一个值是不是变为了true,比如Input.GetBotton(“Fire1”),如果此时玩家按下了Fire1键,Input便会将底层的FIre1键的InputFlag置为true,所以如果两个被激活的脚本中都有Input.GetBotton(“Fire1”)语句,那么当玩家按下Fire1时,这两个脚本都会得到Fire1的InputFlag被置为true这一事件。这样设计可以避免脚本间冲突。
InputFlag在下一Update到来前会被清理。如果下一Update中又被触发,则继续将InputFlag置为true。建议在Update中进行所有的Input调用。
物理系统
球在空中不动是因为它没有刚体物理属性。
为球添加物理刚体后落到地面被拦住,是因为球受到重力作用,且球和地板都有碰撞体。SphereCollider和MeshCollider。全程由物理系统计算得出结果,没有使用脚本。
碰撞体
碰撞体分为三类:
StaticCollider 静态碰撞体 房屋,地面等 总是保持静止
RigidBodyCollider 刚体碰撞体 可运动,受力的作用
KinematicRigidBodyCollider 运动学刚体碰撞体 不受物理学约束,无质量无速度的碰撞体(较少使用)
物体+Collider = 静态碰撞体 StaticCollider
物体+Collider+RigidBody = 刚体碰撞体RigidBodyCollider
物体+Collider+RigidBody+IsKinematic = 运动学刚体碰撞体KinematicRigidBodyCollider
碰撞体的几种形状:
BoxCollider 盒状碰撞体
CapsuleCollider 胶囊状碰撞体
MeshCollider 网格碰撞体
SphereCollider 球状碰撞体
TerrainCollider 地形专用碰撞体
WheelCollider 车轮专用碰撞体
**每种碰撞体都有自己的形状参数可以设置,一个物体可以挂载多个碰撞体。**在制作游戏时,一个物体的碰撞体往往都是使用这种模拟近似的形状来规定的,差不多支持运动逻辑即可,过于精细的碰撞体不会让玩家游戏体验提升多少,反而加大了内存开销,MeshCollider就是完全贴合物体网格的碰撞体。
一般做法:
静态物体:MeshCollider
动态物体:BoxCollider/CapsuleCollider/SphereCollider (PrimitiveCollider)
无关环境物(花草、远景):无Collider
物理材质
两个物体接触时,会进行弹性,摩擦力的计算。PhysicsMaterials用来规定这两个参数。下图为默认情况下(物体没有给定物理材质)物体的物理材质。
DynamicFriction:当物体移动时它的摩擦系数(动摩擦系数)
StaticFriction:当物体静止时它的摩擦系数(静摩擦系数)
Bounciness:物体的弹性系数
FrictionCombine:两物体相遇时此物体的摩擦系数取两物体间的什么值(组合方式)
BounceCombine:两物体相遇时此物体的弹性系数取两物体间的什么值(组合方式)
组合方式的优先级:Average<Minimum<Multiply<Maximum
也就是说如果两个相遇的对象使用了不同组合方式,最终采用的方式根据优先级大小给定。
刚体的运动
在物理系统中,以下参数能让物体运动:
力Force 速度Velocity 冲量Impuse 加速度Acceleration 角速度AngularVelocity 扭矩Torque
像之前一样使用transform.Position移动的方法是不物理的。正确的移动方法是获取物体的刚体组件并使用它的函数。不使用移动方法规定物体该到什么位置,而使用物理方法让物体自己移动到一个位置。
练习:让盒子在力的作用下移动
实现思想:使用物理更新函数,一般使用FixedUpdate来施加作用力。为盒子添加刚体,脚本:
//获取组件
Rigidbody rb;
//Start内
rb = GetComponent<Rigidbody>();
//根据输入的键位加入力
private void FixedUpdate()
{
float v = Input.GetAxis("Vertical");
//施加一个10N的推力
rb.AddForce(0, 0, v * 10);
}
但是盒子一直是以翻转的形式移动的。原因是摩擦力太大了
运动学刚体KinematicRigidBody是不可以被AddForce的。
摩擦力
物体在地面上移动时,由于重力和地面不平整,会受到来自地板的阻力,这个力就是摩擦力。可以通过改变重力,物体质量,地面和物体摩擦系数来更改摩擦力。
给地面和盒子添加冰面材质,它们的动静摩擦系数都是0.1。
可以看到物体在得到一点力后就能驶出很远,说明摩擦力小了。
根据 f t = m v,只要给定力,受力时间,物体质量,就可以求出物体的移动速度。
无摩擦力情况下,1N的力施加到1kg的物体上1s时间,物体的速度就是1m/s
FixedUpdate
FixedUpdate是物理更新,相对于Update帧更新,它的更新较为严格,默认情况下规定每0.02秒更新一次
private void FixedUpdate()
{
Debug.Log("FixedUpdate " + Time.deltaTime);
}
我们知道FixedUpdate方法每0.02秒调用一次,那么在内的AddForce方法就是每秒调用50次,如果AddForce(1,0,0),那么一秒执行50次的意思是:每0.02秒施加1N的力,让物体加速0.02m/s。宏观上施加了1s的推理,微观上推了50次,每次1N,作用0.02s。
FixedUpdate实际上是每次内部物理信息更新前的一次回调方法。Time.fixedDeltaTime代表物理更新间隔,一般是0.02秒。除此以外,Time.time是游戏已进行的时间,是一个折算后的时间,如果需要真实的时间,需要调用Time.realtimeSinceStartup,这个是真实的系统时间。
单线程:
Unity中的所有方法都是单线程进行的,C#中使用Thread.CurrentThread.ManagedThreadId可以获取当前线程的ID
using System.Threading;
void Update()
{
Debug.Log("UpdateThread=" + Thread.CurrentThread.ManagedThreadId);
}
private void FixedUpdate()
{
Debug.Log("FixedUpdateThread=" + Thread.CurrentThread.ManagedThreadId);
}
可以看到不管是Update中的线程ID还是FixedUpdate中的线程ID,都是1。它们不会并发执行,所以不用考虑互斥,重入之类的情况。
帧调度算法:
Unity内部的循环调度伪代码:
Unity_Main_Loop()
{
While(true)
{
检查是否执行Update()
检查是否执行FixedUpdate()
...
}
}
各个调度算法应该尽快执行完毕,避免卡顿
InputFlag
当我们在FixedUpdate中加入一些按键按下或弹起的判断时:
if (Input.GetButtonDown("Jump"))
{
rb.AddForce(0, 5, 0, ForceMode.VelocityChange);
}
发现有时会执行有时不会执行。而当Upda内如果写了很多东西导致卡顿时,这个判断又会被执行很多次。
这是因为InputFlag的改变是Update来控制的,如果Update的间隔比FixedUpdate要小,InputFlag被置为true后,还没等到FixedUpdate来检测InputFlag是否为true,先到的下一个Update已经把InputFlag置回false,导致FixedUpdate不能判断到InputFlag。而如果Update太卡导致间隔比FixedUpdate要大时,Update间多次调用的FixedUpdate会重复检测同一个InputFlag,导致一次按键多次调用的事情发生。
所以在FixedUpdate中,不应使用Input获取事件输入,应当都写在Update中:
GetButtonDown/Up GetKeyDown/Up GetMouseDown/Up
AddForce的方式
rb.AddForce(1,0,0)默认是ForceMode.Force模式,也就是直接加力,另外还有以下几种方式:
private void FixedUpdate()
{
//F * t = m * v 力
rb.AddForce(1, 0, 0, ForceMode.Force);
//a * t = v 加速度
rb.AddForce(1, 0, 0, ForceMode.Acceleration);
//P = m * v 冲量
rb.AddForce(1, 0, 0, ForceMode.Impulse);
//v = v 速度增量
rb.AddForce(1, 0, 0, ForceMode.VelocityChange);
}
不管使用什么方式,归根结底都是要改变物体的速度和方向。
另外,
AddForce是世界坐标系中的移动;
AddRelativeForce是物体自己的坐标系的移动。
练习:实现盒子的加速减速
我们已经知道给物体加速的方式,给物体减速的方式也很多,可以使用反向加速实现,使用摩擦力实现,使用刹车实现。反向加速容易让物体倒退,摩擦力可控性太低,我们选择刹车方式。
使用虚拟键前后左右控制移动,使用空格键控制刹车。
private void FixedUpdate()
{
//横纵向移动
float axisV = Input.GetAxis("Vertical");
float axisH = Input.GetAxis("Horizontal");
if(axisV != 0)
{
rb.AddForce(0, 0, axisV * 10);
}
if (axisH != 0)
{
rb.AddForce(axisH * 10, 0, 0);
}
//减速判断
if (Input.GetButton("Jump"))
{
//获得物体速度
Vector3 velocity = rb.velocity;
//获得物体的移动方向
Vector3 direction = velocity.normalized;
//定义减速度
float slowingSpeed = 20f * Time.fixedDeltaTime;
//物体速度是否大于减速度(防止倒退)
if(velocity.magnitude > slowingSpeed)
{
//向反方向加速
rb.AddForce(direction * (-1) * slowingSpeed, ForceMode.VelocityChange);
}
}
}
Velocity.magnitude代表物体的速率(速度向量的绝对值)。
扭矩
扭矩Torque不是力,而是力与力臂长度(作用点与转轴之间的距离)的乘积,单位:Nm。让一个物体转动的物理学方式,就是给这个物体添加一个扭矩。
在盒子脚本中:
private void FixedUpdate()
{
if (Input.GetButton("Fire1"))
{
//让盒子以自己为坐标轴进行旋转
rb.AddRelativeTorque(0, 10, 0, ForceMode.Force);
}
}
如果盒子的坐标轴没有发生旋转,可以尝试将窗口中的Global坐标系改为Local坐标系。
如果将物体和地面的摩擦系数调为0,根据角动量守恒定律,物体会一直旋转下去。但是刚体组件中有AngularDrag角速度衰减属性,只要它不为0,角速度还是会衰减。
角速度
角速度AngularVelocity,衡量一个物体转速的量,单位是弧度/秒,每秒转一圈的角速度:angularVelocity = 6.28 弧度/秒。
通过以下代码可以打印输出物体的角速度:
if(rb.angularVelocity.magnitude > 0.1f)
{
Debug.Log(rb.angularVelocity);
}
可以看到当角速度达到6.8的时候就不会继续增加了,这是因为有maxAngularVelocity的限制,这个值默认是7,可以修改。
//Start内
//将最大角速度限制为2Π,每秒一圈。
rb.maxAngularVelocity = Mathf.PI * 2;
角速度的增加也有四种方式:
private void FixedUpdate()
{
//F * t = m * v 扭矩
rb.AddRelativeTorque(1, 0, 0, ForceMode.Force);
//a * t = v 角加速度
rb.AddRelativeTorque(1, 0, 0, ForceMode.Acceleration);
//P = m * v 角动量
rbAddRelativeTorque(1, 0, 0, ForceMode.Impulse);
//v = v 角速度增量
rb.AddRelativeTorque(1, 0, 0, ForceMode.VelocityChange);
}
一般使用第一种和第四种。
能量衰减
物体不会一直运动下去,它会受到摩擦力,阻力等因素的影响。为了模拟这种情况,RigidBody的属性中集成了线速度衰减(Drag)和角速度衰减(AngularDrag)功能:
物理碰撞
3D中两物体相遇,会根据对方的碰撞体形态来计算碰撞事件,而不是物体本身的形状。
碰撞回调:
private void OnCollisionEnter(Collision collision)
{
Debug.Log("CollisionEnter! name: " + collision.gameObject.name);
}
刚体碰撞体->静态碰撞体、刚体碰撞体->刚体碰撞体 这两种情况都会发生碰撞。所以如果要发生碰撞,必须有刚体参与。
值得注意的是,MeshCollider+RigidBody不会产生碰撞。但如果必须使用MeshCollider进行碰撞的话,勾选Convex即可,但是这样对内存的开销巨大。最好使用近似模拟。
CCD连续检测
一个刚体碰撞体和一个静态碰撞体使用物理方式运动发生碰撞时,如果速度过快,容易穿过对方而不发生检测。
如果物体速度过快,导致物体的上一帧和下一帧都不在物体内部,也就是一帧的间隔内穿越了物体,这样系统就不会判断两物体相碰撞。
上一帧:
下一帧:
在RigidBody的CollisionDetection中可以修改物体的碰撞检测算法
Discrete:离散的 Continuous:连续的
基于扫掠的CCD碰撞:不断检测前进路径上是否有障碍物,如果有,下一帧发生碰撞。
推算式CCD碰撞:计算两帧的中间状态,判断是否应该碰撞。
越薄的墙壁越容易发生穿透。
组合碰撞体、子物体碰撞体
一个物体可以有多个碰撞体,使用多个碰撞体来模拟一个复杂形状的碰撞体,是Unity推荐的方法。MeshCollider开销过于巨大。
对于一个组合对象,即有子物体的游戏对象,碰撞体可以交给所有子对象,刚体可以交给父级,这样也是可以发生碰撞的。
碰撞触发条件脚本挂载到父级即可触发。无论碰到头还是身体都会触发。子物体挂载脚本是不会触发碰撞事件的
物体的质心
rb.centerOfMass可以查看一个物体的质心所在位置。给一个有刚体,碰撞体的盒子挂载脚本:
Rigidbody rb;
void Start()
{
rb = GetComponent<Rigidbody>();
Debug.Log("Center of mass:" + rb.centerOfMass);
}
当地面和物体的摩擦系数太大,而推力又足够大时,物体会发生翻转而不是平移,这是因为摩擦力和推力不在同一水平线上,两个力共同作用给物体施加了一个扭矩,导致物体翻转。
解决方法:修改物体的质心,使其与物体所受摩擦力在同一水平面上。
//质心默认以物体的轴心为坐标系,且默认坐标为0,0,0
//修改物体质心到物体底面,让其变为"不倒翁"
rb.centerOfMass = new Vector3(0, -0.5f, 0);
可以观察到不管摩擦系数多大,只要力足够大,物体总会平移不会翻转。
对于多碰撞体的情况(不管是一物体有多个碰撞体,还是多个子对象都有碰撞体),默认情况下质心的位置在多个碰撞体的几何中心的平均值点,Debug输出的这个点的坐标是相对于其轴心坐标系的。
练习:跳一跳
步骤:
1,加入场景,人物,跳台:
2,给玩家加入刚体,脚本,改变质心,测试跳跃:
public class PlayerCtrl : MonoBehaviour
{
Rigidbody rb;
public Vector3 jumpspeed;
void Start()
{
rb = GetComponent<Rigidbody>();
rb.centerOfMass = new Vector3(0, 0, 0);
}
void Update()
{
if(Input.GetButtonDown("Jump"))
{
rb.AddForce(jumpspeed, ForceMode.VelocityChange);
}
}
}
防止人物在跳跃结束后站不稳的方法:
1,添加BoxCollider加大底面碰撞体体积。
2,将质心修改到底面的不倒翁。
3,增加摩擦系数制动。
3,蓄力:
void Update()
{
if(Input.GetButtonDown("Jump"))
{
timeVal = Time.time;
}
if (Input.GetButtonUp("Jump"))
{
float chargeTime = Time.time - timeVal;
if(chargeTime < 0.6f)
{
chargeTime = 0.6f;
}
else if(chargeTime > 3f)
{
chargeTime = 3f;
}
jumpspeed.z *= chargeTime;
rb.AddForce(jumpspeed, ForceMode.VelocityChange);
}
}
4,蓄力效果:
当按下Jump时,游戏记录当前台子和玩家的scale,弹起时,将台子和玩家的scale归位。
当按住时:
if(Input.GetButton("Jump"))
{
scaleTime += Time.deltaTime;
if (scaleTime <= 3f)
{
transform.localScale -= new Vector3((-1) * scaleTime * 0.0004f, scaleTime * 0.0004f, (-1) * scaleTime * 0.0004f);
boardA.transform.localScale -= new Vector3((-1) * scaleTime * 0.0004f, scaleTime * 0.0004f, (-1) * scaleTime * 0.0004f);
}
}
5,胜负判定:
private void OnCollisionEnter(Collision collision)
{
if(collision.gameObject.name == "Floor")
{
Debug.Log("Failure!");
}
else if(collision.gameObject.name == "BoardB")
{
Debug.Log("Win!");
}
}
6,UI:
Overlay模式的Canvas界面,添加一个Panel,里面添加一个Text;当游戏胜利或失败时,执行改变文本,SetActive。
新建UIManager脚本,挂载到Canvas上:
using UnityEngine.UI;
public class UIManager : MonoBehaviour
{
public static UIManager Instance;
public GameObject UI_Panel;
public GameObject UI_Text;
void Start()
{
Instance = this;
}
public void OnLose()
{
UI_Panel.SetActive(true);
}
public void OnWin()
{
Text text = UI_Text.GetComponent<Text>();
text.text = "胜利!Space键重启";
UI_Panel.SetActive(true);
}
修改Player的脚本:
private void OnCollisionEnter(Collision collision)
{
if(collision.gameObject.name == "Floor")
{
UIManager.Instance.OnLose();
}
else if(collision.gameObject.name == "BoardB")
{
UIManager.Instance.OnWin();
}
}
脚本实例化:
当一个脚本中的参数或函数被很多脚本调用时,可以考虑全局实例化此脚本,被实例化的脚本可以在任意其他脚本中使用它的参数和函数。实例化方法:
public class XXXX : MonoBehaviour
{
//实例名与脚本同名
public static XXXX Instance;
void Start()
{
//让此实例指向此脚本
Instance = this;
}
}
只要此脚本被初始化,其他脚本就可以调用此函数:
XXXX.Instance.YY = yy;
XXXX.Instance.YYY();
7,重新开始的方法:
使用重新载入场景的方法让游戏重新开始:
using UnityEngine.SceneManagement;
//Update中
if(isJumped && Input.GetButtonDown("Jump"))
{
SceneManager.LoadScene("SampleScene");
}
物理关节
物理关节Joint,又称 J 点,用于将一个刚体连接到另外一个刚体。Unity中的四种物理关节:
FixedJoint 固定链接 用于将一个物体钉死在另一个物体上。
HingeJoint 铰链链接 将物体固定在一个轴上,使其可以转动(门,钟摆)。
SpringJoint 弹簧链接 将一个物体用弹簧链接到另一个物体,它们之间可以伸长缩短。
CharacterJoint 人物关节 让一个物体绕一点旋转(膝盖,脖子)。
FixedJoint
FixedJoint用于固定链接某物,为一个盒子添加FixedJoint组件。
ConnectedBody表示它链接的那个刚体,如果没有,就表示此物体被固定在原地。
BreakForce表示破坏力,如果物体受到的力超过这个值,FixedUpdate就会断开。 Infinity表示无穷大
BreakTorque表示破坏扭矩。
如果此时在盒子边添加一个平台并将平台的RigidBody拖给盒子的ConnectedBody,盒子就作为平台的一部分随它运动,两物体相对静止。当两物体需要轻松分离时,FixedJoint是个不错的选择。
HingeJoint
铰链链接可以规定一个物体的哪个轴被固定,整个物体可以绕着这个轴旋转。
EditAngularLimists可以查看与当前旋转轴相垂直的面
Anchor用来修改轴在物体当中的坐标,是个相对位置,以轴心为坐标原点
Axis用来修改物体绕哪个轴旋转
ConnectedAnchor用来表示此物体链接的物体的轴心坐标,未规定则是自己的轴心。
修改轴向为(0,0,1),可以模拟跷跷板的效果。
勾选UseLimitsk可以设置铰链链接的角度限制,也能让此铰链能有一些反弹。
铰链弹簧:可以将铰链变成自动归位的铰链
勾选UseSpring启用弹簧,可以设置弹簧的拉力,衰减和归位位置。
模拟开门:
铰链马达:
勾选UseMotor可以启动这个铰链的马达功能,此时不要勾选UseLimit。
Motor设置中可以修改这个铰链的目标速度,受到的力。FreeSpin可以控制这个马达是否会制动。
练习:制作一个单摆钟
1,建立模型碰撞体,材质,刚体,铰链关节,限制角度
2,新建质心子对象,将其放到单摆顶部,在脚本中将质心位置设置为它的localPosition。
public Rigidbody rb;
void Start()
{
rb = GetComponent<Rigidbody>();
//找到子对象
//本地坐标为相对于轴心的坐标
rb.centerOfMass = transform.GetChild(2).localPosition;
}
3,向质心施加一点扭矩
void Update()
{
if(Input.GetButtonDown("Jump"))
{
rb.AddRelativeTorque(10, 0, 0, ForceMode.VelocityChange);
}
}
触发器
触发器Trigger用来检测碰撞体之间的碰撞,Collider+IsTrigger可经由物理系统检测两物体是否发生碰撞。
实验:拥有Collider+RigidBody的小球和拥有Collider的墙壁相撞
结果:小球被撞,停下,球和墙的OnCollisionEnter打印输出碰撞发生。
实验:拥有Collider+RigidBody的小球和拥有Collider+IsTrigger的墙壁相撞
结果:小球穿过墙壁,两物体的OnCollisionEnter均没有打印输出。
实验:在先前基础上给墙壁增加OnTriggerEnter判定
结果:小球穿过墙壁,墙壁的OnTriggerEnter检测到小球
private void OnTriggerEnter(Collider other)
{
Debug.Log("Other: " + other.gameObject.name);
}
说明物理系统检测到了它们的碰撞,但是物理系统不接管这个碰撞后发生的事情,触发器Trigger用于检测碰撞,但不需要计算物理。
触发器的事件函数:
OnTriggerEnter:发生接触
OnTriggerStay:接触中
OnTriggerExit:脱离接触
并且发生碰撞时,双方的OnTrigger都能检测到这个碰撞:给小球添加OnTriggerEnter但不要勾选IsTrigger
双方都检测到了碰撞。
触发器的用法
当我们需要检测玩家或物体是否进入了某个地点,某个门,这时我们只需要检测物体是否发生碰撞,而不需要计算碰撞发生之后的物理,就可以使用触发器:
在门的中央创建一个方形碰撞器+IsTrigger,当物体穿过门框但没有与门框发生碰撞也能触发Trigger。
不同类型的碰撞体和触发器可以在同一个物体上使用,空物体上也可以使用碰撞体。
PhysicsDebugger
在Window/Analysics可以找到PhysicsDebugger(物理调试器),通过它可以一目了然当前场景中的碰撞器,触发器,刚体等物件,也会醒目的标出哪些物件是没有物理的。
TimeScale
TimeScale是游戏时间流速控制器,它可以控制整个游戏的时间流速Time.time。例如:
Time.timeScale = 1f 正常的时间流速
Time.timeScale = 0.5f 正常时间流速的一半
Time.timeScale = 0f 时间静止(Time.time不再流动)
测试:小球的脚本:
public class Ball : MonoBehaviour
{
public float moveSpeed;
// Start is called before the first frame update
void Start()
{
Time.timeScale = 1f;
}
// Update is called once per frame
void Update()
{
if(Input.GetButton("Jump"))
{
transform.Translate(0, 0, moveSpeed * Time.deltaTime);
}
}
}
将Time.timeScale改为0.5f:小球明显变慢
将Time.timeScale改为0f:小球不再移动(Time.deltaTime永远为0)
打印输出Time.time:
打印输出Time.realtimeSinceStartup:
开始游戏后经过的真实时间是不受影响的,而且:
Time.time流速减小,Update照常运行,Time.deltaTime被缩短。
Time.time流速减小,FixedUpdate运行速度减慢。
Time.time流速减小,Animator动画的速度减慢。
值得注意的是,Time.timeScale是全局的,它能让全局的时间流速变慢。如果需要让某个动画的速度不受全局时间流速影响:
_animator.updateMode = AnimatorUpdateMode.UnscaledTime;
场景管理
Unity的文件中,游戏是以场景打包的 *.unity,内部包含了游戏对象、组件、各种资源的引用。游戏做的比较大的时候,需要多个场景的切换。在Create菜单中就可以新建场景Scene。Hierarchy层级栏中显示的是当前场景中的信息。场景文件中本身不包含资源,它引用了资源。
加载场景
Unity中有两种场景加载方式:LoadScene(同步加载场景)和LoadSceneAsync(异步加载场景),需要库:UnityEngine.SceneManagement
我们拥有多个场景:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tItvV9sz-1637307327222)(E:\Paintings\UnityProject\test3D\pic\image-20211118204236330.png)]
首先需要将所有需要加载的场景都放到BuildSettings(发布设置)里:File/BuildSettings
可以通过脚本控制场景的切换:
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour
{
void Update()
{
if (Input.GetButtonDown("Fire1"))
{
SceneManager.LoadScene("MenuScene");
}
}
}
点击鼠标左键就可以将场景切换到MenuScene了。必须是已经被添加到BuindSettings的场景才能被加载到。
也可以:
SceneManager.LoadScene(1);
前提是MenuScene的索引序号是1;
场景销毁
默认的,从场景A切换到场景B后,场景A的所有游戏组件,组件都会被销毁。对象在被销毁时会回调一个方法OnDestroy(),我们可以重写它。
//gameManager中
private void OnDestroy()
{
Debug.Log("GameManager Destroyed!");
}
切换场景:
但是有些对象包含了一些重要性的全局数据,如果需要保护这些数据,就需要保护一些对象在切换场景时不被销毁,DontDestroy可以要求Unity在切换场景时保护一些东西不被销毁。
DontDestroyOnLoad(obj);
//GameManager中
void Start()
{
DontDestroyOnLoad(this.gameObject);
}
在运行游戏后,可以发现GameCtrl被提拔了,它变成了全局场景DontDestroyOnLoad的东西。在设计游戏时要规定好要放到DontDestroyOnLoad下的对象,不能把对别的场景下某个对象的引用放到DontDestroyOnLoad,切换场景后全局对象引用的对象会被销毁,报错。
|