In the previous post, we introduced the CocosSharp framework and showed how you to launch it from an application. In this post, we'll introduce Meetup Pop - the sample I made for the Queensland C# Mobile Developers meetup group - then run through the code for the introduction scene.
The Intro Scene - sprites, actions and some touch, oh my!
The intro screen for Meetup Pop is all about the unexpected twist - a plain looking "Balloon Pop" game suddenly transforms into a, well, slightly less plain looking "Meetup Pop". There are a few things that need to happen:
- put the inital "Balloon Pop" and "Loading" text onto the screen
- go out to the meetup api, retrieve the rsvp list, then download the profile images
- prepare and execute the 'surprise'.
This is a good opportunity to show a few of the core classes in CocosSharp mentioned in the previous post.
1. Putting text on the screen, animating the 'loading' text
Having no text in a game at all can be an effective mechanic, but in many instances you're going to want to get some words on the screen. For this purpose in CocosSharp, the CCLabel
class is your friend. From Meetup Pop, the code to set up the loading screen is:
// add a title label
var titleLabel = new CCLabel("Balloon Pop!", "arial", 36f)
.PlaceAt(.5f, .35f, this)
.WithTextCentered();
// add a status label
var statusLabel = new CCLabel("Loading..", "arial", 24f)
.PlaceAt(.5f, .7f, this)
.WithTextCentered();
// add a spinner
var spinner = new CCLabel ("+", "arial", 48f)
.PlaceAt (.5f, .75f, this)
.WithTextCentered ()
.WithOngoingActions (new CCRotateBy (.1f, 180f));
The constructor for a CCLabel
takes parameters for the text to be displayed initially (this can be updated later), the name of the font, and the font size. Note that for the spinner, a CCRotateBy
action is used to rotate the "+" label by 180 degrees indefinitely, providing a basic loading animation.
You probably picked up the fluent-like syntax - this is not standard CocosSharp (sorry), but one of the major benefits of CocosSharp and Xamarin in general is that we get to take advantage of all the features and expressiveness of the C# language. Once you've placed an object relative to the dimensions of the screen or another object more than a couple of times, you'll be looking for the same kind of thing. The extension methods here and later will be covered at the end of the post and are all on github.
2. Getting the meetup profile pictures
The process of retrieving the data from meetup is less relevant for this post (it's a GET against a url with parameters that contain your api key and the specific meetup event you are interested in), but loading the images into CocosSharp is worth looking at.
// ** error handling code omitted **
var validRsvpImageUrls = await GetValidRsvpImageUrls(); // returns List<string> of urls for profile pictures
// download the photos
var imageStreams = await Task.Run(() =>
validRsvpImageUrls
.Select(url => new MemoryStream(new HttpClient().GetByteArrayAsync(url).Result))
.ToList());
// have to load textures on the main thread
List<CCTexture2D> textures = imageStreams.Select(img => new CCTexture2D(img)).ToList();
With the urls from validRsvpImageUrls
, HttpClient
is used to bring down the image data and each is wrapped in a MemoryStream
. This is because CocosSharp's CCTexture2D
can be initialised from a MemoryStream
, giving us a way to load them at runtime after downloading them. Keep in mind that while the download can happen in background, the texture loading must be done on the main thread, so bulk loading all the textures at once may not be an appropriate approach given a large number of images.
3. Animating the 'surprise'
With images of the crowd in memory, the scene is set (pun intended) for a shocking change of pace. The surprise involves:
- Randomly placing participant images around the screen, then animating them into view (with the ongoing shrink/grow animation)
- Creating the "MEETUP POP" banner and bouncing it into view
- Adding a touch listener to the view so that the user can tap to start the game.
Again, I used extension methods to make things a little easier for me.
private void PlaceImages(List<CCTexture2D> textures)
{
var r = new Random();
var i = 0;
textures.ForEach(async tex =>
{
// stagger the appearence of images
await Task.Delay(TimeSpan.FromMilliseconds(i++ * 50f));
// place the sprite on the screen at at random location, starting scaled to zero
var sprite = new CCSprite(tex) { Scale = 0f }
.PlaceAt(r.NextFloat(), r.NextFloat()*.6, this);
// scale up to normal size
await sprite.RunActionsWithTask(new CCEaseOut(new CCScaleTo(.25f, 1f), .3f));
// shrink and grow forever
var duration = .75f.VaryBy(.1f);
var grow = new CCScaleTo(duration, 1.5f);
var shrink = new CCScaleTo(duration, 1f);
var forever = new CCRepeatForever(grow, shrink);
sprite.RunAction(forever);
});
}
Here, for each texture in the list of textures downloaded earlier, a new CCSprite
is created and placed at a random point on the screen (within the first 60% of the screen height). By setting the Scale
property of the sprite to zero, a CCScaleTo
action can then be used to zoom the sprite into view from the background. In this case, the CCScaleTo
action is wrapped in a CCEaseOut
action - an easing action - which alters the interpolation of the animation to give a 'woosh in' effect.
Note that rather than use the standard RunAction()
method on the sprite, which is a 'fire and forget' method, I'm using another extension method RunActionsWithTask()
that takes a sequence of actions and returns an awaitable Task, allowing further execution to be delayed until the completion of the action chain.
Finally, a second sequence of actions is defined for the sprite. This sequence repeatedly shrinks and grows the sprite and is created by wrapping two CCScaleTo
actions (one shrinking, one growing) in a CCRepeatForever
, which does exactly what it says on the box. This sequence of actions will run until the sprite is removed, or StopAllActions()
is called.
Putting the "MEETUP POP" banner on the screen works directly with CCNode
and CCDrawNode
.
private void ShowSurpriseBanner()
{
var container = new CCNode() {
ContentSize = new CCSize(this.VisibleBoundsWorldspace.MaxX*.75f, this.VisibleBoundsWorldspace.MaxY*.1f),
Scale = 5f,
Rotation = -22.5f,
}.PlaceAt(.5f, .35f, this);
var filledBanner = new CCDrawNode() {ContentSize = container.ContentSize}
.FillWith(CCColor3B.Red)
.PlaceAt(.5f, .5f, container);
var newLabel = new CCLabel("MEETUP POP", "consolas", 48f)
.WithTextCentered()
.PlaceAt(.5f, .5f, container);
// bounce into the screen
container.ZOrder = 1000;
container.RunAction(new CCEaseBounceOut(new CCScaleTo(1f, 1f)));
}
Here a CCNode
is being used as a container for the banner and text, which can then be animated and transformed to have all the underlying parts affected in unison. By setting the Scale
property to 5f (500%) and then animating it back to 1f (100%), the node will appear to come in from outside the screen, giving the 'stamp' effect. A CCDrawNode
is used to provide the red background for the banner, easily achieved with another extension method called FillWith
, which draws and fills a rectangle in the given CCDrawNode
's bounding box with the specified colour. On top, we add a CCLabel
, then run the CCScaleTo
action that animates the container to it's final size. By wrapping the scale action in a CCEaseBounceOut
easing action, we get an animation that overuns its final animation point and 'bounces' back, emphasising the idea that the banner fell from behind the screen.
The final bit of code needed for the intro scene is code that detects when the user has tapped the screen. After tapping, we'll create the GameScene
instance and transition to it. Handling an arbitrary touch is easy to do with a CCEventListenerAllAtOnce
.
var touchListener = new CCEventListenerTouchAllAtOnce()
{
OnTouchesBegan = (touches, args) =>
{
var gameScene = GameLayer.Scene(textures, this.Window);
var transition = new CCTransitionRotoZoom(.5f, gameScene);
this.Director.PushScene(transition);
}
};
this.AddEventListener(touchListener);
Pretty basic. The CCEventListenerTouchAllAtOnce
has Action
properties for OnTouchesBegan
, OnTouchesMoved
, OnTouchesEnded
and OnTouchesCancelled
. The AllAtOnce
part of the listener indicates that a single event fires for all touches in play, in contrast to CCEventListenerTouchOneByOne
which fires individual events for each simultaneous touch on the screen. Here we just need to act on the first touch anywhere on the screen, so the all at once variant will be fine.
When a touch occurs, we create a new instance of GameLayer
using the scene pattern mentioned earlier, and place it in a new CCTransition
. There are many subclesses of CCTransition
that provide animated transitions, in this case, CCTransitionRotoZoom
creates a spinning zoom out effect which seems appropriate. We then use Director.PushScene()
passing the transition as parameter to move to the next screen of the game.
Consider yourself introduced
That's the intro screen done! In the next post, we'll cover the implementation of the main game scene, which will show some clever use of actions, specific touch handling and some of the built in particle effects available to you.