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

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 里连接):

$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(用脚本存常量,最简单直观):

# 字符 → 砖块类型: "." = 空位, "1"/"2"/"3" = 1/2/3 血, "U" = 不可破坏
const LAYOUT: Array[String] = [
    "1111111111",
    "2222222222",
    "3333333333",
    "1.2.3.2.1",
]

level.gd 逐行逐字符读,按坐标实例化砖块。改关卡 = 改字符串,代码一行不用动。

8. 游戏状态机(简单版)

Main 里一个枚举 + match 分发:

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