初始化obs/Unity仓库
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
1.单例模式(懒汉式 饿汉式)
|
||||
![[Pasted image 20250514200735.png]]
|
||||
2.属性和字段的区别,属性get set里面如何书写
|
||||
3.字典的使用
|
||||
4.继承关系当中virtual和override对方法的影响
|
||||
5.setparent的两个参数,尤其是第二个对UI的影响
|
||||
6.算法:二分法,移除元素
|
||||
7.ref和out的用处和区别
|
||||
@@ -0,0 +1,6 @@
|
||||
### 1. c#当中,只读属性的本质
|
||||
在LoginManager当中,我们封装了loginData,将他设置为一个只读属性
|
||||
public LoginData LoginData => loginData;(这个写法是表达式体属性)
|
||||
照理说我们不能对其进行修改,但是后续发现,我们可以修改LoginData里面的内容
|
||||
这是因为,只读属性,只是确保当前引用不会被改变,也就是栈当中的地址不会发生改变,但是栈指向的堆内存当中的数据可以修改,也就是说==只读属性只是保护了引用不被改变,而不是引用指向的对象内容。==
|
||||
但如果属性封装的是一个值类型,那么因为值类型的数据存储在栈内存当中,所以值类型完全无法修改.
|
||||
@@ -0,0 +1,26 @@
|
||||
1.Unity当中的样条线Spline
|
||||
spline.EvaluatePosition 获得样条线上某个点的位置
|
||||
spline.EvaluateTangent 获得样条线上某个点的切线
|
||||
spline.EvaluateUpVector 获得与切线垂直的一个朝向 z轴
|
||||
Quaternion.LookRotation(x,y)
|
||||
将物体的z轴对齐x参数,y轴对齐y参数.
|
||||
2.场景加载
|
||||
3.资源加载
|
||||
4.Transform
|
||||
transform.position是相对于世界坐标的
|
||||
transform.localposition是相对于父位置的
|
||||
5.抽象类和接口的区别
|
||||
6.类和结构体的区别
|
||||
7.string和stringbuilder的区别
|
||||
8.动画帧事件的添加,他的脚本挂载在Animatior的物体上
|
||||
Animator挂载在哪个物体上,脚本就挂载在哪个物体上,这样才能调用
|
||||
### 9.ref和out的区别
|
||||
- ref在使用之前必须赋值,在内部可以修改,也可以不修改. out在使用之前不需要赋值,在内部必须修改,否则出错. 同时你无法在方法内部,访问out参数的值
|
||||
- ![[Pasted image 20250525175247.png]]
|
||||
所以图中内容会出错,因为他访问了out参数.
|
||||
### 10.单例模式的写法
|
||||
1.在普通脚本当中
|
||||
![[Pasted image 20250525181319.png]]
|
||||
2.在继承mono的脚本当中
|
||||
![[Pasted image 20250525181358.png]]
|
||||
继承自mono的脚本不可以被new出来,因此我们不能和普通脚本一样new,需要在awake当中,将自己赋值给instance;
|
||||
@@ -0,0 +1,44 @@
|
||||
[相机控制]
|
||||
今天学习的是MMO的15节课摄像机
|
||||
通过多层结构控制摄像机的移动缩放,并且适配安卓平台的手指旋转缩放
|
||||
### 第三人称相机控制
|
||||
在新版本基本上是使用cinemamachine来控制相机的移动,在15年的时候并没有这个插件,因此教程使用多层结构来手搓了一个第三人称相机控制器
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Transform的一些知识
|
||||
|
||||
当你在一个 `Transform` 对象上使用 `foreach` 循环时,你实际上是在遍历它在场景层级 (Hierarchy) 中的所有**子对象 (child objects)**。
|
||||
|
||||
可以把 `pellets` 这个 `Transform` 想象成一个父容器,它下面可以挂载很多子物体。`foreach` 循环会逐一访问它的每一个子物体。
|
||||
|
||||
**在你的代码中:**
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
public Transform pellets; // 你会在Inspector中把一个父物体(比如一个空对象)拖到这里,这个父物体下包含了所有的豆子。
|
||||
|
||||
// ...
|
||||
|
||||
public void NewRound()
|
||||
{
|
||||
// 这个循环会遍历 'pellets' 这个Transform下的每一个子物体的Transform组件
|
||||
foreach (Transform pellet in pellets)
|
||||
{
|
||||
// 'pellet' 在每次循环中,都是一个子物体的 Transform
|
||||
// 比如,你可以在这里重新激活每一个豆子
|
||||
pellet.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**总结一下:**
|
||||
|
||||
- **不是数组**:`pellets` 是单个 `Transform` 组件。
|
||||
- **可以遍历**:因为它被设计成可以像集合一样,让你能方便地访问其所有的子物体。
|
||||
- **遍历内容**:循环遍历的是它所有子物体的 `Transform`。
|
||||
|
||||
这是一个非常有用的 Unity 设计,可以让你很方便地管理一组相关的游戏对象(比如所有的敌人、所有的金币、或者你代码里所有的豆子)。
|
||||
@@ -0,0 +1,33 @@
|
||||
### 1.ransform旋转
|
||||
直接修改transform.rotation以及使用方法transform.rotate的区别是什么?
|
||||
修改transform.rotation是直接控制旋转到一个目标量
|
||||
使用方法transform.rotate是控制物体旋转增加一个目标量.
|
||||
比如当前是60,我们的目标量是30,我们修改transform.rotation之后会变成30,但是使用方法会变成90.
|
||||
### 2.坐标系
|
||||
==在不同的canvas设置模式下,以下三种略有不同==
|
||||
1.recttransform.position 在覆盖模式下,就相当于屏幕坐标,左下角是00
|
||||
2.rt.localPosition 是子物体相对于父物体的坐标,他们是根据轴心点来计算的
|
||||
3.rt.anchoredPosition 是自身中心点距离锚点的坐标,也是inspector中的坐标.
|
||||
|
||||
但是如过是在canvas中的摄像机模式下第一种情况会发生变化
|
||||
recttransform.position在摄像机模式下,坐标就是世界坐标系中的坐标.
|
||||
|
||||
在世界模式下,以上内容就相当于是一个游戏物体,等同于transform.
|
||||
|
||||
补充:真实屏幕像素 = anchoredPosition × canvas.scaleFactor
|
||||
所以anchoredPosition=真实屏幕像素/canvas.scaleFactor
|
||||
这和我们的缩放模式息息相关,只有在缩放模式等于我们的参考分辨率的时候,我们的anchoredPosition和我们的像素才是一样的。
|
||||
在分辨率不同的情况下,都是重新计算过的。
|
||||
|
||||
### 3.Mask遮罩
|
||||
遮罩的内容比较简单
|
||||
我们需要在父物体上放置Mask组件,之后==透明区域会遮挡子物体,不透明区域会显示物体.==
|
||||
值得注意的有两点:
|
||||
1.子物体需要勾选Maskable
|
||||
2.父物体如过想显示自身sprite,需要勾选Mask组件的 Show mask Graphic
|
||||
|
||||
### 4.屏幕坐标转UI相对坐标
|
||||
本内容应该同[[#2.坐标系]]相联系.
|
||||
|
||||
### 5.摄像机Clear Flags参数
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#### 1.Toogle Group组件
|
||||
allow switch off 的作用是什么
|
||||
#### 2.Shadow和Outline组件
|
||||
use graphic alpha的作用是什么
|
||||
|
||||
#### 3.Toogle 组件
|
||||
toggle transition的作用是什么
|
||||
|
||||
#### 4.InputField组件
|
||||
placeholder怎么翻译
|
||||
caret怎么翻译
|
||||
custom caret color什么作用
|
||||
selection color什么作用
|
||||
hide mobile input什么作用
|
||||
should activate on select什么作用
|
||||
|
||||
#### 5.Scroll View(Scroll Rect组件)
|
||||
1.Inertia的作用是什么
|
||||
2.Deceleration Rate的作用是什么
|
||||
3.Scroll Sensitivity的作用是什么
|
||||
4.On Value Changed(Vector2)当值改变的时候的这个值是什么?
|
||||
# 延迟函数
|
||||
### 1.invoke 有两个参数,第一个传入string类型的方法名字,第二个参数是延时多久
|
||||
注意事项:
|
||||
1.延迟函数只能调用无参函数
|
||||
2.延迟函数只能调用当前类中的函数
|
||||
想要解决以上问题,只需要包裹一层方法即可.
|
||||
### 2.重复延迟函数 invokerepeating
|
||||
有三个参数,第一个传入string类型的方法名字,第二个参数是延时多久,第三个是之后间隔多久执行.
|
||||
#### 如何取消重复延迟函数
|
||||
cancelInvoke
|
||||
不输入参数则取消所有,输入参数则定向取消某一个.
|
||||
#### 如何判断有延迟函数
|
||||
isInvoking()
|
||||
### 3.对象失活和销毁对延时函数的影响
|
||||
对象失活,脚本依然存在,延时函数可以执行
|
||||
对象摧毁,脚本同样摧毁,延时函数不可以执行.
|
||||
|
||||
#### 4.destroy也可以延时销毁
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
### UGUI打图集
|
||||
打图集的目的是为了减少DC(Draw Call)可以提高性能,打图集之后可以将n次的DC变成一个图集的DC来提高性能.
|
||||
我们需要在Edit-Projecting setting-Editor-Sprite Packer其中有四种模式
|
||||
|
||||
- Sprite Atlas V1 - Enable For Builds
|
||||
- Sprite Atlas V1 - Always Enabled
|
||||
- Sprite Atlas V2 - Enabled
|
||||
- Sprite Atlas V2 - Enabled for builds
|
||||
总的来说可以从两个标准分为两类
|
||||
从版本角度,有V1 V2两种,V2是当前版本最新的,V1是旧版本的图集.
|
||||
从图集制作的角度,分为在builds的时候才可以打图集(Enable For Builds),以及任何时候都可以打图集(Always Enabled).
|
||||
@@ -0,0 +1,485 @@
|
||||
# 一. 射线检测 (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`),用于判断射线是否碰撞到任何物体。
|
||||
|
||||
**参数说明:**
|
||||
|
||||
1. `ray`: 发出的射线。
|
||||
2. `maxDistance`: 检测的最大距离。
|
||||
3. `layerMask`: 指定检测的层级 (Layer)。
|
||||
4. `queryTriggerInteraction`: 是否与触发器 (Trigger) 交互。
|
||||
- `UseGlobal`: 使用全局设置。
|
||||
- `Collide`: 检测触发器。
|
||||
- `Ignore`: 忽略触发器 (默认值,如果不填,则使用 `UseGlobal`)。
|
||||
|
||||
<!-- end list -->
|
||||
|
||||
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` 参数获取碰撞到的单个物体的详细信息。
|
||||
|
||||
**参数说明:**
|
||||
|
||||
1. `ray`: 发出的射线。
|
||||
2. `out RaycastHit hitInfo`: `out` 关键字表示此参数会传出碰撞信息。
|
||||
3. `maxDistance`: 检测的最大距离。
|
||||
4. `layerMask`: 指定检测的层级。
|
||||
5. `queryTriggerInteraction`: 是否与触发器交互。
|
||||
|
||||
<!-- end list -->
|
||||
|
||||
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。
|
||||
|
||||
**参数说明:**
|
||||
|
||||
1. `ray`: 发出的射线。
|
||||
2. `maxDistance`: 检测的最大距离。
|
||||
3. `layerMask`: 指定检测的层级。
|
||||
4. `queryTriggerInteraction`: 是否与触发器交互。
|
||||
|
||||
<!-- end list -->
|
||||
|
||||
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()`
|
||||
|
||||
**参数说明:**
|
||||
|
||||
1. `center`: 立方体的中心点。
|
||||
2. `halfExtents`: 立方体三个轴向的半尺寸 (大小的一半)。
|
||||
3. `orientation`: 立方体的旋转角度 (四元数 `Quaternion`)。
|
||||
4. `layerMask`: **检测指定层级**。
|
||||
5. `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层)
|
||||
|
||||
### 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) 来管理这些对象。
|
||||
|
||||
一个典型的对象池实现:
|
||||
|
||||
1. 在 `Awake()` 方法中完成单例类的构造。
|
||||
2. 实现三个核心方法:
|
||||
- **初始化填充对象池**: 在开始时创建并存储一定数量的对象。
|
||||
- **从对象池中获取对象**: 当需要对象时,从池中取出一个。
|
||||
- **将对象返回对象池**: 当对象不再使用时,将其返还给池中以备复用。
|
||||
|
||||
---
|
||||
|
||||
# 五. LineRenderer (画线组件)
|
||||
|
||||
`LineRenderer` 组件允许在 Unity 场景中绘制线段。它主要包含编辑器设置和代码控制两大部分。代码控制是重点,通常在脚本中动态实现。
|
||||
|
||||
## 1. 编辑器部分 (Editor Properties)
|
||||
|
||||
_(图片引用)_
|
||||
|
||||
1. **Loop**: 线段的开头和结束是否闭合形成循环。
|
||||
2. **Positions**: 控制线段的顶点个数和每个顶点的坐标。
|
||||
3. **Width**: 线段的粗细 (可以是一个曲线,使粗细沿长度变化)。
|
||||
4. **Color**: 线段的颜色 (可以使用渐变色)。
|
||||
5. **Corner Vertices**: 角顶点个数。数值越大,线段转角处越圆润。
|
||||
6. **End Cap Vertices**: 末端顶点个数。数值越大,线段末端越圆润。
|
||||
7. **Alignment**: 对齐方式 (View, TransformZ)。
|
||||
8. **Texture Mode**: 纹理模式 (Stretch, Tile, DistributePerSegment, Shape)。
|
||||
9. **Generate Lighting Data**: 是否生成光照数据,即光照是否对线段产生影响。
|
||||
10. **Use World Space**: 是否使用世界坐标。
|
||||
- 若勾选,移动 LineRenderer 所在的 GameObject 时,线段本身不会随之移动。
|
||||
- 若不勾选 (使用本地坐标),线段会随 GameObject 移动。
|
||||
|
||||
## 2. 代码部分 (Scripting)
|
||||
|
||||
首先,需要获取 `LineRenderer` 组件的引用。
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
LineRenderer lineRenderer;
|
||||
|
||||
void Start()
|
||||
{
|
||||
lineRenderer = GetComponent<LineRenderer>();
|
||||
}
|
||||
```
|
||||
|
||||
**常用代码操作:**
|
||||
|
||||
1. **首尾相连 (Looping)**:
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
lineRenderer.loop = true; // 或者 false
|
||||
```
|
||||
|
||||
2. **设置开始和结束宽度**:
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
lineRenderer.startWidth = 0.1f;
|
||||
lineRenderer.endWidth = 0.5f;
|
||||
// 也可以使用曲线来控制宽度
|
||||
// lineRenderer.widthCurve = new AnimationCurve(new Keyframe(0, 0.1f), new Keyframe(1, 0.5f));
|
||||
```
|
||||
|
||||
3. **设置开始和结束颜色**:
|
||||
|
||||
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;
|
||||
```
|
||||
|
||||
4. **设置材质 (Material)**:
|
||||
|
||||
C#
|
||||
|
||||
```
|
||||
public Material lineMaterial; // 在 Inspector 中指定
|
||||
// ...
|
||||
lineRenderer.material = lineMaterial;
|
||||
```
|
||||
|
||||
5. **设置顶点 (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.SetActive(false)`:
|
||||
- 会触发该 GameObject 及其所有激活的子物体上所有激活组件的 `OnDisable()` 方法。
|
||||
- **不会**触发 `OnDestroy()`。
|
||||
|
||||
---
|
||||
|
||||
# 八. MonoBehaviour 中的重要内容和 GameObject 的重要内容 (Key Aspects of MonoBehaviour and GameObject)
|
||||
|
||||
- (内容待补充)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,49 @@
|
||||
![[Pasted image 20241218120120.png]]
|
||||
|
||||
RigidBody组件是Unity引擎内置的物理组件,他的存在可以使得GameObject拥有物理属性.但没有物理形状,形状有Collider控制
|
||||
从上往下以此讲解该组件的全部属性.
|
||||
### Mass
|
||||
物体的质量,不影响下落速度
|
||||
### Drag和AngularDrag
|
||||
这两个均为阻力,线性阻力和角阻力
|
||||
### UseGravity
|
||||
是否启用重力,启用之后物体收到重力影响
|
||||
### IsKinematic
|
||||
是否启用运动学,启用之后物体不受到物理引擎的运动学效果
|
||||
例如:
|
||||
1.不会与其他物体产生碰撞反应
|
||||
2.不会有重力,力,冲量的影响.
|
||||
但是可以通过脚本来驱动该物体的运行.
|
||||
### InterPolate
|
||||
![[Pasted image 20241219223339.png]]
|
||||
展开后为上图所示
|
||||
该选项的作用是是否启用插值可以选择提前插值,也可以选择延后插值,
|
||||
具体效果可以修复游戏在游玩时候的卡顿过程.
|
||||
注意:有时候游戏在引擎当中游玩不会有卡顿,在Built之后会有卡顿.
|
||||
### Collision Detection 碰撞检测
|
||||
|
||||
![[Pasted image 20241219223512.png]]
|
||||
一共有4种选择,他们分别是离散检测,持续检测,动态持续检测,推断性检测
|
||||
#### Discrete (离散检测)
|
||||
该检测的频率与FixUpdata相同,因此适用于大部分的场合
|
||||
#### Continuous(连续模式)
|
||||
==一个动态物体和一个静态物体(没有刚体的碰撞器)之间可以使用==
|
||||
该模式修复了离散模式在物体速度过快时出现的穿墙现象,优点显著
|
||||
但是缺点同样显著,其一是系统开销大
|
||||
缺点之二是该方法是通过撞击时间 (TOI) 算法,通过扫掠对象的前向轨迹来计算对象的潜在碰撞(采用对象的当前速度).因此会忽视物体的角运动,导致物体可能在侧边会出现穿墙现象.
|
||||
#### Continuous Dynamic(连续动态模式)
|
||||
类似于Continuous,但是==支持高速移动的物体和其他动态物体的检测==,系统开销最大.
|
||||
#### Continuous Speculative(连续推测模式)
|
||||
系统开销介于离散模式和连续模式之间.
|
||||
同样基于预测,可以防止物体穿墙现象.
|
||||
可以避免Continuous(连续模式)中忽视角运动侧边穿墙的问题.
|
||||
具体原理:https://docs.unity.cn/cn/2022.3/Manual/ContinuousCollisionDetection.html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 182 KiB |
@@ -0,0 +1,128 @@
|
||||
对于碰撞器而言,基本的碰撞器形状,碰撞器的作用不必多谈,这里着重强调的是碰撞器和触发器,以及他们碰撞和触发的三种状态.
|
||||
### Enter Stay Exit
|
||||
以上三种状态表示了物体碰撞刚刚进入的一帧,物体持续碰撞,和物体离开碰撞的一帧
|
||||
他们分别对应了代码当中的
|
||||
OnCollisionEnter
|
||||
OnCollisionStay
|
||||
OnCollisionExit
|
||||
以及触发器的
|
||||
OnTriggerEnter
|
||||
OnTriggerStay
|
||||
OnTriggerExit
|
||||
他们的用法相同,区别在于方法传入的参数不同
|
||||
碰撞器提供的参数类型是Collision
|
||||
触发器提供的参数类型是Collider
|
||||
### 筛选
|
||||
我们需要在碰撞器和触发器内部进行许多逻辑上的处理,这里有一个关键是如何筛选物体
|
||||
比如NPC和玩家同时触发开门触发器,我们只希望玩家触发时才会开门,需要进行筛选.
|
||||
常见的筛选方法有2种: 签名. 层
|
||||
#### 签名
|
||||
由于为我们提供了参数,我们可以获取到触发的物体,所以我们可以使用签名进行筛选
|
||||
譬如:
|
||||
`if (collision.gameObject.tag==("Player"))`
|
||||
`{`
|
||||
|
||||
`}`
|
||||
`if (collision.gameObject.CompareTag("Tag"))`
|
||||
`{`
|
||||
|
||||
`}`
|
||||
二种方式均可,其中第二种方式的效率更高,因为:
|
||||
Unity将标签单独储存在一个数据结构当中,CompareTag方法可以直接访问该数据结构效率更高.
|
||||
#### 层
|
||||
在Unity当中,层被储存为整数,因此使用层进行筛选的效率比Tag高.
|
||||
层的筛选也需要分为筛选特定的单层,和多层
|
||||
比如你需要玩家单独可以开门,那么就可以使用单层筛选
|
||||
但是如过你想要多个物体都可以开门,那么就需要多层
|
||||
#### 单层检测
|
||||
![[Pasted image 20241219231058.png]]
|
||||
#### 多层检测
|
||||
|
||||
![[Pasted image 20241219231128.png]]
|
||||
|
||||
### **1. 关键概念**
|
||||
|
||||
#### **Layer 和 LayerMask**
|
||||
|
||||
- **Layer(层)**: Unity 中的每个 `GameObject` 都可以设置一个 `Layer`,用于分组、筛选或逻辑处理。每个层的编号是从 `0` 到 `31` 的整数。
|
||||
|
||||
- **LayerMask(层掩码)**: `LayerMask` 是一个 32 位的整型数,每一位(bit)对应一个 Layer。如果某一位是 `1`,则表示该层被包含在掩码中;如果是 `0`,则表示不包含。
|
||||
|
||||
例如:
|
||||
|
||||
- 如果 `LayerMask` 的值是 `5`(即二进制 `00000000 00000000 00000000 00000101`),表示包含第 0 层和第 2 层。
|
||||
- 具体映射:
|
||||
- 第 0 位 = 1 → 包含第 0 层
|
||||
- 第 1 位 = 0 → 不包含第 1 层
|
||||
- 第 2 位 = 1 → 包含第 2 层
|
||||
|
||||
---
|
||||
|
||||
### **2. 拆解逻辑**
|
||||
|
||||
#### **(1) `collision.gameObject.layer`**
|
||||
|
||||
- 获取发生碰撞的对象的层编号(一个整数,例如 `0`、`1`、`2` 等)。
|
||||
|
||||
#### **(2) `(1 << collision.gameObject.layer)`**
|
||||
|
||||
- **位移操作 `<<`**: `1 << x` 表示将 `1` 左移 `x` 位。例如:
|
||||
|
||||
- `1 << 0` → `00000000 00000000 00000000 00000001` (表示第 0 层)
|
||||
- `1 << 2` → `00000000 00000000 00000000 00000100` (表示第 2 层)
|
||||
|
||||
**结果:** 这个操作生成一个位掩码,只有对应层的位置是 `1`,其余位置是 `0`。
|
||||
|
||||
|
||||
#### **(3) `targetLayers | (1 << collision.gameObject.layer)`**
|
||||
|
||||
- **按位或操作 `|`**: 将 `targetLayers` 和生成的位掩码进行按位或运算。
|
||||
|
||||
- 如果 `targetLayers` 中已经包含对应的层(对应位置是 `1`),运算结果不变。
|
||||
- 如果 `targetLayers` 中不包含对应的层(对应位置是 `0`),运算结果会将该位置变为 `1`。
|
||||
|
||||
**例子:**
|
||||
|
||||
- `targetLayers = 00000000 00000000 00000000 00000101` (包含第 0 层和第 2 层)
|
||||
- `collision.gameObject.layer = 1`
|
||||
- `1 << 1 = 00000000 00000000 00000000 00000010` (表示第 1 层)
|
||||
- 运算结果:`00000000 00000000 00000000 00000111` (包含第 0、1 和 2 层)
|
||||
|
||||
#### **(4) 比较:`targetLayers == ...`**
|
||||
|
||||
- 比较 `targetLayers` 和运算结果是否相等。
|
||||
- 如果相等,说明碰撞对象的 `Layer` 已经在 `targetLayers` 中。
|
||||
- 如果不相等,说明碰撞对象的 `Layer` 不在 `targetLayers` 中。
|
||||
|
||||
---
|
||||
|
||||
### **3. 具体判断逻辑**
|
||||
|
||||
代码的核心逻辑是:
|
||||
|
||||
1. 通过 `1 << collision.gameObject.layer` 获取碰撞对象的层对应的位掩码。
|
||||
2. 用 `targetLayers | ...` 检查碰撞对象的层是否在 `targetLayers` 中。
|
||||
3. 如果运算结果和 `targetLayers` 相等,说明碰撞对象的层已经被包含。
|
||||
|
||||
**简化版本:** `(targetLayers | (1 << collision.gameObject.layer))` 实际上就是在“试探性”地将碰撞对象的层加入 `targetLayers`,然后检查是否变化。如果没有变化,说明碰撞对象的层已经在 `targetLayers` 中。
|
||||
|
||||
---
|
||||
|
||||
### **4. 更直观的判断方式**
|
||||
|
||||
可以使用 `LayerMask` 的内置方法 `LayerMask.Contains`(在较新 Unity 版本中),或者稍微改写代码,提升可读性:
|
||||
|
||||
#### 替代写法:
|
||||
|
||||
csharp
|
||||
|
||||
复制代码
|
||||
|
||||
`if (((1 << collision.gameObject.layer) & targetLayers) != 0) { // 进行内部逻辑 }`
|
||||
|
||||
#### **逻辑解析:**
|
||||
|
||||
- `(1 << collision.gameObject.layer)` 生成碰撞对象层的位掩码。
|
||||
- `&`(按位与)检测目标层掩码是否包含该层。
|
||||
- 如果结果不为 `0`,说明目标层掩码包含碰撞对象的层。
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
void OnReceive(byte[] data, int length)
|
||||
{
|
||||
// 把收到的数据追加到缓存末尾
|
||||
Array.Copy(data, 0, cacheBuffer, cacheCount, length);
|
||||
cacheCount += length;
|
||||
|
||||
// 循环处理缓存中的完整消息
|
||||
while (true)
|
||||
{
|
||||
// 不足4字节,说明长度头还没收全
|
||||
if (cacheCount < 4)
|
||||
break;
|
||||
|
||||
// 取出前4字节的消息长度
|
||||
int msgLength = BitConverter.ToInt32(cacheBuffer, 0);
|
||||
|
||||
// 检查是否收全整个消息
|
||||
if (cacheCount - 4 < msgLength)
|
||||
break;
|
||||
|
||||
// 提取消息体
|
||||
byte[] msgBody = new byte[msgLength];
|
||||
Array.Copy(cacheBuffer, 4, msgBody, 0, msgLength);
|
||||
|
||||
// ---- 处理消息 ----
|
||||
ProcessMessage(msgBody);
|
||||
|
||||
// 将剩余数据前移
|
||||
int remain = cacheCount - 4 - msgLength;
|
||||
Array.Copy(cacheBuffer, 4 + msgLength, cacheBuffer, 0, remain);
|
||||
cacheCount = remain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//清除
|
||||
ClearPieceAt(piece);
|
||||
yield return new WaitForSeconds(0.25f);
|
||||
//塌陷
|
||||
movingPieces=CollapseColumn(piece);
|
||||
|
||||
三消游戏过程中遇到上述代码,我们在前面通过ClearPieceAt清除了piece列表中每一个piece对应的gameobject.
|
||||
但是在塌陷代码中依旧使用piece其中的坐标,对塌陷进行处理.
|
||||
|
||||
之所以塌陷还可以使用piece,而没有报错的原因是
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
### **Unity 场景物体查找API核心笔记**
|
||||
|
||||
在Unity中,动态查找场景中的游戏对象(GameObject)或组件(Component)是一项基本操作。不同的API适用于不同的场景,理解它们的区别和性能开销至关重要。
|
||||
|
||||
#### **一、 全局场景查找 (Scene-Wide Search)**
|
||||
|
||||
这类API会遍历整个场景的层级树来寻找匹配的对象。**它们的共同点是性能开销较大,绝对禁止在 `Update()` 或 `FixedUpdate()` 等高频函数中每帧调用。** 最适合在 `Awake()` 或 `Start()` 中用于初始化引用。
|
||||
|
||||
1. **按名称查找:`GameObject.Find()`**
|
||||
|
||||
- **API:** `public static GameObject Find(string name);`
|
||||
|
||||
- **作用:** 根据物体的**确切名称**查找一个**激活的(active)**游戏对象。如果存在多个同名对象,它会返回哪一个是不确定的。
|
||||
|
||||
- **返回:** `GameObject`。如果找不到则返回 `null`。
|
||||
|
||||
- **笔记:** 这是最不推荐的查找方式之一。因为它依赖于硬编码的字符串,当场景中物体改名时代码就会失效。性能非常差,应尽量避免使用。
|
||||
|
||||
2. **按标签查找:`FindWithTag()` / `FindGameObjectsWithTag()`**
|
||||
|
||||
- **API (单个):** `public static GameObject FindWithTag(string tag);`
|
||||
|
||||
- **API (多个):** `public static GameObject[] FindGameObjectsWithTag(string tag);`
|
||||
|
||||
- **作用:** 根据标签(Tag)查找一个或所有激活的游戏对象。标签需要在Inspector中预先为GameObject设置。
|
||||
|
||||
- **返回:** 单个返回 `GameObject`,多个返回 `GameObject[]` 数组。
|
||||
|
||||
- **笔记:** 比按名字查找要好,因为它不依赖于具体名称,更灵活。但本质上仍然需要遍历场景,性能开销依然很大。
|
||||
|
||||
3. **按组件类型查找:`FindObjectOfType()` / `FindObjectsOfType()` (常用)**
|
||||
|
||||
- **API (单个):** `public static T FindObjectOfType<T>() where T : Object;`
|
||||
|
||||
- **API (多个):** `public static T[] FindObjectsOfType<T>() where T : Object;`
|
||||
|
||||
- **作用:** 查找场景中挂载了特定组件 `T` 的一个或所有激活的对象。`T` 可以是任何继承自 `Component` 的脚本或Unity内置组件(如 `Camera`, `Light`)。
|
||||
|
||||
- **返回:** 单个返回组件 `T` 的引用,多个返回 `T[]` 数组。
|
||||
|
||||
- **笔记:** 这是最常用和推荐的**全局查找**方法。它不依赖名称或标签,而是直接关联代码逻辑(组件类型),更加健壮。但同样,性能开销大,**仅限初始化时使用**。
|
||||
|
||||
- **现代化替代方案:** Unity 推荐使用 `FindAnyObjectByType<T>()` 和 `FindObjectsByType<T>(...)` 作为更新、性能更好的替代品。
|
||||
|
||||
|
||||
#### **二、 局部层级查找 (Hierarchy Search)**
|
||||
|
||||
这类API只在当前GameObject的子级或父级中进行查找,范围小,**性能远高于全局查找**。
|
||||
|
||||
1. **在自身上查找组件:`GetComponent<T>()`**
|
||||
|
||||
- **API:** `public T GetComponent<T>();`
|
||||
|
||||
- **作用:** 获取挂载在**同一个**游戏对象上的组件 `T`。
|
||||
|
||||
- **笔记:** 这是最常用、最高效的获取自身组件的方法。
|
||||
|
||||
2. **在子级中查找:`transform.Find()` / `GetComponentInChildren<T>()`**
|
||||
|
||||
- **API (按名找子物体):** `public Transform transform.Find(string name);`
|
||||
|
||||
- **API (按组件找子物体):** `public T GetComponentInChildren<T>();`
|
||||
|
||||
- **作用:**
|
||||
|
||||
- `transform.Find()`: 根据名称查找一个**直接子级**的 `Transform`。注意,它不会递归查找孙子级。
|
||||
|
||||
- `GetComponentInChildren<T>()`: 查找自身或其**所有子级**(包括孙子级等)中第一个挂载了组件 `T` 的对象。
|
||||
|
||||
- **笔记:** `GetComponentInChildren` 非常适合用来获取预制体(Prefab)内部的某个部件,例如获取枪械模型上的“枪口特效”组件。
|
||||
|
||||
3. **在父级中查找:`GetComponentInParent<T>()`**
|
||||
|
||||
- **API:** `public T GetComponentInParent<T>();`
|
||||
|
||||
- **作用:** 查找自身或其**所有父级**中第一个挂载了组件 `T` 的对象。
|
||||
|
||||
- **笔记:** 常用于UI或模块化设计。例如,一个按钮可以向上查找到它所属的那个“根面板”控制器脚本。
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **三、 性能与最佳实践总结 (至关重要)**
|
||||
|
||||
| 方法 | 查找范围 | 性能 | 推荐用法 |
|
||||
| ------------------------ | -------- | ----------- | ------------------------------------------------------------------- |
|
||||
| **公开变量引用** | 无 (手动指定) | **极高 (最佳)** | **首选方案!** 在脚本中声明 `public GameObject myObject;`,然后在Inspector中手动拖拽赋值。 |
|
||||
| `GetComponent<T>` | 自身 | 非常高 | 在 `Awake`/`Start` 中获取自身其他组件。 |
|
||||
| `GetComponentInChildren` | 自身及所有子级 | 较高 | 初始化时获取Prefab内部的固定部件。 |
|
||||
| `GetComponentInParent` | 自身及所有父级 | 较高 | 模块化组件向上查找控制器或根对象。 |
|
||||
| `FindObjectsOfType<T>` | 整个场景 | **很低** | **仅限**在管理器类的 `Awake` 中,用于查找并注册场景中所有特定类型的对象。 |
|
||||
| `GameObject.Find()` | 整个场景 | **极低 (最差)** | **强烈不推荐**,仅用于快速原型或调试,正式项目中应被替换。 |
|
||||
#### **核心原则笔记:**
|
||||
|
||||
1. **首选“拖拽引用”:** 在脚本中声明一个 `public` 或 `[SerializeField] private` 变量,然后在Unity编辑器里手动将场景中的物体拖拽到该变量上。这是**零开销**、最安全、最高效的方式。
|
||||
|
||||
2. **“Find”仅用于初始化:** 所有全局查找 (`Find`, `FindObjectOfType` 等) 都应该只在 `Awake()` 或 `Start()` 函数中调用**一次**,并将结果缓存到一个私有变量中,供后续使用。
|
||||
|
||||
3. **杜绝在Update中使用Find:** **永远不要**在 `Update()`, `FixedUpdate()`, `LateUpdate()` 中直接调用任何全局查找API。这是导致游戏卡顿的常见原因之一。
|
||||
|
||||
4. **善用局部查找:** 当物体关系固定时(如Prefab内部),优先使用 `GetComponentInChildren` 或 `transform.Find`,它们的性能远好于全局查找。
|
||||
@@ -0,0 +1,178 @@
|
||||
# C# 对象的序列化与反序列化
|
||||
|
||||
## 1. 什么是序列化?
|
||||
|
||||
**序列化(Serialization)** 是指将对象的状态信息转换为可以存储或传输的形式(例如字节流、文件、内存数据)的过程。
|
||||
反之,**反序列化(Deserialization)** 则是将存储或传输的序列化数据还原为对象的过程。
|
||||
|
||||
在 C# 中,最常用的方式是通过 `BinaryFormatter` 类来完成二进制序列化与反序列化。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心类与方法
|
||||
|
||||
- **BinaryFormatter.Serialize(Stream, object)**
|
||||
将对象序列化到指定的流中。
|
||||
|
||||
- **BinaryFormatter.Deserialize(Stream)**
|
||||
从指定的流中反序列化对象。
|
||||
|
||||
|
||||
> ⚠️ 注意:从 .NET 5 开始,官方已经不推荐使用 `BinaryFormatter`(存在安全风险),更推荐 `System.Text.Json` 或 `XmlSerializer` 等。但在学习和理解基础时,仍可使用。
|
||||
|
||||
---
|
||||
|
||||
## 3. 序列化方式
|
||||
|
||||
### 3.1 文件流方式
|
||||
|
||||
通过 **文件流(FileStream)** 将对象直接保存到文件中。
|
||||
|
||||
```
|
||||
csharp
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
|
||||
[Serializable] // 必须标记为可序列化
|
||||
public class Person
|
||||
{
|
||||
public string Name;
|
||||
public int Age;
|
||||
}
|
||||
|
||||
class Program
|
||||
{
|
||||
static void Main()
|
||||
{
|
||||
Person p = new Person { Name = "Tom", Age = 25 };
|
||||
|
||||
// 序列化到文件
|
||||
using (FileStream fs = new FileStream("person.dat", FileMode.Create))
|
||||
{
|
||||
BinaryFormatter formatter = new BinaryFormatter();
|
||||
formatter.Serialize(fs, p);
|
||||
}
|
||||
|
||||
Console.WriteLine("对象已序列化到文件 person.dat");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
### 3.2 内存流方式
|
||||
|
||||
通过 **内存流(MemoryStream)** 将对象序列化成字节数组,便于网络传输或自定义存储。
|
||||
|
||||
```
|
||||
csharp
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
|
||||
[Serializable]
|
||||
public class Person
|
||||
{
|
||||
public string Name;
|
||||
public int Age;
|
||||
}
|
||||
|
||||
class Program
|
||||
{
|
||||
static void Main()
|
||||
{
|
||||
Person p = new Person { Name = "Alice", Age = 30 };
|
||||
byte[] buffer;
|
||||
|
||||
// 序列化到内存流
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
{
|
||||
BinaryFormatter formatter = new BinaryFormatter();
|
||||
formatter.Serialize(ms, p);
|
||||
|
||||
buffer = ms.ToArray(); // 获取字节数组
|
||||
}
|
||||
|
||||
// 可以将字节数组写入文件或通过网络传输
|
||||
File.WriteAllBytes("person_bytes.dat", buffer);
|
||||
|
||||
Console.WriteLine("对象已序列化为字节数组并写入 person_bytes.dat");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
---
|
||||
|
||||
## 4. 反序列化
|
||||
|
||||
### 4.1 从文件反序列化
|
||||
|
||||
```
|
||||
csharp
|
||||
|
||||
using (FileStream fs = new FileStream("person.dat", FileMode.Open))
|
||||
{
|
||||
BinaryFormatter formatter = new BinaryFormatter();
|
||||
Person p = (Person)formatter.Deserialize(fs);
|
||||
|
||||
Console.WriteLine($"Name: {p.Name}, Age: {p.Age}");
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 从字节数组反序列化
|
||||
|
||||
```
|
||||
csharp
|
||||
|
||||
byte[] buffer = File.ReadAllBytes("person_bytes.dat");
|
||||
|
||||
using (MemoryStream ms = new MemoryStream(buffer))
|
||||
{
|
||||
BinaryFormatter formatter = new BinaryFormatter();
|
||||
Person p = (Person)formatter.Deserialize(ms);
|
||||
|
||||
Console.WriteLine($"Name: {p.Name}, Age: {p.Age}");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
- **必须添加 `[Serializable]` 特性**
|
||||
否则对象无法被序列化。
|
||||
|
||||
- **字段级别的控制**
|
||||
如果某些字段不想被序列化,可以使用 `[NonSerialized]` 特性修饰。
|
||||
|
||||
```
|
||||
csharp
|
||||
|
||||
[NonSerialized]
|
||||
private string password;
|
||||
|
||||
```
|
||||
|
||||
- **安全问题**
|
||||
`BinaryFormatter` 在反序列化时可能存在安全漏洞,不适合用于处理不受信任的数据。
|
||||
|
||||
- **替代方案**
|
||||
|
||||
- 文本序列化(如 `JsonSerializer`、`XmlSerializer`)
|
||||
|
||||
- 跨平台高性能方案(如 `MessagePack`、`Protobuf`)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
- **文件流序列化**:直接将对象保存到磁盘文件中。
|
||||
|
||||
- **内存流序列化**:对象 → 字节数组 → 可保存或传输。
|
||||
|
||||
- **反序列化**:根据文件流或字节数组还原对象。
|
||||
|
||||
|
||||
序列化与反序列化是 C# 中对象持久化与数据传输的核心知识点,实际开发中需结合安全性与场景选择合适的序列化方式。
|
||||
Reference in New Issue
Block a user