Search code examples
vectorsprite-kitios8game-physics

Sprite Kit NPC random movement in a platform game


I've looked around Stack Overflow for a solution but have not found one to a problem I have with random NPC movement. Essentially, what I have coded up to now, is a simple 2D platformer game using Sprite Kit: there's a separate class for the NPC object. I initialize it in my GameScene (SKScene) no problem & so far it's behaving with the physicsWorld I have set up properly. Now I'm at the part where it simply needs to move randomly in any direction. I've set up the boundaries & have made it move with things like SKActions, utilizing things like CGPointMake, that would move the NPC randomly as needed, have it wait a little bit in that location & resume movement. BOOL's helped this process. However, I had difficulty getting the sprite to look left when moving left & looking right when moving right (looking up & down is not needed at all). So I found a way in a book by using Vector's. I set up a method in the NPC class which is used in the GameScene:

-(void)moveToward:(CGPoint)targetPosition
{
    CGPoint targetVector = CGPointNormalize(CGPointSubtract(targetPosition, self.position));
    targetVector = CGPointMultiplyScalar(targetVector, 150); //150 is interpreted as a speed: the larger the # the faster the NPC moves.
    self.physicsBody.velocity = CGVectorMake(targetVector.x, targetVector.y); //Velocity vector measured in meters per second.


    /*SPRITE DIRECTION*/
    [self faceCurrentDirection]; //Every time NPC begins to move, it will face the appropriate direction due to this method.
}

Now all of this works. But the issue at hand is calling this moveToward method appropriately in the update method. The 1st thing I tried was this:

-(void)update:(NSTimeInterval)currentTime
{
    /*Called before each frame is rendered*/

    if (!npcMoving)
    {
        SKAction *moving            = [SKAction runBlock:^{ npcMoving = YES }]; //THIS IS THE CULPRIT!
        SKAction *generate          = [SKAction runBlock:^{ [self generateRandomDestination]; }]; //Creates a random CGFloat X & CGFloat Y.
        SKAction *moveTowards       = [SKAction runBlock:^{ _newLocation = CGPointMake(fX, fY);
                                                            [_npc moveToward:_newLocation]; }]; //Moves NPC to that random location.
        SKAction *wait              = [SKAction waitForDuration:4.0 withRange:2.0]; //NPC will wait a little...
        [_npc runAction:[SKAction sequence:@[moving, generate, moveTowards, wait]] completion:^{ npcMoving = NO; }]; //...then repeat process.
    }
}

The vector method 'moveToward' requires 'update' method to be present for NPC movement to happen. I turn this off with 'npcMoving = YES' in the beginning in hopes that the NPC will move to targeted location & start the process again. This is not the case. If I remove SKAction with 'npcMoving = YES', the 'update' method calls upon the entire sequence of above SKActions every frame, which in turn doesn't move my NPC far. It simply has it change targeted location every frame, in turn creating an 'ADHD' NPC. Could someone please recommend what to do? I absolutely need to retain the vector movement for the directional properties & other future things but I am at a loss on how to properly implement this with the 'update' method.


Solution

  • Actions perform a task over time. If your npcMoving flag is false, you run an action sequence every frame, which means over 10 frames you will have 10 action sequences running simultaneously. That will cause undefined behavior.

    Next, even if you were to stop the existing sequence and run it anew, running an action every frame where at least one action has a duration is practically pointless. Because then that action with duration will not be able to complete its task in the given time because it'll be replaced the next frame.

    Summary: actions with duration are unsuitable for tasks that require adjustment every frame.

    Solutions:

    • perform tasks by changing the actor's properties (ie position etc) as/when needed (ie every frame)
    • decide on a task for the actor, then run the corresponding action sequence for that task and wait for it to end before you decide upon a new task