I have map data like this, each element separated by space is a different tile with own properties:
static get TILEMAP() {
return "0C0003 0W1003 0W0003 0W2003 0W3003 0W4003".split(" "); // example tiles, its much longer for real maps
}
I load tile images like this:
let tiles = {};
const tilemap = Constants.TILEMAP;
function loadTiles() {
for (const tileIndex in tilemap) {
const rawTile = tilemap[tileIndex].substring(0, 3); // 0W1.png for example
const tileImg = new Image();
tileImg.src = `sprites/maps/tiles/${rawTile}.png`;
tiles[rawTile] = tileImg;
}
}
And I draw them to canvas like this:
static rotationKeys = {0: 0, 1: 90, 2: 180, 3: 270};
static tileSize = 50;
function drawMap() {
const tileSize = Constants.tileSize;
gameSettings.mapWidth = 35;
gameSettings.mapHeight = 24;
const startX = Math.max(0, Math.floor(camera.x / (tileSize * scaleFactor)));
const startY = Math.max(0, Math.floor(camera.y / (tileSize * scaleFactor)));
const endX = Math.min(gameSettings.mapWidth, startX + Math.ceil(canvas.width / (tileSize * scaleFactor)) + 1);
const endY = Math.min(gameSettings.mapHeight, startY + Math.ceil(canvas.height / (tileSize * scaleFactor)) + 1);
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
let index = y * gameSettings.mapWidth + x;
const tile = tilemap[index];
const rotation = parseInt(tile[3]); // Get rotation value for this tile
const flip = parseInt(tile[4]);
// Apply rotation transformation
ctx.save(); // Save the current canvas state
ctx.translate((x + 0.5) * tileSize * scaleFactor, (y + 0.5) * tileSize * scaleFactor); // Translate to the center of the tile
ctx.rotate(Constants.rotationKeys[rotation] * Math.PI / 180); // Rotate by the specified angle (converted to radians)
// Apply flip transformation
var xScale = 1;
var yScale = 1;
if (flip == 1 || flip == 3) {
xScale = -1;
}
if (flip == 2 || flip == 3) {
yScale = -1;
}
ctx.scale(xScale, yScale);
try {
ctx.drawImage(tiles[tile.substring(0, 3)], -0.5 * tileSize * scaleFactor, -0.5 * tileSize * scaleFactor, tileSize * scaleFactor, tileSize * scaleFactor); // Draw the rotated image
} catch (error) {
console.error(`Failed to draw tile at (${x}, ${y}):`, error);
console.log("Responsible tile:", tile);
}
ctx.restore();
}
}
}
As you can see, the tiles can go through quite a lot. Rotations or flips or combined.
I’ve tried defining all collisions like this:
static colTransforms = {
"5": {
rotations: {
"0": { x: -1, y: -1, width: 52, height: 38,
flips: {
"1": { x: -1, y: -1, width: 52, height: 38 },
"2": { x: -1, y: 13, width: 52, height: 38 },
"3": { x: -1, y: -1, width: 52, height: 38 }
}},
"1": { x: -1, y: -1, width: 38, height: 52,
flips: {
"1": { x: 13, y: -1, width: 38, height: 52 },
"2": { x: -1, y: -1, width: 38, height: 52 },
"3": { x: 13, y: -1, width: 38, height: 52 }
}},
"2": { x: -1, y: 13, width: 52, height: 38,
flips: {
"1": { x: -1, y: 13, width: 52, height: 38 },
"2": { x: -1, y: -1, width: 52, height: 38 },
"3": { x: -1, y: 13, width: 52, height: 38 }
}},
"3": { x: -1, y: -1, width: 38, height: 52,
flips: {
"1": { x: 13, y: -1, width: 38, height: 52 },
"2": { x: -1, y: -1, width: 38, height: 52 },
"3": { x: 13, y: -1, width: 38, height: 52 }
}}
},
},
"6": {
rotations: {
"0": { x: -1, y: -1, width: 52, height: 26,
flips: {
"1": { x: -1, y: -1, width: 52, height: 26 },
"2": { x: -1, y: 25, width: 52, height: 26 },
"3": { x: -1, y: -1, width: 52, height: 26 }
}},
"1": { x: -1, y: -1, width: 26, height: 52,
flips: {
"1": { x: 25, y: -1, width: 26, height: 52 },
"2": { x: -1, y: -1, width: 26, height: 52 },
"3": { x: 25, y: -1, width: 26, height: 52 }
}},
"2": { x: -1, y: 25, width: 52, height: 26,
flips: {
"1": { x: -1, y: 25, width: 52, height: 26 },
"2": { x: -1, y: -1, width: 52, height: 26 },
"3": { x: -1, y: 25, width: 52, height: 26 }
}},
"3": { x: -1, y: -1, width: 52, height: 26,
flips: {
"1": { x: -1, y: -1, width: 52, height: 26 },
"2": { x: -1, y: 25, width: 52, height: 26 },
"3": { x: -1, y: -1, width: 52, height: 26 }
}}
},
},
"7": {
rotations: {
"0": { x: -1, y: -1, width: 52, height: 13,
flips: {
"1": { x: -1, y: -1, width: 52, height: 13 },
"2": { x: -1, y: 38, width: 52, height: 13 },
"3": { x: -1, y: -1, width: 52, height: 13 }
}},
"1": { x: -1, y: -1, width: 13, height: 52,
flips: {
"1": { x: 38, y: -1, width: 13, height: 52 },
"2": { x: -1, y: -1, width: 13, height: 52 },
"3": { x: 38, y: -1, width: 13, height: 52 }
}},
"2": { x: -1, y: 38, width: 52, height: 13,
flips: {
"1": { x: -1, y: 38, width: 52, height: 13 },
"2": { x: -1, y: -1, width: 52, height: 13 },
"3": { x: -1, y: 38, width: 52, height: 13 }
}},
"3": { x: -1, y: -1, width: 52, height: 13,
flips: {
"1": { x: -1, y: -1, width: 52, height: 13 },
"2": { x: -1, y: 38, width: 52, height: 13 },
"3": { x: -1, y: -1, width: 52, height: 13 }
}}
},
},
"8": {
rotations: {
"0": { x: -1, y: -1, width: 26, height: 26,
flips: {
"1": { x: 25, y: -1, width: 26, height: 26 },
"2": { x: -1, y: 25, width: 26, height: 26 },
"3": { x: 25, y: -1, width: 26, height: 26 }
}},
"1": { x: 25, y: -1, width: 26, height: 26,
flips: {
"1": { x: -1, y: -1, width: 26, height: 26 },
"2": { x: 25, y: 25, width: 26, height: 26 },
"3": { x: -1, y: -1, width: 26, height: 26 }
}},
"2": { x: 25, y: 25, width: 26, height: 26,
flips: {
"1": { x: -1, y: 25, width: 26, height: 26 },
"2": { x: 25, y: -1, width: 26, height: 26 },
"3": { x: -1, y: 25, width: 26, height: 26 }
}},
"3": { x: -1, y: 25, width: 26, height: 26,
flips: {
"1": { x: 25, y: 25, width: 26, height: 26 },
"2": { x: -1, y: -1, width: 26, height: 26 },
"3": { x: 25, y: 25, width: 26, height: 26 }
}}
},
},
"9": {
Lfirst: {
rotations: {
"0": { x: -1, y: -1, width: 52, height: 26,
flips: {
"1": { x: -1, y: -1, width: 52, height: 26 },
"2": { x: -1, y: 25, width: 52, height: 26 },
"3": { x: -1, y: -1, width: 52, height: 26 }
}},
"1": { x: 25, y: 25, width: 26, height: 52,
flips: {
"1": { x: 25, y: -1, width: 26, height: 52 },
"2": { x: -1, y: -1, width: 26, height: 52 },
"3": { x: 25, y: -1, width: 26, height: 52 }
}},
"2": { x: -1, y: 25, width: 52, height: 26,
flips: {
"1": { x: -1, y: 25, width: 52, height: 26 },
"2": { x: -1, y: -1, width: 52, height: 26 },
"3": { x: -1, y: 25, width: 52, height: 26 }
}},
"3": { x: -1, y: 25, width: 52, height: 26,
flips: {
"1": { x: -1, y: -1, width: 52, height: 26 },
"2": { x: -1, y: 25, width: 52, height: 26 },
"3": { x: -1, y: -1, width: 52, height: 26 }
}}
},
},
Lsecond: {
rotations: {
"0": { x: -1, y: -1, width: 26, height: 52,
flips: {
"1": { x: 25, y: -1, width: 26, height: 52 },
"2": { x: -1, y: -1, width: 26, height: 52 },
"3": { x: 25, y: -1, width: 26, height: 52 }
}},
"1": { x: -1, y: -1, width: 52, height: 26,
flips: {
"1": { x: -1, y: -1, width: 52, height: 26 },
"2": { x: -1, y: 25, width: 52, height: 26 },
"3": { x: -1, y: -1, width: 52, height: 26 }
}},
"2": { x: 25, y: -1, width: 26, height: 52,
flips: {
"1": { x: -1, y: -1, width: 26, height: 26 },
"2": { x: 25, y: 25, width: 26, height: 26 },
"3": { x: -1, y: -1, width: 26, height: 26 }
}},
"3": { x: -1, y: -1, width: 26, height: 52,
flips: {
"1": { x: 25, y: -1, width: 26, height: 52 },
"2": { x: -1, y: -1, width: 26, height: 52 },
"3": { x: 25, y: -1, width: 26, height: 52 }
}}
},
}
}
} // transformations for collisions
So that we could later check for collision like this when the player is walking into the tile:
let newTileX;
let newTileY;
let newTileX;
let newTileY;
this.x = this.body.x;
this.y = this.body.y;
if (speedX != null) {
newTileX = Math.floor((this.x + speedX) / 50);
} else {
newTileX = Math.floor(this.x / 50);
}
if (speedY != null) {
newTileY = Math.floor((this.y + speedY) / 50);
} else {
newTileY = Math.floor(this.y / 50);
}
const tileName = Constants.TILEMAP[newTileY * 35 + newTileX];
const rotation = tileName[3]; // Get rotation value for this tile
const flip = tileName[4]; // Get flip value
const collision = tileName[5]; // Get collision type
let collisionShape = null;
switch (parseInt(collision)) {
case 0:
case 1:
case 2:
// The entire tile is walkable
collisionShape = null; // No need to define a collision shape for walkable tiles
break;
case 3:
case 4:
// The entire tile is an obstacle
collisionShape = { x: -1, y: -1, width: 52, height: 52 };
break;
case 5:
case 6:
case 7:
case 8:
if (parseInt(flip) > 0) {
collisionShape = Constants.colTransforms[collision].rotations[rotation].flips[flip];
} else {
collisionShape = Constants.colTransforms[collision].rotations[rotation];
}
break;
case 9:
if (parseInt(flip) > 0) {
collisionShape = [];
console.log(collision)
collisionShape.push(Constants.colTransforms[collision].Lfirst.rotations[rotation].flips[flip]);
collisionShape.push(Constants.colTransforms[collision].Lsecond.rotations[rotation].flips[flip]);
} else {
console.log(collision)
collisionShape = [];
collisionShape.push(Constants.colTransforms[collision].Lfirst.rotations[rotation]);
collisionShape.push(Constants.colTransforms[collision].Lsecond.rotations[rotation]);
}
break
}
// Check for collision only if collision shape is defined
if (collisionShape != null && !Array.isArray(collisionShape)) {
// Calculate the player's local position within the tile
const localPlayerX = (this.x + (speedX || 0)) % 50;
const localPlayerY = (this.y + (speedY || 0)) % 50;
// Calculate the center of the circular area (head radius)
const playerCenterX = localPlayerX + Constants.playerCircle;
const playerCenterY = localPlayerY + Constants.playerCircle;
// Calculate the closest point on the rectangle to the circle
const closestX = this.clamp(playerCenterX, collisionShape.x, collisionShape.x + collisionShape.width);
const closestY = this.clamp(playerCenterY, collisionShape.y, collisionShape.y + collisionShape.height);
// Calculate the distance between the circle's center and the closest point
const distanceX = playerCenterX - closestX;
const distanceY = playerCenterY - closestY;
// Check if the distance is less than or equal to the circle's radius
if ((distanceX * distanceX + distanceY * distanceY) <= (Constants.playerCircle * Constants.playerCircle)) {
// Collision detected, player cannot move
speedX = 0;
speedY = 0;
}
} else if (collisionShape != null && Array.isArray(collisionShape)) {
for (let shape of collisionShape) {
// Calculate the player's local position within the tile
const localPlayerX = (this.x + (speedX || 0)) % 50;
const localPlayerY = (this.y + (speedY || 0)) % 50;
// Calculate the center of the circular area (head radius)
const playerCenterX = localPlayerX + Constants.playerCircle;
const playerCenterY = localPlayerY + Constants.playerCircle;
// Calculate the closest point on the rectangle to the circle
const closestX = this.clamp(playerCenterX, shape.x, shape.x + shape.width);
const closestY = this.clamp(playerCenterY, shape.y, shape.y + shape.height);
// Calculate the distance between the circle's center and the closest point
const distanceX = playerCenterX - closestX;
const distanceY = playerCenterY - closestY;
// Check if the distance is less than or equal to the circle's radius
if ((distanceX * distanceX + distanceY * distanceY) <= (Constants.STICK_FIGURE_HEAD_RADIUS * Constants.STICK_FIGURE_HEAD_RADIUS)) {
// Collision detected, player cannot move
speedX = 0;
speedY = 0;
}
}
}
Its not working as expected though, and honestly the whole approach seems messy to say the least. Some shapes arent flipped or rotated properly (could be my calculations, but it also could be that the code is just doing wrong math). How to actually properly add collision shapes to the individual tiles and make sure they go through the same rotations and flips when necessary?