教程 09 - 时钟和渲染器脚手架

上一篇:输入子系统 | 下一篇:Vulkan 扩展和验证层 | 返回目录


📚 快速导航

目录 (点击展开/折叠)

🎯 本章目标

通过本教程,你将学会:

🎯 目标📝 描述✅ 成果
时钟系统实现高精度时间测量掌握 delta time 计算
渲染架构设计分层渲染系统理解前端/后端分离
API 抽象在 C 中实现多态支持多种图形 API
Vulkan 初始化创建 Vulkan 实例完成基础后端搭建
主循环集成将渲染器集成到游戏循环运行完整的渲染流程

📖 教程概述

在游戏引擎中,时间管理渲染系统是两个最核心的组件。时间管理负责精确控制游戏逻辑的更新频率,而渲染系统负责将游戏世界呈现到屏幕上。

本章我们将实现:

  1. 时钟系统(Clock System) - 用于计算帧间隔时间(delta time)和性能测量
  2. 渲染器架构(Renderer Architecture) - 采用分层设计,支持多种图形 API
  3. Vulkan 后端初始化 - 作为具体渲染后端的实现示例

学习重点

  • 如何设计高精度的时钟系统
  • 如何使用分层架构隔离渲染 API
  • 如何在 C 语言中实现"接口"和"多态"
  • 游戏主循环中的时间管理
  • Vulkan 的基本初始化流程

⏰ 时钟系统设计

为什么需要时钟系统?

在游戏开发中,我们需要:

  • 帧间隔时间(Delta Time):让游戏逻辑与帧率解耦
  • 性能测量:统计某段代码的执行时间
  • 定时器功能:实现倒计时、冷却时间等

时钟结构设计

// engine/src/core/clock.h
typedef struct clock {
    f64 start_time;  // 时钟启动时刻
    f64 elapsed;     // 已流逝的时间(秒)
} clock;

void clock_update(clock* clock);  // 更新时钟(计算流逝时间)
void clock_start(clock* clock);   // 启动时钟
void clock_stop(clock* clock);    // 停止时钟

设计要点

  • start_time 记录时钟启动的绝对时间戳
  • elapsed 存储从启动到现在的时间差
  • 使用 f64(双精度浮点数)保证高精度

时钟实现

// engine/src/core/clock.c
#include "clock.h"
#include "platform/platform.h"

void clock_update(clock* clock) {
    if (clock->start_time != 0) {
        clock->elapsed = platform_get_absolute_time() - clock->start_time;
    }
}

void clock_start(clock* clock) {
    clock->start_time = platform_get_absolute_time();
    clock->elapsed = 0;
}

void clock_stop(clock* clock) {
    clock->start_time = 0;  // 设为 0 表示时钟未运行
}

关键点解析

  1. 依赖平台层

    clock->elapsed = platform_get_absolute_time() - clock->start_time;
    
    • 使用 platform_get_absolute_time() 获取系统时间
    • 跨平台:Windows 用 QueryPerformanceCounter,Linux 用 clock_gettime
  2. 停止检测

    if (clock->start_time != 0) {  // 只有运行中的时钟才更新
    
    • start_time == 0 表示时钟已停止
  3. 重置逻辑

    clock_start() {
        clock->elapsed = 0;  // 重置流逝时间
    }
    

使用示例

// 测量函数执行时间
clock performance_clock;
clock_start(&performance_clock);

expensive_function();

clock_update(&performance_clock);
KINFO("Function took %.3f seconds", performance_clock.elapsed);

🏗️ 渲染器分层架构

为什么需要分层?

游戏引擎需要支持多种图形 API(Vulkan、OpenGL、DirectX)。如果直接在引擎中使用具体 API,会导致:

  • ❌ 代码高度耦合,难以切换 API
  • ❌ 无法在不同平台使用最优 API
  • ❌ 测试困难(无法 mock 渲染器)

解决方案:采用分层架构

┌─────────────────────────────────────┐
│      引擎其他模块                    │
│   (Application, Scene, etc.)        │
└────────────┬────────────────────────┘
             │
             ▼
┌─────────────────────────────────────┐
│   Renderer Frontend (前端接口)       │  ← 引擎看到的统一 API
│   renderer_initialize()              │
│   renderer_draw_frame()              │
└────────────┬────────────────────────┘
             │
             ▼
┌─────────────────────────────────────┐
│   Renderer Backend (后端接口)        │  ← 抽象层,定义函数指针
│   typedef struct renderer_backend {  │
│       b8 (*initialize)(...);         │
│       b8 (*begin_frame)(...);        │
│   } renderer_backend;                │
└────────────┬────────────────────────┘
             │
       ┌─────┴──────┬──────────┐
       ▼            ▼          ▼
┌──────────┐  ┌──────────┐  ┌──────────┐
│ Vulkan   │  │ OpenGL   │  │ DirectX  │  ← 具体实现
│ Backend  │  │ Backend  │  │ Backend  │
└──────────┘  └──────────┘  └──────────┘

分层优势

  • ✅ 引擎代码与图形 API 解耦
  • ✅ 轻松添加新后端
  • ✅ 可以在运行时切换后端
  • ✅ 便于测试和调试

📐 渲染器类型定义

后端类型枚举

// engine/src/renderer/renderer_types.inl
typedef enum renderer_backend_type {
    RENDERER_BACKEND_TYPE_VULKAN,
    RENDERER_BACKEND_TYPE_OPENGL,
    RENDERER_BACKEND_TYPE_DIRECTX
} renderer_backend_type;

渲染数据包

typedef struct render_packet {
    f32 delta_time;  // 帧间隔时间
} render_packet;

设计意图

  • render_packet 封装渲染一帧所需的所有数据
  • 目前只有 delta_time,后续会添加:
    • 相机矩阵
    • 要渲染的物体列表
    • 光照信息

后端接口(核心设计)

typedef struct renderer_backend {
    struct platform_state* plat_state;  // 平台状态(窗口句柄等)
    u64 frame_number;                   // 当前帧号

    // 函数指针 - 实现"接口"的关键
    b8 (*initialize)(struct renderer_backend* backend,
                     const char* application_name,
                     struct platform_state* plat_state);

    void (*shutdown)(struct renderer_backend* backend);

    void (*resized)(struct renderer_backend* backend,
                    u16 width, u16 height);

    b8 (*begin_frame)(struct renderer_backend* backend,
                      f32 delta_time);

    b8 (*end_frame)(struct renderer_backend* backend,
                    f32 delta_time);
} renderer_backend;

函数指针详解

函数指针作用返回值
initialize初始化后端(创建设备、交换链等)成功返回 TRUE
shutdown清理后端资源
resized处理窗口大小改变
begin_frame开始渲染一帧(获取交换链图像、开始记录命令)成功返回 TRUE
end_frame结束渲染一帧(提交命令缓冲区、呈现到屏幕)成功返回 TRUE

🎨 渲染器前端实现

前端是引擎其他模块使用的统一 API,内部持有后端指针并转发调用。

// engine/src/renderer/renderer_frontend.c
#include "renderer_frontend.h"
#include "renderer_backend.h"
#include "core/logger.h"
#include "core/kmemory.h"

// 静态全局后端指针
static renderer_backend* backend = 0;

b8 renderer_initialize(const char* application_name,
                       struct platform_state* plat_state) {
    // 分配后端结构体
    backend = kallocate(sizeof(renderer_backend), MEMORY_TAG_RENDERER);

    // TODO: 后续从配置文件读取
    renderer_backend_create(RENDERER_BACKEND_TYPE_VULKAN, plat_state, backend);
    backend->frame_number = 0;

    // 调用后端的初始化函数
    if (!backend->initialize(backend, application_name, plat_state)) {
        KFATAL("Renderer backend failed to initialize. Shutting down.");
        return FALSE;
    }

    return TRUE;
}

void renderer_shutdown() {
    backend->shutdown(backend);
    kfree(backend, sizeof(renderer_backend), MEMORY_TAG_RENDERER);
}

b8 renderer_begin_frame(f32 delta_time) {
    return backend->begin_frame(backend, delta_time);
}

b8 renderer_end_frame(f32 delta_time) {
    b8 result = backend->end_frame(backend, delta_time);
    backend->frame_number++;  // 帧号递增
    return result;
}

b8 renderer_draw_frame(render_packet* packet) {
    // 开始帧
    if (renderer_begin_frame(packet->delta_time)) {
        // 这里会插入实际的渲染命令

        // 结束帧
        b8 result = renderer_end_frame(packet->delta_time);
        if (!result) {
            KERROR("renderer_end_frame failed. Application shutting down...");
            return FALSE;
        }
    }

    return TRUE;
}

设计模式分析

  1. 单例模式

    static renderer_backend* backend = 0;
    
    • 全局唯一的后端实例
  2. 工厂模式

    renderer_backend_create(RENDERER_BACKEND_TYPE_VULKAN, ...);
    
    • 根据类型创建对应的后端
  3. 代理模式

    b8 renderer_begin_frame(...) {
        return backend->begin_frame(backend, delta_time);
    }
    
    • 前端只是转发调用到后端

🔧 渲染器后端接口

后端接口负责创建和销毁后端实例(工厂)。

// engine/src/renderer/renderer_backend.c
#include "renderer_backend.h"
#include "vulkan/vulkan_backend.h"

b8 renderer_backend_create(renderer_backend_type type,
                           struct platform_state* plat_state,
                           renderer_backend* out_renderer_backend) {
    out_renderer_backend->plat_state = plat_state;

    if (type == RENDERER_BACKEND_TYPE_VULKAN) {
        // 绑定 Vulkan 后端的函数指针
        out_renderer_backend->initialize = vulkan_renderer_backend_initialize;
        out_renderer_backend->shutdown = vulkan_renderer_backend_shutdown;
        out_renderer_backend->begin_frame = vulkan_renderer_backend_begin_frame;
        out_renderer_backend->end_frame = vulkan_renderer_backend_end_frame;
        out_renderer_backend->resized = vulkan_renderer_backend_on_resized;

        return TRUE;
    }

    // 后续可以添加 OpenGL、DirectX 等
    return FALSE;
}

void renderer_backend_destroy(renderer_backend* renderer_backend) {
    // 清空函数指针
    renderer_backend->initialize = 0;
    renderer_backend->shutdown = 0;
    renderer_backend->begin_frame = 0;
    renderer_backend->end_frame = 0;
    renderer_backend->resized = 0;
}

C 语言实现"多态"

在 C++ 中,多态通过虚函数表实现:

class RendererBackend {
    virtual bool Initialize() = 0;
};

class VulkanBackend : public RendererBackend {
    bool Initialize() override { /* Vulkan 实现 */ }
};

在 C 中,我们手动创建"虚函数表":

// 1. 定义"接口"(函数指针)
typedef struct renderer_backend {
    b8 (*initialize)(...);  // 相当于虚函数
} renderer_backend;

// 2. "派生类"实现具体函数
b8 vulkan_renderer_backend_initialize(...) { /* Vulkan 实现 */ }

// 3. "构造函数"中绑定函数指针(相当于设置虚函数表)
out_renderer_backend->initialize = vulkan_renderer_backend_initialize;

// 4. 调用时通过函数指针间接调用(动态分派)
backend->initialize(backend, ...);

优势

  • 无需修改前端代码,就能添加新后端
  • 可以在运行时切换后端

🌋 Vulkan 后端实现

Vulkan 类型定义

// engine/src/renderer/vulkan/vulkan_types.inl
#include <vulkan/vulkan.h>

typedef struct vulkan_context {
    VkInstance instance;                 // Vulkan 实例
    VkAllocationCallbacks* allocator;    // 自定义内存分配器
} vulkan_context;

Vulkan 后端初始化

// engine/src/renderer/vulkan/vulkan_backend.c
#include "vulkan_backend.h"
#include "vulkan_types.inl"
#include "core/logger.h"

// 静态全局 Vulkan 上下文
static vulkan_context context;

b8 vulkan_renderer_backend_initialize(renderer_backend* backend,
                                      const char* application_name,
                                      struct platform_state* plat_state) {
    // TODO: 后续实现自定义分配器
    context.allocator = 0;

    // 1. 填充应用信息
    VkApplicationInfo app_info = {VK_STRUCTURE_TYPE_APPLICATION_INFO};
    app_info.apiVersion = VK_API_VERSION_1_2;
    app_info.pApplicationName = application_name;
    app_info.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
    app_info.pEngineName = "Kohi Engine";
    app_info.engineVersion = VK_MAKE_VERSION(1, 0, 0);

    // 2. 填充实例创建信息
    VkInstanceCreateInfo create_info = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
    create_info.pApplicationInfo = &app_info;
    create_info.enabledExtensionCount = 0;      // 暂无扩展
    create_info.ppEnabledExtensionNames = 0;
    create_info.enabledLayerCount = 0;          // 暂无验证层
    create_info.ppEnabledLayerNames = 0;

    // 3. 创建 Vulkan 实例
    VkResult result = vkCreateInstance(&create_info,
                                       context.allocator,
                                       &context.instance);
    if (result != VK_SUCCESS) {
        KERROR("vkCreateInstance failed with result: %u", result);
        return FALSE;
    }

    KINFO("Vulkan renderer initialized successfully.");
    return TRUE;
}

void vulkan_renderer_backend_shutdown(renderer_backend* backend) {
    // TODO: 销毁 Vulkan 实例
}

void vulkan_renderer_backend_on_resized(renderer_backend* backend,
                                        u16 width, u16 height) {
    // TODO: 重建交换链
}

b8 vulkan_renderer_backend_begin_frame(renderer_backend* backend,
                                       f32 delta_time) {
    return TRUE;  // 暂时空实现
}

b8 vulkan_renderer_backend_end_frame(renderer_backend* backend,
                                     f32 delta_time) {
    return TRUE;  // 暂时空实现
}

Vulkan 初始化流程

┌────────────────────────────────────┐
│  1. 创建 VkApplicationInfo          │
│     - 应用名称和版本                 │
│     - 引擎名称和版本                 │
│     - Vulkan API 版本               │
└──────────┬─────────────────────────┘
           │
           ▼
┌────────────────────────────────────┐
│  2. 创建 VkInstanceCreateInfo       │
│     - 应用信息指针                   │
│     - 启用的扩展列表                 │
│     - 启用的验证层列表               │
└──────────┬─────────────────────────┘
           │
           ▼
┌────────────────────────────────────┐
│  3. 调用 vkCreateInstance           │
│     - 创建 VkInstance 实例          │
│     - 检查返回值                     │
└────────────────────────────────────┘

关键概念

Vulkan 概念作用
VkInstanceVulkan 库的全局句柄,所有操作的入口点
VkApplicationInfo应用和引擎的元数据
Extensions额外功能(如窗口表面、调试工具)
Validation Layers开发期的错误检查和性能警告

🎮 游戏主循环集成

Application 状态扩展

// engine/src/core/application.c
typedef struct application_state {
    game* game_inst;
    b8 is_running;
    b8 is_suspended;
    platform_state platform;
    i16 width;
    i16 height;
    clock clock;        // 新增:时钟
    f64 last_time;      // 新增:上一帧的时间
} application_state;

初始化渲染器

b8 application_create(game* game_inst) {
    // ... 其他初始化 ...

    // 启动渲染器
    if (!renderer_initialize(game_inst->app_config.name, &app_state.platform)) {
        KFATAL("Failed to initialize renderer. Aborting application.");
        return FALSE;
    }

    // ... 游戏初始化 ...

    return TRUE;
}

主循环时间管理

b8 application_run() {
    // 启动主时钟
    clock_start(&app_state.clock);
    clock_update(&app_state.clock);
    app_state.last_time = app_state.clock.elapsed;

    f64 running_time = 0;
    u8 frame_count = 0;
    f64 target_frame_seconds = 1.0f / 60;  // 目标 60 FPS

    while (app_state.is_running) {
        if (!platform_pump_messages(&app_state.platform)) {
            app_state.is_running = FALSE;
        }

        if (!app_state.is_suspended) {
            // 1. 更新时钟,计算 delta time
            clock_update(&app_state.clock);
            f64 current_time = app_state.clock.elapsed;
            f64 delta = (current_time - app_state.last_time);
            f64 frame_start_time = platform_get_absolute_time();

            // 2. 更新游戏逻辑
            if (!app_state.game_inst->update(app_state.game_inst, (f32)delta)) {
                KFATAL("Game update failed, shutting down.");
                app_state.is_running = FALSE;
                break;
            }

            // 3. 渲染游戏画面
            if (!app_state.game_inst->render(app_state.game_inst, (f32)delta)) {
                KFATAL("Game render failed, shutting down.");
                app_state.is_running = FALSE;
                break;
            }

            // 4. 调用渲染器绘制帧
            render_packet packet;
            packet.delta_time = delta;
            renderer_draw_frame(&packet);

            // 5. 帧率限制
            f64 frame_end_time = platform_get_absolute_time();
            f64 frame_elapsed_time = frame_end_time - frame_start_time;
            running_time += frame_elapsed_time;
            f64 remaining_seconds = target_frame_seconds - frame_elapsed_time;

            if (remaining_seconds > 0) {
                u64 remaining_ms = (remaining_seconds * 1000);

                b8 limit_frames = FALSE;
                if (remaining_ms > 0 && limit_frames) {
                    platform_sleep(remaining_ms - 1);
                }

                frame_count++;
            }

            // 6. 更新输入状态(双缓冲交换)
            input_update(delta);

            // 7. 更新上一帧时间
            app_state.last_time = current_time;
        }
    }

    // 关闭渲染器
    renderer_shutdown();

    return TRUE;
}

主循环详解

每一帧的执行顺序:

┌──────────────────────┐
│ 1. 更新时钟           │  clock_update() -> 计算 delta
│    计算 delta time    │  delta = current - last
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│ 2. 更新游戏逻辑       │  game->update(delta)
│    物理、AI、动画...  │  使用 delta 让速度与帧率无关
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│ 3. 渲染游戏          │  game->render(delta)
│    生成渲染命令       │  准备要绘制的数据
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│ 4. 渲染器绘制帧       │  renderer_draw_frame()
│    begin -> end       │  提交到 GPU
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│ 5. 帧率限制          │  如果帧渲染太快,睡眠等待
│    保持 60 FPS       │  platform_sleep()
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│ 6. 更新输入状态       │  input_update()
│    交换双缓冲         │  current -> previous
└──────┬───────────────┘
       │
       ▼
┌──────────────────────┐
│ 7. 保存当前时间       │  last_time = current_time
│    供下一帧计算 delta │  用于下一帧的 delta 计算
└──────────────────────┘

🔍 Delta Time 的重要性

为什么需要 Delta Time?

问题:如果直接使用固定速度更新物体:

void update() {
    player.x += 5;  // 每帧移动 5 像素
}

在不同帧率下的表现:

  • 60 FPS:每秒移动 5 * 60 = 300 像素
  • 30 FPS:每秒移动 5 * 30 = 150 像素
  • 120 FPS:每秒移动 5 * 120 = 600 像素

结果:游戏速度随帧率变化!

解决方案:使用 Delta Time

void update(f32 delta_time) {
    f32 speed = 300.0f;  // 每秒 300 像素
    player.x += speed * delta_time;
}

在不同帧率下:

  • 60 FPS (delta ≈ 0.0167s):移动 300 * 0.0167 ≈ 5 像素/帧
  • 30 FPS (delta ≈ 0.0333s):移动 300 * 0.0333 ≈ 10 像素/帧
  • 120 FPS (delta ≈ 0.0083s):移动 300 * 0.0083 ≈ 2.5 像素/帧

结果:每秒移动距离始终是 300 像素!

Delta Time 计算

// 主循环中的时间计算
clock_update(&app_state.clock);           // 更新时钟
f64 current_time = app_state.clock.elapsed;  // 从启动到现在的总时间
f64 delta = current_time - app_state.last_time;  // 两帧之间的时间差

// 使用 delta 更新游戏
game->update(game, (f32)delta);

// 保存当前时间供下一帧使用
app_state.last_time = current_time;

时间轴图示

时间轴:  0s ----0.016s----0.033s----0.050s---->
         启动     帧1       帧2       帧3

帧1: current = 0.016, last = 0.000, delta = 0.016
帧2: current = 0.033, last = 0.016, delta = 0.017
帧3: current = 0.050, last = 0.033, delta = 0.017

🎯 帧率限制原理

为什么要限制帧率?

  1. 节省电力:移动设备或笔记本电脑
  2. 避免画面撕裂:与显示器刷新率同步
  3. 减少 CPU/GPU 负载:避免不必要的计算

帧率限制实现

f64 target_frame_seconds = 1.0f / 60;  // 60 FPS = 16.67ms/帧

while (app_state.is_running) {
    f64 frame_start_time = platform_get_absolute_time();

    // ... 游戏逻辑和渲染 ...

    f64 frame_end_time = platform_get_absolute_time();
    f64 frame_elapsed_time = frame_end_time - frame_start_time;
    f64 remaining_seconds = target_frame_seconds - frame_elapsed_time;

    if (remaining_seconds > 0) {
        u64 remaining_ms = (remaining_seconds * 1000);

        // 如果启用帧率限制,睡眠剩余时间
        b8 limit_frames = FALSE;
        if (remaining_ms > 0 && limit_frames) {
            platform_sleep(remaining_ms - 1);  // -1 防止睡过头
        }
    }
}

时间分析

目标:16.67ms/帧 (60 FPS)

情况1:游戏逻辑 + 渲染 = 10ms
  剩余时间 = 16.67 - 10 = 6.67ms
  → 睡眠 6ms (留 0.67ms 余量)

情况2:游戏逻辑 + 渲染 = 20ms
  剩余时间 = 16.67 - 20 = -3.33ms
  → 不睡眠,直接进入下一帧(帧率下降到 ~50 FPS)

🧩 架构总览

整体数据流

┌─────────────────────────────────────────────────┐
│                  Application                     │
│  ┌─────────────┐          ┌─────────────┐       │
│  │    Clock    │          │    Input    │       │
│  │  (delta)    │          │  (keyboard) │       │
│  └─────┬───────┘          └──────┬──────┘       │
│        │                         │              │
│        ▼                         ▼              │
│  ┌──────────────────────────────────┐           │
│  │      Game Loop (主循环)          │           │
│  │  1. update(delta)                │           │
│  │  2. render(delta)                │           │
│  │  3. renderer_draw_frame()        │           │
│  └───────────────┬──────────────────┘           │
└──────────────────┼──────────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────────┐
│         Renderer Frontend (统一接口)              │
│  renderer_draw_frame(packet) {                   │
│      backend->begin_frame()                      │
│      backend->end_frame()                        │
│  }                                               │
└───────────────────┬──────────────────────────────┘
                    │
                    ▼
┌──────────────────────────────────────────────────┐
│         Renderer Backend (抽象层)                 │
│  typedef struct {                                │
│      b8 (*begin_frame)(...);  ← 函数指针         │
│      b8 (*end_frame)(...);                       │
│  } renderer_backend;                             │
└───────────────────┬──────────────────────────────┘
                    │
                    ▼
┌──────────────────────────────────────────────────┐
│         Vulkan Backend (具体实现)                 │
│  b8 vulkan_renderer_backend_begin_frame() {      │
│      // Vulkan 具体代码                          │
│  }                                               │
└──────────────────────────────────────────────────┘

文件组织

engine/src/
├── core/
│   ├── clock.h                  # 时钟接口
│   ├── clock.c                  # 时钟实现
│   └── application.c            # 主循环集成
├── renderer/
│   ├── renderer_types.inl       # 渲染器类型定义
│   ├── renderer_frontend.h      # 前端接口
│   ├── renderer_frontend.c      # 前端实现(转发调用)
│   ├── renderer_backend.h       # 后端接口
│   ├── renderer_backend.c       # 后端工厂
│   └── vulkan/
│       ├── vulkan_types.inl     # Vulkan 类型
│       ├── vulkan_backend.h     # Vulkan 后端接口
│       └── vulkan_backend.c     # Vulkan 后端实现

💡 设计模式总结

1. 分层架构(Layered Architecture)

┌─────────────────┐
│   Presentation  │  Frontend (对外接口)
├─────────────────┤
│    Business     │  Backend Interface (抽象层)
├─────────────────┤
│   Data Access   │  Vulkan/OpenGL/DirectX (实现层)
└─────────────────┘

优点

  • 高内聚低耦合
  • 易于测试和替换
  • 清晰的职责划分

2. 策略模式(Strategy Pattern)

// Context
renderer_backend* backend;

// Strategy Interface
typedef struct {
    b8 (*initialize)(...);
} renderer_backend;

// Concrete Strategy
b8 vulkan_renderer_backend_initialize(...) { /* Vulkan 实现 */ }
b8 opengl_renderer_backend_initialize(...) { /* OpenGL 实现 */ }

// 运行时选择策略
if (type == VULKAN) {
    backend->initialize = vulkan_renderer_backend_initialize;
}

3. 工厂模式(Factory Pattern)

// 工厂函数
b8 renderer_backend_create(renderer_backend_type type, ...) {
    if (type == RENDERER_BACKEND_TYPE_VULKAN) {
        // 创建 Vulkan 后端
    } else if (type == RENDERER_BACKEND_TYPE_OPENGL) {
        // 创建 OpenGL 后端
    }
}

4. 门面模式(Facade Pattern)

// 复杂子系统
backend->begin_frame()
backend->end_frame()

// 简单门面
b8 renderer_draw_frame(render_packet* packet) {
    if (renderer_begin_frame(packet->delta_time)) {
        renderer_end_frame(packet->delta_time);
    }
}

⚠️ 常见问题与最佳实践

问题 1:时钟精度不足

症状

f64 delta = current_time - last_time;
// delta 总是 0.015 或 0.016,没有更精细的值

原因

  • Windows timeGetTime() 精度只有 15ms
  • Sleep() 精度不高

解决

// 使用高精度计时器
#ifdef KPLATFORM_WINDOWS
    LARGE_INTEGER frequency, counter;
    QueryPerformanceFrequency(&frequency);
    QueryPerformanceCounter(&counter);
    return (f64)counter.QuadPart / (f64)frequency.QuadPart;
#endif

问题 2:Delta Time 突变

症状

// 正常情况:delta ≈ 0.016s (60 FPS)
// 突然:delta = 2.5s(窗口最小化、调试断点)

危险

player.x += speed * delta;  // delta = 2.5,玩家瞬移!

解决

// 限制最大 delta
const f32 MAX_DELTA = 0.1f;  // 最大 100ms
delta = MIN(delta, MAX_DELTA);

问题 3:帧率不稳定

症状

帧率:60 -> 55 -> 62 -> 58 -> ...(抖动)

原因

  • 操作系统调度不稳定
  • V-Sync 未启用
  • Sleep() 不够精确

解决

// 1. 启用 V-Sync(Vulkan 中的 FIFO 模式)
swapchain_info.presentMode = VK_PRESENT_MODE_FIFO_KHR;

// 2. 使用更精确的睡眠
if (remaining_ms > 1) {
    platform_sleep(remaining_ms - 1);
}
// 剩余时间忙等待
while (platform_get_absolute_time() - frame_start_time < target_frame_seconds);

问题 4:后端切换失败

症状

// 切换到 OpenGL 后崩溃
renderer_backend_create(RENDERER_BACKEND_TYPE_OPENGL, ...);

原因

  • 函数指针未正确初始化
  • 资源未清理干净

解决

// 1. 销毁旧后端
if (backend) {
    backend->shutdown(backend);
    renderer_backend_destroy(backend);
}

// 2. 创建新后端
renderer_backend_create(new_type, ...);

// 3. 验证所有函数指针
KASSERT(backend->initialize != 0);
KASSERT(backend->begin_frame != 0);

最佳实践

  1. 时间单位统一

    // 统一使用秒(f64),避免毫秒和秒混用
    f64 delta_time;  // 秒
    
  2. 帧率无关性测试

    // 测试代码:人为降低帧率
    platform_sleep(50);  // 强制 20 FPS
    // 游戏速度应该不变
    
  3. 后端接口完整性

    // 确保所有后端实现所有函数
    typedef struct renderer_backend {
        b8 (*initialize)(...);   // 必须实现
        void (*shutdown)(...);   // 必须实现
        // ...
    } renderer_backend;
    
  4. 错误处理

    if (!backend->initialize(backend, ...)) {
        KFATAL("Renderer initialization failed");
        return FALSE;  // 不要继续运行
    }
    

🎓 实践练习

练习 1:实现 FPS 计数器

任务:显示当前帧率

提示

static u32 frame_count = 0;
static f64 elapsed_time = 0.0;

void update_fps(f64 delta_time) {
    frame_count++;
    elapsed_time += delta_time;

    if (elapsed_time >= 1.0) {  // 每秒更新一次
        f64 fps = frame_count / elapsed_time;
        KINFO("FPS: %.2f", fps);

        frame_count = 0;
        elapsed_time = 0.0;
    }
}

练习 2:实现性能测量工具

任务:创建一个 MEASURE_TIME

示例用法

MEASURE_TIME("Physics Update") {
    physics_update(delta_time);
}
// 输出:Physics Update took 2.35ms

实现提示

#define MEASURE_TIME(name) \
    for (clock _clock = {0}, *_p = &_clock; \
         _p; \
         (_p ? (clock_update(_p), \
                KINFO("%s took %.3fms", name, _clock.elapsed * 1000), \
                0) : 0), \
         _p = 0) \
    if ((clock_start(_p), 1))

练习 3:添加 OpenGL 后端

任务:仿照 Vulkan 后端,创建 OpenGL 后端

步骤

  1. 创建 opengl_backend.hopengl_backend.c
  2. 实现 opengl_renderer_backend_initialize 等函数
  3. renderer_backend_create 中添加 OpenGL 分支
  4. 测试切换后端

OpenGL 初始化示例

b8 opengl_renderer_backend_initialize(...) {
    // 创建 OpenGL 上下文
    HGLRC gl_context = wglCreateContext(dc);
    wglMakeCurrent(dc, gl_context);

    // 加载 OpenGL 函数
    gladLoadGL();

    KINFO("OpenGL renderer initialized successfully.");
    return TRUE;
}

练习 4:实现可变帧率

任务:允许用户在 30/60/120 FPS 之间切换

配置文件

{
    "target_fps": 60
}

实现

void set_target_fps(u32 fps) {
    app_state.target_frame_seconds = 1.0 / (f64)fps;
}

// 运行时切换
if (input_is_key_down(KEY_F1)) set_target_fps(30);
if (input_is_key_down(KEY_F2)) set_target_fps(60);
if (input_is_key_down(KEY_F3)) set_target_fps(120);

📊 性能分析

各阶段耗时(典型 60 FPS 游戏)

阶段耗时占比
输入处理0.1ms0.6%
游戏逻辑更新3.0ms18%
渲染器绘制10.0ms60%
Vsync 等待3.5ms21%
总计16.6ms100%

优化建议

  1. 减少 CPU 负载

    • 使用对象池避免频繁分配
    • 优化碰撞检测算法
    • 多线程处理物理/AI
  2. 减少 GPU 负载

    • 批处理渲染调用
    • 使用实例化渲染
    • 剔除不可见物体
  3. 平衡 CPU/GPU

    • CPU 占用 < 50%:增加游戏复杂度
    • GPU 占用 < 50%:提升画质

🔗 与其他系统的关系

时钟系统依赖

┌──────────┐
│  Clock   │
└────┬─────┘
     │
     ▼
┌──────────────┐
│   Platform   │  (提供 get_absolute_time)
└──────────────┘

渲染器依赖

┌────────────────┐
│   Renderer     │
└────┬───────────┘
     │
     ├──► Platform (窗口句柄)
     ├──► Memory  (内存分配)
     └──► Logger  (日志输出)

未来扩展

Clock System
    │
    ├──► Animation System (插值计算)
    ├──► Physics System (积分步进)
    ├──► Audio System (时间同步)
    └──► Particle System (生命周期)

Renderer
    │
    ├──► Material System (着色器、纹理)
    ├──► Mesh System (顶点数据)
    ├──► Camera System (视图矩阵)
    └──► Light System (光照计算)

🎯 本章总结

核心概念

概念要点
时钟系统高精度计时、delta time 计算
渲染器分层架构Frontend → Backend → 具体实现
函数指针多态C 语言实现接口和虚函数
游戏主循环更新 → 渲染 → 帧率限制
Delta Time实现帧率无关的游戏逻辑
Vulkan 初始化创建 VkInstance

关键代码

// 1. 时钟系统
clock game_clock;
clock_start(&game_clock);
clock_update(&game_clock);
f64 delta = game_clock.elapsed - last_time;

// 2. 渲染器架构
renderer_backend* backend;
renderer_backend_create(VULKAN, &backend);
backend->initialize(backend, ...);

// 3. 主循环
while (running) {
    f64 delta = update_clock();
    game_update(delta);
    game_render(delta);
    renderer_draw_frame(&packet);
    limit_frame_rate();
}

学到的技能

✅ 设计跨平台的时钟系统
✅ 使用分层架构解耦渲染 API
✅ 在 C 语言中实现多态
✅ 实现帧率无关的游戏逻辑
✅ 理解 Vulkan 的初始化流程
✅ 掌握游戏主循环的设计

下一章预告

在下一章中,我们将深入 Vulkan,实现:

  • 物理设备选择:选择合适的 GPU
  • 逻辑设备创建:创建 Vulkan 设备对象
  • 队列家族查询:获取图形队列和呈现队列
  • 交换链创建:实现双缓冲/三缓冲
  • 第一个三角形:真正在屏幕上绘制内容!

敬请期待 T10 - Vulkan 设备和交换链


📚 参考资源

官方文档

推荐阅读

  • 《Game Engine Architecture》 - Jason Gregory
  • 《Real-Time Rendering》 - Tomas Akenine-Möller
  • 《Vulkan Programming Guide》 - Graham Sellers

相关主题


📖 关注公众号

关注[shangshoushiyanshi],领取章节视频教程


💖 支持作者

如果这篇教程对你有帮助,欢迎请作者喝杯咖啡 ☕

您的支持是我持续创作的动力!

感谢每一位支持者!🙏


📅 最后更新:2025-11-20
✍️ 作者:上手实验室

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值