Simulate water/ make sprite "float" on water in spritekit
Asked Answered
P

2

6

I'm trying to add water to my game. Except for a different background color, there isn't much to it. However, I'd like the player-sprite to float on top of it (or halfway in it). If the player just walks into the water from below, I'd like him to float to the top. If he falls down, I'd like him to slowly change direction and float back up.

I tried making the gravity negative when he's in the water, but this gives me some slightly unwanted effects. For example as he (the player) surfaces, the normal gravity will push him back down, the water will push him up, and so on. Ultimately the player will be "bouncing" in the water, being pushed from one end to another. I'd like him to calmly remain on top of the water when he surfaces. How can I achieve this?

Here's the code I have in my update-loop:

SKNode *backgroundNodeAtPoint = [_bgLayer nodeAtPoint:_ball.position];
  if ([backgroundNodeAtPoint.name isEqualToString:@"WATER"]) {
    self.physicsWorld.gravity = CGVectorMake(self.physicsWorld.gravity.dx, 2);
  } else {
    if (self.physicsWorld.gravity.dy != -4) {
        self.physicsWorld.gravity = CGVectorMake(self.physicsWorld.gravity.dx, -4);
    }
}

Basically this changes my gravity to 2 when the player is in the water, and otherwise changes it to -4 unless it's already -4.

Thanks!

Plerre answered 16/8, 2014 at 23:4 Comment(2)
Have you looked at SKFieldNode? I haven't really played with it yet so I'm not entirely sure it's the right thing to use, but that'd be the first place I'd look.Nonproductive
@MikeS I have actually never heard about the SKFieldNode, but I'll take a look. Thanks!Plerre
S
20

There are three possible options I believe you have with regards to simulating water.

1) As mentioned in the comments you could try to use SKFieldNode (iOS 8+). But from personal experience the field node didn't really do it for me because you don't get much control over your simulation with it unless you heavily customize it, in which case you might as well just do your own calculations from scratch and reduce complexity.

2) You could adjust the linear and rotational damping of your sprite when inside the water. In fact, even apple mentions this in the quote from their documentation. However this won't give you buoyancy.

The linearDamping and angularDamping properties are used to calculate friction on the body as it moves through the world. For example, this might be used to simulate air or water friction.

3) Perform the calculations yourself. In the update method, check when the body enters you "water" and when it does you can calculate viscosity and/or buoyancy and adjust the velocity of your node accordingly. This in my opinion is the best option but also the more difficult.

Edit: I just wrote a quick example of option 3 in Swift. I think it's what you are looking for. I added factor constants on the top so you can adjust it to get exactly what you want. The motion is applied dynamically so it won't interfere with you current velocity (i.e. you can control your character while in the water). Below is the code for the scene and a gif as well. Keep in mind that the delta time is assumed to be 60 frames a second (1/60) and there is no velocity clamping. These are features you may or may not want depending on your game.

enter image description here

Swift

class GameScene: SKScene {
    //MARK: Factors
    let VISCOSITY: CGFloat = 6 //Increase to make the water "thicker/stickier," creating more friction.
    let BUOYANCY: CGFloat = 0.4 //Slightly increase to make the object "float up faster," more buoyant.
    let OFFSET: CGFloat = 70 //Increase to make the object float to the surface higher.
    //MARK: -
    var object: SKSpriteNode!
    var water: SKSpriteNode!
    override func didMoveToView(view: SKView) {
        object = SKSpriteNode(color: UIColor.whiteColor(), size: CGSize(width: 25, height: 50))
        object.physicsBody = SKPhysicsBody(rectangleOfSize: object.size)
        object.position = CGPoint(x: self.size.width/2.0, y: self.size.height-50)
        self.addChild(object)
        water = SKSpriteNode(color: UIColor.cyanColor(), size: CGSize(width: self.size.width, height: 300))
        water.position = CGPoint(x: self.size.width/2.0, y: water.size.height/2.0)
        water.alpha = 0.5
        self.addChild(water)
        self.physicsBody = SKPhysicsBody(edgeLoopFromRect: self.frame)
    }
    override func update(currentTime: CFTimeInterval) {
        if water.frame.contains(CGPoint(x:object.position.x, y:object.position.y-object.size.height/2.0)) {
            let rate: CGFloat = 0.01; //Controls rate of applied motion. You shouldn't really need to touch this.
            let disp = (((water.position.y+OFFSET)+water.size.height/2.0)-((object.position.y)-object.size.height/2.0)) * BUOYANCY
            let targetPos = CGPoint(x: object.position.x, y: object.position.y+disp)
            let targetVel = CGPoint(x: (targetPos.x-object.position.x)/(1.0/60.0), y: (targetPos.y-object.position.y)/(1.0/60.0))
            let relVel: CGVector = CGVector(dx:targetVel.x-object.physicsBody.velocity.dx*VISCOSITY, dy:targetVel.y-object.physicsBody.velocity.dy*VISCOSITY);
            object.physicsBody.velocity=CGVector(dx:object.physicsBody.velocity.dx+relVel.dx*rate, dy:object.physicsBody.velocity.dy+relVel.dy*rate);
        }
    }
    override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {object.position = (touches.anyObject() as UITouch).locationInNode(self);object.physicsBody.velocity = CGVectorMake(0, 0)}
}

Objective-C

#import "GameScene.h"
#define VISCOSITY 6.0 //Increase to make the water "thicker/stickier," creating more friction.
#define BUOYANCY 0.4 //Slightly increase to make the object "float up faster," more buoyant.
#define OFFSET 70.0 //Increase to make the object float to the surface higher.

@interface GameScene ()
@property (nonatomic, strong) SKSpriteNode* object;
@property (nonatomic, strong) SKSpriteNode* water;
@end

@implementation GameScene
-(void)didMoveToView:(SKView *)view {
    _object = [[SKSpriteNode alloc] initWithColor:[UIColor whiteColor] size:CGSizeMake(25, 50)];
    self.object.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:self.object.size];
    self.object.position = CGPointMake(self.size.width/2.0, self.size.height-50);
    [self addChild:self.object];
    _water = [[SKSpriteNode alloc] initWithColor:[UIColor cyanColor] size:CGSizeMake(self.size.width, 300)];
    self.water.position = CGPointMake(self.size.width/2.0, self.water.size.height/2.0);
    self.water.alpha = 0.5;
    [self addChild:self.water];
    self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
}
-(void)update:(NSTimeInterval)currentTime {
    if (CGRectContainsPoint(self.water.frame, CGPointMake(self.object.position.x,self.object.position.y-self.object.size.height/2.0))) {
        const CGFloat rate = 0.01; //Controls rate of applied motion. You shouldn't really need to touch this.
        const CGFloat disp = (((self.water.position.y+OFFSET)+self.water.size.height/2.0)-((self.object.position.y)-self.object.size.height/2.0)) * BUOYANCY;
        const CGPoint targetPos = CGPointMake(self.object.position.x, self.object.position.y+disp);
        const CGPoint targetVel = CGPointMake((targetPos.x-self.object.position.x)/(1.0/60.0), (targetPos.y-self.object.position.y)/(1.0/60.0));
        const CGVector relVel = CGVectorMake(targetVel.x-self.object.physicsBody.velocity.dx*VISCOSITY, targetVel.y-self.object.physicsBody.velocity.dy*VISCOSITY);
        self.object.physicsBody.velocity=CGVectorMake(self.object.physicsBody.velocity.dx+relVel.dx*rate, self.object.physicsBody.velocity.dy+relVel.dy*rate);
    }
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    self.object.position = [(UITouch*)[touches anyObject] locationInNode:self];
    self.object.physicsBody.velocity = CGVectorMake(0, 0);
}
@end
Saccule answered 17/8, 2014 at 19:53 Comment(6)
Hi! I'm sorry I couldn't get back to you earlier, but I've been busy moving, so I haven't had a chance to do anything but that for a while - I really like your third option, it is indeed what I'm looking for. However, I haven't looked into Swift yet (gonna do that after I finish this game). Could you convert this into Objective-C or give me some guidelines to how I can do that?Plerre
@Alekplay sure once I get a chance I'll add objective-c version.Saccule
Thanks! A problem I'm seeing tho, is that your water is made out of one large SKSpriteNode. My water is made out of multiple 32x32``SKSpriteNodes. Any idea how that would work together with what you've written?Plerre
@Alekplay Yes, there is a very simple solution. Just use a large CGRect for the water instead of making it a sprite and place its coordinates so that it fits over your already existing water. So you will have your 32x32 tiles of water, and then you will have an invisible water frame that has its coordinates and size set so that it overlaps your water. Or you can even keep the water as one large sprite and just set its color to clear and place it over your water tiles. Both solutions are essentially the same thing because they both rely on a frame for the water.Saccule
I considered that option, but the water does not necessarily have to be a square. Is it possible to create a SKSpriteNode from a number of other SKSpriteNodes?Plerre
@EpicByte Thank you for your great answer. Really amazes me and works perfect. Do you think it is possible to explain a little the calculations you make in the update method? I am not really sure what it does. Would be a great learning benefit. Thanks in advanceOleo
B
0

When your player makes contact with the water, push him up until he is no longer in contact with the water. At this point, modify the player's contact bit mask to collide with the water thus "making him walk on water".

Alternately, you can trigger the walk on water contact bit mask modification by a strategically placed invisible node instead waiting for water contact to occur.

To revert the player's contact bit mask back to normal use another predetermined contact the player will make, such as land or an invisible node, as the trigger.

Bullfrog answered 17/8, 2014 at 23:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.