上一篇:输入子系统 | 下一篇:Vulkan 扩展和验证层 | 返回目录
📚 快速导航
目录 (点击展开/折叠)🎯 本章目标
通过本教程,你将学会:
| 🎯 目标 | 📝 描述 | ✅ 成果 |
|---|---|---|
| 时钟系统 | 实现高精度时间测量 | 掌握 delta time 计算 |
| 渲染架构 | 设计分层渲染系统 | 理解前端/后端分离 |
| API 抽象 | 在 C 中实现多态 | 支持多种图形 API |
| Vulkan 初始化 | 创建 Vulkan 实例 | 完成基础后端搭建 |
| 主循环集成 | 将渲染器集成到游戏循环 | 运行完整的渲染流程 |
📖 教程概述
在游戏引擎中,时间管理和渲染系统是两个最核心的组件。时间管理负责精确控制游戏逻辑的更新频率,而渲染系统负责将游戏世界呈现到屏幕上。
本章我们将实现:
- 时钟系统(Clock System) - 用于计算帧间隔时间(delta time)和性能测量
- 渲染器架构(Renderer Architecture) - 采用分层设计,支持多种图形 API
- 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 表示时钟未运行
}
关键点解析:
-
依赖平台层:
clock->elapsed = platform_get_absolute_time() - clock->start_time;- 使用
platform_get_absolute_time()获取系统时间 - 跨平台:Windows 用
QueryPerformanceCounter,Linux 用clock_gettime
- 使用
-
停止检测:
if (clock->start_time != 0) { // 只有运行中的时钟才更新start_time == 0表示时钟已停止
-
重置逻辑:
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;
}
设计模式分析:
-
单例模式:
static renderer_backend* backend = 0;- 全局唯一的后端实例
-
工厂模式:
renderer_backend_create(RENDERER_BACKEND_TYPE_VULKAN, ...);- 根据类型创建对应的后端
-
代理模式:
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 概念 | 作用 |
|---|---|
VkInstance | Vulkan 库的全局句柄,所有操作的入口点 |
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
🎯 帧率限制原理
为什么要限制帧率?
- 节省电力:移动设备或笔记本电脑
- 避免画面撕裂:与显示器刷新率同步
- 减少 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);
最佳实践
-
时间单位统一:
// 统一使用秒(f64),避免毫秒和秒混用 f64 delta_time; // 秒 -
帧率无关性测试:
// 测试代码:人为降低帧率 platform_sleep(50); // 强制 20 FPS // 游戏速度应该不变 -
后端接口完整性:
// 确保所有后端实现所有函数 typedef struct renderer_backend { b8 (*initialize)(...); // 必须实现 void (*shutdown)(...); // 必须实现 // ... } renderer_backend; -
错误处理:
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 后端
步骤:
- 创建
opengl_backend.h和opengl_backend.c - 实现
opengl_renderer_backend_initialize等函数 - 在
renderer_backend_create中添加 OpenGL 分支 - 测试切换后端
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.1ms | 0.6% |
| 游戏逻辑更新 | 3.0ms | 18% |
| 渲染器绘制 | 10.0ms | 60% |
| Vsync 等待 | 3.5ms | 21% |
| 总计 | 16.6ms | 100% |
优化建议
-
减少 CPU 负载:
- 使用对象池避免频繁分配
- 优化碰撞检测算法
- 多线程处理物理/AI
-
减少 GPU 负载:
- 批处理渲染调用
- 使用实例化渲染
- 剔除不可见物体
-
平衡 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 设备和交换链!
📚 参考资源
官方文档
- Vulkan Tutorial - Vulkan 入门教程
- Vulkan Specification - Vulkan 官方规范
- Game Programming Patterns - 游戏设计模式
推荐阅读
- 《Game Engine Architecture》 - Jason Gregory
- 《Real-Time Rendering》 - Tomas Akenine-Möller
- 《Vulkan Programming Guide》 - Graham Sellers
相关主题
📖 关注公众号
关注[shangshoushiyanshi],领取章节视频教程
💖 支持作者
如果这篇教程对你有帮助,欢迎请作者喝杯咖啡 ☕
您的支持是我持续创作的动力!
感谢每一位支持者!🙏
📅 最后更新:2025-11-20
✍️ 作者:上手实验室
1107

被折叠的 条评论
为什么被折叠?



