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 site, RSS feed, or Twitter.
A gameplay video: http://www.youtube.com/watch?feature=player_embedded&v=AzmAUYbeBDk
Built Pigeons attack…amazing game!
I didn’t know the existence of the cellsdk. A great framework for multiple device deployment.
But it’s not free, so I prefer cocos2d-x. 😉
@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. 😉
Interesting … By the way, I love pigeons.