这里是全站文章的归档入口,下面是按时间倒序排列的全部文章。若想按主题浏览,请从首页的专题分类进入。
《游戏引擎实现原理》3.2 PAL 平台抽象层
内核的其余部分不应该知道 node:fs 或 window 的存在。本节定义 helio 的 IPlatform 接口(时间、文件、线程、日志四个能力面),给出 NodePlatform 的真实实现,说明浏览器 WebPlatform 的差异,并真跑一段「同一份 measureMillis 代码在 PAL 上读时间戳」的用例。
posts
这里是全站文章的归档入口,下面是按时间倒序排列的全部文章。若想按主题浏览,请从首页的专题分类进入。
内核的其余部分不应该知道 node:fs 或 window 的存在。本节定义 helio 的 IPlatform 接口(时间、文件、线程、日志四个能力面),给出 NodePlatform 的真实实现,说明浏览器 WebPlatform 的差异,并真跑一段「同一份 measureMillis 代码在 PAL 上读时间戳」的用例。
引擎启动时几十个子系统谁先谁后不是拍脑袋定的。本节用 TypeScript 从零实现 helio 内核的 ISubsystem 接口与 SubsystemRegistry,用 DFS 三色标记做依赖拓扑排序、检测循环依赖,并保证关闭顺序严格是启动顺序的逆序,最后在 Node 里真跑断言把顺序验出来。
固定步长让物理与渲染解耦,但游戏逻辑时间本身还需要被缩放。本节实现 TimeService:用 timeScale 做慢动作、paused 做暂停,并把逻辑 delta 与 UI delta 拆成两套,真跑出「暂停时逻辑步为 0 而 UI 动画仍推进」的结果。
上一节量出的帧时间抖得厉害,直接喂给物理会跑出不确定的结果。本节实现 FixedStepLoop:用累加器把不均匀的 dt 变成均匀的物理步,再用 alpha 做渲染插值;并真实复现死亡螺旋,用 maxStepsPerFrame 把它钳制住。
帧预算决定了引擎能否稳住 60Hz。本节把一帧拆成逻辑、提交、GPU 与呈现四个阶段,用 text 时间线标出 16.67ms 的预算边界,再从零实现一个可注入假时钟的 FrameTimer,在 Node 里真跑出 300 帧的耗时分布、百分位与 FPS。
一个引擎最少要有哪些零件?helio 的答案是六个:主循环、内存、句柄、场景/实体、渲染接口、资源表。本节给出这六个零件的最小实现,并用一段 30 行的启动代码在 Node 里真跑出「第 1 帧到第 3 帧」的日志,再用 vitest 断言帧数。
本卷只做「从零实现 + 每节跑出真实产物」,渲染原理、引擎选型、子系统视角这些别人讲透的话题一律外链。本节用一张三列分工表把边界钉死,把这张表编码成可运行的路由函数,并给出 18 章的阅读路线、L1/L2/L3 代码分层怎么用,全部附可复现断言。
从零实现引擎的第一件事是划边界。helio 分成平台、内核、渲染、子系统、游戏五层,依赖只能向下。本节用一张 ASCII 分层图对照 Unity、Unreal、Godot、Bevy,并用 TypeScript 类型把「内核层不得依赖渲染层」钉死在编译期。
对库作者而言,类型层的破坏性变更比运行时更难察觉——代码照跑,用户却编译不过。本节先给出一份类型破坏性变更清单,再用 api-extractor 的 API 报告与类型测试把公开契约锁进 CI,最后梳理 changesets 驱动的发布流程、dist-tag 与弃用周期,让每个版本号都名副其实。
同一个包如何同时被 ESM 与 CJS 消费者使用,又让两套类型各自对得上?本节先讲清 type 字段与文件扩展名如何共同决定模块格式,再给出 tsup 双入口构建与 exports 双条件的完整配置,最后剖析双包最危险的副作用——双包风险导致 instanceof 失效与状态分裂,并给出收敛策略。
讲清包如何向消费者交付类型:用 declaration 与 emitDeclarationOnly 让 tsc 产出 .d.ts 与 declarationMap,用 api-extractor 把散落声明压成单文件;再拆解 package.json 的 types 字段与 exports 的 types 条件,解释它为何必须排在同条件首位。读完你能为包设计出不报错的类型入口。
构建变慢和包变大常常是同一批依赖的两面。本节先给出构建耗时的定位方法,用 --debug 与插件计时找出慢在哪个环节;再做包体积的可视化分析与体积预算;最后落到四条治理手段——依赖替换、按需引入、压缩器与 target 取舍、长期缓存下的 chunk 命名,并给出把体积门禁写进 CI 的可执行脚本。
首屏体积的敌人不是代码总量,而是「用不到的代码被提前加载」。本节先讲动态 import() 在类型层面如何标注模块边界,再拆解 tree-shaking 真正生效的三个前提——ESM 静态语法、sideEffects 声明、压缩器的死代码消除;最后给出 manualChunks 分包策略与 React.lazy、路由懒加载的落地写法,并列出五类让优化静默失效的场景。
Vite 已成为 TS 前端项目的默认构建工具,但它对 TypeScript 的态度常被误解。本节先讲清开发阶段的「按需编译」与构建阶段的「Rollup 打包」为何是两套引擎;再划清 tsconfig 与 Vite 的职责边界;最后落到 vite.config.ts 的类型写法、import.meta.env 与 import.meta.glob 的类型化,以及六个高频坑。
拆开 @PersistenceContext 注入的那个 EntityManager:它为什么是共享代理、如何绑定到当前事务,持久化上下文作为一级缓存怎样工作,实体的四种状态如何转换,以及 persist / merge / detach / refresh 各自的真实语义。
围绕借阅服务的封面上传,讲清 MultipartFile 与 spring.servlet.multipart.* 配置项、Tomcat 容器级的 max-part-count 等真实限制、延迟解析与异常处理位置、流式读写避免 getBytes() 撑爆堆、路径穿越与后缀白名单防护、中文文件名下载编码,以及为什么文件不该进数据库 BLOB。
一个事务调用另一个带事务的方法时,两者是什么关系?本节用「借阅失败但审计日志必须留存」的场景,逐个讲清七种传播行为,重点对比 REQUIRED 与 REQUIRES_NEW,再给出四种隔离级别与脏读、不可重复读、幻读的对照表,并说明实际工作中该怎么选。
对比 asyncio.TaskGroup 与 anyio 4.15.1 的结构化并发,讲清异常组如何打包与传播、asyncio.timeout 的 reschedule 语义,以及 PYTHONASYNCIODEBUG 慢回调警告等调试手段,附实测输出。
从 CPython 源码出发讲清 Task 与 Future 的关系:Future 的三态机、cancel() 把 CancelledError 注入到哪个挂起点、cancelling() 计数与 uncancel() 的语义、shield 的保护边界,并附取消时序实测。
用 selectors 手写一个最小事件循环,再对照 CPython 的 BaseEventLoop._run_once,讲清就绪队列与定时器队列的双队列设计、call_soon 与 call_later 的调度优先级,以及本轮新注册回调为何延后,并附实测执行顺序。