Reputation: 91
As a self-project, I'm trying to make the game 'Asteroids'.
Currently, I'm stuck on trying to figure out how to make it so the lasers fired from my ship appear from the tip of the ship. So far, I've tried experimenting with using the Shape
object's .getBounds2D().getX()
methods, but because getBounds2D()
draws a rectangle around the polygon, the lasers end up appearing from the corner of the imaginary 'box' around my Polygon ship
.
Here's a gif of what I have so far.
Is there a way to 'get' a specific point from a Shape object; where, in this case, that specific point is the tip of the ship.
Main Class:
public class AsteroidGame implements ActionListener, KeyListener{
public static AsteroidGame game;
public Renderer renderer;
public boolean keyDown = false;
public int playerAngle = 0;
public boolean left = false;
public boolean right = false;
public boolean go = false;
public boolean back = false;
public boolean still = true;
public double angle = 0;
public int turnRight = 5;
public int turnLeft = -5;
public Shape transformed;
public ArrayList<Laser> lasers;
public ArrayList<Shape> transformedLasers;
public final int WIDTH = 1400;
public final int HEIGHT = 800;
public Ship ship;
public Rectangle shipHead;
public Shape shipHeadTrans;
public Point headPoint;
public AffineTransform transform = new AffineTransform();
public AffineTransform lasTransform = new AffineTransform();
public AffineTransform headTransform = new AffineTransform();
public AsteroidGame(){
JFrame jframe = new JFrame();
Timer timer = new Timer(20, this);
renderer = new Renderer();
jframe.add(renderer);
jframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jframe.setSize(WIDTH, HEIGHT);
jframe.setVisible(true);
jframe.addKeyListener(this);
jframe.setResizable(false);
int xPoints[] = {800, 780, 800, 820};
int yPoints[] = {400, 460, 440, 460};
//(800, 400) is the initial location of the 'tip' of the ship'.
headPoint = new Point(800, 400);
lasers = new ArrayList<Laser>();
transformedLasers = new ArrayList<Shape>();
ship = new Ship(xPoints, yPoints, 4, 0);
transformed = transform.createTransformedShape(ship);
shipHead = new Rectangle(headPoint);
shipHeadTrans = transform.createTransformedShape(shipHead);
//shipHeadTrans.getBounds2D().
timer.start();
}
public void repaint(Graphics g){
g.setColor(Color.BLACK);
g.fillRect(0, 0, WIDTH, HEIGHT);
Graphics2D g2d = (Graphics2D)g;
//drawing the ship
g2d.setColor(Color.WHITE);
g2d.draw(transformed);
//drawing lasers
g2d.setColor(Color.RED);
for (int i = 0; i < transformedLasers.size(); i++){
System.out.println(i);
g2d.draw(transformedLasers.get(i));
}
}
public void actionPerformed(ActionEvent arg0) {
// TODO Auto-generated method stub
/*The for if and else if statements are just to send the ship
* to the other side of the canvas if it ever leaves the screen
*/
if (transformed.getBounds2D().getMinX() > WIDTH){
double tempAng = ship.getAng();
double diff = 90-tempAng;
transform.rotate(Math.toRadians(diff), ship.getCenterX(), ship.getCenterY());
transform.translate(0,WIDTH);
transform.rotate(Math.toRadians(-diff), ship.getCenterX(), ship.getCenterY());
}
else if (transformed.getBounds2D().getX() < 0){
double tempAng = ship.getAng();
double diff = 90-tempAng;
transform.rotate(Math.toRadians(diff), ship.getCenterX(), ship.getCenterY());
transform.translate(0,-WIDTH);
transform.rotate(Math.toRadians(-diff), ship.getCenterX(), ship.getCenterY());
}
else if (transformed.getBounds2D().getY() > HEIGHT){
double tempAng = ship.getAng();
double diff = 180-tempAng;
transform.rotate(Math.toRadians(diff), ship.getCenterX(), ship.getCenterY());
transform.translate(0,HEIGHT);
transform.rotate(Math.toRadians(-diff), ship.getCenterX(), ship.getCenterY());
}
else if (transformed.getBounds2D().getY() < 0){
double tempAng = ship.getAng();
double diff = 180-tempAng;
transform.rotate(Math.toRadians(diff), ship.getCenterX(), ship.getCenterY());
transform.translate(0,-HEIGHT);
transform.rotate(Math.toRadians(-diff), ship.getCenterX(), ship.getCenterY());
}
if (right){
ship.right();
//rotating the ship
transform.rotate(Math.toRadians(turnRight), ship.getCenterX(), ship.getCenterY());
//rotating the 'tip' of the ship.
headTransform.rotate(Math.toRadians(turnRight), ship.getCenterX(), ship.getCenterY());
}
else if (left){
ship.left();
//rotating the ship
transform.rotate(Math.toRadians(turnLeft), ship.getCenterX(), ship.getCenterY());
//rotating the 'tip' of the ship
headTransform.rotate(Math.toRadians(turnLeft), ship.getCenterX(), ship.getCenterY());
}
if (go){
ship.go();
}
else if (back){
ship.reverse();
}
//moving and shaping each individual laser that had been shot
for (int i = 0; i < transformedLasers.size(); i++){
lasers.get(i).move();
lasTransform = new AffineTransform();
lasTransform.rotate(Math.toRadians(lasers.get(i).getAng()), transformed.getBounds2D().getX(), transformed.getBounds2D().getY());
transformedLasers.set(i, lasTransform.createTransformedShape(lasers.get(i)));
}
//moving the ship
ship.move();
//moving the 'tip'
shipHead.y -= ship.getSpeed();
transformed = transform.createTransformedShape(ship);
shipHeadTrans = headTransform.createTransformedShape(shipHead);
renderer.repaint();
}
//defining a new laser
public void fireLaser(){
Laser tempLaser = new Laser((int)transformed.getBounds2D().getX(), (int)transformed.getBounds2D().getY(), 5, 10, ship.getAng());
lasers.add(tempLaser);
lasTransform = new AffineTransform();
lasTransform.rotate(Math.toRadians(ship.getAng()), transformed.getBounds2D().getX(), transformed.getBounds2D().getY());
transformedLasers.add(lasTransform.createTransformedShape(tempLaser));
}
public static void main(String[] args){
game = new AsteroidGame();
}
@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
if (e.getKeyCode() == KeyEvent.VK_RIGHT){
right = true;
keyDown = true;
}else if (e.getKeyCode() == KeyEvent.VK_LEFT){
left = true;
keyDown = true;
}
else if (e.getKeyCode() == KeyEvent.VK_UP){
go = true;
}
else if (e.getKeyCode() == KeyEvent.VK_DOWN){
back = true;
}
//fire laser
if (e.getKeyCode() == KeyEvent.VK_SPACE){
fireLaser();
}
}
@Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
if (e.getKeyCode() == KeyEvent.VK_RIGHT){
right = false;
}
if (e.getKeyCode() == KeyEvent.VK_LEFT){
left = false;
}
if (e.getKeyCode() == KeyEvent.VK_UP){
go = false;
}
if (e.getKeyCode() == KeyEvent.VK_DOWN){
back = false;
}
still = true;
keyDown = false;
}
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
Ship Class (I don't think it's relevant though)
package asteroidGame;
import java.awt.Polygon;
import java.util.Arrays;
public class Ship extends Polygon{
/**
*
*/
private double currSpeed = 0;
private static final long serialVersionUID = 1L;
public double angle;
public int[] midX;
public int[] midY;
public Ship(int[] x, int[] y, int points, double angle){
super(x, y, points);
midX = x;
midY = y;
this.angle= angle;
}
public void right(){
angle += 5;
}
public void left(){
angle -= 5;
}
public void move(){
for (int i = 0; i < super.ypoints.length; i++){
super.ypoints[i] -= currSpeed;
//System.out.println(super.ypoints[i]);
//System.out.println(super.xpoints[i]);
}
//System.out.println(Arrays.toString(super.ypoints));
}
public double getSpeed(){
return currSpeed;
}
public void reverse(){
if (currSpeed > -15) currSpeed -= 0.2;
}
public void go(){
if (currSpeed < 25) currSpeed += 0.5;
}
public int getCenterX(){
return super.xpoints[2];
}
public int getCenterY(){
return super.ypoints[2];
}
public double getAng(){
return angle;
}
public void test(){
for (int x = 0; x < super.ypoints.length; x++){
super.ypoints[x] += 1000;
}
}
/*
public void decrement(){
if(currSpeed == 0){}
else if (currSpeed > 0 && currSpeed < 15){
currSpeed -= 0.05;
}
else if (currSpeed < 0 && currSpeed > -15){
currSpeed += 0.05;
}
System.out.println("losing speed");
}
*/
}
Laser Class (I don't think this is relevant either, but here ya go.)
package asteroidGame;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
public class Laser extends Rectangle{
private double angle;
public Laser(int x, int y , int width, int height, double ang){
super(x, y, width, height);
angle = ang;
Rectangle tst = new Rectangle();
}
public void move(){
super.y -= 35;
}
public double getAng(){
return angle;
}
public boolean intersects (Rectangle2D r){
//if intersects
if (super.intersects(r)){
return true;
}
else{
return false;
}
}
}
I was thinking of maybe turning the the Shape object transformed
back into a Polygon to get the point, but I'm not sure how or if that would work.
Upvotes: 1
Views: 1514
Reputation: 37845
You can use AffineTransform.transform(Point2D, Point2D)
to transform a single point on your polygon.
Things would be a lot simpler for you if instead of trying to move the ship by using a rotation transform you kept a single (x,y)
location of where the ship is. You'd move the ship's location in move()
instead of trying to translate the polygon. Then when you want to paint the ship you e.g. do:
// Optionally copying the Graphics so the
// transform doesn't affect later painting.
Graphics2D temp = (Graphics2D) g2d.create();
temp.translate(ship.locX, ship.locY);
temp.rotate(ship.angle);
temp.draw(ship);
To move a point based on speed you can do this to find the movement vector:
double velX = speed * Math.cos(angle);
double velY = speed * Math.sin(angle);
locX += timeElapsed * velX;
locY += timeElapsed * velY;
That is essentially a conversion from polar to Cartesian coordinates. The x and y velocities are the legs of a triangle whose hypotenuse is speed
and whose known angle is angle
:
/|
/ |
/ |
/ |
speed / |
/ |
/ |velY
/ angle |
/)_______|
velX
There's an example of doing movement this way in an answer of mine here: https://stackoverflow.com/a/43692434/2891664.
For your comments:
Are you saying that, unlike my initial move function, just to make
ship
hold a single point, and thus I would only translate that instead?
More or less, yes. You'd still have a polygon to hold the ship's shape, but the points on the polygon would be relative to (0,0)
.
Suppose the following definitions:
(x,y)
point on the polygon is pi
. (In other words, one of p0
, p1
, p2
and p3
.)(x,y)
coordinates of the translation are T
Then, after translating the Graphics2D
, each pi
coordinate becomes pi+T
on the panel. So if your polygon points are defined relative to (0,0)
then translating to the ship's (locX,locY)
will move the polygon to a location relative to (locX,locY)
.
It could be simplest then to define the point which is the tip of the polygon as being (0,0)
so that after the translation the tip of the ship is the ship's location:
// Your original points:
int xPoints[] = {800, 780, 800, 820};
int yPoints[] = {400, 460, 440, 460};
// Become these points relative to (0,0):
int xPoints[] = {0, -20, 0, 20};
int yPoints[] = {0, 60, 40, 60};
And to e.g. start the ship in the same place, you would initialize its location to (800,400)
.
I was thinking about this again and realized the rotation is a little more complicated, because you probably don't want to rotate the ship around the tip. You probably want to rotate the ship around its center.
So, here's an MCVE demonstrating how to do all of this.
package mcve.game;
import javax.swing.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.GraphicsConfiguration;
import java.util.Set;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
public class MovementExample implements ActionListener {
public static void main(String[] args) {
SwingUtilities.invokeLater(MovementExample::new);
}
final int fps = 60;
final int period = 1000 / fps;
final JFrame frame;
final GamePanel panel;
final Controls controls;
final Ship ship;
final List<Bullet> bullets = new ArrayList<>();
MovementExample() {
frame = new JFrame("Movement Example");
Dimension size = getMaximumWindowSize(frame);
size.width /= 2;
size.height /= 2;
frame.setPreferredSize(size);
panel = new GamePanel();
frame.setContentPane(panel);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
controls = new Controls();
ship = new Ship(panel.getWidth() / 2,
panel.getHeight() / 2);
new Timer(period, this).start();
}
@Override
public void actionPerformed(ActionEvent e) {
double secondsElapsed = 1.0 / fps;
ship.update(secondsElapsed);
bullets.forEach(b -> b.update(secondsElapsed));
Rectangle bounds = panel.getBounds();
bullets.removeIf(b -> !bounds.contains(b.locX, b.locY));
panel.repaint();
}
class GamePanel extends JPanel {
GamePanel() {
setBackground(Color.WHITE);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
if (ship != null) {
ship.draw(g2);
}
bullets.forEach(b -> b.draw(g2));
g2.dispose();
}
}
abstract class AbstractGameObject {
double maxSpeed;
double rotationAngle;
double locX;
double locY;
double velX;
double velY;
AbstractGameObject(double initialX, double initialY) {
locX = initialX;
locY = initialY;
}
abstract void update(double secondsElapsed);
abstract void draw(Graphics2D g2);
}
class Ship extends AbstractGameObject {
Polygon shape;
double rotationRate;
Ship(double initialX, double initialY) {
super(initialX, initialY);
maxSpeed = 128; // pixels/second
rotationAngle = Math.PI * 3 / 2;
rotationRate = (2 * Math.PI) / 2; // radians/second
int xPoints[] = {0, -20, 0, 20};
int yPoints[] = {0, 60, 40, 60};
shape = new Polygon(xPoints, yPoints, 4);
}
Point2D.Double getTip() {
Point2D.Double center = getCenter();
// The tip is at (0,0) and it's already centered
// on the x-axis origin, so the distance from the
// tip to the center is just center.y.
double distance = center.y;
// Then find the location of the tip, relative
// to the center.
double tipX = distance * Math.cos(rotationAngle);
double tipY = distance * Math.sin(rotationAngle);
// Now find the actual location of the center.
center.x += locX;
center.y += locY;
// And return the actual location of the tip, relative
// to the actual location of the center.
return new Point2D.Double(tipX + center.x, tipY + center.y);
}
Point2D.Double getCenter() {
// Returns the center point of the ship,
// relative to (0,0).
Point2D.Double center = new Point2D.Double();
for (int i = 0; i < shape.npoints; ++i) {
center.x += shape.xpoints[i];
center.y += shape.ypoints[i];
}
center.x /= shape.npoints;
center.y /= shape.npoints;
return center;
}
@Override
void update(double secondsElapsed) {
// See my answer here: https://stackoverflow.com/a/43692434/2891664
// for a discussion of why this logic is the way it is.
double speed = 0;
if (controls.isUpHeld()) {
speed += maxSpeed;
}
if (controls.isDownHeld()) {
speed -= maxSpeed;
}
velX = speed * Math.cos(rotationAngle);
velY = speed * Math.sin(rotationAngle);
locX += secondsElapsed * velX;
locY += secondsElapsed * velY;
double rotation = 0;
if (controls.isLeftHeld()) {
rotation -= rotationRate;
}
if (controls.isRightHeld()) {
rotation += rotationRate;
}
rotationAngle += secondsElapsed * rotation;
// Cap the angle so it can never e.g. get so
// large that it loses precision.
if (rotationAngle > 2 * Math.PI) {
rotationAngle -= 2 * Math.PI;
}
if (controls.isFireHeld()) {
Point2D.Double tipLoc = getTip();
Bullet bullet = new Bullet(tipLoc.x, tipLoc.y, rotationAngle);
bullets.add(bullet);
}
}
@Override
void draw(Graphics2D g2) {
Graphics2D copy = (Graphics2D) g2.create();
copy.setColor(Color.RED);
// Translate to the ship's location.
copy.translate(locX, locY);
// Rotate the ship around its center.
Point2D.Double center = getCenter();
// The PI/2 offset is necessary because the
// polygon points are defined with the ship
// already vertical, i.e. at an angle of -PI/2.
copy.rotate(rotationAngle + (Math.PI / 2), center.x, center.y);
copy.fill(shape);
}
}
class Bullet extends AbstractGameObject {
Ellipse2D.Double shape = new Ellipse2D.Double();
Bullet(double initialX, double initialY, double initialRotation) {
super(initialX, initialY);
maxSpeed = 512;
rotationAngle = initialRotation;
velX = maxSpeed * Math.cos(rotationAngle);
velY = maxSpeed * Math.sin(rotationAngle);
double radius = 3;
shape.setFrame(-radius, -radius, 2 * radius, 2 * radius);
}
@Override
void update(double secondsElapsed) {
locX += secondsElapsed * velX;
locY += secondsElapsed * velY;
}
@Override
void draw(Graphics2D g2) {
Graphics2D copy = (Graphics2D) g2.create();
copy.setColor(Color.BLACK);
copy.translate(locX, locY);
copy.fill(shape);
}
}
// See https://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html
class Controls {
final Set<Integer> keysHeld = new HashSet<>();
Controls() {
bind(KeyEvent.VK_A, "left");
bind(KeyEvent.VK_D, "right");
bind(KeyEvent.VK_W, "up");
bind(KeyEvent.VK_S, "down");
bind(KeyEvent.VK_SPACE, "fire");
}
boolean isLeftHeld() { return keysHeld.contains(KeyEvent.VK_A); }
boolean isRightHeld() { return keysHeld.contains(KeyEvent.VK_D); }
boolean isUpHeld() { return keysHeld.contains(KeyEvent.VK_W); }
boolean isDownHeld() { return keysHeld.contains(KeyEvent.VK_S); }
boolean isFireHeld() { return keysHeld.contains(KeyEvent.VK_SPACE); }
void bind(int keyCode, String name) {
bind(keyCode, name, true);
bind(keyCode, name, false);
}
void bind(int keyCode, String name, boolean isOnRelease) {
KeyStroke stroke = KeyStroke.getKeyStroke(keyCode, 0, isOnRelease);
name += isOnRelease ? ".released" : ".pressed";
panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(stroke, name);
panel.getActionMap()
.put(name, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (isOnRelease) {
keysHeld.remove(keyCode);
} else {
keysHeld.add(keyCode);
}
}
});
}
}
// This returns the usable size of the display which
// the JFrame resides in, as described here:
// http://docs.oracle.com/javase/8/docs/api/java/awt/GraphicsEnvironment.html#getMaximumWindowBounds--
static Dimension getMaximumWindowSize(JFrame frame) {
GraphicsConfiguration config = frame.getGraphicsConfiguration();
Dimension size = config.getBounds().getSize();
Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(config);
size.width -= insets.left + insets.right;
size.height -= insets.top + insets.bottom;
return size;
}
}
There are other ways the tip of the ship could be calculated, but the way I did it in the MCVE is this:
(0,0)
.(0,0)
so this is just the y-coordinate of the center.(x,y)
location of the tip, relative to the center. This is done in a very similar way to the figure above for speed and velocity, but the hypotenuse is the distance between the center and the tip of the ship.It could also all be done with an AffineTransform
, similar to what you are doing in the code in the question, but you'd set it on every update. Something like this:
AffineTransform transform = new AffineTransform();
@Override
void update(double secondsElapsed) {
...
// Clear the previous translation and rotation.
transform.setToIdentity();
// Set to current.
transform.translate(locX, locY);
Point2D.Double center = getCenter();
transform.rotate(rotationAngle + (Math.PI / 2), center.x, center.y);
if (controls.isFireHeld()) {
Point2D.Double tip = new Point2D.Double(0, 0);
transform.transform(tip, tip);
Bullet bullet = new Bullet(tip.x, tip.y, rotationAngle);
bullets.add(bullet);
}
}
You could still use a transform to do calculations that way, but you don't end up with any strangeness from depending on the transform for movement. (In the code in the question, the ship is e.g. only ever moved along the y-axis. The apparent sideways movement is due to the series of rotation concatenations.)
Upvotes: 2