Hi There! 👋

欢迎来到Ca2didi的博客。本人主要关注游戏客户端方向(目前做Unity为主)。

行列指针迷思

在中国的C语言编程领域中,有一对概念叫做“行指针”与“列指针”,他们两个形如下: int arr[3][4]; int (*pRow)[4] = arr; // Row Pointer int *pCol = arr[0]; // Column Pointer 其中行列指针都是用来访问一个二维数组而用,但是其访问的方式有一些不同,比如我要访问二行三列的元素时,行列指针访问其元素的方法分别如下: int value = 0; // Access a Row Pointer value = pRow[2][3]; value = *(*(pRow + 2) + 3); // Access a Column Pointer value = *(pCol + 4 * 2 + 3); 但有趣的事情是,行指针看起来似乎与数组指针长得更像一些,而列指针更加偏向于传统的一般指针。于是我打算从英文资料内去寻找相关资料。但当我尝试使用类似于row pointer或者column pointer的关键词在Google上搜索相关资料时,真正在讨论这个概念的英文页面是从CSDN机器翻译的中文文章。加之这个概念本身与一般指针过于类似,我开始考虑这个概念本身是否是正确的。 先讨论内存结构 一维指针与二维指针的结构是极其类似的,他们两个都是连续的排列在一段连续的内存空间内,类似一条条带一样。但是不同点在于一维指针没有主次序之分,而二维指针存在主次序。用一个具体例子来说明: 假设这里有一个最多可以容纳8个元素的一维指针: 长度为sizeof(int) * 8,那么那将会在内存中开辟出一块连续的空间,并给我们指向这个内存空间起始位置的地址。此时我们可以直接通过对指针本身添加偏差值来获取我们指定一维序的地址。 有一个简化的计算方法是对一维指针添加单个Index符号(也就是[])来计算偏差值并访问其元素。 我们再来假设另外一个可以容纳8个元素的二维指针: 其中二维序长度为2,一维序长度为4,那么这一个指针的长度就是sizeof(int) * 2 * 4。同样的,我们在此也会开辟出一块连续的内存空间,同时也将会获得指向这一块内存空间起始位置的地址。 此时我们同样也是对指针添加偏差值来获得指定序的地址,但有一点不同的是,此处我们并非直接通过指定一维序来计算偏差值,而是通过公式二维序 * 二维序长度 + 一维序作为偏差值来计算目标序的地址。有一个简化的计算方法是对一维指针添加两个Index符号(也就是[][])来计算偏差值并访问其元素。...

2022/6/30 · Ca2didi

为什么我们需要UniTask?谈论异步与Unity。

本文原本是在TDS写的一篇分享文章,现在我拿到我的博客上给大家分享一下。 我之前曾经提到过一个Unity插件叫做UniTask,是用来提供Unity异步操作用的。但是我想各位可能不太明确的一个问题是,为什么Unity需要使用到异步操作?或者还有人会问我,什么是异步?为了讲清楚这个东西,我将会从C#的异步和Unity多线程上的陷阱开始,为大家一步一步梳理UniTask应用场景。 多线程与异步 大家都知道线程这么一回事:程序运行的时候需要运行在至少一个线程上,才能保证程序中所定义的算法可以被执行。 而且稍微有经验的人也知道,操作多线程是一件比较麻烦的事情。一方面是线程安全问题——两个线程同时操作同一处内存时会引发不确定的操作结果——另一方面是如果一个线程需要依赖另外一个线程的结果,那我们还不停判断另外一个线程的状态来指导当前线程执行下一步算法的时机。 第一个问题相对而言会好解决一些:通过锁,保证多个线程操作同一块内存时,操作必定是原子的。但是第二个问题就会麻烦很多,其主要表现在以下方面: 状态机有可能会写错,增加debug成本。 会导致单个线程不断去判断另外一个线程的状态,使之空转浪费宝贵的CPU资源。 如果另外一个线程迟迟不能完成,这将会阻塞这个线程,有可能导致整个程序被卡死。 会让我们写的代码不够直观,增加编码成本(回调地狱)。 因此,为了解决上面的几个问题,我们提出了一种叫做异步的策略。 异步是什么 我觉得网上讲异步讲的都不够直接,我就直接的抛出我所认为异步的本质:异步是用来转让程序运行权的工具。我下面举几个具体的代码例子来示意: void Block() { ... // 做一件非常耗时间的事情 Console.WriteLine("Done"); } // 什么是async async Task BlockAsync() { await Task.Run(() => Block()); // 为什么这里有await return null; } void CallThem() { Block(); // 这个会导致执行这一个函数的线程被阻塞 var task = BlockAsync(); // 而这个却不会 Console.WriteLine("Called"); } 当我调用CallThem()这个函数时你会发现Block()会把这个程序卡死,导致你要等很久才能看到第一个“Done”被打印出来;但是你如果删掉CallThem()内的Block()调用,这个程序却在一瞬间就把“Called”给打印到控制台,不过要等很久控制台上面才会打印“Done”。这是为什么呢? Magic在于这条一行 await Task.Run(() => Block()); 在这行,await代表的是“我要转让运行权给调用我的函数”,也就是说将程序的运行权从函数BlockAsync转让回调用它的CallThem,从而让CallThem可以继续执行下去。不过你也需要注意一点,如果一个函数里面会出现await,那么其函数头必须使用async去标记,具体的缘由后面再说。 顺带一提,Task在这里指的是一个任务。需要注意的是只有可以返回Task、ValueTask或者TaskAwaiter的方法才可以使用await去转让运行权。同时,我们通过Task.Run()将我们的计算放置在了另外一个线程中,当被await的Task执行完成后,这个线程将会接着执行这个函数剩下的部分,直到它遇到了return或者await。 因此不难看出,通过await这一个魔法就能保证我们最大限度地不让程序被卡住,同时还能让我们尽可能榨干一个线程——干不完你我就先不鸟,你好了我再来鸟你。 我想要返回一个值 不过上面那句话还不太准确,因为我们还有可能需要这个函数所返回的值——说不准是我们求他干完而不是想不鸟就不鸟它——比如这样子: int Calc() { ... // 又要做一件非常耗时间的事情 return result; // result = 114514 } async Task<int> CalcAsync() { return Task....

2022/4/9 · Ca2didi

OpenGL 学习笔记 1

我志向学习游戏客户端开发,在此之前已经有了2年左右的业余Unity开发经验。现在升上大学,我想我也应该有时间去学习更为进阶的内容了。因此我准备通过自学OpenGL来写一个小的游戏引擎,来学习Shader与游戏引擎构架。 由于学习笔记对于我来说是对我不容易记住的东西进行补充说明与个人理解,因此不会说的非常详细,仅仅是对参考资料的一个补充。 话不多说,下面就是我的笔记正文。 参考资料 目前参考资料是: LearnOpenGL CN 一个非常好用的OpenGL入门教程网站,必看。 OpenGL Wiki OpenGL的官方文档,够正的。 OpenGL是什么 OpenGL是一个图形库(而且也仅仅是一个图形库,没有输入、声音和窗口管理的库函数),其本质是通过一套开源标准,让硬件与软件得以连接的API。现代显卡都支持OpenGL API。 OpenGL API本质上是一个状态机,因此我们需要在OpenGL每一次绘图之前对OpenGL的参数进行配置,才能让图形如我们所愿。(因此OpenGL也提供了一种参数的简记手段) 由于OpenGL本质上是一套开源标准,因此显卡真正的运行时API与OpenGL API必定不同,OpenGL需要在运行时对实际调用显卡的API进行重定向。这就导致了OpenGL的运行效率会有损失。 GLFW与GLAD 之前提到过OpenGL是没有Input和Audio之类的功能,甚至连在操作系统下创建窗口的功能都没有,因此我们需要额外的库来做支持。GLFW就是做这些事情的一个库了。GLFW实现了系统环境支持,为应用提供了一套很好用的库,关键是它也是跨平台的。 那么GLAD是干什么的呢?GLAD的功能在于为OpenGL提供了重定向功能,使之可以调用到显卡中具体的操作函数(上面提到过了!) 开发环境配置 我之前没有学过C++,因此我这次尝试使用C++来学习OpenGL。学习OpenGL的同时也学会了C++,岂不美哉。 我具体的开发环境是macOS Big Sur(arm64) + Clang + Clion。我尝试使用教程中的做法来自己编译运行库,但是最后终归失败。 最后我使用了homebrew来导入运行库: brew install glfw 安装完成后,我们可以在 /opt/homebrew/Cellar/ 下找到我们的glfw。 接下来按照LearnOpenGL的说法,去 https://glad.dav1d.de 下载glad后,构建项目目录结构如下所示: ProjectRoot |- CMakeLists.txt |- src // 放置本项目源代码 |- includes // 放置头文件 |- libs // 放置引用库 将glad解压缩后,分别把内容中的include和lib放在我们项目中的对应目录。接下来,我们编写CMakeList如下所示: cmake_minimum_required(VERSION 3.20) project(LearnOpenGL) set(CMAKE_CXX_STANDARD 11) # 设置变量 set(GLFW_HOME "/opt/homebrew/Cellar/glfw/3.3.4") # 设置头文件目录 include_directories("${GLFW_HOME}/include") include_directories(/include) # 添加 GLFW3 预编译库 add_library(glfw SHARED IMPORTED) SET_TARGET_PROPERTIES(glfw PROPERTIES IMPORTED_LOCATION "${GLFW_HOME}/lib/libglfw....

2021/10/27 · Ca2didi

我和我的三个父亲

我的人生,到现在为止,最痛苦的事情,莫过于我有三个父亲。 我的父亲们啊,用血肉赐予了我的肉体,用行动赐予了我的灵魂,用金钱赐予了我的生活。 我的母亲并非什么荡妇,也不是叫花子,她也是在人生道路上,稀里糊涂的带来了我的哥哥、我的妹妹和我。但我和我的兄妹不同,他们只有一个父亲,他们永远不需要调节父亲的比例:而我却要同时面对着三个父亲的压力。 有人说,能够被爱是人生最大的幸福——爱我的父亲被迫抛弃我,爱我的父亲被迫袖手无策,爱我的父亲实际上并不爱我。 总有人说,孩子需要被父母爱,但我又觉得,父母需要被孩子爱。你看到一个个孩子被拐卖的家长,呕心沥血去寻找孩子,你真觉得他只是在寻找自己的孩子吗?不如反过来讲,孩子是让父母感受到爱的来源。也就是说,孩子也能去爱自己的父母,无论是有意的,还是无意的。 我愿意去爱,但是每一次爱,我却要顶着另外一个需要我去爱的父亲的压力,最终在空中漂浮,无法着陆。我不愿意去爱,但我却忍不住去认为这个世界是值得被爱的,因为我也是值得被爱的。 我不想漂浮,我想要着陆,但陆地的大风,它刮的太猛烈了,我找不到安全的方式着陆啊! 唯一能让我值得欣慰的事情是,在2008年的地震之后,我的生父离开了舞台,他变成了回忆中曾经需要我去爱的人。但也是正是因为这份血亲的空缺,我的父亲成为了我目前人生的主战场。 我尝试去爱我妹妹的父亲,但是他的虚伪让我难以靠近,我无法走进他的心;我渴望去爱我哥哥的父亲,但是他尊于道义,不能够成为我的父亲,我无法融入他的生活。 是的,我没有父亲。 我有三个父亲,可惜他们都无法成为我的父亲。这大概是痛苦中的最大讽刺吧。

2021/8/1 · Ca2didi

大考之前的一点幻想

我经常幻想,止不住的幻想。倒也不是因为我喜欢,而是我习惯。 幻想,为什么人们都爱讲,幻想是不切实际的?我反而觉得,幻想才是我的生活,那才是真正的真实。 每天醒来第一件事情,就是意识到我还活着。我经常幻想,人生是一场梦,我所认为的真实是不切实际的存在,有可能上一秒钟我还睁着眼睛,下一秒我便陷入到了另外一种存在形式。 晚上失眠,我便在想,我是不想离开这个世界,我还想在我熟悉的世界里面多待一会。熟悉的世界是美好的,是真实的,没有梦中荒诞的恐怖,也没有小说里面虚构的幻影。能醒着,那就是活在熟悉之中,那才真的美好。 但是,生活对于我而言,终究是幻想的。我无法证明,我所生活的不是另外一种幻想,这个幻想切合我所认为的实际,这个幻想远离我最不愿意面对的事情,所以我把一种幻想作为理所当然。毕竟我们所认为的现实,那必然是一个理所当然的世界。 所以,我的的确确是活在幻想之中,我们都是活在幻想之中的,因为没有人能够撇清楚我们究竟是真醒着,还是装醒着。

2021/5/29 · Ca2didi