Introduction to the TileMap Class
TCJSGame’s TileMap Class provides a powerful grid-based level design system for creating complex game worlds. It allows you to build levels using tile-based layouts with collision detection, layer management, and dynamic tile manipulation.
🏗️ Basic TileMap Setup
Creating Tile Definitions
const tiles = [
new Component(0, 0, "green", 0, 0, "rect"), // Index 1 - Grass
new Component(0, 0, "gray", 0, 0, "rect"), // Index 2 - Stone wall
new Component(0, 0, "blue", 0, 0, "rect"), // Index 3 - Water
new Component(0, 0, "brown", 0, 0, "rect"), // Index 4 - Dirt
new Component(0, 0, "yellow", 0, 0, "rect") // Index 5 - Sand
];
// Image-based tiles
const imageTiles = [
new Component(0, 0, "tiles/grass.png", 0, 0, "image"),
new Component(0, 0, "tiles/stone.png", 0, 0, "image"),
new Component(0, 0, "tiles/water.png", 0, 0, "image")
];
Creating Map Layouts
// 2D array representing your level (rows x columns)
const mapLayout = [
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 1, 1, 1, 1, 1, 1, 1, 1, 2],
[2, 1, 1, 3, 3, 3, 1, 1, 1, 2],
[2, 1, 1, 3, 3, 3, 1, 1, 1, 2],
[2, 1, 1, 1, 1, 1, 1, 1, 1, 2],
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
];
// Larger, more complex map
const largeMap = [
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2],
[2, 1, 4, 4, 1, 1, 5, 5, 5, 1, 1, 4, 1, 2],
[2, 1, 4, 1, 1, 5, 5, 5, 5, 5, 1, 1, 1, 2],
[2, 1, 1, 1, 5, 5, 3, 3, 3, 5, 5, 1, 1, 2],
[2, 1, 1, 5, 5, 3, 3, 3, 3, 3, 5, 1, 1, 2],
[2, 1, 1, 5, 3, 3, 3, 3, 3, 3, 5, 1, 1, 2],
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
];
Initializing the TileMap
// Setup display and assign tilemap data
const display = new Display();
display.start(800, 600);
// Assign tiles and map to display
display.tile = tiles;
display.map = mapLayout;
// Create and show the tilemap
display.tileMap(); // Creates tileFace instance
display.tileFace.show(); // Renders the tilemap
// Access the tilemap directly
const tilemap = display.tileFace;
🎯 TileMap Properties and Methods
Core Properties
class TileMap {
constructor(render, map, tile, width, height, scene = 0) {
this.map = map; // 2D map array
this.tile = tile; // Tile definitions
this.tile.unshift(0); // Add empty slot at index 0
this.tileHeight = height / map.length;
this.tileWidth = width / map[0].length;
this.tileList = []; // Generated tile objects
this.scene = scene;
}
}
Basic TileMap Operations
// Get all tiles of a specific type
const grassTiles = tilemap.tiles(1); // All grass tiles (id=1)
const wallTiles = tilemap.tiles(2); // All wall tiles (id=2)
// Get a specific tile by grid coordinates
const tileAtPosition = tilemap.rTile(3, 2); // Tile at column 3, row 2
// Check if tile exists at position
if (tileAtPosition) {
console.log("Tile found:", tileAtPosition.tid, "at", tileAtPosition.tx, tileAtPosition.ty);
}
// Get all tiles (returns entire tileList)
const allTiles = tilemap.tiles();
🚀 Collision Detection Systems
Basic Collision Checking
// Check collision with any tile
if (tilemap.crashWith(player)) {
// Player collided with a solid tile
player.hitBottom();
}
// Check collision with specific tile types
if (tilemap.crashWith(player, 2)) { // Collision with walls (id=2)
player.speedX = 0;
player.speedY = 0;
}
if (tilemap.crashWith(player, 3)) { // Collision with water (id=3)
player.speedX *= 0.5; // Slow movement in water
player.speedY *= 0.5;
}
Advanced Collision System
class AdvancedCollision {
constructor(tilemap) {
this.tilemap = tilemap;
}
checkAllCollisions(player) {
// Check different tile types for different effects
if (this.tilemap.crashWith(player, 2)) { // Walls
this.handleWallCollision(player);
}
if (this.tilemap.crashWith(player, 3)) { // Water
this.handleWaterCollision(player);
}
if (this.tilemap.crashWith(player, 5)) { // Lava
this.handleLavaCollision(player);
}
// Check spikes (id=6) only from top
if (this.checkSpikeCollision(player)) {
this.handleSpikeCollision(player);
}
}
handleWallCollision(player) {
// Stop movement when hitting walls
player.speedX = 0;
player.speedY = 0;
}
handleWaterCollision(player) {
// Apply water physics
player.speedX *= 0.7;
player.speedY *= 0.7;
}
handleLavaCollision(player) {
// Damage player in lava
player.health -= 1;
// Add visual effect
player.color = "red";
setTimeout(() => player.color = "blue", 200);
}
checkSpikeCollision(player) {
// Only spikes hurt when touching from top
const spikeTiles = this.tilemap.tiles(6);
for (let spike of spikeTiles) {
if (player.crashWith(spike) && player.y + player.height < spike.y + 10) {
return true;
}
}
return false;
}
}
// Usage
const collisionSystem = new AdvancedCollision(display.tileFace);
function update() {
collisionSystem.checkAllCollisions(player);
}
🔧 Dynamic Tile Manipulation
Adding and Removing Tiles
// Add a tile at specific grid position
tilemap.add(1, 3, 2); // Add grass (id=1) at column 3, row 2
// Remove a tile from grid position
tilemap.remove(4, 3); // Remove tile at column 4, row 3
// Example: Destructible environment
function destroyTile(x, y) {
const tile = tilemap.rTile(x, y);
if (tile && tile.tid === 2) { // Only destroy stone walls
tilemap.remove(x, y);
// Add destruction effect
createExplosionEffect(tile.x + tile.width/2, tile.y + tile.height/2);
}
}
// Example: Building system
function placeTile(tileId, x, y) {
if (tilemap.rTile(x, y) === null) { // Only place on empty tiles
tilemap.add(tileId, x, y);
}
}
Tile Modification at Runtime
// Change tile properties dynamically
function changeTileColor(tileX, tileY, newColor) {
const tile = tilemap.rTile(tileX, tileY);
if (tile) {
tile.color = newColor;
}
}
// Animate tiles
function animateWaterTiles() {
const waterTiles = tilemap.tiles(3); // All water tiles
waterTiles.forEach((tile, index) => {
// Simple wave animation
tile.y += Math.sin(display.frameNo * 0.1 + index * 0.5) * 0.5;
});
}
// Tile state management
class SmartTile {
constructor(tile, properties = {}) {
this.tile = tile;
this.properties = properties;
this.state = 'normal';
}
activate() {
this.state = 'active';
this.tile.color = 'yellow';
}
deactivate() {
this.state = 'normal';
this.tile.color = this.properties.originalColor;
}
}
🎮 Practical Game Examples
Platformer Level Design
class PlatformerLevel {
constructor() {
this.display = new Display();
this.display.start(800, 600);
this.setupTiles();
this.setupLevel();
this.setupPlayer();
}
setupTiles() {
this.tiles = [
new Component(0, 0, "#7CFC00", 0, 0, "rect"), // 1 - Grass
new Component(0, 0, "#8B4513", 0, 0, "rect"), // 2 - Dirt
new Component(0, 0, "#C0C0C0", 0, 0, "rect"), // 3 - Stone
new Component(0, 0, "#87CEEB", 0, 0, "rect") // 4 - Water
];
}
setupLevel() {
this.levelMap = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 3, 3, 0, 0, 0, 0, 3, 3, 3, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
];
this.display.tile = this.tiles;
this.display.map = this.levelMap;
this.display.tileMap();
this.display.tileFace.show();
}
setupPlayer() {
this.player = new Component(30, 30, "red", 100, 100, "rect");
this.player.physics = true;
this.player.gravity = 0.5;
this.player.bounce = 0.2;
this.display.add(this.player);
// Setup camera to follow player
this.display.camera.worldWidth = this.levelMap[0].length * this.display.tileFace.tileWidth;
this.display.camera.worldHeight = this.levelMap.length * this.display.tileFace.tileHeight;
this.display.camera.follow(this.player, true);
}
update() {
// Player controls
if (this.display.keys[37]) this.player.speedX = -5; // Left
if (this.display.keys[39]) this.player.speedX = 5; // Right
if (this.display.keys[38] && this.player.gravitySpeed === 0) {
this.player.speedY = -12; // Jump
}
// Collision detection
if (this.display.tileFace.crashWith(this.player, 1) ||
this.display.tileFace.crashWith(this.player, 2) ||
this.display.tileFace.crashWith(this.player, 3)) {
this.player.hitBottom();
}
// Water physics
if (this.display.tileFace.crashWith(this.player, 4)) {
this.player.speedX *= 0.7;
this.player.speedY *= 0.7;
}
}
}
RPG World Map
class RPGWorld {
constructor() {
this.display = new Display();
this.display.start(800, 600);
this.setupWorldTiles();
this.createWorldMap();
this.setupPlayer();
}
setupWorldTiles() {
this.tiles = [
new Component(0, 0, "#228B22", 0, 0, "rect"), // 1 - Forest
new Component(0, 0, "#DEB887", 0, 0, "rect"), // 2 - Desert
new Component(0, 0, "#0000FF", 0, 0, "rect"), // 3 - Ocean
new Component(0, 0, "#808080", 0, 0, "rect"), // 4 - Mountains
new Component(0, 0, "#90EE90", 0, 0, "rect"), // 5 - Plains
new Component(0, 0, "#FF0000", 0, 0, "rect") // 6 - Town
];
}
createWorldMap() {
this.worldMap = [
[3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
[3, 4, 4, 1, 1, 1, 1, 4, 4, 3],
[3, 4, 1, 1, 5, 5, 1, 1, 4, 3],
[3, 1, 1, 5, 5, 5, 5, 1, 1, 3],
[3, 1, 5, 5, 6, 5, 5, 5, 1, 3],
[3, 1, 5, 5, 5, 5, 5, 5, 1, 3],
[3, 1, 1, 5, 5, 5, 5, 1, 1, 3],
[3, 4, 1, 1, 2, 2, 1, 1, 4, 3],
[3, 4, 4, 1, 2, 2, 1, 4, 4, 3],
[3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
];
this.display.tile = this.tiles;
this.display.map = this.worldMap;
this.display.tileMap();
this.display.tileFace.show();
}
setupPlayer() {
this.player = new Component(20, 20, "red", 200, 200, "rect");
this.display.add(this.player);
// RPG-style movement (grid-based)
this.moveSpeed = 3;
this.isMoving = false;
}
movePlayer(direction) {
if (this.isMoving) return;
this.isMoving = true;
let targetX = this.player.x;
let targetY = this.player.y;
switch(direction) {
case 'up': targetY -= this.display.tileFace.tileHeight; break;
case 'down': targetY += this.display.tileFace.tileHeight; break;
case 'left': targetX -= this.display.tileFace.tileWidth; break;
case 'right': targetX += this.display.tileFace.tileWidth; break;
}
// Check if movement is valid
if (this.isValidMove(targetX, targetY)) {
move.glideTo(this.player, 0.2, targetX, targetY);
}
setTimeout(() => { this.isMoving = false; }, 200);
}
isValidMove(x, y) {
// Convert world coordinates to grid coordinates
const gridX = Math.floor(x / this.display.tileFace.tileWidth);
const gridY = Math.floor(y / this.display.tileFace.tileHeight);
// Check if within bounds and not ocean/mountains
if (gridX < 0 || gridY < 0 ||
gridX >= this.worldMap[0].length ||
gridY >= this.worldMap.length) {
return false;
}
const tileId = this.worldMap[gridY][gridX];
return tileId !== 3 && tileId !== 4; // Can't move into ocean or mountains
}
update() {
// RPG controls
if (!this.isMoving) {
if (this.display.keys[38]) this.movePlayer('up'); // Up arrow
if (this.display.keys[40]) this.movePlayer('down'); // Down arrow
if (this.display.keys[37]) this.movePlayer('left'); // Left arrow
if (this.display.keys[39]) this.movePlayer('right'); // Right arrow
}
// Check for town entry
this.checkTownEntry();
}
checkTownEntry() {
const gridX = Math.floor(this.player.x / this.display.tileFace.tileWidth);
const gridY = Math.floor(this.player.y / this.display.tileFace.tileHeight);
if (this.worldMap[gridY][gridX] === 6) { // Town tile
console.log("Entering town!");
// Trigger town scene
}
}
}
🔧 Advanced TileMap Techniques
Multi-Layer TileMaps
class MultiLayerTileMap {
constructor() {
this.layers = [];
this.activeLayer = 0;
}
addLayer(tiles, map, zIndex = 0) {
this.layers.push({
tiles: tiles,
map: map,
zIndex: zIndex,
visible: true
});
// Sort layers by z-index
this.layers.sort((a, b) => a.zIndex - b.zIndex);
}
show() {
this.layers.forEach(layer => {
if (layer.visible) {
display.tile = layer.tiles;
display.map = layer.map;
display.tileMap();
// Note: This would need custom rendering for multiple layers
}
});
}
setLayerVisibility(layerIndex, visible) {
if (this.layers[layerIndex]) {
this.layers[layerIndex].visible = visible;
}
}
}
// Usage
const multiMap = new MultiLayerTileMap();
// Background layer (ground)
multiMap.addLayer(
[new Component(0, 0, "green", 0, 0, "rect")],
[[1,1,1],[1,1,1],[1,1,1]],
0
);
// Foreground layer (objects)
multiMap.addLayer(
[new Component(0, 0, "brown", 0, 0, "rect")],
[[0,1,0],[0,0,0],[0,0,1]],
1
);
Procedural Map Generation
class ProceduralMap {
constructor(width, height) {
this.width = width;
this.height = height;
this.map = this.generateEmptyMap();
}
generateEmptyMap() {
return Array(this.height).fill().map(() => Array(this.width).fill(0));
}
generateCave() {
// Simple cellular automata for cave generation
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
// 45% chance of wall
this.map[y][x] = Math.random() < 0.45 ? 2 : 1;
}
}
// Smooth the cave
this.smoothCave(4);
return this.map;
}
smoothCave(iterations) {
for (let i = 0; i < iterations; i++) {
const newMap = this.generateEmptyMap();
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const wallCount = this.countAdjacentWalls(x, y);
if (wallCount > 4) {
newMap[y][x] = 2; // Wall
} else if (wallCount < 4) {
newMap[y][x] = 1; // Floor
} else {
newMap[y][x] = this.map[y][x]; // Unchanged
}
}
}
this.map = newMap;
}
}
countAdjacentWalls(x, y) {
let count = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < this.width && ny >= 0 && ny < this.height) {
if (this.map[ny][nx] === 2) count++;
} else {
count++; // Count edges as walls
}
}
}
return count;
}
}
// Usage
const proceduralMap = new ProceduralMap(20, 15);
const caveLayout = proceduralMap.generateCave();
display.tile = [
new Component(0, 0, "gray", 0, 0, "rect"), // Floor
new Component(0, 0, "darkgray", 0, 0, "rect") // Wall
];
display.map = caveLayout;
display.tileMap();
display.tileFace.show();
⚡ Performance Optimization
Efficient Tile Rendering
class OptimizedTileMap {
constructor() {
this.visibleTiles = [];
this.lastCameraX = 0;
this.lastCameraY = 0;
this.updateThreshold = 50; // Update when camera moves 50 pixels
}
updateVisibleTiles() {
const camera = display.camera;
// Only update if camera moved significantly
if (Math.abs(camera.x - this.lastCameraX) < this.updateThreshold &&
Math.abs(camera.y - this.lastCameraY) < this.updateThreshold) {
return;
}
this.lastCameraX = camera.x;
this.lastCameraY = camera.y;
this.visibleTiles = display.tileFace.tileList.filter(tile => {
return this.isTileVisible(tile);
});
}
isTileVisible(tile) {
return tile.x + tile.width >= display.camera.x &&
tile.x <= display.camera.x + display.canvas.width &&
tile.y + tile.height >= display.camera.y &&
tile.y <= display.camera.y + display.canvas.height;
}
render() {
this.visibleTiles.forEach(tile => {
tile.update(display.context);
});
}
}
// Usage
const optimizedRenderer = new OptimizedTileMap();
function update() {
optimizedRenderer.updateVisibleTiles();
optimizedRenderer.render();
}
🐛 Common Issues and Solutions
TileMap Not Appearing
// Common setup issues and solutions
// 1. Forgot to call show()
display.tileMap(); // Creates tileFace
display.tileFace.show(); // Actually renders tiles
// 2. Tiles array doesn't have null at index 0
const correctTiles = [
new Component(0, 0, "green", 0, 0, "rect")
];
// 3. Map dimensions don't match tile dimensions
function validateMapSize(tilemap) {
const expectedWidth = tilemap.map[0].length * tilemap.tileWidth;
const expectedHeight = tilemap.map.length * tilemap.tileHeight;
console.log(`Map size: ${expectedWidth}x${expectedHeight}`);
}
Performance Issues with Large Maps
// Solutions for large tilemaps
// 1. Use chunk-based loading
class ChunkedTileMap {
constructor(chunkSize = 10) {
this.chunkSize = chunkSize;
this.loadedChunks = new Set();
}
loadChunk(chunkX, chunkY) {
const chunkId = `${chunkX},${chunkY}`;
if (!this.loadedChunks.has(chunkId)) {
// Load chunk data
this.loadedChunks.add(chunkId);
}
}
unloadDistantChunks(playerX, playerY) {
const playerChunkX = Math.floor(playerX / (this.chunkSize * display.tileFace.tileWidth));
const playerChunkY = Math.floor(playerY / (this.chunkSize * display.tileFace.tileHeight));
this.loadedChunks.forEach(chunkId => {
const [chunkX, chunkY] = chunkId.split(',').map(Number);
const distance = Math.abs(chunkX - playerChunkX) + Math.abs(chunkY - playerChunkY);
if (distance > 2) { // Unload chunks more than 2 away
this.loadedChunks.delete(chunkId);
}
});
}
}
📚 Conclusion
TCJSGame’s TileMap Class provides:
- Grid-based level design with visual tile placement
- Efficient collision detection for game physics
- Dynamic tile manipulation at runtime
- Large world support with camera-relative rendering
- Flexible tile definitions for varied game aesthetics
Key features for different game types:
- Platformers: Solid ground and platform tiles
- RPGs: Terrain-based movement restrictions
- Puzzle games: Interactive and movable tiles
- Strategy games: Grid-based unit placement
The TileMap system integrates seamlessly with TCJSGame’s other components, allowing you to create complex, interactive game worlds with minimal code. Combine tilemaps with the camera system for scrolling levels, with physics for collision detection, and with state utilities for interactive environments!