Skip to content

Khemitron Industries

Amateur Game Dev, Gaming, and TTRPGs

Menu
  • Home
  • Game Dev
  • About
Menu

Panic Spiral: Walls and Collisions

Posted on 2025-07-21

It’s been a busy two weeks, although, unfortunately, very little of it was spent on game dev. I’ve had multiple job interviews, and then participated in a puzzle hunt (Glyph) with my friends over the weekend. It’s also been horribly warm, so my sleep schedule suffered greatly. Despite these things, however, I have managed to create the basic ship map in Panic Spiral!

But it wasn’t the simple process I thought it would be.

PixiJS, while very powerful for rendering and animating, is not as full featured as a game engine like Godot. It doesn’t include things like collision boxes, or tile grids as standard. While a tile grid isn’t essential, collision boxes are. Without collision logic, how would we know when the player character has hit a wall? And collision logic can be expanded to check if the player is near something they can interact with.

So today’s post focuses on my quest to implement collision logic using PixiJS.

Implementing collisions

What is collision logic?

As you might expect, this is something we experience every day in our day-to-day lives. When we sit down in a chair, we collide with the chair and are prevented from falling through it. When we open a door, our hands collide with the door handle and we can push or pull on the door. Put simply – collisions are what happens when two objects try to exist in the same space.

However, this isn’t really default behaviour for objects displayed on a computer screen.

If you have multiple windows open, you can move them over each other without them colliding. If you open a program like Paint and draw two boxes, there’s no problem with putting one on top of the other. And when you write a program that moves boxes around on a screen, it, by default, doesn’t prevent those boxes from moving on top of each other.

The two boxes don’t intersect – they have not collided
The two boxes overlap – they have collided

In order to make objects collide, we need to implement two things.

  1. We need to detect whether the objects overlap
  2. We need to tell the code what to do when two (or more) objects do overlap

Detecting overlapping objects

Panic Spiral is a fairly simple game. I don’t have extremely detailed character models, and I’m making things easy for myself by having everything fit on a grid. So walls are always a square, and the player character can be abstracted away as a rectangle. This means that collision logic can be done very easily using multiple axis-aligned bounding boxes.

Basically, I can make everything into a rectangle and just check if the two rectangles overlap.

This, basically, is how my collision logic works

You can replace the rectangles with other simple shapes too. For example, the maths for checking whether a circle and a rectangle overlap is not much more complex than checking if two rectangles overlap. The logic can also be extended into 3D games, but that’s beyond the scope of Panic Spiral.

The code for this with PixiJS is relatively straightforward.

In the following example, the (x,y) coordinate for each object is the top left corner of the box

function isOverlapping(objA: Container, objB: Container) {
  // gets the AABB boxes for both objects
  const boundsA = objA.getBounds();
  const boundsB = objB.getBounds();

  // checks if...
  return (
    // either box overlaps in the x direction
    boundsA.x < boundsB.x + boundsB.width &&
    boundsA.x + boundsA.width > boundsB.x &&
    // and if either box overlaps in the y direction
    boundsA.y < boundsB.y + boundsB.height &&
    boundsA.y + boundsA.height > boundsB.y
  );

  // returns true if any part of one box is within the other box
}

Checking if the player overlaps a wall

For Panic Spiral, I decided to create a CollisonZone object that can be added to any object.

export class CollisionZone {
  // the parent object can define its collision box
  // having enabled as a boolean means collisions can be disabled later
  constructor(public enabled: boolean, private _collisionBox: Container) { }
  
  // allow other objects to get the bounds of the collision box
  public getBounds() {
    return this._collisionBox.getBounds();
  }
}

// since I'm using a tile grid, walls are a type of tile
export class Tile extends Container {
  // collisionZone is nullable - floor tiles do not want a collision,
  // while walls do
  public collisionZone?: CollisionZone;

  // when the tile is created, define whether it is collidable
  constructor(x: number, y: number, collidable = false) {
    // general setup stuff

    // add a collision zone if it is collidable
    if (collidable) {
      // for walls, the collision box is the same as the whole wall
      // this may not be the case for decoration tiles
      this.collisionZone = new CollisionZone(true, this);
    }
  }
}

The overlap logic then lives in a NavigationService class that the PlayerCharacter object can use to check if there’s been a collision.

class NavigationService {
  private _collidableObjects: {collisionZone: CollisionZone}[];

  constructor(map: Tile[]) {
    // collidable objects are tiles with a collisionZone
    // In javascript we can take advantage of a concept called 
    // "truthiness" to check this
    this._collidableObjects = map.filter(t => !!t.collisionZone);
  }

  public collides(bounds: Bounds) {
    return this._collidableObjects
      // filter our collidable objects by whether collision is enabled
      .filter((obj) => obj.collisionZone.enabled)
      // then check if any have AABB overlap with the given bounds
      .some((obj) => this.isOverlapping(bounds, obj.getBounds()));
  }

  private isOverlapping(boundsA: Bounds, boundsB: Bounds) => {
          return (
            boundsA.x < boundsB.x + boundsB.width &&
            boundsA.x + boundsA.width > boundsB.x &&
            boundsA.y < boundsB.y + boundsB.height &&
            boundsA.y + boundsA.height > boundsB.y
          );
      })
}

Since the player character is the only object that moves, we can add the trigger to check for collisions to the player character object.

class PlayerCharacter extends Container {
  private _collisionBox: Container;
  
  // boolean for whether the player's moving or not
  private _isMoving: boolean = false;

  constructor(private _navigationService: NavigationService) {
    // create a transparent rectangle for the collision box
    this._collisionBox = new Graphics()
      .rect(0, 8, 4, 4)
      .stroke("#00000000); // alpha channel 0 makes it transparent

    // add the collision box as a child so it moves with the player
    this.addChild(this._collisionBox);
  }
  
  // update function that gets run every frame 
  public update(ticker: Ticker) {
    // only check for collisions if the player's moving
    if (this._isMoving) {
      // check if there's a collision
      if (this._navigationService.collides(
        this._collisionBox.getBounds())) { 
        // do something on collision
      }
    }
  }
}

Some of this logic is abstracted away into moving state code, but this is the basic premise of how it works.

Preventing the player character from moving into a wall

The logic described above just checks if the player’s collision zone overlaps another collision zone. Now we actually have to do something with that information to stop the player character moving into the wall.

I tried a couple of things and eventually settled on the following algorithm:

  1. Calculate the player’s new position when they’re moving
  2. Use that new position to create a projected set of bounds
  3. Check if the projected bounds collide with a wall
  4. If not, then move the player character into that new position
  5. Otherwise, don’t move the player character into the new position

The code for this is fairly straightforward.

class PlayerCharacter extends Container {
  private _collisionBox: Container;
  private _isMoving: boolean = false;

  constructor(private _navigationService: NavigationService) {
    // same as before. Code omitted here for brevity
  }
  
  public update(ticker: Ticker) {
    if (this._isMoving) {
      // get the new (x,y) coordinate
      const [newX, newY] = 
        this.updatePosition(this.x, this.y, ticker.deltaMS);

      // get the projected bounds and check that it DOESN'T collide
      if (!this._navigationService.collides(
        this.getProjectedBounds(newX, newY))) { 
        // if it doesn't collide, we can safely move the player character
        this.x = newX;
        this.y = newY;
      }
    }
  }

  private updatePosition(x: number, y: number, deltaMS: number) {
    // example here if the player is moving left
    // reduces the player y coordinate by a modifer multiplied by the
    // number of milliseconds that have elapsed
    return [x, y - PLAYER_MOVEMENT.MOVEMENT_SPEED_MODIFIER * deltaMS];
  }

  private getProjectedBounds(newX: number, newY: number) {
    // gets the change in x and y
    const [xDiff, yDiff] = [
      newX - this.x,
      newY - this.y
    ];

    // get the actual bounds of the collision box
    const bounds = this._collisionBox.getBounds();

    // return a new bounds object with the change added on
    return new Bounds(
      bounds.minX + xDiff,
      bounds.maxX + xDiff,
      bounds.minY + yDiff,
      bounds.maxY + yDiff
  };
}

This works fine since there aren’t multiple moving objects. If there were multiple moving objects, then the logic for collisions would need to be handled in a different way. My thought would be that each object would have a function that could be called when a collision is detected, and that function could be used to return an object to its previous position, or perform some other action depending on what other object it has collided with.

I took a short video to demonstrate my collision logic and posted it on Bluesky a little while ago.

I had a lot on this week so didn't do much game dev. My main accomplishment is implementing collision boxes for Panic Spiral. Collision logic doesn't come as standard in PixiJS. It's amazing what little things I've started to miss from Godot. Here's a little tech demo! #gamedev #indiedev #pixijs

[image or embed]

— Khemi (@khemitron-industries.net) July 14, 2025 at 6:14 AM

Closing words

With collisions implemented, I’ve moved on to making the ship map in the game. My next post will go into the details of how I added that – spoiler alert: I had to create my own tile grid the logic for adding tiles to it.

Overall, I’m finding PixiJS quite fun to work with, even if it is lacking features that the big game engines have as standard. PixiJS itself is excellent for animations and I’m finding it very interesting to implement systems that I’ve taken for granted so far. It’s helping to give me a greater appreciation of how things are put together, and the challenge is something that I love.

I’m now at a point where I should be able to spend more time on game development again, so I’m hoping that I’ll be able to progress faster with Panic Spiral and have a working demo soon!

Share on Social Media
facebook tumblr reddit emailwhatsapp

Like this:

Like Loading...

Recent Posts

  • Panic Spiral: Progress Demo #1 2025-08-04
  • Panic Spiral: Mapping the Ship 2025-07-28
  • Panic Spiral: Walls and Collisions 2025-07-21
  • Panic Spiral: Time for a Rewrite 2025-07-07
  • Panic Spiral: Laying Strong Foundations 2025-06-30
©2026 Khemitron Industries | Design: Newspaperly WordPress Theme
%d