15 KiB
一. 射线检测 (Ray Detection)
1. 射线对象 (Ray Object)
1.1 3D世界中的射线 (Ray in a 3D World)
射线是一条具有初始点和方向的直线(注意:方向不是终点)。 声明射线时,需要传入初始点和方向。
C#
// Ray(Vector3 origin, Vector3 direction)
Ray ray = new Ray(origin, direction);
// 示例:从原点 (0,0,0) 指向右方 (1,0,0) 的射线
Ray rayFromOriginToRight = new Ray(Vector3.zero, Vector3.right);
1.2 摄像机发出的射线 (Ray from Camera)
这是从屏幕上的一个点(例如鼠标位置)作为起点,沿着摄像机视口方向发出的一条射线。 其初始点即为摄像机位置,方向由屏幕坐标转换而来。
C#
// 从主摄像机发出,基于当前鼠标在屏幕上的位置
Ray rayFromCamera = Camera.main.ScreenPointToRay(Input.mousePosition);
注意: 单独的射线没有实际作用,需要结合射线检测才有意义。
2. 射线检测 (Raycasting)
2.1 最原始的射线检测 (Basic Raycast)
这种检测方法返回一个布尔值 (bool),用于判断射线是否碰撞到任何物体。
参数说明:
ray: 发出的射线。maxDistance: 检测的最大距离。layerMask: 指定检测的层级 (Layer)。queryTriggerInteraction: 是否与触发器 (Trigger) 交互。UseGlobal: 使用全局设置。Collide: 检测触发器。Ignore: 忽略触发器 (默认值,如果不填,则使用UseGlobal)。
C#
// 示例:检测名为 "Player" 的层级,最大距离1000,忽略触发器
if (Physics.Raycast(ray, 1000f, 1 << LayerMask.NameToLayer("Player"), QueryTriggerInteraction.Ignore))
{
// 射线碰到了 "Player" 层上的物体
}
2.2 获得相交的单个物体的信息 (Getting Information about a Single Hit)
此方法同样返回 bool 值,但能通过 out 参数获取碰撞到的单个物体的详细信息。
参数说明:
ray: 发出的射线。out RaycastHit hitInfo:out关键字表示此参数会传出碰撞信息。maxDistance: 检测的最大距离。layerMask: 指定检测的层级。queryTriggerInteraction: 是否与触发器交互。
C#
RaycastHit hit; // 用来存储碰撞信息
if (Physics.Raycast(ray, out hit, 1000f, 1 << LayerMask.NameToLayer("Enemy"), QueryTriggerInteraction.Collide))
{
// 射线碰到了物体,碰撞信息存储在 'hit' 中
// hit.collider: 碰到的碰撞体
// hit.point: 碰撞点坐标
// hit.normal: 碰撞点处的法线向量
// hit.distance: 射线起点到碰撞点的距离
// GameObject hitObject = hit.collider.gameObject; // 获取碰撞到的游戏对象
}
2.3 获得相交的多个物体 (Getting Information about Multiple Hits)
此方法返回一个 RaycastHit 类型的数组。如果射线没有碰撞到任何物体,则数组长度为0。
参数说明:
ray: 发出的射线。maxDistance: 检测的最大距离。layerMask: 指定检测的层级。queryTriggerInteraction: 是否与触发器交互。
C#
RaycastHit[] hits;
hits = Physics.RaycastAll(ray, 1000f, 1 << LayerMask.NameToLayer("Obstacle"));
if (hits.Length > 0)
{
foreach (RaycastHit hit in hits)
{
// 处理每一个碰撞到的物体
// Debug.Log("Hit: " + hit.collider.name);
}
}
RaycastHit结构体: 在射线检测中非常关键。它记录了大量信息,例如:
collider: 射线命中的碰撞体,通过它可以获取整个游戏对象。point: 射线检测到的碰撞点。normal: 射线与表面相交点的法线向量。distance: 射线起点到相交点的距离。
二. 范围检测 (Overlap Detection)
范围检测是在一个固定的区域内,检测其中所有携带碰撞体 (Collider) 的物体。 Unity 中主要有三种范围检测:
- 盒状范围检测 (
OverlapBox) - 球形范围检测 (
OverlapSphere) - 胶囊体范围检测 (
OverlapCapsule)
这些方法都存储在 Physics 类中,参数大致相同,只是根据形状不同,需要传入不同的点来构造形状。
1. 盒状范围检测 (OverlapBox)
Physics.OverlapBox()
参数说明:
center: 立方体的中心点。halfExtents: 立方体三个轴向的半尺寸 (大小的一半)。orientation: 立方体的旋转角度 (四元数Quaternion)。layerMask: 检测指定层级。queryTriggerInteraction: 是否忽略触发器。
返回值: 范围内的所有碰撞体 (Collider[]) 数组。
重点参数四:检测指定层级 (LayerMask)
Unity 中的 Layer共有32个 (0-31),这刚好可以用一个32位的二进制数来表示。 0000 0000 0000 0000 0000 0000 0000 0000
使用 1 << LayerMask.NameToLayer("LayerName") 创建层掩码: LayerMask.NameToLayer("LayerName") 函数会返回指定层名称在 Layer 中的索引序号。 将数字 1 左移这个索引号的位数,会在32位数字中得到一个全新的数字。例如,如果 "UI" 层是第 5 层 (索引为5): 1 << 5 结果为 0000 0000 0000 0000 0000 0000 0010 0000 (第5位为1)
在这个32位的二进制掩码中,1 代表需要检测该层级,0 代表不需要。
检测多个层级: 使用按位或 (|) 操作可以方便地组合多个层级进行检测。 例如,要检测第0层、第2层和第5层: 0000 0000 0000 0000 0000 0000 0010 0101 (二进制表示)
C#
int layerMask = (1 << LayerMask.NameToLayer("第一层")) |
(1 << LayerMask.NameToLayer("第二层")) |
(1 << LayerMask.NameToLayer("第五层"));
// 或者,如果知道层索引:
// int layerMask = (1 << 0) | (1 << 2) | (1 << 5);
位运算在 LayerMask 中的应用
-
按位与 (
&):- 规则:两个位都为
1时,结果才为1。 - 用途:检测某个特定层是否存在于当前的层掩码中。
- 示例:
- 掩码:
0000 ... 0010 0101(检测0, 2, 5层) - 待检测层 (例如第0层):
0000 ... 0000 0001(1 << 0) (掩码 & 待检测层)结果不为0,证明该层在掩码中。
- 掩码:
- 规则:两个位都为
-
按位或 (
|):- 规则:只要有一个位为
1,结果就为1。 - 用途:将多个层添加到层掩码中。
- 规则:只要有一个位为
-
按位异或 (
^):- 规则:两个位相同为
0,不同为1。 - 用途:动态切换某个层级的检测状态(添加或移除)。
- 示例:
- 原始掩码 (检测第1层和第3层):
0000 ... 1010 - 操作层 (第3层):
0000 ... 1000 原始掩码 ^ 操作层结果:0000 ... 0010(第3层的状态被翻转,现在只检测第1层)
- 原始掩码 (检测第1层和第3层):
- 规则:两个位相同为
Layer 和 LayerMask 的区别是什么?
- Layer: 指的是 Unity 编辑器中为 GameObject 分配的单个层级(例如 "Player", "Enemy", "UI")。每个 GameObject 只能属于一个 Layer。Layer 本身是一个整数索引 (0-31)。
- LayerMask: 是一个32位的整数,用作位掩码 (bitmask)。它的每一位对应一个 Layer。通过设置 LayerMask 中的特定位为1,可以指定射线检测、范围检测等操作应该作用于哪些 Layer。一个 LayerMask 可以同时代表多个 Layer。
三. RPG游戏学习 (RPG Game Study)
1. 导航系统 (Navigation System)
- (内容待补充)
2. 鼠标点击, 角色移动 (Mouse Click, Character Movement)
- (内容待补充)
四. 对象池技术 (Object Pooling)
目前构造的对象池比较简单。这个对象池是一个单例类。我们需要明确:
- 对象池存储的物体是什么类型。
- 对象池存储的物体初始个数是多少。
然后构造一个队列 (Queue) 来管理这些对象。
一个典型的对象池实现:
- 在
Awake()方法中完成单例类的构造。 - 实现三个核心方法:
- 初始化填充对象池: 在开始时创建并存储一定数量的对象。
- 从对象池中获取对象: 当需要对象时,从池中取出一个。
- 将对象返回对象池: 当对象不再使用时,将其返还给池中以备复用。
五. LineRenderer (画线组件)
LineRenderer 组件允许在 Unity 场景中绘制线段。它主要包含编辑器设置和代码控制两大部分。代码控制是重点,通常在脚本中动态实现。
1. 编辑器部分 (Editor Properties)
(图片引用)
- Loop: 线段的开头和结束是否闭合形成循环。
- Positions: 控制线段的顶点个数和每个顶点的坐标。
- Width: 线段的粗细 (可以是一个曲线,使粗细沿长度变化)。
- Color: 线段的颜色 (可以使用渐变色)。
- Corner Vertices: 角顶点个数。数值越大,线段转角处越圆润。
- End Cap Vertices: 末端顶点个数。数值越大,线段末端越圆润。
- Alignment: 对齐方式 (View, TransformZ)。
- Texture Mode: 纹理模式 (Stretch, Tile, DistributePerSegment, Shape)。
- Generate Lighting Data: 是否生成光照数据,即光照是否对线段产生影响。
- Use World Space: 是否使用世界坐标。
- 若勾选,移动 LineRenderer 所在的 GameObject 时,线段本身不会随之移动。
- 若不勾选 (使用本地坐标),线段会随 GameObject 移动。
2. 代码部分 (Scripting)
首先,需要获取 LineRenderer 组件的引用。
C#
LineRenderer lineRenderer;
void Start()
{
lineRenderer = GetComponent<LineRenderer>();
}
常用代码操作:
-
首尾相连 (Looping):
C#
lineRenderer.loop = true; // 或者 false -
设置开始和结束宽度:
C#
lineRenderer.startWidth = 0.1f; lineRenderer.endWidth = 0.5f; // 也可以使用曲线来控制宽度 // lineRenderer.widthCurve = new AnimationCurve(new Keyframe(0, 0.1f), new Keyframe(1, 0.5f)); -
设置开始和结束颜色:
C#
lineRenderer.startColor = Color.red; lineRenderer.endColor = Color.blue; // 也可以使用 Gradient 来控制颜色 // Gradient gradient = new Gradient(); // gradient.SetKeys( // new GradientColorKey[] { new GradientColorKey(Color.red, 0.0f), new GradientColorKey(Color.blue, 1.0f) }, // new GradientAlphaKey[] { new GradientAlphaKey(1.0f, 0.0f), new GradientAlphaKey(1.0f, 1.0f) } // ); // lineRenderer.colorGradient = gradient; -
设置材质 (Material):
C#
public Material lineMaterial; // 在 Inspector 中指定 // ... lineRenderer.material = lineMaterial; -
设置顶点 (Positions):
-
设置顶点数量:
C#
lineRenderer.positionCount = 2; // 例如,一条直线需要2个点 -
设置单个顶点坐标:
C#
// lineRenderer.SetPosition(int index, Vector3 position); lineRenderer.SetPosition(0, new Vector3(0, 0, 0)); lineRenderer.SetPosition(1, new Vector3(1, 1, 0)); -
通过数组一次性设置所有顶点坐标:
C#
Vector3[] positions = new Vector3[] { new Vector3(0, 0, 0), new Vector3(1, 1, 0), new Vector3(2, 0, 0) }; lineRenderer.SetPositions(positions);
-
六. 四元数 (Quaternions)
- (内容待补充,用于表示旋转)
七. 组件和物体的启用和禁用 (Component and GameObject Enabling/Disabling)
1. GameObject 的启用和禁用
1.1 激活 GameObject
C#
// 'this' 指向当前脚本所在的 GameObject
this.gameObject.SetActive(true);
1.2 禁用 GameObject
C#
this.gameObject.SetActive(false);
1.3 查看 GameObject 状态
C#
// 自身是否被设置为激活 (不受父物体影响)
bool isActiveSelf = gameObject.activeSelf;
// 在层级视图中实际的激活状态 (受父物体影响)
bool isActiveInHierarchy = gameObject.activeInHierarchy;
1.4 activeSelf 和 activeInHierarchy 的区别
gameObject.activeSelf: 表示该 GameObject 自身是否被设置为激活状态。如果它被设置为false,即使其所有父对象都激活,它依然是非激活的。gameObject.activeInHierarchy: 表示该 GameObject 在场景中是否真实处于激活状态。如果它自身activeSelf为true,但其任何一个父对象activeSelf为false(导致父对象activeInHierarchy为false),那么该 GameObject 的activeInHierarchy也会是false。
2. 组件的启用和禁用
2.1 激活组件
C#
// 对于当前脚本组件本身
this.enabled = true;
// 对于其他组件 (例如 Collider),需要先获取其引用
Collider myCollider = this.GetComponent<Collider>();
if (myCollider != null)
{
myCollider.enabled = true;
}
2.2 禁用组件
C#
// 对于当前脚本组件本身
this.enabled = false;
// 对于其他组件
Collider myCollider = GetComponent<Collider>();
if (myCollider != null)
{
myCollider.enabled = false;
}
2.3 检查组件的启用状态
C#
// 当前脚本组件的状态
bool isScriptEnabled = this.enabled;
// 其他组件的状态
Collider myCollider = GetComponent<Collider>();
bool isColliderEnabled = false;
if (myCollider != null)
{
isColliderEnabled = myCollider.enabled;
}
3. 相关的生命周期事件 (Lifecycle Events)
3.1 组件相关的生命周期方法:
C#
void OnEnable()
{
// 当组件被启用时调用。
// 这也包括其所在的 GameObject 从非激活状态变为激活状态时。
// 注意: 如果 GameObject 首次激活,OnEnable 会在 Awake 和 Start 之后(对于脚本而言)或之前(对于某些内置组件)被调用,
// 具体取决于脚本执行顺序和组件类型。通常在 Awake 之后,Start 之前或之后。
// 严格来说,是 Awake -> OnEnable -> Start
}
void OnDisable()
{
// 当组件被禁用时调用。
// 这也包括其所在的 GameObject 从激活状态变为非激活状态时。
// 不会触发 OnDestroy()。
}
3.2 GameObject 相关的生命周期影响:
- 调用
gameObject.SetActive(true):- 会触发该 GameObject 及其所有激活的子物体上所有激活组件的
OnEnable()方法。 - 如果该 GameObject 是首次被激活 (之前从未激活过或被实例化后首次激活),还会先调用其上所有组件的
Awake(),然后是OnEnable(),接着是Start()。
- 会触发该 GameObject 及其所有激活的子物体上所有激活组件的
- 调用
gameObject.SetActive(false):- 会触发该 GameObject 及其所有激活的子物体上所有激活组件的
OnDisable()方法。 - 不会触发
OnDestroy()。
- 会触发该 GameObject 及其所有激活的子物体上所有激活组件的
八. MonoBehaviour 中的重要内容和 GameObject 的重要内容 (Key Aspects of MonoBehaviour and GameObject)
- (内容待补充)