框架在不同时间绘画? [英] Frames painting at different times?

查看:139
本文介绍了框架在不同时间绘画?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的游戏中有一个非常烦人的错误,帧的底部似乎比帧的顶部更早渲染,我不确定它为什么会发生。



我正在使用一个JPanel来重绘每个游戏循环,我的游戏循环设置为60FPS。
在绘画功能的开始,它将玩家X和Y设置为变量,然后用于绘制每个元素,(因为它们是相对于玩家绘制的,因为相机跟随玩家) / p>

如果需要,我可以发布任何代码,以帮助诊断问题,但代码太多,我不知道问题的哪一部分;因此,我主要是在询问是否有人对我的解释有什么不妥之处。



我无法发布问题的视频,因为它没有选择视频,但你可以自由地在游戏中看到它,链接到游戏和病毒扫描此处



如果你下载游戏,那么当你打开它时,输入一个名字(或保留默认值),当它询问服务器时点击否。当您使用WASD移动时,您应该在屏幕上的某处看到水平线闪烁效果。如果游戏没有打开,请再试一次,它很可能无法打开(这是一个已知的错误,我计划很快修复它)



<对不好的解释感到抱歉,我发现很难描述我的问题。我已经坚持了几个小时,甚至在搜索互联网之后也找不到解决方案。



编辑:整个源代码:这里



EDIT2:它需要kryonet lib,位于此处



EDIT3: Github

解决方案

这是两个基本原则的演示,但基本上是一系列缓冲,旨在减少 paintComponent 所做的工作量...



一般来说,更快BLIT 图形卡上的图像然后是绘制像素,考虑到这一点,这个例子做了两件事......



首先,它预渲染背景地图。这个例子只是在运行时随机生成地图,但创建的地图大约是全高清的4倍。



其次,它雇用了它自己的双缓冲。 view有两个缓冲区,一个活动和一个更新活动缓冲区被绘制到屏幕上, update 缓冲区是引擎来渲染输出的当前状态...



这很重要,因为视图的缓冲区总是与视图,所以你永远不会渲染任何不出现在屏幕外的东西。



这个例子将其他内容(如动画,特效)的渲染推送到引擎 ...



我在30英寸显示器上以2560x1600的速度运行此示例,但运动很少delta非常小,所以我可以更快地平移,使它变大会使这些问题无效...

  import java.awt。 Dimension; 
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io .IOException;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

公共类TestRender {

public static void main(String [] args){
new TestRender();
}

public TestRender(){
EventQueue.invokeLater(new Runnable(){
@Override
public void run(){
尝试{
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}赶上(ClassNotFoundException的| InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException前){
ex.printStackTrace();
}

JFrame frame = new JFrame(Testing);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}

公共接口查看{

public BufferedImage switchBuffers();
public int getWidth();
public int getHeight();

}

public enum KeyState {
UP,DOWN,LEFT,RIGHT;
}

公共类TestPane扩展JPanel实现View {

私有引擎引擎;

私有BufferedImage活跃;
private BufferedImage更新;

private ReentrantLock lckBuffer;

public TestPane(){
lckBuffer = new ReentrantLock();
initBuffers();
engine = new Engine(this);
engine.gameStart();

InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP,0,false),up_pressed);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,0,false),down_pressed);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0,false),left_pressed);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT,0,false),right_pressed);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP,0,true),up_released);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,0,true),down_released);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0,true),left_released);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT,0,true),right_released);

ActionMap am = getActionMap();
am.put(up_pressed,new AddState(engine,KeyState.UP));
am.put(up_released,new RemoveState(engine,KeyState.UP));
am.put(down_pressed,new AddState(engine,KeyState.DOWN));
am.put(down_released,new RemoveState(engine,KeyState.DOWN));
am.put(left_pressed,new AddState(engine,KeyState.LEFT));
am.put(left_released,new RemoveState(engine,KeyState.LEFT));
am.put(right_pressed,new AddState(engine,KeyState.RIGHT));
am.put(right_released,new RemoveState(engine,KeyState.RIGHT));
}

protected void initBuffers(){
if(getWidth()> 0&& getHeight()> 0){
try {
lckBuffer.lock();
active = new BufferedImage(getWidth(),getHeight(),BufferedImage.TYPE_INT_ARGB);
update = new BufferedImage(getWidth(),getHeight(),BufferedImage.TYPE_INT_ARGB);
} finally {
lckBuffer.unlock();
}
}
}

@Override
public void invalidate(){
super.invalidate();
initBuffers();
}

@Override
public Dimension getPreferredSize(){
return new Dimension(1920,1080);
}

@Override
protected void paintComponent(Graphics g){
super.paintComponent(g);
Graphics2D g2d =(Graphics2D)g.create();
try {
lckBuffer.lock();
if(active!= null){
g2d.drawImage(active,0,0,this);
}
} finally {
lckBuffer.unlock();
}
g2d.dispose();
}

@Override
public BufferedImage switchBuffers(){
try {
lckBuffer.lock();
BufferedImage tmp = active;
active = update;
update = tmp;
repaint();
} finally {
lckBuffer.unlock();
}
返回更新;
}

}

公共静态类引擎{

public static final int MAP_WIDTH = 15 * 4;
public static final int MAP_HEIGHT = 9 * 4;
public static final int X_DELTA = 32;
public static final int Y_DELTA = 32;

//这个值可能存储在别处。
public static final double GAME_HERTZ = 60.0;
//计算我们的目标游戏赫兹每帧需要多少ns。
public static final double TIME_BETWEEN_UPDATES = 1000000000 / GAME_HERTZ;
//我们需要上次更新时间。
static double lastUpdateTime = System.nanoTime();
//存储我们上次渲染的时间。
static double lastRenderTime = System.nanoTime();

//如果我们能够获得与此FPS一样高的值,请不要再渲染。
final static double TARGET_FPS = GAME_HERTZ;
final static double TARGET_TIME_BETWEEN_RENDERS = 1000000000 / TARGET_FPS;

//查找FPS的简单方法。
static int lastSecondTime =(int)(lastUpdateTime / 1000000000);

public static int fps = 60;
public static int frameCount = 0;

private boolean isGameFinished;

私人BufferedImage地图;
private BufferedImage tiles [];

私人查看视图;

private int camX,camY;
private Set< KeyState> keyStates;

public Engine(View bufferRenderer){
keyStates = new HashSet<>(4);
this.view = bufferRenderer;
tiles = new BufferedImage [7];
Random rnd = new Random();
map = new BufferedImage(MAP_WIDTH * 128,MAP_HEIGHT * 128,BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = map.createGraphics();
for(int row = 0; row< MAP_HEIGHT; row ++){
for(int col = 0; col< MAP_WIDTH; col ++){
int tile = rnd.nextInt(7 );
int x = col * 128;
int y = row * 128;
g2d.drawImage(getTile(tile),x,y,null);
}
}
g2d.dispose();
}

protected BufferedImage getTile(int tile){
BufferedImage img = tiles [tile];
if(img == null){
try {
img = ImageIO.read(getClass()。getResource(/+ tile +。png));
img = img.getSubimage(0,64,128,128);
} catch(IOException ex){
ex.printStackTrace();
}
tiles [tile] = img;
}
返回img;
}

public void gameStart(){

Thread gameThread = new Thread(){
//覆盖run()以提供运行行为这个帖子。
@Override
public void run(){
gameLoop();
}
};
gameThread.setDaemon(false);
//启动帖子。 start()调用run(),然后调用gameLoop()。
gameThread.start();
}

public void gameLoop(){
BufferedImage buffer = view.switchBuffers(); //初始缓冲区...
while(!isGameFinished){
double now = System.nanoTime();
lastUpdateTime + = TIME_BETWEEN_UPDATES;
gameUpdate(buffer);
renderBuffer(buffer);
buffer = view.switchBuffers(); //将缓冲区推回
frameCount ++;
lastRenderTime = now;

int thisSecond =(int)(lastUpdateTime / 1000000000);
if(thisSecond> lastSecondTime){
fps = frameCount;
frameCount = 0;
lastSecondTime = thisSecond;
}

//收益率至少达到渲染之间的目标时间。这样可以避免CPU占用。
while(现在 - lastRenderTime< TARGET_TIME_BETWEEN_RENDERS&& now - lastUpdateTime< TIME_BETWEEN_UPDATES){
//Thread.yield();

//这会阻止应用程序占用您的所有CPU。它使这个稍微不准确,但值得。
//您可以删除此行,它仍然可以工作(更好),您的CPU只会爬上某些操作系统。
//对某些操作系统来说,这可能导致非常糟糕的口吃。向下滚动,看看不同人的解决方案。
try {
Thread.sleep(1);
} catch(例外e){
}

now = System.nanoTime();
}
}
}

protected void renderBuffer(BufferedImage buffer){
if(buffer!= null){
Graphics2D g2d = buffer.createGraphics();
g2d.drawImage(map,camX,camY,null);
g2d.dispose();
}
}

protected void gameUpdate(BufferedImage buffer){
//在这里渲染瞬态效果
if(keyStates.contains(KeyState.DOWN) ){
camY - = Y_DELTA;
} else if(keyStates.contains(KeyState.UP)){
camY + = Y_DELTA;
}
if(camY< - (map.getHeight() - view.getHeight())){
camY = - (map.getHeight() - view.getHeight()) ;
}否则if(camY> 0){
camY = 0;
}
if(keyStates.contains(KeyState.RIGHT)){
camX - = Y_DELTA;
} else if(keyStates.contains(KeyState.LEFT)){
camX + = Y_DELTA;
}
if(camX< - (map.getWidth() - view.getWidth())){
camX = - (map.getWidth() - view.getWidth()) ;
}否则if(camX> 0){
camX = 0;
}
}

public void addKeyState(KeyState state){
keyStates.add(state);
}

public void removeKeyState(KeyState state){
keyStates.remove(state);
}

}

公共类AddState扩展AbstractAction {

私有引擎引擎;
私有KeyState状态;

public AddState(Engine engine,KeyState state){
this.engine = engine;
this.state = state;
}

@Override
public void actionPerformed(ActionEvent e){
engine.addKeyState(state);
}

}

公共类RemoveState扩展AbstractAction {

私有引擎引擎;
私有KeyState状态;

public RemoveState(引擎引擎,KeyState状态){
this.engine = engine;
this.state = state;
}

@Override
public void actionPerformed(ActionEvent e){
engine.removeKeyState(state);
}

}

}



<在我的实验过程中,我注意到,如果你试图将内容超出缓冲区的范围(即允许地图的顶部在缓冲区内滑落),你会得到令人讨厌的油漆效果,所以要注意你总是在缓冲区的可视区域内渲染...



可能还有其他需要整理的区域,但这证明了基础...


I have a really annoying bug in my game, the bottom of the frame seems to render earlier than the top of the frame, I'm not sure why it's happening.

I am using a JPanel which does repaint every game loop, my game loop is set to 60FPS. At the start of the paint function, it sets the player X and Y to a variable, which is then used to paint each element, (Since they are painted relative to the player, since the camera follows the player)

I can post any code if needed, to help diagnose the problem, but there is too much code, and I don't know which part of it is the problem; So I am mainly asking if anyone has any idea of what could be wrong, from my explanation.

I cannot post a video of the problem, since it doesn't pick up on video, however feel free to see it yourself in the game, link to game, and virus scan here

If you download the game, then when you open it, enter a name (or leave default) and click no when it asks about a server. When you move around with WASD, you should see a horizontal line flickering effect somewhere on the screen. If the game doesn't open, try again, there is a small chance of it being unable to open (This is a known bug, and im planning to fix it soon)

Sorry for the bad explanation, I find it difficult to describe my problem. I have been stuck with this for hours, and cannot find a solution even after searching the internet for one.

EDIT: Entire Source Code: Here

EDIT2: It requires the kryonet lib, located here

EDIT3: Github

解决方案

The is a demonstration of two basic principles, but is basically a series of buffers designed to reduce the amount of work that the paintComponent does...

Generally speaking, it is faster to BLIT a image onto the graphics card then it is to "paint" pixels, with this in mind, this example does two things...

Firstly, it pre-renders the background map. This example just randomly generates the map when it's run, but creates a map which is about 4 times that of full HD.

Secondly, it employees it's own double buffering. The "view" has two buffers, an active and an update. The active buffer is what gets painted to the screen, the update buffer is what's used by the Engine to render the current state of the output...

This is important, because the view's buffers are ALWAYS the same size of the view, so you are never rendering anything that doesn't appear off the screen.

This example pushes the rendering of additional content (like animation, special effects) to the Engine...

I had this example running on my 30" monitor at 2560x1600 with very little issues, the movement delta is very small so I can pan faster, making it large would have nullified these issues...

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class TestRender {

    public static void main(String[] args) {
        new TestRender();
    }

    public TestRender() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public interface View {

        public BufferedImage switchBuffers();
        public int getWidth();
        public int getHeight();

    }

    public enum KeyState {
        UP, DOWN, LEFT, RIGHT;
    }

    public class TestPane extends JPanel implements View {

        private Engine engine;

        private BufferedImage active;
        private BufferedImage update;

        private ReentrantLock lckBuffer;

        public TestPane() {
            lckBuffer = new ReentrantLock();
            initBuffers();
            engine = new Engine(this);
            engine.gameStart();

            InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "up_pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "down_pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "left_pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "right_pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "up_released");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "down_released");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "left_released");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "right_released");

            ActionMap am = getActionMap();
            am.put("up_pressed", new AddState(engine, KeyState.UP));
            am.put("up_released", new RemoveState(engine, KeyState.UP));
            am.put("down_pressed", new AddState(engine, KeyState.DOWN));
            am.put("down_released", new RemoveState(engine, KeyState.DOWN));
            am.put("left_pressed", new AddState(engine, KeyState.LEFT));
            am.put("left_released", new RemoveState(engine, KeyState.LEFT));
            am.put("right_pressed", new AddState(engine, KeyState.RIGHT));
            am.put("right_released", new RemoveState(engine, KeyState.RIGHT));
        }

        protected void initBuffers() {
            if (getWidth() > 0 && getHeight() > 0) {
                try {
                    lckBuffer.lock();
                    active = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
                    update = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
                } finally {
                    lckBuffer.unlock();
                }
            }
        }

        @Override
        public void invalidate() {
            super.invalidate();
            initBuffers();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(1920, 1080);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            try {
                lckBuffer.lock();
                if (active != null) {
                    g2d.drawImage(active, 0, 0, this);
                }
            } finally {
                lckBuffer.unlock();
            }
            g2d.dispose();
        }

        @Override
        public BufferedImage switchBuffers() {
            try {
                lckBuffer.lock();
                BufferedImage tmp = active;
                active = update;
                update = tmp;
                repaint();
            } finally {
                lckBuffer.unlock();
            }
            return update;
        }

    }

    public static class Engine {

        public static final int MAP_WIDTH = 15 * 4;
        public static final int MAP_HEIGHT = 9 * 4;
        public static final int X_DELTA = 32;
        public static final int Y_DELTA = 32;

        //This value would probably be stored elsewhere.
        public static final double GAME_HERTZ = 60.0;
        //Calculate how many ns each frame should take for our target game hertz.
        public static final double TIME_BETWEEN_UPDATES = 1000000000 / GAME_HERTZ;
        //We will need the last update time.
        static double lastUpdateTime = System.nanoTime();
        //Store the last time we rendered.
        static double lastRenderTime = System.nanoTime();

        //If we are able to get as high as this FPS, don't render again.
        final static double TARGET_FPS = GAME_HERTZ;
        final static double TARGET_TIME_BETWEEN_RENDERS = 1000000000 / TARGET_FPS;

        //Simple way of finding FPS.
        static int lastSecondTime = (int) (lastUpdateTime / 1000000000);

        public static int fps = 60;
        public static int frameCount = 0;

        private boolean isGameFinished;

        private BufferedImage map;
        private BufferedImage tiles[];

        private View view;

        private int camX, camY;
        private Set<KeyState> keyStates;

        public Engine(View bufferRenderer) {
            keyStates = new HashSet<>(4);
            this.view = bufferRenderer;
            tiles = new BufferedImage[7];
            Random rnd = new Random();
            map = new BufferedImage(MAP_WIDTH * 128, MAP_HEIGHT * 128, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g2d = map.createGraphics();
            for (int row = 0; row < MAP_HEIGHT; row++) {
                for (int col = 0; col < MAP_WIDTH; col++) {
                    int tile = rnd.nextInt(7);
                    int x = col * 128;
                    int y = row * 128;
                    g2d.drawImage(getTile(tile), x, y, null);
                }
            }
            g2d.dispose();
        }

        protected BufferedImage getTile(int tile) {
            BufferedImage img = tiles[tile];
            if (img == null) {
                try {
                    img = ImageIO.read(getClass().getResource("/" + tile + ".png"));
                    img = img.getSubimage(0, 64, 128, 128);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                tiles[tile] = img;
            }
            return img;
        }

        public void gameStart() {

            Thread gameThread = new Thread() {
                // Override run() to provide the running behavior of this thread.
                @Override
                public void run() {
                    gameLoop();
                }
            };
            gameThread.setDaemon(false);
            // Start the thread. start() calls run(), which in turn calls gameLoop().
            gameThread.start();
        }

        public void gameLoop() {
            BufferedImage buffer = view.switchBuffers(); // initial buffer...
            while (!isGameFinished) {
                double now = System.nanoTime();
                lastUpdateTime += TIME_BETWEEN_UPDATES;
                gameUpdate(buffer);
                renderBuffer(buffer);
                buffer = view.switchBuffers(); // Push the buffer back
                frameCount++;
                lastRenderTime = now;

                int thisSecond = (int) (lastUpdateTime / 1000000000);
                if (thisSecond > lastSecondTime) {
                    fps = frameCount;
                    frameCount = 0;
                    lastSecondTime = thisSecond;
                }

                //Yield until it has been at least the target time between renders. This saves the CPU from hogging.
                while (now - lastRenderTime < TARGET_TIME_BETWEEN_RENDERS && now - lastUpdateTime < TIME_BETWEEN_UPDATES) {
                //Thread.yield();

                    //This stops the app from consuming all your CPU. It makes this slightly less accurate, but is worth it.
                    //You can remove this line and it will still work (better), your CPU just climbs on certain OSes.
                    //FYI on some OS's this can cause pretty bad stuttering. Scroll down and have a look at different peoples' solutions to this.
                    try {
                        Thread.sleep(1);
                    } catch (Exception e) {
                    }

                    now = System.nanoTime();
                }
            }
        }

        protected void renderBuffer(BufferedImage buffer) {
            if (buffer != null) {
                Graphics2D g2d = buffer.createGraphics();
                g2d.drawImage(map, camX, camY, null);
                g2d.dispose();
            }
        }

        protected void gameUpdate(BufferedImage buffer) {
            // render transient effects here
            if (keyStates.contains(KeyState.DOWN)) {
                camY -= Y_DELTA;
            } else if (keyStates.contains(KeyState.UP)) {
                camY += Y_DELTA;
            }
            if (camY < -(map.getHeight() - view.getHeight())) {
                camY = -(map.getHeight() - view.getHeight());
            } else if (camY > 0) {
                camY = 0;
            }
            if (keyStates.contains(KeyState.RIGHT)) {
                camX -= Y_DELTA;
            } else if (keyStates.contains(KeyState.LEFT)) {
                camX += Y_DELTA;
            }
            if (camX < -(map.getWidth() - view.getWidth())) {
                camX = -(map.getWidth() - view.getWidth());
            } else if (camX > 0) {
                camX = 0;
            }
        }

        public void addKeyState(KeyState state) {
            keyStates.add(state);
        }

        public void removeKeyState(KeyState state) {
            keyStates.remove(state);
        }

    }

    public class AddState extends AbstractAction {

        private Engine engine;
        private KeyState state;

        public AddState(Engine engine, KeyState state) {
            this.engine = engine;
            this.state = state;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            engine.addKeyState(state);
        }

    }

    public class RemoveState extends AbstractAction {

        private Engine engine;
        private KeyState state;

        public RemoveState(Engine engine, KeyState state) {
            this.engine = engine;
            this.state = state;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            engine.removeKeyState(state);
        }

    }

}

During my experimentation, I did notice that, if you tried to render the content "beyond" the range of the buffer (ie allow the top of the map to slip down inside the buffer area), you would get nasty paint effects, so beware that you are always rendering within the viewable area of the buffer...

There are probably other areas that need tidying up, but this demonstrates the basics...

这篇关于框架在不同时间绘画?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆