1

I would like to make a Tetris game in java.

I am using swing timer to drop tiles. But the problem is, when I try to move my tiles left and right, the motion speed is restricted by the timer delay value. Tiles drop every second, therefore, I can only change their positions when the timer resets and repaints. How can I make an independent second action performed, or any other method that can help me in this case, any help is greatly appreciated.

package gui;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;

import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.Timer;

enum Shapes {
    // T, S1, S2, K,L1, L2,
    //above members will be left commented until other tiles are prepared
    I;

    public static Shapes getRandomShapes() {

        Random random = new Random();
        return values()[random.nextInt(values().length)];
    }
}

public class Engine extends JPanel implements KeyListener, ActionListener {


    private static final long serialVersionUID = 1L;
    private int[] xPos = new int[800];
    private int[] yPos = new int[800];
    int moves = 0;
    Timer timer;
    int delay;

    Engine() {
        addKeyListener(this);
        setFocusable(true);
        setFocusTraversalKeysEnabled(false);
        delay = 1000;
        timer = new Timer(delay, this);
        timer.start();
    }

    private boolean left;
    private boolean right;

    private void fall() {
        yPos[0] += 30;
        yPos[1] += 30;
        yPos[2] += 30;
        yPos[3] += 30;

    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(Color.WHITE);
        g.drawRect(5, 5, 482, 842);
        g.setColor(Color.BLACK);
        g.fillRect(7, 7, 478, 838);

        if (Shapes.getRandomShapes() == Shapes.I) {
            if (moves == 0) {
                xPos[0] = 60;
                xPos[1] = 90;
                xPos[2] = 120;
                xPos[3] = 150;
                yPos[0] = 6;
                yPos[1] = 6;
                yPos[2] = 6;
                yPos[3] = 6;
                moves++;
            }
            for (int x = 0; x < 4; x++) {

                ImageIcon Iimage = new ImageIcon(Engine.class.getClassLoader().getResource("images/I.png"));
                Iimage.paintIcon(this, g, xPos[x], yPos[x]);

            }
        }
        g.dispose();
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        // TODO Auto-generated method stub
        timer.start();

        fall();
        if (left) {
            xPos[0] -= 30;
            xPos[1] -= 30;
            xPos[2] -= 30;
            xPos[3] -= 30;
            left = false;
        }
        if (right) {
            xPos[0] += 30;
            xPos[1] += 30;
            xPos[2] += 30;
            xPos[3] += 30;
            right = false;
        }

        repaint();
    }

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

        if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            right = true;
        }
        if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            left = true;

        }
    }


}
Yassine BELDI
  • 562
  • 5
  • 16
  • 1
    Decouple the movement logic (down) from your timer, so the timer can tick at a higher rate. I would capture the `Instant` of the "last drop" and the calculate the amount of time which has passed on each tick, when it's equal to or greater then 1 second, drop the piece down – MadProgrammer Jun 18 '19 at 23:42
  • Thank you for the answer! I am fairly new to the java and programming. Hence I dont know how I can apply your solution to my code. I am searching for "Instant" method on internet to understand right now. I am open to further help at this point :D thank you again! – Emre Başar Jun 18 '19 at 23:49
  • See [Date and Time Classes](https://docs.oracle.com/javase/tutorial/datetime/iso/datetime.html) – MadProgrammer Jun 19 '19 at 00:10
  • First of all you should not be using a KeyListener. You should be using `Key Bindings` to handle the left/right keys. In any case the basic solution is to reset the location of the "piece" in the listener code and then invoke repaint() right away to paint the "piece" in its new location. So it will repaint independently of the Timer. You can check out: https://stackoverflow.com/a/42518798/131872 for an example of this approach. – camickr Jun 19 '19 at 02:17
  • Wow it works perfectly. You both are too amazing!! thanks for the help. – Emre Başar Jun 19 '19 at 12:21

1 Answers1

1

Decouple the movement logic (down) from your timer, so the timer can tick at a higher rate.

I would capture the Instant of the "last drop" and the calculate the amount of time which has passed on each tick, when it's equal to or greater then 1 second, drop the piece down

The following example demonstrates the basic idea of using a Instant to calculate the Duration of time passed and then performing some action as required

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.Instant;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class Drop {

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

    public Drop() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {
        private Instant lastDropTime;
        private JLabel label;
        public TestPane() {
            setLayout(new BorderLayout());
            label = new JLabel("Drop in ...");
            add(label);

            Timer timer = new Timer(5, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent arg0) {
                    if (lastDropTime == null) {
                        lastDropTime = Instant.now();
                    }
                    // I'm using toMillis so I can display the time
                    // but you could just use toSeconds
                    Duration duration = Duration.between(lastDropTime, Instant.now());
                    long millis = duration.toMillis();
                    long seconds = duration.toSeconds();

                    if (seconds >= 1) {
                        // Drop down here...
                        lastDropTime = Instant.now();
                        millis = 1000;
                    }

                    label.setText("Drop in " + (1000 - millis));
                }
            });
            timer.start();
        }

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

    }

}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366