Strange stuttering on moving objects

Hi,

I’ve started to mess around with Java 2D and making a demo of sprite animations, I noticed something weird. I have an object that has two frames of animation which I move frome left to right and right to left when it hits the borders of the window. If I let this run without touching the keyboard, the object stutters a lot while moving in it’s designated path. If I hit any button and keep pressing it, the animation becomes silky smooth. How can that be? I thought this was caused by my use of a KeyAdapter to check key presses, so I changed this. But I still get the stuttering.

I’m running this on Java SDK 7, for your information. Here’s my current code. I’m aware that this is far from perfect but this behavior is very strange to me. (sorry I’m still new to Java)

Anybody knows what is causing this?


package metroidvania.test;

import metroidvania.enums.Dir;
import metroidvania.enums.Direction;
import metroidvania.objects.RipperII;
import metroidvania.objects.Samus;
import metroidvania.utils.SpriteSheetLoader;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.Map;

public class AnimationTest extends JPanel {

    private static final String PRESSED = "pressed";
    private static final String RELEASED = "released";

    private Timer timer;
    private ArrayList<Image[]> samusSpriteAnimations;
    private ArrayList<Image[]> ripperSpriteAnimations;
    private Samus samus;
    private RipperII ripper;
    private Map<Dir, Boolean> dirMap = new EnumMap<>(Dir.class);
    private boolean lastRKeyPress = false;

    private int zoomFactor = 1;

    private int ripperPosX = 600;
    private int ripperPosY = 240;
    private boolean ripperActivated = true;

    public AnimationTest()
    {
        setFocusable(true);
        setBackground(Color.BLACK);
        setDoubleBuffered(true);

        setKeyBindings();

        initialize();

        timer = new Timer(5, new AnimationListener());
        timer.start();
    }

    private void setKeyBindings() {
        for (Dir dir : Dir.values()) {
            dirMap.put(dir, Boolean.FALSE);
        }

        int condition = WHEN_IN_FOCUSED_WINDOW;
        InputMap inputMap = getInputMap(condition);
        ActionMap actionMap = getActionMap();

        for (Dir dir : Dir.values()) {
            KeyStroke keyPressed = KeyStroke.getKeyStroke(dir.getKeyCode(), 0, false);
            KeyStroke keyReleased = KeyStroke.getKeyStroke(dir.getKeyCode(), 0, true);

            inputMap.put(keyPressed, dir.toString() + PRESSED);
            inputMap.put(keyReleased, dir.toString() + RELEASED);

            actionMap.put(dir.toString() + PRESSED, new DirAction(dir, PRESSED));
            actionMap.put(dir.toString() + RELEASED, new DirAction(dir, RELEASED));
        }

    }

    public void initialize()
    {
        samus = new Samus();
        ripper = new RipperII();
        samusSpriteAnimations = new ArrayList<Image[]>();
        ripperSpriteAnimations = new ArrayList<Image[]>();
        loadSprites();
    }

    private void loadSprites() {
        //samus
        addAnimationFrames(samusSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Samus Standing Left.png", 26, 43, 1, 4); //Idle left
        addAnimationFrames(samusSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Samus Standing Right.png", 26, 43, 1, 4); //Idle right
        addAnimationFrames(samusSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Samus Turning Left.png", 24, 46, 1, 3); //Turning left
        addAnimationFrames(samusSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Samus Turning Right.png", 24, 46, 1, 3); //Turning right
        addAnimationFrames(samusSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Samus Running Left.png", 35, 43, 1, 10); //Running left
        addAnimationFrames(samusSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Samus Running Right.png", 35, 43, 1, 10); //Running left

        //ripper
        addAnimationFrames(ripperSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Ripper II Left.png", 35, 14, 1, 2); //Running left
        addAnimationFrames(ripperSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Ripper II Right.png", 35, 14, 1, 2); //Running left
    }

    private void addAnimationFrames(ArrayList<Image[]> spriteAnimations, String spriteSheetFilename, int width, int height, int rows, int columns)
    {
        SpriteSheetLoader ssl = new SpriteSheetLoader(spriteSheetFilename, width, height, rows, columns);
        spriteAnimations.add(ssl.sprites());
    }

    private class DirAction extends AbstractAction {

        private String pressedOrReleased;
        private Dir dir;

        public DirAction(Dir dir, String pressedOrReleased) {
            this.dir = dir;
            this.pressedOrReleased = pressedOrReleased;
        }

        @Override
        public void actionPerformed(ActionEvent evt) {
            if (pressedOrReleased.equals(PRESSED)) {
                dirMap.put(dir, Boolean.TRUE);
            } else if (pressedOrReleased.equals(RELEASED)) {
                dirMap.put(dir, Boolean.FALSE);
            }
        }
    }

    private class AnimationListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            samus.validateKeyPresses(dirMap);

            for(Dir dir : Dir.values())
            {
                if(dir.getKeyCode() == KeyEvent.VK_1 && dirMap.get(dir))
                    zoomFactor = 1;
                else if(dir.getKeyCode() == KeyEvent.VK_2 && dirMap.get(dir))
                    zoomFactor = 2;
                else if(dir.getKeyCode() == KeyEvent.VK_3 && dirMap.get(dir))
                    zoomFactor = 3;
                else if(dir.getKeyCode() == KeyEvent.VK_4 && dirMap.get(dir))
                    zoomFactor = 4;
                else if(dir.getKeyCode() == KeyEvent.VK_5 && dirMap.get(dir))
                    zoomFactor = 5;
                else if(dir.getKeyCode() == KeyEvent.VK_R)
                {
                    if(dirMap.get(dir)) if(!lastRKeyPress) ripperActivated = !ripperActivated;
                    lastRKeyPress = dirMap.get(dir);
                }

            }

            if(ripperActivated)
            {
                if(ripper.getDirection() == Direction.LEFT)
                {
                    if(ripperPosX > 0)
                        ripperPosX--;
                    else{
                        ripper.setDirection(Direction.RIGHT);
                        ripperPosX++;
                    }
                }
                else
                {
                    if(ripperPosX < (640 - 35))
                        ripperPosX++;
                    else {
                        ripper.setDirection(Direction.LEFT);
                        ripperPosX--;
                    }
                }
            }

            repaint();
        }
    }

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

        Graphics2D g2d = (Graphics2D) g;

        String s = "Current frame: " + Integer.toString(samus.getCurrentFrame());
        Font small = new Font("Helvetica", Font.BOLD, 14);

        g2d.setColor(Color.white);
        g2d.setFont(small);
        g2d.drawString(s, 1, 20);

        Image frameImage = samusSpriteAnimations.get(samus.getCurrentAnimation().getCode())[samus.getCurrentFrame()];
        g2d.drawImage(frameImage, 50, 50, frameImage.getWidth(this) * zoomFactor, frameImage.getHeight(this) * zoomFactor, this);

        if(ripperActivated) {
            if (ripper.getDirection() == Direction.LEFT)
                frameImage = ripperSpriteAnimations.get(0)[ripper.getCurrentFrame()];
            else
                frameImage = ripperSpriteAnimations.get(1)[ripper.getCurrentFrame()];

            g2d.drawImage(frameImage, ripperPosX, ripperPosY, frameImage.getWidth(this) * zoomFactor, frameImage.getHeight(this) * zoomFactor, this);
        }
    }
}


I would suggest that you ditch the swing timer and create a proper game loop.

Yeah that too… I’ve downloaded a Space Invaders example that uses a game loop instead and this stutters as well if I run it. I’m thinking that maybe this is related to my hardware configuration?

If you want to do animations in Swing, then you must take a look at Active Rendering. You have to create a [icode]BufferStrategy[/icode] from either the canvas or the JFrame, and use it like this.


long currentTime = System.nanoTime()/NANOS_IN_SECOND;
long gameTime = currentTime;

final int frameTime = 1/targetFPS;

boolean running = true;

while (running)
{
    currentTime = System.nanoTime()/NANOS_IN_SECOND;

    while (currentTime > gameTime)
    {
        update();
        gameTime += frameTime;
    }

    render(bufferStrategy.getDrawGraphics());
}

This is not a perfect game loop, but it’s been a long time I have used Swing, so you may have to optimise it. If you want to do games, I recommend you to look into a game library, and for beginners like you, I recommend to check out Mercury.

+1, A game library will certainly get your game ideas up and running quickly.

However the experience and knowledge gained from building your own game core / game loop is priceless, and if you subsequently go with a game/graphics library you will understand it so much more quickly and clearly, allowing you to get much more benefit from it.

OT:

[quote][…]

        //samus
        addAnimationFrames(samusSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Samus Standing Left.png", 26, 43, 1, 4); //Idle left
        addAnimationFrames(samusSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Samus Standing Right.png", 26, 43, 1, 4); //Idle right
        addAnimationFrames(samusSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Samus Turning Left.png", 24, 46, 1, 3); //Turning left
        addAnimationFrames(samusSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Samus Turning Right.png", 24, 46, 1, 3); //Turning right
        addAnimationFrames(samusSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Samus Running Left.png", 35, 43, 1, 10); //Running left
        addAnimationFrames(samusSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Samus Running Right.png", 35, 43, 1, 10); //Running left

        //ripper
        addAnimationFrames(ripperSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Ripper II Left.png", 35, 14, 1, 2); //Running left
        addAnimationFrames(ripperSpriteAnimations, "E:\\Java Source\\Metroidvania\\Images\\Ripper II Right.png", 35, 14, 1, 2); //Running left

[…]
[/quote]
But you made sure nobody else will notice the stuttering behavior :stuck_out_tongue:
I would advise you to use resources packed within the jar or relative to it. That would allow you to distribute your program without any worries about the users disk name.

-ClaasJG

My prejudice has been to prefer the Util timer to the Swing timer. I find actions on the Event Dispatch Thread (EDT) to sometimes be rather mysterious–I don’t understand the mechanics of this thread well enough to be able to predict when consolidation of EDT events will occur or not.

Since that is a “dark area” for me, my guess is that there is some EDT consolidation happening only when you are not pressing a key. This is a process where if the EDT gets a series of “similar” calls, it drops all but the last in the series. Thus, it jumps ahead rather than executing each in series, creating stuttering.

A proper game loop can be made with either a util.Timer or made in as a separate class. They are functionally equivalent, if the separate class is running in its own thread (as it should be). IMHO, the “update” portion of either form of the game loop should only set flags, not actively alter Swing objects, and the “render” portion can consult those flags when rendering the Swing objects. This avoids the problem of engaging an unsafe Swing object concurrently (the main motivation for using a Swing Timer) and helps prevent the EDT from getting clogged.

5 millis seems rather short for a game loop. That translates to 200 fps if it executes as asserted, which is overkill if monitors are only updating 60fps. 60fps is more like a loop of 16 millis. Not clear to me how that might effect your stuttering or the lack of it when executing a key press. Perhaps the keypresses are enforcing a certain pace, slowing the loop (AnimationListener) execution to a speed where there is no consolidation on the EDT, or are otherwise given some sort of “do not consolidate” priority. As I said, I find this under-the-engine stuff kind of mystifying.

First off, thanks for the input everyone! I’ll definitely try out your recommendations since I’m really still learning how to do things properly with Java!

As for my stuttering problem, it’s only happening when I’m running this on my Windows 2003 Server virtual machine I use for work. Don’t ask me why I’m using a Windows 2003 Server virtual machine in 2014… sigh. Anyway, tried it on my personnal Windows 8 laptop using Java SDK 1.8 and I get no stuttering at all.

Guess that was my problem after all!