Java 面向对象 · 交互课程

示例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、分数/关卡/速度碰撞判定、消行计分、定时下落、绘制
Shape5×5 方块矩阵 s、位置 x、步长 widthrotate() 旋转、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];      // 二维数组:棋盘(行×列)
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()。

三、抽象:从「游戏」到三个类

拆分的依据是变化的独立性:改按键不碰 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 计算步长——外部改不了
    }
}

对照 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

思考题

  1. Shape.width 为什么设为 private?如果改成 public,外部能做出什么坏事?
  2. Game.keyPressed 里同时调 box[0].play(...) 和 box[1].play(...)——两个玩家共用同一套键码数组会怎样?
  3. stick 是 41×22 而不是 40×20,多出来的一行一列用来做什么?(提示:init 里对 stick[i][0]、stick[boxpercolumn][j] 的赋值)

学习小测验

学习小测验

Q1

Erase 中 GameBox 类继承自哪个 Swing 组件?

Q2

Shape 类里 width 被声明为 private,意味着( )

Q3

Game 中 int[] keya = {87, 83, 65, 68} 表示( )

Q4

GameBox 里棋盘 stick 的数据结构是( )

Q5

Game 构造方法里 box[0]、box[1] 两个 GameBox 的作用是( )

Q6

GameBox 内部类 TimerListener.actionPerformed 每次触发时做什么?