天天看點

iPhone Graphics 入門(二)

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.

@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.

@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.

@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 + ormalDiffAngle) % 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.

@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

繼續閱讀