iOS – Maze

My latest app for the iPhone is about creating a maze pattern using the touchscreen and then guiding a ball through the maze by tilting the phone in the appropriate direction. You create the maze in a block-by-block fashion by tapping the screen with your finger. One tap puts a solid block. Another tap at the same location removes the solid block. You can create any maze pattern, for example the one shown below.

Screen Shot 2014-05-23 at 11.28.46 PM

Hitting the CLEAR button removes all solid blocks. The ball bounces off the edges of the domain and also bounces off solid blocks. Implementing the model logic where the ball bounces off the domain edges is quite straightforward. Extending the model to include the solid blocks required some additional thought and this was perhaps the main take away from this project for me.

The entire source code can be downloaded from:

https://github.com/jabhiji/ios-maze.git

The Model class contains all the necessary properties and methods to specify the structure of the maze and to specify the ball location and update the ball location with time. The maze is simply a 2D integer array (LGEO), where the array element is 1 if the location is a solid block and 0 otherwise. Here is the Model class interface:

#import <Foundation/Foundation.h>

@interface Model : NSObject

// LGEO array
@property int nx, ny;
@property NSMutableArray* LGEO;

// domain size
@property float width, height;

// ball
@property float x, y, R, ux, uy, ax, ay;
- (void) updateBallPosition;

// coefficient of restitution
@property float COR;

@end

The name “LGEO” stands for “Logical GEOmetry array” and is a terminology rollover from some of my old CFD research codes, where a similar 3D array is used to describe a porous material. The number of array elements along X and Y is nx and ny respectively and the size of the LGEO array is thus nx*ny. The width and height are obtained based on the size of the UIView and used to calculate the grid size along X and Y. The remaining parameters specify the ball size, location, velocity and acceleration. The implementation file shows the details about how the model handles collisions:

#import "Model.h"

@implementation Model

@synthesize nx, ny;
@synthesize LGEO;
@synthesize width, height;
@synthesize x,y,R,ux,uy,ax,ay;
@synthesize COR;

// override superclass implementation of init
-(id) init
{
    self = [super init];
    if (self) {
        nx = 12;
        ny = 12;
        LGEO = [[NSMutableArray alloc] initWithCapacity:nx*ny];
        for (int i = 0; i < nx*ny; i++) {
            int value = 0;
            [LGEO addObject:@(value)];
        }
        COR = 0.5;
    }
    
    return self;
}

- (void) updateBallPosition
{
    // kinematics
    x += 0.5*ux;
    y += -0.5*uy;
    
    // check for collisions with walls
    if (x > width - R) {
        x = width - R;
        ux = -fabsf(ux)*COR;
    }
    if (y > height - R) {
        y = height - R;
        uy = fabsf(uy)*COR;
    }
    if (x < R) {
        x = R;
        ux = fabsf(ux)*COR;
    }
    if (y < R) {
        y = R;
        uy = -fabsf(uy)*COR;
    }
    
    // check for collision with maze blocks
    float dx = width / nx;
    float dy = height / ny;
    
    // find out array location where the ball center is located
    int xIndex = x/dx;
    int yIndex = y/dy;
    
    // loop over all neighboring squares
    NSMutableArray* xCenter, *yCenter, *distanceFromCenter;
    NSMutableArray *Nindex, *xIndexNbr, *yIndexNbr;
    xCenter = [[NSMutableArray alloc] initWithCapacity:9];
    yCenter = [[NSMutableArray alloc] initWithCapacity:9];
    distanceFromCenter = [[NSMutableArray alloc] initWithCapacity:9];
    Nindex  = [[NSMutableArray alloc] initWithCapacity:9];
    xIndexNbr = [[NSMutableArray alloc] initWithCapacity:9];
    yIndexNbr = [[NSMutableArray alloc] initWithCapacity:9];
    
    [xCenter insertObject:@(dx/2 + (xIndex-1)*dx) atIndex:0];
    [xCenter insertObject:@(dx/2 + (xIndex)  *dx) atIndex:1];
    [xCenter insertObject:@(dx/2 + (xIndex+1)*dx) atIndex:2];
    [xCenter insertObject:@(dx/2 + (xIndex-1)*dx) atIndex:3];
    [xCenter insertObject:@(dx/2 + (xIndex)  *dx) atIndex:4];
    [xCenter insertObject:@(dx/2 + (xIndex+1)*dx) atIndex:5];
    [xCenter insertObject:@(dx/2 + (xIndex-1)*dx) atIndex:6];
    [xCenter insertObject:@(dx/2 + (xIndex)  *dx) atIndex:7];
    [xCenter insertObject:@(dx/2 + (xIndex+1)*dx) atIndex:8];

    [yCenter insertObject:@(dy/2 + (yIndex-1)*dx) atIndex:0];
    [yCenter insertObject:@(dy/2 + (yIndex-1)*dx) atIndex:1];
    [yCenter insertObject:@(dy/2 + (yIndex-1)*dx) atIndex:2];
    [yCenter insertObject:@(dy/2 + (yIndex)  *dx) atIndex:3];
    [yCenter insertObject:@(dy/2 + (yIndex)  *dx) atIndex:4];
    [yCenter insertObject:@(dy/2 + (yIndex)  *dx) atIndex:5];
    [yCenter insertObject:@(dy/2 + (yIndex+1)*dx) atIndex:6];
    [yCenter insertObject:@(dy/2 + (yIndex+1)*dx) atIndex:7];
    [yCenter insertObject:@(dy/2 + (yIndex+1)*dx) atIndex:8];

    [Nindex insertObject:@((xIndex-1) + nx*(yIndex-1)) atIndex:0];
    [Nindex insertObject:@((xIndex  ) + nx*(yIndex-1)) atIndex:1];
    [Nindex insertObject:@((xIndex+1) + nx*(yIndex-1)) atIndex:2];
    [Nindex insertObject:@((xIndex-1) + nx*(yIndex  )) atIndex:3];
    [Nindex insertObject:@((xIndex  ) + nx*(yIndex  )) atIndex:4];
    [Nindex insertObject:@((xIndex+1) + nx*(yIndex  )) atIndex:5];
    [Nindex insertObject:@((xIndex-1) + nx*(yIndex+1)) atIndex:6];
    [Nindex insertObject:@((xIndex  ) + nx*(yIndex+1)) atIndex:7];
    [Nindex insertObject:@((xIndex+1) + nx*(yIndex+1)) atIndex:8];

    [xIndexNbr insertObject:@(xIndex-1) atIndex:0];
    [xIndexNbr insertObject:@(xIndex  ) atIndex:1];
    [xIndexNbr insertObject:@(xIndex+1) atIndex:2];
    [xIndexNbr insertObject:@(xIndex-1) atIndex:3];
    [xIndexNbr insertObject:@(xIndex  ) atIndex:4];
    [xIndexNbr insertObject:@(xIndex+1) atIndex:5];
    [xIndexNbr insertObject:@(xIndex-1) atIndex:6];
    [xIndexNbr insertObject:@(xIndex  ) atIndex:7];
    [xIndexNbr insertObject:@(xIndex+1) atIndex:8];
    
    // check for possible collision with all applicable neighbors
    for (int index = 0; index < 9; index++) {
        int Nval = [[Nindex objectAtIndex:index] intValue];
        int xNbr = [[xIndexNbr objectAtIndex:index] intValue];
        float xCen = [[xCenter objectAtIndex:index] floatValue];
        float yCen = [[yCenter objectAtIndex:index] floatValue];

        if (xNbr >= 0 && xNbr < nx
              && index != 4
            && fabsf(x-xCen) < (dx/2 + R)
            && fabsf(y-yCen) < (dy/2 + R)
            && Nval >= 0 && Nval < nx*ny) {

            // ball coordinates relative to the square center
            float xRel = x - xCen;
            float yRel = y - yCen;

            int val = [[LGEO objectAtIndex:Nval] intValue];
            
            if (val == 1) {
                
            // hit a solid block
            if (fabsf(xRel) > fabsf(yRel)) {
                ux = -COR*ux;
                x += ux;
            } else {
                uy = -COR*uy;
                y += -uy;
            }
                
            }

        }
    }

    // dynamics
    ux += 0.2*ax;
    uy += 0.2*ay;
}

@end

The basic idea of the ball-maze collision logic is this: At each time instant, we find out the location of the ball within the 2D (LGEO) array and find out the status (1 or 0) of all 8 neighboring cells. If the ball hits a neighboring solid block, we reverse the appropriate velocity component depending upon the location of the impact. The picture below shows a schematic of the idea, where I show balls impacting a solid square block from various directions.

Screen Shot 2014-05-24 at 5.46.30 AM

For the balls on the left and right of the block, the x-component of the ball center (relative to the square center) is more than the y-component. In such a case, we reverse the x-component of the velocity. For the balls to the top and bottom, we reverse the y-component of the velocity.

The values in the LGEO array can be changed in real-time by the user. These touch-based events are handled within the Controller. The method used to implement this logic is copied below for reference.

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    for (UITouch* t in touches) {
        CGPoint touchLocation;
        touchLocation = [t locationInView:blackBox];
        float x = touchLocation.x;
        float y = touchLocation.y;

        float width = blackBox.frame.size.width;
        float height = blackBox.frame.size.height;
        
        float dx = width / model.nx;
        float dy = height / model.ny;
        
        // find out array location where finger touches the screen
        int xIndex = x/dx;
        int yIndex = y/dy;
        int N = xIndex + model.nx*yIndex;
        
        // if touch is within array bounds, toggle LGEO value
        if (xIndex >=0 && xIndex < model.nx && yIndex >=0 && yIndex < model.ny) {
            int val = [[model.LGEO objectAtIndex:N] intValue];
            
            // update entry at that location in the model
            [model.LGEO removeObjectAtIndex:N];
            [model.LGEO insertObject:@(1-val) atIndex:N];

            [maze.LGEO removeObjectAtIndex:N];
            [maze.LGEO insertObject:@(1-val) atIndex:N];
        }
    }
}

Notice that we have a bounds check to make sure we don’t specify memory locations outside the specified array limits. Each touch in the UIView window toggles the LGEO array value at that location.

A side note: For those of us coming from C/C++, it may seem a little strange to not have to constantly clean up after our array declarations on the heap. Objective-C implements this clean up automagically via automatic reference counting.

Now that you understand the basic idea of how to specify a maze and implement collision detection, it is just a matter of detail to extend this project to create a game with various difficulty levels and keep track of the score and things of that sort. Perhaps I will return some day to make a game of this sort.

Bis dahin, Happy Xcoding!

Leave a Reply

Your email address will not be published. Required fields are marked *