# 像素猫小屋 · 全文 > 做像素游戏,养 Obsidian 知识库。本文档包含站点全部文章正文(Markdown 原稿),用于 AI 一次性读入。 站点: https://pixelcat.example.com 许可: CC BY-NC-SA 4.0(文章) / MIT(代码) --- ## 把知识库对 AI 开放:llms.txt 与静态发布管线 URL: https://pixelcat.example.com/articles/publish-llms.html 日期: 2026-11-05 | 主题: Obsidian 知识库 | 标签: Obsidian, llms.txt, 发布 | 阅读: 2 分钟 搜索引擎读 HTML,AI 读的是上下文。当有人问「像素猫小屋讲了什么像素动画技巧」,模型要么抓不到你的页面,要么抓到一堆导航栏和 Cookie 提示。**llms.txt 就是为了解决这件事**:一个放在站点根目录的纯文本文件,用 Markdown 告诉模型「这个站有什么、该读哪里」。 ## llms.txt 长什么样 它不是一个严格标准,而是一个被多个站点采用的约定。核心结构很简单: ```markdown # 像素猫小屋 > 一个人做像素游戏与 Obsidian 知识库的记录。 ## 主要页面 - [游戏制作](https://example.com/gamedev.html): 立项、Godot、美术、手感 - [知识库](https://example.com/obsidian.html): 结构、Dataview、模板 ## 关键事实 - 使用 Godot 4,分辨率 320×180,动画 8–12 fps - 夜景色板 16 色,唯一暖光源是灯笼 - Vault 约 2400 条笔记,5 个文件夹 + MOC ## 许可 内容可被引用,请注明来源链接。 ``` 三条写作原则: 1. **每个链接带一句说明**。模型靠这句判断该不该读。 2. **关键事实单独列**。这是模型最可能直接引用的部分,写成短句、给数字。 3. **别放导航和广告**。llms.txt 的每一行都应该是信息,不是装饰。 ## 这个站怎么生成的 站点本身就是一个 Node 脚本,没有框架。核心只有四步:读 Markdown、解析 frontmatter、渲染 HTML、顺手写 llms.txt。 ```js // 读文章 const files = fs.readdirSync(dir).filter(f => f.endsWith(".md")); const articles = files.map(f => { const raw = fs.readFileSync(path.join(dir, f), "utf8"); const meta = parseFrontmatter(raw); // 返回 { data, body } return { ...meta.data, body: meta.body, plain: stripMd(meta.body) }; }); // llms.txt:页面清单 + 关键事实 const lines = ["# " + SITE.name, "", "> " + SITE.tagline, "", "## 文章"]; for (const a of articles) { lines.push("- [" + a.title + "](" + SITE.url + "/articles/" + a.slug + ".html + "): " + a.summary); } fs.writeFileSync("llms.txt", lines.join("\n")); ``` frontmatter 在这里变成双份资产:一份渲染成页面元信息,一份直接进 llms.txt。**你写 summary 的那一刻,就同时写给人和模型看了**,这是整个管线最省力的地方。 ## llms-full.txt:给愿意读全部的模型 llms.txt 是索引,llms-full.txt 是全文。后者把所有文章的 Markdown 原文按顺序拼起来,几千行也无所谓——它的使用者是愿意一次读完全部内容的检索系统。 我的做法是只拼正文,不拼导航和页脚: ```js const full = articles .map(a => "## " + a.title + "\n\n" + a.body.trim()) .join("\n\n---\n\n"); fs.writeFileSync("llms-full.txt", full); ``` ## 三个容易忽略的细节 - **robots.txt 里提一句**。加一行注释指向 llms.txt,部分爬虫会顺着看。 - **sitemap 和 feed 一起给**。llms.txt 不是替代品,是补充:人类入口、订阅入口、机器入口各一份。 - **日期要真**。模型会看更新时间判断内容是否过时,别把全部文章写成同一天。 ## 实测:什么样的内容会被引用 我观察到的规律是:模型更愿意引用**带具体数字和判断的段落**,而不是「本文将介绍……」这类开场。比如「下降重力是上升的 1.5 倍」比「跳跃手感非常重要」有用得多。 所以写作时我会刻意做两件事:每篇至少给一个可复制的数值或代码块;每篇的 summary 写成一句结论,而不是一句介绍。这两件事对人类的阅读体验也更好——这大概不是巧合。 ## 要不要给 AI 单独写一版 不需要。如果同一份内容对人不清楚,对模型也不会清楚。llms.txt 的价值不是「为 AI 优化」,而是**强迫你把站点的结构说清楚**:有哪些页面、每个页面讲什么、哪些事实是确定的。写不清楚,说明你自己也没想清楚。 --- ## 跳跃手感:8 个参数,和它们为什么这样取值 URL: https://pixelcat.example.com/articles/game-feel-jump.html 日期: 2026-11-02 | 主题: 游戏制作 | 标签: 手感, 物理, Godot | 阅读: 2 分钟 「手感」不是玄学,它是一串可以量化的宽容度。玩家按下跳跃键的那一刻,输入是离散的,物理是连续的,中间那层翻译决定了游戏跟不跟手。 下面 8 个参数是像素猫小屋最终采用的数值,来自三十多次试玩调整。它们不是标准答案,但每一个都对应一个具体的失败体验。 ## 参数表 | 参数 | 取值 | 解决的问题 | | --- | --- | --- | | 土狼时间 | 0.10 s | 走出平台边缘后仍能起跳 | | 跳跃缓冲 | 0.12 s | 落地前按下跳跃,落地瞬间自动起跳 | | 起跳速度 | -330 px/s | 跳跃高度约 56 px,等于 3.5 个猫身高 | | 上升重力 | 980 px/s² | 上升过程偏轻 | | 下降重力 | 1500 px/s² | 下落更快,避免发飘 | | 松键截断 | 速度乘 0.45 | 点按小跳,长按大跳 | | 顶点悬停 | 0.08 s,重力乘 0.6 | 最高点停留,给玩家判断时间 | | 落地停顿 | 0.06 s | 落地瞬间的重量感 | ## 为什么是这几个数 土狼时间 0.1 秒大约是 6 帧。低于 0.05 秒玩家感知不到,高于 0.15 秒会出现「明明已经掉下去却又跳起来」的诡异感。 跳跃缓冲 0.12 秒比土狼时间稍长,因为玩家的容错心理是「我按了就该算」。这两个值不需要相等,但差距不要超过 2 倍。 下降重力是上升的 1.5 倍,这是最划算的一条:上升慢让玩家看清落点,下降快让落地干脆。多数游戏落在 1.2 到 2.0 之间,超过 2.2 会像掉进水里。 ## 代码:一次写对 ```gdscript extends CharacterBody2D @export var jump_velocity := -330.0 @export var gravity_up := 980.0 @export var gravity_down := 1500.0 @export var coyote_time := 0.10 @export var jump_buffer := 0.12 @export var apex_threshold := 60.0 @export var apex_gravity_scale := 0.6 var coyote := 0.0 var buffer := 0.0 func _physics_process(delta: float) -> void: if is_on_floor(): coyote = coyote_time else: coyote = maxf(coyote - delta, 0.0) if Input.is_action_just_pressed("jump"): buffer = jump_buffer else: buffer = maxf(buffer - delta, 0.0) if buffer > 0.0 and coyote > 0.0: velocity.y = jump_velocity buffer = 0.0 coyote = 0.0 squash(0.8, 1.2) # 松键截断:只在上升段生效 if Input.is_action_just_released("jump") and velocity.y < 0.0: velocity.y *= 0.45 var g := gravity_up if velocity.y < 0.0 else gravity_down if absf(velocity.y) < apex_threshold: g *= apex_gravity_scale velocity.y += g * delta velocity.y = minf(velocity.y, 900.0) move_and_slide() if is_on_floor() and velocity.y >= 0.0: squash(1.15, 0.85) ``` 注意两个细节:松键截断只作用在上升段,否则会把下落也截断;顶点悬停用「接近 0 的速度」判断,而不是固定高度,这样小跳和大跳都有悬停。 ## 视觉:手感的一半在动画里 数值只解决跟手,重量感要靠动画补: - **起跳**:纵向拉伸 1.2 倍,横向压缩 0.85,持续 0.08 秒 - **下落**:轻微拉长,随速度增加 - **落地**:先压扁到 0.8,再 0.1 秒回弹到 1.05,最后回 1 - **尘土**:落地时在脚下生成 3 到 5 个粒子,向左上、正上、右上散开,寿命 0.25 秒 - **相机**:落地时向下偏移 2 px,0.12 秒回位;不要做整屏震动,会很廉价 ## 两个反直觉的结论 **不要给跳跃加加速度曲线。** 很多人想在起跳瞬间做缓入,结果玩家觉得按键有延迟。起跳必须是瞬时速度赋值,宽容度交给缓冲和截断。 **不要做二段跳,除非关卡为它设计。** 二段跳会让所有平台间距失去意义,第一版我加了,第二天就删了——它让土狼时间变得没必要,也让坠落不再有惩罚。 ## 怎么验证 把游戏给你自己玩 10 分钟,只看一件事:**有没有哪次死亡你觉得「不是我的错」?** 有,就是某个参数还不够宽容。记录下来,一次只调一个,调完再玩 10 分钟。 --- ## 16 色夜景调色板:为什么不是把颜色调暗 URL: https://pixelcat.example.com/articles/pixel-palette-night.html 日期: 2026-10-26 | 主题: 游戏制作 | 标签: 美术, 调色板, 像素 | 阅读: 2 分钟 新手做夜晚场景,第一反应是把白天画面整体调暗。结果通常是:灰、脏、分不清层次。夜晚不是「亮度更低」,而是**光源变了**——只有一盏灯、一个月亮,其他东西靠反射和轮廓活下来。 ## 16 色,够用 这套色板用在像素猫小屋里,从地板到灯笼全靠它。不是标准答案,但它有一个好处:任意两色放在一起都不会脏。 | # | 色值 | 名字 | 主要用途 | | --- | --- | --- | --- | | 1 | `#0B0F1C` | 深夜 0 | 最暗的背景、场景外框 | | 2 | `#161B2E` | 深夜 1 | 墙面阴影、家具暗面 | | 3 | `#232A44` | 深夜 2 | 地板、夜间描边 | | 4 | `#3B4368` | 蓝灰 | 远处的物体、冷反射 | | 5 | `#5A648F` | 雾蓝 | 月光照到的冷面 | | 6 | `#8C9BC4` | 月白 | 高光边缘、玻璃反光 | | 7 | `#0E2A28` | 暗绿 | 植物暗部 | | 8 | `#1E5C4A` | 松绿 | 叶子、猫眼底色 | | 9 | `#37A57E` | 薄荷 | 主强调色,健康与成功 | | 10 | `#7CE0B8` | 荧光薄荷 | 灯光核心、选中态 | | 11 | `#4A2A2E` | 砖红 | 木头暗部 | | 12 | `#8F4A3C` | 陶红 | 箱子、地毯 | | 13 | `#FF9FBE` | 樱花粉 | 猫耳内、腮红、第二个强调色 | | 14 | `#FFC85C` | 灯黄 | 唯一的暖光源,灯笼与月光反射 | | 15 | `#FFF3D6` | 奶油 | 猫的毛、纸、最高光 | | 16 | `#120D1A` | 紫黑 | 描边最深一档,替代纯黑 | ## 规则一:色相跟着光走 同一个物体,亮面和暗面的**色相**要朝光源方向和环境方向各自偏移,只改明度就会变成灰阶。 - 暖光(灯黄 `#FFC85C`)照射:向黄橙偏移 - 冷环境(月白 `#8C9BC4`):向蓝紫偏移 - 交界处:降低饱和度,而不是加黑 举例,木箱的三个面:暗面 `#4A2A2E`、中间 `#8F4A3C`、受光面往 `#FFC85C` 的方向走。注意受光面并不是「更亮的红」,而是偏黄的橙。 ## 规则二:明度分三层,不超过三层 像素画在 48×48 以内,一个物体最多三层明度就够。第四层会让轮廓消失。大物体(比如地板)可以破例到四层,但要保证最亮层只占很小面积。 ## 规则三:光要有来源,并且只给一个 整张图只允许一个主光源。我的场景里是右下角那盏灯笼,`addLight()` 只调用两次:一次给灯笼核心,一次给地面的大范围衰减。 ```js // 简化版:距离平方衰减 + 暖色混合 function addLight(buf, lx, ly, radius, tint, strength) { for (let y = 0; y < buf.h; y++) { for (let x = 0; x < buf.w; x++) { const d = Math.hypot(x - lx, y - ly) / radius; if (d >= 1) continue; const k = (1 - d) * (1 - d) * strength; blend(buf, x, y, tint, k); } } } ``` 用平方衰减而不是线性衰减,边缘会更自然。半径不要超过画面短边的 60%,否则整张图会发灰。 ## 抖动:只在渐变处用 两种颜色之间需要过渡时,用 2×2 棋盘抖动,不要用第三色去「和」。抖动密度最好只有两档(25%、50%),三档以上在低分辨率下会变成噪点。 ## 在 Godot 里换色板 如果你用索引色画图,可以在 shader 里做运行时换色板: ```glsl shader_type canvas_item; uniform sampler2D palette : filter_nearest; uniform float palette_size = 16.0; void fragment() { vec4 c = texture(TEXTURE, UV); float idx = c.r * 255.0 / 16.0; // 把 R 通道当作索引 COLOR = texture(palette, vec2((idx + 0.5) / palette_size, 0.5)); } ``` 这样白天、夜晚、中毒、回忆等状态只需要换一张 16×1 的色板贴图,不用重画所有素材。 ## 最后一条经验 把画面转成灰度看一眼。如果灰度下依然能分清「猫、地板、灯笼、门」,这套配色就成立;如果灰度下一团糊,说明你只是在靠颜色区分物体,而颜色在夜里是最先丢失的东西。 --- ## Godot 4 像素动画:导入、AnimatedSprite2D 与状态机 URL: https://pixelcat.example.com/articles/godot-pixel-animation.html 日期: 2026-10-18 | 主题: 游戏制作 | 标签: Godot, 动画, 源码 | 阅读: 2 分钟 像素动画的难点从来不是画,而是「让 8 张图看起来像活的」。Godot 4 给了足够好的工具,但默认设置几乎每一条都和像素艺术相反。 ## 导入:三处必须改 把 spritesheet 拖进项目后,选中图片,在导入面板里改这三项: | 设置 | 默认值 | 像素项目应该是 | | --- | --- | --- | | Filter | Linear | Nearest | | Mipmaps | 开启 | 关闭 | | Compress Mode | VRAM Compressed | Lossless | Filter 选 Linear 会让像素边缘糊成一片;Mipmaps 在 2D 缩放时引入模糊;VRAM 压缩会让纯色块出现色带——像素画最怕色带。 改完点 Reimport。如果画面还是糊的,检查项目设置里的 `rendering/textures/canvas_textures/default_texture_filter`,把它也改成 Nearest。 ## 节点结构 ```text Cat (CharacterBody2D) ├── Sprite (AnimatedSprite2D) ├── Hitbox (Area2D) │ └── CollisionShape2D ├── Shadow (Sprite2D) └── StateMachine (Node) ├── Idle ├── Walk └── Hop ``` `AnimatedSprite2D` 自带 `SpriteFrames`,适合逐帧动画;如果你需要「同一套动画换不同皮肤」,把 `SpriteFrames` 存成资源文件再用 `sprite_frames = load(...)` 替换。`AnimationPlayer` 更适合补间与事件,两者可以共存,但别用 `AnimationPlayer` 去驱动 `frame`,会打架。 ## 帧率:12 fps 是身体的谎言 绝大多数像素游戏的角色动画不需要 60 fps。把 `SpriteFrames` 的 `fps` 设在 8 到 12 之间,整体反而更像素。经验值: - 待机:6–8 fps,帧数少但循环长 - 走路:10–12 fps,4 到 6 帧 - 跳跃:单帧加缩放,不循环 - 受击:12 fps,2 到 3 帧,配合 0.06 秒停顿 ## 状态机:别用 if 堆 新手常写成 `if velocity.x != 0: walk else idle`,一开始没问题,等到有跳跃、受击、扔东西就开始互相覆盖。用一个显式状态机,每个状态只关心自己的进入、退出和物理更新。 ```gdscript class_name CatState extends Node var cat: CharacterBody2D var machine: Node func enter() -> void: pass func exit() -> void: pass func physics_update(delta: float) -> void: pass ``` ```gdscript extends Node class_name StateMachine @export var initial_state: CatState var current: CatState func _ready() -> void: for child in get_children(): if child is CatState: child.cat = owner child.machine = self current = initial_state current.enter() func transition_to(next: CatState) -> void: if next == current: return current.exit() current = next current.enter() func _physics_process(delta: float) -> void: current.physics_update(delta) ``` ```gdscript extends CatState func physics_update(delta: float) -> void: cat.velocity.x = move_toward(cat.velocity.x, 0, 900 * delta) cat.velocity.y += 980 * delta cat.move_and_slide() if Input.is_action_just_pressed("jump"): machine.transition_to(cat.get_node("StateMachine/Hop")) elif absf(Input.get_axis("left", "right")) > 0.1: machine.transition_to(cat.get_node("StateMachine/Walk")) ``` 用 `class_name` 让每个状态成为独立脚本,加状态只加文件,不改旧代码。这个结构在 8 个状态以内都不需要重构。 ## 动画事件:脚步与尘土 不要用 `AnimationPlayer` 的时间轴触发脚步声,那会把音频和动画资源绑死。更稳的做法是在连接 `frame_changed` 后按帧序号判断: ```gdscript var last_frame := -1 func _on_sprite_frame_changed() -> void: var f := sprite.frame if f != last_frame and (f == 2 or f == 5): audio.stream = sfx_step audio.play() last_frame = f ``` ## 三个常见坑 - **动画被物理插值拖慢**:开了 `physics_interpolation` 之后动画帧可能落后一帧,脚踩地时画面还在空中。把 `AnimatedSprite2D` 的 `process_callback` 设为 `Idle`,或直接关掉插值。 - **翻转后判定框没翻**:`flip_h` 只影响绘制,不影响碰撞。要么把 `Hitbox` 的 `scale.x` 一起翻,要么用 `Node2D` 容器同时翻。 - **精灵抖动**:相机、角色、精灵三者的小数位置叠加会出现 1 像素抖动。把相机位置取整(`round`),角色保持浮点,通常就够了。 这套结构我用了三个项目,从 12 帧的猫到 40 帧的机械臂都没有换过。 --- ## Templater 模板链:从日记到复盘,一条链路写完 URL: https://pixelcat.example.com/articles/templater-chain.html 日期: 2026-10-15 | 主题: Obsidian 知识库 | 标签: Obsidian, Templater, 模板 | 阅读: 1 分钟 大多数人的模板是孤立的:一个日记模板、一个会议模板、一个读书模板,各自很好用,但互不相干。真正省时间的是**链**:下游自动继承上游的结构,你只需要填写变化的部分。 我的链是三段:日 → 周 → 月。每段只多问一个问题,不做更多。 ## 链的形状 ```text 日记(每天,2 分钟) └─ 记录:今天推进了什么 / 卡在哪 / 明天第一件事 └─ 周记(每周日,10 分钟) ⤷ 自动汇总本周日记的三列 └─ 月复盘(每月最后一天,20 分钟) ⤷ 自动汇总四周周记的结论 ``` 关键设计:**周记不重复日记的内容,只提炼模式**;月复盘不重复周记,只做取舍。如果某一层只是把上一层复制一遍,那一层就该删掉。 ## 日记模板 ```javascript <%* const d = tp.date.now("YYYY-MM-DD"); const day = tp.date.now("dddd"); const projects = app.vault.getMarkdownFiles() .filter(f => f.path.startsWith("20 Projects/")) .map(f => f.basename); -%> --- title: <% d %> 日记 date: <% d %> tags: [日记] --- # <% d %> · <% day %> ## 今天推进了什么 - ## 卡在哪 - ## 明天第一件事 - [ ] ## 相关项目 <% projects.map(p => "- [[" + p + "]]").join("\n") %> ``` 这里唯一有点技术含量的是项目列表:它从 `20 Projects/` 目录动态生成,所以新开项目后不用改模板。三个小节的顺序是刻意的——先写推进(有成就感),再写卡点(允许抱怨),最后写明天(强制收敛到一个动作)。 ## 周记模板:自动汇总 ```javascript <%* const start = tp.date.now("YYYY-MM-DD", -6); const end = tp.date.now("YYYY-MM-DD"); const files = app.vault.getMarkdownFiles() .filter(f => /^\d{4}-\d{2}-\d{2} 日记\.md$/.test(f.name)) .filter(f => f.basename >= start && f.basename <= end); let pushed = [], blocked = []; for (const f of files) { const text = await app.vault.read(f); const sec = (name) => { const m = text.match(new RegExp("## " + name + "\n([\s\S]*?)(?=\n## |$)")); return m ? m[1].trim() : ""; }; pushed.push(...sec("今天推进了什么").split("\n").filter(l => l.startsWith("- ") && l.length > 2)); blocked.push(...sec("卡在哪").split("\n").filter(l => l.startsWith("- ") && l.length > 2)); } -%> --- title: <% start %> ~ <% end %> 周记 date: <% end %> tags: [周记] --- ## 本周推进 <% pushed.map(l => l).join("\n") %> ## 反复出现的卡点 <% blocked.map(l => l).join("\n") %> ## 一个模式 (这里必须写一句判断,不许留空) ## 下周只做三件事 - [ ] - [ ] - [ ] ``` 「一个模式」这一节是整条链的核心。它逼你把七条流水账压缩成一句因果判断,比如「我这周三次卡在同一个接口上,因为我一直在改实现而没有先写测试」。 ## 月复盘:只做取舍 月复盘不需要代码,只需要三个问题: 1. 这个月哪件事的投入产出比最高? 2. 哪件事我明明知道没价值却还在做? 3. 下个月我要停止做什么? 第三个问题最重要。所有「要做什么」的清单都会膨胀,只有「停止做什么」能让系统变轻。 ## 配置要点 - 日记模板放在 `90 Meta/Templates/daily.md`,用 Templater 的「文件夹模板」绑定到日记目录 - 周记模板手动触发:命令面板 → Templater: Insert template,因为一周只写一次 - 所有模板第一行必须是 `<%* ... -%>`,否则会在正文留下空行 - 模板里不要用 `tp.file.title` 做逻辑判断,标题会被改名,用 frontmatter 字段更稳 ## 一个反面经验 我一开始让周记自动抓取所有含 `#todo` 的行,结果周记变成 40 行的任务清单,我连续三周没打开它。自动化抓取的范围要小到「你能在一屏内读完并做出判断」,否则自动化只是把噪音搬到了另一个文件。 --- ## Dataview 驾驶舱:三块面板管住 2400 条笔记 URL: https://pixelcat.example.com/articles/dataview-dashboard.html 日期: 2026-10-08 | 主题: Obsidian 知识库 | 标签: Obsidian, Dataview, 自动化 | 阅读: 1 分钟 Dataview 的价值不是「把所有笔记列出来」,而是**回答三个每天都会问自己的问题**。我把它们放在一个叫「今日」的笔记里,打开 Obsidian 第一眼就看这一页。 ## 面板一:今天该做什么 跨全库收集未完成任务,按项目和截止日排序: ```dataview TASK FROM "10 Areas" OR "20 Projects" WHERE !completed GROUP BY file.link SORT rows.text ASC ``` 如果任务太多,加一层过滤,只看本周: ```dataview TASK FROM "20 Projects" WHERE !completed AND (due <= date(today) + dur(7 days) OR !due) SORT due ASC ``` 注意 `due` 是任务行里的行内字段,写成 `- [ ] 写完调色板 📅 2026-10-30`,Tasks 插件或 Dataview 都能识别。 ## 面板二:最近改过什么 我几乎不按日期找笔记,但需要知道「这周我到底动了什么」: ```dataview TABLE WITHOUT ID file.link AS "笔记", dateformat(file.mtime, "MM-dd HH:mm") AS "最后修改", level AS "难度" FROM "10 Areas" OR "20 Projects" OR "30 Resources" WHERE file.mtime >= date(today) - dur(7 days) SORT file.mtime DESC LIMIT 20 ``` `WITHOUT ID` 很重要:默认的 File 列是重复信息,去掉之后表格宽度刚好放得下。 ## 面板三:项目卡在哪 每个 Project 笔记的 frontmatter 里有一个 `stage` 字段,取值只有三个:进行中、卡住、待启动。面板只显示卡住的: ```dataview TABLE WITHOUT ID file.link AS "项目", stage AS "阶段", blocker AS "卡点", dateformat(file.mtime, "yyyy-MM-dd") AS "上次动作" FROM "20 Projects" WHERE stage = "卡住" SORT file.mtime ASC ``` 按「上次动作」升序排,最久没动的排最上面。这一列比任何优先级字段都诚实。 ## 一个 dataviewjs 小用例 需要按标签统计数量时,DQL 不太好写,用 JS 更直接: ```dataviewjs const pages = dv.pages('"30 Resources"'); const counts = {}; for (const p of pages) { for (const t of (p.tags ?? [])) { const key = t.toString().replace('#', ''); counts[key] = (counts[key] ?? 0) + 1; } } const rows = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 12); dv.table(["标签", "笔记数"], rows); ``` 它只做一件事:告诉你哪些标签已经膨胀。超过 30 条的标签,通常该升级成 MOC 了。 ## 性能:三条设置 2400 条笔记时,Dataview 默认配置会明显拖慢启动。我做了三件事: 1. 在设置里关掉 `Enable JavaScript Queries` 的全局刷新,只在需要的页面开 `dataviewjs`。 2. 所有查询都写 `FROM` 限定目录,绝不写 `FROM #tag` 全库扫描。 3. 移动端关闭 Dataview 的自动刷新,只在桌面端用驾驶舱。 如果某一页打开要超过 2 秒,先怀疑 `WHERE` 里用了 `contains(file.content, ...)`——那会读全文。换成标签或 frontmatter 字段。 ## 别做成仪表盘地狱 我最初做了 11 个面板,结果是每天打开先看 11 个面板,然后什么也没做。现在只有 3 个,每个都对应一个具体动作:清任务、看最近、解卡点。**面板数量应该等于你每天真正会做的决定数量**,不是你能想到的统计维度数量。 --- ## 像素猫的第一款游戏:从 0 到可玩 Demo 的 7 天路线 URL: https://pixelcat.example.com/articles/pixel-cat-first-game.html 日期: 2026-10-02 | 主题: 游戏制作 | 标签: 立项, Godot, 像素 | 阅读: 2 分钟 一个人做游戏,第一周最容易死在设定里。我的做法是先把想法压缩成一句话,再用三个问题过一遍: - 玩家每分钟重复做的动作是什么? - 这个动作到第 10 分钟会不会腻?如果会,用什么第二个动作救它? - 我能不能在这周结束前让这个动作真的跑起来? 「像素猫在小屋里收拾东西」听起来很散,过滤之后剩下的是:**玩家不断把物品归类放回原位**。重复动作是「拿起—判断—放下」,第二动作是「猫会累,累了还硬放就会打翻东西」。这样第一周的目标就具体了:一个房间、十二件物品、一套对错反馈。 ## 核心循环长这样 | 阶段 | 玩家在做什么 | 系统给什么反馈 | | --- | --- | --- | | 观察 | 在房间里找散落的物品 | 物品有轻微呼吸动画与轮廓高光 | | 判断 | 决定它属于哪个架子 | 靠近架子时架子亮起,显示类别图标 | | 放置 | 按下交互键 | 正确:清脆音效 + 猫耳朵竖一下;错误:物品滑落 | | 结算 | 完成一轮整理 | 灯变亮一档,猫坐下舔爪,给出用时与失误数 | 这个循环一次 40 到 90 秒,正好是一局。所有系统都为它服务,不为它服务的功能第一周不做。 ## 7 天日程 | 天 | 交付物 | 判据 | | --- | --- | --- | | D1 | 一个能走的猫(方块也行) | WASD 移动不卡、不穿墙 | | D2 | 房间碰撞与相机 | 相机跟随不抖,边界不露黑 | | D3 | 拾取与放置 | 能拿起 1 个物品并放到 1 个架子上 | | D4 | 类别判定与反馈 | 对错各 1 种音效与动画 | | D5 | 12 件物品与计分 | 一局能完整开始与结束 | | D6 | 替换美术与动画 | 猫有走、待机、放置三个动作 | | D7 | 打磨与录制 | 能连续通关 3 次并录出 30 秒片段 | D7 的判据很关键:**能连续通关三次**才算做完。只通关一次通常意味着你在靠运气掩盖 bug。 ## 第 3 天必须做对的事 第一周唯一容易做错的是拾取判定。新手常见写法是在 `_process` 里算距离,结果是手感和帧率绑在一起,掉帧时物品会「粘」在手上。正确做法是用 `Area2D` 的进出信号维护一个候选列表,交互时取最近的那个。 ```gdscript extends Area2D @export var item_id: StringName var held := false func _on_body_entered(body: Node2D) -> void: if held or not body.has_method("register_nearby"): return body.register_nearby(self) func _on_body_exited(body: Node2D) -> void: if body.has_method("unregister_nearby"): body.unregister_nearby(self) func pick_up() -> void: held = true set_deferred("monitoring", false) ``` 猫那边只保留一个按距离排序的数组,交互键按下时取第一个还在范围内的物品。这样即使同屏有 30 件物品,一帧也只有一次 O(n) 排序,而不是每件物品各自做射线检测。 ## 不要做的四件事 - **不要做存档**。第一周没有值得存的东西,做了只是浪费时间。 - **不要做设置菜单**。音量用系统音量,键位固定,等你真的收到抱怨再改。 - **不要做多语言**。中文字体先只留一套,字体文件能省下好几 MB。 - **不要提前做美术**。D1 到 D4 全部用色块,D6 才替换。先画美术的人通常在第 5 天已经不想做了。 ## 收尾:把 demo 变成可判断的东西 做完之后不要立刻加内容。先找三个没看过这个项目的人玩,只问一个问题:**你刚才想做什么,为什么没做成?** 答案会直接告诉你第二周该修什么。我的第一轮答案里出现了三次「我不知道这个东西该放哪」,于是我加了架子高亮,而不是加更多物品。 第二周再谈关卡与音效。第一周的目标只有一个:证明这个循环值得被做成游戏。 --- ## 2400 条笔记之后,我的 Vault 只剩 5 个文件夹 URL: https://pixelcat.example.com/articles/vault-structure.html 日期: 2026-09-20 | 主题: Obsidian 知识库 | 标签: Obsidian, 知识库, 结构 | 阅读: 2 分钟 刚开始用 Obsidian 时,我建了 12 个文件夹:读书、电影、游戏、代码、日记、灵感、人物、健康……两年后发现,**我从来不去点文件夹**。我找笔记的方式是按标题搜、按链接跳、按图谱看。文件夹只在「我该把它放哪」这一刻制造焦虑。 所以我把结构砍到 5 个,规则只有一条:**文件夹表示「这条笔记现在处于什么状态」,不表示「它讲什么」**。讲什么交给标签和 MOC。 ## 目录树 ```text Vault/ ├── 00 Inbox/ 临时收件,每周清空 ├── 10 Areas/ 长期领域,不结项 │ ├── 游戏制作.md (MOC) │ ├── 知识系统.md │ └── 生活运营.md ├── 20 Projects/ 有截止日期的项目 │ ├── 像素猫小屋.md │ └── 2026 秋 阅读计划.md ├── 30 Resources/ 别人的东西 + 我的沉淀 │ ├── 读书笔记/ │ ├── 课程与讲座/ │ └── 代码片段/ ├── 40 Archive/ 归档,只读 └── 90 Meta/ 模板、附件、看板 ├── Templates/ └── Attachments/ ``` 数字前缀不是强迫症,是让排序稳定:在任何视图里,Inbox 永远在最上面,Archive 永远在最下面。 ## 五类各自的判据 | 位置 | 进入条件 | 离开条件 | | --- | --- | --- | | Inbox | 任何来不及归类的输入 | 周日清理时归位或删除 | | Areas | 没有终点的责任,比如「游戏制作」 | 几乎不离开,只更新 MOC | | Projects | 有明确完成标志与日期 | 完成后整体移入 Archive | | Resources | 可被复用的外部资料与提炼 | 被引用进 Areas 或 Projects 后保留 | | Archive | 已结束、只作查询 | 不再修改 | 关键在「Areas 和 Projects 的界线」:凡是能被「做完」的,都是 Project;凡是永远在做的,都是 Area。把这两者混在一起,你的看板会永远有 30 个未完成项。 ## frontmatter 只留六个字段 ```yaml --- title: 2400 条笔记之后,我的 Vault 只剩 5 个文件夹 slug: vault-structure date: 2026-09-20 tags: [obsidian, 结构] level: 入门 summary: 一句话说完这篇笔记解决什么问题。 --- ``` 六个字段不是随便定的: - `title` 和 `slug` 让笔记能被发布成网页(我自己的站点就是这样生成的) - `date` 是排序与「最近更新」视图的唯一依据 - `tags` 控制在 3 个以内,超过就说明你在用标签分类,该建 MOC 了 - `level` 用来区分入门/进阶/源码,搜索时可以过滤 - `summary` 是给未来的自己和 AI 看的,不是摘要,是「这条笔记能解决什么」 不要在 frontmatter 里放状态机(status: draft/review/done),它会和文件夹的位置打架。 ## MOC:三种,够用 **领域 MOC**(Areas 里)回答「这个领域我知道什么」,按主题分组列链接,每季度更新一次。 **项目 MOC**(Projects 里)回答「这个项目现在到哪一步」,只有三块:目标、当前卡点、下一步。写完就删,不留历史。 **索引 MOC**(Resources 里)回答「这类资料有哪些」,按时间或作者排,只加一句话点评。 MOC 不要用 Dataview 自动生成全部,那会变成一堵没有观点的链接墙。手写的那 10 条链接才是你真正读过的。 ## 每周 20 分钟清理 1. 打开 Inbox,逐条处理:能归位就归位,不能就删。目标清零。 2. 看 Projects,把没有下一步动作的项目移到 Archive 或删掉。 3. 打开本周新建的笔记,补上 summary,至少加一条出链。 4. 随机点开三条旧笔记,如果它们没有任何入链,考虑合并或删除。 第 4 步最重要。一个笔记如果没人指向它,它在这个知识库里等于不存在。 ## 一个反例 我见过把 Vault 建成图书馆的人:分类到三级,标签到五个维度,每个笔记都有 20 行 frontmatter。结果是新增一条笔记要 3 分钟决策,最后他不再记笔记了。 结构的目标不是整齐,是**让记录这个动作的成本接近零**。当你不确定放哪时,正确的做法永远是:扔进 Inbox,周日再说。