I'm making a simple game using Cocos2D. The user can touch and trace a path for small moving objects. The objects then follow the path. This is similar to the Flight Control game mechanic.
My code works well almost all the time. However sometimes I attempt to touch and drag an object but the touch callbacks don't fire. Here's how my code works in general:
small moving objects inherit CCLayer
I'm using the touch callbacks that have a single touch parameter (ccTouchMoved:withEvent: etc)
all callbacks like ccTouchBegan:withEvent: are on the objects (instead of the main game layer)
How can I improve the touch handling such that the error noted above is avoided?
Here's my thoughts so far: Perhaps the touch callbacks are not firing because the objects (which are always moving) move slightly just around the time the user attempts to touch the object. This could be fixed by handling the touches on the main game layer using callbacks that have collections of touches as parameters. Since the total object count is relatively small (say less than 50) I can just compare each touch location with each object.
I also tried increasing the touchable area for the objects (note the 1.5 multiplication). This seemed to help but did not eliminate the problem.
- (BOOL) ccTouchBegan:(UITouch *) touch withEvent:(UIEvent *) event {
CGSize size = self.contentSize;
CGRect rect = CGRectMake(-size.width / 2.0,
-size.height / 2.0,
size.width * 1.5,
size.height * 1.5);
return CGRectContainsPoint(rect, [self convertTouchToNodeSpace:touch]);
}
Another thing to try would be increasing the speed of the objects and see if the callbacks don't fire more often. At the moment the objects move slowly.
Any help is much appreciated!
IIRC, node's origin is in its bottom left corner. Try
CGRect rect = CGRectMake(0, 0, size.width, size.height);
return CGRectContainsPoint(rect, [self convertTouchToNodeSpace:touch]);
Related
I'm trying to implement a paging interaction for one of my views. I thought I would just use UISwipeGestureRecognizers. But through trial and error as well as examination of the documentation it appears that
A swipe is a discrete gesture, and thus the associated action message
is sent only once per gesture.
So while I could trigger the page, I wouldn't be able to hook up animation that occurred during the drag.
Is my only alternative to use a UIPanGestureRecognizer and reimplement the basic filtering/calculations of the swipe?
Update/Redux
In hindsight, what I really should have been asking is how to implement a "flick" gesture. If you're not going to roll your own subclass (may bite that off in a bit), you use a UIPanGestureRecognizer as #Lyndsey 's answer indicates. What I was looking for after that (in the comments) was how to do the flick part, where the momentum of the flick contributes to the decision of whether to carry the motion of the flick through or snap back to the original presentation.
UIScrollView has behavior like that and it's tempting to mine its implementation for details on how one decelerates the momentum in a way that would be consistent, but alas the decelerationRate supplied for UIScrollView is "per iteration" value (according to some). I beat my head on how to properly apply the default value of 0.998 to the end velocity of my pan.
In the end, I used code pulled from sites about "flick" computation and did something like this in my gesture handler:
...
else if (pan.state == UIGestureRecognizerStateEnded) {
CGFloat v = [pan velocityInView: self.view].x;
CGFloat a = -4000.0; // 4 pixels/millisecond, recommended on gavedev
CGFloat decelDisplacement = -(v * v) / (2 * a); // physics 101
// how far have we come plus how far will momentum carry us?
CGFloat totalDisplacement = ABS(translation) + decelDisplacement;
// if that is (or will be) half way across our view, finish the transition
if (totalDisplacement >= self.view.bounds.size.width / 2) {
// how much time would we need to carry remainder across view with current velocity and existing displacement? (capped)
CGFloat travelTime = MIN(0.4, (self.view.bounds.size.width - ABS(translation)) * 2 / ABS(v));
[UIView animateWithDuration: travelTime delay: 0.0 options: UIViewAnimationOptionCurveEaseOut animations:^{
// target/end animation positions
} completion:^(BOOL finished) {
if (finished) {
// any final state change
}
}];
}
else { // put everything back the way it was
...
}
}
Yes, use a UIPanGestureRecognizer if you want the specific speed, angle, changes, etc. of the "swipe" to trigger your animations. A UISwipeGestureRecognizer is indeed a single discrete gesture; similar to a UITapGestureRecognizer, it triggers a single action message upon recognition.
As in physics, the UIPanGestureRecognizer's "velocity" will indicate both the speed and direction of the pan gesture. Here are the docs for velocityInView: method which will help you calculate the horizontal and vertical components of the changing pan gesture in points per second.
I'm guessing that it's to make a string out of individual CATextLayers and then position them as required along the curve, then animate. Because that's what's I've got working now, but it loses Kerning. Here's how:
Why isn't my curved text centering itself?
But is Core Text more performant and able to avoid the whole "drawing into a context" nonsense that slows everything down compared to the lean, mean Core Animation way of doing things, and respect kerning? i.e. avoiding drawRect: and all other aspects that greatly slow things down, as in this manner of drawing to the screen:
https://github.com/darcyliu/CocoaSampleCode/tree/master/CoreTextArcCocoa
Imagine a string of 200 characters, bent around a circle, with the ability to animate the spacing between characters, hopefully at a stable 60fps. This is possible with Core Animation, but that's by breaking the string up into individual characters and placing them around the circle with equal spacing, which causes a complete loss of kerning information.
I'm hoping for a way to do this without losing the kerning information and still being able to dynamically adjust spacing at 60fps.
Sure, you can do that. With iOS 7, you don't need to go all the way down to Core Text, though. NSLayoutManager can handle it in many cases. See the CurvyText demo that I wrote for iOS:PTL. You can drag all the control points around and see the text layout along the curve.
To see just how fast this layout can get in pure Core Text and Core Animation, see the PinchText demo from Rich Text, Core Text. This one shows how to adjust Core Text layout to respond to multi-touch, so the text seems to bend towards your fingers. It includes examples of how to animate with Core Animation to get smooth adjustments (and even a small "splash" effect when you remove your finger).
I don't quite know what you mean by "the whole drawing into a context nonsense that slows everything down." I draw these into a context very, very quickly (and Core Animation also does a lot of drawing into contexts).
Bending text around a circle is easier than either of these demos. The trick is to calculate the points along your circle, and use those points to translate and rotate your context before asking the layout manager to draw the glyph. Here's an example drawText from CurvyTextView (which draws along a Bézier curve).
- (void)drawText {
if ([self.attributedString length] == 0) { return; }
NSLayoutManager *layoutManager = self.layoutManager;
CGContextRef context = UIGraphicsGetCurrentContext();
NSRange glyphRange;
CGRect lineRect = [layoutManager lineFragmentRectForGlyphAtIndex:0
effectiveRange:&glyphRange];
double offset = 0;
CGPoint lastGlyphPoint = self.P0;
CGFloat lastX = 0;
for (NSUInteger glyphIndex = glyphRange.location;
glyphIndex < NSMaxRange(glyphRange);
++glyphIndex) {
CGContextSaveGState(context);
CGPoint location = [layoutManager locationForGlyphAtIndex:glyphIndex];
CGFloat distance = location.x - lastX; // Assume single line
offset = [self offsetAtDistance:distance
fromPoint:lastGlyphPoint
andOffset:offset];
CGPoint glyphPoint = [self pointForOffset:offset];
double angle = [self angleForOffset:offset];
lastGlyphPoint = glyphPoint;
lastX = location.x;
CGContextTranslateCTM(context, glyphPoint.x, glyphPoint.y);
CGContextRotateCTM(context, angle);
[layoutManager drawGlyphsForGlyphRange:NSMakeRange(glyphIndex, 1)
atPoint:CGPointMake(-(lineRect.origin.x + location.x),
-(lineRect.origin.y + location.y))];
CGContextRestoreGState(context);
}
}
The "magic" of this is in calculating the transforms you need, which is done in offsetAtDistance:fromPoint:andOffset:, pointForOffset: and angleForOffset:. Those routines are much simpler to write for a circle than a generic Bézier curve, so this is probably a very good starting point. Note that this code is not particularly optimized. It was designed for readability more than speed, but it is still very fast on an iPad 3. If you need it to be faster, there are several techniques, including a lot of pre-calculating that can be done.
The PinchText demo is in pure Core Text and Core Animation, and is quite a bit more complicated since it does all of its math in Accelerate (and really needs to). I doubt you need that since your layout problem isn't that complicated. Some straightforward C can probably calculate everything you need in plenty of time. But the PinchText demo does show how to let Core Animation manage transitions more beautifully. Look at addTouches:inView:scale::
- (void)addTouches:(NSSet *)touches inView:(UIView *)view scale:(CGFloat)scale
{
for (UITouch *touch in touches) {
TouchPoint *touchPoint = [TouchPoint touchPointForTouch:touch inView:view scale:scale];
NSString *keyPath = [self touchPointScaleKeyPathForTouchPoint:touchPoint];
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:keyPath];
anim.duration = kStartTouchAnimationDuration;
anim.fromValue = #0;
anim.toValue = #(touchPoint.scale);
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self addAnimation:anim forKey:keyPath];
[self.touchPointForIdentifier setObject:touchPoint forKey:touchPoint.identifier];
}
}
What's going on here is that it's animating the model data ("scale" here is "how much does this touch impact the layout;" it has nothing to do with transforms). needsDisplayForKey: indicates that when that model data structure is modified, the layer needs to redraw itself. And it completely recomputes and redraws itself into its context every frame. Done correctly, this can be incredibly fast.
This code should hopefully get you started. Not to overly push the book, but the CurvyText demo is discussed extensively in iOS Pushing the Limits chapter 21.
I have a node which is scaled and moved every frame.
The node has a custom draw function, so that only the visible part of that node is drawn each frame.
To determine which part is visible, I need to call:
CGPoint start = [MyNode convertToNodeSpace:_adjustedStart];
CGPoint finish = [MyNode convertToNodeSpace:_adjustedFinish];
where:
_adjustedStart = CGPointZero;
_adjustedFinish = CGPointMake(_winSize.width, 0);
start.x and finish.x are used by my draw method, to determine the width to draw.
Prior to using these methods, I had 60fps, even though sometimes I would draw much more than necessary. After using these methods, I draw exactly the region necessary, but the framerate sometimes drops to 50 (for an instant), making the graphics choppy.
How can I perform the same calculation as convertToNodeSpace / convertToWorldSpace but faster?
I'm developing an iPhone app where the main views are presented the user on the surface of a cube. Users switch views by rotating the cube with a pan gesture.
To achieve this I am using the GKLCubeController class from this GitHub project.
In terms of adding views to a cube and rotating, it works fine. However the angular rotation of the cube doesn't map correctly to the current x position of the finger as it pans across the screen.
The problem is that the cube rotation lags behind the finger movement by about ½ second making the cube feel ‘heavy’ as illustrated in this short screencast.
The code handling the rotation is shown below:
-(void)panHandler:(UIPanGestureRecognizer*)panner{
CGPoint translatedPoint = [panner translationInView:self.view.window];
CGFloat halfWidth = self.view.bounds.size.width / 2.0;
// save our starting points
if([panner state] == UIGestureRecognizerStateBegan) {
startingX = translatedPoint.x;
if (!transformLayer) {
transformLayer = [[CATransformLayer alloc] init];
transformLayer.frame = self.view.layer.bounds;
for (UIView *viewToTranslate in views) {
[viewToTranslate removeFromSuperview];
[transformLayer addSublayer:viewToTranslate.layer];
}
// add in this new layer
[self.view.layer addSublayer:transformLayer];
}
} else if([panner state] == UIGestureRecognizerStateEnded) {
...
} else {
// instantly adjust our transformation layer
CATransform3D transform = CATransform3DIdentity;
transform.m34 = kPerspective;
double percentageOfWidth = (translatedPoint.x - startingX) / self.view.frame.size.width;
transform = CATransform3DTranslate(transform, 0, 0, -halfWidth);
double adjustmentAngle = percentageOfWidth * M_PI_2 + startingAngle;
transform = CATransform3DRotate(transform, adjustmentAngle, 0, 1, 0);
transform = CATransform3DTranslate(transform, 0, 0, halfWidth);
transformLayer.transform = transform;
finishingAngle = adjustmentAngle;
}
}
I've a suspicion the problem is something to do with the conversion of the CGPoint.x returned by UIPanGestureRecognizer translationInView: to a rotation angle. Can anyone confirm whether this is the case, and suggest what the correct maths should be for mapping the touch position x to the rotation of a cube such that the cube edge tracks the finger motion as it pans across the screen?
There are two issues here:
The major performance issue here is the way this class is performing the transform of the sides of the cube. It's giving each side of the cube a complicated transform, and then as you're dragging the cube around, it's taken the relevant sides of the cube, added them to a CATransformLayer, and performing a complicated transform upon that layer (thus, when you look at the individual sides of the cube, you're doing a transform of a transform).
I pulled out that CATransformLayer logic, and updated the transform for the individual sides, and it was dramatically more responsive.
By the way, you may might want to still employ something like this CATransformLayer logic when you animate the letting go of the rotated cube, as that's an excellent way of synchronizing the animation of the individual sides of the cube (otherwise you get some separation in the sides of the cube during the animation). But while dragging, there's too much of a performance hit.
As you continue to refine this, there are possibly other optimizations that can be done, but my testing suggests that getting rid of a transformation on a complicated transformation made a huge impact on performance.
And, by the way, make sure to test this on a device, not the simulator, as the simulator's graphics performance is very different than that of the device.
A minor factor that might contribute a slight initial delay in responsiveness may be the inherent delay in UIPanGestureRecognizer (which looks for a certain amount of movement before recognizing the gesture as a pan, so that other gestures such as taps and the like can trigger if appropriate). It's a modest delay and a very small part of your performance problem, but for the quickest of response times, you might not want to use the UIPanGestureRecognizer. Either subclass your own, or use a UILongPressGestureRecognizer with a minimumPressDuration of 0.0, and you can get instantaneous response to the gesture.
You'll see this respond more quickly to movement (but it's also a gesture that doesn't play well with others, that if you have tap gestures or the like inside the view, they won't be triggered).
I thought doing a simple animation would be easy but is is taking hours and Im not getting even close to have the expected effect...
I need to simulate a simple Flash Motion Tween like for iphone/ipad using xcode
this is the desired effect: http://www.swfcabin.com/open/1340330187
I already tried setup a timer adding X position and it doens't get the same effect, my cooworkers suggested me cocos 2d to do this using actions and sprites, which might would be fine although I wouldn't like to third party frameworks, but if there is a way to do the same with cocos I would definitively use it.
Does anybody have any suggestions, I feel like it might be simpler than I thought
thanks all
If there is no troubles to you, that you will have to do it insinge OpenGL view, it is really very simple. To show some info, you need CCLabel class. To change it's position, you need CCMoveTo/CCMoveBy action, to change opacity, you need CCFadeTo/CCFadeIn/CCFadeOut actions, to make delay you need CCDelayTime. To make it all work together you need CCSpawn and CCSequence.
CCSpawn will run several actions at the same time(for example fade in and move from right to the center), CCSequence will run several actions one by one (sequence to fade in + move to center, delay for same time, sequence to fade out + move from center to the left). Then you should only schedule method, that will create labels and run actions on them. In code it will be something like
lets define full animation time
#define ANIMATION_TIME 4.f
schedule method in any place you want to start animation
[self schedule:#selector(runNextMessage) interval:ANIMATION_TIME];
it will call runNextMessage method every ANIMATION_TIME seconds
- (void) runNextMesage
{
NSString* message = //get next message
CCLabelTTF* label = [CCLabelTTF labelWithString:message
dimensions:desiredDimensionsOfTheLabel
alignment:UITextAlignmentLeft
lineBreakMode:UILineBreakModeWordWrap
fontName:#"Arial"
fontSize:20.f];
CGSize winSize = [[CCDirector sharedDirector] winSize];
// place the label out the right border
[label setPosition: ccp(winSize.width + label.contentSize.width, winSize.height / 2)];
// adding it to the screen
[self addChild:label];
ccTime spawnTime = ANIMATION_TIME / 3;
// create actions to run
id appearSpawn = [CCAction actionOne:[CCMoveTo actionWithDuration:spawnTime]
two:[CCFadeIn actionWithDuration:spawnTime]];
// create show action and disappear action
// create result sequence
id sequence = [CCSequence actions: appearSpawn, showAction, disappearAction, nil];
[label runAction: sequence];
}