シンプルなHTMLとJavaScriptを使って作成されたテトリスゲームに、効果音を追加してみると、ゲームの面白さや臨場感が一気に増しますよ!今回は、バージョン1.7.0から1.8.8へのアップデートにおいて、どのように効果音が実装されたのか、コード例を交えて丁寧に解説いたします。初心者の方でも簡単に理解できるように、ステップバイステップで説明するので、ぜひ最後まで読んでご自身のゲームにも効果音を実装してみてくださいね!一緒に楽しいテトリスを作っていきましょう♪
PCブラウザ版での操作方法はこれさ↓
Qキーは左回り、Wキーは右回りでブロックが回るよ。
左右の矢印キーでブロックを左右に動かせるよ!
下矢印キーはブロックが下に落ちます~。
最初に「START」ボタンを押してね~。
スマホ版はゲーム下のボタンで操作してください!
テトリスゲーム ver1.7.0 の基本機能
トリスのver1.7.0では、ゲームの基礎的な部分がちゃんとできているんですよ!このバージョンでは、ゲーム画面や操作の基本がすごくしっかりしていて、楽しく遊べる状態になっています。
まず、HTMLとCSSを使って、テトリスの画面が作られていますよ。ゲームでブロックが落ちてくる盤面は<canvas>
というものを使って描かれていて、その中でブロックが動くんです。それに、スコアが表示されたり、「スタート」ボタンを押すとゲームが始まったり、次に出てくるブロック(Next Piece)もちゃんと見られるようになっています。これでプレイヤーが遊びやすいように工夫されているんですね。
次に、JavaScriptという魔法みたいなプログラムで、ゲームの中のいろんな動きをコントロールしています。ブロックを左右に動かしたり、くるっと回転させたり、下にどんどん落ちるようにしたりするのは全部JavaScriptのおかげなんです!そして、ブロックが止まったり、ラインが揃ったときに消えてスコアが加算されたり、ゲームオーバーになったらちゃんと終わる、そういう大事な部分も全部JavaScriptが頑張ってくれているんですよ。
そして、もうひとつ大事な要素としてBGM(バックグラウンドミュージック)が流れるようになっています!<audio>
というタグを使って、ゲーム中ずっと音楽がかかっているんです。ゲームが始まるときに音楽が流れて、終わると音楽も止まるので、ゲームの世界に入り込んだような気分で遊べちゃいます。これで、ゲームの楽しさがもっともっとアップするんですよ。
このver1.7.0では、テトリスの基本的な遊びの仕組みがちゃんと整っていて、音楽まで入っているので、すでに十分楽しめるんですね。でも、次のバージョンでさらに面白い効果音が追加されて、もっとワクワクする体験ができるようになるんです!それはまた次の説明でお楽しみに♪
ゲーム画面の構成
テトリスゲームの画面を作るためには、HTMLとCSSが大活躍しますよ!このゲームの見た目は、シンプルだけどとても重要なんです。ちゃんと遊べるゲームにするために、いくつかの要素が組み合わさっているので、一緒に見ていきましょうね。
まず、ゲームの中心になるのは、<canvas>
というタグを使ったキャンバスです。このキャンバスは、プレイヤーが操作するテトリミノ(あの、いろんな形のブロックですね)を表示するための場所です。ブロックがくるくる回ったり、下に落ちてくる動きは、全部このキャンバスの中で描かれていますよ。そして、次に登場するブロック、いわゆる「Next Piece」もこのキャンバスで表示されるので、プレイヤーが次の戦略を立てやすくなっているんです。
それだけじゃなくて、スコアを表示するエリアも用意されていますよ。プレイヤーがゲーム中に得点をどれだけ稼いだかがすぐに分かるように、「Score: 0」とか「Score: 1500」みたいに、数字がリアルタイムで表示されます。これで、得点を見ながらもっと頑張ろうっていう気持ちになりますね!
そして、忘れちゃいけないのが「スタートボタン」!このボタンを押すとゲームが始まるんです。テトリスをプレイする人が直感的に操作できるように、スタートボタンや操作ボタンが画面上に配置されています。例えば、スマホやタブレットで遊ぶ場合は、タッチパネルで操作することができるようになっていて、プレイヤーが指でブロックを動かす感じなんです。
画面の構成は、次のようなHTMLコードで表されていますよ:
<div id="gameContainer">
<canvas id="gameCanvas" width="300" height="600"></canvas>
<div id="nextPiece">Next Piece</div>
<div id="score">Score: 0</div>
<button id="startButton">Start Game</button>
</div>
JavaScriptによるゲームロジック
テトリスで遊ぶとき、ブロックをスムーズに動かせるのは、JavaScriptがいろいろな仕組みを裏で動かしているからなんです。ゲームが始まったらテトリミノ(ブロック)が出てきて、左右に動かしたり、回転させたり、ぐーっと下に落としたりするのが全部JavaScriptでコントロールされています。
ブロックがフィールドの下に行き着いたり、他のブロックに当たると「衝突判定」というのが働いて、そのブロックはそこでピタッと止まって、もう動かなくなるんです。これでどんどんブロックが積み上がっていくわけですね。
同じラインがいっぱいになると、そのラインが消えてスコアが加算されるようになっています。特にテトリス(4列一気に消すやつ)を成功させると、一気に得点が増えるから嬉しいですよね!JavaScriptはその瞬間に「おめでとう!」と言わんばかりにスコアを計算してくれるんですよ。
ブロックがどんどん積み上がって画面の上まで行っちゃうと、残念ながらゲームオーバー。ゲームが終わるタイミングもJavaScriptがしっかり見ていて、ちゃんと「もうここで終わりですよ」って教えてくれます。スコアが表示されたり、再スタートできるようにするのもJavaScriptの仕事なんです。
function movePiece(direction) {
// ブロックの左右移動や回転を制御
}
function checkCollision() {
// ブロックが壁や他のブロックにぶつかったかどうかをチェック
}
function clearLine() {
// ラインが揃ったときにそのラインを消す処理
}
function gameOver() {
// ゲームオーバー時の処理
}
このコードのおかげで、プレイヤーはブロックを自由に動かしたり、ラインを消したりできるんです。
BGMの実装
テトリスゲームに音楽が流れると、なんだかもっと楽しくなりますよね!このバージョン1.7.0では、<audio>
要素を使ってBGMを再生する機能が実装されています。ゲームをスタートした瞬間に音楽が流れて、プレイヤーをワクワクさせてくれます。ゲームの途中で音楽がずっと流れ続けるように、ループ再生もできるようになっているんです。
BGMの再生は、HTMLの<audio>
要素で行われます。これに音楽ファイルを指定しておくと、ゲームが始まると自動的に音楽が流れる仕組みなんですよ。そして、ゲームが終わったら音楽もちゃんと止まるように、JavaScriptで音楽の再生と停止を管理しているんです。例えば、ゲームのスタートボタンが押されたときに音楽がスタートし、ゲームオーバーになったらBGMがフェードアウトする、なんていう操作も簡単にできます。
次のようなコードでBGMを実装しています:
<audio id="bgm" src="bgm.mp3" loop></audio>
この<audio>
タグに音楽ファイル(ここではbgm.mp3
)を指定しておくと、JavaScriptで音楽の再生や停止が自由に制御できるようになります。ループ属性をつけておくことで、音楽が繰り返し流れ続ける設定も簡単にできます。
そして、JavaScriptではこんな風にBGMの再生をコントロールします:
const bgm = document.getElementById("bgm");
function startGame() {
bgm.play(); // ゲーム開始でBGM再生
}
function endGame() {
bgm.pause(); // ゲーム終了でBGMを停止
bgm.currentTime = 0; // 再生位置をリセット
}
これで、ゲームが始まると同時に音楽が流れ、ゲームが終わったら音楽も一緒に止まるという流れができあがります。ループ再生ができるので、ずっと音楽を楽しみながら遊ぶことができるんです。
このBGM機能があることで、プレイヤーはよりゲームに没入できるんですよ。
ver1.8.8 で追加された効果音
テトリスゲームのver1.8.8では、プレイ中の体験をさらに盛り上げるために効果音が追加されました。これにより、ゲームの動きがもっとリアルに感じられて、ブロックの回転や落下、ライン消去などが音で表現されるようになりました。このアップデートにより、ゲームの臨場感がぐっとアップし、まるでアーケード版のような楽しさを味わえます。
効果音があると、ただ画面を見るだけでなく、音を通じてプレイヤーの感覚が刺激され、ブロックの動きが一層ダイナミックに感じられます。例えば、テトリミノが回転するときに「カチッ」という音が鳴ったり、ブロックが画面の下まで落ちると「ドンッ」という音が鳴ったりします。ラインが消えるときの「パリンッ」という音も、プレイヤーにとって達成感を感じさせるポイントになっています。
テトリスといえば、4ラインを一気に消す「テトリス」が大きな目標ですよね。このときの効果音も特別で、成功した瞬間に「ジャーン!」というような音が鳴り、達成感がより一層大きくなります。こうした音の演出が加わることで、ゲームの流れや動きがはっきりとわかりやすくなり、プレイヤーにとって操作感が良くなるんです。
効果音を追加することで、ゲーム自体の雰囲気がより賑やかになり、プレイヤーを引き込む力が強まります。ただブロックを動かしているだけでも、音があるとまるで自分がゲームの中に入り込んでいるような感覚を楽しむことができます。
ver1.8.8での効果音の追加は、ゲーム体験における大きなステップアップでした。効果音のおかげで、プレイヤーは音で動きを感じながらゲームを進めることができ、次々にブロックを消していく楽しさが倍増します。このアップデートは、テトリスがただのレトロゲームから、一段と臨場感あふれるゲーム体験へと進化させる重要なポイントでした。
H3: 効果音の種類
- ブロック回転音
- ブロック落下音
- ライン消去音
- テトリス達成音
H3: 効果音の実装方法
- JavaScriptのAudioオブジェクトを利用
- 各イベント発生時にplaySound関数を呼び出して効果音を再生
- 効果音ファイル(mp3)へのパスを指定
H3: 効果音の音量調整
- BGMと効果音の音量バランスを調整
- Audioオブジェクトのvolumeプロパティで音量を制御
H2: 効果音実装のコード解説
H3: playSound関数の定義
- Audioオブジェクトを生成し、src属性で効果音ファイルを指定
- volumeプロパティで音量を設定
- play()メソッドで効果音を再生
H3: 各イベントへのplaySound関数の組み込み
- ブロック回転時、落下時、ライン消去時などにplaySound関数を呼び出す
- イベント発生時に適切な効果音を再生
H3: 効果音ファイルの配置
- 効果音ファイル(mp3)を適切な場所に配置
- HTMLファイルから相対パスでアクセスできるようにする
H2: まとめ
HTMLとJavaScriptで作成されたシンプルなテトリスゲームに、効果音を追加することで、ゲーム体験をより豊かにすることができます。ver1.8.8では、AudioオブジェクトとplaySound関数を利用することで、回転音、落下音、ライン消去音、テトリス達成音など、様々な効果音が実装されました。
この記事で紹介したコードを参考に、ぜひご自身のテトリスゲームにも効果音を実装してみてください。
tついにここまできましたね。効果音が付けばもう完璧なテトリスの完成です!
どんな方法で実現してるかよくみてね!
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>テトリスゲーム Ver1.8.8</title>
<style>
/* 既存のCSSはそのまま */
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
background-image: url('https://tokodomo.xyz/wp-content/uploads/2024/09/tetris_ver1.8_background.webp');
background-size: cover;
background-position: center;
}
#gameContainer {
position: relative;
width: 100%;
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.3);
z-index: 5;
}
#game {
background-color: #000;
}
#scoreContainer {
font-size: 20px;
background-color: rgba(255, 255, 255, 0.7);
padding: 10px;
position: fixed;
top: 0;
width: 100%;
display: flex;
justify-content: space-between;
box-sizing: border-box;
z-index: 10;
}
#nextPieceContainer {
position: absolute;
top: 100px;
right: 80px;
text-align: center;
}
#nextPiece {
width: 60px;
height: 60px;
background-color: rgba(255, 255, 255, 0.7);
margin-bottom: -5px;
}
#nextLabel {
font-size: 14px;
color: white;
letter-spacing: 2px;
text-transform: uppercase;
font-weight: bold;
}
#finalScore {
font-size: 30px;
color: red;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.7);
padding: 10px;
display: none;
width: 100%;
text-align: center;
}
#controls {
position: fixed;
bottom: 0;
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
box-sizing: border-box;
z-index: 10;
background-color: rgba(0, 0, 0, 0.5);
}
.control-button {
width: 60px;
height: 60px;
background-color: rgba(255, 255, 255, 0.7);
border: none;
border-radius: 50%;
font-size: 20px;
font-weight: bold;
color: black;
display: flex;
justify-content: center;
align-items: center;
}
#moveControl {
width: 140px;
display: flex;
justify-content: space-between;
}
.connected-button {
border-radius: 0;
flex-grow: 1;
margin: 0;
}
.connected-button:first-child {
border-top-left-radius: 50%;
border-bottom-left-radius: 50%;
}
.connected-button:last-child {
border-top-right-radius: 50%;
border-bottom-right-radius: 50%;
}
@media (min-width: 601px) {
#controls {
display: none;
}
}
</style>
</head>
<body>
<div id="scoreContainer">
<div id="score">Score: 0</div>
<button id="startButton" onclick="startGame()">START</button>
</div>
<div id="gameContainer">
<canvas id="game" width="240" height="400"></canvas>
<div id="finalScore">GAME OVER<br>SCORE: 0</div>
<div id="nextPieceContainer">
<canvas id="nextPiece" width="60" height="60"></canvas>
<div id="nextLabel">Next</div>
</div>
</div>
<div id="controls">
<div id="moveControl">
<button class="control-button connected-button" id="leftButton">←</button>
<button class="control-button connected-button" id="rightButton">→</button>
</div>
<button class="control-button" id="rotateButton">↻</button>
<button class="control-button" id="dropButton">⬇</button>
</div>
<!-- BGM用のオーディオ要素 -->
<audio id="bgm" loop>
<source src="https://tokodomo.xyz/wp-content/uploads/2024/09/Fall-of-the-Blocks.mp3" type="audio/mpeg">
</audio>
<script>
// 効果音を再生する関数(リアルタイムに対応)
function playSound(src) {
const sound = new Audio(src);
sound.volume = 0.8; // 効果音のボリュームを上げる
sound.play();
}
const bgm = document.getElementById('bgm');
bgm.volume = 0.3; // BGMのボリュームを下げる
const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
const nextCanvas = document.getElementById('nextPiece');
const nextContext = nextCanvas.getContext('2d');
const scale = 20;
context.scale(scale, scale);
nextContext.scale(10, 10);
const arena = createMatrix(12, 20);
const player = {
pos: {x: 0, y: 0},
matrix: null,
score: 0,
nextPiece: null,
};
const colors = [
null,
'#FF0D72',
'#0DC2FF',
'#0DFF72',
'#F538FF',
'#FF8E0D',
'#FFE138',
'#3877FF',
];
let gameOver = false;
let manualDrop = false; // 手動ドロップを判定するフラグ
let dropStart = false;
let dropSpeed = 50;
let linesCleared = 0; // 消去されたライン数
let level = 1; // 初期レベル
let dropInterval = 1000; // 初期の落下速度(1秒)
// スコアに基づいてレベルを更新する関数
function updateLevel() {
level = Math.floor(player.score / 100) + 1;
dropInterval = Math.max(1000 - (level * 100), 100); // レベルが上がるごとに落下速度が速くなる
}
function createMatrix(w, h) {
const matrix = [];
while (h--) {
matrix.push(new Array(w).fill(0));
}
return matrix;
}
function createPiece(type) {
if (type === 'T') {
return [
[0, 0, 0],
[1, 1, 1],
[0, 1, 0],
];
} else if (type === 'O') {
return [
[2, 2],
[2, 2],
];
} else if (type === 'L') {
return [
[0, 3, 0],
[0, 3, 0],
[0, 3, 3],
];
} else if (type === 'J') {
return [
[0, 4, 0],
[0, 4, 0],
[4, 4, 0],
];
} else if (type === 'I') {
return [
[0, 5, 0, 0],
[0, 5, 0, 0],
[0, 5, 0, 0],
[0, 5, 0, 0],
];
} else if (type === 'S') {
return [
[0, 6, 6],
[6, 6, 0],
[0, 0, 0],
];
} else if (type === 'Z') {
return [
[7, 7, 0],
[0, 7, 7],
[0, 0, 0],
];
}
}
function drawMatrix(matrix, offset, context) {
matrix.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0) {
context.fillStyle = colors[value];
context.fillRect(x + offset.x, y + offset.y, 1, 1);
}
});
});
}
function draw() {
context.fillStyle = '#000';
context.fillRect(0, 0, canvas.width / scale, canvas.height / scale);
const xOffset = (canvas.width / scale - arena[0].length) / 2;
drawMatrix(arena, {x: xOffset, y: 0}, context);
drawMatrix(player.matrix, {x: player.pos.x + xOffset, y: player.pos.y}, context);
nextContext.fillStyle = '#000';
nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
const nextPieceOffset = calculateCenterOffset(player.nextPiece);
drawMatrix(player.nextPiece, nextPieceOffset, nextContext);
}
function calculateCenterOffset(matrix) {
const matrixWidth = matrix[0].length;
const matrixHeight = matrix.length;
const canvasSize = nextCanvas.width / 10;
const offsetX = (canvasSize - matrixWidth) / 2;
const offsetY = (canvasSize - matrixHeight) / 2;
return {x: offsetX, y: offsetY};
}
function merge(arena, player) {
player.matrix.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0) {
arena[y + player.pos.y][x + player.pos.x] = value;
}
});
});
}
function rotate(matrix, dir) {
for (let y = 0; y < matrix.length; ++y) {
for (let x = 0; x < y; ++x) {
[matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
}
}
if (dir > 0) {
matrix.forEach(row => row.reverse());
} else {
matrix.reverse();
}
}
function playerDrop() {
player.pos.y++;
if (collide(arena, player)) {
player.pos.y--;
merge(arena, player);
playerReset();
arenaSweep();
updateScore();
updateLevel(); // レベルを更新
if (collide(arena, player)) {
gameOver = true;
displayFinalScore();
}
if (manualDrop) {
playSound("https://tokodomo.xyz/wp-content/uploads/2024/09/drop.mp3");
manualDrop = false; // フラグをリセット
}
}
dropCounter = 0;
}
function playerMove(offset) {
player.pos.x += offset;
if (collide(arena, player)) {
player.pos.x -= offset;
}
}
function playerReset() {
if (player.nextPiece === null) {
player.nextPiece = createPiece('TJLOSZI'[Math.random() * 7 | 0]);
}
player.matrix = player.nextPiece;
player.nextPiece = createPiece('TJLOSZI'[Math.random() * 7 | 0]);
player.pos.y = 0;
player.pos.x = (arena[0].length / 2 | 0) - (player.matrix[0].length / 2 | 0);
if (collide(arena, player)) {
gameOver = true;
displayFinalScore();
}
}
function playerRotate(dir) {
const pos = player.pos.x;
let offset = 1;
rotate(player.matrix, dir);
while (collide(arena, player)) {
player.pos.x += offset;
offset = -(offset + (offset > 0 ? 1 : -1));
if (offset > player.matrix[0].length) {
rotate(player.matrix, -dir);
player.pos.x = pos;
return;
}
}
playSound("https://tokodomo.xyz/wp-content/uploads/2024/09/rotate.mp3"); // 回転時の効果音
}
function collide(arena, player) {
const [m, o] = [player.matrix, player.pos];
for (let y = 0; y < m.length; ++y) {
for (let x = 0; x < m[y].length; ++x) {
if (m[y][x] !== 0 &&
(arena[y + o.y] &&
arena[y + o.y][x + o.x]) !== 0) {
return true;
}
}
}
return false;
}
function arenaSweep() {
let lines = 0;
outer: for (let y = arena.length - 1; y > 0; --y) {
for (let x = 0; x < arena[y].length; ++x) {
if (arena[y][x] === 0) {
continue outer;
}
}
const row = arena.splice(y, 1)[0].fill(0);
arena.unshift(row);
++y;
player.score += 10 * level;
lines++;
}
if (lines > 0) {
if (lines === 4) {
playSound("https://tokodomo.xyz/wp-content/uploads/2024/09/tetris.mp3");
} else {
playSound("https://tokodomo.xyz/wp-content/uploads/2024/09/line-clear.mp3");
}
}
linesCleared += lines;
}
let dropCounter = 0;
let lastTime = 0;
function update(time = 0) {
if (!gameOver) {
const deltaTime = time - lastTime;
dropCounter += deltaTime;
if (dropCounter > (dropStart ? dropSpeed : dropInterval)) {
playerDrop();
}
lastTime = time;
draw();
requestAnimationFrame(update);
}
}
function updateScore() {
document.getElementById('score').innerText = `Score: ${player.score}`;
document.getElementById('finalScore').innerText = `GAME OVER\nSCORE: ${player.score}`;
}
document.addEventListener('keydown', event => {
if (event.keyCode === 37) {
playerMove(-1);
} else if (event.keyCode === 39) {
playerMove(1);
} else if (event.keyCode === 40) {
event.preventDefault();
manualDrop = true;
playerDrop();
} else if (event.keyCode === 81) {
playerRotate(-1);
} else if (event.keyCode === 87) {
playerRotate(1);
}
});
document.getElementById('leftButton').addEventListener('click', () => playerMove(-1));
document.getElementById('rightButton').addEventListener('click', () => playerMove(1));
document.getElementById('rotateButton').addEventListener('click', () => playerRotate(1));
document.getElementById('dropButton').addEventListener('mousedown', (event) => {
event.preventDefault();
manualDrop = true;
dropStart = true;
});
document.getElementById('dropButton').addEventListener('mouseup', (event) => {
event.preventDefault();
dropStart = false;
});
document.getElementById('dropButton').addEventListener('touchstart', (event) => {
event.preventDefault();
manualDrop = true;
dropStart = true;
});
document.getElementById('dropButton').addEventListener('touchend', (event) => {
event.preventDefault();
dropStart = false;
});
function startGame() {
arena.forEach(row => row.fill(0));
player.score = 0;
gameOver = false;
document.getElementById('finalScore').style.display = "none";
playerReset();
updateScore();
update();
bgm.play();
}
function displayFinalScore() {
document.getElementById('finalScore').style.display = "block";
document.getElementById('finalScore').innerText = `GAME OVER\nSCORE: ${player.score}`;
bgm.pause();
bgm.currentTime = 0;
}
playerReset();
updateScore();
</script>
</body>
</html>
コメント