Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon
11. September 2012

 

In this part, we are going to create the GameScene.cs, the backbone of our game.  Most of the topics we cover here we have actually seen already in prior tutorials.  Let’s jump right in:

 

GameScene.cs

 

using System;
using Sce.PlayStation.Core;
using Sce.PlayStation.HighLevel.GameEngine2D;
using Sce.PlayStation.HighLevel.GameEngine2D.Base;
using Sce.PlayStation.HighLevel.Physics2D;
using Sce.PlayStation.Core.Audio;

namespace Pong
{
    public class GameScene : Scene
    {
    private Paddle _player,_ai;
    public static Ball ball;
    private PongPhysics _physics;
    private Scoreboard _scoreboard;
    private SoundPlayer _pongBlipSoundPlayer;
    private Sound _pongSound;
        
        // Change the following value to true if you want bounding boxes to be rendered
        private static Boolean DEBUG_BOUNDINGBOXS = false;
        
        public GameScene ()
        {
            this.Camera.SetViewFromViewport();
            _physics = new PongPhysics();

            
            ball = new Ball(_physics.SceneBodies[(int)PongPhysics.BODIES.Ball]);
            _player = new Paddle(Paddle.PaddleType.PLAYER, _physics.SceneBodies[(int)PongPhysics.BODIES.Player]);
            _ai = new Paddle(Paddle.PaddleType.AI, _physics.SceneBodies[(int)PongPhysics.BODIES.Ai]);
            _scoreboard = new Scoreboard();
            
            this.AddChild(_scoreboard);
            this.AddChild(ball);
            this.AddChild(_player);
            this.AddChild(_ai);
            
            
            // This is debug routine that will draw the physics bounding box around the players paddle
            if(DEBUG_BOUNDINGBOXS)
            {
                this.AdHocDraw += () => {
                    var bottomLeftPlayer = _physics.SceneBodies[(int)PongPhysics.BODIES.Player].AabbMin;
                    var topRightPlayer = _physics.SceneBodies[(int)PongPhysics.BODIES.Player].AabbMax;
                    Director.Instance.DrawHelpers.DrawBounds2Fill(
                        new Bounds2(bottomLeftPlayer*PongPhysics.PtoM,topRightPlayer*PongPhysics.PtoM));

                    var bottomLeftAi = _physics.SceneBodies[(int)PongPhysics.BODIES.Ai].AabbMin;
                    var topRightAi = _physics.SceneBodies[(int)PongPhysics.BODIES.Ai].AabbMax;
                    Director.Instance.DrawHelpers.DrawBounds2Fill(
                        new Bounds2(bottomLeftAi*PongPhysics.PtoM,topRightAi*PongPhysics.PtoM));

                    var bottomLeftBall = _physics.SceneBodies[(int)PongPhysics.BODIES.Ball].AabbMin;
                    var topRightBall = _physics.SceneBodies[(int)PongPhysics.BODIES.Ball].AabbMax;
                    Director.Instance.DrawHelpers.DrawBounds2Fill(
                        new Bounds2(bottomLeftBall*PongPhysics.PtoM,topRightBall*PongPhysics.PtoM));
                };
            }
            
            //Now load the sound fx and create a player
            _pongSound = new Sound("/Application/audio/pongblip.wav");
            _pongBlipSoundPlayer = _pongSound.CreatePlayer();
            
            Scheduler.Instance.ScheduleUpdateForTarget(this,0,false);
        }
        
        private void ResetBall()
        {
            //Move ball to screen center and release in a random directory
            _physics.SceneBodies[(int)PongPhysics.BODIES.Ball].Position = 
                new Vector2(Director.Instance.GL.Context.GetViewport().Width/2,
                            Director.Instance.GL.Context.GetViewport().Height/2) / PongPhysics.PtoM;
            
            System.Random rand = new System.Random();
            float angle = (float)rand.Next(0,360);
        
            if((angle%90) <=15) angle +=15.0f;
        
            _physics.SceneBodies[(int)PongPhysics.BODIES.Ball].Velocity = 
                new Vector2(0.0f,5.0f).Rotate(PhysicsUtility.GetRadian(angle));
        }
        
        public override void Update (float dt)
        {
            base.Update (dt);
            
            if(Input2.GamePad0.Select.Press)
                Director.Instance.ReplaceScene(new MenuScene());
            
            //We don't need these, but sadly, the Simulate call does.
            Vector2 dummy1 = new Vector2();
            Vector2 dummy2 = new Vector2();
            
            //Update the physics simulation
            _physics.Simulate(-1,ref dummy1,ref dummy2);
            
            //Now check if the ball it either paddle, and if so, play the sound
            if(_physics.QueryContact((uint)PongPhysics.BODIES.Ball,(uint)PongPhysics.BODIES.Player) ||
                _physics.QueryContact((uint)PongPhysics.BODIES.Ball,(uint)PongPhysics.BODIES.Ai))
            {
                if(_pongBlipSoundPlayer.Status == SoundStatus.Stopped)
                    _pongBlipSoundPlayer.Play();
            }
            
            //Check if the ball went off the top or bottom of the screen and update score accordingly
            Results result = Results.StillPlaying;
            bool scored = false;
            
            if(ball.Position.Y > Director.Instance.GL.Context.GetViewport().Height + ball.Scale.Y/2)
            {
                result = _scoreboard.AddScore(true);
                scored = true;
            }
            if(ball.Position.Y < 0 - ball.Scale.Y/2)
            {
                result =_scoreboard.AddScore(false);
                scored = true;
            }
            
            // Did someone win?  If so, show the GameOver scene
            if(result == Results.AiWin) 
                Director.Instance.ReplaceScene(new GameOverScene(false));
            if(result == Results.PlayerWin) 
                Director.Instance.ReplaceScene(new GameOverScene(true));
            
            //If someone did score, but game isn't over, reset the ball position to the middle of the screen
            if(scored == true)
            {
                ResetBall ();
            }
            
            //Finally a sanity check to make sure the ball didn't leave the field.
            var ballPB = _physics.SceneBodies[(int)PongPhysics.BODIES.Ball];
            
            if(ballPB.Position.X < -(ball.Scale.X/2f)/PongPhysics.PtoM ||
               ballPB.Position.X > (Director.Instance.GL.Context.GetViewport().Width)/PongPhysics.PtoM)
            {
                ResetBall();
            }
        }
        
        ~GameScene(){
            _pongBlipSoundPlayer.Dispose();
        }
    }
}

 

Alright, taking it from the top:

 

private Paddle _player,_ai;
public static Ball ball;
private PongPhysics _physics;
private Scoreboard _scoreboard;
private SoundPlayer _pongBlipSoundPlayer;
private Sound _pongSound;

// Change the following value to true if you want bounding boxes to be rendered
private static Boolean DEBUG_BOUNDINGBOXS = false;

 

Here we declare a number of member variables.  We create a Ball object, a Scoreboard object and two Paddle objects, all of which are derived from SpriteUV.  We will cover these in more detail shortly.  We also create an instance of the PhysicsScene we just defined in the last part, PongPhysics.  Next we declare a SoundPlayer and Sound for playing our pong “blip”.  Finally we have a const Boolean value, DEBUG_BOUNDINGBOXS, which you switch to true if you want object bounding boxes on the screen.  This is debug code I originally used to debug a problem and it proved handy so I left it in.

 

At the beginning of the constructor, we:

 

this.Camera.SetViewFromViewport();
_physics = new PongPhysics();


ball = new Ball(_physics.SceneBodies[(int)PongPhysics.BODIES.Ball]);
_player = new Paddle(Paddle.PaddleType.PLAYER, _physics.SceneBodies[(int)PongPhysics.BODIES.Player]);
_ai = new Paddle(Paddle.PaddleType.AI, _physics.SceneBodies[(int)PongPhysics.BODIES.Ai]);
_scoreboard = new Scoreboard();

this.AddChild(_scoreboard);
this.AddChild(ball);
this.AddChild(_player);
this.AddChild(_ai);

 

First we configure our Scene’s camera then create our PongPhysics object. Here we create our Ball, Paddles and Scoreboard objects.  For the Ball, we pass in the index value of the ball rigid body, that we defined in the handy enum PongPhysics.BODIES.  We also pass the PhysicsBody for each Paddle.  We also pass in the Paddle.PaddleType we want to create, telling it whether to create a player or ai paddle.  Obviously, we want one of each.  We then create a Scoreboard object, we will cover each of these shortly.  Finally, we add all four objects to the scene.

 

if(DEBUG_BOUNDINGBOXS)
{
    this.AdHocDraw += () => {
        var bottomLeftPlayer = _physics.SceneBodies[(int)PongPhysics.BODIES.Player].AabbMin;
        var topRightPlayer = _physics.SceneBodies[(int)PongPhysics.BODIES.Player].AabbMax;
        Director.Instance.DrawHelpers.DrawBounds2Fill(
            new Bounds2(bottomLeftPlayer*PongPhysics.PtoM,topRightPlayer*PongPhysics.PtoM));

        var bottomLeftAi = _physics.SceneBodies[(int)PongPhysics.BODIES.Ai].AabbMin;
        var topRightAi = _physics.SceneBodies[(int)PongPhysics.BODIES.Ai].AabbMax;
        Director.Instance.DrawHelpers.DrawBounds2Fill(
            new Bounds2(bottomLeftAi*PongPhysics.PtoM,topRightAi*PongPhysics.PtoM));

        var bottomLeftBall = _physics.SceneBodies[(int)PongPhysics.BODIES.Ball].AabbMin;
        var topRightBall = _physics.SceneBodies[(int)PongPhysics.BODIES.Ball].AabbMax;
        Director.Instance.DrawHelpers.DrawBounds2Fill(
            new Bounds2(bottomLeftBall*PongPhysics.PtoM,topRightBall*PongPhysics.PtoM));
    };
}

 

This is the code that runs if you define DEBUG_BOUNDINGBOXS to true.  Basically it registers an AdHocDraw method, which is a drawing routine that runs outside the normal page draw lifecycle, a way to define custom drawing behavior without having to inherit and override the Draw method.  Basically we simply get the bounding box of each physics object, translate it to screen space and draw it on screen using the DrawHelper.DrawBounds2Fill method.

 

_pongSound = new Sound("/Application/audio/pongblip.wav");
_pongBlipSoundPlayer = _pongSound.CreatePlayer();

Scheduler.Instance.ScheduleUpdateForTarget(this,0,false);

Next would load our sound effect and create a SoundPlayer for it.  Finally, we register our scene to receive updates.

 

private void ResetBall()
{
    //Move ball to screen center and release in a random directory
    _physics.SceneBodies[(int)PongPhysics.BODIES.Ball].Position = 
        new Vector2(Director.Instance.GL.Context.GetViewport().Width/2,
                    Director.Instance.GL.Context.GetViewport().Height/2) / PongPhysics.PtoM;
    
    System.Random rand = new System.Random();
    float angle = (float)rand.Next(0,360);

    if((angle%90) <=15) angle +=15.0f;

    _physics.SceneBodies[(int)PongPhysics.BODIES.Ball].Velocity = 
        new Vector2(0.0f,5.0f).Rotate(PhysicsUtility.GetRadian(angle));
}

 

This simple method resets the game ball back to the center of the screen.  We then set a random direction for it, by getting a random degree from 0 to 360.  We then make sure that the angle is within 15 degrees of 90 or 270 degrees, meaning it is going straight at the side boards.  In this case, we add 15 degrees to the direction to keep this from happening.  This has the unfortunate side effect of overly punishing the top player, but I am ok with that… that’s the AI. Smile  You can easily add more logic to this method to remove this limitation, or simply randomize the addition or subtraction.  Finally, we take our calculated angle, convert it to radians and rotate our velocity vector by it. 

 

Now lets look at our Update method in chunks.

 

if(Input2.GamePad0.Select.Press)
    Director.Instance.ReplaceScene(new MenuScene());

//We don't need these, but sadly, the Simulate call does.
Vector2 dummy1 = new Vector2();
Vector2 dummy2 = new Vector2();

//Update the physics simulation
_physics.Simulate(-1,ref dummy1,ref dummy2);

//Now check if the ball it either paddle, and if so, play the sound
if(_physics.QueryContact((uint)PongPhysics.BODIES.Ball,(uint)PongPhysics.BODIES.Player) ||
    _physics.QueryContact((uint)PongPhysics.BODIES.Ball,(uint)PongPhysics.BODIES.Ai))
{
    if(_pongBlipSoundPlayer.Status == SoundStatus.Stopped)
        _pongBlipSoundPlayer.Play();
}

 

Here we first check to see if the user has pressed the Select button, if so, we replace the game scene with the menu scene, end result is it shows the main menu.  The Simulate method ( we will see shortly ), requires a pair of Vector2’s, neither of which we need so we call them Dummy1 and Dummy2.  Speaking of Simulate, we call it.  This is the call that runs the physics simulation, translating all of the rigid bodies in the scene.  The parameters you pass in are for interacting with touch, which are meaningless for us in this demo.  Next we check if a collision occurred between the ball and either paddle using the method QueryContact, and playing our blip sound if one does.

 

Results result = Results.StillPlaying;
bool scored = false;

if(ball.Position.Y > Director.Instance.GL.Context.GetViewport().Height + ball.Scale.Y/2)
{
    result = _scoreboard.AddScore(true);
    scored = true;
}
if(ball.Position.Y < 0 - ball.Scale.Y/2)
{
    result =_scoreboard.AddScore(false);
    scored = true;
}

 

Here we are simply checking if the ball went off the top or bottom of the screen.  If so, we update the scoreboard and set the scored flag to true.

 

if(result == Results.AiWin) 
    Director.Instance.ReplaceScene(new GameOverScene(false));
if(result == Results.PlayerWin) 
    Director.Instance.ReplaceScene(new GameOverScene(true));

 

Next we check if either the player or AI won and if so, and depending on which, we display the win or lose screen with the GameOverScene.

 

if(scored == true)
{
    ResetBall ();
}

//Finally a sanity check to make sure the ball didn't leave the field.
var ballPB = _physics.SceneBodies[(int)PongPhysics.BODIES.Ball];

if(ballPB.Position.X < -(ball.Scale.X/2f)/PongPhysics.PtoM ||
   ballPB.Position.X > (Director.Instance.GL.Context.GetViewport().Width)/PongPhysics.PtoM)
{
    ResetBall();
}

 

If a score occurred, but the game isn’t over, we reset the ball to the center of the screen.  Finally we make sure that the ball didn’t somehow “escape” the screen, if it did, we reset it.  Generally this *should* never happen.

 

 

That then is the heart of your game, as you can see, most of the game logic is moved into the Paddle and Ball classes, that we will see in the next part.

 

Programming


9. September 2012

 

In this part of the tutorial we are going to setup our PhsyicsScene, the heart of our physics engine.  A physics engine, in really laymans terms, take a bunch of physics objects, composed of shapes and with defined physical properties like mass, force, elasticity, etc…  and simulates movement in a virtualized world.  You then take the outputs from the physics engine and update your game accordingly.

 

Let’s jump right in to the code:

 

PhysicsScene.cs

 

    using System;
    using Sce.PlayStation.Core;
    using Sce.PlayStation.HighLevel.GameEngine2D;
    using Sce.PlayStation.HighLevel.GameEngine2D.Base;
    using Sce.PlayStation.HighLevel.Physics2D;
    
    namespace Pong
    {
        
        public class PongPhysics : PhysicsScene
        {
            // PixelsToMeters
            public const float PtoM = 50.0f;
            private const float BALLRADIUS = 35.0f/2f; 
            private const float PADDLEWIDTH = 125.0f;
            private const float PADDLEHEIGHT = 38.0f;
            private float _screenWidth;
            private float _screenHeight;
            
            
            public enum BODIES { Ball = 0, Player, Ai, LeftBumper, RightBumper };
    
            
            public PongPhysics ()
            {
                _screenWidth = Director.Instance.GL.Context.GetViewport().Width;
                _screenHeight = Director.Instance.GL.Context.GetViewport().Height;
                
                // turn gravity off
                this.InitScene();
                this.Gravity = new Vector2(0.0f,0.0f);
                
                // Set the screen boundaries + 2m or 100pixel
                this.SceneMin = new Vector2(-100f,-100f) / PtoM;
                this.SceneMax = new Vector2(_screenWidth + 100.0f,_screenHeight + 100.0f) / PtoM;
                
                // And turn the bouncy bouncy on
                this.RestitutionCoeff = 1.0f;
                
                this.NumBody = 5; // Ball, 2 paddles, 2 bumpers
                this.NumShape = 3; // One of each of the above
                
                //create the ball physics object
                this.SceneShapes[0] = new PhysicsShape(PongPhysics.BALLRADIUS/PtoM);
                this.SceneBodies[0] = new PhysicsBody(SceneShapes[0],0.1f);
                this.SceneBodies[0].ShapeIndex = 0;
                this.sceneBodies[0].ColFriction = 0.01f;
                this.SceneBodies[0].Position = new Vector2(_screenWidth/2,_screenHeight/2) / PtoM;
                
                
                //Paddle shape
                Vector2 box = new Vector2(PADDLEWIDTH/2f/PtoM,-PADDLEHEIGHT/2f/PtoM);
                this.SceneShapes[1] = new PhysicsShape(box);
                
                //Player paddle
                this.SceneBodies[1] = new PhysicsBody(SceneShapes[1],1.0f);
                this.SceneBodies[1].Position = new Vector2(_screenWidth/2f,0f+PADDLEHEIGHT/2+ 10f) / PtoM;
                this.SceneBodies[1].Rotation = 0;
                this.SceneBodies[1].ShapeIndex = 1;
                
                //Ai paddle
                this.SceneBodies[2] = new PhysicsBody(SceneShapes[1],1.0f);
                float aiX = ((_screenWidth/2f)/PtoM);
                float aiY = (_screenHeight - PADDLEHEIGHT/2 - 10f)/ PtoM;
                this.SceneBodies[2].Position = new Vector2(aiX,aiY);
                this.SceneBodies[2].Rotation = 0;
                this.SceneBodies[2].ShapeIndex = 1;
                
                //Now a shape for left and right bumpers to keep ball on screen
                this.SceneShapes[2] = new PhysicsShape((new Vector2(1.0f,_screenHeight)) / PtoM);
                
                //Left bumper
                this.SceneBodies[3] = new PhysicsBody(SceneShapes[2],PhysicsUtility.FltMax);
                this.SceneBodies[3].Position = new Vector2(0,_screenHeight/2f) / PtoM;
                this.sceneBodies[3].ShapeIndex = 2;
                this.sceneBodies[3].Rotation = 0;
                this.SceneBodies[3].SetBodyStatic();
                
                //Right bumper
                this.SceneBodies[4] = new PhysicsBody(SceneShapes[2],PhysicsUtility.FltMax);
                this.SceneBodies[4].Position = new Vector2(_screenWidth,_screenHeight/2f) / PtoM;
                this.sceneBodies[4].ShapeIndex = 2;
                this.sceneBodies[4].Rotation = 0;
                this.SceneBodies[4].SetBodyStatic();
            }
        }
    }
    

Once again, let’s take it from the top.

 

// PixelsToMeters
public const float PtoM = 50.0f;
private const float BALLRADIUS = 35.0f/2f; 
private const float PADDLEWIDTH = 125.0f;
private const float PADDLEHEIGHT = 38.0f;
private float _screenWidth;
private float _screenHeight;

Most of these are convenience variables for commonly used dimensions.  The most important concept here is PtoM, which is short hand for Pixels To Meters.  By default, the Physics2D engine measures things in meters and kilograms.  So a velocity of (5,0) is 5 meters per second, and a mass of 1 is 1kg.  This value is used to map from pixels to physics units and back, and is going to be used A LOT.  The value 50.0f was chosen pretty much at random by me.

 

public enum BODIES { Ball = 0, Player, Ai, LeftBumper, RightBumper };

These are just shorthand values for accessing our various physics objects by index value.  Obviously if you changed the order of the PhysicsBodies, you need to update this enum.  This is just to make code that access the physics scene externally a bit more readable and is completely optional.

 

The remaining code is entirely in the constructor, let’s take it in chunks:

_screenWidth = Director.Instance.GL.Context.GetViewport().Width;
_screenHeight = Director.Instance.GL.Context.GetViewport().Height;

// turn gravity off
this.InitScene();
this.Gravity = new Vector2(0.0f,0.0f);

We use the screen width and height a lot, so we cache them in a much shorter variable.  Next we init our PhysicsScene, which simply sets a number of values to defaults and should be called before using the Scene ( calling it later will overwrite all of your settings ).  We then set gravity to (0,0) so there will effectively be none.  By default it’s (0,-9.8), which is 9.8 m/s down the Y axis.

 

// Set the screen boundaries + 2m or 100pixel
this.SceneMin = new Vector2(-100f,-100f) / PtoM;
this.SceneMax = new Vector2(_screenWidth + 100.0f,_screenHeight + 100.0f) / PtoM;
                
// And turn the bouncy bouncy on
this.RestitutionCoeff = 1.0f;
                
this.NumBody = 5; // Ball, 2 paddles, 2 bumpers
this.NumShape = 3; // One of each of the above

By default, the PhysicsScene is from –1000,-1000 to 1000,1000.  Notice how we divided the value by PtoM.  This turns it from pixel coordinates to physics coordinates.  Basically we are setting the scene equal to the size of the screen + 100pixels in all directions.  Once a physics object passes out of this area, it stops updating.

 

Next we set RestituionCoeff, (somewhat short) for Coefficient of Restitution.  Let’s think of it as “bounciness”.  A value of 1.0f means something should rebound at the same force it hit, while a value of 0.0 will mean no bounce at all.  A value over 1.0f will cause a whole lot of bounce.  For some exceptionally odd reason, it is set at the scene level in the Phsyics2D library, instead of at the Body level.  Next we touch on another odd decision in the library, the PhysicsScene contains three pre-allocated arrays for Shapes, Bodies and Joins ( 100 items, 250 items and 150 items in size respectively ).  NumBody and NumShape indicate how many items of each you are going to use.  Make perfectly sure that you get the sizes exactly right or you will have problems.  As you can see, we are going to have 5 SceneBodies and 3 SceneShapes in our scene.

 

//create the ball physics object
this.SceneShapes[0] = new PhysicsShape(PongPhysics.BALLRADIUS/PtoM);
this.SceneBodies[0] = new PhysicsBody(SceneShapes[0],0.1f);
this.SceneBodies[0].ShapeIndex = 0;
this.sceneBodies[0].ColFriction = 0.01f;
this.SceneBodies[0].Position = new Vector2(_screenWidth/2,_screenHeight/2) / PtoM;

First we create a new PhysicsShape at position 0 of the SceneShapes array.  This is going to be for our ball, and is created by specifying the radius of the sphere ( again, adjusted from pixels to physics coordinates ).  We then create a PhysicsBody using the PhysicsShape.  The 0.1f represents the mass, in this case 1/10th kg.  ShapeIndex is the index within the SceneShapes array that this SceneBody uses ( and yes, is overwhelmingly redundant, another design decision I don’t understand).  As we will see shortly, multiple Bodies can use the same Shape.  ColFriction is Collision Friction which you can read more about here.  Finally we position the ball within the physics world to match it’s location in the GameEngine2D world ( we will see shortly ), again, converted using PtoM.

 

//Paddle shape
Vector2 box = new Vector2(PADDLEWIDTH/2f/PtoM,-PADDLEHEIGHT/2f/PtoM);
this.SceneShapes[1] = new PhysicsShape(box);

//Player paddle
this.SceneBodies[1] = new PhysicsBody(SceneShapes[1],1.0f);
this.SceneBodies[1].Position = new Vector2(_screenWidth/2f,0f+PADDLEHEIGHT/2+ 10f) / PtoM;
this.SceneBodies[1].Rotation = 0;
this.SceneBodies[1].ShapeIndex = 1;

//Ai paddle
this.SceneBodies[2] = new PhysicsBody(SceneShapes[1],1.0f);
float aiX = ((_screenWidth/2f)/PtoM);
float aiY = (_screenHeight - PADDLEHEIGHT/2 - 10f)/ PtoM;
this.SceneBodies[2].Position = new Vector2(aiX,aiY);
this.SceneBodies[2].Rotation = 0;
this.SceneBodies[2].ShapeIndex = 1;

 

This code sets up the player and ai paddle physics bodies, this time sharing a single PhysicsShape.  Instead of passing a radius to this one, we pass a pair of vectors representing width an height, relative to the center point ( which coincidentally is explained nowhere and learned from trial and error ).  The only other difference is for the AI paddle, I saved the X and Y values to temporary variables, aiX and aiY.  Why?  Because there is an annoying bug in Expression Evaluator, that simply could not handle _screenWidth/2f.  It would treat both numbers an ints, and was showing ( in watch windows and Expression Evaluator ) the wrong values.  In fact, Expression Evaluation thinks 3.0f/2.0f = 1!   This particular (display) bug cost me much of my sanity!

 

//Now a shape for left and right bumpers to keep ball on screen
this.SceneShapes[2] = new PhysicsShape((new Vector2(1.0f,_screenHeight)) / PtoM);

//Left bumper
this.SceneBodies[3] = new PhysicsBody(SceneShapes[2],PhysicsUtility.FltMax);
this.SceneBodies[3].Position = new Vector2(0,_screenHeight/2f) / PtoM;
this.sceneBodies[3].ShapeIndex = 2;
this.sceneBodies[3].Rotation = 0;
this.SceneBodies[3].SetBodyStatic();

//Right bumper
this.SceneBodies[4] = new PhysicsBody(SceneShapes[2],PhysicsUtility.FltMax);
this.SceneBodies[4].Position = new Vector2(_screenWidth,_screenHeight/2f) / PtoM;
this.sceneBodies[4].ShapeIndex = 2;
this.sceneBodies[4].Rotation = 0;
this.SceneBodies[4].SetBodyStatic();

This code simply creates a pair of “bumpers” for the left and right side of the screen.  Something for the ball to ricochet off of.  The only thing of note is SetBodyStatic(), which tells the physics engine the object isn’t going to move, saving some valuable calculation time.  There are 3 modes in Physics2D, Dynamic ( what are ball and paddles are ), which will be fully calculated by the engine, Kinematic, which respond to direct application of motion, but not environmental effects ( like gravity ) as well as Static, which are completely stationary.  We are sticking with Dynamic because we are going to control paddle movement through the application of Force, something Kinematic doesn’t support.

 

We now have a physics scene, in the next part we will create the actual GameEngine2D scene, GameScene.

 

Programming


7. September 2012

 

We are now going to create the MenuScene we saw at the end of part one.  This is a very simple two item menu, that will either start the game or return to the title screen.

 

MenuScene.cs

 

using System;
using Sce.PlayStation.Core;
using Sce.PlayStation.Core.Graphics;
using Sce.PlayStation.Core.Input;

using Sce.PlayStation.HighLevel.GameEngine2D;
using Sce.PlayStation.HighLevel.GameEngine2D.Base;

using Sce.PlayStation.HighLevel.UI;

namespace Pong
{
    public class MenuScene : Sce.PlayStation.HighLevel.GameEngine2D.Scene
    {
        private Sce.PlayStation.HighLevel.UI.Scene _uiScene;
        
        public MenuScene ()
        {
            this.Camera.SetViewFromViewport();
            Sce.PlayStation.HighLevel.UI.Panel dialog = new Panel();
            dialog.Width = Director.Instance.GL.Context.GetViewport().Width;
            dialog.Height = Director.Instance.GL.Context.GetViewport().Height;
            
            ImageBox ib = new ImageBox();
            ib.Width = dialog.Width;
            ib.Image = new ImageAsset("/Application/images/title.png",false);
            ib.Height = dialog.Height;
            ib.SetPosition(0.0f,0.0f);
            
            Button buttonUI1 = new Button();
            buttonUI1.Name = "buttonPlay";
            buttonUI1.Text = "Play Game";
            buttonUI1.Width = 300;
            buttonUI1.Height = 50;
            buttonUI1.Alpha = 0.8f;
            buttonUI1.SetPosition(dialog.Width/2 - 150,200.0f);
            buttonUI1.TouchEventReceived += (sender, e) => {
                Director.Instance.ReplaceScene(new GameScene());
            };
            
            Button buttonUI2 = new Button();
            buttonUI2.Name = "buttonMenu";
            buttonUI2.Text = "Main Menu";
            buttonUI2.Width = 300;
            buttonUI2.Height = 50;
            buttonUI2.Alpha = 0.8f;
            buttonUI2.SetPosition(dialog.Width/2 - 150,250.0f);
            buttonUI2.TouchEventReceived += (sender, e) => {
            Director.Instance.ReplaceScene(new TitleScene());
            };        
                
            dialog.AddChildLast(ib);
            dialog.AddChildLast(buttonUI1);
            dialog.AddChildLast(buttonUI2);
            _uiScene = new Sce.PlayStation.HighLevel.UI.Scene();
            _uiScene.RootWidget.AddChildLast(dialog);
            UISystem.SetScene(_uiScene);
            Scheduler.Instance.ScheduleUpdateForTarget(this,0,false);
        }
        public override void Update (float dt)
        {
            base.Update (dt);
            UISystem.Update(Touch.GetData(0));
            
        }
        
        public override void Draw ()
        {
            base.Draw();
            UISystem.Render ();
        }
        
        ~MenuScene()
        {
            
        }
    }
}

 

The vast majority of code is in the constructor, so let’s take a look ( in pieces ):

 

this.Camera.SetViewFromViewport();
Sce.PlayStation.HighLevel.UI.Panel dialog = new Panel();
dialog.Width = Director.Instance.GL.Context.GetViewport().Width;
dialog.Height = Director.Instance.GL.Context.GetViewport().Height;

 

Once again, MenuScene is inherited from Scene, and we start off by configuring the Camera member to set itself to the same dimensions as the screen.  Next we create a Panel object, which is part of the Sce.PlayStation.HighLevel.UI library.  A panel is a container of other controls.  We want ours to be the full width and height of the display.

 

ImageBox ib = new ImageBox();
ib.Width = dialog.Width;
ib.Image = new ImageAsset("/Application/images/title.png",false);
ib.Height = dialog.Height;
ib.SetPosition(0.0f,0.0f);

 

Next we create an ImageBox, and image box is simply a control that displays an image.  We are creating this to give the illusion that our menu is overlain over our title screen ( where in reality, it is a completely different scene ).  We again want the ImageBox to take up the full dimensions of our panel, so we set it’s Width and Height to the same as the panel.  Next we load the image as an ImageAsset.  ImageAsset is similar to Texture2D, but is specific to the UI library.  This is an annoying trend of the whole PlayStation Mobile library, each library has a tendency to reinvent the wheel… take a look some time at how many degrees to radian functions the SDK has!  Finally we position our imagebox at (0,0), which is the bottom left corner of the screen.   The end result, show the same image that we showed during the title screen.

 

Button buttonUI1 = new Button();
buttonUI1.Name = "buttonPlay";
buttonUI1.Text = "Play Game";
buttonUI1.Width = 300;
buttonUI1.Height = 50;
buttonUI1.Alpha = 0.8f;
buttonUI1.SetPosition(dialog.Width/2 - 150,200.0f);
buttonUI1.TouchEventReceived += (sender, e) => {
    Director.Instance.ReplaceScene(new GameScene());
};

Button buttonUI2 = new Button();
buttonUI2.Name = "buttonMenu";
buttonUI2.Text = "Main Menu";
buttonUI2.Width = 300;
buttonUI2.Height = 50;
buttonUI2.Alpha = 0.8f;
buttonUI2.SetPosition(dialog.Width/2 - 150,250.0f);
buttonUI2.TouchEventReceived += (sender, e) => {
Director.Instance.ReplaceScene(new TitleScene());
};        

 

Next we create a pair of buttons, one to play the game, the other to go back to main menu ( ok… this is brutally superfluous, but what kind of menu has just one menu option!!! Keep in mind, some of this is for demonstration only! ).  We set each one to be 300 pixels wide, 50 pixels in height and 20% transparent.  We then center horizontally and on top of each other vertically.  If either button is clicked we change the scene, either going back to the TitleScene, or going to the GameScene, which is the main scene controlling our game.

 

dialog.AddChildLast(ib);
dialog.AddChildLast(buttonUI1);
dialog.AddChildLast(buttonUI2);
_uiScene = new Sce.PlayStation.HighLevel.UI.Scene();
_uiScene.RootWidget.AddChildLast(dialog);
UISystem.SetScene(_uiScene);
Scheduler.Instance.ScheduleUpdateForTarget(this,0,false);

 

We then add the image box and both buttons to our dialog using AddChildLast(), create a UIScene ( which has *NOTHING* to do with GameEngine2D scenes, by the way ) then set our scene’s RootWidget to our newly created panel.  The UIScene itself can only contain a single control, the RootWidget, which in turn is a container of other widgets.  We then set our newly created scene as the active scene of the UISystem singleton with the call SetScene().  As you can see, the format is very similar to GameEngine2D and the Director/Scene relationship.  We then schedule this scene ( as in GameEngine scene… not the UIScene… confused yet?  Yeah, the naming convention is kind of brutal at times ) to receive updates with a call to ScheduleUpdateForTarget, which will result in the Update() method being called each frame.

 

public override void Update (float dt)
{
    base.Update (dt);
    UISystem.Update(Touch.GetData(0));
    
}

 

Each update, we simply update the UISystem with the current touch data.  It is your applications responsibility to feed input to the UI, which is exactly what we are doing here.

 

public override void Draw ()
{
    base.Draw();
    UISystem.Render ();
}

It is also our game’s responsibility to call the UISystem Render() method each frame we want it displayed.  It is very important that we call the scene base class Draw() method before we call the UISystem.Render(), otherwise the GameEngine rendering process will draw overtop the UI.  Instead of making the main menu separate from the title screen, it would have been easy enough to make the UISystem Update and Render calls only when you want to display the menu, giving you the same end result.

 

In the next part, we will implement yet another different thing named Scene… physics.

 

Programming


20. June 2012

 

This tutorial is going to cover creating transition effects between your GameEngine2D scenes.  It is actually going to be rather short and mostly code, as the subject is actually quite simple and consistent between different effects.

 

All right, lets just right in.  First of all, when need our main application.  This is going to perhaps be the simplest one we have created to date, and all of the code should be familiar to you by now.  Create a new file named AppMain.cs as follows:

 

using System; using System.Collections.Generic; using Sce.Pss.Core; using Sce.Pss.Core.Environment; using Sce.Pss.Core.Graphics; using Sce.Pss.Core.Input; using Sce.Pss.HighLevel.GameEngine2D; using Sce.Pss.HighLevel.GameEngine2D.Base; namespace SceneTransitions { public class AppMain { public static void Main (string[] args) { Director.Initialize(); Scene1 scene1 = new Scene1(); Director.Instance.RunWithScene(scene1); } } }

 

All we are doing is Initializing the Director object, creating our custom scene of type Scene1 and telling it to run.  This literally is about the shortest PlayStation Mobile application you can create.

 

Now lets take a look at Scene1.cs

using System; using Sce.Pss.Core.Graphics; using Sce.Pss.HighLevel.GameEngine2D; using Sce.Pss.HighLevel.GameEngine2D.Base; namespace SceneTransitions { public class Scene1 : Scene { Texture2D texture; TextureInfo ti; public Scene1 () { this.Camera.SetViewFromViewport(); texture = new Texture2D("/Application/pic1.png",false); ti = new TextureInfo(texture); SpriteUV sprite = new SpriteUV(ti); sprite.Quad.S = ti.TextureSizef; sprite.Position = new Sce.Pss.Core.Vector2(0,0); this.AddChild(sprite); this.ScheduleUpdate(1); this.RegisterDisposeOnExitRecursive(); } ~Scene1 () { ti.Dispose(); texture.Dispose(); } public override void Update (float dt) { if((Sce.Pss.Core.Input.GamePad.GetData(0).ButtonsDown & Sce.Pss.Core.Input.GamePadButtons.Cross) == Sce.Pss.Core.Input.GamePadButtons.Cross) { if(Director.Instance.CurrentScene.IsRunning) Director.Instance.ReplaceScene(new Scene2()); } } } }

 

We are declaring a new class Scene1, which is derived from GameEngine2D.Scene. In this scene, in the constructor we are setting the camera to be sized to the viewport, creating a texture from our image pic1.png ( oh yeah, you need to add 3 images to your project, I picked three PS Vita wallpapers I downloaded off the net, you can use whatever files you want, but I assume you name them pic1.png pic2.png and pic3.png ) then create a sprite out of it.  This is all stuff we have done in prior tutorials.  We now add the sprite to our scene, and schedule this scene to receive updates.  Finally we call this.RegisterDisposeOnExitRecursive().  This function call is key, as it tells the Director to dispose of this scene when it is replaced.  If you do not call this, we will quickly run out of memory.

 

Next up is our destructor.  TextureInfo and Texture are both classes that you are expected to manage destruction of yourself ( they implement IDisposable ).  So in the destructor, which is called when our scene is replaced, we need to dispose of both of these items.  ( Actually I believe TextureInfo will take care of Texture2D, but don’t quote me on that ).

 

Next up is our Update() method.  As we have seen in the past, this method is called once every frame and is where your scene’s logic should be located.  In this case, we are waiting on the user hitting the Cross button ( or S key if using the simulator ), in which case we tell the Director to Replace the scene with a new created Scene2, which we will now create.

 

Create a file named Scene2.cs with the following code ( which is going to look extremely familiar… )

 

using System; using Sce.Pss.Core.Graphics; using Sce.Pss.HighLevel.GameEngine2D; using Sce.Pss.HighLevel.GameEngine2D.Base; namespace SceneTransitions { public class Scene2 : Scene { Texture2D texture; TextureInfo ti; public Scene2 () { this.Camera.SetViewFromViewport(); texture = new Texture2D("/Application/pic2.png",false); ti = new TextureInfo(texture); SpriteUV sprite = new SpriteUV(ti); sprite.Quad.S = ti.TextureSizef; sprite.Position = new Sce.Pss.Core.Vector2(0,0); this.AddChild(sprite); this.ScheduleUpdate(1); this.RegisterDisposeOnExitRecursive(); } ~Scene2() { ti.Dispose(); texture.Dispose(); } public override void Update (float dt) { if((Sce.Pss.Core.Input.GamePad.GetData(0).ButtonsDown & Sce.Pss.Core.Input.GamePadButtons.Cross) == Sce.Pss.Core.Input.GamePadButtons.Cross) { if(Director.Instance.CurrentScene.IsRunning) { Director.Instance.ReplaceScene( new TransitionCrossFade( new Scene3() ) { Duration = 4.0f, Tween = (x) => Sce.Pss.HighLevel.GameEngine2D.Base.Math.PowEaseOut( x, 3.0f )} ); } } } } }

 

So, basically it’s a cut and paste job of Scene1, just with a different texture file, and a different looking replace scene call.  I will look closer at that in a second.  Notice too that here we are creating a Scene3 in our Update() call, lets go ahead and create it next.

 

Create Scene3.cs

 

using System; using Sce.Pss.Core.Graphics; using Sce.Pss.HighLevel.GameEngine2D; using Sce.Pss.HighLevel.GameEngine2D.Base; namespace SceneTransitions { public class Scene3 : Scene { Texture2D texture; TextureInfo ti; public Scene3 () { this.Camera.SetViewFromViewport(); texture = new Texture2D("/Application/pic3.png",false); ti = new TextureInfo(texture); SpriteUV sprite = new SpriteUV(ti); sprite.Quad.S = ti.TextureSizef; sprite.Position = new Sce.Pss.Core.Vector2(0,0); this.AddChild(sprite); this.ScheduleUpdate(1); this.RegisterDisposeOnExitRecursive(); } ~Scene3() { ti.Dispose(); texture.Dispose(); } public override void Update (float dt) { if((Sce.Pss.Core.Input.GamePad.GetData(0).ButtonsDown & Sce.Pss.Core.Input.GamePadButtons.Cross) == Sce.Pss.Core.Input.GamePadButtons.Cross) { if(Director.Instance.CurrentScene.IsRunning) { Director.Instance.ReplaceScene( new TransitionSolidFade( new Scene1() ) { Duration = 1.0f, Tween = (x) => Sce.Pss.HighLevel.GameEngine2D.Base.Math.PowEaseOut( x, 3.0f )} ); } } } } }

Again, another cut and paste job, with just the texture file name changed, and a different ReplaceScene() call, this time creating a Scene1, basically creating an infinite application that shows Scene1, then button press, shows Scene2, button press, shows Scene3, button press, shows Scene1, repeat over an over.

 

Now lets look at the key differences between how the transitions work:

 

Scene1:

Director.Instance.ReplaceScene(new Scene2());

Scene2:

Director.Instance.ReplaceScene( new TransitionCrossFade( new Scene3() ) { Duration = 4.0f, Tween = (x) => Sce.Pss.HighLevel.GameEngine2D.Base.Math.PowEaseOut( x, 3.0f )} );

Scene3:

Director.Instance.ReplaceScene( new TransitionSolidFade( new Scene1() ) { Duration = 1.0f, Tween = (x) => Sce.Pss.HighLevel.GameEngine2D.Base.Math.PowEaseOut( x, 3.0f )} );

 

The transition from the first screen, does nothing, it simply transition to the next scene with no visual effect.  Your first image will simply be replaced with your second image.

 

Scene2 however, transitions by creating a new temporary scene, a TransitionCrossFade, which is essential a short lived scene that goes between Scene2 and Scene3 and causes a cross fade effect.  As you can see, we provide a Tween method which controls the rate at which the fade will occur.  A tween are not simply the people responsible for the bizarre success of the Twilight franchise, its actually short had for inbetween, which is essentially just a function that is going to be called over and over during a transition until it is completed.  Tweens are quite common in the animation world. 

 

Scene3 works almost identically to Scene2, but instead of a cross fade, it’s a solid fade.  You can see the difference in the video of this application in action.  There is one very important thing to be aware of here… these temporary scenes, TransitionCrossFade and TransitionSolidFade, if you try to replace them before they have completed running, your application will crash!  Truth is, in real life you won’t be switching scenes this quickly, so it should be a non-issue, but it is something you should be aware of.  If you want to witness this first hand, load this code up in the simulator and just repeatedly hit S, you will crash soon enough.

 

Now, let’s take a look at our code in action:

 

Transition between scenes on the PlayStation Vita Simulator

 

As always, you can download the full sources here.

Programming


13. June 2012

 

In this tutorial we are going to look at the various Gesture detectors built into the UI layer of the PlayStation Mobile SDK.  They all work fairly consistently, so this tutorial is going to be pretty light in explanation. 

 

There are 6 gesture detectors built in to the PS SDK; FlickGestureDetector, DragGestureDetector, TapGestureDetector, LongPressGestureDetector, PinchGestureDetector and DoubleTapGestureDetector.  DoubleTapGestureDetector barely works on the simulator and doesn’t work at all on the actual Vita, so we aren’t going to use it. 

 

Let’s just right in to coding, in this tutorial we are going to create an ImageBox UI widget that you can use with all these various GestureDetectors to flick, scale and drag all over the screen.  Here is the code:

 

using System; using System.Collections.Generic; using Sce.Pss.Core; using Sce.Pss.Core.Environment; using Sce.Pss.Core.Graphics; using Sce.Pss.Core.Input; using Sce.Pss.HighLevel.UI; namespace UIEffects { public class AppMain { private static GraphicsContext _graphics; private static ImageBox _imageBox; private static Label _label; private static FlickGestureDetector _flickGestureDetector; private static DragGestureDetector _dragGestureDetector; private static TapGestureDetector _tapGestureDector; private static LongPressGestureDetector _longPressGestureDetector; private static PinchGestureDetector _pinchGestureDetector; static private bool quit = false; private static float _origWidth; private static float _origHeight; private static float _origX; private static float _origY; // Note, no housecleaning, all event handlers leak! public static void Main(string[] args) { _graphics = new GraphicsContext(); UISystem.Initialize(_graphics); Sce.Pss.HighLevel.UI.Panel dialog = new Panel(); dialog.Width = _graphics.GetViewport().Width; dialog.Height = _graphics.GetViewport().Height; _label = new Label(); _label.Width = dialog.Width; _label.Height = 35; _label.Font = new Sce.Pss.Core.Imaging.Font(Sce.Pss.Core.Imaging.FontAlias.System,32,Sce.Pss.Core.Imaging.FontStyle.Regular); _label.SetPosition(0,0); _label.HorizontalAlignment = HorizontalAlignment.Center; _label.TextColor = new UIColor(0,0,0,255); _imageBox = new Sce.Pss.HighLevel.UI.ImageBox(); _imageBox.Image = new ImageAsset("/Application/image.png"); _imageBox.PivotType = PivotType.MiddleCenter; _imageBox.Width = _imageBox.Image.Width;///2; _imageBox.Height = _imageBox.Image.Height;///2; _imageBox.SetPosition(dialog.Width/2,//-_imageBox.Width/2, dialog.Height/2);///2-_imageBox.Height/2); _origWidth = _imageBox.Width; _origHeight = _imageBox.Height; _origX = _imageBox.X; _origY = _imageBox.Y; _flickGestureDetector = new FlickGestureDetector(); _flickGestureDetector.Direction = FlickDirection.Horizontal; _flickGestureDetector.FlickDetected += delegate(object sender, FlickEventArgs e) { if(e.Speed.X < 0) // Left new MoveEffect(_imageBox,800f,0f,_imageBox.Y,MoveEffectInterpolator.Elastic).Start (); else new MoveEffect(_imageBox,800f,dialog.Width,_imageBox.Y,MoveEffectInterpolator.Elastic).Start (); _label.Text = "Flicked"; }; _dragGestureDetector = new DragGestureDetector(); _dragGestureDetector.DragDetected += delegate(object sender, DragEventArgs e) { var newPos = new Vector2(e.Source.X,e.Source.Y) + e.Distance; e.Source.SetPosition(newPos.X,newPos.Y); _label.Text = "Dragged"; }; _tapGestureDector = new TapGestureDetector(); _tapGestureDector.TapDetected += delegate(object sender, TapEventArgs e) { e.Source.SetPosition(dialog.Width/2,dialog.Height/2); _label.Text = "Tapped"; }; _longPressGestureDetector = new LongPressGestureDetector(); _longPressGestureDetector.MinPressDuration = 3000f; _longPressGestureDetector.LongPressDetected += delegate(object sender, LongPressEventArgs e) { MessageDialog.CreateAndShow(MessageDialogStyle.Ok,"Press results", "Hey, let go of me! " + (e.ElapsedTime/1000).ToString() + " seconds elapsed!"); _label.Text = "Long pressed"; }; _pinchGestureDetector = new PinchGestureDetector(); _pinchGestureDetector.PinchDetected += delegate(object sender, PinchEventArgs e) { Vector2 newSize = new Vector2(e.Source.Width * e.Scale,e.Source.Height * e.Scale); e.Source.SetSize(newSize.X,newSize.Y); _label.Text = "Pinched"; }; _imageBox.AddGestureDetector(_flickGestureDetector); _imageBox.AddGestureDetector(_dragGestureDetector); _imageBox.AddGestureDetector(_tapGestureDector); _imageBox.AddGestureDetector(_longPressGestureDetector); _imageBox.AddGestureDetector(_pinchGestureDetector); dialog.AddChildLast(_imageBox); dialog.AddChildLast(_label); Sce.Pss.HighLevel.UI.Scene scene = new Sce.Pss.HighLevel.UI.Scene(); scene.RootWidget.AddChildLast(dialog); UISystem.SetScene(scene,new SlideTransition(2000.0f,FourWayDirection.Left,MoveTarget.NextScene,SlideTransitionInterpolator.Linear)); while(!quit) { _graphics.SetClearColor(255,255,255,0); _graphics.Clear(); if((GamePad.GetData(0).Buttons & GamePadButtons.Cross) == GamePadButtons.Cross) { //Reset _imageBox.SetSize(_origWidth,_origHeight); _imageBox.SetPosition(_origX,_origY); } if((GamePad.GetData(0).Buttons & GamePadButtons.Triangle) == GamePadButtons.Triangle) { quit = true; } UISystem.Update(Touch.GetData(0)); UISystem.Render(); _graphics.SwapBuffers(); } } } }

 

 

First we create our Graphics context and initialize our UISystem, then create a Panel named dialog that is going to be our active window.  We then create a label for output purposes, as well as an ImageBox that is going to contain our Jet sprite ( oh yeah… add an image to your project Smile I am reusing the Jet sprite from the last tutorial but you can use anything ).  The Image property of our ImageBox contains an ImageAsset, which we construct by simply passing it the filename of the graphic we want to use.  Next we store the original location of the _imageBox, so we can revert back to defaults later if desired.

 

The first gesture detector we are going to look at is the FlickGestureDetector.  This controls rapidly sliding your finger in one direction on the screen… think Angry birds.  The heart of the flick handling is the FlickDetected delegate, repeated here:

 

_flickGestureDetector.FlickDetected += delegate(object sender, FlickEventArgs e) { if(e.Speed.X < 0) // Left new MoveEffect(_imageBox,800f,0f,_imageBox.Y,MoveEffectInterpolator.Elastic).Start (); else new MoveEffect(_imageBox,800f,dialog.Width,_imageBox.Y,MoveEffectInterpolator.Elastic).Start (); _label.Text = "Flicked"; };

 

All we are doing here is detecting if the speed is a positive or negative value.  If it is a negative value, it means we are flicking right to left, otherwise it is left to right.  Obviously if you were interested in velocity, you would take the actual value into account.  We indicated with the line:

    _flickGestureDetector.Direction = FlickDirection.Horizontal;

that we are only interested in flicking along the horizontal axis.  Therefore any vertical flicks are going to be ignored. 

 

The direction simply determines what the Y coordinate is going to be in our MoveEffect call, either 0 ( far left ) or dialog.Width ( far right ).  MoveEffect causes the passed widget ( _imageBox ), to be moved over time (800ms in this case) to the passed coordinates.  The MoveEffectInterpolator determines how the move is going to happen, we choose Elastic which will cause it to have a bit of a spring effect.  For no effect at all, choose the Linear interpolator.  Essentially, if we flick left, we send the jet to the left side of the screen, while if we flick right, we send the jet to the right side of the screen.

 

Next up is the DragGestureDetector.  Dragging is when you tap, hold, then move the widget.  If we detect a drag, we simply want to translate our _imageBox location to the position the user dragged to.  We accomplish this with this code:

 

_dragGestureDetector = new DragGestureDetector(); _dragGestureDetector.DragDetected += delegate(object sender, DragEventArgs e) { var newPos = new Vector2(e.Source.X,e.Source.Y) + e.Distance; e.Source.SetPosition(newPos.X,newPos.Y); _label.Text = "Dragged"; };

 

As you can see, the various Gesture detectors use remarkably similar handlers.  In this case all we do is get the current position of the dragged widget ( the value in e.Source will be the widget being acted upon ) and add the distance it’s been dragged.

 

Now we add a TapGestureDetector.  In the event of a tap, we simply want to relocated the sprite back to the center of the screen.  Here is the code:

 

_tapGestureDector = new TapGestureDetector(); _tapGestureDector.TapDetected += delegate(object sender, TapEventArgs e) { e.Source.SetPosition(dialog.Width/2,dialog.Height/2); _label.Text = "Tapped"; };

 

Nothing really new there.  Again, e.Source represents the Widget object that is being tapped.  When a tap occurs we simply set it’s location to the center of the screen and log the tapped activity.

 

Next up is the LongPressGestureDetector.  A long press is a tap, followed by a hold for a given threshold of time, in our case 3000 milliseconds.  Once the long press occurs and hits the threshold, the delegate is fired.

 

_longPressGestureDetector = new LongPressGestureDetector(); _longPressGestureDetector.MinPressDuration = 3000f; _longPressGestureDetector.LongPressDetected += delegate(object sender, LongPressEventArgs e) { MessageDialog.CreateAndShow(MessageDialogStyle.Ok,"Press results", "Hey, let go of me! " + (e.ElapsedTime/1000).ToString() + " seconds elapsed!"); _label.Text = "Long pressed"; };

 

In the delegate, all we do is pop up a MessageDialog window.  There are two ways of working with MessageDialog, you can create one directly and then show it, or like we’ve done here, you can use the static CreateAndShow method.

 

Finally we have the PinchGesture Detector.  A pinch is a two finger concurrent tap, followed by either sliding your fingers closer together, or further apart.  Ultimately the value of most importance is the e.Scale, which determines if they pinched larger ( out ) or smaller ( in ) and how much.  By multiplying the e.Scale value against the widget’s current size, it will either scale larger or smaller.

 

_pinchGestureDetector.PinchDetected += delegate(object sender, PinchEventArgs e) { Vector2 newSize = new Vector2(e.Source.Width * e.Scale,e.Source.Height * e.Scale); e.Source.SetSize(newSize.X,newSize.Y); _label.Text = "Pinched"; };

 

One really confusing thing I ran into is the PinchGestureDetector’s PinchEndDetector is never fired!  Be aware of this; although truth is, you don’t really need it in the end.  As I mentioned at the beginning, there is also a DoubleTapGestureDetector, but it doesn’t work.

 

Finally, we wire all of these GestureDetectors to our ImageBox widget using this code:

_imageBox.AddGestureDetector(_flickGestureDetector); _imageBox.AddGestureDetector(_dragGestureDetector); _imageBox.AddGestureDetector(_tapGestureDector); _imageBox.AddGestureDetector(_longPressGestureDetector); _imageBox.AddGestureDetector(_pinchGestureDetector);

 

You can have multiple widgets using the same gestures, and as you can see in this example, a single widget can have multiple detectors attached.  The remaining code is all stuff we have covered in prior tutorials.

 

Here is the result in action.  Since the Simulator doesn’t support multitouch, I took a video of my Vita in action.

 

Pinching, zooming, tapping, dragging and pressing on the PlayStation Vita with the PSSDK

 

 

Once again, you can download the full project here.

Programming


GFS On YouTube

See More Tutorials on DevGa.me!

Month List