阅读视图
C++ 教程: 用std::move来移动所有权
📘 C++ 移动语义与 std::move() 教程
C++的std::move用于转移变量/对像的所有权/Ownership。
🔹 什么是移动语义?
在 C++ 中,移动语义通过转移资源所有权/Ownership(如内存或文件句柄)来优化性能,而不是复制它们。
移动语义是在 C++11 中引入的,它允许:
- 更快速地传递大型或昂贵的对象
- 更高效地使用临时值
🔹 什么是 std::move()?
std::move(x)
并不会真的移动任何东西 —— 它只是将 x
转换为一个 右值引用(即 T&&
),告诉编译器:
“你可以把这个对象当作临时对象来处理并移动它。”
要真正实现移动,你的类型必须实现 移动构造函数 或 移动赋值运算符。
✅ 什么时候该用 std::move()?
在以下情况下使用它:
- 你想 转移资源的所有权。
- 你正在处理 复制开销大的对象(如
std::string
、std::vector
、unique_ptr
)。 - 你写的函数按值接收参数,并希望将其移动进成员变量。
🔍 std::string 示例
#include <iostream> #include <string> #include <utility> int main() { std::string a = "hello"; std::string b = std::move(a); std::cout << "b: " << b << std::endl; std::cout << "a: " << a << std::endl; }
🔍 移动 std::vector
std::vector<int> original = {1, 2, 3}; std::vector<int> moved_to = std::move(original); // original 现在为空(但仍然有效)
⚠️ 移动后会发生什么?
移动后:
- 被移动的对象 仍然有效。
- 但其 内容未定义 —— 你只能销毁它或重新赋值。
std::string x = "abc"; std::string y = std::move(x); // x 现在处于有效但未定义的状态 —— 不要再读取它!
🧠 对内建类型使用 std::move()
int x = 42; int y = std::move(x); // 实际是拷贝,因为 int 没有移动语义
这没必要,因为像 int
这样的基本类型不支持移动构造。
🛠️ 自定义类型实现移动语义
class MyBuffer { int* data; size_t size; public: MyBuffer(size_t s) : size(s), data(new int[s]) {} // 移动构造函数 MyBuffer(MyBuffer&& other) noexcept : data(other.data), size(other.size) { other.data = nullptr; other.size = 0; } // 移动赋值运算符 MyBuffer& operator=(MyBuffer&& other) noexcept { if (this != &other) { delete[] data; data = other.data; size = other.size; other.data = nullptr; other.size = 0; } return *this; } ~MyBuffer() { delete[] data; } };
使用示例:
MyBuffer a(1000); MyBuffer b = std::move(a); // 将 a 移动到 b
📦 std::move() 与智能指针
可以用 std::move来操作智能指针,比如 unique_ptr 或 shared_ptr:
#include <memory> std::unique_ptr<int> p1 = std::make_unique<int>(10); std::unique_ptr<int> p2 = std::move(p1); // p1 现在为空指针
🔁 std::shared_ptr 所有权转移
当你“转移所有权”给另一个 shared_ptr
时,你实际上是:
- 将控制块(用于跟踪引用计数)从一个
shared_ptr
移动到另一个。 - 原来的
shared_ptr
变为空(use_count() == 0
)。 - 总体引用计数不变(仍为 1,除非还有其他共享所有者)。
✅ 示例:通过 std::move() 转移所有权
#include <iostream> #include <memory> int main() { std::shared_ptr<int> p1 = std::make_shared<int>(42); std::cout << "p1 use_count: " << p1.use_count() << std::endl; // 1 std::shared_ptr<int> p2 = std::move(p1); // 转移所有权 std::cout << "p1 is " << (p1 ? "not null" : "null") << std::endl; // null std::cout << "p2 use_count: " << p2.use_count() << std::endl; // 1 }
🔍 重要区别:shared_ptr vs unique_ptr
指针类型 | 转移机制 | 允许拷贝 | 主要用途 |
---|---|---|---|
std::unique_ptr |
仅支持 std::move() |
❌ 不允许 | 独占资源所有权 |
std::shared_ptr |
std::move() 或拷贝 |
✅ 允许 | 共享资源所有权,引用计数 |
⚠️ 注意事项
- 你可以使用 move 来转移
shared_ptr
的所有权(源指针将变为空)。 - 你也可以拷贝
shared_ptr
来共享所有权(两个指针都有效,引用计数增加)。 - 只有在你明确希望原来的
shared_ptr
被置空时才使用std::move()
。
🔄 常见使用模式
函数返回值使用移动:
std::string get_name() { std::string name = "Alice"; return std::move(name); }
只有在你想强制进行移动(比如返回函数参数)时才使用 std::move()
。
🚫 不该使用 std::move() 的场景
1. ❌ 不要从还需要使用的变量移动:
std::string s = "test"; std::string t = std::move(s); std::cout << s; // 内容未定义
2. ❌ 不要对 const 对象使用 std::move():
const std::string s = "hi"; std::string t = std::move(s); // 实际是拷贝,因为移动构造<a href="https://justyy.com/archives/67736">函数</a>无法接收 const 参数
🧪 总结速查表
使用场景 | 是否使用 std::move() | 原因 |
---|---|---|
移动大型容器或字符串 | ✅ 是 | 高效转移内存或资源 |
移动智能指针 | ✅ 是 | 转移所有权 |
基本类型(如 int、bool) | 🚫 否 | 没有移动语义,等同于拷贝 |
const 对象 | 🚫 否 | 移动构造函数不接受 const |
临时变量 | 🚫 通常不需要 | 已经是右值了 |
✅ 最后小贴士
如果你不确定该不该用 std::move()
,问自己:
“我是否不再需要这个变量并打算把它交出去?”
如果答案是“是” → 那就用 std::move()
。
C/C++编程
- C++ 教程: 用std::move来移动所有权
- C++中的 const和constexpr 比较
- 简易教程: C++的智能指针
- C++ 编程练习题: 如何合并两个二叉树?
- C++ 编程练习题 - 找出第三大的数
- C++ 编程练习题 - 最多连续的 1
- C++ 编程练习题 - 左子树叶节点之和 (深度优先+广度优先+递归)
- C++ 编程练习题 - 最多水容器 (递归)
- C++的异步编程: std::future, std::async 和 std::promise
- C编程练习题: 翻转整数位
- C++编程练习题: 找出字符串的所有大小小组合
- C/C++ 中的内存管理器(堆与栈)
- C++编程练习题: 对两单向链表求和
英文:Tutorial on C++ std::move (Transfer Ownership)
本文一共 880 个汉字, 你数一下对不对.
相关文章:
- 简易教程: C++的智能指针 C++ 智能指针教程 C++ 中的智能指针提供了自动且安全的内存管理。它们通过 RAII(资源获取即初始化)机制,帮助开发者避免内存泄漏和悬空指针的问题,确保对象在生命周期结束时被正确释放。 本教程将介绍 C++ 中三种主要的智能指针: std::unique_ptr:独占式所有权 std::shared_ptr:共享式所有权 std::weak_ptr:非拥有式弱引用 1. std::unique_ptr unique_ptr 拥有独占所有权。一个资源只能被一个...
- 借助AI快速开源了三个小工具: 写代码越来越像做产品了, AI 真把我宠坏了(Vibe Coding) 程序员的未来?Vibe Coding + AI 一起上! 借助 AI 快速开源了三个小工具 最近,我利用 ChatGPT-4o 和 o4-mini 快速开发并开源了三个小工具。起因其实很简单——每次想转换 YAML/JSON 或进行...
- 被动收入之: 微博红包 今年开始重新经营我的微博帐号 drlai 收到两笔微信红包,应该是来自于官方的支持,150元(成功提现到支付宝)。虽然这不能持久,也没多少,但毕竟实现了零的突破,意义重大。 如果流量上来,内容创作者可能会接受到比较多的赞赏,这也是一个比较简单的变现方法。这也能作为一种被动收入,不过如果不是头部网红,可能杯水车薪,但如果你有好几个类似这样的,也能积少成多! 在用户中心,微博用户可以每天登陆手机微博APP打卡,获取点数和少量的红包钱(几分钱),积少成多! 微博做些小任务可获得积分和几分钱。聊胜于无。 微博的主要盈利模式 微博的主要盈利模式主要包括以下几个方面: 广告收入:微博的大部分收入来源于广告,尤其是品牌广告和效果广告。广告形式包括信息流广告(类似于推文广告)、热门话题广告、开屏广告和视频广告。品牌和企业可以利用微博庞大的用户群和社交互动来提升曝光率、推广品牌和产品。 会员服务:微博提供的VIP会员服务,用户可以支付订阅费用来享受更多的特权,比如个性化的主题、特有的表情包、私密权限设置等。这些会员服务主要面向个人用户,提升其社交体验。 直播和打赏:微博提供直播平台,用户可以通过购买虚拟礼物来支持主播,微博会从这些打赏中抽取一定比例的分成。此外,微博与内容创作者分成,通过内容付费、知识付费等形式变现。 增值服务:针对企业和大V(拥有大量粉丝的用户),微博还提供增值服务,如账号认证、粉丝数据分析、精准推送、推广和营销工具等。这些服务帮助企业提升营销效果,同时也增加了微博的收入来源。 电商和导流:微博上有大量的电商导流业务,尤其是和明星、网红的合作推广。微博用户在浏览社交内容时,可以直接跳转到商品购买链接,微博通过这种方式赚取导流佣金。 游戏联运:微博也会与一些游戏公司合作推出联合运营的游戏,微博负责推广和流量引入,用户充值或付费时,微博可以获得一部分的分成。 这些模式相结合,使得微博能够在广告市场、内容创作和电商等多个领域获利。...
- 步步高学生电脑上 Basic 编程语言 peek 用法示例 步步高学生电脑 是8位FC机的经典之作.它上面的BASIC有三个版本 1.0, 2.0 和 2.1 2.1 版本有个在线帮助,实际上是 help.cmd 1.0 是用 Esc 键退回到 DOS 的,...
- 换了个奥迪Q5大灯花了我1000英镑 我那辆奥迪Q5 SUV今年年检没通过,原因是左前车灯坏了,需要更换。车厂告诉我,光是订购零件就要700多英镑,加上人工费,总费用得1000英镑。但没办法,如果不修,车辆年检(MOT)就过不了,车也不能上路。 MOT是英国的机动车强制性安全检测(Ministry of Transport Test)的简称。 近侧前位置灯不工作 drl/位置灯集成(4.2.1(a)(ii)) Nearside Front Position lamp not working drl/position...
- C++ Ranges 教程 C++20 引入了 ranges(范围),这是一个强大且优雅的抽象,用于处理序列(如数组、vector 等)。相比传统的迭代器或旧式循环,Ranges 提高了代码的可读性、可组合性和性能。 什么是 Range? 在 C++20 中,range(范围) 是一种抽象,代表一个可以迭代的元素序列。它与 views(视图) 和 actions(操作) 如过滤、转换等配合使用非常自然。...
- C++中的 const和constexpr 比较 C++ const 与 constexpr:真正的区别是什么? 一眼看都是定义常量。 为什么这很重要 现代 C++ 鼓励编写不可变、高效且表达力强的代码。两个关键字—const 和 constexpr—是这一理念的核心。它们看起来很相似,但理解它们的不同语义,对于正确利用编译期与运行期行为至关重要。 高层次对比 特性 const constexpr...
- 你给SteemIt中文微信群拖后腿了么? 这年头不缺算法, 就缺数据. 这两天花了很多时间在整API上, 整完之后自己用了一下还觉得真是挺方便的. 今天就突然想看一看自己是否给大家拖后腿了, 于是调用每日中文区微信群排行榜单的API, 刷刷拿着 NodeJs 练手: 1 2 3 4 5 6...
WordPress 古腾堡编辑器中媒体类区块使用说明
在 WordPress 的古腾堡编辑器中,媒体类别的区块专注于插入和展示各种媒体内容,如图片、视频、音频、画廊等。通过这些区块,用户可以轻松地将媒体文件与文本、按钮等内容进行组合,从而丰富文章和页面的展示效果。这些区块不仅能提升内容的互动性,还能使页面看起来更加生动和吸引人。
媒体类别的区块包含图片、图库、音频、封面、文件等 7 种,其中图片区块使用最频繁。除了图片,建议尝试其他区块,以增强文章的多样性。我将在本文中详细说明,并插入真实的例子辅以演示。
1. WordPress 编辑器中的图片区块

当通过左侧区块插入图库区块后,我们可以选择上传图片、从 URL 插入或从媒体库中选择已有图片。上面的截图就是我直接通过 Ctrl + V 粘贴进来的,这相当于拖放操作。
需..... [ 阅读全文 ]
原文链接: https://www.shephe.com/website/wordpress-media-blocks/
版权声明: Kevin's Space 版权所有,转载请用明链标明本文地址
本站相关: 随机文章 | 站长微博 | 关于本站 | 联系站长 | 捐助作者
用户外活动填满五一:勒多曼因篇
null
书接上文。
小舅子他们在特种兵式的几天游玩之后表示最后一天想在家里休息一下,陪着他们吃了一天清淡的食物后,按了个脚,第二天一号他们便各自返回工作地去了。
而我们两口新的征程才刚要开始——下一站勒多曼因。
其实勒多曼因这个行程是五一之前就开始关注了的,因为之前徒步峨眉山、笔架山后加了很多户外群,五一前的偶然一天看到有人发了一个雪山冰湖皮筏艇的照片,并说准备开团出发,景点是我喜欢的那种风格,强度也还能接受,所以进群了解了一下,总共三天行程,:
- 第一天成都集合,乘坐包大巴车出发去康定
- 下午大概四五点到康定,吃晚饭,吃完后坐中转车去老榆林
- 老榆林早起坐转运车出发去山脚下的格因草原开始徒步。
- 徒步20公里,爬升到4300米左右,在山上营地住下。
- 第三天早起冲顶,然后下山。
- 各回各家。
- 我们只需要带两天的路餐,其他的他们包。
- 总费用650元1人,40人成团出发。

当时想着650这价格是真心不贵了,而且恰好能和小舅子他们来的时间错开,所以毫不犹豫的报了两个名。
但是在后面几天接待下舅子的过程中,群里消息不断,组织力度似乎不太够了,总共个加群的人有50多个,接龙说要去的也满了40个人,但是最后交钱的时候却不够包车的40人。
因为人数不够没办法拼大车,群主就开始降级说租20人小车,但是小车价格和大车只便宜了四五百块钱,最后摊下来每个人要多交100元,然后重新组织了一波接龙,不懂贵这100块会产生了多大的化学反应,这次直接20人都没凑齐...。
眼看着要散团了,但是我的瘾又被钓上来了,不想眼看着散团,索性就说我出个车,看有没有其他愿意出车的一起,凑个十几二十个人也还不错,终于在散团之际组织了三个车,最终有17人一起参与了这趟行程。
我们这个7坐车除开我和S,最终分两拨坐了5人。
先是3位泸州的朋友,两男一女,从泸州过来自贡上车,分别是快60的二叔,很会照顾人的刘姐,以及帅气的空少三笑。之后开车去成都接上2泰拳老师和一个川大的大学生。

后面就正式开始发图了,所以这里先吐槽一下这个团。
这个穿山甲不能算是正式的商业团,只能说是有一个大家信的过的群主去联系的车、住宿、餐食,规划好行程后在群里组织的,不过那点组织力几乎是没有,我们因为自驾所以最后减了100的团费,最终收的550元,但是当时承诺的东西一个都没达成,这里罗列已下,下次大家参加这种群性质的团一定要提前问清楚,留好文字存档。
- 说好的只用带路餐,但是最后第一天的晚餐需要自费,第二天的早餐也让我们自费,之后在山上说有鸡汤喝也没有准备,山上的营地的晚餐也很差,更别说早餐馒头还是馊的。
- 说好会花一千多请一个solo过贡嘎的领导,上山后发现人家是带了商团,几乎没管过我们团,另外一个年轻的小伙子说是被叫来管我们,但是就下山看到了他们一下,拍照服务之类的就更别提了。
- 转运车期初以为是什么大巴之类的,上去才知道想多了,全是那种小货卡,上面拉一块篷布就行了,后来听其他上山的说有人坐的车是拉牛羊的,全是屎...。
好了,亏已经吃了,只能说下次注意了。
行程分享正式开始。
我们5月3号从成都出发康定完美错开了车流高峰,全程几乎没有堵车,一路狂奔,车上听二叔说天全服务站有「此生必驾」318的牌子可以打卡,大家商量一致就在天全休息一会儿。


因为是自驾,所以时间比较充裕,路上又没有堵车,我们四点左右就已经到了康定,所以先去刘姐一个朋友开的餐馆吃了一顿菌菇牦牛火锅,老板做的蘸料一绝。

吃完饭才不到5点,所以大家准备一起去逛逛溜溜城。
我们本来是开车进城的,但是穿了一圈都没找到车位,最后在城区里堵了一个小时重新回到康定进城口的边上的一个集中停车场,20元一天,建议大家来这里玩不要想在城区找车位了。


卖青稞大饼,挺香的,下面的店铺15一个,走了一截发现全是从这个店拿的,人家只卖12元,这几步路就有3元的差价。

走到一半发现下雨了,还好穿的冲锋衣。并且凑巧这边有个广场在搞商业活动,似乎是请了什么藏族歌星在唱歌,虽然是藏文歌,但是唱的还挺好听的,不过摇一摇没识别出来。

大家累了坐在路边休息,因为雨大,二叔又没穿冲锋衣,所以暂时和他们分开,我们两口子单独逛去了。

康定旅游局局长同款抖音樱花大街。

溜溜城打卡,为什么叫溜溜城?
“康定溜溜城”这个充满诗意的别称,主要源于康定与《康定情歌》(又名《跑马溜溜的山上》)的深厚渊源。这首传唱世界的民歌让康定以“溜溜”之名广为人知,而“溜溜”在当地方言中更是承载了独特的文化意蕴,在康定及川西方言中,“溜溜”常用来形容事物“美好”“漂亮”“流畅”。


1. 溜溜城打卡 2. 小巷一景
藏族同胞们的广场舞是锅庄,那「锅庄」又是什么呢?。
锅庄(藏语称“果卓”或“歌庄”)是藏族民间广泛流传的一种传统集体舞蹈,尤其在四川、西藏、云南、青海等藏族聚居区盛行。它不仅是重要的娱乐活动,更是藏族文化、宗教和社会交往的载体。

康定的夜景还是非常漂亮的。

天色渐暗后和大家汇合,去往老榆林的民宿休息。
因为对这边的卫生环境不报什么期待,所以提前买了隔脏睡袋,用塑料袋+垃圾桶装了点热水泡脚,早早便躺下休息了。




1. 早起民宿外的风景 2.房间环境 3. 早起窗外的雪山 4. 隔脏睡袋
没有领队,更没有领队说让我们拍出发照,所以我索性叫上大家一起随便拍了一张。

坐上蓬卡就出发了,一路上看不到外面,但感觉早餐都要被颠出来了,估计外面到处都是导弹坑。




1. 格因草原 2. 马帮的马
今天天气不错,依旧没看到领队...。

过木桥,水很凉,都是山上流下来的雪水。低山区


路过一个红石滩


低山区还有很多松树。

在徒步过程中海拔逐渐上升,慢慢树木就变成灌木和草甸,一路上会还经过各种路况:石头路,涉水路,草甸,沼泽等等。


一处绝佳的和雪山合影的机位


我也来一张,在这里不小心把登山杖掉落滚到峡谷下面去了。😅

一爬一个不吱声

有点累,生命力照片一张。

沿途的树木变成了灌木和草甸。

二叔年纪打了,自重也太重,大家都劝他坐马算了,他也听劝,800块直接座马去了营地。后来到营地听他说座马也听危险的,他上去的时候马没站稳前跪了,把他甩到沙地里了,我们在说幸好是沙地...。

终于走了快一半了,两岔河营地吃个午餐

继续出发

在一个绝望坡前碰到了其他队友,我不拍照大家是真不想拍合照....,碰上我这种队友是真挺好的。

最不想走的就是这种河谷路段,全是碎石头。还碰上大风,风里还夹着小粒的冰雹。

最后一个绝望坡,进入雪线了,天上也飘起了小雪。


绝望坡后面的美景

终于到营地了!


营地景色全览


太累了,完全不想拍照用其他人发的照片把。


休息的棚子。营地的饭菜很不好吃,大家草草吃完便都上床准备休息。


因为营地在海拔4300米左右,加上棚子里氧气不太流通,所以帐篷里十几号人晚上过夜的时候都有不同程度的高反,症状基本上都是头疼。我后半夜睡了会,基本上是醒半小时睡半小时左右,我旁边的一个大学生几乎整夜没睡觉,两三点的时候还爬起来说想下山...,一整晚的噪音把他旁边的一个能睡着的老驴折腾到一晚上没怎么睡,感觉老驴也很难受哈哈。
S就是说觉得冷,因为棚子四处漏风,漏风最严重的就是我们躺下后头顶的地方,风大的时候我问不得不起来给她过上急救毯才好些。
后半夜风停了,我帮她把睡袋裹紧,把头罩住就留个鼻子嘴巴出来,才算是踏踏实实睡了好几个小时,比我有出息多了。

第二天四五点大家便陆续起床了,还能看到星空,但是我手机和充电宝都被冻没电了。

早餐的包子是馊的,我就喝了点稀饭,吃了自己带的面包。
因为出发的时候黑黢黢的,手机也没电了,就没拍什么照片,同行的刘姐借了他的充电宝给我用,让我得以续命。
天终于渐渐亮起来,发现我们被大雾追赶着,手机也有一些电了。


跟着前面的队伍的步伐,发现我们没有走常规的泥巴路线,而是条沿着河谷上行的碎石路段,然后踩着雪沿着山脊线的去往山顶,体验顶满,后来听前面的队友说起才知道,他们跟着一个商团的线路走的。

体验非常棒。

开始爬山脊线。几乎六七十度的大坡,踩着雪前进,非常爽!


就冲这一段爬坡这一趟也值了。

这里的雪一脚下去都能没过膝盖,躺雪上面来一张把。

旁边那条是常规上来的路,马上到了,看上面云层上去的时候应该还能看见雪山。

登顶。
说实话因为湖还没完全化开又被白雪覆盖,也不能去湖面拍照,导致山顶的风景和可玩性很一般,这块白色不知道的人还以为只是一片平坦的雪地。

别的不说,先打个卡把。



看好了,这一剑,会很帅!(换我来拍的时候后面忽然就起了大雾,没拍上😭。)

因为风雪,准备下山了。

后面下山的风景就不发了,下山的时候倒是碰上领队了,因为要做扫尾工作,我们又是垫底的,所以一直被催着下山,搞的我们下山速度飞快,后半段几乎都是跑着下山的。
回到起点。

下雨了,安排第一波人坐皮卡,这波人回到民宿衣服基本上都湿了,我依旧做的蓬卡下山,不过因为没休息好,有点晕车的感觉,差点吐了。

之后便是返程,回去的时候只有泸州的三位和我们一起,另外两个坐其他车了。
也好,我们这几个人更聊的来,我们边开边聊,倒也不觉得累,还拉个群准备下次有什么活动再一起出行。
直到晚上11点30分才安全到达自贡,他们另外拼车叫了个野猪儿回泸州。
这次行程结束我缓了三天才缓过来,每天下班回家几乎粘上枕头就睡,每天的睡眠时长破天荒的突破了8个小时,临近9小时!
下次出行真的不想再开车了。
被“快”裹挟
我又来“续上篇”了,起因是我机缘巧合“二刷”了大J的文章「给心累的父母:往这些方向发力,娃自会给你惊喜」。我是个看文章很少点赞和划线的人,除非打心底认同。今天我打算好好三刷的时候发现,我再一次在曾经打动过我的文字下驻足,同时也生出了一丝纳闷,为什么在写上一篇文章时我没有回忆起这些。归因之下,我想还是因为所有事情都“太快了”。

虽然这篇文章主要讲的是育儿相关,但经历了前阵子的挣扎再读,竟是读出了些人生哲理的意味。所以古人诚不我欺,万物皆通、大道至简。你别说你还真别说,焦虑和无力感来源于不确定性,低迷来源于没劲,迹迹可循。育儿可不是件最为不确定产出的事儿么,而因为人本能喜欢确定,所以这中间一直有着不可逾越和克服的障碍。于是因为没法确定,就觉得做什么都没用,无力感和没劲感便蜂拥而至。

那要如何破解呢?既然此路不通,那就换个路子——只看当下。我最近在育儿问题上有个很大的察觉,那便是我很容易“举一反三”。某些行为可能会导致什么样的后果,这些点可以瞬间在我脑子里排成长长一串,这也是我大部分的焦虑来源。“如果我没有及时修正,那么后果不堪设想”。可是我又无法立刻确认行为出现的必然性,万一只是个偶然性事件呢?何必大费周章,反而引得亲子关系紧张,从而影响了真遇事儿时的教育效果。那么便得仔细观察慢慢引导,可我哪来的时间与精力呢?所以只看当下可能是我当下的最优解。既然未来无法预测,过好当下的每一分每一秒才是实实在在能够攥在手心里的东西。

昨天我还跟朋友说起,每当我因为没有做好追高努力而内疚时,想想她的娃——不吃饭不睡觉不喝奶仍旧长得高,我的焦虑便顷刻消散(滑跪补充:没有不尊敬的意思,反而非常感激分享)。重点如果放在未来还没发生的影响,不如着眼当下,当下才是确定的。比如我好累没法陪坡坡跳绳,便不去想会影响她追高,而去想我能得到短暂的喘息和休息,这心情可不就好多了。原来自洽是这样的呀!

如果真要说一个两全其美的法子,那么还是那句老话——做好自己。关注自己,便不会过分关注孩子,从而挑三拣四,影响亲子关系不说,还破坏自己的心情;提升自己,以身作则,便能抓大放小,上梁板正下梁便也歪不到哪儿去。
昨天跟豆哥聊天,说到我有点点领悟到什么事能使我放松,且知其所以然。刷反转快的短剧、购物、看短视频,这些事都能在短时间内提供大量的新鲜感。没劲的时候可不就得寻求新鲜刺激么!(在感情里的话就会走岔路,万万不可啊!)而如果我采用更“慢”的方式休息,比如冥想(发呆)、听音乐、写博客、画画、看闲书,甚至看电影和电视剧,我都会感觉刺激不够,因为太慢了。细究之下发现,我的脑子可能已经被“惯坏了”,类似上瘾机制,是另一层面的“由奢入俭难”。想到这儿突然理解影视作品里那些惨无人道的富人们,要玩穷人的命才觉有趣,可怕又可悲。
为了改变现状,我决定从不惧怕做花时间的事情做起。写一篇博文就是有可能要花上好几天,不是我动作慢没效率,我需要接受现实,而不是逼迫自己。娃习得新技能就是要经历很多次的失败,耗费大量的时间,我要做的仍旧是接受这件事,并且尽可能地提供帮助。

踏踏实实地生活吧,人生的意义其实只在自己心中。
线下买冰箱
昨天去看抽油烟机和燃气灶,顺便也看了一下冰箱。孩子妈只认容声冰箱,所以两个商场就只看容声。
第一个商场容声和海信居然一个店面,卖货的说容声海信一家了,不知道真假。我们看中的一个型号,冰箱身上贴着标价 ¥7*99(中间那个 * 印象不深了)。孩子妈问实价,销售的一顿计算加上这个补那个优惠的,报价 4800,然后我打开京东,搜了一下居然有相同型号的(一般大部分商品都有分电商款,型号不同),上面显示到手价 ¥45**,销售一看就说,那给你 4500,不赚你们钱了,还打包票到送货前有价格更低的给我退差价。
当时我就想买了,因为我们主要是来看油烟机和燃气灶的,既然比线上便宜那就干脆顺手搞定。孩子妈却不急,要店员送礼物。销售在仓库里一顿扒拉,什么米啊锅啊的我们都不需要。然后媳妇又要砍价,销售立场很坚定,我们就走了。
另外一个商场,可能时间比较迟了,到了容声专卖的地方销售都不在,自己走了一圈店员才过来。
我们赶时间就直接问刚才那个型号多少钱,她也看出我们在其他地方问过价,就直接问我上一家报价多少,我也没隐瞒说 4500。她说直接给你全市最低价 3999,如果帮她一个忙 还送 10 年整机保修。
这么一报价我们两个心里都没底了,我问这冰箱型号是不是不生产了,店员说这是去年的款式(网上没查到)。
最后还是定了这款冰箱,帮忙是帮她买一台他们自己的容声冰箱样机,标价一万多,售价3600多,而最后我那条冰箱成交价是 3888 元,发票开出来去掉 10 年保修的价格,冰箱实际发票价格是 3600 元。
所以这价格为什么这么魔幻,不会卖给我的也是样机吧。
认识 WordPress 古腾堡编辑器:界面与功能板块
古腾堡编辑器(Gutenberg Editor)采用“块”式设计理念,编辑器内的每个部分(如标题、段落、图片、列表、视频、按钮等)视为独立的内容块。用户可以像搭积木一样自由拖拽、排列、插入或删除各种块,本篇介绍其界面和板块。
1. WordPress Gutenberg Editor 界面介绍
下图展示的是 WordPress 当前版本(6.8.1)的古腾堡编辑器界面。为了便于整体查看,我将区块属性和区块样板的界面截图进行了拼接。可以看到,默认的古腾堡编辑器界面由顶部工具栏、主编辑区、右侧设置面板和左侧导航等部分组成,各个功能区域布局清晰,便于用户高效进行内容创作与排版。

1.1 顶部工具栏
顶部工具..... [ 阅读全文 ]
原文链接: https://www.shephe.com/website/wordpress-gutenberg-editor/
版权声明: Kevin's Space 版权所有,转载请用明链标明本文地址
本站相关: 随机文章 | 站长微博 | 关于本站 | 联系站长 | 捐助作者
线下买冰箱
昨天去看抽油烟机和燃气灶,顺便也看了一下冰箱。孩子妈只认容声冰箱,所以两个商场就只看容声。
第一个商场容声和海信居然一个店面,卖货的说容声海信一家了,不知道真假。我们看中的一个型号,冰箱身上贴着标价 ¥7*99(中间那个 * 印象不深了)。孩子妈问实价,销售的一顿计算加上这个补那个优惠的,报价 4800,然后我打开京东,搜了一下居然有相同型号的(一般大部分商品都有分电商款,型号不同),上面显示到手价 ¥45**,销售一看就说,那给你 4500,不赚你们钱了,还打包票到送货前有价格更低的给我退差价。
当时我就想买了,因为我们主要是来看油烟机和燃气灶的,既然比线上便宜那就干脆顺手搞定。孩子妈却不急,要店员送礼物。销售在仓库里一顿扒拉,什么米啊锅啊的我们都不需要。然后媳妇又要砍价,销售立场很坚定,我们就走了。
另外一个商场,可能时间比较迟了,到了容声专卖的地方销售都不在,自己走了一圈店员才过来。
我们赶时间就直接问刚才那个型号多少钱,她也看出我们在其他地方问过价,就直接问我上一家报价多少,我也没隐瞒说 4500。她说直接给你全市最低价 3999,如果帮她一个忙 还送 10 年整机保修。
这么一报价我们两个心里都没底了,我问这冰箱型号是不是不生产了,店员说这是去年的款式(网上没查到)。
最后还是定了这款冰箱,帮忙是帮她买一台他们自己的容声冰箱样机,标价一万多,售价3600多,而最后我那条冰箱成交价是 3888 元,发票开出来去掉 10 年保修的价格,冰箱实际发票价格是 3600 元。
所以这价格为什么这么魔幻,不会卖给我的也是样机吧。
【2025也闲谈·廿一】人工智能时代需要怎样的教育
C++中的 const和constexpr 比较
C++ const
与 constexpr
:真正的区别是什么?
一眼看都是定义常量。
为什么这很重要
现代 C++ 鼓励编写不可变、高效且表达力强的代码。两个关键字—const
和 constexpr
—是这一理念的核心。它们看起来很相似,但理解它们的不同语义,对于正确利用编译期与运行期行为至关重要。
高层次对比
特性 | const |
constexpr |
---|---|---|
编译期常量? | 可能 | 一定(否则编译报错) |
支持运行期? | 支持 | 支持(在需要时运行期求值) |
用于数组/模板参数? | 仅当确实是常量 | 保证可用 |
允许函数? | 仅限成员函数限定符 | 支持完整函数且可在编译期求值 |
1 声明不可变数据
const
:构造后不可变
const int runtimeConst = std::rand(); // 是 const,但不是编译期常量
当你只想禁止变量被修改,而不在意值是在编译期还是运行期确定的,const
就足够了。
constexpr
:必须在编译期已知
constexpr int arraySize = 10; int arr[arraySize]; // 始终合法
如果值需要参与要求编译期常量的上下文(如数组大小、模板参数、switch 标签等),你必须使用 constexpr
。
2 函数与方法
const
成员函数
class Widget { public: int value() const {/*…*/} // 保证不会修改 this 对象 };
它保护对象状态,但不提供编译期求值能力。
constexpr
函数
constexpr int square(int n) { return n * n; } static_assert(square(4) == 16, "编译期计算");
constexpr
函数在参数是常量表达式时可以在编译期执行,也可以在运行期使用。
3 常见陷阱
// 1. 编译通过:runtimeConst 只是 const const int runtimeConst = std::rand(); // 2. 编译失败:std::rand() 不是 constexpr constexpr int fails = std::rand();
记住:每个 constexpr
变量本质上都是 const
,但并非所有 const
都是常量表达式。
4 如何选择
- 需要强制编译期计算?使用
constexpr
- 需要不可变性但值可能在运行期确定?使用
const
- 不确定时偏向使用
constexpr
,编译器会提示你是否不合法
5 总结片段
constexpr int ctVal = 42; // 编译期常量 const int rtVal = std::rand(); // 运行期确定,但不可变
正确地选择 const
和 constexpr
能让你的 C++ 代码更安全、更高效、更具表达力。默认使用 constexpr
,当且仅当你明确知道值只能在运行期获取时才使用 const
。
C/C++编程
- C++ 教程: 用std::move来移动所有权
- C++中的 const和constexpr 比较
- 简易教程: C++的智能指针
- C++ 编程练习题: 如何合并两个二叉树?
- C++ 编程练习题 - 找出第三大的数
- C++ 编程练习题 - 最多连续的 1
- C++ 编程练习题 - 左子树叶节点之和 (深度优先+广度优先+递归)
- C++ 编程练习题 - 最多水容器 (递归)
- C++的异步编程: std::future, std::async 和 std::promise
- C编程练习题: 翻转整数位
- C++编程练习题: 找出字符串的所有大小小组合
- C/C++ 中的内存管理器(堆与栈)
- C++编程练习题: 对两单向链表求和

相关文章:
- 简易教程: C++的智能指针 C++ 智能指针教程 C++ 中的智能指针提供了自动且安全的内存管理。它们通过 RAII(资源获取即初始化)机制,帮助开发者避免内存泄漏和悬空指针的问题,确保对象在生命周期结束时被正确释放。 本教程将介绍 C++ 中三种主要的智能指针: std::unique_ptr:独占式所有权 std::shared_ptr:共享式所有权 std::weak_ptr:非拥有式弱引用 1. std::unique_ptr unique_ptr 拥有独占所有权。一个资源只能被一个...
- C++ Ranges 教程 C++20 引入了 ranges(范围),这是一个强大且优雅的抽象,用于处理序列(如数组、vector 等)。相比传统的迭代器或旧式循环,Ranges 提高了代码的可读性、可组合性和性能。 什么是 Range? 在 C++20 中,range(范围) 是一种抽象,代表一个可以迭代的元素序列。它与 views(视图) 和 actions(操作) 如过滤、转换等配合使用非常自然。...
- 被动收入之: 微博红包 今年开始重新经营我的微博帐号 drlai 收到两笔微信红包,应该是来自于官方的支持,150元(成功提现到支付宝)。虽然这不能持久,也没多少,但毕竟实现了零的突破,意义重大。 如果流量上来,内容创作者可能会接受到比较多的赞赏,这也是一个比较简单的变现方法。这也能作为一种被动收入,不过如果不是头部网红,可能杯水车薪,但如果你有好几个类似这样的,也能积少成多! 在用户中心,微博用户可以每天登陆手机微博APP打卡,获取点数和少量的红包钱(几分钱),积少成多! 微博做些小任务可获得积分和几分钱。聊胜于无。 微博的主要盈利模式 微博的主要盈利模式主要包括以下几个方面: 广告收入:微博的大部分收入来源于广告,尤其是品牌广告和效果广告。广告形式包括信息流广告(类似于推文广告)、热门话题广告、开屏广告和视频广告。品牌和企业可以利用微博庞大的用户群和社交互动来提升曝光率、推广品牌和产品。 会员服务:微博提供的VIP会员服务,用户可以支付订阅费用来享受更多的特权,比如个性化的主题、特有的表情包、私密权限设置等。这些会员服务主要面向个人用户,提升其社交体验。 直播和打赏:微博提供直播平台,用户可以通过购买虚拟礼物来支持主播,微博会从这些打赏中抽取一定比例的分成。此外,微博与内容创作者分成,通过内容付费、知识付费等形式变现。 增值服务:针对企业和大V(拥有大量粉丝的用户),微博还提供增值服务,如账号认证、粉丝数据分析、精准推送、推广和营销工具等。这些服务帮助企业提升营销效果,同时也增加了微博的收入来源。 电商和导流:微博上有大量的电商导流业务,尤其是和明星、网红的合作推广。微博用户在浏览社交内容时,可以直接跳转到商品购买链接,微博通过这种方式赚取导流佣金。 游戏联运:微博也会与一些游戏公司合作推出联合运营的游戏,微博负责推广和流量引入,用户充值或付费时,微博可以获得一部分的分成。 这些模式相结合,使得微博能够在广告市场、内容创作和电商等多个领域获利。...
- 借助AI快速开源了三个小工具: 写代码越来越像做产品了, AI 真把我宠坏了(Vibe Coding) 程序员的未来?Vibe Coding + AI 一起上! 借助 AI 快速开源了三个小工具 最近,我利用 ChatGPT-4o 和 o4-mini 快速开发并开源了三个小工具。起因其实很简单——每次想转换 YAML/JSON 或进行...
- 换了个奥迪Q5大灯花了我1000英镑 我那辆奥迪Q5 SUV今年年检没通过,原因是左前车灯坏了,需要更换。车厂告诉我,光是订购零件就要700多英镑,加上人工费,总费用得1000英镑。但没办法,如果不修,车辆年检(MOT)就过不了,车也不能上路。 MOT是英国的机动车强制性安全检测(Ministry of Transport Test)的简称。 近侧前位置灯不工作 drl/位置灯集成(4.2.1(a)(ii)) Nearside Front Position lamp not working drl/position...
- 你给SteemIt中文微信群拖后腿了么? 这年头不缺算法, 就缺数据. 这两天花了很多时间在整API上, 整完之后自己用了一下还觉得真是挺方便的. 今天就突然想看一看自己是否给大家拖后腿了, 于是调用每日中文区微信群排行榜单的API, 刷刷拿着 NodeJs 练手: 1 2 3 4 5 6...
- 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
- 剑桥网红餐厅 The Ivy Cambridge Brasserie 上周刚过40岁生日,媳妇带我去剑桥的一家网红餐厅——The Ivy Cambridge Brasserie。这家餐厅是朋友推荐的,和那家Trinity一样很有名,可惜我们在剑桥待了近10年才知道。 The Ivy Cambridge Brasserie 是剑桥市中心一家备受欢迎的网红餐厅,位于历史悠久的 Trinity Street。餐厅设计时尚典雅,融合了现代与经典元素,为食客提供了一个舒适而奢华的用餐环境。这里的菜单丰富多样,覆盖全天用餐,从早餐到晚餐以及下午茶,提供英式经典美食如松露鸡肉派、英式早餐等,也有一些国际风味的菜肴。得天独厚的位置和独特的氛围使得 The Ivy Cambridge Brasserie...
C++ Ranges 教程
C++20 引入了 ranges(范围),这是一个强大且优雅的抽象,用于处理序列(如数组、vector 等)。相比传统的迭代器或旧式循环,Ranges 提高了代码的可读性、可组合性和性能。
什么是 Range?
在 C++20 中,range(范围) 是一种抽象,代表一个可以迭代的元素序列。它与 views(视图) 和 actions(操作) 如过滤、转换等配合使用非常自然。
传统循环 vs 基于 Range 的循环
#include <iostream> #include <vector> int main() { std::vector<int> v = {1, 2, 3, 4}; // 旧式循环 for (auto it = v.begin(); it != v.end(); ++it) std::cout << *it << ' '; // 基于范围的循环(C++11) for (auto x : v) std::cout << x << ' '; }
Range Views(视图)
View 是惰性的、可组合的范围操作。除非需要,一般不会复制数据。
Filter 和 Transform 示例
#include <iostream> #include <vector> #include <ranges> int main() { std::vector<int> v = {1, 2, 3, 4, 5, 6}; auto even_doubled = v | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * 2; }); for (int n : even_doubled) std::cout << n << ' '; // 输出:4 8 12 }
常见的 Views
View | 描述 |
---|---|
std::views::filter | 保留符合条件的元素 |
std::views::transform | 对每个元素应用函数 |
std::views::take(n) | 获取前 n 个元素 |
std::views::drop(n) | 跳过前 n 个元素 |
std::views::reverse | 反转范围 |
std::views::iota(a, b) | 生成从 a 到 b-1 的范围 |
使用 iota 和 reverse
#include <ranges> #include <iostream> int main() { for (int i : std::views::iota(1, 6) | std::views::reverse) std::cout << i << ' '; // 输出:5 4 3 2 1 }
组合视图操作
你可以使用管道符 |
流式地组合多个视图操作。
#include <vector> #include <ranges> #include <iostream> int main() { std::vector<int> v = {5, 10, 15, 20}; auto result = v | std::views::transform([](int x) { return x + 1; }) | std::views::filter([](int x) { return x % 2 == 0; }); for (int x : result) std::cout << x << ' '; // 输出:6 16 }
实用示例
1. 过滤偶数
#include <iostream> #include <vector> #include <ranges> int main() { std::vector<int> numbers = {1, 2, 3, 4, 5, 6}; auto evens = numbers | std::views::filter([](int n) { return n % 2 == 0; }); for (int n : evens) std::cout << n << ' '; // 输出:2 4 6 }
2. 将奇数翻倍
int main() { std::vector<int> numbers = {1, 2, 3, 4, 5}; auto doubled_odds = numbers | std::views::filter([](int n) { return n % 2 != 0; }) | std::views::transform([](int n) { return n * 2; }); for (int n : doubled_odds) std::cout << n << ' '; // 输出:2 6 10 }
3. 反转序列
int main() { std::vector<int> nums = {10, 20, 30}; auto reversed = nums | std::views::reverse; for (int n : reversed) std::cout << n << ' '; // 输出:30 20 10 }
4. 生成数值序列
#include <ranges> int main() { for (int i : std::views::iota(1, 6)) std::cout << i << ' '; // 输出:1 2 3 4 5 }
5. 获取前 N 个元素
int main() { auto infinite = std::views::iota(1); // 无限序列 auto first5 = infinite | std::views::take(5); for (int i : first5) std::cout << i << ' '; // 输出:1 2 3 4 5 }
6. 计算前 5 个奇数的平方和
#include <numeric> int main() { auto odd_squares = std::views::iota(1) | std::views::filter([](int x) { return x % 2 == 1; }) | std::views::transform([](int x) { return x * x; }) | std::views::take(5); int sum = std::accumulate(odd_squares.begin(), odd_squares.end(), 0); std::cout << "和 = " << sum << '\n'; // 输出:和 = 165 }
7. 判断是否所有元素都为正数
#include <ranges> #include <algorithm> #include <vector> #include <iostream> int main() { std::vector<int> nums = {1, 2, 3}; bool all_positive = std::ranges::all_of(nums, [](int n) { return n > 0; }); std::cout << std::boolalpha << all_positive << '\n'; // 输出:true }
8. 自定义管道函数
auto pipeline = [](const std::vector<int>& v) { return v | std::views::filter([](int x) { return x % 2 == 0; }) | std::views::transform([](int x) { return x * 10; }); }; int main() { std::vector<int> nums = {1, 2, 3, 4}; for (int x : pipeline(nums)) std::cout << x << ' '; // 输出:20 40 }
性能提示
- Ranges 是惰性的:仅在需要时才处理元素。
- 避免不必要的分配与复制。
- 适合处理大型数据或函数管道。
何时不适合使用 Ranges
- 在对性能极度敏感的内循环中,STL 抽象可能较慢。
- 当项目尚未迁移到 C++20。
参考资料
本文一共 415 个汉字, 你数一下对不对.
相关文章:
- 简易教程: C++的智能指针 C++ 智能指针教程 C++ 中的智能指针提供了自动且安全的内存管理。它们通过 RAII(资源获取即初始化)机制,帮助开发者避免内存泄漏和悬空指针的问题,确保对象在生命周期结束时被正确释放。 本教程将介绍 C++ 中三种主要的智能指针: std::unique_ptr:独占式所有权 std::shared_ptr:共享式所有权 std::weak_ptr:非拥有式弱引用 1. std::unique_ptr unique_ptr 拥有独占所有权。一个资源只能被一个...
- 借助AI快速开源了三个小工具: 写代码越来越像做产品了, AI 真把我宠坏了(Vibe Coding) 程序员的未来?Vibe Coding + AI 一起上! 借助 AI 快速开源了三个小工具 最近,我利用 ChatGPT-4o 和 o4-mini 快速开发并开源了三个小工具。起因其实很简单——每次想转换 YAML/JSON 或进行...
- 被动收入之: 微博红包 今年开始重新经营我的微博帐号 drlai 收到两笔微信红包,应该是来自于官方的支持,150元(成功提现到支付宝)。虽然这不能持久,也没多少,但毕竟实现了零的突破,意义重大。 如果流量上来,内容创作者可能会接受到比较多的赞赏,这也是一个比较简单的变现方法。这也能作为一种被动收入,不过如果不是头部网红,可能杯水车薪,但如果你有好几个类似这样的,也能积少成多! 在用户中心,微博用户可以每天登陆手机微博APP打卡,获取点数和少量的红包钱(几分钱),积少成多! 微博做些小任务可获得积分和几分钱。聊胜于无。 微博的主要盈利模式 微博的主要盈利模式主要包括以下几个方面: 广告收入:微博的大部分收入来源于广告,尤其是品牌广告和效果广告。广告形式包括信息流广告(类似于推文广告)、热门话题广告、开屏广告和视频广告。品牌和企业可以利用微博庞大的用户群和社交互动来提升曝光率、推广品牌和产品。 会员服务:微博提供的VIP会员服务,用户可以支付订阅费用来享受更多的特权,比如个性化的主题、特有的表情包、私密权限设置等。这些会员服务主要面向个人用户,提升其社交体验。 直播和打赏:微博提供直播平台,用户可以通过购买虚拟礼物来支持主播,微博会从这些打赏中抽取一定比例的分成。此外,微博与内容创作者分成,通过内容付费、知识付费等形式变现。 增值服务:针对企业和大V(拥有大量粉丝的用户),微博还提供增值服务,如账号认证、粉丝数据分析、精准推送、推广和营销工具等。这些服务帮助企业提升营销效果,同时也增加了微博的收入来源。 电商和导流:微博上有大量的电商导流业务,尤其是和明星、网红的合作推广。微博用户在浏览社交内容时,可以直接跳转到商品购买链接,微博通过这种方式赚取导流佣金。 游戏联运:微博也会与一些游戏公司合作推出联合运营的游戏,微博负责推广和流量引入,用户充值或付费时,微博可以获得一部分的分成。 这些模式相结合,使得微博能够在广告市场、内容创作和电商等多个领域获利。...
- 换了个奥迪Q5大灯花了我1000英镑 我那辆奥迪Q5 SUV今年年检没通过,原因是左前车灯坏了,需要更换。车厂告诉我,光是订购零件就要700多英镑,加上人工费,总费用得1000英镑。但没办法,如果不修,车辆年检(MOT)就过不了,车也不能上路。 MOT是英国的机动车强制性安全检测(Ministry of Transport Test)的简称。 近侧前位置灯不工作 drl/位置灯集成(4.2.1(a)(ii)) Nearside Front Position lamp not working drl/position...
- 试用 Linkedin (领英) 高级帐号 (Premium) Linkedin (领英) 算是比较靠谱的职业社交网站, 在上面有很多猎头, 很多知名公司的HR 无时无刻在招人. 特别领英在被微软收购之后, 名气就变得大了许多. 领英是免费使用的, 但也有付费用户, 有给猎头的, 也有给想找工作的. 价格并不便宜, 对于想找工作的 Job...
- 步步高学生电脑上 Basic 编程语言 peek 用法示例 步步高学生电脑 是8位FC机的经典之作.它上面的BASIC有三个版本 1.0, 2.0 和 2.1 2.1 版本有个在线帮助,实际上是 help.cmd 1.0 是用 Esc 键退回到 DOS 的,...
- 《Steem 指南》之 justyy 在线工具与 API 系列 – Discord 机器人 Discord 聊天频道 Discord 原本是给游戏设计的, 但由于其功能多, 接口开放能力强, 使用的用户越来越多. 我们CN区也有一个Discord 频道, 加入地址为: https://discord.gg/7ctT3Xt 在网页里就可以加入 cnsteem 的大家庭了, 当然也可以下载手机APP或者桌面程序来加入...
- 你给SteemIt中文微信群拖后腿了么? 这年头不缺算法, 就缺数据. 这两天花了很多时间在整API上, 整完之后自己用了一下还觉得真是挺方便的. 今天就突然想看一看自己是否给大家拖后腿了, 于是调用每日中文区微信群排行榜单的API, 刷刷拿着 NodeJs 练手: 1 2 3 4 5 6...
20250517
今天第一次来上海的 Blue Note这种 Club 式的演出场地我还不是很适应。但是边喝酒边听 Jazz 才带感啊。
4/24 第一次听 Brad Mehldau,应该是 ChatGPT 给我推荐的和 Keith Jarrett 类似的音乐家。非常喜欢下面这张专辑,于是上闲鱼买黑胶。没想到一搜就搜出了 Brad Mehldau Trio 五月在国内 Blue Note 演出的票。缘分到这种程度了,不去看真不合适。立即买了两张上海的。

没想到一个月内就听上现场了,除了 Brad Mehldau,贝斯手 Christian Mcbride 简直神了。查了一下他的老师居然是 Ray Brown!还和各种我喜欢的音乐家合作过,能听上他的表演,太开心🥳。

今天又发现了一家很不错的酒店,大华长风华邑。16000 积分兑换,不到六百块钱。酒店门面比六百块钱的亚朵好了一百倍吧,房间升了一级后也是宽敞舒适,居然还有行政酒廊。窗外是个公园,景也不错。早上洗完头发现用的是 Dyson 吹风机,要知道很多洲际现在都是山寨的 Dayson 😂。直接宣布它已经取代瑞金洲际成为我上海看演出的首选。大大降低每次看演出的成本。

20250516
今天五源资本的两个投资人来拜访认识,尽管我一直在拒绝和“投资人”们聊天,但偶尔交流认识一下还行。给他们放了黑胶,也算给他们输出一些东西吧。
中午约博士吃饭,博士送了我们任天堂出的那个闹钟。做这样的周边真有意思,把游戏、角色、声音资源、玩家紧紧连结在一起。我们最近为了去美国,有一些礼物可以拿来送给其他开发者,终于也做起了周边。但是创意贫瘠,只能想到贴纸和徽章。徽章的样本到了,把这个回赠给了博士。我还是很喜欢我们 logo 的设计。

今天一直在听 Holly Cole 的专辑《Temptation》,在闲鱼刷黑胶的时候看到这张专辑的复刻,被介绍深深吸引了。作为一张爵士女声专辑,选曲居然都是 Tom Waits 的歌。Tom Waits 在我心里形象更多是荒诞不经,但是我很喜欢。昨晚在家里 HomePod 播放后没有什么感觉,今天来公司听,不得不说有了完全不同的感觉。应该不是“木耳朵”了!
我的 WordPress 网站安装了哪些插件(2025.06)
WordPress 插件是扩展网站功能的强大工具,用户无需编写代码即可轻松添加各种特性和功能。插件种类繁多,包括 SEO 优化、社交分享、电子商务、网站安全等,用户可以根据需求自由选择和安装,从而显著提升网站的灵活性与可操作性。除了市面上的成熟插件包,我们还可以通过添加增强扩展(如纯代码形式的插件)来进一步扩展功能与性能,例如官方曾经发布的对象存储插件 object-cache.php
。
这些丰富的插件和主题构成了 WordPress 强大的生态系统,正是这一生态让 WordPress 成为全球最受欢迎的建站平台,魅力无可比拟。那么,关于必备插件的推荐,实际上并没有一个固定答案。因为当前的 WordPress 核心程序已经非常完善,从安全性、样式、编辑器等方面都能满足大多数用户的需求。加之海量的主题市场,许多曾经流行的个性功能已经直接集成到系统和主题中了,比如懒加载、灯箱效果、缓存功能等。
因此,我建议你根据实际需求选择和安装插件,这不仅可以节省精力和时间、提高性能,还能避免安装恶意插件导致系统崩溃。如果非要说有哪些插件是必备的,那大概是 Akismet
和 你好多莉
吧。毕竟,当你完成 WordPress 的安装时,它们已经默默地存在了,哈哈。
原文链接: https://www.shephe.com/website/wordpress-plugin-recommendations/
版权声明: Kevin's Space 版权所有,转载请用明链标明本文地址
本站相关: 随机文章 | 站长微博 | 关于本站 | 联系站长 | 捐助作者
游戏组件的一生: 从加载到上屏
title: "游戏引擎是如何运行的——游戏组件的一生: 从加载到上屏" date: 2025-05-17T12:01:47+08:00 categories: ['develop'] draft: true
游戏引擎是如何运行的——游戏组件的一生: 从加载到上屏
1. 小游戏容器与游戏引擎
小游戏容器的设计上可以理解是一种特化版的 WebView,渲染上下文上裁剪了多余的 DOM Element,只保留 Canvas;而脚本引擎上则 JS Polyfill 或是容器 Binding 的方式去对齐 ECMA-262 的标准。此外容器还需要提供 Script 加载与执行、WASM 等新标准处理、以及 Audio 与 Video 等多媒体能力,这些能力都将通过 JSBinding 的形式,将接口包装成 BOM 的形式给到 JS 侧使用。
小游戏容器之所以要设计成符合 Web 标准的容器,是为了兼容不同游戏引擎。这种设计理念的本质是将底层平台能力标准化、通用化,把碎片化的硬件、系统能力屏蔽在容器内部,只向上提供一套与浏览器 BOM、DOM 类似的编程模型,使得各类游戏引擎(如 Cocos、Egret、Laya、Unity WebGL)都可以以 Web 的运行环境的方式接入,避免每个引擎都去适配各家平台的原生能力。这实际上是 WebView 本地化、轻量化的一次再演化,小游戏容器约等于一个轻量浏览器内核。
这个过程中容器负责“平台标准化”,引擎负责“内容生态”,比如
小游戏容器的职责:
- 提供统一的渲染上下文(Canvas/WebGL)。
- 提供统一的脚本运行时(JS/WASM)。
- 提供标准化的输入、音频、视频、多媒体 API。
- 提供网络、存储、支付、分享、广告等平台能力封装。
- 对接安全沙箱、权限管理、性能隔离等系统层。
游戏引擎的职责:
- 提供高层抽象的场景管理、物理引擎、动画、资源管理。
- 提供开发者友好的编辑器、调试工具链。
- 提供跨平台的组件化开发范式(UI、骨骼动画、粒子系统等)。
- 管理游戏生命周期、状态同步、渲染调度。
接下来,以 Cocos 引擎的渲染管线为例,介绍小游戏容器对资源的加载流程以及对游戏组件的渲染流程。
2. 游戏引擎中的三大循环
游戏引擎的渲染管线由三大循环进行驱动,分别是渲染循环、事件循环和游戏循环,以下是梳理出来的三大循环的全景图:

2.1 渲染循环 RenderLoop
首先是渲染循环,它的主流程如下图所示:

整个渲染循环由系统的 Vsync 信号驱动,iOS 由 CADisplayLink 发起,通过应用进程的主线程的 RunLoop 来执行渲染任务,具备一定的帧率控制能力,如 iOS 下可以设定 30/60/90/120 FPS。
在引擎侧,核心流程做了 3 件事:
glFlush
清空 GL 缓冲指令:将上帧未执行的 OpenGL 指令强制刷新,确保显存与帧缓区数据一致,防止由于指令堆积导致的“帧延迟”或“卡顿”。UpdateScheduler
异步任务调度:调度当前帧需要触发的异步任务,例如音频回调、网络事件响应等。保证非渲染逻辑(如数据更新)与渲染解耦,提高主线程并发能力。Tick
驱动 JS 层逻辑:每帧通过 Binding 固定调用 JS 侧 Tick 方法,执行动画、状态更新等与渲染相关的逻辑。从而实现逻辑层与渲染层的解耦,增强跨平台的适配能力。
在容器侧,iOS 通过 CAEAGLayer
处理 GL 指令上屏,主要有两个步骤:
glBindRenderbuffer
绑定 RenderBuffer:将当前帧渲染结果绑定至 RenderBuffer,作为上屏缓冲区。PresentRenderbuffer
显示输出:将 RenderBuffer 内容呈现至屏幕,实现用户可见的最终画面。
在 iOS 渲染体系中,最终负责显示的组件是 CAEAGLLayer。它作为 Layer 树(Layer Tree) 的一部分,直接引用共享内存中的渲染缓冲区(Renderbuffer 数据)。与此同时,系统的 Compositor(合成器) 会将 CAEAGLLayer 的内容与其他 UI 元素(如 UIKit、SwiftUI)进行统一合成,最终输出到屏幕。
在每一帧的 Tick 任务 中,JavaScript 会与游戏引擎协作,生成本帧所需的 Framebuffer(详见 3.5 至 3.10 节)。此时,Core Animation 与 OpenGL ES 通过共享渲染缓冲区实现数据同步。这意味着,OpenGL 渲染结果实质上只是一块 Layer 树中的画布,最终仍需与系统 UI 层级一同被合成为最终显示图像。

当然,本文中涉及的小游戏容器仅使用了 OpenGL 作为渲染后端,随着 Metal、Vulkan 等新一代图形 API 的兴起,RenderBuffer 绑定与上屏流程将更倾向“并行渲染 + 异步上屏”,提升高帧率下的流畅度与低延迟体验。 这个渲染循环的逻辑是同步执行的,因此如果将帧率设置为 60 FPS 时,以上所说的一帧的逻辑没有在 16.6ms 内运行完,便会导致 Jank。

比如在这个 Bad case 中,运行 Tick 任务时,在主线程的 JS 执行了 136ms,就导致了游戏动画卡顿:

因此,为了保证游戏运行的流程性,意味着我们需要不停地打磨性能,尽可能降低同步任务的耗时。性能优化一定要借助 Profiling 工具,以下是一些常用的工具:
- Xcode GPU Frame Debugger:针对 iOS 平台的图形调试工具,能够深入分析 渲染管线级别的性能瓶颈,尤其适合 Metal 与 OpenGL ES 开发场景。
- RenderDoc:业界主流的跨平台图形调试工具,支持捕捉帧数据,分析渲染管线各阶段的资源与性能瓶颈,适用于 OpenGL、Vulkan、DirectX 等 API。
- inspector.js:Web 端可以使用,便于在 WebGL 场景下分析 DrawCall、着色器与资源绑定等性能数据。
- Mali offline shader compiler:https://zhuanlan.zhihu.com/p/161761815,适用于 ARM Mali GPU 的离线着色器编译与分析工具,可用于评估 Shader 复杂度与指令执行成本,优化移动端渲染性能。
- Snapdragon Profiler: 抓帧工具,支持统计 Heavy DrawCall 与 Overdraw,帮助识别渲染瓶颈与冗余计算。
2.2 事件循环 EventLoop
我们向下,从 Tick 任务进入到第二个循环 —— 事件循环。

因为小游戏容器不是 WebView,只有一个 JS 引擎,因此我们需要实现一个事件循环机制,驱动 JS 执行(不一定完全对齐浏览器标准,只需要满足容器要求即可)。由图可见,主要包括 3 个任务:
- 消费 timer 等宏任务:处理通过 setTimeout、setInterval 等方式注册的定时任务,确保定时逻辑的正确触发。
- 消费 rAF 任务:这一步主要是为了驱动 GameLoop 逻辑,游戏主循环通常挂载于 rAF 回调中,用于逐帧更新渲染与逻辑。
- 清空当前帧的 Commands:执行渲染命令、界面更新等待处理的指令,完成本帧渲染周期。
这里重点说一下 rAF 的实现。在早期,rAF 通过 setTimeout(0) 来模拟实现,链路如下:

可以发现这里是存在问题的:
- 不合规范:是使用 setTimeout 0 模拟的,并非 vsync 直接驱动。
- 链路太长:Native 来维护 Timer 队列,等待 vsync 信号消费完之后再回调给 JS。
后来按照 WHATWG 标准进行了重构,

优点如下:
- 标准化:vsync 后直接触发 JS 的调用
- 开销小:JS 维护 Timers 队列,移除原生层中转的 JSBinding 调用开销。
可见渲染性能的优化,关乎在很多实现的细节上,需要挖掘与打磨。
最终,通过以上的事件循环,容器能够维持 JS 引擎与渲染系统之间的协同工作,实现游戏的持续运行与更新。
2.3 游戏循环 GameLoop
这一部分展开来说就是第 3 章——游戏组件的一生:

在展开画卷之前,介绍一下传统的使用 OpenGL 作为渲染后端的小游戏容器的渲染流程:

首先是资源加载,涉及到两种完全不同的资源处理——脚本资源和静态资源。脚本资源由 JS Runtime 进行处理,而静态资源则针对不同类型的文件又有各自的处理方案——包括图片、字体、音频、视频、还有比较特殊的骨骼动画。因为本文主要说渲染,就不展开介绍资源加载流程了。
之后,这些资源被游戏引擎渲染关键处理,由 JS 驱动生成 WebGL 指令,通过 JS Binding 最终调用到 C++ 或 Native 侧的 OpenGL 指令集上 —— WebGL 是 OpenGL 的子集,因此可以一一对应。

这个过程往往会出现很多渲染瓶颈,因此其中会涉及到很多优化项。我们根据硬件资源来看,主要关注 CPU、GPU 和带宽。而在当下移动端硬件资源并不富裕的场景下,对于游戏的优化,本质上变成了“平衡的艺术” —— 我们需要去平衡 CPU、GPU 和带宽资源。即如果瓶颈不能消灭,就需要转移瓶颈,比如经常见的是从 CPU 移到 GPU —— 使用 Computer Shader、GPU skinning、Animation Bake、GPU particles 等等。
对于 CPU,这是最常见的瓶颈。这里不展开说游戏业务侧的优化项(减少 DrawCall 的 Culling、Batching 这些),而是从容器侧提供一些优化思路。
- 比如上面的 JS Binding 调用可能会导致瓶颈,那我们可能会去做合批,从两方面去实现,一方面是调用次数合批,做 CommandBuffer 增加吞吐;另一方面可以做调用实现的合并,比如提供 GFX 高级图形库。
- 还比如一些 JS 同步任务会阻塞主线程,那么就把计算密集型的任务转到 Native 去做。
- 比如 JS 自身解释执行的执行效率,那就想办法用 JIT 或者 WASM。
- 再比如 GC 上,也有一些优化的地方。
对于 GPU,如果产生瓶颈了,一般是由于 Fragment Shader 指令太复杂,或者 Vertex Buffer 过大,比如 3D 渲染中的三角形面数超过阈值,一般移动端场景下需要控制在 50 万面到 150 万面之间。另外,高 Overdraw 也会导致 GPU 多做很多无用功。
对于带宽瓶颈,则主要是靠压缩纹理(桌面端还可以用延迟渲染和后处理技术)。在网上有这么一个结论:
如果你的游戏跑 60 帧,那么每帧可用的带宽将会是 21024/60 = 34M, 假设你的 GBuffer 的分辨率是 1280 \ 1080,那么写一次 GBuffer(RGBA 4 个字节)的带宽大小为: 12801080\4/1024/1024 = 5.2M, 如果 3 张则是 15.6M.
考虑到一般你的游戏都会有 Overdraw, 假设 Overdraw 比较合理在 1.5 左右,那么这样的带宽消耗就能占到 15.6 * 1.5 = 23.4 M。 考虑到你还要渲染场景,ui 和角色等内容,这样很容易就超过了每秒 34M 的推荐带宽占用。
下图是一个常见的同步渲染管线:
- 应用层提供顶点数据
- 构建顶点着色器对顶点进行标准化
- 图元装配构建几何图元
- 光栅化阶段,将图元离散化为片元,每个片元对应屏幕上的像素区域
- 片元着色器对每个片元执行纹理采样、颜色计算、雾效等像素级处理。
- 进行测试与混合操作(Alpha、深度、模板测试),并将结果写入帧缓冲区 Framebuffer。

构建完 Framebuffer 后,就回到了我们 2.1 节所说的 CAEAGLLayer 绘制上屏了。
接下来,我们就展开画卷,看看游戏组件的一生。
3. 游戏组件的一生
对于游戏组件从加载到上屏的流程我画了一张图:

把这个流程可以简单拆成 10 个阶段:

为了介绍清楚这个流程,我准备了一个最简单的 Cocos 游戏 Demo。这个是 Demo 的场景设计:

这个是主场景的代码:
const { ccclass } = cc._decorator;
@ccclass
export default class Helloworld extends cc.Component {
protected onLoad(): void {
console.log('onLoad');
}
start () {
console.log('Hello World');
}
}
3.1 Load Assets

首先是资源加载,前文介绍过游戏资源可以分为静态资源和脚本资源。由于静态资源的加载流程涉及的内容太多了,本节只简单介绍下脚本资源加载。
包括 3 类脚本资源:
- 内置脚本:引擎启动的时候进行加载,包括注册 JS Binding、实现 window 对象(基础的 BOM 和 Canvas DOM 对象)、polyfill 补齐 ES 标准等等。这个脚本内置在容器里,容器启动 JS 引擎的时候直接加载。这一步可以做多实例和预执行,以加快启动速度。
- 入口脚本:容器需要一个入口脚本,类似与 Web 里的 HTML,以便引入游戏入口资源。
- 动态加载的脚本:由游戏入口资源引入,比如游戏框架代码、游戏包里的 JS 资源等等。
这里可以容器侧可以提供离线资源、preload、prefetch、预执行等方式进行优化,同时在 JS 引擎方面也可以扩展做下 Code Cache,避免重复的编译耗时。
3.2 Component Scheduler

脚本资源加载执行后,游戏组件代码会进入到组件调度器中进行优先级调度。
Cocos 组件的生命周期如下图左所示,在 3 个关键的生命周期环境分别存在对应的调度器,每个调度器里设计了三个优先级队列,本质上每个队列的内容是由链表进行组织,顺序执行注册好的 invoker。

具体而言,从业务侧视角来看:在场景编辑器中创建节点(Node)时,业务方可以为其命名,并通过勾选“active”属性来决定该节点是否默认激活。一旦节点被标记为激活,加载阶段将由 Node Activator 负责激活该节点,接着 Component Activator(组件调度器的一部分)会依次激活该节点所挂载的各个组件,同时触发组件所在场景(Scene)的激活流程。最终,激活后的场景会将节点挂载入层级树,并完成组件 Invoker 的注册,交由调度器统一调度与管理。
整体流程如下图所示:

我们的 Demo 游戏组件的 start
生命周期下打印了一个 “Hello World”,调度堆栈如下所示:



3.3 Render Scene

当场景激活并挂载了对应组件之后,接下来便是渲染场景,这一步就涉及到从 JS 调用到了 Native —— 即需要将 Scene 数据传递给 Native 侧,从而触发 Native 的 Render 流程。

JS 和 Native 互相调用的方式有很多,适用场景也不同,这里也不展开说了。需要注意的是,在架构设计上,这里可以对 Binding 层做一层抽象,以便容器对接不同的 JS 引擎实现。

另外需要注意的是 Binding 要做好两端的 GC,因此 Binding 的实现上需要符合 RAII 原则:

3.4 Batcher

当 Native 拿到节点之后,便需要进行 Batch,这一步属于计算密集型,因此选择放在 Native 侧去做。
Batch 的流程比较复杂,核心思想是通过 DFS 对场景中的 Node 进行遍历,计算并装配(Assembler)顶点数据,得到顶点缓冲(VertexBuffer)和索引缓冲(IndexBuffer):

我们 Demo 游戏的场景树结构相对简单,遍历从 root 开始向下遍历(别忘了 Camera):

装配的计算流程比较复杂,下面仅对装配的结果做一个拆解,方便读者理解数据的由来。对于小恐龙而言,它是一个 Sprite2D,装备时会转成 Texture2D 处理,而后者在这个环节的核心,是需要拿到网格数据(Mesh Buffer)。下图是最后计算得到的 Mesh Buffer。

Mesh Buffer 由 Vertex Buffer 构成,这里装配的 Mesh Buffer,共 80 字节,其中每个顶点 20 字节,那么可以容易拆出 4 个 Vertex Buffer,同时根据 a_uv 的定义和偏移能拿到各自的 uv 坐标:

例如,根据顶点着色器的代码我们知道这个 Vertex Buffer 包括 3 部分数据:
a_position
: 偏移量 0,8 字节。vec2,能算出来一个坐标。a_uv0
: 偏移量 8,8 字节。vec2,就是 x,y,算出来之后是(0,1)。a_color
: 偏移量 16,4 字节。vec4,RGBA,数值是 0xFFFFFFFF,即白色透明。

我们把四个顶点的坐标都算一下,可以拿到宽高和左上角的坐标,其实可以发现,这个数据就是业务侧在场景编辑器里对 Node 的宽高和坐标设置:

顶点装配完毕之后的 Node 会被放进 Models 里,最后做成 Scece Tree 中的 models 节点:

3.5 Setup

这个环节主要由两个逻辑组成:
- 设置 Framebuffer 和 Viewport
- 将 Scene 里的各个 Model 转成 drawItems 队列
首先是第一个部分,设置 Framebuffer 和 Viewport。具体而言,包括以下步骤:

- 通过
setFrameBuffer
函数调用glBindFramebuffer
绑定 Framebuffer 帧缓冲对象,并分别附加颜色缓冲(COLOR_ATTACHMENT
,存储渲染的颜色信息)、深度缓冲(DEPTH_ATTACHMENT
,存储每个像素的深度信息,用于深度测试)和模板缓冲(STENCIL_ATTACHMENT
,存储模板测试的结果),确保后续绘制有正确的渲染目标。 setViewport
调用glViewport
设置视口,决定最终渲染区域在屏幕上的映射范围setup clear
依次执行glClearColor
、glClearDepth
和glClearStencil
,初始化颜色、深度和模板缓冲的清除值,为每一帧绘制提供干净的初始状态。
unsigned int fbo;
glGenFramebuffers(1, &fbo);
接下来,游戏引擎会将 Scene 里的各个 Model 转成一对一的 DrawItem,一个 DrawItem 的数据结构如下所示:

最后,引擎将这些 DrawItem 组装成 DrawItems 队列,以便后续流程处理。

3.6 Render Stage

接下来进入 Render Stage 阶段,渲染管线会开始对 DrawItems 进行分类处理。根据渲染的 Material 的需求,DrawItems 会被分发至三个不同的 Pass,分别对应 Opaque、Shadowcast 和 Transparent 三个阶段,关系到材质属性和阴影投射:
- Opaque:用于绘制完全遮挡光线的物体,如墙面、地板、角色模型等。这类物体会首先渲染,通过深度缓冲区(Z-Buffer)完成遮挡剔除,避免后续无效绘制,提升渲染效率。
- Shadowcast:专门处理场景中的阴影投射。此阶段会根据光源信息,对具有投影能力的物体进行阴影绘制,为场景添加真实感与空间深度,尤其适用于强光源或需要表现光影效果的环境。
- Transparent:负责绘制允许光线穿透的半透明物体,如玻璃、水面、特效粒子等。透明物体通常需要根据视角进行深度排序,以保证前后层次正确渲染,避免视觉穿插错误。
通过将 DrawItems 按照物体特性分发至不同 Pass,渲染管线能够有针对性地对 Effect 进行实现。
业务侧可以在代码里创建一个指定的 Material,之后管线就会走到对应的 pass 进行处理:
// 创建一个立方体网格
const cube = new cc.MeshRenderer();
cube.mesh = cc.GizmoMesh.createBox(1, 1, 1);
// 设置材质为不透明
const opaqueMaterial = cc.Material.create();
opaqueMaterial.initialize({
effectName: 'builtin-unlit',
technique: 'opaque',
});
cube.setMaterial(opaqueMaterial, 0);
因为我们 Demo 较为简单,因此最后生成的 StageInfo 只包含 Opaque Pass:

当然,在 Cocos 中也是支持自定义渲染管线,实际上就是自定义这个环节的 Passes,定义完之后可以直接应用在 Opaque、Shadowcast 和 Transparent 三个阶段之上:

3.7 ModelView Transformation

经过 Passes 之后,场景中的 DrawItems 会根据其属性被分别送入 OpaqueStage Renderer、Shadowcast Renderer 和 Transparent Renderer 进行初步处理。各个 Renderer 在此阶段主要负责更新与视图相关的 Uniforms(如矩阵、材质参数等),以确保后续渲染过程中所需的视角、空间信息正确。这一部分可归类为 View Transformation 阶段,统一完成视图坐标系下的变换数据准备。
紧接着不同的渲染阶段会有差异化的预处理操作:不透明物体和透明物体会分别执行 SubmitLight 以提交光照信息,而投影阶段则专门进行 SubmitShadow 以生成阴影数据。同时,透明阶段由于涉及深度排序问题,还会额外执行 Calculate zdist 以计算对象的深度信息。
所有这些预处理完成之后,最终将统一进入 ModelView Transformation 阶段,得到视图投影矩阵,从而完成从模型空间到屏幕空间的最终变换,以便于后续的图元栅格化与像素着色工作。
在讲解 ModelView Transformation 之前,先来介绍下游戏系统中的坐标系统的定义。一般会涉及物体坐标、世界坐标与相机坐标三种主要坐标系。
- 物体坐标系:以物体自身的中心点(anchor 通常设置为(0.5, 0.5))为原点,用于描述物体内部各个部位的位置关系,便于定义复杂物体内部的原子结构关系。
- 世界坐标系:则是以整个场景的中心作为原点,用来统一描述场景内所有物体、相机以及光源的位置关系,确保场景整体的空间一致性。
- 相机坐标系:以相机的位置作为原点,是为了将 3D 空间转化为 2D 图像,以便进行计算和渲染。

基于这套坐标系统下,观测变换(Viewing Transformation)主要包括视图变换、模型变换与投影变换三个步骤。
- 视图变换:可看作是将相机放置到场景中的过程,主要是定义相机的朝向和位置。
- 模型变换:对物体进行放置或调整位置、旋转以及缩放等操作。
- 投影变换:类似于摄影,通过投影方式,将三维物体的信息映射到二维的屏幕空间。

下面重点说说投影变换(Projection Transformation),它分为正交投影(Orthographic Projection)与透视投影(Perspective Projection)两种方式。
- 正交投影常用于工程制图软件,不体现远近透视效果;
- 透视投影广泛应用于游戏、渲染引擎中,能更真实地模拟人眼观察到的空间透视效果。
而透视投影的数学本质是压缩加上正交投影的结合,实际将一个无限延伸的观察空间(视锥体)转化为一个便于计算的立方体。

这里简单画了个图来介绍透视变换的实现,fov(视角)定义相机的视场宽度,可以分为水平fov与垂直 fov;distance 定义投影平面与相机之间的距离。视景空间通过近裁剪平面(near)和远裁剪平面(far)定义渲染的范围,通过相似三角形的计算,最终将 3D 空间映射到 2D 屏幕(Canvas)。

说完了透视投影,我们再看看投影变换的另一种方式——正交投影。其通常有实现的方式有两种:
- 直接舍弃Z坐标,将 3D 物体转化为 2D 物体,直观但无法表现空间深度;
- 将观察空间变换成标准的立方体后,利用变换矩阵进行计算。

综上,坐标转换流程具体包括物体坐标到世界坐标,再到相机坐标,接着到投影坐标,最终映射到屏幕坐标。
- 首先,在编辑器中定义坐标相加的关系,将物体放置到场景中;
- 之后,通过视图变换调整相机位置、模型变换调整物体位置;
- 再经过投影变换将 3D 空间投射到 2D 空间;
- 最后进行坐标系转换,确保渲染到正确的屏幕位置。

在这个过程中,会计算得到视图矩阵(View Matrix)、投影矩阵(Projection Matrix),最终矩阵相乘拿到视图投影矩阵(Model-View-Projection Matrix)。我们结合 Demo 游戏的断点数据,分别看看他们仨是怎么计算得到的。
首先是视图矩阵,它负责将世界坐标系转化为相机坐标系,其中包含坐标轴的缩放和平移操作。实际计算中,通常涉及坐标轴补齐,即齐次坐标的补齐过程,确保矩阵运算的有效性。

之后是计算投影矩阵,它用于将相机空间进一步映射到标准化的设备空间(Normalized Device Coordinates, NDC),矩阵中的缩放系数根据屏幕的宽高比和设定的正交高度来计算。

最终的渲染过程通常使用视图投影矩阵(Model-View-Projection Matrix, MVP)。视图投影矩阵是视图矩阵与投影矩阵的组合,用于最终的顶点变换和着色器渲染计算。

3.8 Link Program

接下来进入到着色器的创建与 Link 阶段,首先是创建图元:

之后是创建顶点着色器和片元着色器:

值得关注的是,在 Cocos 中有一共有 11 个内置着色器,其中前 5 个处理 2D 渲染相关,builtin-clear-stencil|vs|fs 用于清楚模板缓冲区,7-10 3D 渲染相关,最后一个用于处理 3D 光照:
- builtin-2d-spine|vs|fs
- builtin-2d-graphics|vs|fs
- builtin-2d-label|vs|fs
- builtin-2d-sprite|vs|fs
- builtin-2d-gray-sprite|vs|fs
- builtin-clear-stencil|vs|fs
- builtin-3d-trail|particle-trail:vs_main|tinted-fs:add
- builtin-3d-trail|particle-trail:vs_main|tinted-fs:multiply
- builtin-3d-trail|particle-trail:vs_main|no-tint-fs:addSmooth
- builtin-3d-trail|particle-trail:vs_main|no-tint-fs:premultiplied
- builtin-unlit|unlit-vs|unlit-fs
文中的 Demo 是使用内置着色器模板进行创建的。
接着创建着色器程序,Link 上我们创建的顶点着色器和片元着色器。紧接着,设置着色器中所需要的 Uniforms 变量,这里就包括纹理和我们上一步计算得到的视图投影矩阵:

最终,我们的 Framebuffer 会附着上颜色附件、深度附件与模板附件:

需要注意的是对于刚创建完的 FrameBuffer 不能立即使用,因为它还不完整(Complete)。而一个完整的帧缓冲需要满足以下的条件:
- 附加至少一个缓冲(颜色、深度或模板缓冲)。
- 至少有一个 GL_COLOR_ATTACHMENT。
- 所有的附件都必须是完整的(保留了内存)。
- 若开启 Multisampling,则每个缓冲都应该有相同的样本数(sample)。
因此需要使用 glCheckFramebufferStatus
对缓冲区的完整性做出检查:
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
// ...
// notify native: getInstance()->glErrorCallback(GL_ERROR, errMsg);
return;
}
3.9 Blend & Test

接着依次进入执行 混合(Blend)、深度测试(Depth Test)、模版测试(Stencil Test)。
首先是 Blend,顾名思义讲两个颜色进行混合。下图展示了混合方程的计算方式:

OpenGL 中常用的混合函数如下图所示:

下面是一个简单的例子,使用着色器来创建红色蒙版的 Blend 效果:


深度测试(Depth Test)在图形渲染中用来决定每个像素是否显示。启用深度测试时,OpenGL会将当前片段的深度值与深度缓冲区的值进行比较。如果通过测试,深度缓冲区将更新为新的深度值,否则该片段会被丢弃。下图展示了 OpenGL 中常用的深度测试函数:

而模板测试(Stencil Test)则用于限制渲染区域。通过模板缓冲区,可以在渲染时创建特殊的区域标记,只有符合模板缓冲区设定条件的片段才会被渲染到屏幕上。模板缓冲区允许实现诸如阴影、镜面效果、轮廓高亮等复杂渲染效果。下图展示了 OpenGL 中常用的模版测试函数:

上述的结果最终都会与 Framebuffer 的 Attachment 机制相关联。Framebuffer 的 Attachment 机制决定了渲染结果如何输出到缓冲区中。Framebuffer 通常会附带多个 buffer,包括颜色缓冲区(GL_COLOR_ATTACHMENT)、深度缓冲区(GL_DEPTH_ATTACHMENT)和模板缓冲区(GL_STENCIL_ATTACHMENT),他们共同决定了最终渲染的结果。

3.10 Commit & Draw Pass

到了管线的最后一步,便是提交(Commit)和绘制(Draw)。
在 Cocos 中每一帧会存储两种状态,一个是当前画面帧的状态(currentState),另一个是我们即将渲染帧的状态(nextState)。

我们需要依次计算 nextState 中的各个部分的 state,之后将 nextState 和 currentState 的状态值做 diff,如果某个环节的状态值不一致,便会触发 commit 操作。以便管线最大程度利用缓存结果。

下面依次介绍下管线中需要管理的状态值:
- Blend States、Depth States、Stencil States
- Cull Mode
- Vertex Buffer
- Program
- Textures
- Uniforms
其中 Program 通常在管线初始化时所有的着色器都会准备好,非极端情况下缓存不会失效,因此上面的图中没有标出这个状态。
Blend States、Depth States、Stencil States 分别存储了我们前文所说的 Blend、Depth Test、Stencil Test 过程中涉及到的 GL 调用的参数和部分结果,这里就不详述了。
接着是 Cull Mode,根据顶点的索引的顺逆时针来用来区分正面与反面,如果状态值和 currentState 不一样,便触发 glCullFace
的调用来进行 commit。

对应顶点缓冲区也是一样有状态值管理,如果变脏了,就需要重新调用 glBindBuffer
进行绑定:

着色器程序也是一样的,如果脏了,就重新调用 glUseProgram
进行设置:

接着便是对 Textures 的检查和提交,这里有两个知识点:
- 纹理的应用:具体涉及到
glActiveTexture
和glBindTexture
。首先使用glActiveTexture
函数来选择当前要激活的纹理单元,这一步决定了接下来绑定的纹理将作用于哪个纹理单元上。然后,通过glBindTexture
函数将具体的纹理对象绑定到特定的纹理目标上。通过这种机制,纹理对象与对应的纹理单元和目标进行关联,从而完成纹理的激活与绑定操作。 - 纹理单元:用于表示显卡可以同时管理的多个纹理。默认情况下,
GL_TEXTURE0
纹理单元总是被激活的状态。此外,OpenGL 规范保证至少支持 16 个纹理单元(即从GL_TEXTURE0
到GL_TEXTURE15
)。纹理单元是按顺序定义的,因此我们可以通过诸如GL_TEXTURE0 + 8
的方式便捷地访问特定编号的纹理单元,以便在复杂的渲染场景中实现多纹理同时使用。

当前面的状态值都准备并提交完毕后,最后需要管理的状态值是 Uniforms,这一步如果有脏区产生,也需要重新提交 Uniforms 变量。比如游戏 Demo,涉及到的 Uniforms 变量有 cc_matViewProj
和 texture
:

最后就是绘制了,其中在每一帧的绘制前都需要调用 glClear
清理 Freambuffer 的状态。下图展示了 gl 指令调用的时序:

由于游戏 Demo 比较简单,绘制只需要准备好纹理和 Uniforms 即可,最后调用 glDrawArrays
或 glDrawElements
将准备好 Framebuffer 绘制上屏:

至此,经历了这一系列的管线处理之后,我们的 Demo 游戏在小游戏容器内完成了上屏。

扩展阅读
- 《GAMES 101》
- 《计算机图形学入门:3D渲染指南》
- 《
LearnOpenGL 》
对网吧的偏见
我对网吧的偏见,源于小学时候。
学校对面有一家网吧,从知道那是网吧开始,到它停止营业,从未进去过,因为时不时就有老师或主任在放学后去网吧逮学生,第二天在广播里通报,再加上家人和老师不断地告知网吧里全是不三不四的社会人,很危险,随时都会被敲诈,现在回想起来,并不无道理。
怕被老师逮到,也怕被坏人欺负,再加上我平时几乎没有机会接触电脑,直到今天,去网吧的次数屈指可数。小学对面的网吧,当时的价格是2元/小时,包宿应该更便宜,但对于每天零花钱只有1元的我来说,也是承担不起的价格;第一次去网吧,得益于同学请客,已经记不清当时坐在电脑面前玩什么游戏,只记得那时候的心情十分忐忑,即便是周六,也担心突然被老师逮到,去网吧上网似乎成了一种罪过;还有一次在同学的带领下,去过一趟所谓的黑网吧,不查身份证,不管你多大,给钱就开机,房间很小,头一次觉得这里确实是一些不三不四的社会人……后来随着学习压力增加,学习时间紧张,读初一的时候家里买了电脑,便再也没有去过网吧,高中毕业之后约过同学去包宿,难受和煎熬,想玩又犯困,以及现在,双休前的最后一个工作日,如果兴致来了,去楼下的网吧玩两个小时,就已经很满足了。
前段时间还在犹豫,想买游戏本打游戏,考虑到自己不仅打得菜,而且平时很少有时间玩,还不如想玩的时候去网吧玩一会来的划算,于是买了 Mac mini M4。附近有不少网吧,以前叫网吧,现在叫网咖的居多,不仅配置和环境更好,价格也更贵了。第一次来南昌,陪女朋友考试,考试期间我就去附近的一家网鱼网咖玩游戏,10元/小时的价格让我惊讶,这还是大厅的价,如果是包厢岂不是更贵?今年寒假,和几个同学开了一间电竞酒店房,有四台电脑,这边四个同学玩游戏,那边四个同学在打牌,累了困了就在床上睡一会,又一次刷新了我对网吧、网咖、电竞酒店的认识和体验。
不怕别人笑话,因为去网吧去得少,我都不清楚上机的过程,如何让老板给我开一台机子,怎么登陆,没带身份证怎么办……也是在高中毕业之后和同学一起去网吧次数多了,渐渐熟悉一点,没带身份证不碍事,打开微信使用 V 上网公众号实名激活就行,接着再让老板充值网费,随便找电脑开机等等。
申公豹说得对,人心中的成见是一座大山,从小对网吧的偏见在我心中也是一座大山,给它打上了不太好的标签,但也影响不了什么,从现在来看,网吧是让我在工作之余放松自我的地方,话又说回来,有条件的话还是尽量在自家房间里玩游戏,环境更好,今天去附近的网吧玩了两个小时,虽然开着空调,但是空气中弥漫着一股奇怪的味道,人多、嘈杂,或许以后还是会选择配置一台 Windows 电脑,每个男人心中都有一个电竞房的梦想。
20250515
今天的惊喜来自和菜头老师在公众号槽边往事写的文章提到了谜底黑胶,《聊点我喜欢的:Wiim 和谜底黑胶》。
之前谜底黑胶也有在别的文章或者视频出镜过,不同的是和菜头老师去“调查”了我,他看到了我的日常,我的黑胶设备,他理解了为什么我会而且我能做出这样一个产品。有一种被懂了的感觉。
今天研究了 Claude + Apple Music MCP 的一些玩法,希望它能给我推荐音乐创建歌单。不过我找的这个 MCP 不太合适,有空要尝试一下另一个。
下午高兴完了就去打麻将了,蛰伏两圈,每圈仅小胡一把。第三圈直接连庄外加四个财神加持,一把翻身。前面两圈让我放平心态,今天就是来陪玩的,于是只专心搞大的,当财神看我可怜分配给我四个白板的时候,我抓住了机会。人生就要抓住机会啊。