4

I'm trying to implement a camera for a 2D game that I'm making... The goal will to have the cam keep the player in the center and the sprites relative to the camera.

To get the hang of normalocity's post, I tried starting off simple by making a Camera Test project, where I'd simulate a camera by drawing a sprite to a JPanel, and moving a "camera" object (which is the JPanel) around and setting the sprite's x,y relative to that.

The Camera, as I said, is the JPanel... and I've added a "world", which is a class with an x,y of 0,0, and w=1000, h=1000. I've included the sprite's location relative to the world as well as the camera. When I move the camera up, the sprite moves down and the player stays in the middle as expected..

enter image description here

But if I keep pressing down, the sprite seems to keep drawing over itself.

enter image description here

My questions are:

  • Am I on the right track in implementing a camera given the code below?
  • Why does the sprite start to draw over itself there? It should just disappear off the viewPort/JPanel

Thanks!

Now with PaintComponent(g) added, my JPanel bg color of gray now slides off. Is this supposed to happen?

enter image description here


EDIT: SSCCE of my program:

Main Class:

import java.awt.Dimension;
import java.awt.Toolkit;
import javax.swing.JFrame;

@SuppressWarnings("serial")
public class MainSSCCE extends JFrame {
static MainSSCCE runMe;

public MainSSCCE() {
    JFrame f = new JFrame("Camera Test");
    CameraSSCCE cam = new CameraSSCCE(0, 0, 500, 500);
    f.add(cam);
    f.setSize(cam.getWidth(), cam.getHeight());    
    f.setVisible(true);
    f.setResizable(false);
    f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); 
    Dimension screensize = Toolkit.getDefaultToolkit().getScreenSize();
    f.setLocation( (screensize.width - f.getWidth())/2,
         (screensize.height - f.getHeight())/2-100 );
}

public static void main(String[] args) {
    runMe = new MainSSCCE();
}
}

Camera Class:

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JPanel;

//Camera is the JPanel that will draw all objects... each object location will be in relation to the World
public class CameraSSCCE extends JPanel implements KeyListener {
    //add world to camera...
    private static final long serialVersionUID = 1L;
    private int camX, camY, camH, camW;
    private SpriteSSCCE sprite;
    private PlayerSSCCE player;
    private WorldSSCCE world;

    public CameraSSCCE(int x, int y, int w, int h) {
        camX = x;
        camY = y;
        camW = w;       
        camH = h;   
        sprite = new SpriteSSCCE(this, 300, 300, 20, 20);
        player = new PlayerSSCCE(this, camW/2, camH/2, 25, 40);
        world = new WorldSSCCE(this, 0, 0, 1000, 1000);

        addKeyListener(this);
        setFocusable(true);
    }

    public int getWidth() {
        return camW;
    }

    public int getHeight() {
        return camH;
    }    

    @Override   
    protected void paintComponent(Graphics g) { 
        super.paintComponent(g);

        //cam is 500 x 500
        g.setColor(Color.gray);
        g.fillRect(camX, camY, camW, camH);     

        //draw sprite at JPanel location if in camera sight
        if (((sprite.getX()-camX) >= camX) && ((sprite.getX()-camX) <= (camX+camW)) && ((sprite.getY()-camY) >= camY) && ((sprite.getY()-camY) <= (camY+camH))) {
            g.setColor(Color.green);
            g.fillRect(sprite.getX()-camX, sprite.getY()-camY, 20, 20); 

            //Cam Sprite Location
            g.setColor(Color.white);
            g.drawString("Camera Sprite Location: (" + (sprite.getX()-camX) + ", " + (sprite.getY()-camY) + ")", sprite.getX()-camX, sprite.getY()-camY);                   
        }

        //Player location (center of Camera... Camera follows player)
        g.setColor(Color.cyan);
        g.fillRect(player.getX()-player.getWidth(), player.getY()-player.getWidth(), player.getWidth(), player.getHeight());

        g.setColor(Color.white);
        //World Sprite Location
        g.drawString("World Sprite Location: (" + sprite.getX() + ", " + sprite.getY() + ")", sprite.getX(), sprite.getY());

        //Cam Player Location
        g.drawString("Cam Player Location: (" + (camW/2-player.getWidth()) + ", " + (camH/2-player.getHeight()) + ")", camW/2-player.getWidth(), camH/2-player.getHeight());
    }

    public void keyPressed(KeyEvent e) {
        //move camera right in relation to World
        if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            camX+=5;
        }
        //move camera left in relation to World
        if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            camX-=5;
        }
        //move camera up in relation to World
        if (e.getKeyCode() == KeyEvent.VK_UP) {
            camY-=5;
        }
        //move camera down in relation to World
        if (e.getKeyCode() == KeyEvent.VK_DOWN) {
            camY+=5;
        }
        repaint();
    }   

    public void keyReleased(KeyEvent e) {}
    public void keyTyped(KeyEvent e) {}

}

World Class:

public class WorldSSCCE {
    private int x, y, w, h;
    private CameraSSCCE camera;

    public WorldSSCCE(CameraSSCCE cam, int x, int y, int w, int h) {
        camera = cam;               
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
    }

    public int getX() {
        return this.x;
    }

    public int getY() {
        return this.y;  
    }

    public int getWidth() {
        return this.w;
    }

    public int getHeight() {
        return this.h;
    }
}

Player Class:

import java.awt.Dimension;

public class PlayerSSCCE {
    private int x, y, w, h;
    private CameraSSCCE cam;

    public PlayerSSCCE(CameraSSCCE cm, int x, int y, int w, int h) {
        cam = cm;               
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
    }

    public int getX() {
        return this.x;
    }

    public int getY() {
        return this.y;  
    }

    public int getWidth() {
        return this.w;
    }

    public int getHeight() {
        return this.h;
    }

    public void setX(int val) {
        this.x += val;
    }

    public void setY(int val) {
        this.y += val;
    }   
}

Sprite Class:

import java.awt.Color;
import java.awt.Graphics;

public class SpriteSSCCE {
    private int xLoc, yLoc, width, height;
    private CameraSSCCE world;

    public SpriteSSCCE(CameraSSCCE wld, int x, int y, int w, int h) {
        xLoc = x;
        yLoc = y;
        width = w;
        height = h;
        world = wld;    
    }

    public int getX() {
        return xLoc;
    }

    public int getY() {
        return yLoc;    
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }


    public void paintComponent(Graphics g) {
        g.setColor(Color.green);
        g.fillRect(xLoc, yLoc, width, height);      
    }


}
David Kroukamp
  • 36,155
  • 13
  • 81
  • 138
user3871
  • 12,432
  • 33
  • 128
  • 268
  • 1
    Just a guess, but maybe call `super(g)` in your `paintComponent` method. – sdasdadas Jul 30 '13 at 18:19
  • 1
    Just as a side note: 2D cameras usually end up needing to have more sophisticated positioning than just "always show the player in the middle". This behind-the-scenes video about the development of "Insanely Twisted Shadow Planet" goes into what they did to make good camera positioning logic: http://www.youtube.com/watch?v=aAKwZt3aXQM – Robert J. Walker Jul 30 '13 at 18:19
  • @RobertJ.Walker Thanks. I figured that was the case. I was trying to get the main cam logic first before trying anything crazy. THanks for the vid! – user3871 Jul 30 '13 at 18:22
  • A simpler way to position camera is using translated `Graphics`. Saves doing calculations manually, and you can use the same coordinate system for all objects on the game screen. – kiheru Jul 30 '13 at 18:27

2 Answers2

3

1) You have not honored the paint chain by calling super.paintComponent(g) in paintComponent(..):

@Override
protected void paintComponent(Graphics g) {    
    super.paintComponent(g);

    //do drawing here
}

As per Java docs:

protected void paintComponent(Graphics g)

Further, if you do not invoker super's implementation you must honor the opaque property, that is if this component is opaque, you must completely fill in the background in a non-opaque color. If you do not honor the opaque property you will likely see visual artifacts.

2) Also notice the @Override annotation I added and the fact that I changed public modifier to protected as thats what the access level is defined as in the implementation class which we should keep unless for a specific reason.

3) Also Swing uses Keybindings have a read on How to Use Key Bindings

4) Also have a read on Concurrency in Swing specifically on The Event Dispatch Thread which dictates all swing components be created on EDT via SwingUtillities.invokeXXX(..) block:

SwingUtilities.invokeLater(new Runnable() {
   @Override
    public void run() {
         //create and manipulate swing components here
    }
});

5) You extend the JFrame class and create an instance, this is not what you want rather remove the extends JFrame from the class declaration:

public class MainSSCCE extends JFrame { //<-- Remove extends JFrame

    public MainSSCCE() {
       JFrame f = new JFrame("Camera Test");//<-- instance is created here
    }
}
Community
  • 1
  • 1
David Kroukamp
  • 36,155
  • 13
  • 81
  • 138
  • Thanks David. I've updated the code above with logic to detect when to draw the sprite so long that it's in the Cam's view. Is that correct? Also, now that I've added the `super.paintComponent(g);`, pressing right or left now moves the gray background of the JPanel. Is that supposed to happen? See above for code edit and picture – user3871 Jul 30 '13 at 18:31
  • @Growler well if I understand the Camera is moving around the `JPanel` into *areas* which were previously not seen/not drawn this I think a `repaint()` call on `JPanel` is necessary after each movement has taken place i.e after key event – David Kroukamp Jul 30 '13 at 18:35
  • You can see above that the `repaint()` is called after key is pressed in `keyPressed(KeyEvent e)` – user3871 Jul 30 '13 at 18:37
  • @Growler sorry I missed that, let me test. Please next time post an [SSCCE](http://sscce.org) – David Kroukamp Jul 30 '13 at 18:39
  • Well I do increment/decrement the `camX, camY` when I press the arrow keys in order to move the camera... which is the actual JPanel coordinates. And since that area is painted as gray, it will move it around... but It's supposed to stay stationary as the JPanel "moves around" on the World – user3871 Jul 30 '13 at 18:39
  • maybe try `fillRect(0,0,camW,camH)` – David Kroukamp Jul 30 '13 at 18:41
  • @Growler Sorry cant test your code missing `World` and `Sprite` and `Player` classes. Please post an SSCCE (link in above comment) for better help sooner. – David Kroukamp Jul 30 '13 at 18:44
  • I've added an SSCCE above – user3871 Jul 30 '13 at 18:45
  • @Growler its getting late,, but what I can say is the `JPanel` is not moving around the grey block is change `fillRect(camX,camY,camW,camH)` to `fillRect(0,0,camW,camH)`. Also ensure `JPanel` is bigger than the `JFrame` than move the `JPanel` via `setLocation` and dont forget to call `revalidate()` and than `repaint()` tom reflect changes to visible moved components – David Kroukamp Jul 30 '13 at 18:59
  • Thank you. Just real quickly... the camera logic overall is on the right track, right? – user3871 Jul 30 '13 at 19:01
  • besides above comment looks okay but as I said its getting late so i might have missed something – David Kroukamp Jul 30 '13 at 19:04
2

Your world is a virtual area larger than the screen (or your jpanel for what matters). All objects' positions are relative to the world. Let's call them absolute coordinates.

Your camera is a small rectangular portion of the world (your panel). By moving it you see different world portions. If you could move the camera like in the post you link to, then at some point you would not be able to see neither the player nor the other sprite.

Since your goal is to keep the player centered on the screen what does this mean for our world? This means that the player and the camera are moving together in relation to the world.

Given the above it does not make sense to draw a camera sprite as in your first screenshot. The camera sprite should be either invisible or it should be drawn in the same position with the player sprite. Nor it makes sense to change the camera's absolute coordinates without changing the player's. Those two are moving together. (take this into account in your keyPressed() methods)

Now when you are drawing, you are drawing from the camera's point of view (or in other words in the camera's coordinate system). From that point of view, the camera always see a rectangle of (0, 0, cameraWidth, cameraHeight). That's what you should use when clearing the area with gray color. This will fix your moving background issue. Since camera and player always have the same absolute coordinates the player will always be in the same place (this is what we want). The rest of the sprites will be seen relative to camera.

For each one of them you translate them in the camera's coordinate system when you do (sprite.x - cam.x) and (sprite.y - cam.y). Since they are translated, you only need to check if they are inside the camera's rectangle (0, 0, cameraWidth, cameraHeight). If they are you go ahead and draw them. If not ignore them.

I hope that helps

Note: cameraWidth, cameraHeight are your jpanel's dimensions

c.s.
  • 4,786
  • 18
  • 32