SCNPhysicsWorld力模拟更新



我正在尝试让客户端预测/协调为客户端/服务器模型工作。我一直在使用Scenekit来渲染3D世界和处理物理计算。

为了实现这一点,我需要能够强制物理模拟"计算"给定数量的帧(施加力、检查碰撞、更新节点位置/方向),而无需渲染这些更新。这样,当我确定客户端对服务器发送的内容预测错误时,我可以重新确定物理模拟中的所有节点的方向,以匹配服务器发送的信息(包括设置速度/角速度等物理属性),然后快进到服务器尚未处理的"事件",返回到客户端的当前时间点。

我知道SceneKit没有使用确定性物理引擎,但我希望我能得到看起来"足够好"的东西(我的预测失误系统有一个阈值来确定给定的预测是否与服务器的预测不一致)。

过程大致如下:

  1. 客户端根据本地输入/当前本地模拟状态本地运行物理模拟
  2. 每次物理模拟运行时,客户端都会生成自己的主机数据包,并为当前帧日期缓存这些数据包
  3. 客户端从服务器接收一个主机数据包,说明物理模拟应该是什么样子。该数据包基于客户端过去发送给服务器的数据包。主机数据包包含一个标识符(一个日期),客户端将其与之前发送到服务器的数据包捆绑在一起
  4. 客户端会查找它以前缓存的版本,该版本显示了它在过去对这个日期的物理世界的看法。如果它与主机发送的内容差异太大,我们就确定我们有一个预测错误,需要纠正
  5. 客户端重新定向场景中的所有节点,以匹配主机数据包的状态(设置位置、旋转、速度、角速度等)
  6. 然后,客户端在主机数据包的日期之后迭代它已发送到服务器的所有数据包(服务器尚未作为该主机数据包一部分处理的客户端数据包),并将它们重新应用于相应的节点
  7. 我正在努力解决的部分:在第6部分中,每次客户端将一组历史数据包应用于节点时,我都需要强制物理模拟"处理"这些更改(应用这些数据包产生的力,检查冲突,更新节点位置),然后再移到服务器尚未处理的下一组数据包

我试过玩物理世界的timeStep,但增加这个属性似乎也会减少每个物理"周期"施加的力(我们得到了更多的物理模拟,但最终结果只是通过在检查碰撞之前将物理体移动更短的距离来进行更"准确"的模拟)。

我曾尝试过使用物理世界的速度,但a)增加该值会降低物理模拟的准确性,b)由于某些原因,在场景编辑器中创建的场景的默认速度为6而不是1,因此在这里确定适当的值有点令人困惑,c)在下一次模拟尝试运行之前,它似乎不会产生任何影响。

我试着玩场景视图的sceneTime,每次处理一组历史输入时将其增加1,我认为这是有效的,但经过仔细调查,似乎没有任何作用。

我尝试过暂停场景,应用我的更改,然后播放场景,但暂停场景也会暂停物理模拟。

有没有办法让SCNPhysicsWorld在循环中手动/重复更新其物理模拟,而不触发任何渲染调用?

[EDIT]PhysicsKit现在是PhyKit,因为苹果内部PhysicsKit框架存在命名空间/链接器问题。它现在也是iOS/AcOS的跨平台,构建为可通过SPM或Cocoapods获得的xcframework。回购包含所有更新。

经过大量的尝试和错误,我无法获得任何接近强制模拟更新的东西来使用SceneKit。在联系苹果公司时,我还被告知目前不支持此功能。

我决定使用一个单独的物理引擎来实现我的目标,而不是SceneKit的,这将使我能够将SCNNode定向为外部物理引擎的计算。

为此,我创建了PhysicsKit,这是一个围绕流行的Bullet物理引擎的开源包装器,可以在这里找到:https://github.com/AdamEisfeld/PhyKit

该框架可通过Cocoapods获得,我将在未来增加对迦太基/SPM的支持。repo和类文档中的自述文件应该有助于启动和运行,但为了在这篇文章中包含一些实际代码,这里有一个简短的演示,演示我们如何使用该框架来触发物理模拟步骤,显示一个弹性球与地平面碰撞:

import UIKit
import SceneKit
import PhysicsKit
class ViewController: UIViewController {

let sceneView = SCNView()
let scene = SCNScene(named: "scenes.scnassets/world.scn")!
let physicsWorld = PKPhysicsWorld()
let physicsScene = PKPhysicsScene(isMotionStateEnabled: true)
var sceneTime: TimeInterval? = nil
let cameraNode = SCNNode()

override func viewDidLoad() {

super.viewDidLoad()

setupSceneView()

setupGroundPlane()
setupBouncyBall()

}

private func setupSceneView() {
// Embed the scene view into our view
sceneView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(sceneView)
NSLayoutConstraint.activate([
sceneView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
sceneView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
sceneView.topAnchor.constraint(equalTo: view.topAnchor),
sceneView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
// Play the sceneview so we get constant calls to the delegate's update(atTime...) function
sceneView.isPlaying = true
// Set the delegate so we can update our physics world with the scene view
sceneView.delegate = self
// Set the scene
sceneView.scene = scene

// Configure the camera
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
sceneView.pointOfView = cameraNode
cameraNode.position = SCNVector3(0, 0, 20)

}

private func setupGroundPlane() {

// Create a visual node representing a ground plane, and add it to our scene
let groundGeometry = SCNFloor()
let groundNode = SCNNode(geometry: groundGeometry)
scene.rootNode.addChildNode(groundNode)

// Create a physics body for the ground and add it to our physics world
let groundShape = PKCollisionShapeStaticPlane()
let groundBody = PKRigidBody(type: .static, shape: groundShape)
physicsWorld.add(groundBody)

// Shift the ground down 10 units
groundBody.position = .vector(0, -10, 0)

// Make the ground a little bouncy
groundBody.restitution = 0.6

// Wire the transform of the ground's rigid body to it's node
physicsScene.attach(groundBody, to: groundNode)
}

private func setupBouncyBall() {

// Create a visual node representing a bouncy ball, and add it to our scene
let ballGeometry = SCNSphere(radius: 1.0)
let ballNode = SCNNode(geometry: ballGeometry)
scene.rootNode.addChildNode(ballNode)

// Create a physics body for the bouncy ball and add it to our physics world
let ballShape = PKCollisionShapeSphere(radius: 1.0)
let ballBody = PKRigidBody(type: .dynamic(mass: 1.0), shape: ballShape)
physicsWorld.add(ballBody)

// Make the ball a little bouncy
ballBody.restitution = 0.6

// Wire the transform of the ball's rigid body to it's node
physicsScene.attach(ballBody, to: ballNode)

}

}
extension ViewController: SCNSceneRendererDelegate {

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
sceneTime = sceneTime ?? time
let physicsTime = time - sceneTime!

// Here we get to the root of the SO question: We can now manually increment the physicsWorld's
// simulation time, irrespective of the scene's render loop. We could iteratively increment
// this simulationTime value as many times as we want in a single render cycle of the scene view.
physicsWorld.simulationTime = physicsTime
physicsScene.iterativelyOrientAllNodesToAttachedRigidBodies()
}

}

我不会把它标记为一个公认的答案,因为它并不能真正回答最初的问题,但我想把它包括在这里,以备将来参考,如果它对任何人都有帮助的话。也许在未来,SceneKit将更新以支持手动物理步骤。

  • 亚当

相关内容

  • 没有找到相关文章

最新更新