Search code examples
iosvectorcocos2d-iphonegame-physics

Restrict sprite movement to vertical and horizontal


I have been battling with this for some time and my noob brain can't quite work it out. I have a standard tile map and currently use the following code to move my enemy sprite around the map

-(void) movePlayer:(ccTime)deltaTime {

    if (CGPointEqualToPoint(self.position, requestedPosition))
        return;


    float step = kPlayerSpeed * deltaTime;
    float dist = ccpDistance(self.position, requestedPosition);
    CGPoint vectorBetweenAB = ccpSub(self.position, requestedPosition);

    if (dist <= step) {
        self.position = requestedPosition;
        [self popPosition];
    } else {
        CGPoint normVectorBetweenAB = ccpNormalize(vectorBetweenAB);
        CGPoint movementVectorForThisFrame = ccpMult(normVectorBetweenAB, step);

        if (abs(vectorBetweenAB.x) > abs(vectorBetweenAB.y)) {
            if (vectorBetweenAB.x > 0) {
                [self runAnimation:walkLeft];
            } else {
                [self runAnimation:walkRight];
            }
        } else {
            if (vectorBetweenAB.y > 0) {
                [self runAnimation:walkDown];
            } else {
                [self runAnimation:walkUp];
            }
        }

        if (self.position.x > movementVectorForThisFrame.x) {
            movementVectorForThisFrame.x = -movementVectorForThisFrame.x;
        }
        if (self.position.y > movementVectorForThisFrame.y) {
            movementVectorForThisFrame.y = -movementVectorForThisFrame.y;
        }
        self.position = ccpAdd(self.position, movementVectorForThisFrame);
    }

}

movePlayer: is called by the classes updateWithDeltaTime: method. the ivar requestedPosition is set in the updateWithDeltaTime method as well, it basically gets the next point out of a queue to move to. These points can be anywhere on the map, so if they are in a diagonal direction from the enemy the enemy sprite will move directly to that point. But how do I change the above code to restrict the movement to vertical and horizontal movement only so that the enemies movement 'staircases' its way along a diagonal path, taking the manhattan distance (I think its called). As shown by my crude drawing below... S being the start point F being the finish and the numbers being each intermediate point along its path to create a staircase type diagonal movement. Finally I intend to be able to toggle this behaviour on and off, so that I can choose whether or not I want the enemy to move free around the map or be restricted to this horizontal / vertical movement only.

 | | | | | | | | | |
 | | | | | | | | | |
 | |F| | | | | | | |
 | |5|4| | | | | | |
 | | |3|2| | | | | |
 | | | |1|S| | | | |
 | | | | | | | | | |
 | | | | | | | | | |
 | | | | | | | | | |
 | | | | | | | | | |

Solution

  • So the current solution I have found is as follows...

    -(void)pathfind {
        //Get a reference to the tile map
        CCTMXTiledMap *tileMap = (CCTMXTiledMap *)[[[[CCDirector sharedDirector] runningScene] getChildByTag:kStartLayer] getChildByTag:kMapNode];
        NSAssert(tileMap != nil, @"No Tile map");
    
        //Convert self.position to tile coordinates and set to both vars. stepTileCoord so that we can add a tile in direction of travel.
        CGPoint selfTileCoord = [tileMap tileCoordFromLocation:self.position];
        CGPoint stepTileCoord = [tileMap tileCoordFromLocation:self.position];
    
        //Init this var to keep track of the last tile worked out so the next one can be worked out from it. We start at self.position.
        CGPoint lastTileCoord = selfTileCoord;
    
        //For loop, iterates over each orginal position in the requestedPositionQueue.
        for (int i=0; i < [requestedPositionQueue count]; i++) {
    
            //Get the current original path tile coordinate.
            CGPoint requestedTileCoord = [[requestedPositionQueue objectAtIndex:i] CGPointValue];
            NSMutableArray *tempPoints = [NSMutableArray array];
    
            //While loop interates until it has worked out every intermediate step to the original path tile.
            while (!CGPointEqualToPoint(requestedTileCoord, stepTileCoord)) {
    
                //FIXME: what happens if path takes us back over the orignal start point?
                if (CGPointEqualToPoint(selfTileCoord, requestedTileCoord)) {
                    //Not sure if this will cause an issue.
                    break;
                }
    
                CGPoint vectorToDest = ccpSub(lastTileCoord, requestedTileCoord);
    
                if (abs(vectorToDest.x) > abs(vectorToDest.y)) {
                    if (vectorToDest.x > 0) {
                        stepTileCoord.x -= 1;
                    } else {
                        stepTileCoord.x += 1;
                    }
                } else {
                    if (vectorToDest.y > 0) {
                        stepTileCoord.y -= 1;
                    } else {
                        stepTileCoord.y += 1;
    
                    }
                }
    
                //If the tile worked out is the requestedTileCoord then break, no need to add it to tempPoints as it will already be in the requestedPosition Queue.
                if (CGPointEqualToPoint(requestedTileCoord, stepTileCoord)) {
                    break;
    //                CCLOG(@"%@", [requestedPositionQueue description]);
                } else {
                    //Save this tile to tempPoints arrway. And save it to lastTileCoord.
                    [tempPoints addObject:[NSValue valueWithCGPoint:stepTileCoord]];
                    lastTileCoord = stepTileCoord;
    //                CCLOG(@"\n{%f, %f}", lastTileCoord.x, lastTileCoord.y);
                }
            }
            //As we have now got an array, tempPoints, with all the intermediate step points to current point out of requestedPositionQueue we shall add all objects in tempPoints to requestedPositionQueue. NSIndexSet will ensure its added at the correct location in array.
            NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(i, [tempPoints count])];
            [requestedPositionQueue insertObjects:tempPoints atIndexes:indexes];
    //        CCLOG(@"%@", [requestedPositionQueue description]);
            //i is updated so the next point pulled out of the array isn't one of the worked out intermediate steps.
            i += [tempPoints count];
        }
    }
    

    So to begin with, I have the path of tile coordinates for the character saved in an ivar array for example...

    requestedPositionQueue = [NSMutableArray arrayWithObjects:
                                          [NSValue valueWithCGPoint:CGPointMake(7, 18)],
                                          [NSValue valueWithCGPoint:CGPointMake(11, 18)],
                                          [NSValue valueWithCGPoint:CGPointMake(7, 22)],
                                          [NSValue valueWithCGPoint:CGPointMake(7, 18)],
                                          [NSValue valueWithCGPoint:CGPointMake(11, 22)],
                                          nil];
    

    So as you can see this has some points which would require so form of diagonal movement. I then call the pathfind method and this iterates over the requestedPositionQueue and adds all the intermediate tile coordinates ensuring it always picks a tile either vertically or horizontally but never diagonally next to the current tile until it completes the entire path.

    Then in the update: method I iterate over the requestedPositionArray, taking the next tile coordinate from it, convert it to a pixel position then assign that to my requestedPosition ivar and then run the movePlayer: method (code in the original post).

    I have put comments in the pathfind method for my own benefit and thus may not be that comprehensive but they may help your understanding of my thinking.

    Finally this is the best I have come up with which allows me to ensure I can either move my character to a pixel position without restrictions or if I want move them there but confine them to only moving vertically or horizontally via tile centres. So if you can see a way to refine and optimise this code please do, or any constructive criticism will always be appreciated.

    EDIT: The FIXME: in there highlights a potential bug that I haven't had time to look at yet. I will repost once I have investigated and fixed if required.