有陣子沒玩這個了。
其實這玩意兒學(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)該都能看懂并理解。
游戲截圖:

簡單總結(jié)一下:
-
創(chuàng)建工程。
和之前的Demo一樣,我直接新建的Single View App。原因就不多重復(fù)。
Single View App 初始化場景。
SceneKit中所有的物體、行為都要在SCNScene中,而SCNScene需要在SCNView中。
在Demo中,包括SCNView、SCNScene、floor、camera、light等,這些一開始就要準(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的話歡迎斧正。有什么疑問的話也歡迎留言討論。
