RSS

Exploring iPhone Graphics Part 2

April 25th, 2008 Posted in Software Development, iPhone

Graphics Icon

In Part 1 of this series of articles we drew some simple graphic primitives on the iPhone display. In this article we are going to look at how to do some simple animation. The goal of this example is to create a 2D ball that bounces around the iPhone screen.

First of all we need an object to represent a point in our 2 dimensional coordinate system.





The Point2D object will take care of this and just keeps track of X and Y values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@interface Point2D : NSObject
{
    CGFloat X;
    CGFloat Y;
}
 
@property (assign) CGFloat X;
@property (assign) CGFloat Y;
 
- (id)initWithX:(CGFloat)x Y:(CGFloat)y;
- (void)addVector:(Vector2D*)vector;
 
@end
 
@implementation Point2D
 
@synthesize X;
@synthesize Y;
 
- (id)init
{
    if (self = [super init])
    {
        X = 0.0;
        Y = 0.0;
    }
 
    return self;
}
 
- (id)initWithX:(CGFloat)x Y:(CGFloat)y;
{
    if (self = [super init])
    {
        X = x;
        Y = y;
    }
 
    return self;
}
 
- (void)addVector:(Vector2D*)vector
{
    X += vector.endPoint.X;
    Y += vector.endPoint.Y;
}
 
@end

It can be initialized with an X and Y value using the initWithX:Y method. The addVector method will be used to move our ball around the screen.

Next we need a class that represents a vector. The vector will be used to determine the direction that our ball is currently moving in and its speed. The Vector2D class will take of of this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@interface Vector2D : NSObject
{
    CGFloat angle;
    CGFloat length;
    Point2D* endPoint;
}
 
@property (assign) CGFloat angle;
@property (assign) CGFloat length;
@property (assign) Point2D* endPoint;
 
- (id)initWithX:(CGFloat)x Y:(CGFloat)y;
- (void)setAngle:(CGFloat)degrees;
 
@end
 
// Geometry constants
#define PI 3.14159
#define ONEEIGHTYOVERPI 57.29582
#define PIOVERONEEIGHTY  0.01745
 
@implementation Vector2D
 
@synthesize angle;
@synthesize length;
@synthesize endPoint;
 
- (id)init
{
    if (self = [super init])
    {
        angle = 0.0;
        length = 0.0;
        endPoint = [[Point2D alloc] init];
    }
 
    return self;
}
 
- (id)initWithX:(CGFloat)x Y:(CGFloat)y;
{
    if (self = [super init])
    {
        endPoint = [[Point2D alloc] initWithX:x Y:y];
 
        // Calculate the angle based on the end point
        angle = atan2(-endPoint.Y, endPoint.X) * ONEEIGHTYOVERPI;
        if(angle < 0)
        {
            angle += 360;
        }
 
        // Calculate the length of the vector
        length = sqrt(endPoint.X * endPoint.X + endPoint.Y * endPoint.Y);
    }
 
    return self;
}
 
- (void)setAngle:(CGFloat)degrees
{
    angle = degrees;
    double radians = angle * PIOVERONEEIGHTY;
    endPoint.X = length * cos(radians); // could speed these up with a lookup table
    endPoint.Y = -(length * sin(radians));
}
 
@end

The Vector2D class manages the angle of the vector and the length of the vector. It also keeps track of the end point of the vector for convenience since we’ll be using the end point to move our ball. The vector can be initialize with an end point using the initWithX:Y method. The angle and length will automatically be calculated.

The angle is calculated from the end point by using the atan2 function. This function returns the angle in radians so we multiply the result by 180/PI to get the angle in degrees. You’ll also notice that we pass a negative Y coordinate to the atan2 function. This is because in our screen coordinate system the Y axis starts at zero at the top of the screen and has positive values as your go down the screen. The geometry functions we are using expect the opposite so we need to reverse the Y coordinates.

If the atan2 function returns a negative number we add 360 to make it positive.

To get the length of the vector we take the square root of x squared + y squared.

Calling the setAngle method will change the angle of the vector and update it’s end point to match the angle. First we convert the angle in degrees to radians by multiplying the angle by PI/180. We get the x end point by multiplying the length of the vector by the cosine of the angle. We get the y end point by multiplying the length of the vector by the sine of the angle. Notice we used a negative on the y value of the endpoint to fix the coordinate system problem.

Now we need a class to represent the ball we are going to move around the screen. I’m calling it Object2D because later this will most likely become a base class for other types of objects. It’ll just be a ball for now though.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
@interface Object2D : NSObject
{
    Point2D* position;
    Vector2D* vector;
    CGSize size;
}
 
@property (assign) Point2D* position;
@property (assign) Vector2D* vector;
@property (assign) CGSize size;
 
- (id)initWithPosition:(Point2D*)pos vector:(Vector2D*)vec;
- (void)move:(CGRect)bounds;
- (void)bounce:(CGFloat)boundryNormalAngle;
- (void)draw:(CGContextRef)context;
 
@end
 
// Screen edge normals
#define kLeftNorm 0.0
#define kLeftTopNorm 315.0
#define kTopNorm 270.0
#define kRightTopNorm 225.0
#define kRightNorm 180.0
#define kRightBottomNorm 135.0
#define kBottomNorm 90.0
#define kLeftBottomNorm 45.0
 
#define kDefaultSize 25.0
 
@implementation Object2D
 
@synthesize position;
@synthesize vector;
@synthesize size;
 
- (id)init
{
    if (self = [super init])
    {
        position = [[Point2D alloc] init];
        vector = [[Vector2D alloc] init];
        size.width = kDefaultSize;
        size.height = kDefaultSize;
    }
 
    return self;
}
 
- (id)initWithPosition:(Point2D*)pos vector:(Vector2D*)vec 
{
    if (self = [super init])
    {
        position = [pos retain];
        vector = [vec retain];
        size.width = kDefaultSize;
        size.height = kDefaultSize;
    }
 
    return self;
}
 
- (void)move:(CGRect)bounds
{
    // Move the ball by adding the vector to the position
    [position addVector:vector];
 
    // If the ball has hit the edge of the screen bounce it
    if (position.X <= bounds.origin.x && position.Y <= bounds.origin.y)
    {
        position.X = bounds.origin.x;
        position.Y = bounds.origin.y;
        [self bounce:kLeftTopNorm];
    }
    else if (position.X <= bounds.origin.x && position.Y+size.height >= bounds.size.height)
    {
        position.X = bounds.origin.x;
        position.Y = bounds.size.height - size.height;
        [self bounce:kLeftBottomNorm];
    }
    else if (position.X+size.width >= bounds.size.width && position.Y <= bounds.origin.y)
    {
        position.X = bounds.size.width - size.width;
        position.Y = bounds.origin.y;
        [self bounce:kRightTopNorm];
    }
    else if (position.X+size.width >= bounds.size.width && position.Y+size.height >= bounds.size.height)
    {
        position.X = bounds.size.width - size.width;
        position.Y = bounds.size.height - size.height;
        [self bounce:kRightBottomNorm];
    }
    else if (position.X <= bounds.origin.x)
    {
        position.X = bounds.origin.x;
        [self bounce:kLeftNorm];
    }
    else if (position.X+size.width >= bounds.size.width)
    {
        position.X = bounds.size.width - size.width;
        [self bounce:kRightNorm];
    }
    else if (position.Y <= bounds.origin.y)
    {
        position.Y = bounds.origin.y;
        [self bounce:kTopNorm];
    }
    else if (position.Y+size.height >= bounds.size.height)
    {
        position.Y = bounds.size.height - size.height;
        [self bounce:kBottomNorm];
    }
}
 
- (void)bounce:(CGFloat)boundryNormalAngle
{
    double angle = vector.angle;
    double oppAngle = (int)(angle + 180) % 360;
    double normalDiffAngle;
 
    if (boundryNormalAngle >= oppAngle)
    {
        normalDiffAngle = boundryNormalAngle - oppAngle;
        angle = (int)(boundryNormalAngle + normalDiffAngle) % 360;        
    }
 
    if (boundryNormalAngle < oppAngle)
    {
        normalDiffAngle = oppAngle - boundryNormalAngle;
        angle = boundryNormalAngle - normalDiffAngle;
        if (angle < 0)
        {
            angle += 360;
        }
    }
 
    // Set the new vector angle
    [vector setAngle:angle];
}
 
- (void)draw:(CGContextRef)ctx
{
    CGContextSetRGBFillColor(ctx, 255, 0, 0, 1);
    CGContextFillEllipseInRect(ctx, CGRectMake(position.X, position.Y, size.width, size.height));
}

The data stored for our ball is just its current position and its current vector. We also have a size that determines the width and height of the ball. If you just call init on the object you get a ball at 0,0 with a zero vector so it won’t move. If you call initWithPostion:vector you can pass in the initial position and vector.

The move method is called to actually move the ball. It adds the vector to the position then it checks to see if the ball has hit any of the screen edges including landing exactly in the corners of the screen. If it has hit a screen edge the bounce method is called and passed the normal angle for the screen edge that was hit.

The bounce method uses the normal angle passed to it to calculate the correct bounce angle for the ball. Then it calls setAngle on the vector to update it.

The draw method is passed a graphics context and draws the ball on it.

Now all we need is a view to make this work.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@interface GraphicsView : UIView
{
    Object2D* ball;
    NSTimer* timer;
}
 
- (void)tick;
 
@end
 
@implementation GraphicsView
 
- (id)initWithFrame:(CGRect)frameRect
{
    self = [super initWithFrame:frameRect];
 
    // Create a ball 2D object in the upper left corner of the screen
    // heading down and right
    ball = [[Object2D alloc] init];
    ball.position = [[Point2D alloc] initWithX:0.0 Y:0.0];
    ball.vector = [[Vector2D alloc] initWithX:5.0 Y:4.0];
 
    // Start a timer that will call the tick method of this class
    // 30 times per second
    timer = [NSTimer scheduledTimerWithTimeInterval:(1.0/30.0)
        target:self selector:@selector(tick) userInfo:nil repeats:YES];
 
    return self;
}
 
- (void)tick
{
    // Update the balls position
    [ball move:self.bounds];
 
    // Tell the view that it needs to re-draw itself
    [self setNeedsDisplay];
}
 
- (void)drawRect:(CGRect)rect
{
    // Clear the display and draw the ball
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextClearRect(ctx, rect);
    [ball draw:ctx];
}

The view class derives from UIView and will be created by the application delegate class. It just creates a single Object2D to represent our ball and starts an NSTimer to move the ball around. We initialize the ball at 0,0 on the screen and give it a vector with an X,Y of 5,4. We then start a timer that will be called every 1/30th of a second. The timer will call the tick method of the GraphicsView class.

The tick method calls the ball’s move method and passes in the bounds of the view to use for our bounce testing. It then calls setNeedsDisplay to notify the view that it needs to redraw itself.

The drawRect method gets the graphics context, clears it and then calls the ball’s draw method to tell it to draw itself.

That’s all there is to it. Pretty simple, eh? Next time we’ll tie in the accelerometer and maybe add some physics to simulate gravity.

Here are all of the source files:
main.m - ExploringGraphics2AppDelegate.h - ExploringGraphics2AppDelegate.m - GraphicsView.h - GraphicsView.m - Object2D.h - Object2D.m - Point2D.h - Point2D.m - Vector2D.h - Vector2D.m

Share and Enjoy:
  • StumbleUpon
  • Digg
  • Reddit
  • del.icio.us
  • Suggest to Techmeme via Twitter
  • Technorati
  • Slashdot
  • HackerNews
  • Twitter
  • Facebook
  • Print this article!

RSS feed | Trackback URI

5 Comments »

Comment by i42sasoi Subscribed to comments via email
2008-06-25 10:32:55

Hi,

first thanks a lot for your work. I think you make easy.

Reading this second part, inspired me with a problem I have drawing images.
I receive some images from a socket, and I´ve tried to display on the screen,I tried with [self setNeedsDisplay] but It is not call when a frame is received, It just show the last frame received.
I tried to make a function like:


- (void) draw:(CGRect) rect
{

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, rect);

[self drawImage:context];
}

-(void) drawImage:(CGContextRef)ctx
{
CGContextDrawImage(ctx, CGRectMake(0.0, 0.0,320,480), imageFrame.CGImage);
}

imageFrame is the image i´ve received throught the socket.
Then i call draw every time a frame is received, but I received the next error:

CGContextClearRect: invalid context
CGContextDrawImage: invalid context

Do you know what I mean?

Sorry If It is offtopic, but I dont know anybody to ask.
Sorry for my poor English too.
Thanks.

 
Comment by i42sasoi Subscribed to comments via email
2008-06-26 05:28:58

The problem is that the context is always null.Do you know any way to fix it?

 
Comment by iamdavehi Subscribed to comments via email
2008-07-02 06:15:08

Do you know if this works on the Beta 8 SDK?
i keep getting an exception error.

*** Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key window.”

any ideas?

thanks
-Dave

 
Comment by david Subscribed to comments via email
2008-10-18 20:41:01

Great tutorial! Reminds me of animating with actionscript book I read last year. Similar topics, but what I don’t understand is why you allocate memory for position and vector (point2d and vector2 classes) in the init method of Object2d, and then again another alloc in the Graphics View class where point2d and vector2d are allocated again in the initwithframe method?

I commented out the two allocs in the object2d init method and the animation still ran with out errors. Do you really need to allocate two separate copies of point2d and vector2d?

For what its worth memory management has allways confused me…

Thanks.

 
Comment by GNull Subscribed to comments via email
2009-03-14 03:52:00

I can’t seem to get this Tutorial working either. Would it be possible if someone provided the XCode project in compressed format?

I keep getting the following error. It compiles fun but when it actually launches the simulator (or on the iPhone) it just crashes. The report mentions the following:

Application Specific Information:
iPhone Simulator 2.2 (77.4.9), iPhone OS 2.2.1 (5H11)
*** Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key viewController.’

 
Name (required)
E-mail (required - never shown publicly)
URI
Subscribe to comments via email
Your Comment (smaller size | larger size)
You may use <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped=""> in your comment.