RSS


Announcement

I am more than happy to announce that I have built another personal website, so I have transferred all the posts and pages here to the new site. This blog will cease updating. Thank you for your support in past. Please follow me to the new website.

New Personal Website

 

 
Leave a comment

Posted by on October 16, 2012 in Uncategorized

 

Step by Step: How to build a scroll view by cocos2d


One of the dilemmas that many game engines are facing is whether they should provide many built-in high-level UI gadgets for developers, e.g. scroll view. Some of them feel that game engines should be condense and only focus on providing good gaming-related solutions, like graphics and sounds. However, as developers, we really need some neat UI gadgets for our game. Recently, I was trying to build a vertical scroll view for my iPhone game using cocos2d. Now I’d like share some pains and gains during the process.

Before implementing my own scroll view, I searched on the Internet and found some extension libraries for cocos2d. There is a scroll view for cocos2d, CCScrollLayer. Unfortunately, this gadget is more like a page viewer, not a viewer for a list of items. So I decided to build my own version. Below is a scroll view capture from my game that is under developing. Of course, if you prefer to read the code file directly, you can download it from here. Yet I suggest you to skim this blog to get the general idea of how to implementing scroll view, and it is going to help you understand raw code. :)

cocos2d-scrollview

Designing Phase

From CCScrollLayer, I borrowed some design ideas.

1. This gadget is inherited from CCLayer in order to receive touch events.

2. Every item in the list is a sub-layer of scroll view, and we need to pass down touch events to items in the list. Every item is a sub-class of CCLayer, i.e., we can have buttons, images and everything in the item layer.

Implementing Phase

If you want to follow my steps to build a simple scroll view, it is better grabbing some cookies and drinks before starting, we will have a lot of coding work, but most importantly, have fun!

Step 1: Create a scroll view class

Create a scroll view layer class, and write a basic initialization method for use like below.

   1: @interface FGScrollLayer : CCLayer

   2: {

   3:     // Holds pages.

   4:     NSMutableArray *layers_;

   5:

   6:     // Holds current pages width offset.

   7:     CGFloat pagesOffset_;

   8:

   9:     // Holds the height of every page

  10:     CGFloat pageHeight_;

  11:

  12:     // Holds the width of every page

  13:     CGFloat pageWidth_;

  14:

  15:     // Holds the maximum upper position

  16:     CGFloat maxVerticalPos_;

  17:

  18:     // Holds the real responsible rect in the screen

  19:     CGRect realBound;

  20: }

  21:

  22: /** Offset, that can be used to let user see next/previous page. */

  23: @property(readwrite) CGFloat pagesOffset;

  24:

  25: /** Page height, this version requires that each page shares the same height and width */

  26: @property(readonly) CGFloat pageHeight;

  27: @property(readonly) CGFloat pageWidth;

  28:

  29: #pragma mark Init/Creation

  30:

  31: /** Creates new scrollLayer with given pages & width offset.

  32:  * @param layers NSArray of CCLayers, that will be used as pages.

  33:  * @param pageSize indicates the size of every page, now this version requires each page 

  34:  * share the same page size

  35:  * @param widthOffset Length in X-coord, that describes length of possible pages

  36:  * @param visibleRect indicates the real position and size on the screen

  37:  * intersection. */

  38: +(id) nodeWithLayers:(NSArray *)layers pageSize:(CGSize)pageSize pagesOffset: (int) pOffset visibleRect: (CGRect)rect;

  39:

  40: /** Inits scrollLayer with given pages & width offset.

  41:  * @param layers NSArray of CCLayers, that will be used as pages.

  42:  * @param pageSize indicates the size of every page, now this version requires each page

  43:  * share the same page size

  44:  * @param pagesOffset Length in X-coord, that describes length of possible pages

  45:  * @param visibleRect indicates the real position and size on the screen

  46:  * intersection. */

  47: -(id) initWithLayers:(NSArray *)layers pageSize:(CGSize)pageSize pagesOffset: (int) pOffset visibleRect: (CGRect)rect;

  48:

  49: @end

.h file

   1: @implementation FGScrollLayer

   2:

   3: @synthesize pagesOffset = pagesOffset_;

   4: @synthesize pageHeight = pageHeight_;

   5: @synthesize pageWidth = pageWidth_;

   6:

   7: +(id) nodeWithLayers:(NSArray *)layers pageSize:(CGSize)pageSize pagesOffset:(int)pOffset visibleRect:(CGRect)rect{

   8:     return [[[self alloc] initWithLayers: layers pageSize:pageSize pagesOffset:pOffset visibleRect:rect] autorelease];

   9: }

  10:

  11: -(id) initWithLayers:(NSArray *)layers pageSize:(CGSize)pageSize pagesOffset:(int)pOffset visibleRect:(CGRect)rect{

  12:     if ( (self = [super init]) )

  13:     {

  14:         NSAssert([layers count], @"FGScrollLayer#initWithLayers:widthOffset: you must provide at least one layer!");

  15:

  16:         // Enable Touches/Mouse.

  17: #ifdef __IPHONE_OS_VERSION_MAX_ALLOWED

  18:         self.isTouchEnabled = YES;

  19: #endif

  20:

  21:         // Save offset.

  22:         self.pagesOffset = pOffset;

  23:

  24:         // Save array of layers.

  25:         layers_ = [[NSMutableArray alloc] initWithArray:layers copyItems:NO];

  26:

  27:         // Save pages size for later calculation

  28:         pageHeight_ = pageSize.height;

  29:         pageWidth_ = pageSize.width;

  30:         maxVerticalPos_ = pageHeight_ * [layers_ count] - rect.size.height + 5;

  31:

  32:         realBound = rect;

  33:

  34:         [self updatePages];

  35:     }

  36:     return self;

  37: }

  38:

  39: - (void) dealloc

  40: {

  41:     self.delegate = nil;

  42:

  43:     [layers_ release];

  44:     layers_ = nil;

  45:

  46:     [super dealloc];

  47: }

  48:

  49: - (void) updatePages

  50: {

  51:     // Loop through the array and add the screens if needed.

  52:     int i = 0;

  53:     for (CCLayer *l in layers_)

  54:     {

  55:         l.position = ccp(realBound.origin.x,  realBound.origin.y + (realBound.size.height - i * (pageHeight_ - self.pagesOffset)));

  56:         if (!l.parent)

  57:             [self addChild:l];

  58:         i++;

  59:     }

  60: }

.m file

Note: the realBound variable indicates the real position and size of the scroll view in the screen. And in the method updatePages, we calculate the real position for each item according to the realBound. So here we can simply leave scroll view layer with default position(0,0).

Step 2: Write the touch handler

   1: @protocol FGScrollLayerDelegate

   2:

   3: @optional

   4:

   5: /** Called when scroll layer begins scrolling.

   6:  * Usefull to cancel CCTouchDispatcher standardDelegates.

   7:  */

   8: - (void) scrollLayerScrollingStarted:(FGScrollLayer *) sender;

   9:

  10: /** Called at the end of moveToPage:

  11:  */

  12: - (void) scrollLayer: (FGScrollLayer *) sender scrolledToPageNumber: (int) page;

  13:

  14: @end

Put above code in the .h file outside of the definition of class FGScrollLayer. We need a delegate to help us handle all touch events. Besides, we need to decide whether we need to steal touch events or sending those events to items to handle.

After defining a delegate, put below code into the definition of class FGScrollLayer.

   1: NSObject <FGScrollLayerDelegate> *delegate_;

   2:

   3: // The screen coord of initial point the user starts their swipe.

   4: CGFloat startSwipe_;

   5:

   6: // The coord of initial position the user starts theri swipe.

   7: CGFloat startSwipeLayerPos_;

   8:

   9: // For what distance user must slide finger to start scrolling menu.

  10: CGFloat minimumTouchLengthToSlide_;

  11:

  12: // Internal state of scrollLayer (scrolling or idle).

  13: int state_;

  14:

  15: BOOL stealTouches_;

  16:

  17: #ifdef __IPHONE_OS_VERSION_MAX_ALLOWED

  18: // Holds the touch that started the scroll

  19: UITouch *scrollTouch_;

  20: #endif

Also add some properties in the .h file. See below:

   1: @property (readwrite, assign) NSObject <FGScrollLayerDelegate> *delegate;

   2:

   3: #pragma mark Scroll Config Properties

   4:

   5: /** Calibration property. Minimum moving touch length that is enough

   6:  * to cancel menu items and start scrolling a layer.

   7:  */

   8: @property(readwrite, assign) CGFloat minimumTouchLengthToSlide;

   9:

  10: /** If YES - when starting scrolling FGScrollLayer will claim touches, that are

  11:  * already claimed by others targetedTouchDelegates by calling CCTouchDispatcher#touchesCancelled

  12:  * Usefull to have ability to scroll with touch above menus in pages.

  13:  * If NO - scrolling will start, but no touches will be cancelled.

  14:  * Default is YES.

  15:  */

  16: @property(readwrite) BOOL stealTouches;

Now turn to the implementation side. Put those code below into initialization method in the .m file.

   1: self.stealTouches = YES;

   2:

   3: // Set default minimum touch length to scroll.

   4: self.minimumTouchLengthToSlide = 30.0f;

And add several methods to handle touch events.

   1: enum

   2: {

   3:     kFGScrollLayerStateIdle,

   4:     kFGScrollLayerStateSliding,

   5: };

   6:

   7: #ifdef __IPHONE_OS_VERSION_MAX_ALLOWED

   8: @interface CCTouchDispatcher (targetedHandlersGetter)

   9:

  10: - (id<NSFastEnumeration>) targetedHandlers;

  11:

  12: @end

  13:

  14: @implementation CCTouchDispatcher (targetedHandlersGetter)

  15:

  16: - (id<NSFastEnumeration>) targetedHandlers

  17: {

  18:     return targetedHandlers;

  19: }

  20:

  21: @end

  22: #endif

  23: #ifdef __IPHONE_OS_VERSION_MAX_ALLOWED

  24:

  25: /** Register with more priority than CCMenu's but don't swallow touches. */

  26: -(void) registerWithTouchDispatcher

  27: {

  28: #if COCOS2D_VERSION >= 0x00020000

  29:     CCTouchDispatcher *dispatcher = [[CCDirector sharedDirector] touchDispatcher];

  30:     int priority = kCCMenuHandlerPriority - 1;

  31: #else

  32:     CCTouchDispatcher *dispatcher = [CCTouchDispatcher sharedDispatcher];

  33:     int priority = kCCMenuTouchPriority - 1;

  34: #endif

  35:

  36:     [dispatcher addTargetedDelegate:self priority: priority swallowsTouches:NO];

  37: }

  38:

  39: /** Hackish stuff - stole touches from other CCTouchDispatcher targeted delegates.

  40:  Used to claim touch without receiving ccTouchBegan. */

  41: - (void) claimTouch: (UITouch *) aTouch

  42: {

  43: #if COCOS2D_VERSION >= 0x00020000

  44:     CCTouchDispatcher *dispatcher = [[CCDirector sharedDirector] touchDispatcher];

  45: #else

  46:     CCTouchDispatcher *dispatcher = [CCTouchDispatcher sharedDispatcher];

  47: #endif

  48:

  49:     // Enumerate through all targeted handlers.

  50:     for ( CCTargetedTouchHandler *handler in [dispatcher targetedHandlers] )

  51:     {

  52:         // Only our handler should claim the touch.

  53:         if (handler.delegate == self)

  54:         {

  55:             if (![handler.claimedTouches containsObject: aTouch])

  56:             {

  57:                 [handler.claimedTouches addObject: aTouch];

  58:             }

  59:         }

  60:         else

  61:         {

  62:             // Steal touch from other targeted delegates, if they claimed it.

  63:             if ([handler.claimedTouches containsObject: aTouch])

  64:             {

  65:                 if ([handler.delegate respondsToSelector:@selector(ccTouchCancelled:withEvent:)])

  66:                 {

  67:                     [handler.delegate ccTouchCancelled: aTouch withEvent: nil];

  68:                 }

  69:                 [handler.claimedTouches removeObject: aTouch];

  70:             }

  71:         }

  72:     }

  73: }

  74:

  75: -(void)ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event

  76: {

  77:     if( scrollTouch_ == touch ) {

  78:         scrollTouch_ = nil;

  79:     }

  80: }

  81:

  82: -(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event

  83: {

  84:     if( scrollTouch_ == nil ) {

  85:         scrollTouch_ = touch;

  86:     } else {

  87:         return NO;

  88:     }

  89:

  90:     CGPoint touchPoint = [touch locationInView:[touch view]];

  91:     touchPoint = [[CCDirector sharedDirector] convertToGL:touchPoint];

  92:

  93:     startSwipe_ = touchPoint.y;

  94:     startSwipeLayerPos_ = [self position].y;

  95:     state_ = kFGScrollLayerStateIdle;

  96:     return YES;

  97: }

  98:

  99: - (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event

 100: {

 101:     if( scrollTouch_ != touch ) {

 102:         return;

 103:     }

 104:

 105:     CGPoint touchPoint = [touch locationInView:[touch view]];

 106:     touchPoint = [[CCDirector sharedDirector] convertToGL:touchPoint];

 107:

 108:

 109:     // If finger is dragged for more distance then minimum - start sliding and cancel pressed buttons.

 110:     // Of course only if we not already in sliding mode

 111:     if ( (state_ != kFGScrollLayerStateSliding)

 112:         && (fabsf(touchPoint.y-startSwipe_) >= self.minimumTouchLengthToSlide) )

 113:     {

 114:         state_ = kFGScrollLayerStateSliding;

 115:

 116:         // Avoid jerk after state change.

 117:         startSwipe_ = touchPoint.y;

 118:         startSwipeLayerPos_ = [self position].y;

 119:         previousTouchPointY = touchPoint.y;

 120:

 121:         if (self.stealTouches)

 122:         {

 123:             [self claimTouch: touch];

 124:         }

 125:

 126:         if ([self.delegate respondsToSelector:@selector(scrollLayerScrollingStarted:)])

 127:         {

 128:             [self.delegate scrollLayerScrollingStarted: self];

 129:         }

 130:     }

 131:

 132:     if (state_ == kFGScrollLayerStateSliding)

 133:     {

 134:         CGFloat desiredY = startSwipeLayerPos_ + touchPoint.y - startSwipe_;

 135:         [self setPosition:ccp(0, desiredY)];

 136:     }

 137: }

 138: #endif

Basically, I borrowed these touch events handlers from CCScrollLayer, and I modified it a little bit for our own use. The structure is derived from CCScrollLayer, including the stealing touch events idea. Here we use variables startWipe_ and minimumTouchLengthToSlide_ to help us check whether user are sliding their fingers or simply touching the screen. If it is a sliding, we steal it; otherwise we send it to behind layers, e.g. items.

Step 4: Decides which items should be visible

With handling touch events, our scroll view is able to move accordingly. However, we need to restrict scroll view to some specific area, and that is the reason why we pass realBound as an initializing parameter. So, we need to update visibility of each item when moving scroll layer.

Put these two methods in the implementation file(.m file).

   1: /**

   2:  * According to current position, decide which pages are visible

   3:  */

   4: -(void)updatePagesAvailability{

   5:     CGPoint currentPos = [self position];

   6:     if (currentPos.y > 0) {

   7:         int visibleBoundUp = currentPos.y / pageHeight_;

   8:         visibleBoundUp = MIN([layers_ count], visibleBoundUp);

   9:         for (int i = 0; i < visibleBoundUp; i++) {

  10:             [[layers_ objectAtIndex:i] setVisible:NO];

  11:         }

  12:         if (visibleBoundUp < [layers_ count]) {

  13:             int visibleBoundDown = (currentPos.y + realBound.size.height) / pageHeight_;

  14:             visibleBoundDown = MIN([layers_ count] - 1, visibleBoundDown);

  15:             for (int i = visibleBoundUp; i <= visibleBoundDown; i++) {

  16:                 [[layers_ objectAtIndex:i] setVisible:YES];

  17:             }

  18:             if (visibleBoundDown < [layers_ count] - 1) {

  19:                 for (int i = visibleBoundDown + 1; i <= [layers_ count] - 1; i++) {

  20:                     [[layers_ objectAtIndex:i] setVisible:NO];

  21:                 }

  22:             }

  23:         }

  24:     }

  25:     else if (currentPos.y <= 0){

  26:         CGFloat gapY = -currentPos.y;

  27:         int visibleBound = (realBound.size.height - gapY) / pageHeight_;

  28:         // index visibleBound itself should be invisible

  29:         if (visibleBound < 0) {

  30:             for (int i = 0; i < [layers_ count]; i++) {

  31:                 [[layers_ objectAtIndex:i] setVisible:NO];

  32:             }

  33:             return;

  34:         }

  35:         visibleBound = MIN([layers_ count] - 1, visibleBound);

  36:         for (int i = 0; i <= visibleBound; i++) {

  37:             [[layers_ objectAtIndex:i] setVisible:YES];

  38:         }

  39:         for (int i = visibleBound + 1; i < [layers_ count]; i++) {

  40:             [[layers_ objectAtIndex:i] setVisible:NO];

  41:         }

  42:     }

  43: }

  44:

  45: -(void)setPosition:(CGPoint)position{

  46:     [super setPosition:position];

  47:     [self updatePagesAvailability];

  48: }

The first method here is to calculate each item’s visibility, and the second method here is to override setPosition to call update method. Besides, add one line to the updatePages method.

   1: [self updatePagesAvailability];

Step 5: Apply a simple sliding algorithm.

Okay, until now, we have finished the basic work of implementing a scroll view. Yet it is not a good one, since users’ sliding feelings are not fluent enough. Here, I have a very simple sliding algorithm. I know this is not a perfect solution, and this algorithm is far from Apple’s elegant sliding implementation. I just present it for lazy persons who just want it work. My simple idea is to calculate and store the finger moving speed. When users release their fingers, calculate the momentum according to their before finger moving speed and apply a following inertia effect. Below is how exactly I did it.

   1: // these two variables are to make a sliding effect on scroll view

   2: static CGFloat previousTouchPointY = -1;

   3: static CGFloat moveSpeed = 0;

   4: - (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event

   5: {

   6:     if( scrollTouch_ != touch ) {

   7:         return;

   8:     }

   9:

  10:     CGPoint touchPoint = [touch locationInView:[touch view]];

  11:     touchPoint = [[CCDirector sharedDirector] convertToGL:touchPoint];

  12:

  13:

  14:     // If finger is dragged for more distance then minimum - start sliding and cancel pressed buttons.

  15:     // Of course only if we not already in sliding mode

  16:     if ( (state_ != kFGScrollLayerStateSliding)

  17:         && (fabsf(touchPoint.y-startSwipe_) >= self.minimumTouchLengthToSlide) )

  18:     {

  19:         state_ = kFGScrollLayerStateSliding;

  20:

  21:         // Avoid jerk after state change.

  22:         startSwipe_ = touchPoint.y;

  23:         startSwipeLayerPos_ = [self position].y;

  24:         previousTouchPointY = touchPoint.y;

  25:

  26:         if (self.stealTouches)

  27:         {

  28:             [self claimTouch: touch];

  29:         }

  30:

  31:         if ([self.delegate respondsToSelector:@selector(scrollLayerScrollingStarted:)])

  32:         {

  33:             [self.delegate scrollLayerScrollingStarted: self];

  34:         }

  35:     }

  36:

  37:     if (state_ == kFGScrollLayerStateSliding)

  38:     {

  39:         CGFloat desiredY = startSwipeLayerPos_ + touchPoint.y - startSwipe_;

  40:         [self setPosition:ccp(0, desiredY)];

  41:

  42:         // enable scroll bar to be visible

  43:         [scrollBar setVisible:YES];

  44:         [scrollBlock setVisible:YES];

  45:

  46:         // update scrolling effect variables

  47:         moveSpeed = touchPoint.y - previousTouchPointY;

  48:         previousTouchPointY = touchPoint.y;

  49:     }

  50: }

  51:

  52: /**

  53:  * After touching, generate an inertia effect.

  54:  */

  55: - (void)moveToDesiredPos:(CGFloat)desiredY{

  56:     CCAction* slidingAction = nil;

  57:     if (desiredY > maxVerticalPos_) {

  58:         slidingAction = [CCSequence actions:[CCMoveTo actionWithDuration:0.10 position:ccp([self position].x, desiredY)], [CCMoveTo actionWithDuration:0.15 position:ccp([self position].x, maxVerticalPos_)], nil];

  59:     }

  60:     else if (desiredY < 0){

  61:         slidingAction = [CCSequence actions:[CCMoveTo actionWithDuration:0.10 position:ccp([self position].x, desiredY)],[CCMoveTo actionWithDuration:0.15 position:ccp([self position].x, 0)], nil];

  62:     }

  63:     else{

  64:         CGFloat interPosY = (desiredY - [self position].y) * 0.7 + [self position].y;

  65:         slidingAction = [CCSequence actions:[CCMoveTo actionWithDuration:0.15 position:ccp([self position].x, interPosY)],[CCMoveTo actionWithDuration:0.3 position:ccp([self position].x, desiredY)], nil];

  66:     }

  67:     [self runAction:slidingAction];

  68: }

  69:

  70: - (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event

  71: {

  72:     [scrollBar setVisible:NO];

  73:     [scrollBlock setVisible:NO];

  74:

  75:     if( scrollTouch_ != touch )

  76:         return;

  77:     scrollTouch_ = nil;

  78:

  79:     if (ABS(moveSpeed) > 10) {

  80:         CGFloat desiredDesY = [self position].y + moveSpeed * 5;

  81:         [self moveToDesiredPos:desiredDesY];

  82:     }

  83:     else{

  84:         if ([self position].y > maxVerticalPos_) {

  85:             [self runAction:[CCMoveTo actionWithDuration:0.3 position:ccp([self position].x, maxVerticalPos_)]];

  86:         }else if ([self position].y < 0){

  87:             [self runAction:[CCMoveTo actionWithDuration:0.3 position:ccp([self position].x, 0)]];

  88:         }

  89:     }

  90:

  91:     // restore scrolling effect variables to default value

  92:     moveSpeed = 0;

  93:     previousTouchPointY = -1;

  94: }

Step 6: What about the appearance of scroll view layer?

Everyone’s scroll view should look different, we need to add some decorative images to our viewer, e.g. we’d like to put our view into a frame, or we need a slider bar to indicate the current position of the whole list. What about those needs I just mentioned? Okay, here is the solution. You may create a class that inherit from this scroll layer, and specify whatever variables and images that you need to decorate your viewer. Here I provide my simple version. I wrote these variables into scroll layer class because they are very common for a standard scroll view, if you don’t need them, you may delete them or ignore them in your derived class.

Put below code in the definition file(.h file).

   1: /*Decoration and slide bars*/

   2: // Scroll bars on the right

   3: CCSprite* scrollBar;

   4: CGFloat scrollBarPosY;

   5:

   6: // Scroll block that indicates the current position in whole scorll view content

   7: CCSprite* scrollBlock;

   8: CGFloat scrollBlockUpperBound;

   9: CGFloat scrollBlockLowerBound;

  10:

  11: // Decoration

  12: // Holds position to maintain their position fixed even in setPosition

  13: CCSprite* upperBound;

  14: CGFloat upperBoundPosY;

  15: CCSprite* lowerBound;

  16: CGFloat lowerBoundPosY;

And add those code in setPosition method in .m file.

   1: CGFloat scrollBlockDesiredY = scrollBlockUpperBound - (scrollBlockUpperBound - scrollBlockLowerBound) * position.y / maxVerticalPos_;

   2: if (scrollBlockDesiredY > scrollBlockUpperBound) {

   3:     scrollBlockDesiredY = scrollBlockUpperBound;

   4: }else if (scrollBlockDesiredY < scrollBlockLowerBound){

   5:     scrollBlockDesiredY = scrollBlockLowerBound;

   6: }

   7: [scrollBlock setPosition:ccp([scrollBlock position].x, scrollBlockDesiredY - position.y)];

   8: [lowerBound setPosition:ccp([lowerBound position].x, lowerBoundPosY - position.y)];

   9: [upperBound setPosition:ccp([upperBound position].x, upperBoundPosY - position.y)];

  10: [scrollBar setPosition:ccp([scrollBar position].x, scrollBarPosY - position.y)];

Add below code in ccTouchEnded and ccTouchCanceled methods.

   1: [scrollBar setVisible:NO];

   2: [scrollBlock setVisible:NO];

Add below code in ccTouchMoved method.

   1: // enable scroll bar to be visible

   2: [scrollBar setVisible:YES];

   3: [scrollBlock setVisible:YES];

Now the only left thing is to provide proper images and values to the corresponding variables in your initialization method.

Finished Version

After reading above messy code fragments, you may want to download a complete version. Click here! :)

Conclusion

Scroll view is not a simple gadget that every game engine will provide. Besides, every game needs its own style scroll view. Therefore it is hard to offer an universally suitable scroll view. This blog offers a provides an adapter class for implementing your own scroll view. Implementing a scroll view by cocos2d is not a easy task, at least it is a tedious task. And it requires a lot of effort and carefulness in adjusting the relative position and calculating the visibility of each item. You may derive your own class from this layer, and this layer already help you achieve a lot of tedious work, e.g. touch events handling and scrolling. What left to do is to provide decorative images to make it look nice and elegant.

 
10 Comments

Posted by on October 5, 2012 in Technical notes

 

Tags: , , , , ,

Tip: How to directly run java classes files that reside in jar files without using “-jar” command


We all know that jar files are archives for java classes, libs, and other execution-depended files, e.g. images. Usually jar files are independent, that is to say, we may run them using command “>$ java –jar filename”. However, for some research purposes, we need to extract raw class files from a jar file to analyze and execute. At first, I thought it’s a piece of cake. Open a jar file and extract all files out. Find the entrance class(which contains static main method, usually will be indicated in the manifest files), and input “>$ java entrance_filename”. Yet everything doesn’t go as smooth as I expected. I kept encountering “NoClassDefFoundError” error. I searched the internet and found this error indicates that class loader can’t find classes which are available during the compiling time. Causes to this problem vary in many different ways. Below are two possible causes when we try to directly run classes files that reside in jar files.

Forget to check manifest files to include necessary classpath.

“NoClassDefFoundError” error sometimes can be a real error. Be sure to check manifest files included in the jar files. Sometimes manifest files will has information of extra class path information to support execution of this jar file. We need to also include those class paths when execute raw class files. If you miss this part. There is no doubt some classes’ definition are really missing. So be sure to include all class paths if manifest files indicate any.

Execute the entrance class in a wrong package level.

For a large project, we may have many sub-folders in a jar file. And our classes are most likely defined in different packages, which in most cases correspond to different sub-folder levels. After checking and including all the possible class paths, I still got this “NoClassDefFoundError” error every time. And I searched for many hours and tried a lot of solutions. None of them works. When I was staring at the error message with frustration, (the message is like this, “XXX NoClassDefFoundError: (XXX/entrance) wrong name…” that indicated that even the entrance class can not be found properly), I suddenly realized that there is a prefix “XXX/” before entrance class name. This message doesn’t say entrance class is a wrong name, but “XXX/entrance” is a wrong name. I “cd” back to higher folder level and re-type the “java XXX/entrance” command. Then everything works like a charm. To sum up, my experience shows that we should execute the classes at a right folder level that usually corresponds to the class package level.

Conclusion

Jar files actually contain no magic. They are merely collections of necessary files. As a result, it is absolutely feasible to extract all files and execute them by raw “java” command if we configure the command correctly. Smile

 
Leave a comment

Posted by on September 21, 2012 in Technical notes

 

Tags: , , , , ,

Several principles that improve code quality


Recently I read a book “Code Simplicity” written by Max Kanat-Alexander. This is a very book aiming at helping improving code quality. After finishing this thin book, I want to share something here to benefit more junior programmers like me.

Code’s simplicity determines code’s quality

Code changes from time to time, and the factors causing code to change usually are not predictable. Thus we don’t know how our code is going to change in the future. Though currently we may have a very good design, there is some degree of potential that we need a new feature in the future that is not compatible with current design. As a result, when we consider the cost of developing a software, we not only need to consider the cost of implementation, but also have to take maintenance cost into account. Consequently, if our project is going to last for a long time, we may put weight more efforts into the maintenance side than the developing aspect. If our project is a disposable software, it is OK (usually preferable) to make a software that just works currently. However, our projects usually are not transient falling stars, we have to maintain them for a long time to keep serving people or other projects. What’s more, if your software works currently and we have no future change any more, how can we measure whether your code is good or not? Is it really meaningful to say that your code is poor since it is hard to understand? But your project helps us to solve problems and we don’t expect more from your project. That’s good enough already! Only when your code will change from time to time will we start to care about whether you have a good code quality. As a result, code’s quality is closely relating to software’s maintainability. Thus reducing the maintenance cost is the key to improve code’s quality, not the implementation or how perfectly current design meets our need.

Maintenance cost is heavily depend on the cost for programmers to understand code. Programmers are smart persons, it is a piece of cake for them to comprehend a complex software project as long as you give them enough time. However, this is not the case in real life. We have budget, programmers deadlines, too. When you order a programmer to add a feature, they just want things to be done before deadline. The most likely situation is that they try to modify the current code and add some codes to make it work. Every feature is handled in this way, after two months, you modified code 20 times. The design becomes ugly, and the complexity of your code is soaring. Now it’s damn hard to add any feature. Yet the changes are still a lot and needed, you can’t follow the changes any more because your code is so complicated that even adding a small feature is a big big task. Building the code with simplicity is needed! Even if it may increase the cost of implementation, it keeps your project alive and avoid your project to become a gigantic monster that you have to kill. Practically speaking, being simple is flexible to future changes and reduces your maintenance cost, thus improves your code quality.

If you want to change your code, before changing it, think carefully that whether you will greatly lift the complexity level of your project. If the answer is YES, please try to think that whether adjusting your design will help you reduce the complexity. If the answer is YES, don’t hesitate to do this. It may seems that you spend a lot of time for a small change, you save a lot of time for the future and keep your project maintainable, which extends your project’s life to serve more people or other projects.

Don’t write code that isn’t needed

Remember, we can’t predict future. It is not necessary to save currently unused code for future. You may think that writing those codes may save future time, actually in most cases, it just increases your burden to understand it and debug it in future. Changes are definitely going to happen in the future, it is understandable that a programmer wants to do something for future use. Yet we are not foreseers, we just guess our future. The code you leave for the future only has certain chances to be used. However, it adds your code’s complexity because it requires more time to understand what your code is doing. It increases both implementation cost and maintenance cost. Moreover, when you are evolving your project, the unused code are unlikely to be tested over time. As a result, if “fortunately” those out-dated codes will be put into use in the future, we are likely to introduce strange bugs because that those codes are not compatible with current version. We spend a lot of time to debug it. What’s worse is that when encountering a bug, we may first doubt our newly-written code since we assume that old code are less likely to be buggy, which also increase our debugging time. It is more acceptable that we write a new one, an up-to-date version.

Use incremental design

I have a list of requirements, and now I am going to design a project. There are two strategies, I deliver a grand design including all the features, or I implement a feature first with a simple design and then add features one by one. For the former one, since we stand higher level, we usually tend to build a complex design that every part in this design is intricately related with others, and this design will perfectly meets the current requirement. However, if there is any fault in the design or that any feature changes in the future, it’s hard to adjust since we already start to build the big framework. Changing girders for a house is not a tiny issue. For incremental design (the latter one), we are doing project like we are gradually adding features for our current project. Every time we introduce a small change, we are possibly refining our design. By this way, we also keeps our design meeting tightly with required features. More importantly, we tend to write high quality code since we tend to have a simpler design and implementation. Once there is a change of feature, we may roll back to the previous version and try to re-implement this. Or we can regard it as another new feature, and add this change to our feature list, which will not cause a development catastrophe – replacing the whole blueprint. And we may have another rule to support incremental design: the whole project’s simplicity level is decided by every individual piece’s simplicity.

Never “fix” anything unless it’s a real problem

Compare optimizing your code and implementing a new feature, which one is more desirable? Of course the latter one, budget is limit, we can’t afford spending time on invisible improvement. Moreover, there is a development rule saying that the defect you introduce is proportional to the size of your changes. Unless you are simplifying your code, which reduces the maintenance cost in the long run, you should not change your code without seeing any problem. We don’t need to change, if your project works.

Conclusion

Code’s quality closely relates to maintenance cost. If there is no change in the future and your projects works, how can we know whether your code quality is good or poor. Maintenance cost embodies code’s quality, and simplicity of your code determines your maintenance cost. How to keep your code simple? That’s a big question, and we have a lot of useful design patterns, code conventions, good comment habits and so on to help us achieve this goal. Maybe next time I will try to share some detailed skills about making code simple. Right now I share some general concepts. If you have some thoughts about this topic, please feel free to discuss with us. Sharing is a virtue.

 
Leave a comment

Posted by on August 24, 2012 in Technical notes

 

Tags: , , , , , ,

How to DESIGN acceleration control style in cocos2d?


Are you trying to design a game that tilting is your major control style? If your answer is YES, this is a right article for you. Recently I want to design a game using device acceleration to control my hero, however, it is more difficult than it seems to be. Sad smile

PS: this article uses cocos2d to implement all tricks, readers better have some basic understanding of cocos2d.

Step 1: get acceleration value from device.

  • In you CCLayer file, enable the acceleration detection.
   1: self.isAccelerometerEnabled = YES;

  • Add this method in you CCLayer file.
   1: - (void)accelerometer:(UIAccelerometer*)accelerometer didAccelerate:(UIAcceleration*)acceleration

   2: {

   3:     [self setAccelerometer:acceleration];

   4: }

  • Moreover, you may adjust the rate of receiving acceleration data from device by below code.
   1: [[UIAccelerometer sharedAccelerometer] setUpdateInterval:1/60];

Step 2: map our acceleration data to object’s movement Intuitively,

We have three ways to translate those data, including mapping to acceleration, to speed, and to position.

Mapping to acceleration

object.acceleration = acceleration * MULTIPLIER

Here, we neither manipulate the position nor control the velocity directly, we use acceleration data to affect  acceleration of an object. We translate device acceleration data to our object’s acceleration directly. Imagine that when you tilt our device to the left, our object gains an acceleration toward left, which gradually change the velocity leftward. Unfortunately, this mapping is very hard to for player to learn. Our human brains have no problem in estimating movement with a constant speed. Yet if an object’s speed is changing even changing gradually, it is hard for us to calculate the destination position in a short time. We are not math genius, who are able to calculate a polynomial distance formula in a very short time. As a result, though this method can provide the smoothest movement among three methods because this is how our world is operating, we may feel awkward controlling in this way. If you still can’t understand why, imagine a frictionless slope, can you quickly tell me how much time will a ball slide down from the top even if you know the length and angle of this slope?

Mapping to velocity

object.velocity = acceleration * MULTIPLIER

How much you tilt your device directly decides your object’s speed. For example, if you slightly tilt your device to the left and your object is going to have speed like 30 pixels per second, holding your device in a landscape view will greatly cause your speed change to like  3000 pixels per second. Compare to changing acceleration, changing speed is easier for us to understand. Human brain is familiar with calculating such linear formula. We calculate movement all the time when playing FPS games. We are comfortable with this consistent moving style. However, to achieve a smooth movement, we have to manipulate the acceleration instead of velocity. Suddenly setting velocity from a big value to a small value will cause a huge speed gap, which make objects move stiffly. We appreciate smooth movement. In our world, everything changing gradually pleases us. Therefore, instead of changing speed instantly, we set a target velocity and “gradually” set speed to the target velocity. See the below code.

   1: -(void)updateVelocity:(ccTime)dt{

   2:     float newVelocity = 0;

   3:     if (velocityX - targetVelocityX > 0) {

   4:         newVelocity = velocityX - acce_value * dt;

   5:         if (newVelocity < targetVelocityX) {

   6:             newVelocity = targetVelocityX;

   7:         }

   8:     }

   9:     else{

  10:         newVelocity = velocityX + acce_value * dt;

  11:         if (newVelocity > targetVelocityX) {

  12:             newVelocity = targetVelocityX;

  13:         }

  14:     }

  15:     velocityX = newVelocity;

  16: }

We “slowly” change the velocity to our target velocity along with past time in the update circle. In this way, we achieve our smoothness goal and how smooth it will be depends on your acceleration. Note that the bigger your acceleration is, less smoothness your movement is. We need to adjust to a balance.

Mapping to position

object.position = acceleration * MULTIPLIER

Instead of manipulating speed, we map acceleration data to object’s position directly. This is perfectly easy, why not use this way? Players want simple and intuitive controlling way, let’s meet their needs. Unfortunately, it is not easy as what you think. Acceleration data from device is not stable as you may think. Even if you think you are holding your iPhone very steadily, the acceleration data changes all the time that usually varies around 0.1 if you hold really steadily. This is bad for our direct translating to position. You may have guessed that our object will “shake” like your device “is”. This is annoying. I have tried many methods to counteract the “shaking”, e.g. I used a wrap to pre-process the raw data from device. This wrap will compare the previous data with current data, only when their gap is bigger than a threshold (0.1) will we transfer this data to the upper methods for using. However, the result of applying a wrapper is that the object will move inconsistently. That is to say, the object will dash to next position suddenly and then stay steadily quite. This feeling is like you are playing a game which the only movement is teleporting. It is a paradox that if you lower your threshold, your object shakes, and if you raise your threshold, your object “teleports”. Therefore, if you only use some tilting levels, it is OK and preferable to use this simple method. However, if you want a precise control style, just forget this method.

Conclusion

Comparisons of three styles

I draw a graph to generalize above three methods’ difference. Mapping to acceleration is very hard to learn because our brain are not familiar with speed-changing movement. Yet this is the smoothest one because everything changes gradually. The second type – mapping to speed is easier to learn and is smooth as well if we still manipulate the acceleration instead of changing the speed suddenly. And the last one – position is very simple to understand and master, however, it is not stable. Only when we need several levels of tilting can we make use of this control style.

Aside from above tedious explanation, I have written a small xcode project to let you try all these control styles. Thousands lines of explanation is not as useful as some hands-on practices. If you are interested, please download here. Smile

snapshot

Press the top icon will iteratively change the control style among above three types. Sliding three icons in the bottom will respectively change the magnitude of mapping to position, speed, and acceleration. For example, under the velocity control style, increasing the velocity value by sliding the second bar will increase your maximum target speed, meanwhile changing acceleration slider bar will affect how fast your velocity is able to change. You may modify the code to test new ways of controlling, if you find a better solution, I’d appreciate if you share with us. Sharing is a virtue. Smile

 
Leave a comment

Posted by on July 27, 2012 in Game, Game Design, Technical notes

 

Tags: , , , , ,

High Completion Yet NOT perfect Innovation – my impression on Diablo 3


It’s either me or the game that is a little bit wrong. I played Diablo 2: Lord of Destruction(LoD) several years ago and played that game for a few years. This game touched my heart and made me crazy. However, I feel unsatisfactory to this newly released sequel, Diablo 3.

Diablo 3

Let’s begin with some comparisons.

Big Changes compare to Diablo 2 : Lord of Destruction

  • Character Development

Diablo 2 : LoD

Each leveling up, your character gains 5 points. You may allocate those points freely to 5 attributes, including strength, dexterity, energy, stamina, and vitality. For characters in different classes, they share the same formula of calculating basic above 5 attributes.

Diablo 3

You no longer control the development of your characters by allocating basic attribute points. Your characters will gain points automatically.

Diablo3 - Character Developement System

Comments:

For our old legacy players from Diablo 2: LoD, this change is very hard to get used to. Since in Diablo 2: LoD, we have to carefully calculate the points to meet some equipment requirements. Interestingly, a brutal sorceress (with high strength points) is very powerful since she not only usually generate high damage, but also is able to wear some heavy armors, which provide great physical and magic protection. This character development invariance introduced by customized points allocation strategy really inspires player’s imagination of playing Diablo 2. I am not playing WoW, Diablo 3 should have their own culture. So for this change, I have a negative impression.

  • Skills

Diablo 2: LoD

Same as attribute points, you gain 1 precious point after each leveling up. You can freely allocate this point to any available skill. In a battle, you may quickly switch between the skills you learned and cast different skills.

Diablo 3:

You no loner need to worry about choosing skills, you own all the available skills. Yet you cannot cast all the skills in one battle. There is only 6 slots for active skills and 3 slots for passive skills. Normally you have to carefully fill the skill slots before entering a battle. Plus, 6 slots corresponds to 6 categories respectively, you cannot use skills from the same category at the same time.

Diablo 3 - Skills                             Diable 3 - Skill Slots

Comments:

This change converts the problem of choosing skills to learn to the problem of choosing skills to fight. It’s very hard to say which one is better, at least this is an innovation to the Diablo series. However, the fixed number of slots is a very annoying design, at least for me. In fact, it’s possible to replace the skills during a battle, yet when a skill is in CD, I can’t change it. Even if I have switched to another skill, I still need to wait for this switching time penalty – switching CD, which means that I have wait for another CD to use this new skill. In a boss fight, time is too precious to wait for this long switching CD. Moreover, I only have 6 skills to use in a battle, and those 6 skills are from 6 categories. You cannot choose 2 skills from same category at the same time. This restriction eliminates much trouble from designers, however, it kills much joy from us as well. In all, I am not sure about giving all skills to players, however, the fixed slot and categories do hinder the imagination of players.

Heritage from Diablo 2 : LoD

  • Random Labyrinth, Random Equipment, and Random Elite Monsters

Repeatable levels are one of the keys to success of Diablo series, and the randomness is the core of repeatable levels. The above 3 factors compose a random level, and they always motivate players to play more. Hoping encountering another horde of monsters, hoping for another better equipment (even if a tiny bit better), players are enjoying their time in rewarding of slaughtering or getting a better weapon. In Diablo 3, we see that designers keep playing this trick to entertaining players. Now we may only need a bit more exp. to level up, or we simply run another place to tick the quest list, or we just get a new equipment and can’t wait to try it on. All those easily reachable rewards build the core of Diablo series.

Other

  • Multi-players Cooperation

There is one more part that I want to complain about Diablo 3 – imperfect teamwork system. In multi-players mode, especially in low-difficulty modes, like normal or nightmare, you may feel your character is omnipotent, you don’t see the point of owning a witch doctor in your team, or you don’t understand why a wizard is necessary even if you want to speed up the slaughtering. It is true that in inferno levels, tank is needed and high DPS is welcome. There is no big difference in owning a demon hunter or a wizard. Plus, the bad balance between multi-players mode and solo mode ruins much the fun of teamwork. Yet the good news is that Blizzard have already notices this problem and promise to alleviate this problem in later patches. (currently 1.02c)

Conclusion

Diablo 3 inherits most of the culture in Diablo series, and like most of Blizzard games, it’s well balanced and completed. Blizzard’s games have incomparable data balance. Also, their games are famous for high completion and great details. You can feel the tears and efforts of developers when playing the their games. However, in recent years, hindered by its fame, Blizzard is hesitate to innovate, like WoW, StarCraft 2. Surprisingly, Diablo 3 took a great step in breaking the old rules of Diablo series, although some innovation are controversial. Yet this brave action worth lots applaud. We not only believe in Blizzard’s great balance adjustment ability, but also long for a great innovative giant in game industry. In conclusion, this game is a well polished game, and deserve high score. However, this game bears too much expectation, we need another higher standard.

One more words, in metacritic, the score for Diablo 3 is 88, however user score is very low, only 4.0. I guess we players are too strict to Blizzard because we love she so much as well. Smile

 
2 Comments

Posted by on June 18, 2012 in Game, Game Review

 

Tags: , , ,

A Customized Movie–my impression about Journey


I just finished Journey the second time, and after watching the credits I start to think about what make this game unique. There are two things left in my mind, these are beautiful images and really fluent control experience. I have to say, this game is more like a movie.

(Unfortunately, I am not lucky enough to find another player who happens to play Journey at the same time. So I don’t have multi-play experience about Journey)

image

Game is a kind of experience, and should provide some emotions, which, I believe, is what Jenova pursues. Though the story is quite direct, (some may say it has a very profound plot, whatever.) Journey is an unbelievable experience. Its vision is too beautiful so that you don’t pay much attention on caring about other complicated things. A simple and direct story is good enough, or it will affect the immersion in gorgeous images. During the play, most of the time I forgot that I was playing a game, since I don’t need think about the strategy and even don’t need to press hard button combinations. I was totally lost in the experience Jenovo presented in the game. Moreover, I believe Journey borrows many techniques from film industry, e.g. the cut-scenes are well edited and the camera position is calculatedly located, e.g. the Journey title appears at the very beginning when you enter a new game and the camera will zoom out, which just like the beginning of a movie. In whole, this game provides a movie-like experience to players.

Moreover, the control experience is terrifically polished. User experience is insanely emphasized in this game. When you jump down from a sand slope, you ski down to the bottom fluently. Whenever you climb a slope, definitely you will notice you are advancing slowly. I believe Jenovo himself must play this game hundred times to polish these details and adjust the parameter to achieve this control feeling and the difficulty level. Oh, and the jump feeling, you fall down like a feather, not a solid body. All these create an elegant fairyland experience, and player explores and customizes this fairyland by their game controllers.

In conclusion, this game is more like a customized movie than a traditional game. However, it is an excellent and fresh game. It is not a simple combination between movies and games. It’s an innovative exploration on creative art.

 
Leave a comment

Posted by on May 2, 2012 in Game, Game Review

 

Tags: , , , , , , ,

 
Follow

Get every new post delivered to your Inbox.