Home / Comments / work in progress
Duplicate Snippet

Embed Snippet on Your Site

work in progress

3d cartoon style racing game that I can import to scratch, other racers and a start button/menu included

Code Preview
php
<?php
<!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>Toon Racers 3D</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Fredoka+One&display=swap');
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
            background-color: #87CEEB; /* Sky blue */
            font-family: 'Fredoka One', cursive, sans-serif;
            touch-action: none;
        }
        #game-container {
            width: 100vw;
            height: 100vh;
            position: absolute;
            top: 0;
            left: 0;
            z-index: 1;
        }
        /* UI Layer */
        #ui-layer {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 10;
            pointer-events: none; /* Let clicks pass through to game if needed */
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }
        /* Start Menu */
        #start-menu {
            background: rgba(255, 255, 255, 0.9);
            padding: 40px 60px;
            border-radius: 30px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.3);
            text-align: center;
            pointer-events: auto;
            border: 4px solid #FFD700;
            transition: opacity 0.3s ease, transform 0.3s ease;
        }
        #start-menu h1 {
            color: #FF4500;
            font-size: 4rem;
            margin: 0 0 10px 0;
            text-shadow: 2px 2px 0px #FFF, 4px 4px 0px rgba(0,0,0,0.1);
            letter-spacing: 2px;
        }
        #start-menu p {
            color: #333;
            font-size: 1.2rem;
            margin-bottom: 30px;
        }
        .controls-info {
            display: flex;
            justify-content: center;
            gap: 20px;
            margin-bottom: 30px;
        }
        .key-btn {
            background: #eee;
            border: 2px solid #ccc;
            border-bottom: 4px solid #ccc;
            padding: 10px 15px;
            border-radius: 8px;
            font-weight: bold;
            color: #555;
        }
        #start-btn {
            background-color: #4CAF50;
            color: white;
            font-family: 'Fredoka One', cursive, sans-serif;
            font-size: 1.8rem;
            padding: 15px 50px;
            border: none;
            border-radius: 50px;
            cursor: pointer;
            box-shadow: 0 6px 0 #2E7D32, 0 10px 10px rgba(0,0,0,0.2);
            transition: all 0.1s ease;
            text-transform: uppercase;
        }
        #start-btn:active {
            transform: translateY(6px);
            box-shadow: 0 0 0 #2E7D32, 0 4px 4px rgba(0,0,0,0.2);
        }
        #start-btn:hover {
            background-color: #45a049;
        }
        /* HUD */
        #hud {
            position: absolute;
            bottom: 30px;
            left: 30px;
            background: rgba(0, 0, 0, 0.5);
            padding: 15px 25px;
            border-radius: 15px;
            color: white;
            display: none;
            border: 3px solid rgba(255,255,255,0.2);
        }
        #speedometer {
            font-size: 2rem;
            margin: 0;
            text-shadow: 2px 2px 0 #000;
        }
        
        #grass-warning {
            color: #FF6B6B;
            font-size: 1rem;
            display: none;
            margin-top: 5px;
            animation: blink 1s infinite;
        }
        @keyframes blink {
            0%, 100% { opacity: 1; }
            50% { opacity: 0; }
        }
        /* Loading Screen */
        #loading {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 2rem;
            color: white;
            z-index: 20;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
        }
        /* Mobile Controls */
        #mobile-controls {
            position: absolute;
            bottom: 20px;
            width: 100%;
            display: none;
            justify-content: space-between;
            padding: 0 20px;
            box-sizing: border-box;
            pointer-events: none;
        }
        .touch-zone {
            display: flex;
            gap: 10px;
            pointer-events: auto;
        }
        .touch-btn {
            width: 60px;
            height: 60px;
            background: rgba(255, 255, 255, 0.3);
            border: 2px solid rgba(255, 255, 255, 0.6);
            border-radius: 50%;
            display: flex;
            justify-content: center;
            align-items: center;
            font-size: 1.5rem;
            color: white;
            user-select: none;
        }
        .touch-btn:active {
            background: rgba(255, 255, 255, 0.6);
        }
        @media (max-width: 768px) {
            #mobile-controls { display: flex; }
            #start-menu { padding: 30px 20px; width: 85%; }
            #start-menu h1 { font-size: 2.5rem; }
            .controls-info { display: none; } /* Hide keyboard info on mobile */
            #hud { top: 20px; left: 20px; bottom: auto; padding: 10px 15px; }
            #speedometer { font-size: 1.5rem; }
        }
    </style>
</head>
<body>
    <div id="loading">Loading Engine...</div>
    <div id="game-container"></div>
    <div id="ui-layer">
        <div id="start-menu">
            <h1>Toon Racers</h1>
            <p>Race against the bots! Stay on the track to maintain top speed.</p>
            
            <div class="controls-info">
                <div>
                    <div class="key-btn">↑</div>
                    <div>Accelerate</div>
                </div>
                <div>
                    <div class="key-btn">↓</div>
                    <div>Brake/Reverse</div>
                </div>
                <div>
                    <div class="key-btn">←</div><div class="key-btn">→</div>
                    <div>Steer</div>
                </div>
            </div>
            <button id="start-btn">Start Race</button>
        </div>
    </div>
    <div id="hud">
        <h2 id="speedometer">0 MPH</h2>
        <div id="grass-warning">Off Track! Slowing down...</div>
    </div>
    <div id="mobile-controls">
        <div class="touch-zone">
            <div class="touch-btn" id="btn-left">←</div>
            <div class="touch-btn" id="btn-right">→</div>
        </div>
        <div class="touch-zone">
            <div class="touch-btn" id="btn-brake">↓</div>
            <div class="touch-btn" id="btn-accel">↑</div>
        </div>
    </div>
    <!-- Three.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script>
        // --- GAME CONSTANTS & VARIABLES ---
        let scene, camera, renderer;
        let playerCar;
        let aiCars = [];
        let trackCurve, trackPoints;
        let isPlaying = false;
        // Player Physics
        let speed = 0;
        const maxSpeed = 1.2;
        const acceleration = 0.015;
        const braking = 0.03;
        const steering = 0.04;
        const trackWidth = 14;
        // Input state
        const keys = { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false, w: false, s: false, a: false, d: false };
        // Colors
        const CAR_COLORS = [0x2196F3, 0xFFC107, 0x9C27B0, 0xFF9800]; // AI Colors
        const PLAYER_COLOR = 0xF44336; // Red
        // --- INITIALIZATION ---
        window.onload = function() {
            document.getElementById('loading').style.display = 'none';
            initScene();
            createEnvironment();
            createPlayer();
            createAI();
            setupInput();
            
            // Start the render loop immediately for background animation
            animate();
            
            // Start Menu Listener
            document.getElementById('start-btn').addEventListener('click', startGame);
        };
        function initScene() {
            const container = document.getElementById('game-container');
            
            // Scene setup
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x87CEEB); // Sky blue
            scene.fog = new THREE.Fog(0x87CEEB, 30, 250); // Adds depth
            // Camera setup
            camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.set(0, 20, 40);
            // Renderer setup
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.shadowMap.enabled = true;
            renderer.shadowMap.type = THREE.PCFSoftShadowMap;
            container.appendChild(renderer.domElement);
            // Lighting
            const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
            scene.add(ambientLight);
            const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
            dirLight.position.set(100, 150, 50);
            dirLight.castShadow = true;
            dirLight.shadow.camera.top = 150;
            dirLight.shadow.camera.bottom = -150;
            dirLight.shadow.camera.left = -150;
            dirLight.shadow.camera.right = 150;
            dirLight.shadow.mapSize.width = 2048;
            dirLight.shadow.mapSize.height = 2048;
            scene.add(dirLight);
            // Handle window resize
            window.addEventListener('resize', () => {
                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(window.innerWidth, window.innerHeight);
            });
        }
        function createEnvironment() {
            // 1. Ground
            const groundGeo = new THREE.PlaneGeometry(1000, 1000);
            const groundMat = new THREE.MeshStandardMaterial({ 
                color: 0x689F38, // Grass green
                roughness: 1,
                metalness: 0
            });
            const ground = new THREE.Mesh(groundGeo, groundMat);
            ground.rotation.x = -Math.PI / 2;
            ground.receiveShadow = true;
            scene.add(ground);
            // 2. Track Design (A winding circuit)
            trackCurve = new THREE.CatmullRomCurve3([
                new THREE.Vector3(0, 0, 150),
                new THREE.Vector3(120, 0, 120),
                new THREE.Vector3(160, 0, 40),
                new THREE.Vector3(90, 0, -60),
                new THREE.Vector3(120, 0, -140),
                new THREE.Vector3(0, 0, -160),
                new THREE.Vector3(-100, 0, -120),
                new THREE.Vector3(-150, 0, 0),
                new THREE.Vector3(-80, 0, 80)
            ], true);
            // Precompute points for distance checking (off-road detection)
            trackPoints = trackCurve.getSpacedPoints(200);
            // Create visible track
            const tubeGeo = new THREE.TubeGeometry(trackCurve, 200, trackWidth, 8, true);
            const trackMat = new THREE.MeshStandardMaterial({ 
                color: 0x424242, // Asphalt dark grey
                roughness: 0.9,
            });
            const trackMesh = new THREE.Mesh(tubeGeo, trackMat);
            trackMesh.scale.y = 0.05; // Flatten the tube to make it a flat road
            trackMesh.position.y = 0.1; // Slightly above ground to prevent Z-fighting
            trackMesh.receiveShadow = true;
            scene.add(trackMesh);
            // Add track edge lines (curbs)
            const curbGeo = new THREE.TubeGeometry(trackCurve, 200, trackWidth + 0.5, 8, true);
            const curbMat = new THREE.MeshStandardMaterial({ color: 0xFFFFFF });
            const curbMesh = new THREE.Mesh(curbGeo, curbMat);
            curbMesh.scale.y = 0.04;
            curbMesh.position.y = 0.05;
            scene.add(curbMesh);
            // 3. Scenery (Trees)
            createTrees();
        }
        function createTrees() {
            const treeGeo = new THREE.ConeGeometry(3, 10, 5); // Low poly look
            const treeMat = new THREE.MeshStandardMaterial({ color: 0x2E7D32, flatShading: true });
            const trunkGeo = new THREE.CylinderGeometry(0.5, 0.8, 3, 5);
            const trunkMat = new THREE.MeshStandardMaterial({ color: 0x5D4037, flatShading: true });
            for (let i = 0; i < 150; i++) {
                const x = (Math.random() - 0.5) * 400;
                const z = (Math.random() - 0.5) * 400;
                const pos = new THREE.Vector3(x, 0, z);
                // Check distance to track so trees don't block the road
                let tooClose = false;
                for (let pt of trackPoints) {
                    if (pos.distanceToSquared(pt) < (trackWidth + 10) * (trackWidth + 10)) {
                        tooClose = true;
                        break;
                    }
                }
                if (!tooClose) {
                    const treeGroup = new THREE.Group();
                    
                    const leaves = new THREE.Mesh(treeGeo, treeMat);
                    leaves.position.y = 6;
                    leaves.castShadow = true;
                    
                    const trunk = new THREE.Mesh(trunkGeo, trunkMat);
                    trunk.position.y = 1.5;
                    trunk.castShadow = true;
                    treeGroup.add(leaves);
                    treeGroup.add(trunk);
                    
                    // Random scale and rotation for variety
                    const scale = 0.8 + Math.random() * 0.6;
                    treeGroup.scale.set(scale, scale, scale);
                    treeGroup.rotation.y = Math.random() * Math.PI;
                    treeGroup.position.copy(pos);
                    
                    scene.add(treeGroup);
                }
            }
        }
        function createCarModel(colorHex) {
            const carGroup = new THREE.Group();
            // Materials
            const bodyMat = new THREE.MeshStandardMaterial({ color: colorHex, flatShading: true });
            const windowMat = new THREE.MeshStandardMaterial({ color: 0x212121, flatShading: true });
            const wheelMat = new THREE.MeshStandardMaterial({ color: 0x111111, flatShading: true });
            // Main Body
            const bodyGeo = new THREE.BoxGeometry(2.4, 0.8, 4.5);
            const body = new THREE.Mesh(bodyGeo, bodyMat);
            body.position.y = 0.8;
            body.castShadow = true;
            carGroup.add(body);
            // Cabin (Top)
            const cabinGeo = new THREE.BoxGeometry(1.8, 0.7, 2.2);
            const cabin = new THREE.Mesh(cabinGeo, windowMat);
            cabin.position.y = 1.55;
            cabin.position.z = -0.2;
            cabin.castShadow = true;
            carGroup.add(cabin);
            // Wheels
            const wheelGeo = new THREE.CylinderGeometry(0.5, 0.5, 0.4, 12);
            wheelGeo.rotateZ(Math.PI / 2); // Align cylinders to act like wheels
            const wheelPositions = [
                [-1.3, 0.5, 1.5],  // Front Left
                [1.3, 0.5, 1.5],   // Front Right
                [-1.3, 0.5, -1.5], // Back Left
                [1.3, 0.5, -1.5]   // Back Right
            ];
            wheelPositions.forEach(pos => {
                const wheel = new THREE.Mesh(wheelGeo, wheelMat);
                wheel.position.set(pos[0], pos[1], pos[2]);
                wheel.castShadow = true;
                carGroup.add(wheel);
            });
            // Headlights
            const lightGeo = new THREE.BoxGeometry(0.4, 0.2, 0.1);
            const lightMat = new THREE.MeshStandardMaterial({ color: 0xFFFF00, emissive: 0xFFFF00, emissiveIntensity: 0.5 });
            
            const leftLight = new THREE.Mesh(lightGeo, lightMat);
            leftLight.position.set(-0.8, 0.8, 2.26);
            carGroup.add(leftLight);
            const rightLight = new THREE.Mesh(lightGeo, lightMat);
            rightLight.position.set(0.8, 0.8, 2.26);
            carGroup.add(rightLight);
            // Give the car group a property so we can rotate the wheels later if we want
            carGroup.userData.wheels = carGroup.children.filter(c => c.geometry.type === 'CylinderGeometry');
            return carGroup;
        }
        function createPlayer() {
            playerCar = createCarModel(PLAYER_COLOR);
            
            // Set initial position and rotation
            const startPoint = trackCurve.getPointAt(0);
            const lookPoint = trackCurve.getPointAt(0.01);
            
            playerCar.position.copy(startPoint);
            playerCar.lookAt(lookPoint);
            scene.add(playerCar);
        }
        function createAI() {
            // Create 4 AI cars scattered around the track
            for(let i=0; i<4; i++) {
                const aiCarMesh = createCarModel(CAR_COLORS[i]);
                scene.add(aiCarMesh);
                
                aiCars.push({
                    mesh: aiCarMesh,
                    progress: (i + 1) * 0.15, // Spread them out along the curve (0 to 1)
                    baseSpeed: 0.0006 + Math.random() * 0.0003, // Slight speed variations
                    offset: (Math.random() - 0.5) * 8 // Random offset from center lane
                });
            }
        }
        function setupInput() {
            // Keyboard Controls
            window.addEventListener('keydown', (e) => {
                if (keys.hasOwnProperty(e.key)) keys[e.key] = true;
                if (keys.hasOwnProperty(e.key.toLowerCase())) keys[e.key.toLowerCase()] = true;
            });
            window.addEventListener('keyup', (e) => {
                if (keys.hasOwnProperty(e.key)) keys[e.key] = false;
                if (keys.hasOwnProperty(e.key.toLowerCase())) keys[e.key.toLowerCase()] = false;
            });
            // Touch Controls for Mobile
            const setupTouch = (id, keyTarget) => {
                const el = document.getElementById(id);
                el.addEventListener('touchstart', (e) => { e.preventDefault(); keys[keyTarget] = true; });
                el.addEventListener('touchend', (e) => { e.preventDefault(); keys[keyTarget] = false; });
            };
            setupTouch('btn-left', 'ArrowLeft');
            setupTouch('btn-right', 'ArrowRight');
            setupTouch('btn-accel', 'ArrowUp');
            setupTouch('btn-brake', 'ArrowDown');
        }
        function startGame() {
            document.getElementById('start-menu').style.opacity = '0';
            setTimeout(() => {
                document.getElementById('start-menu').style.display = 'none';
                document.getElementById('hud').style.display = 'block';
                isPlaying = true;
            }, 300);
        }
        function getDistanceToTrack(pos) {
            let minDiv = Infinity;
            // A simple brute-force check against precomputed points
            // Very fast for 200 points in JS
            for(let pt of trackPoints) {
               let d = pos.distanceToSquared(pt);
               if(d < minDiv) minDiv = d;
            }
            return Math.sqrt(minDiv);
        }
        function updatePlayer() {
            // Input handling
            const isAccelerating = keys.ArrowUp || keys.w;
            const isBraking = keys.ArrowDown || keys.s;
            const isSteeringLeft = keys.ArrowLeft || keys.a;
            const isSteeringRight = keys.ArrowRight || keys.d;
            // Track off-road penalty
            const distToCenter = getDistanceToTrack(playerCar.position);
            const isOffRoad = distToCenter > trackWidth;
            
            // UI feedback
            document.getElementById('grass-warning').style.display = isOffRoad ? 'block' : 'none';
            // Variable friction and top speed based on terrain
            let currentFriction = isOffRoad ? 0.90 : 0.98; // Grass slows you down much faster
            let currentMaxSpeed = isOffRoad ? maxSpeed * 0.4 : maxSpeed;
            // Apply acceleration
            if (isAccelerating) {
                speed += acceleration;
            } else if (isBraking) {
                speed -= braking;
            }
            // Apply friction/drag
            speed *= currentFriction;
            // Clamp speed
            if (speed > currentMaxSpeed) speed = currentMaxSpeed;
            if (speed < -currentMaxSpeed * 0.5) speed = -currentMaxSpeed * 0.5; // Reverse is slower
            // Steering (only steer if moving, invert steering if reversing)
            if (Math.abs(speed) > 0.05) {
                const turnDirection = speed > 0 ? 1 : -1;
                if (isSteeringLeft) playerCar.rotateY(steering * turnDirection);
                if (isSteeringRight) playerCar.rotateY(-steering * turnDirection);
            }
            // Move Car Forward (ThreeJS models usually face Z+, but our geometry is aligned Z+)
            // Our car headlights are at +Z, so +Z is forward.
            playerCar.translateZ(speed);
            // Animate wheels based on speed
            playerCar.userData.wheels.forEach(wheel => {
                wheel.rotation.x += speed * 0.5;
            });
            // Update Speedometer UI
            const displaySpeed = Math.abs(Math.round(speed * 100)); // Arbitrary multiplier for visual UI
            document.getElementById('speedometer').innerText = `${displaySpeed} MPH`;
        }
        function updateAI() {
            aiCars.forEach(ai => {
                // Move along the curve
                ai.progress += ai.baseSpeed;
                if (ai.progress > 1) ai.progress -= 1; // Loop around
                // Get position and direction on the curve
                const pt = trackCurve.getPointAt(ai.progress);
                const tangent = trackCurve.getTangentAt(ai.progress);
                
                // Calculate position with offset
                // Cross product to get the "right" vector relative to the track direction
                const up = new THREE.Vector3(0, 1, 0);
                const right = new THREE.Vector3().crossVectors(tangent, up).normalize();
                
                // Apply offset
                const offsetPos = pt.clone().add(right.multiplyScalar(ai.offset));
                
                ai.mesh.position.copy(offsetPos);
                
                // Keep wheels on ground
                // (Geometry center is shifted up, so we don't need to add height here)
                
                // Look ahead
                const lookTarget = offsetPos.clone().add(tangent);
                ai.mesh.lookAt(lookTarget);
                // Animate AI wheels
                ai.mesh.userData.wheels.forEach(wheel => {
                    wheel.rotation.x += ai.baseSpeed * 50;
                });
            });
        }
        function updateCamera() {
            if (isPlaying) {
                // Chase camera for player
                // Position camera behind and above the car
                const relativeCameraOffset = new THREE.Vector3(0, 8, -20); // Z is negative because car faces +Z
                const cameraOffset = relativeCameraOffset.applyMatrix4(playerCar.matrixWorld);
                
                // Smooth camera follow
                camera.position.lerp(cameraOffset, 0.1);
                
                // Look slightly ahead of the car
                const lookAtTarget = playerCar.position.clone();
                lookAtTarget.y += 2;
                camera.lookAt(lookAtTarget);
            } else {
                // Idle menu camera spin
                const time = Date.now() * 0.0005;
                camera.position.x = Math.cos(time) * 100;
                camera.position.z = Math.sin(time) * 100;
                camera.position.y = 50;
                camera.lookAt(0, 0, 0);
            }
        }
        // Main Game Loop
        function animate() {
            requestAnimationFrame(animate);
            if (isPlaying) {
                updatePlayer();
            }
            
            updateAI();
            updateCamera();
            renderer.render(scene, camera);
        }
    </script>
</body>
</html>

Comments

Add a Comment