見出し画像

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に関する知見を発信していきますので、よければフォローお願いします!


この記事が気に入ったらサポートをしてみませんか?