ClaudeのArtifacts機能でマインクラフトを作ろう!
Three.jsでマインクラフト風ゲームを作ろう!
こんにちは!DiningXの吉波です。
最近ClaudeのArtifacts機能が楽しすぎて遊んでます🙌
本日(2024/7/1)Xでこんな投稿をしました。
すると思ったより反響があり、中にはプロンプトやコードを知りたいという方もいらっしゃったので、マインクラフトを作るまでの一連の流れをnoteに記したいと思います!
はじまり
今回は、Three.jsという3Dモデルが作れるJavaScriptライブラリを使って、簡単なマインクラフト風の3Dゲームを作成する方法をトライしました。僕自身、Webゲーム作ったことはこれまでになかったので、とりあえずClaudeとの対話を通じて、ステップバイステップで開発を進めていきました。
最初のプレビュー画面では、3Dは愚か、真っ暗な画面にコンソールログのような白文字が出てくるくらいでした
スマホで動かしたい!
やはり、できることならスマホでゲームしたいですよね…!ということで、次の指令としてClaudeにこんな言葉を伝えてみました。
「スマホで操作したい。キャラを歩かせたい。3次元マップで、自然も入れたい」
すると、画面右下の方に移動ボタンのようなものが出てきましたね。
対話の様子
他にも色んな対話を試しました。なかなか3Dゲーム画面にならなかったので、もどかしかったです笑
最終版
ということで、最終版のClaudeの出力がこちらです!モデルは3.5 Sonnetを利用しました!
最終的に生成されたコード
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Smooth Mobile Minecraft-like Game</title>
<style>
body { margin: 0; overflow: hidden; }
#joystick { position: absolute; bottom: 20px; left: 20px; width: 120px; height: 120px; }
#jumpBtn { position: absolute; bottom: 20px; right: 20px; width: 80px; height: 80px; background: rgba(255,255,255,0.5); border: none; border-radius: 40px; font-size: 24px; }
</style>
</head>
<body>
<div id="joystick"></div>
<button id="jumpBtn">Jump</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/nipplejs/0.9.0/nipplejs.min.js"></script>
<script>
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 以下、前回のコードと同じ
const worldSize = 50;
const blockSize = 1;
const simplex = new SimplexNoise();
const materials = {
grass: new THREE.MeshLambertMaterial({ color: 0x3bab17 }),
dirt: new THREE.MeshLambertMaterial({ color: 0x8b4513 }),
stone: new THREE.MeshLambertMaterial({ color: 0x808080 }),
water: new THREE.MeshLambertMaterial({ color: 0x4040ff, transparent: true, opacity: 0.7 }),
sand: new THREE.MeshLambertMaterial({ color: 0xffff00 }),
snow: new THREE.MeshLambertMaterial({ color: 0xffffff }),
};
const blocks = {};
function createBlock(x, y, z, material) {
const geometry = new THREE.BoxGeometry(blockSize, blockSize, blockSize);
const block = new THREE.Mesh(geometry, material);
block.position.set(x, y, z);
scene.add(block);
blocks[`${Math.round(x)},${Math.round(y)},${Math.round(z)}`] = block;
}
function generateTerrain() {
for (let x = 0; x < worldSize; x++) {
for (let z = 0; z < worldSize; z++) {
const height = Math.floor(simplex.noise2D(x * 0.1, z * 0.1) * 10 + 10);
for (let y = 0; y < height; y++) {
let material;
if (y === height - 1) {
if (height < 8) material = materials.sand;
else if (height > 15) material = materials.snow;
else material = materials.grass;
} else if (y > height - 4) {
material = materials.dirt;
} else {
material = materials.stone;
}
createBlock(x, y, z, material);
}
if (height < 8) {
for (let y = height; y < 8; y++) {
createBlock(x, y, z, materials.water);
}
}
}
}
}
generateTerrain();
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
directionalLight.position.set(10, 20, 10);
scene.add(directionalLight);
// Character
const characterGeometry = new THREE.BoxGeometry(0.6, 1.8, 0.6);
const characterMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 });
const character = new THREE.Mesh(characterGeometry, characterMaterial);
character.position.set(worldSize / 2, 15, worldSize / 2);
scene.add(character);
camera.position.set(character.position.x, character.position.y + 5, character.position.z + 5);
camera.lookAt(character.position);
// Movement variables
let moveVector = new THREE.Vector3();
let characterVelocity = new THREE.Vector3();
const moveSpeed = 5;
let isJumping = false;
const jumpForce = 8;
const gravity = -9.8;
// Joystick
const joystick = nipplejs.create({
zone: document.getElementById('joystick'),
mode: 'static',
position: { left: '60px', bottom: '60px' },
color: 'white',
size: 120
});
joystick.on('move', (evt, data) => {
const angle = data.angle.radian;
const force = Math.min(data.force, 1);
moveVector.x = Math.cos(angle) * force;
moveVector.z = -Math.sin(angle) * force;
});
joystick.on('end', () => {
moveVector.set(0, 0, 0);
});
// Jump button
const jumpBtn = document.getElementById('jumpBtn');
jumpBtn.addEventListener('touchstart', jump);
function jump() {
if (!isJumping) {
isJumping = true;
characterVelocity.y = jumpForce;
}
}
function checkCollision(x, y, z) {
const roundX = Math.round(x);
const roundY = Math.round(y);
const roundZ = Math.round(z);
return blocks[`${roundX},${roundY},${roundZ}`] !== undefined;
}
let lastTime = 0;
const targetFPS = 60;
const timeStep = 1000 / targetFPS;
function gameLoop(currentTime) {
requestAnimationFrame(gameLoop);
if (currentTime - lastTime < timeStep) return;
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
updateGame(deltaTime);
render();
}
function updateGame(deltaTime) {
// Apply movement
characterVelocity.x = moveVector.x * moveSpeed;
characterVelocity.z = moveVector.z * moveSpeed;
// Apply gravity
if (isJumping) {
characterVelocity.y += gravity * deltaTime;
}
// Update position
const potentialPosition = character.position.clone().add(characterVelocity.clone().multiplyScalar(deltaTime));
// Check collisions
if (!checkCollision(potentialPosition.x, character.position.y, character.position.z)) {
character.position.x = potentialPosition.x;
}
if (!checkCollision(character.position.x, potentialPosition.y, character.position.z)) {
character.position.y = potentialPosition.y;
} else {
characterVelocity.y = 0;
isJumping = false;
}
if (!checkCollision(character.position.x, character.position.y, potentialPosition.z)) {
character.position.z = potentialPosition.z;
}
// Update camera
const cameraOffset = new THREE.Vector3(0, 5, 5);
camera.position.copy(character.position).add(cameraOffset);
camera.lookAt(character.position);
TWEEN.update();
}
function render() {
renderer.render(scene, camera);
}
requestAnimationFrame(gameLoop);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
まとめ
ゲーム開発経験がない僕でも、Claudeを使って簡単なWebゲームを作ることができました!「追加して欲しい機能をうまく伝えること」が鍵かなって思っています。今後ともClaudeに関する知見を発信していきますので、よければフォローお願いします!