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