- 0.00 Unity入门+实践 重点合并 - 2. 单例模式的使用 - 3. 物体移动和力的应用 - 4.项目实践中的一些总结: - 关键改进点: - 总结:
- 0.01快捷键
- 0.03反射
- 0.04 Inspector窗口
- 0.05Unity的工作机制
- 0.06脚本相关 生命周期函数
- 0.07Transform的学习
- 0.08基础知识总结
- 0.49摄像机相关知识
- 0.53光源
- 0.56刚体参数说明图
- 刚体参数说明图
- 0.57碰撞器参数说明
- 0.58碰撞器物体材质
- 0.59-0.62碰撞生命周期,碰撞检测,刚体
- 0.63音频导入
- Unity入门
我主要在这里留下了一些我自己学习上不懂的,来加强学习
重点错误:一个物体子物体有Rb和collider,它本身是一个空的gameobject,如果在它身上挂载脚本去检测是否和别的物体碰撞是不会触发的,这是为什么?
-
碰撞检测的前提条件:
- 参与碰撞的两个物体必须都有Collider组件,且至少有一个物体还需要Rigidbody组件。
- 如果父对象没有Collider和Rigidbody,即使子对象有,父对象上的脚本也无法检测到碰撞。
-
空GameObject的作用和限制:
- 通常用于组织场景结构或作为子对象的容器。
- 如果没有Collider或Rigidbody,它不会参与物理碰撞,无法触发碰撞事件。
-
Transform的几个常用方法:
- TransformPoint考虑位置、旋转和缩放。
- TransformDirection只考虑旋转。
- TransformVector考虑旋转和缩放,不考虑位置。(向量:在游戏开发中,向量常用来表示方向和距离,但不具有具体位置。)
Transform child = transform.Find("Parent/Child");
//`Transform.Find` 方法可以通过路径查找子对象或子对象的子对象。路径中的斜杠 `/` 用于区分父子关系。
四元数 vs 欧拉角:transform.rotation
使用四元数表示旋转,虽然计算精确,但不直观。transform.eulerAngles
使用更易理解的三维向量表示旋转角度,适合直接操作。
单例模式的正确用法:
- 单例模式通常在
Awake
方法中初始化Instance
,确保在使用类名加Instance
访问时,单例已经正确初始化。
常见问题:
- 如果在
Awake
中未初始化Instance
,将导致在其他地方使用类名.Instance
时出现null
引用,无法正确访问单例对象。
泛型约束:
- 如果使用泛型单例模式,需要对泛型参数进行约束,例如限定为某个基类或接口。确保泛型类型在单例中能够被正确使用。
public class Singleton<T> : MonoBehaviour where T : class
{
public static T Instance { get; private set; }
protected virtual void Awake()
{
Instance = this as T;
}
}
你要有Collider或者是trigger,可以没有rigidbody,挂载脚本,就能响应OnCollider/OnTrigger 重点:如果是Rigidbody里面的子物体有脚本来检测是否碰撞,是不会触发的
重要概念:
-
AddForce:通过
AddForce
给物体施加力,使其沿指定方向移动。适用于模拟物理效果的场景。 -
改变 Position/Translate:直接设置物体的位置,用于不需要物理模拟的情况。
-
爆炸力 (
AddExplosionForce
):针对单个Rigidbody
使用,施加爆炸效果,力的作用受物体质量、阻尼等物理属性影响。 -
不要在
Awake
中调用依赖于其他对象或组件状态的方法,以确保初始化顺序的正确性。 -
所有状态在
Start
时都应该是安全和稳定的,以便你可以在Start
中安全地进行依赖于其他组件的初始化。 -
检查所有在
Start
中初始化的变量,确保它们不会在不合适的时机被访问。
- 尽量精简UI标签,可以把多个同类型的数据放在一个标签下处理。
- 注意在Awake中绑定必要的组件引用,否则可能出现空引用错误。
- 理解GameDataManager的使用,注意它没有继承MonoBehaviour。
- List可以直接移除指定索引之后的所有元素,不需要遍历。
- 单例模式下切换场景时instance的重新指向。
- 跟随目标移动可以直接用偏移量,而不是用坐标相减的方式。
-
武器实例化的代码改进:
- 确保currentWeapon引用的是Instantiate生成的对象,而不是预制体。
- 保证生成的武器位置和旋转与挂载点weaponPoint对齐。
- 对currentWeapon的操作将作用于正确的实例化武器对象。
-
场景切换与单例模式的问题
- 在切换场景时,如果
SettingPanel
使用单例模式,新的场景中会生成一个新的SettingPanel
实例。在这个过程中,新的SettingPanel
会在其Awake()
方法中将单例Instance
重新指向自己,导致先前场景中的Instance
被覆盖。 - 每次场景切换,新的
SettingPanel
会覆盖原有的单例实例,这可能会导致引用不一致,尤其是在GameDataManager
或其他管理器中需要访问该SettingPanel
时。 - 可以在
Awake()
中使用DontDestroyOnLoad
,并确保在新场景中不会再次生成SettingPanel
的实例。
- 在切换场景时,如果
public class SettingPanel : MonoBehaviour
{
public static SettingPanel Instance { get; private set; }
protected virtual void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject); // 保留实例跨场景
}
else
{
Destroy(gameObject); // 如果已经有一个实例存在,销毁新的实例
}
}
}
public void SetWeapon(Weapon weapon)
{
this.currentWeapon = weapon; //实际上这里是错误的使用,currentWeapon和下面的代码一点关系都没用
Instantiate(weapon.gameObject, weaponPoint.position, weaponPoint.rotation, weaponPoint);
}
-
正确引用实例化对象:修改后的代码确保
currentWeapon
引用的是通过Instantiate
生成的武器对象,而不是传入的预制体。 -
保证位置和旋转正确:通过实例化后再赋值给
currentWeapon
,确保了该对象的位置和旋转与weaponPoint
对齐。 -
后续使用正确对象:现在,任何对
currentWeapon
的操作(例如在Fire
方法中)都会作用于正确的实例化武器对象,从而保证其行为符合预期。
原代码的问题在于 currentWeapon
引用了预制体,而非实例化后的武器对象,导致位置和行为异常。通过将 currentWeapon
赋值为实例化后的武器对象,问题得以解决,确保了武器的位置和行为正确无误。
操作类别 | 操作方法 | 功能 |
---|---|---|
左键相关 | 鼠标单击 | 选择单个物体 |
鼠标框选 | 选择多个物体 | |
按住Ctrl + 鼠标单击 |
多选物体 | |
鼠标右键按下 + 移动鼠标 | 旋转视口 | |
长按ALT 键 + 鼠标左键 + 移动鼠标 |
相对观察视口中心点旋转 | |
选中物体之后,按F 键 |
居中显示物体(或者在层级窗口中双击对象) | |
右键相关 | 鼠标右键按下 + 移动鼠标 | 旋转视口 |
鼠标右键按下 + WASD |
漫游场景 | |
鼠标右键按下 + WASD + Shift |
快速漫游场景 | |
长按ALT 键 + 鼠标右键 + 移动鼠标 |
相对屏幕中心点拉近拉远 | |
中键相关 | 滚动鼠标中间 | 相对屏幕中心点拉近拉远 |
鼠标中间按下 + 移动鼠标 | 平移观察视口 | |
长按ALT 键 + 滚动鼠标中间 |
鼠标指哪就朝哪拉近拉远 |
- WASD 漫游: 该操作方式类似于第一人称视角游戏的移动方式,
W
前进、S
后退、A
左移、D
右移,配合鼠标右键可以在3D空间中自由漫游。 - 按F键居中: 如果在场景中丢失了物体位置,可以通过选中物体并按
F
键快速将视口定位到该物体。
特性名称 | 描述 | 使用示例 |
---|---|---|
[SerializeField] |
允许一个私有字段在Inspector中可见,便于没有公开Setter 的字段进行调整。 |
private int myValue; |
[HideInInspector] |
隐藏公共字段或通过[SerializeField] 公开的私有字段在Inspector中的显示。 |
[HideInInspector] public float speed; |
[Range(min, max)] |
在Inspector中为数值字段创建一个滑动条,限制可以设置的最小值和最大值。 | [Range(0, 10)] public float speed; |
[Tooltip("text")] |
为Inspector中的字段添加一个鼠标悬停时显示的提示信息。 | [Tooltip("The speed of the player")] public float speed; |
[Header("text")] |
在Inspector中为字段组添加一个标题。 | [Header("Player Settings")] public float speed; |
[Space(height)] |
在Inspector中的字段之间添加空间。 | [Space(10)] public float speed; |
[ReadOnly] |
使字段在Inspector中为只读,这通常用于显示但不允许修改的值。 | [ReadOnly] public float speed; |
在Unity中,你还可以使用一些特性来为脚本在Inspector中添加按钮,这些按钮可以执行特定的功能。
-
[ContextMenu("Function Name")]
- 描述:
ContextMenu
特性可以用于在Inspector中为一个脚本添加上下文菜单项,点击该菜单项时会调用指定的方法。 - 使用示例:
- 描述:
public class MyComponent : MonoBehaviour
{
[ContextMenu("Reset Speed")]
void ResetSpeed()
{
speed = 0f;
}
public float speed;
}
- **效果**: 在Inspector窗口中,右键点击该脚本的组件标题,选择"Reset Speed"即可调用`ResetSpeed`方法。
-
[Button] (需要自定义)
- 描述: Unity本身没有原生的
[Button]
特性,但可以通过自定义Editor脚本为组件添加按钮。可以使用EditorGUILayout.Button
在自定义Editor中为组件添加按钮。 - 使用示例:
- 描述: Unity本身没有原生的
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
MyComponent myComponent = (MyComponent)target;
if(GUILayout.Button("Reset Speed"))
{
myComponent.ResetSpeed();
}
}
}
- **效果**: 这会在Inspector窗口中为`MyComponent`脚本添加一个名为“Reset Speed”的按钮,点击该按钮时会调用`ResetSpeed`方法。
Unity的工作机制可以简洁地总结为以下几个关键点:
-
游戏对象(GameObject):Unity场景中的所有实体都是游戏对象,它们组成了游戏世界。
-
组件(Component):游戏对象的功能由附加在其上的组件定义,如Transform、Renderer、Collider等。
-
脚本(Script):用户可以编写脚本组件来定义游戏对象的行为和交互逻辑。
-
场景(Scene):游戏环境由一个或多个场景组成,每个场景都是一个独立的游戏关卡或地图。
-
资源(Asset):游戏中使用的所有资源,如模型、纹理、音频等,都作为资源导入并管理。
-
游戏循环(Game Loop):Unity通过不断执行游戏循环来更新游戏状态和渲染画面。
表格进一步说明Unity的游戏循环:
阶段 | 描述 |
---|---|
初始化(Initialization) | 加载资源,初始化游戏对象和组件。 |
输入事件处理(Input Events) | 处理用户输入事件,如键盘、鼠标、触摸等。 |
游戏逻辑更新(Game Logic Update) | 根据输入和游戏规则更新游戏对象的状态和行为。 |
物理模拟(Physics Simulation) | 执行物理引擎模拟,处理碰撞、力和运动。 |
动画更新(Animation Update) | 更新游戏对象的动画状态。 |
渲染(Rendering) | 将游戏世界渲染为可视图像。 |
帧结束(End of Frame) | 等待下一帧开始。 |
Unity通过这种循环机制,不断地处理输入、更新游戏状态、模拟物理、更新动画并渲染画面,从而创造出交互式的游戏体验。
阶段 | 函数/事件 | 说明 |
---|---|---|
编辑器阶段 | Awake() |
在场景加载时调用,所有对象的初始化都在这里进行,但不依赖其他对象的状态。 |
OnEnable() |
在对象被激活时调用,包括首次加载时以及每次激活时。 | |
游戏开始前 | Start() |
在游戏开始后第一次更新帧前调用,通常用于依赖其他对象初始化的操作。 |
游戏更新阶段 | Update() |
每帧调用一次,用于处理游戏逻辑的主要部分,例如输入检测和持续的状态更新。 |
FixedUpdate() |
每固定时间间隔调用一次,用于处理物理计算等与帧率无关的逻辑。 | |
LateUpdate() |
在每帧的最后调用,通常用于依赖其他对象已经更新完成的数据,例如摄像机跟随。 | |
渲染阶段 | OnPreRender() |
在摄像机开始渲染场景前调用,可以用于在渲染前的最后调整。 |
OnRenderObject() |
在摄像机渲染某个对象时调用,可以在渲染过程中执行自定义渲染命令。 | |
OnPostRender() |
在摄像机完成渲染后调用,通常用于在场景渲染结束后做最后的调整或特效处理。 | |
物理和碰撞阶段 | OnCollisionEnter() |
在碰撞发生的第一帧调用,用于处理物理碰撞事件。 |
OnCollisionStay() |
在持续碰撞期间每帧调用,用于处理持续的碰撞逻辑。 | |
OnCollisionExit() |
当碰撞结束时调用,用于处理碰撞结束后的逻辑。 | |
触发器阶段 | OnTriggerEnter() |
当一个对象进入触发器时调用。 |
OnTriggerStay() |
在对象停留在触发器内时每帧调用。 | |
OnTriggerExit() |
当对象离开触发器时调用。 | |
禁用和销毁阶段 | OnDisable() |
在对象被禁用或销毁之前调用,用于做清理工作。 |
OnDestroy() |
在对象销毁时调用,用于释放资源或做最后的清理。 |
- Awake 和 Start:这两个是初始化阶段的主要函数,
Awake
先于Start
被调用。 - Update 和 FixedUpdate:
Update
每帧调用,用于大部分游戏逻辑。FixedUpdate
是物理帧,用于物理相关的计算。 - LateUpdate:在
Update
之后调用,适合用来处理需要在其他更新之后再执行的逻辑。 - 渲染阶段:包含了渲染前后可以插入自定义逻辑的函数。
- 碰撞和触发器:分别处理物理碰撞和触发器事件。
- 禁用和销毁:用于处理对象被禁用或销毁时的清理工作。
在Unity中,继承自MonoBehaviour
的脚本可以在Awake
或Start
方法中进行初始化。选择哪个方法取决于初始化的需求和执行时机的不同:
Awake()
:该方法在脚本实例化后、场景加载时立即调用。适用于需要在其他组件的Start()
方法之前完成初始化的场景。Start()
:在所有Awake()
方法调用之后,并在第一次Update()
之前调用。适用于依赖于其他组件或对象的初始化逻辑。
注意:切记不要在继承自MonoBehaviour
的脚本中使用new
关键字创建实例!
此外,Unity中的所有生命周期函数都是在主线程中按顺序依次执行的,这确保了游戏逻辑的连贯性和同步性。
总结:
- 根据初始化的需求选择在
Awake()
或Start()
方法中进行操作。 - 不要在继承自
MonoBehaviour
的脚本中使用new
。 - 生命周期函数在主线程中按顺序执行,确保同步。
- 含义: 不要在
Awake
方法中调用其他方法,尤其是那些依赖于其他对象或组件状态的方法。 - 原因:
Awake()
是Unity中最早被调用的生命周期方法之一,通常用于初始化脚本本身的数据。此时,其他对象的Awake()
方法可能尚未被调用完成,因此,如果在Awake()
中调用依赖于其他对象状态的方法,可能会遇到未初始化或不稳定的状态。- 例如,如果你在
Awake()
中试图访问另一个对象或组件,而该对象或组件的Awake()
方法尚未执行完毕,你可能会遇到NullReferenceException
或者得到错误的数据。
- 适用范围: 这一准则不仅适用于
MonoBehaviour
,也适用于ScriptableObject
,因为这两种类型都可能涉及到复杂的依赖关系和初始化过程。
- 含义: 所有在
Start
方法中需要访问的状态应该在此时已安全、稳定,并且完全初始化。 - 原因:
Start()
方法在所有Awake()
方法执行完毕后被调用,此时场景中的所有对象和组件已经完成了初始化。因此,Start()
方法通常用于设置依赖于其他对象或组件的初始状态。- 通过确保所有状态在
Start()
时已准备就绪,你可以在Start()
中安全地调用方法并访问数据,而不会担心初始化过程的不完整性。
- 实践建议: 在编写代码时,确保
Start()
方法中的变量和状态都已在Awake()
或其他合适的初始化阶段完成初始化。例如,确保在Awake()
或更早阶段初始化了所有的组件引用或脚本实例。
- 含义: 检查在
Start
方法中初始化的所有变量的使用情况,确保在使用这些变量时它们已正确初始化。 - 原因:
- 如果某些变量在
Start()
中被初始化,而你在Start()
之前的其他方法(如Awake()
或构造函数)中尝试使用这些变量,可能会导致未初始化的问题。 - 为了防止未初始化的问题,建议你在
Start()
之前的任何方法中避免访问这些变量,或者明确检查变量是否已被正确初始化。
- 如果某些变量在
- 实践建议: 在开发过程中,可以使用工具或调试手段,确保在代码执行过程中不会访问未初始化的变量。
- 不要在
Awake
中调用依赖于其他对象或组件状态的方法,以确保初始化顺序的正确性。 - 所有状态在
Start
时都应该是安全和稳定的,以便你可以在Start
中安全地进行依赖于其他组件的初始化。 - 检查所有在
Start
中初始化的变量,确保它们不会在不合适的时机被访问。
- TransformPoint 考虑位置、旋转和缩放。
- TransformDirection 只考虑旋转。
- TransformVector 考虑旋转和缩放,但不考虑位置。(向量:在游戏开发中,向量常用来表示方向和距离,但不具有具体位置。)
- 用途:
TransformPoint
用于将一个点从局部坐标(相对于Transform自身的坐标)转换为世界坐标。 - 工作方式:此方法考虑了对象的位置、旋转和缩放。如果你有一个相对于某个对象的点的局部坐标,使用这个方法可以得到该点在世界坐标系中的确切位置。
- 示例:
Vector3 localPoint = new Vector3(1, 0, 0);
Vector3 worldPoint = transform.TransformPoint(localPoint);
如果 `transform` 位置是 (2, 0, 0),旋转是 0 度,缩放是 (1, 1, 1),那么 `worldPoint` 将是 (3, 0, 0)。
- 用途:
TransformDirection
用来将一个方向从局部坐标转换到世界坐标。 - 工作方式:它只考虑对象的旋转。这对于将局部方向向量(例如前方、上方)转换为世界坐标系中的方向向量非常有用,通常用于计算前进方向等。
- 示例:
Vector3 localDirection = new Vector3(0, 0, 1);
Vector3 worldDirection = transform.TransformDirection(localDirection);
如果 `transform` 的旋转使其朝向北方,那么 `worldDirection` 将指向北方。
- 用途:
TransformVector
用于将一个向量从局部空间转换到世界空间。 - 工作方式:此方法考虑对象的旋转和缩放,但不考虑位置。这在转换速度向量或其他不依赖于具体位置的向量时特别有用。
- 示例:
Vector3 localDirection = new Vector3(0, 0, 1);
Vector3 worldDirection = transform.TransformDirection(localDirection);
如果 `transform` 的缩放是 (2, 1, 1),那么 `worldVector` 将是 (2, 0, 0)(反映了缩放)。
这三个方法在Unity中的应用非常广泛,适用于不同的场景和需求,根据实际的使用场景选择合适的方法非常关键。
写几个我认为重点的知识
Rigidbody.interpolation
属性有三个选项:
- None(默认值):不进行插帧。这是最基本的设置,物体会按照物理引擎计算的位置和旋转进行更新。这在高帧率和稳定帧率下效果最好。
- Interpolate:使用插帧。这种模式下,物体的位置和旋转将会在当前帧和前一帧之间进行线性插值。这可以使物体的运动在低帧率或不稳定帧率下显得更加平滑。
- Extrapolate:使用外推。这种模式下,物体的位置和旋转将会基于当前帧和前一帧的速度进行外推。这可以使物体的运动在低帧率下显得更加自然和连贯。
Constraints
这个属性有两类约束:
- 位置约束(Freeze Position):控制物体在 x、y、z 轴上的移动。
- 旋转约束(Freeze Rotation):控制物体在 x、y、z 轴上的旋转。
碰撞检测方式 | 说明 | 游戏示例 |
---|---|---|
Discrete | 离散碰撞检测,每帧检测一次。适用于低速移动或静止的物体。 | 1. 益智游戏中的方块堆叠 2. 冒险游戏中的物品收集 |
Continuous | 连续碰撞检测,在物体移动路径上进行检测。适用于高速移动的物体。 | 1. 赛车游戏中的车辆碰撞 2. 射击游戏中的子弹碰撞 |
Continuous Dynamic | 连续动态碰撞检测,综合了 Discrete 和 Continuous 的优点。自动选择最优检测方式。 | 1. 物理益智游戏"Angry Birds" 2. 开放世界游戏"塞尔达传说:旷野之息"中的物理交互 |
Continuous Speculative | 连续推测碰撞检测,在 Continuous 的基础上增加了预测功能,提高了高速碰撞的精确度。 | 1. 格斗游戏中的高速攻击判定 2. 体育游戏如"FIFA"中的足球碰撞 |
静态摩擦力是指阻止两个物体之间相对静止状态被打破的摩擦力。当两个物体彼此接触但没有相对运动时,静态摩擦力起作用。静态摩擦力的大小一般大于动态摩擦力。
- 想象你试图推一辆静止的汽车。你需要用力到达一定程度才能让汽车开始移动。这个阻力就是静态摩擦力。
- 在Unity中,如果你有两个接触的物体,其中一个物体没有动,而你试图让它动,这时起作用的就是静态摩擦力。
动态摩擦力(也称为滑动摩擦力)是指阻止两个相对运动的物体之间滑动的摩擦力。当两个物体已经开始相对运动时,动态摩擦力起作用。动态摩擦力通常小于静态摩擦力。
你要有Collider或者是trigger,可以没有rigidbody,挂载脚本,就能响应OnCollider/OnTrigger 重点:如果是Rigidbody里面的子物体有脚本来检测是否碰撞,是不会触发的
rb.AddExplosionForce
是针对单个物体上的 Rigidbody
组件使用的,它将爆炸力作用于这个特定的物体。每个 Rigidbody
需要单独接受力的作用,因为每个物体可能会有不同的质量、阻尼等物理属性,这些属性会影响它们对力的响应。
动量定理 F⋅t=m⋅vF \cdot t = m \cdot vF⋅t=m⋅v 描述了力的作用时间和物体质量与其速度变化之间的关系:
- F:力
- t:时间
- m:质量
- v:速度
-
添加力 (AddForce)
- 世界坐标系:
- 添加力使对象沿世界坐标系的Z轴正方向移动:
rigidBody.AddForce(Vector3.forward * 10);
- 使对象沿自己的前方移动:
rigidBody.AddForce(this.transform.forward * 10);
- 添加力使对象沿世界坐标系的Z轴正方向移动:
- 本地坐标系:
- 相对于自身坐标的前方移动:
rigidBody.AddRelativeForce(Vector3.forward * 10);
- 相对于自身坐标的前方移动:
- 力的影响:无阻力时,对象将持续移动。
- 世界坐标系:
-
添加扭矩 (AddTorque)
- 世界坐标系:使对象旋转:
rigidBody.AddTorque(Vector3.up * 10);
- 本地坐标系:使对象相对于自身坐标旋转:
rigidBody.AddRelativeTorque(Vector3.up * 10);
- 世界坐标系:使对象旋转:
-
直接改变速度
- 速度的方向相对于世界坐标系:
rigidBody.velocity = Vector3.forward * 5;
- 速度的方向相对于世界坐标系:
-
模拟爆炸效果
- 对所有影响范围内的对象应用爆炸力:
rigidBody.AddExplosionForce(100, Vector3.zero, 10);
- 对所有影响范围内的对象应用爆炸力:
-
Acceleration
- 忽略物体质量,提供持续加速度:
rigidBody.AddForce(Vector3.forward * 10, ForceMode.Acceleration);
- 忽略物体质量,提供持续加速度:
-
Force
- 考虑物体质量的持续力:
v = 10 * 0.02 / 2 = 0.1m/s
(物体质量为2kg)
- 考虑物体质量的持续力:
-
Impulse
- 瞬间力,考虑物体质量:
v = 10 * 1 / 2 = 5m/s
(物体质量为2kg)
- 瞬间力,考虑物体质量:
-
VelocityChange
- 瞬时速度变化,忽略质量和时间:
v = 10 * 1 / 1 = 10m/s
- 瞬时速度变化,忽略质量和时间:
- 每物理帧的位移计算,例如,通过
VelocityChange
方式,速度为10m/s
,每帧 (0.02s) 的位移为:0.2m