SceneKit自學(xué)之路(終)

有陣子沒玩這個了。

其實這玩意兒學(xué)了我工作也用不上,之前本來想學(xué)來玩玩ARKit,結(jié)果手機(jī)太舊了不支持(手動捂臉)。本著有始有終,花了兩天時間留下最后一個相對完整的Demo,SceneKit的學(xué)習(xí)也暫告一段落。如果有什么不懂的,也可以留言討論。
LittlerJumper:下載地址:https://github.com/anjohnlv/LittleJumper


我把它稱為粗暴版跳一跳,Demo很粗暴也很簡陋。

為了方便初學(xué)者學(xué)習(xí)SceneKit,整個Demo,SceneKit內(nèi)容純代碼完成。且沒有任何框架設(shè)計,所有的代碼都在ViewController中,加上注釋一共接近400行。

注釋還是很詳細(xì)了。應(yīng)該都能看懂并理解。

游戲截圖:


Little Jumper

簡單總結(jié)一下:

  • 創(chuàng)建工程。
    和之前的Demo一樣,我直接新建的Single View App。原因就不多重復(fù)。


    Single View App
  • 初始化場景。
    SceneKit中所有的物體、行為都要在SCNScene中,而SCNScene需要在SCNView中。
    在Demo中,包括SCNView、SCNScenefloor、cameralight等,這些一開始就要準(zhǔn)備好的元素。我為了體現(xiàn)游戲的操作過程,把這些初始化都放在了自己身上懶加載。
    Demo里的注釋很詳細(xì),我挑一些說
    想要目標(biāo)始終在視線范圍內(nèi),我們得在“小人”跳走后讓鏡頭跟隨??墒侨绻恢弊岀R頭跟隨小人,會讓整個游戲看起來特別晃。所以我讓相機(jī)跟隨站臺,每成功跳一次,將相機(jī)移動觀察新站臺。

-(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,所以細(xì)節(jié)方面沒有太講究。
如果這是這么寫,你會發(fā)現(xiàn),跳著跳著就看不見了。原因是你的光源始終照在遠(yuǎn)處,離光源遠(yuǎn)了,自然就看不到了。在這里,我是直接讓光源始終跟隨鏡頭。實現(xiàn)方法是在初始化相機(jī)之后,直接將光源設(shè)為相機(jī)的子節(jié)點。

-(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其實很簡單。整個游戲過程梳理下來:

初始化->點擊屏幕蓄力->釋放跳躍->判斷成功->移動相機(jī)->生成下一個跳臺->下一次跳躍->判斷失敗->游戲結(jié)束

蓄力的過程用到了長按手勢,對,就和寫App里的長按一樣。SCNView是基于UIView的,可以直接將手勢加在上面。設(shè)置longPressGesture.minimumPressDuration = 0;保證短按也能監(jiān)聽到。這里有一個知識點是自定義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];
}

很簡單地實現(xiàn)了顏色的漸變動畫。力量越大,顏色越紅。在釋放跳躍的瞬間,取消Action即可。

[self.jumper removeAllActions];

跳躍的過程得先提后面生成新跳臺。Demo里新跳臺的生成,是范圍內(nèi)隨機(jī)大小,隨機(jī)顏色、隨機(jī)方向、隨機(jī)距離。所以跳躍的時候,需要判斷“小人”和目標(biāo)跳臺的方向。我們要保證方向向量上單位力量為恒定的,這樣當(dāng)通過時間來增加力量時才有意義。
這個是數(shù)學(xué)知識,已知三角形斜邊長度和兩邊直角邊比,求直角邊長度。這個不多說。

移動相機(jī)上文已經(jīng)提到就不再復(fù)述,接下來著重說明一下自己生成下一個站臺的方法:

-(void)createNextPlatform {
    self.nextPlatform = ({
        SCNNode *node = [SCNNode node];
        node.geometry = ({
            //隨機(jī)大小
            int radius = (arc4random() % kMinPlatformRadius) + (kMaxPlatformRadius-kMinPlatformRadius);
            SCNCylinder *cylinder = [SCNCylinder cylinderWithRadius:radius height:2];
            //隨機(jī)顏色
            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;
        });
        //隨機(jī)位置
        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里我盡量采用語法糖來包裹所有節(jié)點。
如上文所說,新站臺生成,是范圍內(nèi)的隨即大小、隨機(jī)顏色、隨機(jī)方向、隨機(jī)距離。隨機(jī)大小和隨機(jī)顏色很好理解。隨機(jī)的方向和隨機(jī)的距離的實現(xiàn),是在x-z平面,首先在范圍內(nèi),隨機(jī)一個x坐標(biāo),然后根據(jù)最大距離和兩元相切的最小距離,計算了一個z坐標(biāo)的區(qū)間,再取z的隨機(jī)坐標(biāo)。以此達(dá)到隨機(jī)方向和隨機(jī)距離的效果。

Demo中得跳躍、碰撞等效果,均是使用的模擬的物理效果。使用很簡單,說起來又是很多知識點。想了解更多可以點擊這里。
在Demo我分別監(jiān)聽了小人與站臺,以及小人和地板的碰撞。

- (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,我在第一次收到碰撞后進(jìn)行如下配置,按理說不應(yīng)該收到碰撞回調(diào)了。可實際上還是會來。于是我直接將跳過的臺子的categoryBitMask改為CollisionDetectionMaskOldPlatform,保證每個臺子只會收到一次。上面的掉落又沒有這個bug。
            //bodyB.contactTestBitMask = CollisionDetectionMaskNone;
            bodyB.categoryBitMask = CollisionDetectionMaskOldPlatform;
            [self jumpCompleted];
        }
    }
}

判斷小人與地板碰撞,則游戲結(jié)束。
小人與新站臺碰撞,則移動相機(jī)并生成下一個站臺。
這里要注意的是,需要判斷識別第一次碰撞。

最后,游戲結(jié)束。彈出的界面是UIView實現(xiàn)的。SceneKit就是一個framework,可以和其他UIKit之類的完全無縫銜接。

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


我覺得學(xué)習(xí)一門語言,主要是學(xué)習(xí)他的框架、他的流程,精益求精者會去關(guān)注他的實現(xiàn)原理。而學(xué)習(xí)Api,只是表面工作。其實在寫這個Demo的時候,還遇到了一些未解的迷之bug。比如注釋里提到的contactTestBitMask取消了仍然會收到通知,比如加大重力后出現(xiàn)的無法平靜的小人等等。

隨意啦隨意啦。反正SceneKit告一段落,撒花。
有什么bug的話歡迎斧正。有什么疑問的話也歡迎留言討論。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 目前網(wǎng)上關(guān)于SceneKit的教程還是略少。對于完全沒有任何游戲開發(fā)經(jīng)驗的人(我)來說還是有一定挑戰(zhàn)。記下此篇,加...
    anjohnlv閱讀 3,246評論 1 14
  • SceneKit 是一個OC 框架,開始之前,先熟悉一下SceneKit 的三維坐標(biāo)系: 很清楚的看到,Scene...
    淘氣CC閱讀 5,899評論 0 5
  • 在你可以稱為「青春」的時間里,哪件事讓你感到幸運? 一個人,一件事,或者看到了一本書,有沒有哪件事哪個人在你稱為青...
    達(dá)達(dá)令閱讀 538評論 0 1
  • 一、將問題員工轉(zhuǎn)化為正能量員工的工具,要不要? 1.1當(dāng)員工敘述問題之后,你就說“我聽懂了,我明白了”,你問您一個...
    669c5bc6dd72閱讀 671評論 0 0
  • 朋友講起佢隻貓識得屙喺屋企廁所嘅地板,唔駛用貓砂兜同貓砂,一屙就沖落水渠,得咗。聽完我望一望屎霸同奧利奧,諗起馬雲(yún)...
    海苦麻閱讀 381評論 0 0

友情鏈接更多精彩內(nèi)容