想象你正在制作一个2D游戏,角色可以在一个大世界里走动,但你的屏幕只能显示世界的一部分。你需要让背景随着角色移动,但当角色走到世界边缘时,背景不能再滚动,而且在屏幕中间区域走动时,背景也不该移动。
核心思路
-
世界比屏幕大:背景图片(世界)比游戏窗口大得多
-
视口概念:屏幕就像一扇窗户,只显示世界的一部分
-
角色带动视口:角色靠近屏幕边缘时,背景才开始移动
-
边界限制:背景不能滚出世界边界
图片示意
1.一开始
2.“死区”内移动时
3.背景中间区域,可显示屏幕的边缘
4.背景的边缘(实操中随着不同游戏有一定不同处理,如反向出现,死亡判定,掉落触发动画,根本不允许走到此位置等等)
代码片段
1. 游戏基本设置与视口初始化
// 游戏窗口尺寸
private static final int SCREEN_WIDTH = 800;
private static final int SCREEN_HEIGHT = 600;
// 背景尺寸(比屏幕大)
private final int BG_WIDTH = 1600;
private final int BG_HEIGHT = 1200;
// 角色世界坐标(从世界中心开始)
private int playerX = BG_WIDTH / 2;
private int playerY = BG_HEIGHT / 2;
// 视口位置(决定显示世界的哪一部分)
private int viewportX;
private int viewportY;
// 初始化视口位置(使角色在屏幕中央开始)
private void centerViewportOnPlayer() {
viewportX = playerX - SCREEN_WIDTH / 2; // 计算视口X位置
viewportY = playerY - SCREEN_HEIGHT / 2; // 计算视口Y位置
// 确保视口不超出背景边界(核心保护机制)
viewportX = Math.max(0, Math.min(viewportX, BG_WIDTH - SCREEN_WIDTH));
viewportY = Math.max(0, Math.min(viewportY, BG_HEIGHT - SCREEN_HEIGHT));
}
2. 滚动触发区域设置
// 定义滚动触发区域(屏幕边缘区域)
private final int SCROLL_ZONE_LEFT = 150; // 左侧触发区宽度
private final int SCROLL_ZONE_RIGHT = 150; // 右侧触发区宽度
private final int SCROLL_ZONE_TOP = 100; // 顶部触发区高度
private final int SCROLL_ZONE_BOTTOM = 100; // 底部触发区高度
// 滚动速度系数(控制滚动平滑度)
private final float SCROLL_SPEED = 0.5f;
// 在绘制方法中可视化滚动区域(调试用)
g2d.setColor(new Color(255, 68, 0, 50)); // 半透明橙色
g2d.fillRect(0, 0, SCROLL_ZONE_LEFT, SCREEN_HEIGHT); // 左侧区域
g2d.fillRect(SCREEN_WIDTH - SCROLL_ZONE_RIGHT, 0, SCROLL_ZONE_RIGHT, SCREEN_HEIGHT); // 右侧区域
// ... 其他区域类似
3. 键盘控制角色移动
// 键盘事件监听器
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int step = 5; // 每次按键移动距离
// WASD控制角色在世界中的位置
switch (e.getKeyCode()) {
case KeyEvent.VK_W -> playerY -= step; // 上移
case KeyEvent.VK_S -> playerY += step; // 下移
case KeyEvent.VK_A -> playerX -= step; // 左移
case KeyEvent.VK_D -> playerX += step; // 右移
}
// 角色移动后更新视口
updateViewport();
repaint(); // 请求重绘画面
}
});
4. 视口更新核心逻辑(最关键部分)
private void updateViewport() {
// 计算角色在屏幕上的位置(世界坐标 → 屏幕坐标)
int playerScreenX = playerX - viewportX;
int playerScreenY = playerY - viewportY;
int scrollDX = 0; // 水平滚动量
int scrollDY = 0; // 垂直滚动量
// 水平滚动检测
if (playerScreenX < SCROLL_ZONE_LEFT) {
// 角色进入左侧区域:背景向右滚动(视口左移)
scrollDX = (int)((playerScreenX - SCROLL_ZONE_LEFT) * SCROLL_SPEED);
} else if (playerScreenX > SCREEN_WIDTH - SCROLL_ZONE_RIGHT) {
// 角色进入右侧区域:背景向左滚动(视口右移)
scrollDX = (int)((playerScreenX - (SCREEN_WIDTH - SCROLL_ZONE_RIGHT)) * SCROLL_SPEED);
}
// 垂直滚动检测(原理同上)
if (playerScreenY < SCROLL_ZONE_TOP) {
scrollDY = (int)((playerScreenY - SCROLL_ZONE_TOP) * SCROLL_SPEED);
} else if (playerScreenY > SCREEN_HEIGHT - SCROLL_ZONE_BOTTOM) {
scrollDY = (int)((playerScreenY - (SCREEN_HEIGHT - SCROLL_ZONE_BOTTOM)) * SCROLL_SPEED);
}
// 应用滚动量
viewportX += scrollDX;
viewportY += scrollDY;
// 边界约束(防止视口移出背景)
viewportX = Math.max(0, Math.min(viewportX, BG_WIDTH - SCREEN_WIDTH));
viewportY = Math.max(0, Math.min(viewportY, BG_HEIGHT - SCREEN_HEIGHT));
}
5. 背景绘制优化
// 只绘制视口可见部分(性能关键!)
g2d.drawImage(background,
0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, // 在屏幕上的绘制区域
viewportX, viewportY, // 背景源图像起点(世界坐标)
viewportX + SCREEN_WIDTH, // 背景源图像终点X
viewportY + SCREEN_HEIGHT, // 背景源图像终点Y
null);
6. 角色绘制与坐标转换
// 将角色世界坐标转换为屏幕坐标
int playerScreenX = playerX - viewportX;
int playerScreenY = playerY - viewportY;
// 绘制角色(红色方块)
g2d.setColor(PLAYER_COLOR);
g2d.fillRect(
playerScreenX - PLAYER_SIZE/2, // 屏幕X位置(居中)
playerScreenY - PLAYER_SIZE/2, // 屏幕Y位置(居中)
PLAYER_SIZE, PLAYER_SIZE);
// 调试信息(显示坐标)
g2d.drawString("角色世界坐标: (" + playerX + ", " + playerY + ")", 10, 20);
g2d.drawString("视口位置: (" + viewportX + ", " + viewportY + ")", 10, 40);
7. 游戏启动入口
public static void main(String[] args) {
// 使用SwingUtilities确保线程安全
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("滚动背景游戏");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new ScrollBackgroundGame()); // 添加游戏面板
frame.pack(); // 自动调整窗口大小
frame.setLocationRelativeTo(null); // 屏幕居中
frame.setVisible(true); // 显示窗口
});
}