Pong 是史上第一款商业电子游戏。在控制台里做 Pong 不需要图形库:用 ANSI 转义序列清屏重绘,用 termios 实现无阻塞按键,一个 while 循环就能跑出实时动画。这也是"游戏主循环 + 帧率控制"的入门套路。

玩法介绍

60×20 场地里小球自动运动,碰左右上墙反弹;底部挡板用 a/d 移动接球,接到得分,漏接结束,按 q 退出。每帧暂停 80 毫秒,动画就"动"起来了。

第 1 步:场地与状态变量

游戏状态就是一组变量:球的位置、速度分量、挡板位置。速度每帧叠加到位置,就产生了运动——最简单的"物理":

#include <iostream>
#include <thread>
#include <chrono>
#include <cstdlib>
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>

const int W = 60, H = 20;
int ballX = W / 2, ballY = H / 2;
int vx = 1, vy = 1;          // 每帧移动 1 格
int paddleX = W / 2 - 3;     // 挡板左端坐标
const int paddleW = 6;
int score = 0;

第 2 步:无阻塞键盘输入

默认 std::cin 要等回车,游戏可等不起。用 termios 关掉"行缓冲 + 回显",用 fcntl 把标准输入设为非阻塞:有键立即返回,没键返回 -1:

void setNonBlocking() {
    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
}

void hideEcho() {
    termios t;
    tcgetattr(STDIN_FILENO, &t);
    t.c_lflag &= ~(ICANON | ECHO);   // 关行缓冲与回显
    tcsetattr(STDIN_FILENO, TCSANOW, &t);
}

int readKey() {
    char c;
    if (read(STDIN_FILENO, &c, 1) == 1) return c;
    return -1;
}

第 3 步:绘制一帧

动画原理:先清屏再整帧重画。\033[2J 清屏、\033[H 光标回左上角。逐字符判断:球画 O,挡板画 =,墙画 |-:

void draw() {
    std::cout << "\033[2J\033[H";   // 清屏 + 光标回原点
    for (int y = 0; y < H; ++y) {
        for (int x = 0; x < W; ++x) {
            if (x == ballX && y == ballY) std::cout << "O";
            else if (y == H - 2 && x >= paddleX && x < paddleX + paddleW)
                std::cout << "=";
            else if (x == 0 || x == W - 1) std::cout << "|";
            else if (y == 0) std::cout << "-";
            else std::cout << " ";
        }
        std::cout << "\n";
    }
    std::cout << "得分:" << score << "  a/d 移动,q 退出\n";
}

第 4 步:球的运动与碰撞

每帧把速度加到位置。撞左右墙或上墙时速度取反,球就"反弹"。球到挡板行时判断:接住则反弹得分,否则游戏结束:

void update() {
    ballX += vx;
    ballY += vy;
    if (ballX <= 1 || ballX >= W - 2) vx = -vx;   // 左右墙
    if (ballY <= 1) vy = -vy;                      // 上墙
    if (ballY == H - 3) {                          // 挡板行
        if (ballX >= paddleX && ballX < paddleX + paddleW) {
            vy = -vy;
            ++score;
        } else {
            std::cout << "游戏结束!得分:" << score << "\n";
            std::exit(0);
        }
    }
    if (ballY >= H - 1) ballY = H - 2;            // 出界保护
}

第 5 步:主循环与编译运行

主循环节奏固定:读按键 → 移挡板 → 更新球 → 绘制 → 睡 80 毫秒。sleep_for 控制帧率。把第 1~4 步的代码与下面的 main() 存进 pong.cpp:

int main() {
    setNonBlocking();
    hideEcho();
    while (true) {
        int key = readKey();
        if (key == 'a' || key == 'A') paddleX -= 2;
        else if (key == 'd' || key == 'D') paddleX += 2;
        else if (key == 'q') break;
        if (paddleX < 1) paddleX = 1;
        if (paddleX + paddleW > W - 1) paddleX = W - 1 - paddleW;
        update();
        draw();
        std::this_thread::sleep_for(std::chrono::milliseconds(80));
    }
    return 0;
}
g++ -std=c++17 -o pong pong.cpp
./pong

本代码面向 Linux / macOS 的 g++;Windows 用户可换成 <conio.h>_kbhit() / _getch(),其余不变。

💡 改进方向:① 接球位置决定反弹角度,更有手感;② 每得 5 分加速;③ 加第二个挡板双人对战;④ 用 std::vector 记录最高分并存入文件。