简体   繁体   中英

How do you use re-size all Graphic2D

In java how can you make a game fully realizable! But so logic and graphics can work with it? I have tried using SCALE methods. But this doesn't allow perfect full-screen for every computer. So I made this:

    public void resize(int WIDTH, int HEIGHT, boolean UNDECORATED) {

          frame.setPreferredSize(new Dimension(WIDTH, HEIGHT));
          frame.setMaximumSize(new Dimension(WIDTH, HEIGHT));
          frame.setMinimumSize(new Dimension(WIDTH, HEIGHT));

          this.WIDTH = WIDTH;
          this.HEIGHT = HEIGHT;
          frame.setUndecorated(UNDECORATED);
          frame.setSize(WIDTH, HEIGHT);
      }

So you can set your screen size to whatever you want! It works but the graphics will not work with it? Is there a way in Graphics2D to stretch all the graphics so it fits? For example if there was a method that existed like:

            G2D.resize(WIDTH, HEIGHT, Image.NEAREST_PARENT_RESCALE);

Any idea?

Things I have tried:

  • Drawing all graphics to a Buffered-image then drawing that Image onto the screen size.
  • Just using SCALE and doing WIDTH * SCALE etc.
  • Lots of math

Things I do not mind

  • If you have a WIDE-SCREEN it stretches graphic2D objects to the size.
  • If you have a SQUARE-SCREEN it squishes graphics2D objects to the size.

So how can I make a perfectly resealable game using Graphics2D, JFrame.

In the most generic form, one can consider this as a classical problem of graphics programming, namely, as the transformation from world coordinates to screen coordinates . You have an object that has a size of "1.0 x 1.0" in your world coordinate system (regardless of which unit this has). And this object should be painted so that it has a size of, for example, "600 pixels * 600 pixels" on the screen.

Broadly speaking, there are at least three options to achieve this in Swing:

  • You can draw into an image, and then draw a scaled version of the image
  • You can draw into a scaled Graphics2D object
  • You can draw scaled objects

Each of this has possible advantages and disadvantages, and hidden caveats.

Drawing into an image, and drawing a scaled version of the image:

This might look like a simple solution, but has a potential drawback: The image itself has a certain resolution (size). If the image is too small, and you are scaling it up to fill the screen, it may appear blocky. If the image is too large, and you are scaling it down to fit into the screen, pixels of the image may be lost.

In both cases, there are several tuning parameters for the process of scaling the image. In fact, scaling an image is far more tricky than it looks at the first glance. For details, one may refer to the article The Perils of Image.getScaledInstance() by Chris Campbell.

Drawing into a scaled Graphics2D object

The Graphics2D class already offers the full functionality that is necessary to create the transformation between the world coordinate system and the screen coordinate system . This is accomplished by the Graphics2D class by internally storing an AffineTransform , which describes this transformation. This AffineTransform may be modified directly via the Graphics2D object:

void paintSomething(Graphics2D g) {
    ...
    g.draw(someShape);

    // Everything that is painted after this line will
    // be painted 3 times as large: 
    g.scale(3.0, 3.0);

    g.draw(someShape); // Will be drawn larger 
}

Some care has to be taken to properly manage the transform that is stored in the Graphics2D object. In general, one should create a backup of the original AffineTransform before applying additional transformations, and restore this original transform afterwards:

// Create a backup of the original transform
AffineTransform oldAT = g.getTransform();

// Apply some transformations
g.scale(3.0, 4.0);
g.translate(10.0, 20.0);

// Do custom painting the the transformed graphics
paintSomething(g):

// Restore the original transformation
g.setTransform(oldAT);

(Another advice for the last method: The Graphics2D#setTransform method should never be used to apply a new coordinate transform on top of an existing transform. It is solely intended for restoring an "old" transform, as shown in this example (and in the documentation of this method)).

One potential drawback of scaling with the Graphics2D class is that afterwards, everything will be scaled. Particularly, this scaling will also affect line widths (that is, the width of the Stroke ). For example, consider a sequence of calls like this one:

// By default, this will paint a line with a width (stroke) of 1.0:
g.draw(someLine);

// Apply some scaling...
g.scale(10.0, 10.0);

// Now, this will paint the same line, but with a width of 10. 
g.draw(someLine);

The second call will cause a line to be drawn that is 10 pixels wide. This may not be desired in many cases. This effect can be avoided with the third alternative:

Drawing scaled objects

The transformation between the world coordinate system and the screen coordinate system can also be maintained manually. It is convenient to represent this as an AffineTransform . The AffineTransform class can be used to create transformed versions of Shape object, that can then be drawn directly into an ( un -transformed) Graphics2D object. This is accomplished with the AffineTransform#createTransformedShape method:

void paintSomething(Graphics2D g) {
    ...
    // Draw some shape in its normal size
    g.draw(someShape);

    // Create a scaling transform
    AffineTransform at = AffineTransform.getScaleInstance(3.0, 3.0);

    // Create a scaled version of the shape
    Shape transformedShape = at.createTransformedShape(someShape);

    // Draw the scaled shape
    g.draw(transformedShape);
}

This is probably the most versatile approach. The only potential drawback is that, when many small, simple shapes are drawn, this will cause many, small temporary transformed shapes to be created, which may cause reduced performance. (There are ways to alleviate this problem, but detailed performance considerations and optimizations are beyond the scope of this answer).


Summary

The follwing image shows the comparison of all approaches. Some example objects (represented as Shape objects) are drawn. Each row compares the three different scaling methods mentioned above. With their "default" size, the objects fill a rectangle in world coordinates that has a size of 100x100. In the first two rows, they are scaled up to fill an area on the screen of 190x190 pixels. In the last two rows, they are scaled down to fill an area on the screen of 60x60 pixels. (These sizes have been chosen in order to have some "odd" scaling factors of 1.9 and 0.6. Certain effects (artifacts) may not appear when the scaling factors are whole numbers, or exactly 0.5, for example).

For the upscaling and the downscaling, there additionally is a comparison between the "standard" way of painting, and "high quality" painting (indicated by the "(HQ)" in the title of each panel). The "high quality" here simply means that the rendering hints

KEY_ANTIALIAS = VALUE_ANTIALIAS_ON
KEY_RENDERING = VALUE_RENDER_QUALITY

have been set:

缩放方法

Here is the corresponding program, as an MCVE :

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class ScalingMethodComparison
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new GridLayout(0,1));

        Dimension larger = new Dimension(190,190);
        Dimension smaller = new Dimension(60,60);

        f.getContentPane().add(createPanel(larger, false));
        f.getContentPane().add(createPanel(larger, true));
        f.getContentPane().add(createPanel(smaller, false));
        f.getContentPane().add(createPanel(smaller, true));

        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static JPanel createPanel(Dimension d, boolean highQuality)
    {
        JPanel p = new JPanel(new GridLayout(1,3));
        for (ScalingMethodComparisonPanel.ScalingMethod scalingMethod : 
            ScalingMethodComparisonPanel.ScalingMethod.values())
        {
            p.add(createPanel(d, scalingMethod, highQuality));
        }
        return p;
    }

    private static JPanel createPanel(
        Dimension d, ScalingMethodComparisonPanel.ScalingMethod scalingMethod, 
        boolean highQuality)
    {
        JPanel p = new JPanel(new GridLayout(1,1));
        p.setBorder(BorderFactory.createTitledBorder(
            scalingMethod.toString()+(highQuality?" (HQ)":"")));
        JPanel scalingMethodComparisonPanel = 
            new ScalingMethodComparisonPanel(
                createObjects(), d, scalingMethod, highQuality);
        p.add(scalingMethodComparisonPanel);
        return p;
    }

    // Returns a list of objects that should be drawn, 
    // occupying a rectangle of 100x100 in WORLD COORDINATES
    private static List<Shape> createObjects()
    {
        List<Shape> objects = new ArrayList<Shape>();
        objects.add(new Ellipse2D.Double(10,10,80,80));
        objects.add(new Rectangle2D.Double(20,20,60,60));
        objects.add(new Line2D.Double(30,30,70,70));
        return objects;
    }
}


class ScalingMethodComparisonPanel extends JPanel
{
    private static final Color COLORS[] = {
        Color.RED, Color.GREEN, Color.BLUE,
    };

    enum ScalingMethod
    {
        SCALING_IMAGE,
        SCALING_GRAPHICS,
        SCALING_SHAPES,
    }

    private final List<Shape> objects;
    private final ScalingMethod scalingMethod;
    private final boolean highQuality;

    private final Dimension originalSize = new Dimension(100,100);
    private final Dimension scaledSize;

    private BufferedImage image;

    public ScalingMethodComparisonPanel(
        List<Shape> objects,
        Dimension scaledSize,
        ScalingMethod scalingMethod,
        boolean highQuality)
    {
        this.objects = objects;
        this.scaledSize = new Dimension(scaledSize);
        this.scalingMethod = scalingMethod;
        this.highQuality = highQuality;
    }

    @Override
    public Dimension getPreferredSize()
    {
        return new Dimension(scaledSize);
    }

    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        g.setColor(Color.WHITE);
        g.fillRect(0,0,getWidth(), getHeight());

        if (highQuality)
        {
            g.setRenderingHint( 
                RenderingHints.KEY_ANTIALIASING, 
                RenderingHints.VALUE_ANTIALIAS_ON);
            g.setRenderingHint(
                RenderingHints.KEY_RENDERING, 
                RenderingHints.VALUE_RENDER_QUALITY);
        }

        if (scalingMethod == ScalingMethod.SCALING_IMAGE)
        {
            paintByScalingImage(g);
        }
        else if (scalingMethod == ScalingMethod.SCALING_GRAPHICS)
        {
            paintByScalingGraphics(g);
        }
        else if (scalingMethod == ScalingMethod.SCALING_SHAPES)
        {
            paintByScalingShapes(g);
        }
    }

    private void paintByScalingImage(Graphics2D g)
    {
        if (image == null)
        {
            image = new BufferedImage(
                originalSize.width, originalSize.height,
                BufferedImage.TYPE_INT_ARGB);
        }
        Graphics2D ig = image.createGraphics();
        paintObjects(ig, null);
        ig.dispose();

        g.drawImage(image, 0, 0, scaledSize.width, scaledSize.height, null);
    }

    private void paintByScalingGraphics(Graphics2D g)
    {
        AffineTransform oldAT = g.getTransform();
        double scaleX = (double)scaledSize.width / originalSize.width;
        double scaleY = (double)scaledSize.height / originalSize.height;
        g.scale(scaleX, scaleY);
        paintObjects(g, null);
        g.setTransform(oldAT);
    }

    private void paintByScalingShapes(Graphics2D g)
    {
        double scaleX = (double)scaledSize.width / originalSize.width;
        double scaleY = (double)scaledSize.height / originalSize.height;
        AffineTransform at = 
            AffineTransform.getScaleInstance(scaleX, scaleY);
        paintObjects(g, at);
    }



    private void paintObjects(Graphics2D g, AffineTransform at)
    {
        for (int i=0; i<objects.size(); i++)
        {
            Shape shape = objects.get(i);
            g.setColor(COLORS[i%COLORS.length]);
            if (at == null)
            {
                g.draw(shape);
            }
            else
            {
                g.draw(at.createTransformedShape(shape));
            }
        }
    }
}

This is actually quite easy in Java. In a Graphics2d environment, the logical coordinate system (the coordinates you use in the drawing routines) and the physical coordinate system (the coordinates as they appear) on the screen are completely unrelated. Every time you draw onto a Graphics2d object, the logical coordinates are first translated to the physical coordinates by an AffineTransform object, and this AffineTransform object can be modified. For this you can use the Graphics2D.scale(double,double) , Graphics2D.rotate(double) , Graphics2D.translate(double,double) and Graphics2D.shear(double,double) methods.

So if you first call

g2d.scale(2.0,2.0);

then all your graphics that you subsequently draw will be twice as large in both directions.

If I understood you correctly all you want is to draw your graphics in different resolutions without removing or adding any content.

Well one of the "things you have tried" can do that.

Drawing to a fixed size BufferedImage will ensure that all your components are visible within that BufferedImage (assuming you draw them correctly and relative to it's fixed size) then you can just draw the image to your flexible size screen.

Here's a full runnable code example that does that:

import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;

import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class Test extends Canvas implements Runnable {

    // fixed size for the image
    private static final int WIDTH = 640;
    private static final int HEIGHT = 480;

    private BufferedImage image;
    private boolean running;
    private Thread t;

    public Test(Dimension dims) {
        super();
        setPreferredSize(dims); // actual screen size
        image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
        running = false;
    }

    public synchronized void start() {
        if (running)
            return;
        t = new Thread(this);
        running = true;
        t.start();
    }

    public synchronized void stop() {
        if (!running)
            return;
        running = false;
        boolean retry = true;
        while (retry) {
            try {
                t.join();
                retry = false;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void render() {
        // draw to your image
        Graphics2D g2d = (Graphics2D) image.getGraphics().create();
        g2d.fillRect((WIDTH / 2) - 25, (HEIGHT / 2) - 25, 50, 50);
        g2d.dispose();

        // draw the image to your screen
        BufferStrategy bs = getBufferStrategy();
        if (bs == null) {
            createBufferStrategy(3);
            return;
        }
        g2d = (Graphics2D) bs.getDrawGraphics().create();
        g2d.drawImage(image, 0, 0, getWidth(), getHeight(), null);
        g2d.dispose();
        bs.show();
    }

    public void run() {
        // approximately sync rendering to 60 FPS don't use it as it is.
        // there are much better ways to do this.
        long startTime = System.currentTimeMillis();
        long frameTime = 1000 / 60;
        long tick = 0;
        while (running) {
            while ((System.currentTimeMillis() - startTime) > tick) {
                render();
                tick += frameTime;
            }
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Test test = new Test(new Dimension(800, 600));

        JFrame frame = new JFrame("Fit to screen");
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                test.stop();
                frame.dispose();
                super.windowClosing(e);
            }
        });
        frame.getContentPane().add(test);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setResizable(false);
        frame.setVisible(true);
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                test.start();
            }
        });
    }

}

This is only a quick implementation there are things that can be done better in that code bu you get the picture. Hope this helps.

Maybe this will help :

Scaling graphics2D that contains basic shapes has a drawback : thickness of lines are doubled if the scale is doubled, that's a problem in an application implementing a zoom feature... The only way I found is to make the preferred size of the container bigger and then, draw the shapes.

Here's a zoom function using mouse wheel and the pixel of the object pointed by the mouse stays under the mouse pointer. It took me a long time to figure out how to do that properly, but I finally found out...(the application is an astrolabe and I wanted to zoom in and out)

The graphics2D belongs to a JPanel that is contained in the bottom part of a JSplitPane :

public void mouseWheelMoved(MouseWheelEvent e) {

Dimension dim = new Dimension(), oldDim = this.getPreferredSize();
double newX, newY;
Rectangle rect, oldRect;
    
if(this.mousewheel >= 0){
  this.mousewheel += -e.getWheelRotation() * this.mousewheelSensibility;
}
else {
  this.mousewheel = 0;
}

dim.setSize(this.astro.splitBottomDimension.getWidth() + this.mousewheel, this.astro.splitBottomDimension.getHeight() + this.mousewheel);

oldRect = this.getVisibleRect();
    
this.mouseX = e.getX();
this.mouseY = e.getY();
this.setPreferredSize(dim);

newX = this.mouseX / oldDim.getWidth() * dim.getWidth();
newY = this.mouseY / oldDim.getHeight() * dim.getHeight();
rect = new Rectangle((int)newX - (this.mouseX - oldRect.x), (int)newY - (this.mouseY - oldRect.y), oldRect.width, oldRect.height);

this.scrollRectToVisible(rect); 
    
this.revalidate();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM