I'm working in spritekit/IOS where I have a button that runs an action when it is clicked. The button works fine, but my action is terrible. On click I run this action.
actionMove=[SKAction moveTo:CGPointMake(300,300)duration:2.0];
Which then causes my sprite does a spinning flying kick too that location. I was needed a little help cause I tried replacing the y axis with
_sprite.position+10
But it gives me an error.
Thanks
What you need is the action moveByX:y:duration:...
Sort of.
You can do this...
SKAction *rise = [SKAction moveByX:0 y:10 duration:1];
SKAction *fall = [SKAction moveByX:0 y:-10 duration:1];
SKAction *jump = [SKAction sequence:#[rise, fall]];
Then you can run the jump action on your sprite.
This will move the sprite up by 10 points over 1 second and then down by 10 points over 1 second.
However, it will not look like a jump. It will not accelerate or anything.
If you are including something like a jump in your game then you would normally want to have some sort of collision detection with the floor and use forces to move your sprite instead of actions.
Collision detection method
Sprite Kit Physics Engine
There are two general ways you could do this. The easiest (although longest learning curve) is to use SKPhysicsBody and the built in physics engine in SpriteKit.
You would add a SKPhysicsBody to your character and a boundary SKPhysicsBody to the floor.
Then to make the character jump you would use something like...
[character.physicsBody applyImpulse:CGVectorMake(0, 1)];
This will apply an immediate force to the character so it "jumps" in the air. Then with the physics engine you have built in gravity and so the character will start to slow down and fall until it hits the floor again.
There is a bit of setup for this (less than you would think) but once the setup is done the rest is VERY VERY VERY simple.
Manually
To do it manually is another case. This requires no setup but a LOT of maths. The way this works is to use the update: function and then each update cycle calculate what the velocity and position of the character is and to update the position accordingly.
You really don't want to do this though.
Make things really easy for yourself by adding SKPhysicsBody to the things that need to collide (the character and the floor) and then exploit the gravity and applyImpulse. It will make it much easier, it will perform faster and it will look a million times better :D
There you go :
CGPoint pos = _sprite.position;
pos.y = pos.y + 10;
_sprite.position = pos;
If you are going to use force (and physics) to move your player then using impulse will give you better results:
_sprite.physicsBody applyImpulse:CGVectorMake(0, <value>);
The above depends on a number of factors, including gravity value and the mass of the _sprite.physicsBody. Experimenting is the best way to figure out what works here.
If you are not using physics you can use the followPath action and easing to get a somewhat realistic jumping effect (similar to cocos2d's jump to action). A simple example of how this can be implemented as a category method on SKAction:
+ (SKAction *)jumpWithStartPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint height:(CGFloat)height {
UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
[bezierPath moveToPoint:startPoint];
CGPoint peak = CGPointMake(endPoint.x - startPoint.x, startPoint.y + height);
[bezierPath addCurveToPoint:endPoint controlPoint1:peak controlPoint2:peak];
SKAction *jumpAction = [SKAction followPath:bezierPath.CGPath asOffset:NO orientToPath:NO duration:.5f];
jumpAction.timingMode = SKActionTimingEaseIn;
return jumpAction;
}
Related
I am working on a SpriteKit game where in one of the scenes I have to update score based on collision between two sprites. I intend to increment score by 1 whenever those two SKSpriteNodes collide with each other. There is a collision detection method in SpriteKit, which takes care of collision detection itself. I am using that default method didBeginContact: to detect collision, remove one of the objects involved in collision and increment score by 1. There are multiple objects of the same kind falling from the top and there is a basket like object that a player can move horizontally to catch those falling objects. On colliding with the basket, objects falling from the top are removed and score get incremented. The issue is very simple, that the score is being incremented not only by 1, but by 2,3,4,5 as well. That means instead of detecting single collision as it should be the case, it is detecting more than one collisions and hence incrementing score accordingly.
I have viewed here another question like this but that solution can never apply to mine. In my game for a limited time, similar objects keep falling from top until the time ends. Is there any possible way to solve this issue. I have tried using bool variable like didCollide inside collision detection method and even in a separate score incrementing method but the issue does not get resolved.
Here is the code I am trying in collision detection method.
-(void)didBeginContact:(SKPhysicsContact *)contact {
if (contact.bodyA.categoryBitMask == Stones) {
[contact.bodyA.node removeFromParent];
if (contactOccurred == NO) {
contactOccurred = YES;
[self updateScore:contactOccurred];
}
}
else {
[contact.bodyB.node removeFromParent];
if (contactOccurred == NO) {
ContactOccurred = YES;
[self updateScore:contactOccurred];
}
}
}
Code snippet for the method to increment score is here.
-(void)updateScore:(BOOL)collisionOccurred {
if (collisionOccurred == YES) {
contactOccurred = NO;
self.score= self.score + 1;
}
}
I have solved the issue myself after some reading from Apple's documentation and experimenting different options. Issue was that the didBeginContact method that is a default method provided by SpriteKit to detect contacts and collisions, was called multiple times as Joern tried to explain in his answer. But objects involved in collision in my game are not of irregular shapes. One kind of objects are oval shaped while the other kind of object is of more or less rectangular shape.Yet the method was being called more than once whenever the contact was made between two objects.
How did I solve it?
I tried applying the technique Joern suggested although I knew it wasn't the real solution I was looking but more of a temporary cover up. But surprisingly it did not work for my game as score would still increment randomly. I removed my oval shaped sprites and replaced them with simple round solid colour sprites just in case my oval shaped sprites were not smooth near the edges. Even then the problem continued that led me to Apple's documentation on this link a link. I came to know that to have best performance from a physical body and better accuracy for collision detection, one should go for simpler physical bodies when possible. A Circular physical body is most performance efficient as it is pretty fast to process and the simplest. Rectangular physics body comes next, followed by a polygonal physics body and physics body from an image texture on last. More complex the phycis body is, more is going to be computational cost and chances of losing accuracy increase. I had created physical bodies of my colliding objects using image texture. Physical bodies created from texture somehow were the cause (or at least in my case) why didContactMethod was being called multiple times. Even physical body of a simple round sprite created from texture was incrementing score by 2 instead of 1. I changed the code to create physical bodies of circular shape for my oval shaped sprites and everything is perfect now without needing to change category for to be removed nodes or any boolean flag.
Avoiding multiple calls to didBeginContact method by use of Boolean flags or any other way can be a cover up but not the solution which works in few cases but won't work in others. Try using simplest physics bodies where possible especially when you start getting inaccurate results from collisions and contacts.
When you are using SKSpriteNodes whose SKPhysicsBodies have irregular shapes they can have more than one contact point when they are touching. That's why didBeginContact can be called multiple times for the same two SKSpriteNodes before the Node is removed from its parent.
See this example for a contact with 2 contact points (which causes didBeginContact to be called twice for the same two objects):
To avoid this you can change the physicsBody's categoryBitMask in didBeginContact so that all following contacts are not recognized any more:
-(void)didBeginContact:(SKPhysicsContact *)contact {
if (contact.bodyA.categoryBitMask == Stones) {
contact.bodyA.categoryBitMask = None
[contact.bodyA.node removeFromParent];
self.score= self.score + 1;
} else if (contact.bodyB.categoryBitMask == Stones) {
contact.bodyB.categoryBitMask = None
[contact.bodyB.node removeFromParent];
self.score= self.score + 1;
}
}
Obviously you have to define the None bitmask value and exclude it from the contactTestBitMask of your basket like SKSpriteNode. That way after changing the categoryBitMask of your Stone SKSpriteNode from Stones to None all further contacts will be ignored.
i'm trying to make a barrier in uikit dynamic sso this barrier don't get affected by gravity but when a object hits it it start floating instead of keeping in place and stop the object, I was planing to set a high density but can't get it to work.
I have the same question with you. but i cant find the correct answer.if you want wo use UIDynamic to creat barrier,that the better thing is to create a barrier to add below behavior.
UIDynamicItemBehavior itemBehaviorNoBounce = [[UIDynamicItemBehavior alloc] init];
itemBehaviorNoBounce.allowsRotation = NO;
itemBehaviorNoBounce.elasticity = 0;
this can make the item collision a little, but if you have a lot of thing to crash,that is a not good way to use UIDynamic for a barrier. beacause the calculate of the item is complex. And you cant control it.
In my application i am using CABasicAnimation for animation. I want to change the speed of the animation dynamically so i have added one slider to change the speed. Following is my animation code. But i am not able to change the speed, when i change the value of speed nothing happens.
CABasicAnimation * a = [CABasicAnimation animationWithKeyPath:#"position"];
[a setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
CGPoint startPt = CGPointMake(self.view.bounds.size.width + displayLabel.bounds.size.width / 2,
displayLabel.frame.origin.y);
CGPoint endPt = CGPointMake(displayLabel.bounds.size.width / -2, displayLabel.frame.origin.y);
[a setFromValue:[NSValue valueWithCGPoint:startPt]];
[a setToValue:[NSValue valueWithCGPoint:endPt]];
[a setAutoreverses:NO];
[a setDuration:speeds];
[a setRepeatCount:HUGE_VAL];
[displayLabel.layer addAnimation:a forKey:#"rotationAnimation"];
- (IBAction)speedSlider:(id)sender {
speeds = slider.value;
}
I think the best way to change speed is change your layer's time system
displayLabel.layer.timeOffset =
[displayLabel.layer convertTime:CACurrentMediaTime() fromLayer:nil];
displayLabel.layer.beginTime = CACurrentMediaTime();
displayLabel.layer.speed= slider.value;
You can see this for advance. https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW2
EDIT: It looks like you will have a further problem, though: it doesn't look like you can change values like that on a running animation. You will have to remove the current animation and add a new one with the altered value. This may need a bit of care to prevent a jarring effect when you add the new animation.
From the thread above, you might be able to do this by not repeating the animation, but by using a delegate (see here) to keep re-adding the animation, and setting the new speed for the next animation cycle instead.
Original post:
You are changing the value that you had originally passed in to the animation. This isn't going to affect the running animation. You'll need to get a reference to that, and change the duration property of the animation object. Something like this in your action method:
CABasicAnimation *a = [displayLabel.layer animationForKey:#"rotationAnimation"];
a.duration = slider.value;
I think jrturton is correct that you can't change properties on an animation that is already running. But what you could do is break the animation into short segments and change the speed for the next segment when the slider value changes.
Instead of animating from point A to point D, you'd animate from A-B, then B-C, then C-D. Use the parent class's animationDidStop to check the current point, check the slider value, and kick off the next animation.
This might produce jerky motion, but if you use very small segments, you might be able to smooth it out.
u should stop the animation and restart a new with a new duration time
but remember to log down the fromValue and the toValue , and use the old toValue as the new fromValue to perform a seamless change
set speed as what you need.
a.duration=0.5;
Try this...
If you just want Autoscrolling text then you can also use one class
http://blog.stormyprods.com/2009/10/simple-scrolling-uilabel-for-iphone.html
It might also work in your case, try it.
The UIScrollView has a lot of information available to the programmer, but I dont see an obvious way to control the location that the control stop at after decelerating from a scroll gesture.
Basically I would like the scrollview to snap to specific regions of the screen. The user can still scroll like normal, but when they stop scrolling the view should snap to the most relevant location, and in the case of a flick gesture the deceleration should stop at these locations too.
Is there an easy way to do something like this, or should I consider the only way to accomplish this effect to write a custom scrolling control?
Since the UITableView is a UIScrollView subclass, you could implement the UIScrollViewDelegate method:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
And then compute what the closest desired target content offset is that you want, and set that on the inout CGPoint parameter.
I've just tried this and it works well.
First, retrieve the unguided offset like this:
CGFloat unguidedOffsetY = targetContentOffset->y;
Then Figure out through some math, where you'd want it to be, noting the height of the table header. Here's a sample in my code dealing with custom cells representing US States:
CGFloat guidedOffsetY;
if (unguidedOffsetY > kFirstStateTableViewOffsetHeight) {
int remainder = lroundf(unguidedOffsetY) % lroundf(kStateTableCell_Height_Unrotated);
log4Debug(#"Remainder: %d", remainder);
if (remainder < (kStateTableCell_Height_Unrotated/2)) {
guidedOffsetY = unguidedOffsetY - remainder;
}
else {
guidedOffsetY = unguidedOffsetY - remainder + kStateTableCell_Height_Unrotated;
}
}
else {
guidedOffsetY = 0;
}
targetContentOffset->y = guidedOffsetY;
The last line above, actually writes the value back into the inout parameter, which tells the scroll view that this is the y-offset you'd like it to snap to.
Finally, if you're dealing with a fetched results controller, and you want to know what just got snapped to, you can do something like this (in my example, the property "states" is the FRC for US States). I use that information to set a button title:
NSUInteger selectedStateIndexPosition = floorf((guidedOffsetY + kFirstStateTableViewOffsetHeight) / kStateTableCell_Height_Unrotated);
log4Debug(#"selectedStateIndexPosition: %d", selectedStateIndexPosition);
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:selectedStateIndexPosition inSection:0];
CCState *selectedState = [self.states objectAtIndexPath:indexPath];
log4Debug(#"Selected State: %#", selectedState.name);
self.stateSelectionButton.titleLabel.text = selectedState.name;
OFF-TOPIC NOTE: As you probably guessed, the "log4Debug" statements are just logging. Incidentally, I'm using Lumberjack for that, but I prefer the command syntax from the old Log4Cocoa.
After the scrollViewDidEndDecelerating: and scrollViewDidEndDragging:willDecelerate: (the last one just when the will decelerate parameter is NO) you should set the contentOffset parameter of your UIScrollView to the desired position.
You also will know the current position by checking the contentOffset property of your scrollview, and then calculate the closest desired region that you have
Although you don't have to create your own scrolling control, you will have to manually scroll to the desired positions
To add to what felipe said, i've recently created a table view that snaps to cells in a similar way the UIPicker does.
A clever scrollview delegate is definitely the way to do this (and you can also do that on a uitableview, since it's just a subclass of uiscrollview).
I had this done by, once the the scroll view started decelerating (ie after scrollViewDidEndDragging:willDecelerate: is called), responding to scrollViewDidScroll: and computing the diff with the previous scroll event.
When the diff is less than say a 2 to 5 of pixels, i check for the nearest cell, then wait until that cell has been passed by a few pixels, then scroll back in the other direction with setContentOffset:animated:.
That creates a little bounce effect that is very nice for user experience, as it gives a good feedback on the snapping.
You'll have to be clever and not do anything when the table is bouncing at the top or bottom (comparing the offset to 0 or the content size will tell you that).
It works pretty well in my case because the cells are small (about 80-100px high), you might run into problems if you have a regular scroll view with bigger content areas.
Of course, you will not always decelerate past a cell, so in this case i just scroll to the nearest cell, and the animation looks jerky. Turns out with the right tuning, it barely ever happens, so i'm cool with this.
Spend a few hours tuning the actual values depending on your specific screen and you can get something decent.
I've also tried the naive approach, calling setContentOffset:animated: on scrollViewDidEndDecelerating: but it creates a really weird animation (or just plain confusing jump if you don't animate), that gets worse the lower the deceleration rate is (you'll be jumping from a slow movement to a much faster one).
So to answer the question:
- no, there is no easy way to do this, it'll take some time polishing the actual values of the previous algorithm, which might not work at all on your screen,
- don't try to create your own scroll view, you'll just waste time and badly reinvent a beautiful piece of engineering apple created with truck loads of bug. The scrollview delegate is the key to your problem.
Try something like this:
- (void) snapScroll;
{
int temp = (theScrollView.contentOffset.x+halfOfASubviewsWidth) / widthOfSubview;
theScrollView.contentOffset = CGPointMake(temp*widthOfSubview , 0);
}
- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
{
if (!decelerate) {
[self snapScroll];
}
}
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self snapScroll];
}
This takes advantage of the int's drop of the post-decimal digits. Also assumes all your views are lined up from 0,0 and only the contentOffset is what makes it show up in different areas.
Note: hook up the delegate and this works perfectly fine. You're getting a modified version - mine just has the actual constants lol. I renamed the variables so you can read it easy
in a simple drawing application I have a model which has a NSMutableArray curvedPaths holding all the lines the user has drawn. A line itself is also a NSMutableArray, containing the point objects. As I draw curved NSBezier paths, my point array has the following structure: linePoint, controlPoint, controlPoint, linePoint, controlPoint, controlPoint, etc... I thought having one array holding all the points plus control points would be more efficient than dealing with 2 or 3 different arrays.
Obviously my view draws the paths it gets from the model, which leads to the actual question: Is there a way to optimize the following code (inside the view's drawRect method) in terms of speed?
int lineCount = [[model curvedPaths] count];
// Go through paths
for (int i=0; i < lineCount; i++)
{
// Get the Color
NSColor *theColor = [model getColorOfPath:[[model curvedPaths] objectAtIndex:i]];
// Get the points
NSArray *thePoints = [model getPointsOfPath:[[model curvedPaths] objectAtIndex:i]];
// Create a new path for performance reasons
NSBezierPath *path = [[NSBezierPath alloc] init];
// Set the color
[theColor set];
// Move to first point without drawing
[path moveToPoint:[[thePoints objectAtIndex:0] myNSPoint]];
int pointCount = [thePoints count] - 3;
// Go through points
for (int j=0; j < pointCount; j+=3)
{
[path curveToPoint:[[thePoints objectAtIndex:j+3] myNSPoint]
controlPoint1:[[thePoints objectAtIndex:j+1] myNSPoint]
controlPoint2:[[thePoints objectAtIndex:j+2] myNSPoint]];
}
// Draw the path
[path stroke];
// Bye stuff
[path release];
[theColor release];
}
Thanks,
xonic
Hey xon1c, the code looks good. In general it is impossible to optimize without measuring performance in specific cases.
For example, lets say the code above is only ever called once. It draws a picture in a view and it never needs redrawing. Say the code above takes 50 milliseconds to run. You could rewrite it in openGL and do every optimisation under the sun and get that time down to 20 milliseconds and realistically the 30 milliseconds that you have saved makes no difference to anyone and you pretty much just wasted your time and added a load of code-bloat that is going to be more difficult to maintain.
However, if the code above is called 50 times a second and most of those times it is drawing the same thing then you could meaningfully optimise it.
When it comes to drawing the best way to optimise is to is to eliminate unnecessary drawing.
Each time you draw you recreate the NSBezierPaths - are they always different? You may want to maintain the list of NSBezier paths to draw, keep that in sync with your model, and keep drawrect solely for drawing the paths.
Are you drawing to areas of your view which don't need redrawing? The argument to drawrect is the area of the view that needs redrawing - you could test against that (or getRectsBeingDrawn:count:), but it may be in your case that you know that the entire view needs to be redrawn.
If the paths themselves don't change often, but the view needs redrawing often - eg when the shapes of the paths aren't changing but their positions are animating and they overlap in different ways, you could draw the paths to images (textures) and then inside drawrect you would draw the texture to the view instead of drawing the path to the view. This can be faster because the texture is only created once and uploaded to video memory which is faster to draw to the screen. You should look at Core Animation if this is what you need todo.
If you find that drawing the paths is too slow you could look at CGPath
So, on the whole, it really does depend on what you are doing. The best advice is, as ever, not to get sucked into premature optimisation. If your app isn't actually too slow for your users, your code is just fine.