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

How to Build a Slot Machine with Three.js|A Practical Example of a 3D Slot Game Running on the Web

Japanese

3D Slot Machine Game in the Web Browser Using Three.js (Built with Three.js r145)

Using JavaScript and Three.js, I created a 3D slot machine that runs directly in the web browser.
In this article, I explain the basic components of a slot machine game—such as the reel rotation animation and the winning combination logic—along with implementation examples.
This is a great introduction for anyone who wants to try building a game with Three.js or learn how slot machines work.

This 3D slot machine runs in the web browser. Press the Start button to spin the reels, and press the Stop button to stop them.

Stop
Stop
Stop
Start
 

Source Code

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=2.0,user-scalable=yes">
  <meta charset="UTF-8">
  <script src="./three_r145/three.min.js"></script>
  <script src="./three_r145/OrbitControls.js"></script>

<style>
.slot3DB{
  display:inline-block;
  box-sizing:border-box;
  width:100%;
  min-width:20px;
  max-width:calc(100% - 0.5em);
  margin:6px;
  padding: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;
  word-break:break-all;
  word-wrap:anywhere;
}
.slot3DB:active{
  box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.4) inset;
}
.slot3DB:hover{}
.slot3DS{
  background:#cfc;
  padding:1em 4em;
}
</style>


<script>
  var png=CreateTextPng();
  var scene,camera,renderer;
  //       position   1: accelerate  2: decelerate   speed
  var slot3D={d:[0,0,0],ds:[0,0,0],dv:[0,0,0],status:-1,bstop:[null,null,null],bstart:null,g:[null,null,null],result:null};
  var main = function () {
    // Get the target canvas and retrieve its 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( FOV(deg), aspect ratio, near clipping, far clipping );
    camera = new THREE.PerspectiveCamera(20, w/h, 0.1, 10000);
    camera.position.set(0, 0, 24); // set camera position
    //camera.rotation.set(0,0,0); // set camera rotation to (0°,0°,0°)
    camera.lookAt(new THREE.Vector3(0, 0, 0)); // make the camera look at (0,0,0)
    scene.add(camera); // add camera to the 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

    // ■ Lights ------------------------------------------------
    // Ambient light (soft light from all directions; prevents completely dark areas)
    var AmbientLight=new THREE.AmbientLight(0xffffff,0.4);
    scene.add(AmbientLight); // add ambient light to the scene
    // Spotlight (color, intensity, distance, angle, penumbra[0–1], decay)
    const spotLight=new THREE.SpotLight(0xffffff, 1, 160, Math.PI/8, 0.4, 1);
    spotLight.position.set(8,12,20);
    spotLight.target.position.set(0,0,0);
    spotLight.castShadow=true; // enable shadows
    spotLight.shadow.mapSize.width = 4096;  // high-quality shadows
    spotLight.shadow.mapSize.height = 4096; // high-quality shadows
    spotLight.shadow.radius=10;  // soften shadow edges
    scene.add(spotLight);
    scene.add(spotLight.target);
  
    let m=new THREE.MeshLambertMaterial({
      color:0xdddddd,side:THREE.FrontSide
    });
    let mm=new THREE.MeshLambertMaterial({
      color:0xff3344,side:THREE.FrontSide,transparent:true,opacity:0.6
    });
    let bgeo=new THREE.BoxGeometry(1,1,1);
    let bm1=new THREE.Mesh(bgeo,m);
    bm1.scale.set(10,8,2);
    bm1.position.set(0,1,-3);
    bm1.castShadow=true;
    bm1.receiveShadow=true;
    scene.add(bm1);
    let bm2=new THREE.Mesh(bgeo,m);
    bm2.scale.set(10,2,4);
    bm2.position.set(0,4,0);
    bm2.castShadow=true;
    bm2.receiveShadow=true;
    scene.add(bm2);
    let bm3=new THREE.Mesh(bgeo,m);
    bm3.scale.set(10,4,6);
    bm3.position.set(0,-5,-1);
    bm3.castShadow=true;
    bm3.receiveShadow=true;
    scene.add(bm3);
    let bm4=new THREE.Mesh(bgeo,m);
    bm4.scale.set(2,6,4);
    bm4.position.set(-4,0,0);
    bm4.castShadow=true;
    bm4.receiveShadow=true;
    scene.add(bm4);
    let bm5=new THREE.Mesh(bgeo,m);
    bm5.scale.set(2,6,4);
    bm5.position.set(4,0,0);
    bm5.castShadow=true;
    bm5.receiveShadow=true;
    scene.add(bm5);
    let bm6=new THREE.Mesh(bgeo,mm);
    bm6.castShadow=true;
    bm6.scale.set(6,0.1,0.1);
    bm6.position.set(0,0.72,2-0.05);
    scene.add(bm6);
    let bm7=new THREE.Mesh(bgeo,mm);
    bm7.castShadow=true;
    bm7.scale.set(6,0.1,0.1);
    bm7.position.set(0,-0.5,2-0.05);
    scene.add(bm7);
    

    // Load texture
    let texture = new THREE.TextureLoader().load( png );
    let material=new THREE.MeshLambertMaterial({
      color:0xffffff, map:texture, side:THREE.FrontSide,
      transparent:false, opacity:1.0,
    });
    // ■ Create cylinder geometry(top radius, bottom radius, height, radial segments [8], height segments [1], open-ended [false], start angle [0], sweep angle [Math.PI*2])
    let cgeo=new THREE.CylinderGeometry(2,2,1.6,32,1,true,0,Math.PI*2);
    let m1=new THREE.Mesh(cgeo,material);
    let m2=new THREE.Mesh(cgeo,material);
    let m3=new THREE.Mesh(cgeo,material);
    m1.castShadow=true;
    m1.receiveShadow=true;
    m2.castShadow=true;
    m2.receiveShadow=true;
    m3.castShadow=true;
    m3.receiveShadow=true;

    m1.rotation.set(-Math.PI/10,0,-Math.PI/2);
    m1.position.set(-1.6,0,0);
    m2.rotation.set(-Math.PI/10,0,-Math.PI/2);
    m2.position.set(0,0,0);
    m3.rotation.set(-Math.PI/10,0,-Math.PI/2);
    m3.position.set(1.6,0,0);
    let g1=new THREE.Group();
    g1.add(m1);
    let g2=new THREE.Group();
    g2.add(m2);
    let g3=new THREE.Group();
    g3.add(m3);
    scene.add(g1);
    scene.add(g2);
    scene.add(g3);
    slot3D.g[0]=g1;
    slot3D.g[1]=g2;
    slot3D.g[2]=g3;
    for(let i=0;i<3;i++){
      slot3D.bstop[i]=document.querySelector('#bstop'+(i+1));
      slot3D.bstop[i].addEventListener('click',function(){
        let id=parseInt(event.target.id.substr(-1))-1;
        if(slot3D.status==1 && slot3D.ds[id]==1){
          slot3D.ds[id]=2;
        }
      });
    }
    slot3D.bstart=document.querySelector('#bstart');
    slot3D.bstart.addEventListener('click',function(){
      slot3D.result.innerHTML='';
      if(slot3D.status==1){return;}
      slot3D.status=1;
      for(let i=0;i<slot3D.d.length;i++){
        slot3D.ds[i]=1; // accelerate
        slot3D.dv[i]=0; // speed
      }
    });
    slot3D.result=document.querySelector('#slotr');

    // Create OrbitControls (enables camera movement via mouse drag and mouse wheel)
    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.target.set(0,0,0);
    controls.update();
    controls.enabled=false;
    setInterval(renderLoop,33);
  }

  function renderLoop () {
    renderer.render(scene, camera);
    starting();
  }

  window.addEventListener('DOMContentLoaded', main, false);

  function starting(){
    if(slot3D.status==-1){return;}
    for(let i=0;i<slot3D.d.length;i++){
      if(slot3D.ds[i]==1){
        slot3D.dv[i]+=Math.floor(1+Math.random()*3);
        if(slot3D.dv[i]>30){slot3D.dv[i]=30;}
      }else if(slot3D.ds[i]==2){
        if(slot3D.dv[i]>2){
          slot3D.dv[i]-=(Math.floor(Math.random()*slot3D.dv[i]/10)+1);
          if(slot3D.dv[i]<2){
            if(slot3D.d[i]%2==1){slot3D.d[i]++;}
            slot3D.dv[i]=2;
          }
        }else{
          if(slot3D.d[i]%2==1){slot3D.d[i]++;}
          slot3D.dv[i]=2;
        }
      }
      if(slot3D.ds[i]!=0){
        slot3D.d[i]+=slot3D.dv[i];
        slot3D.d[i]%=1000;
        if(slot3D.ds[i]==2&&slot3D.dv[i]==2&&(slot3D.d[i]%100)==0){
          slot3D.ds[i]=0;
          slot3D.dv[i]=0;
        }
      }
    }
    if(slot3D.ds[0]==0 && slot3D.ds[1]==0 && slot3D.ds[2]==0){
      slot3D.status=0;
      if(slot3D.d[0]==700 && slot3D.d[1]==700 && slot3D.d[2]==700){
        slot3D.result.innerHTML="Jackpot";
      }else if(slot3D.d[0]==slot3D.d[1] && slot3D.d[1]==slot3D.d[2]){
        slot3D.result.innerHTML="Win";
      }else if(slot3D.d[0]==slot3D.d[1] || slot3D.d[1]==slot3D.d[2] || slot3D.d[0]==slot3D.d[2]){
        slot3D.result.innerHTML="Close";
      }else{
        slot3D.result.innerHTML="Lose";
      }
    }
    for(let i=0;i<slot3D.g.length;i++){
      slot3D.g[i].rotation.set(-Math.PI/500*slot3D.d[i],0,0);
    }
  }

  // Return PNG image
  function CreateTextPng(){
    let can=document.createElement("canvas");
    can.width='800';
    can.height='80';
    let ctx=can.getContext("2d", {willReadFrequently:true});
    let family=
      'Verdana,Roboto,"Droid Sans",YuGothic,Meiryo,'+
      '"Hiragino Kaku Gothic ProN",sans-serif';
    ctx.font="80px "+family;
    ctx.textBaseline="ideographic";
    ctx.textAlign="center";
    // Make background transparent
    //ctx.globalCompositeOperation = 'destination-out';
    ctx.globalCompositeOperation = 'source-over';
    ctx.fillStyle="rgb(255,255,255)";
    ctx.fillRect(0,0,can.width,can.height);
    // Normal drawing mode
    ctx.globalCompositeOperation = 'source-over';
    ctx.fillStyle="rgb(0,0,0)";
    for(let i=0;i<10;i++){
      ctx.save();
      ctx.rotate(-Math.PI/2); // counterclockwise rotation, radians, pivot at (0,0)
      ctx.translate(-80,0);
      ctx.fillText(i,40,i*80+80);
      ctx.restore();
    }

    let png=can.toDataURL('image/png');
    return png;
  }

</script>

</head>
<body>

  <h3 class="diag"></h3>
  <p class="normal">
    <br>
    <canvas id="can" style="width:800px; height:480px; max-width:calc(100vw - 32px);max-height:calc((100vw - 32px) * 480 / 800);-ms-touch-action:none;touch-action:none;cursor:grab;"></canvas>
    <br>
  </p>
  <div style="display:flex;flex-wrap:wrap;justify-content:center;width:100%;max-width:800px;">
    <div style="width:20%;text-align:center;"><a class="slot3DB" id="bstop1">Stop</a></div>
    <div style="width:20%;text-align:center;"><a class="slot3DB" id="bstop2">Stop</a></div>
    <div style="width:20%;text-align:center;"><a class="slot3DB" id="bstop3">Stop</a></div>
  </div>
  <div style="width:100%;max-width:800px;text-align:center;"><a class="slot3DB slot3DS" id="bstart">Start</a></div>
  <div id="slotr" style="width:100%;max-width:360px;font-size:32px;color:red;font-weight:bold;"> </div>

</body>
</html>

Back to the List of 3D JavaScript Content