温馨提示:使用PC端浏览器阅读可获得最佳体验

阅读本文时,请时不时就对照参考图看一下。

什么是overview?

如果你有使用过3D模型制作工具,例如3dsMax等等,在编辑模型时这些软件通常会展示四个视图:

  • 前视图
  • 左视图
  • 顶视图
  • 透视图

【HLSDK系列】overview(俯视图)-LMLPHP

overview类似与顶视图。

HL引擎可以给任意一张地图生成overview,为了生成overview,你需要加上 -dev 参数来启动游戏,进入任意一张地图,然后在控制台输入 dev_overview 1 即可。

【HLSDK系列】overview(俯视图)-LMLPHP

你会看到游戏画面变成了类似上图这样,这正是当前地图的overview,也就是顶视图。

同时我们还需要注意一下画面顶部显示的一些参数。

Overview: Zoom 1.57, Map Origin (-248.00, 16.00, 192.00), Z Min 673.00, Z Max -296.00, Rotated 0

然后把这个画面截图,就获得了一张overview图片(下文简称OV图),HL和CS已经制作好了一些,它们放在 valve/overviews 或者 cstrike/overviews 目录下。

生成overview的原理

为了正确使用OV图,我们有必要了解一下它是怎么生成的。

我制作了一个简单的地图模型:

【HLSDK系列】overview(俯视图)-LMLPHP

上图坐标系中:

横向为 X轴 ,纵向为 Y轴 。

蓝色矩形是 世界区域 ,位置以 O 为中心,大小为 8192×8192 ,这个大小是引擎规定的。 O 是 世界中心点 。

紫色矩形是 overview区域 (下文简称 OV区域 ),位置和大小由地图作者制作的地图决定,引擎会自动计算,图中大小为 3000×3000 。 ORIGIN 是 OV区域 的中心点。

灰色区域是地图模型,仅仅作为观赏用。

当你输入 dev_overview 1 后,引擎就会把 OV区域 显示到游戏窗口:

【HLSDK系列】overview(俯视图)-LMLPHP

假设 游戏窗口 大小(不包含窗口的边框)为 800×800 ,那么引擎就是把 OV区域 缩小到 800×800 来显示了。

缩小倍数

再次提醒, OV图 实际上就是 游戏窗口 的截图,所以它们的大小是相同的。

继续之前,需要先了解坐标系统, OV区域 使用 世界坐标 。而 游戏窗口 使用 窗口坐标 。

世界坐标最小值: x = -4096, y = -4096  世界矩形左下角
世界坐标最大值: x = +4096, y = +4096 世界矩形右上角
窗口坐标最小值: x =   0, y =   0  窗口左上角
窗口坐标最大值: x = 800, y = 800 窗口右下角

我们首先需要关注的是,引擎做了一个缩操作。

引擎把 OV区域 缩小到 游戏窗口 大小,也就是 3000×3000 缩小到 800×800 ,我们只需要知道引擎缩小了多少倍,就能将 世界坐标 单位转换为 窗口坐标 单位。

计算方法很简单:

scale.x = overview.width ÷ window.width
scale.y = overview.height ÷ window.height

代入参考图中的数据:

scale.x = 3000 ÷ 800  = 3.75
scale.y = 3000 ÷ 800 = 3.75

参考点

参考图中 P 的坐标是 800,1400 ,这是 世界坐标 ,以 世界中心点 作为参考点。但引擎缩小 OV区域 的时候,显然不是以 世界中心点 为中心缩放的。

引擎会以 ORIGIN 为中心来缩小 OV区域 。这意味着,如果我们要缩小 P 的坐标,就不能以 世界中心点 为参考点来缩小,否则会产生错位。

既然如此,那就把 P 的参考点也变成 ORIGIN 不就行了。

我们已经知道 OV区域 的 中心点 是 ORIGIN ,那就可以计算出 P 以 OV区域 的 中心点 为参考点的新坐标了。

计算如下:

P2.x = P.x - ORIGIN.x
P2.y = P.y - ORIGIN.y

代入参考图中的数据:

P2.x = 800 - 500   = 300
P2.y = 1400 - 500 = 900

好了,现在让我们忘掉 世界中心点 吧。现在 ORIGIN 才是 中心点 。

【HLSDK系列】overview(俯视图)-LMLPHP

然后我们把 P2 的坐标按照上文中计算出来的缩小倍数来缩小,就能得到 P2 的 缩小后的OV区域 坐标 。

P3.x = P2.x ÷ scale.x
P3.y = P2.y ÷ scale.y

代入参考图中的数据:

P3.x = 300 ÷ 3.75  = 80
P3.x = 900 ÷ 3.75 = 240

此时 P3 坐标的单位已经和 窗口坐标 单位一致。

缩小后的OV区域 和 缩小后的P2的坐标 如下:

【HLSDK系列】overview(俯视图)-LMLPHP

(什么?地形变了?那是因为我重新画过了-.-)

但是别忘了, 窗口坐标 的坐标值范围是:

窗口坐标最小值: x =   0, y =   0  窗口左上角
窗口坐标最大值: x = 800, y = 800 窗口右下角

而我们上面计算出的 P3 是以 0,0 为参考点的,显然 窗口坐标 的 中心点 不是 0,0 ,我们需要计算出来。

计算 窗口坐标 的 中心点 如下:

O2.x = window.width ÷ 2
O2.y = window.height ÷ 2

代入参考图中的数据:

O2.x = 800 ÷ 2  = 400
O2.y = 800 ÷ 2 = 400

如下图:

【HLSDK系列】overview(俯视图)-LMLPHP

然后我们把 P3 的参考点转为 窗口坐标 的 中心点 ,如下:

P4.x = O2.x + P3.x
P4.y = O2.y + P3.y

代入参考图中的数据:

P4.x = 400 + 80 = 480
P4.y = 400 - 240 = 160

得到最终P4的坐标:

【HLSDK系列】overview(俯视图)-LMLPHP

因为 OV图 就是 游戏窗口 的截图,所以 P4 在 OV图 中也是一样的坐标。

计算OV区域的大小

如果你还记得

Overview: Zoom 1.57, Map Origin (-248.00, 16.00, 192.00), Z Min 673.00, Z Max -296.00, Rotated 0

你应该会注意到这里并没有提供 OV区域 的大小,而 OV区域 的大小是我们最终计算出 P4 所必需的。

为此,引擎提供了 Zoom 参数。它的计算方法如下:

zoom.x = 世界区域.width ÷ overview.width
zoom.y = 世界区域.height ÷ overview.height

代入参考图中的数据:

zoom.x = 8192 ÷ 3000  = 2.73
zoom.y = 8192 ÷ 3000 = 2.73

我们已经知道 世界区域 的大小,因此计算出 OV区域 的大小非常容易:

overview.width = 世界区域.width ÷ zoom.x
overview.height = 世界区域.height ÷ zoom.y

代入参考图中的数据:

overview.width = 8192 ÷ 2.73  = 3000
overview.height = 8192 ÷ 2.73 = 3000

OV区域的中心点

OV区域 的 中心点 (即 ORIGIN )也是必须的,所以引擎提供了这个值:

Overview: Zoom 1.57, Map Origin (-248.00, 16.00, 192.00), Z Min 673.00, Z Max -296.00, Rotated 0

编写代码

有了计算方法,和必需的已知条件,我们就可以开始写代码了。

定义一个结构体 overview_t 来组织 OV图 的数据:

typedef struct {
GLuint textureId; // GL纹理ID
GLuint width; // OV图宽度
GLuint height; // OV图高度
GLfloat zoom; // 用于计算OV区域大小
GLfloat originX; // OV区域中心点X坐标
GLfloat originY; // OV区域中心点Y坐标
} overview_t;

定义一个变量 g_overview 来存储 OV图 的数据:

overview_t g_overview;

void loadOverviewImage() {
// 这两个函数请自己搞定
loadTexture("overviews/cs_italy.tga", &g_overview.textureId, &g_overview.width, &g_overview.height);
loadInfo("overviews/cs_italy.txt", &g_overview.zoom, &g_overview.originX, &g_overview.originY);
}

将 OV图 绘制到HUD上:

void HUD_Redraw() {
gExportfuncs.HUD_Redraw(); RECT rc;
rc.left = ;
rc.top = ;
rc.right = rc.left + g_overview.width;
rc.bottom = rc.top + g_overview.height; glBindTexture(GL_TEXTURE_2D, g_overview.textureId);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
glBegin(GL_QUADS);
// -------- 左上角 -------
glTexCoord2f(0.0f, 0.0f);
glVertex2f(rc.left, rc.top);
// -------- 右上角 -------
glTexCoord2f(1.0f, 0.0f);
glVertex2f(rc.right, rc.top);
// -------- 右下角 -------
glTexCoord2f(1.0f, 1.0f);
glVertex2f(rc.right, rc.bottom);
// -------- 左下角 -------
glTexCoord2f(0.0f, 1.0f);
glVertex2f(rc.left, rc.bottom);
glEnd();
}

计算 OV区域 大小:

typedef struct {
GLfloat width;
GLfloat height;
} SIZE_t; SIZE_t overview_size;
overview_size.width = 8192.0f / g_overview.zoom;
overview_size.height = 8192.0f / g_overview.zoom / 1.3333; // 4÷3=1.3333

你应该注意到了计算 OV区域 高度时,额外再除了一个 1.3333 ,这是因为引擎只给出了 zoom.x 和 zoom.y 的其中一个。

引擎总是认为,游戏窗口的宽度一定会大于高度(比例是4:3),而 OV区域 总是正方形。

为了保证生成 OV区域 显示在游戏窗口中不会变形(把正方形拉成长方形显示肯定会变形呀),实际上缩小 OV区域 的高度时会缩得比宽度更多一点。

所以我们计算 OV区域 的高度时,也要这么做。

补充:无论实际 游戏窗口 的宽高是多少,显示 OV区域 时,引擎都始终认为宽高比例是 : ,所以写固定的 1.333 就行了。如果你用宽屏模式去查看 OV区域 ,将会是变形的(被拉宽了)。

计算缩小比例:

float scaleX = overview_size.width / g_overview.width;
float scaleY = overview_size.height / g_overview.height;

取一个 世界坐标 来测试:

typedef struct {
GLfloat x;
GLfloat y;
} POINT_t; cl_entity_t* local = gEngfuncs.GetLocalPlayer(); // 取本机客户端对应的玩家实体 POINT_t P;
P.x = local->curstate.origin[]; // X
P.y = local->curstate.origin[]; // Y

将 P 的 参考点 转换为 OV区域 的 中心点 :

POINT_t P2;
P2.x = P.x - g_overview.originX;
P2.y = P.y - g_overview.originY;

将 世界坐标 的 单位 转换为 窗口坐标 的 单位 :

POINT_t P3;
P3.x = P2.x / scaleX;
P3.y = P2.y / scaleY;

计算 窗口坐标 的 中心点 :

POINT_t overview_image_origin;
overview_image_origin.x = g_overview.width / 2.0f;
overview_image_origin.y = g_overview.height / 2.0f;

将 P3 的 参考点 转换为 窗口坐标 的 中心点 :

POINT_t P4;
P4.x = overview_image_origin.x + P3.x;
P4.y = overview_image_origin.y - P3.y;

绘制 P4 到HUD上:

gEngfuncs.pfnFillRGBA(P4.x - , P4.y - ,  // X,Y
, , // width,height
, , , ); // R,G,B,A

参考代码

typedef struct {
GLfloat x;
GLfloat y;
} POINT_t; typedef struct {
GLfloat width;
GLfloat height;
} SIZE_t; typedef struct {
GLuint textureId; // OV图文理ID
GLuint width; // OV图宽度
GLuint height; // OV图高度
GLfloat zoom; // 用于计算OV区域大小
GLfloat originX; // OV区域中心点X坐标
GLfloat originY; // OV区域中心点Y坐标
bool rotated; // OV区域是否需要旋转
} overview_t; overview_t g_overview = { }; void HUD_Init(void)
{
gExportfuncs.HUD_Init(); LoadTexture("overviews/cs_siege.tga",
&g_overview.textureId,
&g_overview.width,
&g_overview.height); LoadInfo("overviews/cs_siege.txt",
&g_overview.zoom,
&g_overview.originX,
&g_overview.originY,
&g_overview.rotated);
} int HUD_Redraw(float time, int intermission)
{
gExportfuncs.HUD_Redraw(time, intermission); RECT rc;
rc.left = ;
rc.top = ;
rc.right = rc.left + g_overview.width;
rc.bottom = rc.top + g_overview.height; // ------------- 把OV图绘制到HUD上 -------------
glBindTexture(GL_TEXTURE_2D, g_overview.textureId);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
glBegin(GL_QUADS);
// -------- 左上角 -------
glTexCoord2f(0.0f, 0.0f);
glVertex2f(rc.left, rc.top);
// -------- 右上角 -------
glTexCoord2f(1.0f, 0.0f);
glVertex2f(rc.right, rc.top);
// -------- 右下角 -------
glTexCoord2f(1.0f, 1.0f);
glVertex2f(rc.right, rc.bottom);
// -------- 左下角 -------
glTexCoord2f(0.0f, 1.0f);
glVertex2f(rc.left, rc.bottom);
glEnd(); // -------------- 计算OV区域大小 ---------------
SIZE_t overview_size;
overview_size.width = 8192.0f / g_overview.zoom;
overview_size.height = 8192.0f / g_overview.zoom / 1.3333f; // --------------- 计算缩小比例 ----------------
float scaleX = overview_size.width / g_overview.width;
float scaleY = overview_size.height / g_overview.height; // --------------- 取自己的坐标 ----------------
cl_entity_t* local = gEngfuncs.GetLocalPlayer();
POINT_t P;
P.x = local->curstate.origin[];
P.y = local->curstate.origin[]; // ------- 将P的参考点转为OV区域的中心点 --------
POINT_t P2;
P2.x = P.x - g_overview.originX;
P2.y = P.y - g_overview.originY; // ------- 将P2的坐标单位转为窗口坐标单位 -------
POINT_t P3;
P3.x = P2.x / scaleX;
P3.y = P2.y / scaleY; // -------------- 计算OV图中心点 ---------------
POINT_t overview_image_origin;
overview_image_origin.x = g_overview.width / 2.0f;
overview_image_origin.y = g_overview.height / 2.0f; // -------- 将P3的参考点转为OV图的中心点 --------
POINT_t P4;
if (g_overview.rotated) {
P4.x = overview_image_origin.x + (P3.x);
P4.y = overview_image_origin.y + (-P3.y);
} else {
P4.x = overview_image_origin.x + (-P3.y);
P4.y = overview_image_origin.y + (-P3.x);
} // -------------- 把P4绘制到HUD上 --------------
gEngfuncs.pfnFillRGBA(rc.left + (P4.x - ), rc.top + (P4.y - ), // X,Y
, , // width,height
, , , ); // R,G,B,A return ;
}

载入HL的OV的配置文件

bool LoadOverviewInfo(const char* fileName, overview_t* data) {
char* buffer = (char*)gEngfuncs.COM_LoadFile((char*)fileName, , nullptr);
if (!buffer) {
return false;
}
char* parsePos = buffer;
char token[];
bool parseSuccess = false;
while (true) {
parsePos = gEngfuncs.COM_ParseFile(parsePos, token);
if (!parsePos) {
break;
}
if (!stricmp(token, "global")) {
parsePos = gEngfuncs.COM_ParseFile(parsePos, token);
if (!parsePos) {
goto error;
}
if (strcmp(token, "{")) {
goto error;
}
while (true) {
parsePos = gEngfuncs.COM_ParseFile(parsePos, token);
if (!parsePos) {
goto error;
}
if (!stricmp(token, "zoom")) {
parsePos = gEngfuncs.COM_ParseFile(parsePos, token);
data->zoom = atof(token);
}
else if (!stricmp(token, "origin")) {
parsePos = gEngfuncs.COM_ParseFile(parsePos, token);
data->originX = atof(token);
parsePos = gEngfuncs.COM_ParseFile(parsePos, token);
data->originY = atof(token);
parsePos = gEngfuncs.COM_ParseFile(parsePos, token);
}
else if (!stricmp(token, "rotated")) {
parsePos = gEngfuncs.COM_ParseFile(parsePos, token);
data->rotated = atoi(token) != ;
}
else if (!stricmp(token, "}")) {
break;
}
else {
goto error;
}
}
}
else if (!stricmp(token, "layer")) {
parsePos = gEngfuncs.COM_ParseFile(parsePos, token);
if (!parsePos) {
goto error;
}
if (strcmp(token, "{")) {
goto error;
}
while (true) {
parsePos = gEngfuncs.COM_ParseFile(parsePos, token);
if (!stricmp(token, "image")) {
parsePos = gEngfuncs.COM_ParseFile(parsePos, token);
strcpy(data->image, token);
}
else if (!stricmp(token, "height")) {
parsePos = gEngfuncs.COM_ParseFile(parsePos, token);
}
else if (!stricmp(token, "}")) {
break;
}
else {
goto error;
}
}
}
else {
goto error;
}
}
parseSuccess = true;
error:
if (buffer) {
gEngfuncs.COM_FreeFile(buffer);
}
return parseSuccess;
}
05-02 10:09