Creating a basic game in CocosSharp (Part 3)

In the previous post, we looked at what was involved in getting the shock surprise intro scene for Meetup Pop up and running. In the process, we saw a bunch of the CocosSharp classes in use, including sprites, nodes, actions and transitions. In this post, we'll deal with the final screen of Meetup Pop, the actual game screen.

Before we get into the code again, it's worth spending a bit of time discussing in more detail the flow of the game. It's very simple, but there are a few subtleties that might not immediately be obvious.

The rules of Meetup Pop

  • the player starts with an initial number of lives
  • if the players lives count is zero, it's game over
  • balloons travel from the bottom of the screen to the top
    • If the player does not pop a balloon before it reaches the top, lives count is decremented
    • When the player pops a balloon, lives count is incremented
  • balloons come in 'waves'
    • waves consist of a random number of balloons between one and an upper limit. The upper limit starts at two and increases by one every five waves
    • after each wave, the speed of the balloons is increased by 2.5% compounding. This doesn't sound like a lot, but it quickly increases the difficulty significantly!
  • the background of the game changes based on how well the player is currently doing - brilliant green when lives are highest, dark and rainy when near game over.

Pretty basic. The cool thing about this ruleset is that we can implement its logic without having to manage the game loop frame-by-frame. CocosSharp certainly supports this method of logic programming (via the CCNode.Schedule method), but in our case, we can get by with smart use of CCActions.

Implementing the Game Scene

Based on the rules previously mentioned, we need to perform a few key tasks:

  1. Set up the game view and initalise starting values
  2. Launch waves of balloons for the player to tap
  3. Handle balloons being tapped and being missed
  4. Handle game over and resetting the game.

Looking again at the GameLayer class, you can see the variables that keep track of the game logic, and a few methods for handling events that occur - BalloonPopped, MissedBalloon, GameOver.

1. Setting up the game view

As with the intro scene, most of the magic happens in the AddedToScene override. In terms of setting up the scene, there's not a lot of work involved:

// add sun and rain particle effects
var topOfscreen = this.VisibleBoundsWorldspace.Center.Offset(0f, this.VisibleBoundsWorldspace.MaxY/2f);  
_rain = new CCParticleRain(topOfscreen) { Scale = 0 };

this.AddChild(_rain);

// init labels
_scoreLabel = new CCLabel(String.Format("Score: {0}", _currScore), "Consolas", 18f) { Color = CCColor3B.Black }  
    .PlaceAt(.1f, .05f, this)
    .WithTextAlignment(CCTextAlignment.Left);

_livesLabel = new CCLabel(String.Format("Lives: {0}", _numLives), "Consolas", 18f) { Color = CCColor3B.Black }  
    .PlaceAt(1f - .1f, .05f, this)
    .WithTextAlignment(CCTextAlignment.Right);


SetBackgroundColour();

The new class in this snippet is the particle effect - CCParticleRain. Out of the box, CocosSharp comes with a number of particle effect generators, and you can define your own quite easily using a tool like Particle Designer. CCParticleRain is a builtin that will do just fine (and there are plenty more - CCParticleExplosion, CCParticleSun, CCParticleFlower, CCParticleFire and CCParticleGalaxy just to name a few). The rain effect is used to change the background of the screen when things are getting particularly dire - the worse the player's situation, the heavier the rain. Initially, the rain effect h6as a scale of 0 so that it is not visible. We also add labels for score and lives, nothing you haven't seen already. Finally, the SetBackgroundColour method is called for the first time. This method runs after every balloon is popped or missed and adjusts the background of the game to reflect the state of the player - bright green when things are going well, dark red when the player's life count is low. In combination with the rain, the effect adds a bit of character to an otherwise basic game.

Notice how the screen initially gets brighter, then progressively darker.

2. Launching waves of balloons

The core logic for the game lives in a continuous while loop, that launches waves of balloons until the game over sequence has finished. When game over occurs, one final massive wave of balloons is launched for effect, so the loop has a little extra logic to handle that, most of which I have omitted, but it is fairly straightforward.

The wave loop performs the following actions:

    // determine number of balloons to fire
    // if game over, fire heaps for effect
    var numBalloons = !_gameOver ? r.Next(1, _maxAtOnce + 1) : 500;

    // generate the balloons
    var balloons = Enumerable.Range(0, numBalloons)
        .Select(_ =>
        {
            // pick a texture at random for the new balloon
            var randomIndex = r.Next(0, this.PeopleTextures.Count - 1);
            var randomTexture = this.PeopleTextures[randomIndex];

            // create sprite with touch handler 
            return new CCSprite(randomTexture)
                .WithTouchHelper(h => h.TouchBegan
                                        .Where(__=> !_gameOver)                        
                                        .Subscribe(___ => BalloonPopped(h)));
        }).ToList();

First, the number of balloons to fire is determined. This is a random number between 1 and the current maximum number of balloons that can be launched based on the wave number (r is a local instance of System.Random). Note that if _gameOver is true, a lot of balloons are going to be generated.

Once the count is determined, the balloons are generated with a random texture each time, such that we should get a wave of varying meetup participants each time. A TouchHelper class is attached to the balloon that calls BalloonPopped when the balloon is touched, provided _gameOver is not true. TouchHelper is a class I wrote to help handle hit detection for CCNode instances. It's available in the meetup-pop repository but has the caveat that it does not account for scaling or other transforms you might apply to your CCNode. In this case, we don't do any of that so it is fine. We will go over the BalloonPopped method later in the article.

    // launch the balloons from the bottom of the screen
    var i = 0;
    balloons.ForEach(async b =>
    {
        // wait a little so that each balloons are slightly staggered
        await Task.Delay(TimeSpan.FromMilliseconds(50 * (++i)));

        // place under the screen
        var randomXPos = r.NextFloat().Between(.1f, .9f);

        b.PlaceAt(randomXPos, 1.1f, this);

        // create the launch action
        var timeToReachTop = _currTimeToReachTop.VaryBy(.15f); // use current duration with some variability
        var targetPoint = new CCPoint(b.PositionX, b.VisibleBoundsWorldspace.MaxY + 50); // move to a point above the top of the screen;

        var moveToTop = new CCMoveTo(timeToReachTop, targetPoint);

        // create the fail action; this runs after the balloon reaches the top of the screen 
        // if the move action completes then this ballon was not tapped in time
        var failAction = new CCCallFuncN(MissedBalloon);

        // combine the move and fail in sequence
        var seq = new CCSequence(moveToTop, failAction);

        // launch the balloon
        b.RunAction(seq);
    });

Once the balloons have been generated, the next task is to launch them progressively from the bottom of the screen:

  • The first part of this code snippet ensures that balloon launches are staggered.
  • Each balloon is then assigned a random point along the width of the screen, excluding the first and last 10% (because it can be hard to touch objects at the extremes of the screen).
  • The PlaceAt helper is then used to place the balloon on the screen, at 110% of the height (because of the CocosSharp co-ordinate system, that is 10% below the bottom of the screen).
  • The point to which the balloon will fly and the time to reach the top is calculated, with some variability added.
  • Finally the action to move the balloon to the top is created, this deserves a few words -

We use a CCMoveTo action, which takes a duration parameter and target point, and interpolates the travel between points. However, before we run the move action, we wrap it in a CCSequence which first executes the move action, then executes a CCCallFuncN action - an action that runs code - in this case the MissedBalloon method, which takes a life away from the player. As you'll see in the next section, we rely on the fact that the BalloonPopped method cancels the actions running on the balloon that was popped, preventing a life from being taken away when the player pops the balloon, but allowing us to use the sequence to save us from writing code to check whether balloons moved off the screen.

    // after each round, increase the difficulty by reducing the time 
    // for balloons to reach the top
    _currTimeToReachTop = _currTimeToReachTop * durationMultiplier;
    _currWave++;

    // if there have been enough waves, increase max balloon count 
    if (_currWave % increaseMaxBalloonCountEvery == 0)
        _maxAtOnce = (int)(_maxAtOnce + 1);
}

Finally, the wave characteristics are updated - increase the speed of the balloons by applying the duration multiplier, increase the wave number, and potentially increase the maximum number of balloons that can be launched for the next wave.

3. Handling balloons being tapped and missed

We've seen from the previous section that balloons are generated with a TouchHelper that calls BalloonPopped when a balloon is popped, which prevents the balloon from reaching the top and calling the MissedBalloon method. As for what's in the actual methods:

In `BalloonPopped`, we add to the score and player's life count, update the labels and background colour, then animate away the popped balloon. In `MissedBalloon`, we do similiar things - take away a life, then update labels and background colour. However, we also need to check whether this missed balloon has taken lives to zero, and if so, call the `GameOver` method.
private async void BalloonPopped(TouchHelper<CCSprite> h)  
{
    var poppedBalloon = h.Node;

    // points scored relative to the speed of the balloons
    _currScore += (int)(2000f - _currTimeToReachTop * 1000f);
    _scoreLabel.Text = String.Format("Score: {0:N0}", _currScore);

    // get an extra life each success
    _numLives += 1;
    _livesLabel.Text = String.Format("Lives: {0}", _numLives);

    // update the background colour
    SetBackgroundColour();

    // stop the balloon from moving
    poppedBalloon.StopAllActions();

    // make it spin away
    poppedBalloon.RunAction(new CCRepeatForever(new CCRotateBy(.1f, 360f)));
    await poppedBalloon.RunActionsWithTask(new CCScaleTo(.5f, 0f));

    //cleanup
    poppedBalloon.RemoveFromParent();
}
private void MissedBalloon(CCNode balloon)  
{
    // if already game over, don't worry
    if (_gameOver)
        return;

    // decrement lives and update label
    _numLives -= 1;
    _livesLabel.Text = String.Format("Lives: {0}", _numLives);

    // update the background colour
    SetBackgroundColour();

    // if we are out of lives, it's game over time
    if (_numLives <= 0)
        GameOver();

    // remove the balloon from the game
    balloon.RemoveFromParent();
}
4. Game Over and resetting

The difficulty of meetup-pop increases very quickly, making it basically impossible to beat after just a few waves (perhaps not the best design decision). We need a way to signal game over and allow the player to restart the game. In Meetup Pop, this all happens in the GameOver method. There isn't anything that we haven't seen before in this method - it sets the _gameOver flag to true, which triggers the massive balloon wave, then places a Game Over overlay on the screen and waits for the player to tap to restart the game. One point worth covering is that in Meetup Pop, we can simply transition into a new instance of our GameScene to restart the game, rather than worrying about re-initialising variables and returning to a clean state from within the current scene:

// add touch listener, restart game on touch
var touchListener = new CCEventListenerTouchAllAtOnce()  
{
    OnTouchesBegan = (touches, args) =>
    {
        this.Director.ReplaceScene(new CCTransitionRotoZoom(.5f, GameLayer.Scene(this.PeopleTextures, this.Window)));
        this.RemoveAllChildren(true);
    }
};

this.AddEventListener(touchListener);  

Note that you can not pass the same instance of your scene into ReplaceScene - that doesn't make sense and will cause Bad Things to happen.

Phew!

And that covers the majority of Meetup Pop! It's clear this game won't be charting in the App Store anytime soon, but it did demonstrate a bunch of the fundamentals. The last piece I promised out of this was the set of helper extension methods that were referenced through the code samples - I'll make sure these come in a later post :)