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

Three.jsで作るバッティングゲーム|Webで遊べる3D野球体験

English

Webサイトで3Dバッティングゲーム(Three.js[r145])

JavaScriptとThree.jsを使って、Webブラウザ上で遊べる3Dバッティングゲームを作ってみました。
本記事では、バットを振る動作の実装ボールとの当たり判定など、野球ゲームの基本構成を実装コード付きで解説します。
「three.jsでゲームを作ってみたい」「野球ゲームの仕組みを学びたい」という方におすすめの内容です。

「バットを振る」ボタンを押してボールを打ちます。
上手にボールに当てるとホームランになります。
「視点1」、「視点2」、「視点3」、「視点4」ボタンを押すと、視点を変えることができます。

視点1
視点2
視点3
視点4
バットを振る

3Dバッティングゲームのソースコード(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">視点1</a><br>
    <a onclick="cPos=1;changePos();" class="bt101">視点2</a><br>
    <a onclick="cPos=2;changePos();" class="bt101">視点3</a><br>
    <a onclick="cPos=3;changePos();" class="bt101">視点4</a><br>
  </div>
  <div>
    <a onMousedown="furu()" onTouchStart="furu()" class="bt102">バットを振る</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:回転,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 () {
    //描画先canvasを取得して幅と高さ取得
    let can=document.getElementById('can');
    let w = parseInt(can.style.width);//幅取得
    let h = parseInt(can.style.height);//高さ取得

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

    //■カメラ
    //THREE.PerspectiveCamera( 画角(度), アスペクト比, カメラに写る最短距離, カメラに写る最長距離 );
    camera = new THREE.PerspectiveCamera(20, w/h, 0.1, 10000);
    camera.position.set(0, 1.8, 66);//カメラの位置を設定
    //camera.rotation.set(0,0,0);//カメラの角度を(0度,0度,0度)にする
    camera.lookAt(new THREE.Vector3(0, 0, 0));//カメラの向きを(0,0,0)座標にする
    scene.add(camera);//カメラをシーンに追加

    //■レンダラー
    renderer = new THREE.WebGLRenderer({canvas:can, antialias: true});//アンチエイリアス有効
    renderer.setSize(w, h);
    //シャドー(影)マップを有効にする
    renderer.shadowMap.enabled = true;
    renderer.setClearColor(0x000000, 1);//背景色を設定

    //■光源------------------------------------------------
    //環境光(すべての物体にすべての方向から与える光)光の当たらない部分でも真っ黒にならない
    let AmbientLight=new THREE.AmbientLight(0xffffff,0.4);
    scene.add( AmbientLight );//環境光をシーンに追加

    //スポットライト(色,強さ,距離,角度(angle),半影[0-1],減衰[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;//影を表示可能に
    spot1.shadow.mapSize.width = 4096;  //影をきれいにする
    spot1.shadow.mapSize.height = 4096; //影をきれいにする
    spot1.shadow.radius=4;  //影をぼかす(renderer.shadowMap.type=THREE.PCFShadowMapの時)
    scene.add(spot1);
    scene.add(spot1.target);
    //スポットライト(色,強さ,距離,角度(angle),半影[0-1],減衰[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;//影を表示可能に
    spot2.shadow.mapSize.width = 4096;  //影をきれいにする
    spot2.shadow.mapSize.height = 4096; //影をきれいにする
    spot2.shadow.radius=4;  //影をぼかす(renderer.shadowMap.type=THREE.PCFShadowMapの時)
    scene.add(spot2);
    scene.add(spot2.target);

    //地面
    //テクスチャのロード
    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;//落ちてきた影を表示する
    gm.rotation.x=Math.PI/2;
    gm.rotation.z=Math.PI;
    scene.add(gm);


    //■グループの作成
    g0=new THREE.Group;
    //■直方体ジオメトリの作成(幅、高さ、奥行き)
    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);

    //左腕
    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);

    //右腕
    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);

    //■球体ジオメトリの作成(半径, 水平分割数幅, 垂直分割数, 水平方向開始角度, 垂直方向開始角度)
    let sphereGeometry=new THREE.SphereGeometry(0.2,16,16,0,Math.PI*2);
    //ランバートシェーディング材質(光源の影響を受ける光沢の無い材質)の作成
    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;


    //■バット
    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),
    ];
    //Y↑軸で回転させて旋盤(回転体)ジオメトリ作成(点配列,分割数,開始角[0],角度[Math.PI*2])
    let latheGeometry = new THREE.LatheGeometry(points, 12, 0, Math.PI*2);
    //ジオメトリとマテリアルからメッシュを作成
    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;


    //■バッター
    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);//カメラの位置を設定
    //オービットコントロールの作成(マウスの左右ボタンドラッグ、ホイール等でカメラが動くようになる)
    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の位置
    if(hh==0||hh==1){
      if(step<100){
        //何もしない
      }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="ストライク";
        }
      }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);//カメラの位置を設定
        controls.update();
        document.querySelector(".info").innerHTML="";
      }
    }
    step++;
    
    if(bstep==0){
      //バットの回転を元に戻す
      gBat.rotation.set(0,0,0);
    }else{
      if(bstep<7){
        //バットの回転
        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="ホームラン";
          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="ヒット";
          v.x=(g3.position.z-50)/8;
          v.y=0.3;
          v.z=-0.4;
          step=0;
        }else{
          hh=1;
          document.querySelector(".info").innerHTML="ストライク";
        }
      }
      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;
    }
  }

  //視点の切り替え
  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);//カメラの位置を設定
    controls.update();
  }
</script>

Javascriptで3Dコンテンツ一覧へ