我正在使用 SceneKit 和 DAE 环境以及 mixamo 中的角色,我一直在寻找方法来显示加载屏幕,同时所有内容都最好在 SwiftUI 中加载,而不是在整个 GameView 加载之前冻结 UI 并部分显示环境
这是保存场景的 GameSceneView
import SwiftUI
import SceneKit
///UIView representable for the GameScene and Fighters
struct GameSceneView: UIViewRepresentable {
let fighter: Fighter
let enemyFighter: Fighter
let isPracticeMode: Bool
typealias UIViewType = SCNView
let scene = SCNScene(named: "3DAssets.scnassets/GameScene.scn")!
let cameraNode = SCNNode()
let lightNode = SCNNode()
func makeUIView(context: Context) -> UIViewType {
setUpCamera()
setUpLight()
setUpFighters()
let sceneView = SCNView()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.scene = scene
return sceneView
}
func updateUIView(_ sceneView: UIViewType, context: Context) {}
}
private extension GameSceneView {
func setUpFighters() {
scene.rootNode.addChildNode(fighter.daeHolderNode)
scene.rootNode.addChildNode(enemyFighter.daeHolderNode)
}
func setUpCamera() {
// create and add a camera to the scene
cameraNode.camera = SCNCamera()
cameraNode.position = .init(x: -6, y: 4, z: isPracticeMode ? 2.5 : 3.2) //X: zooms in and out, Y: shifts vertically, Z: horizontal changes
cameraNode.eulerAngles = .init(x: 0, y: -89.5, z: 0) //X: zooms in and out, Y: rotates horizontally, Z: rotates vertically
scene.rootNode.addChildNode(cameraNode)
}
func setUpLight() {
lightNode.light = SCNLight()
lightNode.position = .init(x: -5, y: 3.5, z: 3.2)
lightNode.eulerAngles = .init(x: 0, y: -89.5, z: 0)
scene.rootNode.addChildNode(lightNode)
}
}
还有战斗机类
import SceneKit
///3D model of the fighter
struct Fighter {
//MARK: Properties
var fighterType: FighterType
///The parent node which contain all of the nodes
private(set) var daeHolderNode = SCNNode()
///Bone node that contains the animation players
private(set) var animationsNode: SCNNode?
//MARK: Get-only properties
private(set) lazy var textNode: SCNNode = {
let node = SCNNode(geometry: damageText)
node.name = "textNode"
node.position = textPosition
let scale: Float = 1.5
node.scale = SCNVector3(x: scale, y: scale, z: scale)
let yAngle: Float = 0
node.eulerAngles = .init(x: 0, y: yAngle, z: 0)
node.runAction(SCNAction.hide())
return node
}()
private lazy var damageText: SCNText = {
let depth: CGFloat = 2
var text = SCNText(string: "", extrusionDepth: depth)
let font = UIFont.systemFont(ofSize: 13, weight: .black)
text.font = font
text.alignmentMode = CATextLayerAlignmentMode.center.rawValue
text.firstMaterial?.diffuse.contents = UIColor.white
return text
}()
private lazy var textPosition: SCNVector3 = {
return SCNVector3(x: isEnemy ? -37 : -2, y:120, z: isEnemy ? 10 : -30)
}()
private lazy var textNodeAction: SCNAction = {
let showAction = SCNAction.unhide()
let zoomInAction = SCNAction.scale(to: 3, duration: 0.15)
let zoomOutAction = SCNAction.scale(to: 1.5, duration: 0.1)
let initialActions = SCNAction.sequence([showAction, zoomInAction, zoomOutAction])
//From current node's location, go up by 100
let upwardAction = SCNAction.move(by: SCNVector3(x:0, y:100, z:0), duration: 2)
//Keep going up but slower and begin hiding the node
let hideAction = SCNAction.hide()
let upwardAction2 = SCNAction.move(by: SCNVector3(x:0, y:50, z:0), duration: 2)
let hidingActions = SCNAction.group([hideAction, upwardAction2])
//Return to original position
let returnAction = SCNAction.move(to: textPosition, duration: 0)
let sequence = SCNAction.sequence([initialActions, upwardAction, hidingActions, returnAction])
return sequence
}()
//MARK: - Initializers
init(type: FighterType, isEnemy: Bool) {
self.fighterType = type
self.isEnemy = isEnemy
defaultAnimation = .idleFight
createNode()
}
//MARK: - Public Methods
mutating func switchFighter(to nextFighterType: FighterType) {
fighterType = nextFighterType
}
mutating func positionNode(asHorizontal: Bool = false) {
daeHolderNode.scale = SCNVector3Make(fighterType.scale, fighterType.scale, fighterType.scale)
var xPosition: Float = isEnemy ? 1.5 : 0 //further
let yPosition: Float = 0.5 //vertical position
var zPosition: Float = isEnemy ? 2.7 : 3 //horizontal position
var angle: Float = isEnemy ? -89.5 : 90
if asHorizontal {
xPosition = 2
zPosition = isEnemy ? 3 : 1.5
angle = isEnemy ? 180 : 0
}
daeHolderNode.position = SCNVector3Make(xPosition, yPosition, zPosition)
daeHolderNode.eulerAngles = .init(x: 0, y: angle, z: 0)
//Add textNode to the parent node holder
daeHolderNode.addChildNode(textNode)
}
mutating func showResult(_ attackResult: AttackResult) {
damageText.string = attackResult.damageText
textNode.runAction(textNodeAction)
}
mutating func loadAnimations(animations: Set<AnimationType>) {
for animationType in animations {
addAnimationPlayer(animationType)
}
}
///Plays an animation if animationType is new
func playAnimation(_ animationType: AnimationType) {
//Get and stop the default animation
guard let defaultAnimationPlayer = animationsNode?.animationPlayer(forKey: defaultAnimation.rawValue) else {
LOGE("No default animation player found for \(defaultAnimation)")
return
}
let blendDuration: CGFloat = 0.3
if animationType != defaultAnimation {
//Stopping withBlendOutDuration prevents node from going back to T-position before playing the next animation
defaultAnimationPlayer.stop(withBlendOutDuration: blendDuration)
}
//Play next animation
guard let player = animationsNode?.animationPlayer(forKey: animationType.rawValue) else {
LOGDE("Animation played not loaded \(animationType)")
return
}
player.play()
//Handle end of animation
if animationType.isKillAnimationType {
//Pause fighter's animations after playing the kill animation. Reduce duration by 0.2 to not let the fighter stand back up
runAfterDelay(delay: player.animation.duration - blendDuration) {
pauseAnimations()
}
} else {
//At the end of the played animation, resume to default animation if we're not playing it already
if animationType != defaultAnimation {
runAfterDelay(delay: player.animation.duration - blendDuration) {
defaultAnimationPlayer.play()
}
}
}
}
func resumeAnimations() {
animationsNode?.isPaused = false
}
func pauseAnimations() {
animationsNode?.isPaused = true
}
func stopAnimations() {
animationsNode?.removeAllAnimations()
}
}
extension Fighter: Hashable {
//MARK: - Hashable Required Methods
static func == (lhs: Fighter, rhs: Fighter) -> Bool {
return lhs.fighterType == rhs.fighterType
}
public func hash(into hasher: inout Hasher) {
return hasher.combine(fighterType)
}
}
extension Fighter {
///Create node from default animation
mutating func createNode() {
daeHolderNode = SCNNode(daePath: fighterType.defaultDaePath)
//Add materials and images programmatically
for child in daeHolderNode.childNodes {
if let partName = child.name,
let part = SkeletonType(rawValue: partName) {
child.geometry?.firstMaterial = fighterType.getMaterial(for: part)
}
}
//Assign the daeHolderNode's bones as the node to animate
animationsNode = daeHolderNode.childNode(withName: fighterType.bonesName, recursively: true)
}
}
我创建了一个自定义 UIKit 进度条来添加一些更好的效果,并调用 ProgressUpdate 来移动进度条。 一个 gameNodes (sharedInstance) 类,我在其中缓存节点信息并与其他类共享节点信息。 gameNodes->loadModels() 每 N/Total 回调一次以更新栏。 在 gameViewController->viewDidLoad 中,我在创建 UIKit 组件并对齐它们后加载了模型。 我尝试使用启动屏幕来提供帮助,但我一定做错了,因为它从未真正起作用。 由于必须加载 scenekit(就我而言),因此需要一些等待时间,因此它在所有设备上并不完美。 然而,在模型加载时,它确实产生了我认为合理的反馈。