0。前言微信最近推出的小程序“跳转”真是火遍了大江南北。作为开发者看到它之后,不禁会想:能不能把它和ARKit结合起来,玩转AR场景呢?于是就有了这个想法。有了之前的经验,现在有了这个demo:ARBottleJump。下面简单介绍一下如何制作这样一款小游戏。一、预备知识首先,我们需要对SceneKit和ARKit有一定的基础了解。对于SceneKit,你至少要知道:SCNNode、SCNGeometry、SCNAction、SCNVector3等基础类及其常用的属性和方法(见苹果文档)。如果你对ARKit还不熟悉,那你可以看看我之前写的一篇文章:ARKitFirstLook。准备就绪后,让我们进入正题!2.总体思路我把制作这个小游戏的步骤分为以下几个子步骤:放置方块让瓶子跳起来判断游戏失败2.1放置方块我们知道在ARKit中对于现实世界有一个三维坐标系统。并且通过观察微信上的“跳一跳”可以发现,放置下一个方块的位置要么在当前方块的左边,要么在右边。为了简单起见,我们让方块放在坐标系的XZ平面上,每次随机决定是在x轴方向还是z轴方向延伸。示意图如下:蓝色的代表依次生成的块,可以看出它们的生成路径(红色箭头)都平行于x轴或z轴。首先新建一个枚举类,枚举下一个方块可能出现的方向://随机方向枚举enumNextDirection:Int{caseleft=0caseright=1}然后声明一个数组,记录所有出现过的方块:privatevarboxNodes:[SCNNode]=[]***是生成框的方法:privatefuncgenerateBox(atrealPosition:SCNVector3){//生成框letbox=SCNBox(width:kBoxWidth,height:kBoxWidth/2.0,length:kBoxWidth,chamferRadius:0.0)letnode=SCNNode(geometry:box)//给盒子上色letmaterial=SCNMaterial()material.diffuse.contents=UIColor.randomColor()box.materials=[material]//如果盒子数量为空,表示盒子是正在初始化,直接把方框的位置放在你点击的位置ifboxNodes.isEmpty{node.position=realPosition}else{//如果不为空,则游戏进行中//先随机生成一个方向nextDirection=NextDirection(rawValue:Int(arc4random()%2))!//计算它与t的距离当前方格根据随机数letdeltaDistance=Double(arc4random()%25+25)/100.0//范围:0.25~0.5//根据是左(x轴)还是右(z轴),判断下一个方块的位置y,realPosition.z+Float(deltaDistance))}}//添加子节点并将其添加到方形数组中sceneView.scene.rootNode.addChildNode(node)boxNodes.append(node)}通过以上方法可以在游戏中生成方块那么,这个方法是什么时候调用的呢?首先是开始游戏时。我们通过点击来决定从哪里开始游戏。这里我们重写了touchesBegan(_:_:)方法(其实还有touchesEnd(_:_:)),具体为什么会在后面解释。overridefunctouchesBegan(_touches:Set,withevent:UIEvent?){...//添加瓶子funcaddConeNode(){bottleNode.position=SCNVector3(boxNodes.last!.position.x,boxNodes.last!.position.y+Float(kBoxWidth)*0.75,boxNodes.last!.position.z)sceneView.scene.rootNode.addChildNode(bottleNode)}//点击测试,是否获取到某个特征点的3D坐标?funcanyPositionFrom(location:CGPoint)->(SCNVector3)?{letresults=sceneView.hitTest(location,types:.featurePoint)guard!results.isEmptyelse{returnnil}returnSCNVector3.positionFromTransform(results[0].worldTransform)}letlocation=touches。first?.location(in:sceneView)ifletposition=anyPositionFrom(location:location!){generateBox(at:position)addConeNode()generateBox(at:boxNodes.last!.position)}...}其实最好用ARKit的地方应该是这里的anyPositionFrom(_:)方法。这里使用点击测试hitTest(_:_:)判断屏幕上是否有点触及任意特征点。如果可用,SCNVector3的扩展用于将检索到的真实世界坐标转换为虚拟世界坐标。之后的所有操作都转换到虚拟世界的坐标系中。可以看出,当点击的位置能够通过命中测试方法成功获取到至少一个位置时,这个位置就是我们要生成/开始游戏的位置。然后调用一次generateBox(_:)在这个位置生成一个盒子,然后在这个盒子上添加棋子addConeNode(),最后生成一个瓶子会跳到的盒子。第二个生成方格的地方是棋子成功落到下一个方格的时候,后面会讲到。2.2让瓶子跳跃如前所述,我们需要重写touchesBegan(_:_:)和touchesEnd(_:_:)。在“跳跃”中,决定瓶子能飞多远的因素是按下屏幕的时间。通过这两种方法,一个开始,一个结束,就可以得到压合开始和结束的时间,通过求差,就可以很容易的得到压合的时长。通过这个长度进行一些函数计算,就可以得到下次要移动的距离。因此,很多关键逻辑都可以放在这两个方法中。首先声明一个元组记录按下屏幕的开始和结束时间:privatevartouchTimePair:(begin:TimeInterval,end:TimeInterval)=(0,0)然后声明一个闭包通过时间差计算移动距离,这里我们简单的进行一个除法:privateletdistanceCalculateClosure:(TimeInterval)->CGFloat={returnCGFloat($0)/4.0}下面是两个方法。按下开始时:overridefunctouchesBegan(_touches:Set,withevent:UIEvent?){...ifboxNodes.isEmpty{同2.1中的代码}else{//游戏进行中,按下屏幕,记录开始时间touchTimePair.begin=(event?.timestamp)!}}press结束时,不仅记录结束时间并计算时间差,还会根据时间差移动瓶子:overridefunctouchesEnded(_touches:Set,withevent:UIEvent?){...//记录结束时间touchTime{Pair.end=(event?.timestamp)!//计算两者的时间差letdistance=distanceCalculateClosure(touchTimePair.end-touchTimePair.begin)//根据两个方向,决定移动的方向varactions=[SCNAction()]ifnextDirection==。left{letmoveAction1=SCNAction.moveBy(x:distance,y:kJumpHeight,z:0,duration:kMoveDuration)letmoveAction2=SCNAction.moveBy(x:distance,y:-kJumpHeight,z:0,duration:kMoveDuration)actions=[SCNAction.rotateBy(x:0,y:0,z:-.pi*2,duration:kMoveDuration*2),SCNAction.sequence([moveAction1,moveAction2])]}else{letmoveAction1=SCNAction.moveBy(x:0),y:kJumpHeight,z:distance,duration:kMoveDuration)letmoveAction2=SCNAction.moveBy(x:0,y:-kJumpHeight,z:distance,duration:kMoveDuration)actions=[SCNAction.rotateBy(x:.pi*2,y:0,z:0,持续时间ion:kMoveDuration*2),SCNAction.sequence([moveAction1,moveAction2])]}...为了模仿微信跳转的动画效果,使用了SCNAction的group和sequence方法。组是指两个动作并行,顺序是指两个动作连续进行。所以最终叠加的效果是这样的:紧接着上面的代码,我们移动瓶子,当它移动结束后,判断游戏是否失败。同样,这里是生成下一个区块的地方。bottleNode.runAction(SCNAction.group(actions),completionHandler:{[weakself]//获取最新的块,也就是瓶子要跳过的块letboxNode=(self?.boxNodes.last!)!//如果盒子里没有瓶子,则游戏失败if(self?.bottleNode.isNotContainedXZ(in:boxNode))!{//记录高分,提示失败等}else{//如果有,则gamecontinuesandgeneratesthenextAbox...generateBox(at:(self?.boxNodes.last!.position)!)}})}2.3判断游戏失败由于我们的盒子和瓶子是沿坐标轴或其平行线移动行,所以2.2节中提到的isNotContainedXZ(in:)方法可以描述如下:boxNode.position.x)>width/2.0{returntrue}iffabs(position.z-boxNode.position.z)>width/2.0{returntrue}returnfalse}具体意思是比较box的中心点和机器人tle在x轴和z轴上的绝对值,只要任何一个大于block宽度的一半,就认为瓶子在block的范围之外。示意图如下(红色代表瓶子的中心点):当然,如果力求简单,可以把方块变成圆柱体,所以只需要判断两者之间的距离关系即可中心点和圆柱横截面的半径。这样,一般的游戏流程就完成了。首先是生成一个方块,然后让瓶子根据按下的时间移动,移动完成后判断游戏是否失败,这样就形成了游戏逻辑的闭环。3.有点偷懒和可以优化的地方由于时间仓促,我在很多地方都做了一点偷懒。例如:ARKit初始化时,3D坐标系的方向就确定了。所以在整个游戏过程中,x和z轴的方向不能改变。生成的块形状单一,不像微信还有圆柱、圆桌等,界面有点丑(毕竟用的是原生的SCNGeometry),那以后还能有什么改进呢?首先,坐标轴的方向是可以改变的,比如x轴是用户当前手机每次朝向的位置。其次,可以在动画效果、美学和音效方面进行一些改进或增强。***,如果能在二维平面上打破图案,甚至结合现实世界的物体跳跃,那就更好了。
