2
\$\begingroup\$

Why is my code lagging? It gets those lagspikes that will ruin the whole game. I know JavaScript in the browser maybe isn't the best for games, but it doesn't have to lag?

var canvas;
var gameInterval = false;
var gameSpeed = 250;
var xTiles = 200;
var yTiles = 200;
var xScale = 40;
var yScale = 40;
var tiles = [];

var snake = [];
var snakeDirFirst = 2;
var snakeDirSec = "changeAble";
var timeStamp;

window.onload = function() {
  canvas = {
    "el": document.getElementById("canvas")
  };
  canvas.ctx = canvas.el.getContext("2d");
  canvas.el.height = window.innerHeight;
  canvas.el.width = window.innerWidth;
  //xScale = canvas.el.width/xTiles;
  //yScale = canvas.el.height/yTiles;
  //xTiles = Math.ceil(canvas.el.width / xScale);
  //yTiles = Math.ceil(canvas.el.height / yScale);
  for (var x = 0; x < xTiles; x++) {
    tiles[x] = [];
    placeWall(x, 0);
    placeWall(x, yTiles - 1);
  }
  for (var y = 0; y < yTiles; y++) {
    placeWall(0, y);
    placeWall(xTiles - 1, y);
  }
  for (var i = 0; i < 20; i++) placeFood();
  snake.push(new SnakePiece(1, 1));
  snake[0].draw();
  toggleGameLoop();
}

function placeFood(x, y) {
  var x = x || Math.floor(Math.random() * (xTiles - 2)) + 1;
  var y = y || Math.floor(Math.random() * (yTiles - 2)) + 1;
  if (tiles[x][y] === undefined) {
    tiles[x][y] = "food";
    canvas.ctx.fillStyle = "green";
    canvas.ctx.fillRect(x * xScale + xScale * 0.05, y * yScale + yScale * 0.05, xScale * 0.9, yScale * 0.9);
  } else {
    canvas.ctx.fillStyle = "green";
    canvas.ctx.beginPath();
    canvas.ctx.arc(x * xScale + xScale / 2, y * yScale + yScale / 2, 10, 0, 2 * Math.PI, false);
    canvas.ctx.fill();
    placeFood();
  }
}

var placeWall = function(x, y) {
  tiles[x][y] = "wall";
  canvas.ctx.fillRect(x * xScale + xScale * 0.05, y * yScale + yScale * 0.05, xScale * 0.9, yScale * 0.9);
}

var SnakePiece = function(x, y) {
  this.x = x;
  this.y = y;

  this.draw = function(fat) {
    canvas.ctx.fillStyle = "rgb(255, 0, 208)";
    canvas.ctx.fillRect(this.x * xScale + ((fat) ? 0 : xScale * 0.05), this.y * yScale + ((fat) ? 0 : yScale * 0.05), xScale * ((fat) ? 1 : 0.9), yScale * ((fat) ? 1 : 0.9));
    tiles[this.x][this.y] = "snake";
  }
  this.erase = function() {
    delete tiles[this.x][this.y];
    canvas.ctx.clearRect(this.x * xScale, this.y * yScale, xScale, yScale);
  }
}

function gameLoop() {
  //console.log(Date.now() - timeStamp - gameSpeed)
  timeStamp = Date.now();
  var head = snake[0];
  var newHead = snake.pop();
  if (snake.length !== 1 || head.x !== newHead.x || head.y !== newHead.y) newHead.erase();
  newHead.x = 0 + head.x;
  newHead.y = 0 + head.y;
  switch (snakeDirFirst) {
    case 0:
      newHead.y--;
      break;
    case 1:
      newHead.x++;
      break;
    case 2:
      newHead.y++;
      break;
    case 3:
      newHead.x--;
      break;
  }
  if (tiles[newHead.x][newHead.y] == "food") {
    snake.unshift(new SnakePiece(head.x, head.y));
    snake.unshift(newHead);
    newHead.draw(true);
    placeFood();
  } else if (tiles[newHead.x][newHead.y] === undefined) {
    snake.unshift(newHead);
    newHead.draw();
  }
  if (typeof snakeDirSec === "number") snakeDirFirst = snakeDirSec;
  snakeDirSec = "changeAble";
}

function draw() {
  for (var x = 0; x < xTiles; x++) {
    for (var y = 0; y < yTiles; y++) {
      switch (tiles[x][y]) {
        case "food":
          canvas.ctx.fillStyle = "green";
          break;
        case "wall":
          canvas.ctx.fillStyle = "grey";
          break;
        case "snake":
          canvas.ctx.fillStyle = "pink";
          break;
        case undefined:
          canvas.ctx.fillStyle = "white";
          break;
      }
      canvas.ctx.beginPath();
      canvas.ctx.arc(x * xScale + xScale / 2, y * yScale + yScale / 2, 10, 0, 2 * Math.PI, false);
      canvas.ctx.fill();
      canvas.ctx.stroke();
    }
  }
}

window.onkeydown = function(e) {
  var snakeDir;
  if (e.keyCode == 32) toggleGameLoop();
  else {
    switch (e.keyCode) {
      case 37:
        snakeDir = 3;
        break;
      case 38:
        snakeDir = 0;
        break;
      case 39:
        snakeDir = 1;
        break;
      case 40:
        snakeDir = 2;
        break;
    }
    if (snakeDirSec == "changeAble") {
      snakeDirFirst = snakeDir;
      snakeDirSec = "not"
    } else {
      snakeDirSec = snakeDir;
    }
  }
}

function toggleGameLoop() {
  if (gameInterval !== false) {
    window.clearInterval(gameInterval);
    gameInterval = false;
  } else {
    gameInterval = window.setInterval(function() {
      gameLoop();
    }, gameSpeed);
  }
}
html,
body {
  margin: 0;
}
<canvas id="canvas"></canvas>

To keep lag at minimum only what is need to render is rendered and only what needs to be erased is erased, also no all-over checks, it only checks the head for food, collisions and such through its position.

The goal is to get the game lag-free on a phone.

<style>html, body{margin:0;}</style>
<canvas id="canvas"></canvas>
<script>

var canvas;
var gameInterval = false;
var gameSpeed = 250;
var xTiles;
var yTiles;
var xScale = 40;
var yScale = 40;
var tiles = [];

var snake = [];
var snakeDirFirst = 2; //direction of the snake
var snakeDirSec = "changeAble"; // second direction if you press more than 1 key in that frame
var timeStamp;

window.onload = function(){
    canvas = {"el":document.getElementById("canvas")};
    canvas.ctx = canvas.el.getContext("2d"); // the draw context canvas.ctx....
    canvas.el.height = window.innerHeight;
    canvas.el.width = window.innerWidth;
    //xScale = canvas.el.width/xTiles;
    //yScale = canvas.el.height/yTiles;
    xTiles = Math.ceil(canvas.el.width/xScale);
    yTiles = Math.ceil(canvas.el.height/yScale);        
    for(var x=0; x<xTiles; x++){
        tiles[x] = [];
        placeWall(x,0);
        placeWall(x,yTiles-1);
    }
    for (var y = 0; y <yTiles; y++) {
        placeWall(0,y);
        placeWall(xTiles-1,y);
    }
    placeFood();
    snake.push(new SnakePiece(1,1));  //starting piece
    snake[0].draw(); //draw first piece
    toggleGameLoop(); //start the game (unpause)
}

//Placeing food on a random spot (or as the parameters say), making tiles[x][y] = "food" and drawing it directly
function placeFood(x,y){
    var x = x || Math.floor(Math.random() * (xTiles-2))+1;
    var y = y || Math.floor(Math.random() * (yTiles-2))+1;
    if(tiles[x][y] === undefined){
        tiles[x][y] = "food";
        canvas.ctx.fillStyle = "green";
        canvas.ctx.fillRect(x*xScale+xScale*0.05,y*yScale+yScale*0.05,xScale*0.9,yScale*0.9);
    }else{
        canvas.ctx.fillStyle = "green";
        canvas.ctx.beginPath();
        canvas.ctx.arc(x*xScale+xScale/2, y*yScale+yScale/2, 10, 0, 2 * Math.PI, false);
        canvas.ctx.fill();
        placeFood(); 
    }
}

// placeing walls, much like food but with a wall instead, making tiles[x][y] = "wall" and drawing it directily
var placeWall = function(x,y){
    tiles[x][y] = "wall";
    canvas.ctx.fillRect(x*xScale+xScale*0.05,y*yScale+yScale*0.05,xScale*0.9,yScale*0.9);
}

//a square on the snake simply, can be drawn or cleared out of the canvas
var SnakePiece = function(x,y){
    this.x = x;
    this.y = y;

    this.draw = function(fat){
        canvas.ctx.fillStyle = "rgb(255, 0, 208)";
        canvas.ctx.fillRect(this.x*xScale+((fat)?0:xScale*0.05),this.y*yScale+((fat)?0:yScale*0.05),xScale*((fat)?1:0.9),yScale*((fat)?1:0.9));
        tiles[this.x][this.y] = "snake";
    }
    this.erase = function(){
        delete tiles[this.x][this.y];
        canvas.ctx.clearRect(this.x*xScale,this.y*yScale,xScale,yScale);
    }
}

function gameLoop(){
    console.log(Date.now()-timeStamp-gameSpeed) //lagg check
    timeStamp = Date.now();

    //takes the tail (last piece in snake array) and place it first
    var head = snake[0];
    var newHead = snake.pop();
    // ALWAYS erase the tail piece from canvas,exept when the snake is one piece long and has just eaten it first food. (the start)
    if(snake.length !== 1 || head.x !== newHead.x || head.y !== newHead.y) newHead.erase();
    newHead.x = 0+head.x;
    newHead.y = 0+head.y;
    switch(snakeDirFirst){ //placing the new head (former tail) in front of the old head with the addition of the direction
        case 0:
            newHead.y--;
            break;
        case 1:
            newHead.x++;
            break;
        case 2:
            newHead.y++;
            break;
        case 3:
            newHead.x--;
            break;
    }
    // I the head lands on food, two pieces are added on top of each other so when the last piece is removed there are still one there, making it look like the food in the snakes stomach becomes its tail 
    if(tiles[newHead.x][newHead.y] == "food"){
        snake.unshift(new SnakePiece(head.x,head.y));
        snake.unshift(newHead);
        newHead.draw(true);
        placeFood();
    }else if(tiles[newHead.x][newHead.y] === undefined){ //on a normal tile, the new head is actually added to the snake array and drawn
        snake.unshift(newHead);
        newHead.draw();
    }
    //If the tile isnt food or a normal tile, its a wall, and then not adding its head but still removing its tail will making in shorter and shorter for every gameframe(gameloop)

    //setting the direction of the snake for the next frame(gameloop) or keeping it the same
    if(typeof snakeDirSec === "number")snakeDirFirst = snakeDirSec;
    snakeDirSec = "changeAble";
}

//debug tool to see which tiles are occupied at this specific moment
    function draw(){
        for(var x = 0; x<xTiles; x++){
            for(var y = 0; y<yTiles; y++){
                switch(tiles[x][y]){
                    case "food":
                        canvas.ctx.fillStyle = "green";
                        break;
                    case "wall":
                        canvas.ctx.fillStyle = "grey";
                        break;
                    case "snake":
                        canvas.ctx.fillStyle = "pink";
                        break;
                    case undefined:
                        canvas.ctx.fillStyle = "white";
                        break;
                }
                canvas.ctx.beginPath();
                canvas.ctx.arc(x*xScale+xScale/2, y*yScale+yScale/2, 10, 0, 2 * Math.PI, false);
                canvas.ctx.fill();
                canvas.ctx.stroke();
            }
        }
    }

//Sets the direction
window.onkeydown = function(e){
    var snakeDir;
    if(e.keyCode == 32) toggleGameLoop();
    else{
        switch(e.keyCode){
                case 37:
                    snakeDir = 3;
                    break;
                case 38:
                    snakeDir = 0;
                    break;
                case 39:
                    snakeDir = 1;
                    break;
                case 40:
                    snakeDir = 2;
                    break;
            }
            if(snakeDirSec == "changeAble"){
                snakeDirFirst = snakeDir;
                snakeDirSec = "not"
            }else{
                snakeDirSec = snakeDir;
            }
        }
    }


    function toggleGameLoop(){
        if(gameInterval !== false){
            window.clearInterval(gameInterval);
            gameInterval = false;
        }else{
            gameInterval = window.setInterval(function(){
                gameLoop();
            },gameSpeed);
        }
    }
\$\endgroup\$
2
  • 3
    \$\begingroup\$ Did you run your code in the snippet? It's throwing errors at me constantly. The second time I tried to run it, I immediately got "Uncaught RangeError: Maximum call stack size exceeded". \$\endgroup\$
    – anon
    Commented Mar 7, 2017 at 13:39
  • \$\begingroup\$ @QPaysTaxes fixed it, go to fullscreen to se the whole game better \$\endgroup\$
    – Dwadelfri
    Commented Mar 8, 2017 at 9:34

1 Answer 1

1
\$\begingroup\$

One cause for the observed delays might be your placeFood method which loops over random tiles until a free tile has been found. When the number of available free tiles is small compared to the total amount, this method might loop for a long time (e.g. on average 20000 times on 200 x 200 tiles with only one free tile).

Apart from above issue, the overall performance is already pretty good. However, you might want to organize your code in modules or classes and thereby encapsulate functionality as well as remove references to global variables.

Also, try to keep your code decoupled. The observer design pattern helps a lot with that. If applied to the tile set or board, this pattern allows clear separation of your logic from the representation.

The following code exhibits above improvements, but there is no 'gold standard'. You might want to pick some ideas you deem useful and add them to your own implementation:

const TILE_FREE = 0,
      TILE_FOOD = 1,
      TILE_WALL = 2,
      TILE_SNAKE = 3;

class Board {
  constructor(width, height) {
    this._width = width;
    this._height = height;
    this._tiles = new Array(width);
    for (let x = 0; x < width; x++) {
      this._tiles[x] = new Array(height).fill(TILE_FREE);
    }
    this._free = width * height;
    this._snake = [];
    this._handlers = {};
  }
  get width() {
    return this._width;
  }
  get height() {
    return this._height;
  }
  clear() {
    for (let x = 0; x < this._width; x++) {
      for (let y = 0; y < this._height; y++) {
        this._tiles[x][y] = TILE_FREE;
        this._fire("tile", x, y, TILE_FREE);
      }
    }
    this._snake = [];
    this._free = this._width * this._height;
  }
  placeFood() {
    let n = Math.floor(Math.random() * this._free);
    
    for (let x = 0; x < this._width; x++) {
      for (let y = 0; y < this._height; y++) {
        if (this._tiles[x][y] == TILE_FREE) {
          if (n == 0) {
            this._tiles[x][y] = TILE_FOOD;
            this._free--;
            this._fire("tile", x, y, TILE_FOOD);
            return;
          } else {
            n--;
          }
        }
      }
    }
  }
  placeWall(x, y) {
    if (this._tiles[x][y] != TILE_WALL) {
      this._tiles[x][y] = TILE_WALL;
      this._fire("tile", x, y, TILE_WALL);
      this._free--;
    }
  }
  placeSnake(x, y) {
    if (this._tiles[x][y] != TILE_SNAKE) {
      this._snake.push([x, y]);
      this._tiles[x][y] = TILE_SNAKE;
      this._fire("tile", x, y, TILE_SNAKE);
      this._free--;
    }
  }
  moveSnake(dx, dy) {
    let head = this._snake[this._snake.length - 1],
        x = head[0] + dx,
        y = head[1] + dy;
    
    let back = this._snake[0];
    this._tiles[back[0]][back[1]] = TILE_FREE; 
       
    let tile = this._tiles[x][y];
    if (tile == TILE_WALL || tile == TILE_SNAKE) {
      // Crash:
      this._tiles[back[0]][back[1]] = TILE_SNAKE;
      this._fire("crash", x, y);
      
    } else if (tile == TILE_FOOD) {
      // Eat:
      this._tiles[back[0]][back[1]] = TILE_SNAKE;
      this._snake.push([x, y]);
      
      this._tiles[x][y] = TILE_SNAKE;
      this._fire("tile", x, y, TILE_SNAKE);
      this._fire("eat", x, y);
      
    } else {
      // Move:
      this._snake.shift();
      this._fire("tile", back[0], back[1], TILE_FREE);

      this._snake.push([x, y]);
      this._tiles[x][y] = TILE_SNAKE;
      this._fire("tile", x, y, TILE_SNAKE);
    }
  }
  subscribe(event, handler) {
  	this._handlers[event] = this._handlers[event] || [];
    this._handlers[event].push(handler);
  }
  _fire(event, ...args) {
    for (let handler of this._handlers[event] || []) {
      handler(...args);
    }
  }
}

class Game {
  constructor(width, height, speed = 250) {
    this._board = new Board(width, height);
    this._speed = speed;
    this._interval;
    this._current;
    this._next;
  }
  get board() {
    return this._board;
  }
  initialize() {
    this._board.clear();
    
    for (let x = 0; x < this._board.width; ++x) {
      this._board.placeWall(x, 0);
      this._board.placeWall(x, this._board.height - 1);
    }
    for (let y = 0; y < this._board.height; ++y) {
      this._board.placeWall(0, y);
      this._board.placeWall(this._board.width - 1, y);
    }
    
    for (let i = 0; i < 20; ++i) {
      this._board.placeFood();
    }
    
    this._board.placeSnake(Math.floor(this._board.width / 2), Math.floor(this._board.height / 2));
    this._current = undefined;
    this._next = undefined;
  }
  start() {
    if (!this._interval) {
      this._interval = setInterval(this.step.bind(this), this._speed);
    }
  }
  stop() {
    if (this._interval) {
      clearInterval(this._interval);
      this._interval = undefined;
    }
  }
  step() {
    if (this._current) {
      this._board.moveSnake(this._current[0], this._current[1]);
      if (this._next) {
        this._current = this._next;
      }
      this._next = undefined;
    }
  }
  move(dx, dy) {
    if (!this._next) {
      this._current = [dx, dy];
      this._next = this._current;
    } else {
      this._next = [dx, dy];
    }
  }
}

let canvas = document.getElementById("canvas"),
    ctx = canvas.getContext("2d"),
    height = document.body.clientHeight,
    width = document.body.clientWidth,
    game = new Game(10, 10);

canvas.width = Math.min(width, height);
canvas.height = Math.min(width, height);

let scaleX = Math.floor(canvas.width / game._board.width),
    scaleY = Math.floor(canvas.height / game._board.height);
  
game.board.subscribe("tile", function(x, y, tile) {
  // Draw tile:  
  x = x * scaleX;
  y = y * scaleY;
  ctx.clearRect(x, y, scaleX, scaleY);
  
  if (tile == TILE_WALL) {
    ctx.fillStyle = "rgb(0, 0, 0)";
    ctx.fillRect(x, y, scaleX, scaleY);
  } else if (tile == TILE_FOOD) {
    ctx.fillStyle = "rgb(100, 100, 255)";
    ctx.beginPath();
    ctx.ellipse(x + scaleX / 2, y + scaleY / 2, scaleX / 2, scaleY / 2, 0, 0, 2 * Math.PI);
    ctx.fill();
  } else if (tile == TILE_SNAKE) {
    ctx.fillStyle = "rgb(255, 0, 200)";
    ctx.fillRect(x, y, scaleX, scaleY);
  }
});

game.board.subscribe("eat", function(x, y, tile) {
  // Place new food:
  game._board.placeFood();
});

game.board.subscribe("crash", function(x, y, tile) {
  // Restart game:
  game.stop();
  game.initialize();
  game.start();
});

document.onkeydown = function(e) {
  if (e.keyCode == 32) {
    game.stop();
  } else {
    game.start();
    if (e.keyCode == 37) {
      game.move(-1, 0);
    } else if (e.keyCode == 38) {
      game.move(0, -1);
    } else if (e.keyCode == 39) {
      game.move(1, 0);
    } else if (e.keyCode == 40) {
      game.move(0, 1);
    }
  }
}

game.initialize();
game.start();
html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
}
<canvas id="canvas"></canvas>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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