2

I am creating a retro arcade game in Java. The screen resolution for the game is 304 x 256, which I want to keep to preserve the retro characteristics of the game (visuals, animations, font blockiness, etc.).

But when I render this on a large desktop display, it is too small, as one would expect.

I'd like to be able to scale the window up say by a constant factor, without having to code the various paint(Graphics) methods to be knowledgeable about the fact that there's a scale-up. That is, I'd like the rendering code believe that the screen is 304 x 256. I also don't want to have to change my desktop resolution or go into full screen exclusive mode. Just want a big window with scaled up pixels, essentially.

I'd be looking for something along the following lines:

scale(myJFrame, 4);

and have all the contents automatically scale up.

UPDATE: Regarding input, my game happens to use keyboard input, so I don't myself need the inverse transform that trashgod describes. Still I can imagine that others would need that, so I think it's an appropriate suggestion.

  • 1) [actually implemented in Nimbus L&F](https://docs.oracle.com/javase/tutorial/uiswing/lookandfeel/size.html), 2) to avoids usage of own NImbus's `Painter` together with `paintComponen`t, 3) no idea about, whats your reason(s) for `paint(g)` – mKorbel Jun 05 '17 at 07:32
  • Thanks mKorbel. My understanding of Nimbus is that I can give specific components rendering hints to indicate the desired size. In my case, though, the components are basically custom (things like a score bar, a game "arena", etc.) and most of what's going on is painting game sprites (player, enemies, projectiles, etc.). I know I can recode my components to perform the pixel scaling themselves, but I'm asking whether there's a way to do this globally. –  Jun 05 '17 at 07:45
  • uuupps you are on wrong side of barrier == programatically, from hd (majority of laptops) to 4k, no idea about how the scaling to works with e.g. Win7/10 Magnifier ... – mKorbel Jun 05 '17 at 07:59
  • don't reinvent the wheeeeeel, part of (top of) custom L&F has implemented three important steps in API, Native OS, screen resolution, then the test, attempts for Font scaling, combinations of chars v.s. pixels, (methods from SwingUtilities) – mKorbel Jun 05 '17 at 08:07
  • 1) the simplest multiplier hd == 1, FullHD == 2, 2k == 4, 4k == 8, 2) at beginning to avoids the odd multipliers, 3) multipiers is base for (JPanel) getPreferredSize and base (for paints players, enemies and so forth) rendered on the hd screens, you forgot any reaction to the no idea about, whats your reason(s) for paint(g), forgot about paint(), there is paintComponent exclusivelly for Swing – mKorbel Jun 05 '17 at 08:26
  • Re: paint() vs paintComponent(), the reason is that other than using a JFrame, it's not a Swing UI at all. I don't want any standard Swing buttons or labels or tabbed panes or anything. (It's basically a clone of a 1980s era game and I want it to look/behave like that.) So I'm actually good with not calling paintBorder() or paintChildren(). –  Jun 05 '17 at 08:31
  • With respect to the multipliers--is there a way to apply this idea globally? Or would I need to account for the multiplier explicitly in my rendering code? –  Jun 05 '17 at 08:33
  • 1) there must be relationships between screens resolution == multiplier (e.g. hd == 1, FullHD == 2, 2k == 4, 4k == 8,) and multipliers for height/wieght (Graphics rendered by paintComponent), 2) true is that real numbers are hd == 1, FullHD == 2, 2k == 3, 4k == 4, but to avoids at beginning, 3) your 1st. result is (e.g.) square on the screen for all standard screens resolutions from empty JFrame + JPanel + override JPanle#getPreferredSize, and JFrame#pack(), before setVisible(two last code lines in constructor), thats everything from me, I'm used matrix from old SwingX – mKorbel Jun 05 '17 at 08:46
  • Thanks mKorbel for your thoughts. I am hoping to avoid introducing multipliers throughout the code (e.g. preferredSize, painting, animation step sizes) but in the event that I have to include them, I expect it will be along the lines you describe. –  Jun 05 '17 at 08:56

3 Answers3

2

One approach, suggested here, is to rely on drawImage() to scale an image of the content. Your game would render itself in the graphics context of a BufferedImage, rather than your implementation of paintComponent(). If the game includes mouse interaction, you'll have to scale the mouse coordinates as shown. In the variation below, I've given the CENTER panel a preferred size that is a multiple of SCALE = 8 and added the original as an icon in the WEST of a BorderLayout. As the default, CENTER, ignores a component's preferred size, you may want to add it to a (possibly nested) panel having FlowLayout. Resize the frame to see the effect.

f.setLayout(new FlowLayout());
f.add(new Grid(NAME));
//f.add(new JLabel(ICON), BorderLayout.WEST);

image

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.UIManager;

/**
 * @see https://stackoverflow.com/a/44373975/230513
 * @see http://stackoverflow.com/questions/2900801
 */
public class Grid extends JPanel implements MouseMotionListener {

    private static final String NAME = "OptionPane.informationIcon";
    private static final Icon ICON = UIManager.getIcon(NAME);
    private static final int SCALE = 8;
    private final BufferedImage image;
    private int imgW, imgH, paneW, paneH;

    public Grid(String name) {
        super(true);
        imgW = ICON.getIconWidth();
        imgH = ICON.getIconHeight();
        image = new BufferedImage(imgW, imgH, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = (Graphics2D) image.getGraphics();
        ICON.paintIcon(null, g2d, 0, 0);
        g2d.dispose();
        this.addMouseMotionListener(this);
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(imgW * SCALE, imgH * SCALE);
    }

    @Override
    protected void paintComponent(Graphics g) {
        paneW = this.getWidth();
        paneH = this.getHeight();
        g.drawImage(image, 0, 0, paneW, paneH, null);
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        Point p = e.getPoint();
        int x = p.x * imgW / paneW;
        int y = p.y * imgH / paneH;
        int c = image.getRGB(x, y);
        this.setToolTipText(x + "," + y + ": "
            + String.format("%08X", c));
    }

    @Override
    public void mouseDragged(MouseEvent e) {
    }

    private static void create() {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(new Grid(NAME));
        f.add(new JLabel(ICON), BorderLayout.WEST);
        f.pack();
        f.setVisible(true);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                create();
            }
        });
    }
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Thanks trashgod. I tried this technique and had some luck, but ran into a challenge around an interaction with the LayoutManager. I had a JPanel with a BorderLayout, and the BorderLayout adopted the scaled up preferredSize, with the result that the south component was very far away from the center. At any rate it's good to know that this was on the right track so I'll give it another shot. Thank you. –  Jun 06 '17 at 04:32
  • @WillieWheeler: For reference, I added a simple `FlowLayout` change above; note how the resize behavior changes. See also the game cited [here](https://stackoverflow.com/a/2144019/230513); [Listing 1](http://robotchase.sourceforge.net/overview-summary.html#listing1) is the original source. – trashgod Jun 06 '17 at 09:27
  • FlowLayout did the trick here. My goal is a little different that what you do here, but this certainly pointed me in the right direction. I'll post an answer with my code. –  Jun 07 '17 at 08:20
2

One approach, suggested here, is to rely on the graphics context's scale() method and construct to an inverse transform to convert between mouse coordinates and image coordinates. In the example below, note how the original image is 256 x 256, while the displayed image is scaled by SCALE = 2.0. The mouse is hovering over the center the image; the tooltip shows an arbitrary point in the display and the center point (127, 127) in the original.

image

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;

/** @see https://stackoverflow.com/a/2244285/230513 */
public class InverseTransform {

    private static final double SCALE = 2.0;

    public static void main(String[] args) {
        JFrame frame = new JFrame("Inverse Test");
        BufferedImage image = getImage(256, 'F');
        AffineTransform at = new AffineTransform();
        at.scale(SCALE, SCALE);
        frame.add(new ImageView(image, at));
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

    private static BufferedImage getImage(int size, char c) {
        final Font font = new Font("Serif", Font.BOLD, size);
        BufferedImage bi = new BufferedImage(
            size, size, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = bi.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setPaint(Color.white);
        g2d.fillRect(0, 0, size, size);
        g2d.setPaint(Color.blue);
        g2d.setFont(font);
        FontMetrics fm = g2d.getFontMetrics();
        int x = (size - fm.charWidth(c)) / 2;
        int y = fm.getAscent() + fm.getDescent() / 4;
        g2d.drawString(String.valueOf(c), x, y);
        g2d.setPaint(Color.black);
        g2d.drawLine(0, y, size, y);
        g2d.drawLine(x, 0, x, size);
        g2d.fillOval(x - 3, y - 3, 6, 6);
        g2d.drawRect(0, 0, size - 1, size - 1);
        g2d.dispose();
        return bi;
    }

    private static class ImageView extends JPanel {

        private BufferedImage image;
        private AffineTransform at;
        private AffineTransform inverse;
        private Graphics2D canvas;
        private Point oldPt = new Point();
        private Point newPt;

        @Override
        public Dimension getPreferredSize() {
            return new Dimension( // arbitrary multiple of SCALE
                (int)(image.getWidth()  * SCALE * 1.25),
                (int)(image.getHeight() * SCALE * 1.25));
        }

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g;
            try {
                g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
                inverse = g2d.getTransform();
                inverse.invert();
                g2d.translate(this.getWidth() / 2, this.getHeight() / 2);
                g2d.transform(at);
                g2d.translate(-image.getWidth() / 2, -image.getHeight() / 2);
                inverse.concatenate(g2d.getTransform());
                g2d.drawImage(image, 0, 0, this);
            } catch (NoninvertibleTransformException ex) {
                ex.printStackTrace(System.err);
            }
        }

        ImageView(final BufferedImage image, final AffineTransform at) {
            this.setBackground(Color.lightGray);
            this.image = image;
            this.at = at;
            this.canvas = image.createGraphics();
            this.canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
            this.canvas.setColor(Color.BLACK);
            this.addMouseMotionListener(new MouseMotionAdapter() {

                @Override
                public void mouseMoved(MouseEvent e) {
                    Point m = e.getPoint();
                    Point i = e.getPoint();
                    try {
                        inverse.inverseTransform(m, i);
                        setToolTipText("<html>Mouse: " + m.x + "," + m.y
                            + "<br>Inverse: " + i.x + "," + i.y + "</html>");
                    } catch (NoninvertibleTransformException ex) {
                        ex.printStackTrace();
                    }
                }
            });
        }
    }
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Though I don't need the mouse part, your AffineTransform helped me. I added an answer showing what I did. –  Jun 07 '17 at 09:07
2

Thanks to trashgod for pointing me in the right direction with his two answers. I was able to combine elements of both answers to arrive at something that works for what I need to do.

So first, my goal was to scale up an entire UI rather than scaling up a single icon or other simple component. By "an entire UI" I specifically mean a JPanel containing multiple custom child components laid out using a BorderLayout. There are no JButtons or any other interactive Swing components, and no mouse input (it's all keyboard-based input), so really I just need to scale a 304 x 256 JPanel up by a factor of 3 or 4.

Here's what I did:

package bb.view;

import javax.swing.JComponent;
import javax.swing.JPanel;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;

import static bb.BBConfig.SCREEN_WIDTH_PX;   // 304
import static bb.BBConfig.SCREEN_HEIGHT_PX;  // 256

public class Resizer extends JPanel {
    private static final int K = 3;
    private static final Dimension PREF_SIZE =
        new Dimension(K * SCREEN_WIDTH_PX, K * SCREEN_HEIGHT_PX);
    private static final AffineTransform SCALE_XFORM =
        AffineTransform.getScaleInstance(K, K);

    public Resizer(JComponent component) {
        setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
        add(component);
    }

    @Override
    public Dimension getPreferredSize() {
        return PREF_SIZE;
    }

    @Override
    public void paint(Graphics g) {
        Graphics2D g2 = (Graphics2D) g;
        g2.setTransform(SCALE_XFORM);
        super.paint(g2);
    }
}

Some important elements of the solution:

  • Using a FlowLayout here shrinkwraps the child component, which is what I want. (Thanks trashgod for that.) That is, I don't want the child to expand to fill the Resizer preferred size, because that wrecks the child component's layout. Specifically it was creating this huge gap between the child component's CENTER and SOUTH regions.
  • I configured the FlowLayout with left alignment and hgap, vgap = 0. That way my scale transform would have the scaled up version anchored in the upper left corner too.
  • I used an AffineTransform to accomplish the scaling. (Again thanks trashgod.)
  • I used paint() instead of paintComponent() because the Resizer is simply a wrapper. I don't want to paint a border. I basically want to intercept the paint() call, inserting the scale transform and then letting the JPanel.paint() do whatever it would normally do.
  • I didn't end up needing to render anything in a separate BufferedImage.

The end result is that the UI is large, but the all the code other than this Resizer thinks the UI is 304 x 256.