HEngine游戏引擎(01-30)

1. 观察者模式(Observe Pattern)

这种模式常见于窗口系统的设计和游戏开发领域,举个例子,如下图所示,当主角移到周围的格子时,如何做出合适的反应:
在这里插入图片描述
我第一反应想的是,在主角的移动函数里做判断,大概是这么写:

1
2
3
4
5
6
7
8
9
class Agent
{
if(agentMoved)
{
if(targetType == "袭击") ...
if(targetType == "加血") ...
if(targetType == "困住") ...
}
}

这样写的缺点有两个:

  • 角色类与加血、袭击和困住这些事件类的耦合太高
  • 如果加一个新的事件,还要去修改Agent类的代码,这不适合拓展

还有一种方法,就是加血这些事件类,每隔一段时间就判断主角是否在其范围内,这样会造成CPU消耗,也不好。
所以Observe Pattern能很好的解决这个问题,当角色触发这个事件时,能第一时间让所有可能响应的事件收到这个消息,就好像一个主播,给所有的订阅者发送推送一样。

为了降低主角类与事件类的耦合性,设置一个规定,所有主角这种类(或者说类似于主播这种会发送通知的类),需要创建一个存放所有订阅者的数据结构(ObserverList),再规定几个统一的接口,用于添加订阅,取消订阅,发送订阅,就是以下内容:
在这里插入图片描述
而所有的observer类,需要定义一个接口,作为收到消息时的响应函数,这里就叫做Update函数好了,所以写出来两个接口代码是这样是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//观察者
public interface Observer {
public void update();
}

//被观察者
abstract public class Subject {

private List<Observer> observerList = new ArrayList<Observer>();

public void attachObserver(Observer observer) {
observerList.add(observer);
}

public void detachObserver(Observer observer){
observerList.remove(observer);
}

public void notifyObservers(){
for (Observer observer: observerList){
observer.update();
}
}
}

对于这些响应的类,只要确保实现了update函数就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//陷阱
public class Trap implements Observer {
@Override
public void update() {
if(inRange()){
System.out.println("陷阱 困住主角!");
}
}

private boolean inRange(){
//判断主角是否在自己的影响范围内,这里忽略细节,直接返回true
return true;
}
}

对于主角类,只需要在移动的时候,调用notifyAllObservers就行了:

1
2
3
4
5
6
public class Hero extends Subject{
void move(){
System.out.println("主角向前移动");
notifyObservers();
}
}

而创建主播类与用户类之间连接的代码则是在运行程序里进行执行的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
// 在游戏执行时建立这些连接
public static void main(String[] args) {
//初始化对象
Hero hero = new Hero();
Monster monster = new Monster();
Trap trap = new Trap();
Treasure treasure = new Treasure();
//注册观察者
hero.attachObserver(monster);
hero.attachObserver(trap);
hero.attachObserver(treasure);
//移动事件
hero.move();
}
}

这样写代码又简洁,又容易拓展,对于设计Event系统来说非常好用.


2. Vsync(垂直同步)

Vsync(Vertical Synchronization,垂直同步) 是一种限制帧率的技术,它让 GPU 的帧渲染与显示器的刷新率同步,防止屏幕撕裂(Screen Tearing)

工作原理

  • 显示器每秒刷新固定次数,比如 60Hz 意味着每秒刷新 60 次。
  • 如果 GPU 渲染的帧率比显示器刷新率 ,屏幕可能会在一次刷新周期内接收两帧画面,导致撕裂。
  • Vsync 强制 GPU 在屏幕刷新完成后再提交新帧,从而减少撕裂,但可能导致输入延迟帧率下降

Vsync 在 OpenGL 中的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <GLFW/glfw3.h>

int main()
{
// 初始化 GLFW
if (!glfwInit()) return -1;

GLFWwindow* window = glfwCreateWindow(800, 600, "Vsync Example", NULL, NULL);
if (!window) return -1;

glfwMakeContextCurrent(window);

// 启用 Vsync(1 = 开启,0 = 关闭)
glfwSwapInterval(1);

while (!glfwWindowShouldClose(window))
{
// 渲染逻辑 ...
glfwSwapBuffers(window);
glfwPollEvents();
}

glfwTerminate();
return 0;
}

关闭 Vsync

1
glfwSwapInterval(0);

3. 回调函数(Callback Function)

回调函数 是一种间接调用的方式,允许将函数作为参数传递,从而在某些特定条件下执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

// 普通回调函数
void MyCallback(int value)
{
std::cout << "Callback called with value: " << value << std::endl;
}

// 函数作为参数
void ExecuteCallback(void (*callback)(int), int num)
{
callback(num);
}

int main()
{
ExecuteCallback(MyCallback, 42);
return 0;
}

4. std::function和std::bind

std::bindstd::function 是 C++ 中处理回调、函数指针、事件系统的重要工具,它们可以让函数更加灵活地传递、存储和调用。

std::function 是 C++ 标准库中的一个函数包装器,它可以存储、拷贝和调用任何可调用对象(如普通函数、lambda 表达式、函数指针、成员函数等)。

(1)存储普通函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <functional>

void Hello()
{
std::cout << "Hello, World!" << std::endl;
}

int main()
{
std::function<void()> func = Hello; // std::function 绑定普通函数
func(); // 调用函数
return 0;
}

(2)存储 Lambda 表达式

1
2
std::function<int(int, int)> add = [](int a, int b) { return a + b; };
std::cout << add(2, 3) << std::endl; // 输出 5

(3)存储类的成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass
{
public:
void Print() { std::cout << "Hello from MyClass!" << std::endl; }
};

int main()
{
MyClass obj;
std::function<void(MyClass&)> func = &MyClass::Print; // 绑定成员函数
func(obj); // 通过 std::function 调用成员函数
return 0;
}

std::bind 用于绑定函数及其参数,返回一个新的可调用对象,常用于回调、事件系统等。

(1)绑定普通函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <functional>

void Print(int a, int b)
{
std::cout << "a: " << a << ", b: " << b << std::endl;
}

int main()
{
auto boundFunc = std::bind(Print, 10, 20); // 绑定参数 10, 20
boundFunc(); // 输出: a: 10, b: 20
return 0;
}

(2)绑定部分参数

1
2
auto boundFunc = std::bind(Print, 10, std::placeholders::_1); 
boundFunc(99); // 绑定 a=10,b 由调用者提供,输出: a: 10, b: 99

(3)绑定成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass
{
public:
void Print(int value) { std::cout << "Value: " << value << std::endl; }
};

int main()
{
MyClass obj;
auto boundFunc = std::bind(&MyClass::Print, &obj, std::placeholders::_1);
boundFunc(42); // 输出: Value: 42
return 0;
}

5. enumenum class

普通 enum

1
2
3
4
5
6
enum Color
{
Red, // 0
Green, // 1
Blue // 2
};
  • enum 会把枚举值提升到全局作用域,所以 RedGreenBlue 可以直接使用。
  • enum 的底层类型默认是 int,但可以隐式转换成整数类型:
1
int colorValue = Green; // 1

enum class(强类型枚举)

1
2
3
4
5
6
enum class Color
{
Red, // 0
Green, // 1
Blue // 2
};
  • enum class 不能隐式转换为 int,避免了命名冲突:
1
2
int colorValue = Color::Green; // ❌ 错误,需要显式转换
int colorValue = static_cast<int>(Color::Green); // ✅ 正确
  • enum class 作用域限定,避免与其他 enum 或全局变量冲突:
1
2
3
4
5
enum class Color { Red, Green, Blue };
enum class TrafficLight { Red, Yellow, Green };

Color myColor = Color::Red; // ✅ 正确
TrafficLight light = TrafficLight::Red; // ✅ 正确
  • 你可以指定底层类型:
1
2
3
4
5
6
enum class Color : uint8_t
{
Red = 1,
Green = 2,
Blue = 3
};

6. C++ 宏中的 ###

在 C++ 宏定义中:

  • # 用于将宏参数转换为字符串。
  • ## 用于连接(拼接)两个标识符。

# 操作符(字符串化)

1
2
3
4
5
6
7
8
9
#include <iostream>

#define STRINGIFY(x) #x

int main()
{
std::cout << STRINGIFY(Hello World) << std::endl; // 输出: "Hello World"
return 0;
}
  • #xx 变成字符串。

## 操作符(标识符拼接)

1
2
3
4
5
6
7
8
9
10
#include <iostream>

#define CONCAT(a, b) a##b

int main()
{
int xy = 10;
std::cout << CONCAT(x, y) << std::endl; // 输出 10
return 0;
}
  • ##xy 连接成 xy,所以 CONCAT(x, y) 实际上变成了 xy

7. override 关键字和虚函数

在 C++11 之前,虚函数的覆盖是隐式的,容易出错。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base
{
public:
virtual void Speak()
{
std::cout << "Base speaking!" << std::endl;
}
};

class Derived : public Base
{
public:
void Speak(int x) // ❌ 这个不会覆盖 Base::Speak()
{
std::cout << "Derived speaking!" << std::endl;
}
};

这里 Derived::Speak(int x) 由于参数不同,并没有真正覆盖 Base::Speak(),但编译器不会报错。这可能会导致意外的行为。

使用 override 关键字

C++11 引入了 override 关键字,可以显式指定一个函数是覆盖基类的虚函数:

1
2
3
4
5
6
7
8
class Derived : public Base
{
public:
void Speak() override // ✅ 现在编译器会检查是否正确覆盖
{
std::cout << "Derived speaking!" << std::endl;
}
};

如果 Base::Speak() 发生变更(比如改名或修改参数),编译器会报错,防止潜在的错误。

结合 final 关键字

  • override 只是确保函数正确覆盖了基类的虚函数。
  • final 关键字可以防止子类继续重写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base
{
public:
virtual void Speak() {}
};

class Derived : public Base
{
public:
void Speak() override final {} // ✅ 允许 Derived 重新定义,但禁止再被派生类覆盖
};

class MoreDerived : public Derived
{
public:
// void Speak() override {} // ❌ 错误,无法覆盖 final 函数
};

抽象类和纯虚函数

如果一个类有纯虚函数,那么它就成为抽象类,不能实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Animal
{
public:
virtual void Speak() = 0; // 纯虚函数
};

class Dog : public Animal
{
public:
void Speak() override
{
std::cout << "Woof!" << std::endl;
}
};

int main()
{
// Animal a; // ❌ 错误,不能实例化抽象类
Dog d;
d.Speak(); // 输出 "Woof!"

return 0;
}

8.关于__debugbreak

方便使用引擎来判断数据类型是否为null,创建了一个Assert类型的宏,为了创建这个宏,需要知道两个宏的操作,一个是Variadic Arguments(可变的参数),这个之前也用到过了,对应宏的参数是__VA_ARGS__,第二个就是一个新的宏,叫做__debugbreak

相当于C#里调试的Debug.break(),调试的时候如果执行到这里会自动打断点

1
2
3
4
5
6
7
8
9
#ifdef HE_ENABLE_ASSERTS
#define HENGINE_ASSERT(x, ...) if(!x) {LOG_ERROR("Assertion Failed At: {0}", __VA_ARGS__);\
__debugbreak();}
#define HENGINE_CORE_ASSERT(x, ...) if(!x) {CORE_LOG_ERROR("Assertion Failed At: {0}", __VA_ARGS__);\
__debugbreak();}
#else
#define HENGINE_ASSERT(x, ...) // 非Debug状态下这行代码毫无作用
#define HENGINES_CORE_ASSERT(x, ...)
#endif

9.C++ Lambda 表达式

Lambda 表达式(Lambda Expression)是 C++11 引入的一种 匿名函数,可以用来创建临时的、可传递的 函数对象,特别适用于 回调函数、STL 算法 等场景。
它的语法结构如下:

1
[捕获列表] (参数列表) -> 返回类型 { 函数体 }

1. Lambda 的基本语法

1
2
auto lambda = []() { std::cout << "Hello, Lambda!" << std::endl; };
lambda(); // 调用 lambda,输出 "Hello, Lambda!"
  • []捕获列表,这里为空,表示不捕获任何变量。
  • ()参数列表,这里为空,表示没有参数。
  • {} 内是 函数体

2. 捕获列表(Capture List)

Lambda 可以 捕获外部变量,有几种方式:

(1)值捕获(Copy Capture)

1
2
3
int a = 10;
auto lambda = [a]() { std::cout << a << std::endl; };
lambda(); // 输出 10
  • a值(拷贝) 方式传递到 Lambda 里,不会影响外部的 a
  • Lambda 内修改 a 不会影响外部 a(因为是拷贝)。

(2)引用捕获(Reference Capture)

1
2
3
4
int a = 10;
auto lambda = [&a]() { a = 20; };
lambda();
std::cout << a << std::endl; // 输出 20
  • &a引用方式 捕获 a,Lambda 里修改 a 会影响外部 a

(3)默认捕获(= 和 &)

  • [=](默认值捕获):所有变量都按 \值* 方式捕获*
  • [&](默认引用捕获):所有变量都按 \引用* 方式捕获*
1
2
3
4
5
6
7
int x = 5, y = 10;
auto lambda1 = [=]() { std::cout << x + y << std::endl; }; // 捕获 x, y 但不可修改
auto lambda2 = [&]() { x = 20; y = 30; }; // 捕获 x, y 并且可以修改

lambda1(); // 输出 15
lambda2();
std::cout << x << ", " << y << std::endl; // 输出 20, 30
  • [=]x, y 以值传递 方式进入 Lambda,无法修改它们。
  • [&]x, y 以引用 方式进入 Lambda,可以修改它们。

(4)混合捕获

1
2
3
4
int a = 10, b = 20;
auto lambda = [=, &b]() { b = 30; std::cout << a << ", " << b << std::endl; };
lambda();
std::cout << b << std::endl; // 输出 30
  • [=, &b]a 按值 捕获,而 b 按引用 捕获。
  • Lambda 里修改 b 会影响外部,但 a 不会变

3. Lambda 的参数

Lambda 可以带参数,就像普通函数一样:

1
2
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 5) << std::endl; // 输出 8

4. Lambda 的返回类型

  • 如果 返回值能自动推导,可以省略 -> 返回类型
1
2
auto multiply = [](double x, double y) { return x * y; };
std::cout << multiply(2.5, 4.0) << std::endl; // 输出 10.0
  • 如果返回值 不能自动推导(如 return 类型不同),要手动指定:
1
2
3
4
5
auto divide = [](int a, int b) -> double {
if (b == 0) return 0.0;
return (double)a / b;
};
std::cout << divide(5, 2) << std::endl; // 输出 2.5

5. Lambda 作为函数参数

Lambda 在 STL 算法 里很常见:

1
2
3
std::vector<int> nums = {1, 2, 3, 4, 5};
std::for_each(nums.begin(), nums.end(), [](int n) { std::cout << n << " "; });
// 输出 1 2 3 4 5
  • std::for_each 遍历 nums,每个元素都调用 Lambda 进行处理。

6. Lambda 作为回调函数

Lambda 适用于 事件回调

1
2
3
4
5
6
7
8
void Process(std::function<void(int)> callback) {
for (int i = 0; i < 3; i++)
callback(i);
}

int main() {
Process([](int i) { std::cout << "Processing: " << i << std::endl; });
}
  • Process 接受一个 std::function<void(int)> 作为回调。
  • Process 在循环里调用 Lambda,并传入 i

7. Lambda 的 mutable

普通 Lambda 默认不能修改捕获的值

1
2
int x = 10;
auto lambda = [x]() { x = 20; }; // ❌ 错误,x 是 const 不能修改

可以用 mutable 让 Lambda 里的 x 变为 可修改副本

1
2
3
4
int x = 10;
auto lambda = [x]() mutable { x = 20; std::cout << x << std::endl; };
lambda(); // 输出 20
std::cout << x << std::endl; // 输出 10(原来的 x 没变)
  • mutablex 在 Lambda 内可变,但不会影响外部 x

HEngine游戏引擎(31-60)

1.遍历二维数组需要注意的问题

参考:https://stackoverflow.com/questions/33722520/why-is-iterating-2d-array-row-major-faster-than-column-major

使用C++的目的就是性能,操作时要注意内存的细节,如二维数组应该按照行进行遍历,因为C++里数组是按行进行存储的

1
2
3
4
5
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
sum += array[i][j]; // ✅ 按行访问,内存访问是连续的
}
}

HEngine游戏引擎(61-90)

1. C++ std::filesystem

std::filesystem 是 C++17 引入的文件系统库,提供了一组强大的 API 用于操作文件和目录,包括路径处理、文件查询、文件操作等。该库位于 <filesystem> 头文件中,属于 std::filesystem 命名空间。


  1. 基本概念

(1)主要组成部分

  • std::filesystem::path —— 表示文件或目录路径
  • std::filesystem::directory_entry —— 代表目录中的一个条目
  • std::filesystem::directory_iterator —— 用于遍历目录中的条目
  • std::filesystem::recursive_directory_iterator —— 递归遍历目录
  • std::filesystem::file_status —— 存储文件的属性信息

  1. 常见用法

(1)路径操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <filesystem>

int main() {
std::filesystem::path p = "C:/Users/Example/file.txt";

std::cout << "Path: " << p << std::endl;
std::cout << "Filename: " << p.filename() << std::endl;
std::cout << "Extension: " << p.extension() << std::endl;
std::cout << "Parent Path: " << p.parent_path() << std::endl;
std::cout << "Is absolute: " << p.is_absolute() << std::endl;

return 0;
}

输出

1
2
3
4
5
Path: C:/Users/Example/file.txt
Filename: file.txt
Extension: .txt
Parent Path: C:/Users/Example
Is absolute: 1

(2)文件和目录操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main() {
fs::path dir = "test_directory";

// 创建目录
if (!fs::exists(dir)) {
fs::create_directory(dir);
std::cout << "Directory created: " << dir << std::endl;
}

// 创建文件
fs::path file = dir / "example.txt";
std::ofstream(file) << "Hello, Filesystem!"; // 创建并写入内容

// 判断文件是否存在
if (fs::exists(file)) {
std::cout << "File exists: " << file << std::endl;
}

// 删除文件
fs::remove(file);
std::cout << "File deleted: " << file << std::endl;

// 删除目录
fs::remove(dir);
std::cout << "Directory deleted: " << dir << std::endl;

return 0;
}

(3)遍历目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main() {
fs::path dir = "."; // 当前目录

std::cout << "Contents of directory: " << fs::absolute(dir) << "\n";

for (const auto& entry : fs::directory_iterator(dir)) {
std::cout << (entry.is_directory() ? "[DIR] " : "[FILE] ") << entry.path() << "\n";
}

return 0;
}

(4)获取文件信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main() {
fs::path file = "test.txt";

if (fs::exists(file)) {
std::cout << "Size: " << fs::file_size(file) << " bytes\n";
std::cout << "Last write time: " << fs::last_write_time(file).time_since_epoch().count() << "\n";
} else {
std::cout << "File does not exist.\n";
}

return 0;
}

  1. 总结
  • std::filesystem 提供了高效、跨平台的文件操作能力。
  • std::filesystem::path 用于管理路径信息。
  • std::filesystem::directory_iteratorstd::filesystem::recursive_directory_iterator 用于遍历目录。
  • std::filesystem 还可以进行文件创建、删除、拷贝、移动等操作。

HEngine游戏引擎(91-120)