Simulate water / make a sprite "float" on water in spritekit

I am trying to add water to my game. Except for a different background color, this is not much. However, I would like for player-sprite on top of it (or halfway in it). If a player just enters the water from below, I would like him to rise to the top. If he falls, I would like him to slowly change direction and swim.

I tried to make gravity negative when it is in water, but it gives me some unwanted effects. For example, when he (the player) lands, normal gravity will push him back, water will push him and so on. Ultimately, the player will "bounce" in the water, being pushed from one end to the other. I would like him to calmly stay on top of the water when he lands. How can I achieve this?

Here is 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); } } 

This basically changes my gravity to 2 when the player is in the water, and otherwise changes it to -4 if he is already -4 .

Thanks!

+7
ios sprite-kit game-physics
source share
2 answers

There are three possible options that, in my opinion, are relevant to water modeling.

1) As mentioned in the comments, you can try using SKFieldNode (iOS 8+). But from personal experience, the node field didn’t actually do this for me, because you don’t get much control over your simulation unless you configure it strongly, in which case you could just do your own calculations from scratch and reduce the complexity .

2) You can adjust the linear and rotational attenuation of your sprite in water. In fact, even an apple mentions this in a quote from its documentation. However, this will not give you buoyancy.

The linearDamping and angularDamping properties are used to calculate the friction of a body as it moves around the world. For example, it can be used to simulate the friction of air or water.

3) Perform the calculations yourself. In the update method, check when the body introduces you “water”, and when possible, you can calculate the viscosity and / or buoyancy and adjust the speed of your node accordingly. This, in my opinion, is the best option, but also more difficult.

Change I just wrote a quick example of option 3 in Swift. I think this is what you are looking for. I added factor constants at the top so you can tweak it to get exactly what you want. The movement is applied dynamically, so it will not interfere with your current speed (i.e. you can control your character in the water). Below is the code for the scene and gif. Keep in mind that delta time is considered to be 60 frames per second (1/60) and there is no speed clamp. You may or may not want these features 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 
+20
source share

When your player makes contact with water, push him until he no longer makes contact with water. At this point, change the player’s contact bitmask to collide with the water, thus "forcing him to walk on water."

Alternatively, you can activate the modification of the bit mask of the contact with the water contact using a strategically located invisible node, and not wait for the water to contact.

To return a player’s bit-to-bit to normal mode, use another predefined contact that the player will make, such as ground or an invisible node, as a trigger.

0
source share

All Articles