Double dispatch for collision handling with Sprite

2019-03-31 21:28发布

问题:

I'm using SpriteKit's collision detection. It has a callback that looks like this:

- (void)didBeginContact:(SKPhysicsContact *)contact

The contact object has two physics bodies:

SKPhysicsBody *bodyA;
SKPhysicsBody *bodyB;

My game will have lots of objects, and of course I can test the categoryBitMask to find out what collided with what. But given that I intend to have many kinds (not more than 32 of course) and might dynamically introduce new types, what's the most elegant way to do dynamic double dispatch to code the logic for collisions, explosions, points scored, etc that will result from all these collisions? Of course I can build a giant hairy if-statement, but I was hoping for something cleaner.

Maybe a lookup table storing selectors for the appropriate handlers? And then I index the lookup table by some combination of the categoryBitMasks? I'd love to hear some suggestions.

回答1:

I've created a working example of double dispatch for an SkPhysicsBodyContact using Pong as my game of choice. The working code is available on my github.

https://github.com/kouky/iOS-SpriteKit-Pong

You actually need to use the Visitor pattern to perform the double dispatch in your contact delegate as in objective-c we can't overload class methods arguments.

- (void)didBeginContact:(SKPhysicsContact *)contact
{

    SKPhysicsBody *firstBody, *secondBody;
    firstBody = contact.bodyA;
    secondBody = contact.bodyB;

    VisitablePhysicsBody *firstVisitableBody = [[VisitablePhysicsBody alloc] initWithBody:firstBody];
    VisitablePhysicsBody *secondVisitableBody = [[VisitablePhysicsBody alloc] initWithBody:secondBody];

    [firstVisitableBody acceptVisitor:[ContactVisitor contactVisitorWithBody:secondBody forContact:contact]];
    [secondVisitableBody acceptVisitor:[ContactVisitor contactVisitorWithBody:firstBody forContact:contact]];

}

The VisitablePhysicsBody and ContactVisitor are middlemen required to perform the double dispatch, they're pretty simple and the source code is in the project repo. They ultimately allow you to have classes which are solely concenred with handling contacts for certain types of nodes.

For example in my Pong example there is a class called BallNodeContactVisitor which only receives messages when contacts arise which involve a BallNode. There are methods within the class which follow a naming convention and allow us to determine the outcome of the BallNode contact with other node types such as PaddleNode.

@implementation BallNodeContactVisitor

// Handles contacts with PlayfieldScene edges
- (void)visitPlayfieldScene:(SKPhysicsBody *)playfieldBody
{

    BallNode *ball = (BallNode *) self.body.node;
    PlayfieldScene *playfield = (PlayfieldScene *) playfieldBody.node;
    // Perform something
}

// Handles contacts with PaddleNodes
- (void)visitPaddleNode:(SKPhysicsBody *)paddleBody
{
    BallNode *ball = (BallNode *) self.body.node;
    PaddleNode *paddle= (PaddleNode *) paddleBody.node;
    // Perform something else
}

@end


回答2:

Following is how the contact dispatch works in Kobold Kit.

The gist of it: you send each contacting node a message didBeginContact:withOtherBody: so each node on its own knows with which other body it made or lost contact. If you need the other body's node, you can get that from the SKPhysicsBody node property.

-(void) didBeginContact:(SKPhysicsContact *)contact
{
    SKPhysicsBody* bodyA = contact.bodyA;
    SKPhysicsBody* bodyB = contact.bodyB;
    SKNode* nodeA = bodyA.node;
    SKNode* nodeB = bodyB.node;
    for (id<KKPhysicsContactEventDelegate> observer in _physicsContactObservers)
    {
        SKNode* observerNode = observer.node;
        if (observerNode == nodeA)
        {
            [observer didBeginContact:contact otherBody:bodyB];
        }
        else if (observerNode == nodeB)
        {
            [observer didBeginContact:contact otherBody:bodyA];
        }
    }
}

-(void) didEndContact:(SKPhysicsContact *)contact
{
    SKPhysicsBody* bodyA = contact.bodyA;
    SKPhysicsBody* bodyB = contact.bodyB;
    SKNode* nodeA = bodyA.node;
    SKNode* nodeB = bodyB.node;
    for (id<KKPhysicsContactEventDelegate> observer in _physicsContactObservers)
    {
        SKNode* observerNode = observer.node;
        if (observerNode == nodeA)
        {
            [observer didEndContact:contact otherBody:bodyB];
        }
        else if (observerNode == nodeB)
        {
            [observer didEndContact:contact otherBody:bodyA];
        }
    }
}


回答3:

Why not inspect SKNode subclasses?

Then you can easily inspect for contacted body.node classes. Then create a configuration dictionary that gives you what method to call.

Some node subclasses...

Player : SKSpriteNode
Bullet : SKSpriteNode
Monster : SKSpriteNode

...then create methods like...

-(void)player:(Player*) player didBeginContactWithMonster:(Monster*) monster;
-(void)player:(Player*) player didBeginContactWithBullet:(Bullet*) bullet;

...then create a configuration like...

NSDictionary *contactDispatch = @{
@"player:didBeginContactWithMonster:" : @[ [Player class], [Mosnter class] ],
@"player:didBeginContactWithBullet:" : @[ [Player class], [Bullet class] ]
};

So you can check for containments of the contacted bodies, then instantiate a selector with NSSelectorFromString, then call it with performSelector:withObject:withObject:.

It may be optimized, normalized in may ways, but feels me as a way clean solution. Have not proofed yet, just came across.

Just noticed you just wrote almost the same at the end of the question itself. Will post it anyway. :D Wow, the comment suggestions just does the same. Aw.