ロゴシュミグラム

全体のソースコード

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3Dテトリス</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
width: 100%;
border-radius: 8px;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.162.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.162.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
const canvasWidth = 400;
const canvasHeight = 500;
const widthLength = 3;
const heightLength = 4;
const blockSideLength = 3;
const fullHeightLength = heightLength + blockSideLength;
const dropInterval = 1000;
let gameState = 'initial';
let block;
let squares;
let timeoutId;
const backgroundColor = 0x1A202C;
const gridColor = 0x888888;
const topLineColor = 0xff5b00;
const blockMaterial = new THREE.MeshLambertMaterial({ color: 0x00ff00 });
const fixedBlockMaterial = new THREE.MeshLambertMaterial({ color: 0x00c3ff, transparent: true, opacity: 0.5 });
const completeMaterial = new THREE.MeshLambertMaterial({ color: 0xFFD700, transparent: true, opacity: 0.5 });
const overflowMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 });
const lineMaterial = new THREE.LineBasicMaterial({ color: gridColor });
const wallMaterial = new THREE.MeshLambertMaterial({ color: 0xaaaaaa, transparent: true, opacity: 0.5, side: THREE.DoubleSide });
const squareGeometry = new THREE.BoxGeometry(1, 1, 1);
let renderer;
let scene;
let camera;
let controls;
const createRenderer = () => {
const canvas = document.getElementById('three-canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
canvas.style.maxWidth = canvasWidth + 'px';
renderer = new THREE.WebGLRenderer({ canvas: canvas });
};
const createScene = () => {
scene = new THREE.Scene();
scene.background = new THREE.Color(backgroundColor);
};
const createCamera = () => {
camera = new THREE.PerspectiveCamera(5, canvasWidth / canvasHeight);
camera.position.set(widthLength * 12, fullHeightLength * 6, widthLength * 25);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.4;
controls.target = new THREE.Vector3(widthLength / 2, fullHeightLength / 2, widthLength / 2);
};
const addLight = () => {
const light = new THREE.DirectionalLight(0xFFFFFF, 10);
light.position.set(widthLength, fullHeightLength * 10, widthLength * 4);
scene.add(light);
};
const adGrids = () => {
const bottomGrid = new THREE.GridHelper(widthLength, widthLength, gridColor, gridColor);
bottomGrid.position.set(widthLength / 2, 0, widthLength / 2);
scene.add(bottomGrid);
const topGrid = new THREE.GridHelper(widthLength, 1, topLineColor, topLineColor);
topGrid.position.set(widthLength / 2, heightLength, widthLength / 2);
scene.add(topGrid);
};
const addVerticalLines = () => {
const lineGeometries = [];
for (let i = 0; i <= widthLength; i++) {
const xlineGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(i, 0, 0),
new THREE.Vector3(i, heightLength, 0),
]);
lineGeometries.push(xlineGeometry);
const zlineGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, i),
new THREE.Vector3(0, heightLength, i),
]);
lineGeometries.push(zlineGeometry);
}
const linesGeometry = mergeGeometries(lineGeometries);
const lines = new THREE.LineSegments(linesGeometry, lineMaterial);
scene.add(lines);
};
const addWall = () => {
const bottomWallGeometry = new THREE.PlaneGeometry(widthLength, widthLength);
bottomWallGeometry.rotateX(90 * Math.PI / 180);
bottomWallGeometry.translate(widthLength / 2, 0, widthLength / 2);
const backWallGeometry = new THREE.PlaneGeometry(widthLength, heightLength);
backWallGeometry.translate(widthLength / 2, heightLength / 2, 0);
const leftWallGeometry = new THREE.PlaneGeometry(widthLength, heightLength);
leftWallGeometry.rotateY(90 * Math.PI / 180);
leftWallGeometry.translate(0, heightLength / 2, widthLength / 2);
const wallGeometry = mergeGeometries([bottomWallGeometry, backWallGeometry, leftWallGeometry]);
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
scene.add(wall);
};
const animate = () => {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
};
const init3D = () => {
createRenderer();
createScene();
createCamera();
addLight();
addWall();
adGrids();
addVerticalLines();
animate();
};
const blockShapeList = [
[
[
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
],
[
[0, 0, 0],
[0, 1, 1],
[0, 1, 0],
],
[
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
],
],
[
[
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
],
[
[1, 1, 0],
[1, 1, 0],
[0, 0, 0],
],
[
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
],
],
];
const initSquares = () => {
squares = [];
for (let y = 0; y < fullHeightLength; y++) {
squares[y] = [];
for (let z = 0; z < widthLength; z++) {
squares[y][z] = [];
for (let x = 0; x < widthLength; x++) {
const square = new THREE.Mesh(squareGeometry, fixedBlockMaterial);
square.position.set(x + 0.5, y + 0.5, z + 0.5);
square.visible = false;
scene.add(square);
squares[y][z][x] = square;
}
}
}
};
const convertCoordinate = (coordinate, position) => {
return {
x: coordinate.x + position.x,
y: coordinate.y + position.y,
z: coordinate.z + position.z
};
};
const initBlock = () => {
block = {
coordinates: [],
position: { x: 0, y: heightLength, z: 0 }
};
const randomIndex = Math.floor(Math.random() * blockShapeList.length);
const blockShape = blockShapeList[randomIndex];
for (let y = 0; y < blockSideLength; y++) {
for (let z = 0; z < blockSideLength; z++) {
for (let x = 0; x < blockSideLength; x++) {
if (blockShape[y][z][x]) {
block.coordinates.push({ x, y, z });
}
}
}
};
displayBlock(block);
};
const displayBlock = (block) => {
block.coordinates.forEach((coordinate) => {
const { x, y, z } = convertCoordinate(coordinate, block.position);
squares[y][z][x].visible = true;
squares[y][z][x].material = blockMaterial;
});
};
const hideBlock = (block) => {
block.coordinates.forEach((coordinate) => {
const { x, y, z } = convertCoordinate(coordinate, block.position);
squares[y][z][x].visible = false;
squares[y][z][x].material = fixedBlockMaterial;
});
};
const replaceBlock = (newBlock) => {
hideBlock(block);
displayBlock(newBlock);
block = newBlock;
};
const moveBlock = (event) => {
const movedBlock = structuredClone(block);
switch (event.target.value) {
case 'left': movedBlock.position.x += -1;
break;
case 'right': movedBlock.position.x += 1;
break;
case 'front': movedBlock.position.z += 1;
break;
case 'back': movedBlock.position.z += -1;
break;
}
if (!checkCollision(movedBlock)) {
replaceBlock(movedBlock);
}
};
const checkCollision = (block) => {
const isMobable = block.coordinates.some((coordinate) => {
const { x, y, z } = convertCoordinate(coordinate, block.position);
return checkWallCollision(x, y, z) || checkBlockCollision(x, y, z);
});
return isMobable;
};
const checkWallCollision = (x, y, z) => {
return x < 0 || x >= widthLength || z < 0 || z >= widthLength || y < 0;
};
const checkBlockCollision = (x, y, z) => {
return squares[y][z][x].visible && squares[y][z][x].material === fixedBlockMaterial;
};
const rotateBlock = (event) => {
const rotatedBlock = structuredClone(block);
const tmp = blockSideLength - 1;
rotatedBlock.coordinates = block.coordinates.map((coordinate) => {
let rotatedX =
event.target.value === 'left' ? Math.abs(coordinate.z - tmp)
: event.target.value === 'right' ? coordinate.z : coordinate.x;
let rotatedY =
event.target.value === 'front' ? Math.abs(coordinate.z - tmp)
: event.target.value === 'back' ? coordinate.z : coordinate.y;
let rotatedZ =
event.target.value === 'back' ? Math.abs(coordinate.y - tmp)
: event.target.value === 'right' ? Math.abs(coordinate.x - tmp)
: event.target.value === 'left' ? coordinate.x : coordinate.y;
return { x: rotatedX, y: rotatedY, z: rotatedZ };
});
if (!checkCollision(rotatedBlock)) {
replaceBlock(rotatedBlock);
}
};
document.querySelectorAll('.move-button').forEach((button) => {
button.addEventListener('click', moveBlock);
});
document.querySelectorAll('.rotate-button').forEach((button) => {
button.addEventListener('click', rotateBlock);
});
const dropBlock = () => {
timeoutId = setTimeout(dropBlock, dropInterval);
const droppedBlock = structuredClone(block);
droppedBlock.position.y--;
if (checkCollision(droppedBlock)) {
if (checkGameover()) {
gameover();
} else {
changeBlockMaterial(fixedBlockMaterial);
const completedLayerIndexes = findCompleteLayers();
if (completedLayerIndexes.length !== 0) {
hilightAndRemoveLayers(completedLayerIndexes);
} else {
initBlock();
}
}
} else {
replaceBlock(droppedBlock);
};
};
const changeBlockMaterial = (material) => {
block.coordinates.forEach((coordinate) => {
const { x, y, z } = convertCoordinate(coordinate, block.position);
squares[y][z][x].material = material;
});
};
const checkGameover = () => {
const isGameover = block.coordinates.some((coordinate) => {
const { y } = convertCoordinate(coordinate, block.position);
return y >= heightLength;
});
return isGameover;
};
const gameover = () => {
changeBlockMaterial(overflowMaterial);
gameState = "gameover";
clearTimeout(timeoutId);
};
const findCompleteLayers = () => {
const completedLayerIndexes = [];
for (let y = 0; y < heightLength; y++) {
if (checkCompleteLayer(y)) {
completedLayerIndexes.push(y);
};
}
return completedLayerIndexes;
};
const checkCompleteLayer = (y) => {
for (let z = 0; z < widthLength; z++) {
for (let x = 0; x < widthLength; x++) {
if (!squares[y][z][x].visible) {
return false;
}
}
}
return true;
};
const highlightLayers = (completedLayerIndexes) => {
completedLayerIndexes.forEach((y) => {
for (let z = 0; z < widthLength; z++) {
for (let x = 0; x < widthLength; x++) {
squares[y][z][x].material = completeMaterial;
}
}
});
};
const removeLayers = (completedLayerIndexes) => {
completedLayerIndexes.reverse().forEach((removingY) => {
for (let y = removingY; y < heightLength; y++) {
for (let z = 0; z < widthLength; z++) {
for (let x = 0; x < widthLength; x++) {
squares[y][z][x].material = fixedBlockMaterial;
squares[y][z][x].visible = squares[y + 1][z][x].visible;
}
}
}
});
};
const hilightAndRemoveLayers = (completedLayerIndexes) => {
clearTimeout(timeoutId);
highlightLayers(completedLayerIndexes);
setTimeout(() => {
removeLayers(completedLayerIndexes);
initBlock();
timeoutId = setTimeout(dropBlock, dropInterval);
}, dropInterval);
};
const start = () => {
if (gameState === 'initial' || gameState === 'stop') {
dropBlock();
gameState = 'running';
}
};
const stop = () => {
if (gameState === 'running') {
clearTimeout(timeoutId);
gameState = 'stop';
}
};
const reset = () => {
if (!(gameState === 'initial')) {
clearTimeout(timeoutId);
resetSquares();
initBlock();
gameState = 'initial';
}
};
const resetSquares = () => {
for (let y = 0; y < fullHeightLength; y++) {
for (let z = 0; z < widthLength; z++) {
for (let x = 0; x < widthLength; x++) {
squares[y][z][x].material = fixedBlockMaterial;
squares[y][z][x].visible = false;
}
}
}
};
document.getElementById('start-button').addEventListener('click', start);
document.getElementById('stop-button').addEventListener('click', stop);
document.getElementById('reset-button').addEventListener('click', reset);
init3D();
initSquares();
initBlock();
</script>
</head>
<body>
<canvas id="three-canvas"></canvas>
<div>
<button id="start-button">スタート</button>
<button id="stop-button">ストップ</button>
<button id="reset-button">リセット</button>
</div>
<div>
<div>移動</div>
<button class="move-button" value="left">←</button>
<button class="move-button" value="back">↑</button>
<button class="move-button" value="front">↓</button>
<button class="move-button" value="right">→</button>
</div>
<div>
<div>回転</div>
<button class="rotate-button" value="left">←</button>
<button class="rotate-button" value="back">↑</button>
<button class="rotate-button" value="front">↓</button>
<button class="rotate-button" value="right">→</button>
</div>
</body>
</html>