wiki:Pong/En:Vaihe7
Last modified 5 years ago Last modified on 2014-10-14 15:04:47

Languages:







Pong game, part 7

In this part we'll add a scoring system to the game. In Pong a player gets a point when the ball moves past the other player's paddle.

http://kurssit.it.jyu.fi/npo/2010/luentomateriaali/www/pong-animaatio2.gif

1. A subroutine call for the counters

We need some sort of a counter to keep track of the players' scores. Creating the counters is its own thing so let's make it a subroutine.

Add a call to AddCounters to Begin:

public override void Begin()
{
    CreateLevel();
    SetControls();
    AddCounters();
    StartGame();
}

For now, add an "empty" subroutine AddCounters to get the code to even compile:

void AddCounters()
{
   // ...
}

2. Creating a counter

There will be two counters in the game, one for each player.

We'll make a subroutine for creating the counters, because as you may guess, they're created in almost the exact same way.

Create a new subroutine CreateScoreCounter:

IntMeter CreateScoreCounter()
{
    IntMeter counter = new IntMeter(0);
    counter.MaxValue = 10;
    return counter;
}

The subroutine returns a value of the type IntMeter, which refers to a counter that deals with whole numbers (the type for whole numbers, i.e. integers, is int).

We give the default value of the counter as a parametre to its construtor (new IntMeter...). The default value is naturally zero.

We can set a maximum value for the counter, after which it stops counting. Let's set it to ten, for example.

We could certainly just use an int variable for counting, but using IntMeter makes things easier for us later on, particularly when we want to display the score on the screen.

3. Displaying the score

An IntMeter object won't display the score on its own; it only retains the numeric value in memory. We need text fields i.e. something like a Label for displaying the numbers.

Let's create a Label displaying the value of the score counter in the same subroutine.

Let's pass the coordinates as parametres to the subroutine, since the counters are put in different places of the level.

Modify the CreateScoreCounter subroutine:

IntMeter CreateScoreCounter(double x, double y)
{
    IntMeter counter = new IntMeter(0);
    counter.MaxValue = 10;

    Label display = new Label();
    display.BindTo(counter);
    display.X = x;
    display.Y = y;
    display.TextColor = Color.White;
    display.BorderColor = Level.Background.Color;
    display.Color = Level.Background.Color;
    Add(display);

    return counter;
}

The text field is bound to display the value of the counter with the call display.BindTo(counter). Now the label is updated automatically, when the value of the counter changes.

We set the colour of the text display to white to set it apart from the background (display.TextColor = Color.White).

4. Adding the counters to the game

We'll soon need to access the counters from other parts of the program, so let's make them attributes.

Something to think about: Why don't we have to make the text displays attributes?

Add variables player1Score and player2Score of the type IntMeter to the list of attributes:

public class Pong : PhysicsGame
{    
    Vector velocityUp = new Vector(0, 200);
    Vector velocityDown = new Vector(0, -200);

    PhysicsObject ball;
    PhysicsObject paddle1;
    PhysicsObject paddle2;

    IntMeter player1Score;
    IntMeter player2Score;

    public override void Begin()
    {
        CreateLevel();
        SetControls();
        AddCounters();
        StartGame();
    }

    // ...

We already have an empty subroutine for creating the counters.

Add to CreateCounters lines that call the counter creating subroutine, and store the created counters in variables:

void AddCounters()
{
    player1Score = CreateScoreCounter(Screen.Left + 100.0, Screen.Top - 100.0);
    player2Score = CreateScoreCounter(Screen.Right - 100.0, Screen.Top - 100.0);
}

The first parametre of CreateScoreCounter is the x coordinate and the second one is the y coordinate. The locations of objects that we want to add to the field are given in terms of the screen, not the level.

The x coordinates of the counters are calculated with the help of the x coordinates for the left and right sides of the screen (Screen.Left and Screen.Right).

The y coordinate is calculated by starting from the sop of the screen (Screen.Top).

source:/trunk/help_symbols/try_to_run.png

When you now run the program, you should be able to see two counters at top of the screen both displaying the value 0.

5. Collision handling

In order for us to keep track of the score, we need to know when the ball goes past one of the paddles. We can get this information by checking for when the ball hits the left or the right border of the level.

Jypeli has a subroutine called AddCollisionHandler just for reacting to collisions.

AddCollisionHandler takes two parametres:

  • A PhysicsObject, whose collisions we are interested in.
  • and a subroutine, which is called when the object collides with something.

Add the following call to CreateLevel, after the part where we create and add the ball:

AddCollisionHandler(ball, HandleBallCollision);

source:/trunk/help_symbols/does_not_work_yet.png

The subroutine in which the collision is handled has to be of the following form:

  • The return value is void (i.e. the subroutine doesn't return anything).
  • It has two parametres and they are of the type PhysicsObject.
  • The first parametre is the object whose collisions we are listening to, i.e. the collider (in this case it's the ball).
  • The second parametre is the (unknown) target of the collision.

The subroutine can be named freely, as long as the same name is given as a parametre in the call to AddCollisionHandler.

Add the following subroutine:

void HandleBallCollision(PhysicsObject ball, PhysicsObject target)
{

}

We know that the ball has hit something only when the collision handler gets called. To figure out what it has collided with, we need to do some comparisons.

We could compare whether the target and the left border are the same object, because then we would know that the ball has hit the left border. How could we do this?

To make the comparison easy to do, it would be handy to have the left border represented by some variable.

Let's change the code so that the borders are created one by one, not all at the same time.

Remove the following line from CreateLevel:

Level.CreateBorders(1.0, false);

In its place, put the creation of first the left border like this:

PhysicsObject leftBorder = Level.CreateLeftBorder();
leftBorder.Restitution = 1.0;
leftBorder.IsVisible = false;

Now we have to change to attributes of the borders afterwards.

Do the rest of the borders in the same way after the left border:

  • right border (CreateRightBorder),
  • bottom border (CreateBottomBorder) and
  • top border (CreateTopBorder).

Now that the borders exist as variables we can check whether the target of the collision is the same object as leftBorder or rightBorder.

When checking whether two things are equal, we use the == notation.

If the ball collides with the right border, we'll increment the value of Player 1's score counter (player1Score.Value) by one. The value of a counter is stored in its Value attribute. Then we do the same thing for the left border.

Add the checks to HandleBallCollision:

void HandleBallCollision(PhysicsObject ball, PhysicsObject target)
{
    if (target == rightBorder)
    {
        player1Score.Value += 1;
    }
    else if (target == leftBorder)
    {
        player2Score.Value += 1;
    }
}

The notation += means that we add what is on the right side to what is on the left side.

The word else in front of the second if clause means that the if clause after else can be executed only if the condition for the previous if clause was untrue.

This means that of the two if clauses at most one is executed at the same time. It's certainly also possible that neither of them is executed, if the ball hits a paddle, for example.

source:/trunk/help_symbols/does_not_work_yet.png

In order for us to have access to the border in HandleBallCollision, they too have to be added to the list of attributes.

Also do the following changes:

public class Pong : PhysicsGame
{
    Vector velocityUp = new Vector(0, 200);
    Vector velocityDown = new Vector(0, -200);

    PhysicsObject ball;
    PhysicsObject paddle1;
    PhysicsObject paddle2;

    PhysicsObject leftBorder;
    PhysicsObject rightBorder;

    IntMeter player1Score;
    IntMeter player2Score;

    // ...

    void CreateLevel()
    {
        ball = new PhysicsObject(40.0, 40.0);
        ball.Shape = Shapes.Circle;
        ball.X = -200.0;
        ball.Y = 0.0;
        ball.Restitution = 1.0;
        Add(ball);
        AddCollisionHandler(ball, HandleBallCollision);

        paddle1 = CreatePaddle(Level.Left + 20.0, 0.0);
        paddle2 = CreatePaddle(Level.Right - 20.0, 0.0);

        PhysicsObject leftBorder = Level.CreateLeftBorder();
        leftBorder.Restitution = 1.0;
        leftBorder.IsVisible = false;

        PhysicsObject rightBorder = Level.CreateRightBorder();
        rightBorder.Restitution = 1.0;
        rightBorder.IsVisible = false;

        PhysicsObject topBorder = Level.CreateTopBorder();
        topBorder.Restitution = 1.0;
        topBorder.IsVisible = false;

        PhysicsObject bottomBorder = Level.CreateBottomBorder();
        bottomBorder.Restitution = 1.0;
        bottomBorder.IsVisible = false;

        Level.Background.Color = Color.Black;

        Camera.ZoomToLevel();
    }
    // ...

source:/trunk/help_symbols/try_to_run.png

6. Finetuning

The ball may not behave quite how we wanted it to. You can try to finetune the playability of the game by changing the attributes of the ball, for example. See what its attributes KineticFriction and CanRotate do and try changing them. Does that have an effect on the gameplay experience? If you think the ball slows down annoyingly, you can try fixing physics bugs with this trick.

7. The end result

The Pong game with all of its paddles and score counters looks approximately like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Jypeli;
using Jypeli.Assets;
using Jypeli.Controls;
using Jypeli.Effects;
using Jypeli.Widgets;

public class Pong : PhysicsGame
{
    Vector nopeusYlos = new Vector(0, 200);
    Vector nopeusAlas = new Vector(0, -200);

    PhysicsObject pallo;
    PhysicsObject maila1;
    PhysicsObject maila2;

    PhysicsObject vasenReuna;
    PhysicsObject oikeaReuna;

    IntMeter pelaajan1Pisteet;
    IntMeter pelaajan2Pisteet;

    public override void Begin()
    {
        LuoKentta();
        AsetaOhjaimet();
        LisaaLaskurit();
        AloitaPeli();
    }

    void LuoKentta()
    {
        pallo = new PhysicsObject(40.0, 40.0);
        pallo.Shape = Shape.Circle;
        pallo.X = -200.0;
        pallo.Y = 0.0;
        pallo.Restitution = 1.0;
        pallo.KineticFriction = 0.0;
        pallo.MomentOfInertia = Double.PositiveInfinity;
        Add(pallo);
        AddCollisionHandler(pallo, KasittelePallonTormays);

        maila1 = LuoMaila(Level.Left + 20.0, 0.0);
        maila2 = LuoMaila(Level.Right - 20.0, 0.0);

        vasenReuna = Level.CreateLeftBorder();
        vasenReuna.Restitution = 1.0;
        vasenReuna.KineticFriction = 0.0;
        vasenReuna.IsVisible = false;

        oikeaReuna = Level.CreateRightBorder();
        oikeaReuna.Restitution = 1.0;
        oikeaReuna.KineticFriction = 0.0;
        oikeaReuna.IsVisible = false;

        PhysicsObject ylaReuna = Level.CreateTopBorder();
        ylaReuna.Restitution = 1.0;
        ylaReuna.KineticFriction = 0.0;
        ylaReuna.IsVisible = false;

        PhysicsObject alaReuna = Level.CreateBottomBorder();
        alaReuna.Restitution = 1.0;
        alaReuna.IsVisible = false;
        alaReuna.KineticFriction = 0.0;

        Level.BackgroundColor = Color.Black;

        Camera.ZoomToLevel();
    }

    PhysicsObject LuoMaila(double x, double y)
    {
        PhysicsObject maila = PhysicsObject.CreateStaticObject(20.0, 100.0);
        maila.Shape = Shape.Rectangle;
        maila.X = x;
        maila.Y = y;
        maila.Restitution = 1.0;
        maila.KineticFriction = 0.0;
        Add(maila);
        return maila;
    }

    void LisaaLaskurit()
    {
        pelaajan1Pisteet = LuoPisteLaskuri(Screen.Left + 100.0, Screen.Top - 100.0);
        pelaajan2Pisteet = LuoPisteLaskuri(Screen.Right - 100.0, Screen.Top - 100.0);
    }

    IntMeter LuoPisteLaskuri(double x, double y)
    {
        IntMeter laskuri = new IntMeter(0);
        laskuri.MaxValue = 10;
        
        Label naytto = new Label();
        naytto.BindTo(laskuri);
        naytto.X = x;
        naytto.Y = y;
        naytto.TextColor = Color.White;
        naytto.BorderColor = Level.BackgroundColor;
        naytto.Color = Level.BackgroundColor;
        Add(naytto);

        return laskuri;
    }

    void KasittelePallonTormays(PhysicsObject pallo, PhysicsObject kohde)
    {
        if (kohde == oikeaReuna)
        {
            pelaajan1Pisteet.Value += 1;
        }
        else if (kohde == vasenReuna)
        {
            pelaajan2Pisteet.Value += 1;
        }
    }

    void AloitaPeli()
    {
        Vector impulssi = new Vector(500.0, 0.0);
        pallo.Hit(impulssi);
    }

    void AsetaOhjaimet()
    {
        Keyboard.Listen(Key.A, ButtonState.Down, AsetaNopeus, "Pelaaja 1: Liikuta mailaa ylös", maila1, nopeusYlos);
        Keyboard.Listen(Key.A, ButtonState.Released, AsetaNopeus, null, maila1, Vector.Zero);
        Keyboard.Listen(Key.Z, ButtonState.Down, AsetaNopeus, "Pelaaja 1: Liikuta mailaa alas", maila1, nopeusAlas);
        Keyboard.Listen(Key.Z, ButtonState.Released, AsetaNopeus, null, maila1, Vector.Zero);

        Keyboard.Listen(Key.Up, ButtonState.Down, AsetaNopeus, "Pelaaja 2: Liikuta mailaa ylös", maila2, nopeusYlos);
        Keyboard.Listen(Key.Up, ButtonState.Released, AsetaNopeus, null, maila2, Vector.Zero);
        Keyboard.Listen(Key.Down, ButtonState.Down, AsetaNopeus, "Pelaaja 2: Liikuta mailaa alas", maila2, nopeusAlas);
        Keyboard.Listen(Key.Down, ButtonState.Released, AsetaNopeus, null, maila2, Vector.Zero);

        Keyboard.Listen(Key.F1, ButtonState.Pressed, ShowControlHelp, "Näytä ohjeet");
        Keyboard.Listen(Key.Escape, ButtonState.Pressed, ConfirmExit, "Lopeta peli");

        ControllerOne.Listen(Button.DPadUp, ButtonState.Down, AsetaNopeus, "Liikuta mailaa ylös", maila1, nopeusYlos);
        ControllerOne.Listen(Button.DPadUp, ButtonState.Released, AsetaNopeus, null, maila1, Vector.Zero);
        ControllerOne.Listen(Button.DPadDown, ButtonState.Down, AsetaNopeus, "Liikuta mailaa alas", maila1, nopeusAlas);
        ControllerOne.Listen(Button.DPadDown, ButtonState.Released, AsetaNopeus, null, maila1, Vector.Zero);

        ControllerTwo.Listen(Button.DPadUp, ButtonState.Down, AsetaNopeus, "Liikuta mailaa ylös", maila2, nopeusYlos);
        ControllerTwo.Listen(Button.DPadUp, ButtonState.Released, AsetaNopeus, null, maila2, Vector.Zero);
        ControllerTwo.Listen(Button.DPadDown, ButtonState.Down, AsetaNopeus, "Liikuta mailaa alas", maila2, nopeusAlas);
        ControllerTwo.Listen(Button.DPadDown, ButtonState.Released, AsetaNopeus, null, maila2, Vector.Zero);

        ControllerOne.Listen(Button.Back, ButtonState.Pressed, ConfirmExit, "Lopeta peli");
        ControllerTwo.Listen(Button.Back, ButtonState.Pressed, ConfirmExit, "Lopeta peli");
    }

    void AsetaNopeus(PhysicsObject maila, Vector nopeus)
    {
        if ((nopeus.Y < 0) && (maila.Bottom < Level.Bottom))
        {
            maila.Velocity = Vector.Zero;
            return;
        }
        if ((nopeus.Y > 0) && (maila.Top > Level.Top))
        {
            maila.Velocity = Vector.Zero;
            return;
        }

        maila.Velocity = nopeus;
    }
}