zoukankan      html  css  js  c++  java
  • Writing a Tile Engine in JavaFX

    http://jayskills.com/blog/2013/01/09/writing-a-tile-engine-in-javafx/

    ————————————————————————————————————————————————————————————————————

    Artikel Navigation

    Writing a Tile Engine in JavaFX

    With the advent of embedded versions of JavaFX, our framework has become more interesting for game development, since we now can target small consumer devices like tablets & smartphones. So I decided to do a little more experimenting with JavaFX for writing Games. This time I wanted to use Canvas to have more control over the rendering in order to be able to optimize the performance on smaller devices. These are my experiences when writing a Tile Engine.

    What’s a Tile Engine?

    Back in the early days game consoles & computers had very limited resources. So in order to have games with thousands of large screens developers needed to come up with a method to store the screens in a format other than a bitmap per screen. So Tile Engines were invented that can generate large screens from a limited set of re-usable smaller graphics (Tiles). This saves ram and improves rendering performance.

    TileMaps

    The instructions how to generate the screen are stored in TileMaps. Those maps are typically organized as a 2-dimensional matrix of Tile ids. Usually the tiles are organized in layers to allow for a simple Z-ordering and more flexibility in combining graphics with different backgrounds. Usually TileMaps also support storing of meta data, for example if certain tiles are blocked, or spawn points for enemies.

    A TileMap with several layers created with the

    TileSets

    The tiles referenced in the map are usually stored in TileSets that consist of a single bitmap and meta information about how to divide it into tiles. Here’s an example of such an image from opengameart.com, a site that hosts game assets with Open Source Licences. In my examples I use some of these graphics.

    A typical TileSet Image sized 1024 x 1024 (^2 = good for graphics cards)

    ObjectGroups

    One additional feature of the TMX format are Object Layers. These special layers can be used to define freeform shapes and polylines and assign properties to them. The basic idea behind that is that we can use them to define areas where Sprites are created (spawnpoints), exits, portals, and non-rectangular collision shapes. It’s up to the creator of the TileEngine or the developer who builds games with it to define how to handle the ObjectGroups. I’m planning to use them extensively, and they are a very nice extension point for declaratively defining the gameplay. You can e.g. use them to define animations, skript dialogs, etc..

    Spawnpoints defined in the

    Workflow, Tools & Formats

    The idea of tilemaps also allows for a nice workflow. Graphic designers can create the assets and game designers can import them into a level editor like “Tiled” and design the levels via drag & drop. The maps are stored in a machine readable TileMap format. Tiled for example uses the TMX Map format for storing the TileMap. That’s a very simple XML format, that can then be loaded by the TileEngine. For my implementation I decided to use the TMX Format, so I can use “Tiled” for designing the levels.

    Implementation in JavaFX

    For the implementation I decided to use JavaFX Canvas immediate mode rendering instead of the retained mode rendering when using individual Nodes. This gives me a bit more control for optimizing the performance on small devices like the Raspberry Pi.

    Reading TMX/TSX files

    The first thing we need is a way to read the TileMap (TMX) and TileSet (TSX) files. With JAXB it’s pretty simple to create a TileMapReader that can create POJOs from a file. So if you use the Engine you simply call:

    TileMap map = TileMapReader.readMap(“path/to/my/map.tmx”);

    The Camera

    Since in most games the TileMaps will be larger than the screen, only a part of the Map is rendered. Usually the map is centered on the hero. You can do that by simply tracking the map position of upper left corner of the screen. We refer to this as our Camera position. The position is then updated from the hero’s position just before the TileMap is rendered like this:

    // the center of the screen is the preferred location of our hero
    
    double centerX = screenWidth / 2;
    
    double centerY = screenHeight / 2;
    
    cameraX = hero.getX() - centerX;
    
    cameraY = hero.getY() - centerY;
    

    We just need to make sure the camera doesn’t leave the tilemap:

    // if we get too close to the borders
    
    if (cameraX >= cameraMaxX) {
    
    cameraX = cameraMaxX;
    
    }
    
    if (cameraY >= cameraMaxY) {
    
    cameraY = cameraMaxY;
    
    }

    Rendering the TileMap using Canvas

    Then it’s really easy to render the tiles. we simply loop through the layers and ask the tilemap to render the correct image at the current position. First we need to find out which tiles are currently visible, and the offset, since our hero moves pixel by pixel, not tile by tile:

    // x,y index of first tile to be shown
    
    int startX = (int) (cameraX / tileWidth);
    
    int startY = (int) (cameraY / tileHeight);
    
    // the offset in pixels
    
    int offX = (int) (cameraX % tileWidth);
    
    int offY = (int) (cameraY % tileHeight);
    
    Then we loop through the visible layers and draw the tile:
    
    for (int y = 0; y < screenHeightInTiles; y++) {
    
    for (int x = 0; x < screenWidthInTiles; x++) {
    
    // get the tile id of the tile at this position
    
    int gid = layer.getGid((x + startX) + ((y + startY) * tileMap.getWidth()));
    
    graphicsContext2D.save();
    
    // position the graphicscontext for drawing
    
    graphicsContext2D.translate((x * tileWidth) - offX, (y * tileHeight) - offY);
    
    // ask the tilemap to draw the tile
    
    tileMap.drawTile(graphicsContext2D, gid);
    
    // restore the old state
    
    graphicsContext2D.restore();
    
    }
    
    }

    The TileMap will then find out which Tileset the Tile belongs to and ask the TileSet to draw it to the Context. Drawing itself is as simple as finding the correct coordinates in your TileSets Image:

    public void drawTile(GraphicsContext graphicsContext2D, int tileIndex) {
    
    int x = tileIndex % cols;
    
    int y = tileIndex / cols;
    
    // TODO support for margin and spacing
    
    graphicsContext2D.drawImage(tileImage, x * tilewidth, y* tileheight, tilewidth, tileheight, 0, 0, tilewidth, tileheight);
    
    }

    Game Loop. So we can simplify it to:

    The Game Loop is again very simple. I’m using a TimeLine and a KeyFrame to fire a pulse for the game at a certain framerate (FPS):

    final Duration oneFrameAmt = Duration.millis(1000 / FPS);
    
    final KeyFrame oneFrame = new KeyFrame(oneFrameAmt,
    
    new EventHandler() {
    
    @Override
    
    public void handle(Event t) {
    
    update();
    
    render();
    
    }
    
    });
    
    TimelineBuilder.create()
    
    .cycleCount(Animation.INDEFINITE)
    
    .keyFrames(oneFrame)
    
    .build()
    
    .play();
    

    Sprites

    Every call to update in the TileMapCanvas loops through all Sprites and updates them. Basic Sprites currently contain one TileSet with a walkcycle like this:

    Since sprites typically have a lot of transparent space around them, in order to have some extra room for animated behavior like like swinging a sword, I decided to allow to add a MoveBox and a CollisionBox for convenience. The CollisionBox can be used to define an area where our hero can be hurt. The MoveBox should be placed around the legs, so it can pass in front of forbidden tiles while the upper body is overlapping the tile. The blueish area around our “hero” is the sprite boundary:

    https://www.youtube.com/watch?v=08H6LZkcqXw

    Sprites can also have a timed Behavior. On every update the Sprite loops through it’s behaviors and checks if it’s time to fire. If so it’s “behave” method is called. If we have an enemy, like the skeleton in the sample app, we can add it’s AI here. Our Skeleton has for example a very simple behavior to make it follow our hero. It also checks for collision and causes damage to our hero like that:

    monsterSprite.addBehaviour(new Sprite.Behavior() {
    
    @Override
    
    public void behave(Sprite sprite, TileMapCanvas playingField) {
    
    if (sprite.getCollisionBox().intersects(hero.getCollisionBox())) {
    
    hero.hurt(1);
    
    }
    
    }
    
    });
    

    The default interval is a second. If you need other intervals you can set them. Behaviors are reusable, different sprites can share the same Behavior instance. Behaviors are similar to KeyFrames, and I’m currently also using them to time the Animations (increase the tile index for the next render call).

    ObjectGroupHandler

    As mentioned in the beginning ObjectGroups are handy extension points. In my example game I use them for defining the spawn points of our hero and the monsters. Currently you simply add an ObjectGroupHandler which in turn uses the information in the ObjectGroup to create the Hero and Monster sprites and add Behavior to them:

    class MonsterHandler implements ObjectGroupHandler {
    
    Sprite hero;
    
    @Override
    
    public void handle(ObjectGroup group, final TileMapCanvas field) {
    
    if (group.getName().equals("sprites")) {
    
    for (TObject tObject : group.getObjectLIst()) {
    
    if (tObject.getName().equals("MonsterSpawner")) {
    
    try {
    
    double x = tObject.getX();
    
    double y = tObject.getY();
    
    TileSet monster = TileMapReader.readSet("/de/eppleton/tileengine/resources/maps/BODY_skeleton.tsx");
    
    Sprite monsterSprite = new Sprite(monster, 9, x, y, "monster");
    
    monsterSprite.setMoveBox(new Rectangle2D(18, 42, 28, 20));
    
    field.addSprite(monsterSprite);
    
    monsterSprite.addBehaviour(new Sprite.Behavior() {
    
    @Override
    
    public void behave(Sprite sprite, TileMapCanvas playingField) {
    
    if (sprite.getCollisionBox().intersects(hero.getCollisionBox())) {
    
    hero.hurt(1);
    
    }
    
    }
    
    });
    
    }

    Putting it all together

    To create a sample game all you need to do is create TileMaps, TileSets, one or more ObjectGroupHandler(s) to create the Sprites and add Behavior, and you’re ready to play:

    // create the world
    
    TileMap tileMap = TileMapReader.readMap("/de/eppleton/tileengine/resources/maps/sample.tmx");
    
    // initialize the TileMapCanvas
    
    TileMapCanvas playingField = new TileMapCanvas(tileMap, 0, 0, 500, 500);
    
    // add Handlers, can also be done declaratively.
    
    playingField.addObjectGroupHandler(new MonsterHandler());
    
    // display the TileMapCanvas
    
    StackPane root = new StackPane();
    
    root.getChildren().add(playingField);
    
    Scene scene = new Scene(root, 500, 500);
    
    playingField.requestFocus();
    
    primaryStage.setTitle("Tile Engine Sample");
    
    primaryStage.setScene(scene);
    
    primaryStage.show();

    That was the starting point of my Tile Engine. In the meantime it has evolved a bit into a more general purpose 2D-engine, so also Sprites that are not using TileSets and Layers that are freely rendered are supported. But it works pretty well so far.

    Dieser Eintrag wurde in Allgemein,javafx geschrieben. Link merken.

    ——————————————————————————————————
    傲轩游戏网
  • 相关阅读:
    从jvm的角度来看java的多线程
    jvm常用优化方案和方法
    JVM GC 机制与性能优化
    JVM 类加载机制详解
    (转)Java 详解 JVM 工作原理和流程
    Callable,Runnable异同
    使用Toast进行用户提醒(转)
    学好Java只需要做到这7点,年薪20W起步
    C# 之泛型详解
    C#中的委托和事件
  • 原文地址:https://www.cnblogs.com/cuizhf/p/2861013.html
Copyright © 2011-2022 走看看