回想一下学unity也学了不少东西了,可是很多东西还是学了忘忘了学,于是可能还得当场百度一下才能回想的起来。为加强自己的熟练程度,继续来更新博客了。
文章目录
左右移动
实现移动有三种方式:
- 直接改变位置
- 给物体添加速度
- 给物体添加力
1.直接改变位置的方法
transfor.Translate(Vector3);
如果双方都没有刚体组件,则会直接穿过。
如果玩家是刚体,则会出现如下情况:
这种方法会存在一点小问题,在3维空间中,当角色接近不可移动的物体时,如果继续按方向键,人物则会有穿模抖动现象,穿入到物体中。
2.给物体添加速度的方法
需要给物体挂接RigidBody组件,然后调用:
Rigidbody rigidbody;
rigidbody.velocity=new Vector3(0,0,0);//可以直接用向量改变速度
3.给物体添加力的方法
Rigidbody rigidbody;
rigidbody.AddForce(new Vector3(0,0,0));//可以直接用向量改变速度
(第三种方法会使得玩家具有惯性,如果想让玩家在空中有这种惯性但是在地面上的惯性很小可以给地板添加物理材质,将其的摩擦系数提高)
例如马里奥:
乘以Time.deltaTime的作用
首先需要明确一点,这个是用在Update函数中,在FixedUpdate函数中存在着一个类似的变量:Time.fixedDeltaTime默认是0.02s.
+
-+
Time.deltaTime是:完成上一帧所用的时间(以秒为单位)(只读)。
此属性提供当前帧和上一帧之间的时间。
乘以Time.deltaTime有几个作用
- 如果在update函数中不乘上Time.deltaTime,那么每帧由于其时间是不一样的,假如一帧0.01秒,一帧0.03秒,那么这两个时间内,移动的都是一样的距离,那这就不是匀速了,乘上时间就可以实现匀速运动.
2.不乘time.deltaTime,移动则是按照一帧移动多少距离,乘了之后就变成一秒移动多少距离
跳跃
普通的跳跃只需要给玩家添加向上的速度或者向上的力即可,然后加入一个玩家是否在地面的判断(目的是防止无限跳)。
判断是否在地面
三种方法
- 给地面设置tag然后调用碰撞enter和exit函数
- 对地面进行射线检测
- 在底部添加小物体然后使用球形检测
- 角色控制器自带的isGrounded方法
第一种方法:
private void OnCollisionEnter(Collision collision)
{
if (collision.collider.tag == "Ground")
{
isGround = true;
}
}
private void OnCollisionExit(Collision collision)
{
if (collision.collider.tag == "Ground")
{
isGround = false;
}
}
这种方法隐患很大,不建议使用。例如当物体处于地面上并解除台阶的侧面然后离开时,此时会OnCollisionExit函数设置为离开地面的状态。
第二种方法
射线检测
射线检测的官方文档:
代码如下:
bool isGrounded = Physics.Raycast(transform.position, Vector3.down, playerHeight/2+0.1f);
球形检测法:
在物体底部创建一个物体:
public Transform groundcheck;
public LayerMask layerMask;
float checkRadius = 0.2f;
// Update is called once per frame
void Update()
{
bool isGround = Physics.CheckSphere(groundcheck.position, checkRadius, layerMask);
}
这个layermask是可以用来过滤一些层次,比如此处我们只需要地面环境具有这个检测的效果,可以将地面的layer设置一下:
蓄力跳跃
注意到例如马里奥的游戏中,短按跳的低,长按跳的高,实现的方法如下:
public float MaxJumpTime=0.3f; //跳跃的最大蓄力时间
float timeJump; //跳跃当前的蓄力时间
bool jumpState = false;//玩家当时是否处于跳跃蓄力的状态
public float jumpStartSpeed = 4f;//起跳速度
public float jumpContinueSpeed = 0.7f;
void Jump()
{
if (Input.GetKeyDown(KeyCode.W)&&jumpState==false)
{
jumpState = true; //进入跳跃状态
timeJump = 0.01f;//蓄力时间清零
rigidbody.velocity = new Vector3(0.0f, rigidbody.velocity.y + jumpStartSpeed, 0.0f);
Debug.Log("player start jump");
}
else if (Input.GetKey(KeyCode.W) && timeJump <= MaxJumpTime && jumpState)
{
timeJump += Time.deltaTime;//蓄力时间增加
rigidbody.velocity = new Vector3(0.0f, rigidbody.velocity.y + jumpContinueSpeed, 0.0f);
Debug.Log("player is xuli");
}
else if (Input.GetKeyUp(KeyCode.W))
{
jumpState = false;//退出跳跃状态
timeJump = 0;//蓄力时间清零
}
}
上面的代码实现了跳跃和蓄力跳,接下来是在update函数中如何调用:
void Update(){
if (Input.GetKeyDown(KeyCode.W) && isGrounded == true ||jumpState==true )//前半个条件是进入起跳的条件,后半个条件是进入蓄力跳的条件
{
Debug.Log("is jumping");
Jump();
}
}
随后便可实现轻按小跳重按大跳的方法(图自行想象)
角色控制器的使用方法
角色控制器更注重实现人物的移动而不展现物理规律。rigidbody更注重展现物理规律。例如上坡,用角色控制器很好实现但是rigidbody很难。
使用例子如下:
float speed = 10f;
float rotateSpeed = 1f;
// Start is called before the first frame update
CharacterController cc;
void Start()
{
cc = GetComponent<CharacterController>();
}
// Update is called once per frame
void Update()
{
float horizontal = Input.GetAxis("Horizontal");
float veritcal = Input.GetAxis("Vertical");
var move = transform.forward * speed * veritcal * Time.deltaTime;
cc.Move(move);
transform.Rotate(Vector3.up, horizontal * rotateSpeed);//旋转轴和旋转角度
}
效果如下:
详细的transform.Rotate函数方法使用如下:
https://docs.unity3d.com/cn/current/ScriptReference/Transform.Rotate.html
在角色控制器中模拟重力
添加了CharacterController组件后,人物将不受力影响,这时即使加上Rigidbody组件,并启用Use Gravity,人物也不会受重力影响,在脚本中通过rigidbody对人物施加力也是无效的。也就是说CharacterController屏蔽了Rigidbody的所有属性和方法。要控制CharacterController移动,可以通过在脚本中调用Move方法使其移动。
但是要用角色控制器
private Vector3 velocity = Vector3.zero;
float gravity = -9.8f;
bool isGround = Physics.CheckSphere(groundcheck.position, checkRadius, layerMask);
if (isGround == true) Debug.Log("is in ground");
if (!isGround)
{
velocity.y += gravity * Time.deltaTime;//v=v0-gt
cc.Move(velocity * Time.deltaTime);//在一帧内移动这段时间的速度乘以距离
}
else
{
velocity.y = 0;
}
}
(transform的速度不需要刚体)
注意,如果此时一帧内有两个move函数其实互不冲突,它是一个“移动的矢量形式的结果”。
几个参数的定义:
slope limit可以上的斜坡的角度
step offset可以上的台阶的高度
Skin Width: 皮肤厚度。该参数决定了两个碰撞体可以相互渗入的深度,较大的参数值会产生抖动现象,较小的参数值会导致所控制的游戏对象被卡住,较为合理的设置是该参数值为Radius值的10%。
Character Controller的Skin Width是非常重要的属性,因此必须正确的设置。如果角色被卡住了,通常是由于Skin Width的厚度太小而导致的该值可以使其他的对象轻微的穿过Character Controller,并且可以避免抖动且防止角色卡住。
角色控制器里的碰撞效果
Character Controller不会对施加给他的作用力做出反应,也不会作用于其他的刚体。如果想让Character Controller组建能够够作用于其他的刚体对象,可以通过脚本【OnControllerColliderHit()函数】在与其碰撞的对象上使用一个作用力。另外如果想让 Character Controller受物理效果影响,那最好用刚体来替代它。
private void OnControllerColliderHit(ControllerColliderHit hit)
{
if (hit.transform.tag == "Collider")
{
hit.rigidbody.AddForce(transform.forward * speed);
}
}
Inspector组件中的debug界面
这里存在一个界面可以用来实时的查看一些值,例如刚体中的速度。
(注:有角色控制器就不需要collider组件了。)
Cinemachine的功能及使用方法
只有follow没有look at的结果:
注意 aim不是简单的盯着某个物体看,它会旋转视角从而始终盯着人物的前方。
有follow有look at的结果:
实现人物视角始终看向鼠标的操作
(俯视角下的实现机制)
需要求出鼠标和玩家位置的夹角。
但是玩家的位置是世界坐标系而不是屏幕坐标系下的,所以需要将玩家位置从世界坐标系转换到屏幕坐标系:
Camera.main等同于GameObject.Find(“MainCamera”).GetComponent()
void playerLookAtMouse()
{
Vector3 playerPosInScreen = Camera.main.WorldToScreenPoint(transform.position);
Vector3 distance = Input.mousePosition - playerPosInScreen;
float angle = Mathf.Atan2(distance.x, distance.y) * Mathf.Rad2Deg;//弧度转角度
transform.eulerAngles = new Vector3(transform.eulerAngles.x, angle, transform.eulerAngles.z);
}
碰撞发生的条件总结
在 unity 里面要想实现碰撞,需要满足两个条件。
- 两个物体都具有碰撞组件。
- 运动的组件拥有刚体。
注意!如果两个物体满足第一个条件后,**第二个条件这里是运动的组件拥有刚体,**如果一个刚体触碰另外一个没有刚体的物体,那么运动的物体会被撞开,而没刚体的物体则不会有任何移动!
当两个碰撞的物体,其中一个勾选上 Is Trigger 时,会触发:OnTriggerEnter、OnTriggerStay、OnTriggerExit
当不勾选Is Trigger 时,会触发:OnCollisionEnter、OnCollisionStay、OnCollisionExit。
自动在脚本里给物体添加组件
[RequireComponent(typeof(Rigidbody))]
即可。
全局音乐
首先核心是下面这个函数:
在unity中我们经常要用到DontDestroyOnLoad来使一个gameobject在切换场景的时候不被销毁而保留下来,但是有时会遇到这样的情况,在Loading场景建立一个空物体,我给它起名叫test,上面挂一个脚本,如图
脚本里的代码是这样的
void Start ()
{
DontDestroyOnLoad(this);
}
接下来从一个场景跳转到另外一个场景时,该物体会被保留。
可是当重新切回来时,此时会导致场景中多了一个这个物体。
如何解决这个问题?最简单的方法如下,由于场景切换时,静态变量依然保留,所以可以用静态变量记录。
方法二,用单例实现:
单例模式
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
一般情况下我们都会把类的构造器定义成public的访问权限,允许任何类在任何情况下都可以创建该类的对象。但是某些情况下允许其它类自由创建该类的对象没有任何意义,甚至还可能造成系统性能的下降,因为频繁地创建对象和回收对象会带来系统的开销问题。
如果一个类始终只能创建一个实例,则这个类被称为单例类。
使用起来有三个限制:
-
总之,在一些特殊场景下,为了避免其他类自由创建该类的对象,应该把该类的构造器使用private修饰,从而把该类的所有构造器隐藏起来。
-
根据良好的封装原则:一旦把该类的构造器隐藏起来,就需要提供一个public方法作为该类的访问入口,用于创建该类的对象,且该方法必须使用static修饰,因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类。
-
除此之外,该类还必须缓存已经创建的对象,否则该类无法知道是否已经创建过对象,也就无法保证只创建一个对象。为此该类需要使用一个成员变量来保存曾经创建的对象,因为该成员变量需要被上面的静态方法访问,所以该成员变量必须使用static修饰。
因此,代码如下:
接下来实现全局音乐,代码参考上面:
public class MusicManager : MonoBehaviour
{
private static MusicManager instance;
private MusicManager() { }
public static MusicManager GetInstance()
{
if (instance == null)
{
//此处创建该实例
DontDestroyOnLoad(instance);
}
return instance;
}
}
然后使用其他物体去调用这个脚本创造实例即可,方法如下:
然后用空物体挂接该脚本,并执行:
但是仅仅这样还不够,因为我们并没有实际创造出来这个实例,并且,donDestroyOnLoad需要的是包含有GameObject的物体。
在unity中,GameObject可以添加脚本作为组件,而类的对象也可以添加GameObject作为其组件,(事实上GameObject就是一个类。)
此处instance是MusicManager的对象,因此需要用MusicManager的对象去初始化它,可以用下面这种方式,给gameobject添加一个MusicManager的实例对象去初始化自身。
为了实现播放音乐,可以在上面添加如下的代码:
这样即可实现,该功能。
当然,那四行语句的功能也可以用一个脚本来实现:
[RequireComponent(typeof(AudioSource))]
public class MusicController : MonoBehaviour
{
private AudioClip music;
private AudioSource musicPlayer;
void Awake()
{
// 获取 AudioSource
musicPlayer = this.GetComponent<AudioSource>();
// 打开单曲循环
musicPlayer.loop = true;
// 关闭开始时就播放的属性
musicPlayer.playOnAwake = false;
}
void UpdateMusicSetting()
{
// 读取音乐
music = (AudioClip)Resources.Load("bgMusic");
// 将读取到的音乐赋值给 audioClip
musicPlayer.clip = music;
// 播放
musicPlayer.Play();
Debug.Log("bgmusicPlay");
}
void Start()
{
UpdateMusicSetting();
}
}
按顺序加载场景
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
如果加载当前场景就不用+1
动态数组
使用List实现,代码如下:
List<GameObject> BoxList;
void Start()
{
BoxList=new List<GameObject>();
BoxList.Add(pushableBox);
foreach (GameObject pushablebox in BoxList)
{
……
}
}
使用代码加载文件及挂接组件
在代码开始的上方加一个这个即可实现:
加载文件:Resources.Load(path)读取Resources文件夹下的路径的文件。然后可以将其类型转换给其他类型的物体来承接。
旋转的实现及Quaternion的讲解
三种旋转:
矩阵旋转
优点:
旋转轴可以是任意向量;
缺点:
旋转其实只需要知道一个向量+一个角度,一共4个值的信息,但矩阵法却使用了16个元素;
而且在做乘法操作时也会增加计算量,造成了空间和时间上的一些浪费;
欧拉旋转
优点:
很容易理解,形象直观;
表示更方便,只需要3个值(分别对应x、y、z轴的旋转角度);但按我的理解,它还是转换到了3个3*3的矩阵做变换,效率不如四元数;
缺点:
之前提到过这种方法是要按照一个固定的坐标轴的顺序旋转的,因此不同的顺序会造成不同的结果;
会造成万向节锁(Gimbal Lock)的现象。这种现象的发生就是由于上述固定坐标轴旋转顺序造成的。理论上,欧拉旋转可以靠这种顺序让一个物体指到任何一个想要的方向,但如果在旋转中不幸让某些坐标轴重合了就会发生万向节锁,这时就会丢失一个方向上的旋转能力,也就是说在这种状态下我们无论怎么旋转(当然还是要原先的顺序)都不可能得到某些想要的旋转效果,除非我们打破原先的旋转顺序或者同时旋转3个坐标轴。这里有个视频可以直观的理解下;
由于万向节锁的存在,欧拉旋转无法实现球面平滑插值;
四元数旋转
优点:
可以避免万向节锁现象;
只需要一个4维的四元数就可以执行绕任意过原点的向量的旋转,方便快捷,在某些实现下比旋转矩阵效率更高;
可以提供平滑插值;
缺点:
比欧拉旋转稍微复杂了一点点,因为多了一个维度;
理解更困难,不直观;
四元数
旋转的localRotation、eulerAngles和Rotate的使用方法及区别
rotation是四元数,eulerAngles是欧拉角。
带有local的代表是物体相对于父级物体的相对旋转程度,不带local代表的是在世界坐标系下的绝对旋转。
可以调用transform的eulerAngles来改变物体的旋转例如:
transform.eulerAngles = new Vector3(0, 0, 90);
还可以通过改变其rotation:
transform.rotation = Quaternion.Euler(new Vector3(0f, 0, 90f));
事实上,上面两句话的意思是一样的。
transform.Rotate有两种:
一种是绕着轴旋转多少度:
/`
一种是围绕某个轴旋转多少度.
运动学(Kinematic)刚体
运动学刚体 (Kinematic Rigidbody) 是指启用了“运动学”(is Kinematic) 选项的刚体。运动学刚体不受力、重力或碰撞的影响。它们通过设置变换的位置和旋转或对其进行动画处理来驱动,运动学刚体还可以与其他非运动学刚体 (non-Kinematic Rigidbody) 进行交互。
运动学刚体 (Kinematic Rigidbody) 可在与其他刚体 (Rigidbody) 碰撞时正确唤醒它们,并会对置于其上的刚体 (Rigidbody) 应用摩擦力。
下面是针对运动学刚体 (Kinematic Rigidbody) 的几个使用情况的示例:
-
希望某个对象处于物理控制下,但是其他情况下则是通过脚本或动画进行控制。例如,可以创建一个动画角色,其骨骼附加了与关节 (Joint) 连接的刚体,以用作布娃娃。大多数时候该角色处于动画控制下,此时会创建运动学刚体 (Kinematic Rigidbody)。但是当命中它时,又希望它变为布娃娃并受物理影响。为实现此目的,只需禁用“运动学”选项,即让“is Kinematic”为false即可。
-
需要某个对象可以推动其他对象、但不推动自己移动的对象。例如,如果有一个动画平台,并且要在其上放置一些刚体箱体,则应使该平台成为运动学刚体而不仅仅是不带刚体的碰撞器 (Collider)。
-
可能需要某个对象有一个经过动画处理的运动学刚体,并使用一个可用的关节让真实刚体 跟随其后。