I'm having some trouble with an algorithm for a player controller I'm writing. I'm using Cocos2D to make a platformer style game (think Super Mario Bros.). My controller has a left and right button, and you touch the right half of the screen to jump.
The problem I'm having is when you're holding the right or left button, then hold the screen to jump and let go of the directional button, the player will continue to go in that direction until the user lifts their finger off the screen.
The other problem is if the user holds the screen to jump, taps a directional button then lets go of the directional while still holding the screen, the player will continue in that direction. Half the time the player stops on release of the screen, the other half the player will continue going on release until the screen is tapped again.
This is my code to register which part of the controller are tapped:
- (void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch* touch in touches)
{
CGPoint point = [self convertTouchToNodeSpace: touch];
// Create a rect around right half of the window
CGSize winSize = [CCDirector sharedDirector].winSize;
CGRect jumpBox = CGRectMake(winSize.width / 2,
0,
winSize.width / 2,
winSize.height);
// left button
if (CGRectContainsPoint(leftButtonSprite.boundingBox, point))
{
self.isPressingLeftButton = YES;
self.isPressingRightButton = NO;
}
// right button
else if (CGRectContainsPoint(rightButtonSprite.boundingBox, point))
{
self.isPressingRightButton = YES;
self.isPressingLeftButton = NO;
}
// player's trying to jump
if (CGRectContainsPoint(jumpBox, point))
{
self.isTouchingScreen = YES;
}
}
}
- (void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch* touch in touches)
{
CGPoint point = [self convertTouchToNodeSpace: touch];
// Create a rect around right half of the window
CGSize winSize = [CCDirector sharedDirector].winSize;
CGRect jumpBox = CGRectMake(winSize.width / 2,
0,
winSize.width / 2,
winSize.height);
// left button
if (CGRectContainsPoint(leftButtonSprite.boundingBox, point))
{
// CCLOG(#"Pressing left");
self.isPressingLeftButton = YES;
self.isPressingRightButton = NO;
}
// right button
else if (CGRectContainsPoint(rightButtonSprite.boundingBox, point))
{
// CCLOG(#"Pressing right");
self.isPressingRightButton = YES;
self.isPressingLeftButton = NO;
}
// player's trying to jump
if (CGRectContainsPoint(jumpBox, point))
{
// CCLOG(#"Jumping");
self.isTouchingScreen = YES;
}
}
}
- (void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
if (self.isTouchingScreen)
{
CCLOG(#"Stop jumping");
self.isTouchingScreen = NO;
}
else
{
CCLOG(#"Stop moving");
self.isPressingLeftButton = NO;
self.isPressingRightButton = NO;
}
}
This code is in my HUDLayer.m which is a child in my GameplayLayer.m. GameplayLayer uses update: to figure out which of the flags from HUDLayer is set and moves the player accordingly
Figured it out. Kind of a n00b mistake. In ccTouchesEnded:withEvent:, I had to convertTouchToNodeSpace and check if it intersects the rects of the directional buttons or jump half of the screen as done in ccTouchesBegan:withEvent:
Hope this helps someone some day. Happy coding :)
Related
At the moment, I have a puzzle game, with some objects that are movable using the ccTouchesBegan && ccTouchesMoved method
Called in ccTouchesBegan:
if (CGRectContainsPoint(newBoundingBox, touchLocation))
{
//Set the selectedSprite to the sprite with newBoundingBox
}
Then in ccTouchesMoved i do:
CGPoint translation = ccpSub(touchLocation, oldTouchLocation);
newPos = ccpAdd(selSprite.position, translation);
selectedSprite.position = newPos;
Now this all works fine and dandy, HOWEVER, some of the objects that I am moving are about 140x40px (on the 320x480 iPhone), and sometimes the ccTouchesBegan method won't acknowledge the boundingBox containing the touchLocation.
My question is, Can I increase the size of the boundingBox of the object so that if the user "misses" with there touch a little bit, it will help them out? It's a little frustrating requiring you to be so precise on a touch screen to move an obstacle.
EDIT: for iPhonic:
How I get touchLocation, this is in ccTouchesBegan:
NSSet *touchSet = [event allTouches];
int numberOfTouches = [touchSet count];
CGPoint touchLocation = [self convertTouchToNodeSpace:[touches anyObject]];
allTouches is passed automatically when the a touch begins.
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint location = [self convertTouchToNodeSpace: touch];
for (CCSprite *tempSprite in sprites )
{
//Increase the size of bounding box..
CGRect rect=tempSprite.boundingBox;
rect.size.height=rect.size.height + 20;
rect.size.width=rect.size.width + 20;
if (CGRectContainsPoint(rect, location))
{
//Your stuffs
}
}
}
I am creating a program with cocos2d where I have 2 ccMenuItems each linked to an image: In this case, one is a left arrow, the other is a right arrow. I also have an image in the center of my view that will rotate depending on the arrow pressed.
When I put my finger on one of the two menu items, either left or right, I want the center image to rotate as long as my finger is on the button. That is where I got lost. I tried using the following code:
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint p = [touch locationInView:[touch view]];
if (CGRectContainsPoint(leftArrow, p) || CGRectContainsPoint(rightArrow, p)) {
return YES;
}
return NO;
}
- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint p = [touch locationInView:[touch view]];
if (CGRectContainsPoint(leftArrow, p)) {
[gun runAction:[CCRepeatForever actionWithAction:[CCRotateTo actionWithDuration:0.2 angle:180]]];
}
if (CGRectContainsPoint(rightArrow, p)) {
[gun runAction:[CCRepeatForever actionWithAction:[CCRotateTo actionWithDuration:0.2 angle:0]]];
}
}
Using this code, when I press on one of the two menu items, the ccTouchBegan method isn't even called. The method is only called when I touch elsewhere.
How do I work with continuous actions when holding down a ccMenuItem.
Thank you for your help!
You have to subclass CCMenuItem to get such behaviour:
////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////
#interface RepeatMenuItem : CCMenuItemSprite
{
CGFloat speed;
}
#end
And implementation:
////////////////////////////////////////////////////////////////////
// RepeatMenuItem
////////////////////////////////////////////////////////////////////
#pragma mark -
#pragma mark RepeatMenuItem
#implementation RepeatMenuItem
-(void) selected
{
[super selected];
block_(self);
speed = 0.8;
[self schedule:#selector(repeatEvent:) interval:speed];
}
-(void) unselected
{
[self unschedule:#selector(repeatEvent:)];
[super unselected];
}
-(void) activate
{
}
-(void) repeatEvent:(id)sender
{
CGFloat minSpeed = 0.05;
if (speed > minSpeed)
speed = speed/3;
if (speed < minSpeed)
speed = minSpeed;
[self unschedule:#selector(repeatEvent:)];
block_(self);
[self schedule:#selector(repeatEvent:) interval:speed];
}
#end
As I understand, two methods you posted are in your CCLayer's subclass.
First of all, if you want to handle touches by yourself, you must remove menu items. CCMenu has much more touch priority and if it handles that the touch hit any menu item, it will swallow it. Thats why you receive touches only when touch is outside menu items.
The second one, what is leftArrow and rightArrow? Rects of your arrows?
And as for me, in such case I can propose you to run update method and change rotation property of your center image in it on every tick. I mean, that rotation value will be 0 if no one arraw was touched and will have positive or negative value due to the arrow you touched.
You can extend the CCMenuItemWhatever class you are using for these menu items, and override the 'selected' and 'unselected' methods. The menu grabs the touches (ie IT answers YES on its own ccTouchBegan function, thus the touch dispatcher does not propagate it to you).
-(void) selected{
[super selected];
// start your animation here
}
-(void) unselected {
[super unselected];
// stop your animation here.
}
Here is my movement method:
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
location = [touch locationInView: [touch view]];
CGPoint location_ = [[CCDirector sharedDirector] convertToGL:location];
NSLog(#"Click Position = (%f,%f)",location_.x,location_.y);
float moveSpeed = 40.0;
float moveDist = sqrt(pow(abs(location_.x - sprite.position.x),2) + pow(abs(location_.y - sprite.position.y),2));
float moveTime = (moveDist / moveSpeed);
[sprite runAction:[CCMoveTo actionWithDuration:moveTime position:location_]];
}
And here's my init method.
-(id) init
{
// always call "super" init
// Apple recommends to re-assign "self" with the "super" return value
if( (self=[super init])) {
self.isTouchEnabled = YES;
// create and initialize a Label
[self scheduleUpdate]; // available since v0.99.3
[self schedule: #selector(update:) interval:0.5];
CGSize size = [[CCDirector sharedDirector] winSize];
bg = [CCSprite spriteWithFile:#"testBG.png"];
bg.position = ccp( size.width /2 , size.height/2 );
[self addChild: bg];
sprite = [CCSprite spriteWithFile:#"testSprite.png"];
sprite.position = ccp( size.width /2 , size.height/2 );
[self addChild: sprite];
[self runAction:[CCFollow actionWithTarget:sprite]];
}
return self;
}
My window follows the sprite around, but like I said, the sprite will go to a different point than touched after the first touch. Can anyone tell me what's going on?
edit: I'm thinking that it may have to do with the co-ordinate system itself, or maybe it's to do with
[touches anyObject];? My searches have turned up little.
edit2: I've found that if I return my sprite back to the middle of the bg sprite again, it'll behave normally again from there, until it's too far off.
I believe the problem is with [self runAction:[CCFollow actionWithTarget:sprite]];.
selfis your Scene that you don't want to move around.
The pattern for doing this would be to add a CCNodethat you would call a your _contentNodeto your sceneand add everything you would like to move as a child.
Then you would call [_contentNode runAction:[CCFollow actionWithTarget:sprite]];
IN this I'll suggest you a simple thing... Before adding a new action, stop the previous action on the sprite...
In your ccTouchesBegan method
[sprite stopAllActions];
[sprite runAction:[CCMoveTo actionWithDuration:moveTime position:location_]];
I think this will help as you sprite will stop once you touch..
In current case its undergoing an action.. Suppose its duration is 2 sec. and on next touch you run action of 1 sec. Then it will go to wrong point as previous action is still not completed.
Hope this helps.. :)
If you have a normal UITextView with loads of text in it you can swipe up and down:
if you swipe left or right in a very horizontal line, nothing will happen
if you swipe left or right with a very slight angle, you will swipe up or down
So there is obviously some kind of class definition which tells the UITextView when to react to a swipe and when not to. Something like a maximumVariance constant, I guess.
My problem is that I'm trying to subclass an UITextView to enable left/right swipe. However, you will need to swipe in a pretty straight line, otherwise, you'll get an up or down swipe. So my question is if there is someway to re-define the standard variance variable of an UITextView?
Here is my subclass .m file. Note that the kMaximumVariance doesn't really have any effect as this is overwritten by whatever variance the standard UITextView class provides:
//
// SwipeTextView.m
// Swipes
//
#import "SwipeTextView.h"
#import "PageView.h"
#define kMinimumGestureLength 15
#define kMaximumVariance 100
#implementation SwipeTextView
#synthesize delegate, gestureStartPoint;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
UITouch *touch =[touches anyObject];
gestureStartPoint = [touch locationInView:self];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
[self resignFirstResponder]; // this hides keyboard if horizonal swipe
UITouch *touch = [touches anyObject];
CGPoint currentPosition = [touch locationInView:self];
CGFloat deltaXX = (gestureStartPoint.x - currentPosition.x); // positive = left, negative = right
CGFloat deltaYY = (gestureStartPoint.y - currentPosition.y); // positive = up, negative = down
CGFloat deltaX = fabsf(gestureStartPoint.x - currentPosition.x); // will always be positive
CGFloat deltaY = fabsf(gestureStartPoint.y - currentPosition.y); // will always be positive
NSLog(#"deltaXX (%.1f) deltaYY (%.1f) deltaX (%.1f) deltaY (%.1f)",deltaXX, deltaYY, deltaX, deltaY);
if (deltaX >= kMinimumGestureLength && deltaY <= kMaximumVariance) {
if (deltaXX > 0) {
NSLog(#"Horizontal Left swipe - FINISHED detected");
}
else {
NSLog(#"Horizontal Right swipe - FINISHED detected");
}
}
}
#end
EDIT:
Suggestion to cancel touches reaching UITextView (see below):
// Prevent recognizing touches
- (BOOL)gestureRecognizer:(UISwipeGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if ([gestureRecognizer direction] == UISwipeGestureRecognizerDirectionLeft) {
// Left swipe: I want to handle this
return YES;
}
if ([gestureRecognizer direction] == UISwipeGestureRecognizerDirectionRight) {
// Right swipe: I want to handle this
return YES;
}
if ([gestureRecognizer direction] == UISwipeGestureRecognizerDirectionUp) {
// Up swipe: textView needs to handle this
return NO;
}
if ([gestureRecognizer direction] == UISwipeGestureRecognizerDirectionDown) {
// Down swipe: textView needs to handle this
return NO;
} else
return YES;
}
-(void)viewDidLoad
{
UITapGestureRecognizer *GesRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(doSomething:)];
[GesRec setCancelsTouchesInView:NO];
[self addGestureRecognizer:GesRec];
[GesRec release];
}
-(void)doSomething
{
NSLog(#"doSomething");
}
Try adding an UISwipeGestureRecognizer to the view and set cancelsTouchesInView to false so the swipe gesture is not passed through. This should let you detect left and right motion more accurately.
If this doesn't do the trick, implement your own subclass of UIGestureRecognizer and add that instead.
Edit
You add the gesture recognizer to the UITextView in your controller's viewDidLoad method.
In your delegate methods, you can't return NO in gestureRecognizer:shouldReceiveTouch: because it means the gesture recognizer won't examine the incoming touch object. In your case you want to return YES when the swipe is horizontal and NO otherwise, so vertical swipes are passed through while horizontal swipe is completed handled by you.
No subclass needed. set textView.userInteractionEnabled to NO and than put a transparent UIView over your textView. handle touches and gestures anyway you want on that UIView and make the textView respond to these programatically.
I am trying to let the finger drag CCSprite with the following code:
-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint location = [touch locationInView: [touch view]];
CGPoint convertedCoordinate = [[CCDirector sharedDirector] convertToGL:location];
CGRect redCircleRect = CGRectMake(redCircle.position.x, redCircle.position.y, redCircle.contentSize.width, redCircle.contentSize.height);
CGRect redCircle2Rect = CGRectMake(redCircle2.position.x, redCircle2.position.y, redCircle2.contentSize.width, redCircle2.contentSize.height);
if(CGRectContainsPoint(redCircleRect, convertedCoordinate)) {
//redCircle.position = ccp(convertedCoordinate.x, convertedCoordinate.y);
NSLog(#"Touched");
spriteIndex = 1;
}
else if(CGRectContainsPoint(redCircle2Rect, convertedCoordinate)) {
//redCircle2.position = ccp(convertedCoordinate.x, convertedCoordinate.y);
NSLog(#"Touched2");
spriteIndex = 2;
}
else {
spriteIndex = 0;
}
return YES;
}
-(void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint location = [touch locationInView: [touch view]];
CGPoint convertedCoordinate = [[CCDirector sharedDirector] convertToGL:location];
if (spriteIndex == 1) {
redCircle.position = ccp(convertedCoordinate.x, convertedCoordinate.y);
}
else if (spriteIndex == 2) {
redCircle2.position = ccp(convertedCoordinate.x, convertedCoordinate.y);
}
//CGRect RedCircleRect = CGRectMake(redCircle.position.x, redCircle.position.y, redCircle.contentSize.width, redCircle.contentSize.height);
//CGRect RedCircle2Rect = CGRectMake(redCircle.position.x, redCircle.position.y, redCircle.contentSize.width, redCircle.contentSize.height);
}
My logic is as follows:
In ccTouchesBegan I get the location of the touch, convert it to the right coordinates, then make two CGRect's that are, supposedly, the same dimensions and location of the two sprites I have declared. Then, I check if the finger is touching one of those two rectangles using CGRectContainsPoint. In there, I have a global int called spriteIndex that I assign a value according to what circle was touched.
Next, in ccTouchesMoved I check if spriteIndex has been given the "index" of one of the two sprites I am trying to move, and if so, set the location of the sprite that is selected to the touch location.
The Problem:
This code only sort of works. It lets me drag the CCSprites, but only if I click the center. And the sprite is 40 by 40 pixels, so it is by no means small.
Found the answer on another forum:
CGRect redCircleRect = CGRectMake(redCircle.position.x, redCircle.position.y, redCircle.contentSize.width, redCircle.contentSize.height);
CGRect redCircle2Rect = CGRectMake(redCircle2.position.x, redCircle2.position.y, redCircle2.contentSize.width, redCircle2.contentSize.height);
should be:
CGRect redCircleRect = CGRectMake(redCircle.position.x-20.0, redCircle.position.y-20.0, 40.0, 40.0);
CGRect redCircle2Rect = CGRectMake(redCircle2.position.x-20.0, redCircle2.position.y-20.0, 40.0, 40.0);
This is because, the CGRect copies are being made from the center pointer where it should be made from the lower left.