Te sorprenderá lo poco que se necesita código para crear un juego de arcade en 2D
FXGL, el marco de desarrollo de juegos JavaFX , es exactamente lo que necesita para ampliar sus habilidades de Java y convertirse en un desarrollador de juegos. FXGL es una dependencia que agrega a su proyecto Java y JavaFX ; no necesita ninguna instalación o configuración adicional. Funciona de inmediato en todas las plataformas.
Gracias a la API FXGL simple y limpia, puede crear juegos 2D con un código mínimo y entregarlos como un solo
Puede ver ejemplos básicos del uso de la biblioteca FXGL en el proyecto principal de GitHub . Los juegos más complejos se proporcionan en un proyecto separado, FXGLGames . También puede utilizar FXGL para crear aplicaciones comerciales con controles de interfaz de usuario complejos; también hay controles de interfaz 3D en la biblioteca, pero aún son experimentales.
En este artículo, verá todo el código necesario para crear un juego divertido en el que Duke dispara en círculo mientras se mueve, evitando los servidores en la nube flotantes. Antes de continuar, es posible que desee ver un video de un minuto que muestra cómo se juega el juego.https://player.vimeo.com/video/556576921
En un artículo de seguimiento, verá cómo controlar el juego con un joystick en un dispositivo Raspberry Pi . Por ahora, sin embargo, el objetivo es crear el juego en sí.
Activos de imagen y código fuente
El proyecto terminado, llamado JavaMagazineFXGL, está disponible en GitHub . Este es un proyecto de Maven con el que puedes construir
<dependencies>
<dependency>
<groupId>com.github.almasb</groupId>
<artifactId>fxgl</artifactId>
<version>${fxgl.version}</version>
</dependency>
</dependencies>
El código completo para el juego consta de sólo unas pocas clases, que incluyen las
De forma predeterminada, FXGL carga imágenes del
Figura 1. El personaje de Duke: duke.png
Figura 2. Los obstáculos de la nube: cloud-network.png
Figura 3. Bala de Duke: sprite_bullet.png
La EntityFactory
Todos los objetos del juego en una aplicación FXGL son de tipo
public enum EntityType {
BACKGROUND, CENTER, DUKE, CLOUD, BULLET
}
Al definir una enumeración con los tipos, es más fácil hacer referencia a ellos más adelante, como en la detección de colisiones. Para cada tipo de entidad, un
@Spawns("background")
public Entity spawnBackground(SpawnData data) {
return entityBuilder(data)
.type(EntityType.BACKGROUND)
.view(new Rectangle(data.<Integer>get("width"),
data.<Integer>get("height"), Color.YELLOWGREEN))
.with(new IrremovableComponent())
.zIndex(-100)
.build();
}
@Spawns("center")
public Entity spawnCenter(SpawnData data) {
return entityBuilder(data)
.type(EntityType.CENTER)
.collidable()
.viewWithBBox(new Circle(data.<Integer>get("x"),
data.<Integer>get("y"), data.<Integer>get("radius"), Color.DARKRED))
.with(new IrremovableComponent())
.zIndex(-99)
.build();
}
Para el personaje de Duke y las nubes, le di a las imágenes una
@Spawns("duke")
public Entity newDuke(SpawnData data) {
return entityBuilder(data)
.type(EntityType.DUKE)
.viewWithBBox(texture("duke.png", 50, 50))
.collidable()
.with((new AutoRotationComponent()).withSmoothing())
.with(new PlayerComponent())
.build();
}
@Spawns("cloud")
public Entity newCloud(SpawnData data) {
return entityBuilder(data)
.type(EntityType.CLOUD)
.viewWithBBox(texture("cloud-network.png", 50, 50))
.with((new AutoRotationComponent()).withSmoothing())
.with(new CloudComponent())
.collidable()
.build();
}
La viñeta no necesita un componente personalizado, porque los FXGL
@Spawns("bullet")
public Entity newBullet(SpawnData data) {
return entityBuilder(data)
.type(EntityType.BULLET)
.viewWithBBox(texture("sprite_bullet.png", 22, 11))
.collidable()
.with(new ProjectileComponent(data.get("direction"), 350), new OffscreenCleanComponent())
.build();
}
El CloudComponent
La
En este caso, el juego mueve la nube en la dirección que se calculó aleatoriamente al inicializar el componente. El juego comprueba si la nube llega al borde del juego y la elimina del mundo del juego si lo hace.
public class CloudComponent extends Component {
private final Point2D direction = new Point2D(
FXGLMath.random(-1D, 1D),
FXGLMath.random(-1D, 1D)
);
@Override
public void onUpdate(double tpf) {
entity.translate(direction.multiply(3));
checkForBounds();
}
private void checkForBounds() {
if (entity.getX() < 0) {
remove();
}
if (entity.getX() >= getAppWidth()) {
remove();
}
if (entity.getY() < 0) {
remove();
}
if (entity.getY() >= getAppHeight()) {
remove();
}
}
public void remove() {
entity.removeFromWorld();
}
}
El PlayerComponent
El
El
El
public class PlayerComponent extends Component {
private static final double ROTATION_CHANGE = 0.01;
private Point2D direction = new Point2D(1, 1);
@Override
public void onUpdate(double tpf) {
entity.translate(direction.multiply(1));
checkForBounds();
}
private void checkForBounds() {
if (entity.getX() < 0) {
die();
}
if (entity.getX() >= getAppWidth()) {
die();
}
if (entity.getY() < 0) {
die();
}
if (entity.getY() >= getAppHeight()) {
die();
}
}
public void shoot() {
spawn("bullet", new SpawnData(
getEntity().getPosition().getX() + 20,
getEntity().getPosition().getY() - 5)
.put("direction", direction));
}
public void die() {
inc("lives", -1);
if (geti("lives") <= 0) {
getDialogService().showMessageBox("Game Over",
() -> getGameController().startNewGame());
return;
}
entity.setPosition(0, 0);
direction = new Point2D(1, 1);
right();
}
public void up() {
if (direction.getY() > -1) {
direction = new Point2D(direction.getX(), direction.getY() - ROTATION_CHANGE);
}
}
public void down() {
if (direction.getY() < 1) {
direction = new Point2D(direction.getX(), direction.getY() + ROTATION_CHANGE);
}
}
public void left() {
if (direction.getX() > -1) {
direction = new Point2D(direction.getX() - ROTATION_CHANGE, direction.getY());
}
}
public void right() {
if (direction.getX() < 1) {
direction = new Point2D(direction.getX() + ROTATION_CHANGE, direction.getY());
}
}
}
Anulaciones de la aplicación de clase principal y FXGL
Con las tres clases anteriores, todo está preparado para crear el juego. Todo lo que queda es la
- Campos de instancia de su subclase de
GameApplication
-
, que inicializa la configuración de la aplicaciónInitSettings()
- Configuración de servicios, después de lo cual puede llamar de forma segura a cualquier
métodoFXGL.*
- Lo siguiente, que se ejecuta en un hilo de interfaz de usuario de JavaFX
-
, que inicializa entradas, como pulsaciones de teclas y botones del mouseinitInput()
-
, que se llama una vez por vida de la aplicación, antesonPreInit()initGame()
-
- Lo siguiente, que no se ejecuta en el hilo de la interfaz de usuario de JavaFX
-
, que se puede anular para proporcionar variables globalesinitGameVars()
-
, que inicializa los objetos de cada juegoinitGame()
-
, que inicializa los controladores de colisiones y las propiedades físicasinitPhysics()
-
, que inicia la ejecución del bucle principal del juego en el hilo de la interfaz de usuario de JavaFXinitUI()
-
Debido a que el juego debe ejecutarse en pantalla completa con las dimensiones correctas, la aplicación lee estos valores en el
private final GameFactory gameFactory = new GameFactory();
private Entity player;
private static int screenWidth;
private static int screenHeight;
public static void main(String[] args) {
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
screenWidth = (int) screenSize.getWidth();
screenHeight = (int) screenSize.getHeight();
launch(args);
}
Anulaciones FXGL.
@Override
protected void initSettings(GameSettings settings) {
settings.setHeight(screenHeight);
settings.setWidth(screenWidth);
settings.setFullScreenAllowed(true);
settings.setFullScreenFromStart(true);
settings.setTitle("Oracle Java Magazine - FXGL");
}
Override
@Override
protected void initInput() {
onKey(KeyCode.LEFT, "left", () -> this.player.getComponent(PlayerComponent.class).left());
onKey(KeyCode.RIGHT, "right", () -> this.player.getComponent(PlayerComponent.class).right());
onKey(KeyCode.UP, "up", () -> this.player.getComponent(PlayerComponent.class).up());
onKey(KeyCode.DOWN, "down", () -> this.player.getComponent(PlayerComponent.class).down());
onKeyDown(KeyCode.SPACE, "Bullet", () -> this.player.getComponent(PlayerComponent.class).shoot());
}
El juego necesita un valor para el número de vidas que quedan y un valor para la puntuación. Ambos se definen
@Override
protected void initGameVars(Map<String, Object> vars) {
vars.put("score", 0);
vars.put("lives", 5);
}
¡Agreguemos algunas cosas al mundo del juego! Primero, agregue la fábrica de entidades que genera las entidades del juego. Luego, es hora de agregar los engendros: primero un fondo de pantalla completa, luego un círculo en el medio de la pantalla y, finalmente, Duke como la entidad del jugador.
@Override
protected void initGame() {
getGameWorld().addEntityFactory(this.gameFactory);
// Background color
spawn("background", new SpawnData(0, 0).put("width", getAppWidth())
.put("height", getAppHeight()));
// Circle in the middle of the screen
int circleRadius = 80;
spawn("center", new SpawnData(
(getAppWidth() / 2) - (circleRadius / 2),
(getAppHeight() / 2) - (circleRadius / 2))
.put("x", (circleRadius / 2))
.put("y", (circleRadius / 2))
.put("radius", circleRadius));
// Add the player
this.player = spawn("duke", 0, 0);
}
A continuación, defina los controladores de colisiones de la siguiente manera:
- Siempre que Duke golpea una nube o el círculo central, el jugador pierde una vida.
- Siempre que una bala golpea una nube, la puntuación del jugador aumenta; ambas entidades deben eliminarse del juego.
Use lambdas para definir el tipo de entidades que deben manejarse y las acciones que deben tomarse.
@Override
protected void initPhysics() {
onCollisionBegin(EntityType.DUKE, EntityType.CENTER, (duke, center) ->
this.player.getComponent(PlayerComponent.class).die());
onCollisionBegin(EntityType.DUKE, EntityType.CLOUD, (enemy, cloud) ->
this.player.getComponent(PlayerComponent.class).die());
onCollisionBegin(EntityType.BULLET, EntityType.CLOUD, (bullet, cloud) -> {
inc("score", 1);
bullet.removeFromWorld();
cloud.removeFromWorld();
});
}
El jugador necesita ver la puntuación y las variables de vidas restantes definidas en
@Override
protected void initUI() {
Text scoreLabel = getUIFactoryService().newText("Score", Color.BLACK, 22);
Text scoreValue = getUIFactoryService().newText("", Color.BLACK, 22);
Text livesLabel = getUIFactoryService().newText("Lives", Color.BLACK, 22);
Text livesValue = getUIFactoryService().newText("", Color.BLACK, 22);
scoreLabel.setTranslateX(20);
scoreLabel.setTranslateY(20);
scoreValue.setTranslateX(90);
scoreValue.setTranslateY(20);
livesLabel.setTranslateX(getAppWidth() - 100);
livesLabel.setTranslateY(20);
livesValue.setTranslateX(getAppWidth() - 30);
livesValue.setTranslateY(20);
scoreValue.textProperty().bind(getWorldProperties().intProperty("score").asString());
livesValue.textProperty().bind(getWorldProperties().intProperty("lives").asString());
getGameScene().addUINodes(scoreLabel, scoreValue, livesLabel, livesValue);
}
Esas fueron todas las anulaciones llamadas durante la inicialización necesarias para el juego. Hay una inicialización final, pero se llama cada fotograma cuando el juego está en estado de juego: el juego siempre tendrá 10 nubes en la pantalla. Al agregar una nube por cuadro, si es necesario, las nubes aparecerán una por una al comienzo del juego.
@Override
protected void onUpdate(double tpf) {
if (getGameWorld().getEntitiesByType(EntityType.CLOUD).size() < 10) {
spawn("cloud", getAppWidth() / 2, getAppHeight() / 2);
}
}
Fuente https://blogs.oracle.com/javamagazine/java-javafx-fxgl-game-development