-
Notifications
You must be signed in to change notification settings - Fork 1
#003 Maps & Map Tiles
Now that you've seen how to create a Screen and work with the TileGrid/Tile objects at a basic level, we can begin to create the building blocks of a Roguelike game. The first step in creating this game is to define what a map is.
For tutorial sections, such as this, which do not cover the usage of VTerminal, there will be no Code Explanation section(s). It's assumed that you understand Java and can read the code used.
In our game we want to display one, or more, maps. Each map is a grid of tiles that represents either part of the environment or an entity. The map can be of an arbitrary width and height, but we don't want to allow the map to expand or contract in size as that would be a little more complex than this tutorial needs to be.
Before creating the Map class, we must first create the MapTile class. We call it a "MapTile" because VTerminal already uses a class called "Tile" and we don't want to confuse the two.
The definition of a MapTile is as follows:
- Sprite
- The visual representation of the tile.
- E.g. A character with a fore/background color.
- Movement Cost
- The cost for an entity to move across the tile.
- E.g. A dirt tile is easy to walk across, so the cost may be 1.
- E.g. A mud tile is difficult to walk across, so the cost may be 2.
- Solid
- Whether the tile is solid.
- E.g. If the tile is a solid rock or a cliff-face, then an entity cannot walk across it.
- Visited
- Whether the player has visited the tile before.
- Visible
- Whether the tile is currently within the player's line of sight.
As you may have noticed, we need to create a Sprite class in order to define what a MapTile looks like. To keep this as simple as possible, we're going to hardcode the Sprite class as an enum. This allows us to reference the sprites from anywhere in the program at any time and saves us from dealing with slightly more complicated code which could load the sprites from a JSON file.
The definition of a Sprite is as follows:
- Character
- A unicode character.
- Background Color
- The color displayed behind the character.
- Used to display the Sprite when it's in the player's line of sight.
- Foreground Color
- The color of the character.
- Used to display the Sprite when it's in the player's line of sight.
- Dark Background Color
- A shaded version of the Background Color.
- Used to display a darkened version of the Sprite, when the sprite isn't in the player's line of sight.
- Dark Foreground Color
- A shaded version of the Foreground Color.
- Used to display a darkened version of the Sprite, when the sprite isn't in the player's line of sight.
Now that we have our definitions of the MapTile and Sprite classes, we can now define the Map class as follows:
- 2D Array of MapTiles
- A function to update the Layer that displays the Map.
- When we alter the Map's array of MapTiles, these changes are not automaticlly shown on the Map's Layer. We have to copy the changes from each MapTile to each VTerminal Tile on the Layer.
- Functions to get the width/height of the Map.
- Functions to get the width/height of the Layer (We'll call this the View).
package com.valkryst.VTerminal_Tutorial;
import com.valkryst.VTerminal.misc.ColorFunctions;
import lombok.Getter;
import java.awt.Color;
public enum Sprite {
UNKNOWN('?', Color.BLACK, Color.RED),
DARKNESS('█', Color.BLACK, Color.BLACK),
DIRT('▒', new Color(0x452F09), new Color(0x372507)),
GRASS('▒', new Color(0x3D4509), new Color(0x303707)),
WALL('#', new Color(0x494949), new Color(0x3C3C3C)),
PLAYER('@', new Color(0, 0, 0 ,0), Color.GREEN),
ENEMY('E', new Color(0, 0, 0, 0), Color.RED);
/** The character. */
@Getter private final char character;
/** The background color. */
@Getter private final Color backgroundColor;
/** The foreground color. */
@Getter private final Color foregroundColor;
/** The dark background color. */
@Getter private final Color darkBackgroundColor;
/** The dark foreground color. */
@Getter private final Color darkForegroundColor;
/**
* Constructs a new Sprite.
*
* @param character
* The character.
*
* @param backgroundColor
* The background color.
*
* @param foregroundColor
* The foreground color.
*/
Sprite(final char character, final Color backgroundColor, final Color foregroundColor) {
this.character = character;
if (backgroundColor == null) {
this.backgroundColor = Color.MAGENTA;
} else {
this.backgroundColor = backgroundColor;
}
if (foregroundColor == null) {
this.foregroundColor = Color.MAGENTA;
} else {
this.foregroundColor = foregroundColor;
}
darkBackgroundColor = ColorFunctions.shade(this.backgroundColor, 0.5);
darkForegroundColor = ColorFunctions.shade(this.foregroundColor, 0.5);
}
}
The ColorFunctions.shade() function takes a color and shades it by some percentage. In this case, we're shading the input color by 0.5 (50%).
- If we ran ColorFunctions.shade(Color.WHITE, 0.5), then the output color would be grey (127r, 127g, 127b).
- If we ran ColorFunctions.shade(Color.WHITE, 1.0), then the output color would be black (0r, 0g, 0b).
Also note that the Player and Enemy sprites are declared using new Color(red, green, blue, alpha) and that their background color is entirely transparent. This isn't important at the moment, but it will allow us to display the Player and Enemy sprites on top of other sprites later on.
package com.valkryst.VTerminal_Test;
import lombok.Data;
@Data
public class MapTile {
/** The sprite. */
private Sprite sprite = Sprite.WALL;
/** The cost for an entity to move across the tile. */
private int movementCost = 1;
/** Whether or not the tile is solid. */
private boolean solid = true;
/** Whether or not the tile has been seen before. */
private boolean visited = false;
/** Whether or not the tile is visible. */
private boolean visible = false;
/** Constructs a new MapTile. */
public MapTile() {}
/**
* Constructs a new MapTile.
*
* @param sprite
* The sprite.
*/
public MapTile(final Sprite sprite) {
if (sprite == null) {
this.sprite = Sprite.UNKNOWN;
}
this.sprite = sprite;
}
}
package com.valkryst.VTerminal_Tutorial;
import com.valkryst.VTerminal.Tile;
import com.valkryst.VTerminal.component.Layer;
import lombok.Getter;
import java.awt.*;
public class Map extends Layer {
/** The mapTiles. */
@Getter private MapTile[][] mapTiles;
/** Constructs a new Map. */
public Map() {
super(new Dimension(80, 40));
final int viewWidth = getViewWidth();
final int viewHeight = getViewHeight();
// Set the Layer to display all tiles as empty and black.
for (int y = 0 ; y < viewHeight ; y++) {
for (int x = 0 ; x < viewWidth ; x++) {
final Tile tile = super.tiles.getTileAt(x, y);
tile.setCharacter(' ');
tile.setBackgroundColor(Color.BLACK);
}
}
// Initialize the MapTiles array.
mapTiles = new MapTile[viewHeight][viewWidth];
for (int y = 0 ; y < viewHeight ; y++) {
for (int x = 0 ; x < viewWidth ; x++) {
mapTiles[y][x] = new MapTile();
}
}
this.updateLayerTiles();
}
/** Updates the Map's Layer, so that any changes made to the Map's tiles are displayed on the Layer. */
public void updateLayerTiles() {
for (int y = 0 ; y < getViewHeight() ; y++) {
for (int x = 0 ; x < getViewWidth() ; x++) {
final MapTile mapTile = mapTiles[y][x];
final Sprite mapTileSprite = mapTile.getSprite();
final Tile layerTile = super.tiles.getTileAt(x, y);
layerTile.setCharacter(mapTileSprite.getCharacter());
if (mapTile.isVisible()) {
layerTile.setBackgroundColor(mapTileSprite.getBackgroundColor());
layerTile.setForegroundColor(mapTileSprite.getForegroundColor());
} else {
layerTile.setBackgroundColor(mapTileSprite.getDarkBackgroundColor());
layerTile.setForegroundColor(mapTileSprite.getDarkForegroundColor());
}
}
}
}
/**
* Retrieves the width of the map.
*
* @return
* The width of the map.
*/
public int getMapWidth() {
return mapTiles.length;
}
/**
* Retrieves the height of the map.
*
* @return
* The height of the map.
*/
public int getMapHeight() {
return mapTiles[0].length;
}
/**
* Retrieves the width of the view.
*
* @return
* The width of the view.
*/
public int getViewWidth() {
return super.tiles.getWidth();
}
/**
* Retrieves the height of the view.
*
* @return
* The height of the view.
*/
public int getViewHeight() {
return super.tiles.getHeight();
}
}
You may have noticed that the Map class extends VTerminal's Layer class. The Layer class is a wrapper, around the TileGrid class, which allows us to add the Map to our Screen without any extra hassle.
For now, the Layer and the Map will be of equal size. This means that the entirety of the Map can be displayed on the Screen at once, there are no off-screen MapTiles and we do not need to complicate the code by allowing the player's view of the Map to move with the player.
To make things simple, let's say that our Maps will always be a maximum of 80x40 characters.
Now that we have our Map, MapTile, and Sprite classes created, we can rewrite our Driver class to create a Map and display it on the Screen. Because the Map class extends the Layer class, we can add the Map to the Screen using the addComponent() function as the Layer class is one of the VTerminal components.
package com.valkryst.VTerminal_Tutorial;
import com.valkryst.VTerminal.Screen;
import java.io.IOException;
public class Driver {
public static void main(String[] args) throws IOException {
final Screen screen = new Screen(81, 41);
screen.addCanvasToFrame();
final Map map = new Map();
screen.addComponent(map);
screen.draw();
}
}
Notice how there's a grey border on the bottom and right-hand sides of the Screen and how we now have our Map displaying as a large 80x40 grey rectangle where every tile displays the '#' character. The grey borders are to show you that the Map is actually a component being displayed ontop of the Screen. To remove the borders, simply change new Screen(81, 41) to new Screen(80, 40).
package com.valkryst.VTerminal_Tutorial;
import com.valkryst.VTerminal.Screen;
import java.io.IOException;
public class Driver {
public static void main(String[] args) throws IOException {
final Screen screen = new Screen(80, 40);
screen.addCanvasToFrame();
final Map map = new Map();
screen.addComponent(map);
screen.draw();
}
}
Just like you've seen with the Screen class, you can access the VTerminal Tile objects of the Layer that the Map extends. This allows you to, for example, grab the tile at location (10x, 10y) and set it to have a red background.
package com.valkryst.VTerminal_Tutorial;
import com.valkryst.VTerminal.Screen;
import java.awt.*;
import java.io.IOException;
public class Driver {
public static void main(String[] args) throws IOException {
final Screen screen = new Screen(80, 40);
screen.addCanvasToFrame();
final Map map = new Map();
screen.addComponent(map);
map.getTileAt(10, 10).setBackgroundColor(Color.RED);
screen.draw();
}
}