• 【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;
    }
  • 相关阅读:
    base64模块的使用
    14-类的结构之一:属性
    13-类和对象
    12-数组的常见异常
    11-Arrays工具类的使用
    10-二维数组
    09-一维数组
    08-数组的概述
    07-流程控制
    06-运算符
  • 原文地址:https://www.cnblogs.com/crsky/p/9441540.html
Copyright © 2020-2023  润新知