I have created a 2048 clone, I wanted to take on a simple project since I have never actually finished anything. Now I have, though I'm not a hundred percent happy with the result.
Images so you know what we're discussing:
My concerns
- Handling assets
This is the first project in which I use the AssetManager class. I have handled assets by giving the assetManager to each object in its constructor, is this how I'm supposed to do this? Also, I use a few different font sizes, so I had to do this:
FileHandleResolver resolver = new InternalFileHandleResolver();
assetManager.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(resolver));
assetManager.setLoader(BitmapFont.class, ".ttf", new FreetypeFontLoader(resolver));
FreetypeFontLoader.FreeTypeFontLoaderParameter font = new FreetypeFontLoader.FreeTypeFontLoaderParameter();
font.fontFileName = "ClearSans-Bold.ttf";
font.fontParameters.size = HEIGHT / 10;
assetManager.load("mediumFont.ttf", BitmapFont.class, font);
FreetypeFontLoader.FreeTypeFontLoaderParameter font2 = new FreetypeFontLoader.FreeTypeFontLoaderParameter();
font2.fontFileName = "ClearSans-Bold.ttf";
font2.fontParameters.size = HEIGHT / 6;
assetManager.load("bigFont.ttf", BitmapFont.class, font2);
FreetypeFontLoader.FreeTypeFontLoaderParameter font3 = new FreetypeFontLoader.FreeTypeFontLoaderParameter();
font3.fontFileName = "ClearSans-Regular.ttf";
font3.fontParameters.size = HEIGHT / 30;
assetManager.load("smallFont.ttf", BitmapFont.class, font3);
etc.
Is there a better way to handle many different font sizes? I already do this for the size of the numbers on the tiles:
void setFontSize() {
BitmapFont tileFont = assetManager.get("tileFont.ttf", BitmapFont.class);
tileFont.getData().setScale(2.0f / BOARD_SIZE);
BitmapFont smallTileFont = assetManager.get("smallTileFont.ttf", BitmapFont.class);
smallTileFont.getData().setScale(2.0f / BOARD_SIZE);
}
- Handling constants
I have a few constants like the color of certain text elements, should I create a new class and create color objects in them?
- OOP question
I have created new classes for a loss screen and win screen. They of course both only need to be instanced once, and they always look the same, should I not have used OOP for this? Here is the loss class and how I show it:
LossScreen.java
class LossScreen extends Group {
private BitmapFont mediumFont, smallFont;
private GlyphLayout layout = new GlyphLayout();
private Texture backgroundTexture, buttonTexture;
private Rectangle tryAgainRect;
private int zIndex = 0;
LossScreen (int x, int y, AssetManager assetManager) {
setBounds(x, y, TotallyNot2048.WIDTH, TotallyNot2048.HEIGHT);
smallFont = assetManager.get("smallBoltFont.ttf", BitmapFont.class);
mediumFont = assetManager.get("mediumFont.ttf", BitmapFont.class);
backgroundTexture = Helper.createRoundedRectangleTexture(getWidth(), getHeight(), new Color(200f / 256f, 200f / 256f, 200f / 256f, .8f), 15);
layout.setText(smallFont, "Try again");
buttonTexture = Helper.createRoundedRectangleTexture(layout.width * 1.5f, layout.height * 3f, new Color(143f / 256f, 122f / 256f, 102f / 256f, 1f), 3);
tryAgainRect = new Rectangle(getX() + getWidth() / 2 - buttonTexture.getWidth() / 2, getY() + getHeight() / 3, layout.width * 1.5f, layout.height * 3f);
listenForInput();
}
private void listenForInput() {
addListener(new InputListener() {
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
if (pointer == Input.Buttons.LEFT)
if (tryAgainRect.contains(TotallyNot2048.getMousePosInGame().x, TotallyNot2048.getMousePosInGame().y))
((Board) getParent()).restartGame();
return true;
}
});
}
public void setZIndex(int zIndex) {
this.zIndex = zIndex;
}
public int getZIndex() {
return zIndex;
}
@Override
public void draw(Batch batch, float parentAlpha) {
super.draw(batch, parentAlpha);
batch.setColor(getColor().r, getColor().g, getColor().b, getColor().a * parentAlpha);
batch.draw(backgroundTexture, 0, 0);
layout.setText(mediumFont, "Game over!");
mediumFont.setColor(119f / 256f, 110f / 256f, 101f/ 256f, mediumFont.getColor().a);
mediumFont.getColor().a = getColor().a;
mediumFont.draw(batch, "Game over!", getWidth() / 2 - layout.width / 2, getHeight() / 2 + layout.height / 2);
mediumFont.getColor().a = 1f;
batch.draw(buttonTexture, getWidth() / 2 - buttonTexture.getWidth() / 2, getHeight() / 3);
layout.setText(smallFont, "Try again");
smallFont.setColor(new Color(245f / 256f, 241f / 256f, 237f / 256f, 1f * getColor().a));
smallFont.draw(batch, "Try again", getWidth() / 2 - layout.width / 2, getHeight() / 3 + layout.height * 2);
batch.setColor(getColor().r, getColor().g, getColor().b, 1f);
}
@Override
public void act(float delta) {
super.act(delta);
}
}
In "board.java" I check whether other moves are available, if not, this code is called and an "isLost" variable is set to true which prevents user input, "lossScreen" is an actor that upon setting to visible automatically calls the draw method in the LossScreen class:
private void showLossScreen() {
lossScreen.setVisible(true);
lossScreen.addAction(Actions.fadeIn(.4f));
}
- Game logic
This is the game logic I use for moving the tiles, "isExists()" checks if the tile is not already about to be deleted (it is still present because it needs to show an animation), "setMoveParameters()" gives data to a "Tile" that will be used by the tile to show an animation. "tiles[]" is an array of Tile objects (Tile extends Actor). Besides genericMove and genericUpgrade, I have four methods for each move direction:
private boolean genericMove(int i, int j, int move_x, int move_y) {
if ((tiles[i + j] != null && !tiles[i + j].isExists()) || tiles[i + j] == null) {
tiles[i + j] = new Tile(tiles[i], this);
addActor(tiles[i + j]);
tiles[i + j].setMoveParameters(move_x, move_y, tiles[i].getValue(), false);
removeActor(tiles[i]);
tiles[i].dispose();
tiles[i] = null;
return true;
}
return false;
}
private boolean genericUpgradeMove(int i, int j, int move_x, int move_y) {
if (tiles[i + j] != null && tiles[i + j].isExists() && tiles[i] != null) {
if (tiles[i + j].getValue() == tiles[i].getValue() && !tiles[i + j].isUpgradedThisTurn() && !tiles[i].isUpgradedThisTurn()) {
updateScore(tiles[i + j].getValue() * 2);
tiles[i + j].setMoveParameters(0, 0, tiles[i + j].getValue() * 2, false);
Tile tile = new Tile(tiles[i], this);
tempTiles.add(tile);
tile.setMoveParameters(move_x, move_y, tiles[i].getValue(), true);
addActor(tile);
removeActor(tiles[i]);
tiles[i].dispose();
tiles[i] = null;
return true;
}
}
return false;
}
private boolean moveRight() {
boolean tileMoved = false;
for (int i = tiles.length - 2; i >= 0; i--) {
if (tiles[i] != null && tiles[i].isExists()) {
if ((i + 1) % BOARD_SIZE != 0) {
tileMoved = genericMove(i, 1, MOVE_DISTANCE, 0);
if (tileMoved) {
moveRight();
break;
}
tileMoved = genericUpgradeMove(i, 1, MOVE_DISTANCE, 0);
if (tileMoved) {
moveRight();
break;
}
}
}
}
return tileMoved;
}
These methods are preceded by one of four methods for each direction like:
private boolean tryMoveRight() {
pause();
updateIndices();
if (moveRight()) {
updateTileGraphics();
addNewTile();
checkState();
return true;
} else {
resume();
return false;
}
}
"pause()" causes no new user input to be taken in, resume resumes user input, updateIndices() makes sure all tiles are in their right position, so if their did happen to be new move instructions given by the user, the tile is first put into the position it should have been to begin with. "updateTileGraphics()" uses the data given by "setMoveParameters()" for eachs Tile. "checkState()" checks if the game is lost or won. "moveUp()" returns true or false depending on whether one or more tiles were able to move / slide into each other.
- Making faster input possible
Currently, when the tiles are still showing animations, no new input is accepted, this results in not being able to play the game really fast, I haven't figured out a way to fix this without making the animations really fast.
These are my main questions, of course their are much more things I'm not certain of whether I used best practices, but I don't know if anyone is interested in reviewing the entire project.