<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>2D Car Racing Game</title>
<style>
body {
margin: 0;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #333;
font-family: Arial, sans-serif;
}
#gameContainer {
width: 90vw;
max-width: 400px;
height: 85vh;
max-height: 700px;
background-color: #4CAF50; /* Grass/Shoulders */
overflow: hidden;
position: relative;
border: 5px solid black;
box-shadow: 0 0 15px rgba(0,0,0,0.5);
}
#road {
width: 70%; /* Road width relative to gameContainer */
height: 100%;
background-color: #555; /* Road color */
position: absolute;
left: 15%; /* Centering the road (100% - 70%)/2 */
top: 0;
overflow: hidden; /* Clip obstacles and lines */
}
.roadLine {
width: 10px;
height: 50px;
background-color: white;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
#playerCar {
width: 50px;
height: 80px;
background-color: #f44336; /* Red */
position: absolute;
z-index: 10;
border-radius: 10px 10px 0 0;
border: 2px solid #333;
box-sizing: border-box;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.obstacle {
width: 50px;
height: 80px;
position: absolute;
z-index: 5;
border-radius: 10px 10px 0 0;
border: 2px solid #333;
box-sizing: border-box;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
#score {
position: absolute;
top: 10px;
left: 10px; /* Position relative to gameContainer */
color: white;
font-size: clamp(16px, 4vw, 24px); /* Responsive font size */
z-index: 20;
text-shadow: 1px 1px 2px black;
}
#gameOverScreen {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.85);
color: white;
padding: 20px;
text-align: center;
border-radius: 10px;
z-index: 100;
width: 80%;
box-sizing: border-box;
}
#gameOverScreen h2 {
margin-top: 0;
font-size: clamp(20px, 5vw, 30px);
}
#gameOverScreen p {
font-size: clamp(16px, 4vw, 24px);
}
#restartButton {
padding: 10px 20px;
font-size: clamp(14px, 3.5vw, 20px);
margin-top: 15px;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
transition: background-color 0.3s;
}
#restartButton:hover {
background-color: #45a049;
}
#mobileControls {
margin-top: 10px;
display: none; /* Hidden by default, shown by JS if on mobile */
user-select: none; /* Prevent text selection on button press */
}
.control-btn-row {
display: flex;
justify-content: center;
align-items: center;
}
.control-btn {
background-color: #666;
border: 2px solid #444;
color: white;
padding: 10px;
width: 60px; /* Fixed size for touch targets */
height: 50px;
text-align: center;
font-size: 20px;
cursor: pointer;
border-radius: 8px;
margin: 5px;
box-shadow: 0 2px 3px rgba(0,0,0,0.3);
}
.control-btn:active {
background-color: #555;
transform: translateY(1px);
}
</style>
</head>
<body>
<div id="gameContainer">
<div id="road">
<!-- Road lines will be added here by JS -->
<div id="playerCar"></div>
<!-- Obstacles will be added here by JS -->
</div>
<div id="score">Score: 0</div>
<div id="gameOverScreen" style="display: none;">
<h2>Game Over</h2>
<p id="finalScore">Your Score: 0</p>
<button id="restartButton">Restart</button>
</div>
</div>
<div id="mobileControls">
<div class="control-btn-row">
<button class="control-btn" id="btnUp">▲</button>
</div>
<div class="control-btn-row">
<button class="control-btn" id="btnLeft">◄</button>
<button class="control-btn" id="btnDown">▼</button>
<button class="control-btn" id="btnRight">►</button>
</div>
</div>
<script>
const gameContainer = document.getElementById('gameContainer');
const road = document.getElementById('road');
const playerCarElement = document.getElementById('playerCar');
const scoreDisplay = document.getElementById('score');
const gameOverScreen = document.getElementById('gameOverScreen');
const finalScoreDisplay = document.getElementById('finalScore');
const restartButton = document.getElementById('restartButton');
const mobileControls = document.getElementById('mobileControls');
const PLAYER_CAR_WIDTH = 50;
const PLAYER_CAR_HEIGHT = 80;
const OBSTACLE_WIDTH = 50;
const OBSTACLE_HEIGHT = 80;
const ROAD_LINE_HEIGHT = 50;
const ROAD_LINE_GAP = 30;
const OBSTACLE_COLORS = ['#007bff', '#6f42c1', '#28a745', '#fd7e14', '#17a2b8'];
let player = {
x: 0, // Relative to road, from left
y: 0, // Relative to road, from top (used for style.top)
speed: 7, // Pixels per frame for horizontal/vertical movement
width: PLAYER_CAR_WIDTH,
height: PLAYER_CAR_HEIGHT
};
let game = {
score: 0,
isOver: true,
animationFrameId: null,
roadSpeed: 5, // Initial speed of road/obstacles
baseRoadSpeed: 5,
speedIncrement: 0.002, // How much roadSpeed increases per frame
obstacleSpawnCounter: 0,
obstacleSpawnInterval: 80, // Frames between potential spawns, decreases over time
minObstacleSpawnInterval: 30,
roadLines: [],
obstacles: []
};
let keys = {
ArrowUp: false,
ArrowDown: false,
ArrowLeft: false,
ArrowRight: false
};
function isMobileDevice() {
return ('ontouchstart' in window || navigator.maxTouchPoints > 0);
}
function startGame() {
game.isOver = false;
game.score = 0;
game.roadSpeed = game.baseRoadSpeed;
game.obstacleSpawnCounter = 0;
game.obstacleSpawnInterval = 80;
scoreDisplay.textContent = 'Score: 0';
gameOverScreen.style.display = 'none';
// Clear previous obstacles and road lines
game.obstacles.forEach(obs => obs.element.remove());
game.obstacles = [];
game.roadLines.forEach(line => line.element.remove());
game.roadLines = [];
// Initial player position (bottom center of the road)
player.x = road.offsetWidth / 2 - player.width / 2;
player.y = road.offsetHeight - player.height - 20; // 20px from bottom
playerCarElement.style.left = player.x + 'px';
playerCarElement.style.top = player.y + 'px';
playerCarElement.style.display = 'block';
// Create initial road lines
const numLines = Math.ceil(road.offsetHeight / (ROAD_LINE_HEIGHT + ROAD_LINE_GAP)) + 1;
for (let i = 0; i < numLines; i++) {
const lineElement = document.createElement('div');
lineElement.className = 'roadLine';
lineElement.style.height = ROAD_LINE_HEIGHT + 'px';
const lineTop = i * (ROAD_LINE_HEIGHT + ROAD_LINE_GAP) - ROAD_LINE_GAP; // Start lines from top
lineElement.style.top = lineTop + 'px';
road.appendChild(lineElement);
game.roadLines.push({ element: lineElement, top: lineTop });
}
if (game.animationFrameId) {
cancelAnimationFrame(game.animationFrameId);
}
gameLoop();
}
function updatePlayerPosition() {
let dx = 0;
let dy = 0;
if (keys.ArrowLeft) dx -= player.speed;
if (keys.ArrowRight) dx += player.speed;
if (keys.ArrowUp) dy -= player.speed;
if (keys.ArrowDown) dy += player.speed;
player.x += dx;
player.y += dy;
// Boundary checks for player car within the road
player.x = Math.max(0, Math.min(player.x, road.offsetWidth - player.width));
player.y = Math.max(0, Math.min(player.y, road.offsetHeight - player.height));
playerCarElement.style.left = player.x + 'px';
playerCarElement.style.top = player.y + 'px';
}
function moveRoadLines() {
game.roadLines.forEach(line => {
line.top += game.roadSpeed;
if (line.top > road.offsetHeight) {
line.top = -ROAD_LINE_HEIGHT - ROAD_LINE_GAP; // Move to top above screen
}
line.element.style.top = line.top + 'px';
});
}
function spawnObstacle() {
const obstacleElement = document.createElement('div');
obstacleElement.className = 'obstacle';
obstacleElement.style.width = OBSTACLE_WIDTH + 'px';
obstacleElement.style.height = OBSTACLE_HEIGHT + 'px';
const obstacleX = Math.random() * (road.offsetWidth - OBSTACLE_WIDTH);
obstacleElement.style.left = obstacleX + 'px';
obstacleElement.style.top = -OBSTACLE_HEIGHT + 'px'; // Start above screen
obstacleElement.style.backgroundColor = OBSTACLE_COLORS[Math.floor(Math.random() * OBSTACLE_COLORS.length)];
road.appendChild(obstacleElement);
game.obstacles.push({
element: obstacleElement,
x: obstacleX,
y: -OBSTACLE_HEIGHT,
width: OBSTACLE_WIDTH,
height: OBSTACLE_HEIGHT
});
}
function moveObstacles() {
for (let i = game.obstacles.length - 1; i >= 0; i--) {
const obs = game.obstacles[i];
obs.y += game.roadSpeed;
obs.element.style.top = obs.y + 'px';
if (obs.y > road.offsetHeight) {
obs.element.remove();
game.obstacles.splice(i, 1);
}
}
}
function checkCollision(rect1, rect2) {
// rect1 and rect2 are objects {x, y, width, height}
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}
function handleCollisions() {
const playerRect = {
x: player.x,
y: player.y,
width: player.width,
height: player.height
};
for (const obs of game.obstacles) {
if (checkCollision(playerRect, obs)) {
gameOver();
break;
}
}
}
function updateScore() {
game.score++;
scoreDisplay.textContent = `Score: ${game.score}`;
}
function increaseDifficulty() {
game.roadSpeed += game.speedIncrement;
// Decrease spawn interval but not below min
if (game.obstacleSpawnInterval > game.minObstacleSpawnInterval) {
game.obstacleSpawnInterval -= 0.05; // Gradually decrease
}
}
function gameOver() {
game.isOver = true;
cancelAnimationFrame(game.animationFrameId);
playerCarElement.style.display = 'none'; // Hide car
gameOverScreen.style.display = 'block';
finalScoreDisplay.textContent = `Your Score: ${game.score}`;
}
function gameLoop() {
if (game.isOver) return;
updatePlayerPosition();
moveRoadLines();
game.obstacleSpawnCounter++;
if (game.obstacleSpawnCounter >= Math.max(game.minObstacleSpawnInterval, game.obstacleSpawnInterval)) {
spawnObstacle();
game.obstacleSpawnCounter = 0;
}
moveObstacles();
handleCollisions();
if (game.isOver) return; // Collision might have happened
updateScore();
increaseDifficulty();
game.animationFrameId = requestAnimationFrame(gameLoop);
}
// Event Listeners
window.addEventListener('keydown', (e) => {
if (keys.hasOwnProperty(e.key)) {
keys[e.key] = true;
e.preventDefault(); // Prevent page scroll for arrow keys
}
});
window.addEventListener('keyup', (e) => {
if (keys.hasOwnProperty(e.key)) {
keys[e.key] = false;
e.preventDefault();
}
});
restartButton.addEventListener('click', startGame);
// Mobile Controls Setup
if (isMobileDevice()) {
mobileControls.style.display = 'block';
const controlButtons = {
btnUp: 'ArrowUp',
btnDown: 'ArrowDown',
btnLeft: 'ArrowLeft',
btnRight: 'ArrowRight'
};
for (const [btnId, keyName] of Object.entries(controlButtons)) {
const btn = document.getElementById(btnId);
if (btn) {
btn.addEventListener('touchstart', (e) => {
keys[keyName] = true;
e.preventDefault();
});
btn.addEventListener('touchend', (e) => {
keys[keyName] = false;
e.preventDefault();
});
}
}
}
// Set initial player car size for JS logic (could also get from element.offsetWidth but this is safer before display)
playerCarElement.style.width = player.width + 'px';
playerCarElement.style.height = player.height + 'px';
// Start game automatically or show a start screen
// For now, let's require a click on restart (which acts as start initially)
// To make it start automatically:
// document.addEventListener('DOMContentLoaded', startGame);
// But since it's all in one file, simply calling startGame() might be delayed
// Let's make the game over screen the initial screen with "Start Game"
gameOverScreen.style.display = 'block';
finalScoreDisplay.innerHTML = "Press Start to Play!";
document.querySelector('#gameOverScreen h2').textContent = "2D Car Racer";
restartButton.textContent = "Start Game";
restartButton.addEventListener('click', () => {
if (restartButton.textContent === "Start Game") {
restartButton.textContent = "Restart";
document.querySelector('#gameOverScreen h2').textContent = "Game Over";
}
startGame();
});
</script>
</body>
</html>
0 Comments