Ivan Moen left a comment asking for an explanation about detecting which sprites have been touched in Cocos2d for the iPhone, and it was something I was intended to write about (eventually), so it seemed like a ripe time to address it.
Before we start, I'd like to mention that Luke Hatcher created much of the code that these snippets are inspired by.
Broadly, there are three different approaches to adding touch detection to pixels in Cocos2d iPhone. Which one you should choose depends on the needs of your application. While considering this topic, it's important to keep in mind that you're not just detecting touches, you're integrating a user interface management system to your application.
The three approaches are:
Dumb input management. This isn't dumb in the sense of stupid, but instead is dumb in the sense of a dumb missile that will keep flying straight until it hits something. A more precise description would be ignorant of global state.
While usually not usable as-is in non-demo applications, this approach underpins the other two approaches, and is thus important.
Simply subclass
CocosNodeand implement any or all of these three methods (you don't have to define them in the interface, they're already defined by a superclass).-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; oint location = [touch locationInView: [touch view]]; [self doWhateverYouWantToDo]; [self doItWithATouch:touch]; } -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; oint location = [touch locationInView: [touch view]]; [self doWhateverYouWantToDo]; [self doItWithATouch:touch]; } -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; oint location = [touch locationInView: [touch view]]; [self doWhateverYouWantToDo]; [self doItWithATouch:touch]; }
The distinction between the three methods is,
touchesBeganis fired when the user first presses their finger on the screen,touchesMovedis fired after the user has pressed their finger on the screen and moves it (but before they pick it up), andtouchesEndedis fired when the user picks their finger up.Using these three methods, you can easily fire actions whenever a
Sprite(or any otherCocos2dsubclass) is touched. For a simple application that may be sufficient.Top-down global input management. The next approach allows a very high level of control over handling input, but is prone to creating a monolithic method that handles all input management for your application.
First, it requires that you have references to all
Spriteobjects that you are interested in detecting input for. You can do that by managing the references manually, or can setup the subclass to track all instances.You can track instance references fairly easily, modeling after this code:
@interface MySprite : Sprite {} +(NSMutableArray *)allMySprites; +(void)track: (MySprite *)aSprite; +(void)untrack: (MySprite *)aSprite; @end
And the implementation:
@implementation MySprite static NSMutableArray * allMySprites = nil; +(NSMutableArray *)allMySprites { @synchronized(allMySprites) { if (allMySprites == nil) allMySprites = [[NSMutableArray alloc] init]; return allMySprites; } return nil; } +(void)track: (MySprite *)aSprite { @synchronized(allMySprites) { [[MySprite allMySprites] addObject:aSprite]; } } +(void)untrack: (MySprite *)aSprite { @synchronized(allMySprites) { [[MySprite allMySprites] removeObject:aSprite]; } } -(id)init { self = [super init]; if (self) [MySprite track:self]; return self; } -(void)dealloc { [MySprite untrack:self]; [super dealloc]; }
So, maybe this is a bit of a pain to set up, but it can be pretty useful in other situations as well (like discovering which instances of
MySpriteare within a certain distance of a point).Then, you implement the three methods from above in your
Sceneobject, and use it to handle and route clicks.- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint location = [touch locationInView: [touch view]]; NSArray * mySprites = [MySprite allMySprites]; NSUInteger i, count = [mySprites count]; for (i = 0; i < count; i++) { MySprite * obj = (MySprite *)[mySprites objectAtIndex:i]; if (CGRectContainsPoint([obj rect], location)) { // code here is only executed if obj has been touched } } }
The advantage of this approach is that you have an extremely granular level of control over input management. If you only wanted to perform actions on touches that touch two instances of
MySprite, you could do that. Or you could only perform actions when a certain global condition is activated, and so on. This approach lets you make decisions at the point in your application that has the most information.But it can get unwieldy depending on the type of logic you want to implement for your user input management. To help control that, I usually roll a simple system for user input modes.
The implementation depends on your specific app, but you'd start by subclassing
NSObjectinto aUIModeobject.@interface UIMode : NSObject {} -(id)init; -(void)setupWithObject: (id)anObject; -(void)tearDown: (UIMode *)nextMode; -(void)tick: (ccTime)dt; -(BOOL)touchBeganAt: (CGPoint)aPoint; -(BOOL)touchMovedAt: (CGPoint)aPoint; -(BOOL)touchEndedAt: (CGPoint)aPoint; @end
The implementation of all those classes for
UIModeshould be inert stubs that can then be overridden in subclasses as appropriate. My system is to have thetouch?Atmethods returnYESif they decide to handle a specific touch, and otherwise returnNO. This lets user interface modes implement custom logic, or to let a touch pass on to your default touch handling.Next update the interface for your subclass of
Scenelike this:@interface MyScene : Scene { UIMode * currentMode; } -(UIMode *)currentMode; -(void)setCurrentMode: (UIMode)aMode;
Then, in your implementation you'd add some code along these lines:
-(UIMode *)currentMode { return currentMode; } -(void)setCurrentMode: (UIMode *)aMode { if (currentMode != nil) { // this tearDown method is part of the imagined // UIMode class, and lets a UIMode disable itself // with knowledge of the subsequent UIMode for proper // transitions between modes [currentMode tearDown:aMode]; [currentMode release]; } currentMode = [aMode retain]; }
Finally, you'd need to update the
touchesBegan:withEventmethod to query theUIModewhether it wants to handle each specific click.- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint location = [touch locationInView: [touch view]]; // forward the specified location to the UIMode, and abort // standard click handling if the UIMode decides to handle // the click UIMode * uim = [self currentMode]; if (uim != nil && [uim touchBeganAt:location]==YES) return; NSArray * mySprites = [MySprite allMySprites]; NSUInteger i, count = [mySprites count]; for (i = 0; i < count; i++) { MySprite * obj = (MySprite *)[mySprites objectAtIndex:i]; if (CGRectContainsPoint([obj rect], location)) { // code here is only executed if obj has been touched } } }
This is the approach I prefer, because it is fairly simple, and allows an extremely high amount of flexibility. I realize that I dumped a ton of code here, and apologize. Hopefully you can still find the thread of thought intertwined into the jumble.
Bottom-up global input management. I won't provide much code for this approach, as it isn't one that I use, but it's a compromise between the first and second approaches.
For each instance of some
MySpriteclass, override thetouchesBegan:withEvent:(and moved and ended variants as well, if you want them) method, and then notify a global object about the touch occuring.It would look something like this:
-(void)touchesBegan: (NSSet *)touches withEvent: UIEvent *)event { CurrentScene * s = [self currentScene]; // Not a real method. [s mySpriteTouched:self]; }
Of course, this means you'd need to pass a reference to the current scene to each instance of
MySprite, or you can use a singleton to simplify.static CurrentScene *sharedScene = nil; +(CurrentScene *)sharedScene { @synchronized(self) { if (sharedScene = nil) [[self alloc] init]; } } return sharedGame; } +(void)releaseSharedScene { @synchronized(self) { if (sharedScene != nil) [sharedScene release]; sharedScene = nil; } } +(id)allocWithZone: (NSZone *)zone { @synchronized(self) { if (sharedScene = nil) { sharedScene = [super allocWithZone:zone]; return sharedScene; } } return nil; } -(id)retain { return self; } -(unsigned)retaiCount { return UINT_MAX; } -(void)release {} -(id)autorelease { return self; }
The code is a bit of a clusterfuck, in my humble opinion, but it is still quite convenient, as it allows us to convert the
touchesBegan:withEventmethod to this:-(void)touchesBegan: (NSSet *)touches withEvent: UIEvent *)event { [[CurrentScene sharedScene] mySpriteTouched:self]; }
And we don't have to explicitly pass the reference to the
CurrentSceneinstance to each instance ofMySprite. Objective-C has a lot of these painful pieces of code that are rather annoying to implement, but can save a lot of effort once they are implemented. My advice is to use them, early and infrequently.
Well, there you have it, three approaches to handling touch detection for Cocos2d iPhone, presented in a confusing and at most halfway organized article.
Let me know if you have any questions, but I hope this is enough to get you moving in the right direction.
Mostly because I wasn't perfectly clean on the topic myself, this article was slightly incomplete. First, the object to subclass is not
CososNode, but instead you must subclass Layer for touch detection.Second, you need to modify your
Layersubclassesinitmethod to have the lineisTouchEnabled = YES;. With that you shouldn't have any trouble getting it working. I'll have some complete example code in the next day or two.Thank you for the great article but...
Please! Change the article, I've been spending hours finding out why it would not respond to touches... Only to find out you made the corrections in the comments.
Regards,
Maarten
Great article!
I have an observation about using CGRectContainsPoint to try to detect a Sprite though...
I don't think the pseudo-code above will work for detecting your MySprite object. There is no 'rect' support either for Sprite or for NSObject - I think because the Sprites are able to be scaled and rotated so a trivial rectangle can not be easily resolved.
To detect if your point is within a Sprite's bounds you would need to multiply it through the transformation matrix to do it properly.
It may be a good addition to your MySprite class to add the 'rect' operation?
I'm pretty new to objective-c/cocoa/iphone programming here but have found the few resources I have found for cocos2d to be pretty helpful. I'm trying to get the example above to work.. I subclassed Sprite and implimented a simple rect method like this..
This returns a rect, but my limited knowledge is preventing me from debugging it. I'm trying to capture a touch inside the sprite using the CGRectContainsPoint method descibed.. I have an NSLog telling me that the sprite was touched, but it registers in what seems like the same SIZED box, but not where the sprite actually is.
Can anyone help me by telling me how I might 'trace' the CGRect so I can see exactly where it's placing it in relation to my sprite or maybe if they know why the CGRect and my sprite might be in different locations? It almost seems like 0,0 is not in the same places for the Sprite and the CGRect.. Thanks!
Solved my own problem! Seems a UIKit point and an OpenGL point aren't quite the same spot.. Found this method in the Director class..
That seems to have cleared up the top left, bottom left problem.. Hope it helps anyone else..
In version 0.6.2 of the cocos2d iPhone library you must use the "cc" versions of the event handlers.
Example:
-(BOOL)ccTouchesBegan:(NSSet )touches withEvent:(UIEvent )event;
rather than
-(void)touchesBegan:(NSSet )touches withEvent:(UIEvent )event;
Also, you should return kEventHandled if you handled the event or kEventIgnored if you did not handle the event.
Thanks Jeremie for clearing up the ccTouchesBegan problem!! I was having trouble running the example on 0.6.2. Glad I found this post thanks!
Ok.. New problem.. I've completely dumbed down the touch detection to JUST the following method in my layer..
I've tried with and without backgrounds.. there are no other sprites in the layer.. just a blank layer. In the simulator, this works fine. I get a "Touched!" every time I touch the simulator in my log.. On my iPhone 3G, however, it catches the first touch.. but then hangs there. I can tap to my hearts content but will not see another "Touched!" in my log until I press the home button (at which time, I'll see every "Touched!" I didn't see while the program was running) ... I've been trying to figure this out all night and can't for the life of me. Any ideas?
Hi and thanks for all these snippets :)
If you have the "one shot touch only" problem, it seems to be because of the use of pop/push scene. You must use replaceScene when you load your scene. It worked for me, as my layer did respond correctly in the simulator and not on the device. Now it's ok :)
Bye and long live cocos !!! Using it from one week now and already loving it !
I'm having the same "one shot touch only" problem as described above, and I'm not using pop/push scence, only replaceScene. Anyone have additional suggestions?
Interestingly enough, the problem goes away when I just start GameScene directly instead of starting MenuScene and then having MenuScene start GameScene.
Check out this link for the touch lockup issue
Hello,
I'm trying to implement touch detection on a sprite. Your original post seems to say (in the 1st scenario) that simply subclassing Sprite (which is a subclass of CocosNode if I'm not mistaken) and implementing the touchesBegan mehod should work.
However, this does not seem to work for me. If this is indeed valid, could you post an example of a simple sprite subclass implementing the touchesBegan etc. methods ?
On the other hand , one of the comments mentions that only Layers + Layer subclasses work with touch events in which case, what I wanted to do is not possible and only the 2nd method would work. Is this correct instead?
I'm a bit apprehensive with regards to the second method of handling touch events especially as far as performance is concerned (with many Sprites etc.). Is this a valid concern? or am I thinking too "high-level" as far as the framework is concerned and I shouldn't expect touches to be propagated to the relevant sprite by default?
Lastly, if the case is that only Layers can receive the touch events, why isn't the rect() method implemented by default in Sprites etc. ?
I'm a beginner in iphone programming/ObjC so apologies if any of this is obvious.
Thanks,
Alex
Hi...
Thank you for updating the cocos2d tutorial on monoclestudios.com for version 0.7.0 ...
Hi All, i am trying to move sprite (car) left and right as it's center initially ,i wrote the code it's moving left and right but i need to move only in next line (assume i am using three lanes and car is middle lane when i use tuch move left it should move only exatelly the next lane) could any one help me how to do this. thanks for advance
bhanu
For those that are having issues sub-classing Sprite I have solved all of the issues.
First off, instead of overriding init, you will need to overwrite the individual methods, for me I am using initWithFile, that is where you will need to call the track method.
Secondly, the rect is not part of Sprite so you will need to create your own rect property. Also as mentioned above the coordinates are slightly wierd so you will need to convert them using the Director.
Thirdly, if you do not return kEventIgnored and kEventHandled properly some of your clicks may get ignored or missed.
I have tested this with the ability to drag a sprite across the screen and the coordinate translation I have come up seems to work perfectly. I am still a bit baffled as to why I had to do what I did to get the coordinates to match up, perhaps someone could explain the reason.
Hope this helps someone, took me quit a bit of trial and error to arrive at this solution.
hi i am trying to animate a .png file which has several images on it in a series for animation however i am not able to clip the images and then use them...there is no method that i have discovered yet that helps me with clipping in cocos2d
thanks n regards .....
@Eric Malamisura
From what I have read, you have to convert the point because cocos2d uses x & y in relation to the bottom left of the screen, whereas the standard location is from the upper left.
I'm having some problems with defining a rect for my sprite. The sprite is created using the spriteWithFile function, which displays fine. However, when running [sprite contentSize].x (or y actually) the returned value is zero.
Has anyone experienced this?
@Richard CGSize has width & height elements, not x & y ... perhaps that's your issue? Or maybe just a post typo and your code uses height & width.
A sprites position is its center, while the rectangle wants the origin. That's why your rect is of...
Reply to this entry