04-引擎组装
引擎组装——引擎有哪些子系统,怎么组装一句话
引擎本身是一个多子系统协作的系统,组装方式类似通信程序的网络系统:每个子系统有独立的生命周期和运行方式,由引擎统一调度;引擎负责「怎么运行」,不知道游戏规则。
一、引擎可能包含的子系统123456789101112引擎可能包含的子系统:├── 渲染系统 ← GPU 交互、Draw Call、渲染管线├── 物理系统 ← 碰撞检测、刚体模拟、约束求解├── 音频系统 ← 声音播放、3D 音效、混音├── 输入系统 ← 键盘/鼠标/手柄/触摸├── 资源系统 ← 加载、缓存、异步加载、引用计数├── 场景系统 ← 场景图、节点管理、摄像机├── 脚本系统 ← Lua/Python 绑定、脚本执行├── UI 系统 ← 控件树、事件处理、布局├── 动画系统 ← 骨骼动画、状态机、混合├── 粒子系统 ← 粒子发射、更新、渲染└── 网络系统 ← 同步、预测、回滚(游戏专用)
ECS 只 ...
03-趣味设计
趣味设计——核心机制、反馈密度与受控自由一句话
「有趣」不等于「机制越多越好」,而更像是核心机制产生的反馈价值 × 玩家愿意持续接收这个反馈的成本。一个机制就是一台「趣味发生器」,关卡负责让玩家在受控自由里反复触发它。
游戏系统论把趣味浓缩成一句话(趣味 = 认知闭环的密度);本线展开 11 篇的完整内容:Bug 即机制、核心机制可探索、反馈密度、教学曲线、内容不完整与交互闭环、开发者疲劳与新手测试、第三关陷阱、AI 测试与玩法涌现、关卡设计公式、受控自由与甜点区。
一、为什么充满 Bug 的游戏反而有趣
会觉得它「有趣」,恰恰可能不是因为它做得好,而是因为它暴露出了游戏系统本身的趣味性。
即使角色、敌人、平台、金币全都像 Bug 产物一样乱七八糟,玩家仍然能够看懂:我是谁 → 我能怎么移动 → 什么东西会伤害我 → 什么东西可以收集 → 我要怎么到达右边。这说明游戏的核心乐趣并不完全来自画面。
趣味来源六因素
Bug 反而制造了「探索感」:正常游戏是「开发者设计规则 → 玩家学习规则 → 玩家利用规则」;Bug 游戏变成「玩家看到异常 → 猜测规则 → 尝试 → 发现 ...
02-游戏类型演化
游戏类型演化——赛车、马里奥、格斗、冒险格斗各自的演化流程一句话
游戏系统论讲通用演化(空窗口→方块→角色→关卡→完整游戏);本线讲具体游戏类型的演化分支——每类游戏的核心对象不同,演化顺序也不同:赛车先有世界和相机,格斗先有动作和判定,冒险格斗先让角色走起来再打起来。
一、四类游戏的核心演化区别1234赛车: 方块 → 车 → 赛道 → 驾驶 → 相机 → 比赛马里奥: 方块 → 角色 → 地面 → 地图 → 敌人 → 关卡格斗: 方块 → 角色 → 动作 → 攻击 → 判定 → 对战冒险格斗: 方块 → 角色 → 地图 → 动作 → 战斗 → 探索 → 成长
格斗游戏最重要的第一个「不是车也不是地图」,而是先让两个方块在屏幕上产生攻击和反馈。 冒险格斗最核心的开发顺序:先让角色在世界里走起来,再让角色打起来,最后让世界丰富起来。
二、赛车游戏赛道流式加载:近景赛道怎么做赛车游戏里「往前只有近景的赛道」通常不是把整条赛道提前建出来,而是采用动态加载 + 视野范围内生成的方式。
不是没有远处,而是不让远处作为高成本对象存在。
通常组合 ...
01-游戏系统论
游戏系统论——从想法到引擎:一个游戏如何从零演化、建模、设计、组织
游戏开发不是「写一个 Game Loop」。它是一个完整系统:先有分析→设计的演化(想清楚做什么),再有实现的演化(从窗口到引擎一点点做出来);游戏世界是一个「实体+状态+规则+交互+时间演化」的模型;工程的推进遵循一条十二阶段的完整流程;而技术架构回答「对象是什么、谁负责运行、怎么计算、谁提供底层能力」。 四条线串在一起,就是游戏系统的完整图景。
一、两条演化:设计与实现的独立路线游戏开发不是一条线,而是两条演化同时在走:
12上层:灵感 → 核心玩法 → 玩法循环 → 设计 → 分析 → 技术 → 验证 → 迭代(先做什么)下层:窗口 → 方块 → 移动 → … → 物理/AI/UI/音频/存档(怎么从零做出来)
它们互相独立:想法不写代码也能演进,引擎没有玩法也能演进。真正的开发是两条线在可玩核心处汇合——围绕一个核心玩法(移动/跳跃/攻击)不断迭代,每加一个系统都反过来调整前面的设计。
一·A、设计演化:从灵感到技术设计的 12 阶段第 1 阶段:灵感(Idea)只有一句话,甚至没有 ...
06-工程目录的组织
工程目录的组织
前面几篇解决了「模块内部怎么构成」:功能函数 = 数据结构 + 算法 + 接口,模块 = 一个功能的完整组合。这一篇解决下一个问题:模块怎么组织成一个工程的目录? 答案不是「按技术分层堆目录」,而是「按功能组织模块,模块内部再按职责拆分」——并由此得到 Utils 与 Infrastructure 的判断标准、Core 层的定位,以及一套可以从需求一路映射到文件的大一统工程目录。
一、两种组织方式:按层堆目录 vs 按功能组织1.1 按层堆目录(错误示范)有人把「功能 = 算法 + 数据结构 + 接口」直接翻译成三个顶层目录:
12345678910Application├── UI├── ViewModel├── Service├── Domain├── Infrastructure├── Interface ← 所有接口├── Algorithm ← 所有算法├── DataStructure ← 所有数据结构└── Utils
问题在于:算法、数据结构、接口并不是一个功能,而是一个功能的三个组成部分。
一个 ...
05-模板-同功能不同类型的抽象
模板:同功能、不同类型的抽象
封装解决了「很多地方做同一件事」的重复;但还有另一种重复——同一件事,只是类型不同。写十个几乎一样的函数,只因为参数是 int、float、string、结构体……模板把「类型」也变成参数,让一个函数覆盖所有类型。这是和原子化接口正交的另一条线。
一、先看一种被忽略的重复原子化接口线解决了这种重复:
12open(path); // 到处都是close(fd);
到处复制的是同一个调用。封装一次,处处复用。
但还有另一种重复,形态完全不同——同一段逻辑,参数类型不同:
1234567891011121314151617181920// 从文件偏移处读一个 intint32_t read_i32_at(int fd, size_t off) { int32_t v; if (pread(fd, &v, sizeof(v), off) != sizeof(v)) throw ReadError(); return v;}// 从文件偏移处读一个 float —— 函数体几乎一样float read_f32_ ...
04-从现实问题到数据结构
从现实问题到数据结构
数据结构不是凭空发明的一堆容器名字。它是对现实问题的分析、抽象的产物:先有现实中的事物和关系,抽出共同结构,才落成数据结构。而且数据结构不止一种——领域数据(业务对象)、通用结构(Core)、平台数据(Infrastructure)是三类完全不同的东西,各有各的来源和用途。
一、数据结构从哪里来教材常按容器讲:数组、链表、栈、队列、树、图、哈希表……好像它们是独立存在的知识。
但它们其实是从现实问题里长出来的。每个数据结构背后都有一个现实场景在逼它出现:
1234567现实问题 抽象出来 数据结构──────────────────────────────────────────────────────一排人按顺序排队,先来的先服务 先进先出 队列一叠盘子,后放的先拿走 后进先出 栈点名册按学号排好,快速找某个人 编号 + 查找 数组 / 哈希表家族谱:父 → 子 → 孙 层级归属 树城市间的路 ...
03-算法类型与描述形式
算法类型与描述形式
算法不只是「一段逻辑」。不同类型的算法,表达重点完全不同:有的重点在执行步骤,有的在状态变化,有的在递归关系,有的在数学关系。表达形式必须匹配表达重点——流程图、状态图、递归树、公式推导各有适用场景。「一个算法必须有流程图」是错的,形式由类型决定。
一、从两个算法开始同样是「一个函数实现的算法」,二分查找和快速排序画成流程图,观感完全不同。
二分查找——重点是步骤:
123456789101112131415开始 ↓设置 left=0, right=n-1 ↓left <= right ? ├─ 否 → 返回 -1 └─ 是 → mid=(left+right)/2 ↓ a[mid]==target ? ├─ 是 → 返回 mid └─ 否 → a[mid]<target ? ├─ 是 → left=mid+1 └─ 否 → right=mid-1 ↓ 回到 left<=right 判断(循环)
流程图画 ...
02-接口先行
接口先行——先设计接口,再写实现
接口有两种形态(函数导出 / 类封装)。但还有一个更深层的问题:接口应该先于实现存在吗? 接口先行的答案是「是」——先定义「能做什么」,再写「怎么做」。接口先行本身只需要函数声明与空实现就足够了;虚函数是另一个问题的产物——当多套接口(多套实现)同时存在时,运行时怎么决定调用哪一套。不要把两者混为一谈。
一、先有实现,后有接口最初的编程方式是先写实现,再给使用者看头文件:
1234567891011// 先写实现// search.cScanResult search_pattern(const uint8_t* data, size_t len, const uint8_t* pattern, size_t pat_len) { // 具体搜索算法}// 后有接口// search.hScanResult search_pattern(const uint8_t* data, size_t len, const ...
01-接口形态演化
接口形态演化——状态决定接口形式
库或组件对外暴露接口,但接口有两种本质不同的形态:无状态的用函数导出,有状态的用类封装。这不是风格偏好,而是由「有没有状态」这个客观事实决定的。
一、函数导出:无状态的接口最简单的接口形式——导出一个函数:
123// math.hint add(int a, int b);double sqrt(double x);
调用者只需要知道函数名和参数,不需要知道内部实现。函数没有状态——每次调用都是独立的,不依赖上次调用的结果。
123调用者 → add(1, 2) → 3调用者 → add(3, 4) → 7两次调用之间没有任何关联
函数导出的特征:
1234✅ 无状态:每次调用独立✅ 无需初始化:直接调用✅ 无需生命周期管理:调用完就结束✅ 简单直接:调用者负担最小
函数导出的前提:无状态。
二、当状态出现需求变了——需要一个「搜索器」,它打开一个文件,在文件中搜索多次:
1234// 如果用函数导出:Searcher* searcher_open(const char* path);SearchResult searcher_find( ...
