Search code examples
javaswingkeyboardkeylistenerkeyevent

I want my object to flow! The Keyboard Delay - Java


I don't know which component should I use to create a moving square on my JPanel. So I decided to use another JPanel to make an object. My first goal was moving the object but you know the games, when you press a button on your keyboard it is not performing like it's typing, it is performing like it is in the game. I mean the character movement is pretty delayed when you're typing. How can I fix the delay without changing my computer's keyboard delay settings? Also IDK let me know if you know a better way to create a moving square.

This is the source code:

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;


public class Frame extends JFrame implements KeyListener {

    private static final long serialVersionUID = 1L;
    private static int width = 800;
    private static int height = width / 16 * 9;

    static //// For Action
    Panel panel = new Panel();
    Square sq = new Square();
    private int x = 0;
    private int y = 0;

    // Constructor
    Frame() {
        this.setSize(width, height);
        this.setLocationByPlatform(true);
        this.setLayout(null);
        this.setResizable(false);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setFocusable(true);
        this.setName("Window");
        this.addKeyListener(this);

        // Action is starting
        sq.setLocation(x, y);
        panel.add(sq);
        this.add(panel);

    }

    // KeyListener==
    @Override
    public void keyTyped(KeyEvent e) {
        // TODO Auto-generated method stub

    }

      //My problem is here, or I'm wrong!?

    @Override
    public void keyPressed(KeyEvent e) {
            switch (e.getKeyCode()) {
            case KeyEvent.VK_W:
                sq.setLocation(x, y - 5);
                y-=5;
                this.repaint();
                System.out.println(e.getKeyChar());
                break;
            case KeyEvent.VK_S:
                sq.setLocation(x, y + 5);
                y+=5;
                this.repaint();
                System.out.println(e.getKeyChar());
                break;
            case KeyEvent.VK_A:
                sq.setLocation(x - 5, y);
                x-=5;
                this.repaint();
                System.out.println(e.getKeyChar());
                break;
            case KeyEvent.VK_D:
                sq.setLocation(x + 5, y);
                x+=5;
                this.repaint();
                System.out.println(e.getKeyChar());
                break;
            
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
        
    }

    // Main
    public static void main(String arguments[]) {
        Frame frame = new Frame();
        frame.add(panel);
        frame.setVisible(true);
    }
}



And this is my Square class, and I'm not sure about the fact that it is extending JPanel:

import java.awt.Color;
import javax.swing.JPanel;

public class Square extends JPanel {
    
    private static final long serialVersionUID = 1L;
    private int width = 70;
    private int height = 70;
    
    Square(){
        setSize(width, height);
        setBackground(Color.white);
    }
}

And finally my Panel Class:

public class Panel extends JPanel{

    private static final long serialVersionUID = 1L;
    private static int width = 800;
    private static int height = width / 16 * 9;

    Panel() {
        this.setSize(width, height);
        this.setBackground(Color.DARK_GRAY);
        this.setLayout(null);
    }
}

I need your help guys. Thanks for all of the replies.


Solution

  • One solution one can find, is to use a javax.swing.Timer that will constantly move the character and have the speed of the character as a property of it. So whenever the Timer fires its action listeners, one of them will add the current speed to the location of the game's object (ie the character). When you press a key, the action listener will just adjust the speed of the animation Timer (according to what key was pressed) and then [re]start the Timer. When you release all motion-related keys, the corresponding action listener will stop the Timer.

    General suggestions:

    1. Make sure you don't block the EDT.
    2. As already suggested in the comments of the question, use Key Bindings for more flexibility in the long run (instead of KeyListeners).

    Two variations follow, for the character representation:

    Variation 1: Custom painting

    You can have every object of the game's world to be painted inside the world's JComponent (for example a JPanel) by overriding the paintComponent method.

    Follows sample code...

    import java.awt.Color;
    import java.awt.Component;
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.Point;
    import java.awt.event.ActionEvent;
    import java.awt.geom.Point2D;
    import java.awt.geom.Rectangle2D;
    import java.util.ArrayList;
    import java.util.Objects;
    import javax.swing.AbstractAction;
    import javax.swing.ActionMap;
    import javax.swing.Icon;
    import javax.swing.InputMap;
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.KeyStroke;
    import javax.swing.SwingUtilities;
    import javax.swing.Timer;
    
    public class CustomPaintedCharacters {
        
        //This is the customly painted object representing an entity in the World:
        public static class GameObject {
            private final World world;
            private final Icon how;
            private final Point2D.Double where; //Important: use a Point2D.Double... Not a plain Point, because of precision.
    
            public GameObject(final World world, final Point2D where, final Icon how) {
                this.world = Objects.requireNonNull(world);
                this.how = Objects.requireNonNull(how);
                this.where = copyToDouble(where);
            }
    
            public void setPosition(final Point2D where) {
                this.where.setLocation(where);
            }
    
            public Point2D getPosition() {
                return copyToDouble(where);
            }
            
            public World getWorld() {
                return world;
            }
    
            public Icon getIcon() {
                return how;
            }
            
            public Rectangle2D getBounds() {
                return new Rectangle2D.Double(where.getX(), where.getY(), how.getIconWidth(), how.getIconHeight());
            }
    
            public final void paint(final Graphics2D g) {
                g.translate(where.getX(), where.getY()); /*Use as much of the precision as possible...
                ie instead of calling 'how.paintIcon' with rounded coordinates converted to ints, we can
                translate to where we want to by using doubles...*/
                how.paintIcon(world, g, 0, 0);
            }
        }
        
        public static interface RectangleIcon extends Icon {
            @Override
            default void paintIcon(final Component comp, final Graphics g, final int x, final int y) {
                g.fillRect(x, y, getIconWidth(), getIconHeight());
            }
        }
        
        public static class DefaultRectangleIcon implements RectangleIcon {
            private final Color c;
            private final int w, h;
            
            public DefaultRectangleIcon(final Color c, final int width, final int height) {
                this.c = Objects.requireNonNull(c);
                w = width;
                h = height;
            }
            
            @Override
            public void paintIcon(final Component comp, final Graphics g, final int x, final int y) {
                g.setColor(c);
                RectangleIcon.super.paintIcon(comp, g, x, y);
            }
            
            @Override
            public int getIconWidth() {
                return w;
            }
    
            @Override
            public int getIconHeight() {
                return h;
            }
        }
        
        public static class World extends JPanel {
            
            private final ArrayList<GameObject> gameObjects;
            
            public World() {
                this.gameObjects = new ArrayList<>();
            }
            
            public GameObject createGameObject(final Point2D where, final Icon how) {
                final GameObject go = new GameObject(this, where, how);
                gameObjects.add(go);
                return go;
            }
            
            @Override
            protected void paintComponent(final Graphics g) {
                super.paintComponent(g); //Don't forget this!
                gameObjects.forEach(go -> { //Paint every object in the world...
                    final Graphics2D graphics = (Graphics2D) g.create();
                    go.paint(graphics);
                    graphics.dispose();
                });
            }
            
            @Override
            public Dimension getPreferredSize() {
                if (isPreferredSizeSet())
                    return super.getPreferredSize();
                final Rectangle2D bounds = new Rectangle2D.Double(); //Important: use a Rectangle2D.Double... Not a plain Rectangle, because of precision.
                gameObjects.forEach(go -> bounds.add(go.getBounds()));
                return new Dimension((int) Math.ceil(bounds.getX() + bounds.getWidth()), (int) Math.ceil(bounds.getY() + bounds.getHeight()));
            }
        }
        
        public static class Animation extends Timer {
            private final Point2D.Double lvel; //Important: use a Point2D.Double... Not a plain Point, because of precision.
            
            public Animation(final int delay, final GameObject go) {
                super(delay, null);
                Objects.requireNonNull(go);
                lvel = new Point2D.Double();
                super.setRepeats(true);
                super.setCoalesce(true);
                super.addActionListener(e -> {
                    final Point2D pos = go.getPosition();
                    go.setPosition(new Point2D.Double(pos.getX() + lvel.getX(), pos.getY() + lvel.getY()));
                    go.getWorld().repaint();
                });
            }
            
            public void setLinearVelocity(final Point2D lvel) {
                this.lvel.setLocation(lvel);
            }
            
            public Point2D getLinearVelocity() {
                return copyToDouble(lvel);
            }
        }
        
        /*Adds two points using a limit. As you have probably understood yourself, and as I have tested
        it, the keys pressed with key bindings are keeping to invoke events repeatedly which would mean
        for example that if we add to the velocity of a GameObject again and again (after holding the
        same direction-button for a while) then the velocity would add up to a number greater than
        intended so we must ensure this will not happen by forcing each coordinate to not exceed the
        range of [-limit, limit].*/
        private static Point2D addPoints(final Point2D p1, final Point2D p2, final double limit) {
            final double limitAbs = Math.abs(limit); //We take the absolute value, in case of missusing the method.
            return new Point2D.Double(Math.max(Math.min(p1.getX() + p2.getX(), limitAbs), -limitAbs), Math.max(Math.min(p1.getY() + p2.getY(), limitAbs), -limitAbs));
        }
        
        /*This method solely exists to ensure that any given Point2D we give (even plain Points) are
        converted to Point2D.Double instances because we are working with doubles, not ints. For
        example imagine if we were to update a plain Point with a Point2D.Double; we would lose precision
        (in the location or velocity of the object) which is highly not wanted, because it could mean
        that after a while, the precision would add up to distances that the character would be supposed
        to have covered, but it wouldn't. I should note though that doubles are also not lossless, but
        they are supposed to be better in this particular scenario. The lossless solution would probably
        be to use BigIntegers of pixels but this would be a bit more complex for such a short
        demonstration of another subject (so we settle for it).*/
        private static Point2D.Double copyToDouble(final Point2D pt) {
            return new Point2D.Double(pt.getX(), pt.getY());
        }
        
        private static AbstractAction restartAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
            final Point2D acc = copyToDouble(acceleration); //Deffensive copy.
            
            //When we press a key (eg UP) we want to activate the timer once (and after changing velocity):
            return new AbstractAction() {
                @Override
                public void actionPerformed(final ActionEvent e) {
                    animation.setLinearVelocity(addPoints(animation.getLinearVelocity(), acc, max));
                    if (!animation.isRunning())
                        animation.restart();
                }
            };
        }
        
        private static AbstractAction stopAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
            final Point2D acc = copyToDouble(acceleration); //Deffensive copy.
            
            /*When we release a key (eg UP) we want to undo the movement of the character in the corresponding
            direction (up) and possibly even stop the animation (if both velocity coordinates are zero):*/
            return new AbstractAction() {
                @Override
                public void actionPerformed(final ActionEvent e) {
                    if (animation.isRunning()) {
                        //Decrement the velocity:
                        final Point2D newlvel = addPoints(animation.getLinearVelocity(), acc, max);
                        animation.setLinearVelocity(newlvel);
                        
                        //If both velocities are zero, we stop the timer, to speed up EDT:
                        if (newlvel.getX() == 0 && newlvel.getY() == 0)
                            animation.stop();
                    }
                }
            };
        }
        
        private static void installAction(final InputMap inmap, final ActionMap actmap, final Animation animation, final String onPressName, final Point2D off, final double max) {
    
            //One key binding for key press:
            final KeyStroke onPressStroke = KeyStroke.getKeyStroke(onPressName); //By default binds to the key-press event.
            inmap.put(onPressStroke, onPressName + " press");
            actmap.put(onPressName + " press", restartAnimationAction(animation, off, max));
            
            //One key binding for key release:
            final KeyStroke onReleaseStroke = KeyStroke.getKeyStroke(onPressStroke.getKeyCode(), onPressStroke.getModifiers(), true); //Convert the key-stroke of key-press event to key-release event.
            inmap.put(onReleaseStroke, onPressName + " release");
            actmap.put(onPressName + " release", stopAnimationAction(animation, new Point2D.Double(-off.getX(), -off.getY()), max));
        }
        
        public static Animation installAnimation(final GameObject go, final int delayMS, final double stepOffset) {
            final World world = go.getWorld();
            
            final InputMap in = world.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
            final ActionMap act = world.getActionMap();
    
            final Animation anim = new Animation(delayMS, go);
            
            /*The Strings used in each invocation of 'installAction' are very important as they will
            define the KeyStroke obtained by 'KeyStroke.getKeyStroke' method calls inside 'installAction'.
            So you shouldn't need to change those for this particular demonstration.*/
            installAction(in, act, anim, "LEFT", new Point2D.Double(-stepOffset, 0), stepOffset);
            installAction(in, act, anim, "RIGHT", new Point2D.Double(stepOffset, 0), stepOffset);
            installAction(in, act, anim, "UP", new Point2D.Double(0, -stepOffset), stepOffset);
            installAction(in, act, anim, "DOWN", new Point2D.Double(0, stepOffset), stepOffset);
            
            return anim;
        }
        
        public static void main(final String[] args) {
            SwingUtilities.invokeLater(() -> { //Make sure to 'invokeLater' EDT related code.
    
                final World world = new World();
    
                final GameObject worldCharacter = world.createGameObject(new Point(200, 200), new DefaultRectangleIcon(Color.CYAN.darker(), 100, 50));
    
                final JFrame frame = new JFrame("Move the character");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.getContentPane().add(world);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
    
                installAnimation(worldCharacter, 10, 2);
            });
        }
    }
    

    Variation 2: Extending a JComponent

    You can also extend a JComponent for any game's object representation (eg the character).

    In this solution, you can use other JComponents added to the character, changing in this way how the character looks without having to paint everything in paintComponent.

    For example, if you have an external image for the character, you can:

    1. Load the image with ImageIO.read (which will give you a BufferedImage, ie a non-animated image fully loaded into memory).
    2. Construct an ImageIcon by supplying the BufferedImage (which will give you an implementation of the Icon interface, suitable for the next step).
    3. Use a JLabel as the character and supply it with the ImageIcon created in step 2.

    But it is not just this, because, by having a JComponent as the character you may add to it other JComponents as it already is a Container, thus having many more options.

    It would be best here to use a LayoutManager for positioning the character (which is a JComponent) inside the parent (which is another JComponent). You can use the setLocation method of the JComponent game objects to position them inside the world. As far as I know, it is highly discouraged to use null as a layout for the world, so I created a small LayoutManager from scratch as a suggestion on how you can go about it.

    Follows sample code of a JLabel as the character without images, just to demonstrate the concept...

    import java.awt.Color;
    import java.awt.Component;
    import java.awt.Container;
    import java.awt.Dimension;
    import java.awt.Insets;
    import java.awt.LayoutManager;
    import java.awt.Point;
    import java.awt.Rectangle;
    import java.awt.event.ActionEvent;
    import java.awt.geom.Point2D;
    import java.io.Serializable;
    import java.util.ArrayList;
    import java.util.Objects;
    import java.util.function.BiFunction;
    import javax.swing.AbstractAction;
    import javax.swing.ActionMap;
    import javax.swing.BorderFactory;
    import javax.swing.InputMap;
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JPanel;
    import javax.swing.KeyStroke;
    import javax.swing.SwingUtilities;
    import javax.swing.Timer;
    
    public class CharacterOfJComponent {
        
        public static class ManualLayout implements LayoutManager, Serializable {
            
            public static void setOperatedSize(final Dimension input,
                                               final BiFunction<Integer, Integer, Integer> operator,
                                               final Dimension inputOutput) {
                inputOutput.setSize(operator.apply(input.width, inputOutput.width), operator.apply(input.height, inputOutput.height));
            }
    
            protected Dimension getGoodEnoughSize(final Component comp,
                                                  final Dimension defaultSize) {
                final Dimension dim = new Dimension(defaultSize);
                if (comp != null) { // && comp.isVisible()) {
                    /*Start with default size, and then listen to max and min
                    (if both max and min are set, we prefer the min one):*/
                    if (comp.isMaximumSizeSet())
                        setOperatedSize(comp.getMaximumSize(), Math::min, dim);
                    if (comp.isMinimumSizeSet())
                        setOperatedSize(comp.getMinimumSize(), Math::max, dim);
                }
                return dim;
            }
    
            protected Dimension getLayoutComponentSize(final Component comp) {
                return getGoodEnoughSize(comp, (comp.getWidth() <= 0 && comp.getHeight() <= 0)? comp.getPreferredSize(): comp.getSize());
            }
    
            @Override
            public void addLayoutComponent(final String name,
                                           final Component comp) {
            }
    
            @Override
            public void removeLayoutComponent(final Component comp) {
            }
    
            @Override
            public Dimension preferredLayoutSize(final Container parent) {
                return minimumLayoutSize(parent); //Preferred and minimum coincide for simplicity.
            }
    
            @Override
            public Dimension minimumLayoutSize(final Container parent) {
                final Component[] comps = parent.getComponents();
                if (comps == null || comps.length <= 0)
                    return new Dimension();
                final Rectangle totalBounds = new Rectangle(comps[0].getLocation(), getLayoutComponentSize(comps[0]));
                for (int i = 1; i < comps.length; ++i)
                    totalBounds.add(new Rectangle(comps[i].getLocation(), getLayoutComponentSize(comps[i])));
                final Insets parins = parent.getInsets();
                final int addw, addh;
                if (parins == null)
                    addw = addh = 0;
                else {
                    addw = (parins.left + parins.right);
                    addh = (parins.top + parins.bottom);
                }
                return new Dimension(Math.max(0, totalBounds.x + totalBounds.width) + addw, Math.max(0, totalBounds.y + totalBounds.height) + addh);
            }
    
            @Override
            public void layoutContainer(final Container parent) {
                for (final Component comp: parent.getComponents())
                    comp.setSize(getLayoutComponentSize(comp)); //Just set the size. The locations are taken care by the class's client supposedly.
            }
        }
        
        public static class GameObject extends JLabel {
            private final Point2D.Double highPrecisionLocation;
            
            public GameObject() {
                highPrecisionLocation = copyToDouble(super.getLocation());
            }
            
            public void setHighPrecisionLocation(final Point2D highPrecisionLocation) {
                this.highPrecisionLocation.setLocation(highPrecisionLocation);
                final Insets parentInsets = getParent().getInsets();
                setLocation((int) Math.round(highPrecisionLocation.getX()) + parentInsets.left + parentInsets.right,
                            (int) Math.round(highPrecisionLocation.getY()) + parentInsets.top + parentInsets.bottom);
            }
            
            public Point2D getHighPrecisionLocation() {
                return copyToDouble(highPrecisionLocation);
            }
            
            @Override
            public World getParent() {
                return (World) super.getParent();
            }
        }
        
        public static class World extends JPanel {
            public World() {
                super(new ManualLayout());
            }
            
            public ArrayList<GameObject> getGameObjects() {
                final ArrayList<GameObject> gos = new ArrayList<>();
                for (final Component child: getComponents())
                    if (child instanceof GameObject)
                        gos.add((GameObject) child);
                return gos;
            }
        }
        
        /*Adds two points using a limit. As you have probably understood yourself, and as I have tested
        it, the keys pressed with key bindings are keeping to invoke events repeatedly which would mean
        for example that if we add to the velocity of a GameObject again and again (after holding the
        same direction-button for a while) then the velocity would add up to a number greater than
        intended so we must ensure this will not happen by forcing each coordinate to not exceed the
        range of [-limit, limit].*/
        private static Point2D addPoints(final Point2D p1, final Point2D p2, final double limit) {
            final double limitAbs = Math.abs(limit); //We take the absolute value, in case of missusing the method.
            return new Point2D.Double(Math.max(Math.min(p1.getX() + p2.getX(), limitAbs), -limitAbs), Math.max(Math.min(p1.getY() + p2.getY(), limitAbs), -limitAbs));
        }
        
        /*This method solely exists to ensure that any given Point2D we give (even plain Points) are
        converted to Point2D.Double instances because we are working with doubles, not ints. For
        example imagine if we were to update a plain Point with a Point2D.Double; we would lose precision
        (in the location or velocity of the object) which is highly not wanted, because it could mean
        that after a while, the precision would add up to distances that the character would be supposed
        to have covered, but it wouldn't. I should note though that doubles are also not lossless, but
        they are supposed to be better in this particular scenario. The lossless solution would probably
        be to use BigIntegers of pixels but this would be a bit more complex for such a short
        demonstration of another subject (so we settle for it).*/
        private static Point2D.Double copyToDouble(final Point2D pt) {
            return new Point2D.Double(pt.getX(), pt.getY());
        }
        
        public static class Animation extends Timer {
            private final Point2D.Double lvel; //Important: use a Point2D.Double... Not a plain Point, because of precision.
            
            public Animation(final int delay, final GameObject go) {
                super(delay, null);
                Objects.requireNonNull(go);
                lvel = new Point2D.Double();
                super.setRepeats(true);
                super.setCoalesce(true);
                super.addActionListener(e -> {
                    final Point2D pos = go.getHighPrecisionLocation();
                    go.setHighPrecisionLocation(new Point2D.Double(pos.getX() + lvel.getX(), pos.getY() + lvel.getY()));
                    go.getParent().revalidate(); //Layout has changed.
                    go.getParent().repaint();
                });
            }
            
            public void setLinearVelocity(final Point2D lvel) {
                this.lvel.setLocation(lvel);
            }
            
            public Point2D getLinearVelocity() {
                return copyToDouble(lvel);
            }
        }
        
        private static AbstractAction restartAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
            final Point2D acc = copyToDouble(acceleration); //Deffensive copy.
            
            //When we press a key (eg UP) we want to activate the timer once (and after changing velocity):
            return new AbstractAction() {
                @Override
                public void actionPerformed(final ActionEvent e) {
                    animation.setLinearVelocity(addPoints(animation.getLinearVelocity(), acc, max));
                    if (!animation.isRunning())
                        animation.restart();
                }
            };
        }
        
        private static AbstractAction stopAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
            final Point2D acc = copyToDouble(acceleration); //Deffensive copy.
            
            /*When we release a key (eg UP) we want to undo the movement of the character in the corresponding
            direction (up) and possibly even stop the animation (if both velocity coordinates are zero):*/
            return new AbstractAction() {
                @Override
                public void actionPerformed(final ActionEvent e) {
                    if (animation.isRunning()) {
                        //Decrement the velocity:
                        final Point2D newlvel = addPoints(animation.getLinearVelocity(), acc, max);
                        animation.setLinearVelocity(newlvel);
                        
                        //If both velocities are zero, we stop the timer, to speed up EDT:
                        if (newlvel.getX() == 0 && newlvel.getY() == 0)
                            animation.stop();
                    }
                }
            };
        }
        
        private static void installAction(final InputMap inmap, final ActionMap actmap, final Animation animation, final String onPressName, final Point2D off, final double max) {
    
            //One key binding for key press:
            final KeyStroke onPressStroke = KeyStroke.getKeyStroke(onPressName); //By default binds to the key-press event.
            inmap.put(onPressStroke, onPressName + " press");
            actmap.put(onPressName + " press", restartAnimationAction(animation, off, max));
            
            //One key binding for key release:
            final KeyStroke onReleaseStroke = KeyStroke.getKeyStroke(onPressStroke.getKeyCode(), onPressStroke.getModifiers(), true); //Convert the key-stroke of key-press event to key-release event.
            inmap.put(onReleaseStroke, onPressName + " release");
            actmap.put(onPressName + " release", stopAnimationAction(animation, new Point2D.Double(-off.getX(), -off.getY()), max));
        }
        
        public static Animation installAnimation(final GameObject go, final int delayMS, final double stepOffset) {
            final World world = go.getParent();
            
            final InputMap in = world.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
            final ActionMap act = world.getActionMap();
    
            final Animation anim = new Animation(delayMS, go);
            
            /*The Strings used in each invocation of 'installAction' are very important as they will
            define the KeyStroke obtained by 'KeyStroke.getKeyStroke' method calls inside 'installAction'.
            So you shouldn't need to change those for this particular demonstration.*/
            installAction(in, act, anim, "LEFT", new Point2D.Double(-stepOffset, 0), stepOffset);
            installAction(in, act, anim, "RIGHT", new Point2D.Double(stepOffset, 0), stepOffset);
            installAction(in, act, anim, "UP", new Point2D.Double(0, -stepOffset), stepOffset);
            installAction(in, act, anim, "DOWN", new Point2D.Double(0, stepOffset), stepOffset);
            
            return anim;
        }
        
        public static void main(final String[] args) {
            SwingUtilities.invokeLater(() -> { //Make sure to 'invokeLater' EDT related code.
                
                final World world = new World();
                
                final GameObject worldCharacter = new GameObject();
                worldCharacter.setText("Character");
                worldCharacter.setBorder(BorderFactory.createLineBorder(Color.CYAN.darker(), 2));
                
                world.add(worldCharacter);
                
                worldCharacter.setHighPrecisionLocation(new Point(200, 200));
    
                final JFrame frame = new JFrame("Move the character");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.getContentPane().add(world);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
    
                installAnimation(worldCharacter, 10, 2);
            });
        }
    }
    

    As you can see, there is a custom LayoutManager named ManualLayout. It respects the minimum, preferred and maximum sizes of the layed out Components. Then the client of this class is responsible for accomodating with the parent's insets and child's location using setLocation method accordingly.

    In the long run, since you may want to put multiple objects in the world, which may overlap, then you can use a JLayeredPane as the World.

    Additional notes

    1. You can mix those variations.
    2. Don't forget to add super.paintComponent call in overriden paintComponent in custom painting variation.
    3. In case you want to be able to pause and resume the simulation, you may create an action listener which sets the other keyboard actions enabled or disabled.
    4. Only tested in JDK/JRE 8, but I think it should work in others also. At least the concept should work.