SparseSet
稀疏集
SparseSet(稀疏集) 是一种专为有界整数 ID设计的高性能集合数据结构,核心优势是插入、删除、查询、清空均为 O (1) 时间复杂度,且遍历高效、缓存友好。它通过稀疏数组(sparse)+ 密集数组(dense)+ 元素计数(n) 实现,广泛用于游戏引擎(如 ECS)、编译器、图算法等场景。
核心结构
SparseSet 由三部分组成:
- dense[](密集数组):按插入顺序存储集合中的实际元素值,仅包含存在的元素,内存紧凑、遍历高效。
- sparse[](稀疏数组):以元素值为索引,存储该元素在 dense 数组中的下标位置;未存在的元素对应位置值无意义。
- count(元素计数):记录当前集合中元素的总数,dense[0..count-1] 为有效元素。
ORCA避障算法图解
算法图解
ORCA避障算法,出自2011年的一篇论文《Optimal Reciprocal Collision Avoidance》。该算法的思想不算复杂,但实现上有很多细节需要注意。网上已有很多对该算法的讲解,但是大多都比较粗略,很多细节并未详解,缺少很多图解来帮助理解,因此本文着重通过图解的形式,辅以文字,来剖析该算法的思想,并对关键源码进行解释。
速度障碍(VO, Velocity Obstacle)
如图所示,假设存在A、B两个对象,它们以图中的速度(速度是向量,包含方向和模大小)运动。

Csharp in & out & ref
碰撞检测
粗筛阶段(Broad Phase)
SAP(扫描线算法)
不论碰撞体本身是圆形、OBB、多边形,都可以获得顶点集合的minPoint: (minX, minZ)和maxPoint: (maxX, maxZ),得到最大外接矩形也就是AABB,将所有待检测对象的AABB投影到对应的轴上。每个对象在每个轴上都会得到一个线段,两个不同对象的线段如果在某一个轴上没有交集,就证明一定不存在碰撞,反之,在所有轴上都存在交集,则这两个对象的AABB一定发生了碰撞,如果对象本身就是AABB那就检测完毕,否则需要进一步判断两个对象是否实际碰撞。
算法
先从X轴开始,遍历每个对象,投影可以产生两个端点minValue, maxValue,将所有对象的投影端点都放入一个数组内,并按坐标从小到大进行排序。维护两个列表:活跃列表[]和重叠对列表[]。扫描线从左到右扫描这些端点。
Csharp Span
Csharp fixed
1、固定托管对象的地址
.NET的GC在进行可达性遍历后会将所有使用到的内存进行压缩(Compact),空出一整块未使用的内存方便后续申请使用,这时候,原先代码里还在使用的内存就可能会变动位置,指针就会失效。所以fixed在这里的作用就是告诉GC不要compact我这个托管对象,常常配合指针进行较为底层的操作。
1 | unsafe |
- 这种方式会有一定内存损耗,可能会影响GC产生内存碎片。
- fixed只用用于内建类型数组,别用自己引用类型元素数组,它是不会递归pin的
2、固定大小缓冲区字段fixed buffer
玩家控制镜头
视角移动和缩放
假如你的游戏场景是在XZ平面的,而你的相机是俯视角的,你希望玩家可以拖动屏幕来移动视角,双指交互可以缩放视角。针对这个需求,在FGUI里,包含用户输入的组件:SwipeGesture和PinchGesture。拖动相机分成两个阶段:第一阶段,手指按下并拖动,此时要求场景完全跟随手指移动(跟手);第二阶段,当手指松开后,根据松开前delta阈值,低于阈值则不进行惯性操作,否则需要根据松开前的速度进行惯性制动位移。
正交相机
正交相机的orthographicSize代表相机高度的一半对应的世界距离,将它乘以2再除以屏幕高度,就可以得到单位像素对应的世界距离。FGUI屏幕delta可以转化为unity屏幕像素delta,最终对应世界空间的delta。
通过这种映射关系,移动视角的第一阶段,可以算出世界空间的delta;移动视角的第二阶段,可以算出世界空间的velocity。
透视相机
给定路径节点下NPC如何平滑移动
NPC平滑运动
NPC被赋予一组路线比如List
1 转角圆弧
在转角时,根据预留量(可以设定与速度线性相关)提前脱离路径转弯,转弯的圆弧是两组边的内切圆。此时NPC的转弯角速度可以根据运动速度、预留量,转角的角度,精确求得。
1 | using System.Collections.Generic; |
Csharp枚举类型的装拆箱
1 优化位枚举
1.1 原生HasFlag问题
- 原生的HasFlag方法会将枚举装箱为Enum类型,产生性能开销
- Enum本身是一个引用类型,所有具体的枚举类型比如MyColor都是值类型,并且继承自Enum(特殊的继承规则,就像所有C#对象都继承自object,它也是引用类型)。当调用基类Enum的方法时,自身需要装箱成Enum,参数也需要传递给Enum flag,会进行两次装箱
- HasFlagFast 用泛型值类型参数替代 Enum 引用类型参数,从根源上避免了装箱。Unsafe.As 只是进一步优化了类型转换的性能,而泛型值类型参数才是避免装箱的关键
1 | // 原生判断函数是Enum的实例方法,Enum本身是抽象类(引用类型) |