こんにちはyukiです。
今回は、JavaScriptでテトリスを作りましたので、記録しておきます。
(ネットの記事を結構参考にしました。一部動かなかったのでそこはなんとか頑張りました。)
今後、C#などでも作りたい。。。
テトリスのリンクはこちら。
※PC用です。
Contents
ソースコード
ソースコードも置いておきます。
HTML
<!DOCTYPE html>
<html lang="jan">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="/tetris.css"
/>
<title>テトリス</title>
</head>
<body>
<div id="container">
<canvas id="main-canvas"></canvas>
<canvas id="next-canvas"></canvas>
<img
id="img-howto"
src="/tetris/images/howto.png"
alt=""
/>
<button id="start-btn">開始</button>
</div>
<script src="/tetris/tetris.js"></script>
</body>
</html>
CSS
#container {
position: relative;
margin: auto;
width: 700px;
height: 600px;
}
#main-canvas {
position: absolute;
top: 0;
left: 0;
width: 360px;
height: 600px;
}
#next-canvas {
position: absolute;
top: 0;
left: 400px;
}
#img-howto {
position: absolute;
bottom: 200px;
left: 380px;
}
#start-btn {
position: absolute;
left: 400px;
bottom: 0;
width: 180px;
text-align: center;
font-size: 16px;
color: #fff;
text-decoration: none;
font-weight: bold;
padding: 12px 24px;
border: none;
border-radius: 4px;
background-image: linear-gradient(to top, #c471f5 0%, #fa71cd 100%);
transition: 0.5s;
background-size: 200%;
}
#start-btn:hover {
background-position: right center;
}
JavaScript
const START_BTN_ID = "start-btn";
const MAIN_CANVAS_ID = "main-canvas";
const NEXT_CANVAS_ID = "next-canvas";
const GAME_SPEED = 500;
const BLOCK_SIZE = 32;
const COLS_COUNT = 10;
const ROWS_COUNT = 20;
const SCREEN_WIDTH = COLS_COUNT * BLOCK_SIZE;
const SCREEN_HEIGHT = ROWS_COUNT * BLOCK_SIZE;
const NEXT_AREA_SIZE = 160;
const BLOCK_SOURCE = [
"images/block-0.png",
"images/block-1.png",
"images/block-2.png",
"images/block-3.png",
"images/block-4.png",
"images/block-5.png",
"images/block-6.png",
];
window.onload = function () {
Asset.init(() => {
// initメソッドのコールバックを追加
let game = new Game(); // GamepadではなくGameクラスを生成
document.getElementById(START_BTN_ID).onclick = function () {
game.start();
this.blur();
};
});
};
// 素材を管理するクラス
// ゲーム開始前に初期化する
class Asset {
// ブロック用Imageの配列
static blockImage = [];
// 初期化処理
// callback には、init完了後に行う処理を渡す
static init(callback) {
let loadCnt = 0;
for (let i = 0; i <= 7; i++) {
// i <= 7 に修正
let img = new Image();
img.src = BLOCK_SOURCE[i];
img.onload = function () {
loadCnt++;
Asset.blockImage.push(img);
// 全ての画像読み込みが終われば、callback実行
if (loadCnt >= BLOCK_SOURCE.length && callback) {
callback();
}
};
}
}
}
class Game {
constructor() {
this.initMainCanvas();
this.initNextCanvas();
}
// メインキャンバスの初期化
initMainCanvas() {
this.mainCanvas = document.getElementById(MAIN_CANVAS_ID);
this.mainCtx = this.mainCanvas.getContext("2d");
this.mainCanvas.width = SCREEN_WIDTH;
this.mainCanvas.height = SCREEN_HEIGHT;
this.mainCanvas.style.border = "4px solid #555";
}
// ネクストキャンバスの初期化
initNextCanvas() {
this.nextCanvas = document.getElementById(NEXT_CANVAS_ID);
this.nextCtx = this.nextCanvas.getContext("2d");
this.nextCanvas.width = NEXT_AREA_SIZE;
this.nextCanvas.height = NEXT_AREA_SIZE;
this.nextCanvas.style.border = "4px solid #555";
}
// ゲーム開始の処理(STARTボタンクリック時)
start() {
// フィールドとミノを初期化
this.field = new Field();
// 最初のミノを読み込み
this.popMino();
// 初回描画
this.drawAll();
// 落下処理
clearInterval(this.timer);
this.timer = setInterval(() => this.dropMino(), 1000);
// キーボードのイベント登録
this.setKeyEvent();
}
// 新しいミノを読み込む
popMino() {
this.mino = this.nextMino || new Mino(); // もともとのコードに誤りがあったため修正
this.mino.spawn();
this.nextMino = new Mino();
// ゲームオーバー判定
if (!this.valid(0, 1)) {
this.drawAll();
clearInterval(this.timer); // clearIntervalのタイプミスを修正
alert("ゲームオーバー");
}
}
// 図形の描画
drawAll() {
// 表示クリア
this.mainCtx.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
this.nextCtx.clearRect(0, 0, NEXT_AREA_SIZE, NEXT_AREA_SIZE);
// 落下済みのミノを描画
this.field.drawFixedBlocks(this.mainCtx);
// 再描画
this.nextMino.drawNext(this.nextCtx);
this.mino.draw(this.mainCtx);
}
// ミノの落下処理
dropMino() {
if (this.valid(0, 1)) {
this.mino.y++;
} else {
// Minoを固定する(座標変換してFieldに渡す)
this.mino.blocks.forEach((e) => {
e.x += this.mino.x;
e.y += this.mino.y;
});
this.field.blocks = this.field.blocks.concat(this.mino.blocks);
this.field.checkLine();
this.popMino();
}
this.drawAll();
}
// 次の移動が可能かチェック
valid(moveX, moveY, rot = 0) {
let newBlocks = this.mino.getNewBlocks(moveX, moveY, rot);
return newBlocks.every((block) => {
return (
block.x >= 0 &&
block.y >= -1 &&
block.x < COLS_COUNT &&
block.y < ROWS_COUNT &&
!this.field.has(block.x, block.y)
);
});
}
// キーボードイベント
setKeyEvent() {
window.addEventListener("keydown", (e) => {
switch (e.keyCode) {
case 37: // 左
if (this.valid(-1, 0)) this.mino.x--;
break;
case 39: // 右
if (this.valid(1, 0)) this.mino.x++;
break;
case 40: // 下
if (this.valid(0, 1)) this.mino.y++;
break;
case 32: // スペースキー
// スペースキーが押されたときに回転させる
this.rotateMino();
break;
}
this.drawAll();
});
}
rotateMino() {
if (this.valid(0, 0, 1)) {
// 回転可能かチェック
this.mino.rotate(); // ミノを回転させる
this.drawAll(); // 画面を再描画
}
}
}
class Block {
// 基準地点からの座標
// 移動中 ⇒ Minoの左上
// 配置後 ⇒ Fieldの左上
constructor(x, y, type) {
this.x = x;
this.y = y;
// 描画しないときはタイプを固定しない
if (type >= 0) this.setType(type);
}
setType(type) {
this.type = type;
this.image = Asset.blockImage[type];
}
// Minoに属するときは、Minoの位置をオフセットに指定
// Fieldに属するときは、(0, 0)を起点とするので不要
draw(offsetX = 0, offsetY = 0, ctx) {
let drawX = this.x + offsetX;
let drawY = this.y + offsetY;
// 画面外は描画しない
if (drawX >= 0 && drawX < COLS_COUNT && drawY >= 0 && drawY < ROWS_COUNT) {
ctx.drawImage(
this.image,
drawX * BLOCK_SIZE,
drawY * BLOCK_SIZE,
BLOCK_SIZE,
BLOCK_SIZE
);
}
}
// 次のミノを描画する
// タイプごとに余白を調整して、中央に表示
drawNext(ctx) {
let offsetX = 0;
let offsetY = 0;
switch (this.type) {
case 0:
offsetX = 0.5;
offsetY = 0;
break;
case 1:
offsetX = 0.5;
offsetY = 0;
break;
default:
offsetX = 1;
offsetY = 0.5;
break;
}
ctx.drawImage(
this.image,
(this.x + offsetX) * BLOCK_SIZE,
(this.y + offsetY) * BLOCK_SIZE,
BLOCK_SIZE,
BLOCK_SIZE
);
}
}
class Mino {
constructor() {
this.type = Math.floor(Math.random() * 7); // Math.ramdom() ではなく Math.random()
this.initBlocks();
}
initBlocks() {
let t = this.type;
switch (t) {
case 0: // I型
this.blocks = [
new Block(0, 2, t),
new Block(1, 2, t),
new Block(2, 2, t),
new Block(3, 2, t),
];
break;
case 1: // O型
this.blocks = [
new Block(1, 1, t),
new Block(2, 1, t),
new Block(1, 2, t),
new Block(2, 2, t),
];
break;
case 2: // T型
this.blocks = [
new Block(1, 1, t),
new Block(0, 2, t),
new Block(1, 2, t),
new Block(2, 2, t),
];
break;
case 3: // J型
this.blocks = [
new Block(1, 1, t),
new Block(0, 2, t),
new Block(1, 2, t),
new Block(2, 2, t),
];
break;
case 4: // L型
this.blocks = [
new Block(2, 1, t),
new Block(0, 2, t),
new Block(1, 2, t),
new Block(2, 2, t),
];
break;
case 5: // S型
this.blocks = [
new Block(1, 1, t),
new Block(2, 1, t),
new Block(0, 2, t),
new Block(1, 2, t),
];
break;
case 6: // Z型
this.blocks = [
new Block(0, 1, t),
new Block(1, 1, t),
new Block(1, 2, t),
new Block(2, 2, t),
];
break;
}
}
// フィールドに生成する
spawn() {
this.x = Math.floor(COLS_COUNT / 2) - 2; // Math.floor を追加
this.y = -3;
}
// フィールドに描画する
draw(ctx) {
this.blocks.forEach((block) => {
block.draw(this.x, this.y, ctx);
});
}
// 次のミノを描画する
drawNext(ctx) {
this.blocks.forEach((block) => {
block.drawNext(ctx);
});
}
// ミノを回転させるメソッド
rotate() {
let centerX = 0;
let centerY = 0;
// ミノの中心座標を計算
this.blocks.forEach((block) => {
centerX += block.x;
centerY += block.y;
});
centerX /= this.blocks.length;
centerY /= this.blocks.length;
let newBlocks = this.blocks.map((block) => {
let relativeX = block.x - centerX;
let relativeY = block.y - centerY;
// 中心周りで回転
let rotatedX = Math.round(centerX - relativeY);
let rotatedY = Math.round(centerY + relativeX);
return new Block(rotatedX, rotatedY, this.type);
});
this.blocks = newBlocks; // ブロックの位置を更新
}
getNewBlocks(moveX, moveY, rot) {
let newBlocks = this.blocks.map((block) => {
return new Block(block.x, block.y);
});
newBlocks.forEach((block) => {
// 移動させる場合
if (moveX || moveY) {
block.x += moveX;
block.y += moveY;
}
// 回転させる場合
if (rot) {
let oldX = block.x;
block.x = block.y;
block.y = 3 - oldX;
}
// グローバル座標に変換
block.x += this.x;
block.y += this.y;
});
return newBlocks;
}
}
class Field {
constructor() {
this.blocks = [];
}
drawFixedBlocks(ctx) {
this.blocks.forEach((block) => block.draw(0, 0, ctx));
}
checkLine() {
for (var r = 0; r < ROWS_COUNT; r++) {
var c = this.blocks.filter((block) => block.y === r).length;
if (c === COLS_COUNT) {
this.blocks = this.blocks.filter((block) => block.y !== r);
this.blocks
.filter((block) => block.y < r)
.forEach((upper) => upper.y++);
}
}
}
has(x, y) {
return this.blocks.some((block) => block.x == x && block.y == y);
}
}