Crafting Pong: Making Things Move

This is the second 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 III?

In the first post, we were able to modify the sample project included with Playcraft and render all the various entities on the screen. This works out well for smaller projects, but I found it to become a bit cumbersome dealing with all the logic in a single game.js file. While, in the long run, having everything in a single file is ideal (reducing HTTP requests), for development, I like to keep my logic separated.

That is why I stripped out the scenes, layers, and entities and created wrapper objects in their own files. First, let’s put the GameScene in it’s own file (I created a scenes subfolder under js), leaving just the Pong game object in game.js. To load this file, all we have to do is update the scripts array in our index.html.

This allowed me to more easily manage multiple scenes, in case I want to add an Intro Scene or Game Over Scene. Now, thinking ahead, let’s say I wanted to add a background to the game scene. I could simply add more logic to this GameScene object, but I foresee that becoming difficult to manage as well. What if I wanted to have a background, a foggy overlay, gameplay and a pause menu? So, I decided to create a GameplayLayer object and included it in the pc.start().

index.html – Multiple Scripts

<script>
   pc.start( 'pcGameCanvas', 'Pong', 'js/', [
      'layers/gameplayLayer.js',
      'scenes/gameScene.js',
      'game.js'
   ] );
</script>

The GameplayLayer will be an extension of an EntityLayer. In Playcraft, there are regular Layers, EntityLayers and TileLayers, which all serve different purposes. An EntityLayer’s purpose is to handle entities. When we extend the pc.EntityLayer, we need to make sure our init function has the same parameters. So, looking at the API, we have a layer id, and dimensions for the “world” that our entities live in.

layers/gameplayLayer.js – Starting Point

GameplayLayer = pc.EntityLayer.extend( 'GameplayLayer',
   { },
   {
      init : function( layerId, worldSizeX, worldSizeY )
      {
         this._super( layerId, worldSizeX, worldSizeY );
         // ...
      }
   }
);

Instead of just copying all of the code for creating the entities, I’ll separate them into their own container objects, as well. You’ll often be accessing properties of the entity’s various components, and providing a container object made it easier for me to access things like entity position. All of this is not necessary, but in the long run, I think it will pay off. I ended up extending a pc.Base class, which is just a generic class, and added in accessors for the entity object and it’s components.

entities/ball.js – Entity container object

Ball = pc.Base.extend( 'Ball',
   { },
   {
      entity : null,
      spatial : null,
      sprite : null,

      init : function( layer )
      {
         this._super( layer );

         this.entity = pc.Entity.create( layer );

         this.sprite = this.entity.addComponent(
            pc.components.Circle.create( { color : '#FFFFFF' } )
         );

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

I did the same for the player and opponent entities as well, making sure to include all of them in pc.start(). Now, each entity resides in its own file, allowing me to separate each entity’s logic and avoid a big wall of code. I’m sure an IDE would come in handy, but I’ve always found myself coding in simple text editors like Notepad and vim. Now, we add our entities to the gameplay layer and update our GameplayScene to use the new GameplayLayer object.

layers/gameplayLayer.js – Adding entities

ball : null,
player : null,
opponent : null, 

init : function( layerId, worldSizeX, worldSizeY )
{
   this._super( layerId, worldSizeX, worldSizeY );

   // Render System required for entities to use Spatial and shape components
   this.addSystem( new pc.systems.Render( ) );

   this.ball = new Ball( this );
   this.player = new Player( this );
   this.opponent = new Opponent( this );
}

scenes/gameplayScene.js – Adding layer

// ball : null,
// player : null,
// opponent : null,
gameplayLayer : null,
playerScore : null,
opponentScore : null, 

init : function( )
{
   this._super( );

   this.gameplayLayer = this.addLayer(
      new GameplayLayer( 'GameplayLayer', 10000, 10000 );
   );

   // ...
}

After testing, we see that the game is still doing what it did before, but with the benefit of separated logic. All of this work is supposed to make it easier to manage, so let’s see how it works as we add the movement. Note: If you’ve renamed your gameLayer to gameplayLayer, be sure to update any other references, like the scores.

By now, I’m sure you’ve seen the input action and event listener that was included in the sample project. All you have to do is bind an action to an input event, then in the onAction function call, you handle that event. For Pong, we will need to move the player up and down using the arrow keys. So, first, let’s bind the actions to the input events to the init function for our GameplayScene. The ‘UP’ and ‘DOWN’ parameters refer to the input event, in this case the Up Arrow and Down Arrow.

scenes/gameplayScene.js – Bind input actions

init : function( )
{
   // ...
   pc.device.input.bindAction( this, 'MOVE_UP', 'UP' );
   pc.device.input.bindAction( this, 'MOVE_DOWN', 'DOWN' );
},

Then, in the onAction function of our GameplayScene, we tell the engine what to do. In this case, when the MOVE_UP action is triggered, we want to move the player up. Conversely, if the MOVE_DOWN action is triggered, we want to move the player down.

scenes/gameplayScene.js – Handle actions

// ...
onAction : function( actionName, event, pos )
{
   if( actionName === 'MOVE_UP' )
   {
      this.gameplayLayer.player.moveUp( );
   }
   else if( actionName === 'MOVE_DOWN' )
   {
      this.gameplayLayer.player.moveDown( );
   }
},

entities/player.js – Input Actions

moveDown : function( )
{
   this.spatial.pos.y += 2;
},

moveUp : function( )
{
   this.spatial.pos.y -= 2;
}

When we review what happens, we notice that it triggers the action with a short delay. There are also input states, which are triggered every update cycle while an input is held. So, lets change the input to use states, and now we see that holding the Up arrow causes the player to continuously move up. Notice that handling states doesn’t trigger the onAction function, so we test in the process function.

scenes/gameplayScene.js – Input States

init : function( )
{
   // ... 
   pc.device.input.bindState( this, 'MOVE_UP', 'UP' );
   pc.device.input.bindState( this, 'MOVE_DOWN', 'DOWN' );
},

// onAction : function( actionName, event, pos )
// {
//    if( actionName === 'MOVE_UP' )
//    {
//       this.gameplayLayer.player.moveUp( );
//    }
//    else if( actionName === 'MOVE_DOWN' )
//    {
//       this.gameplayLayer.player.moveDown( );
//    }
// },

process : function( )
{
   // ...
   // Handle Input
   if( pc.device.input.isInputState( this, 'MOVE_UP' ) )
   {
      this.gameplayLayer.player.moveUp( );
   }
   else if( pc.device.input.isInputState( this, 'MOVE_DOWN' ) )
   {
      this.gameplayLayer.player.moveDown( );
   }

   pc.device.ctx.clearRect( 0, 0, pc.device.canvas.width, pc.device.canvas.height );
   this._super( );
}

The ball movement is done similarly to the player control, but instead of doing it on an input event, the ball is just always moving. So, what we want to do is update the ball position every time the game processes an update cycle.

scenes/gameplayScene.js – Process ball

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

The opponent is controlled by the computer, and artificial intelligence is a topic all its own. For simplicity’s sake, we can make the opponent try to return the volley by moving it whenever the ball is moving towards them. Again, we want to do this check every time the game processes an update cycle.

scenes/gameplayScene.js – Process opponent

process : function( )
{
   // ...
   // Handle AI
   if( this.gameplayLayer.ball.spatial.pos.y < this.gameplayLayer.opponent.spatial.pos.y )
   {
      this.gameplayLayer.opponent.moveUp( );
   }
   else
   {
      this.gameplayLayer.opponent.moveDown( );
   }
   // ...
}

entities/opponent.js – Actions

init : function( )
{
   // ...
},

moveDown : function( )
{
   this.spatial.pos.y += 1;
},

moveUp : function( )
{
   this.spatial.pos.y -= 1;
}

Movement

Now that the we got things moving and some crappy AI, we need to be able to handle when things move into one another. In the next post I’ll look at the Physics component and system, which is is used for making entities collide.

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

To be concluded…