Revolve a SCNode object in the 3D Space using SceneKit - objective-c

I am trying to learn my way around SceneKit and as an example, I thought I'd try to build a solar system using the SDK. I am able to add SCNode objects and configure its material properties like pattern and lighting. Also, I am able to rotate the planets, but I can't seem to understand how do I "revolve" them along a particular path.
My code so far looks like:
// create a new scene
SCNScene *scene = [SCNScene scene];
// create and add a camera to the scene
SCNNode *cameraNode = [SCNNode node];
cameraNode.camera = [SCNCamera camera];
[scene.rootNode addChildNode:cameraNode];
// place the camera
cameraNode.position = SCNVector3Make(0, 0, 2);
// create and add a 3d sphere - sun to the scene
SCNNode *sphereNode = [SCNNode node];
sphereNode.geometry = [SCNSphere sphereWithRadius:0.3];
[scene.rootNode addChildNode:sphereNode];
// create and configure a material
SCNMaterial *material = [SCNMaterial material];
material.diffuse.contents = [NSImage imageNamed:#"SunTexture"];
material.specular.contents = [NSColor whiteColor];
material.specular.intensity = 0.2;
material.locksAmbientWithDiffuse = YES;
// set the material to the 3d object geometry
sphereNode.geometry.firstMaterial = material;
// create and add a 3d sphere - earth to the scene
SCNNode *earthNode = [SCNNode node];
earthNode.geometry = [SCNSphere sphereWithRadius:0.15];
NSLog(#"Sun position = %f, %f, %f", sphereNode.position.x, sphereNode.position.y, sphereNode.position.z);
earthNode.position = SCNVector3Make(0.7, 0.7, 0.7);
NSLog(#"Earth position = %f, %f, %f", earthNode.position.x, earthNode.position.y, earthNode.position.z);
//earthNode.physicsBody = [SCNPhysicsBody dynamicBody];
[scene.rootNode addChildNode:earthNode];
SCNMaterial *earthMaterial = [SCNMaterial material];
earthMaterial.diffuse.contents = [NSImage imageNamed:#"EarthTexture"];
earthNode.geometry.firstMaterial = earthMaterial;
I read somewhere in AppleDocs that you need to add planets in the Sun's co-ordinate system and not in the root node but I am not sure if I understood that properly anyway.
Let me know how do you get about doing this.

SceneKit doesn't provide an animate-along-path API. There are several ways to set up something like a solar system, though.
Ever seen an orrery? This is the model that bit in the SCNNode documentation is talking about; it's also what the WWDC presentation does on the "Scene Graph Summary" slide. The trick is that you use one node to represent the frame of reference for a planet's revolution around the sun — like the armature that holds a planet in an orrery, if you make this node rotate around its central axis, any child node you attach to it will follow a circular path. The relevant code for this is in ASCSceneGraphSummary.m in that sample code project (condensed to just show node hierarchy setup here):
// Sun
_sunNode = [SCNNode node];
_sunNode.position = SCNVector3Make(0, 30, 0);
[self.contentNode addChildNode:_sunNode];
// Earth-rotation (center of rotation of the Earth around the Sun)
SCNNode *earthRotationNode = [SCNNode node];
[_sunNode addChildNode:earthRotationNode];
// Earth-group (will contain the Earth, and the Moon)
_earthGroupNode = [SCNNode node];
_earthGroupNode.position = SCNVector3Make(15, 0, 0);
[earthRotationNode addChildNode:_earthGroupNode];
// Earth
_earthNode = [_wireframeBoxNode copy];
_earthNode.position = SCNVector3Make(0, 0, 0);
[_earthGroupNode addChildNode:_earthNode];
// Moon-rotation (center of rotation of the Moon around the Earth)
SCNNode *moonRotationNode = [SCNNode node];
[_earthGroupNode addChildNode:moonRotationNode];
// Moon
_moonNode = [_wireframeBoxNode copy];
_moonNode.position = SCNVector3Make(5, 0, 0);
[moonRotationNode addChildNode:_moonNode];
You can't make an animation that follows a specific path you model in 3D space, but you could make a keyframe animation that interpolates between several positions in 3D space. The (untested) code below moves a node around a square shape in the xz-plane... add more points to get a rounder shape.
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPaths:#"position"];
anim.values = #[
[NSValue valueWithSCNVector3:SCNVector3Make(0, 0, 1)],
[NSValue valueWithSCNVector3:SCNVector3Make(1, 0, 0)],
[NSValue valueWithSCNVector3:SCNVector3Make(0, 0, -1)],
[NSValue valueWithSCNVector3:SCNVector3Make(-1, 0, 0)],
];
[node addAnimation:anim forKey:"orbit"];
Use physics! The SCNPhysicsField class models radial gravity, so you can just drop a gravity field into a scene, add some physics bodies for planets, and sit back and watch your solar system cataclysmically destroy itself come to life! Getting realistic behavior here takes a lot of trial and error — you'll need to set an initial velocity for each planet tangential to what you expect its orbit to be, and tweak that velocity to get it right. Researching some ephemeris data for our solar system might help.

Related

Physics Bodies are being offset from their node upon creation

I have been working on a puzzle game that uses a customizable grid with levels that are preset and created when the scene inits. They are comprised of static blocks scattered around the grid and a movable player controlled block. The gravity of the world is controllable and the block falls in that direction (up, down, left, right). I am using SKContactDelegate to track when the player block touches a block or the edge of the grid and stops it in place.
The problems I am having involve the physics bodies of the blocks and grid edge.
I am using bodyWithRectOfSize for the blocks and bodyWithEdgeLoopFromRect for the grid border.
The physics bodies of 1x1 blocks placed in the grid and the player 1x1 block have normal physics bodies (as they should be). However, larger blocks ex: 1x5, the bodies are shifted down on the y axis for no reason. Also depending on the grid size, the grid edge would be offset by a random number. Note: the bodies are offset and the nodes themselves are in the right place.
This is the code for creating the blocks; this one works fine
(cellsize is the size of each grid space)
Basic 1x1 block
-(SKShapeNode *)basic {
CGRect blockRect = CGRectMake(10, 10, self.cellSize, self.cellSize);
UIBezierPath *blockPath = [UIBezierPath bezierPathWithRoundedRect:blockRect cornerRadius:8];
SKShapeNode *blockNode = [SKShapeNode node];
blockNode.path = blockPath.CGPath;
blockNode.fillColor =[UIColor blackColor];
blockNode.lineWidth = 0;
blockNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:blockRect.size];
blockNode.physicsBody.categoryBitMask = blockCategory;
blockNode.physicsBody.dynamic = NO;
return blockNode;
}
and custom blocks (offset down on the y axis)
-(SKShapeNode *)basicWithWidth:(int)width WithHeight:(int)height {
CGRect blockRect = CGRectMake(10, 10, self.cellSize * width, self.cellSize * height);
UIBezierPath *blockPath = [UIBezierPath bezierPathWithRoundedRect:blockRect cornerRadius:8];
SKShapeNode *blockNode = [self basic];
blockNode.path = blockPath.CGPath;
blockNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:blockRect.size];
blockNode.physicsBody.categoryBitMask = blockCategory;
blockNode.physicsBody.dynamic = NO;
return blockNode;
}
And here is the grid edge
SKShapeNode *edge = [SKShapeNode node];
CGRect edgeRect = CGRectMake(10, 10, 260, 260);
UIBezierPath *edgeShape = [UIBezierPath bezierPathWithRoundedRect:edgeRect cornerRadius:8];
edge.path = edgeShape.CGPath;
edge.lineWidth = 0;
edge.fillColor = [UIColor grayColor];
edge.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:edgeRect];
edge.physicsBody.categoryBitMask = boardCategory;
[self addChild:edge];
Note: The edge and blocks are children to a "board" node that is a child of the game scene
Thank you for reading this.
When you use SKPhysicsBody's bodyWithRectangleOfSize:, it is centered on its node's origin. What you want to do in your case is center them on the center of the rect that defines your node. To do that, use bodyWithRectangleOfSize:center: instead.
For example, you define your block with the rect:
CGRect blockRect = CGRectMake(10, 10, self.cellSize, self.cellSize);
So, you'll want your physicsBody centered on the center of that rect. You easily get the center of a CGRect with:
CGPoint center = CGPointMake(CGRectGetMidX(blockRect), CGRectGetMidY(blockRect));
To create the physicsBody centered there, use:
blockNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:blockRect.size center:center];
Note: bodyWithRectangleOfSize:center: is only available in iOS 7.1 and later
iOS 7.0 method
For iOS 7.0, you can use bodyWithPolygonFromPath: instead, it just takes an additional line of code:
CGPathRef blockPath = CGPathCreateWithRect(blockRect, nil);
blockNode.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath: blockPath];

Is it possible to use a circle (SKShapeNode) as a mask in Sprite Kit?

I'm trying to create a circular mask in a Sprite Kit project. I create the circle like this (positioning it at the centre of the screen):
SKCropNode *cropNode = [[SKCropNode alloc] init];
SKShapeNode *circleMask = [[SKShapeNode alloc ]init];
CGMutablePathRef circle = CGPathCreateMutable();
CGPathAddArc(circle, NULL, CGRectGetMidX(self.frame), CGRectGetMidY(self.frame), 50, 0, M_PI*2, YES);
circleMask.path = circle;
circleMask.lineWidth = 0;
circleMask.fillColor = [SKColor blueColor];
circleMask.name=#"circleMask";
and further down the code, I set it as the mask for the cropNode:
[cropNode setMaskNode:circleMask];
... but instead of the content showing inside a circle, the mask appears as a square.
Is it possible to use a SKShapeNode as a mask, or do I need to use an image?
After much swearing, scouring the web, and experimentation in Xcode, I have a really hacky fix.
Keep in mind that this is a really nasty hack - but you can blame that on Sprite Kit's implementation of SKShapeNode. Adding a fill to a path causes the following:
adds an extra node to your scene
the path becomes unmaskable - it appears 'over' the mask
makes any non-SKSpriteNode sibling nodes unmaskable (e.g. SKLabelNodes)
Not an ideal state of affairs.
Inspired by Tony Chamblee's progress timer, the 'fix' is to dispense with the fill altogether, and just use the stroke of the path:
SKCropNode *cropNode = [[SKCropNode alloc] init];
SKShapeNode *circleMask = [[SKShapeNode alloc ]init];
CGMutablePathRef circle = CGPathCreateMutable();
CGPathAddArc(circle, NULL, CGRectGetMidX(self.frame), CGRectGetMidY(self.frame), 50, 0, M_PI*2, YES); // replace 50 with HALF the desired radius of the circle
circleMask.path = circle;
circleMask.lineWidth = 100; // replace 100 with DOUBLE the desired radius of the circle
circleMask.strokeColor = [SKColor whiteColor];
circleMask.name=#"circleMask";
[cropNode setMaskNode:circleMask];
As commented, you need to set the radius to half of what you'd normally have, and the line width to double the radius.
Hopefully Apple will look at this in future; for now, this hack is the best solution I've found (other than using an image, which doesn't really work if your mask needs to be dynamic).
Since I cannot use a SKShapeNode as mask I decided to convert it to a SKSpriteNode.
Here it is my Swift code:
let shape : SKShapeNode = ... // create your SKShapeNode
var view : SKView = ... // you can get this from GameViewController
let texture = view.textureFromNode(shape)
let sprite = SKSpriteNode(texture: texture)
sprite.position = ...
cropNode.mask = sprite
And it does work :)
Yes, it is impossible to use fill-colored shapenode in current Sprite-Kit realization. It's a bug, I think.
But!
You always can render the shape to texture and use it as mask!
For ex, let edge is SKShapeNode created before.
First, render it to texture before adding to view (in this case it will clean from another nodes)
SKTexture *Mask = [self.view textureFromNode:edge];
[self addChild:edge]; //if you need physics borders
Second,
SKCropNode *cropNode = [[SKCropNode alloc] init];
[cropNode setMaskNode:[SKSpriteNode spriteNodeWithTexture:Mask]];
[cropNode addChild: [SKSpriteNode spriteNodeWithTexture:rocksTiles size:CGSizeMake(w,h)]];
cropNode.position = CGPointMake(Mask.size.width/2,Mask.size.height/2);
//note, anchorpoint of shape in 0,0, but rendered texture is in 0.5,0.5, so we need to dispose it
[self addChild:cropNode];
There is a slightly awkward solution to this issue that I believe is slightly less nasty than any of the mooted hacks around this issue. Simply wrap your SKShapeNode in an SKNode. I haven't tested what kind of performance hit this might cause, but given that SKNode is non-drawing, I'm hoping it won't be too significant.
i.e.
let background = SKSpriteNode(imageNamed: "Background")
let maskNode = SKShapeNode(path: MyPath)
let wrapperNode = SKNode()
let cropNode = SKCropNode()
wrapperNode.addChild(maskNode)
cropNode.maskNode = wrapperNode
cropNode.addChild(background)
Setting alpha of mask node to .0 does the trick:
circleMask.alpha = .0;
EDIT:
Seems to work for Mac OS only.

SKPhysicsBody and [SKNode setScale:]

In my current project using SpriteKit, I have a bunch of sprites that need to be scaled up and down independently at various times. The problem is that when I scale the node, the physics body doesn't scale with it so it screws up the physics. Here's a small example I put together for the purpose of this question:
CGSize objectSize = CGSizeMake(100, 100);
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:CGRectMake(0, 0, self.size.width, self.size.height)];
SKSpriteNode *n1 = [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:objectSize];
n1.position = CGPointMake(self.size.width/2, 2*self.size.height/3);
n1.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:objectSize];
n1.physicsBody.affectedByGravity = YES;
[self addChild:n1];
SKSpriteNode *n2 = [SKSpriteNode spriteNodeWithColor:[UIColor redColor] size:objectSize];
n2.position = CGPointMake(self.size.width/2, self.size.height/3);
n2.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:objectSize];
n2.physicsBody.affectedByGravity = YES;
[self addChild:n2];
[n1 setScale:0.5];
Notice how the blue sprite (scaled down) sits on top of the red one but you can tell its physics body still has the dimension I told it, and it didn't scale.
So obviously, scaling down the node doesn't scale down the physicsBody. So my question is if I have to manually do it, how do I go about it?
I tried swapping the body with one of the right size when scaling, but then things get really convoluted if the old body had joints, etc... It'd be a lot simpler if I could just scale the existing body somehow.
Using either an SKAction or the Update loop, you can create a new SKPhysicsBody with the proper scale and apply it to the object. However, by doing so, you will lose the velocity. To fix this, you can do the following:
SKPhysicsBody *newBody = [SKPhysicsBody bodyWithRectangleOfSize:boxObject.size];
newBody.velocity = boxObject.physicsBody.velocity;
boxObject.physicsBody = newBody;
Physics body shapes can't be scaled. Definitely not in Box2D which Sprite Kit uses internally.
Besides internal optimizations, scaling a physics body would have far reaching implications. For example scaling a body's shape up could get the body stuck in collisions. It would affect how joints interact. It would definitely change the body's density or mass and thus its behavior.
You could use a SKNode to which you add the sprite without a physics body, and then add additional SKNode with bodies of given sizes. You could then enable or disable the bodies when you start scaling the sprite. If you time this right the player won't notice that the collision shape simply went from full size to half size while the sprite animates that scaling transition.
You would have to calculate the body's density and perhaps other properties according to its size though.
For anyone else reading this answer, I believe that now Physics Bodies scale with the SKNode. I am using Xcode 8.2.1 and Swift 3.0.2. I start a new Project, select Game, fill in the details. Change 2 files:
GameViewController.swift (add one line)
view.showsFPS = true
view.showsNodeCount = true
// add this to turn on Physics Edges
view.showsPhysics = true
Game Scene.swift (replace the sceneDidLoad()) function with this:
override func sceneDidLoad() {
var found = false
self.enumerateChildNodes(withName: "testSprite") {
node, stop in
found = true
}
if !found {
let testSprite = SKSpriteNode(color: UIColor.white, size: CGSize(width: 200.0, height: 200.0))
testSprite.physicsBody = SKPhysicsBody(polygonFrom: CGPath(rect: CGRect(x: -100.0, y: -100.0, width: 200.0, height: 200.0), transform: nil))
testSprite.physicsBody?.affectedByGravity = false
testSprite.name = "testSprite"
let scaleAction = SKAction.repeatForever(SKAction.sequence([
SKAction.scale(to: 2.0, duration: 2.0),
SKAction.scale(to: 0.5, duration: 2.0)
]))
testSprite.run(scaleAction)
self.addChild(testSprite)
}
}
The physics boundary is scaling with the sprite node.
I had the exact same problem, I think its a bug in sprite kit.
Try using an action to scale, this works on whole scene.
[self runAction:[SKAction scaleTo:0.5 duration:0]];
My original question
Is this what you want to happen
- (void) anthoertest
{
CGSize objectSize = CGSizeMake(100, 100);
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:CGRectMake(0, 0, self.size.width, self.size.height)];
SKSpriteNode *n1 = [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:objectSize];
n1.position = CGPointMake(self.size.width/2, 2*self.size.height/3);
n1.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:n1.size];
n1.physicsBody.affectedByGravity = YES;
[self addChild:n1];
SKSpriteNode *n2 = [SKSpriteNode spriteNodeWithColor:[UIColor redColor] size:objectSize];
n2.position = CGPointMake(self.size.width/2, self.size.height/3);
n2.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:n2.size];
n2.physicsBody.affectedByGravity = YES;
[self addChild:n2];
[self shrinkMe:n1];
}
- (void) shrinkMe:(SKSpriteNode *) s
{
[s setScale:0.5];
s.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:s.size];
}
The physics body depends on the CGPath of the SKNode; you'll need to change that somehow. You might try scaling the CGPath with something like the following:
//scale up
CGFloat scaleFactor = 1.1;
CGAffineTransform scaleTransform = CGAffineTransformIdentity;
scaleTransform = CGAffineTransformScale(scaleTransform, scaleFactor, scaleFactor);
CGPathRef scaledPathRef = CGPathCreateCopyByTransformingPath(exampleNode.path, &scaleTransform);
exampleNode.path = scaledPathRef;
But keep in mind this won't update the appearance of nodes already in an SKScene. You might need to combine resizing the CGPath with an SKAction that scales the node.
I had this same problem, all I did was define my physics body in my update current time function and it would scale with it.
For you it would be something like this:
override func sceneDidLoad() {
SKSpriteNode *n1 = [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:objectSize];
n1.position = CGPointMake(self.size.width/2, 2*self.size.height/3);
}
override func update(_ currentTime: TimeInterval) {
n1.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:objectSize];
n1.physicsBody.affectedByGravity = YES;
}
The only problem is that you have to redefine your physics properties every time in updateCurrentTime. You can make some of these properties like it's restitution equal a variable so you can change its properties by changing the variable elsewhere. Redefining it anywhere else only works until the next frame is called.

Changing the layer shape for CPTPlotSpaceAnnotation?

I want to add a widget to my graph that a user can click and drag. I've managed to do this using CPTPlotSpaceAnnonation (because I also want it to also scroll with the data) and CPTBorderedLayer. The basic functionality works and now I want to improve the look of the widget. So far it's just a green circle.
The follow code generates the circle below, notice how the shadow is clipped to the rect of the layer and the border follows the layer and not the circle,
How can I stroke the circle and not the layer? I guess I can avoid clipping by making the frame of the layer larger. Is possible to use CAShapeLayer class with the Core Plot annotations? This would be an easy way of drawing the widgets.
// Add a circular annotation to graph with fill and shadow
annotation = [[CPTPlotSpaceAnnotation alloc]
initWithPlotSpace:graph.defaultPlotSpace
anchorPlotPoint:anchorPoint];
// Choosing line styles and fill for the widget
NSRect frame = NSMakeRect(0, 0, 50.0, 50.0);
CPTBorderedLayer *borderedLayer = [[CPTBorderedLayer alloc] initWithFrame:frame];
// Circular shape with green fill
CGPathRef outerPath = CGPathCreateWithEllipseInRect(frame, NULL);
borderedLayer.outerBorderPath = outerPath;
CPTFill *fill = [CPTFill fillWithColor:[CPTColor greenColor]];
borderedLayer.fill = fill;
// Basic shadow
CPTMutableShadow *shadow = [CPTMutableShadow shadow];
shadow.shadowOffset = CGSizeMake(0.0, 0.0);
shadow.shadowBlurRadius = 6.0;
shadow.shadowColor = [[CPTColor blackColor] colorWithAlphaComponent:1.0];
borderedLayer.shadow = shadow;
// Add to graph
annotation.contentLayer = borderedLayer;
annotation.contentLayer.opacity = 1.0;
[graph addAnnotation:annotation];
Don't set the border paths of the layer. Instead, just set the cornerRadius to half the width of the frame.
borderedLayer.cornerRadius = frame.size.width * 0.5;

Simple way of using irregular shaped buttons

I've finally got my main app release (Tap Play MMO - check it out ;-) ) and I'm now working on expanding it.
To do this I need to have a circle that has four seperate buttons in it, these buttons will essentially be quarters. I've come to the conclusion that the circlular image will need to be constructed of four images, one for each quarter, but due to the necessity of rectangular image shapes I'm going to end up with some overlap, although the overlap will be transparent.
What's the best way of getting this to work? I need something really simple really, I've looked at this
http://iphonedevelopment.blogspot.com/2010/03/irregularly-shaped-uibuttons.html
Before but not yet succeeded in getting it to work. Anyone able to offer some advice?
In case it makes any difference I'll be deploying to a iOS 3.X framework (will be 4.2 down the line when 4.2 comes out for iPad)
Skip the buttons and simply respond to touches in your view that contains the circle.
Create a CGPath for each area that you want to capture touches, when your UIview receives a touch, check for membership inside the paths.
[Edited answer to show skeleton implementation details -- TomH]
Here's how I would approach the problem: (I haven't tested this code and the syntax may not be quite right, but this is the general idea)
1) Using PS or your favorite image creation application, create one png of the quarter circles. Add it to your XCode project.
2) Add a UIView to the UI. Set the UIView's layer's contents to the png.
self.myView = [[UIView alloc] initWithRect:CGRectMake(10.0, 10.0, 100.0, 100,0)];
[myView.layer setContents:(id)[UIImage loadImageNamed:#"my.png"]];
3) Create CGPaths that describe the region in the UIView that you are interested in.
self.quadrantOnePath = CGPathCreateMutable();
CGPathMoveToPoint(self.quadrantOnePath, NULL, 50.0, 50.0);
CGPathAddLineToPoint(self.quadrantOnePath, NULL, 100.0, 50.0);
CGPathAddArc(self.quadrantOnePath, NULL, 50.0, 50.0, 50.0, 0.0, M_PI2, 1);
CGPathCloseSubpath(self.quadrantOnePath);
// create paths for the other 3 circle quadrants too!
4) Add a UIGestureRecognizer and listen/observe for taps in the view
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
[tapRecognizer setNumberOfTapsRequired:2]; // default is 1
5) When tapRecognizer invokes its target selector
- (void)handleGesture:(UIGestureRecognizer *) recognizer {
CGPoint touchPoint = [recognizer locationOfTouch:0 inView:self.myView];
bool processTouch = CGPathContainsPoint(self.quadrantOnePath, NULL, touchPoint, true);
if(processTouch) {
// call your method to process the touch
}
}
Don't forget to release everything when appropriate -- use CGPathRelease to release paths.
Another thought: If the graphic that you are using to represent your circle quadrants is simply a filled color (i.e. no fancy graphics, layer effects, etc.), you could also use the paths you created in the UIView's drawRect method to draw the quadrants too. This would address one of the failings of the approach above: there isn't a tight integration between the graphic and the paths used to check for the touches. That is, if you swap out the graphic for something different, change the size of the graphic, etc., your paths used to check for touches will be out of sync. Potentially a high maintenance piece of code.
I can't see, why overlapping is needed.
Just create 4 buttons and give each one a slice of your image.
edit after comment
see this great project. One example is exactly what you want to do.
It works by incorporating the alpha-value of a pixel in the overwritten
pointInside:withEvent: and a category on UIImage, that adds this method
- (UIColor *)colorAtPixel:(CGPoint)point {
// Cancel if point is outside image coordinates
if (!CGRectContainsPoint(CGRectMake(0.0f, 0.0f, self.size.width, self.size.height), point)) {
return nil;
}
// Create a 1x1 pixel byte array and bitmap context to draw the pixel into.
// Reference: http://stackoverflow.com/questions/1042830/retrieving-a-pixel-alpha-value-for-a-uiimage
NSInteger pointX = trunc(point.x);
NSInteger pointY = trunc(point.y);
CGImageRef cgImage = self.CGImage;
NSUInteger width = self.size.width;
NSUInteger height = self.size.height;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
int bytesPerPixel = 4;
int bytesPerRow = bytesPerPixel * 1;
NSUInteger bitsPerComponent = 8;
unsigned char pixelData[4] = { 0, 0, 0, 0 };
CGContextRef context = CGBitmapContextCreate(pixelData,
1,
1,
bitsPerComponent,
bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGColorSpaceRelease(colorSpace);
CGContextSetBlendMode(context, kCGBlendModeCopy);
// Draw the pixel we are interested in onto the bitmap context
CGContextTranslateCTM(context, -pointX, pointY-(CGFloat)height);
CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, (CGFloat)width, (CGFloat)height), cgImage);
CGContextRelease(context);
// Convert color values [0..255] to floats [0.0..1.0]
CGFloat red = (CGFloat)pixelData[0] / 255.0f;
CGFloat green = (CGFloat)pixelData[1] / 255.0f;
CGFloat blue = (CGFloat)pixelData[2] / 255.0f;
CGFloat alpha = (CGFloat)pixelData[3] / 255.0f;
return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
}
Here's an awesome project that solves the problem of irregular shaped buttons so easily:
http://christinemorris.com/2011/06/ios-irregular-shaped-buttons/