c_tetris

第 2 课:项目结构 📁

2.1 本课目标

理解 C 语言项目的组织方式,学会如何设计合理的文件结构。


2.2 为什么需要良好的项目结构?

问题场景

想象一下,如果你把所有的代码都放在一个文件里:

// ❌ 糟糕的做法:main.c 有 2000 行
#include <stdio.h>

// 100 行变量定义
// 500 行方块逻辑
// 300 行碰撞检测
// 400 行渲染代码
// 200 行输入处理
// 500 行游戏循环

int main() { ... }

问题:

模块化设计的好处

✅ 每个文件负责一个功能
✅ 快速找到需要的代码
✅ 多人可以并行开发
✅ 代码可以在其他项目复用

2.3 我们的项目结构

tetris/
├── src/                    # 源代码目录
│   ├── main.c              # 程序入口(约 80 行)
│   ├── game.c/h            # 游戏核心逻辑(约 120 行)
│   ├── board.c/h           # 游戏板管理(约 50 行)
│   ├── tetromino.c/h       # 方块定义和操作(约 110 行)
│   ├── render.c/h          # ncurses 渲染(约 230 行)
│   ├── input.c/h           # 键盘输入处理(约 90 行)
│   └── score.c/h           # 分数和最高分(约 130 行)
├── docs/                   # 教程文档
├── data/                   # 游戏数据
│   └── highscores.txt      # 最高分记录
├── obj/                    # 编译中间文件(自动生成)
├── Makefile                # 构建配置
└── README.md               # 项目说明

2.4 文件命名规则

C 语言文件类型

扩展名 用途 内容
.c 源文件 函数的实现(具体代码)
.h 头文件 函数的声明(接口)

为什么要分 .c.h

头文件(.h)- 接口

// tetromino.h
#ifndef TETROMINO_H
#define TETROMINO_H

// 告诉编译器:有这么一个函数
void tetromino_init(Tetromino* t, TetrominoType type);

#endif

源文件(.c)- 实现

// tetromino.c
#include "tetromino.h"

// 实际代码
void tetromino_init(Tetromino* t, TetrominoType type) {
    t->type = type;
    t->x = BOARD_WIDTH / 2 - 2;
    t->y = 0;
    // ... 具体实现
}

好处:


2.5 各模块详细职责

main.c - 程序入口

职责: 初始化、游戏主循环、清理资源

// main.c 核心结构
#include "game.h"
#include "render.h"
#include "input.h"

int main(void) {
    // 1. 初始化
    render_init();
    load_high_scores();
    
    Game* game = game_create();
    game_init(game);
    
    // 2. 游戏主循环
    while (game->running) {
        input_handle_game(game, input_get_key());
        game_update(game);
        game_render(game);
        napms(50);
    }
    
    // 3. 清理
    game_destroy(game);
    render_cleanup();
    return 0;
}

为什么这样设计?


game.c/h - 游戏核心

职责: 游戏状态管理、方块生成、游戏逻辑更新

// game.h - 声明
typedef struct {
    Board board;           // 游戏板
    Tetromino current;     // 当前方块
    Tetromino next;        // 下一个方块
    Tetromino ghost;       // 影子方块
    int score;
    int level;
    bool running;
    bool game_over;
} Game;

Game* game_create(void);
void game_init(Game* game);
void game_update(Game* game);
void game_calculate_ghost(Game* game);

为什么需要 Game 结构体?


board.c/h - 游戏板

职责: 管理 20x10 的游戏网格、锁定方块、消除行

// board.h
#define BOARD_WIDTH   10
#define BOARD_HEIGHT  20

typedef struct {
    int cells[BOARD_HEIGHT][BOARD_WIDTH];
} Board;

void board_init(Board* board);
void board_lock_tetromino(Board* board, Tetromino* t);
int board_clear_lines(Board* board);

为什么用二维数组?

cells[20][10] 直观表示游戏区域:

行 0  [0][0] [0][1] ... [0][9]
行 1  [1][0] [1][1] ... [1][9]
...
行 19 [19][0] [19][1] ... [19][9]

- 0 表示空格
- 1-7 表示不同颜色的方块

tetromino.c/h - 方块

职责: 方块定义、移动、旋转

// tetromino.h
typedef enum {
    TETRO_I, TETRO_O, TETRO_T, TETRO_S,
    TETRO_Z, TETRO_J, TETRO_L
} TetrominoType;

typedef struct {
    TetrominoType type;
    int x, y;           // 位置
    int shape[4][4];    // 4x4 形状矩阵
} Tetromino;

void tetromino_init(Tetromino* t, TetrominoType type);
void tetromino_rotate(Tetromino* t);

为什么用 4x4 矩阵?


render.c/h - 渲染

职责: 使用 ncurses 绘制游戏界面

// render.h
void render_init(void);           // 初始化 ncurses
void render_cleanup(void);        // 清理
void render_game(Game* game);     // 绘制整个游戏
void render_ghost(Tetromino* t);  // 绘制影子

为什么单独分离渲染模块?


input.c/h - 输入处理

职责: 读取键盘输入、转换为游戏操作

// input.h
void input_handle_game(Game* game, int key);

为什么需要输入处理模块?


score.c/h - 计分系统

职责: 计算分数、管理最高分记录

// score.h
int score_calculate_lines(int lines, int level);
void save_high_score(const char* name, int score, ...);
void load_high_scores(void);

2.6 模块依赖关系

                    main.c
                      │
         ┌────────────┼────────────┐
         ↓            ↓            ↓
      game.c     render.c      input.c
         │            │            │
    ┌────┴────┐       │            │
    ↓         ↓       │            │
 board.c   tetromino.c│            │
    │         │       │            │
    └────┬────┘       │            │
         ↓            ↓            ↓
                  score.c

依赖规则:


2.7 Makefile 详解

# 1. 定义变量
CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -Isrc -O2
LDFLAGS = -lncurses

# 2. 查找所有源文件
SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:src/%.c=obj/%.o)

# 3. 最终目标
tetris: $(OBJS)
	$(CC) $(OBJS) -o tetris $(LDFLAGS)

# 4. 编译规则
obj/%.o: src/%.c
	$(CC) $(CFLAGS) -c $< -o $@

# 5. 清理
clean:
	rm -rf obj/*.o tetris

关键概念:

语法 含义
$(VAR) 引用变量
$< 第一个依赖文件
$@ 目标文件
wildcard 通配符匹配文件

2.8 动手实践

练习 1:查看编译过程

# 查看详细编译过程
make clean
make -n  # -n 表示只显示命令,不执行

练习 2:添加调试输出

# 在 main.c 的 main() 函数开头添加:
printf("游戏启动!\n");

# 重新编译运行
make
make run

练习 3:统计代码行数

# 统计所有 .c 文件的行数
wc -l src/*.c

# 输出示例:
   80 src/main.c
  120 src/game.c
   ...
  900 total

✅ 本课检查清单


📝 小作业

  1. 打开 src/game.h,找出 Game 结构体有哪些成员

  2. src/ 目录中,哪个文件负责绘制游戏界面?

  3. 如果要添加”音效”功能,应该创建什么文件?放在哪里?


下一课:数据结构