Background
Im trying to create fancy, smooth and fast analog gauge with some dial inertia simulation etc. I want to avoid OpenGL if this is possible.
Problem
My code in Java is much slower than I expect.
I want my dial to move in time shorter than 0.5 second from minimum value (0) to maximum value (1024, i can change this, but I need smoothness).
I tried to measure time spent on repaint and paintComponent methods to find problem.
Repaint takes about 40us, paintComponent takes 300us, on my machine (Core Duo 2GHz, Windows 7).
It seems to be fast enough (1/0.000340s = ~3000 "runs" per second).
I think that video card is bottleneck and it slows my code, but I have no idea what to do with it.
Question
How to make my code faster and keep animation smooth as possible?
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import javax.swing.*;
public class Main extends JPanel {
private static final Point2D CENTER = new Point2D.Double(PREF_W / 2.0,
PREF_W / 2.0);
private static final double RADIUS = PREF_W / 2.0;
private static final Color LARGE_TICK_COLOR = Color.DARK_GRAY;
private static final Color CENTER_HUB_COLOR = Color.DARK_GRAY;
private static final Stroke LARGE_TICK_STROKE = new BasicStroke(4f);
private static final Stroke LINE_TICK_STROKE = new BasicStroke(8f);
private static final int LRG_TICK_COUNT = 18;
private static final double TOTAL_LRG_TICKS = 24;
private static final double LRG_TICK_OUTER_RAD = 0.9;
private static final double LRG_TICK_INNER_RAD = 0.8;
private static final int START_TICK = 10;
private static final double CENTER_HUB_RADIUS = 10;
private static final double DIAL_INNER_RAD = 0.00;
private static final double DIAL_OUTER_RAD = 0.75;
private static final Color DIAL_COLOR = Color.DARK_GRAY;
private BufferedImage backgroundImg;
private static final int PREF_W = 400; //
private static final int PREF_H = 400;
private static final double INIT_VALUE = 0;
public static final int MAX_VALUE = 1024; // resolution
public static int delay = 1; // delay (ms) between value changes
private double theta;
private double cosTheta;
private double sinTheta;
private static long microtime;
public Main() {
setBackground(Color.white);
backgroundImg = createBackgroundImg();
setSpeed(INIT_VALUE);
}
public void setSpeed(double speed) {
if (speed < 0) {
speed = 0;
} else if (speed > MAX_VALUE) {
speed = MAX_VALUE;
}
this.theta = ((speed / MAX_VALUE) * LRG_TICK_COUNT * 2.0 + START_TICK)
* Math.PI / TOTAL_LRG_TICKS;
cosTheta = Math.cos(theta);
sinTheta = Math.sin(theta);
microtime = System.nanoTime()/1000;
repaint();
System.out.println("Repaint (us) = " + (System.nanoTime()/1000 - microtime));
}
private BufferedImage createBackgroundImg() {
BufferedImage img = new BufferedImage(PREF_W, PREF_H,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = img.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(LARGE_TICK_COLOR);
g2.setStroke(LARGE_TICK_STROKE);
for (double i = 0; i < LRG_TICK_COUNT; i++) {
double theta = (i * 2.0 + START_TICK) * Math.PI / TOTAL_LRG_TICKS;
double cosTheta = Math.cos(theta);
double sinTheta = Math.sin(theta);
int x1 = (int) (LRG_TICK_INNER_RAD * RADIUS * cosTheta + CENTER.getX());
int y1 = (int) (LRG_TICK_INNER_RAD * RADIUS * sinTheta + CENTER.getY());
int x2 = (int) (LRG_TICK_OUTER_RAD * RADIUS * cosTheta + CENTER.getX());
int y2 = (int) (LRG_TICK_OUTER_RAD * RADIUS * sinTheta + CENTER.getY());
g2.drawLine(x1, y1, x2, y2);
}
g2.setColor(CENTER_HUB_COLOR);
int x = (int) (CENTER.getX() - CENTER_HUB_RADIUS);
int y = (int) (CENTER.getY() - CENTER_HUB_RADIUS);
int width = (int) (2 * CENTER_HUB_RADIUS);
int height = width;
g2.fillOval(x, y, width, height);
g2.dispose();
return img;
}
#Override
protected void paintComponent(Graphics g) {
System.out.println("Paint component (us) = " + (System.nanoTime()/1000 - microtime));
super.paintComponent(g);
if (backgroundImg != null) {
g.drawImage(backgroundImg, 0, 0, this);
}
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setStroke(LINE_TICK_STROKE);
g.setColor(DIAL_COLOR);
int x1 = (int) (DIAL_INNER_RAD * RADIUS * cosTheta + CENTER.getX());
int y1 = (int) (DIAL_INNER_RAD * RADIUS * sinTheta + CENTER.getY());
int x2 = (int) (DIAL_OUTER_RAD * RADIUS * cosTheta + CENTER.getX());
int y2 = (int) (DIAL_OUTER_RAD * RADIUS * sinTheta + CENTER.getY());
g.drawLine(x1, y1, x2, y2);
microtime = System.nanoTime()/1000;
}
#Override
public Dimension getPreferredSize() {
return new Dimension(PREF_W, PREF_H);
}
private static void createAndShowGui() {
final Main mainPanel = new Main();
JFrame frame = new JFrame("DailAnimation");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
new Timer(delay, new ActionListener() {
double speed = 0;
#Override
public void actionPerformed(ActionEvent evt) {
speed ++;
if (speed > Main.MAX_VALUE) {
speed = 0;
}
mainPanel.setSpeed(speed);
}
}).start();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
Little code description:
There is a timer, that changes gauge value. Timer interval is defined by delay variable at the beginning.
This is complete, one file code, you can just paste it in your IDE and compile.
You could try the following:
Switch from BufferedImage to VolatileImage
Precalculate your sin and cos function results and store them in arrays
Switch to Active painting instead of calling repaint
Related
final int X_START = 1 ;
final int Y_START = 250 ;
final int X_END = 500 ;
final int Y_END = 250 ;
final int SPEED_FACTOR = 5 ;
int xCenter;
int yCenter;
int ellip1Center;
int ellip2Center;
void setup(){
size(500,500);
}
void draw(){
calculateDimension();
drawSpaceship();
moveSpaceship();
}
void calculateDimension(){
xCenter = X_START;
yCenter =(Y_START +Y_END)/2;
ellip1Center = xCenter +50;
ellip2Center = xCenter +20;
}
void drawSpaceship(){
background(0);
ellipse(ellip1Center, yCenter, 20, 20);
ellipse(ellip2Center, yCenter, 30, 30);
}
void moveSpaceship(){
xCenter+= SPEED_FACTOR ;
}
I need help with the moveSpaceship command.
I need to move both ellipses from X_start to X_END and back to X_START simultaneously.
Split the function calculateDimension int 2 functions. One function which initializes the reference position (xCenter, yCenter) and on function which updates the positions of the ellipses (ellip1Center, ellip2Center). Add a new variable speed, that holds the current speed of the ellipses, which may change to a negative speed, if the ellipse reached the end of the track.
int speed;
void initDimension(){
xCenter = X_START;
yCenter =(Y_START +Y_END)/2;
speed = SPEED_FACTOR;
}
void calculateDimension(){
ellip1Center = xCenter +50;
ellip2Center = xCenter +20;
}
Call initDimension in setup and calculateDimension in draw:
void setup(){
size(500,500);
initDimension();
}
void draw(){
calculateDimension();
drawSpaceship();
moveSpaceship();
}
Change the direction of the ellipses by changing the variable speed, if the end or the start of the track is reached:
void moveSpaceship(){
xCenter += speed;
if (xCenter >= X_END-65) speed = -SPEED_FACTOR;
if (xCenter <= 0) speed = SPEED_FACTOR;
}
I have a list of 2d points which is a closed loop, 2D, concave polygon.
I want to generate a second polygon, which is fully inside the first polygon and each vertex/edge of the first polygon has a constant distance to each vertex/edge of the second polygon.
Basically, the first polygon would be "outer wall" and the second would be "inner wall", with the distance between two walls constant.
How to do something like that?
For the case that you do not care about self-intersections, the construction is pretty straightforward:
For each vertex of the polygon:
Take the previous and next line segment
Compute the normals of these line segments
Shift the line segments along the normal
Compute the intersection of the shifted line segments
Below is a MCVE implemented in Java/Swing. The actual computation takes place in computeOffsetPolygonPoints, and it should be easy to translate this to other languages and APIs.
For the case that you also have to handle self-intersections, things might become trickier. Then it would be necessary to define the intended result, particularly for the case that the polygon itself self-intersects...
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
public class InnerPolygonShape
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(() -> createAndShowGUI());
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
InnerPolygonShapePanel innerPolygonShapePanel =
new InnerPolygonShapePanel();
JSlider offsetSlider = new JSlider(0, 100, 40);
offsetSlider.addChangeListener(e ->
{
double alpha = offsetSlider.getValue() / 100.0;
double offset = -50.0 + alpha * 100.0;
innerPolygonShapePanel.setOffset(offset);
});
f.getContentPane().setLayout(new BorderLayout());
f.getContentPane().add(innerPolygonShapePanel, BorderLayout.CENTER);
f.getContentPane().add(offsetSlider, BorderLayout.SOUTH);
f.setSize(800,800);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class InnerPolygonShapePanel extends JPanel
implements MouseListener, MouseMotionListener
{
private final List<Point2D> points;
private Point2D draggedPoint;
private double offset = -10.0;
public InnerPolygonShapePanel()
{
this.points = new ArrayList<Point2D>();
points.add(new Point2D.Double(132,532));
points.add(new Point2D.Double(375,458));
points.add(new Point2D.Double(395,267));
points.add(new Point2D.Double(595,667));
addMouseListener(this);
addMouseMotionListener(this);
}
public void setOffset(double offset)
{
this.offset = offset;
repaint();
}
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.BLACK);
paint(g, points);
List<Point2D> offsetPolygonPoints =
computeOffsetPolygonPoints(points, offset);
g.setColor(Color.BLUE);
paint(g, offsetPolygonPoints);
}
private static void paint(Graphics2D g, List<Point2D> points)
{
for (int i = 0; i < points.size(); i++)
{
int i0 = i;
int i1 = (i + 1) % points.size();
Point2D p0 = points.get(i0);
Point2D p1 = points.get(i1);
g.draw(new Line2D.Double(p0, p1));
}
g.setColor(Color.RED);
for (Point2D p : points)
{
double r = 5;
g.draw(new Ellipse2D.Double(p.getX()-r, p.getY()-r, r+r, r+r));
}
}
private static List<Point2D> computeOffsetPolygonPoints(
List<Point2D> points, double offset)
{
List<Point2D> result = new ArrayList<Point2D>();
Point2D absoluteLocation = new Point2D.Double();
for (int i = 0; i < points.size(); i++)
{
// Consider three consecutive points (previous, current, next)
int ip = (i - 1 + points.size()) % points.size();
int ic = i;
int in = (i + 1) % points.size();
Point2D pp = points.get(ip);
Point2D pc = points.get(ic);
Point2D pn = points.get(in);
// Compute the line segments between the previous and the current
// point, and the current and the next point, and compute their
// normal
Point2D line0 = difference(pc, pp);
Point2D direction0 = normalize(line0);
Point2D normal0 = rotateCw(direction0);
Point2D line1 = difference(pn, pc);
Point2D direction1 = normalize(line1);
Point2D normal1 = rotateCw(direction1);
// Shift both line segments along the normal
Point2D segment0p0 = add(pp, offset, normal0);
Point2D segment0p1 = add(pc, offset, normal0);
Point2D segment1p0 = add(pc, offset, normal1);
Point2D segment1p1 = add(pn, offset, normal1);
// Compute the intersection between the shifted line segments
intersect(
segment0p0.getX(), segment0p0.getY(),
segment0p1.getX(), segment0p1.getY(),
segment1p0.getX(), segment1p0.getY(),
segment1p1.getX(), segment1p1.getY(),
null, absoluteLocation);
result.add(new Point2D.Double(
absoluteLocation.getX(), absoluteLocation.getY()));
}
return result;
}
#Override
public void mouseDragged(MouseEvent e)
{
if (draggedPoint != null)
{
draggedPoint.setLocation(e.getX(), e.getY());
repaint();
}
}
#Override
public void mousePressed(MouseEvent e)
{
final double thresholdSquared = 10 * 10;
Point2D p = e.getPoint();
Point2D closestPoint = null;
double minDistanceSquared = Double.MAX_VALUE;
for (Point2D point : points)
{
double dd = point.distanceSq(p);
if (dd < thresholdSquared && dd < minDistanceSquared)
{
minDistanceSquared = dd;
closestPoint = point;
}
}
draggedPoint = closestPoint;
}
#Override
public void mouseReleased(MouseEvent e)
{
draggedPoint = null;
}
#Override
public void mouseMoved(MouseEvent e)
{
// Nothing to do here
}
#Override
public void mouseClicked(MouseEvent e)
{
// Nothing to do here
}
#Override
public void mouseEntered(MouseEvent e)
{
// Nothing to do here
}
#Override
public void mouseExited(MouseEvent e)
{
// Nothing to do here
}
private static Point2D difference(Point2D p0, Point2D p1)
{
double dx = p0.getX() - p1.getX();
double dy = p0.getY() - p1.getY();
return new Point2D.Double(dx, dy);
}
private static Point2D add(Point2D p0, double factor, Point2D p1)
{
double x0 = p0.getX();
double y0 = p0.getY();
double x1 = p1.getX();
double y1 = p1.getY();
return new Point2D.Double(x0 + factor * x1, y0 + factor * y1);
}
private static Point2D rotateCw(Point2D p)
{
return new Point2D.Double(p.getY(), -p.getX());
}
private static Point2D normalize(Point2D p)
{
double x = p.getX();
double y = p.getY();
double length = Math.hypot(x, y);
return new Point2D.Double(x / length, y / length);
}
// From https://github.com/javagl/Geom/blob/master/src/main/java/
// de/javagl/geom/Intersections.java
private static final double DOUBLE_EPSILON = 1e-6;
/**
* Computes the intersection of the specified lines.
*
* Ported from
* http://www.geometrictools.com/LibMathematics/Intersection/
* Wm5IntrSegment2Segment2.cpp
*
* #param s0x0 x-coordinate of point 0 of line segment 0
* #param s0y0 y-coordinate of point 0 of line segment 0
* #param s0x1 x-coordinate of point 1 of line segment 0
* #param s0y1 y-coordinate of point 1 of line segment 0
* #param s1x0 x-coordinate of point 0 of line segment 1
* #param s1y0 y-coordinate of point 0 of line segment 1
* #param s1x1 x-coordinate of point 1 of line segment 1
* #param s1y1 y-coordinate of point 1 of line segment 1
* #param relativeLocation Optional location that stores the
* relative location of the intersection point on
* the given line segments
* #param absoluteLocation Optional location that stores the
* absolute location of the intersection point
* #return Whether the lines intersect
*/
public static boolean intersect(
double s0x0, double s0y0,
double s0x1, double s0y1,
double s1x0, double s1y0,
double s1x1, double s1y1,
Point2D relativeLocation,
Point2D absoluteLocation)
{
double dx0 = s0x1 - s0x0;
double dy0 = s0y1 - s0y0;
double dx1 = s1x1 - s1x0;
double dy1 = s1y1 - s1y0;
double invLen0 = 1.0 / Math.sqrt(dx0*dx0+dy0*dy0);
double invLen1 = 1.0 / Math.sqrt(dx1*dx1+dy1*dy1);
double dir0x = dx0 * invLen0;
double dir0y = dy0 * invLen0;
double dir1x = dx1 * invLen1;
double dir1y = dy1 * invLen1;
double dot = dotPerp(dir0x, dir0y, dir1x, dir1y);
if (Math.abs(dot) > DOUBLE_EPSILON)
{
if (relativeLocation != null || absoluteLocation != null)
{
double c0x = s0x0 + dx0 * 0.5;
double c0y = s0y0 + dy0 * 0.5;
double c1x = s1x0 + dx1 * 0.5;
double c1y = s1y0 + dy1 * 0.5;
double cdx = c1x - c0x;
double cdy = c1y - c0y;
double dot0 = dotPerp(cdx, cdy, dir0x, dir0y);
double dot1 = dotPerp(cdx, cdy, dir1x, dir1y);
double invDot = 1.0/dot;
double s0 = dot1*invDot;
double s1 = dot0*invDot;
if (relativeLocation != null)
{
double n0 = (s0 * invLen0) + 0.5;
double n1 = (s1 * invLen1) + 0.5;
relativeLocation.setLocation(n0, n1);
}
if (absoluteLocation != null)
{
double x = c0x + s0 * dir0x;
double y = c0y + s0 * dir0y;
absoluteLocation.setLocation(x, y);
}
}
return true;
}
return false;
}
/**
* Returns the perpendicular dot product, i.e. the length
* of the vector (x0,y0,0)x(x1,y1,0).
*
* #param x0 Coordinate x0
* #param y0 Coordinate y0
* #param x1 Coordinate x1
* #param y1 Coordinate y1
* #return The length of the cross product vector
*/
private static double dotPerp(double x0, double y0, double x1, double y1)
{
return x0*y1 - y0*x1;
}
}
I'm trying to create a health system for my 2d asteroid shoooter. I have the health icons instantiating now using:
Transform newHealthIcon = ((GameObject)Instantiate(healthIconGUI.gameObject)).transform;
However, when I try and set it's transform position and rotation the texture becomes invisible and no longer shows.
Transform newHealthIcon = ((GameObject)Instantiate(healthIconGUI.gameObject, transform.position, Quaternion.identity)).transform;
Here's the full code:
using UnityEngine;
using System.Collections;
public class Health : MonoBehaviour {
public int startingHealth;
public int healthPerIcon;
public GUITexture healthIconGUI;
private ArrayList healthIcons = new ArrayList();
public int maxIconsPerRow;
private float spacingX;
private float spacingY;
void Start () {
spacingX = healthIconGUI.pixelInset.width;
spacingY = healthIconGUI.pixelInset.height;
AddHealth (startingHealth / healthPerIcon);
}
public void AddHealth(int n) {
for (int i = 0; i < n; i++) {
Transform newHealthIcon = ((GameObject)Instantiate(healthIconGUI.gameObject, transform.position, Quaternion.identity)).transform;
newHealthIcon.parent = transform;
int y = Mathf.FloorToInt(healthIcons.Count / maxIconsPerRow);
int x = healthIcons.Count - y * maxIconsPerRow;
newHealthIcon.GetComponent<GUITexture> ().pixelInset = new Rect(x * spacingX, y * spacingY, 1, 1);
healthIcons.Add (newHealthIcon);
}
}
public void ModifyHealth (int amount) {
}
}
I am trying to develop a Tower Defense Game using javafx and I am having trouble as to how to make it so that the enemies move around the screen. Which classes and methods should I be using in order to approach this problem?
A tower defense game is too much to be covered on SO. I had a little bit of spare time and modified the engine I created in this thread.
Here's the main class with the game loop where the game is loaded, input is checked, sprites are moved, collision is checked, score is updated etc. In opposite to the other engine here you don't need keyboard input. Instead use a mouse click to position a tower. I added 4 initial towers.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextBoundsType;
import javafx.stage.Stage;
public class Game extends Application {
Random rnd = new Random();
Pane playfieldLayer;
Pane scoreLayer;
Image playerImage;
Image enemyImage;
List<Tower> towers = new ArrayList<>();;
List<Enemy> enemies = new ArrayList<>();;
Text scoreText = new Text();
int score = 0;
Scene scene;
#Override
public void start(Stage primaryStage) {
Group root = new Group();
// create layers
playfieldLayer = new Pane();
scoreLayer = new Pane();
root.getChildren().add( playfieldLayer);
root.getChildren().add( scoreLayer);
playfieldLayer.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
createTower(e.getX(), e.getY());
});
scene = new Scene( root, Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT);
primaryStage.setScene( scene);
primaryStage.show();
loadGame();
createScoreLayer();
createTowers();
AnimationTimer gameLoop = new AnimationTimer() {
#Override
public void handle(long now) {
// add random enemies
spawnEnemies( true);
// check if target is still valid
towers.forEach( tower -> tower.checkTarget());
// tower movement: find target
for( Tower tower: towers) {
tower.findTarget( enemies);
}
// movement
towers.forEach(sprite -> sprite.move());
enemies.forEach(sprite -> sprite.move());
// check collisions
checkCollisions();
// update sprites in scene
towers.forEach(sprite -> sprite.updateUI());
enemies.forEach(sprite -> sprite.updateUI());
// check if sprite can be removed
enemies.forEach(sprite -> sprite.checkRemovability());
// remove removables from list, layer, etc
removeSprites( enemies);
// update score, health, etc
updateScore();
}
};
gameLoop.start();
}
private void loadGame() {
playerImage = new Image( getClass().getResource("player.png").toExternalForm());
enemyImage = new Image( getClass().getResource("enemy.png").toExternalForm());
}
private void createScoreLayer() {
scoreText.setFont( Font.font( null, FontWeight.BOLD, 48));
scoreText.setStroke(Color.BLACK);
scoreText.setFill(Color.RED);
scoreLayer.getChildren().add( scoreText);
scoreText.setText( String.valueOf( score));
double x = (Settings.SCENE_WIDTH - scoreText.getBoundsInLocal().getWidth()) / 2;
double y = 0;
scoreText.relocate(x, y);
scoreText.setBoundsType(TextBoundsType.VISUAL);
}
private void createTowers() {
// position initial towers
List<Point2D> towerPositionList = new ArrayList<>();
towerPositionList.add(new Point2D( 100, 200));
towerPositionList.add(new Point2D( 100, 400));
towerPositionList.add(new Point2D( 800, 200));
towerPositionList.add(new Point2D( 800, 600));
for( Point2D pos: towerPositionList) {
createTower( pos.getX(), pos.getY());
}
}
private void createTower( double x, double y) {
Image image = playerImage;
// center image at position
x -= image.getWidth() / 2;
y -= image.getHeight() / 2;
// create player
Tower player = new Tower(playfieldLayer, image, x, y, 0, 0, 0, 0, Settings.PLAYER_SHIP_HEALTH, 0, Settings.PLAYER_SHIP_SPEED);
// register player
towers.add( player);
}
private void spawnEnemies( boolean random) {
if( random && rnd.nextInt(Settings.ENEMY_SPAWN_RANDOMNESS) != 0) {
return;
}
// image
Image image = enemyImage;
// random speed
double speed = rnd.nextDouble() * 1.0 + 2.0;
// x position range: enemy is always fully inside the screen, no part of it is outside
// y position: right on top of the view, so that it becomes visible with the next game iteration
double x = rnd.nextDouble() * (Settings.SCENE_WIDTH - image.getWidth());
double y = -image.getHeight();
// create a sprite
Enemy enemy = new Enemy( playfieldLayer, image, x, y, 0, 0, speed, 0, 1,1);
// manage sprite
enemies.add( enemy);
}
private void removeSprites( List<? extends SpriteBase> spriteList) {
Iterator<? extends SpriteBase> iter = spriteList.iterator();
while( iter.hasNext()) {
SpriteBase sprite = iter.next();
if( sprite.isRemovable()) {
// remove from layer
sprite.removeFromLayer();
// remove from list
iter.remove();
}
}
}
private void checkCollisions() {
for( Tower tower: towers) {
for( Enemy enemy: enemies) {
if( tower.hitsTarget( enemy)) {
enemy.getDamagedBy( tower);
// TODO: explosion
if( !enemy.isAlive()) {
enemy.setRemovable(true);
// increase score
score++;
}
}
}
}
}
private void updateScore() {
scoreText.setText( String.valueOf( score));
}
public static void main(String[] args) {
launch(args);
}
}
Then you need a base class for your sprites. You can use it for enemies and towers.
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
public abstract class SpriteBase {
Image image;
ImageView imageView;
Pane layer;
double x;
double y;
double r;
double dx;
double dy;
double dr;
double health;
double damage;
boolean removable = false;
double w;
double h;
boolean canMove = true;
public SpriteBase(Pane layer, Image image, double x, double y, double r, double dx, double dy, double dr, double health, double damage) {
this.layer = layer;
this.image = image;
this.x = x;
this.y = y;
this.r = r;
this.dx = dx;
this.dy = dy;
this.dr = dr;
this.health = health;
this.damage = damage;
this.imageView = new ImageView(image);
this.imageView.relocate(x, y);
this.imageView.setRotate(r);
this.w = image.getWidth(); // imageView.getBoundsInParent().getWidth();
this.h = image.getHeight(); // imageView.getBoundsInParent().getHeight();
addToLayer();
}
public void addToLayer() {
this.layer.getChildren().add(this.imageView);
}
public void removeFromLayer() {
this.layer.getChildren().remove(this.imageView);
}
public Pane getLayer() {
return layer;
}
public void setLayer(Pane layer) {
this.layer = layer;
}
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
public double getR() {
return r;
}
public void setR(double r) {
this.r = r;
}
public double getDx() {
return dx;
}
public void setDx(double dx) {
this.dx = dx;
}
public double getDy() {
return dy;
}
public void setDy(double dy) {
this.dy = dy;
}
public double getDr() {
return dr;
}
public void setDr(double dr) {
this.dr = dr;
}
public double getHealth() {
return health;
}
public double getDamage() {
return damage;
}
public void setDamage(double damage) {
this.damage = damage;
}
public void setHealth(double health) {
this.health = health;
}
public boolean isRemovable() {
return removable;
}
public void setRemovable(boolean removable) {
this.removable = removable;
}
public void move() {
if( !canMove)
return;
x += dx;
y += dy;
r += dr;
}
public boolean isAlive() {
return Double.compare(health, 0) > 0;
}
public ImageView getView() {
return imageView;
}
public void updateUI() {
imageView.relocate(x, y);
imageView.setRotate(r);
}
public double getWidth() {
return w;
}
public double getHeight() {
return h;
}
public double getCenterX() {
return x + w * 0.5;
}
public double getCenterY() {
return y + h * 0.5;
}
// TODO: per-pixel-collision
public boolean collidesWith( SpriteBase otherSprite) {
return ( otherSprite.x + otherSprite.w >= x && otherSprite.y + otherSprite.h >= y && otherSprite.x <= x + w && otherSprite.y <= y + h);
}
/**
* Reduce health by the amount of damage that the given sprite can inflict
* #param sprite
*/
public void getDamagedBy( SpriteBase sprite) {
health -= sprite.getDamage();
}
/**
* Set health to 0
*/
public void kill() {
setHealth( 0);
}
/**
* Set flag that the sprite can be removed from the UI.
*/
public void remove() {
setRemovable(true);
}
/**
* Set flag that the sprite can't move anymore.
*/
public void stopMovement() {
this.canMove = false;
}
public abstract void checkRemovability();
}
The towers are subclasses of the sprite base class. Here you need a little bit of math because you want the towers to rotate towards the enemies and let the towers fire when the enemy is within range.
import java.util.List;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.Image;
import javafx.scene.layout.Pane;
public class Tower extends SpriteBase {
SpriteBase target; // TODO: use weakreference
double turnRate = 0.6;
double speed;
double targetRange = 300; // distance within tower can lock to enemy
ColorAdjust colorAdjust;
double rotationLimitDeg=0.0;
double rotationLimitRad = Math.toDegrees( this.rotationLimitDeg);
double roatationEasing = 10;
double targetAngle = 0;
double currentAngle = 0;
boolean withinFiringRange = false;
public Tower(Pane layer, Image image, double x, double y, double r, double dx, double dy, double dr, double health, double damage, double speed) {
super(layer, image, x, y, r, dx, dy, dr, health, damage);
this.speed = speed;
this.setDamage(Settings.TOWER_DAMAGE);
init();
}
private void init() {
// red colorization (simulate "angry")
colorAdjust = new ColorAdjust();
colorAdjust.setContrast(0.0);
colorAdjust.setHue(-0.2);
}
#Override
public void move() {
SpriteBase follower = this;
// reset within firing range
withinFiringRange = false;
// rotate towards target
if( target != null)
{
// parts of code used from shane mccartney (http://lostinactionscript.com/page/3/)
double xDist = target.getCenterX() - follower.getCenterX();
double yDist = target.getCenterY() - follower.getCenterY();
this.targetAngle = Math.atan2(yDist, xDist) - Math.PI / 2;
this.currentAngle = Math.abs(this.currentAngle) > Math.PI * 2 ? (this.currentAngle < 0 ? (this.currentAngle % Math.PI * 2 + Math.PI * 2) : (this.currentAngle % Math.PI * 2)) : (this.currentAngle);
this.targetAngle = this.targetAngle + (Math.abs(this.targetAngle - this.currentAngle) < Math.PI ? (0) : (this.targetAngle - this.currentAngle > 0 ? ((-Math.PI) * 2) : (Math.PI * 2)));
this.currentAngle = this.currentAngle + (this.targetAngle - this.currentAngle) / roatationEasing; // give easing when rotation comes closer to the target point
// check if the rotation limit has to be kept
if( (this.targetAngle-this.currentAngle) > this.rotationLimitRad) {
this.currentAngle+=this.rotationLimitRad;
} else if( (this.targetAngle-this.currentAngle) < -this.rotationLimitRad) {
this.currentAngle-=this.rotationLimitRad;
}
follower.r = Math.toDegrees(currentAngle);
// determine if the player ship is within firing range; currently if the player ship is within 10 degrees (-10..+10)
withinFiringRange = Math.abs( Math.toDegrees( this.targetAngle-this.currentAngle)) < 20;
}
super.move();
}
public void checkTarget() {
if( target == null) {
return;
}
if( !target.isAlive() || target.isRemovable()) {
setTarget( null);
return;
}
//get distance between follower and target
double distanceX = target.getCenterX() - getCenterX();
double distanceY = target.getCenterY() - getCenterY();
//get total distance as one number
double distanceTotal = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
if( Double.compare( distanceTotal, targetRange) > 0) {
setTarget( null);
}
}
public void findTarget( List<? extends SpriteBase> targetList) {
// we already have a target
if( getTarget() != null) {
return;
}
SpriteBase closestTarget = null;
double closestDistance = 0.0;
for (SpriteBase target: targetList) {
if (!target.isAlive())
continue;
//get distance between follower and target
double distanceX = target.getCenterX() - getCenterX();
double distanceY = target.getCenterY() - getCenterY();
//get total distance as one number
double distanceTotal = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
// check if enemy is within range
if( Double.compare( distanceTotal, targetRange) > 0) {
continue;
}
if (closestTarget == null) {
closestTarget = target;
closestDistance = distanceTotal;
} else if (Double.compare(distanceTotal, closestDistance) < 0) {
closestTarget = target;
closestDistance = distanceTotal;
}
}
setTarget(closestTarget);
}
public SpriteBase getTarget() {
return target;
}
public void setTarget(SpriteBase target) {
this.target = target;
}
#Override
public void checkRemovability() {
if( Double.compare( health, 0) < 0) {
setTarget(null);
setRemovable(true);
}
}
public boolean hitsTarget( SpriteBase enemy) {
return target == enemy && withinFiringRange;
}
public void updateUI() {
if( withinFiringRange) {
imageView.setEffect(colorAdjust);
} else {
imageView.setEffect(null);
}
super.updateUI();
}
}
The enemy class is easier. It needs only movement. However, in your final version the enemies should consider obstacles during movement. In this example I add a health bar above the enemy to show the health.
import javafx.scene.image.Image;
import javafx.scene.layout.Pane;
public class Enemy extends SpriteBase {
HealthBar healthBar;
double healthMax;
public Enemy(Pane layer, Image image, double x, double y, double r, double dx, double dy, double dr, double health, double damage) {
super(layer, image, x, y, r, dx, dy, dr, health, damage);
healthMax = Settings.ENEMY_HEALTH;
setHealth(healthMax);
}
#Override
public void checkRemovability() {
if( Double.compare( getY(), Settings.SCENE_HEIGHT) > 0) {
setRemovable(true);
}
}
public void addToLayer() {
super.addToLayer();
// create health bar; has to be created here because addToLayer is called in super constructor
// and it wouldn't exist yet if we'd create it as class member
healthBar = new HealthBar();
this.layer.getChildren().add(this.healthBar);
}
public void removeFromLayer() {
super.removeFromLayer();
this.layer.getChildren().remove(this.healthBar);
}
/**
* Health as a value from 0 to 1.
* #return
*/
public double getRelativeHealth() {
return getHealth() / healthMax;
}
public void updateUI() {
super.updateUI();
// update health bar
healthBar.setValue( getRelativeHealth());
// locate healthbar above enemy, centered horizontally
healthBar.relocate(x + (imageView.getBoundsInLocal().getWidth() - healthBar.getBoundsInLocal().getWidth()) / 2, y - healthBar.getBoundsInLocal().getHeight() - 4);
}
}
The health bar
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
public class HealthBar extends Pane {
Rectangle outerHealthRect;
Rectangle innerHealthRect;
public HealthBar() {
double height = 10;
double outerWidth = 60;
double innerWidth = 40;
double x=0.0;
double y=0.0;
outerHealthRect = new Rectangle( x, y, outerWidth, height);
outerHealthRect.setStroke(Color.BLACK);
outerHealthRect.setStrokeWidth(2);
outerHealthRect.setStrokeType( StrokeType.OUTSIDE);
outerHealthRect.setFill(Color.RED);
innerHealthRect = new Rectangle( x, y, innerWidth, height);
innerHealthRect.setStrokeType( StrokeType.OUTSIDE);
innerHealthRect.setFill(Color.LIMEGREEN);
getChildren().addAll( outerHealthRect, innerHealthRect);
}
public void setValue( double value) {
innerHealthRect.setWidth( outerHealthRect.getWidth() * value);
}
}
And then you need some global settings like this
public class Settings {
public static double SCENE_WIDTH = 1024;
public static double SCENE_HEIGHT = 768;
public static double TOWER_DAMAGE = 1;
public static double PLAYER_SHIP_SPEED = 4.0;
public static double PLAYER_SHIP_HEALTH = 100.0;
public static int ENEMY_HEALTH = 100;
public static int ENEMY_SPAWN_RANDOMNESS = 50;
}
These are the images:
player.png
enemy.png
So summarized the gameplay is for now:
click on the screen to place a tower ( ie a smiley)
when an enemy is in range, the smiley becomes angry (i just change the color to red), in your final version the tower would be firing
as long as the tower is firing at the enemy, the health is reduced. i did it by changing the health bar depending on the health
when the health is depleted, the enemy is killed and the score is updated; you'd have to add an explosion
So all in all it's not so easy to create a tower defense game. Hope it helps as a start.
Heres' a screenshot:
I am trying to put together a dialog that should look like this:
Fill in the below fields
_______________ likes ____________________
where the "_" lines are the EditFields.
I am sticking all the fields in a HorizontalFieldManager, which I add to the dialog. Unfortunately, the first EditField consumes all the space on the first line. I have tried to override the getPreferredWidth() method of the EditField by creating my own class extending BasicEditField, but have had no success.
Surely there must be a simple way to force a certain size for an edit field. What am I missing?
Just like DaveJohnston said:
class LikesHFManager extends HorizontalFieldManager {
EditField mEditFieldLeft;
LabelField mLabelField;
EditField mEditFieldRight;
String STR_LIKES = "likes";
int mLabelWidth = 0;
int mEditWidth = 0;
int mOffset = 4;
public LikesHFManager() {
mEditFieldLeft = new EditField();
mLabelField = new LabelField(STR_LIKES);
mEditFieldRight = new EditField();
mLabelWidth = mLabelField.getFont().getAdvance(STR_LIKES);
int screenWidth = Display.getWidth();
mEditWidth = (screenWidth - mLabelWidth) >> 1;
mEditWidth -= 2 * mOffset;
// calculate max with of one character
int chMaxWith = mEditFieldLeft.getFont().getAdvance("W");
// calculate max count of characters in edit field
int chMaxCnt = mEditWidth / chMaxWith;
mEditFieldLeft.setMaxSize(chMaxCnt);
mEditFieldRight.setMaxSize(chMaxCnt);
add(mEditFieldLeft);
add(mLabelField);
add(mEditFieldRight);
}
protected void sublayout(int maxWidth, int maxHeight) {
int x = 0;
int y = 0;
int editHeight = mEditFieldLeft.getPreferredHeight();
int labelHeight = mLabelField.getPreferredHeight();
setPositionChild(mEditFieldLeft, x, y);
layoutChild(mEditFieldLeft, mEditWidth, editHeight);
x += mEditWidth;
x += mOffset;
setPositionChild(mLabelField, x, y);
layoutChild(mLabelField, mLabelWidth, labelHeight);
x += mLabelWidth;
x += mOffset;
setPositionChild(mEditFieldRight, x, y);
layoutChild(mEditFieldRight, mEditWidth, editHeight);
x += mEditWidth;
setExtent(x, Math.max(labelHeight, editHeight));
}
}
Try subclassing HorizontalFieldManager and override the sublayout method:
protected void sublayout(int maxWidth, int maxHeight) { }
In this method you should call setPositionChild() and layoutChild() for each component you are adding so you can control the positioning and size of each.
You should also override the layout method of each component and call
setExtent(getPreferredWidth(), getPreferredHeight());
this will make use of your implementation of the getPreferred... methods you have already written.
Hope this helps.
Building on Max Gontar's solution, this should solve the general problem of assigning width to sub Fields of HorizontalFieldManagers:
import net.rim.device.api.ui.container.*;
import net.rim.device.api.ui.*;
public class FieldRowManager extends HorizontalFieldManager {
public FieldRowManager(final long style)
{
super(style);
}
public FieldRowManager()
{
this(0);
}
private SubField FirstSubField = null;
private SubField LastSubField = null;
private static class SubField
{
public final Field Field;
public final int Width;
public final int Offset;
private SubField Next;
public SubField(final FieldRowManager container, final Field field, final int width, final int offset)
{
Field = field;
Width = width;
Offset = offset;
if (container.LastSubField == null)
{
container.FirstSubField = this;
}
else
{
container.LastSubField.Next = this;
}
container.LastSubField = this;
}
public SubField getNext()
{
return Next;
}
}
public void add(final Field field)
{
add(field, field.getPreferredWidth());
}
public void add(final Field field, final int width)
{
add(field, width, 0);
}
public void add(final Field field, final int width, final int offset)
{
new SubField(this, field, width, offset);
super.add(field);
}
protected void sublayout(final int maxWidth, final int maxHeight)
{
int x = 0;
int height = 0;
SubField subField = FirstSubField;
while (subField != null)
{
final Field field = subField.Field;
final int fieldHeight = field.getPreferredHeight();
this.setPositionChild(field, x, 0);
this.layoutChild(field, subField.Width, fieldHeight);
x += subField.Width+subField.Offset;
if (fieldHeight > height)
{
height = fieldHeight;
}
subField = subField.getNext();
}
this.setExtent(x, height);
}
}
Just call the overloads of the add method to specify the width, and the offset space before the next Field. Though this does not allow for deleting/replacing Fields.
It is irksome that RIM does not provide this functionality in the standard library. HorizontalFieldManager should just work this way.