3D Арканоид
by LuckyGlider65536 lines22.7 KB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Арканоид</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #0a0a1a, #1a1a3e);
font-family: 'Arial', sans-serif;
}
.game-wrapper {
background: linear-gradient(145deg, #1a1a2e, #16213e);
padding: 20px;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.8);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 5px 15px 5px;
color: #e0e0e0;
}
.header-left {
display: flex;
gap: 30px;
font-size: 20px;
font-weight: bold;
}
.header-left span {
background: rgba(255,255,255,0.06);
padding: 6px 16px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.05);
}
.header-left .score-val { color: #f7d44a; }
.header-left .lives-val { color: #ff6b6b; }
.restart-btn {
background: linear-gradient(135deg, #e94560, #c73652);
color: white;
border: none;
padding: 8px 24px;
border-radius: 8px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: 0.3s;
box-shadow: 0 4px 15px rgba(233,69,96,0.3);
}
.restart-btn:hover {
transform: scale(1.05);
box-shadow: 0 6px 25px rgba(233,69,96,0.5);
}
canvas {
display: block;
border-radius: 10px;
background: #0a0a1a;
cursor: none;
box-shadow: inset 0 0 50px rgba(78,205,196,0.05);
}
.footer {
color: #6a6a8a;
text-align: center;
padding-top: 12px;
font-size: 14px;
letter-spacing: 0.5px;
}
.footer kbd {
background: rgba(255,255,255,0.08);
padding: 2px 10px;
border-radius: 4px;
border: 1px solid rgba(255,255,255,0.05);
color: #b0b0d0;
}
</style>
</head>
<body>
<div class="game-wrapper">
<div class="header">
<div class="header-left">
<span>🏆 <span class="score-val" id="scoreDisplay">0</span></span>
<span>❤️ <span class="lives-val" id="livesDisplay">3</span></span>
</div>
<button class="restart-btn" id="restartButton">🔄 Рестарт</button>
</div>
<canvas id="gameCanvas" width="640" height="420"></canvas>
<div class="footer">
<kbd>←</kbd> <kbd>→</kbd> или мышь • <kbd>Пробел</kbd> или клик — запуск мяча
</div>
</div>
<script>
(function(){
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('scoreDisplay');
const livesSpan = document.getElementById('livesDisplay');
// ---------- РАЗМЕРЫ ----------
const W = 640, H = 420;
// ---------- 3D ПАРАМЕТРЫ ----------
// "Глубина" — насколько сильно уходят вдаль верхние ряды
const DEPTH_FACTOR = 0.35;
// Смещение по Y для создания перспективы (верхние ряды выше)
const PERSPECTIVE_OFFSET = 60;
// ---------- ИГРОВЫЕ ПАРАМЕТРЫ ----------
const PADDLE_WIDTH = 90;
const PADDLE_HEIGHT = 14;
const PADDLE_SPEED = 5.5;
const BALL_RADIUS = 7;
const BRICK_ROWS = 7;
const BRICK_COLS = 9;
const BRICK_WIDTH = 58;
const BRICK_HEIGHT = 22;
const BRICK_PADDING = 4;
const BRICK_TOP_OFFSET = 35;
// ---------- СОСТОЯНИЕ ----------
let paddle = { x: W/2 - PADDLE_WIDTH/2, y: H - 38, w: PADDLE_WIDTH, h: PADDLE_HEIGHT };
let ball = {
x: W/2, y: H - 52,
r: BALL_RADIUS,
dx: 0, dy: 0,
speed: 5.0
};
let bricks = [];
let score = 0;
let lives = 3;
let gameRunning = true;
let gameStarted = false;
let animId = null;
let keys = { left: false, right: false };
// ---------- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ 3D ----------
// Получить коэффициент масштаба для ряда (0 = низ, 1 = верх)
function getDepthScale(row) {
// row 0 = самый верхний (дальний), row (BRICK_ROWS-1) = нижний (ближний)
const t = row / (BRICK_ROWS - 1); // 0..1
// Чем выше ряд, тем сильнее сжатие
return 1 - t * DEPTH_FACTOR;
}
// Получить Y-смещение для ряда (перспектива)
function getRowOffsetY(row) {
const t = row / (BRICK_ROWS - 1);
return -t * PERSPECTIVE_OFFSET;
}
// Трансформировать координаты кирпича с учётом 3D
function getBrickScreenCoords(brick) {
const scale = getDepthScale(brick.row);
const offsetY = getRowOffsetY(brick.row);
// Центр поля смещается так, чтобы верхние ряды "уезжали" вверх-вглубь
const cx = W/2;
const xOff = (brick.x - cx) * scale;
const screenX = cx + xOff;
const screenY = brick.y + offsetY;
const screenW = brick.w * scale;
const screenH = brick.h * scale;
return { x: screenX, y: screenY, w: screenW, h: screenH };
}
// ---------- ИНИЦИАЛИЗАЦИЯ КИРПИЧЕЙ ----------
function initBricks() {
const colors = [
['#ff6b6b', '#ee5a24'],
['#feca57', '#f9a825'],
['#f7d44a', '#f5a623'],
['#4ecdc4', '#0abde3'],
['#45b7d1', '#0984e3'],
['#a29bfe', '#6c5ce7'],
['#fd79a8', '#e84393']
];
bricks = [];
const totalWidth = BRICK_COLS * (BRICK_WIDTH + BRICK_PADDING) - BRICK_PADDING;
const startX = (W - totalWidth) / 2;
for (let row = 0; row < BRICK_ROWS; row++) {
const scale = getDepthScale(row);
// Для верхних рядов кирпичи немного уже — создаём иллюзию перспективы
const effectiveW = BRICK_WIDTH * scale;
const effectiveH = BRICK_HEIGHT * scale;
const effectivePad = BRICK_PADDING * scale;
const rowTotal = BRICK_COLS * (effectiveW + effectivePad) - effectivePad;
const rowStartX = (W - rowTotal) / 2;
for (let col = 0; col < BRICK_COLS; col++) {
const x = rowStartX + col * (effectiveW + effectivePad);
const y = BRICK_TOP_OFFSET + row * (BRICK_HEIGHT + BRICK_PADDING);
const color = colors[row % colors.length];
bricks.push({
row: row,
col: col,
x: x,
y: y,
w: BRICK_WIDTH, // храним базовый размер
h: BRICK_HEIGHT,
alive: true,
color: color[0],
glow: color[1]
});
}
}
}
// ---------- СБРОС МЯЧА ----------
function resetBall() {
ball.x = paddle.x + paddle.w/2;
ball.y = paddle.y - ball.r - 1;
ball.dx = 0;
ball.dy = 0;
gameStarted = false;
}
function startBall() {
if (gameStarted) return;
const angle = (Math.random() * 50 + 20) * Math.PI / 180;
const dir = Math.random() > 0.5 ? 1 : -1;
ball.dx = ball.speed * Math.cos(angle) * dir;
ball.dy = -ball.speed * Math.sin(angle);
gameStarted = true;
}
// ---------- ОБНОВЛЕНИЕ ----------
function update() {
if (!gameRunning || !gameStarted) return;
// Управление
if (keys.left && paddle.x > 0) paddle.x -= PADDLE_SPEED;
if (keys.right && paddle.x + paddle.w < W) paddle.x += PADDLE_SPEED;
// Движение мяча
ball.x += ball.dx;
ball.y += ball.dy;
// Стены
if (ball.x - ball.r < 0) { ball.x = ball.r; ball.dx = -ball.dx; }
if (ball.x + ball.r > W) { ball.x = W - ball.r; ball.dx = -ball.dx; }
if (ball.y - ball.r < 0) { ball.y = ball.r; ball.dy = -ball.dy; }
// Платформа (с учётом 3D — платформа плоская, без искажений)
if (ball.dy > 0 &&
ball.y + ball.r >= paddle.y &&
ball.y + ball.r <= paddle.y + paddle.h + 4 &&
ball.x >= paddle.x - ball.r &&
ball.x <= paddle.x + paddle.w + ball.r) {
const hitPos = (ball.x - paddle.x) / paddle.w;
const angle = (hitPos - 0.5) * Math.PI / 2.2;
const speed = Math.sqrt(ball.dx*ball.dx + ball.dy*ball.dy);
ball.dx = speed * Math.sin(angle);
ball.dy = -speed * Math.cos(angle);
const curSpeed = Math.sqrt(ball.dx*ball.dx + ball.dy*ball.dy);
const newSpeed = Math.min(curSpeed * 1.01, 6.8);
const ratio = newSpeed / curSpeed;
ball.dx *= ratio;
ball.dy *= ratio;
}
// Потеря мяча
if (ball.y + ball.r > H) {
lives--;
updateLives();
if (lives === 0) {
gameRunning = false;
setTimeout(() => {
alert('💔 Игра окончена! Попробуйте снова.');
resetGame();
}, 150);
} else {
resetBall();
paddle.x = (W - paddle.w) / 2;
}
}
// Столкновения с кирпичами (с использованием 3D-координат)
for (const brick of bricks) {
if (!brick.alive) continue;
const scr = getBrickScreenCoords(brick);
const bx = ball.x, by = ball.y, r = ball.r;
// Проверка пересечения с трансформированным прямоугольником
if (bx + r > scr.x && bx - r < scr.x + scr.w &&
by + r > scr.y && by - r < scr.y + scr.h) {
brick.alive = false;
score += 10;
updateScore();
// Определяем сторону столкновения
const overlapX = Math.min(bx + r - scr.x, scr.x + scr.w - (bx - r));
const overlapY = Math.min(by + r - scr.y, scr.y + scr.h - (by - r));
if (overlapX < overlapY) {
ball.dx = -ball.dx;
} else {
ball.dy = -ball.dy;
}
// Проверка победы
if (bricks.every(b => !b.alive)) {
gameRunning = false;
setTimeout(() => {
alert('🎉 ПОБЕДА! Вы разбили все кирпичи в 3D! 🎉');
resetGame();
}, 150);
}
break;
}
}
}
// ---------- ОТРИСОВКА ----------
function draw() {
ctx.clearRect(0, 0, W, H);
// --- Пол с эффектом глубины ---
const grad = ctx.createLinearGradient(0, H-60, 0, H);
grad.addColorStop(0, 'rgba(78,205,196,0.0)');
grad.addColorStop(0.5, 'rgba(78,205,196,0.03)');
grad.addColorStop(1, 'rgba(78,205,196,0.08)');
ctx.fillStyle = grad;
ctx.fillRect(0, H-60, W, 60);
// --- Линии перспективы на поле ---
ctx.strokeStyle = 'rgba(78,205,196,0.04)';
ctx.lineWidth = 1;
for (let i = 0; i < 12; i++) {
const t = i / 12;
const y = H - 20 - t * 200;
const spread = 60 + t * 40;
ctx.beginPath();
ctx.moveTo(W/2 - spread, y);
ctx.lineTo(W/2 + spread, y);
ctx.stroke();
}
// --- Кирпичи (с 3D трансформацией) ---
for (const brick of bricks) {
if (!brick.alive) continue;
const scr = getBrickScreenCoords(brick);
// Основной цвет с 3D-затемнением (верхние ряды темнее)
const scale = getDepthScale(brick.row);
const brightness = 0.55 + 0.45 * scale;
ctx.shadowColor = brick.glow;
ctx.shadowBlur = 12 * scale + 4;
// Градиент для объёма
const grad2 = ctx.createLinearGradient(scr.x, scr.y, scr.x, scr.y + scr.h);
grad2.addColorStop(0, brick.color);
grad2.addColorStop(0.5, brick.color);
grad2.addColorStop(1, brick.glow);
ctx.fillStyle = grad2;
// Скруглённый прямоугольник
const radius = 4 * scale + 1;
ctx.beginPath();
ctx.roundRect(scr.x, scr.y, scr.w, scr.h, radius);
ctx.fill();
// Блик
ctx.shadowBlur = 0;
ctx.fillStyle = `rgba(255,255,255,${0.12 * scale + 0.05})`;
ctx.beginPath();
ctx.roundRect(scr.x + 3*scale, scr.y + 2*scale, scr.w - 6*scale, scr.h * 0.3, 2);
ctx.fill();
}
ctx.shadowBlur = 0;
// --- Платформа (с лёгким свечением) ---
ctx.shadowColor = '#4ecdc4';
ctx.shadowBlur = 20;
const pGrad = ctx.createLinearGradient(paddle.x, paddle.y, paddle.x, paddle.y + paddle.h);
pGrad.addColorStop(0, '#6ef0e8');
pGrad.addColorStop(0.5, '#4ecdc4');
pGrad.addColorStop(1, '#2ea89e');
ctx.fillStyle = pGrad;
ctx.beginPath();
ctx.roundRect(paddle.x, paddle.y, paddle.w, paddle.h, 8);
ctx.fill();
// Блик на платформе
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.beginPath();
ctx.roundRect(paddle.x + 8, paddle.y + 3, paddle.w - 16, 5, 3);
ctx.fill();
// --- Мяч (3D-шарик) ---
ctx.shadowColor = '#f7d44a';
ctx.shadowBlur = 25;
const grad3 = ctx.createRadialGradient(
ball.x - 3, ball.y - 4, 1,
ball.x, ball.y, ball.r + 2
);
grad3.addColorStop(0, '#fff9e6');
grad3.addColorStop(0.3, '#f7d44a');
grad3.addColorStop(0.8, '#e8a317');
grad3.addColorStop(1, '#b8860b');
ctx.fillStyle = grad3;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fill();
// Блик на мяче
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.beginPath();
ctx.arc(ball.x - 2.5, ball.y - 3, 2.2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.beginPath();
ctx.arc(ball.x - 1, ball.y - 5, 1, 0, Math.PI * 2);
ctx.fill();
// --- Подсказка "Нажми пробел" ---
if (!gameStarted && gameRunning) {
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.font = '20px Arial';
ctx.textAlign = 'center';
ctx.fillText('🔄 Нажми ПРОБЕЛ или кликни', W/2, H/2 + 10);
ctx.fillStyle = 'rgba(255,255,255,0.06)';
ctx.font = '14px Arial';
ctx.fillText('чтобы запустить мяч', W/2, H/2 + 40);
}
}
// ---------- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ----------
function updateScore() {
scoreSpan.textContent = score;
}
function updateLives() {
livesSpan.textContent = lives;
}
function resetGame() {
score = 0;
lives = 3;
gameRunning = true;
gameStarted = false;
updateScore();
updateLives();
initBricks();
paddle.x = (W - paddle.w) / 2;
resetBall();
if (animId) cancelAnimationFrame(animId);
gameLoop();
}
// ---------- ИГРОВОЙ ЦИКЛ ----------
function gameLoop() {
update();
draw();
animId = requestAnimationFrame(gameLoop);
}
// ---------- ПОЛИФИЛ roundRect ----------
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
if (r > w/2) r = w/2;
if (r > h/2) r = h/2;
this.moveTo(x + r, y);
this.lineTo(x + w - r, y);
this.quadraticCurveTo(x + w, y, x + w, y + r);
this.lineTo(x + w, y + h - r);
this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
this.lineTo(x + r, y + h);
this.quadraticCurveTo(x, y + h, x, y + h - r);
this.lineTo(x, y + r);
this.quadraticCurveTo(x, y, x + r, y);
this.closePath();
return this;
};
}
// ---------- ОБРАБОТЧИКИ ----------
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'Left' || e.key === 'a' || e.key === 'A') {
keys.left = true;
e.preventDefault();
} else if (e.key === 'ArrowRight' || e.key === 'Right' || e.key === 'd' || e.key === 'D') {
keys.right = true;
e.preventDefault();
} else if (e.key === ' ' || e.key === 'Space') {
e.preventDefault();
if (!gameStarted && gameRunning) startBall();
}
});
document.addEventListener('keyup', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'Left' || e.key === 'a' || e.key === 'A') {
keys.left = false;
e.preventDefault();
} else if (e.key === 'ArrowRight' || e.key === 'Right' || e.key === 'd' || e.key === 'D') {
keys.right = false;
e.preventDefault();
}
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const mouseX = (e.clientX - rect.left) * scaleX;
paddle.x = Math.min(Math.max(mouseX - paddle.w/2, 0), W - paddle.w);
});
canvas.addEventListener('click', () => {
if (!gameStarted && gameRunning) startBall();
});
document.getElementById('restartButton').addEventListener('click', resetGame);
// ---------- СТАРТ ----------
initBricks();
paddle.x = (W - paddle.w) / 2;
resetBall();
gameLoop();
})();
</script>
</body>
</html>Game Source: 3D Арканоид
Creator: LuckyGlider65
Libraries: none
Complexity: complex (536 lines, 22.7 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: 3d-luckyglider65" to link back to the original. Then publish at arcadelab.ai/publish.