Initial: playable 3-level Breakout with menus, audio, and power-ups

This commit is contained in:
2026-08-12 17:27:11 +08:00
commit 319e75447a
45 changed files with 1594 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
# my-bricks 架构设计(Godot 4 + GDScript)
> 面向完全没接触过 Godot 的新手。原则:**每个脚本只做一件事**,场景之间用信号通信,参数全部可配置。
## 1. 技术选型
- **引擎**:Godot 4.x 稳定版(4.3 或更新,直接官网下载标准版)
- **语言**:GDScript(Godot 4 内置的是 GDScript 2.0,带类型标注,写起来更像 Python)
- **维度**:2D 场景,不碰 3D
- **版本控制**:Git 可选,但从 M1 开始建议每完成一个里程碑提交一次
## 2. 项目目录结构
```
my-bricks/
├── project.godot # 项目配置文件(自动生成)
├── icon.svg
├── scenes/ # 场景文件(.tscn,在编辑器里搭)
│ ├── main.tscn # 主场景:流程控制 + 游戏世界 + UI
│ ├── ball.tscn
│ ├── paddle.tscn
│ ├── brick.tscn
│ └── ui/
│ ├── main_menu.tscn
│ └── hud.tscn
├── scripts/ # 脚本(与场景同名,附着在根节点上)
│ ├── main.gd
│ ├── ball.gd
│ ├── paddle.gd
│ ├── brick.gd
│ ├── level.gd
│ └── ui/
│ ├── main_menu.gd
│ └── hud.gd
├── data/ # 数据驱动的关卡定义
│ └── level_1.gd
└── assets/
├── audio/
└── images/
```
新手习惯建议:**场景和脚本放不同目录**(scenes/ 与 scripts/),文件名一一对应,找起来不迷路。
## 3. 场景树与节点职责
主场景结构(每个框 = 一个节点,注释 = 职责):
```
Main (Node2D) # main.gd
│ # 游戏状态机、关卡切换、
│ # 分数与生命的"权威数据"
├── World (Node2D)
│ ├── Level (Node2D) # level.gd:读关卡数据,生成砖块
│ │ └── Brick * n (StaticBody2D) # brick.gd:血量、被击碎
│ ├── Paddle (CharacterBody2D) # paddle.gd:输入、移动
│ ├── Ball (CharacterBody2D) # ball.gd:发射、反弹、出界判定
│ └── LoseZone (Area2D) # 底部一条"死亡线",球碰触发扣命
├── Walls (StaticBody2D) # 左右 + 天花板(或代码夹逼,二选一)
└── UI (CanvasLayer) # 界面不受相机影响
├── HUD # hud.gd:分数/生命/提示文本
└── MainMenu # main_menu.gd:开始/退出按钮
```
**职责划分(重要)**:分数、生命、当前关卡**只有 Main 知道**;砖块不知道分数谁管,它只发"我碎了"的信号。这样以后加音效、粒子、成就,都只是多连一个信号,不用改核心逻辑。
## 4. 信号设计(解耦的关键)
用信号代替"到处 get_parent 找对象":
```
brick.gd: signal destroyed(points: int) # 砖块被击碎(带上分值)
ball.gd: signal lost # 球出界
signal brick_hit # 命中砖块(用于计连击/提速)
main.gd: 负责连接以上信号 → 加分、扣命、切关卡
```
典型写法(在 Main 里连接):
```gdscript
$World/Ball.brick_hit.connect(_on_ball_hit_brick)
$World/Ball.lost.connect(_on_ball_lost)
```
**为什么用信号**:`brick` 不需要知道"谁在计分",只广播"我碎了"。Main 收到后加分、判断是否过关。组件互不依赖,新手改起来不容易改坏别处。
## 5. 物理层设置(一次配好)
项目设置 → 物理层,给两层命名:
- **Layer 1 = World**:挡板、墙壁
- **Layer 2 = Brick**:所有砖块
球(Ball)的碰撞 mask 勾选 1 + 2:既能撞挡板/墙,也能撞砖块。
砖块之间互不碰撞,挡板不会被砖块挡住——物理交互简单可控。
## 6. 碰撞方案(新手建议)
- **球 / 挡板 / 砖块** 用 `CharacterBody2D` + `move_and_collide()`:
- 球在 `_physics_process` 里每帧手动移动,碰撞时读 `collision.get_normal()` 做镜面反射。
- 优点:速度完全由自己控制,行为可预测、好调试;比 `RigidBody2D`(模拟物理)少很多"球为什么乱飞"的玄学。
- **出界检测** 用 `Area2D`(LoseZone):Area 是"探测"不是"碰撞",球经过它 `body_entered` 信号触发扣命,最合适。
> 新手容易踩的坑:`move_and_collide` 必须在 `_physics_process`(物理帧)里调用,放进 `_process`(渲染帧)会导致表现不稳定。
## 7. 关卡数据格式
`data/level_1.gd`(用脚本存常量,最简单直观):
```gdscript
# 字符 → 砖块类型: "." = 空位, "1"/"2"/"3" = 1/2/3 血, "U" = 不可破坏
const LAYOUT: Array[String] = [
"1111111111",
"2222222222",
"3333333333",
"1.2.3.2.1",
]
```
`level.gd` 逐行逐字符读,按坐标实例化砖块。**改关卡 = 改字符串**,代码一行不用动。
## 8. 游戏状态机(简单版)
Main 里一个枚举 + `match` 分发:
```gdscript
enum GameState { MENU, PLAYING, LEVEL_CLEAR, GAME_OVER }
var state: GameState = GameState.MENU
func _process(_delta):
match state:
GameState.PLAYING:
_handle_playing()
GameState.GAME_OVER:
_handle_game_over()
```
比散落的 `if game_over: ...` 清晰得多,后期加暂停状态也容易。
## 9. 常用新手写法速查
| 场景 | 写法 |
|------|------|
| 引用节点 | `@onready var ball = $World/Ball` |
| 编辑器可调参数 | `@export var ball_speed := 400.0` |
| 每帧更新 | `func _process(delta)` 或 `func _physics_process(delta)` |
| 物理移动 | `move_and_collide(velocity * delta)` |
| 连接信号 | `node.signal_name.connect(_on_signal)` |
## 10. 关键风险与对策
| 风险 | 对策 |
|------|------|
| 球撞挡板后直上直下、不可控 | M2 优先做"按击中位置分档反弹角度" |
| 球速太快/太慢导致手感差 | 球速、加速系数全部 `@export`,随时在编辑器调 |
| 一个场景里塞太多逻辑 | 每完成一个功能检查职责是否单一,超标就拆场景/拆脚本 |
| 调试困难 | 球速调慢 + `print()` 打碰撞日志,配合 Godot 断点调试 |
+86
View File
@@ -0,0 +1,86 @@
# my-bricks 游戏设计文档
> 版本:v1(调研阶段)| 引擎:Godot 4 + GDScript | 定位:新手练手小游戏
## 1. 一句话概述
打砖块(Breakout)类小游戏:玩家控制挡板反弹小球,击碎场上全部砖块即可过关。
目标是**跑通一个完整的玩法闭环**,同时作为 Godot / GDScript 的入门练习项目。
## 2. 设计目标与边界
**做:**
- 完整闭环:移动挡板 → 反弹小球 → 击碎砖块 → 得分/过关 → 失败/重开
- 新手可维护:每个场景/脚本单一职责,场景之间用信号(signal)解耦
- 数据驱动关卡:关卡用字符串网格定义,改关卡不用改代码
**不做(第一版明确排除,防止范围蔓延):**
- 不联网、不做存档系统
- 不做复杂物理模拟(重力、流体、刚体堆叠等)
- 不做完整美术管线,先用纯色几何图形占位,后期可换
## 3. 核心玩法
### 3.1 玩家操作
| 操作 | 行为 |
|------|------|
| ← → / A D | 挡板左右移动 |
| 鼠标移动 | 挡板跟随(可选,二选一) |
| 空格 | 发球 / 重开 / 进入下一关 |
### 3.2 核心循环
1. 回合开始:球停在挡板上方,按空格发射
2. 球在场地内反弹;碰到砖块 → 砖块掉血,击碎则得分
3. 球从底部出界 → 失去一条命,球重置回到挡板
4. 生命归零 → Game Over;砖块清空 → 过关进入下一关
### 3.3 反弹规则(手感核心)
- **挡板碰撞**:反弹角度由球击中挡板的**横向位置**决定——中心位置竖直反弹,越靠边缘角度越大。这是打砖块手感的核心,优先级最高。
- **墙壁 / 天花板**:镜面反弹(x 或 y 分量取反)。
- **砖块**:镜面反弹 + 轻微随机扰动,防止球在水平/垂直路径上"鬼打墙"死循环。
### 3.4 砖块
- 砖块有生命值(1 / 2 / 3 血),用颜色区分。
- 每击碎一块按类型给分,分数写在砖块数据里。
- 预留"不可破坏"类型,供后期关卡使用。
### 3.5 难度曲线
- 球速随击砖次数**缓慢上升**(每关内)。
- 每关基础球速提高一个台阶。
- 球速/挡板速度都用可配置参数(编辑器里能改),方便调手感。
## 4. 道具系统(第二里程碑,非第一版必需)
- 候选道具:**加宽挡板、多球、穿透球、减速、加命**,先挑 1~2 个实现。
- 实现思路:击碎砖块有概率掉落,掉落物用 Area2D 向下落,被挡板接到后生效。
- 设计原则:道具锦上添花,不依赖道具也能正常通关。
## 5. 关卡设计
- 关卡用「字符串网格」定义:每行一个字符串 = 一行砖块,每个字符映射一种砖块类型。
- 第一版准备 3~5 个示例关卡,从"整面墙"到"留空位、混合血量"。
- 具体数据格式见 [architecture.md](architecture.md) 第 7 节。
## 6. 界面与流程
```
主菜单 → 游戏(HUD 显示分数/生命/关卡) → 过关提示 → 下一关
↘ Game Over → 返回主菜单
```
- 主菜单:开始游戏 / 退出
- HUD:分数、剩余生命、当前关卡
- 状态机:`MENU → PLAYING → LEVEL_CLEAR / GAME_OVER`
## 7. 美术与音频方向
- **美术**:纯色/渐变几何图形(ColorRect 或简单 Sprite2D)即可,先不引入图片资源。
- **音频**:免费素材或程序合成;需要 4 类音效——命中、碎砖、出界、过关。
- **视觉反馈**(小成本提升手感):碎砖时粒子飞出(GPUParticles2D)或简单 Tween 缩放,命中时球/砖颜色闪动。
## 8. 第一版验收标准
- [ ] 键盘能完整玩完一关并进入下一关
- [ ] 生命耗尽出现 Game Over,可重开
- [ ] HUD 分数与生命显示正确、实时更新
- [ ] 球与挡板的反弹角度可控、可预期(不出现"一直直上直下")
+104
View File
@@ -0,0 +1,104 @@
# my-bricks 开发里程碑
> 新手学习路径,按"每步都能跑、每步都能看到效果"划分。
> 时间只是业余时间的大致估算,按你自己的节奏来,不必赶。
总原则:
- 每个里程碑结束时,游戏**必须能运行**,绝不攒一堆代码一起调试。
- 一个里程碑内写坏的代码,当步修好,不留债到下一步。
- 每完成一个里程碑,`git commit` 一次(从 M1 开始建仓库)。
---
## M0 环境搭建(约半天)
**目标**:装好工具,跑通"空场景 → 画面上有个方块"。
- [ ] 下载安装 Godot 4 稳定版(官网 godotengine.org,选 4.x 标准版)
- [ ] 新建项目,分辨率设为 1280×720,主场景随便选一个
- [ ] 场景里放一个 ColorRect(或任意节点),按 F5 运行看到它
- [ ] 熟悉三个面板:场景树(左)、属性(右)、运行按钮(右上)
- [ ] 打开项目设置 → 物理层,命名 Layer 1 = World、Layer 2 = Brick(给 M1 做准备)
**验收**:能运行一个空场景,节点能选中、能改属性。
**遇到的坑记下来**:装不上/运行不了等环境问题,优先查 Godot 官方文档和社区。
---
## M1 核心玩法闭环(约 1~2 天)
**目标**:完成"能玩一局":挡板、球、砖块、计分、生命、Game Over。
- [ ] 创建 `paddle.tscn`:CharacterBody2D + CollisionShape2D + 可见图形(ColorRect/Sprite)
- `paddle.gd`:键盘 ←→/AD + 鼠标移动,`@export` 挡板速度
- [ ] 创建 `ball.tscn`:CharacterBody2D + CollisionShape2D + 图形
- `ball.gd`:初始停在挡板上(发球状态)→ 空格发射 → `_physics_process` 里 `move_and_collide`
- 撞墙/砖块/挡板:读 `collision.get_normal()` 做镜面反射
- [ ] 创建 `brick.tscn`:StaticBody2D + CollisionShape2D + ColorRect
- `brick.gd`:`@export` 血量;被撞扣血,归零销毁并发 `destroyed(points)` 信号
- [ ] `level.gd`:读 `data/level_1.gd` 的字符串网格,按坐标生成一排砖块
- [ ] `main.tscn` + `main.gd`:组装 World/Paddle/Ball/LoseZone/UI
- LoseZone:屏幕底部一条 Area2D,球经过 `body_entered` → 扣命 + 重置球
- HUD:分数 Label、生命 Label(先用系统字体,样式后补)
- 状态机:`PLAYING` / `GAME_OVER`(先只做这两个,MENU 留到 M3)
- [ ] 球速、挡板速度、初始命数全部 `@export`,方便调
**验收**:
- 能打碎所有砖块,分数正确累加
- 球出界扣命,命尽出现 Game Over
- 重开后一切重置正常
---
## M2 手感打磨(约 1 天)
**目标**:把"好玩"的核心做出来。这是打砖块最值得花时间的一步。
- [ ] **挡板反弹角度分档**(核心):按球击中挡板的横向偏移换算反弹角
- 中心 → 接近竖直;边缘 → 大角度斜飞。公式写一个函数,M1 里先用它替换纯镜面
- [ ] 球速随击砖次数缓慢上升(设上限),控制单局节奏
- [ ] 碎砖反馈:GPUParticles2D 飞出碎片,或 Tween 缩放 + 颜色闪动
- [ ] 命数用图标显示(简单 Sprite 排一排),比纯数字直观
**验收**:反弹可控、可预期;连续玩 3 分钟不觉得"球不受我控制"。
---
## M3 内容扩展(约 1~2 天)
**目标**:从"一关"变成"一个游戏"。
- [ ] 关卡数据驱动化:`data/` 下加 `level_2.gd`、`level_3.gd`,Main 按顺序加载
- [ ] 过关判定:砖块清空 → `LEVEL_CLEAR` 提示 → 空格/按钮进下一关
- [ ] 主菜单场景:开始游戏 / 退出(状态机补上 `MENU`)
- [ ] 音效:命中、碎砖、出界、过关 4 个 AudioStreamPlayer2D(素材先用免费 CC0)
- [ ] 道具(选 1~2 个):加宽挡板、多球(难度大可选做)
- 击碎砖块概率掉落 → Area2D 下落 → 挡板接住生效 → 定时器恢复
**验收**:从主菜单开始,能连续玩 3 个关卡通关;有音效、有道具。
---
## M4 收尾发布(约 1 天)
**目标**:把作品拿得出手。
- [ ] 视觉统一:占位图形换成顺眼的配色/贴图(可选,纯色也完全可行)
- [ ] 导出:桌面(macOS/Windows)或 Web(HTML5),导出前按导出模板指引走一遍
- [ ] 写 README:游戏介绍 + 操作说明(放项目根目录)
- [ ] 按 [design.md](design.md) 第 8 节过一遍验收清单,修掉残留问题
**验收**:导出的程序在另一台机器/浏览器能独立运行。
---
## 里程碑总览
| 里程碑 | 内容 | 产物 |
|--------|------|------|
| M0 | 环境搭建 | 能运行的空场景 |
| M1 | 核心玩法闭环 | 能玩一局(无菜单) |
| M2 | 手感打磨 | 反弹可控、有反馈 |
| M3 | 内容扩展 | 菜单 + 多关卡 + 音效 + 道具 |
| M4 | 收尾发布 | 可发布的成品 |