Home > OS >  How to add vision to a JComponent
How to add vision to a JComponent

Time:11-08

I have a dynamic number of JComponents moving in a JPanel. I want to make them stop when another Jcomponent is in it's way. I've tried to make a search about 2d raycasting, unfortunately i don't know how to implement it in my code. Stackoferflow have some answers for this but there they're building colliders, I need something more similar to raycasting.

Heres the JPanel code Map.java:

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

public class Map extends JPanel implements ActionListener {
    final int windowWidth = 1300;
    final int windowHeight = 750;

    Map() {
        this.setPreferredSize(new Dimension(windowWidth, windowHeight));
        Timer timer = new Timer(3000, this);
        timer.start();
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        // painting map
    }

    // Update method of the frame
    @Override
    public void actionPerformed(ActionEvent e) {
        Car car = new Car();
        this.add(car);
        // For loop responds for data cleaning
        for(int i=0;i<this.getComponents().length;i  ) {
            Car iCar = (Car) this.getComponents()[i];
            if(iCar.x >= 1400 || iCar.x <= -300 || iCar.y <= -300 || iCar.y >= 950) {
                this.remove(i);
            }
        }
        System.out.println(this.getComponentCount());
        this.updateUI();
    }
}

Heres JComponent code Car.java:

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Random;

public class Car extends JComponent implements ActionListener {
    private BufferedImage car;
    private final GeoPoint startPoint = GeoSides.pickGeoPoint();
    public int x = 0;
    public int y = 0;
    private int xVelocity;
    private int yVelocity;
    private final int imageX = 160;
    private final int imageY = 120;
    private double turnAngle = 0;
    Timer timer;

    // Initializing Car() object
    Car() {
        this.setPreferredSize(new Dimension(170, 130));
        this.timer = new Timer(1, this); // Setting up the timer for update method
        this.timer.start();
        try {
            Random randomize = new Random();
            car = ImageIO.read(new File("src/assets/"   (randomize.nextInt(10)   1)   ".png")); // Getting a random image of car
        } catch (IOException e) {
            e.printStackTrace();
        }
        // Setting start position
    }

    // This method should be called when another car is in the way
    public void stopCar() {
        xVelocity = 0;
        yVelocity = 0;
    }

    // Method scales the image and rotate it if needed
    private void drawCar(Graphics2D render2D, double rotationAngle) {
        //Image scaling and rotating
    }

    // Inherited method from JComponent
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        this.setLocation(new Point(x, y));

        Graphics2D render2D  = (Graphics2D) g;
        drawCar(render2D, turnAngle);
    }

    // Frame update of car's x, y speed and turn angle
    @Override
    public void actionPerformed(ActionEvent e) {
        switch (startPoint){
            // Setting destination points
        }

        x  = xVelocity;
        y  = yVelocity;
        this.repaint();

    }
}

CodePudding user response:

  1. Don't call updateUI, it's not doing what you think it's doing. To schedule a new paint pass, you should be calling repaint on the component which has changed.
  2. I'd, personally, not use components this way.
  3. Your "main loop" should be ensuring that any changes to a "entity" won't cause a collision, if they do, then you need to take appropriate action
  4. Calling this.setLocation(new Point(x, y)); inside your paintComponent is going to cause you no end up issues, as the Graphics context is actually translated (so that 0x0 is the location of the component within it's parent) before paintComponent is called
  5. Swing Timer does not scale well. That is, having more Timers could actually have a departmental effect on performance. Better to have a single Timer onto which you could attach multiple listeners, or better yet, a single Timer and single listener acting as the "main loop"

What you mean by "main loop"?

"Main loop" is a common term (especially in gaming, but Swing will call it the "Event Dispatching Thread" or "main event loop"). In context to you, this would be the Timer's ActionListener in your Map class.

This should be called on a regular bases, it is responsible for updating the entities, based on their needs, performing collision detection, updating the state in any other meaningful way and scheduling repaints.

Example

The following example simplifies the idea. It uses a single "main loop" which is responsible for updating the "entities" position, performing bounds and collision detection and scheduling the repaints.

Please, beware that this is an overtly simplified example. I would, personally, make Entity an interface and then make a dedicated Car class which implemented, but this is just to demonstrate the core concepts

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
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 MainPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class MainPane extends JPanel {

        private Timer ticker;

        private List<Entity> entities;

        public MainPane() {
            entities = new ArrayList<>(32);
            // For demonstration purposes
            int y = (200 - 20) / 2;

            entities.add(new Entity(0, y, 1, 0, Color.RED));
            entities.add(new Entity(400 - 20, y, -1, 0, Color.BLUE));
        }

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

        @Override
        public void addNotify() {
            super.addNotify();
            if (ticker != null) {
                ticker.stop();
                ticker = null;
            }
            ticker = new Timer(5, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    tickDidOccur();
                }
            });
            ticker.start();
        }

        @Override
        public void removeNotify() {
            super.removeNotify();
            if (ticker != null) {
                ticker.stop();
                ticker = null;
            }
        }

        // It's more efficent then creating a local variable each time
        private List<Entity> entitiesToBeRemoved = new ArrayList<>(32);

        protected void tickDidOccur() {
            entitiesToBeRemoved.clear();
            // So some choices.  You can either update ALL the enties and then
            // do the collision detection, or, you can do the collision detection
            // after each enity is been updated.

            Rectangle viewBounds = getBounds();
            for (Entity entity : entities) {
                Rectangle bounds = entity.peekNextPosition();
                if (!viewBounds.intersects(bounds)) {
                    entitiesToBeRemoved.add(entity);
                } else {
                    boolean conflict = false;
                    for (Entity other : entities) {
                        if (other == entity) {
                            continue;
                        }
                        // Please note, depending on the amount of change
                        // it's possible that the enities may be positioned
                        // further apart.  You will need to access this based
                        // on your needs.
                        // Bascially what this is going to do, is check to see
                        // if the next update can be perform or not, if there
                        // is a conflict, the entities are stopped and the
                        // current entities update is discarded
                        if (bounds.intersects(other.bounds)) {
                            other.stop();
                            entity.stop();
                            conflict = true;
                        }
                    }
                    if (!conflict) {
                        // Commit the next position
                        entity.update();
                    }
                }
                // This is more of a "long winded" bounds check, but as you
                // can see, we can simply make use of the functionality
                // Rectangle provides to do the same thing
                //if (bounds.x   bounds.width < 0 || bounds.y   bounds.height < 0) {
                //    entitiesToBeRemoved.add(entity);
                //} else if (bounds.x > getWidth() || bounds.y > getHeight()) {
                //    entitiesToBeRemoved.add(entity);
                //}
            }

            entities.removeAll(entitiesToBeRemoved);
            repaint();
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            for (Entity entity : entities) {
                // Because I trust no one
                Graphics2D g2d = (Graphics2D) g.create();
                entity.paint(g2d);
                g2d.dispose();
            }
        }

    }

    public class Entity {

        private Rectangle bounds;
        private Point velocity;

        private Color color;

        public Entity(int x, int y, int xDelta, int yDelta, Color color) {
            bounds = new Rectangle(x, y, 20, 20);
            velocity = new Point(xDelta, yDelta);
            this.color = color;
        }

        public Rectangle getBounds() {
            return bounds;
        }

        public Color getColor() {
            return color;
        }

        public void stop() {
            velocity = new Point(0, 0);
        }

        public void start(int xDelta, int yDelta) {
            velocity = new Point(0, 0);
        }

        public Rectangle peekNextPosition() {
            Rectangle next = new Rectangle(bounds);
            int x = next.x   velocity.x;
            int y = next.y   velocity.y;

            next.setLocation(x, y);
            return next;
        }

        public Rectangle update() {
            int x = bounds.x   velocity.x;
            int y = bounds.y   velocity.y;

            bounds.setLocation(x, y);
            return bounds;
        }

        public void paint(Graphics2D g) {
            Graphics2D g2d = (Graphics2D) g.create();
            // Now what ever we do wont' have a cascading effect on what
            // ever comes next.  This is really helpful for when you need
            // to apply AffineTransformation
            g2d.setColor(getColor());
            g2d.fill(bounds);
            g2d.dispose();
        }
    }
}

Why wouldn't use "component" based entities?

The main reasons are, they are "heavy". They have a lot of core functionality wrapped around them which is designed to do other work, they just aren't well designed for this kind of work.

Components already have a lot of location/size operations associated with them, which is generally managed by the layout management API, so you could (all too easily) end up fighting that.

Components are, generally, not well suited to this kind of work.

  • Related