You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
my-bricks/docs/architecture.md

155 lines
6.2 KiB

# 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 断点调试 |