【HLSDK系列】overview(俯视图)

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

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

什么是overview?

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

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

overview类似与顶视图。

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

你会看到游戏画面变成了类似上图这样,这正是当前地图的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图,我们有必要了解一下它是怎么生成的。

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

上图坐标系中:

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

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

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

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

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

假设 游戏窗口 大小(不包含窗口的边框)为 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 才是 中心点 。

然后我们把 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的坐标 如下:

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

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

窗口坐标最小值: 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

如下图:

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

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

代入参考图中的数据:

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

得到最终P4的坐标:

因为 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 = 0;
    rc.top = 0;
    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区域 时,引擎都始终认为宽高比例是 4:3 ,所以写固定的 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[0]; // X
P.y = local->curstate.origin[1]; // 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 - 4, P4.y - 4,  // X,Y
                      8, 8,                // width,height
                      255, 255, 255, 255); // 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 = { 0 };

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 = 0;
    rc.top = 0;
    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[0];
    P.y = local->curstate.origin[1];

    // ------- 将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 - 4), rc.top + (P4.y - 4),  // X,Y
                          8, 8,                                       // width,height
                          255, 255, 255, 255);                        // R,G,B,A
    
    return 1;
}

 载入HL的OV的配置文件

bool LoadOverviewInfo(const char* fileName, overview_t* data) {
    char* buffer = (char*)gEngfuncs.COM_LoadFile((char*)fileName, 5, nullptr);
    if (!buffer) {
        return false;
    }
    char* parsePos = buffer;
    char token[128];
    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) != 0;
                }
                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;
}

 

posted @ 2018-08-08 11:04  Akatsuki-  阅读(1661)  评论(0编辑  收藏  举报