0

When the game is first started, the player is spawned, looking in direction 0 (right). Pressing the right key (vk_right) turns the player sprite left, but the direction is being set to send the player in the (what seems like) correct direction. The left and right keys change the character's direction variable, while the up/down keys accelerate/decelerate the character.

I'm not very good at trigonometry, so I probably have a few things wrong here and there (which is why I'm posting this). I can't seem to understand how to get the character to move in the same direction he is 'looking' (direction variable).

Here is the Player.java class:

package rpg2d;

import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import javax.swing.ImageIcon;

public class Player implements KeyListener{

    private double x,y,direction;
    private double speed;
    private final int max_speed;
    private int hp,lives;
    private Image img;
    //other variables

    public Player(int x, int y, int hp, int lives, String imgpath) {
        this.x = x;
        this.y = y;
        this.hp = hp;
        this.lives = lives;
        img = new ImageIcon(this.getClass().getResource(imgpath)).getImage();
        //loads the player image from the string path
        max_speed = 6;
        speed = 0;
        direction = 0;
    }

    //returns the direction the player is 'facing' as an int
    public int getDirection() {
        return (int)direction;
    }

    //updates the player's location
    public void move() {
        x += speed * Math.cos(-1*direction);
        y += speed * Math.sin(-1*direction);
    }

    @Override
    public void keyPressed(KeyEvent e) {
        int key = e.getKeyCode();
        if(key == KeyEvent.VK_LEFT){
            turn(.6);
            //this is meant to turn the player int a clockwise direction by 0.6
        } else if (key == KeyEvent.VK_RIGHT){
            turn(-.6);
            //counterclockwise by 0.6
        }
        if (key == KeyEvent.VK_DOWN){
            speed -= 0.3;
            if(speed < 0) speed = 0;
            //decelerate until stopped
        } else if (key == KeyEvent.VK_UP){
            speed += 0.3;
            if(speed > max_speed) speed = max_speed;
            //accelerates until it hits maximum speed
        }
    }

    private void turn(double degrees) {
        direction += degrees;
        if(direction > 180) direction -= 180;
        else if(direction < -180) direction += 180;
        /* I honestly don't know whether 180 and -180 are the max and min
         * for Polar Coordinates, so this could be a problem.
         */
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    @Override
    public void keyTyped(KeyEvent arg0) {
    }

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

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

    public double getWidth() {
        return img.getWidth(null);
    }

    public double getHeight() {
        return img.getHeight(null);
    }

    public Image getImg() {
        return img;
    }

}

Here is the main game class:

package rpg2d;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Main {

    boolean gameRunning = true;
    Graphics2D g;
    JFrame frame;
    JPanel screen;
    Player player;

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

    @SuppressWarnings("serial")
    public Main() {
        frame = new JFrame("2D RPG Test");
        frame.setSize(800,600);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        player = new Player(10,10,100,3,"/img/player.png");
        screen = new JPanel() {
            @Override
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                Graphics2D g2d = (Graphics2D)g;
                AffineTransform trans = new AffineTransform();
                AffineTransform old = new AffineTransform();
                trans.setToIdentity();
                trans.rotate(Math.toRadians(player.getDirection()),player.getWidth()/2,player.getHeight()/2);
                g2d.setTransform(trans);
                g2d.drawImage(player.getImg(), player.getX(), player.getY(), (int)player.getWidth(), (int)player.getHeight(), null);
                g2d.setTransform(old);
                g2d.drawString("X: " + player.getX() + " Y: " + player.getY(), 5,10);
                trans.setToIdentity();
            }
        };

        frame.add(screen);
        frame.setVisible(true);
        screen.addKeyListener(player);
        screen.requestFocus();
        Thread t = new Thread(new Runnable() {
            public void run() {
                gameLoop();
            }
        });
        t.setDaemon(false);
        t.start();
    }

    public void update() {
        player.move();
    }

    public void gameLoop() {
        final int FPS = 60;
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            public void run() {
                update();
                screen.repaint();
            }
        }, 0, FPS);
    }
}
Levi
  • 53
  • 2
  • 8
  • If this were my question, I'd get rid of the rant at the bottom as it's distracting and serves no purpose. The question would be much better served by your taking the time to describe your code in greater detail so that we could understand it. If I could understand your code at a glance, I'd likely have given you an answer by now. As an aside, it's usually not safe to use a java.util.Timer with a Swing application. Better to use a javax.swing.Timer. – Hovercraft Full Of Eels Jul 04 '14 at 01:27
  • Sorry about the rant. It's just that every time I post anything like this, I end up getting those kind of responses. I'll update the post with explanations of the code. – Levi Jul 04 '14 at 01:31
  • I would say the problem is with your image, not your code. When I created a simple arrow shape at runtime, point to the right, it worked fine. Consider posting a copy of your image – MadProgrammer Jul 04 '14 at 01:33
  • Please understand that when you post stuff here for help, you are getting free help from volunteers. We'll try to answer your question, but we'll also try to help with any other problems with your code that we might see. Myself, I'd be thankful for that extra effort that you're getting at no cost. For instance, I'd tell you also to avoid using KeyListeners and instead to favor Key Bindings, and to avoid giving your class a Graphics or Graphics2D field as that can be a dangerous thing to do and risks your throwing a NPE. – Hovercraft Full Of Eels Jul 04 '14 at 01:33
  • 1
    You should avoid using `Thread` and `java.util.Timer` as Swing is not Thread safe, it is possible that while you are calling `update`, Swing is painting, which could cause dirty paints/updates. Consider using a `javax.swing.Timer` or create a offscreen buffer onto which you can paint and then push to the UI through a synchronized mechanism – MadProgrammer Jul 04 '14 at 01:35
  • I'd also have a rant about `KeyListener`s...but you don't seem to be receptive to friendly advice ;) – MadProgrammer Jul 04 '14 at 01:36
  • Since you're extending `JPanel`, override [`getPreferredSize()`](http://stackoverflow.com/a/7229662/230513) and `pack()` the enclosing `Window`. – trashgod Jul 04 '14 at 01:41
  • Sorry, again. The 'rant' wasn't explained properly. In previous instances where people complained about my using a JPanel, no one ever took time to explain why it was such a dirty thing to do. I understand you are all just volunteers, and I'm thankful for the help. When I say things like that (I know you're not psychic; I just forget I'm on the internet sometimes) I tend to mean that I don't want non-constructive criticisms. Some people tend to complain without explaining why they think it is wrong. The image is just a 17x17 placeholder sprite. Nearly a circle with a straight line on side. – Levi Jul 04 '14 at 01:45
  • I have no issue with you using `JPanel`...I know some people "like" to use `Canvas` as they can get access to the `BufferStrategy` and "control" the paint process, but to be honest, unless you're doing some really complex stuff, it's just another level of complexity - IMHO :P. It could also be that you are mixing `Thread`, `java.util.Timer` and Swing...that's not going to end well ;) – MadProgrammer Jul 04 '14 at 02:15
  • Honestly, every time I search google for a good java 2d game programming tutorial, I always end up with one showing me to use JPanel. Maybe I'm just using the wrong keywords, but anyways, what is a good way to push a bufferedImage to the JFrame, or is that even possible? – Levi Jul 04 '14 at 03:32
  • Apart from the user of `Thread` and `java.util.Timer`, your current approach is find. If you wanted to use some kind "double buffering" technique, you would need to create two (or more) `BufferedImages`, one would be the "active" buffer, which is what get's painted to the screen (via the `paintComponent` method probably). When you need to update the "active" buffer, you would first paint what you want to one of the offscreen buffers and in a synchronized block, paint that to the the active buffer... – MadProgrammer Jul 04 '14 at 03:39

2 Answers2

3

I made several improvements:

  1. When you do Cos and Sin, you must convert the degrees to Radians, as those functions take Radians (radians are a different unit for degrees).
  2. I changed the - and + of the right and left key. It's conventional that counter clockwise is increasing not the other way around ;)
  3. I modified the turn method as it had some slight mistakes (-180 is 0 in trig)

Comment if there are more errors and I'll take another look :) Good luck!

public void move() {
    x += speed * Math.cos(Math.toRadians(direction));
    y += speed * Math.sin(Math.toRadians(direction));
}

@Override
public void keyPressed(KeyEvent e) {
    int key = e.getKeyCode();
    if(key == KeyEvent.VK_LEFT){
        turn(-.6);
        //this is meant to turn the player int a clockwise direction by 0.6
    } else if (key == KeyEvent.VK_RIGHT){
        turn(.6);
        //counterclockwise by 0.6
    }
   .....//rest of code}

private void turn(double degrees) {
    direction += degrees;
    if(direction > 180) direction = 180;
    else if(direction < 180) direction = 0;
    }
Dean Leitersdorf
  • 1,303
  • 1
  • 11
  • 22
  • You know, you could get rid of the `Math.cos` and `Math.sin` if you translated the `Graphics` context to the local context of the player... – MadProgrammer Jul 04 '14 at 03:24
  • I'm adding this right now to test it; but something I noticed: if direction is less than 180, it will get set to 0, which means it will always be 0 unless it is 180. also, what is the maximum for polar coordinates system (180 or 360)? – Levi Jul 04 '14 at 03:29
  • Polar coords: 360 degrees if I remember correctly. A - sign adds 180 – Dean Leitersdorf Jul 04 '14 at 03:57
  • Okay, I'm not certain what is wrong. No exceptions thrown, no crashing, but the sprite is rotating around the top left (0,0) of the panel. I don't understand this.. – Levi Jul 04 '14 at 04:35
  • Agh. Ignore that last comment. Apparently I forgot the last two arguments in the rotate method. – Levi Jul 04 '14 at 04:40
2

Adding to Dean's efforts...

In the absence of an image, I create a simple arrow shape, which starts out pointing right...

public Player(int x, int y, int hp, int lives, String imgpath) {
    this.x = x;
    this.y = y;
    this.hp = hp;
    this.lives = lives;
    //img = new ImageIcon(this.getClass().getResource(imgpath)).getImage();

    BufferedImage arrow = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2d = arrow.createGraphics();
    g2d.setColor(Color.RED);
    g2d.drawLine(0, 0, 16, 8);
    g2d.drawLine(16, 8, 0, 16);
    g2d.drawLine(0, 16, 0, 0);
    g2d.dispose();
    img = arrow;

    max_speed = 6;
    speed = 0;
    direction = 0;
}

Now, this rendered fine to start with.

When I started moving the player around I had some issues, so I updated the paint code slightly.

@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D) g.create();
    AffineTransform trans = new AffineTransform();
    trans.setToIdentity();
    trans.translate(player.x, player.y);
    trans.rotate(Math.toRadians(player.getDirection()), player.getWidth() / 2, player.getHeight() / 2);
    g2d.setTransform(trans);
    g2d.drawImage(player.getImg(), 0, 0, (int) player.getWidth(), (int) player.getHeight(), null);
    g2d.dispose();

    g2d = (Graphics2D) g.create();
    g2d.drawString("X: " + player.getX() + " Y: " + player.getY(), 5, 10);
    g2d.dispose();
    trans.setToIdentity();
}

Essentially, I made a copy of the Graphics context...

Graphics2D g2d = (Graphics2D) g.create();

Using the AffineTransform, I translated the context to the player's current position, this simplified the rotation code as I didn't then need to offset the anchor point...

trans.translate(player.x, player.y);
trans.rotate(Math.toRadians(player.getDirection()), player.getWidth() / 2, player.getHeight() / 2);

This also means that it's easier to draw the image...

g2d.drawImage(player.getImg(), 0, 0, (int) player.getWidth(), (int) player.getHeight(), null);

The beauty of all this is the fact that all the changes are contextual to the copy of the Graphics context, you don't need to "undo" it all...Just don't forget to dispose of the copy when you finished.

I'd also discourage you from using KeyListener and favor the Key bindings API instead.

I also replaced your Thread and java.util.Timer with a javax.swing.Timer instead...

public void gameLoop() {
    Timer timer = new Timer(16, new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
            update();
            screen.repaint();
        }
    });
    timer.start();
}

Also, beware, the last parameter to scheduleAtFixedRate is the number of milliseconds between calls, so you current code is generating about 16fps. 60fps is roughly 16 milliseconds...

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • From the comments on the question as well as using dean's information, I was able to tweak it to get almost exactly what you have now. (The only difference is when you translate and rotate; where I used `af.translate(-player.getWidth()/2, -player.getHeight()/2); af.rotate(Math.toRadians(player.getDirection()),player.getX() - player.getWidth()/2,player.getY() - player.getHeight()/2);` – Levi Jul 04 '14 at 04:51
  • @Levi Yes, I agree, I just wanted to add the paint update and suggestions for the timer and key bindings on top, because they will haunt you if you're not careful ;) – MadProgrammer Jul 04 '14 at 04:53
  • I do have a question that has just come up though. Key Bindings seems to only allow for catching of one key at a time; pressing more than one cancels out the first key. Is there a reason for this, or is there a better way to deal with key bindings? – Levi Jul 04 '14 at 04:55
  • You'll get similar issues with `KeyListener`. Basically what happens is a key event is triggered when you first press the key, then based on the OS, the key will repeat, at first with a small delay, then shorter delays after that. What people generally do is have a "press" and "release" flag and then based on the value of the flag, update the state accordingly. This generally cleaner and more robust and you'll get more consistant results across platforms – MadProgrammer Jul 04 '14 at 05:00
  • And that is perfect. Thank you. It isn't exactly what I expected of key bindings, however it works. – Levi Jul 04 '14 at 05:02
  • 1
    Key bindings don't suffer from the same focus related issues that plague `KeyListener` ;) – MadProgrammer Jul 04 '14 at 05:04