The scoring in this game needs a display. Up to this point you can only see the score in the XCode console window.
The scoring data you are tracking is the number of attempts and the number of hits. The number of attempts is a class property that increments each time a new moving target is started from the top of the screen.
The number of hits is incremented each time the player intercepts the moving target.
Using the number of attempts and number of hits, we can display three values. One is the total attempts. The second is the number of misses which is the number of hits minus the number of misses. The third value is the number of hits.
We will just display them at the top middle of the screen with a colon separating each value.
For the score display, we will use a graphic containing the exact characters we need. Those characters are the digits 0 to 9, a space and a colon.
To display the graphic characters you will use the Cocos2D CCLabelBMFont class. You can pack all the characters into one file. Then the CCLabelBMFont treats each character like a CCSprite. This means that each individual character can be rotated, scaled, translated, and tinted like other Cocos2D CCSprite objects. We do not have much more of a need other than to place them on the screen and layer them underneath the game pieces.
Glyph Designer is the tool I use to create the files needed for CCLabelBMFont. There is a fee for Glyph Designer. I have not used another tool like it but the $30 US was a great value and the online tutorial videos made getting use out of it a snap. Plus Cocos2D is a product Glyph Designer supports.
Lesson Downloads
- Game piece images, score glyph files including Glyph Designer project file and sound for project.
- IPhone images for project. Icons and splash screen.
- Completed Project. This is built in Kobold2d 1.0.1.
Step 1 – Build an Empty-Project Template
You can continue with the Lesson 6 project and make changes noted here.
Otherwise the steps for creating this project from an Empty-Project template are the same as Lesson 1 except to substitute Lesson 7 for Lesson 1 in the project name and to include the red_ball.png, red_ball-hd.png and explosion.caf files when you add the game pieces. The explosion.caf goes in same group as the images. Then you can use the code presented here.
In either case your Projectfiles->Resources group should have the files shown in the image to the left.
[ad name=”Google Adsense”]
Step 2 – Set Properties in config.lua
There are no new configuration properties from Lesson 6. Complete config.lua file is included here for your copy convenience.
--[[ * Kobold2D™ --- http://www.kobold2d.org * * Copyright (c) 2010-2011 Steffen Itterheim. * Released under MIT License in Germany (LICENSE-Kobold2D.txt). --]] --[[ * Need help with the KKStartupConfig settings? * ------ http://www.kobold2d.com/x/ygMO ------ --]] local config = { KKStartupConfig = { -- load first scene from a class with this name, or from a Lua script with this name with .lua appended FirstSceneClassName = "GameLayer", -- set the director type, and the fallback in case the first isn't available DirectorType = DirectorType.DisplayLink, DirectorTypeFallback = DirectorType.NSTimer, MaxFrameRate = 60, DisplayFPS = YES, EnableUserInteraction = YES, EnableMultiTouch = NO, -- Render settings DefaultTexturePixelFormat = TexturePixelFormat.RGBA8888, GLViewColorFormat = GLViewColorFormat.RGB565, GLViewDepthFormat = GLViewDepthFormat.DepthNone, GLViewMultiSampling = NO, GLViewNumberOfSamples = 0, Enable2DProjection = NO, EnableRetinaDisplaySupport = YES, EnableGLViewNodeHitTesting = NO, EnableStatusBar = NO, -- Orientation & Autorotation DeviceOrientation = DeviceOrientation.LandscapeLeft, AutorotationType = Autorotation.None, ShouldAutorotateToLandscapeOrientations = NO, ShouldAutorotateToPortraitOrientations = NO, AllowAutorotateOnFirstAndSecondGenerationDevices = NO, -- Ad setup EnableAdBanner = NO, PlaceBannerOnBottom = YES, LoadOnlyPortraitBanners = NO, LoadOnlyLandscapeBanners = NO, AdProviders = "iAd, AdMob", -- comma seperated list -> "iAd, AdMob" means: use iAd if available, otherwise AdMob AdMobRefreshRate = 15, AdMobFirstAdDelay = 5, AdMobPublisherID = "YOUR_ADMOB_PUBLISHER_ID", -- how to get an AdMob Publisher ID: http://developer.admob.com/wiki/PublisherSetup AdMobTestMode = YES, -- Mac OS specific settings AutoScale = NO, AcceptsMouseMovedEvents = NO, WindowFrame = RectMake(1024-640, 768-480, 640, 480), EnableFullScreen = NO, }, } return config
Step 3 – GameLayer.h
Line 18 shows the CCLabelBMFont object for displaying the score.
/* * Kobold2D™ --- http://www.kobold2d.org * * Copyright (c) 2010-2011 Steffen Itterheim. * Released under MIT License in Germany (LICENSE-Kobold2D.txt). */ #import "kobold2d.h" @interface GameLayer : CCLayer { CCSprite* player; CGPoint playerVelocity; CCSprite* movingTarget; float movingTargetMoveDuration; int totalAttempts; int totalHits; CCLabelBMFont* scoreLabel; } @end
Step 4 – GameLayer.m: Add initScore and setScore Methods for Score Display
Complete GameLayer.m file is included here for your copy convenience.
/* * Kobold2D™ --- http://www.kobold2d.org * * Copyright (c) 2010-2011 Steffen Itterheim. * Released under MIT License in Germany (LICENSE-Kobold2D.txt). */ #import "GameLayer.h" #import "SimpleAudioEngine.h" @interface GameLayer (PrivateMethods) -(void) initMovingTarget; -(void) movingTargetUpdate:(ccTime)delta; -(void) startMovingTargetSequence; -(void) endMovingTargetSequence; -(void) checkForCollision; -(void) initScore; -(void) setScore; @end // Velocity deceleration const float deceleration = 0.4f; // Accelerometer sensitivity (higher = more sensitive) const float sensitivity = 6.0f; // Maximum velocity const float maxVelocity = 100.0f; @implementation GameLayer -(id) init { if ((self = [super init])) { // Enable accelerometer input events. [KKInput sharedInput].accelerometerActive = YES; [KKInput sharedInput].acceleration.filteringFactor = 0.2f; // Preload the sound effect into memory so there's no delay when playing it the first time. [[SimpleAudioEngine sharedEngine] preloadEffect:@"explosion.caf"]; // Initialize the total attempts to hit a moving target. totalAttempts = 0; // Initialize the total hits totalHits = 0; // Graphic for player player = [CCSprite spriteWithFile:@"green_ball.png"]; [self addChild:player z:0 tag:1]; // Position player CGSize screenSize = [[CCDirector sharedDirector] winSize]; float imageHeight = [player texture].contentSize.height; player.position = CGPointMake(screenSize.width / 2, imageHeight / 2); glClearColor(0.1f, 0.1f, 0.3f, 1.0f); // First line of title CCLabelTTF* label = [CCLabelTTF labelWithString:@"Kobold2d Intro Tutorial" fontName:@"Arial" fontSize:30]; label.position = [CCDirector sharedDirector].screenCenter; label.color = ccCYAN; [self addChild:label z:-1]; // Second line of title CCLabelTTF* label2 = [CCLabelTTF labelWithString:@"Lesson 7" fontName:@"Arial" fontSize:24]; label2.color = ccCYAN; label2.position = CGPointMake([CCDirector sharedDirector].screenCenter.x ,label.position.y - label.boundingBox.size.height); [self addChild:label2 z:-1]; // The score label initialized [self initScore]; // Add below game action [self addChild:scoreLabel z:-1]; // Seed random number generator srandom((UInt32)time(NULL)); // Initialize our moving target. [self initMovingTarget]; // Start animation - the update method is called. [self scheduleUpdate];; } return self; } -(void) dealloc { #ifndef KK_ARC_ENABLED [super dealloc]; #endif // KK_ARC_ENABLED } #pragma mark Player Movement -(void) acceleratePlayerWithX:(double)xAcceleration { // Adjust velocity based on current accelerometer acceleration playerVelocity.x = (playerVelocity.x * deceleration) + (xAcceleration * sensitivity); // Limit the maximum velocity of the player sprite, in both directions (positive & negative values) if (playerVelocity.x > maxVelocity) { playerVelocity.x = maxVelocity; } else if (playerVelocity.x < -maxVelocity) { playerVelocity.x = -maxVelocity; } } #pragma mark update -(void) update:(ccTime)delta { // Gain access to the user input devices / states KKInput* input = [KKInput sharedInput]; [self acceleratePlayerWithX:input.acceleration.smoothedX]; // Accumulate up the playerVelocity to the player's position CGPoint pos = player.position; pos.x += playerVelocity.x; // The player constrainted to inside the screen CGSize screenSize = [[CCDirector sharedDirector] winSize]; // Half the player image size player sprite position is the center of the image float imageWidthHalved = [player texture].contentSize.width * 0.5f; float leftBorderLimit = imageWidthHalved; float rightBorderLimit = screenSize.width - imageWidthHalved; // Hit left boundary if (pos.x < leftBorderLimit) { pos.x = leftBorderLimit; // Set velocity to zero playerVelocity = CGPointZero; } // Hit right boundary else if (pos.x > rightBorderLimit) { pos.x = rightBorderLimit; // Set velocity to zero playerVelocity = CGPointZero; } // Move the player player.position = pos; // Collision check [self checkForCollision]; } #pragma mark MovingTarget -(void) initMovingTarget { NSLog(@"initMovingTarget"); // This is the image movingTarget = [CCSprite spriteWithFile:@"red_ball.png"]; // Add CCSprite for movingTarget [self addChild:movingTarget z:0 tag:2]; // Set the starting position and start movingTarget play sequence [self startMovingTargetSequence]; movingTargetMoveDuration = 4.0f; // Unschedule the selector just in case. If it isn't scheduled it won't do anything. [self unschedule:@selector(movingTargetUpdate:)]; // Schedule the movingTarget update logic to run at the given interval. [self schedule:@selector(movingTargetUpdate:) interval:0.1f]; } -(void) startMovingTargetSequence { NSLog(@"startMovingTargetSequence"); // Update score to display increment in totalAttempts [self setScore]; // Increment total attempts to hit a moving target. totalAttempts ++; // Get the window size CGSize screenSize = [[CCDirector sharedDirector] winSize]; // Get the image size CGSize imageSize = [movingTarget texture].contentSize; // Generate a random x starting position with offsets for center registration point. int randomX = CCRANDOM_0_1() * (screenSize.width / imageSize.width); movingTarget.position = CGPointMake(imageSize.width * randomX + imageSize.width * 0.5f, screenSize.height + imageSize.height); // Schedule the movingTarget update logic to run at the given interval. [self schedule:@selector(movingTargetUpdate:) interval:0.1f]; } -(void) movingTargetUpdate:(ccTime)delta { // CCSprite->CCNode no sequence of actions running. if ([movingTarget numberOfRunningActions] == 0) { NSLog(@"movingTargetUpdate"); // Determine below screen position. CGPoint belowScreenPosition = CGPointMake(movingTarget.position.x, - ( [movingTarget texture].contentSize.height)); // CCAction to move a CCNode object to the position x,y based on position. CCMoveTo* moveEnd = [CCMoveTo actionWithDuration:movingTargetMoveDuration position:belowScreenPosition]; // Call back function for the action. CCCallFuncN* callEndMovingTargetSequence = [CCCallFuncN actionWithTarget:self selector:@selector(endMovingTargetSequence)]; // Create a sequence, add the actions: the moveEnd CCMoveTo and the call back function for the end position. CCSequence* sequence = [CCSequence actions:moveEnd,callEndMovingTargetSequence, nil]; // Run the sequence. [movingTarget runAction:sequence]; } } -(void) endMovingTargetSequence { NSLog(@"endMovingTargetSequence"); [movingTarget stopAllActions]; // Terminate running the moveTargetUpdate interval. [self unschedule:@selector(movingTargetUpdate:)]; // Decrease the moving target move duration to increase the speed. movingTargetMoveDuration -= 0.1f; // Moving target move duration is below 2 then hold at 2. if (movingTargetMoveDuration < 2.0f) { movingTargetMoveDuration = 2.0f; } NSLog(@"movingTargetMoveDuration: %f",movingTargetMoveDuration); // Set the starting position and start movingTarget play sequence [self startMovingTargetSequence]; } #pragma mark Collision Check -(void) checkForCollision { // Size of the player and target. Both are assumed squares so width suffices. float playerImageSize = [player texture].contentSize.width; float targetImageSize = [movingTarget texture].contentSize.width; // Compute their radii. Tweak based on drawing. float playerCollisionRadius = playerImageSize *.4; float targetCollisionRadius = targetImageSize *.4; // This collision distance will roughly equal the image shapes. float maxCollisionDistance = playerCollisionRadius + targetCollisionRadius; // Distance between two points. float actualDistance = ccpDistance(player.position, movingTarget.position); // Are the two objects closer than allowed? if (actualDistance < maxCollisionDistance) { // Play a sound effect [[SimpleAudioEngine sharedEngine] playEffect:@"explosion.caf"]; totalHits++; NSLog(@"HIT! Total attempts: %i. Total hits: %i", totalHits, totalAttempts); [self setScore]; [self endMovingTargetSequence]; } } #pragma mark score -(void) initScore { CGSize screenSize = [[CCDirector sharedDirector] winSize]; // Load font information. scoreLabel = [CCLabelBMFont labelWithString:@"+0 : 0" fntFile:@"score_glyphs.fnt"]; scoreLabel.position = CGPointMake(screenSize.width / 2, screenSize.height); // Set anchorPoint y position to align with the top of screen. scoreLabel.anchorPoint = CGPointMake(0.5f, 1.0f); } -(void) setScore { [scoreLabel setString:[NSString stringWithFormat:@"%i : %i : %i", totalAttempts,totalAttempts-totalHits, totalHits]]; } @end
Two private methods initScore and setScore are declared on lines 17 and 18.
/* * Kobold2D™ --- http://www.kobold2d.org * * Copyright (c) 2010-2011 Steffen Itterheim. * Released under MIT License in Germany (LICENSE-Kobold2D.txt). */ #import "GameLayer.h" #import "SimpleAudioEngine.h" @interface GameLayer (PrivateMethods) -(void) initMovingTarget; -(void) movingTargetUpdate:(ccTime)delta; -(void) startMovingTargetSequence; -(void) endMovingTargetSequence; -(void) checkForCollision; -(void) initScore; -(void) setScore; @end
The CCLabelBMFont could be declared in the init method. I choose a separate method to stop the growth of the init method and also organize related coded into reusable pieces.
That called for getting the screen center again so the score can be centered.
The score_glyphs.fnt file is human readable and contains the information CCLabelBMFont needs to break out the images in score_glyphs.png. Both score_glyphs.fnt and score_glyphs.png where exported from Glyph Designer.
Line 235 sets up the storing score so the score_glyphs.png is cached.
The anchorPoint property is a convenient way to offset the y position. The Cocos2d documentation recommends not to change the defaults: “All inner characters are using an anchorPoint of (0.5f, 0.5f) and it is not recommend to change it because it might affect the rendering”. However it works so we will use it.
Line 242 displays the three score values of totalAttempts, the number of misses and the totalHits.
#pragma mark score -(void) initScore { CGSize screenSize = [[CCDirector sharedDirector] winSize]; // Load font information. scoreLabel = [CCLabelBMFont labelWithString:@"0 : 0 : 0" fntFile:@"score_glyphs.fnt"]; scoreLabel.position = CGPointMake(screenSize.width / 2, screenSize.height); // Set anchorPoint y position to align with the top of screen. scoreLabel.anchorPoint = CGPointMake(0.5f, 1.0f); } -(void) setScore { [scoreLabel setString:[NSString stringWithFormat:@"%i : %i : %i", totalAttempts,totalAttempts-totalHits, totalHits]]; }
[ad name=”Google Adsense”]
Step 5 – GameLayer.m: Initialize the Score in the init Method
On line 66 the score added to the game layer.
On lines 57 and 64 the game information overlay is moved back so the moving target appears over them.
Line 59 updates the subtitle which helps keep track what we are looking at when testing.
-(id) init { if ((self = [super init])) { // Enable accelerometer input events. [KKInput sharedInput].accelerometerActive = YES; [KKInput sharedInput].acceleration.filteringFactor = 0.2f; // Preload the sound effect into memory so there's no delay when playing it the first time. [[SimpleAudioEngine sharedEngine] preloadEffect:@"explosion.caf"]; // Initialize the total attempts to hit a moving target. totalAttempts = 0; // Initialize the total hits totalHits = 0; // Graphic for player player = [CCSprite spriteWithFile:@"green_ball.png"]; [self addChild:player z:0 tag:1]; // Position player CGSize screenSize = [[CCDirector sharedDirector] winSize]; float imageHeight = [player texture].contentSize.height; player.position = CGPointMake(screenSize.width / 2, imageHeight / 2); glClearColor(0.1f, 0.1f, 0.3f, 1.0f); // First line of title CCLabelTTF* label = [CCLabelTTF labelWithString:@"Kobold2d Intro Tutorial" fontName:@"Arial" fontSize:30]; label.position = [CCDirector sharedDirector].screenCenter; label.color = ccCYAN; [self addChild:label z:-1]; // Second line of title CCLabelTTF* label2 = [CCLabelTTF labelWithString:@"Lesson 7" fontName:@"Arial" fontSize:24]; label2.color = ccCYAN; label2.position = CGPointMake([CCDirector sharedDirector].screenCenter.x ,label.position.y - label.boundingBox.size.height); [self addChild:label2 z:-1]; // The score label initialized [self initScore]; // Add below game action [self addChild:scoreLabel z:-1]; // Seed random number generator srandom((UInt32)time(NULL)); // Initialize our moving target. [self initMovingTarget]; // Start animation - the update method is called. [self scheduleUpdate];; } return self; }
Step 6 – GameLayer.m: Update Score For Misses
Line 155 updates the score before incrementing the number of attempts on line 157. If we do it after, the misses would show a value of 1 more than actual each time a new moving target it put into play.
-(void) startMovingTargetSequence { NSLog(@"startMovingTargetSequence"); // Update score to display increment in totalAttempts [self setScore]; // Increment total attempts to hit a moving target. totalAttempts ++; // Get the window size CGSize screenSize = [[CCDirector sharedDirector] winSize]; // Get the image size CGSize imageSize = [movingTarget texture].contentSize; // Generate a random x starting position with offsets for center registration point. int randomX = CCRANDOM_0_1() * (screenSize.width / imageSize.width); movingTarget.position = CGPointMake(imageSize.width * randomX + imageSize.width * 0.5f, screenSize.height + imageSize.height); // Schedule the movingTarget update logic to run at the given interval. [self schedule:@selector(movingTargetUpdate:) interval:0.1f]; }
Step 7 – GameLayer.m: Update Score For Hits
On each hit you want to update the score. The checkForCollision method handles the hits for this game.
You are already displaying score data in the checkForCollision method. So you just need to drop in a call to the setScore method on line 226 just after the class property totalHits is incremented.
#pragma mark Collision Check -(void) checkForCollision { // Size of the player and target. Both are assumed squares so width suffices. float playerImageSize = [player texture].contentSize.width; float targetImageSize = [movingTarget texture].contentSize.width; // Compute their radii. Tweak based on drawing. float playerCollisionRadius = playerImageSize *.4; float targetCollisionRadius = targetImageSize *.4; // This collision distance will roughly equal the image shapes. float maxCollisionDistance = playerCollisionRadius + targetCollisionRadius; // Distance between two points. float actualDistance = ccpDistance(player.position, movingTarget.position); // Are the two objects closer than allowed? if (actualDistance < maxCollisionDistance) { // Play a sound effect [[SimpleAudioEngine sharedEngine] playEffect:@"explosion.caf"]; totalHits++; NSLog(@"HIT! Total attempts: %i. Total hits: %i", totalHits, totalAttempts); [self setScore]; [self endMovingTargetSequence]; } }
That should give you a good starting point for displaying a graphic glyph score.
[ad name=”Google Adsense”]