Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
566 lines (443 sloc) 20.8 KB

02 Create first prototype

Intention

In the last article 01 Setup the project I show how to setup the new project with another GitHub project from me Project-Template-afterburnerfx-Naoghuman and what are the advantages from using this template.

In this article I will describes the steps and decisions which I make during the implementation from the first prototype. Click on the picture to see the SokubanFX v0.1.0-PROTOTYPE in action 😄 in YouTube.

sokubanfx_v0.1.0-PROTOTYPE.png

You can download the prototype here: SokubanFX-0.1.0-PROTOTYPE_2016-04-30_08-22.zip

Content

Create first prototype

Like I said in the section Intention here are the steps and decisions which I make during the implementation from the first prototype from my new JavaFX, Maven game SokubanFX.

So here is the importants decision from me -> no game-engine 😳 .

Normally a game have a game-loop which runs with xy fps. The engine will then loops over the update() and render() methods. JavaFX comes with own classes around this topic like Animation -> the parent class from Timeline and Transition.

If the functionality from this classes and all sub-classes not enough, then I have the possibility to use the class AnimationTimer which triggers the method handle(long) in every frame (which leeds then to a game-engine 😁 ).

Basic functionalities from the project

Because I used my project template Project-Template-afterburnerfx-Naoghuman to create the SokubanFX project all basic functionalities which I want was almost implemented 😀 😎 .

Logging, ActionEvent- and Properties-Handling, support for the JavaFX development, easy generation from an executable jar file aso.

For more detailed informations plz see the article 01 Setup the project.

Functionalities like collision, evaluation, movement...

In this section I will speak about collision, evaluation and movement. Beside loading and converting a map (which I described later in this article) this are the main funcitionalities in the game.

Following workflow is given:
a) User want to move the player, for example he pressed the key DOWN.
b) Now all possible collisions must be checked, before something can moved or not.
c) With the CheckMovementResult then the evaluation from the player (and optional a box) will be done or not.
d) Now the map with eventually new coordinates can be displayed.
e) Finally after a successful move the game should be checked if the map is finished. If so the next map can be shown.

a) User pressed the key DOWN

public class GamePresenter implements Initializable, IActionConfiguration, IRegisterActions {

    public void onActionButtonDown() {
        LoggerFacade.INSTANCE.debug(this.getClass(), "On action Button down"); // NOI18N
        
        final CheckMovementResult checkMovementResult = MapFacade.INSTANCE.playerMoveTo(EDirection.DOWN, actualMapModel);
        this.evaluatePlayerMoveTo(checkMovementResult);
        
        final boolean shouldCheckIfMapIsFinished = checkMovementResult.isCheckIsMapFinish();
        if (shouldCheckIfMapIsFinished) {
            final boolean isMapFinish = MapFacade.INSTANCE.isMapFinish(actualMapModel);
            this.evaluateIsMapFinish(isMapFinish);
        }
    }
    ...
}

b) Check possible collisions -> returns what happen.

public class MapMovement {
    public CheckMovementResult checkMovePlayerTo(EDirection direction, MapModel mapModel) {
        LoggerFacade.INSTANCE.debug(this.getClass(), "Check move player to direction: " + direction.toString()); // NOI18N

        // Player -> Wall
        final CollisionResult collisionResultCheckCollisionPlayerWall = CollisionChecker.getDefault().checkCollisionPlayerWall(direction, mapModel);
        final CheckMovementResult checkMovementResult = CheckMovementResult.getDefault();
        if (collisionResultCheckCollisionPlayerWall.equals(CollisionResult.PLAYER_AGAINST__WALL)) {
            checkMovementResult.setAnimation(EAnimation.WHAT_HAPPEN);
            checkMovementResult.setMovement(EMovement.NONE);
            
            return checkMovementResult;
        }
	
	// and do all other collision checks here also.
    }
    ...
}
public enum CollisionResult {
    
    NONE,                      // ...
    PLAYER_AGAINST__BOX,       // player -> box
    PLAYER_AGAINST__BOX_BOX,   // player -> box -> box
    PLAYER_AGAINST__BOX_NONE,  // player -> box -> none
    PLAYER_AGAINST__BOX_PLACE, // player -> box -> place
    PLAYER_AGAINST__BOX_PLACE_AND_FINISH, // player -> box -> place -> finish
    PLAYER_AGAINST__BOX_WALL,  // player -> box -> wall
    PLAYER_AGAINST__WALL;      // player -> wall
    
}

c) Evalutate the CheckMovementResult -> new Coordinates for the player and optional a box.

public class GamePresenter implements Initializable, IActionConfiguration, IRegisterActions {
    private void evaluatePlayerMoveTo(CheckMovementResult checkMovementResult) {
        // Animation will here done later
        
        final EMovement movement = checkMovementResult.getMovement();
        if (
                movement.equals(EMovement.PLAYER)
                || movement.equals(EMovement.PLAYER_AND_BOX)
        ) {
            // Update player
            final Coordinates player = actualMapModel.getPlayer();
            player.setX(player.getTranslateX());
            player.setY(player.getTranslateY());
            
            if (movement.equals(EMovement.PLAYER)) {
                this.displayMap();
                return;
            }
            
            // Update box
            final Coordinates boxToMove = movement.getCoordinatesBoxToMove();
            final List<Coordinates> boxes = actualMapModel.getBoxes();
            for (Coordinates box : boxes) {
                if (
                        box.getX() == boxToMove.getX()
                        && box.getY() == boxToMove.getY()
                ) {
                    box.setX(boxToMove.getTranslateX());
                    box.setY(boxToMove.getTranslateY());
                }
            }
            
            this.displayMap();
        }
    }
    ...
}

d) Display the map -> shows the actual updated map.

public class GamePresenter implements Initializable, IActionConfiguration, IRegisterActions {
    private void displayMap() {
        LoggerFacade.INSTANCE.debug(this.getClass(), "Display Map"); // NOI18N
        
        lMapInfo.setText("Map " + actualMapModel.getLevel()); // NOI18N
        
        final List<String> mapAsStrings = MapFacade.INSTANCE.convertMapCoordinatesToStrings(actualMapModel);
        taMapDisplay.setText(null);
        mapAsStrings.stream().forEach((line) -> {
            taMapDisplay.appendText(line);
            taMapDisplay.appendText("\n"); // NOI18N
        });
    }
    ...
}

e) Finally evaluate if the map is finished. If yes show the new map.

public class GamePresenter implements Initializable, IActionConfiguration, IRegisterActions {
    private void evaluateIsMapFinish(boolean isMapFinish) {
        LoggerFacade.INSTANCE.debug(this.getClass(), "Evaluate is Map finish"); // NOI18N
        
        // Keep going :)
        if (!isMapFinish) {
            return;
        }
        
        // Map is finish !!
        final int actualMap = PreferencesFacade.INSTANCE.getInt(
                IMapConfiguration.PROP__ACTUAL_MAP,
                IMapConfiguration.PROP__ACTUAL_MAP__DEFAULT_VALUE);
        PreferencesFacade.INSTANCE.putInt(
                IMapConfiguration.PROP__ACTUAL_MAP,
                actualMap + 1);
        
        // load next map
        this.loadActualMap();
        this.displayMap();
    }
    ...
}
Persistence from the maps

In my old Java Swing2D game Sokuban-Clone the maps are persist as txt-files.

sokuban-clone-map-txt.png

This maps was readed with:

public final boolean loadTileMap(int level)
{
    ArrayList<String> lines = new ArrayList();
    
    int w = 0;
    int h = 0;
    
    String line = null;
    BufferedReader reader = null;
    try
    {
      reader = new BufferedReader(new InputStreamReader(
        ClassLoader.getSystemResourceAsStream(
        "maps/map" + level + ".txt")));
      while (Boolean.TRUE.booleanValue())
	  {
        line = reader.readLine();
        if (line == null)
		{
          reader.close(); break;
        }
        if (!line.startsWith("#"))
		{
          lines.add(line);
          w = Math.max(w, line.length());
        }
      }
    }
    catch (IOException e)
    {
      e.printStackTrace();
    }
	
    ...
}

In the method are a lot of more code for converting the map but that

In SokubanFX I decide to be a little lazy and use for all maps one properties-file.

sokuban-map-properties.png

So its really easy to read a map and convert it to a List<String>:

class MapLoader implements IMapConfiguration {
    
    MapLoader() {
        this.init();
    }
    
    // Register the maps.properties file as a ResourceBundle.
    private void init() {
        LoggerFacade.INSTANCE.debug(this.getClass(), "Init MapLoader"); // NOI18N
        
        PropertiesFacade.INSTANCE.register(KEY__MAP__RESOURCE_BUNDLE);
    }
    
    // Returns the mapped value from the key.
    private String getProperty(String propertyKey) {
        return PropertiesFacade.INSTANCE.getProperty(KEY__MAP__RESOURCE_BUNDLE, propertyKey);
    }
	
    /**
     * Loads the map from the ResourceBundle and returns the loaded map as a List<String>.
     * 
     * @param level Which map should be loaded?
     * return The loaded map as a list from strings. 
     */
    public List<String> loadMapAsStrings(int level) {
        LoggerFacade.INSTANCE.debug(this.getClass(), "Load map as Strings: " + level); // NOI18N
        
        final List<String> mapAsStrings = FXCollections.observableArrayList();
        final String mapAsString = this.getProperty(KEY__MAP__POINT + level);
        final String[] splits = mapAsString.split(";"); // NOI18N
        mapAsStrings.addAll(Arrays.asList(splits));
        
        return mapAsStrings;
    }

}
Textbased maps in views

Because we are in a prototype I decided for me to show the game without graphics, means text based:

Preview view preview-view.png

Game view game-view.png

Loading a map from the .properties file as List<String>:

class MapLoader implements IMapConfiguration {
    public List<String> loadMapAsStrings(int level) {
        LoggerFacade.INSTANCE.debug(this.getClass(), "Load map as Strings: " + level); // NOI18N
        
        final List<String> mapAsStrings = FXCollections.observableArrayList();
        final String mapAsString = this.getProperty(KEY__MAP__POINT + level);
        final String[] splits = mapAsString.split(";"); // NOI18N
        mapAsStrings.addAll(Arrays.asList(splits));
        
        return mapAsStrings;
    }
    ...
}

Converting a List<String> to a MapModel:

public class MapConverter {
    public MapModel convertStringsToMap(final int level, final List<String> mapAsStrings) {
        LoggerFacade.INSTANCE.debug(this.getClass(), "Convert strings to MapModel"); // NOI18N
        
        final MapModel mapModel = new MapModel();
        mapModel.setLevel(level);
        mapModel.setMapAsStrings(mapAsStrings);
        
        int columns = 0;
        int rows = 0;
        for (String line : mapAsStrings) {
            for (int x = 0; x < line.length(); x++) {
                columns = Math.max(columns, x + 1);
                
                final Character c = line.charAt(x);
                final int x1 = x + 1;
                final int y1 = rows + 1;
                switch(c) {
                    case 'A': // NOI18N
                    case 'B': { mapModel.addWall(x1, y1);   break; } // NOI18N
                    
                    case '0': { mapModel.setPlayer(x1, y1); break; } // NOI18N
                    case '1': { mapModel.addBox(x1, y1);    break; } // NOI18N
                    case '2': { mapModel.addPlace(x1, y1);  break; } // NOI18N
                    
                    case '-': // NOI18N
                    default: { }
                }
            }
            
            rows = rows + 1;
        }
        
        mapModel.setColumns(columns);
        mapModel.setRows(rows);
        
        return mapModel;
    }
    ...
}

Converting a MapModel to a List<String>:

public class MapConverter {
    public List<String> convertMapCoordinatesToStrings(MapModel mapModel) {
        final List<String> mapAsStrings = FXCollections.observableArrayList();
        final int columns = mapModel.getColumns();
        final int rows = mapModel.getRows();
        final int level = mapModel.getLevel();
        
        final Coordinates player = mapModel.getPlayer();
        final List<Coordinates> boxes = mapModel.getBoxes();
        final List<Coordinates> places = mapModel.getPlaces();
        final List<Coordinates> walls = mapModel.getWalls();

        // - = Empty sign
        for (int ro = 0; ro < rows; ro++) {
            final StringBuilder sb = new StringBuilder();
            for (int col = 0; col < columns; col++) {
                sb.append("-"); // NOI18N
            }
            mapAsStrings.add(sb.toString());
        }
        
        // A, B = Walls from the level
        for (int ro = 1; ro <= rows; ro++) {
            for (int col = 1; col <= columns; col++) {
                for (Coordinates wall : walls) {
                    if (wall.getX() == col && wall.getY() == ro) {
                        final StringBuilder sb = new StringBuilder();
                        sb.append(mapAsStrings.get(ro - 1));
                        sb.replace(col - 1, col, level % 2 != 0 ? "A" : "B"); // NOI18N
                        mapAsStrings.remove(ro - 1);
                        mapAsStrings.add(ro - 1, sb.toString());
                    }
                }
            }
        }

        ...
    }
    ...
}

Conclusion

With the decisions to read the maps from a .properties file and show the maps as text-based I was be able to focused my energie on the main functionalities in the game like collisions, evaluations and movement.
So I have my prototype in a few hours implemented 👍 .

Download:
SokubanFX-0.1.0-PROTOTYPE_2016-04-30_08-22.zip

YouTube: sokubanfx_v0.1.0-PROTOTYPE.png

About the autor

Let me introduce myself. My name is Peter Rogge and I'm a software developer in Wolfsburg, Germany.

Since 2008 I work by H+D International Group which is an IT- and engineering service provider represented nationally and internationally in over 20 locations with the head-quarters in Wolfsburg, Germany.

In my free time I investigate between 2009 an 2012 some time in NetBeans RCP (Rich Client Platform) development.
See


Since `2011` I change my focus to [JavaFX] ([JavaFX 2.0] - [JavaFX 8]). Although in `2015` I saw a video from [Adam Bien] where he mention he would love to write a [NetBeans RCP] plugin for his library [afterburner.fx] when he had more time. So I decided to do this: * See the [GitHub] project [NetBeansIDE-AfterburnerFX-Plugin] (since 09.2015) which is really helpful to speed up the development from [JavaFX] applications in combination with the library [afterburner.fx]. * The `interview` [Afterburner.fx NetBeans Plugin Release] (11.2015) with [Adam Bien] and me. * Have a look in the `video` [DI, IoC and MVP With Java FX -- afterburner.fx Deep Dive] where [Adam Bien] introduce my plugin (at 48:00).
Contact

Any question? Some helpful criticism?

Articles in this series

Articles