Crafting Pong: Physical Challenge

This is the third part of a series of articles documenting my experience with building a Pong clone in the Playcraft Engine for JavaScript. Looking for Part I or Part II?

In the previous post, we were able to get entities moving around the screen, simply by updating it’s spatial position in increments. However, everything is just drawn without regard to other entities on the screen. In order to make things collide, we need to add a Physics component to our entities, so we can take advantage of all the great functionality included with Playcraft. The physics component requires a physics system to be added to the layer, so we’ll start with updating our GameplayLayer to include it. I added a debug flag to mine, just because I think it looks cool while I’m developing. It comes in handy when things are rotating.

layers/gameplayLayer.js – Physics system

init : function( )
{
   // ...
   this.addSystem( new pc.systems.Render( ) );
   this.addSystem( new pc.systems.Physics( { debug : true } ) );
   // ...
}

Now, we will want to add the physics component to each of our entities. To make them interact with each other, I added a collisionGroup, which is one way of making entities interact with each other. Advanced collision detection can be done using the collisionMask, but for now let’s just keep it simple. I also changed how the ball moves, to utilize the physics component for movement. An impulse (instant movement) with magnitude 2 is applied at an angle 0, causing the ball the move to the right.

scenes/gameScene.js – No more spatial movement

process : function( )
{
   // Handle Ball
   //this.gameplayLayer.ball.spatial.pos.y -= 1;
   //this.gameplayLayer.ball.spatial.pos.x += 2;
}

entities/ball.js – Physics Component

{
   entity : null,
   spatial : null,
   sprite : null,
   physics : null,

   init : function( layer )
   {
      // ...
      this.physics = this.entity.addComponent( pc.components.Physics.create( {
         ball : 1,
         collisionGroup : 1,
         mass : 1
      } ) );
      this.physics.applyImpulse( 2, 0 );
   }
}

entities/player.js, entities/opponent.js – Physics Component

{
   entity : null,
   spatial : null,
   sprite : null,
   physics : null,

   init : function( layer )
   {
      // ...
      this.physics = this.entity.addComponent( pc.components.Physics.create( {
         collisionGroup : 1,
         mass : 100
      } ) );
   }
}

One thing I ran into while working with multiple files was caching. Sometimes when I made updates to a file then refreshed the browser, the changes would not take effect. For me, the quickest and easiest way to fix that behavior was to add a timestamp to the querystring of the files in the index.html.

index.html – Prevent Caching

<script>
pc.start( 'pcGameCanvas', 'Pong', 'js/', [
   'entities/ball.js?' + Date.now( ),
   'entities/player.js?' + Date.now( ),
   'entities/opponent.js?' + Date.now( ),
   'layers/gameplayLayer.js?' + Date.now( ),
   'scenes/gameScene.js?' + Date.now( ),
   'game.js?' + Date.now( )
] );
</script>

Now, the entities react to collisions with other objects of the same group. It’s not perfect, as the ball slows down and seems to push the paddle back when it collides, but it’s a great start. So, what happens when the ball hits the top of the screen or gets past a paddle? Well, nothing right now. The ball just keeps going.

entities/ball.js – Changing Direction

init : function( )
{
   // ...
   this.spatial = this.entity.addComponent( pc.components.Spatial.create( {
      dir : -45,
      x : pc.device.canvas.width / 2 - 8,
      y : pc.device.canvas.height / 2 - 8,
      w : 15,
      h : 15
   } ) );
   // ...
}

To keep the ball in play, we need to add a few more entities to the gameplay layer to represent the walls and goals. Because this is the only place I will be using these entities (probably won’t need walls in the title scene), I decided to add functions to the gameplay layer for creating them, instead of creating object wrappers like the other entities. The walls and goals don’t really do anything, so there isn’t much advantage in doing so.

layers/gameplayLayer.js – Walls and Goals

init : function( )
{
   // ...
   this.createWall( this, 0, -33, pc.device.canvas.width, 33 );
   this.createWall( this, 0, pc.device.canvas.height, pc.device.canvas.width, 33 );

   this.createGoal( this, this.player, pc.device.canvas.width, 0, 33, pc.device.canvas.height );
   this.createGoal( this, this.opponent, -33, 0, 33, pc.device.canvas.height );
},

createGoal : function( layer, paddle, x, y, w, h )
{
   var goal = pc.Entity.create( layer );

   goal.addComponent( pc.components.Spatial.create( {
      x : x,
      y : y,
      w : w,
      h : h
   } ) );

   goal.addComponent( pc.components.Physics.create( {
      collisionGroup : 1,
      immovable : true
   } ) );
},

createWall : function( layer, x, y, w, h )
{
   var wall = pc.Entity.create( layer );

   wall.addComponent( pc.components.Spatial.create( {
      x : x,
      y : y,
      w : w,
      h : h
   } ) );

   wall.addComponent( pc.components.Physics.create( {
      collisionGroup : 1,
      immovable : true
   } ) );
}

The ball now stays within the confines of the viewport, and reacts to collisions with the paddle. There are a lot of physics options that can be set, like friction, damping, and bounciness. Really the only thing we want to do is invert the ball’s Y velocity when it hits a wall, and invert it’s X velocity when it hits a paddle. In order to do this, we need to create our own physics system. With Playcraft, we can extend the default physics system and override the functions for when collisions occur.

layers/gameplayLayer.js – Custom Physics

GameplayPhysics = pc.systems.Physics.extend( 'GameplayPhysics',
   { },
   {
      onCollision : function( aType, bType, entityA, entityB, force, fixtureAType, fixtureBType, contact )
      {
      },

      onCollisionStart : function( aType, bType, entityA, entityB, fixtureAType, fixtureBType, contact )
      {
      },

      onCollisionEnd : function( aType, bType, entityA, entityB, fixtureAType, fixtureBType, contact )
      {
      }
   }
);

GameplayLayer = pc.EntityLayer.extend( 'GameplayLayer',
   { },
   {
      // ...
      init : function( )
      {
         // ...
         this.addSystem( new pc.systems.Render( ) );
         this.addSystem( new GameplayPhysics( { debug : true } ) );
         // ...
      }
   },
   // ...

For our Pong clone, we will want to detect when the ball collides with a wall or with a paddle. In order to detect which entities are which in the custom physics collision functions, I went back and tagged all of the entities. Anytime I called pc.Entity.create (ball, paddles, walls, goals), I added a meaningful tag:

this.entity.addTag( 'BALL' );     // entities/ball.js
this.entity.addTag( 'PADDLE' );   // entities/opponent.js
this.entity.addTag( 'PADDLE' );   // entities/player.js

// createGoal( )                  // layers/gameplayLayer.js
if( paddle === this.player )
{
   goal.addTag( 'PLAYER_GOAL' );
}
else
{
   goal.addTag( 'OPPONENT_GOAL' );
}

// createWall( )
wall.addTag( 'WALL' );

We will also need to change the way we detect collisions. Instead of using a collisionGroup, we are going to use Playcraft’s collision masking method to give us more control over what entities collide. The collisionMask property is a bitwise field, so we’ll need to create our masks first.

layers/gameplayLayer.js – Collision Masks

CollisionType =
{
   NONE :   0x0000, // BIT MASK
   BALL :   0x0001, // 0000001
   GOAL :   0x0002, // 0000010
   PADDLE : 0x0004, // 0000100
   WALL :   0x0008  // 0001000
};

GameplayPhysics = pc.systems.Physics.extend( 'GameplayPhysics',
   // ...

Now, we can update our entities’ physics component to use the bit masks instead of simple collision groups.

// entities/ball.js
this.physics = this.entity.addComponent( pc.components.Physics.create( {
   bounce : 1,
   collisionCategory : CollisionType.BALL,
   //collisionGroup : 1,
   collisionMask : CollisionType.GOAL | CollisionType.PADDLE | CollisionType.WALL,
   mass : 1
} ) );

// entities/opponent.js, entities/player.js
this.physics = this.entity.addComponent( pc.components.Physics.create( {
   collisionCategory : CollisionType.PADDLE,
   //collisionGroup : 1,
   collisionMask : CollisionType.BALL | CollisionType.WALL,
   fixedRotation : true,
   mass : 100
} ) );

// layers/gameplayLayer.js
goal.addComponent( pc.components.Physics.create( {
   collisionCategory : CollisionType.GOAL,
   //collisionGroup : 1,
   collisionMask : CollisionType.BALL,
   immovable : true
} ) );

wall.addComponent( pc.components.Physics.create( {
   collisionCategory : CollisionType.WALL,
   //collisionGroup : 1,
   collisionMask : CollisionType.BALL | CollisionType.PADDLE,
   immovable : true
} ) );

Finally, we can update our custom physics model to make it do what we want it to do, and that’s invert the x or y velocity of the ball when it collides with a paddle or wall.

layers/gameplayLayer.js – Custom Physics

onCollisionStart : function( aType, bType, entityA, entityB, fixtureAType, fixtureBType, contact )
{
   if( aType == pc.BodyType.ENTITY && bType == pc.BodyType.ENTITY )
   {
      if( entityA.hasTag( 'BALL' ) && entityB.hasTag( 'PADDLE' ) )
      {
         var paddlePhysics = entityB.getComponent( 'physics' );
         paddlePhysics.setCollisionMask( CollisionType.WALL );
         paddlePhysics.setLinearVelocity( 0, paddlePhysics.getLinearVelocity( ).y );

         var ballPhysics = entityA.getComponent( 'physics' );
         ballPhysics.setLinearVelocity( -ballPhysics.getLinearVelocity( ).x, ballPhysics.getLinearVelocity( ).y );
      }
      else if( entityA.hasTag( 'BALL' ) && entityB.hasTag( 'WALL' ) )
      {
         var ballPhysics = entityA.getComponent( 'physics' );
         ballPhysics.setLinearVelocity( ballPhysics.getLinearVelocity( ).x, -ballPhysics.getLinearVelocity( ).y );
      }
   }
},

onCollisionEnd : function( aType, bType, entityA, entityB, fixtureAType, fixtureBType, contact )
{
   if( entityA.hasTag( 'BALL' ) )
   {
      if( entityB.hasTag( 'PADDLE' ) )
      {
         entityB.getComponent( 'physics' ).setCollisionMask( CollisionType.BALL | CollisionType.WALL );
      }
   }
}

After a quick refresh, we now have (almost) exactly what we’re looking for. The reason I change the collision mask to exclude the ball, then set it back, is to make the collision not affect the paddle. Without this, the ball causes the paddle to slide backward. The only thing that’s not correct is that the ball bounces off the goals, instead of scoring points. We want the ball to pass through the goal, score a point for the correct paddle, then reset back to the middle. I updated the goals’ physics to only detect collisions, not react to them, then updated what happens when the ball passes through it.

layers/gameplayLayer.js = Goal Physics

// GameplayPhysics
onCollisionEnd : function( aType, bType, entityA, entityB, fixtureAType, fixtureBType, contact )
{
   if( entityA.hasTag( 'BALL' ) )
   {
      var gameplayScene = pc.device.game.getFirstActiveScene( ).object( );

      if( entityB.hasTag( 'PADDLE' ) )
      {
         entityB.getComponent( 'physics' ).setCollisionMask( CollisionType.BALL | CollisionType.WALL );
      }
      else if( entityB.hasTag( 'OPPONENT_GOAL' ) )
      {
         var opponentScore = parseInt( gameplayScene.opponentScore.getComponent( 'text' ).text[ 0 ] );
         gameplayScene.opponentScore.getComponent( 'text' ).text[ 0 ] = opponentScore + 1;
         gameplayScene.gameplayLayer.ball = new Ball( gameplayScene.gameplayLayer );
      }
      else if( entityB.hasTag( 'PLAYER_GOAL' ) )
      {
         var playerScore = parseInt( gameplayScene.playerScore.getComponent( 'text' ).text[ 0 ] );
         gameplayScene.playerScore.getComponent( 'text' ).text[ 0 ] = playerScore + 1;
         gameplayScene.gameplayLayer.ball = new Ball( gameplayScene.gameplayLayer  );
      }
   }
}

// GameplayLayer
createGoal : function( layer, paddle, x, y, w, h )
{
   // ...
   goal.addComponent( pc.components.Physics.create( {
      collisionCategory : CollisionType.GOAL,
      collisionMask : CollisionType.BALL,
      immovable : true,
      sensorOnly : true
   } ) );
}

After refreshing the browser, we now have a (technically speaking) working game of Pong! I made a few tweaks in order to make the paddles not go through the walls, make the ball start in a different direction each time, and gave the paddles similar movement to make it fair. To make the game more challenging, how would you make the ball speed up each time it hits a paddle?

// layers/gameplayLayer.js
// - GameplayPhysics.onCollisionStart
if( entityA.hasTag( 'BALL' ) && entityB.hasTag( 'PADDLE' )
{
   // ...
   ballPhysics.setLinearVelocity( -ballPhysics.getLinearVelocity( ).x * 1.25, ballPhysics.getLinearVelocity( ).y );
}

// scenes/gameScene.js
// - GameScene.process
// Handle AI
if( this.gameplayLayer.ball.physics.getLinearVelocity( ).x > 0 )
{
   if( this.gameplayLayer.ball.spatial.pos.y < this.gameplayLayer.opponent.spatial.pos.y + this.gameplayLayer.opponent.spatial.dim.y / 2 )
   // ...
}

// entities/opponent.js, entities/player.js
init : function( layer )
{
   // ...
   this.physics = this.entity.addComponent( pc.components.Physics.create( {
      // ...
      linearDamping : 0.5,
   } ) );
},

moveDown : function( )
{
   this.physics.applyForce( 100, 90 );
},

moveUp : function( )
{
   this.physics.applyForce( 100, -90 );
}

// entities/ball.js
this.spatial = this.entity.addComponent( pc.components.Spatial.create( {
   dir : Math.floor( Math.random( ) * 4 ) * 90 + ( Math.random( ) * 70 + 10 ),
   // ...
} ) );

Playcraft-Pong

Wow. There’s a lot of stuff involved in making a seemingly very simple game, but it wasn’t that hard! I can honestly say, using the Playcraft engine, it was relatively easy to go from nothing to an actual game. Taking it from here, we could add second player controls, intro and game over scenes, and implement our own crazy physics to make the game our own.

View the full index.html Source
View the full js/game.js Source
View the full js/entities/ball.js Source
View the full js/entities/opponent.js Source
View the full js/entities/player.js Source
View the full js/layers/gameplayLayer.js Source
View the full js/scenes/gameScene.js Source

Play Pong on MrSlayer.com anytime you feel like it, or maybe now you’ll want to play your version.