🎮ArcadeLab

City Drive Open World

by GoldenKnight36
1480 lines20.8 KB
▶ Play
<!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">
<title>City Drive Open World</title>

<style>
*{
  margin:0;
  padding:0;
  box-sizing:border-box;
}

html,body{
  width:100%;
  height:100%;
  overflow:hidden;
  background:#79a9c7;
  font-family:Arial,sans-serif;
  user-select:none;
}

canvas{
  display:block;
}

#ui{
  position:fixed;
  inset:0;
  pointer-events:none;
  z-index:20;
}

#title{
  position:absolute;
  top:18px;
  left:20px;
  color:white;
  font-size:20px;
  font-weight:bold;
  text-shadow:0 2px 5px #000;
}

#stats{
  position:absolute;
  top:18px;
  right:20px;
  text-align:right;
  color:white;
  text-shadow:0 2px 5px #000;
}

#speed{
  font-size:32px;
  font-weight:bold;
}

#mode{
  font-size:14px;
  margin-top:3px;
  opacity:.9;
}

#help{
  position:absolute;
  left:20px;
  bottom:20px;
  padding:10px 14px;
  background:rgba(0,0,0,.55);
  color:white;
  border-radius:9px;
  font-size:13px;
  line-height:1.6;
}

#message{
  position:absolute;
  left:50%;
  bottom:100px;
  transform:translateX(-50%);
  color:white;
  background:rgba(0,0,0,.65);
  padding:10px 18px;
  border-radius:20px;
  opacity:0;
  transition:.2s;
  font-weight:bold;
}

#cross{
  position:absolute;
  left:50%;
  top:50%;
  transform:translate(-50%,-50%);
  color:white;
  font-size:20px;
  text-shadow:0 1px 3px black;
}

#mini{
  position:absolute;
  left:20px;
  top:65px;
  width:130px;
  height:130px;
  border-radius:12px;
  background:rgba(0,0,0,.7);
  border:2px solid rgba(255,255,255,.7);
}

#mobile{
  display:none;
}

@media(max-width:700px){
  #help{
    display:none;
  }

  #mobile{
    display:block;
    position:absolute;
    inset:0;
  }

  .mb{
    position:absolute;
    width:62px;
    height:62px;
    border-radius:50%;
    border:1px solid rgba(255,255,255,.4);
    background:rgba(0,0,0,.45);
    color:white;
    font-size:25px;
    pointer-events:auto;
    touch-action:none;
  }

  #up{
    bottom:135px;
    left:85px;
  }

  #down{
    bottom:35px;
    left:85px;
  }

  #left{
    bottom:85px;
    left:20px;
  }

  #right{
    bottom:85px;
    left:150px;
  }

  #enter{
    right:25px;
    bottom:75px;
    width:78px;
    height:78px;
    font-size:14px;
  }

  #boost{
    right:25px;
    bottom:170px;
    width:65px;
    height:65px;
    font-size:13px;
  }
}
</style>
</head>

<body>

<div id="ui">

  <div id="title">
    CITY DRIVE
  </div>

  <div id="stats">
    <div id="speed">0 KM/H</div>
    <div id="mode">ON FOOT</div>
  </div>

  <canvas id="mini" width="130" height="130"></canvas>

  <div id="cross">+</div>

  <div id="help">
    WASD = Move / Drive<br>
    SHIFT = Boost<br>
    E = Enter / Exit Car<br>
    Mouse = Look Around
  </div>

  <div id="message"></div>

  <div id="mobile">
    <button class="mb" id="up">▲</button>
    <button class="mb" id="down">▼</button>
    <button class="mb" id="left">◀</button>
    <button class="mb" id="right">▶</button>
    <button class="mb" id="boost">BOOST</button>
    <button class="mb" id="enter">ENTER</button>
  </div>

</div>

<script>
(function(){

"use strict";

const THREE = window.THREE;

if(!THREE){
  document.body.innerHTML =
    '<div style="color:white;background:#111;height:100vh;display:flex;align-items:center;justify-content:center;font:20px Arial;text-align:center;padding:30px">Three.js did not load. Please refresh the ArcadeLab preview.</div>';
  return;
}

/* =========================
   BASIC SETUP
========================= */

const scene = new THREE.Scene();

scene.background = new THREE.Color(0x79a9c7);

scene.fog = new THREE.Fog(
  0x79a9c7,
  180,
  850
);

const camera = new THREE.PerspectiveCamera(
  65,
  innerWidth/innerHeight,
  0.1,
  1200
);

const renderer = new THREE.WebGLRenderer({
  antialias:true,
  powerPreference:"high-performance"
});

renderer.setPixelRatio(
  Math.min(devicePixelRatio,1.5)
);

renderer.setSize(
  innerWidth,
  innerHeight
);

renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;

document.body.appendChild(renderer.domElement);


/* =========================
   LIGHTING
========================= */

const hemi = new THREE.HemisphereLight(
  0xbfe4ff,
  0x59634e,
  2.1
);

scene.add(hemi);

const sun = new THREE.DirectionalLight(
  0xffffff,
  2.2
);

sun.position.set(
  150,
  260,
  100
);

sun.castShadow=true;

sun.shadow.mapSize.width=1024;
sun.shadow.mapSize.height=1024;

sun.shadow.camera.left=-300;
sun.shadow.camera.right=300;
sun.shadow.camera.top=300;
sun.shadow.camera.bottom=-300;

scene.add(sun);


/* =========================
   WORLD
========================= */

const WORLD=2400;
const ROAD=240;

const groundMat =
  new THREE.MeshStandardMaterial({
    color:0x55724d,
    roughness:1
  });

const ground =
  new THREE.Mesh(
    new THREE.PlaneGeometry(WORLD,WORLD),
    groundMat
  );

ground.rotation.x=-Math.PI/2;
ground.receiveShadow=true;

scene.add(ground);


/* =========================
   MATERIAL HELPERS
========================= */

function mat(color){
  return new THREE.MeshStandardMaterial({
    color:color,
    roughness:.82
  });
}

const roadMat=mat(0x25282b);
const sidewalkMat=mat(0x777878);
const whiteMat=mat(0xe9e9e9);
const darkMat=mat(0x151719);
const glassMat=
  new THREE.MeshStandardMaterial({
    color:0x355468,
    roughness:.25,
    metalness:.25
  });


/* =========================
   ROADS
========================= */

function makeRoads(){

  const roadSize=WORLD;

  for(
    let x=-WORLD/2;
    x<=WORLD/2;
    x+=ROAD
  ){

    const r=new THREE.Mesh(
      new THREE.BoxGeometry(
        82,
        .08,
        roadSize
      ),
      roadMat
    );

    r.position.set(x,.04,0);
    r.receiveShadow=true;
    scene.add(r);

    const s1=new THREE.Mesh(
      new THREE.BoxGeometry(
        7,
        .1,
        roadSize
      ),
      sidewalkMat
    );

    s1.position.set(x-48,.07,0);
    scene.add(s1);

    const s2=s1.clone();
    s2.position.x=x+48;
    scene.add(s2);

    for(
      let z=-WORLD/2;
      z<WORLD/2;
      z+=35
    ){

      const line=new THREE.Mesh(
        new THREE.BoxGeometry(
          2,
          .11,
          18
        ),
        whiteMat
      );

      line.position.set(
        x,
        .11,
        z
      );

      scene.add(line);
    }
  }


  for(
    let z=-WORLD/2;
    z<=WORLD/2;
    z+=ROAD
  ){

    const r=new THREE.Mesh(
      new THREE.BoxGeometry(
        roadSize,
        .08,
        82
      ),
      roadMat
    );

    r.position.set(0,.05,z);
    r.receiveShadow=true;

    scene.add(r);

    const s1=new THREE.Mesh(
      new THREE.BoxGeometry(
        roadSize,
        .1,
        7
      ),
      sidewalkMat
    );

    s1.position.set(0,.08,z-48);
    scene.add(s1);

    const s2=s1.clone();
    s2.position.z=z+48;
    scene.add(s2);

    for(
      let x=-WORLD/2;
      x<WORLD/2;
      x+=35
    ){

      const line=new THREE.Mesh(
        new THREE.BoxGeometry(
          18,
          .11,
          2
        ),
        whiteMat
      );

      line.position.set(
        x,
        .11,
        z
      );

      scene.add(line);
    }
  }
}

makeRoads();


/* =========================
   BUILDINGS
========================= */

const buildingColors=[
  0x59636b,
  0x746e67,
  0x656b70,
  0x80786b,
  0x4f5962,
  0x6d7072
];

const buildings=[];

function makeBuilding(x,z){

  const w=55+Math.random()*35;
  const d=55+Math.random()*35;
  const h=35+Math.random()*100;

  const g=new THREE.BoxGeometry(
    w,h,d
  );

  const b=new THREE.Mesh(
    g,
    mat(
      buildingColors[
        Math.floor(
          Math.random()*buildingColors.length
        )
      ]
    )
  );

  b.position.set(
    x,
    h/2,
    z
  );

  b.castShadow=true;
  b.receiveShadow=true;

  scene.add(b);

  buildings.push(b);


  /* roof */

  if(Math.random()<.45){

    const roof=new THREE.Mesh(
      new THREE.BoxGeometry(
        w*.5,
        4,
        d*.5
      ),
      darkMat
    );

    roof.position.set(
      x,
      h+2,
      z
    );

    roof.castShadow=true;

    scene.add(roof);
  }
}


/* City blocks */

for(
  let x=-WORLD/2+120;
  x<WORLD/2-120;
  x+=ROAD
){

  for(
    let z=-WORLD/2+120;
    z<WORLD/2-120;
    z+=ROAD
  ){

    const spots=[
      [-65,-65],
      [65,-65],
      [-65,65],
      [65,65]
    ];

    for(const p of spots){

      if(Math.random()<.78){

        makeBuilding(
          x+p[0],
          z+p[1]
        );
      }
    }
  }
}


/* =========================
   TREES
========================= */

function makeTree(x,z){

  const trunk=new THREE.Mesh(
    new THREE.CylinderGeometry(
      2.5,3,14,8
    ),
    mat(0x68482d)
  );

  trunk.position.set(
    x,
    7,
    z
  );

  trunk.castShadow=true;

  scene.add(trunk);


  const crown=new THREE.Mesh(
    new THREE.SphereGeometry(
      11,
      8,
      7
    ),
    mat(
      Math.random()<.5
      ?0x285b32
      :0x34733b
    )
  );

  crown.position.set(
    x,
    19,
    z
  );

  crown.castShadow=true;

  scene.add(crown);
}


for(let i=0;i<110;i++){

  const x=
    Math.random()*WORLD-WORLD/2;

  const z=
    Math.random()*WORLD-WORLD/2;

  const rx=Math.abs(
    ((x+WORLD/2)%ROAD)-ROAD/2
  );

  const rz=Math.abs(
    ((z+WORLD/2)%ROAD)-ROAD/2
  );

  if(rx>55 && rz>55){
    makeTree(x,z);
  }
}


/* =========================
   CAR CREATION
========================= */

const carColors=[
  0xc52b2b,
  0x1e4d9c,
  0xe4e4e4,
  0x17191b,
  0xd59b27,
  0x2e7440
];

function createCar(color){

  const group=new THREE.Group();


  const body=new THREE.Mesh(
    new THREE.BoxGeometry(
      12,
      4,
      24
    ),
    mat(color)
  );

  body.position.y=5;

  body.castShadow=true;

  group.add(body);


  const cabin=new THREE.Mesh(
    new THREE.BoxGeometry(
      9,
      4.5,
      11
    ),
    glassMat
  );

  cabin.position.set(
    0,
    8,
    -1
  );

  cabin.castShadow=true;

  group.add(cabin);


  const front=new THREE.Mesh(
    new THREE.BoxGeometry(
      9.5,
      1.5,
      3
    ),
    mat(0x222222)
  );

  front.position.set(
    0,
    7,
    -8
  );

  group.add(front);


  const wheelGeo=
    new THREE.CylinderGeometry(
      2.7,
      2.7,
      2,
      12
    );

  const wheelMat=mat(0x111111);

  const wheels=[];

  for(const x of [-7,7]){

    for(const z of [-7.5,7.5]){

      const w=new THREE.Mesh(
        wheelGeo,
        wheelMat
      );

      w.rotation.z=Math.PI/2;

      w.position.set(
        x,
        3,
        z
      );

      w.castShadow=true;

      group.add(w);
      wheels.push(w);
    }
  }

  group.userData.wheels=wheels;

  return group;
}


/* =========================
   PLAYER CAR
========================= */

const player={
  x:0,
  z:20,
  angle:0,
  speed:0,
  inCar:false,
  car:null
};

const playerCar=createCar(
  0xb92c2c
);

playerCar.position.set(
  player.x,
  0,
  player.z
);

scene.add(playerCar);

player.car=playerCar;


/* =========================
   TRAFFIC
========================= */

const traffic=[];

function addTraffic(x,z,angle){

  const c=createCar(
    carColors[
      Math.floor(
        Math.random()*carColors.length
      )
    ]
  );

  c.position.set(
    x,0,z
  );

  c.rotation.y=angle;

  c.userData.speed=
    12+Math.random()*10;

  c.userData.angle=angle;

  scene.add(c);

  traffic.push(c);
}


for(let i=0;i<28;i++){

  const horizontal=
    Math.random()<.5;

  const roadIndex=
    Math.floor(
      Math.random()*8
    )-4;

  if(horizontal){

    addTraffic(
      Math.random()*1800-900,
      roadIndex*ROAD,
      Math.random()<.5?0:Math.PI
    );

  }else{

    addTraffic(
      roadIndex*ROAD,
      Math.random()*1800-900,
      Math.PI/2
    );
  }
}


/* =========================
   PLAYER CHARACTER
========================= */

const person=new THREE.Group();

const body=new THREE.Mesh(
  new THREE.CapsuleGeometry(
    2.2,
    6,
    4,
    8
  ),
  mat(0x263b58)
);

body.position.y=7;

person.add(body);


const head=new THREE.Mesh(
  new THREE.SphereGeometry(
    2.3,
    12,
    8
  ),
  mat(0xc98d67)
);

head.position.y=14;

person.add(head);


person.position.set(
  player.x,
  0,
  player.z+25
);

scene.add(person);


/* =========================
   INPUT
========================= */

const keys={};

addEventListener(
  "keydown",
  function(e){

    keys[e.key.toLowerCase()]=true;

    if(e.key.toLowerCase()==="e"){
      enterExitCar();
    }
  }
);

addEventListener(
  "keyup",
  function(e){
    keys[e.key.toLowerCase()]=false;
  }
);


/* =========================
   MOBILE BUTTONS
========================= */

function bindButton(id,key){

  const b=document.getElementById(id);

  if(!b)return;

  b.addEventListener(
    "pointerdown",
    e=>{
      e.preventDefault();
      keys[key]=true;
    }
  );

  b.addEventListener(
    "pointerup",
    e=>{
      e.preventDefault();
      keys[key]=false;
    }
  );

  b.addEventListener(
    "pointercancel",
    ()=>{
      keys[key]=false;
    }
  );

  b.addEventListener(
    "pointerleave",
    ()=>{
      keys[key]=false;
    }
  );
}

bindButton("up","w");
bindButton("down","s");
bindButton("left","a");
bindButton("right","d");
bindButton("boost","shift");

document.getElementById(
  "enter"
).addEventListener(
  "pointerdown",
  e=>{
    e.preventDefault();
    enterExitCar();
  }
);


/* =========================
   MOUSE CAMERA
========================= */

let dragging=false;
let lastX=0;
let cameraYaw=0;
let cameraPitch=.42;

renderer.domElement.addEventListener(
  "pointerdown",
  e=>{
    dragging=true;
    lastX=e.clientX;
  }
);

addEventListener(
  "pointerup",
  ()=>{
    dragging=false;
  }
);

addEventListener(
  "pointermove",
  e=>{

    if(!dragging)return;

    const dx=e.clientX-lastX;

    lastX=e.clientX;

    cameraYaw-=dx*.006;

    cameraPitch=
      Math.max(
        .2,
        Math.min(
          .8,
          cameraPitch
        )
      );
  }
);


/* =========================
   ENTER / EXIT
========================= */

function enterExitCar(){

  if(player.inCar){

    player.inCar=false;

    player.speed=0;

    person.visible=true;

    person.position.set(
      player.x+12,
      0,
      player.z
    );

    showMessage(
      "Exited vehicle"
    );

    return;
  }


  let closest=null;
  let closestDist=40;

  for(const c of traffic){

    const d=
      c.position.distanceTo(
        new THREE.Vector3(
          player.x,
          0,
          player.z
        )
      );

    if(d<closestDist){

      closest=c;
      closestDist=d;
    }
  }


  if(
    player.car.position.distanceTo(
      new THREE.Vector3(
        player.x,
        0,
        player.z
      )
    )<40
  ){
    closest=player.car;
  }


  if(closest){

    if(closest!==player.car){

      player.car.position.copy(
        closest.position
      );

      player.car.rotation.copy(
        closest.rotation
      );

      scene.remove(closest);

      traffic.splice(
        traffic.indexOf(closest),
        1
      );
    }

    player.x=
      player.car.position.x;

    player.z=
      player.car.position.z;

    player.angle=
      player.car.rotation.y;

    player.inCar=true;

    person.visible=false;

    showMessage(
      "Vehicle entered"
    );
  }
}


/* =========================
   MESSAGE
========================= */

let messageTimer=0;

function showMessage(t){

  const el=
    document.getElementById(
      "message"
    );

  el.textContent=t;
  el.style.opacity=1;

  messageTimer=2;
}


/* =========================
   MOVEMENT
========================= */

const clock=new THREE.Clock();

function update(dt){

  if(player.inCar){

    let acceleration=0;

    if(keys["w"])
      acceleration+=55;

    if(keys["s"])
      acceleration-=45;

    player.speed+=
      acceleration*dt;

    if(!keys["w"] && !keys["s"]){
      player.speed*=
        Math.pow(.12,dt);
    }

    const maxSpeed=
      keys["shift"]?115:82;

    player.speed=
      Math.max(
        -35,
        Math.min(
          maxSpeed,
          player.speed
        )
      );


    const steering=
      (keys["a"]?-1:0)+
      (keys["d"]?1:0);

    if(Math.abs(player.speed)>2){

      player.angle+=
        steering*
        dt*
        1.8*
        Math.min(
          Math.abs(player.speed)/45,
          1.5
        )*
        (player.speed>=0?1:-1);
    }


    player.x+=
      Math.sin(player.angle)*
      player.speed*
      dt;

    player.z+=
      Math.cos(player.angle)*
      player.speed*
      dt;


    player.x=
      THREE.MathUtils.clamp(
        player.x,
        -WORLD/2+20,
        WORLD/2-20
      );

    player.z=
      THREE.MathUtils.clamp(
        player.z,
        -WORLD/2+20,
        WORLD/2-20
      );


    player.car.position.set(
      player.x,
      0,
      player.z
    );

    player.car.rotation.y=
      player.angle;


    for(
      const w of player.car.userData.wheels
    ){
      w.rotation.x-=
        player.speed*dt*.8;
    }


  }else{

    let dx=0;
    let dz=0;

    if(keys["w"])dz-=1;
    if(keys["s"])dz+=1;
    if(keys["a"])dx-=1;
    if(keys["d"])dx+=1;


    if(dx||dz){

      const len=
        Math.hypot(dx,dz);

      dx/=len;
      dz/=len;

      const sp=
        keys["shift"]?13:7;

      player.x+=dx*sp*dt;
      player.z+=dz*sp*dt;

      player.x=
        THREE.MathUtils.clamp(
          player.x,
          -WORLD/2+20,
          WORLD/2-20
        );

      player.z=
        THREE.MathUtils.clamp(
          player.z,
          -WORLD/2+20,
          WORLD/2-20
        );

      person.position.set(
        player.x,
        0,
        player.z
      );

      person.rotation.y=
        Math.atan2(
          dx,
          dz
        );
    }

    player.speed=0;
  }


  /* traffic */

  for(const c of traffic){

    const s=c.userData.speed;

    c.position.x+=
      Math.sin(
        c.userData.angle
      )*s*dt;

    c.position.z+=
      Math.cos(
        c.userData.angle
      )*s*dt;


    if(c.position.x>WORLD/2)
      c.position.x=-WORLD/2;

    if(c.position.x<-WORLD/2)
      c.position.x=WORLD/2;

    if(c.position.z>WORLD/2)
      c.position.z=-WORLD/2;

    if(c.position.z<-WORLD/2)
      c.position.z=WORLD/2;
  }


  /* message timer */

  if(messageTimer>0){

    messageTimer-=dt;

    if(messageTimer<=0){
      document.getElementById(
        "message"
      ).style.opacity=0;
    }
  }


  /* HUD */

  document.getElementById(
    "speed"
  ).textContent=
    Math.round(
      Math.abs(player.speed)*1.25
    )+" KM/H";

  document.getElementById(
    "mode"
  ).textContent=
    player.inCar
    ?"DRIVING"
    :"ON FOOT";
}


/* =========================
   CAMERA
========================= */

function updateCamera(){

  let targetX=
    player.inCar
    ?player.x
    :person.position.x;

  let targetZ=
    player.inCar
    ?player.z
    :person.position.z;


  const distance=
    player.inCar?70:48;

  const height=
    player.inCar?34:25;


  const desiredX=
    targetX+
    Math.sin(cameraYaw)*
    distance;

  const desiredZ=
    targetZ+
    Math.cos(cameraYaw)*
    distance;


  camera.position.x+=
    (desiredX-camera.position.x)*.08;

  camera.position.y+=
    (height-camera.position.y)*.08;

  camera.position.z+=
    (desiredZ-camera.position.z)*.08;


  camera.lookAt(
    targetX,
    5,
    targetZ
  );
}


/* =========================
   MINIMAP
========================= */

const mini=
  document.getElementById("mini");

const mctx=
  mini.getContext("2d");

function drawMini(){

  const w=130;

  mctx.clearRect(
    0,0,w,w
  );

  mctx.fillStyle="#24352b";

  mctx.fillRect(
    0,0,w,w
  );


  const scale=
    w/WORLD;


  mctx.fillStyle="#444";

  for(
    let x=-WORLD/2;
    x<=WORLD/2;
    x+=ROA

Game Source: City Drive Open World

Creator: GoldenKnight36

Libraries: none

Complexity: complex (1480 lines, 20.8 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: city-drive-open-world-goldenknight36" to link back to the original. Then publish at arcadelab.ai/publish.