从OI到外挂-第一集-使用并查集合并实现透视物资列表
引言
众所周知,在OI中,并查集是用来维护集合之间关系的数据结构,它能以优秀的时间复杂度完成集合的合并,元素所属集合的查找,还有种类并查集等等拓展玩法,特别有趣。
如今的大逃杀类外挂的一个重要功能就是 物资透视 ,即将地上的物品通过绘制文字的形式显示出来
,让玩家能够清楚的知道物资的位置。但是这也会出现一定的问题,首先是物品堆叠问题,如果两个物品靠的太近,绘制时名字就会重叠在一起,这样两个物品的名字都看不清;其次是物品堆叠过于杂乱,玩家不能够很清楚的辨识出自己想要的物资所处位置,即便是加上了不同类型物资以不同颜色绘制,依然效果不理想。
因此,一种新的绘制方式被急切需要,我们能否想象一种方式,使之能够将隔得很近的物资自动合并,并以列表的形式绘制出来?此时,我们自然的想到了OI中的一种数据结构:并查集
并查集简介
并查集的种类有很多种,其中最广泛使用的是按秩合并并查集(也称启发式合并并查集),还有路径压缩并查集。我们今天使用的就是路径压缩并查集。
并查集,顾名思义,有“并”和“查”两种功能。想象这样一个场景,对于每一个我们要绘制的物资,我们都知道它的三维坐标,也就自然而然知道它到其他物资的距离,我们先简化模型,把物资的位置看作二维平面上的点,如图:
可以发现,图中有一些聚集的点,形成了一“团”,这通常和大逃杀类游戏中,武器和子弹生成拜访在一起的逻辑是相符合的。接下来我们按照如下规则进行连线,对于任意的$A$和$B$物品,如果他们之间的距离$d < D$,(其中$D$为设定的最大合并距离阈值),那么我们在他们之间连线,然后就得到了下面这幅图
我们可以看到,靠的近的物资被连成了一体,用集合的角度来看,他们就都处于了同一个集合,并查集实际上实现的功能就是这样,将两个集合合并,同时可以查询每个元素属于哪个集合
如何在外挂中使用
既然我们都知道了每个物资的三维坐标,那么我们同样也可以计算出它到每个其他物资的距离,如果这个距离小于阈值$D$,那么我们就在并查集里面把他们合并,对于每个物资都做了同样的操作过后,此时的并查集维护的信息,就是所有小堆物品的集合信息了,我们分别遍历每个集合,把集合里面的物资以列表的形式绘制出来就行了
此时存在一个问题,如何确定一堆物品的最终绘制位置?以其中任意一个物品的坐标为标准?其实,我们有更好的做法,那就是假设该集合每个物资的重量相等,那么只需要求出该集合所有物资的重心,在重心出绘制列表就可以了。怎么求重心?所有的物资的$x,y,z$坐标分别相加,然后除以物品数就得到了重心,高中物理,就不用我多说了吧。
Coding Time!
外挂中使用到的并查集
/*
并查集
*/
namespace BCJ{
int fa[2000];
void initFa() {
for (int i = 1; i < 2000; i++) fa[i] = i;
}
int get(int x) {
if (fa[x] == x) return x;
return fa[x] = get(fa[x]);
}
void merge(int x,int y) {
int fax = get(x),fay = get(y);
if (fax == fay) return;
fa[fax] = fay;
}
}
一个非常简单的路径压缩并查集,因为游戏优化,物资数不会太多,这里开2000的数组完全足够了
绘制时的调用
void 绘制物品列表() {
float distanceInmerge = 1.5; //最小合并距离
int margin = 2;
BCJ::initFa();
static std::vector<WorldObject> itemList;
itemList.clear();
for (WorldObject obj : worldObjs) {
if (obj.type == Item) {
itemList.push_back(obj);
}
}
int itemCnt = itemList.size();
std::vector< std::vector<WorldObject> > buk(itemCnt+2);
std::vector< Vector3 > gravityCenter(itemCnt+2);
for (int i = 0; i < itemList.size(); i++) {
WorldObject cur = itemList[i];
for (int j = i + 1; j < itemList.size(); j++) {
WorldObject that = itemList[j];
float dis = (that.location - cur.location).GetLength() / 100;
if (dis <= distanceInmerge) {
BCJ::merge(i+1,j+1);
}
}
}
for (int i = 0; i < itemList.size(); i++) {
int belongto = BCJ::get(i+1);
buk[belongto].push_back(itemList[i]);
gravityCenter[belongto] = gravityCenter[belongto] + itemList[i].location;
}
for (int i = 1; i <= itemCnt; i++) {
if (buk[i].size() == 0) continue;
sort(buk[i].begin(),buk[i].end(),typeCmp);
Vector3 gc = gravityCenter[i] / buk[i].size();
屏幕范围 pos2d = { 0,0,0,0 };
if (数据::WorldToScreen_2d(gc, pos2d)) {
for (int j = 0; j < buk[i].size(); j++) {
WorldObject cur = buk[i][j];
ImVec2 textsize = bigger_font->CalcTextSizeA(20, 9999, 9999, cur.objName.c_str());
ImVec2 start1 = ImVec2(pos2d.X - 1, pos2d.Y + textsize.y * j + margin * j) ^ gameDrawOffset;
ImVec2 start = ImVec2(pos2d.X, pos2d.Y + textsize.y * j + margin * j) ^ gameDrawOffset;
ImVec4 col = config.物品颜色选项[cur.itemType];
ImGui::GetOverlayDrawList()->AddText(bigger_font, 20, start1, ImGui::GetColorU32(ImVec4(1-col.x,1-col.y,1-col.z,0.8)), cur.objName.c_str());
ImGui::GetOverlayDrawList()->AddText(bigger_font,20,start, ImGui::GetColorU32(col),cur.objName.c_str());
if (是否高级物资(cur.objName)) {
ImGui::GetOverlayDrawList()->AddLine(ImVec2(start1.x, start1.y + textsize.y + 1), ImVec2(start1.x + textsize.x, start1.y + textsize.y + 1), ImGui::GetColorU32(ImVec4(1 - col.x, 1 - col.y, 1 - col.z, 0.8)), 2); //下划线
ImGui::GetOverlayDrawList()->AddLine(ImVec2(start.x,start.y + textsize.y + 1), ImVec2(start.x + textsize.x, start.y + textsize.y + 1), ImGui::GetColorU32(col),2); //下划线
}
}
}
}
}
这里的 $distanceInmerge$ 就是我们之前提到的阈值$D$,根据测试,在hpjy中阈值为$1.5m$比较合适
作为一个OIer + espmaker
我大受震撼
By dreamforest at August 20th, 2022 at 01:25 pm.
王氦与到此
By 我 at November 23rd, 2020 at 12:15 pm.
开挂,举报了
By lbwnb at April 22nd, 2020 at 05:55 pm.
牜大了
By 1 at April 22nd, 2020 at 05:33 pm.