Friday, November 12, 2010

Simple Game: Tap Ball (Catch The Ball)

It has been a looong time since I wrote any tutorials / sample codes. Recently a friend asked to create a simple base for a game where there is a character moving around randomly and upon tapping it, the character stops. So I created this tutorial for it.


Initially I just wanted to put a ball as the character, but end up with a weird creature image. It is just an image with a transparent background anyway.



If you are making a game instead of just animations, you should really consider using cocos2d. I have NEVER used cocos2d yet. I design my games from just using normal codings - coregraphics. If you plan to make graphic extensive game, then OpenGL is the only answer.

Ok, to create this simple game, we need to declare some objects and some methods. Obviously we need a UIImageView for the sprite. We also need a timer and a button.


IBOutlet UIImageView *Ball1;

NSTimer *mainTimer;

IBOutlet UIButton *startButton;


To animate objects smoothly in XCode, normally we use the UIView animation routine (that commitAnimations block of code). But the problem with this code is the object's property is set immediately to the final property set, even when the object is still animating. For example,
if we animate a UIImageView such as below:

myball.frame =  CGRectMake(0,0,50,50);

[UIView beginAnimations: @"MovingTheBall" context: nil]; 

[UIView setAnimationDelegate: self];

[UIView setAnimationDuration: 1.0];

[UIView setAnimationCurve: UIViewAnimationCurveEaseInOut];

myball.frame =  CGRectMake(100,100,50,50); <---

[UIView commitAnimations];

The moment the iOS runs the code up to the <-- arrow, myball already set to location (100,100) immediately even before it completes animating the movement. So this type of movement animation is useless for a game, because in a game you need to know exactly where the ball is at any instant. Thus we need to use NSTimer and move the UIImageView sprite step by step. And at each step, we will be able to check for collisions/touch location/and so on.

Next we define the methods needed for this project.


-(IBAction)startMove;

-(void)moveBall;

-(BOOL)Intersecting:(CGPoint)loctouch:(UIImageView *)enemyimg;

Then we need a few "Game Variables"
CGPoint Destination; // to be assigned with new destination of the sprite

CGFloat xamt, yamt; // x and y steps to move 

CGFloat speed = 50; // speed 

We need a method to trigger the ball movement when we tap on the button. startMove is the method:
-(IBAction)startMove {

startButton.hidden = YES; // we don't want user to instantiate another timer, so hide the button after it has been tapped.

// assign a random location within the view of size 320x480.

Destination = CGPointMake(arc4random() % 320, arc4random() % 480); 

// calculate steps based on speed specified and distance between the current location of the sprite and the new destination

xamt = ((Destination.x - Ball1.center.x) / speed);

yamt = ((Destination.y - Ball1.center.y) / speed);

// ask timer to execute moveBall repeatedly each 0.2 seconds. Execute the timer. 

mainTimer = [NSTimer scheduledTimerWithTimeInterval:(0.02) target:self selector:@selector(moveBall) userInfo:nil repeats: YES];

} 

Link this method to the Start button. When a user tap the start button, the button will dissappear, a new location destination for the sprite is given, the steps to take all the way to the new destination are calculated and then the move timer is fired up every 0.02 seconds. (the repeats: YES part tells the iOS to repeat calling the method moveBall forever).

Let's take a look at moveBall method (read the comments inside the function):

-(void)moveBall {

 // xdist and ydist is to check the current distance of the sprite to the destination point.

        // we are only interested in the distance so direction (-ve or +ve) does not matter, hence

        // we use fabs() function

 CGFloat xdist = fabs(Destination.x-Ball1.center.x); // fabs gives always +ve value

 CGFloat ydist = fabs(Destination.y-Ball1.center.y);

 

        // Normally, to compare 2 points, we can use CGPointEqualToPoint, but that requires an

        // exact match of points. In our case, since our steps are in CGFloat, each steps make the

        // location to be a non round number, but our destination is always a round number. So

        // we cannot use CGPointEqualToPoint. Instead we just check for the distance, if it is close

        // enough to the destination, then it is time to give it a new destination.

 if ((xdist<5)&&(ydist<5)) {

  Destination = CGPointMake(arc4random() % 320, arc4random() % 480);

  xamt = ((Destination.x - Ball1.center.x) / speed);

  yamt = ((Destination.y - Ball1.center.y) / speed);

 } else {

  //continue to move it if distance is still far

  Ball1.center = CGPointMake(Ball1.center.x+xamt, Ball1.center.y+yamt);

 }

}



Now, we need to check when user touch the screen, and if user does touch it, check whether the touch intersect with the sprite or not, if it touched, then stop animation and show startButton.

To detect touches, use one of the Touches delegate functions:


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

 UITouch *touch = [[event allTouches] anyObject];

 CGPoint location = [touch locationInView:self.view];

 // if user touched ball, stop timer

 if ([self Intersecting:location :Ball1]) {

  [mainTimer invalidate];

  mainTimer = nil;

  startButton.hidden = NO;

 }

} 




As you can see, we use another custom method called Intersecting. This is my custom method that I create to check for UIImageView intersection with a point. Sorry for the names of variables, I just copied it right out of my new game. This is a function that returns a BOOLEAN result. If the ImageView and the point intersects, it will return YES, if not, NO. And yes, you can also use CGRectContainsPoint which does the same thing. :P


-(BOOL)Intersecting:(CGPoint)loctouch:(UIImageView *)enemyimg {
 CGFloat x1 = loctouch.x;

 CGFloat y1 = loctouch.y;

 

 CGFloat x2 = enemyimg.frame.origin.x;

 CGFloat y2 = enemyimg.frame.origin.y;

 CGFloat w2 = enemyimg.frame.size.width;

 CGFloat h2 = enemyimg.frame.size.height;

 

 if ((x1>x2)&&(x1<x2+w2)&&(y1>y2)&&(y1<y2+h2))

  return YES;

 else

  return NO;

 
}


So the TouchesBegan delegate just to get the point of touch, and the check that point whether it intersects with the current location of the sprite. If it intersects, then we just stop the sprite from moving (Invalidate the timer, which will stop it), and show the startButton.

That's all folks.