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 () { 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 () { if (!glfwInit ()) return -1 ; GLFWwindow* window = glfwCreateWindow (800 , 600 , "Vsync Example" , NULL , NULL ); if (!window) return -1 ; glfwMakeContextCurrent (window); glfwSwapInterval (1 ); while (!glfwWindowShouldClose (window)) { glfwSwapBuffers (window); glfwPollEvents (); } glfwTerminate (); return 0 ; }
关闭 Vsync
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::bind 和 std::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; 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;
(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); 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 ); boundFunc (); 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 ); return 0 ; }
5. enum 和 enum class
普通 enum
1 2 3 4 5 6 enum Color { Red, Green, Blue };
enum 会把枚举值提升到全局作用域,所以 Red、Green、Blue 可以直接使用。
enum 的底层类型默认是 int,但可以隐式转换成整数类型:
enum class(强类型枚举)
1 2 3 4 5 6 enum class Color { Red, Green, Blue };
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; return 0 ; }
## 操作符(标识符拼接)
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; return 0 ; }
## 将 x 和 y 连接成 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) { 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 {} }; class MoreDerived : public Derived { public : };
抽象类和纯虚函数
如果一个类有纯虚函数 ,那么它就成为抽象类 ,不能实例化:
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 () { Dog d; d.Speak (); 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, ...) #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;
&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; }; auto lambda2 = [&]() { x = 20 ; y = 30 ; }; lambda1 (); lambda2 ();std::cout << x << ", " << y << std::endl;
[=] 让 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;
[=, &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;
4. Lambda 的返回类型
如果 返回值能自动推导 ,可以省略 -> 返回类型:
1 2 auto multiply = [](double x, double y) { return x * y; };std::cout << multiply (2.5 , 4.0 ) << std::endl;
如果返回值 不能自动推导(如 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;
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 << " " ; });
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
可以用 mutable 让 Lambda 里的 x 变为 可修改副本 :
1 2 3 4 int x = 10 ;auto lambda = [x]() mutable { x = 20 ; std::cout << x << std::endl; };lambda (); std::cout << x << std::endl;
mutable 让 x 在 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)主要组成部分
std::filesystem::path —— 表示文件或目录路径
std::filesystem::directory_entry —— 代表目录中的一个条目
std::filesystem::directory_iterator —— 用于遍历目录中的条目
std::filesystem::recursive_directory_iterator —— 递归遍历目录
std::filesystem::file_status —— 存储文件的属性信息
常见用法
(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 ; }
总结
std::filesystem 提供了高效、跨平台的文件操作能力。
std::filesystem::path 用于管理路径信息。
std::filesystem::directory_iterator 和 std::filesystem::recursive_directory_iterator 用于遍历目录。
std::filesystem 还可以进行文件创建、删除、拷贝、移动等操作。
HEngine游戏引擎(91-120)