Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Mini GTA‑3 Demo – Walk + Jump</title>
- <!-- Import‑map for the bare specifier "three" -->
- <script type="importmap">
- {
- "imports": {
- "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js"
- }
- }
- </script>
- <script type="module">
- import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
- import { GLTFLoader } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/GLTFLoader.js';
- import { PointerLockControls } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/PointerLockControls.js';
- // ---------- GLOBALS ----------
- let scene, camera, renderer, controls;
- let clock = new THREE.Clock();
- const mixers = []; // AnimationMixers for animated models
- let isPaused = false;
- const keys = {}; // pressed keys / touch‑button states
- let assetsToLoad = 2; // robot + car
- // Player character
- const player = new THREE.Group(); // will contain the robot model
- const cameraOffset = new THREE.Vector3(0, 2, -5); // behind & slightly above player
- // Jump / gravity
- const GRAVITY = -30; // units / second²
- const JUMP_SPEED = 12; // initial upward velocity
- let verticalVelocity = 0;
- let isGrounded = true;
- // Drag‑to‑look fallback (when pointer‑lock is blocked)
- let isDragging = false;
- let prevMouse = { x: 0, y: 0 };
- const dragSens = 0.002;
- // ---------- UI ----------
- const ui = document.createElement('div');
- ui.innerHTML = `
- <button id="startBtn" class="uiBtn">Start</button>
- <button id="pauseBtn" class="uiBtn">Pause</button>
- <div id="info" class="info">WASD / Arrow keys – move | Space – jump | Drag / mouse to turn</div>
- <div id="loadingOverlay" class="loadingOverlay">Loading…</div>
- <div id="clickOverlay" class="clickOverlay">Click “Start” to begin</div>
- <!-- On‑screen movement arrows -->
- <div class="touchControls">
- <button id="upBtn" class="touchBtn">▲</button>
- <button id="leftBtn" class="touchBtn">◄</button>
- <button id="rightBtn" class="touchBtn">►</button>
- <button id="downBtn" class="touchBtn">▼</button>
- </div>
- `;
- document.body.appendChild(ui);
- document.getElementById('startBtn').onclick = () => requestPointerLock();
- document.getElementById('pauseBtn').onclick = () => { isPaused = !isPaused; };
- // ---------- STYLES ----------
- const style = document.createElement('style');
- style.textContent = `
- body { margin:0; overflow:hidden; font-family:Arial,Helvetica,sans-serif; background:#111; }
- .uiBtn {
- position:fixed; top:10px; left:10px; margin-right:8px;
- padding:8px 12px; background:#222; color:#fff; border:none;
- border-radius:4px; cursor:pointer; font-size:14px;
- z-index:2000; pointer-events:auto;
- }
- .uiBtn:hover { background:#444; }
- .info {
- position:fixed; bottom:10px; left:10px; color:#ddd;
- background:rgba(0,0,0,0.5); padding:6px 10px; border-radius:4px;
- font-size:13px; z-index:2000;
- }
- .loadingOverlay, .clickOverlay {
- position:fixed; inset:0; background:rgba(0,0,0,0.8);
- color:#fff; display:flex; align-items:center; justify-content:center;
- font-size:24px; z-index:500; pointer-events:none;
- }
- .clickOverlay { display:none; }
- .touchControls {
- position:fixed; bottom:80px; left:50%; transform:translateX(-50%);
- display:grid; grid-template-columns:repeat(3, 60px); grid-gap:8px;
- z-index:2000;
- }
- .touchBtn {
- width:60px; height:60px; font-size:24px; background:#222; color:#fff;
- border:none; border-radius:8px; cursor:pointer; opacity:0.8;
- }
- .touchBtn:hover { opacity:1; background:#444; }
- .touchBtn:active { background:#666; }
- `;
- document.head.appendChild(style);
- // ---------- INITIALISE ----------
- init();
- animate();
- function init() {
- // Scene & Camera
- scene = new THREE.Scene();
- scene.background = new THREE.Color(0x87ceeb); // sky blue
- camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
- camera.position.set(0, 2, 5); // will be overwritten each frame
- // Renderer (focusable canvas)
- renderer = new THREE.WebGLRenderer({ antialias:true });
- renderer.setSize(window.innerWidth, window.innerHeight);
- renderer.domElement.tabIndex = 0; // allow focus
- renderer.domElement.style.outline = 'none';
- document.body.appendChild(renderer.domElement);
- // Lights
- const ambient = new THREE.AmbientLight(0xffffff, 0.6);
- scene.add(ambient);
- const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
- dirLight.position.set(10,20,10);
- scene.add(dirLight);
- // Controls (only for mouse‑move capture)
- controls = new PointerLockControls(camera, renderer.domElement);
- scene.add(controls.getObject());
- // Add player group to the scene
- scene.add(player);
- // Ground (road)
- const roadTex = new THREE.TextureLoader().load('https://threejs.org/examples/textures/road.jpg');
- roadTex.wrapS = roadTex.wrapT = THREE.RepeatWrapping;
- roadTex.repeat.set(20,20);
- const roadMat = new THREE.MeshStandardMaterial({ map:roadTex });
- const roadGeo = new THREE.PlaneGeometry(200,200);
- const road = new THREE.Mesh(roadGeo, roadMat);
- road.rotation.x = -Math.PI/2;
- scene.add(road);
- // Buildings (brick boxes)
- const brickTex = new THREE.TextureLoader().load('https://threejs.org/examples/textures/brick_diffuse.jpg');
- brickTex.wrapS = brickTex.wrapT = THREE.RepeatWrapping;
- brickTex.repeat.set(1,1);
- const brickMat = new THREE.MeshStandardMaterial({ map:brickTex });
- const makeBuilding = (x,z,w,h,d) => {
- const geo = new THREE.BoxGeometry(w,h,d);
- const mesh = new THREE.Mesh(geo, brickMat);
- mesh.position.set(x, h/2, z);
- scene.add(mesh);
- };
- makeBuilding(-20, -30, 8, 20, 8);
- makeBuilding(15, -25, 12, 30, 12);
- makeBuilding(-35, 10, 10, 25, 10);
- makeBuilding(30, 20, 14, 35, 14);
- // ---------- LOADERS ----------
- const loader = new GLTFLoader();
- // Robot – the player character
- loader.load(
- 'https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb',
- (gltf) => {
- const model = gltf.scene;
- model.scale.set(0.3,0.3,0.3); // make the character smaller
- model.position.set(0,0,0);
- player.add(model); // attach to player group
- // Prepare animation actions (idle + walk)
- const mixer = new THREE.AnimationMixer(model);
- mixers.push(mixer);
- const idleClip = gltf.animations.find(c => c.name.toLowerCase().includes('idle')) || gltf.animations[0];
- const walkClip = gltf.animations.find(c => c.name.toLowerCase().includes('walk')) || gltf.animations[1];
- const idleAction = mixer.clipAction(idleClip);
- const walkAction = mixer.clipAction(walkClip);
- idleAction.play(); // start with idle
- // Store actions for later use
- player.userData.idleAction = idleAction;
- player.userData.walkAction = walkAction;
- assetLoaded();
- },
- undefined,
- (err) => console.error('Robot load error:', err)
- );
- // Car model (static scenery)
- loader.load(
- 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/VC/glTF/VC.gltf',
- (gltf) => {
- const car = gltf.scene;
- car.scale.set(0.5,0.5,0.5);
- car.position.set(5,0,5);
- scene.add(car);
- if (gltf.animations.length) {
- const mixer = new THREE.AnimationMixer(car);
- mixers.push(mixer);
- const clip = gltf.animations[0];
- const action = mixer.clipAction(clip);
- action.play();
- }
- assetLoaded();
- },
- undefined,
- (err) => console.error('Car load error:', err)
- );
- // Resize handling
- window.addEventListener('resize', onWindowResize);
- // Keyboard handling
- document.addEventListener('keydown', (e) => keys[e.code] = true);
- document.addEventListener('keyup', (e) => keys[e.code] = false);
- // Pointer‑lock events (optional)
- document.addEventListener('pointerlockchange', onPointerLockChange);
- document.addEventListener('pointerlockerror', () => {
- console.warn('Pointer lock failed – falling back to mouse‑drag control');
- });
- // ---- On‑screen movement arrows ----
- const touchMap = {
- up: document.getElementById('upBtn'),
- down: document.getElementById('downBtn'),
- left: document.getElementById('leftBtn'),
- right: document.getElementById('rightBtn')
- };
- Object.entries(touchMap).forEach(([dir, btn]) => {
- const press = () => {
- keys[dir] = true;
- requestPointerLock(); // try to lock pointer again
- };
- const release = () => keys[dir] = false;
- btn.addEventListener('mousedown', press);
- btn.addEventListener('touchstart', (e) => { e.preventDefault(); press(); });
- btn.addEventListener('mouseup', release);
- btn.addEventListener('mouseleave', release);
- btn.addEventListener('touchend', (e) => { e.preventDefault(); release(); });
- btn.addEventListener('touchcancel', (e) => { e.preventDefault(); release(); });
- });
- // ---- Mouse‑drag fallback (when pointer‑lock is blocked) ----
- renderer.domElement.addEventListener('mousedown', (e) => {
- if (e.button === 0 && document.pointerLockElement !== renderer.domElement) {
- isDragging = true;
- prevMouse.x = e.clientX;
- prevMouse.y = e.clientY;
- }
- });
- window.addEventListener('mousemove', (e) => {
- if (isDragging) {
- const dx = e.clientX - prevMouse.x;
- prevMouse.x = e.clientX;
- prevMouse.y = e.clientY;
- player.rotation.y -= dx * dragSens;
- }
- });
- window.addEventListener('mouseup', () => { isDragging = false; });
- window.addEventListener('mouseleave', () => { isDragging = false; });
- }
- function requestPointerLock() {
- renderer.domElement.requestPointerLock();
- }
- function onPointerLockChange() {
- const overlay = document.getElementById('clickOverlay');
- const startBtn = document.getElementById('startBtn');
- if (document.pointerLockElement === renderer.domElement) {
- startBtn.style.display = 'none';
- overlay.style.display = 'none';
- } else {
- startBtn.style.display = 'inline-block';
- overlay.style.display = 'flex';
- }
- }
- function assetLoaded() {
- assetsToLoad--;
- if (assetsToLoad === 0) {
- const overlay = document.getElementById('loadingOverlay');
- if (overlay) overlay.style.display = 'none';
- }
- }
- function onWindowResize() {
- camera.aspect = window.innerWidth / window.innerHeight;
- camera.updateProjectionMatrix();
- renderer.setSize(window.innerWidth, window.innerHeight);
- }
- // ---------- ANIMATION LOOP ----------
- function animate() {
- requestAnimationFrame(animate);
- const delta = clock.getDelta();
- // Update all mixers (robot idle/walk, car wheels)
- mixers.forEach(m => m.update(delta));
- // -----------------
- // INPUT & MOVEMENT
- // -----------------
- if (!isPaused) {
- const speed = 5 * delta;
- const move = new THREE.Vector3();
- // Gather movement input
- if (keys['KeyW'] || keys['ArrowUp'] || keys['up']) move.z -= 1;
- if (keys['KeyS'] || keys['ArrowDown'] || keys['down']) move.z += 1;
- if (keys['KeyA'] || keys['ArrowLeft'] || keys['left']) move.x -= 1;
- if (keys['KeyD'] || keys['ArrowRight']|| keys['right']) move.x += 1;
- // ----- JUMP -----
- if (keys['Space'] && isGrounded) {
- verticalVelocity = JUMP_SPEED;
- isGrounded = false;
- }
- // Apply gravity
- verticalVelocity += GRAVITY * delta;
- player.position.y += verticalVelocity * delta;
- // Simple ground collision
- if (player.position.y <= 0) {
- player.position.y = 0;
- verticalVelocity = 0;
- isGrounded = true;
- }
- // ----- MOVEMENT -----
- if (move.lengthSq() > 0) {
- move.normalize();
- // Convert local movement to world space using player's yaw
- const yaw = player.rotation.y;
- const forward = new THREE.Vector3(
- Math.sin(yaw),
- 0,
- Math.cos(yaw)
- );
- const right = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0,1,0));
- const worldMove = new THREE.Vector3();
- worldMove.addScaledVector(forward, -move.z); // forward is -Z in Three.js
- worldMove.addScaledVector(right, move.x);
- worldMove.multiplyScalar(speed);
- player.position.add(worldMove);
- // ---- WALK ANIMATION ----
- if (player.userData.walkAction && player.userData.idleAction) {
- player.userData.idleAction.stop();
- player.userData.walkAction.play();
- }
- } else {
- // ---- IDLE ANIMATION ----
- if (player.userData.walkAction && player.userData.idleAction) {
- player.userData.walkAction.stop();
- player.userData.idleAction.play();
- }
- }
- }
- // -----------------
- // CAMERA FOLLOW
- // -----------------
- const offsetWorld = cameraOffset.clone().applyAxisAngle(new THREE.Vector3(0,1,0), player.rotation.y);
- camera.position.copy(player.position).add(offsetWorld);
- camera.lookAt(player.position);
- // -----------------
- // ROTATION FROM POINTER LOCK
- // -----------------
- if (document.pointerLockElement === renderer.domElement) {
- const onMouseMove = (e) => {
- player.rotation.y -= e.movementX * 0.002;
- };
- // Attach once per frame (the listener will be removed automatically when lock ends)
- document.addEventListener('mousemove', onMouseMove);
- }
- renderer.render(scene, camera);
- }
- </script>
- </head>
- <body>
- <!-- UI elements are injected by the script above -->
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment