Cocos2D & ccDrawLine - Drawing smooth lines - ios

I have some troubles when I try to draw lines with cocos2d! I store points, got from touchMoved method, in a NSMutableArray, and pass that array to a subclass of CCNode called Lines that I use to draw the lines from the array of points. The problem is that the line is not smooth when I swipe slowly, but when I swipe faster, the line is way much smoother. See the pictures below :
Slow Swipe :
Fast Swipe :
I tried to solve the problem with ccpDistance, which calculate the distance between the last saved point and if it's not far enough I don't save it. I also tried to draw little circles at each saved positions, but this isn't really nice neither. Here's my code :
In my GameScene :
- (void) ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint location = [touch locationInView:[touch view]];
location = [[CCDirector sharedDirector] convertToGL:location];
if (ccpDistance(lastPoint, location) > 10) {
//SAVE THE POINT
[linePoints addObject:[NSValue valueWithCGPoint:location]];
[line updatePoints:linePoints];
lastPoint = location;
}
}
And my Line Class :
- (void) updatePoints:(NSMutableArray *)_point
{
points = _point;
}
- (void) draw
{
if ([points count] > 0) {
ccGLEnable(GL_LINE_STRIP);
ccDrawColor4B(209, 75, 75, 255);
float lineWidth = 6.0 * CC_CONTENT_SCALE_FACTOR();
glLineWidth(lineWidth);
int count = [points count];
for (int i = 0; i < (count - 1); i++){
CGPoint pos1 = [[points objectAtIndex:i] CGPointValue];
CGPoint pos2 = [[points objectAtIndex:i+1] CGPointValue];
ccDrawLine(pos1, pos2);
ccDrawSolidCircle(pos2, 2.5, 20);
}
}
}
Also, is there something in my code that could be done better to improve performance? Right now I don't have any problems even with 1000+ points, but just in case...
Any help will be greatly appreciated! Thanks in advance!

Ok, I found a website explaining really clearly how to do smooth lines, and it worked wonderfully! There's still anti-aliasing that's left to do, but maybe I will never do, since it looks really great on retina devices. Here's the website : Drawing Smooth Lines with Cocos2D
And here's the result :
Also, for those interested in the finished code, here it is :
Line.m
- (void) drawCurPoint:(CGPoint)curPoint PrevPoint:(CGPoint)prevPoint
{
float lineWidth = 6.0;
ccColor4F red = ccc4f(209.0/255.0, 75.0/255.0, 75.0/255.0, 1.0);
//These lines will calculate 4 new points, depending on the width of the line and the saved points
CGPoint dir = ccpSub(curPoint, prevPoint);
CGPoint perpendicular = ccpNormalize(ccpPerp(dir));
CGPoint A = ccpAdd(prevPoint, ccpMult(perpendicular, lineWidth / 2));
CGPoint B = ccpSub(prevPoint, ccpMult(perpendicular, lineWidth / 2));
CGPoint C = ccpAdd(curPoint, ccpMult(perpendicular, lineWidth / 2));
CGPoint D = ccpSub(curPoint, ccpMult(perpendicular, lineWidth / 2));
CGPoint poly[4] = {A, C, D, B};
//Then draw the poly, and a circle at the curPoint to get smooth corners
ccDrawSolidPoly(poly, 4, red);
ccDrawSolidCircle(curPoint, lineWidth/2.0, 20);
}
- (void) draw
{
if ([points count] > 0) {
ccGLEnable(GL_LINE_STRIP);
ccColor4F red = ccc4f(209.0/255.0, 75.0/255.0, 75.0/255.0, 1.0);
ccDrawColor4F(red.r, red.g, red.b, red.a);
float lineWidth = 6.0 * CC_CONTENT_SCALE_FACTOR();
glLineWidth(lineWidth);
int count = [points count];
for (int i = 0; i < (count - 1); i++){
CGPoint pos1 = [[points objectAtIndex:i] CGPointValue];
CGPoint pos2 = [[points objectAtIndex:i+1] CGPointValue];
[self drawCurPoint:pos2 PrevPoint:pos1];
}
}
}
As for the GameScene, nothing changed there (See the question for the code)! Note that you can change the line if (ccpDistance(lastPoint, location) > X), where X is the minimum distance between two points before the game saves another one. The lower X is, the smoother the line will be, but you will have way more points in your array, which could affect performance!
Anyway, thank you guys for your suggestions and your help, it helped me to get in the right way!

I think that you could smooth up your line drawing with some averaging.
- (void) updatePoints:(NSMutableArray *)_point
{
points = _point;
int count = [points count];
for (int i = 3; i < (count - 4); i++) {
CGPoint pos1 = [[points objectAtIndex:i - 2] CGPointValue];
CGPoint pos2 = [[points objectAtIndex:i - 1] CGPointValue];
CGPoint pos3 = [[points objectAtIndex:i] CGPointValue];
CGPoint pos4 = [[points objectAtIndex:i + 1] CGPointValue];
CGPoint pos5 = [[points objectAtIndex:i + 2] CGPointValue];
CGFloat xpos = (pos1.x + pos2.x + 2 * pos3.x + pos4.x + pos5.x)/6;
...
(now calcuclate ypos similarly and store the point into an array)
}
}

Related

Opposite of CGPathCreateCopyByStrokingPath?

Are there any core or other framework methods to return a CGPath or collection of some type that represents the Fill portion of a path object?
I am looking for something that would basically do the opposite of CGPathCreateCopyByStrokingPath.
With the help of an article on low-level-text-rendering I was able to come up with a path representing each letter of the alphabet.
The problem is that the path is the outline of the letter and what I would actually like to have is a path that represents only the fill portion of the letter instead of the stroke. For example the letter "A" would just consist of 3 distinct lines.
I am currently trying to work around this problem with class that acts much like an over complicated 16 segment display. Here is some sample code from that effort for anyone who is interested:
const NSUInteger segmentsPerDimension = 4;
const CGFloat oneOverTheNumberOfSegments = 1.0 / segmentsPerDimension;
#implementation PathSegments
- (instancetype)initWithRect:(CGRect)rect {
self = [super init];
[self setSegmentBounds:rect];
_segments = [[NSMutableArray alloc] init];
...
[self addVerticalSegments];
...
return self;
}
- (void)addVerticalSegments {
for (NSUInteger i = 0; i <= segmentsPerDimension; i++) {
[self createColumnSegmentsForRow:i];
}
}
- (void)createColumnSegmentsForRow:(NSUInteger)row {
for (NSUInteger i = 0; i < segmentsPerDimension; i++) {
CGFloat x = _segmentBounds.size.width * oneOverTheNumberOfSegments * row;
CGFloat yEnd = _segmentBounds.size.height * oneOverTheNumberOfSegments * i;
CGFloat yStart = _segmentBounds.size.height * oneOverTheNumberOfSegments * (i + 1);
[self addLineSegmentWithXStart:x YStart:yStart XEnd:x YEnd:yEnd];
}
}
...
#end
I have determined that it does not make sense to return the fill portion of a path. While a path can be defined in such a way to draw a circle, rectangle or custom shape by filling an outline, it would be ambiguous to copy the fill portion to another path because it would simply be the same path you started with.
A segmented path which is then combined into one full path using GPathAddPath(...) seems to be the way to go on this one. With a series lines and curves you can roll your own letter shapes.
A snippet based on the assumption you have defined a set of points to use as segments of the path:
_generatedSegmentPath = CGPathCreateMutable();
CGMutablePathRef subPath = CGPathCreateMutable();
...
if (CGPathIsEmpty(subPath)) {
CGPoint start = [[points objectAtIndex:startIndex] CGPointValue];
CGPathMoveToPoint(subPath, nil, start.x, start.y);
}
if (points.count == 2) {
CGPoint point = [[points objectAtIndex:startIndex + indexChange] CGPointValue];
CGPathAddLineToPoint(subPath, nil, point.x, point.y);
}
else {
CGPoint controlPoint = [[points objectAtIndex:startIndex + indexChange] CGPointValue];
CGPoint endPoint = [[points objectAtIndex:startIndex + (2 * indexChange)] CGPointValue];
CGPathAddQuadCurveToPoint(subPath, nil, controlPoint.x, controlPoint.y, endPoint.x, endPoint.y);
}
Then to combined the subpaths together:
CGPathAddPath(_generatedSegmentPath, nil, subPath);

Sticking nodes to SkSpriteNode after enlargement

Problem
As of right now, I've just realised a pretty major design flaw in my application..
So, the problem is:
A rifle shot is fired, and lands based on no trajectory as of right now, I'm toying with the idea. However, the bullets land and their marks are left as nodes.
-(void)applyShot:(int) posX with:(int) posY {
SKSpriteNode *impact = [[SKSpriteNode alloc] initWithColor:[UIColor grayColor] size:CGSizeMake(2, 2)];
impact.zPosition = 1;
impact.userInteractionEnabled = NO;
impact.position = CGPointMake(posX , posY);
[backgroundImage addChild:impact];
}
And posX / posY are sent this way.
//Find difference between centre and background moved distance.
CGPoint positionNow = CGPointMake(backgroundImage.position.x, backgroundImage.position.y);
CGPoint positionPrev = CGPointMake(0.5, 0.5);
float xdiff = positionNow.x - positionPrev.x;
float ydiff = positionNow.y - positionPrev.y;
//Calculate ydrop
int newYDrop = yDrop * 10;
//
CGPoint newPositionOne = CGPointMake(0.5 - xdiff, 0.5 - ydiff);
newPositionOne = CGPointMake((newPositionOne.x + [self myRandom:15 withFieldLower:-15]), (newPositionOne.y - newYDrop));
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(secondsDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^(void){
[self applyShot:newPositionOne.x with:newPositionOne.y];
});
Which seems to work fine, until I started toying with zoom.
Now, this application basically is a 1080p image for the reticle, and the background enlarges to give the (zoom) effect, then the background is touch-dragged/inverted to give the reticle moving effect.
Then I managed to get it to fire exactly where the crosshair was, which was good.. Then I started toying with zoom and noticed this.
So if it's not too easy to notice, the (bullet hits) are the grey marks, however, when you enlarge the backgroundImage they aren't tied to it. so they remain the same spread.
Solution
Now what I need is to either,
A: tie the nodes to the backgroundImage
B: enlarge the spread to the same factor as the enlargement.
But I'm pretty confused at how to achieve this.
Additional problem
When calibrating the drop, the drop will remain a burden no matter of the zoom as of right now. so if it's a 1 mil-dot drop on 6x zoom, it's also a 1 mil-dot drop at 10x zoom or 1x zoom ( I keep saying zoom when I mean enlargement )
How can I achieve a calibration for drop intensity no matter what enlargement the background image is at?
Thanks for reading and confusing yourself with my dire problems, it's appreciated!
What I've Tried
saving the children to a mutable array, then recreating them after a zoom change;
//recreate children
[backgroundImage removeAllChildren];
for (int i=0; i < [shotsHitX count]; i++) {
double posX = ([shotsHitX[i] doubleValue] / 5) * rifleZoom;
double posY = ([shotsHitY[i] doubleValue] / 5) * rifleZoom;
SKSpriteNode *impact = [[SKSpriteNode alloc] initWithColor:[UIColor grayColor] size:CGSizeMake(2, 2)];
impact.zPosition = 1;
impact.userInteractionEnabled = NO;
impact.position = CGPointMake(posX , posY);
[shotsHitX addObject:[NSNumber numberWithDouble:posX]];
[shotsHitY addObject:[NSNumber numberWithDouble:posY]];
[backgroundImage addChild:impact];
}
//end recreate children
Crash with memory error.
not so good at this!
[backgroundImage removeAllChildren];
for (int i=0; i < [shotsHitX count]; i++) {
double posX = ([shotsHitX[i] doubleValue] / 5) * rifleZoom;
double posY = ([shotsHitY[i] doubleValue] / 5) * rifleZoom;
SKSpriteNode *impact = [[SKSpriteNode alloc] initWithColor:[UIColor grayColor] size:CGSizeMake(2, 2)];
impact.zPosition = 1;
impact.userInteractionEnabled = NO;
impact.position = CGPointMake(posX , posY);
[backgroundImage addChild:impact];
}
//end recreate children
Works, however, now It doesn't just seem quite right..
I think the problem is when the initial zoom goes in it works, then when it reverts it's mixing zoom shots in the array with old shots.. Here we go again, MORE ARRAYS.
//recreate children
[backgroundImage removeAllChildren];
for (int i=0; i < [shotsHitRem count]; i+= 2) {
double posX = ([shotsHitRem[i] doubleValue] / 5) * rifleZoom;
double posY = ([shotsHitRem[i+1] doubleValue] / 5) * rifleZoom;
SKSpriteNode *impact = [[SKSpriteNode alloc] initWithColor:[UIColor grayColor] size:CGSizeMake(2, 2)];
impact.zPosition = 1;
impact.userInteractionEnabled = NO;
impact.position = CGPointMake(posX , posY);
[backgroundImage addChild:impact];
//add olds m4
if ([shotsHitM4 count] > 0) {
posX = ([shotsHitM4[i] doubleValue] / 2) * rifleZoom;
posY = ([shotsHitM4[i+1] doubleValue] / 2) * rifleZoom;
impact.position = CGPointMake(posX, posY);
[backgroundImage addChild:impact];
}
}
//end recreate children
Now I crash attempting to add a sknode which already has a parent
Confusing as it should removeAllChildren before looping
Well after some major messing around
[backgroundImage setScale:rifleZoom];
Programming is my favourite, oh yeah.. Five hours, Oh yeah.. For one line, oh yeah!
I wasn't scaling before, I was creating a new cgsize. and that was my problem.
I now have the issue of scaling and trying to render a new centrepoint as it still remembers the centrepoint of scale = 1 and scale = 2, nightmare.

Vertically scrolling background - Sprite Kit

I'm working on a game that is in portrait mode and can't seem to get this background to scroll smoothly on the y-axis. It doesn't properly add a second background so there is always a gap between the 2 backgrounds.
Heres the code:
static const float BG_VELOCITY = 100.0;
static inline CGPoint CGPointAdd(const CGPoint a, const CGPoint b)
{
return CGPointMake(a.x + b.x, a.y + b.y);
}
static inline CGPoint CGPointMultiplyScalar(const CGPoint a, const CGFloat b)
{
return CGPointMake(a.x * b, a.y * b);
}
Methods:
-(void)initalizingScrollingBackground
{
for (int i = 0; i < 2; i++) {
SKSpriteNode *bg = [SKSpriteNode spriteNodeWithImageNamed:#"bg"];
bg.zRotation = M_PI_2;
bg.position = CGPointMake(320, self.frame.origin.y);
bg.anchorPoint = CGPointZero;
bg.name = #"bg";
[self addChild:bg];
}
}
- (void)moveBg
{
[self enumerateChildNodesWithName:#"bg" usingBlock: ^(SKNode *node, BOOL *stop)
{
SKSpriteNode * bg = (SKSpriteNode *) node;
CGPoint bgVelocity = CGPointMake(0, -BG_VELOCITY);
CGPoint amtToMove = CGPointMultiplyScalar(bgVelocity,_dt);
bg.position = CGPointAdd(bg.position, amtToMove);
//Checks if bg node is completely scrolled of the screen, if yes then put it at the end of the other node
if (bg.position.y <= -bg.size.height)
{
bg.position = CGPointMake(bg.position.x,
bg.position.y + bg.size.height*2);
}
}];
}
What am I doing wrong that it has a gap in between the two backgrounds. Note: Background size is 568 x 320 if that is necessary info.
Thanks.
There a few problems. The biggest is the fact that your image is made in landscape mode. Simply edit the image in photoshop or preview to rotate it 90 degrees, then take out the zRotation. This will work for your height dilemma. Then change bg.position = CGPointMake(320,... etc to bg.position = CGPointMake (0, i * bg.size.height);
Also change your if statement to:
if (bg.position.y <= -bg.size.height)
{
bg.position = CGPointMake(0, bg.position.y + bg.size.height*2);
}
And your code will work.

How to continuously check collisions in Tiled tilemaps during a CCMoveTo

Hello good people of stack overflow
I stand before you today with a (not very great) cocos2d issue.
How can I check collisions WHILE in an action, instead of checking the target tile?
I am working with a tilemap that has a meta layer where there are collidable tiles. The following code works when I click the collidable tile, but not when I click beyond it, if that happens he will just walk straight through.
Currently, I detect collisions with this code:
-(void)setPlayerPosition:(CGPoint)position {
CGPoint tileCoord = [self tileCoordForPosition:position];
int tileGid = [_meta tileGIDAt:tileCoord];
if (tileGid) {
NSDictionary *properties = [_tileMap propertiesForGID:tileGid];
if (properties) {
NSString *collision = properties[#"Collidable"];
if (collision && [collision isEqualToString:#"True"]) {
return;
}
}
}
float speed = 10;
CGPoint currentPlayerPos = _player.position;
CGPoint newPlayerPos = position;
double PlayerPosDifference = ccpDistance(currentPlayerPos, newPlayerPos) / 24;
int timeToMoveToNewPostition = PlayerPosDifference / speed;
id moveTo = [CCMoveTo actionWithDuration:timeToMoveToNewPostition position:position];
[_player runAction:moveTo];
//_player.position = position;
}
By the way, this gets called from this method:
-(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
[_player stopAllActions];
CGPoint touchLocation = [touch locationInView:touch.view];
touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];
touchLocation = [self convertToNodeSpace:touchLocation];
CGPoint playerPos = _player.position;
CGPoint diff = ccpSub(touchLocation, playerPos);
int diffx = diff.x;
int diffy = diff.y;
int adiffx = diffx / 24;
int adiffy = diffy / 24; //Porque my tile size is 24
if ( abs(diff.x) > abs(diff.y) ) {
if (diff.x > 0) {
playerPos.x = playerPos.x + adiffx * 24;
} else {
playerPos.x = playerPos.x + adiffx * 24;
}
} else {
if (diff.y > 0) {
playerPos.y = playerPos.y + adiffy * 24;
}else{
playerPos.y = playerPos.y + adiffy * 24;
}
}
[self setPlayerPosition:playerPos];
}
I tried
[self schedule:#selector(setPlayerPosition:) interval:0.5];
Without any luck, that just instantly crashed the app at this
NSAssert( pos.x < _layerSize.width && pos.y < _layerSize.height && pos.x >=0 && pos.y >=0, #"TMXLayer: invalid position");
And i can't really blame it for crashing there.
How do I constantly check for collisions with the collidable meta tiles while a CCMoveTo is running?
In your init, schedule a collision detecting method:
[self schedule:#selector(checkCollisions:)];
Then define it as follows:
-(void)checkCollisions:(ccTime)dt
{
CGPoint currentPlayerPos = _player.position;
CGPoint tileCoord = [self tileCoordForPosition:currentPlayerPos];
int tileGid = [_meta tileGIDAt:tileCoord];
if (tileGid)
{
NSDictionary *properties = [_tileMap propertiesForGID:tileGid];
if (properties)
{
NSString *collision = properties[#"Collidable"];
if (collision && [collision isEqualToString:#"True"])
{
[_player stopAllActions];
return;
}
}
}
}
I using a similar meta layer. I personally skipped using MoveTo, and created my own update code that both moves the sprite and does collision detection.
I figure out what the potential new position would be, then I find out what tile position is in all four corners of the sprite at that new position, then I cycle through all tiles that the sprite would be on and if any are collidable, I stop the sprite.
I actually go a step further and check if moving only the x, or only the y changes the results, then move to that position instead if it does.

Smoothing hand drawn Bezier Curve

I am trying to smoothen my hand drawn Bezier curves, but not able to achieve, it, I got the code of smoothing the curve from the book written by erica sudan , which I tried to implement , but dont know why I am not able to produce a smooth curve, below is my code , I request all of you to please go through it ..
- (void)drawRect:(CGRect)rect
{
myPath.pathColor = [UIColor redColor];
for (BezeirPath *_path in m_pathArray)
{
[_path.pathColor setStroke];
[_path.bezeirPath strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *mytouch=[[touches allObjects] objectAtIndex:0];
m_previousPoint1 = [mytouch previousLocationInView:self];
m_previousPoint2 = [mytouch previousLocationInView:self];
m_currentPoint = [mytouch locationInView:self];
CGPoint controlPoint = CGPointMake(m_previousPoint1.x+(m_previousPoint1.x - m_previousPoint2.x), m_previousPoint1.y +(m_previousPoint1.y - m_previousPoint2.y));
[myPath.bezeirPath addQuadCurveToPoint:m_currentPoint controlPoint:controlPoint];
self.previousPoint3 = controlPoint;
[myPath.bezeirPath smoothedPath:myPath.bezeirPath :40];//Called the smoothing function.
[self setNeedsDisplay];
}
//Smoothing function
-(UIBezierPath*)smoothedPath:(UIBezierPath*)bpath: (NSInteger) granularity
{
NSArray *points = pointsFromBezierPath(bpath);
if (points.count < 4) return bpath;
// This only copies lineWidth. You may want to copy more
UIBezierPath *smoothedPath = [UIBezierPath bezierPath];
smoothedPath.lineWidth = bpath.lineWidth;
// Draw out the first 3 points (0..2)
[smoothedPath moveToPoint:POINT(0)];
for (int index = 1; index < 3; index++)
[smoothedPath addLineToPoint:POINT(index)];
// Points are interpolated between p1 and p2,
// starting with 2..3, and moving from there
for (int index = 4; index < points.count; index++)
{
CGPoint p0 = POINT(index - 3);
CGPoint p1 = POINT(index - 2);
CGPoint p2 = POINT(index - 1);
CGPoint p3 = POINT(index);
// now add n points starting at p1 + dx/dy up until p2
// using Catmull-Rom splines
for (int i = 1; i < granularity; i++)
{
float t = (float) i * (1.0f / (float) granularity);
float tt = t * t;
float ttt = tt * t;
CGPoint pi; // intermediate point
pi.x = 0.5 * (2*p1.x+(p2.x-p0.x)*t +
(2*p0.x-5*p1.x+4*p2.x-p3.x)*tt +
(3*p1.x-p0.x-3*p2.x+p3.x)*ttt);
pi.y = 0.5 * (2*p1.y+(p2.y-p0.y)*t +
(2*p0.y-5*p1.y+4*p2.y-p3.y)*tt +
(3*p1.y-p0.y-3*p2.y+p3.y)*ttt);
[smoothedPath addLineToPoint:pi];
}
[smoothedPath addLineToPoint:p2];
}
// finish by adding the last point
[smoothedPath addLineToPoint:POINT(points.count - 1)];
return smoothedPath;
}
I suggest watching "Building Advanced Gesture Recognizers" from WWDC 2012. You can skip the beginning - the part you are most interested in is at 38:23.

Resources