Guest post: The Making of Pigeons Attack with Cell SDK

Pigeons Attack was born as a sample to show how the accelerometer API works. You move forward and backward –tilting your phone- your just washed car in order to avoid pigeons get it dirty. Once the semaphore becomes green, leave quickly the scene to check how many impacts you had. The less you have the better.

Within the following sections you will be guided through the development process, starting from a new Cell SDK project.

The Background

Following the Security Zone guideline the graphic content will be the same for every device. The background is placed at the center of the screen:

SetBackground(Image.CreateImage("Images/background_ios"), Adjustment.CENTER);

Nothing else. Just one single line. 🙂

The Pigeons

Except the background, the game is composed of images which change its properties depending on the game state. All those images are “packed” within a single sprite sheet.

Each sprite can be recovered calling SubImage(x, y, width, height). So, the first thing is to create the sprite sheet image:

Image iSpriteSheet = Image.CreateImage("Images/spritesheet");

Each pigeon consist on a Label with an Image inside. Although two different images of the pigeons are provided, just one is used and, with the vertical flip effect, it is spread along the rest:

Label[] lPigeons = new Label[5];
Image iPigeonLookingAtLeft = iSpriteSheet.SubImage(0, 240, 67, 79);
Image iPigeonLookingAtRight = iSpriteSheet.SubImage(0, 240, 67, 79);
iPigeonLookingAtRight.Effect = Image.EffectType.FLIP_VERTICAL;

lPigeons[0] = new Label(iPigeonLookingAtLeft);
lPigeons[1] = new Label(iPigeonLookingAtLeft);
lPigeons[2] = new Label(iPigeonLookingAtRight);
lPigeons[3] = new Label(iPigeonLookingAtLeft);
lPigeons[4] = new Label(iPigeonLookingAtRight);

AddComponentDeviceAgnostic(lPigeons[0], 401, 79);
AddComponentDeviceAgnostic(lPigeons[1], 398, 207);
AddComponentDeviceAgnostic(lPigeons[2], 397, 345);
AddComponentDeviceAgnostic(lPigeons[3], 397, 495);
AddComponentDeviceAgnostic(lPigeons[4], 406, 634);

Instead of using AddComponent(), AddComponentDeviceAgnostic() differentiates between iOS (iPhone 4) screen size and Windows Phone and Android one. This philosophy is part of the Security Zone guideline.

private void AddComponentDeviceAgnostic(Component c, float x, float y)
{
#if WINDOWS_PHONE || ANDROID
	AddComponent(c, x, y);
#else
	AddComponent(c, DeviceAgnosticX(x), DeviceAgnosticY(y));
#endif
}

private Vector2 DeviceAgnosticPosition(float x, float y)
{
#if WINDOWS_PHONE || ANDROID
	return new Vector2(x, y);
#else
	return new Vector2(DeviceAgnosticX(x), DeviceAgnosticY(y));
#endif
}

private float DeviceAgnosticX(float x)
{
#if WINDOWS_PHONE || ANDROID
	return x;
#else
	return x + 81;
#endif
}

private float DeviceAgnosticY(float y)
{
#if WINDOWS_PHONE
	return y;
#else
	return y + 79;
#endif
}

The Car

The car is made of the chassis itself and the two tires. As the car -as a single piece- will move, it’s easier to put the car components inside a Container, and just move this last thing:

// Car component
cCar = new Container<CoordLayout>(new CoordLayout())
{
    BackgroundColor = Color.Transparent,
    BringToFront = false
};
// Car itself
lCar = new Label(iSpriteSheet.SubImage(0, 0, 164, 238));
cCar.Layout.AddComponent(lCar, 0, 0);
// Tires
Image iTire = iSpriteSheet.SubImage(69, 240, 52, 51);
lLeftTire = new Label(iTire);
cCar.Layout.AddComponent(lLeftTire, 0, 44);
lRightTire = new Label(iTire);
cCar.Layout.AddComponent(lRightTire, 0, 137);
AddComponentDeviceAgnostic(cCar, 24, 11);

The car moves based on the accelerometer data. When using the accelerometer, it is a must to initialize it before accessing its info:

if (AccelerometerSensor.Instance.IsConnected)
{
    accelerometerDetected = true;
    AccelerometerSensor.Instance.Start();
}

Instead of continuously asking for AccelerometerSensor.Instance.IsConnected property, it is faster to check for an accelerometerDetected bool.

Within the Update() method, the car position is modified:

if (accelerometerDetected)
{
    offset = -AccelerometerSensor.Instance.Data3.Y;
    Vector2 temp = cCar.Position;
    temp.Y = MathHelper.Clamp(temp.Y + offset * 25, minClamp, maxClamp);
    cCar.Position = temp;
}

minClamp and maxClamp values mean the min and max floor positions the car can have:

minClamp = DeviceAgnosticY(11);
// The car won't overpass the semaphore until it turns green
maxClamp = DeviceAgnosticY(545);

Pigeons’ Shits Poos

From a programming point of view, there will be just one poo, hidden by default, which will be placed at a pigeon’s position and animated for the falling to the floor –or the car, who knows.

Image iShit = iSpriteSheet.SubImage(0, 321, 44, 23);
lShit = new Label(iShit);
// The shit will be hidden since the very beginning, so doesn't matter where to place it
AddComponent(lShit, -100, -100);
lShit.Visible = false;

Each pigeon’s position is pre-calculated for a quicker reference in the following cycles:

shitPositions = new Vector2[5];
shitPositions[0] = DeviceAgnosticPosition(390, 124);
shitPositions[2] = DeviceAgnosticPosition(390, 356);
shitPositions[3] = DeviceAgnosticPosition(389, 540);
shitPositions[4] = DeviceAgnosticPosition(403, 647);

The animation, just one, will take the poo from the pigeon’s position to the floor:

elapsedTimeBetweenShits = TimeSpan.Zero;

// 60 will cause the animation to take 1 s to complete
aShitFalling = Animation.CreateAnimation(75);
aShitFalling.AnimationType = AnimationType.Relative;
aShitFalling.AddKey(new KeyFrame(0, Vector2.Zero));
aShitFalling.AddKey(new KeyFrame(aShitFalling.NumFrames - 1, new Vector2(-400, 0)));
// Once the shit touches the road it must disappear
aShitFalling.EndEvent += delegate { lShit.Visible = false; };

The last thing is to loop the animation inside the Update() method:

if ((elapsedTimeBetweenShits += gameTime.ElapsedGameTime) >= TimeSpan.FromSeconds(INTERVAL_BETWEEN_SHITS))
{
    elapsedTimeBetweenShits = TimeSpan.Zero;
    lShit.Position = shitPositions[random.Next(0, shitPositions.Length)];
    lShit.Visible = true;
    aShitFalling.Play(lShit);
}

INTERVAL_BETWEEN_SHITS is set to 2 (seconds).

The impact between the poos and the car is pretty easy thanks to Intersects():

if (lShit.Visible && lCar.Intersects(lShit))
{
    impacts++;
    aShitFalling.Stop();
}

The Semaphore

Three lights compose it (from top to bottom): red, amber and green:

lRed = new Label(iSpriteSheet.SubImage(0, 346, 28, 32));
AddComponentDeviceAgnostic(lRed, 265, 716);
lAmber = new Label(iSpriteSheet.SubImage(123, 240, 27, 32));
AddComponentDeviceAgnostic(lAmber, 238, 715);
lGreen = new Label(iSpriteSheet.SubImage(69, 293, 27, 33));
AddComponentDeviceAgnostic(lGreen, 212, 717);
lAmber.Visible = lGreen.Visible = false;

By default, the red one is on for the duration of the gameplay:

private TimeSpan PLAYING_TIME = TimeSpan.FromSeconds(60);

, and, after this time, the amber one turns on for 5 seconds before, finally, the green one occupies the rest:

elapsedGameTime += gameTime.ElapsedGameTime;

// Amber
if (!isAmber && elapsedGameTime >= PLAYING_TIME - TimeSpan.FromSeconds(5))
{
    lRed.Visible = false;
    lAmber.Visible = true;
    isAmber = true;
}
// Green
else if (!isGreen && elapsedGameTime >= PLAYING_TIME)
{
    lAmber.Visible = false;
    lGreen.Visible = true;
    isGreen = true;
    // This will allow the car to exit through the right side of the screen
    maxClamp = DeviceAgnosticY(1000);
}

The Impacts Count

Finally, once the car abandons the dangerous zone (overpasses the green semaphore), a title is shown with the amount of impacts received.

First, it is defined at the Initialize() method, and hidden until the end of the game:

Font fImpacts = Font.CreateFont("Fonts/spritefont_0_9");
lImpacts = new Label("0", Color.White, Color.Transparent) { Font = fImpacts, Rotation = MathHelper.PiOver2 };
AddComponentDeviceAgnostic(lImpacts, 312, 107);
lImpacts.Visible = false;
lImpactsBrand = new Label(iSpriteSheet.SubImage(166, 0, 130, 492));
AddComponentDeviceAgnostic(lImpactsBrand, 175, 201);
lImpactsBrand.Visible = false;

, when it shows, inside the Update() one, the final result:

if (!lImpacts.Visible && isGreen && lCar.Position.Y >= Height + 100)
{
    lImpacts.Text = impacts > 9 ? "9" : impacts.ToString();
    lImpacts.Visible = lImpactsBrand.Visible = true;
}

Just The Beginning

The entire source code can be found at GitHub, ready to run on Android, iOS and Windows Phone devices.

Take a look to the Documentation page where you will find lot of samples and tutorials.

This post has been written by the CELLSDK team. For more info:

This post is part of iDevBlogADay, a group of indie iOS development blogs. You can keep up with iDevBlogADay through the web siteRSS feed, or Twitter.

5 thoughts on “Guest post: The Making of Pigeons Attack with Cell SDK

  1. @raspodev: Thanks, we love it too!

    @Jose Antonio Andujar: Thanks for your comment. Cell SDK also has a free version to give it a try. You could have Mariano Ninja running on iOS, Android & Windows Phone with a single effort. 😉

Leave a Reply