Todo
- Photon isMine
- RPC
- ECS
物理 Rigidbody
代码:Rigidbody 不能被移除
如题,因为 Rigidbody 继承自 Component 组件,而常见的脚本、Collider 都继承自 Behaviour。
定义:静态碰撞体和动态碰撞体
区别,没带 Rigidbody 和带了 Rigidbody
Static Collider,指的是不带 Rigidbody 的 Collider Dynamic Collider,带了 Rigidbody 组件的 Collider
碰撞检测(Continuous Collision Detection, CCD)
Unity Manual: https://docs.unity3d.com/6000.2/Documentation/Manual/ContinuousCollisionDetection.html
Collision Dectation:
- Discrete,离散,每次物理检测检查碰撞,效率最高,但容易漏掉
除此以外,下面三种 Continuous 都是提前预算碰撞来实现的碰撞检测
- Continuous,持续检查与静态碰撞体的碰撞(不带 Rigidbody 的 Collider)
- Continuous Dynamic,持续监测 Rigidbody 与静态碰撞体和动态碰撞体的碰撞
这两种检测方式是基于 Sweep-based CCD,最精确的检测方式同时也最耗性能,但不能预测旋转,只能预测线性运动(比如一根棍子高速旋转的场景,检测不到棍子扫到的物体)
- Continuous Speculative,推测检测
基于 Speculative CCD ,支持线性运动和角运动,采用 AABB 包围盒检测,精确度相比于 Sweep-based CCD 更低,预测结果可能出错导致偏离预期轨迹。
Tip
碰撞检测推荐尝试顺序:
- Discrete
- Continuous Speculative
- Continuous
- Continuous Dynamic
更完整的选择顺序:
还要更完整的检测顺序建议自己去读一下 Unity 文档,表格表示并没有把文档中的情况都涵盖到位,还是需要自己去看一下各种算法怎么实现的,缺陷可能在哪里才能选择最适合自己的检测模式。
视频推荐: 【Unity】3分钟搞懂Unity的4种碰撞检测模式 CC中字熟 https://www.bilibili.com/video/BV1de4y1E7Qm/
Tip:不要对刚体应用 Transform
碰撞器 Collider & Trigger
使用 Layer 过滤来优化碰撞检测效率
在 Unity 开发中,
Collider和Trigger的检测效率对性能有很大影响,尤其是在场景中存在大量物体时。一个常见的优化手段就是使用 Layer 来进行碰撞过滤,而不是依赖 Tag 或逐个对象判断。
为什么用 Layer 而不是 Tag?
- 性能差异:Layer 在 Unity 内部使用整数表示,而 Tag 是字符串,整数比较远比字符串比较高效。
- 位运算支持:LayerMask 可以通过位屏蔽(二元运算)进行快速过滤,非常适合批量判断。
比较单个Layer:
//这样可以将字符串名称转换成 Layer 的整数 ID。
int layerId = LayerMask.NameToLayer("Enemy");
if (other.gameObject.layer == layerId)
{
Debug.Log("检测到敌人");
}多个 Layer 的比较:
当需要判断一个物体是否属于多个目标 Layer 时,推荐使用
LayerMask配合位运算:
public LayerMask targetLayers;
void OnTriggerEnter(Collider other)
{
if ((targetLayers.value & (1 << other.gameObject.layer)) != 0)
{
Debug.Log("进入目标 Layer 范围");
}
}这里的关键是 (1 << other.gameObject.layer),它会生成对应 Layer 的掩码,然后与目标 LayerMask 做按位与运算。如果结果不为 0,就说明当前物体的 Layer 在目标集合内。
讲讲位运算和 LayerMask 的存储方式:
LayerMask 的存储方式:
Unity 一共有 32 个 Layer(编号 0–31)。每个物体的 gameObject.layer 是一个 int,表示它在哪个 Layer 上(例如 Layer 8 = “Player”)。
LayerMask 本质上是一个 32 位整数,每一位表示一个 Layer 是否启用:
- 1 << 8 → 表示第 8 个 Layer(Player)。
- 1 << 9 → 表示第 9 个 Layer(Enemy)。
- 1 << 8 | 1 << 9 → 同时包含 Player 和 Enemy 两个 Layer。
<< 左移运算:
会把一个数的二进制 整体往左移动 N 位,右边空出来的位置补 0
// x << n = x * 2^n
1 << 0 // 0000...0001 → 结果 = 1
1 << 1 // 0000...0010 → 结果 = 2
1 << 2 // 0000...0100 → 结果 = 4
1 << 3 // 0000...1000 → 结果 = 8&按位与运算符(bitwise AND):
对两个整数的二进制表示逐位比较:如果两个数在同一位上 都是 1 则结果为 1。否则结果为 0。
| 按位或运算符(bitwise OR):
对两个整数的二进制表示逐位比较:只要某一位上 有一个是 1 , 结果就是 1。两个都为 0 结果才是 0。
可以用| 来合并多个 Layer 掩码:
// 只包含 Layer 8
int maskA = 1 << 8;
// 只包含 Layer 9
int maskB = 1 << 9;
// 合并两个 → 同时包含 Layer 8 和 9
int maskAB = maskA | maskB; 位屏蔽(bitmask):
位屏蔽就是用一个整数(
LayerMask)的二进制位来表示一组 Layer 是否被选中。
综上,对于下面代码就很好理解了:
void OnCollisionEnter(Collision other)
{
// 将要检测的 targetLayers 的 value 与 碰撞进来的 other.gameObject.layer 进行比较,如果不为 0,则代表 targetLayer 包含 other.gameObject.layer
if ((targetLayers.value & (1 << other.gameObject.layer)) != 0)
{
// 命中
}
}或者我们也可以这样写:
void OnCollisionEnter(Collision other)
{
// 如果 targetLayers 和 other.gameObject.layer 进行按位或运算(也就是合并)之后,依旧和 targetLayers 相等,就表示 targetLayers 包含 other.gameObject.layer
// 比如 targetLayers 为 0011, other.gameObject.layer = 1000, 按位或运算之后为 1011, 就不对,表示没有命中; 而如果是 0011 和 0001 进行按位或运算之后,依旧是 0011
if(targetLayers == (targetLayers | (1 << other.gameObject.layer)))
{
// 命中
}
}-------------- 实战应用 --------------
Unity 制作雪花飘落特效
link: https://www.youtube.com/watch?v=a_cr6vEcHzc date: 2025年12月25日 补充,可以看看 UIParticle
核心:利用 Partical System + 两个 Sprite 制作雪花飘落特效
踩坑:要在 UI 前面创建雪花飘落特效,需要把 Canvas 的 Render Mode 设置为 Screen Space-Camera,否则 UI 会一直渲染在最上层。
制作过程:
创建粒子系统:Create > Effects > Particle System
设置渲染层级:调整 UI Canvas 和 Particle System 的 Render 层级
应用自定义雪花贴图:在 PS 中画两个透明背景的雪花并导入 Unity, Render 模块中的 Material 调整为 Sprites-Default (Unity 6 中选择 Sprites-Unlit-Default)
调整发射形状:Shape 属性
配置雪花飘落属性:Lifetime, Emission, Start Size, Start Color, Noise, Start Speed, Rotation over Lifetime, Pre-warm(直接铺满)
Unity 自定义工具:快速锁定 Inspector 面板
link: https://www.youtube.com/watch?v=-3SnFiJwgRM date: 2025年12月15日
Inspector锁定 Lock() 函数 实现具体的功能,在这个视频中,是锁定 Inspector 面板。
Valid() 函数 是一个验证函数,告诉Unity编辑器再特定条件下是否应该启用/禁用关联菜单项。 eg:
[MenuItem("Edit/Lock Inspector %L"),true]
public static bool Valid(){
//只有当 Unity 编辑器中存在至少一个当前被追踪或活跃的检视器窗口时,这个“锁定”菜单项才可用
return ActiveEditorTracker.sharedTracker.activeEditors.Length != 0;
}通过给 Lock 和 Valid 函数添加共同的属性(Attribute),可以将 Valid 函数的判断绑定到菜单项中,从而影响 Lock 函数的启用。
Transform 比例约束锁定(利用反射)
var propInfo = transform.GetType().GetProperty("constrainProportionScale", BindingFlags.NonPublic | BindingFlags.Instance);
value = (bool) propInfo.GetValue(transform,null)
propInfo.SetValue(transform, !value, null);Unity 新项目模板
link: https://www.youtube.com/watch?v=nVieP57TD20
link2(git-amend) : https://www.youtube.com/watch?v=-Wkbi4i2EwU date: 2025年12月15日
Git Amend:
- 每周四下载好最新的 Unity 版本、Hub
- 准备自己的模板工程
- 找到 Unity 项目模板文件路径:
Unity\Hub\Editor\xxxx.x.xx\Editor\Data\Resources\PackageManager\ProjectTemplates - 创建自定义模板
- 拿一个Unity的模板 tgz 文件,解压查看
- 删除以下文件,并把自定义模板工程的相关目录复制过来:
package/ProjectData~/Assetpackage/ProjectData~/Packagespackage/ProjectData~/ProjectSettings- 删除 ProjectSettings 里面的 ProjectVersion.txt
- 修改 package/package.json 中的字段
- name,包名
- displayName,在 Unity Hub 中显示的模板名称
- 压缩为 tgz 文件,文件名称和 package.json 中保持一致
- 压缩文件的目录结构示例:
- com.unity.template.projectSample.tgz
- package
- package.json
- ProjectData~
- xxx
- package
- com.unity.template.projectSample.tgz
Jason Storey:
- 项目文件夹命名:
U.<Projectname>, 方便通过 Everything 之类的快速查找指定项目 - Root Namespace:把脚本都放置在同一个命名空间下面,这样如果要复用不同项目之间的脚本,即使有重名,复制过来也不容易报错。
- 创建项目初始化脚本:
- 创建默认文件夹:_Project, Scenes, Scripts, Arts, etc.
- 添加一个 asmdf(Assembly Definition File)
- 导出 .unitypackage,方便创建新项目使用
- 替换Unity的默认MonoBehaviour代码模板
- Editor 安装路径下的:
Data\Resources\ScriptTemplates eg:C:\Program Files\Unity\Hub\Editor\2020.3.16f1\Editor\Data\Resources\ScriptTemplates` - 替换掉
81-C# Script-NewBehaviourScript.cs.txt里面的内容
- Editor 安装路径下的:
河流效果的实现
link: https://youtube.com/shorts/1LevhRBxOsQ date: 2025年12月15日
- 创造曲线
- 添加Mesh,调整宽度
- Form texture,如果有高度差,就显示白色浪花
- 可漂浮物体:向下射线检测,检测水面
- 跟随水流方向飘:对物体添加力,让它沿着曲线方向游动
改变单个物体重力
比如我们场景中有多个玩家(PlayerController),希望玩家踩空的时候变成浮空的状态,怎么办?
法一:AddForce 法二:修改 Drag & AnglerDrag
利用数据驱动Unity中的游戏系统
link: https://www.youtube.com/watch?v=6qd22ulEds4
date: 2025年12月5日、2025年12月9日
SOAP
Scriptable Object Architecture Pattern
SOAP 的核心是让 GameObjects 之间解耦,而通过读写共享项目中的数据来完成功能。
数据驱动示例:技能组成
技能:AbilityData(ScriptableObject),技能由多个 AbilityEffect 组成,Effect 尽量原子化,比如击退、扣血、减速等等。
数据对象:AbilityData,只维护一个 List<AbilityEfeect> effects 列表
技能的应用:
在玩家身上添加组件:Ability Executor,传入 AbilityData、Target。在组件的声明周期函数中检测按键,随后响应函数。
响应函数中,遍历 AbilityData 中的 Effects,然后调用 Effects 的 Execute 方法,挨个执行。
实际的流程:
创建技能、选择Effects,然后把技能拖到玩家身上的 Ability Executor 中
自定义属性Drawer
Odin 能实现的,自己写一定能实现。不过视频中的例子通过遍历派生类来生成下拉框的数据,要是技能类型多了起来,感觉不是个很好的选择,还是 Odin 的拖拽输入框比较好用。
Rider 的使用以及对 Unity 的特殊优化
link: https://youtu.be/h564F6pLOsE?si=LPW56koTyZNi6N3d link2: https://www.bilibili.com/video/BV14jDHYuE4U/ link3: https://youtu.be/ra-fpMO5tpA?si=M8H1k3FbEQNtcN_t (没看,有点长,happyNerd的视频)
date: 2025年12月4日
- 跳转到 Unity Manul
- 可以查看挂载了该类关联的 Prefab、Object
- 可以查看使用了该类的场景(动态加载应该不能查看)
- 可以查看为什么有些运算符不建议使用,比如不应该对 Monobehaviour 使用
?.运算符,并且可以链接跳转到 github 相关的链接中 - Profile 分析代码优化
- 依赖断点
transform 是否有必要缓存
link: https://www.youtube.com/watch?v=uTJe7M1E3Tg
date: 2025年12月2日
结论:
- IDE 中测试:缓存这个行为本身就会有 30% 的性能提升,特别是在 Update() 方法中频繁调用时。
- Unity 中测试:每秒会有 3 帧的性能提升;帧时间上,有 10% 的性能提升。
其他收获:
- 每个程序员都应该自己测试了再持有结论
- 应该做自己的基准测试 BenchMark
- transform 在 Unity 底层是使用 C++ 跨界访问。
论证过程:
- 写 C# 代码,在 IDE 中比较,有差异
- 在 Unity 中运行,有差异
- 打包运行 Windows Standalone,没差异
- Json 论坛中的人运行,有差异
- 问题出在 VSync,会弥补帧差距
乞题谬误
Begging the question, 一种逻辑谬误,在论证中把尚未被证实的结论当成理所当然的前提,从而未能提供任何真正的证据
涅槃谬误
涅槃謬誤(英語:nirvana fallacy)或完美主義謬誤(perfectionist fallacy)是一種非形式謬誤,係宣稱某個解決方案因為無法做到涅槃(完美),所以該方案便沒用。
稻草人谬误(straw man)
稻草人谬误是一种在论证中通过歪曲、夸张或虚构对方论点,转而攻击被篡改后的替身论点(即“稻草人”)的非形式逻辑谬误。其核心特征为将原论点简化为极端或荒谬形态,例如错误引用、曲解原意或强加未提出的观点。
避障第三人称摄像机的实现
link: https://www.youtube.com/watch?v=QrDgrCO22aU
date: 2025年12月2日
原理:从玩家位置向摄像机理想位置发送一条射线,如果检测到障碍物,就将摄像机的位置应用为 hit.distance - minimumDistance(摄像机距墙最小距离)
构成:三部分——Hierarchy保持相对位置、Controller通过读取输入设置旋转、RayCaster 通过射线更新位置。
这里有四个层级:
- CamRoot:放在 Player 下面,设置 LocalOffset 为玩家头顶
- CamControlls:挂CamController和CamDistanceRayCaster
- CamTarget:LocalOffset 设置为玩家身后
- CamTransform:MainCamera,摄像机本体。
- CamTarget:LocalOffset 设置为玩家身后
- CamControlls:挂CamController和CamDistanceRayCaster

CamRoot是一直在玩家头顶的。 CamControlls上的CamController会读取输入控制自身旋转,这样就控制了CamTarget和相机本体相对于玩家的旋转。 CamControlls上的 RayCaster 组件则是控制相机本地的 position,用来避障。

原理还是很简单,但是之前在地铁上看的时候没搞懂代码里面的变量和 Hierarchy 面板中的Obj 的对应关系,一直搁置,今天运行了一下工程才搞懂。
状态机
状态机负责管理状态切换,每个状态需要包含:Enter、(Loop)、Exit
Demo复盘:InputSystem与多人分屏
有空再来整理吧,参考链接:https://www.bilibili.com/video/BV1HA411d7YQ/
- InputActions
- Control Schemes
- 根据InputSystem发出的事件,可以改变UI的图标
- Control Schemes
- PlayerInput组件
- 提供一个玩家控制,一个输入与
- 标识接受哪一个控制器的输入
- Actions、键位映射
- Auto-Switch:用户自动切换设备
- UI Input Module(不同玩家不同UI)
- Camera:切分镜头
- Behavior
- SendMessage(调用玩家Obj其他组件上的函数,下面举例了)
- 反射(Private也可以调用)
- OnMove
- Invoke Unity Events
- 拖脚本函数
-
★Invoke C Sharp Events
- SendMessage(调用玩家Obj其他组件上的函数,下面举例了)
- 脚本框架
- PlayerPawn(玩家身体)
- SetMovement
- PlayerController(读取输入)
- PlayerPawn(玩家身体)
- 每个玩家
- Player(Prefab)
- 模型/Obj
- PlayerPawn
- PlayerController(Prefab)
- 这个放到InputSystem Manager中
- PlayerInput
- PlayerController.cs
- PlayerPrefab(Player)
- Player(Prefab)
Demo复盘:道具系统与UI
- Item
- enum ItemType
- GetSprite
- IsStackable
- ItemAssets 道具资产
- pfItemWorld
- swordSprite
- healthPotionSprite
- …
- Inventory
- OnItemListChanged
- itemList
- useItemAction
- Inventory(useItemAction)
- AddItem
- RemoveItem
- UseItem
- GetItemList
- Player
- UseItem
- new Inventory
- onTriggerEnter
- ItemWorldSpawner
- public Item item;
- ItemInWorld.SpawnItemInWorld(this.pos,item)
- ItemInWorld
- SpawnItemWorld
- Instantiate
- SetItem
- this.item = item
- GetItem
- DestroySelf
- SpawnItemWorld
- UI Inventory
- (SetPlayer)
- SetInventory
- Inventory_OnItemListChanged
- RefreshInventoryItems
------------ 游戏算法 ------------
柏林噪声地形生成
link: https://youtu.be/mXGM8-zzRiE?si=UEnj8RHJO55NUDL2 link(mainly): https://www.youtube.com/watch?v=CSa5O6knuwI
生成顺序:
- 地形生成
- 水域生成
- 表面层:泥土、沙子
- 特征与结构:村庄、数目
地形生成算法演化:
- 纯随机:地基高度+随机范围,没有连续性
- 正弦曲线:地极高度+sin(x) * 倍率,通过改振幅和频率可以调整
- 南北方向和东西方向:圆滑山丘
- 没有随机性
- Perlin Noise:梯度噪音的一种,可看作高度图,基于亮度
- 地基高度 + 柏林噪声 * 倍率:平滑
- octaves(八度):将多个不同尺度的噪声图叠加在一起
- 简单好用的技巧:第二个噪声的振幅是前面的一半,频率是前面的2倍
- 缺少戏剧性的显示特征:悬崖、河谷、高原
- 特征添加:靠不同的噪音图
- 大陆性:高大陆性意味着高海拔,调整 Curve,来制作高原
- 侵蚀(Erosion)
- 山峰与山谷(Peaks&Valleys)
- 3D 噪音:密度(Density)
- 正密度视为实体、负密度视为空气
- 密度向上减低,向下增加
- 压缩因子、高度偏移量
- 可以生成洞穴
- 转变思维:生成连续洞穴
- 噪音图黑白边界是空气
- 生物群系:方块种类、动植物生成
- 增加噪音图:温度、湿度
- 通过表格,大陆性和侵蚀决定 生物群系组
- 再通过湿度温度表格,决定具体的生物群系
- 其他细节:温暖群系相互联系,而不是沙漠挨着雪地