我是JavaFX的新手,我正在尝试编写一个动画2D角色穿过屏幕的游戏(例如最初的塞尔达传说游戏)。我在Swing中完成了这个,通过创建我自己的Sprite类并覆盖Swing中的paintComponent()方法,然后放入我自己的g2d.drawImage(...);在那里调用,它会将正确的子帧从Sprite复制到JPanel中正确的x,y目的地,从而实现动画(行走)2D图像在屏幕上的移动。
如何在JavaFX中执行此操作?我找到了关于如何创建精灵的这个很好的例子: http://blog.netopyr.com/2012/03/09/creating-a-sprite-animation-with-javafx/ 在JavaFX中,但我如何处理PaintComponent和drawImage(...)部分? JavaFX是否有与PaintComponent等效的方法重新绘制所有内容? 我试过调用primaryStage.show();方法,但是没有用。
我想我还不清楚如何设置我的主游戏循环,触发刷新并在不同的x,y位置在场景上绘制图像?
JavaFX与drawImage(...)方法等效的是什么?这种方法允许我在任何给定的x,y偏移处将源图像的一部分复制到目标场景中。这就是我在Swing中在屏幕上移动动画精灵的方法。
最诚挚的问候,
Zareh
答案 0 :(得分:23)
您的问题太多,无法涵盖在SO上。但是我创造了一个简单的引擎"为你开始。它的一般用途,也适合您的需求。
加载游戏的游戏循环的主类,检查输入,移动精灵,检查碰撞,更新分数等等
public class Game extends Application {
Random rnd = new Random();
Pane playfieldLayer;
Pane scoreLayer;
Image playerImage;
Image enemyImage;
List<Player> players = new ArrayList<>();
List<Enemy> enemies = new ArrayList<>();
Text collisionText = new Text();
boolean collision = false;
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);
scene = new Scene( root, Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT);
primaryStage.setScene( scene);
primaryStage.show();
loadGame();
createScoreLayer();
createPlayers();
AnimationTimer gameLoop = new AnimationTimer() {
@Override
public void handle(long now) {
// player input
players.forEach(sprite -> sprite.processInput());
// add random enemies
spawnEnemies( true);
// movement
players.forEach(sprite -> sprite.move());
enemies.forEach(sprite -> sprite.move());
// check collisions
checkCollisions();
// update sprites in scene
players.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() {
collisionText.setFont( Font.font( null, FontWeight.BOLD, 64));
collisionText.setStroke(Color.BLACK);
collisionText.setFill(Color.RED);
scoreLayer.getChildren().add( collisionText);
// TODO: quick-hack to ensure the text is centered; usually you don't have that; instead you have a health bar on top
collisionText.setText("Collision");
double x = (Settings.SCENE_WIDTH - collisionText.getBoundsInLocal().getWidth()) / 2;
double y = (Settings.SCENE_HEIGHT - collisionText.getBoundsInLocal().getHeight()) / 2;
collisionText.relocate(x, y);
collisionText.setText("");
collisionText.setBoundsType(TextBoundsType.VISUAL);
}
private void createPlayers() {
// player input
Input input = new Input( scene);
// register input listeners
input.addListeners(); // TODO: remove listeners on game over
Image image = playerImage;
// center horizontally, position at 70% vertically
double x = (Settings.SCENE_WIDTH - image.getWidth()) / 2.0;
double y = Settings.SCENE_HEIGHT * 0.7;
// create player
Player player = new Player(playfieldLayer, image, x, y, 0, 0, 0, 0, Settings.PLAYER_SHIP_HEALTH, 0, Settings.PLAYER_SHIP_SPEED, input);
// register player
players.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() {
collision = false;
for( Player player: players) {
for( Enemy enemy: enemies) {
if( player.collidesWith(enemy)) {
collision = true;
}
}
}
}
private void updateScore() {
if( collision) {
collisionText.setText("Collision");
} else {
collisionText.setText("");
}
}
public static void main(String[] args) {
launch(args);
}
}
sprite的基类,包括诸如移动等常用方法
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();
}
精灵类的子类,如播放器......
public class Player extends SpriteBase {
double playerShipMinX;
double playerShipMaxX;
double playerShipMinY;
double playerShipMaxY;
Input input;
double speed;
public Player(Pane layer, Image image, double x, double y, double r, double dx, double dy, double dr, double health, double damage, double speed, Input input) {
super(layer, image, x, y, r, dx, dy, dr, health, damage);
this.speed = speed;
this.input = input;
init();
}
private void init() {
// calculate movement bounds of the player ship
// allow half of the ship to be outside of the screen
playerShipMinX = 0 - image.getWidth() / 2.0;
playerShipMaxX = Settings.SCENE_WIDTH - image.getWidth() / 2.0;
playerShipMinY = 0 - image.getHeight() / 2.0;
playerShipMaxY = Settings.SCENE_HEIGHT -image.getHeight() / 2.0;
}
public void processInput() {
// ------------------------------------
// movement
// ------------------------------------
// vertical direction
if( input.isMoveUp()) {
dy = -speed;
} else if( input.isMoveDown()) {
dy = speed;
} else {
dy = 0d;
}
// horizontal direction
if( input.isMoveLeft()) {
dx = -speed;
} else if( input.isMoveRight()) {
dx = speed;
} else {
dx = 0d;
}
}
@Override
public void move() {
super.move();
// ensure the ship can't move outside of the screen
checkBounds();
}
private void checkBounds() {
// vertical
if( Double.compare( y, playerShipMinY) < 0) {
y = playerShipMinY;
} else if( Double.compare(y, playerShipMaxY) > 0) {
y = playerShipMaxY;
}
// horizontal
if( Double.compare( x, playerShipMinX) < 0) {
x = playerShipMinX;
} else if( Double.compare(x, playerShipMaxX) > 0) {
x = playerShipMaxX;
}
}
@Override
public void checkRemovability() {
// TODO Auto-generated method stub
}
}
......和敌人
public class Enemy extends SpriteBase {
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);
}
@Override
public void checkRemovability() {
if( Double.compare( getY(), Settings.SCENE_HEIGHT) > 0) {
setRemovable(true);
}
}
}
您还需要一个输入机制来控制播放器精灵
public class Input {
/**
* Bitset which registers if any {@link KeyCode} keeps being pressed or if it is released.
*/
private BitSet keyboardBitSet = new BitSet();
// -------------------------------------------------
// default key codes
// will vary when you let the user customize the key codes or when you add support for a 2nd player
// -------------------------------------------------
private KeyCode upKey = KeyCode.UP;
private KeyCode downKey = KeyCode.DOWN;
private KeyCode leftKey = KeyCode.LEFT;
private KeyCode rightKey = KeyCode.RIGHT;
private KeyCode primaryWeaponKey = KeyCode.SPACE;
private KeyCode secondaryWeaponKey = KeyCode.CONTROL;
Scene scene;
public Input( Scene scene) {
this.scene = scene;
}
public void addListeners() {
scene.addEventFilter(KeyEvent.KEY_PRESSED, keyPressedEventHandler);
scene.addEventFilter(KeyEvent.KEY_RELEASED, keyReleasedEventHandler);
}
public void removeListeners() {
scene.removeEventFilter(KeyEvent.KEY_PRESSED, keyPressedEventHandler);
scene.removeEventFilter(KeyEvent.KEY_RELEASED, keyReleasedEventHandler);
}
/**
* "Key Pressed" handler for all input events: register pressed key in the bitset
*/
private EventHandler<KeyEvent> keyPressedEventHandler = new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
// register key down
keyboardBitSet.set(event.getCode().ordinal(), true);
}
};
/**
* "Key Released" handler for all input events: unregister released key in the bitset
*/
private EventHandler<KeyEvent> keyReleasedEventHandler = new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
// register key up
keyboardBitSet.set(event.getCode().ordinal(), false);
}
};
// -------------------------------------------------
// Evaluate bitset of pressed keys and return the player input.
// If direction and its opposite direction are pressed simultaneously, then the direction isn't handled.
// -------------------------------------------------
public boolean isMoveUp() {
return keyboardBitSet.get( upKey.ordinal()) && !keyboardBitSet.get( downKey.ordinal());
}
public boolean isMoveDown() {
return keyboardBitSet.get( downKey.ordinal()) && !keyboardBitSet.get( upKey.ordinal());
}
public boolean isMoveLeft() {
return keyboardBitSet.get( leftKey.ordinal()) && !keyboardBitSet.get( rightKey.ordinal());
}
public boolean isMoveRight() {
return keyboardBitSet.get( rightKey.ordinal()) && !keyboardBitSet.get( leftKey.ordinal());
}
public boolean isFirePrimaryWeapon() {
return keyboardBitSet.get( primaryWeaponKey.ordinal());
}
public boolean isFireSecondaryWeapon() {
return keyboardBitSet.get( secondaryWeaponKey.ordinal());
}
}
以及一些全局设置
public class Settings {
public static double SCENE_WIDTH = 400;
public static double SCENE_HEIGHT = 800;
public static double PLAYER_SHIP_SPEED = 4.0;
public static double PLAYER_SHIP_HEALTH = 100.0;
public static double PLAYER_MISSILE_SPEED = 4.0;
public static double PLAYER_MISSILE_HEALTH = 200.0;
public static int ENEMY_SPAWN_RANDOMNESS = 100;
}
您可以使用任何图像作为精灵。我从维基百科那里拿走了我的:
player.png
enemy.png
如果你将它全部放入游戏包中,你可以启动Game.java。它会给你一个可控的笑脸,带有向下滚动的僵尸表情符号。你必须逃避他们。我为您保留了没有透明度的图像,以便您注意到我使用了简单的矩形碰撞检测。您可能会进行逐像素碰撞检测。
看起来像这样:
我不认为这是 解决方案,它只是 解决方案。例如,您必须限制动画计时器。或者您可能希望设置每秒的移动而不是每帧等。
如果您想了解更多信息,请随时查看我发现自己的博客How to create a 2D Shoot'em'up with JavaFX。在那里你也会找到e。 G。如何添加动画精灵(我还从您发布的链接中了解到),滚动背景,其他图层顶部的云层等等。我希望它可以帮助您。