Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
200 views
in Technique[技术] by (71.8m points)

javascript - HTML JS Canvas Game: Tile Collision Bug Makes Player Teleport Up

I'm a beginner at game development and have been struggling with getting collision done right between an array of tiles and a player rectangle. This game features jumping and gravity. First of all the collision works, but is very clunky. Sometimes when the player ends up on top of a tile and a little to the edge, it will instantly teleport to either the right or left side (depending on what edge/corner) and fall of it. This also happens when colliding with the bottom of the tile; the player will instantly teleport to the side and go up further. From what I understand the tile collision detector confuses the collision with one or the other side because when the player hits the edge of a tile the detector reads it as if it collided with both and decides to place the player elsewhere base on the highest coordinate velocity (aka speedX and speedY). I figured this out by setting speedY = 0 every time it hits the top of a tile, which fixed the issue, but another one came out of it. Now, if the player is on top of a tile and then falls of and shortly strafe back, it doesn't collide with the tile's side, but it rather quickly goes back on top of it again.

I just need some tips on how i should resolve this, because everything I try leads to another problem. I've heard this is a common bug among developing 2D tile based games.

Here is a jsfiddle with the code in action: https://jsfiddle.net/8121u356/

And here is the display of my entire code:

function startGame() {
    gameArea.start();
    actor = new player(32, 32, "green", 32, 32);
}

var mapArray = [
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,0,0,0,0],
    [0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,1,1,1,0,0,0,1,1,0,0,0,0,0,0],
    [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]

];

var levelRows = 20;
var levelCols = 20;
 var gameArea = {
     canvas : document.getElementById('canvas'),

     start : function() {
         this.context = this.canvas.getContext("2d");
         document.body.insertBefore(this.canvas, document.body.childNodes[0]);
         requestAnimationFrame(updateGameArea);
         window.addEventListener('keydown', function (e) {
             gameArea.keys = (gameArea.keys || []);
             gameArea.keys[e.keyCode] = true;
         });

         window.addEventListener('keyup', function (e) {
             gameArea.keys = (gameArea.keys || []);
             gameArea.keys[e.keyCode] = false;
         })
     },

     clear : function(){
         this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
     },
     render : function() {
         context = this.canvas.getContext("2d");
         var tileSize = 32;
         for(i=0;i<levelRows;i++){
             for(j=0;j<levelCols;j++){
                 if(mapArray[i][j]==1){
                     context.fillStyle = "gray";
                     context.fillRect(j*tileSize,i*tileSize,tileSize,tileSize);
                 }
             }
         }
     }
 };

function TileCollisionManager(object) {
    let tileSize = 32;
    let baseCol = Math.floor(object.x / tileSize);
    let baseRow = Math.floor(object.y / tileSize);
    let colOverlap = object.x % tileSize;
    let rowOverlap = Math.floor(object.y % tileSize);

    if (object.speedX > 0) { 
        if ((mapArray[baseRow][baseCol + 1] && !mapArray[baseRow][baseCol]) ||
            (mapArray[baseRow + 1][baseCol + 1] && !mapArray[baseRow + 1][baseCol] && rowOverlap)) {
            object.x = baseCol * tileSize;
        }
    }

    if (object.speedX < 0) {
        if ((!mapArray[baseRow][baseCol + 1] && mapArray[baseRow][baseCol]) ||
            (!mapArray[baseRow + 1][baseCol + 1] && mapArray[baseRow + 1][baseCol] && rowOverlap)) {
            object.x = (baseCol + 1) * tileSize;
        }
    }

    if (object.speedY > 0) { 
        if ((mapArray[baseRow + 1][baseCol] && !mapArray[baseRow][baseCol]) || 
            (mapArray[baseRow + 1][baseCol + 1] && !mapArray[baseRow][baseCol + 1] && colOverlap)) {
            object.y = ((baseRow) * tileSize);
            object.jumping = false;
            object.speedY = 0;
        }
    }

    if (object.speedY < 0) { 
        if ((!mapArray[baseRow + 1][baseCol] && mapArray[baseRow][baseCol]) ||
            (!mapArray[baseRow + 1][baseCol + 1] && mapArray[baseRow][baseCol + 1] && colOverlap)) {
            object.y = (baseRow + 1) * tileSize;
            object.speedY = 5;
        }
    }
 }
  function updateGameArea() {
      gameArea.clear();
      gameArea.render();
      actor.update();
      actor.newPos();
      actor.speedX = 0;
      actor.speedY += actor.gravity;


      if (gameArea.keys && gameArea.keys[39]) {
          actor.speedX = 4;
      }
      if (gameArea.keys && gameArea.keys[37]) {
          actor.speedX = -4;
      }
      if (gameArea.keys && gameArea.keys[32]) { 
          if (!actor.jumping) {
              actor.jumping = true;
              actor.speedY = -actor.speed * 3;
          }
      }


      TileCollisionManager(actor);
      requestAnimationFrame(updateGameArea);
  }


  function player (width, height, color, x, y) { 
      this.width = width;
      this.height = height;
      this.x = x;
      this.y = y;
      this.speedX=0;
      this.speedY=0;
      this.gravity=0.3;
      this.speed=3;
      this.jumping=false;
      this.color = color;
      this.update = function () {
          ctx = gameArea.context;
          ctx.fillStyle = this.color;
          ctx.fillRect(
              this.x,
              this.y,
              this.width, this.height);
      };
      this.newPos = function () {
          this.x += this.speedX;
          this.y += this.speedY;
      };
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

A quick fix for you.

I have seen you post this question for the 3rd time. You are not getting an answer because the best solution is rather a lot of code, complex, and requires a lot of changes to your code.

So what I have done is create a very quick and simple solution.

Solve collisions in the correct order.

Rather than check the position at the end of a move, I changed the code to check at every pixel moved. This is needed as you have to find the collisions in the correct order as the player moves from one position to the next. If you hit a wall on the side before the top or bottom, or the other way around it makes a difference, and is what is causing you to have problems. You checked x first then y, which for many situation is the wrong way around.

I also added a object to the actor called canMove It has 4 properties that are set at the start of each frame and are used to prevent the player moving in a direction that is blocked. If you let the player move in a blocked direction it will get stuck on the wall while you have the key down in that direction.

I hacked into your code

Sorry I made a bit of a mess but am short on time.

Also to help me write the changes I made a few other mods, I scaled that game to fit the window (the scaling and resize is all done in the clear function). I changed the keyboard interface to prevent default on keys pressed and set up arrow to jump as well as space (I hate using space to jump :P). Also change the map to use strings as it is a pain typing in changes as an array.

I was not sure how you wanted the actor to react when it is hit on the head. I made it so that it bounces down at the same speed as it moves up, but it does make it harder to jump and slide into narrow passages.

So I think I have most of it done, so you can move on with your game.

If you have questions, ask in the comments.

   // NOTE  var | 0 is the same as Math.floor(var)

var mapArray = [
        "#                  #",
        "#                  #",
        "#  ###             #",
        "#           #      #",
        "#       ##  #####  #",
        "#                  #",
        "#                  #",
        "#    ##            #",
        "#      ##          #",
        "#                  #",
        "#      #####       #",
        "#                  #",
        "#                  #",
        "#       #####      #",
        "#                  #",
        "#                  #",
        "#       #     ##   #",
        "#      ###         #",
        "#     ##### ##     #",
        "####################",
    ].map(row => row.split("").map(cell=>cell==="#" ? 1 : 0));


    var levelRows = 20;
    var levelCols = 20;
    var tileX = 32;
    var tileY = 32;
    var gameArea = {
     canvas : document.getElementById('canvas'),
     ctx : document.getElementById('canvas').getContext("2d"),
     keys : {  // set them here so that can block defaults
        "37" : false,
        "38" : false,     // also jump
        "39" : false,
        "32" : false,     // jump
     },

     start : function() {
         document.body.insertBefore(this.canvas, document.body.childNodes[0]);
         requestAnimationFrame(updateGameArea);
         function keyEvent(e) { 
             if(gameArea.keys["" + e.keyCode] !== undefined){
              gameArea.keys["" + e.keyCode] = e.type === "keydown" 
              e.preventDefault();
             }
         }
         addEventListener('keydown', keyEvent);
         addEventListener('keyup', keyEvent);
         focus();
        },

        clear(){
            var minSize = Math.min(innerWidth,innerHeight);
            if (this.ctx.canvas.width !== minSize|| this.ctx.canvas.height !== minSize) {
                this.ctx.canvas.width = minSize;
                this.ctx.canvas.height = minSize;
            }
                
            this.ctx.setTransform(1,0,0,1,0,0);
            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
            
            // the next line scales the canvas rendering to fit.
            this.ctx.setTransform(
                minSize / (levelCols * tileX),
                0,
                0,
                minSize/ (levelRows * tileY),
                0,0
            );
        },
        render() {
            var ctx = this.ctx;
            for(i=0;i<levelRows;i++){
                for(j=0;j<levelCols;j++){
                    if(mapArray[i][j]==1){
                        ctx.fillStyle = "gray";
                        ctx.fillRect(j*tileX,i*tileY,tileX,tileY);
                    }
                }
            }
        }
    };

    function updateGameArea() { 
        gameArea.clear();
        actor.canMove.check();
        actor.speedX = 0;
        if(actor.canMove.down){
            actor.speedY += actor.gravity;
        }

        if (gameArea.keys[39] && actor.canMove.right) {
            actor.speedX = 4;
        }
        if (gameArea.keys[37]  && actor.canMove.left) {
            actor.speedX = -4;
        }
        if (actor.canMove.up && (gameArea.keys[32] || gameArea.keys[38])) { //jump
            if (!actor.jumping) {
                actor.jumping = true;
                actor.speedY = -actor.speed * 3;
            }
        }
        actor.move();  // collision is done here
        
        gameArea.render();
        actor.draw();

        requestAnimationFrame(updateGameArea);
    }

    function Player (width, height, color, x, y) { //player component
        this.width = width;
        this.height = height;
        this.x = x;
        this.y = y;
        this.speedX=0;
        this.speedY=0;
        this.gravity=0.3;
        this.speed=3;
        this.jumping=false;
        this.color = color;
        this.canMove = {
           left : true,
           right : true,
           up : true,
           down : true,
           actor : this,
           clear(){
              this.left = true;
              this.right = true;
              this.up = true;
              this.down = true;
           },
           check(){
              this.clear();
              var x = this.actor.x | 0;
              var y = this.actor.y | 0;
              var cx = x / tileX | 0;
              var cy = y / tileY | 0;
              if(x % tileX === 0){
                 if(getMap(cx-1,cy) === 1){                 
                   this.left = false;
                   if(y % tileY !== 0 && getMap(cx-1,cy +1) === 1){
                      this.left = false;
                   }
                 }
                 if(getMap(cx+1,cy) === 1){
                   this.right = false;
                   if(y % tileY !== 0 && getMap(cx+1,cy +1) === 1){
                      this.right = false;
                   }
                 }
              }
              if(y % tileY === 0){
                 if(getMap(cx,cy-1) === 1){                 
                   this.up = false;
                   if(x % tileX !== 0 && getMap(cx+1,cy -1) === 1){
                      this.up = false;
                   }
                 }
                 if(getMap(cx,cy+1) === 1){
                   this.down = false;
                   if(x % tileX !== 0 && getMap(cx+1,cy +1) === 1){
                      this.down = false;
                   }
                 }
              }
           }
        };

        this.draw = function () {
            var ctx = gameArea.ctx;
            ctx.fillStyle = this.color;
            ctx.fillRect(  this.x,this.y, this.width, this.height);
        };
        this.move = function() {
          var x = this.x;
          var y = this.y;
          var sx = this.speedX;
          var sy = this.speedY;
          var speed = Math.sqrt(sx * sx + sy * sy);
          if(speed > 0){
            sx /= speed;
            sy /= speed;
            for(var i = 0; i < speed; i++){
              var xx = (x + sx * i) | 0;
              var yy = (y + sy * i) | 0;
              var cx = xx / tileX | 0;
              var cy = yy / tileY | 0;
              if(sy > 0){
                if(getMap(cx,cy+1) === 1 || (xx % tileX !== 0 && getMap(cx + 1,cy+1))){
                  this.y = y = cy * tileY;
                  this.speedY = sy = speed < 4 ? 0 : -3;
                  this.jumping = false;
                }
              }else if(sy < 0){
                if(getMap(cx,cy) === 1 || (xx % tileX !== 0 && getMap(cx + 1,cy))){
                  cy += 1;
                  this.y = y = cy * tileY;
                  this.speedY = sy = -sy;  // changing -sy to 0 will stick momentarily to the roof.
                }
              }
              if(sx > 0){
                if(getMap(cx+1,cy) === 1 ||  (yy % tileY !== 0 && getMap(cx + 1,cy+1))){
                  this.x = x = cx * tileX;
                  this.speedX = sx = 0;
                }

              }else if(sx < 0){
                if(getMap(cx,cy) === 1 || (yy % tileY !== 0 && getMap(cx,cy+1))){
                  cx += 1;
                  this.x = x = cx * tileX;
                  this.speedX = sx = 0;
                }          
              }
            }
          }
          this.x += this.speedX;
          this.y += this.speedY;
        }
    }

    function getMap(x,y){
      if(y < 0 || y >= levelRows || x < 0 || x >= levelCols){
         return 1;
      }
      return mapArray[y][x];
    }
 

    gameArea.start();
    actor = new Player(32, 32, "green", 32, 32);
canvas { 
   position : absolute;
   top : 0px;
   left : 0px;
}
<canvas id = "canvas" width="640" height="640"></canvas>

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...