示例1 · Erase 单机版
擦除主题俄罗斯方块:基础语法、抽象、封装
示例1 · Erase 单机版
贯穿式教学示例(一):以「擦除主题俄罗斯方块」(Erase-JAVA,包
com.zmofun.erase)为载体,把基础语法、抽象、封装放进真实代码。素材在本仓库素材/Erase-JAVA/,以下代码均摘自其源码(有删节)。
一、项目概览:一个游戏,三个类
Erase 是双人同屏的擦除版俄罗斯方块:左边面板用 W/S/A/D 控制,右边面板用方向键控制。核心包 com.zmofun.erase 只有三个主类:
| 类 | 职责(状态) | 行为(方法) |
|---|---|---|
Game extends JFrame | 两个棋盘 box[2]、菜单、按键映射 | 组装窗口/菜单,转发按键事件 |
GameBox extends JPanel | 棋盘 int[][] stick、当前方块 curshape、分数/关卡/速度 | 碰撞判定、消行计分、定时下落、绘制 |
Shape | 5×5 方块矩阵 s、位置 x、步长 width | rotate() 旋转、move() 移动、paint() 绘制 |
抽象就是把「一个游戏」拆成三个类:每个类只关心自己的状态与行为,彼此通过方法调用协作。
二、基础语法在真实代码里的落点
字段:类型即承诺
// GameBox(节选)
boolean start, pause; // 布尔:游戏状态
int score, level, speed, layer; // 整数:计分与难度
int[] controlkey = new int[4]; // 一维数组:四个控制键码
long inter; // 长整型:上次下落的时间戳
Shape curshape; // 引用类型:当前方块对象
int[][] stick = new int[41][22]; // 二维数组:棋盘(行×列)
- 基本类型(
boolean/int/long)存值;引用类型(Shape、Timer)存地址; Game里两组键码一目了然:
int[] keya = { 87, 83, 65, 68 }; // W S A D → 玩家1
int[] keyb = { 38, 40, 37, 39 }; // ↑ ↓ ← → → 玩家2
构造方法:对象出生即就绪
public GameBox() {
timer = new Timer(200, new TimerListener()); // 每 200ms 触发一次
timer.start();
}
Game 的构造方法则展示了组合:窗口创建时 new 出两个棋盘并各自 init(...)(位置、格子尺寸、键位)——has-a 关系在代码里的样子。
方法:行为封装在对象里
GameBox.play(int key) 是「处理一次按键」的完整逻辑:先查 canrotate()/canmove(direct) 做碰撞判定,通过才执行 curshape.rotate() / curshape.move(...);定时器到点自动下落一行,落不动就 gameover()。
三、抽象:从「游戏」到三个类
- Game = 窗口 + 菜单 + 键盘入口。它不画任何格子,只负责把
keyPressed转发给两个GameBox.play(...); - GameBox = 一块棋盘的完整生命周期:初始化(
init)、碰撞(canmove/canrotate)、消行计分(addscore)、绘制(paintComponent); - Shape = 一个 5×5 的方块矩阵,自带旋转与移动。
拆分的依据是变化的独立性:改按键不碰 Shape;改方块形状不碰菜单代码。
四、封装:以 Shape 为例
public class Shape {
protected int x; // 同包/子类可访问
private int width = 25; // 只有 Shape 自己能碰
int[][] s = new int[5][5]; // 包级私有:同包可见
public void setX(int x) { this.x = x; }
public int getX() { return x; }
public void move(int direct) {
if (direct == 2)
x = x + width / 5; // 用私有 width 计算步长——外部改不了
}
}
width是内部细节(像素步长),外部只需要「向右移动一格」这个行为,所以private;x允许同包的GameBox直接读写(碰撞判定要读坐标)——protected恰好覆盖这个场景;s(方块矩阵)是包级私有:GameBox.canrotate()需要逐格检查,同包可见即可。
对照 C 语言:对象 = 数据 + 行为绑定
同一功能在 C 里要手写结构体 + 函数指针 + 显式传 this(摘自 proto/ShapeEx.java 注释):
typedef struct shape {
int x;
int (*move)(int direct, struct shape *this); // 函数指针当"方法"
struct shape *next;
} SHAPE;
SHAPE s1;
s1.move = move; // 手动把函数挂到对象上
s1.move(2, &s1); // 手动传 this
Java 的 s1.move(2) 一行搞定:编译器替你做了数据与行为的绑定、this 的传递——这就是「面向对象」省掉的样板。
五、知识点映射
| 课程知识点 | Erase 中的落点 |
|---|---|
| 类与对象 | Game/GameBox/Shape 三类;box[0] = new GameBox() |
| 构造方法 | GameBox() 建 Timer;Game() 组装窗口 |
| 字段类型 | boolean/int/long/int[]/int[][]/引用 全齐 |
| 封装 | Shape 的 protected x / private width / 包级 s |
| 组合(has-a) | Game 持有 GameBox[2];GameBox 持有 Shape curshape |
| 继承 | GameBox extends JPanel、Game extends JFrame |
思考题
Shape.width为什么设为private?如果改成public,外部能做出什么坏事?Game.keyPressed里同时调box[0].play(...)和box[1].play(...)——两个玩家共用同一套键码数组会怎样?stick是 41×22 而不是 40×20,多出来的一行一列用来做什么?(提示:init里对stick[i][0]、stick[boxpercolumn][j]的赋值)
学习小测验
学习小测验
Erase 中 GameBox 类继承自哪个 Swing 组件?
Shape 类里 width 被声明为 private,意味着( )
Game 中 int[] keya = {87, 83, 65, 68} 表示( )
GameBox 里棋盘 stick 的数据结构是( )
Game 构造方法里 box[0]、box[1] 两个 GameBox 的作用是( )
GameBox 内部类 TimerListener.actionPerformed 每次触发时做什么?