Home > Software engineering >  How to spawn multiple circles in a 2d grid? (JAVA)
How to spawn multiple circles in a 2d grid? (JAVA)

Time:05-30

I am currently trying to make a game where circles are spawned and the player must click on it to gain a score. There are a bunch of details there but I want to ask this question.

Some variables are unused there or out of place since the original code was from a snake game made by "Bro Code" on youtube. I am trying to use his code as a foundation.

How to spawn multiple circles without limit in a 2d grid? (I tested the spawnTarget() method in the always-triggering listener and it only allows a single circle to exist.)

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.Random;


public class GamePanel extends JPanel implements ActionListener {

    static final int SCREEN_WIDTH = 1600;
    static final int SCREEN_HEIGHT = 1000;
    static final int UNIT_SIZE = 25;
    static final int GAME_UNITS = (SCREEN_WIDTH*SCREEN_HEIGHT)/UNIT_SIZE;
    static final int DELAY = 75;
    final int x[] = new int[GAME_UNITS];
    final int y[] = new int[GAME_UNITS];
    int bodyParts = 6;
    int applesEaten = 0;
    int appleX;
    int appleY;
    boolean running = false;
    Timer timer;
    Random random;

    //
    JPanel clockPanel;
    JLabel clock;
    long startTime;
    long endTime;
    //
    long elapsedSeconds;
    long elapsedTenthSeconds;
    //

    //

    GamePanel() {
        random = new Random();
        this.setPreferredSize(new Dimension(SCREEN_WIDTH,SCREEN_HEIGHT));
        this.setBackground(Color.black);
        this.setFocusable(true);
        this.addKeyListener(new MyKeyAdapter());
        startGame();
    }

    public void startGame() {
        running = true;
        timer = new Timer(DELAY,this);
        timer.start();
        clockMethod();
    }


    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        draw(g);
    }

    public void draw(Graphics g) {
        if (running) {
            // optional grid
            for(int i=0; i<SCREEN_WIDTH/UNIT_SIZE; i  ) {
                g.drawLine(0, i*UNIT_SIZE, SCREEN_WIDTH, i*UNIT_SIZE);
                g.drawLine(i*UNIT_SIZE, 0, i*UNIT_SIZE, SCREEN_HEIGHT);
            }

            // apple
            g.setColor(Color.red);
            g.fillOval(appleX, appleY, UNIT_SIZE, UNIT_SIZE);

            // score
            g.setColor(Color.white);
            g.setFont(new Font("Courier New", Font.BOLD, 40));
            FontMetrics metrics = getFontMetrics(g.getFont());
            g.drawString(String.valueOf(applesEaten),(SCREEN_WIDTH - metrics.stringWidth(String.valueOf(applesEaten)))/2,2*g.getFont().getSize());
        }
        else {
            gameOver(g);
        }
    }

    public void newTargetCoords() {
        appleX = random.nextInt((int)(SCREEN_WIDTH/UNIT_SIZE))*UNIT_SIZE;
        appleY = random.nextInt((int)(SCREEN_HEIGHT/UNIT_SIZE))*UNIT_SIZE;
    }

    public void move() {

    }

    public void spawnTarget() {
        newTargetCoords();
    }

    public void checkApple() {
        if ((x[0] == appleX)&&(y[0] == appleY)) {
            bodyParts  ;
            applesEaten  ;
        }
    }

    public void checkCollisions() {

        if (!running) {
             timer.stop();
        }

    }

    public void gameOver(Graphics g) {
        // score
        g.setColor(Color.white);
        g.setFont(new Font("Courier New", Font.BOLD, 20));
        FontMetrics metrics1 = getFontMetrics(g.getFont());
        g.drawString("score: "   applesEaten,(SCREEN_WIDTH - metrics1.stringWidth("score: "   applesEaten))/2,g.getFont().getSize());

        // Game Over text
        g.setColor(Color.green);
        g.setFont(new Font("Courier New", Font.PLAIN, 40));
        FontMetrics metrics2 = getFontMetrics(g.getFont());
        g.drawString("game over",(SCREEN_WIDTH - metrics2.stringWidth("game over"))/2,SCREEN_HEIGHT/2);
    }

    public void restartGame() {
        setVisible(false);
        new GameFrame();
    }

    public void clockMethod() {
        clockPanel = new JPanel();

        clock = new JLabel("00:00");

        clockPanel.add(clock);

        startTime = System.currentTimeMillis();

        add(clockPanel);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (running) {
            move();
            checkApple();
            checkCollisions();
        }
        repaint();

        if(timer.isRunning())
        {
            endTime = System.currentTimeMillis();

            // elapsed quarter seconds for spawns
            elapsedTenthSeconds = (endTime-startTime)/100;

            // put elapsed seconds into variable
            elapsedSeconds = (endTime-startTime)/1000;

            // declare formatting
            int min = (int)elapsedSeconds/60;
            int sec = (int)elapsedSeconds`;
            String minStr = (min<10 ? "0" : "") min;
            String secStr = (sec<10 ? "0" : "") sec;

            // display elapsed time (minutes:seconds)
            clock.setText(minStr ":" secStr);

            // spawn circle
            spawnTarget();
        }
    }

    public class MyKeyAdapter extends KeyAdapter {
        @Override
        public void keyPressed(KeyEvent e) {
            if (e.getKeyCode() == KeyEvent.VK_R) {
                restartGame();
            }
        }
    }
}

CodePudding user response:

since the original code was from a snake game made by "Bro Code" on youtube

As a "general" recommendation, I'd avoid YouTube for learning code, apart from going out of date really fast, SO seems to spending an lot of time correcting the code examples coming from YouTube.

First, I'd recommend you take the time to read through Painting in AWT and Swing and Performing Custom Painting to make sure you have a baseline understanding of the painting process in Swing.

As to your question, if need to keep track what is visible. Depending on how you want it to workflow, I might have a pool of "entities" which which you can randomly pick and move them to a "visible entities" pool. When a new paint cycle runs, you'd simply paint the "visible entities".

You need to take into consideration how long an entity should be displayed and each time your "game loop" runs, you'd need to check to see if any of the visible entities have "died", at which point you'd remove from the "visible entities" pool and put them back in the "entities pool".

For example...

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

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

    public Main() {
        EventQueue.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 StopWatch {
        private Instant startedAt;
        private Duration duration;

        public void setDuration(Duration duration) {
            this.duration = duration;
        }

        public Duration getDuration() {
            return duration;
        }

        public void start() {
            startedAt = Instant.now();
        }

        public Instant getStartedAt() {
            return startedAt;
        }

        public Duration getTimeRemaining() {
            Instant startedAt = getStartedAt();
            Duration duration = getDuration();
            if (startedAt == null || duration == null) {
                return Duration.ZERO;
            }
            Duration runtime = Duration.between(startedAt, Instant.now());
            return duration.minus(runtime);
        }

        public boolean hasTimeRemaining() {
            Duration timeRemaining = getTimeRemaining();
            return timeRemaining.toMillis() > 0;
        }
    }

    public class Target {
        private int row;
        private int col;

        private StopWatch stopWatch = new StopWatch();

        public Target(int row, int col) {
            this.row = row;
            this.col = col;
        }

        public int getColumn() {
            return col;
        }

        public int getRow() {
            return row;
        }

        public void spawn(Duration lifeSpan) {
            stopWatch = new StopWatch();
            stopWatch.setDuration(lifeSpan);
            stopWatch.start();
        }

        public void die() {
            stopWatch = null;
        }

        public Instant getBirthDate() {
            if (stopWatch == null) {
                return null;
            }
            return stopWatch.getStartedAt();
        }

        public Duration getLifeSpan() {
            if (stopWatch == null) {
                return null;
            }
            return stopWatch.getDuration();
        }

        public Duration getTimeRemaining() {
            if (stopWatch == null) {
                return Duration.ZERO;
            }
            return stopWatch.getTimeRemaining();
        }

        public boolean isAlive() {
            if (stopWatch == null) {
                return false;
            }
            return stopWatch.hasTimeRemaining();
        }
    }

    public class TestPane extends JPanel {

        private List<Target> targets;
        private List<Target> visibleTargets;

        private int rows = 4;
        private int cols = 4;

        private Target clickedTarget;

        public TestPane() {
            targets = new ArrayList<>(getRows() * getColumns());
            visibleTargets = new ArrayList<>(getRows() * getColumns());

            for (int row = 0; row < rows; row  ) {
                for (int col = 0; col < cols; col  ) {
                    targets.add(new Target(row, col));
                }
            }

            Timer timer = new Timer(5, new ActionListener() {
                private Random rnd = new Random();
                private List<Target> deadTargets = new ArrayList<>(getRows() * getColumns());
                private StopWatch respawnStopWatch;

                protected void restartRespawnClock() {
                    // Spawn a new target every second
                    respawnStopWatch.setDuration(Duration.ofSeconds(rnd.nextInt(1)   1));
                    respawnStopWatch.start();
                }

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (respawnStopWatch == null) {
                        respawnStopWatch = new StopWatch();
                        restartRespawnClock();
                    }

                    if (!respawnStopWatch.hasTimeRemaining()) {
                        restartRespawnClock();
                        if (!targets.isEmpty()) {
                            Collections.shuffle(targets);
                            Target target = targets.remove(0);
                            Duration lifeSpan = Duration.ofSeconds(rnd.nextInt(5)   3);
                            target.spawn(lifeSpan);
                            visibleTargets.add(target);
                        }
                    }
                    deadTargets.clear();
                    for (Target target : visibleTargets) {
                        if (!target.isAlive()) {
                            deadTargets.add(target);
                        }
                    }
                    visibleTargets.removeAll(deadTargets);
                    targets.addAll(deadTargets);
                    repaint();
                }
            });
            timer.start();

            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                    for (Target target : visibleTargets) {
                        Rectangle bounds = getBoundsFor(target);
                        if (bounds.contains(e.getPoint())) {
                            clickedTarget = target;
                            return;
                        }
                    }
                }
            });
        }

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

        public int getRows() {
            return rows;
        }

        public int getColumns() {
            return cols;
        }

        protected Rectangle getBoundsFor(Target target) {
            int width = getWidth() / getColumns();
            int height = getHeight() / getRows();

            int x = target.getColumn() * width;
            int y = target.getRow() * height;

            return new Rectangle(x, y, width, height);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            for (Target target : visibleTargets) {
                Rectangle bounds = getBoundsFor(target);
                if (target == clickedTarget) {
                    g2d.fillOval(bounds.x, bounds.y, bounds.width, bounds.height);
                } else {
                    g2d.drawOval(bounds.x, bounds.y, bounds.width, bounds.height);
                }
            }
            g2d.dispose();
        }

    }
}

A couple of things to note...

  • I'm not caching the Target bounds as they are dynamically calculate based on the current size of the component. Realistically, you could use a ComponentListener and when componentResized is called, invalidate the cache, but this is an additional enhancement you could investigate yourself.
  • The time between spawning a new target is randomly between 1-2 seconds, this could be tweaked to use milliseconds instead of seconds, but I'm sure most users won't see the difference
  • A target has a random live span of between 2-7 seconds, feel free to modify it.

The example also demonstrates a simple way to detect when a target has been clicked, in the example above, it will simple cause the target to be filled in.

I would also consider using seperate components for the game and game over screens, possibly switching between them using a CardLayout. This reduces the complexity of the classes.

  • Related