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 110×100 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.