5
\$\begingroup\$

This is an iterative review of this question: 3D 1st Person Snake Game #1 The next iteration can be found here: 3D 1st Person Snake Game #3 Here is a snippet of the code:

<html>
  <style>
    canvas{
      position:fixed;
      left:0;
      top:0;
      width:99%;
      height:99%;
    }
    * {
      padding:0px;
      margin:0px
    }
    #score,#speed,#highscore,#maxspeed,#lag,#debug{
      position: fixed;
      z-index: 100;
      font-size:20px;
      font-family:Verdana;
      left:15px;
      width: 100%;
    }
    #gameover{
      position:absolute;
      z-index: 100;
      font-size:60px;
      font-family:Verdana;
      margin: 0;
      top: 50%;
      left: 50%;
      opacity:0;
      transform: translate(-50%, -50%);
    }
    #maxspeed, #score, #josh, #speed, #highscore, #lag {
      display:block;
    }
    #maxspeed {
      width: 100%;
      top:50px;
      display:none;
    }

    #score {
      top: 10px;
    }
    #speed {
      top: 30px;
    }

    #highscore {
      top:30px;
    }

    #lag {
      top: 70px;
    }
  </style>
  <div id="stats">
    <div id="debug"></div>
    <div id="score">Score: 0</div>
    <div id="speed">Speed: 1</div>
    <div id="highscore">Highscore: 1</div>
    <div id="maxspeed">Highest Speed: 1</div>
    <span id="lag">Lag: 0ms</span>
    <div id="gameover" align="center">Game Over</div>
  </div>
  <div id="canvas"></div>
  <p id="p"></p>
  <p id="e"></p>
  <script type="text/javascript" src="https://cdn.rawgit.com/alexgibson/shake.js/master/shake.js"></script>
  <script src="https://threejs.org/build/three.min.js"></script>
  <script>
    //========================
    // One times
    //========================
    const random = Math.random,
          floor = function(a){ return ~~a},
          newGeometry = THREE.Geometry,
          newBufferGeometry = THREE.BufferGeometry,
          newMesh = THREE.Mesh,
          newLineSegments = THREE.LineSegments,
          newMeshBasicMaterial = THREE.MeshBasicMaterial,
          newVector3 = THREE.Vector3,
          newLineBasicMaterial = THREE.LineBasicMaterial,
          newBoxGeometry = THREE.BoxGeometry,
          newBoxBufferGeometry = THREE.BoxBufferGeometry,
          arena = new THREE.Group(),
          applegeometry = new THREE.SphereBufferGeometry( 0.5, 32, 32 ),
          superapplematerial= new newMeshBasicMaterial({color:"gold"}),
          applematerial= new newMeshBasicMaterial({color:"limegreen"}),
          floormaterial = new newMeshBasicMaterial({color:"lightgrey"}),
          wallmaterial = new newMeshBasicMaterial( { color: "grey" } ),
          linecube = new newBoxBufferGeometry( 1,1,1 ),
          edges = new THREE.EdgesGeometry( linecube ),
          geometry = new newBoxBufferGeometry( 1,1,1 ),
          stringify = JSON.stringify


  </script>
  <script id="script">
    (function(THREE,ArenaWidth, ArenaLength){
      'use strict'
      const arenaLines = new THREE.Group(),
            arenaWall = new newGeometry(),
            arenaFloor = new newGeometry(),
            wall1 = new newBoxGeometry( ArenaWidth+1, 1, 1 )
      .translate((ArenaWidth/2)-2,1,-2);
      arenaWall.merge(wall1);
      arenaWall.merge(wall1.translate(0,0, ArenaLength));
      const wall2 = new newBoxGeometry( 1, 1, ArenaLength+1 ).translate(ArenaWidth-2,1,(ArenaLength/2)-2);
      arenaWall.merge(wall2);
      arenaWall.merge(wall2.translate(-ArenaWidth,0,0));
      arenaFloor.merge(new newBoxGeometry( ArenaWidth+1, 1, ArenaLength+1 ).translate((ArenaWidth/2)-2,0,(ArenaLength/2)-2))
      arenaWall.mergeVertices()
      arenaFloor.mergeVertices()
      // Get the meshs
      window.arenaWallMesh = new newMesh(arenaWall, wallmaterial);
      window.arenaFloorMesh = new newMesh(arenaFloor, floormaterial);

      var speed = 100,
          maxwidth = 40,
          maxdepdth = 40,
          size = 0.5,
          score = 0,
          maxheight = 0,
          lagNum = 0,
          highscoreNum,
          showlag,
          maxspeedNum,
          showmaxspeed,
          showhighscore

      //========================
      // Threejs
      //========================

      var scene = new THREE.Scene(),
          camera= new THREE.PerspectiveCamera(75,window.innerWidth/window.innerHeight,0.1,60),
          renderer = new THREE.WebGLRenderer(),
          extrasLookup = {
            1:30,
            2:50,
            3:70,
            4:90
          }
      //========================
      // Reductions
      //========================
      const newScene = THREE.Scene

      if(true /*localStorage.getItem("3dhighscore")==undefined*/){
        highscoreNum=0
      } else {
        highscoreNum = localStorage.getItem("3dhighscore")
      }
      if(true /*localStorage.getItem("showlag")==undefined*/){
        showlag=false
      } else {
        showlag = localStorage.getItem("showlag")
      }
      if(true /*localStorage.getItem("3dmaxspeed")==undefined*/){
        maxspeedNum=100
      } else {
        maxspeedNum = localStorage.getItem("3dmaxspeed")
      }
      if(showmaxspeed==undefined){
        showmaxspeed=false
      }
      if(showhighscore==undefined){
        showhighscore=false
      }

      renderer.setSize( window.innerWidth, window.innerHeight );
      document.getElementById("canvas").appendChild( renderer.domElement );

      function animate() {
        requestAnimationFrame( animate )
        renderer.render( scene, camera );
      }
      class Location{
        constructor(x,y,z){
          return [x,y,z]
        }
      }

      //========================
      // Dom
      //========================
      const maxSpeedDom = document.getElementById("maxspeed"),
            maxSpeedDomStyle = maxSpeedDom.style,
            lagdom = document.getElementById("lag"),
            lagdomstyle = lagdom.style,
            highScoreDom = document.getElementById("highscore"),
            highScoreDomStyle = highScoreDom.style,
            scoredom = document.getElementById("score"),
            speeddom = document.getElementById("speed"),
            gameOverDom = document.getElementById("gameover"),
            gameOverDomStyle = gameOverDom.style;
      //========================
      // Snake
      //========================
      class Snake {
        constructor(){
          this.segments = [
            new Location(7,1,5),
            new Location(6,1,5),
            new Location(5,1,5)
          ];
          this.direction = "forwards";
          this.nextDirection = "forwards";
          this.nextHead = new Location(8,1,5);
        }
        draw(){
          let snakeobject = new THREE.Group();
          let j = this.segments.length
          this.segments.forEach((item,index) => {
            let i = index
            let segmentColor=`rgb(${255-(255/j|0)*index+randomNumberBetween(-(j-index),j-index)},00,00)`
            const cube = new newMesh(geometry, new newMeshBasicMaterial({ color: segmentColor})),
                  x = item[0],
                  y = item[1],
                  z = item[2];
            let pos = new newVector3(x,y,z)

            const line = new newLineSegments(edges, new newLineBasicMaterial({ color: segmentColor,linewidth:2 }));
            cube.position.copy(pos);
            line.position.copy(pos);
            snakeobject.add(line,cube);
          })
          scene.add(snakeobject);
        }
        move() {
          this.segments.unshift(this.nextHead);
          if(this.checkCollision()) {
            this.segments.shift();
            gameOver();
            return;
          };
          var nextHeadPos = this.nextHead;
          this.direction = this.nextDirection;
          (directionSwitch[this.direction])(nextHeadPos)
          if(stringify(this.segments[0]) === stringify(apple.pos)) {
            speed=0.95*speed;
            score++;
            if(apple.type === "superapple") {
              speed = 1.10*speed;
              score += 4;
            }
            apple.move();
          } else {
            this.segments.pop();
          };
        }
        changeDirection(key) {
          switch(key){
            case 39:
            case 68:
            case 37:
            case 65:
              keyLookup[key][this.direction]()
              break;
            case 192:
            case 55:
            case 56:
              keyLookup[key]()
              break;
                    }
        }
        checkCollision() {
          const head = this.segments[0]
          const headz = head[2];
          const headx = head[0];
          const wallCollision = headz === -2 || headz === ArenaLength-2 || headx === -2 || headx === ArenaWidth-2;
          var selfCollision = false;
          this.segments.forEach(function(item, index) {
            if(index) {
              if(item[0] === head[0] && item[2] === head[2]) {
                selfCollision = true;
              };
            };
          });
          const collision = selfCollision || wallCollision;
          return collision;
        }
      }

      //========================
      // Drawing
      //========================
      function drawStats() {
        var extras = 1;
        if(score > highscoreNum) {
          highscoreNum = score;
          //localStorage.setItem("3dhighscore", highscore);
        }
        if(speed < maxspeed) {
          maxspeedNum = speed;
          //localStorage.setItem("3dmaxspeed", maxspeed);
        }

        scoredom.innerHTML=`Score: ${score}`;
        speeddom.innerHTML=`Speed: ${(((100/speed*100)|0)/100)}`;

        if(showhighscore) {
          extras++;
          highscore.style.display = "block";
          highscore.style.top = extrasLookup[extras]
          highscore.innerHTML = `Highscore: ${highscoreNum}`;

        } else {
          highScoreDomStyle.display = "none";
        };

        if(showmaxspeed) {
          extras++;
          maxspeed.style.display = "block";
          maxspeed.style.top = extrasLookup[extras]
          maxspeed.innerHTML = `Highest Speed: ${(((100/maxspeed*100)|0)/100)}`;
        } else {
          maxspeed.style.display = "none";
        };

        if(showlag) {
          extras++;
          lag.style.display = "block";
          lag.style.top = extrasLookup[extras]
          lag.innerHTML = `Lag: ${lagNum}ms`;
        } else {
          lagdomstyle.display = "none";
        };
      }
      function renderArena() {
        ;
      }

      //========================
      // Apple
      //========================
      class Apple {
        constructor(){
          this.pos = new Location(2,1,2)
        }
        move(){

          // Set the location to a random x and z.
          var x = randomNumberBetween(0,ArenaWidth-4)
          var z = randomNumberBetween(0,ArenaLength-4)
          this.pos = new Location(x,1,z)
          if(randomNumberBetween(1,11)===1&&score>15){                  
            apple.type="superapple"
          } else {
            apple.type="apple"
          }
        }
        draw() {
          var sphere

          // Generate a new sphere.
          if(this.type=="superapple") {
            sphere = new newMesh( applegeometry, superapplematerial );
          } else {
            sphere = new newMesh( applegeometry, applematerial );
          }

          // Get the data location.
          var applepos=apple.pos

          // Set the location of the visual apple to the data location.
          sphere.position.set(applepos[0],applepos[1],applepos[2])
          scene.add(sphere);
        }
      }
      //========================
      // Misc
      //========================
      function newGame(key) {
        if(key==82) {

          // Stop the game.
          clearTimeout(gameLoop);
          s=function(){};

          // Start a new game.
          var game = document.getElementById("canvas");
          game.removeChild(game.childNodes[0]);
          eval(document.getElementById("script").innerHTML);
        }
      }
      gameOverDomStyle.opacity=0;
      function gameOver() {
        s = function() {
          camera.position.copy(new newVector3((ArenaWidth/2)-2,Math.sqrt(
            Math.pow(ArenaWidth/2,2)+Math.pow(ArenaLength/2,2)),(ArenaLength/2)-2));
          camera.lookAt(new newVector3((ArenaWidth/2)-2,0,(ArenaLength/2)-2));
        };
        document.onkeydown=function(e) {
          newGame(e.keyCode);    
        }
        //listen to shake event
        var shakeEvent = new Shake({threshold: 15});
        shakeEvent.start();
        window.addEventListener('shake', function(){
          newGame(82)
        }, false);

        //stop listening
        function stopShake(){
          shakeEvent.stop();
        }

        //check if shake is supported or not.
        if(!("ondevicemotion" in window)){alert("Not Supported");}
        gameOverDomStyle.opacity = 1;
      }

      function randomNumberBetween(a,b) {
        return a+floor(random()*(b-a));
      }
      var apple = new Apple(),
          snake = new Snake()
      var directionSwitch={
        "forwards":function(a){this.nextHead = new Location(a[0]+1, a[1], a[2])},
        "backwards":function(a){this.nextHead = new Location(a[0]-1, a[1], a[2])},
        "left":function(a){this.nextHead = new Location(a[0], a[1], a[2]-1)},
        "right":function(a){this.nextHead = new Location(a[0], a[1], a[2]+1)}
      }
      for(var x in directionSwitch){
        directionSwitch[x] = directionSwitch[x].bind(snake)
      }
      var right = {
        "right":function(){this.nextDirection = "backwards"},
        "backwards":function(){this.nextDirection = "left"},
        "forwards":function(){this.nextDirection = "right"},
        "left":function(){this.nextDirection = "forwards"}
      }
      for(var x in right){
        right[x] = right[x].bind(snake)
      }
      var left = {
        "right":function(){snake.nextDirection = "forwards"},
        "forwards":function(){snake.nextDirection = "left"},
        "left":function(){snake.nextDirection = "backwards"},
        "backwards":function(){snake.nextDirection = "right"}
      }
      for(var x in left){
        left[x] = left[x].bind(snake)
      }
      var keyLookup = {
        "39":right,
        "68":right,
        "37":left,
        "65":left,
        "192":function(){showlag=!showlag},
        "55":function(){showhighscore=!showhighscore},
        "56":function(){showmaxspeed = !showmaxspeed}
      }
      snake.draw();
      apple.move();

      var background = new THREE.Color( "white" ),
          fog = new THREE.FogExp2( "random", 0.02625 );

      let s = function() {
        // Start for lag detection.
        var start = performance.now();

        // Make a fresh scene.
        scene = new newScene();

        // Move the snake.
        snake.move();

        // Render various things onto the scene.
        scene.add(arenaWallMesh,arenaFloorMesh)
        snake.draw();
        apple.draw();
        drawStats();


        // Get relevent snake parts.
        let head = snake.segments[0];
        let nextHead = snake.nextHead;

        // Set camera position and target.
        camera.position.set(head[0],head[1],head[2]);
        camera.lookAt(new newVector3(nextHead[0],nextHead[1],nextHead[2]));

        // Set background and fog.
        scene.background = background;
        scene.fog = fog;
        let end = performance.now();
        // Reschedule the next update
        gameLoop = setTimeout(function(){
          requestAnimationFrame(s)
        },speed)

        // End for lag detection and calculation of lag.
        lagNum = floor((end-start))
      }
      animate()
      var gameLoop = setTimeout(function(){
        requestAnimationFrame(s)
      },speed);
      document.onkeydown = function(e) {
        snake.changeDirection(e.keyCode);
      }
    })(window.THREE, (((window.innerWidth/10)|0)*10-2*10)/10, (((window.innerHeight/10)|0)*10-2*10)/10)
  </script>

\$\endgroup\$
  • \$\begingroup\$ Note: For the snippet, click "run", "Full page", purposely lose, and then click "r". Unfortunately the snippet makes the game small... \$\endgroup\$ – FreezePhoenix May 14 '18 at 16:49
  • 2
    \$\begingroup\$ Oh maaan. It's so hard to play. Sorry, now got zero time to write an actual review! i.stack.imgur.com/gfiZi.png \$\endgroup\$ – Igor Soloydenko May 14 '18 at 18:02
3
\$\begingroup\$

Feedback

I like how the DOM elements are stored in constants at the beginning of the code. It seems that a lot of the feedback from previous answers has been incorporated, except a few things like resetting the game without using eval(), and other es-6 features like classes. The CSS styles look succinct and appear to be sufficient.

Suggestions/Comments

  • In the function checkCollision() it might be fitting to use Array.prototype.some() instead of assigning the value of a boolean inside the callback of a forEach(). Also, that loop call comes after the check for a wall collision - if there is a wall collision, the function could immediately return true instead of also looping through the segments.

  • As was mentioned by samurai8 in the answer to your previous review there are still places where var is used instead of let

  • Blindman's answer on the first game mentioned using semi-colons - it appears most lines have them but some still don't.
  • The HTML snippet doesn't have a <body> tag - apparently it still works and perhaps you have one in other environments but it feels weird not to have one.
  • The event listener for shake could be simplified: Use Function.prototype.bind() to create a partially applied function:

    window.addEventListener('shake', function(){
      newGame(82)
    }, false);
    

    Can be simplified to:

    window.addEventListener('shake', newGame.bind(null,82), false);
    
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.