Cocos2d-iphone v3 touch event detection on CCNode with children - ios

I have a CCNode that contains multiple CCSprite children.
I would like to receive touch events in my parent CCNode if any of the children have been touched.
This behaviour seems like it should be supported, I may be missing something.
My solution is to setUserInteractionEnabled = YES on all children and bubble the event up to the parent.
I do this by subclassing the CCSprite class overriding their method :
- (void) touchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
[super touchBegan:touch withEvent:event];
}
I am wondering if there is a more elegant, simple and generic way of accomplishing the same behaviour ?

You could override hitTestWithWorldPos: of your 'containing' node, either calling hitTestWithWorldPos on specific children or, iterating through all children as you see fit. Perhaps something like this:
-(BOOL) hitTestWithWorldPos:(CGPoint)pos
{
BOOL hit = NO;
hit = [super hitTestWithWorldPos:pos];
for(CCNode *child in self.children)
{
hit |= [child hitTestWithWorldPos:pos];
}
return hit;
}
edit: just to be clear, then you would only need to setUserInteractionEnabled for the container, and only process the touch using the touch events of the containing node.
edit2:
so, I thought about it for a bit more and here's a quick category you can add that will enable a quick hit test for all children of a node recursively.
CCNode+CCNode_RecursiveTouch.h
#import "CCNode.h"
#interface CCNode (CCNode_RecursiveTouch)
{
}
-(BOOL) hitTestWithWorldPos:(CGPoint)worldPos forNodeTree:(id)parentNode shouldIncludeParentNode:(BOOL)includeParent;
#end
CCNode+CCNode_RecursiveTouch.m
#import "CCNode+CCNode_RecursiveTouch.h"
#implementation CCNode (CCNode_RecursiveTouch)
-(BOOL) hitTestWithWorldPos:(CGPoint)worldPos forNodeTree:(id)parentNode shouldIncludeParentNode:(BOOL)includeParent
{
BOOL hit = NO;
if(includeParent) {hit |= [parentNode hitTestWithWorldPos:worldPos];}
for( CCNode *cnode in [parentNode children] )
{
hit |= [cnode hitTestWithWorldPos:worldPos];
(cnode.children.count)?(hit |= [self hitTestWithWorldPos:worldPos forNodeTree:cnode shouldIncludeParentNode:NO]):NO; // on recurse, don't process parent again
}
return hit;
}
#end
usage would just be .. in the containing class, override hitTestWithWorldPos like this:
-(BOOL) hitTestWithWorldPos:(CGPoint)pos
{
BOOL hit = NO;
hit = [self hitTestWithWorldPos:pos forNodeTree:self shouldIncludeParentNode:NO];
return hit;
}
and of course, don't forget to include the category header.

-(void) touchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
//Do whatever you like...
//Bubble the event up to the next responder...
[[[CCDirector sharedDirector] responderManager] discardCurrentEvent];
}

Related

CCButton not working while touchMoved Implemented

I have to place functionality where CCButton can be dragged to proper position, wherever user needs to place them.
I have create a custom class for this but the issue is click method of the button is not being called when user clicks on the button.
touchyButton.h
#import "cocos2d.h"
#import "cocos2d-ui.h"
#interface touchyButton : CCButton { ... }
#property (nonatomic, assign) BOOL touchMoved;
#end
touchyButton.m
#import "touchyButton.h"
#implementation touchyButton
- (void) touchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(#"touchMoved...", nil);
self.touchMoved = YES;
self.anchorPoint = ccp(0.5, 0.5);
CGPoint touchLoc = [touch locationInNode:self.parent];
//CGPoint inTouchLoc = [self convertToNodeSpace:self.anchorPoint];
//CGPoint touchP = ccpAdd(touchLoc, inTouchLoc);
//self.position = [self.parent convertToNodeSpace: touchP];
self.position = touchLoc;
}
- (void) touchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
self.touchMoved = NO;
NSLog(#"touchEnded...", nil);
}
#end
As the code explains, we are just trying to move the button wherever on the screen when user drags on the button.
Calling of the button in main code where it needs to be displayed.
touchyButton *btnRight = [touchyButton buttonWithTitle: #"" spriteFrame:[[CCSprite spriteWithImageNamed: #"arrR.png"] spriteFrame]];
[btnRight setBackgroundOpacity:0.5f forState: CCControlStateNormal];
[btnRight setAnchorPoint: ccp(1, 0.5)];
[btnRight setPosition: ccp(viewS.width - 10.f, viewS.height/2)];
[self addChild: btnRight];
[btnRight setTarget:self selector: #selector(performRightJump:)];
Now, when user clicks on the button, the button goes into selected state but performRightJump never fires. Can anyone suggest any alternative how I can implement the button with dragging behaviour with target action working..? Any hint would be appreciated as well.
One more thing is, in current code I can only be able to move the button's anchor point to the new touch point. Any idea how I can move the button in real fashion? The current method causes problem of tapping of first time for move, button's anchor point jumps to the tapped point.
this code is cocos2dx but useful for you
create a new class classA and classB
add this code
classA.h
{
//here implement
CC_SYNTHESIZE_RETAIN(classB *, _ classB, _ ClassB);
}
classA.cpp
bool classA::init()
{
set_classB(classB::initwithclass());
this->addChild(get_ClassB(),0);
//here create button
return true;
}
bool classA:: onTouchBegan (Touch *touch ,Event *event)
{
_classB->setposition(touchLoc);
return true;
}
void classA:: onTouchMoved(Touch *touch, Event *event)
{
}
void classA:: onTouchEnded(Touch *touch ,Event *event)
{
}
//as well as this code use in cocos2d

Detecting a touch anywhere on the screen

I am wanting to know when a user has touched anywhere on the screen of my app.
I have looked into using -(UIResponder *)nextResponder but unfortunately this will not work, as I am also reloaded a table automatically, so this gets trigged when that occurs.
I have also tried a gesture recognizer, with the following code. But this will only recognise touches on the view. Where as I have many buttons the user will be using to operate the app. I would like to avoid adding a gesture recogniser or code for this in every button and segment control I have on the screen
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapOnView:)];
[self.mainView addGestureRecognizer:tap];
- (void)tapOnView:(UITapGestureRecognizer *)sender
{
//do something
}
I have also tried -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event , but this has the same issue as the gesture recognizer.
I was wondering if there is any way I could achieve this task. I was hoping that I may be able to recognise the type of event from within the nextResponder, and then I could detect if it is button for example.
EDIT: The reason I am working on this is that my app needs to stay active and the screen cannot be locked (so I have disabled screen locking). To avoid excessive use of power, I need to dim the screen, but then return the brightness back to the original level once the app is touched. I need this feature to only occur on 1 of my viewcontrollers.
As mentioned by Ian MacDonald, using hitTest:: is a great solution to detect user interaction on an app wide scale, including when buttons, textfields, etc, are selected.
My solution was to subclass UIWindow and implement the hitTest method.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// do your stuff here
// return nil if you want to prevent interaction with UI elements
return [super hitTest:point withEvent:event];
}
You could attach your UITapGestureRecognizer to your [[UIApplication sharedApplication] keyWindow].
Alternatively, you could override hitTest: of your root UIView.
Is there a particular task you are hoping to accomplish? There may be a better way than assigning an "anywhere" gesture.
Edit: Use hitTest:.
#interface PassthroughView : UIView
#property (readonly) id target;
#property (readonly) SEL selector;
#end
#implementation PassthroughView
- (void)setTarget:(id)target selector:(SEL)selector {
_target = target;
_selector = selector;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
[_target performSelector:_selector];
return nil;
}
#end
#implementation YourUIViewController {
PassthroughView *anytouchView;
}
- (void)viewDidLoad {
// Add this at the end so it's above all other views.
anytouchView = [[PassthroughView alloc] initWithFrame:self.view.bounds];
[anytouchView setAutoresizingMask:UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight];
[anytouchView setTarget:self selector:#selector(undim)];
[anytouchView setHidden:YES];
[self.view addSubview:anytouchView];
}
- (void)undim {
[anytouchView setHidden:YES];
}
- (void)dim {
[anytouchView setHidden:NO];
}
#end
Your edit adds more clarity to your question.
The reason I am working on this is that my app needs to stay active
and the screen cannot be locked (so I have disabled screen locking).
To avoid excessive use of power, I need to dim the screen, but then
return the brightness back to the original level once the app is
touched.
Since you are controlling the screen brightness, you can add one transparent view controller before dimming screen on top of your root controller which does only one job, listen to tap using Tap gesture. And on tap you can dismiss the view controller and adjust brightness to previous state.
By doing so you dont have to worry about buttons being clicked as they will be below the transparent view controller. Since its a whole new view controller sitting on top of stack you dont have to modify your existing code as well.
Ok I have had a similar problem before.
As I remember I subclassed the UIWindow for full screen detection and made it First responder.
Than I overridden the touch to handle from subclasses.
You can also use code to identify the control that is been touched.
#import <QuartzCore/QuartzCore.h>
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view setMultipleTouchEnabled:YES];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Enumerate over all the touches
[touches enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
// Get a single touch and it's location
UITouch *touch = obj;
CGPoint touchPoint = [touch locationInView:self.view];
...
}];
}
To disable the locking of screen I used below code:
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];
I used following functions to dim or increase the screen brightness
[[UIScreen mainScreen] setBrightness:0.0f]; //and
[[UIScreen mainScreen] setBrightness:1.0f];

First Tap touch event

Hi Im making a cocos2d game that has a sprite flying and falling, Im trying to have a first tap touch event for example when the user touches the screen for the first time it'll start the game animation and start the physics engine. Whats happening is that when the user starts the game the sprite falls down right away, can anyone give me a hand with this?
right now Im using something like this but Im not sure how to get the physics engine to wait until the user touches the screen for the first time.
CCSprite *_pixie
CCNode *_start;
BOOL *_firstTap;
CCPhysicsNode *_physicsNode;
-(void)didLoadFromCCB{
_physicsNode.collisionDelegate = self;
_firstTap = True;
}
- (void)touchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
if(_firstTap == TRUE){
_start.visible = FALSE;
_firstTap = False;
}
//flying sounds & so on
if (!_gameOver) {
[[OALSimpleAudio sharedInstance] playEffect:MAGIC volume:0.4 pitch:1 pan:0 loop:NO];
[_pixie.physicsBody applyImpulse:ccp(0, 420.f)];
[_pixie.physicsBody applyAngularImpulse:11000.f];
_sinceTouch = 0.f;
}
}
- (void)update:(CCTime)delta {
if(_firstTap == FALSE){
float yVelocity = clampf(_pixie.physicsBody.velocity.y, -1 * MAXFLOAT, 200.f);
if ((_sinceTouch > .5f)) {
[_pixie.physicsBody applyAngularImpulse:-40000.f*delta];
}
}
}
Change
BOOL *_firstTap;
to
BOOL _firstTap; //No asterisk
And also make sure that you set _firsttap = YES in viewDidLoad functions
- (void)viewDidLoad
{
[super viewDidLoad];
_firstTap = YES;
}
Looks to me like the first touch Boolean value may not be defined until after the update code is called. You are also mixing BOOL values with bool values.
Objective-C : BOOL vs bool

how to disregard touch events in topmost uiview when it is clear and a different uiview can handle them

I have a clear UIView which has gesture recognizers attached to it.
This clear uiview covers the entire super view to allow for the gestures to be invoked from anywhere on it.
Under this clear UIView sit different components such as tables,buttons,collectionview etc.
The clear UIView has no idea what is under it any time.
What I want - if a view which is under the clear uiview can handle a touch event (or any type of gesture) - the clear view should disregard that event - and the event will pass through to the underlying view which could handle it.
I tried
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
but I don't know how to make sure the underlying view can handle it.
-(id)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
id hitView = [super hitTest:point withEvent:event];
if (hitView == self)
{
return nil;
}
else
{
return hitView;
}
}
Add this to your to clear view.
If the hit on clear view means just return nil.
You can override pointInside: withEvent: method. This method returns a boolean value indicating whether the receiver contains the specified point. So if we return NO then your upper clear view will become transparent for touch events and they will be passed to underlying views.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// Clear UIView will now respond to touch events if return NO:
return NO;
}
use below code for your case->
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *hitTestView = [super hitTest:point withEvent:event];
if(hitTestView!=nil){
//check for gesture
if([hitTestView.gestureRecognizers count]>0)
return hitTestView;
//if it is subclass of UIControl like UIButton etc
else if([hitTestView isKindOfClass:[UIControl class]])
return hitTestView;
//if can handle touches
else if([hitTestView respondsToSelector:#selector(touchesBegan:withEvent:)])
return hitTestView;
else
return nil;
}
else{
return self;
}
}
In the above code if the subView which is hitView can anyway handle touch ,we return that object to handle that touch. If there is no such hitTest view, then we return the view itself.
I used some of these suggestions and used the following solution:
I added the gesture recognizer to the bottom most superview in the heirarchy (and not the top most)
Then in that class over rid
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *v = [super hitTest:point withEvent:event];
// if v is nil then touch wasn't in this view or its subviews
if (v == nil)
{
return nil;
}
// in any case if the topview was hidden than return the default value
if (self.myTopView.hidden)
{
return v;
}
// if the view isn't hidden but the touch returned a control - than we can pass the touch to the control
if ([v isKindOfClass:[UIControl class]])
{
return v;
}
// decide on what threshold to decide is a touch
CGFloat threshHold = 40;
// if the touch wasn't on a control but could initiate a gesture than that view should get the touch
if (v.gestureRecognizers)
{
threshHold = 30;
// return v;
}
// check if the threshold should be bigger
if ([self someCondition])
{
threshHold = 100;
}
// threshold according to its position - this is the dynamic part
if (point.y > (self.myTopView.frame.origin.y - threshold))
{
return self.handleBarView;
}
return v;
}

iOS Multi-Touch Not Working

I have the regular OpenGL / EAGL setup going on:
#interface EAGLView : UIView {
#public
EAGLContext* context;
}
#property (nonatomic, retain) EAGLContext* context;
#end
#implementation EAGLView
#synthesize context;
+ (Class)layerClass {
return [CAEAGLLayer class];
}
#end
#interface EAGLViewController : UIViewController {
#public
EAGLView* glView;
}
#property(nonatomic, retain) EAGLView* glView;
#end
#implementation EAGLViewController
#synthesize glView;
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
for (UITouch* touch in touches) {
CGPoint location = [touch locationInView:glView];
int index;
for (index = 0; index < gCONST_CURSOR_COUNT; ++index) {
if (sCursor[index] == NULL) {
sCursor[index] = touch;
break;
}
}
}
[super touchesBegan:touches withEvent:event];
}
That implementation includes corresponding touchesEnded/Canceled/Moved as well. The code fully works and tracks well.
I also make sure that I'm giving proper values for everything:
sViewController = [EAGLViewController alloc];
CGRect rect = [[UIScreen mainScreen] applicationFrame];
sViewController.glView = [[EAGLView alloc] initWithFrame:CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height)];
Assert(sViewController.glView);
sViewController.glView.userInteractionEnabled = YES;
sViewController.glView.multipleTouchEnabled = YES;
sViewController.glView.exclusiveTouch = YES;
It all compiles just fine, but I'm never receiving more than one UITouch. I don't mean in a single touchesBegan, but the index never goes past 0. I also set a breakpoint for the second time it enters that function, and putting two fingers on doesn't make it trigger.
If you want to detect multiple touches (and/or distinguish between a one finger, two finger etc. touch), try using a UIPanGestureRecognizer. When you set it up, you can specify the minimum and maximum number of touches. Then attach it to the view where you want to detect the touches. When you receive events from it, you can ask it how many touches it received and branch accordingly.
Here's the apple documentation:
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIPanGestureRecognizer_Class/Reference/Reference.html
If you do this, you might not need to use the touchesBegan/Moved/Ended methods at all and, depending on how you set up the gesturerecognizer, touchesBegan/Moved/Ended may never get called.
Use [event allTouches] in place of touches. touches represents only the touches that have 'changed'. From the apple docs:
If you are interested in touches that have not changed since the last
phase or that are in a different phase than the touches in the
passed-in set, you can find those in the event object. Figure 3-2
depicts an event object that contains touch objects. To get all of
these touch objects, call the allTouches method on the event object.
It seems all I was missing was this:
sViewController.view = sViewController.glView;

Resources