Mode7風 疑似3Dレーシングの実装例【タッチ&マウス対応】

Mode7風 疑似3Dレーシングの実装例【タッチ&マウス対応】
今回は、HTML5 と JavaScript を使って実装した Mode7 風の疑似3Dレーシングゲームのサンプルコードを詳しく解説します。
この実装例は、画像選択でローカル画像を利用できる機能や、タッチ・マウスの長押しで複数の操作を同時に入力できる設計になっています。
- 画像選択対応: リモート画像の読み込みで CORS の問題が発生した場合、
<input type="file">
を使ってローカル画像をテクスチャとして利用できます。 - 操作パネル(タッチ&マウス対応): マウスおよびタッチの長押し操作に対応しており、アクセル/ブレーキと左右操作を同時に入力できます。
操作ボタンは、画面左側に左右操作用、右側にアクセル/ブレーキ用を配置しています。 - Mode7 効果: キャンバス下部の各ピクセルにテクスチャ座標を計算し、疑似3Dの地面表現を実現しています。
ソースコード
下記のコードをコピーしてお使いください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>最終版 Mode7風 疑似3Dレーシング - 画像選択対応</title>
<style>
/* ブラウザの余白をなくす */
html, body {
margin: 0;
padding: 0;
background: #87CEEB;
}
.touch-button {
position: absolute;
padding: 10px 20px;
background: rgba(0,0,0,0.5);
color: #fff;
border: none;
border-radius: 5px;
font-size: 16px;
z-index: 20;
user-select: none;
}
#btnLeft { bottom: 60px; left: 20px; }
#btnRight { bottom: 60px; left: 120px; }
#btnUp { bottom: 100px; right: 20px; }
#btnDown { bottom: 20px; right: 20px; }
canvas {
display: block;
}
#fileInput {
position: absolute;
top: 10px;
left: 10px;
z-index: 10;
}
</style>
</head>
<body>
<input type="file" id="fileInput" accept="image/*">
<canvas id="gameCanvas"></canvas>
<!-- 操作用ボタン -->
<button class="touch-button" id="btnLeft">左</button>
<button class="touch-button" id="btnRight">右</button>
<button class="touch-button" id="btnUp">アクセル</button>
<button class="touch-button" id="btnDown">ブレーキ</button>
<script>
// キャンバスのセットアップ
const canvas = document.getElementById('gameCanvas'),
ctx = canvas.getContext('2d');
let horizon;
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
horizon = canvas.height / 2;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// カメラ・視点・速度パラメータ
const camHeight = 100;
let camX = 0, camY = 0, camAngle = 0;
const fov = Math.PI / 3;
let moveSpeed = 0;
const acceleration = 0.2, friction = 0.02, rotSpeed = 0.03;
const keys = {};
// キーボード操作
document.addEventListener('keydown', e => { keys[e.key] = true; });
document.addEventListener('keyup', e => { keys[e.key] = false; });
// マウスとタッチの操作イベント登録
function addInputEvents(btnId, keyName) {
const btn = document.getElementById(btnId);
btn.addEventListener('mousedown', () => { keys[keyName] = true; });
btn.addEventListener('mouseup', () => { keys[keyName] = false; });
btn.addEventListener('mouseleave', () => { keys[keyName] = false; });
btn.addEventListener('touchstart', e => {
e.preventDefault();
keys[keyName] = true;
});
btn.addEventListener('touchend', e => {
e.preventDefault();
keys[keyName] = false;
});
btn.addEventListener('touchcancel', e => {
e.preventDefault();
keys[keyName] = false;
});
}
addInputEvents('btnUp', 'ArrowUp');
addInputEvents('btnDown', 'ArrowDown');
addInputEvents('btnLeft', 'ArrowLeft');
addInputEvents('btnRight', 'ArrowRight');
// テクスチャ画像の設定
const texture = new Image();
texture.crossOrigin = "Anonymous";
texture.src = 'https://doiworks.com/wp-content/uploads/2025/03/Gemini_Generated_Image_rkmnrrkmnrrkmnrr.jpg';
texture.onload = () => { initGame(); };
// ファイル選択によるローカル画像の読み込み
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', event => {
const file = event.target.files[0];
if(file){
const reader = new FileReader();
reader.onload = e => {
texture.src = e.target.result;
};
reader.readAsDataURL(file);
}
});
function initGame() {
const offCanvas = document.createElement('canvas');
offCanvas.width = texture.width;
offCanvas.height = texture.height;
const offCtx = offCanvas.getContext('2d');
offCtx.drawImage(texture, 0, 0);
const textureImageData = offCtx.getImageData(0, 0, texture.width, texture.height);
const textureData = textureImageData.data;
function gameLoop() {
update();
render(textureData, texture.width, texture.height);
requestAnimationFrame(gameLoop);
}
gameLoop();
}
function update() {
if(keys['ArrowUp']) moveSpeed += acceleration;
if(keys['ArrowDown']) moveSpeed -= acceleration;
if(!keys['ArrowUp'] && !keys['ArrowDown']){
if(moveSpeed > 0){
moveSpeed -= friction;
if(moveSpeed < 0) moveSpeed = 0;
} else if(moveSpeed < 0){ moveSpeed += friction; if(moveSpeed > 0) moveSpeed = 0;
}
}
camX += Math.cos(camAngle) * moveSpeed;
camY += Math.sin(camAngle) * moveSpeed;
if(keys['ArrowLeft']) camAngle -= rotSpeed;
if(keys['ArrowRight']) camAngle += rotSpeed;
}
function render(textureData, texWidth, texHeight) {
ctx.fillStyle = "#87CEEB";
ctx.fillRect(0, 0, canvas.width, canvas.height);
const groundHeight = canvas.height - horizon;
const groundImage = ctx.createImageData(canvas.width, groundHeight);
const groundData = groundImage.data;
for (let screenY = horizon; screenY < canvas.height; screenY++) {
const p = (screenY - horizon) / (canvas.height - horizon);
const distance = camHeight / (p + 0.0001);
const halfViewWidth = distance * Math.tan(fov / 2);
const centerX = camX + distance * Math.cos(camAngle);
const centerY = camY + distance * Math.sin(camAngle);
for (let screenX = 0; screenX < canvas.width; screenX++) {
const u = (screenX - canvas.width / 2) / (canvas.width / 2);
const offset = u * halfViewWidth;
const worldX = centerX - offset * Math.sin(camAngle);
const worldY = centerY + offset * Math.cos(camAngle);
const worldXInt = Math.floor(worldX);
const worldYInt = Math.floor(worldY);
const screenIndex = ((screenY - horizon) * canvas.width + screenX) * 4;
if (worldXInt < 0 || worldXInt >= texWidth || worldYInt < 0 || worldYInt >= texHeight) {
groundData[screenIndex] = 0;
groundData[screenIndex + 1] = 0;
groundData[screenIndex + 2] = 0;
groundData[screenIndex + 3] = 0;
} else {
const texIndex = (worldYInt * texWidth + worldXInt) * 4;
groundData[screenIndex] = textureData[texIndex];
groundData[screenIndex + 1] = textureData[texIndex + 1];
groundData[screenIndex + 2] = textureData[texIndex + 2];
groundData[screenIndex + 3] = textureData[texIndex + 3];
}
}
}
ctx.putImageData(groundImage, 0, horizon);
}
</script>
</body>
</html>
ジミニー Code Editor
下記のエディタ内で、コードを実行・編集できます。ぜひお試しください!
コメント ( 0 )
トラックバックは利用できません。
この記事へのコメントはありません。