I am trying to make a cryptex program using JavaFX, but sometimes the rendering gets corrupted.
What it should be showing:
However, sometimes the rotation snags it show this:
Why is this happening? What can I do to fix this? I've found lots of other posts saying that JavaFX should handle all of the rendering for you, but forcing a redraw seems like the only solution, and I couldn't figure out how to do that either.
Here is my code:
import java.awt.Image;
import java.util.Timer;
import java.util.TimerTask;
import javafx.application.Application;
import javafx.scene.effect.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.*;
import javafx.stage.Stage;
public class Cryptex extends Application{
private Spool[] spools;
private double radius = 100, perspectiveScale = 0.1;
Stage stage;
Scene scene;
public static void main(String[] args) {
launch();
}
#Override
public void start(Stage primaryStage){
stage = primaryStage;
stage.setTitle("Criptex");
Group root = new Group();
scene = new Scene(root, 550, 400);
stage.setScene(scene);
stage.show();
spools = new Spool[5];
spools[0] = new Spool("jdkwndityc", -150);
spools[1] = new Spool("lqhmnxfgso", -75);
spools[2] = new Spool("usjnzuvbid", 0);
spools[3] = new Spool("ihgkewobde", 75);
spools[4] = new Spool("cdelsprkar", 150);
for (Spool sp : spools){
sp.angle = Math.random()*Math.PI*2;
root.getChildren().add(sp);
}
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
#Override
public void run() {
for (Spool s : spools){
if (s.angle != 0){
if (Math.abs(s.distToAngle(0)) < Math.toRadians(1)){
System.out.println("snap");
s.snapToAngle(0);
for (Spool.Cell c : s.cells){
c.draw(s.angle);
}
}
else {
s.rotate(s.directionToAngle(0));
System.out.println((s.x+150)/75+1 + ": "+Math.round((s.angle/Math.PI)*180));
for (Spool.Cell c : s.cells){
c.draw(s.angle);
}
}
}
}
//System.out.println("--");
}
}, 0, 10);
}
public class Spool extends Group{
double angle;
char[] chars;
Image[] letters;
int x;
Cell[] cells;
public Spool(String charList, int x){
chars = charList.toCharArray();
letters = new Image[chars.length];
this.x = x;
angle = 0;
cells = new Cell[chars.length];
for (int i = 0; i < chars.length; i++){
cells[i] = new Cell(i);
this.getChildren().add(cells[i]);
}
}
public void rotate(double distance){
angle += Math.toRadians(distance);
if (angle >= Math.PI*2){
angle -= Math.PI*2;
}
if (angle < 0){
angle += Math.PI*2;
}
}
//TODO: check if this is way off
public double distToAngle(double angle){
rotate(0);
if ((this.angle > angle && this.angle - angle > Math.PI) ||
(this.angle < angle && angle - this.angle > Math.PI)){
return Math.abs(this.angle - angle);
}
else {
return -Math.abs(this.angle - angle);
}
}
public double closestAngle(){
int closest = 0;
for (int i = 0; i < chars.length; i++){
if (Math.abs(distToAngle(i*((Math.PI*2)/chars.length)))
< Math.abs(distToAngle(closest*((Math.PI*2)/chars.length)))){
closest = i;
}
}
return closest;
}
public boolean snapToAngle(double angle){
if (Math.abs(this.distToAngle(angle)) > Math.toRadians(1)){
if (this.distToAngle(angle) > 0){
this.rotate(-1);
}
else {
this.rotate(1);
}
return false;
}
else if (this.angle != angle){
this.angle = angle;
}
return true;
}
public double indexToAngle(int index){
return ((Math.PI*2)/chars.length)*index;
}
public double perspectiveWidth(double d){
return Math.sqrt(Math.pow(radius, 2) - Math.pow(d, 2)) * perspectiveScale;
}
public double toAngle(int index){
return ((Math.PI*2)/chars.length)*(index) + angle + ((Math.PI*2)/(chars.length*2));
}
public int directionToAngle(double angle){
return (int) Math.signum(this.distToAngle(angle));
}
public class Cell extends Group {
private int index;
PerspectiveTransform pt = new PerspectiveTransform();
public Cell(int index){//, double angle, Stage stage){
this.index = index;
Text text = new Text();
//System.out.println("char: " + String.valueOf(chars[c]));
text.setText(String.valueOf(chars[index]).toUpperCase());
text.setFont(Font.font("Monospaced", FontWeight.BOLD, 36));
text.setFill(Color.BLACK);
text.setX(9);
text.setY(32);
Rectangle rect = new Rectangle(40, 40);
rect.setFill(Color.BEIGE);
rect.setStrokeType(StrokeType.OUTSIDE);
rect.setStrokeWidth(3);
rect.setStroke(Color.BLACK);
this.draw(angle);
this.setCache(true);
this.getChildren().addAll(rect, text);
}
public void draw(double angle){
double cx = stage.getWidth()/2 + x;
double cy = (stage.getHeight()-100)/2;
if (cy + radius*Math.sin(toAngle(index)+angle) <= cy + radius*Math.sin(toAngle(index+1)+angle)){
this.setVisible(true);
pt.setUlx(cx - 20 - perspectiveWidth(radius*Math.sin(toAngle(index)+angle)));
pt.setUrx(cx + 20 + perspectiveWidth(radius*Math.sin(toAngle(index)+angle)));
pt.setLrx(cx + 20 + perspectiveWidth(radius*Math.sin(toAngle(index+1)+angle)));
pt.setLlx(cx - 20 - perspectiveWidth(radius*Math.sin(toAngle(index+1)+angle)));
pt.setUly(cy + radius*Math.sin(toAngle(index)+angle));
pt.setUry(cy + radius*Math.sin(toAngle(index)+angle));
pt.setLry(cy + radius*Math.sin(toAngle(index+1)+angle));
pt.setLly(cy + radius*Math.sin(toAngle(index+1)+angle));
this.setEffect(pt);
}
else {
this.setVisible(false);
}
}
}
}
}
Your issue is the same as the one seen here, using a java.util.Timer directly inside JavaFX is not thread safe.
This is because timer uses it's own background thread, but if you want to do any updates to the GUI, you need to be using the JavaFX GUI thread (a special thread JavaFX creates and handles). Trying to touch the GUI from another thread creates problems like the ones you are seeing.
You can pass changes to JavaFX components to the special JavaFX thread by wrapping the changes in a Platform.runLater block.
The code looks like this:
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
#Override
public void run() {
Platform.runLater(() -> { //Lambda for Runnable
for (Spool s : spools){
if (s.angle != 0){
if (Math.abs(s.distToAngle(0)) < Math.toRadians(1)){
System.out.println("snap");
s.snapToAngle(0);
for (Spool.Cell c : s.cells){
c.draw(s.angle);
}
}
else {
s.rotate(s.directionToAngle(0));
System.out.println((s.x+150)/75+1 + ": "
+ Math.round((s.angle/Math.PI)*180));
for (Spool.Cell c : s.cells){
c.draw(s.angle);
}
}
}
}
});
}
}, 0, 10);
Pure JavaFX Style:
You can also use a JavaFX Timeline and not have to worry about the thread issues:
Timeline timer = new Timeline(new KeyFrame(
Duration.millis(10),
event -> {//Same for loop as above}
));
timer.setCycleCount(Timeline.INDEFINITE);
timer.play();
Related
I'm currently working on an application which uses the draw function to animate "bouncing images" in a JPanel. To accomplish it I had to learn to use threads. When I used them in my code someone recommended that instead of using threads directly I can use stuff like Executor framework and ExecutorService.
Right now my problem is that when adding new images I need to make sure that they don't get created inside each other. When the program detects that they would be intersecting it should wait some amount of time, while all the other threads keep running, so images are still getting moved and drawn in current positions.
What happens however is that when I make one of the threads sleep to wait for a spot to be empty the whole program seems to freeze. The only thing that seems to be running is the image moving function.
Code in a Github Gist
Here is the code:
This is the BouncingImages class
/* NOTE: requires MyImage.java */
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.swing.*;
public class BouncingImages extends JFrame implements ActionListener {
public static void main(String[] args) {
new BouncingImages();
}
static boolean imagesLoaded = true;
JPanel resetPanel = new JPanel();
JPanel runningPanel = new JPanel();
JPanel pausedPanel = new JPanel();
JPanel btnPanel = new JPanel();
public static AnimationPanel animationPanel;
ArrayList <MyImage> imageList = new ArrayList <MyImage>();
private volatile boolean stopRequested = false; //maybe should use AtomicBoolean
boolean isRunning = false;
boolean isReset = true;
ExecutorService service = Executors.newCachedThreadPool();
Future f;
//here is the part of the code responsible for creating a JFrame
BouncingImages() {
//set up button panel
JButton btnStart = new JButton("Start");
JButton btnResume = new JButton("Resume");
JButton btnAdd = new JButton("Add");
JButton btnAdd10 = new JButton("Add 10");
JButton btnStop = new JButton("Stop");
JButton btnReset = new JButton("Reset");
JButton btnExit = new JButton("Exit");
btnStart.addActionListener(this);
btnResume.addActionListener(this);
btnAdd.addActionListener(this);
btnAdd10.addActionListener(this);
btnStop.addActionListener(this);
btnReset.addActionListener(this);
btnExit.addActionListener(this);
resetPanel.add(btnStart);
runningPanel.add(btnAdd);
runningPanel.add(btnAdd10);
runningPanel.add(btnStop);
pausedPanel.add(btnResume);
pausedPanel.add(btnReset);
animationPanel = new AnimationPanel();
resetButtons();
this.add(btnPanel, BorderLayout.SOUTH);
this.add(animationPanel);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.pack(); //since the JPanel is controlling the size, we need pack() here.
this.setLocationRelativeTo(null); //after pack();
this.setVisible(true);
}
//I use different JPanels with designated buttons to make them display different buttons when the program is running, paused or completely restarted.
public void resetButtons() {
btnPanel.updateUI();
if (isReset) {
btnPanel.removeAll();
btnPanel.add(resetPanel, BorderLayout.SOUTH);
} else {
if (isRunning) {
btnPanel.removeAll();
btnPanel.add(runningPanel, BorderLayout.SOUTH);
}
if (!isRunning) {
btnPanel.removeAll();
btnPanel.add(pausedPanel, BorderLayout.SOUTH);
}
}
}
//ActionListener for Buttons
#Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("Start")) {
startAnimation();
isRunning = true;
isReset = false;
resetButtons();
}
if (e.getActionCommand().equals("Resume")) {
startAnimation();
isRunning = true;
resetButtons();
}
if (e.getActionCommand().equals("Add")) {
addImage();
}
if (e.getActionCommand().equals("Add 10")) {
for(int i = 0; i <10; i++) {
addImage();
startAnimation();
}
}
if (e.getActionCommand().equals("Stop")) {
pauseAnimation();
isRunning = false;
resetButtons();
}
if (e.getActionCommand().equals("Reset")) {
imageList.clear();
repaint();
isReset = true;
resetButtons();
}
if (e.getActionCommand().equals("Exit")) {
System.exit(0);
}
}
//This function starts all the animations using the Runnable AnimationThread
void startAnimation() {
//this starts the program
if (f == null) {
f = service.submit(new AnimationThread());
}
//this starts the program after it got paused
else if (f.isCancelled()) {
f = service.submit(new AnimationThread());
}
}
//this pauses all the animations
void pauseAnimation() {
f.cancel(true);
}
//here is the part of the code that I have problems with. I'm not sure how to make the program wait for a spot to be empty while all the other threads are running rather than pausing them all.
void addImage(){
int i = 0;
MyImage image;
while (true) {
image= new MyImage("image.png");
if (checkCollision(image)) {
if(i > 100){
System.out.println("Something went wrong");
System.exit(0);
}
try {
i++;
Thread.sleep(50);
} catch (InterruptedException e) {}
} else {
System.out.println("image added");
break;
}
}
imageList.add(image);
}
//this part of the program does all the image moving. It uses the function move image from the MyImage class and a checkCollision function from this class.
void moveAllImages() {
for (MyImage image : imageList) {
image.moveImage(animationPanel);
image.calculatePoints();
checkCollision(image);
}
}
//this checks all the collisions with other images. It can be used for checking if a image can be created in some spot and also for all the bouncing callculations
boolean checkCollision(MyImage currentImage){
if(imageList.isEmpty()) return false;
for(MyImage im : imageList){
if(currentImage == im) continue;
if(currentImage.intersects(im)){
if(im.contains(currentImage.ml) || im.contains(currentImage.mr)){
currentImage.undoMove();
currentImage.vx = -currentImage.vx;
return true;
}
}
if(im.contains(currentImage.mt) || im.contains(currentImage.mb)){
currentImage.undoMove();
currentImage.vy = - currentImage.vy;
return true;
}
if(currentImage.contains(im.ml) || currentImage.contains(im.mr)){
currentImage.undoMove();
currentImage.vx = -currentImage.vx;
return true;
}
if (im.contains(currentImage.tl) || im.contains(currentImage.tr) || im.contains(currentImage.bl) || im.contains(currentImage.br)) {
currentImage.undoMove();
currentImage.vx *= -1;
currentImage.vy *= -1;
return true;
}
}
return false;
}
//This class does drawing graphics and nothing else
public class AnimationPanel extends JPanel {
AnimationPanel() {
this.setBackground(Color.BLACK);
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
this.setPreferredSize(new Dimension((int) screenSize.getWidth() / 2, (int) screenSize.getHeight() / 2));
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
for (MyImage im : imageList) {
g.drawImage(im.image, im.x, im.y, im.width, im.height, null);
}
}
}
//this is the Runnable AnimationThread which does all the image moving and repainting.
private class AnimationThread implements Runnable {
#Override
public void run() {
while (!f.isCancelled()) {
moveAllImages();
animationPanel.repaint();
try {
Thread.sleep(5);
}
catch (InterruptedException e) {
System.out.println(e.getMessage());
f.cancel(true);
}
}
}
}
}
This is the MyImage class
package bouncer;
import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
//NOTE: This class requires BouncingImages.java
/* This class combines an image with a rectangle
* which allows for easy movement and collision detection
*/
class MyImage extends Rectangle {
//this gives it an x,y,width,height
static int biggestDim = 0;
BufferedImage image;
Color color = Color.RED;
//these are the speeds of movement
int vx = 1;
int vy = 1;
int lastx = -1;
int lasty = -1;
Point ctr, tl, tr, bl, br, ml, mr, mt, mb;
MyImage(String filename) {
vx = (int) (Math.random() * 5 + 1);
vy = (int) (Math.random() * 5 + 1);
//load the image
try {
image = ImageIO.read(new File(filename));
width = image.getWidth(null);
height = image.getHeight(null);
}
catch (IOException e) {
System.out.println("ERROR: image file \"" + filename + "\" not found");
//e.printStackTrace();
BouncingImages.imagesLoaded = false;
width = 100 + (int) (Math.random() * 100);
height = width - (int) (Math.random() * 70);
color = Color.getHSBColor((float) Math.random(), 1.0f, 1.0f); // a quick way to get random colours
//System.exit(0);
}
//update the variable containing the biggest dimension
if (width > biggestDim) biggestDim = width;
if (height > biggestDim) biggestDim = height;
calculatePoints();
}
void calculatePoints() {
//Calculate points
//corners
tl = new Point(x, y);
tr = new Point(x + width, y);
bl = new Point(x, y + height);
br = new Point(x + width, y + height);
//center
ctr = new Point(x + width / 2, y + height / 2);
//mid points of sides
ml = new Point(x, y + height / 2);
mr = new Point(x + width, y + height / 2);
mt = new Point(x + width / 2, y);
mb = new Point(x + width / 2, y + height);
}
void moveImage(BouncingImages.AnimationPanel panel) {
lastx = x;
lasty = y;
x += vx;
y += vy;
if (x < 0 && vx < 0) {
x = 0;
vx = -vx;
}
if (y < 0 && vy < 0) {
y = 0;
vy = -vy;
}
if (x + width > panel.getWidth() && vx > 0) {
x = panel.getWidth() - width;
vx = -vx;
}
if (y + height > panel.getHeight() && vy > 0) {
y = panel.getHeight() - height;
vy = -vy;
}
}
void undoMove() {
if (lastx > 0) {
x = lastx;
y = lasty;
}
lastx = lasty = -1;
}
}
You are blocking the main thread in method addImage in Thread.sleep(50). Instead, you could ensure that you call method addImage asynchronously, e.g.
if (e.getActionCommand().equals("Add")) {
CompletableFuture.runAsync(this::addImage);
}
if (e.getActionCommand().equals("Add 10")) {
CompletableFuture.runAsync(() -> addImages(10));
}
with
private void addImages(int numberOfImages) {
for(int i = 0; i < numberOfImages; i++) {
addImage();
startAnimation();
}
}
If you want to experiment a bit more with threads, then you can think about how to do it "by hand".
I'm trying to make my Pedestrian object move, and it moves but at a certain point it flies away from the screen. The Pedestrian moves by a List of points. First the Pedestrian is added to toDraw to paint it and in startAndCreateTimer I loop through the same list to move the Vehicles Maybe it's because of this line i = (double) diff / (double) playTime; I actually don't want to set a playtime how not to do that, could this be the problem or is it something else? Here a link with the point where the Pedestrian flies away (starts north of left roundabout) http://gyazo.com/23171a6106c88f1ba8ca438598ff4153.
class Surface extends JPanel{
Track track=new Track();
public List<Vehicle> toDraw = new ArrayList<>();
private Long startTime;
private long playTime = 4000;
private double i;
public Surface(){
startAndCreateTimer();
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
//Make sure the track is painted first
track.paint(g);
for (Vehicle v : toDraw) {
v.paint(g);
}
}
public void repaintPanel(){
this.repaint();
}
private void startAndCreateTimer(){
Timer timer = new Timer(100, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (startTime == null) {
startTime = System.currentTimeMillis();
}
long now = System.currentTimeMillis();
long diff = now - startTime;
i = (double) diff / (double) playTime;
for (Vehicle v : toDraw){
v.update(i);
}
repaintPanel();
}
});
timer.start();
}
}
Pedestrian java
public class Pedestrian extends Vehicle {
BufferedImage pedestrian;
Point pedestrianPosition;
double pedestrianRotation = 0;
int pedestrianW, pedestrianH;
int counter=0;
List<LanePoint>pedestrianPath;
boolean lockCounter=false;
public Pedestrian(int x, int y){
try {
pedestrian = ImageIO.read(Car.class.getResource("images/human.png"));
} catch (IOException e) {
System.out.println("Problem loading pedestrian images: " + e);
}
pedestrianPosition = new Point(x,y);
pedestrianW = pedestrian.getWidth();
pedestrianH = pedestrian.getHeight();
}
#Override
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.rotate(Math.toRadians(pedestrianRotation), pedestrianPosition.x, pedestrianPosition.y);
g2d.drawImage(pedestrian, pedestrianPosition.x, pedestrianPosition.y, null);
}
#Override
public void setPath(List<LanePoint> path) {
pedestrianPath=path;
}
/*Update*/
#Override
public void update(double i){
if (counter < pedestrianPath.size()) {
Point startPoint = new Point(pedestrianPosition.x, pedestrianPosition.y);
LanePoint endPoint = new LanePoint(pedestrianPath.get(counter).x, pedestrianPath.get(counter).y,pedestrianPath.get(counter).lanePointType,pedestrianPath.get(counter).lanePointToTrafficLight,pedestrianPath.get(counter).laneTrafficLightId,pedestrianPath.get(counter).degreesRotation);
pedestrianPosition.x=(int)Maths.lerp(startPoint.x,endPoint.x,i);
pedestrianPosition.y=(int)Maths.lerp(startPoint.y,endPoint.y,i);
pedestrianRotation=endPoint.degreesRotation;
if(pedestrianPosition.equals(new Point(endPoint.x,endPoint.y))){
/*PEDESTRIAN SIGN UP*/
if (endPoint.lanePointType.equals(LanePoint.PointType.TRAFFICLIGHT) && endPoint.lanePointToTrafficLight.equals(LanePoint.PointToTrafficLight.INFRONTOF)){
try {
Roundabout.client.sendBytes(new byte []{0x03,endPoint.laneTrafficLightId.byteValue(),0x01,0x00});
} catch (IOException ex) {
ex.printStackTrace();
}
}
/*PEDESTRIAN SIGN OFF*/
else if (endPoint.lanePointType.equals(LanePoint.PointType.TRAFFICLIGHT) && endPoint.lanePointToTrafficLight.equals(LanePoint.PointToTrafficLight.UNDERNEATH)) {
if (Surface.trafficLights.get(endPoint.laneTrafficLightId).red) {
lockCounter = true;
} else {
try {
Roundabout.client.sendBytes(new byte[]{0x03, endPoint.laneTrafficLightId.byteValue(), 0x00, 0x00});
lockCounter=false;
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
if (!lockCounter) {
counter++; //Increment counter > sets next point
}
}
}
}
}
Maths.java
public class Maths {
//Lineat interpolation
public static double lerp(double a, double b, double t) {
return a + (b - a) * t;
}
}
So, basically you are calculating the position of the object between to points based on the amount of time that has passed. This is good.
So at t = 0, the object will be at the start point, at t = 0.5, it will be halfway between the start and end point, at t = 1.0 it will be at the end point.
What happens when t > 1.0? Where should the object be? - hint, it should be nowhere as it should have been removed or reset...
This and this are basic examples of "time line" based animation, meaning that, over a period of time, the position of the object is determined by using different points (along a time line)
So, in order to calculate the position along a line, you need three things, the point you started at, the point you want to end at and the duration (between 0-1)
Using these, you can calculate the point along the line between these two points based on the amount of time.
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.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public static class TestPane extends JPanel {
protected static final double PLAY_TIME = 4000.0;
private Point2D startAt = new Point(0, 0);
private Point2D endAt = new Point(200, 200);
private Point2D current = startAt;
private Long startTime;
public TestPane() {
Timer timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (startTime == null) {
startTime = System.currentTimeMillis();
}
long time = System.currentTimeMillis() - startTime;
double percent = (double) time / PLAY_TIME;
if (percent > 1.0) {
percent = 1.0;
((Timer) e.getSource()).stop();
}
current = calculateProgress(startAt, endAt, percent);
repaint();
}
});
timer.start();
}
protected Point2D calculateProgress(Point2D startPoint, Point2D targetPoint, double progress) {
Point2D point = new Point2D.Double();
if (startPoint != null && targetPoint != null) {
point.setLocation(
calculateProgress(startPoint.getX(), targetPoint.getY(), progress),
calculateProgress(startPoint.getX(), targetPoint.getY(), progress));
}
return point;
}
protected double calculateProgress(double startValue, double endValue, double fraction) {
return startValue + ((endValue - startValue) * fraction);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.GREEN);
g2d.draw(new Line2D.Double(startAt, endAt));
g2d.setColor(Color.RED);
g2d.fill(new Ellipse2D.Double(current.getX() - 5, current.getY() - 5, 10, 10));
g2d.dispose();
}
}
}
So, using current = calculateProgress(startAt, endAt, percent);,
you can see that the dot moves evenly between the start and end points.
If we change it to something more like what you seem to be doing, current = calculateProgress(current, endAt, percent);,
you can see that it speeds down the line and finally eases out, which isn't what you really want...
Updated with time line theory
Let's imagine you have a time line, which has a length of t and along this time line, you have 5 events (or key frames) (e1 - e5), each occurring after each other.
e1 starts at 0 and e5 ends at 1
As you can see, the events occur at irregular intervals and run for different lengths of time.
t1 runs for 25% of the time line
t2 runs for 25% of the time line
t3 runs for 12.5% of the time line
t3 runs for 37.5% of the time line
So, based on t, you need to determine which events are been executed. So when t is 0.12, we are running about half way through t1 (between e1 & e2).
You then need to calculate local time/difference between the key frames (0-0.25 along the timeline)
localTime = 1.0 - ((t - e1) / (e2 - e1))
= 1.0 - ((0.12 - 0) / (0.25 - 0))
= 1.0 - (0.12 / 0.25)
= 1.0 - 0.48
= 0.52
Where t is the time along the time line, e1 is the time of the first event (0) and e2 is the time of the second event (0.25), which gives us the duration along the t1 (in this example)
This is then the value of your linear interpolation for the given time slice.
Runnable example...
I took a look at your code, but there's a lot of work that needs to be done to get this to work.
Basically, you need to know how long the path is and the amount that each segment is of that path (as a percentage). With this, we can create a "time line" of "key frames" which determines how far along the "path" your object is based on the amount of time that has passed and the amount of time it "should" take to travel.
So, the first thing I did was create a Path class (kind of mimics your Lists, but has some additional methods)
public class Path implements Iterable<Point> {
private List<Point> points;
private double totalLength = 0;
public Path(Point... points) {
this.points = new ArrayList<>(Arrays.asList(points));
for (int index = 0; index < size() - 1; index++) {
Point a = get(index);
Point b = get(index + 1);
double length = lengthBetween(a, b);
totalLength += length;
}
}
public double getTotalLength() {
return totalLength;
}
public int size() {
return points.size();
}
public Point get(int index) {
return points.get(index);
}
public double lengthBetween(Point a, Point b) {
return Math.sqrt(
(a.getX() - b.getX()) * (a.getX() - b.getX())
+ (a.getY() - b.getY()) * (a.getY() - b.getY()));
}
#Override
public Iterator<Point> iterator() {
return points.iterator();
}
}
Mostly, this provides the totalLength of the path. We use this to calculate how much each segment takes up later
I then borrowed the TimeLine class from this previous answer
public class Timeline {
private Map<Double, KeyFrame> mapEvents;
public Timeline() {
mapEvents = new TreeMap<>();
}
public void add(double progress, Point p) {
mapEvents.put(progress, new KeyFrame(progress, p));
}
public Point getPointAt(double progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
KeyFrame[] keyFrames = getKeyFramesBetween(progress);
double max = keyFrames[1].progress - keyFrames[0].progress;
double value = progress - keyFrames[0].progress;
double weight = value / max;
return blend(keyFrames[0].getPoint(), keyFrames[1].getPoint(), 1f - weight);
}
public KeyFrame[] getKeyFramesBetween(double progress) {
KeyFrame[] frames = new KeyFrame[2];
int startAt = 0;
Double[] keyFrames = mapEvents.keySet().toArray(new Double[mapEvents.size()]);
while (startAt < keyFrames.length && keyFrames[startAt] <= progress) {
startAt++;
}
if (startAt >= keyFrames.length) {
startAt = keyFrames.length - 1;
}
frames[0] = mapEvents.get(keyFrames[startAt - 1]);
frames[1] = mapEvents.get(keyFrames[startAt]);
return frames;
}
protected Point blend(Point start, Point end, double ratio) {
Point blend = new Point();
double ir = (float) 1.0 - ratio;
blend.x = (int) (start.x * ratio + end.x * ir);
blend.y = (int) (start.y * ratio + end.y * ir);
return blend;
}
public class KeyFrame {
private double progress;
private Point point;
public KeyFrame(double progress, Point point) {
this.progress = progress;
this.point = point;
}
public double getProgress() {
return progress;
}
public Point getPoint() {
return point;
}
}
}
Now, as they stand, they are not compatible, we need to take each segment and calculate the length of the segment as a percentage of the total length of the path and create a key frame for the specified point along the time line...
double totalLength = path.getTotalLength();
timeLine = new Timeline();
timeLine.add(0, path.get(0));
// Point on time line...
double potl = 0;
for (int index = 1; index < path.size(); index++) {
Point a = path.get(index - 1);
Point b = path.get(index);
double length = path.lengthBetween(a, b);
double normalised = length / totalLength;
// Normalised gives as the percentage of this segment, we need to
// translate that to a point on the time line, so we just add
// it to the "point on time line" value to move to the next point :)
potl += normalised;
timeLine.add(potl, b);
}
I did this deliberately, to show the work you are going to need to do.
Need, I create a Ticker, which just runs a Swing Timer and reports ticks to Animations
public enum Ticker {
INSTANCE;
private Timer timer;
private List<Animation> animations;
private Ticker() {
animations = new ArrayList<>(25);
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
// Prevent possible mutatation issues...
Animation[] anims = animations.toArray(new Animation[animations.size()]);
for (Animation animation : anims) {
animation.tick();
}
}
});
}
public void add(Animation animation) {
animations.add(animation);
}
public void remove(Animation animation) {
animations.remove(animation);
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
}
public interface Animation {
public void tick();
}
This centralises the "clock", be allows Animations to determine what they would like to do on each tick. This should be more scalable then creating dozens of Timers
Okay, that's all fun and games, but how does it work together? Well, here's a complete runnable example.
It takes one of your own paths and creates a TimeLine out of it and animates a object moving along it.
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.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
Path path = new Path(
new Point(440, 40),
new Point(440, 120),
new Point(465, 90),
new Point(600, 180),
new Point(940, 165),
new Point(940, 145),
new Point(1045, 105),
new Point(1080, 120),
new Point(1170, 120),
new Point(1200, 120),
new Point(1360, 123),
new Point(1365, 135),
new Point(1450, 170),
new Point(1457, 160),
new Point(1557, 160));
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane(path));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
Ticker.INSTANCE.start();
}
});
}
public enum Ticker {
INSTANCE;
private Timer timer;
private List<Animation> animations;
private Ticker() {
animations = new ArrayList<>(25);
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
// Prevent possible mutatation issues...
Animation[] anims = animations.toArray(new Animation[animations.size()]);
for (Animation animation : anims) {
animation.tick();
}
}
});
}
public void add(Animation animation) {
animations.add(animation);
}
public void remove(Animation animation) {
animations.remove(animation);
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
}
public interface Animation {
public void tick();
}
public static final double PLAY_TIME = 4000d;
public class TestPane extends JPanel implements Animation {
private Path path;
private Path2D pathShape;
private Timeline timeLine;
private Long startTime;
private Point currentPoint;
public TestPane(Path path) {
this.path = path;
// Build the "path" shape, we can render this, but more importantally
// it allows use to determine the preferred size of the panel :P
pathShape = new Path2D.Double();
pathShape.moveTo(path.get(0).x, path.get(0).y);
for (int index = 1; index < path.size(); index++) {
Point p = path.get(index);
pathShape.lineTo(p.x, p.y);
}
// Build the time line. Each segemnt (the line between any two points)
// makes up a percentage of the time travelled, we need to calculate
// the amount of time that it would take to travel that segement as
// a percentage of the overall length of the path...this
// allows us to even out the time...
double totalLength = path.getTotalLength();
timeLine = new Timeline();
timeLine.add(0, path.get(0));
// Point on time line...
double potl = 0;
for (int index = 1; index < path.size(); index++) {
Point a = path.get(index - 1);
Point b = path.get(index);
double length = path.lengthBetween(a, b);
double normalised = length / totalLength;
// Normalised gives as the percentage of this segment, we need to
// translate that to a point on the time line, so we just add
// it to the "point on time line" value to move to the next point :)
potl += normalised;
timeLine.add(potl, b);
}
currentPoint = path.get(0);
Ticker.INSTANCE.add(this);
}
#Override
public Dimension getPreferredSize() {
Dimension size = pathShape.getBounds().getSize();
size.width += pathShape.getBounds().x;
size.height += pathShape.getBounds().y;
return size;
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.GREEN);
g2d.draw(pathShape);
g2d.setColor(Color.RED);
g2d.fill(new Ellipse2D.Double(currentPoint.x - 5, currentPoint.y - 5, 10, 10));
g2d.dispose();
}
#Override
public void tick() {
if (startTime == null) {
startTime = System.currentTimeMillis();
}
long diff = System.currentTimeMillis() - startTime;
double t = (double)diff / PLAY_TIME;
if (t > 1.0) {
t = 1.0d;
// Don't call me any more, I'm already home
Ticker.INSTANCE.remove(this);
}
currentPoint = timeLine.getPointAt(t);
repaint();
}
}
public class Path implements Iterable<Point> {
private List<Point> points;
private double totalLength = 0;
public Path(Point... points) {
this.points = new ArrayList<>(Arrays.asList(points));
for (int index = 0; index < size() - 1; index++) {
Point a = get(index);
Point b = get(index + 1);
double length = lengthBetween(a, b);
totalLength += length;
}
}
public double getTotalLength() {
return totalLength;
}
public int size() {
return points.size();
}
public Point get(int index) {
return points.get(index);
}
public double lengthBetween(Point a, Point b) {
return Math.sqrt(
(a.getX() - b.getX()) * (a.getX() - b.getX())
+ (a.getY() - b.getY()) * (a.getY() - b.getY()));
}
#Override
public Iterator<Point> iterator() {
return points.iterator();
}
}
public class Timeline {
private Map<Double, KeyFrame> mapEvents;
public Timeline() {
mapEvents = new TreeMap<>();
}
public void add(double progress, Point p) {
mapEvents.put(progress, new KeyFrame(progress, p));
}
public Point getPointAt(double progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
KeyFrame[] keyFrames = getKeyFramesBetween(progress);
double max = keyFrames[1].progress - keyFrames[0].progress;
double value = progress - keyFrames[0].progress;
double weight = value / max;
return blend(keyFrames[0].getPoint(), keyFrames[1].getPoint(), 1f - weight);
}
public KeyFrame[] getKeyFramesBetween(double progress) {
KeyFrame[] frames = new KeyFrame[2];
int startAt = 0;
Double[] keyFrames = mapEvents.keySet().toArray(new Double[mapEvents.size()]);
while (startAt < keyFrames.length && keyFrames[startAt] <= progress) {
startAt++;
}
if (startAt >= keyFrames.length) {
startAt = keyFrames.length - 1;
}
frames[0] = mapEvents.get(keyFrames[startAt - 1]);
frames[1] = mapEvents.get(keyFrames[startAt]);
return frames;
}
protected Point blend(Point start, Point end, double ratio) {
Point blend = new Point();
double ir = (float) 1.0 - ratio;
blend.x = (int) (start.x * ratio + end.x * ir);
blend.y = (int) (start.y * ratio + end.y * ir);
return blend;
}
public class KeyFrame {
private double progress;
private Point point;
public KeyFrame(double progress, Point point) {
this.progress = progress;
this.point = point;
}
public double getProgress() {
return progress;
}
public Point getPoint() {
return point;
}
}
}
}
Now, if I was doing this, I would create a method either in Path or as a static utility method, that took a Path and returned a TimeLine automatically ;)
I'm trying to make a match 3 game. I am trying to create some visual aid to what is actually happening by first marking the gems that need to be deleted "black", and after that letting gravity do it's job. I'm struggling to do this, I called repaint(); after I marked them "black", but it doesn't seem to work. I also tried adding in revalidate(); as suggested in another question but that doesn't seem to fix the problem either. Here's the piece of code that's troubling me.
Trouble code:
public void deletePattern(Set<Gem> gemsToDelete){
for(Gem gem : gemsToDelete)
gem.setType(7);
repaint(); //This doesn't seem to work
doGravity();
switchedBack = true;
checkPattern();
}
I want to repaint the board before doGravity() and after the enhanced for loop. Could it be that I'm not using the thread correctly in the doGravity() method?
Here's the full code:
Board.java
package Game;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.LinkedHashSet;
import java.util.Set;
public class Board extends JPanel{
final int BOARDWIDTH = 8;
final int BOARDHEIGHT = 8;
private static final Color COLORS[] = { new Color(255, 0, 0), new Color(255, 128, 0), new Color(255, 255, 0), new Color(0, 255, 0), new Color(0, 255, 255), new Color(0, 0, 255), new Color(127, 0, 255), new Color(0, 0, 0), new Color(0, 0, 0), new Color(255, 255, 255)};
boolean isAlive, isPattern, switchedBack;
boolean isFirstSelected = false;
Gem[][] gems;
int fromX, fromY, toX, toY;
public Board() {
gems = new Gem[BOARDWIDTH][BOARDHEIGHT];
addMouseListener(new MouseInputAdapter());
}
int cellWidth() { return (int) getSize().getWidth() / BOARDWIDTH; }
int cellHeight() { return (int) getSize().getHeight() / BOARDHEIGHT; }
public void start(){
isPattern = switchedBack = false;
isAlive = true;
fillBoard();
checkPattern();
switchedBack = false;
}
public void paint(Graphics g) {
super.paint(g);
for (int x = 0; x < BOARDWIDTH; x++) {
for (int y = 0; y < BOARDHEIGHT; y++)
drawCell(g, x, y, gems[x][y]);
}
}
public void fillBoard(){
for (int x = 0; x < BOARDWIDTH; x++) {
for (int y = 0; y < BOARDHEIGHT; y++)
gems[x][y] = new Gem();
}
}
public void drawCell(Graphics g, int x, int y, Gem gem) {
x = x * cellWidth();
y = y * cellHeight();
g.setColor(COLORS[gem.getType()]);
g.fillRect(x, y, x + cellWidth(), y + cellHeight());
}
class MouseInputAdapter extends MouseAdapter { #Override public void mouseClicked(MouseEvent e) { selectGems(e); } }
public void selectGems(MouseEvent e){
int x = e.getX() / cellWidth();
int y = e.getY() / cellHeight();
if(!isFirstSelected) {
fromX = x;
fromY = y;
isFirstSelected = true;
}else{
toX = x;
toY = y;
if((Math.abs(fromX - toX) == 1 ^ Math.abs(fromY - toY) == 1) & (gems[fromX][fromY].getType() != gems[toX][toY].getType())) {
switchGems();
isFirstSelected = false;
}
}
}
public void switchGems(){
int tempType = gems[fromX][fromY].getType();
gems[fromX][fromY].setType(gems[toX][toY].getType());
gems[toX][toY].setType(tempType);
checkPattern();
switchedBack = false;
repaint();
}
public void checkPattern() {
Set<Gem> gemsToDelete = new LinkedHashSet<>();
isPattern = false;
for (int x = 0; x < BOARDWIDTH; x++) {
for (int y = 0; y < BOARDHEIGHT; y++) {
if (x + 2 < BOARDWIDTH && (gems[x][y].getType() == gems[x + 1][y].getType()) && (gems[x + 1][y].getType() == gems[x + 2][y].getType())) { //Checks for 3 horizontal gems in a row
isPattern = true;
gemsToDelete.add(gems[x][y]);
gemsToDelete.add(gems[x + 1][y]);
gemsToDelete.add(gems[x + 2][y]);
}
if (y + 2 < BOARDHEIGHT && (gems[x][y].getType() == gems[x][y + 1].getType()) && (gems[x][y + 1].getType() == gems[x][y + 2].getType())) { //Check for 3 vertical gems in a row
isPattern = true;
gemsToDelete.add(gems[x][y]);
gemsToDelete.add(gems[x][y + 1]);
gemsToDelete.add(gems[x][y + 2]);
}
}
}
if(!gemsToDelete.isEmpty())
deletePattern(gemsToDelete);
if(!isPattern && !switchedBack){
switchedBack = true;
switchGems();
}
}
public void deletePattern(Set<Gem> gemsToDelete){
for(Gem gem : gemsToDelete)
gem.setType(7);
repaint(); //This doesn't seem to work
doGravity();
switchedBack = true;
checkPattern();
}
public void doGravity(){
try{
Thread.sleep(1000);
}catch(InterruptedException e){e.printStackTrace();}
for (int y = 0; y < BOARDHEIGHT; y++) {
for (int x = 0; x < BOARDWIDTH; x++) {
if(gems[x][y].getType() == 7){
for (int i = y; i >= 0; i--) {
if(i == 0)
gems[x][i].setType(gems[x][i].genType());
else
gems[x][i].setType(gems[x][i-1].getType());
}
}
}
}
}
}
Gem.java
package Game;
public class Gem {
private int type;
public Gem(){
this.type = genType();
}
public int genType(){
return (int) (Math.random() * 7);
}
public void setType(int type){
this.type = type;
}
public int getType(){
return type;
}
}
Game.java
package Game;
import javax.swing.*;
public class Game extends JFrame{
public Game(){
Board board = new Board();
getContentPane().add(board);
board.start();
setTitle("Game");
setSize(600, 600);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}
public static void main(String[] args){
Game game = new Game();
game.setLocationRelativeTo(null);
game.setVisible(true);
}
}
Your code is initiated via a mouse click. Code invoked from a Swing listener is executed on the Event Dispatch Thread (EDT), which is also responsible for painting the GUI
The Thread.sleep() in your doGravaity() method causes the EDT to sleep, therefore the GUI can't repaint() itself until the whole looping code is finished, at which point it will just paint the final state of your animation.
Instead of sleeping, you need to use a Swing Timer to schedule animation. So basically, in the deletePattern() method you would start the Timer to do the gravity animation. This will free up the EDT to repaint itself and when the Timer fires you would animate your components one move and then do repaint() again. When the components are finished moving you stop the timer.
Read the section from the Swing tutorial on Concurrency for more information about the EDT.
Call this.invalidate() or this.postInvalidate() which then forces a repaint.
I stumbled upon a problem: if an image is moving at a high speed across the screen it is rendered incorrectly producing a ghosting effect. I think we can rule out my monitor being the problem as this type of movement was flawless in swing (with the same framerate).
Looks like:
Code (merged from 3 classes):
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.awt.Dimension;
import java.awt.Toolkit;
public class Constructor extends Application{
Image player, shot;
static Dimension screen = new Dimension(Toolkit.getDefaultToolkit().getScreenSize());
static int wid = screen.width;
static int hei = screen.height;
static boolean up, down, left, right, rotleft , rotright;
static double x = (wid/2)-109;
static double y = (hei/1.5)-132;
static double velx = 0, vely = 0, velx2 = 0, vely2 = 0;
static double forspeed = 0, sidespeed = 0;
static int rotat = 0;
public void load(){
player = new Image("res/sprite.png");
}
#Override
public void start(final Stage frame) throws Exception{
load();
frame.setTitle("DEFAULT");
frame.initStyle(StageStyle.UNDECORATED);
Group root = new Group();
final ImageView ship = new ImageView();
ship.setImage(player);
root.getChildren().add(ship);
frame.setScene(new Scene(root, wid, hei, Color.BLACK));
frame.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>(){
public void handle(KeyEvent key) {
if(key.getCode()==KeyCode.W)
up = true;
if(key.getCode()==KeyCode.S)
down = true;
if(key.getCode()==KeyCode.Q)
left = true;
if(key.getCode()==KeyCode.E)
right = true;
if(key.getCode()==KeyCode.A)
rotleft = true;
if(key.getCode()==KeyCode.D)
rotright = true;
}
});
frame.addEventHandler(KeyEvent.KEY_RELEASED, new EventHandler<KeyEvent>(){
public void handle(KeyEvent key) {
if(key.getCode()==KeyCode.ESCAPE)
{
frame.close();
System.exit(0);
}
if(key.getCode()==KeyCode.W)
up = false;
if(key.getCode()==KeyCode.S)
down = false;
if(key.getCode()==KeyCode.Q)
left = false;
if(key.getCode()==KeyCode.E)
right = false;
if(key.getCode()==KeyCode.A)
rotleft = false;
if(key.getCode()==KeyCode.D)
rotright = false;
}
});
frame.setAlwaysOnTop(true);
frame.setHeight(hei);
frame.setWidth(wid);
frame.setResizable(false);
frame.setFullScreen(true);
frame.show();
new AnimationTimer() {
#Override
public void handle(long now) {
gameloop();
ship.setTranslateX(x);
ship.setTranslateY(y);
ship.setRotate(rotat);
}
}.start();
}
public static void gameloop(){
if(Shared.up)
forspeed += 1;
if(Shared.down)
forspeed -= 1;
if(Shared.right)
sidespeed += 1;
if(Shared.left)
sidespeed -= 1;
if(Shared.rotleft)
rotat -=3;
if(Shared.rotright)
rotat +=3;
velx = Math.cos(Math.toRadians(rotat-90))*forspeed + Math.cos(Math.toRadians(rotat))*sidespeed;
vely = Math.sin(Math.toRadians(rotat-90))*forspeed + Math.sin(Math.toRadians(rotat))*sidespeed;
if(!Shared.up && !Shared.down)
{
if(forspeed > 0)
forspeed -= 0.2;
else if (forspeed < 0)
forspeed += 0.2;
}
if(!Shared.right && !Shared.left)
{
if(sidespeed > 0)
sidespeed -= 0.2;
else if (sidespeed < 0)
sidespeed += 0.2;
}
x += velx;
y += vely;
screencolisions();
}
private static void screencolisions() {
// LEFT RIGHT
if(x < 0)
{
x = 0;
sidespeed = 0;
}
else if (x+218 > Shared.wid)
{
x = Shared.wid-218;
sidespeed = 0;
}
// UP DOWN
if(y < 0)
{
y = 0;
forspeed = 0;
}
else if (y+164 > Shared.hei)
{
y = Shared.hei-164;
forspeed = 0;
}
}
public static void main(String[] args){
Application.launch(args);
}
}
Well first thing is you are using AWT classes in Javafx .. They are not friends (most of the time).
Instead of using Dimension and the AWT toolkit,
Use the provided Javafx Screen class
Screen screen = Screen.getPrimary();
wid = screen.getBounds().getWidth();
hei = screen.getBounds().getHeight();
// Visual bounds will be different depending on OS and native toolbars etc..
// think of it as desktop bounds vs whole screen
For example:
public class ScreenBounds extends Application {
#Override
public void start(Stage primaryStage) {
Screen screen = Screen.getPrimary();
System.out.println("ScreenBounds : " + screen.getBounds() + "\nVisualBounds : " + screen.getVisualBounds());
Platform.exit();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
prints:
compile-single:
run-single:
ScreenBounds : Rectangle2D [minX = 0.0, minY=0.0, maxX=1680.0, maxY=1050.0, width=1680.0, height=1050.0]
VisualBounds : Rectangle2D [minX = 0.0, minY=40.0, maxX=1680.0, maxY=1050.0, width=1680.0, height=1010.0]
As you can see no Syntax error as you described..
Though that error is probably due to your variables being Integers not Doubles like the method returns;
ScheduledService Timer:
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.util.Duration;
/**
*
* #author jdub1581
*/
public class NanoTimer extends ScheduledService<Void> {
private final long ONE_NANO = 1000000000L;
private final double ONE_NANO_INV = 1f / 1000000000L;
private long startTime, previousTime;
private double frameRate, deltaTime;
private final NanoThreadFactory tf = new NanoThreadFactory();
public NanoTimer() {
super();
this.setPeriod(Duration.millis(16));// equivalent to 60 fps
this.setExecutor(Executors.newCachedThreadPool(tf));
}
public double getTimeAsSeconds() {
return getTime() * ONE_NANO_INV;
}
public long getTime() {
return System.nanoTime() - startTime;
}
public long getOneSecondAsNano() {
return ONE_NANO;
}
public double getFrameRate() {
return frameRate;
}
public double getDeltaTime() {
return deltaTime;
}
private void updateTimer() {
deltaTime = (getTime() - previousTime) * (1.0f / ONE_NANO);
frameRate = 1.0f / deltaTime;
previousTime = getTime();
}
#Override
public void start() {
super.start();
if (startTime <= 0) {
startTime = System.nanoTime();
}
}
#Override
public void reset() {
super.reset();
startTime = System.nanoTime();
previousTime = getTime();
}
private boolean init = true;
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
updateTimer();
// perform NON UI calculations here
return null;
}
};
}
#Override
protected void succeeded() {
super.succeeded();
//update the UI here
}
#Override
protected void failed() {
getException().printStackTrace(System.err);
}
#Override
public String toString() {
return "ElapsedTime: " + getTime() + "\nTime in seconds: " + getTimeAsSeconds()
+ "\nFrame Rate: " + getFrameRate()
+ "\nDeltaTime: " + getDeltaTime();
}
/*==========================================================================
creates a daemon thread for use
*/
private class NanoThreadFactory implements ThreadFactory {
public NanoThreadFactory() {
}
#Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "NanoTimerThread");
t.setDaemon(true);
return t;
}
}
}//=============================================================================
Whatever class you put it in, just call the start method, probably best nested as a private class so variable can be used inside of it.. Or rewrite as you need..
I use it here: My Cloth Simulation and it works well doing 50k+ calculations per frame
I recently was coding but encountered an null pointer exception
the stack trace says
Exception in thread "main" java.lang.NullPointerException
at com.masterkgames.twisteddream.level.SpawnLevel.generateLevel(SpawnLevel.java:34)
at com.masterkgames.twisteddream.level.Level.<init>(Level.java:22)
at com.masterkgames.twisteddream.level.SpawnLevel.<init>(SpawnLevel.java:16)
at com.masterkgames.twisteddream.Game.<init>(Game.java:49)
at com.masterkgames.twisteddream.Game.main(Game.java:138)
below are the 3 mentioned classes
Spawnlevel.java:
package com.masterkgames.twisteddream.level;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
import com.masterkgames.twisteddream.level.tile.Tile;
public class SpawnLevel extends Level {
private Tile[] tiles;
private int[] levelPixels;
public SpawnLevel(String path) {
super(path);
}
protected void loadLevel(String path){
try{
BufferedImage image = ImageIO.read(SpawnLevel.class.getResource(path));
int w = image.getWidth();
int h = image.getHeight();
tiles = new Tile[w * h];
levelPixels = new int[w * h];
image.getRGB(0,0,w,h,levelPixels,0,w);
}catch(IOException e){
e.printStackTrace();
}
}
protected void generateLevel(){
for(int i = 0; i < levelPixels.length; i++){
if(levelPixels[i] == 0xff00) tiles[i] = Tile.Grass;
if(levelPixels[i] == 0xffff00) tiles[i] = Tile.Rose;
if(levelPixels[i] == 0x7f7f00) tiles[i] = Tile.Stone;
}
}
}
level.java:
package com.masterkgames.twisteddream.level;
import com.masterkgames.twisteddream.graphics.Screen;
import com.masterkgames.twisteddream.level.tile.Tile;
public class Level {
public Screen screen;
protected int width, height;
protected Tile[] tiles;
protected int[] tilesInt;
public Level(int width, int height) {
this.width = width;
this.height = height;
tilesInt = new int[width * height];
generateLevel();
}
public Level(String path) {
loadLevel(path);
generateLevel();
}
protected void generateLevel() {
}
private void loadLevel(String path) {
}
public void update() {
}
private void time() {
}
public void render(int xScroll, int yScroll, Screen screen) {
screen.setOffset(xScroll, yScroll);
int x0 = xScroll >> 4;
int x1 = (xScroll + screen.width + 16) >> 4;
int y0 = yScroll >> 4;
int y1 = (yScroll + screen.height + 16) >> 4;
for (int y = y0; y < y1; y++) {
for (int x = x0; x < x1; x++) {
// getTile(x, y).render(x, y, screen);
if (x + y * 16 < 0 || x + y * 16 >= 256) {
Tile.Void.render(x, y, screen);
continue;
}
tiles[x + y * 16].render(x, y, screen);
}
}
}
public Tile getTile(int x, int y) {
if (x < 0 || y < 0)
return Tile.Void;
if (x >= width || y >= height)
return Tile.Void;
if (tilesInt[x + y * width] == 0)
return Tile.Grass;
if (tilesInt[x + y * width] == 1)
return Tile.Rose;
if (tilesInt[x + y * width] == 2)
return Tile.Stone;
return Tile.Void;
}
}
and lastly
game.java:
package com.masterkgames.twisteddream;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import javax.swing.JFrame;
import com.masterkgames.twisteddream.entity.mob.Player;
import com.masterkgames.twisteddream.graphics.Screen;
import com.masterkgames.twisteddream.input.Keyboard;
import com.masterkgames.twisteddream.level.Level;
import com.masterkgames.twisteddream.level.SpawnLevel;
public class Game extends Canvas implements Runnable {
private static final long serialVersionUID = 1L;
public static int width = 300;
public static int height = 168;
public static int scale = 3;
public String Title = "Twisted Dream";
private Thread thread;
private boolean running = false;
private Screen screen;
private Keyboard key;
private Level level;
private Player player;
private JFrame frame;
private BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
private int[] pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
public Game() {
Dimension size = new Dimension(width * scale, height * scale);
setPreferredSize(size);
screen = new Screen(width,height);
key = new Keyboard();
level = new SpawnLevel("/textures/level.png");
player = new Player(key);
frame = new JFrame();
addKeyListener(key);
}
public synchronized void start() {
thread = new Thread(this, "Display");
thread.start();
running = true;
}
public synchronized void stop() {
running = false;
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void run() {
long lastTime = System.nanoTime();
long timer = System.currentTimeMillis();
final double ns = 1000000000.0 / 60.0;
double delta = 0;
int frames = 0;
int updates = 0;
requestFocus();
while (running) {
long nowTime = System.nanoTime();
delta += (nowTime - lastTime) / ns;
lastTime = nowTime;
while (delta >= 1){
update();
updates++;
delta--;
}
render();
frames++;
if(System.currentTimeMillis() - timer > 1000){
timer += 1000;
System.out.println(updates + " ups, " + frames + " fps");
frame.setTitle(Title + " | " + updates + " ups, " + frames + " fps");
updates = 0;
frames = 0;
}
}
stop();
}
public void update() {
key.update();
player.update();
}
public void render() {
BufferStrategy bs = getBufferStrategy();
if (bs == null) {
createBufferStrategy(3);
return;
}
screen.clear();
int xScroll = player.x - screen.width / 2;
int yScroll = player.y - screen.height / 2;
level.render(xScroll, yScroll, screen);
player.render(screen);
for(int i = 0; i < pixels.length; i++){
pixels[i] = screen.pixels[i];
}
Graphics g = bs.getDrawGraphics();
g.drawImage(image,0,0,getWidth(),getHeight(),null);
g.setFont(new Font("arial", 0, 15));
g.setColor(Color.white);
g.dispose();
bs.show();
}
public static void main(String[] args) {
Game game = new Game();
game.frame.setResizable(false);
game.frame.setTitle(game.Title);
game.frame.add(game);
game.frame.pack();
game.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
game.frame.setLocationRelativeTo(null);
game.frame.setVisible(true);
game.start();
}
}
p.s this issue according to the stack trace is in the line for(int i = 0; i < levelPixels.length; i++) in SpawnLevel.java
in Level contructor you have generateLevel(); where you are using levelPixels, which is not initialized
contructor call
private void loadLevel(String path) {
}
from class Level, you have to call method loadLevel in SpawnLevel constructor
You are accessing the null array levelPixels.
Here is where your error is:
In your main you call a new SpawnLevel(path)
But SpawnLevel(path) calls the super constructor, which calls its blank yet defined loadLevel and generateLevel. The loadLevel and generateLevel in SpawnLevel are never called, and the member variables are never initialized.
So basically, you tried to use the Level class as an abstract class but you didnt do it correctly. The member variables should be in the Level class if they are to be shared by children of that class. Im guessing this is what you want to do.
you should really get familiar with the debugger, it will be a lifesaver for little problems like this.