在SpriteKit中模拟水/使精灵在水上“漂浮”

6
我正在尝试给我的游戏添加水。除了不同的背景颜色外,没有太多其他内容。然而,我希望player-sprite浮在水面上(或者在水中间)。如果玩家从下方走进水里,我希望他能漂浮到水面上。如果他掉落下去,我希望他能缓慢改变方向并浮出水面。
我尝试在他处于水中时将重力变为负值,但这会给我带来一些不太理想的效果。例如,当他(玩家)浮出水面时,正常重力会将他推回水下,而水会将他推上来,反复循环。最终玩家会在水中"弹跳",被从一端推到另一端。我希望他浮在水面上时能保持平静。我该如何实现这一点?
这是我在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);
    }
}

基本上,当玩家在水中时,这会将我的重力更改为2,否则将其更改为-4,除非它已经是-4
谢谢!

你看过SKFieldNode吗?我还没有真正尝试过它,所以我不确定它是否是正确的选择,但那应该是我首先会查看的地方。 - Mike S
@MikeS 我其实从来没有听说过 SKFieldNode,但我会去看一下。谢谢! - Aleksander
2个回答

20
我认为您在模拟水时有三种可能的选择。
1)如评论中所述,您可以尝试使用SKFieldNode(iOS 8+)。但是根据个人经验,该字段节点并没有真正满足我的需求,因为除非您对其进行大量自定义,否则您无法对模拟进行很好地控制,如果这样的话,您可能会最好从头开始计算并减少复杂性。
2)当精灵在水中时,您可以调整其线性和旋转阻尼。实际上,甚至苹果在他们的文档中也提到了这一点。但是这不会给您浮力。

linearDamping和angularDamping属性用于计算物体在移动过程中的摩擦力。例如,这可以用来模拟空气或水的摩擦。

3)自己执行计算。在update方法中,检查物体何时进入“水”以及何时可以计算粘度和/或浮力,并相应地调整节点的速度。在我看来,这是最好的选择,但也是更困难的选择。 编辑:我刚刚用Swift写了一个选项3的快速示例。我认为这就是您要寻找的内容。我在顶部添加了因子常数,因此您可以将其调整为完全符合您的要求。运动是动态应用的,因此它不会干扰您当前的速度(即您可以在水中控制您的角色)。下面是场景的代码和gif。请注意,假定delta time为每秒60帧(1/60),并且没有速度夹紧。这些是您可能需要或不需要的功能,具体取决于您的游戏。
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

你好!很抱歉之前没能及时回复你,因为我一直在忙着搬家,所以有段时间除了这个事情以外什么都没做。我非常喜欢你的第三个选项,它确实是我正在寻找的。然而,我还没有研究过Swift(等我完成这个游戏后会去学习),你能把它转换成Objective-C或者给我一些指导吗? - Aleksander
1
@Alekplay 确定,一旦我有机会,我会添加Objective-C版本。 - Epic Byte
1
@Alekplay,我已经添加了它。 - Epic Byte
谢谢!我看到的一个问题是,你的水是由一个大的SKSpriteNode制成的。我的水是由多个32x32SKSpriteNode组成的。你有什么想法如何将它与你所写的内容结合起来? - Aleksander
@EpicByte,非常感谢您的出色回答。真的让我惊叹,而且完美无缺。您认为能否稍微解释一下您在update方法中进行的计算?我不太确定它具体是做什么的。这将是一个很好的学习机会。提前致谢。 - boehmatron
显示剩余3条评论

0
当你的玩家接触到水时,将他推起直到他不再与水接触。此时,修改玩家的接触位掩码以与水碰撞,从而“让他在水上行走”。
或者,你可以通过一个策略性放置的隐形节点来触发步行水上接触位掩码的修改,而不是等待水接触发生。
为了将玩家的接触位掩码恢复正常,使用另一个预定的玩家接触点,例如陆地或隐形节点,作为触发器。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接