トップへ(mam-mam.net/)

Build a Batting Game with Three.js | Playable 3D Baseball Experience on the Web

Japanese

3D Batting Game on the Web (Three.js [r145])

Using JavaScript and Three.js, I created a 3D batting game playable directly in the web browser.
In this article, we explain the implementation of the bat-swinging motion and collision detection with the ball, covering the basic structure of a baseball game with implementation code included.
Recommended for those who want to try making a game with three.js or learn how a baseball game works.

Press the "Swing Bat" button to hit the ball.
If you connect well with the ball, it becomes a home run.
Press the "View 1", "View 2", "View 3", or "View 4" buttons to change the perspective.

View 1
View 2
View 3
View 4
Swing Bat

Source Code for the 3D Batting Game (Three.js [r145])

<canvas id="can" style="display:block;width:1000px; height:500px; max-width:calc(100vw - 32px);max-height:calc((100vw - 32px) * 500 / 1000);margin:0;padding:0;cursor:grab;" width="1000" height="500"></canvas>
<div style="display:flex">
  <div>
    <a onclick="cPos=0;changePos();" class="bt101">View 1</a><br>
    <a onclick="cPos=1;changePos();" class="bt101">View 2</a><br>
    <a onclick="cPos=2;changePos();" class="bt101">View 3</a><br>
    <a onclick="cPos=3;changePos();" class="bt101">View 4</a><br>
  </div>
  <div>
    <a onMousedown="furu()" onTouchStart="furu()" class="bt102">Swing Bat</a>
  </div>
</div>
<div class="info" style="font-size:32px;height:1.2em;"></div>
<style>
  .bt102{
    display:inline-block;
    margin:4px;
    padding:1.5em 1em;
    color:#000;
    text-shadow:1px 1px 2px #999;
    background:#fff;
    border:0px none #000;
    text-decoration:none;
    box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.4);
    cursor:pointer;
    border-radius:10px 10px 10px 10px;
    vertical-align:middle;
    user-select: none;
    font-size:24px;
  }
  .bt102:active{
    box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.4) inset;
  }

  .bt101{
    display:inline-block;
    margin:4px;
    padding:0.1em 0.5em;
    color:#000;
    text-shadow:1px 1px 2px #999;
    background:#fff;
    border:0px none #000;
    text-decoration:none;
    box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.4);
    cursor:pointer;
    border-radius:10px 10px 10px 10px;
    vertical-align:middle;
    user-select: none;
    font-size:24px;
  }
  .bt101:active{
    box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.4) inset;
  }
</style>

<script>
  var controls;
  var hh=0;
  var bstep=0;//bat
  var gBat;
  var scene,camera;
  var step=0;//0~15:rotation,15~
  var bf={x:0, y:2, z:16};
  var v= {x:0, y:1, z:-1};
  var cam=[
    {x:0, y:2.0, z:66},
    {x:0, y:4.0, z:66},
    {x:-40, y:4, z:50},
    {x:0, y:30, z:120},
  ];
  var tar=[
    {x:0, y:1.8, z:50},
    {x:0, y:1.6, z:50},
    {x:0, y:1.6, z:50},
    {x:0, y:1.6, z:20},
  ];
  cPos=1;

  var main = function () {
    //Get target canvas and retrieve width and height
    let can=document.getElementById('can');
    let w = parseInt(can.style.width);//get width
    let h = parseInt(can.style.height);//get height

    //■Scene
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0xccddff);

    // ■ Camera
    // THREE.PerspectiveCamera( field of view (degrees), aspect ratio, near clipping plane, far clipping plane );
    camera = new THREE.PerspectiveCamera(20, w/h, 0.1, 10000);
    camera.position.set(0, 1.8, 66);// set camera position
    //camera.rotation.set(0,0,0);// set camera angle to (0°,0°,0°)
    camera.lookAt(new THREE.Vector3(0, 0, 0));// set camera direction to (0,0,0)
    scene.add(camera);// add camera to scene

    // ■ Renderer
    renderer = new THREE.WebGLRenderer({canvas:can, antialias: true});// enable antialiasing
    renderer.setSize(w, h);
    // Enable shadow map
    renderer.shadowMap.enabled = true;
    renderer.setClearColor(0x000000, 1);// set background color

    // ■ Light ------------------------------------------------
    // Ambient light (light applied to all objects from all directions). Prevents unlit areas from being completely black.
    let AmbientLight=new THREE.AmbientLight(0xffffff,0.4);
    scene.add( AmbientLight );// add ambient light to scene

    // Spotlight (color, intensity, distance, angle, penumbra[0-1], decay[1,1])
    let AmbientLight=new THREE.AmbientLight(0xffffff,0.4);
    scene.add( AmbientLight );// add ambient light to scene

    // Spotlight (color, intensity, distance, angle, penumbra[0-1], decay[1,1])
    let spot1=new THREE.SpotLight(0xffffff, 0.6,1000,Math.PI/4, 0.4, 1);
    spot1.position.set(0,100,50);
    spot1.target.position.set(0,0,50);
    spot1.castShadow=true;// enable shadows
    spot1.shadow.mapSize.width = 4096;  // high quality shadows
    spot1.shadow.mapSize.height = 4096; // high quality shadows
    spot1.shadow.radius=4;  // soften shadows (when renderer.shadowMap.type=THREE.PCFShadowMap)
    scene.add(spot1);
    scene.add(spot1.target);
    // Spotlight (color, intensity, distance, angle, penumbra[0-1], decay[1,1])
    let spot2=new THREE.SpotLight(0xffffff, 0.6,1000,Math.PI/4, 0.4, 1);
    spot2.position.set(0,100,-50);
    spot2.target.position.set(0,0,-50);
    spot2.castShadow=true;// enable shadows
    spot2.shadow.mapSize.width = 4096;  // high quality shadows
    spot2.shadow.mapSize.height = 4096; // high quality shadows
    spot2.shadow.radius=4;  // soften shadows (when renderer.shadowMap.type=THREE.PCFShadowMap)
    scene.add(spot2);
    scene.add(spot2.target);

    // Ground
    // Load texture
    let tx1 = new THREE.TextureLoader().load("./imgs/field.jpg");
    let mat1=new THREE.MeshLambertMaterial({
      color:0xffffff, map:tx1 ,side:THREE.DoubleSide,
      transparent:true, opacity:1.0,
    });
    let pg=new THREE.PlaneGeometry(140,140,100,100);
    let gm=new THREE.Mesh(pg,mat1);
    gm.receiveShadow=true;// display shadows cast onto ground
    gm.rotation.x=Math.PI/2;
    gm.rotation.z=Math.PI;
    scene.add(gm);


    // ■ Create group
    g0=new THREE.Group;
    // ■ Create box geometry (width, height, depth)
    let boxGeometry=new THREE.BoxGeometry(1,1,1);
    let mat2=new THREE.MeshLambertMaterial({
      color:0x666666,
      side:THREE.FrontSide,
      transparent:false, opacity:1.0,
      wireframe:false, wireframeLinewidth:0,
    });
    let mesh11=new THREE.Mesh(boxGeometry,mat2);
    mesh11.scale.set(0.2 ,1, 0.2);
    mesh11.position.set(-0.2,0.5,0);
    mesh11.castShadow=true;
    g0.add(mesh11);
    let mesh12=new THREE.Mesh(boxGeometry,mat2);
    mesh12.scale.set(0.2 ,1, 0.2);
    mesh12.position.set( 0.2,0.5,0);
    mesh12.castShadow=true;
    g0.add(mesh12);
    let mesh13=new THREE.Mesh(boxGeometry,mat2);
    mesh13.scale.set(0.6 ,1, 0.2);
    mesh13.position.set( 0,1.5,0);
    mesh13.castShadow=true;
    g0.add(mesh13);
    let mesh14=new THREE.Mesh(boxGeometry,mat2);
    mesh14.scale.set(0.3 ,0.3, 0.3);
    mesh14.position.set( 0,2.1,0);
    mesh14.castShadow=true;
    g0.add(mesh14);
    scene.add(g0);
    g0.position.set(0.6,0,16);

    // Left arm
    g1=new THREE.Group;
    let mesh15=new THREE.Mesh(boxGeometry,mat2);
    mesh15.scale.set(0.2 ,0.8, 0.2);
    mesh15.position.set( 0,-0.4,0);
    g1.add(mesh15);
    g1.position.set(0.9,1.94,16);
    g1.rotation.set(0,0,Math.PI/8);
    scene.add(g1);

    // Right arm
    g2=new THREE.Group;
    let mesh16=new THREE.Mesh(boxGeometry,mat2);
    mesh16.scale.set(0.2 ,0.8, 0.2);
    mesh16.position.set( 0.1,-0.4,0);
    g2.add(mesh16);
    g2.position.set(0,2,0);
    g2.rotation.set(0,0,-Math.PI/8);
    scene.add(g2);
    g2.position.set(0.2,1.94,16);

    // ■ Create sphere geometry (radius, horizontal segments, vertical segments, horizontal start angle, vertical start angle)
    let sphereGeometry=new THREE.SphereGeometry(0.2,16,16,0,Math.PI*2);
    // Create Lambert material (non-glossy material affected by light sources)
    let matB=new THREE.MeshLambertMaterial({color:0xffffff, side:THREE.FrontSide, wireframe:false, wireframeLinewidth:1});
    mb=new THREE.Mesh(sphereGeometry,matB);
    mb.castShadow=true;
    mb.position.set(0,-0.8,0);
    g3=new THREE.Group;
    g3.position.set(bf.x, bf.y, bf.z);
    g3.add(mb);
    scene.add(g3);
    step=0;


    // ■ Bat
    let matBat=new THREE.MeshLambertMaterial({color:0x880000, side:THREE.FrontSide, wireframe:false, wireframeLinewidth:1});
    let points=[
      new THREE.Vector2(0.00, 0.00),
      new THREE.Vector2(0.08, 0.00),
      new THREE.Vector2(0.08, 0.08),
      new THREE.Vector2(0.04, 0.08),
      new THREE.Vector2(0.08, 1.50),
      new THREE.Vector2(0.00, 1.54),
    ];
    // Create lathe (rotational body) geometry by rotating around Y↑ axis (points array, segments, start angle [0], angle [Math.PI*2])
    let latheGeometry = new THREE.LatheGeometry(points, 12, 0, Math.PI*2);
    // Create mesh from geometry and material
    let mBat=new THREE.Mesh(latheGeometry,matBat);
    mBat.receiveShadow=true;
    mBat.castShadow=true;
    gBat=new THREE.Group;
    gBat.add(mBat);
    gBat.position.set(-1.2,1.45,50);
    scene.add(gBat);
    bstep=0;


    // ■ Batter
    g4=new THREE.Group;
    let mat3=new THREE.MeshLambertMaterial({
      color:0xDDDDDD,
      side:THREE.FrontSide,
      transparent:false, opacity:1.0,
      wireframe:false, wireframeLinewidth:0,
    });
    let mesh21=new THREE.Mesh(boxGeometry,mat3);
    mesh21.scale.set(0.2 ,1, 0.2);
    mesh21.position.set(-0.2,0.5,0);
    mesh21.castShadow=true;
    g4.add(mesh21);
    let mesh22=new THREE.Mesh(boxGeometry,mat3);
    mesh22.scale.set(0.2 ,1, 0.2);
    mesh22.position.set( 0.2,0.5,0);
    mesh22.castShadow=true;
    g4.add(mesh22);
    let mesh23=new THREE.Mesh(boxGeometry,mat3);
    mesh23.scale.set(0.6 ,1, 0.2);
    mesh23.position.set( 0,1.5,0);
    mesh23.castShadow=true;
    g4.add(mesh23);
    let mesh24=new THREE.Mesh(boxGeometry,mat3);
    mesh24.scale.set(0.3 ,0.3, 0.4);
    mesh24.position.set( 0,2.1,0);
    mesh24.castShadow=true;
    g4.add(mesh24);
    let mesh25=new THREE.Mesh(boxGeometry,mat3);
    mesh25.scale.set(0.2 ,0.8, 0.2);
    mesh25.position.set(0.2,1.6, 0.3);
    mesh25.rotation.set(-Math.PI*2/6,0,-Math.PI/6);
    g4.add(mesh25);
    let mesh26=new THREE.Mesh(boxGeometry,mat3);
    mesh26.scale.set(0.2 ,0.8, 0.2);
    mesh26.position.set(-0.5,1.6,0.3);
    mesh26.rotation.set(-Math.PI*2/6,0,-Math.PI/6);
    g4.add(mesh26);
    scene.add(g4);
    g4.position.set(-1.8,0,50);
    g4.rotation.set(0,Math.PI/2,0);


    // ★
    camera.position.set(cam[cPos].x, cam[cPos].y, cam[cPos].z);// set camera position
    // Create orbit controls (camera moves with mouse drag, wheel, etc.)
    controls = new THREE.OrbitControls(camera, renderer.domElement);
    // ★
    controls.target.set(tar[cPos].x, tar[cPos].y, tar[cPos].z);
    controls.update();
    controls.enabled=false;
    //renderLoop();
    //setInterval(renderLoop,20);
    setTimeout(renderLoop,20);
  }

  function renderLoop () {
    // g3.position.z = 50 position
    if(hh==0||hh==1){
      if(step<100){
        // do nothing
      }else if(step<120){
        g2.rotation.x+=Math.PI/20;
        g3.rotation.x+=Math.PI/20;
      }else if(step<140){
        g2.rotation.x+=Math.PI/20;
        g3.position.z+=0.3;
        g3.position.y-=0.0115;
      }else if(step<=250){
        g2.rotation.x=0;
        g3.position.z+=0.3;
        g3.position.y-=0.0115;
      }else if(step<=320){
        g2.rotation.x=0;
        g3.position.z+=0.3;
        g3.position.y-=0.0115;
        if(hh==0){
          hh=1;
          document.querySelector(".info").innerHTML="Strike";
        }
      }else{
        g2.rotation.x=0;
        g3.rotation.x=0;
        g3.position.x=bf.x;
        g3.position.y=bf.y;
        g3.position.z=bf.z;
        step=0;
        hh=0;
        document.querySelector(".info").innerHTML="";
      }
    }else if(hh==2 || hh==3){
      v.y-=0.005;
      g3.position.x+=v.x;
      g3.position.y+=v.y;
      g3.position.z+=v.z;
      if(g3.position.y<-0.7){
        g3.position.y=-0.7;
        v.y=-v.y*0.5;
        v.z=v.z*0.8;
        v.x=v.x*0.8;
      }
      controls.target.x=g3.position.x;
      controls.target.y=g3.position.y;
      controls.target.z=g3.position.z;
      controls.update();
      if(step>300){
        hh=0;
        step=0;
        g2.rotation.x=0;
        g3.rotation.x=0;
        g3.position.x=bf.x;
        g3.position.y=bf.y;
        g3.position.z=bf.z;
        // ★
        controls.target.set(tar[cPos].x, tar[cPos].y, tar[cPos].z);
        camera.position.set(cam[cPos].x, cam[cPos].y, cam[cPos].z);// set camera position
        controls.update();
        document.querySelector(".info").innerHTML="";
      }
    }
    step++;
    
    if(bstep==0){
      // reset bat rotation
      gBat.rotation.set(0,0,0);
    }else{
      if(bstep<7){
        // bat rotation
        gBat.rotation.x=Math.PI/2/6*bstep;
      }else if(bstep==7&&(hh!=2&&hh!=3)){
        if(g3.position.z>49.7 && g3.position.z<50.3){
          hh=3;
          document.querySelector(".info").innerHTML="Home Run";
          v.x=(g3.position.z-50)/4;
          v.y=0.5;
          v.z=-0.5;
          step=0;
        }else if(g3.position.z>48.2 && g3.position.z<51.8){
          hh=2;
          document.querySelector(".info").innerHTML="Hit";
          v.x=(g3.position.z-50)/8;
          v.y=0.3;
          v.z=-0.4;
          step=0;
        }else{
          hh=1;
          document.querySelector(".info").innerHTML="Strike";
        }
      }
      gBat.rotation.z=-Math.PI/2/6*bstep;
      bstep++;javascript:furu()
      if(bstep>18){bstep=0;}
    }
    
    renderer.render( scene, camera );
    setTimeout(renderLoop,20);
  }

  window.addEventListener( 'DOMContentLoaded', main, false );
  function furu(){
    if(bstep==0){
      bstep=1;
    }
  }

  // Switch viewpoint
  function changePos(){
    controls.target.set(tar[cPos].x, tar[cPos].y, tar[cPos].z);
    camera.position.set(cam[cPos].x, cam[cPos].y, cam[cPos].z);// set camera position
    controls.update();
  }
</script>

Back to the list of 3D content with JavaScript