You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何仅绘制一次瓦片地图而非每帧重绘以优化Java游戏性能

如何仅绘制一次瓦片地图而非每帧重绘以优化Java游戏性能

嘿,我完全理解你的困扰——每帧都遍历所有瓦片重新绘制整个地图,确实会把游戏帧率拖得很慢,尤其是当地图规模不小的时候。既然你想要只绘制一次地图,之后只在上面更新动态元素(比如玩家),那用**缓冲图像(BufferedImage)**来预先渲染地图就是最适合的方案啦!

先看看你遇到的问题场景:

My game runs at seconds per frame when I draw my tilemap to gamepanel, as it does it every frame and just clogs up. I'll include some code of the tile, map and panel classes. Hopefully, someone can help.

What I would like is for a way to create the map and display it once, with everything else updating on top of it. I am new to Java and came from writing games in pygame (which is odd as I thought I'd have more issues like this with Python due to GIL)

你的原始代码

GamePanel类

package main;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;

import javax.swing.JPanel;

import entity.Player;

import tile.TileMap;

public class GamePanel extends JPanel implements Runnable {
    // Screen Settings
    public final int tileSize = 25;
    public final int maxScreenColumn = 26;
    public final int maxScreenRow = 18;

    final int screenWidth = tileSize * maxScreenColumn;
    final int screenHeight = tileSize * maxScreenRow;

    final int FPS = 60;

    Thread gameThread;
    
    KeyHandler keyH = new KeyHandler();

    public int playerX = tileSize * 13;
    public int playerY = tileSize * 10;
    public int playerSpeed = 3;

    TileMap tileM = new TileMap(this);

    Player player = new Player(this, keyH);

    //  set player's default pos

    public GamePanel() {
        this.setPreferredSize(new Dimension(screenWidth, screenHeight));
        this.setBackground(Color.black);
        this.setDoubleBuffered(true);
        this.addKeyListener(keyH);
        this.setFocusable(true);
    }

    public void startGameThread() {
        gameThread = new Thread(this);
        gameThread.start();
    }

    public void run() {
        double drawInterval = 1000000000/FPS;
        double nextDrawTime = System.nanoTime() + drawInterval;

        while (gameThread != null) {
            //  1. Update info such as character pos etc...
            update();
            //  2. Draw to the gamePanel object...
            repaint();

            try {
                double remainingTime = nextDrawTime - System.nanoTime();

                if (remainingTime < 0) {
                    remainingTime = 0;
                }

                Thread.sleep((long)remainingTime / 1000000);

                nextDrawTime += drawInterval;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void update() {
        player.update();
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);

        Graphics2D g2 = (Graphics2D)g;
        
        tileM.drawTileMap(g2);
        
        //  tile.draw(g2);

        player.draw(g2);

        g2.dispose();
    }
}

TileMap类

package tile;

import main.GamePanel;
import java.awt.Graphics2D;

public class TileMap {

    GamePanel gp;

    public String tileMap[][] = 
        {   
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" }
        };

    public TileMap(GamePanel gp) {
        this.gp = gp;
    }

    public void drawTileMap (Graphics2D g2) {
        for (int col = 0; col < gp.maxScreenColumn-1; col++) {
            for (int row = 0; row < gp.maxScreenRow-1; row++) {
                if (tileMap[row][col].equalsIgnoreCase("G")) {
                    // 这里应该是你绘制瓦片的逻辑,比如绘制草地图片
                }
            }
        }
    }
}

解决方案:用缓冲图像预渲染地图

核心思路就是把整个瓦片地图一次性绘制到一张BufferedImage里,之后每帧只需要把这张图片画到面板上,不用再遍历所有瓦片。这样能大幅减少每帧的绘制工作量,提升帧率。

修改后的TileMap类

package tile;

import main.GamePanel;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;

public class TileMap {

    GamePanel gp;
    private BufferedImage mapBuffer; // 用来存储预渲染好的地图

    public String tileMap[][] = 
        {   
            // 你的瓦片数组内容保持不变
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" },
            // ... 其他行省略
            {"G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G" }
        };

    public TileMap(GamePanel gp) {
        this.gp = gp;
        initMapBuffer(); // 初始化时就预渲染地图
    }

    // 预渲染地图到缓冲图像
    private void initMapBuffer() {
        // 创建和游戏面板尺寸一致的缓冲图像
        mapBuffer = new BufferedImage(gp.screenWidth, gp.screenHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2 = mapBuffer.createGraphics();

        // 一次性绘制所有瓦片到缓冲图
        for (int col = 0; col < gp.maxScreenColumn-1; col++) {
            for (int row = 0; row < gp.maxScreenRow-1; row++) {
                if (tileMap[row][col].equalsIgnoreCase("G")) {
                    // 替换成你实际的瓦片绘制逻辑,比如绘制草地图片
                    // 示例:g2.drawImage(grassTileImage, col * gp.tileSize, row * gp.tileSize, gp.tileSize, gp.tileSize, null);
                    // 这里用黑色填充代替,你根据自己的资源修改
                    g2.fillRect(col * gp.tileSize, row * gp.tileSize, gp.tileSize, gp.tileSize);
                }
                // 如果有其他瓦片类型(比如石头、水),在这里添加对应的绘制逻辑
            }
        }
        g2.dispose(); // 释放图形资源
    }

    // 修改绘制方法,现在只需要画预渲染好的缓冲图
    public void drawTileMap (Graphics2D g2) {
        g2.drawImage(mapBuffer, 0, 0, null);
    }

    // 可选:如果地图需要更新(比如瓦片被破坏),调用这个方法刷新缓冲图
    public void refreshMap() {
        initMapBuffer();
    }
}

GamePanel类无需改动

你的GamePanel里的paintComponent方法完全不用改,因为现在tileM.drawTileMap(g2)只是绘制一张已经做好的图片,速度极快,不会再拖慢帧率了!


额外小提示

  1. 地图更新处理:如果游戏里有需要动态修改地图的场景(比如挖掉一块草地),只需要修改tileMap数组后,调用tileM.refreshMap()重新生成缓冲图即可,不用每帧都重绘。
  2. 保持双缓冲:你已经开启了setDoubleBuffered(true),这个设置能有效减少画面闪烁,一定要保留哦!

这样修改后,地图只会在初始化时绘制一次,之后每帧只需要快速绘制这张缓冲图,帧率应该会立刻提升,再也不会被瓦片绘制拖慢啦!

备注:内容来源于stack exchange,提问作者Bobby Diditall

火山引擎 最新活动