SceneKit自学之路(终)

有阵子没玩这个了。

其实这玩意儿学了我工作也用不上,之前本来想学来玩玩ARKit,结果手机太旧了不支持(手动捂脸)。本着有始有终,花了两天时间留下最后一个相对完整的Demo,SceneKit的学习也暂告一段落。如果有什么不懂的,也可以留言讨论。
LittlerJumper:下载地址:https://github.com/anjohnlv/LittleJumper


我把它称为粗暴版跳一跳,Demo很粗暴也很简陋。

为了方便初学者学习SceneKit,整个Demo,SceneKit内容纯代码完成。且没有任何框架设计,所有的代码都在ViewController中,加上注释一共接近400行。

注释还是很详细了。应该都能看懂并理解。

游戏截图:


Little Jumper

简单总结一下:

  • 创建工程。
    和之前的Demo一样,我直接新建的Single View App。原因就不多重复。


    SceneKit自学之路(终)_第1张图片
    Single View App
  • 初始化场景。
    SceneKit中所有的物体、行为都要在SCNScene中,而SCNScene需要在SCNView中。
    在Demo中,包括SCNViewSCNScenefloorcameralight等,这些一开始就要准备好的元素。我为了体现游戏的操作过程,把这些初始化都放在了自己身上懒加载。
    Demo里的注释很详细,我挑一些说
    想要目标始终在视线范围内,我们得在“小人”跳走后让镜头跟随。可是如果一直让镜头跟随小人,会让整个游戏看起来特别晃。所以我让相机跟随站台,每成功跳一次,将相机移动观察新站台。

-(void)moveCameraToCurrentPlatform {
    SCNVector3 position = self.platform.presentationNode.position;
    position.x += 20;
    position.y += 30;
    position.z += 20;
    SCNAction *move = [SCNAction moveTo:position duration:0.5];
    [self.camera runAction:move];
    [self createNextPlatform];
}

x,y,z值是目测的,由于只是个Demo,所以细节方面没有太讲究。
如果这是这么写,你会发现,跳着跳着就看不见了。原因是你的光源始终照在远处,离光源远了,自然就看不到了。在这里,我是直接让光源始终跟随镜头。实现方法是在初始化相机之后,直接将光源设为相机的子节点。

-(SCNNode *)camera {
    if (!_camera) {
        _camera = ({
            SCNNode *node = [SCNNode node];
            node.camera = [SCNCamera camera];
            node.camera.zFar = 200.f;
            node.camera.zNear = .1f;
            [self.scene.rootNode addChildNode:node];
            node.eulerAngles = SCNVector3Make(-0.7, 0.6, 0);
            node;
        });
        [_camera addChildNode:self.light];
    }
    return _camera;
}
  • 事件
    这个Demo其实很简单。整个游戏过程梳理下来:

初始化->点击屏幕蓄力->释放跳跃->判断成功->移动相机->生成下一个跳台->下一次跳跃->判断失败->游戏结束

蓄力的过程用到了长按手势,对,就和写App里的长按一样。SCNView是基于UIView的,可以直接将手势加在上面。设置longPressGesture.minimumPressDuration = 0;保证短按也能监听到。这里有一个知识点是自定义SCNAction的使用。

-(void)updateStrengthStatus {
    SCNAction *action = [SCNAction customActionWithDuration:kMaxPressDuration actionBlock:^(SCNNode * node, CGFloat elapsedTime) {
        CGFloat percentage = elapsedTime/kMaxPressDuration;
        self.jumper.geometry.firstMaterial.diffuse.contents = [UIColor colorWithRed:1 green:1-percentage blue:1-percentage alpha:1];
    }];
    [self.jumper runAction:action];
}

很简单地实现了颜色的渐变动画。力量越大,颜色越红。在释放跳跃的瞬间,取消Action即可。

[self.jumper removeAllActions];

跳跃的过程得先提后面生成新跳台。Demo里新跳台的生成,是范围内随机大小,随机颜色、随机方向、随机距离。所以跳跃的时候,需要判断“小人”和目标跳台的方向。我们要保证方向向量上单位力量为恒定的,这样当通过时间来增加力量时才有意义。
这个是数学知识,已知三角形斜边长度和两边直角边比,求直角边长度。这个不多说。

移动相机上文已经提到就不再复述,接下来着重说明一下自己生成下一个站台的方法:

-(void)createNextPlatform {
    self.nextPlatform = ({
        SCNNode *node = [SCNNode node];
        node.geometry = ({
            //随机大小
            int radius = (arc4random() % kMinPlatformRadius) + (kMaxPlatformRadius-kMinPlatformRadius);
            SCNCylinder *cylinder = [SCNCylinder cylinderWithRadius:radius height:2];
            //随机颜色
            cylinder.firstMaterial.diffuse.contents = ({
                CGFloat r = ((arc4random() % 255)+0.0)/255;
                CGFloat g = ((arc4random() % 255)+0.0)/255;
                CGFloat b = ((arc4random() % 255)+0.0)/255;
                UIColor *color = [UIColor colorWithRed:r green:g blue:b alpha:1];
                color;
            });
            cylinder;
        });
        node.physicsBody = ({
            SCNPhysicsBody *body = [SCNPhysicsBody dynamicBody];
            body.restitution = 1;
            body.friction = 1;
            body.damping = 0;
            body.allowsResting = YES;
            body.categoryBitMask = CollisionDetectionMaskPlatform;
            body.collisionBitMask = CollisionDetectionMaskJumper|CollisionDetectionMaskFloor|CollisionDetectionMaskOldPlatform|CollisionDetectionMaskPlatform;
            body.contactTestBitMask = CollisionDetectionMaskJumper;
            body;
        });
        //随机位置
        node.position = ({
            SCNVector3 position = self.platform.presentationNode.position;
            int xDistance = (arc4random() % (kMaxPlatformRadius*3-1))+1;
            position.z -= ({
                double lastRadius = ((SCNCylinder *)self.platform.geometry).radius;
                double radius = ((SCNCylinder *)node.geometry).radius;
                double maxDistance = sqrt(pow(kMaxPlatformRadius*3, 2)-pow(xDistance, 2));
                double minDistance = (xDistance>lastRadius+radius)?xDistance:sqrt(pow(lastRadius+radius, 2)-pow(xDistance, 2));
                double zDistance = (((double) rand() / RAND_MAX) * (maxDistance-minDistance)) + minDistance;
                zDistance;
            });
            position.x -= xDistance;
            position.y += 5;
            position;
        });
        [self.scene.rootNode addChildNode:node];
        node;
    });
}

为了直观地看出每一步做了什么,Demo里我尽量采用语法糖来包裹所有节点。
如上文所说,新站台生成,是范围内的随即大小、随机颜色、随机方向、随机距离。随机大小和随机颜色很好理解。随机的方向和随机的距离的实现,是在x-z平面,首先在范围内,随机一个x坐标,然后根据最大距离和两元相切的最小距离,计算了一个z坐标的区间,再取z的随机坐标。以此达到随机方向和随机距离的效果。

Demo中得跳跃、碰撞等效果,均是使用的模拟的物理效果。使用很简单,说起来又是很多知识点。想了解更多可以点击这里。
在Demo我分别监听了小人与站台,以及小人和地板的碰撞。

- (void)physicsWorld:(SCNPhysicsWorld *)world didBeginContact:(SCNPhysicsContact *)contact{
    SCNPhysicsBody *bodyA = contact.nodeA.physicsBody;
    SCNPhysicsBody *bodyB = contact.nodeB.physicsBody;
    if (bodyA.categoryBitMask==CollisionDetectionMaskJumper) {
        if (bodyB.categoryBitMask==CollisionDetectionMaskFloor) {
            bodyB.contactTestBitMask = CollisionDetectionMaskNone;
            [self performSelectorOnMainThread:@selector(gameDidOver) withObject:nil waitUntilDone:NO];
        }else if (bodyB.categoryBitMask==CollisionDetectionMaskPlatform) {
            //这里有个小bug,我在第一次收到碰撞后进行如下配置,按理说不应该收到碰撞回调了。可实际上还是会来。于是我直接将跳过的台子的categoryBitMask改为CollisionDetectionMaskOldPlatform,保证每个台子只会收到一次。上面的掉落又没有这个bug。
            //bodyB.contactTestBitMask = CollisionDetectionMaskNone;
            bodyB.categoryBitMask = CollisionDetectionMaskOldPlatform;
            [self jumpCompleted];
        }
    }
}

判断小人与地板碰撞,则游戏结束。
小人与新站台碰撞,则移动相机并生成下一个站台。
这里要注意的是,需要判断识别第一次碰撞。

最后,游戏结束。弹出的界面是UIView实现的。SceneKit就是一个framework,可以和其他UIKit之类的完全无缝衔接。

以上,加注释400行代码,粗暴版跳一跳完成。收工!


我觉得学习一门语言,主要是学习他的框架、他的流程,精益求精者会去关注他的实现原理。而学习Api,只是表面工作。其实在写这个Demo的时候,还遇到了一些未解的迷之bug。比如注释里提到的contactTestBitMask取消了仍然会收到通知,比如加大重力后出现的无法平静的小人等等。

随意啦随意啦。反正SceneKit告一段落,撒花。
有什么bug的话欢迎斧正。有什么疑问的话也欢迎留言讨论。

你可能感兴趣的:(SceneKit自学之路(终))