top of page
Search
cedarcantab

Phaser Coding Tips1&2 Revisited, Part 2: Pixel level collision detection

Updated: Mar 25, 2022


Having learned how to get pixel level data of the HTML canvas directly from Phaser3 framework, I set about recreating the rolling barrels in the classic Donkey Kong game.



I suspect that if you created the platform/girders as a tilemap and each girder tile had a property that contained information on the "slope" angle or angle of the normal to the slope and set up colliders, you could code the barrels to behave using the standard Arcade physics engine (maybe). You might be able to use Matters.js. However, in this particular case, the play-area of platforms is one single png image file. And in anycase, for simple games like this I think it is simpler and quicker to write your own collision detection routines based on pixel information.


Custom collision detection algorithm

In the case of Donkey Kong, if you look at how the platforms are constructed, they are a mixture of flat segments and sloping segments - in fact, even the slopes actually consist of flat platform segments. Importantly, the color used for the platform is not used by any other game objects. Hence, for example, by detecting the color of just a few pixels around the feet of the player character you can in effect determine what state the player is in. For example, if you check points immediately below the player's feet, the number of points that is "touching" the platform can tell you the "shape" of the platform beneath the player. In the example I've created, I have put some restrictions on


Turning our attention back to barrels, I have constrained their movement to 1 pixel or less each frame, making the collision detection routine easy (ie you don't need to worry about checking points beyond the immediate periphery - no need for any raycasting like stuff). In fact, in my example I only check 4 points:

  1. pixel at the bottom left hand corner of the sprite

  2. pixel at the bottom right hand corner of the sprite

  3. pixel immediately below (1)

  4. pixel immediately below (2)

If for example, (3)+(4) are both empty, it means that there is nothing immediately below barrel - so it should fall. For the barrel, which can only move downwards, that is actually all the information you need. For the player though, if he is moving right, and (2) is not empty, it means that the player's "foot" has bumped into a platform segment so he needs to be moved up, and so forth. Very straight forward, but perfectly effective for situations where the objects do not move too fast and surroundings that needs detecting are located according to simple rules.


Create the background


First, I create the playscreen simply as a png image file written to canvas texture.


 create() {
    this.canvas = this.textures.createCanvas('canvastexture', 224, 256);
    this.background = this.textures.get('background').getSourceImage();
    this.canvas.draw(0,0, this.background);          
    this.add.image(0, 0, 'canvastexture').setOrigin(0);
   

Create the characters as Phaser.GameObjects.Sprite


I then created the barrels and the main character as GameObjects.Sprite. I have not used arcade physics engine/body in this example.


As we are using pixel collision, it is important to understand precisely where the game objects are positioned on the screen, and which pixel to detect.


The main character is a 16x16 image. Phaser's sprite objects are by default positioned with their x,y coordinate in the middle (originX = originY = 0.5). Taking the example of this sprite object instantiated at {x: 30, y: 236}, I confirmed the screen coordinates of the various properties of the sprite returne by the methods provided by Phaser Sprite game object.







displayWidth = 16

displayHeight = 16

displayOriginX = 8

displayOriginY = 8

getCenter() ={30,236}

getBottomCenter() = {30, 244}

getBottomRight() = {38, 244}

getBottomLeft() = {22, 244}

getTopRight()= {38, 228}

getTopLeft()={22, 228}

getLeftCenter() == {22, 236}


It is important to note that whilst getTopLeft sits "within" the sprite, getTopRight pixel is actually "outside" the sprite. Furthermore, getBottomLeft is within the left boundary of the sprite but is "beneath" the sprite. As for getBottomRight, this sits outside the sprite to the right and bottom.


The reason is clear if you look at the underlying code of the relevant methods. First take a look at the code for getTopLeft, as below.


getTopLeft: function (output, includeParent)
    {
        if (!output) { output = new Vector2(); }
        output.x = this.x - (this.displayWidth * this.originX);
        output.y = this.y - (this.displayHeight * this.originY);
        return this.prepareBoundsOutput(output, includeParent);
    },

And the code for getBottomRight is as follows.


getBottomRight: function (output, includeParent)
    {
        if (!output) { output = new Vector2(); }
        output.x = (this.x - (this.displayWidth * this.originX)) + this.displayWidth;
        output.y = (this.y - (this.displayHeight * this.originY)) + this.displayHeight;
        return this.prepareBoundsOutput(output, includeParent);

You can see why the getBottomRight results in the pixel which is outside the sprite.


I ended up overwriting the above methods with my own version so that (i) the pixels are positioned consistently but more importantly, (ii) I wanted to make sure that the coordinates are always integers, since getPixel fails if you give it a floating value.


  getBottomLeft(output) {
    if (!output) { output = new Phaser.Math.Vector2(); }
    output.x = Math.floor(this.x - this.displayWidth * this.originX);
    output.y = Math.floor(this.y + this.displayHeight * this.originY);
    return output;
  }

  getBottomRight(output) {
    if (!output) { output = new Phaser.Math.Vector2(); }
    output.x = Math.floor(this.x + this.displayWidth * this.originX - 1);
    output.y = Math.floor(this.y + this.displayHeight * this.originY);
    return output;
  }

Having created the above, I wrote the following simple method to get back data about what is at the "feet" of the game object


  checkPlatform() {
    const bottomLeft = this.getBottomLeft();
    const bottomRight = this.getBottomRight();
    const right = this.scene.canvas.getPixel(bottomRight.x, bottomRight.y-1);
    const left = this.scene.canvas.getPixel(bottomLeft.x, bottomLeft.y-1);
    const leftbelow = this.scene.canvas.getPixel(bottomLeft.x, bottomLeft.y);
    const rightbelow = this.scene.canvas.getPixel(bottomRight.x, bottomRight.y);

    return {
      leftUp: left.red === 236 && right.red === 0,
      rightUp: left.red === 0 && right.red === 236,
      float: leftbelow.red === 0 && rightbelow.red === 0,
      onGround: leftbelow.red === 236 && rightbelow.red === 236
    }
  }

The code is self-explanatory but in essence, it gets the color of the pixels at 4 different locations at the "feet" of the game object - left and right most pixels of the bottom row of the game object, and the left and right pixels in the row immediately below the game object. With this information, you can tell whether the game object is "snug against the ground", the player is "floating", or the platform is sloping up towards the right, or sloping upwards towards the left.


 update() {
    const groundCheck = this.checkPlatform();
    if (groundCheck.float) {
      this.y += 1;
      const recheck = this.checkPlatform();    
      if ((this.velocityX > 0 && this.x >= SCREEN_WIDTH - 12) || (this.velocityX < 0 && this.x <= 12)) {       
        this.velocityX *= -1;
        this.toggleFlipX()
      }
    } else {
      this.x += this.velocityX;
    }
    // if the barrel is has reached the bottom left of the playscreen, reset the position to top-left
    if (this.y >= SCREEN_HEIGHT-15 && this.x < this.displayWidth) {
      this.setPosition(20,10);
      this.velocityX = 0.5;
      this.toggleFlipX();
    };
  }

If there is nothing immediately below the game object, the y-coordinate is incremented by 1 until the game object is "snug" against the platform. I have "hard-coded" a check to see if the barrel is "falling" at the edge of the screen, as opposed falling to the girder which is 1 pixel lower (ie still on sloping platform). When the barrel is falling down at the edge of the screen, its direction is reversed when it hits the next platform.


Main Player

Having coded the above I also added a player that can be moved along the platform. In comparison to the code for the barrel, the logic includes a check to see if the player should be moved up depending on the slope of the platform underneath the player. For example, if it is detected that the platform is sloping upwards towards the right when the player is moving right, then the player will be moved up by 1 pixel. Similarly, if the player is moving left and the platform is sloping upwards towards the left, the player will be moved upwards.


  update() {
      const onLadder = this.checkLadder();
   //   console.log("LADDER!", onLadder)
      const keysDown = this.getKeysDownState();
      const groundStatus = this.checkPlatform();
      if (groundStatus.float) this.y +=1;
      if (keysDown.left) {
        if (groundStatus.leftUp) this.y--;        
        this.x--;
        this.setFlipX(false);
        this.play('walk', true)
      } else if (keysDown.right) {
        if (groundStatus.rightUp) this.y--
        this.x++;
        this.setFlipX(true); 
        this.play('walk', true)
      } else {
        this.play('idle', true)
      }

I got as far as coding a method to detect if the player is standing on top of the ladder. But that is as far as I got.


  checkLadder() {
    let leftFlag = false;
    let rightFlag = false;
    for (let leftX = this.x-8; leftX<this.x; leftX++) {
      leftFlag = leftFlag || (this.scene.canvas.getPixel(leftX, this.y+7).red ===19); 
    }
    for (let rightX = this.x+1; rightX<this.x+8; rightX++) {
      rightFlag = rightFlag || (this.scene.canvas.getPixel(rightX, this.y+7).red ===19); 
    }
    return (rightFlag && leftFlag)
  }

Here's the code for your perusal.





32 views0 comments

Recent Posts

See All

p2 naive broadphase

var Broadphase = require('../collision/Broadphase'); module.exports = NaiveBroadphase; /** * Naive broadphase implementation. Does N^2...

sopiro motor constranit

import { Matrix2, Vector2 } from "./math.js"; import { RigidBody } from "./rigidbody.js"; import { Settings } from "./settings.js";...

Σχόλια


記事: Blog2_Post
bottom of page