美文网首页SceneKit学习
[SceneKit专题]如何制作一个像Stack的游戏

[SceneKit专题]如何制作一个像Stack的游戏

作者: 苹果API搬运工 | 来源:发表于2017-09-25 23:42 被阅读191次

    说明

    SceneKit系列文章目录

    更多iOS相关知识查看github上WeekWeekUpProject

    在本教程中,你将会学习如何制作一个类似Stack这样的游戏.

    392x696bb.jpg

    本教程将包含以下内容:

    • 可视化创建3D场景.
    • 编程加载并呈现3D场景.
    • 使用节点的物理形体.
    • 结合使用UIKit与SceneKit.
    • 在SceneKit游戏中播放音频.

    开始

    下载初始项目starter project.
    在初始项目里面,你会发现SceneKit目录文件中带有音频和场景文件.另外,还有一些SCNVector3类扩展来执行简单的向量算术运算及生成渐变图片.还有App Icon也已经添加进去了!花点时间熟悉一下项目吧.

    你将会创建一个类似于Stack的游戏.这个游戏的目标是在一块方块上叠放另一个方块.需要小心的是:方块叠放时稍微偏一点,多余部分就会被切掉.完全没对齐,那就game over了!

    建立场景

    你将从建立你的游戏场景开始.打开GameScene.scn.

    game_scene-1.png
    拖拽一个新的camera到场景中,然后选择Node Inspector并重命名节点为Main Camera.设置位置为X: 3, Y: 4.5, Z: 3,旋转为X: -40, Y: 45, Z:0:
    camera_node_inspector.png

    切换到Attributes Inspector并切换相机的Projection typeOrthographic.
    下一步,添加灯光到场景中.
    从对象库中拖拽一个新的方向光到场景中,命名为Directional Light.因为相机只看到了场景的一侧,你不必去照亮看不见的令一侧.回到Attributes Inspector,设置位置为X: 0, Y: 0, Z: 0,旋转为X: -65, Y: 20, Z:-30:

    directional_light.png

    神奇,亮起来了!

    现在回到塔的顶部.你需要一个基础方块来支承这个塔,来让玩家在上面建造.拖拽一个盒子到场景中,设置属性:

    • 在Node Inspector中,更改名字为Base Block,并设置位置为X:0,Y:-4,Z:0.
    • 在Attributes Inspector中,更改尺寸为Width: 1, Height: 8, Length: 1.
    • 在Material Inspector中,更改漫反射颜色为#434343.
      base_block_diffuse_color-e1486870178228.png

    你需要添加一个动态形体到基础方块,切换到Physics Inspector中,并将物理形体改为Static.

    base_block_physics.png

    现在让我们配上漂亮的背景颜色!在选中基础方块的同时,切换到Scene Inspector,并拖拽文件Gradient.png到背景选择框中:

    scene_background-650x307.png

    你需要一个方法来显示给玩家,他们的塔已经堆放了多高.打开Main.storyboard;看到它已经有一个SCNView.添加一个label在SCNView顶部并设置文本为0.然后添加一个约束将label对齐到中心,像这样:

    center_constraint.png

    添加另一个约束将label顶部与屏幕顶部对齐.


    top_constraint.png

    然后切换到Attributes Inspector中,切换字体为Custom, Thonburi, Regular, 50.

    label_text_settings.png

    然后使用assistant editor来添加一个从label到控制器的引用,命名为scoreLabel:

    score_label.gif

    编译运行,看看现在有什么了.


    build_and_run_1-1.png

    添加你的第一块方块

    知道怎么让塔越来越高么?对,创建更多方块.
    创建一些属性来帮你追踪正在使用的方块.为此,打开ViewController.swift()并在viewDidLoad()之前添加下面变量:

    //1
    var direction = true
    var height = 0
    
    //2
    var previousSize = SCNVector3(1, 0.2, 1)
    var previousPosition = SCNVector3(0, 0.1, 0)
    var currentSize = SCNVector3(1, 0.2, 1)
    var currentPosition = SCNVector3Zero
    
    //3
    var offset = SCNVector3Zero
    var absoluteOffset = SCNVector3Zero
    var newSize = SCNVector3Zero
    
    //4
    var perfectMatches = 0
    

    这段代码含义:

    1. direction用来表示方块的位置是上升还是下降,height变量表示塔有多高.
    2. previousSizepreviousPosition变量表示当前层的尺寸和位置.
    3. 你需要使用offset,absoluteOffset,newSize变量来计算新层的尺寸.
    4. perfectMatches变量表示玩家完美对齐上一层的次数.

    现在,是时间添加方块到场景中了.在viewDidLoad()底部添加下面代码:

    //1
    let blockNode = SCNNode(geometry: SCNBox(width: 1, height: 0.2, length: 1, chamferRadius: 0))
    blockNode.position.z = -1.25
    blockNode.position.y = 0.1
    blockNode.name = "Block\(height)"
        
    //2
    blockNode.geometry?.firstMaterial?.diffuse.contents =
          UIColor(colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
    scnScene.rootNode.addChildNode(blockNode)
    

    代码含义:

    1. 你用SCNNode的立方体创建一个新的方块,放置在Z轴和Y轴,并根据放置在塔上的height属性对其命名.
    2. 根据不断增长的高度,计算得出漫反射颜色的红色分量.最后,将节点添加到场景上.

    创建并运行,会看到你的新方块出现在屏幕上!


    build_and_run_2.png

    移动方块

    现在已经有一条新的方块用来放置.但是,我想如果方块是移动的会更好玩.
    要实现这个移动,需要设置控制器作为场景渲染代理,并实现SCNSceneRendererDelegate协议中的方法.
    在类的底部添加这个扩展:

    extension ViewController: SCNSceneRendererDelegate {
      func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    
      }
    }
    

    这里我们需要实现SCNSceneRendererDelegate协议,添加renderer(_:updateAtTime:).
    renderer(_:updateAtTime:)里面添加下面代码:

    // 1
    if let currentNode = scnScene.rootNode.childNode(withName: "Block\(height)", recursively: false) {
          // 2
          if height % 2 == 0 {
            // 3
            if currentNode.position.z >= 1.25 {
              direction = false
            } else if currentNode.position.z <= -1.25 {
              direction = true
            }
            
            // 4
            switch direction {
            case true:
              currentNode.position.z += 0.03
            case false:
              currentNode.position.z -= 0.03
            }
          // 5
          } else {
            if currentNode.position.x >= 1.25 {
              direction = false
            } else if currentNode.position.x <= -1.25 {
              direction = true
            }
           
            switch direction {
            case true:
              currentNode.position.x += 0.03
            case false:
              currentNode.position.x -= 0.03
            }
          }
        }
    

    代码含义:

    1. 根据名字找到场景中的方块.
    2. 根据层的位置,沿X轴或Z轴移动方块.奇数层沿Z轴运动,偶数层沿X轴运动.用求余操作符(%)来得到余数,判断奇偶.
    3. 如果方块的位置到了1.25或者-1.25,改变其方向,向另一方向运动.
    4. 根据方向,沿Z轴前后移动.
    5. 重复相同代码,只改为沿X轴.

    默认情况下,SceneKit会暂停场景.为了看到场景中物体的移动,在viewDidLoad的底部添加下面代码:

    scnView.isPlaying = true
    scnView.delegate = self
    

    这段代码中,将这个控制器设置为场景的渲染代理,这样就能执行刚才写的代理方法了.
    创建运行,查看运动!


    032.png

    处理点击

    现在,我们已经让方块移动了,还需要在玩家点击屏幕时添加一个新方块并重设老方块的尺寸.切换到Main.storyboard并添加一个tap gesture recognizerSCNView,像这样:

    Screen-Shot-2017-04-20-at-5.29.44-PM-650x357.png

    现在在控制器里面用辅助编辑器创建一个动作并命名为handleTap.

    tap_action-650x374.png

    切换到标准编辑区,并打开ViewController.swift,然后在handlTap(_:)内部添加代码:

    if let currentBoxNode = scnScene.rootNode.childNode(
      withName: "Block\(height)", recursively: false) {
          currentPosition = currentBoxNode.presentation.position
          let boundsMin = currentBoxNode.boundingBox.min
          let boundsMax = currentBoxNode.boundingBox.max
          currentSize = boundsMax - boundsMin
          
          offset = previousPosition - currentPosition
          absoluteOffset = offset.absoluteValue()
          newSize = currentSize - absoluteOffset
          
          currentBoxNode.geometry = SCNBox(width: CGFloat(newSize.x), height: 0.2, 
            length: CGFloat(newSize.z), chamferRadius: 0)
          currentBoxNode.position = SCNVector3Make(currentPosition.x + (offset.x/2),
            currentPosition.y, currentPosition.z + (offset.z/2))
          currentBoxNode.physicsBody = SCNPhysicsBody(type: .static, 
            shape: SCNPhysicsShape(geometry: currentBoxNode.geometry!, options: nil))
    }
    

    这里我们从场景中得到currentBoxNode.然后计算偏移及新方块的尺寸.从而改变方块的尺寸和位置,并给它一个静态物理形体.

    resize_current_block.png

    偏移等于上一层和当前层位置的差值.通过从当前尺寸上减去差值的绝对值,就得到了新尺寸.
    注意到,把当前节点设置位置到偏移处,方块的边缘完美对齐了上一个层的边缘.这创造出一种切掉方块的错觉.

    下一步,你需要一个方法来创建塔上的下一个方块.在handleTap(_:)下面添加代码:

    func addNewBlock(_ currentBoxNode: SCNNode) {
      let newBoxNode = SCNNode(geometry: currentBoxNode.geometry)
      newBoxNode.position = SCNVector3Make(currentBoxNode.position.x, 
        currentPosition.y + 0.2, currentBoxNode.position.z)
      newBoxNode.name = "Block\(height+1)"
      newBoxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(
        colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
        
      if height % 2 == 0 {
        newBoxNode.position.x = -1.25
      } else {
        newBoxNode.position.z = -1.25
      }
        
      scnScene.rootNode.addChildNode(newBoxNode)
    }
    

    这里我们创建了一个和上一个方块相同尺寸的新节点.放置在当前方块上方,并根据层高改变X或Z轴的位置.最后,改变漫反射颜色并将其添加到场景中.

    你需要使用handleTap(_:)来保持属性为最新.在handleTap(_:)里的if else语句中添加代码:

    addNewBlock(currentBoxNode)
    
    if height >= 5 {
      let moveUpAction = SCNAction.move(by: SCNVector3Make(0.0, 0.2, 0.0), duration: 0.2)
      let mainCamera = scnScene.rootNode.childNode(withName: "Main Camera", recursively: false)!
      mainCamera.runAction(moveUpAction)
    }
          
    scoreLabel.text = "\(height+1)"
          
    previousSize = SCNVector3Make(newSize.x, 0.2, newSize.z)
    previousPosition = currentBoxNode.position
    height += 1
    

    要做的第一件事就是调用addNewBlock(_:).如果塔的尺寸大于或等于5,将相机上移.

    还需要更新分数,设置前一个尺寸和位置等于当前尺寸和位置.你可以使用newSize因为你设置当前节点的尺寸为newSize.然后增加高度.

    创建并运行.一切看起来堆垛地很完美!


    build_and_run_4.png

    实现物理效果

    游戏正确地重设了方块尺寸,但是如果被砍掉的部分能从塔上掉落,游戏会看起来更酷.

    addNewBlock(_:)下面定义新方法:

    func addBrokenBlock(_ currentBoxNode: SCNNode) {
        let brokenBoxNode = SCNNode()
        brokenBoxNode.name = "Broken \(height)"
        
        if height % 2 == 0 && absoluteOffset.z > 0 {
          // 1
          brokenBoxNode.geometry = SCNBox(width: CGFloat(currentSize.x), 
            height: 0.2, length: CGFloat(absoluteOffset.z), chamferRadius: 0)
          
          // 2
          if offset.z > 0 {
            brokenBoxNode.position.z = currentBoxNode.position.z - 
              (offset.z/2) - ((currentSize - offset).z/2)
          } else {
            brokenBoxNode.position.z = currentBoxNode.position.z - 
              (offset.z/2) + ((currentSize + offset).z/2)
          }
          brokenBoxNode.position.x = currentBoxNode.position.x
          brokenBoxNode.position.y = currentPosition.y
          
          // 3
          brokenBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic, 
            shape: SCNPhysicsShape(geometry: brokenBoxNode.geometry!, options: nil))
          brokenBoxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(colorLiteralRed: 0.01 * 
            Float(height), green: 0, blue: 1, alpha: 1)
          scnScene.rootNode.addChildNode(brokenBoxNode)
    
        // 4
        } else if height % 2 != 0 && absoluteOffset.x > 0 {
          brokenBoxNode.geometry = SCNBox(width: CGFloat(absoluteOffset.x), height: 0.2, 
            length: CGFloat(currentSize.z), chamferRadius: 0)
          
          if offset.x > 0 {
            brokenBoxNode.position.x = currentBoxNode.position.x - (offset.x/2) - 
              ((currentSize - offset).x/2)
          } else {
            brokenBoxNode.position.x = currentBoxNode.position.x - (offset.x/2) + 
              ((currentSize + offset).x/2)
          }
          brokenBoxNode.position.y = currentPosition.y
          brokenBoxNode.position.z = currentBoxNode.position.z
          
          brokenBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic, 
            shape: SCNPhysicsShape(geometry: brokenBoxNode.geometry!, options: nil))
          brokenBoxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(
            colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
          scnScene.rootNode.addChildNode(brokenBoxNode)
        }
      }
    

    这里你创建了一个新节点并用height来命名.你使用了if语句来确定坐标轴,并确保偏移大于0,因为等于时不会产生一个方块碎片.

    1. 刚才,你减去偏移量来设置新尺寸.这里,你无需计算,所需尺寸正是偏移量.
    2. 改变碎片部分的位置.
    3. 添加物理形体到该碎片上来让它掉落.还需要改变颜色并添加到场景中.
    4. 在X轴上重复同样操作.

    你根据当前位置偏移量的一半来得到碎片的位置.然后,根据方块位置的正负,添加或扣除当前尺寸减去偏移的一半.


    create_broken_block.png

    handleTap(_:)里面的addNewBlock(_:)之前调用该方法:

    addBrokenBlock(currentBoxNode)
    

    当碎片节点掉落出视线时,还在不停掉落,并没有销毁.在renderer(_:updateAtTime:)里面最上方添加代码:

    for node in scnScene.rootNode.childNodes {
      if node.presentation.position.y <= -20 {
        node.removeFromParentNode()
      }
    }
    

    这段代码会删除Y值小于-20的所有节点.
    运行看看切下的方块!


    build_and_run_5.png

    结束触摸

    现在游戏机制的核心部分已经完成了,还有一些收尾工作.当玩家完美对齐上一层时应该有奖励.还有,现在还没有输赢判断,当你失败后也无法开始一个新游戏! 游戏还没有声音,需要添加一些声音.

    处理完美对齐情况

    在处理完美对齐的情况,在addBrokenBlock(_:)中添加下面方法:

    func checkPerfectMatch(_ currentBoxNode: SCNNode) {
        if height % 2 == 0 && absoluteOffset.z <= 0.03 {
          currentBoxNode.position.z = previousPosition.z
          currentPosition.z = previousPosition.z
          perfectMatches += 1
          if perfectMatches >= 7 && currentSize.z < 1 {
            newSize.z += 0.05
          }
          
          offset = previousPosition - currentPosition
          absoluteOffset = offset.absoluteValue()
          newSize = currentSize - absoluteOffset
        } else if height % 2 != 0 && absoluteOffset.x <= 0.03 {
          currentBoxNode.position.x = previousPosition.x
          currentPosition.x = previousPosition.x
          perfectMatches += 1
          if perfectMatches >= 7 && currentSize.x < 1 {
            newSize.x += 0.05
          }
          
          offset = previousPosition - currentPosition
          absoluteOffset = offset.absoluteValue()
          newSize = currentSize - absoluteOffset
        } else {
          perfectMatches = 0
        }
      }
    

    如果玩家放置位置与上一块在0.03之内,就认为是完美匹配.只要误差足够近,就设置当前方块的位置等于上一个方块的位置.

    通过设置当前和上一次位置相等,让它们在数值上完全匹配并重新计算偏移和新尺寸.在handleTap(_:)里面计算偏移和新尺寸之后,调用这个方法:

    checkPerfectMatch(currentBoxNode)
    

    处理完全错位情况

    现在已经处理了完美对齐的情况和部分对齐的情况,但你还需要处理完全错失的情况.

    handleTap(_:)checkPerfectMatch(_:)之前,添加下面代码:

    if height % 2 == 0 && newSize.z <= 0 {
            height += 1
            currentBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic, 
              shape: SCNPhysicsShape(geometry: currentBoxNode.geometry!, options: nil))
            return
          } else if height % 2 != 0 && newSize.x <= 0 {
            height += 1
            currentBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic, 
              shape: SCNPhysicsShape(geometry: currentBoxNode.geometry!, options: nil))
            return
          }
    

    如果玩家错失了方块,计算出的新尺寸应该是负的,检查这个值就知道玩家是否错失了方块.如果玩家错失了,你将高度增加一,这样移动的代码就不再移动移动当前方块了.然后你再添加一个动态物理形体让方块掉落.

    最后,return,这样代码就不再运行了,如checkPerfectMatch(_:),和addBrokenBlock(_:).

    添加音效

    因为音频文件很短,可以预先加载进来.在属性声明中添加一个新的字典属性,命名为sounds:

    var sounds = [String: SCNAudioSource]()
    

    下一步,在viewDidLoad下面添加两个方法:

    func loadSound(name: String, path: String) {
      if let sound = SCNAudioSource(fileNamed: path) {
        sound.isPositional = false
        sound.volume = 1
        sound.load()
        sounds[name] = sound
      }
    }
      
    func playSound(sound: String, node: SCNNode) {
      node.runAction(SCNAction.playAudio(sounds[sound]!, waitForCompletion: false))
    }
    

    第一个方法从指定目录加载音频文件并储存到sounds字典中.第二个方法播放储存在sounds字典中的方法.

    viewDidload()中间添加下面代码:

    loadSound(name: "GameOver", path: "HighRise.scnassets/Audio/GameOver.wav")
    loadSound(name: "PerfectFit", path: "HighRise.scnassets/Audio/PerfectFit.wav")
    loadSound(name: "SliceBlock", path: "HighRise.scnassets/Audio/SliceBlock.wav")
    

    有好几个地方需要播放音效.在handleTap(_:)中,在每一个检查玩家是否错失方块的if语句中,添加下面的代码:

    playSound(sound: "GameOver", node: currentBoxNode)
    

    在调用addNewBlock之后,添加一行:

    playSound(sound: "SliceBlock", node: currentBoxNode)
    

    滚动到checkPerfectMatch(_:),在两个if语句中分支中添加一行:

    playSound(sound: "PerfectFit", node: currentBoxNode)
    

    创建并运行---有音效的游戏更有意思了,对吧?

    处理输赢条件

    游戏如何结束呢?现在我们来处理这个问题!

    进入Main.storyboard,拖拽一个新的按钮到视图中.改变文本的颜色为#FF0000,文本内容Play.然后改变字体为Custom, Helvetica Neue, 66.

    play_button_attribute.png

    下一步,设置对齐方式align为中心对齐center,并固定底边constant100.

    play_button_pin.png

    拖拽引线到控制器命名为playButton.然后创建一个动作命名为playGame并写入以下代码:

    playButton.isHidden = true
        
    let gameScene = SCNScene(named: "HighRise.scnassets/Scenes/GameScene.scn")!
    let transition = SKTransition.fade(withDuration: 1.0)
    scnScene = gameScene
    let mainCamera = scnScene.rootNode.childNode(withName: "Main Camera", recursively: false)!
    scnView.present(scnScene, with: transition, incomingPointOfView: mainCamera, completionHandler: nil)
        
    height = 0
    scoreLabel.text = "\(height)"
        
    direction = true
    perfectMatches = 0
        
    previousSize = SCNVector3(1, 0.2, 1)
    previousPosition = SCNVector3(0, 0.1, 0)
        
    currentSize = SCNVector3(1, 0.2, 1)
    currentPosition = SCNVector3Zero
        
    let boxNode = SCNNode(geometry: SCNBox(width: 1, height: 0.2, length: 1, chamferRadius: 0))
    boxNode.position.z = -1.25
    boxNode.position.y = 0.1
    boxNode.name = "Block\(height)"
    boxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(colorLiteralRed: 0.01 * Float(height),
      green: 0, blue: 1, alpha: 1)
    scnScene.rootNode.addChildNode(boxNode)
    

    你注意到,你已经重置了游戏中的所有变量为默认值,并添加了第一个方块.

    因为已经添加了第一块方块,移除viewDidLoad(_:)中下面的代码,从声明blockNode到添加到场景中.

       //1
        let blockNode = SCNNode(geometry: SCNBox(width: 1, height: 0.2, length: 1, chamferRadius: 0))
        blockNode.position.z = -1.25
        blockNode.position.y = 0.1
        blockNode.name = "Block\(height)"
        
        //2
        blockNode.geometry?.firstMaterial?.diffuse.contents =
          UIColor(colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
        scnScene.rootNode.addChildNode(blockNode)  
    

    在刚才创建的方法下面定义一个新方法:

    func gameOver() {
        let mainCamera = scnScene.rootNode.childNode(
          withName: "Main Camera", recursively: false)!
        
        let fullAction = SCNAction.customAction(duration: 0.3) { _,_ in
          let moveAction = SCNAction.move(to: SCNVector3Make(mainCamera.position.x, 
            mainCamera.position.y * (3/4), mainCamera.position.z), duration: 0.3)
          mainCamera.runAction(moveAction)
          if self.height <= 15 {
            mainCamera.camera?.orthographicScale = 1
          } else {
            mainCamera.camera?.orthographicScale = Double(Float(self.height/2) / 
              mainCamera.position.y)
          }
        }
        
        mainCamera.runAction(fullAction)
        playButton.isHidden = false
      }
    

    这里,你缩放摄像机镜头来露出整个塔.最后,设置play按钮为可见,这样玩家就可以开始一个新游戏.

    handleTap(_:)内部,在完全错失方块的if语句中,调用gameover(),放在return语句之前,两个if语句里面都放:

    gameOver()
    

    编译运行.当你失败时,就能重新开始一个新游戏了.

    启动图片

    游戏启动时会有难看的白屏.打开LaunchScreen.storyboard并拖拽进一个图像视图.四周对齐屏幕:

    img_view_pin.png

    更改图片为Gradient.png

    img_view_image.png

    现在我们已经将白屏替换为了漂亮的渐变图!

    恭喜你,你已经完成了!你可以从这里下载最终完成版final project
    end

    相关文章

      网友评论

      • 黄瓜CBS:感谢分享!
        我是一个新人开发者,现在在用SceneKit做一个简单的小游戏,结果在模型导入方面碰见了一些问题,不知道您有什么看法。
        我用blender制作了dae的模型文件并在XCode中转换为了scn文件。之后用一个空白的scn文件将各个部分组装起来。iPad上看和SceneEditor上看到的完全一致。然而,有时我在右边栏改变其中某一个部件的参数之后,SceneEditor上看到的是正常的模型iPad上看到的模型则经常会出现大量部件消失了的情况。现在我只能通过删除消失了的部件并重新添加的方式来让整个模型再让iPad能够正常显示整个scene。我不清楚具体是什么地方出现了错误。
        黄瓜CBS:@史前图腾 我已经尝试过了各种方法,但是由于要改变贴图和纹理,所以不得不转换成SCN格式的文件。因此最终还是会出现我说的情况。我这两天仔细的观察对比之后发现了一些端倪但是还是无法解决。在SceneEditor中选中一个最终能正常显示的模型,在reference里FullPath显示为/xxxx.scn。然而在经过编辑或者,即便不编辑,使用Open As -> Quick Look 打开之后,文件里的大部分模型的FullPath会自动丢失"/"因此读取这个文件的时候会大概读到的就是nil了。因此才会出现丢失或者空白场景的问题。然而这个FullPath是不能更改的,我尝试重新关联文件也无法给FullPath加上下划线。可以说是很难处理了。
        苹果API搬运工:@黄瓜CBS 试试.obj文件.看这里http://www.jianshu.com/p/317516ec5344末尾有导入的步骤
        黄瓜CBS:需要补充的是在debug里面并没有发现任何错误。甚至我在直接撤销引起模型消失的动作仍旧无法让模型返回之前的样子。必须重建。

      本文标题:[SceneKit专题]如何制作一个像Stack的游戏

      本文链接:https://www.haomeiwen.com/subject/chggsxtx.html