Reinventing the Wheel with Pong, Cont.

If you haven’t checked out the first part of this tutorial, I’d recommend doing so. If you’re just interested in the “finished” code/project, you can find the assets and build files here. Note that you only need to open “index.html” in your web browser to play the current build.

Today we will continue our Pong tutorial, and hopefully find ways to integrate our ideas into our playable framework. Let’s go ahead and open Unity, as well as MonoDevelop (or your IDE of choice). Hopefully you saved your progress! We should have a serviceable pong clone, though not a very fun one. Now that we have the skeleton of a game, what features should it have? If you read the article that prefaced this tutorial, “How To Implement Your Ideas (pt. 1)“, you will know that the same ingredients can be used to make thousands of different recipes. More importantly, we don’t even need to describe the details of each ingredient to convey the meaning of what they represent as a whole.

Our Pong game is a skeleton, perhaps with some muscle fibers and basic organs for functionality. We could add some fur, some paws, and an adorable nose, and we would get… well, we would get… a pet? See, there is an inherit misunderstanding here. Is it a dog, or a cat, or something else entirely? Wouldn’t it have been simpler to reference one of these animals from the start? If I take my dog to the vet, the veterinarians will know the ways in which it is different from a cat, without me ever having to describe its uniqueness.

For our purposes, I could tell you my game has a health bar, environmental interaction, and adaptive enemy AI. This is not helpful, because your idea of my game is now being stretched and pulled by the thousands of tropes and genres that define so many popular games – FPS, RPG, platformer, etc… It may even cause you exclude certain genres that typically lack these features – visual novels, sandbox-creation games, and board games, to name a few. A game is not just it’s skeleton. It’s the combination of each of its structural and functional components that creates an experience. You can play 20 hex-grid, turn-based, historical grand strategy games and walk away with wildly different experiences not only from each game, but from each play session! My point is that your game could be another basic pong tutorial, but the ideas you implement on top of that framework can make it unique and engaging nonetheless.

All of that said, the game I mentioned previously with a health bar, environmental interaction, and adaptive enemy AI, is our Pong game! Well, not yet, we have some more work to do. First, let’s think about how these feature could factor into our existing framework of two paddles, a puck, and a few walls


 

Health Bar

how do we measure the health of two rectangles when there are no incoming laser beams or punches to dodge? Taken one step further, what is health? Can we abstract it to something relevant to our game? Possibly! In fighting games, health is a measure of your performance. If your performance is good, your health remains high. The more mistakes you make, the lower your health gets, until… poof, you’re gone! Not forever, of course. You’ll probably respawn somewhere with some amount of your total health gifted back to you.

Classic 2D fighting games saw many variations to this formula. In some, health could be recovered if certain actions were taken quickly after damage was dealt. In others, both players regained their full health at the start of each round. In other genres, health can be found or earned by defeating opponents or exploring the environment. So already, the idea of a health bar in and of itself is a little unhelpful. We have points in Pong, much like in tennis, but what does it matter? The players remain unchanged for their efforts, and each new round feels like the last.

Let’s change this. Most health bars represent linear progressions from 100% to 0%, so let’s make our paddles living health bars. Every time a player hits the puck, the puck “chips off” a bit of the paddle, shrinking the paddle’s height by a tiny amount. As the volley goes on, the difficult ramps up, with players rushing to move a smaller and smaller paddle to reach trajectories of a faster and faster moving puck. Of course, we should cap our shrinkage so that the players will have at least some fighting chance in longer volleys. Lastly, at the end of the volley, we want the players to return to a normal size. Perhaps we can borrow from fighting games here and restore only a percentage of the paddle’s size, so that players start with 100% size on the first volley, but on consecutive volleys, their max health will only be restore to a percent matching the total average health of the walls behind the players. Last time, we set the “health” of each wall to 3, so if Player One has scored one point, and Player Two has score one point, this average percentage would be (2 + 2) / (3 + 3) = 67%. Once a player scores 3 points, the walls reset their “health”, the players fully reset their size, and a new round begins.

We can start in the Paddle script by creating methods for shrinking and resetting the paddle size each round. The puck can call the shrink method every time it collides with the paddle, and the game manager can control when the paddles need to reset, and by what percentage. For this reason, the resetting method will need to take in some floating point value for the percentage average of the current wall health. Finally, we will need variables to represent the original height of the paddle (for when we reset it), the current height of the paddle, the minimum size the paddle is allowed to shrink to, and the amount that the paddle shrinks every time it is hit. Let’s look at what we’ve got so far:

 


[SerializeField][Tooltip(What is the starting height of the paddle?)][Range(4f, 8f)]
    protected float originalHeight = 6f;

    protected float currentHeight;

[SerializeField][Tooltip(How small can the paddle get before it stops shrinking?)][Range(0.5f, 4f)]
    protected float minimumSize = 1.5f;

[Tooltip(Every hit will reduce the paddle height by this amount:)][Range(0f, 2f)]
    public float shrinkModifierConstant = 0.2f;

 

    protected virtual void Start() {
        body = gameObject.GetComponent<Rigidbody2D> ();
        originalPosition = body.transform.position;
        gameObject.transform.localScale = new Vector3 (gameObject.transform.localScale.x, originalHeight, gameObject.transform.localScale.z);
        currentHeight = originalHeight;
    }

 

    public void ResetSize(float proportion) {
       
    }

    public void Shrink() {
       
    }


 

We will serialize the original height of the paddle, so we can adjust it via the script as opposed to the transform field in the editor. We will also limit the height to a reasonable size, so that the paddles aren’t cramped up between the top and bottom walls. Similarly, our minimum size should be within a reasonable range, and it should be adjustable in the editor for balancing purposes. We will set the shrink modifier “constant” to 0.2, which allows for decent pacing each volley. Finally, our methods are now named and the ResetSize method is parameterized with a float called proportion. Let’s add our code:


    public void ResetSize(float proportion) {
        if (currentHeight < originalHeight * proportion ) {
            currentHeight = originalHeight * proportion;
            gameObject.transform.localScale = new Vector3 (gameObject.transform.localScale.x, currentHeight, gameObject.transform.localScale.z);
        }
    }

    public void Shrink() {
        if (gameObject.transform.localScale.y >= minimumSize) {
            gameObject.transform.localScale -= new Vector3 (0, shrinkModifierConstant, 0);
            currentHeight = gameObject.transform.localScale.y;
        }
    }


 

Okay, so how will we call these methods? I think the Puck should call Shrink() every time it collides with the paddle. We should do this in the OnCollisionEnter2D method:

 


void OnCollisionEnter2D (Collision2D other) {
        if (other.gameObject.tag == Player || other.gameObject.tag == AI) {

           

            if (other.gameObject.tag == Player) {
                other.gameObject.GetComponent<PlayerController> ().Shrink ();
            } else {
                other.gameObject.GetComponent<PlayerAI> ().Shrink ();
            }
        }
    }


 

This code will work, but we can do better. That being said, I will not demonstrate how in this tutorial. I challenge you to fix it! We have been talking about abstraction, so see if you can find a more concise way to call the Shrink() method with the Puck. Keep in mind the relationships between the PlayerController, the PlayerAI, and the Paddle scripts.

Moving on, we will also need a way to refer to the ResetSize() method. Let’s do that in the game manager, every time we score a point, using Countdown():


IEnumerator Countdown() {
        
        yield return new WaitForSeconds (1);

        float progressPercentage =
            (float)(leftWall.health + rightWall.health) /
            (float)(leftWall.maxHealth + rightWall.maxHealth);
        leftPlayer.GetComponent<Paddle> ().ResetSize (progressPercentage);
        rightPlayer.GetComponent<Paddle> ().ResetSize (progressPercentage);

        …


        text.text = ;
        text.enabled = false;
        background.enabled = false;
    }


 

Perfect! Now when we play, our paddles will gradually shrink, with each hit, and recover some of their size after each volley until the end of the round. I think it would be more satisfying if the reset wasn’t so abrupt, though. I want it to look like the paddles are growing in chunky, pixely steps, where one might imagine the Mario mushroom sound effect playing in the back of their minds as it happens. If you remember from the last lesson, we can have a method execute over time by changing its return type to IEnumerator, and calling the method via MonoBehavior’s StartCoroutine() method. Let’s do this, and try to make the change happen gradually:


    public void ResetSize(float proportion) {
        StartCoroutine (GradualReset (proportion));
    }

    private IEnumerator GradualReset(float proportion) {
        if (currentHeight < originalHeight * proportion ) {
            float heightDifference = originalHeight * proportion  currentHeight;
            while (heightDifference > 0) {
                currentHeight += shrinkModifierConstant;
                gameObject.transform.localScale = new Vector3 (gameObject.transform.localScale.x, currentHeight, gameObject.transform.localScale.z);
                heightDifference -= 0.2f;
                yield return new WaitForSeconds (0.1f);
            }
        }
    }


 

Now, whenever the ResetSize method is called, it calls another method that can use “yield return new WaitForSeconds (0.1f)” to space out little bursts of growth by a tenth of a second. Furthermore, we don’t have to change our game manager script to call the new GradualReset method, because ResetSize does it for us! This is much more satisfying to watch, in my opinion, and it will be even better if we could add some sound effects later (hint hint). By the way, if you have been paying close attention to the paddles, you might notice that their x position in the editor has been changing by incredibly small amounts every time the puck collides with them. Unity’s physics can be all too real sometimes, despite our efforts to reduce the mass of the puck and freeze the x positions of both paddles. We can fix this quickly by adding a method in Paddle called ResetXPosition, and call it every time we start a new round through the game manager:


    public void ResetXPosition() {
        gameObject.transform.position = originalPosition;
    }


In the Game Manager:

    void FixedUpdate() {

        if (leftWall.isBroken || rightWall.isBroken) {           

           …

            leftPlayer.GetComponent<Paddle> ().ResetXPosition ();
            rightPlayer.GetComponent<Paddle> ().ResetXPosition ();
            print(New Round);
    }


 

Environmental Interaction

Our pong game is boring. It might occupy your attention for a couple rounds, but the monochromatic aesthetic and lack of interaction aren’t doing the game any favors. Perhaps we could add some color to the mix? We have already imagined that the puck “chips away” at each paddle upon impact. What if each hit imparted the puck with a little bit of color from the paddle’s material? Perhaps the paddles are just rectangular blocks of color that sacrifice a bit of themselves every time they hit away the puck, defending their territory from other invasive colors. How can we show the puck “absorbing” the colors of the paddles? I think adding a trail to the puck would be a cool way of displaying its momentum, direction, and history. Maybe the puck will remain white, but its trail will change colors to that of the last paddle that it hit.

First, select the Puck prefab, and in the inspector, click “Add Component”. We will add two components to our puck, a Particle System and a Trail Renderer, both found in the “Effects” category. You can play around with the seemingly limitless options these components have, but if you are intimated by the sheer amount of choices available to you, fret not! I have arrange the components for our project as follows:

 

 

* as a note, if any of the in-editor options are confusing, Unity’s website and API documentation are great resources for learning the ins and outs of the engine.

When we play the game, a white trail now follows the puck around. I think the trail should start out white, but then change to either a red or blue color, depending on which paddle was most recently hit. Let’s reference these additions in our Puck script with:

    private TrailRenderer trail;

In Initialize(), we should declare this trail as the one on our game object by adding:

trail = gameObject.GetComponent<TrailRenderer> ();

In the Puck’s IEnumerator ResetPosition() method, we should make sure to reset the color of the trail to white:

    trail.startColor = Color.white;

Finally, we can add a method called ColorChange that takes in a new color and sets the trail’s starting color to the new color:

    public void ColorChange(Color playerColor) {
        trail.startColor = playerColor;
    }

Great! Now we can use the game manager to check where the puck is headed, and change/reset its color accordingly. In the GameManager’s FixedUpdate(), add:


    void FixedUpdate() {

      

        if (puck.body.velocity.x > 0 && puck.initialCollision) {
            puck.ColorChange (leftPlayer.GetComponent<Renderer> ().material.color);
        } else if (puck.body.velocity.x < 0 && puck.initialCollision) {
            puck.ColorChange (rightPlayer.GetComponent<Renderer> ().material.color);
        } else {
            puck.ColorChange (Color.white);
        }
    }


 

So this is kind of cheating. Instead of directly changing color based on the last paddle hit, the game manager changes the puck’s color depending on its direction. It is serviceable nonetheless, although is does require a bool that I have not yet mentioned. Let’s add it, and then explain why this implementation needs such a thing. Open the Puck script, and add the following:


    [HideInInspector]
    public bool initialCollision;

    void OnCollisionEnter2D (Collision2D other) {
        if (other.gameObject.tag == Player || other.gameObject.tag == AI) {

            initialCollision = true;



        }

    }

public IEnumerator ResetPosition() {
       
        newSpeed = originalSpeed;


        initialCollision = false;


        float direction = Random.Range (0, 2);
       
    }


 

We need to monitor the initial collision, because if the puck hasn’t collided with a paddle, then it shouldn’t take any new colors. Since we are changing colors based on the puck’s direction, this initialCollision bool acts as a toggle to keep the puck’s trail white for the duration of the “serve” (the puck’s initial velocity at the start of the volley), at least until it hits a paddle.

Alright, our paddle now inherits the color of the last paddle to hit it. This alone is a cool effect, but let’s give this a bit of in-game significance and gravitas. I want something to happen when the puck hits a wall to make the moment satisfying for the scoring player, and tense for the defending player. What if our puck, which has thematically been “wiping paint” off of the paddles, then paints the walls with the color it currently holds? If the red player scores, the blue player should have a visible reminder of the effects of poor defense (and vice versa). This could also provide visual feedback for the progress of the round, since we currently cannot tell if the round has just started, or if there is a 2-2 tiebreak volley coming up.

In order to do this, we need to make our BreakableWall scripts aware of their wall’s Renderer components. We should initialize the Renderer in Start(), and we should remember to reset the color of the renderer to white at the start of each new round via ResetHealth():


public class BreakableWall : MonoBehaviour {

    private Renderer render;


   

    void Start() {
        render = gameObject.GetComponent<Renderer> ();
       
    }

   

    public void ResetHealth() {
       
        render.material.color = Color.white;
    }
}


 

Alright, now we should write a method to actually change the color of the wall, depending on the color of the incoming puck’s trail. Let’s think for a moment… how are we going to change the wall’s color? Will we simply make it a one-to-one copy of the puck’s trail color? That might be a powerful visual mark, but it doesn’t serve the purpose of showing us the progress of the game. We need a method that takes an input color, mixes it with the current color, and returns the result. So if our puck’s trail color is red, the first time it hits the white wall, the wall will turn pink. The second hit will produce a color that is a mix between pink and red, and then pink-red + red, and so on. The ratio of white to red each hit should be as follows:

  • 100 / 0
  • 50 / 50
  • 25 / 75
  • 12.5 / 87.5
  • etc.

So, how can we add/subtract colors using math? Every color that passes through your computer can be interpreted using some variation of four individual components. RGBA use 0 – 255 value sliders for the amounts of red, green, and blue in a color, plus a fourth value slider for the color’s apha, or transparency. The RGBA color [0, 255, 0, 255] is pure, opaque green. The RGBA color [100, 0, 100, 50] gives a slightly transparent purple color. If we could add the individual values of these four components together, we could potentially mix two colors. We can make a new script called ColorMixer to do this. ColorMixer will be a static class, meaning it doesn’t associate with any one particular object.

To conceptualize static classes, imagine a school wants to keep track of the average GPA of all of its students. To do this, it needs to convert A-B-C-D-F grades or 0-100 point grades into a 0.0 – 4.0 GPA scale. The administrators and teachers decide to make a handy conversion sheet, where any style of grade can be input into the sheet, and the sheet will convert the input into GPA. One teacher who uses an A-B-C system can type “A” into the sheet, and the student’s grade will be recorded as 4.0. Another teacher can type 25/100, and that poor student’s grade will be recorded as 1.0.

Thinking about programming, static classes can be extremely useful as “tool” or “helper” classes. More importantly, static classes, like the GPA calculator, don’t “belong” to any particular object, or student. As such, they only use static variables and methods, and they don’t need to be instanced before they are used. By instanced, I am referring to what we do when we write code like this:


using System.Collections;
using UnityEngine;

public class MakeAThing : MonoBehaviour {

    public GameObject thing;

    void Start() {
        thing = new GameObject(Something);
    }


 

Static classes cannot be instantiated, or “made”, like this thing was made. Back to our project, let’s write our code for the class ColorMixer:


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ColorMixer : MonoBehaviour {

    public static Color ColorMix (Color colorOne, Color colorTwo) {

        float r = 0;
        float g = 0;
        float b = 0;
        float a = 0;

        r = (colorOne.r + colorTwo.r) / 2;
        g = (colorOne.g + colorTwo.g) / 2;
        b = (colorOne.b + colorTwo.b) / 2;
        a = (colorOne.a + colorTwo.a) / 2;

        return new Color(r,g,b,a);
    }
}


 

Alright, we simply take the four values of each color supplied to the method, and return a color that is composed of the averages of those four value pairs. Let’s use our new static class in BreakableWall by calling it a new public method called ColorMark:

    public void ColorMark(Color puckColor) {
        render.material.color = ColorMixer.ColorMix (render.material.color, puckColor);
    }

We will then let Puck execute this method every time it collides with a breakable wall. Specifically, we will make this a trigger event, meaning that we will use OnTriggerEnter2D instead of OnCollisionEnter2D. We should check the “trigger” boxes in the inspector for the box collider components of both walls, if we haven’t done so already. Turning these colliders into triggers means that the walls will no longer detect physical collisions or interact physically with the puck, but rather they will send some message to the script for custom interactions. This means that once the wall is hit, the ball will continue to move through it as though the wall doesn’t exist. This is fine, and I explain this choice in a moment. Let’s add our wall-painting effect to the OnTriggerEnter2D method in Puck:

    void OnTriggerEnter2D (Collider2D other) {
        if (other.gameObject.tag == Left Wall || other.gameObject.tag == Right Wall) {
            other.gameObject.GetComponent<BreakableWall> ().damage();
            other.GetComponent<BreakableWall> ().ColorMark (trail.startColor);
            isDestroyed = true;
        }
    }

If everything goes well, we should be able to see the impact of our puck on the walls, and the walls should reset their colors at the end of each round. However, it’s still a little strange to see the puck float through the wall, and the trail effect is now acting strange! We can fix this by resetting the trail at the start of each volley, as well as the puck’s sprite:


public class Puck : MonoBehaviour {

   
    private SpriteRenderer sprite;
    private TrailRenderer trail;

   

    public void Initialize() {
        body = gameObject.GetComponent<Rigidbody2D> ();
        sprite = gameObject.GetComponent<SpriteRenderer> ();
        trail = gameObject.GetComponent<TrailRenderer> ();
       
    }

   

    void OnTriggerEnter2D (Collider2D other) {
        sprite.enabled = false;
        StartCoroutine(TrailKill());
        if (other.gameObject.tag == Left Wall || other.gameObject.tag == Right Wall) {
           
            other.GetComponent<BreakableWall> ().ColorMark (trail.startColor);
        }
    }

    IEnumerator TrailKill() {
        // wait a little bit so the trail can finish sinking into the point of impact before resetting it         yield return new WaitForSeconds (trail.time + 0.1f);
        trail.enabled = false;
    }

    public IEnumerator ResetPosition() {
        gameObject.GetComponent<Collider2D> ().enabled = false;
        yield return new WaitForSeconds (timer * 0.5f + 1); // one for a breather and then the countdown (1/2 second for each tick)
        gameObject.GetComponent<Collider2D> ().enabled = true;
        body.velocity = Vector2.zero;
        gameObject.transform.position = Vector2.zero;
        sprite.enabled = true;
        trail.enabled = true;
        trail.startColor = Color.white;
       
    }

   
}


 

We disable the sprite upon hitting a wall, and wait just a bit longer to disable the trail. This gives the effect of the trail “sinking into” the wall, imbuing it with color. We then enable those components once more in the ResetPosition() method.

The final touches of our visual effects should involve the top and bottom walls, I think. After all, the puck interacts with them way more than it does with the left and right walls. Instead of mixing colors with these walls, it would be cool if the puck could instead cause the top and bottom walls to flash the color of the puck, like pin-ball machine bumpers. If you haven’t already, give the top and bottom walls a plain white material (you can copy or use the same material that the side walls have). We will also give them a script called ColorFlash, which needs to do a few things for us:

  • reference the wall’s material, so we can change it’s color
  • reference two colors: the natural wall color, and whatever color the incoming puck brings when it hits the wall
  • A flash time, or the total time it takes for the wall to flash from white -> puck’s trail color -> white
  • A variable for the current time that acts as a control timer. We will transition from white -> puck’s trail color -> white, only if our control timer has reached a certain point
  • A variable that represents half of the flash time. For the first half of the flash time, we will transition from white to the puck’s trail color. For the second half of this time, we will change the color back to white.
  • We will let the Update() method handle the flash itself, because Update runs according to the in-game time. However, the puck will access this class via a public Flash() method, which “sets the timer” and assigns the puck’s trail color to the second Color variable we referenced.
  • Finally, we might want a Reset() method to change the wall’s color to white at the start of each volley, just in case

 


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ColorFlash : MonoBehaviour {

    private Renderer render;
    private Color originalColor = Color.white;
    private Color puckColor = Color.white;

    [HideInInspector]
    public bool impacted;
    private bool cooldown;
    [SerializeField]
    private float flashTime = 0.1f;
    private float limit;
    private float controlTime = 0f;

    void Start() {
        render = gameObject.GetComponent<Renderer> ();
        originalColor = render.material.color;
        limit = flashTime / 2;
    }
    void Update() {
        if (controlTime > limit  0.05f && impacted) {
            impacted = false;
            cooldown = true;
            controlTime = 0f;
            render.material.color = puckColor;
        }
        if (controlTime < limit && impacted) {
            render.material.color = Color.Lerp (render.material.color, puckColor, controlTime);
            controlTime += Time.deltaTime * 2;
        }
        if (controlTime > limit  0.05f && cooldown) {
            cooldown = false;
            controlTime = 0f;
            render.material.color = originalColor;
        }
        if (controlTime < limit && cooldown) {
            render.material.color = Color.Lerp (render.material.color, originalColor, controlTime);
            controlTime += Time.deltaTime;
        }
    }
    public void Flash (Color hitColor) {
        puckColor = hitColor;
        impacted = true;
        controlTime = 0f;
    }
    public void Reset() {
        render.material.color = Color.white;
        Debug.Log (Resetting Top Wall and Bottom Wall colors);
    }
}


 

Dealing with this in update is a little messy, and it requires two boolean variables to act as toggles, but it works. I challenge those of you following along with this tutorial with a second “homework” assignment: see if you can rewrite this code using a switch statement. Anyway, we have completed out final visual effect! Let’s reference these walls in our Game Manager (don’t forget to drag the game objects from the scene view into the proper inspector fields!):


public class GameManager : MonoBehaviour {

   
    [SerializeField]
    ColorFlash topWall, bottomWall;
   

    void Start() {
       
        topWall = GameObject.FindGameObjectWithTag(Top Wall).GetComponent<ColorFlash>();
        bottomWall = GameObject.FindGameObjectWithTag (Bottom Wall).GetComponent<ColorFlash>();

    }

    void FixedUpdate() {

        if (leftWall.isBroken || rightWall.isBroken) {
           
            topWall.Reset ();
            bottomWall.Reset ();
            print(New Round);
        }
    }


 

To actually see this effect, we’ll remind our Puck script to send the appropriate message to any objects with ColorFlash that it bumps into:

    void OnCollisionEnter2D (Collision2D other) {
       
        if (other.gameObject.GetComponent<ColorFlash> ()) {
            other.gameObject.GetComponent<ColorFlash> ().Flash (trail.startColor);
        }
    }


 

Balancing and Tweaking

Only a few more small changes left to go! The first is more of a gripe – I hate when the ball gets “trapped” in a near-vertical trajectory, bouncing off the walls at 90 degree angles and taking forever to reach the other player. Let’s give ourselves the benefit of the doubt that we won’t cheat, and add in a “level out” button that resets the velocity of the puck mid-match. We obviously only want this button to work if the puck is traveling up and down at such a steep angle that it would get “stuck”. Let’s add a method in our Puck class that checks if this is the case, and returns the result as a bool variable:

    public bool verticalOrientation() {
        float angle = Mathf.Atan2(body.velocity.y, body.velocity.x) * Mathf.Rad2Deg;
        if ((angle > 80 && angle < 100) || (angle > 170 && angle < 190)) {
            return true;
        }
        return false;
    }

Then in the GameManager, we can add another input in our Update() method, just after our pause button:

    void Update() {

        …

        if (Input.GetKeyDown (KeyCode.Space) && puck.verticalOrientation()) {
            float yDir = 0;
            if (puck.body.velocity.y > 0) {
                yDir = puck.originalSpeed / 2;
            } else {
                yDir = puck.originalSpeed / 2;
            }
            puck.body.velocity = new Vector2 (puck.originalSpeed, Random.Range(0, yDir));
        }
    }

There, no more excruciatingly long waits between intense volleys just because the puck gets stuck! What else can we improve upon? I think we could make the AI player a little more adaptive, so that the more we win against it, the faster and more difficult it becomes. Conversely, if our Pong skills are not up to par, the AI should have pity on us and slow down a bit while we get our bearings. In GameManager, we can add the following code to FixedUpdate():


    void FixedUpdate() {

        if (leftWall.isBroken || rightWall.isBroken) {
            
            if (leftWall.isBroken) {
                print (Player 2 has won the round!);
                if (rightPlayer.GetComponent<PlayerAI> ()) {
                    rightPlayer.GetComponent<PlayerAI> ().speed–;
                    print (The AIs speed has decreased to  + rightPlayer.GetComponent<PlayerAI> ().speed);
                }
            }

            if (rightWall.isBroken) {
                print (Player 1 has won the round!);
                if (rightPlayer.GetComponent<PlayerAI> ()) {
                    rightPlayer.GetComponent<PlayerAI> ().speed++;
                    print (The AIs speed has increased to  + rightPlayer.GetComponent<PlayerAI> ().speed);
                }
            }
                
           
        }

       
    }


 

Another cool feature we could add is screen shake, say whenever a wall is hit. Screen shake, when used sparingly, can add impact and feedback to significant moments during play. Let’s add a script to the Main Camera called ScreenShake, and give it the following code:


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ScreenShake : MonoBehaviour {

    private Vector2 currentPosition;
    private Vector3 initialPosition;
    [HideInInspector]
    public float time;
    [Tooltip(How hard do you want the camera to shake?)][Range(0, 10)]
    public float intensity;
    [Tooltip(How quickly should the camera cooldown?)][Range(0, 10)]
    public float cooldownRate;

    void Start() {
        currentPosition = gameObject.transform.localPosition;
        initialPosition = currentPosition;
    }

    void Update() {
        if (time > 0) {
            currentPosition = initialPosition + Random.insideUnitSphere * intensity;
            time -= Time.deltaTime * cooldownRate;
        }
        else {
            time = 0f;
            currentPosition = initialPosition;
        }
    }

}


 

By default, the shaking time is 0. We can add a little time, maybe half a second, in BreakableWall, whenever the wall takes damage. That time will tick down in the screen shake script, and cause shaking until it reaches 0 again. In BreakableWall:


   
    private ScreenShake screen;

    void Start() {
        screen = GameObject.FindObjectOfType<ScreenShake> ();
       
    }

    public void damage() {
       
        screen.time = 0.5f;
    }
}


 

The final addition to our game will be… dun-du-du-dun… sound! Create a new script called SoundManager, and attach it to the Game Manager prefab. We also want to add two Audio Source components to the Game Manager. These will be our audio channels for sound effects (SFX) and music, respectively. Let’s add a reference to both in our SoundManager script, as well as a few methods of playing sound. Our first method will take a single audio clip as a parameter and play it as a sound effect. The second method will take an array of audio clips and play one randomly, at a slightly random pitch for variety:


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SoundManager : MonoBehaviour {

    public AudioSource fxSource;
    public AudioSource musicSource;

    public void PlaySingle (AudioClip clip) {
        fxSource.clip = clip;
        fxSource.Play ();
    }

    public void PlayRandom (AudioClip[] clips) {
        int randomSound = Random.Range (0, clips.Length);
        float randomPitch = Random.Range (0.95f, 1.05f);

        fxSource.pitch = randomPitch;
        fxSource.clip = clips [randomSound];
        fxSource.Play ();
    }
}


 

Let’s test out our sound manager by giving a sound to the countdown text at the start of each volley. Open GameManager, and add a reference to the SoundManager. Then, add a sound clip of your choice and play the clip through the SoundManager in the Countdown() method:


public class GameManager : MonoBehaviour {


    SoundManager soundManager;
    [SerializeField]
    private AudioClip countdownClip;
   

    void Start() {
       
        soundManager = GameObject.FindObjectOfType<SoundManager> ();
        canvas = GameObject.FindObjectOfType<Canvas>();
       
    }

    IEnumerator Countdown() {
       
        for (int i = puck.timer; i > 0; i) {
           
            soundManager.PlaySingle (countdownClip);
            yield return new WaitForSeconds (0.5f);
        }
       
    }
}


 

I chose to make a sound clip with the resource BFXR. Save your clips in an Audio folder, and drag the Countdown Clip to the proper field in the inspector:

GMSound

Also, we can add a main theme song to our game by opening the second audio source (our music source) and adding the AudioClip song of our choosing. I’m rather fond of a track I found from the Free Music Archive, “Electro Swing” by Creo. Be sure to select “Loop” so that the song repeats for extended play sessions. I’ve also pitched the song down a bit in the editor and lowered the volume, for a less bombastic presence in the final game.

Music.PNG

 

Go ahead a make a few more sounds with BFXR or some other software, and return to Unity when you’ve finished. We’ll add all of those sounds into an array in the Puck class, so that every time the puck hits something, a random sound from its audio list will play. We can also choose one particularly powerful sound to trigger every time a wall is hit.



public class Puck : MonoBehaviour {

   
    private SoundManager soundManager;
    public AudioClip[] hitSounds;
    public AudioClip scoreSound;
   

    public void Initialize() {
       
        soundManager = GameObject.FindObjectOfType<SoundManager> ();
    }

    void OnCollisionEnter2D (Collision2D other) {
       
        soundManager.PlayRandom (hitSounds);
    }

    void OnTriggerEnter2D (Collider2D other) {
       
        soundManager.PlaySingle (scoreSound);
    }

   

}


 

Let’s drag our clips to the appropriate fields in the Puck prefab:

PuckSounds

 

One final adjustment. Let’s add a few volume options in the GameManager so that we can adjust the volume of the sounds or mute the music:


    

void Start() {
       
        print (W/S to move player 1. Up/Down Arrows to move player 2. M mutes music. Period increases volume by 10%. Comma decreases volume by 10%. Escape pauses the game);
    }

    void Update() {

        …

        if (Input.GetKeyDown (KeyCode.M)) {
            soundManager.musicSource.mute = !soundManager.musicSource.mute;
        }

        if (Input.GetKeyDown (KeyCode.Period)) {
            soundManager.musicSource.volume += 0.1f;
            soundManager.fxSource.volume += 0.1f;
        }

        if (Input.GetKeyDown (KeyCode.Comma)) {
            soundManager.musicSource.volume -= 0.1f;
            soundManager.fxSource.volume -= 0.1f;
        }
    }


 

There you have it! We’ve finished implementing our ideas onto the classic game of Pong, and we’ve created a pretty fun, unique experience in the process. Of course, that’s not to say that these ideas haven’t been seen before, but rather that we managed to realize them in unfamiliar and exciting ways. I hope you will take the time to accept the challenges I gave you in this lesson, and even add your own ideas into this project. Feel free to use any resources from this lesson in your own projects, and if you liked this tutorial, please check out this site regularly for new content! If you have any comments, questions, or suggestions, please leave a comment below or contact me directly. Thanks for reading, and I’ll see you next time!

Advertisements

One thought on “Reinventing the Wheel with Pong, Cont.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s