Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon

13. September 2012

 

In this section we are going to implement the Ball and the Paddle class.  Let’s jump right in with the Ball class

 

Ball.cs

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

namespace Pong
{
    public class Ball : SpriteUV
    {
        private PhysicsBody _physicsBody;
        // Change this value to make the game faster or slower
        public const float BALL_VELOCITY = 5.0f;
        
        public Ball (PhysicsBody physicsBody)
        {
            _physicsBody = physicsBody;
            
            this.TextureInfo = new TextureInfo(new Texture2D("Application/images/ball.png",false));
            this.Scale = this.TextureInfo.TextureSizef;
            this.Pivot = new Sce.PlayStation.Core.Vector2(0.5f,0.5f);
            this.Position = new Sce.PlayStation.Core.Vector2(
                Director.Instance.GL.Context.GetViewport().Width/2 -Scale.X/2,
                Director.Instance.GL.Context.GetViewport().Height/2 -Scale.Y/2);
            
            
            //Right angles are exceedingly boring, so make sure we dont start on one
            //So if our Random angle is between 90 +- 25 degrees or 270 +- 25 degrees
            //we add 25 degree to value, ie, making 90 into 115 instead
            System.Random rand = new System.Random();
            float angle = (float)rand.Next(0,360);
        
            if((angle%90) <=25) angle +=25.0f;
            this._physicsBody.Velocity = new Vector2(0.0f,BALL_VELOCITY).Rotate(PhysicsUtility.GetRadian(angle));;
            
            Scheduler.Instance.ScheduleUpdateForTarget(this,0,false);
        }
        
        public override void Update (float dt)
        {

            this.Position = _physicsBody.Position * PongPhysics.PtoM;

            
            // We want to prevent situations where the balls is bouncing side to side
            // so if there isnt a certain amount of movement on the Y axis, set it to + or - 0.2 randomly
            // Note, this can result in the ball bouncing "back", as in it comes from the top of the screen
            // But riccochets back up at the user.  Frankly, this keeps things interesting imho
            var normalizedVel = _physicsBody.Velocity.Normalize();
            if(System.Math.Abs (normalizedVel.Y) < 0.2f) 
            {
                System.Random rand = new System.Random();
                if(rand.Next (0,1) == 0)
                    normalizedVel.Y+= 0.2f;
                
                else
                    normalizedVel.Y-= 0.2f;
            }
            
            // Pong is a mess with physics, so just fix the ball velocity
            // Otherwise the ball could get faster and faster ( or slower ) on each collision
            _physicsBody.Velocity = normalizedVel * BALL_VELOCITY;

        }
        
        ~Ball()
        {
            this.TextureInfo.Texture.Dispose();
            this.TextureInfo.Dispose();
        }
    }
}

 

Once again, we will take it from the top.

private PhysicsBody _physicsBody;
// Change this value to make the game faster or slower
public const float BALL_VELOCITY = 5.0f;

 

These are Ball’s two member variables.  The first one is a reference to the PhysicsBody that represents the ball in the physics engine.  The second is the velocity the game ball is going to move at.  You can optionally change the difficulty of this game by increasing that value.

 

public Ball (PhysicsBody physicsBody)
{
    _physicsBody = physicsBody;
    
    this.TextureInfo = new TextureInfo(new Texture2D("Application/images/ball.png",false));
    this.Scale = this.TextureInfo.TextureSizef;
    this.Pivot = new Sce.PlayStation.Core.Vector2(0.5f,0.5f);
    this.Position = new Sce.PlayStation.Core.Vector2(
        Director.Instance.GL.Context.GetViewport().Width/2 -Scale.X/2,
        Director.Instance.GL.Context.GetViewport().Height/2 -Scale.Y/2);
    
    
    //Right angles are exceedingly boring, so make sure we dont start on one
    //So if our Random angle is between 90 +- 25 degrees or 270 +- 25 degrees
    //we add 25 degree to value, ie, making 90 into 115 instead
    System.Random rand = new System.Random();
    float angle = (float)rand.Next(0,360);

    if((angle%90) <=25) angle +=25.0f;
    this._physicsBody.Velocity = new Vector2(0.0f,BALL_VELOCITY).Rotate(PhysicsUtility.GetRadian(angle));;
    
    Scheduler.Instance.ScheduleUpdateForTarget(this,0,false);
}

 

We pass the PhysicsBody in to the constructor, so we take a reference to it.  We then load our ball texture, scale it to match the texture size in pixels, set it’s pivot point to the center, then locate it in the middle of the screen.  The constructor ends with pretty much the same code as we used in ResetBall() in the game scene.  This code is pretty thoroughly explained in the comment.  Finally we register this class to receive updates each frame.

 

public override void Update (float dt)
{

    this.Position = _physicsBody.Position * PongPhysics.PtoM;

    
    // We want to prevent situations where the balls is bouncing side to side
    // so if there isnt a certain amount of movement on the Y axis, set it to + or - 0.2 randomly
    // Note, this can result in the ball bouncing "back", as in it comes from the top of the screen
    // But riccochets back up at the user.  Frankly, this keeps things interesting imho
    var normalizedVel = _physicsBody.Velocity.Normalize();
    if(System.Math.Abs (normalizedVel.Y) < 0.2f) 
    {
        System.Random rand = new System.Random();
        if(rand.Next (0,1) == 0)
            normalizedVel.Y+= 0.2f;
        
        else
            normalizedVel.Y-= 0.2f;
    }
    
    // Pong is a mess with physics, so just fix the ball velocity
    // Otherwise the ball could get faster and faster ( or slower ) on each collision
    _physicsBody.Velocity = normalizedVel * BALL_VELOCITY;

}

… speaking of updates, here is our Update() method.  This will be called once per frame by the Scheduler object ( which itself is called by the Director object ) and is passed the value dt, which is the elapsed time since the last time update was called, in seconds.

 

Most of the code in Update is to deal with the ball hitting the left or right screen at an exceedingly boring angle.  Without this code, you can run in to a situation where the ball will simply bounce back and forth for hours.  Here, if the Y velocity isn’t at least 20% off center, we add a further 20% to it.  Finally we set the velocity to our fixed BALL_VELOCITY, over riding the physics engine. 

 

~Ball()
{
    this.TextureInfo.Texture.Dispose();
    this.TextureInfo.Dispose();
}

Clean up…

 

Now lets take a look at the code behind the paddle objects.

 

Paddle.cs

 

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

namespace Pong
{
    public class Paddle : SpriteUV
    {
        public enum PaddleType { PLAYER, AI };
        
        private PaddleType _type;
        private PhysicsBody _physicsBody;
        private float _fixedY;
        
        public Paddle (PaddleType type, PhysicsBody physicsBody)
        {
            _physicsBody = physicsBody;
            _type = type;

            this.TextureInfo = new TextureInfo(new Texture2D("Application/images/Paddle.png",false));
            this.Scale = this.TextureInfo.TextureSizef;
            this.Pivot = new Sce.PlayStation.Core.Vector2(0.5f,0.5f);
            
            if(_type== PaddleType.AI)
            {
                this.Position = new Sce.PlayStation.Core.Vector2(
                    Director.Instance.GL.Context.GetViewport().Width/2 - this.Scale.X/2,
                    10 + this.Scale.Y/2);                    
            }
            else
            {
                this.Position = new Sce.PlayStation.Core.Vector2(
                    Director.Instance.GL.Context.GetViewport().Width/2 - this.Scale.X/2,
                    Director.Instance.GL.Context.GetViewport().Height - this.Scale.Y/2 - 10);
            }
            
            // Cache the starting Y position, so we can reset and prevent any vertical movement from the Physics Engien
            _fixedY = _physicsBody.Position.Y;
            
            // Start with a minor amount of movement
            _physicsBody.Force = new Vector2(-10.0f,0);
            
            Scheduler.Instance.ScheduleUpdateForTarget(this,0,false);
        }
        
        // This method will fix the physics bounding box to the sprites current position
        // Not currently used, was used for debug, left in for interest sake only
        private void ClampBoundingBox()
        {
            var bbBL = new Vector2(Position.X- Scale.X/2, Position.Y- Scale.Y/2) / PongPhysics.PtoM;
            var bbTR = new Vector2(Position.X+ Scale.X/2, Position.Y+ Scale.Y/2) / PongPhysics.PtoM;
            _physicsBody.AabbMin = bbBL;
            _physicsBody.AabbMax = bbTR;
            
        }
        public override void Update (float dt)
        {
            // Reset rotation to prevent "spinning" on collision
            _physicsBody.Rotation = 0.0f;
            
            
            if(_type == PaddleType.PLAYER)
            {
                if(Input2.GamePad0.Left.Down)
                {
                    _physicsBody.Force = new Vector2(-30.0f,0.0f);
                }
                if(Input2.GamePad0.Right.Down)
                {
                    _physicsBody.Force = new Vector2(30.0f,0.0f);
                }
            }
            else if(_type == PaddleType.AI)
            {
                if(System.Math.Abs (GameScene.ball.Position.X - this.Position.X) <= this.Scale.Y/2)
                    _physicsBody.Force = new Vector2(0.0f,0.0f);
                else if(GameScene.ball.Position.X < this.Position.X)
                    _physicsBody.Force = new Vector2(-20.0f,0.0f);
                else if(GameScene.ball.Position.X > this.Position.X)
                    _physicsBody.Force = new Vector2(20.0f,0.0f);
            }
            
            //Prevent vertical movement on collision.  Could also implement by making paddle Kinematic
            //However, lose ability to use Force in that case and have to use AngularVelocity instead
            //which results in more logic in keeping the AI less "twitchy", a common Pong problem
            if(_physicsBody.Position.Y != _fixedY)
                _physicsBody.Position = new Vector2(_physicsBody.Position.X,_fixedY);
            
            this.Position = _physicsBody.Position * PongPhysics.PtoM;
        }
        
        ~Paddle()
        {
            this.TextureInfo.Texture.Dispose ();
            this.TextureInfo.Dispose();
        }
    }
}

 

Alright… from the top

 

public enum PaddleType { PLAYER, AI };

private PaddleType _type;
private PhysicsBody _physicsBody;
private float _fixedY;

PaddleType is a simple enum specifying the different paddle types, the player or the computer.  It just adds a bit of readability to the code.  Like the ball, we take a reference to the PhysicsBody controlling the paddle movement, _fixedY is for, well, fixing the Y position, we will see this in action shortly.

 

public Paddle (PaddleType type, PhysicsBody physicsBody)
{
    _physicsBody = physicsBody;
    _type = type;

    this.TextureInfo = new TextureInfo(new Texture2D("Application/images/Paddle.png",false));
    this.Scale = this.TextureInfo.TextureSizef;
    this.Pivot = new Sce.PlayStation.Core.Vector2(0.5f,0.5f);
    
    if(_type== PaddleType.AI)
    {
        this.Position = new Sce.PlayStation.Core.Vector2(
            Director.Instance.GL.Context.GetViewport().Width/2 - this.Scale.X/2,
            10 + this.Scale.Y/2);                    
    }
    else
    {
        this.Position = new Sce.PlayStation.Core.Vector2(
            Director.Instance.GL.Context.GetViewport().Width/2 - this.Scale.X/2,
            Director.Instance.GL.Context.GetViewport().Height - this.Scale.Y/2 - 10);
    }
    
    // Cache the starting Y position, so we can reset and prevent any vertical movement from the Physics Engien
    _fixedY = _physicsBody.Position.Y;
    
    // Start with a minor amount of movement
    _physicsBody.Force = new Vector2(-10.0f,0);
    
    Scheduler.Instance.ScheduleUpdateForTarget(this,0,false);
}

 

Our constructor is pretty straight forward.  We cache our PaddleType and PhysicsShape values.  Then we loaded the paddle texture, set its scale and pivot.  Then if it is an AI paddle, we position it 10 pixels from the top of the screen in the center, while if it is the player we position it 10 from the bottom of the screen.  In both cases, we add half of the sprites height to the equation, since we set the sprite pivot point to it’s center and the pivot point is where transforms are performed relative to.  Next we copy the sprites Y position into _fixedY.  We apply a small bit of motion so that paddles start off moving, then schedule this class to receive updates.

 

public override void Update (float dt)
{
    // Reset rotation to prevent "spinning" on collision
    _physicsBody.Rotation = 0.0f;
    
    
    if(_type == PaddleType.PLAYER)
    {
        if(Input2.GamePad0.Left.Down)
        {
            _physicsBody.Force = new Vector2(-30.0f,0.0f);
        }
        if(Input2.GamePad0.Right.Down)
        {
            _physicsBody.Force = new Vector2(30.0f,0.0f);
        }
    }
    else if(_type == PaddleType.AI)
    {
        if(System.Math.Abs (GameScene.ball.Position.X - this.Position.X) <= this.Scale.Y/2)
            _physicsBody.Force = new Vector2(0.0f,0.0f);
        else if(GameScene.ball.Position.X < this.Position.X)
            _physicsBody.Force = new Vector2(-20.0f,0.0f);
        else if(GameScene.ball.Position.X > this.Position.X)
            _physicsBody.Force = new Vector2(20.0f,0.0f);
    }
    
    //Prevent vertical movement on collision.  Could also implement by making paddle Kinematic
    //However, lose ability to use Force in that case and have to use AngularVelocity instead
    //which results in more logic in keeping the AI less "twitchy", a common Pong problem
    if(_physicsBody.Position.Y != _fixedY)
        _physicsBody.Position = new Vector2(_physicsBody.Position.X,_fixedY);
    
    this.Position = _physicsBody.Position * PongPhysics.PtoM;
}

 

In the ball Update method we start off making sure the rigid body controlling the paddle hasn’t rotated as a result of collisions… pong paddles don’t rotate!  Next we check if we are controller the player or the AI.  If it’s the player, we check the state of the left or right gamepad, and if either is pressed we either add or subtract Force from the paddle.  As a result, two presses left will have double the force, while a press left and a press right effectively has no force.  In the event of the AI, instead of being fed by input, we check the current ball location and apply force as a result.  You can add a great deal more logic here to make the AI perform better.  The catch is, it is easy to make Pong unbeatable, so don’t make it too good!  Finally we want to make sure the Physics system hasn’t moved the AI on the Y axis and if it has, move it back.

 

For the record, the paddles could be implemented as a Joint constrained to the X axis.  This has advantages and disadvantages.  First it would prevent the need to fix the Y axis.  However, you would lose the ability to bounce off the side bumpers.

 

Finally, we update the paddle’s position to match it’s rigid body’s position.  Then in the destructor, we clean things up.  That is more or less the entire logic of the game. 

 

Now lets take a look at the ScoreBoard logic.

 

Scoreboard.cs

using System;

using Sce.PlayStation.Core.Imaging;
using Sce.PlayStation.Core.Graphics;
using Sce.PlayStation.Core;
using Sce.PlayStation.HighLevel.GameEngine2D;
using Sce.PlayStation.HighLevel.GameEngine2D.Base;

namespace Pong
{
    public enum Results { PlayerWin, AiWin, StillPlaying };
    
    public class Scoreboard : SpriteUV
    {
        public int playerScore = 0;
        public int aiScore = 0;
        
        public Scoreboard ()
        {
            this.TextureInfo = new TextureInfo();
            UpdateImage();
            
            this.Scale = this.TextureInfo.TextureSizef;
            this.Pivot = new Vector2(0.5f,0.5f);
            this.Position = new Vector2(Director.Instance.GL.Context.GetViewport().Width/2,
                                        Director.Instance.GL.Context.GetViewport().Height/2);
            
        }
        
        private void UpdateImage()
        {
            Image image = new Image(ImageMode.Rgba,new ImageSize(110,100),new ImageColor(0,0,0,0));
            Font font = new Font(FontAlias.System,50,FontStyle.Regular);
            image.DrawText(playerScore + " - " + aiScore,new ImageColor(255,255,255,255),font,new ImagePosition(0,0));
            image.Decode();

            var texture  = new Texture2D(110,100,false,PixelFormat.Rgba);
            if(this.TextureInfo.Texture != null)
                this.TextureInfo.Texture.Dispose();
            this.TextureInfo.Texture = texture;
            texture.SetPixels(0,image.ToBuffer());
            font.Dispose();
            image.Dispose();
        }
        public void Clear()
        {
            playerScore = aiScore = 0;
            UpdateImage();
        }
        
        public Results AddScore(bool player)
        {
            if(player)
                playerScore++;
            else
                aiScore++;
            if(playerScore > 3) return Results.PlayerWin;
            if(aiScore > 3) return Results.AiWin;
            
            UpdateImage();

            return Results.StillPlaying;
        }
    }
}

Scoreboard is a simple dynamic sprite.  It keeps track of and displays the game score on screen.  It works just like the titlescreen, logic-wise, except that it generates it’s own image.  All of that logic is handled in UpdateImage, so let’s take a closer look at it.

 

private void UpdateImage()
{
    Image image = new Image(ImageMode.Rgba,new ImageSize(110,100),new ImageColor(0,0,0,0));
    Font font = new Font(FontAlias.System,50,FontStyle.Regular);
    image.DrawText(playerScore + " - " + aiScore,new ImageColor(255,255,255,255),font,new ImagePosition(0,0));
    image.Decode();

    var texture  = new Texture2D(110,100,false,PixelFormat.Rgba);
    if(this.TextureInfo.Texture != null)
        this.TextureInfo.Texture.Dispose();
    this.TextureInfo.Texture = texture;
    texture.SetPixels(0,image.ToBuffer());
    font.Dispose();
    image.Dispose();
}

The scoreboard starts life as an Image object 110x100 pixels in size.  We create a 50 point font using the only font included, System. We then draw the score in white on the image.  We then create a Texture2D to hold the image and a TextureInfo to hold the texture.  We then copy the pixels from our image to our texture using SetPixels and passing in the image as a byte array via ToBuffer.  At this point we are done with the image and font objects, so we dispose of them.

 

The only thing that remains is the game over screen, which we will look at now:

 

GameOver.cs

using System;
using Sce.PlayStation.Core;
using Sce.PlayStation.Core.Graphics;
using Sce.PlayStation.Core.Audio;
using Sce.PlayStation.HighLevel.GameEngine2D;
using Sce.PlayStation.HighLevel.GameEngine2D.Base;
using Sce.PlayStation.Core.Input;

namespace Pong
{
    public class GameOverScene : Scene
    {
        private TextureInfo _ti;
        private Texture2D _texture;
        
        public GameOverScene (bool win)
        {
            this.Camera.SetViewFromViewport();
            if(win)
                _texture = new Texture2D("Application/images/winner.png",false);
            else
                _texture = new Texture2D("Application/images/loser.png",false);
            _ti = new TextureInfo(_texture);
            SpriteUV titleScreen = new SpriteUV(_ti);
            titleScreen.Scale = _ti.TextureSizef;
            titleScreen.Pivot = new Vector2(0.5f,0.5f);
            titleScreen.Position = new Vector2(Director.Instance.GL.Context.GetViewport().Width/2,
                                               Director.Instance.GL.Context.GetViewport().Height/2);
            this.AddChild(titleScreen);
            
            Scheduler.Instance.ScheduleUpdateForTarget(this,0,false);
            
            Touch.GetData(0).Clear();
        }
        
        public override void Update (float dt)
        {
            base.Update (dt);
            int touchCount = Touch.GetData(0).ToArray().Length;
            if(touchCount > 0 || Input2.GamePad0.Cross.Press)
            {
                Director.Instance.ReplaceScene( new TitleScene());
            }
        }
        
        ~GameOverScene()
        {
            _texture.Dispose();
            _ti.Dispose ();
        }
    }
}

 

This code is almost identical to the title screen logic, so we wont go through it in detail.  Basically, you pass a boolean in to the constructor to let it know who won, the player or AI.  If the Player won, we display the winner.png graphic, while if the AI won, we display the loser.png graphic.  In the Update method, we check for a touch and if one occurs, set the TitleScene menu as the active scene.

 

And… that is it.  A complete simply Pong clone made with the PlayStation Mobile SDK.  It is by no means perfect, but it is a complete game and can be used as the foundation of a much better game.  I hope you enjoyed the series.  In the next part, we will simply be putting all of the code together on a single page, as well as a link to download the entire project, assets and all.

 

Programming , , ,

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 , , ,

7. September 2012

 

In this series we are going to create a complete Pong clone using the PlayStation Mobile SDK.  The main purpose is to show how all the various pieces ( GameEngine2D, Physics2D, Scenes, Audio, etc… ), fit together to create a full game, something the current documentation doesn’t really

The end results of this tutorial series ( minus sound… it has sound too )
cover.  This tutorial series is going to assume you have already gone through the other PlayStation Mobile tutorials on this site, so I may not be going in to detail on areas that have already been covered.

 

 

I am going to present in the following format.  I will specify the filename, then give the complete source code, followed by a nearly line by line explanation of what the source is doing.

 

 

To complete this tutorial, you are going to need to add references to the libraries Sce.PlayStation.HighLevel.GameEngine2D, Sce.PlayStation.HighLevel.Physics2D and Sce.PlayStation.HighLevel.UI. There is a bug in the PlayStation Mobile Studio that causes auto completion of newly added libraries to not work unless a) you reload the project b) you add ( then remove ) another library c) you exit and restart Studio.

 

Let’s jump right in.

 

AppMain.cs

 

using System;
using Sce.PlayStation.HighLevel.UI; 
using Sce.PlayStation.HighLevel.GameEngine2D;

namespace Pong
{
    public class AppMain
    {
        public static void Main (string[] args)
        {
            Director.Initialize();
            UISystem.Initialize(Director.Instance.GL.Context);
            Director.Instance.RunWithScene(new TitleScene());                
        }
    }
}

 

Every C# application has a Main, and as you can see, ours does remarkably little.  Thanks to the Scene based organization of GameEngine2D, it is fairly easy to divide out your application into logical chunks.  Here we initialize the Director and UISystem singletons, create and run a TitleScene scene, to predictably enough, display a title screen.  Let’s look at that now.

 

TitleScene.cs

 

using System;
using Sce.PlayStation.Core;
using Sce.PlayStation.Core.Graphics;
using Sce.PlayStation.Core.Audio;
using Sce.PlayStation.HighLevel.GameEngine2D;
using Sce.PlayStation.HighLevel.GameEngine2D.Base;
using Sce.PlayStation.Core.Input;
 
namespace Pong
{
    public class TitleScene : Scene
    {
        private TextureInfo _ti;
        private Texture2D _texture;
        
        private Bgm _titleSong;
        private BgmPlayer _songPlayer;
        
        public TitleScene ()
        {
            this.Camera.SetViewFromViewport();
            _texture = new Texture2D("Application/images/title.png",false);
            _ti = new TextureInfo(_texture);
            SpriteUV titleScreen = new SpriteUV(_ti);
            titleScreen.Scale = _ti.TextureSizef;
            titleScreen.Pivot = new Vector2(0.5f,0.5f);
            titleScreen.Position = new Vector2(Director.Instance.GL.Context.GetViewport().Width/2,
                                              Director.Instance.GL.Context.GetViewport().Height/2);
            this.AddChild(titleScreen);
            
            Vector4 origColor = titleScreen.Color;
            titleScreen.Color = new Vector4(0,0,0,0);
            var tintAction = new TintTo(origColor,10.0f);
            ActionManager.Instance.AddAction(tintAction,titleScreen);
            tintAction.Run();
            
            _titleSong = new Bgm("/Application/audio/titlesong.mp3");
            
            if(_songPlayer != null)
            _songPlayer.Dispose();
            _songPlayer = _titleSong.CreatePlayer();
            
            Scheduler.Instance.ScheduleUpdateForTarget(this,0,false);

            // Clear any queued clicks so we dont immediately exit if coming in from the menu
            Touch.GetData(0).Clear();
        }
        
        public override void OnEnter ()
        {
            _songPlayer.Loop = true;
            _songPlayer.Play();
        }
        public override void OnExit ()
        {
            base.OnExit ();
            _songPlayer.Stop();
            _songPlayer.Dispose();
            _songPlayer = null;
        }
        
        public override void Update (float dt)
        {
            base.Update (dt);
            var touches = Touch.GetData(0).ToArray();
            if((touches.Length >0 && touches[0].Status == TouchStatus.Down) || Input2.GamePad0.Cross.Press)
            {
                Director.Instance.ReplaceScene(new MenuScene());
            }
        }
    
        ~TitleScene()
        {
            _texture.Dispose();
            _ti.Dispose ();
        }
    }
}
 

 

This code creates a title screen, loops the background music and waits for the user to touch the screen, at which point it displays the main menu scene.  Let’s take a closer look.

 

private TextureInfo _ti;
private Texture2D _texture;
        
private Bgm _titleSong;
private BgmPlayer _songPlayer;

 

First we declare our member variables.  A TextureInfo holds a texture, and important UV information about it, especially if that texture contains multiple sprites.  Texture2D is the texture itself and holds the image pixel data.  Bgm is a class for holding a music file ( Bgm == background music ), while a BgmPlayer is predictably enough, for playing a Bgm file.  It is important to note, all 4 of these classes implement IDisposable, so you need to release them when you are done, or you will leak memory.

 

public TitleScene ()
    {
        this.Camera.SetViewFromViewport();
        _texture = new Texture2D("Application/images/title.png",false);
        _ti = new TextureInfo(_texture);
        SpriteUV titleScreen = new SpriteUV(_ti);
        titleScreen.Scale = _ti.TextureSizef;
        titleScreen.Pivot = new Vector2(0.5f,0.5f);
        titleScreen.Position = new Vector2(Director.Instance.GL.Context.GetViewport().Width/2,
                                       Director.Instance.GL.Context.GetViewport().Height/2);
        this.AddChild(titleScreen);

 

This code is the first half of our constructor, called when the class is created.  First thing we do is calibrate the scene camera ( TitleScene inherited from Scene which has a camera property ) to set it’s dimensions to match the full viewport.  Next we load the texture title.png from the images folder.  By default, all folders and files added to a project will be under the /Application folder when you run.  Note, these files are all read only!  If you need write access, you need to put them in special folders, described elsewhere in the tutorials.  The false in Texture2D’s constructor is to indicate we do not want to generate a mipmap.  ( A mip map is a special multi resolution version of a texture, used for variable levels of detail, and is not applicable to this application ).

 

One we have loaded our texture, we pass it to the constructor TextureInfo.  Again, TextureInfo simply holds a texture and UV information to go with that texture.  We then use the TextureInfo in our constructor to SpriteUV, which is the actual graphic we are going to display on screen.  All of the graphics in our game are going to be SpriteUVs, and our title screen is no exception.  Now that we have created our title screen graphic, we set it’s size equal to the pixel size of the source image, set it’s pivot to it’s middle point, and it’s position to the center of the screen. 

 

Two important concepts here are the Pivot point and Positioning. The pivot is the location your sprite is going to be transformed relative to, by setting it to the middle of the sprite, this makes rotating a heck of a lot easier, but you now have to remember, when your objet is at say (0,0), only a quarter of it will be visible on screen.  So when checking boundaries, you will have to add half the width and/or height to the equation.  Pivot is represented as a pair of x,y coordinates going from 0 to 1.  (0,0) is the bottom left corner of the sprite, while (1,1) would be the top right.  Obviously then, (0.5,0.5) is the middle.

 

The positioning in GameEngine2D starts at the bottom left corner of the screen.  This is taken from Cocos2D, which GameEngine2D is modeled on, which in turn takes it from OpenGL.  If you are used to the origin being at the top left, this can take a bit of getting used to. Director.Instance.GL.Context allows you some access to the underlying OpenGL context, and will be used quite often throughout the application.  The Director singleton does take care of most of the gritty rendering details, so we wont have to get too low level.  Finally AddChild() adds the child to the scene for displaying and updating.

 

 

    Vector4 origColor = titleScreen.Color;
    titleScreen.Color = new Vector4(0,0,0,0);
    var tintAction = new TintTo(origColor,10.0f);
    ActionManager.Instance.AddAction(tintAction,titleScreen);
    tintAction.Run();
        
    _titleSong = new Bgm("/Application/audio/titlesong.mp3");

    if(_songPlayer != null)
        _songPlayer.Dispose();
    _songPlayer = _titleSong.CreatePlayer();
            
    Scheduler.Instance.ScheduleUpdateForTarget(this,0,false);
            
    // Clear any queued clicks so we dont immediately exit if coming in from the menu
    Touch.GetData(0).Clear();
    }

 

This is the second half of our constructor and most of it revolves around an easy special effect.  First we are caching a copy of the titleScreen’s current color value.  We then set the color to black and completely transparent ( color is made up of four numbers, the Red, Green, Blue as well as Alpha ( transparency ) values, ranging from 0 (none) to 255(full)).  Next we create an Action of type TintTo and over a period of 10 seconds, revert our color back to normal.  Finally we register our action using the ActionManager singleton’s AddAction() method, passing in the action to run, as well as the target node to perform the action on.  Think of actions as reusable “threads” of activity to perform various tasks on Nodes.  If you look at the class hierarchy of GameEngine2D, you will discover most of the class are inherited from Node and can thus have actions performed on them.  ( Scene and SpriteUV are both derived from Node ).  Now that our action is created and registered, we simply run it.  The end result of all of this is the title screen graphic will fade from black to full color over a period of 10 seconds.

 

Next up we load our title song from disk in the folder audioAgain, all folders by default are added under the Application folder and are read only.  Bgm files only support the mp3 format.  The only method of importance for a Bgm file is CreatePlayer() which creates a BgmPlayer instance.  A warning up front, the lifecycle of BgmPlayer is extremely odd, there is something going on behind the scenes with these classes that makes dealing with them extremely irritating, which is why you see a Dispose() call on a class that shouldn’t possibly exist.  Next we register our Scene class with the Scheduler singleton.  This singleton is responsible for calling Update each frame.  The end result is that our Update() method will get called each frame ( ideally 60 times a second ).  The second parameter ( 0 ) is used to set the priority that Scheduler should give your classes Update() calls, but it is currently ignored.  The final parameter to ScheduleUpdateForTarget is simply telling Scheduler if we want to start with our updating paused, which we don’t.  The last line is a bit of a hack, but a required one.  Later on, we will return to the TitleScreen from a different direction, and it is possible that if the user double tapped the screen that a tap is queued up to be processed, which would result in the title screen immediately going away.  This line simply clears out all outstanding touch events to prevent this possibility.

 

public override void OnEnter ()
    {
        _songPlayer.Loop = true;
        _songPlayer.Play(); 
    }
    public override void OnExit ()
    {
        base.OnExit ();
        _songPlayer.Stop();
        _songPlayer.Dispose();
        _songPlayer = null;
    }

OnEnter() is called when your Scene is actually displayed.  It is called after the constructor and it is possible for your Scene to potentially be shown and hidden multiple times.  OnExit() is called when your scene is replaced.  We our scene is first displayed, we start our background music playing and we want it to loop.  On exit, we stop and dispose of the sound and _songPlayer.  Again, managing the memory on these classes is tricky, there is something odd behind the scenes.

 

public override void Update (float dt)
{
    base.Update (dt);
    var touches = Touch.GetData(0).ToArray();
    if((touches.Length >0 && touches[0].Status == TouchStatus.Down) || Input2.GamePad0.Cross.Press)
    {
        Director.Instance.ReplaceScene(new MenuScene());
    }
}

 

This is the method that the Scheduler will call every frame.  It is called before rendering occurs.  Dt represents the elapsed time, in seconds, since Update was last called.  All we are doing here is checking to see if any touches have occurred, or if the user pressed the Cross ( X ) gamepad button, in which case we replace the scene with our MenuScene, which we will create shortly.  Basically, update sits around waiting for the user to tap the screen or hit X, in which case it loads and displays the menu.

 

 

~TitleScene()
    {
        _texture.Dispose();
        _ti.Dispose ();
    }

 

Finally out destructor that, which simply disposes of the title screen Texture2D and TextureInfo objects.  We dispose of the BGMPlayer when the scene changes.  The Bgm file is apparently leaked, but really isn’t.  As I said, something odd is going on.  Play around with the lifecycle of BgmPlayer and Bgm and you will see what I mean!

 

 

 

The resources

 

Our game requires a number of media files, the following is a description of each.  You can use mine or create your own, I release all files ( I own ) to the public domain free of constraints.  You need to create a pair of folders in your project ( within PlayStation Mobile Studio ), audio and images.  For each file, be sure to add it the the appropriate sub folder, then right click it in the Solution panel, and set it’s build action to content!  This step is very important, or you may receive errors when you try to run your application.

 

In the end, your project should look like this:

 

image

 

Now the files.  In the final part of this tutorial, I will make the entire project available as a zip file, but you can grab individual resources below.

ball.png

ball

paddle.png

Paddle

 

title.png

title

 

winner.png

winner

 

loser.png

loser

 

 

In the next part we will create the menu scene, making use of the UI library.

 

Programming , ,

Month List

Popular Comments