Skip to content

SpriteSheets — The Movie

Initially I just wanted to provide original content in my blog and provide stuff that I couldn’t find elsewhere (or at least not the way I wanted or needed it), but this one is so cool that you should also be able to watch it if you browse my blog, courtesy of Andreas Loew of Code’n’Web, who provides the excellent and absolutely essential tool Texture Packer and Physics Editor (both of which I heavily use and recommend)!.

If you do for example Cocos2D programming, this is what you need to know about sprite sheets!

A Magnifying Glass for Cocos2D

UPDATED VERSION 12/5/2012

During development of our new iOS game, using Cocos2D, I needed a magnifying glass that you could move over the screen and enlarge the portions below it. I did something similar before with UIKit, and that wasn’t a big deal at all, however with OpenGL it is a different story. I experimented with CCLens3D effects, but wasn’t satisfied with it. Surfing around didn’t reveal much, except that there a) is a need, and b) no real complete solution around. At the end of this blog, you can download everything you need for a great magnifying glass Cocos2D solution!

Based on how to grab a screenshot to get the data that is currently shown (discussed e.g. here in the Cocos2D forum), I came up with a solution to simply take the UIKit technique I knew already: grab the data (in this case from the OpenGL buffer, but just the piece that I need), redraw it enlarged into an UIView (depending on a variable magnification scale) and then let that view hover on top of the OpenGL view. This solves at the same time the issue that any loupe graphics you use in Cocos2D would be part of the contents of the current OpenGL buffer and be enlarged as well, ending up with a funny mirror-in-mirror effect.

The UIView will be wrapped by a CCNode-derived class, so you can use it like other nodes within your scene. The solution is surprisingly lean and easy to use, does not eat much of your precious memory and is fast. In contrast to the well-known textedit loupe, I chose to not enlarge the area below your finger, but that below the center of the loupe. This turned out to be much more intuitive for my use-case, but of course you can do it differently by simply changing the grabbed rectangle coordinates.

The loupe will hover quite a bit above your finger, which is determined by setting the anchor position in the initialization. You can choose a different setting here, or make it even dynamic by adapting the anchor in setPosition (so the loupe will move around your finger when approaching the lower screen border — pretty nice effect).
For the loupe frame itself you can create whatever graphics you like. My example provides a very thin and simple rounded frame with a little 3D effect and shadow, but you can design whatever you like. For simplicity I chose a centered inner area, which makes rendering the magnified area very easy. If you need it, however, you can also go for a masked approach and mask out the inner part by providing an according masking image (and the required coding for it, of course. Ray Wenderlich provides some great samples for masking in his blog!).

Update 5/12/2012: For Cocos2D V2.0 it is no longer required to explicitly rotate the contents according to device orientation. The coding to download below  now checks the Cocos-Version and omits the rotation now automatically.

If you create your own loupe image with different proportions, you need to change the percentage of the inner part. You can compute that simply by dividing the diameter of the inner part by the total side-length of your image. Included you’ll find all required graphics for iPhone & iPad, ready & tested for standard and retina resolutions. I chose to have a relatively larger loupe for the iPhone/iPod, since magnification was even more required here than on the iPad, but it’s up to you what size of graphics you provide.

Once you imported the Magnifier class and the images into your project, you set the loupe up like that somewhere in your scene:

  Magnifier *magnifier = [[[Magnifier alloc] initWithMagnification: 2.0] autorelease];
  magnifier.position = <some position, e.g. touchLocation in ccTouchesBegan/Moved>;
  [self addChild:magnifier];

and there you go. Whenever you change the position, the magnified area will be updated as well automatically. If you want the magnified content to change dynamically also while you don’t move the loupe, you need to call the magnify method either periodically from a timer method, or at points in time where you know that content has changed. From my experience, though, having it updated on position changes is completely sufficient.

Update: Fix for iOS6 and above

There is nothing else to do for all iOS versions up to iOS 5.1. For newer versions of iOS in the future, however, you will need to tweak the way OpenGL handles graphics buffering, otherwise you will see a unicolor area only in the loupe. Fortunately, this is pretty easy to do: just replace this standard EAGLView initialization in your app delegate:

EAGLView *glView = [EAGLView viewWithFrame:[window bounds]
	                       pixelFormat:kEAGLColorFormatRGBA8 // or whatever you use...
			       depthFormat:0];

with this one to make sure that OpenGL will retain it’s backbuffers:

EAGLView *glView = [EAGLView viewWithFrame:[window bounds]
                               pixelFormat:kEAGLColorFormatRGBA8 // or whatever you use...
                               depthFormat:0
                        preserveBackbuffer:YES
                                sharegroup:nil
                             multiSampling:NO
                           numberOfSamples:0];

The Beef

The loupe graphics and Magnifier class ready for consumption can be downloaded here.

Prevent blurry fonts when using CCLabelTTF

Note: this in an updated blog (August 11th, 2012) to the original version, including a new download!

When using CCLabelTTF to use True Type fonts with Cocos2D, you might have noticed that they sometimes look pretty blurry, while in other places they appear reasonably sharp. What’s wrong here?

The rendering of the fonts tries to strictly consider the exact position of a font, which is given as a floating point number. If this position is not exactly hitting an actual pixel on the screen, the rendering is adjusted accordingly by introducing additional anti-aliasing, which makes the font appear to start exactly between two pixels. In theory. In reality, it simply looks blurry now, and I am sure in favor of a sharp font you would have rather adjusted a correct 0.3 pixel offset to the nearest pixel instead.

Fortunately, it is really not a big deal to do exactly this. We just need to derive a new class from CCLabelTTF and check (and possibly correct) the top left position each time it might change. Considering the complete node structure we are embedded in, we need to round the top left corner coordinates (in world/screen space) to a full pixel, and then recompute the position all the way back to our local coordinate system and fix the position parameter accordingly. Why the top left corner? Because the string is ultimately converted to a Cocos2D texture using the core NSString drawInRect method, which is based on the UIKit coordinate system and has its (0,0) coordinates always top left (rather than Cocos2D bottom left origin).
To make sure that rounding errors don’t sum up and make your label move uncontrolled, we always remember the originally intended position and adjust always referring to that position.

As a reward for reading this blog, you can simply download a ready-made class SharpLabelTTF right away! You can use it instead of CCLabelTTF and integrate it exactly like that. You don’t need to consider anything else.

Using CCLabelTTF:

Using SharpLabelTTF:

And here is the beef:

@implementation SharpLabelTTF
- (void) fixPosition {

    CGSize dim = self.texture.contentSize;    
    
    [super setPosition:intendedPosition_];
    if (scaleX_ < 0.3 || scaleY_ < 0.3) return;

    // compute world (= screen) coordinate of top left position of label  
    CGPoint worldSpaceTopleft = [self convertToWorldSpace: ccp(0,dim.height)];

    // align origin on a pixel boundary on screen coordinates
    worldSpaceTopleft.x = roundf(worldSpaceTopleft.x * CC_CONTENT_SCALE_FACTOR()) / CC_CONTENT_SCALE_FACTOR();
    worldSpaceTopleft.y = roundf(worldSpaceTopleft.y * CC_CONTENT_SCALE_FACTOR()) / CC_CONTENT_SCALE_FACTOR();

    // transform back into node space
    CGPoint nodeSpaceTopleft = [self convertToNodeSpace:worldSpaceTopleft];

    // adjust position by the computed delta
    CGPoint delta = ccpSub(nodeSpaceTopleft, ccp(0,dim.height));
    CGPoint newPos = ccpAdd(position_, delta);

    // finally set the position data
    [super setPosition:newPos];      
}

// capture modification calls to adjust position
- (void)onEnter {
    [self fixPosition];
    [super onEnter];
}

- (void)setParent:(CCNode *)parent {
    [super setParent:parent];
    [self fixPosition];
}

- (void)setString:(NSString *)str {
    [super setString:str];
    [self fixPosition];
}

- (void)setPosition:(CGPoint)position {
    intendedPosition_ = position;
    [self fixPosition];
}

-(void)setRotation:(float)rotation {
    [super setRotation:rotation];
    [self fixPosition];
}

@end

Automatically Create Icons up to iPad Retina Resolution

September 11th: Updated for iOS 7  

With iPhone and iPad retina and non-retina resolutions, there is a bunch of  icons you need to create if you need to support all of them. There is probably tons of artwork you need to fix, and nobody can help you with that. But here is a little support for you to at least have your icons correctly and easily in place.

If you are developing an app for the iPhone/iPod or the iPad, or both as universal app, there is the Apple bible for what you need to provide as icon formats:

  • App icon for the iPhone in 57×57 px
  • App icon for the iPad in 72×72 px
  • Settings & search result icon for the iPhone/iPod/iPad in 29×29 px
  • Search result icon for the iPad in 50×50 px
  • Added September 11th: For iOS 7, you will also need icons sized 120×120 px and 80×80 px (iPhone), plus 76×76 px/152×152 px and 40×40 px/80×80 px (iPad). Nowadays the names of the icon files don’t really matter any longer, but I kept the old name schema for them.

These are all formats that are supported from iOS 7 on:

icons

For retina displays, you need to provide each of them with the quadruple resolution as @2x variants (well, the last two are somewhat optional, but since we are automating it anyway now…), plus a new retina format to be future proof. So you end up with 9 icon files. What you probably usually do is take your large master artwork and then save all the required resolutions. This is quite a tedious hassle, since for the sake of supporting older devices you should stick to certain naming conventions, and doing all the resizing and renaming manually can be pretty error prone.

I created a small Automator action that takes my large 1024×1024 master artwork as input and creates all required files from it. Just drag your input file to the IconCreator.app automation tool, and it will create a folder Icons next to it containing all the required files, plus a 512×512 iTunesArtwork file (note: no extension!) that is if you distribute adHoc IPAs to testers, so that they don’t just see white squares for your app in iTunes. The Apple guidelines also demand a corresponding @2x retina file, so this will also be created. These itunesArtwork files should not be included in the final AppStore upload.

The input can actually be larger or smaller than that, but smaller is not recommended (since at least the iTunesArtwork will get pixelated then). After all, you’ll anyhow need the 1024x1024px format to upload to iTunes Connect. If you don’t drag a file to the droplet, but start it directly, a file-selector will ask you to select the input graphics.

You can download the latest version 1.3 of the automation task right here.

After adding the resulting icon files to your project, the only thing that is then still left to be done is adding them to your CFBundleIcons(~ipad) key in the info.plist file. Just copy the following entries and add them to your info.plist using a regular text editor. You can just insert it at an appropriate position between any two other entries (i.e. right before any existing <key>…</key> entry):

<key>CFBundleIconFiles</key>
 <array>
  <string>Icon-40</string>
  <string>Icon-60</string>
  <string>Icon.png</string>
  <string>Icon@2x.png</string>
  <string>Icon-72.png</string>
  <string>Icon-72@2x.png</string>
  <string>Icon-Small-50.png</string>
  <string>Icon-Small-50@2x.png</string>
  <string>Icon-Small.png</string>
  <string>Icon-Small@2x.png</string>
 </array>

<key>CFBundleIconFiles~ipad</key>
 <array>
  <string>Icon-40</string>
  <string>Icon-76</string>
  <string>Icon-60</string>
  <string>Icon.png</string>
  <string>Icon@2x.png</string>
  <string>Icon-72.png</string>
  <string>Icon-72@2x.png</string>
  <string>Icon-Small-50.png</string>
  <string>Icon-Small-50@2x.png</string>
  <string>Icon-Small.png</string>
  <string>Icon-Small@2x.png</string>
 </array>

With newer XCode versions, you can also add the icons manually to the individual requested formats by simply dragging them into their places.

Modal Alerts for Cocos2D

Cocos2D is insanely great for game development, and a lot of otherwise complex or tedious gaming tasks are almost no-brainers here. The one thing, however, that I found less straightforward, is bringing up small alerts to ask for some user confirmation, while at the same time blocking the rest of the application.

There are actually tons of bits and pieces around that solve the one or the other part of it, but no one-shot solution as I wanted it to be. What I was looking for is:

  • Small alert-box with custom design fitting to the overall game look-and-feel
  • Alike support for iPad and iPhone, incl. retina
  • Predefined “Ask”, “Confirm” and “Tell” type of alerts that can be easily called from within the code
  • Modal operation, i.e. lock and dim the game scene and ideally have a no-hassle modal kind of control-flow within the calling code without the need for additional callbacks that make the code hard to read from there on
  • Fire-and-forget operation without need for any further cleanup
  • Nice animations

So what I finally came up with is actually a bit more powerful and complex than what I want to present here. But this stripped-down version is a great start to do your own further customizations, and it is fully functional and can be integrated right away. You just ask for the dialog to be brought up with your own message, and you’re done. Code that should be executed when pressing a button is written inline as a block, thus being kind of a modal control-flow schema. And if you want code to be executed right away, just put it directly after the statement itself. Asking the user for a simple confirmation is as easy as this:

[ModalAlert Ask: @"Do you want to cancel the game?"
        onLayer:self // this is usually the current scene layer
       yesBlock: ^{
           // code that should be executed on "yes" goes here 
           ...
           }
        noBlock: ^{
           // code that should be executed on "no" goes here
           ...
           }];
// control flow proceeds here right away (non-modal) ...

That’s all. Everything is encapsulated into simple class-functions and requires no allocations or anything on the caller side. There is a predefined call for a Yes/No question, an OK/Cancel confirmation and a Tell-function that is just to be confirmed with an OK-button.

In principle a semi-transparent black color layer is created that covers the whole current scene and swallows all touches, except those that should go to the dialog sprite and the buttons on it. The dialogbox itself and the button are images that fit for your purpose, and you can design them any way you like (color and size). For this sample, I provided three different sizes for iPhone retina and non-retina, and an extra one for the iPad. The non-retina size would be too small for the iPad, and HD resolution too large and would look clumsy. So from the HD retina resolution, the iPad is sized 70% of it, and the SD non-retina is as usual 50%. If you change the size you might also need to tweak some of the sizings, however they are relative to the dialog box size and should fit at least as long as you approximately retain the height/width relation.

Beside the fact that you now have a fully functional base to start from for the ultimate and universal alerting library, you can take away some interesting things from the code, if you are new to Cocos2D or iPhone programming in general, like animations and block programming (which BTW is only available starting with iOS 4).

Where could you go from here? Include localization for the predefined buttons, provide more formats, provide a function where you can also customize the button texts, center the message text vertically, draw all boxes dynamically using OpenGL (instead of having to provide images), include a nice image in the dialog box to make it more friendly, etc…there is tons you could elaborate on. But probably you won’t even need much more.

So here is the coding of the ModalAlert class. I leave out the *.h file here, because it is pretty unspectacular. All the code files and the required images for the sample can be downloaded as ZIP file to get you jump-started.

 
// ModalAlert.m
// 
// Created by Hans-Juergen Richstein
// Copyright 2012 ROMBOS. All rights reserved. 

#import "ModalAlert.h"
#import "cocos2d.h"

// constants
#define kDialogTag 1234
#define kAnimationTime 0.4f
#define kDialogImg @"dialogBox.png"
#define kButtonImg @"dialogButton.png"
#define kFontName @"MarkerFelt-Thin"

// class that implements a black colored layer that will cover the whole screen 
// and eats all touches except within the dialog box child 
@interface CoverLayer : CCLayerColor {
}
@end
@implementation CoverLayer
- (id)init {
    self = [super init];
    if (self) {
        [self initWithColor:ccc4(0,0,0,0)
                      width:[CCDirector sharedDirector].winSize.width
                     height:[CCDirector sharedDirector].winSize.height];
        self.isTouchEnabled = YES;
    }
    return self;
}

- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    CGPoint touchLocation = [self convertTouchToNodeSpace: touch];
    CCNode *dialogBox = [self getChildByTag: kDialogTag];

    // eat all touches outside of dialog box
    return !CGRectContainsPoint(dialogBox.boundingBox, touchLocation);
}

- (void) registerWithTouchDispatcher {
    [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:INT_MIN+1 swallowsTouches:YES];
}
@end

// the actual ModalAlert implemenation class
@implementation ModalAlert

// this is called to gracefully close the alert, cleanup everything
// and call the according block belonging to the pressed button
+ (void) CloseAlert: (CCSprite*) alertDialog onCoverLayer: (CCLayer*) coverLayer executingBlock: (void(^)())block {
    // shrink dialog box 
    [alertDialog runAction:[CCScaleTo actionWithDuration:kAnimationTime scale:0]];

    // in parallel, fadeout and remove cover layer and execute block 
    // (note: you can't use CCFadeOut since we don't start at opacity 1!) 
    [coverLayer runAction:[CCSequence actions:
                           [CCFadeTo actionWithDuration:kAnimationTime opacity:0],
                           [CCCallBlock actionWithBlock:^{
                                [coverLayer removeFromParentAndCleanup:YES];
                                if (block) block();
                            }],
                           nil]];
}

+ (void) ShowAlert: (NSString*) message onLayer: (CCLayer *) layer
          withOpt1: (NSString*) opt1 withOpt1Block: (void(^)())opt1Block
           andOpt2: (NSString*) opt2 withOpt2Block: (void(^)())opt2Block {

    // create the cover layer that "hides" the current application     
    CCLayerColor *coverLayer = [CoverLayer new];
    [layer addChild:coverLayer z:INT_MAX]; // put to the very top to block application touches
    [coverLayer runAction:[CCFadeTo actionWithDuration:kAnimationTime opacity:80]]; // smooth fade-in to dim with semi-transparency

    // open the dialog 
    CCSprite *dialog = [CCSprite spriteWithFile:kDialogImg];
    dialog.tag = kDialogTag;
    dialog.position = ccp(coverLayer.contentSize.width/2, coverLayer.contentSize.height/2);
    dialog.opacity = 220; // make it a bit transparent for a cooler look

    // add the alert text
    CGSize msgSize = CGSizeMake(dialog.contentSize.width * 0.9, dialog.contentSize.height * 0.55);
    float fontSize = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)?42:30;

    CCLabelTTF *dialogMsg = [CCLabelTTF labelWithString:message dimensions: msgSize alignment:UITextAlignmentCenter
                                               fontName:kFontName fontSize:fontSize];
    dialogMsg.position = ccp(dialog.contentSize.width/2, dialog.contentSize.height * 0.6);
    dialogMsg.color = ccBLACK;
    [dialog addChild: dialogMsg]; 

    // add one or two buttons, as needed
    CCMenuItemSprite *opt1Button = [CCMenuItemSprite itemFromNormalSprite:[CCSprite spriteWithFile: kButtonImg]
                                                             selectedSprite:[CCSprite spriteWithFile: kButtonImg]
                                                                    block:^(id sender){
                                                                        // close alert and call opt1block when first button is pressed                                                                         [self CloseAlert:dialog onCoverLayer: coverLayer executingBlock:opt1Block];
                                                                    } ];
    opt1Button.position = ccp(dialog.textureRect.size.width * (opt2 ? 0.27f:0.5f), opt1Button.contentSize.height * 0.8f);

    CCLabelTTF *opt1Label = [CCLabelTTF labelWithString:opt1 dimensions:opt1Button.contentSize alignment:UITextAlignmentCenter
                                               fontName:kFontName fontSize:fontSize];
    opt1Label.anchorPoint = ccp(0, 0.1);
    opt1Label.color = ccBLACK;
    [opt1Button addChild: opt1Label];    

    // create second button, if requested
    CCMenuItemSprite *opt2Button = nil;
    if (opt2) {
        opt2Button = [CCMenuItemSprite itemFromNormalSprite:[CCSprite spriteWithFile: kButtonImg]
                                             selectedSprite:[CCSprite spriteWithFile: kButtonImg]
                                                      block:^(id sender){
                                                          // close alert and call opt2block when second button is pressed
                                                          [self CloseAlert:dialog onCoverLayer: coverLayer executingBlock:opt2Block];
                                                      } ];

        opt2Button.position = ccp(dialog.textureRect.size.width * 0.73f, opt1Button.contentSize.height * 0.8f);

        CCLabelTTF *opt2Label = [CCLabelTTF labelWithString:opt2 dimensions:opt2Button.contentSize alignment:UITextAlignmentCenter
                                                   fontName:kFontName fontSize:fontSize];
        opt2Label.anchorPoint = ccp(0, 0.1);
        opt2Label.color = ccBLACK;
        [opt2Button addChild: opt2Label];
    }

    // add the menu to the dialog
    CCMenu *menu = [CCMenu menuWithItems:opt1Button, opt2Button, nil];
    menu.position = CGPointZero;

    [dialog addChild:menu];
    [coverLayer addChild:dialog];

    // open the dialog with a nice popup-effect 
    dialog.scale = 0;
    [dialog runAction:[CCEaseBackOut actionWithAction:[CCScaleTo actionWithDuration:kAnimationTime scale:1.0]]];
}

+ (void) Ask: (NSString *) question onLayer: (CCLayer *) layer yesBlock: (void(^)())yesBlock noBlock: (void(^)())noBlock {
    [self ShowAlert:question onLayer:layer withOpt1:@"Yes" withOpt1Block:yesBlock andOpt2:@"No" withOpt2Block:noBlock];
}

+ (void) Confirm: (NSString *) question onLayer: (CCLayer *) layer okBlock: (void(^)())okBlock cancelBlock: (void(^)())cancelBlock {
    [self ShowAlert:question onLayer:layer withOpt1:@"Ok" withOpt1Block: okBlock andOpt2:@"Cancel" withOpt2Block:cancelBlock];
}

+ (void) Tell: (NSString *) statement onLayer: (CCLayer *) layer okBlock: (void(^)())okBlock {
    [self ShowAlert:statement onLayer:layer withOpt1:@"Ok" withOpt1Block: okBlock andOpt2:nil withOpt2Block:nil];
}

@end

“A textbook case of developer greed”: the curse of one-way AppStore communication

AppStore reviews are often the only way to get any feedback from app users. They rarely use e-mail for communication with the makers of an app, so as a developer you can just listen to what people think of your app. And sometimes it really requires a good portion of stoicism to stand what you read, being unable to provide any reply to that person, or to the public in general that will also read it.

These days I was really heart-stricken by one comment for Dash Race in the US AppStore. It was titled “A textbook case of developer greed” and made this statement:

Pay 99 cents for a pretty okay game, sure! But then pay 99 more cents for the right to import specific developer-supplied graphics into the game? And the sum of all those “track packs” costing far more than the base price of the game itself? I feel like I just paid for the right to pay more, hyuck!

And this was worth 2 stars. So for those that don’t know it, in Dash Race people can buy additional tracks through in-app purchases. Each individual pack of two tracks is $0.99, and the biggest collection, containing all 9 additionally available tracks, costs $2.99. Not that you would really need them to have a lot of fun with the 10 tracks included in Dash Race, but if you really get into it, you might want all of them for additional challenges. Anyway, all together it would sum up to around an incredible $5. So as for the internationally valid comparison mark of a menu at a burger chain with a big “M”, this would hardly buy you a burger, soft drink and fries anywhere in the world. I suppose that the author works for no salary at all and feeds his family by growing potatoes and salad in his garden. Anything else would be pure greed, right? How I would love to discuss that with him…

There is more where you would really like to talk to the people. “Can’t download app…” was one of those, worth 1 star. For sure not my fault, and I would have been glad to discuss the potential glitch of the AppStore that broke the download after taking the money, and what to do now about it to get what he paid for. The same for “Can’t download tracks — unhappy…” → 2 stars. “Great game, would even be better with a multiplayer option” — aaaargh, it HAS multiplayer, of course. How can I tell her?

And less bad, but also with a questionable motivation, are those 4 star ratings that on the one hand side tell you how great your app is (which is highly appreciated, of course 😉 ), but then they keep one star that you will only get once you implement one or the other great idea that this user just had. “The fifth star is awarded once you introduce feature x“, “I’ll give you five as soon as you build ‘y’ for me“. I think it is still ok if a developer left out something obvious (“I’ll give 5 stars once your game gets Gamecenter integration“), but seemingly people think that $0.99 buys them a ticket for an additional wishlist of their personal ideas. I am glad for any input for additional features and consider it of course for future updates if it makes sense, but I won’t do it just for an additional star by someone. For some reason, this actually rather demotivates me to consider it…not sure how other developers think about that and whether that had ever worked in any case. It would be much more motivating to discuss such stuff with users, rather than feeling blackmailed after having already delivered something that is already well worth the ridiculously low price. There is always an e-mail contact either usually available in the app itself, or in the AppStore!

If I would have one big wish that Apple could fulfill to make my life easier, then it would be a way of answering to an AppStore review. It could well be completely anonymous (Apple just takes my comment and routes it to the reviewer, who can then answer or simply ignore it), but please, PLEASE let me reply to some of them, and of course for all stores worldwide. At least it would relief some of the pain of just being able to listen…

The Secrets of the Dash Race Challenge Mode Scoring

Here is the short version for the impatient reader: if you believe in conspiracy once you lost a Dash Race, specifically against a computer controlled opponent, then the classic mode is the better choice for you: the first one passing the finish line wins. Straightforward classic formula. Just choose it in the start screen and there you go.

The challenge mode, however, adds a scoring system on top of the classic game. And this scoring system has its own dynamics, and this also implies that you sometimes won’t win though you made it first to the finish. In this blog I will reveal some (but not all!) of the mechanics behind it to make you understand who wins and who looses, and how you can shape up to be on the winner side more often than not (provided you are not driving against someone else who read this as well).

Points are awarded for every move you make. If it’s a faint-hearted short move that took you ages to complete, you’ll get far less than for a bold speedy (i.e. long) move that you quickly decided upon. Let’s call cooler moves better for now: faster, quick completion, more impact and bothering your opponents. The score is over-proportionally higher for such better moves. Two times two squares will score less than one four square move.

Additionally, you are awarded points for tricks. Top speeds, (mean) blocks, cool curves, square dances, (scary) scratches and more: each will provide an uplift on the score for that move. Doing a trick with a fast (long) move will provide much more points than with slow ones.

The absolute score per move is computed relatively to a regular default length of the track. If it’s a long track, then individual scores are lower, but you score more of them. As a result, it doesn’t matter whether you drive on Arkas or do a Boat Race. In both cases you can end up with a similar score reflecting your actual driving capabilities and nerve strength. Once you exceed the regular length of a track, scores are drastically reduced, eventually being zero. So it doesn’t help to do small circles in front of the finish line to gain more points: you’ll get almost no additional points, and you are risking to be disqualified after exceeding the regular track length for an (undisclosed) amount. Instead, heading as fast as possible for the finish line (and at best hitting your starting position or the dedicated sweet spot on tracks with a finish different from the start) will give you a boost bonus that you shouldn’t miss to become the final winner. It is so high that in approx. 80% of the cases this alone will secure your golden cup. So how is the bonus computed? It is the sum of a fixed percentage of the current score of each opponent that is still behind you. The reasoning is that you get a higher bonus if the others fare good, too. If it’s a hot race with good drivers close behind you, then you’ll be awarded more points for the mental strength to surpass them all. If you are winning against a bunch of loosers, you get less bonus, because that’s not a big deal then anyway. Bottom line: the chance for a high score rises with the number of skilled opponents. Which is fair, because it’s much harder to prevail in a 4 player race than with just one opponent.

Sometimes people believe the robots somehow cheat, especially if they win though they didn’t even come in first. Cheating robots, however, is a fairy tale, at least for Dash Race. Technically, robots do their move like any other player. The whole scoring afterwards is done completely independent of who performed the move, human or robot. But robots are not performing bad in general — what else should they do the whole day being locked into that game? The key here is speed. The timing (i.e. how fast you complete a move) is key for the score you are awarded each move. While the robots do their move instantaneously to not bore you, they are accounted for a real-life amount of time for the move (anything else would be pretty unfair, right?). It is a random timing around an average that is oriented along the timing of all participating human players. If one human is fast and one is slow, robots are somewhere in between on average. If all are fast, robots will keep up. There is however a limit to the fairness of the robots (in the end they are a bit human, too…). If you are faring very poor (i.e. do slow moves only), their patience is used up and they will leave you behind. Where this limit is depends on the driving skills that you chose for the robots. Bad robots will align with lower reaction speeds, but Vettel-type of AI drivers won’t care for you if you take your time on moves, maybe drinking a relaxed tea at the same time. If this is what you want, then read the first paragraph again.

In the aftermath of a game, look at the statistics of each participant on the winner screen. Probably the one bringing the golden cup home has an excellent average move time (better than yours). Then click into the details screen of the winner and look what additional trick scores he achieved. Compare that with yours. Probably you then get some insight why you are not on the top of the winner stand.

Don’t get depressed if you won’t win all the time. After all it’s just a game!