Using GameplayKit, I'm trying to make an enemy character pursue the player while avoiding obstacles in a SpriteKit game.
However, the agent in agentDidUpdate(_:) has a position and velocity of nan.
Here's what I'm doing now:
Configure the obstacles:
//node is defined here
let obstacle = GKCircleObstacle(radius: Float(node.size.width*0.5))
obstacle.position = vector_float2(x: Float(node.position.x), y: Float(node.position.y))
levelObstacles.append(obstacle)
Configure a GKEntity (etc.) for the player SKSpriteNode, saving a reference to the component and agent for use in #3:
//node is defined here
let entity1 = GKEntity()
heroAgent = GKAgent2D()
heroComponent = GKSKNodeComponent(node: node)
heroAgent.delegate = self // <-- self is a GKAgentDelegate
node.entity = entity1
heroEntity = entity1
if let comp = heroComponent {
entity1.addComponent(comp)
entity1.addComponent(heroAgent)
}
Configure a GKEntity (etc.) for the enemy SKSpriteNode, using the saved hero component and agent to set up the necessary behaviors:
//node is defined here
let entity = GKEntity()
let agent = GKAgent2D()
let avoid = GKGoal(toAvoid: levelObstacles, maxPredictionTime: 5.0)
let pursue = GKGoal(toInterceptAgent: heroAgent, maxPredictionTime: 5.0)
nodeComponent = GKSKNodeComponent(node: node)
agent.behavior = GKBehavior(goals: [avoid, pursue], andWeights: [100.0, 50.0])
agent.delegate = self // <-- self is a GKAgentDelegate
node.entity = entity
if let comp = nodeComponent {
entity.addComponent(comp)
entity.addComponent(agent)
}
nodeEntity = entity
In the SKScene's update(_:) method, call the enemy entity's update(deltaTime:) method:
nodeEntity?.update(deltaTime: currentTime-lastFrameTime)
Print the agent's position and velocity in agentDidUpdate(_:):
func agentDidUpdate(_ agent: GKAgent) {
if let a = agent as? GKAgent2D {
print("agent position: \(a.position), agent velocity: \(a.velocity)") //Prints a position and velocity of "-nan"
}
}
Question: Why are the agent's position and velocity nan inside agentDidUpdate(_:), and how do I solve the problem?
Update: The problem is apparently related to GKGoal(toInterceptAgent:maxPredictionTime:). If I use a different goal, like GKGoal(toFleeAgent:) for example, there are actual values available in agentDidUpdate(_:).
I was able to get this working by setting the player agent's position to the actual SpriteKit node's position inside the SKScene's update(_:) method, like this:
heroAgent.position = vector_float2(x: Float(mainCharacter?.position.x ?? 0.0), y: Float(mainCharacter?.position.y ?? 0.0))
I'm not really sure why that worked, but it did.
Related
In an AR app, I want to detect collisions between the user walking around and the walls of an AR node that I construct. In order to do that, I create an invisible cylinder right in front of the user and set it all up to detect collisions.
The walls are all part of a node which is a child of sceneView.scene.rootNode.
The cylinder, I want it to be a child of sceneView.pointOfView so that it would always follow the camera.
However, when I do so, no collisions are detected.
I know that I set it all up correctly, because if instead I set the cylinder node as a child of sceneView.scene.rootNode as well, I do get collisions correctly. In that case, I continuously move that cylinder node to always be in front of the camera in a renderer(updateAtTime ...) function. So I do have a workaround, but I'd prefer it to be a child of pointOfView.
Is it impossible to detect collisions if nodes are children of different root nodes?
Or maybe I'm missing something in my code?
The contactDelegate is set like that:
sceneView.scene.physicsWorld.contactDelegate = self so maybe this only includes sceneView.scene, but will exclude sceneView.pointOfView???
Is that the issue?
Here's what I do:
I have a separate file to create and configure my cylinder node which I call pov:
import Foundation
import SceneKit
func createPOV() -> SCNNode {
let pov = SCNNode()
pov.geometry = SCNCylinder(radius: 0.1, height: 4)
pov.geometry?.firstMaterial?.diffuse.contents = UIColor.blue
pov.opacity = 0.3 // will be set to 0 when it'll work correctly
pov.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil)
pov.physicsBody?.isAffectedByGravity = false
pov.physicsBody?.mass = 1
pov.physicsBody?.categoryBitMask = BodyType.cameraCategory.rawValue
pov.physicsBody?.collisionBitMask = BodyType.wallsCategory.rawValue
pov.physicsBody?.contactTestBitMask = BodyType.wallsCategory.rawValue
pov.simdPosition = simd_float3(0, -1.5, -0.3) // this position only makes sense when setting as a child of pointOfView, otherwise the position will always be changed by renderer
return pov
}
Now in my viewController.swift file I call this function and set is as a child of either root nodes:
pov = createPOV()
sceneView.pointOfView?.addChildNode(pov!)
(Don't worry right now about not checking and unwrapping).
The above does not detect collisions.
But if instead I add it like so:
sceneView.scene.rootNode.addChildNode(pov!)
then collisions are detected just fine.
But then I need to always move this cylinder to be in front of the camera and I do it like that:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
guard let pointOfView = sceneView.pointOfView else {return}
let currentPosition = pointOfView.simdPosition
let currentTransform = pointOfView.simdTransform
let orientation = SCNVector3(-currentTransform.columns.2.x, -currentTransform.columns.2.y, -currentTransform.columns.2.z)
let currentPositionOfCamera = orientation + SCNVector3(currentPosition)
DispatchQueue.main.async {
self.pov?.position = currentPositionOfCamera
}
}
For completeness, here's the code I use to configure the node of walls in ViewController (they're built elsewhere in another function):
node?.physicsBody = SCNPhysicsBody(type: .dynamic, shape: SCNPhysicsShape(node: node!, options: nil))
node?.physicsBody?.isAffectedByGravity = false
node?.physicsBody?.mass = 1
node?.physicsBody?.damping = 1.0 // remove linear velocity, needed to stop moving after collision
node?.physicsBody?.angularDamping = 1.0 // remove angular velocity, needed to stop rotating after collision
node?.physicsBody?.velocityFactor = SCNVector3(1.0, 0.0, 1.0) // will allow movement only in X and Z coordinates
node?.physicsBody?.angularVelocityFactor = SCNVector3(0.0, 1.0, 0.0) // will allow rotation only around Y axis
node?.physicsBody?.categoryBitMask = BodyType.wallsCategory.rawValue
node?.physicsBody?.collisionBitMask = BodyType.cameraCategory.rawValue
node?.physicsBody?.contactTestBitMask = BodyType.cameraCategory.rawValue
And here's my physycsWorld(didBegin contact) code:
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
if contact.nodeA.physicsBody?.categoryBitMask == BodyType.wallsCategory.rawValue || contact.nodeB.physicsBody?.categoryBitMask == BodyType.wallsCategory.rawValue {
print("Begin COLLISION")
contactBeginLabel.isHidden = false
}
}
So I print something to the console and I also turn on a label on the view so I'll see that collision was detected (and the walls indeed move as a whole when it works).
So Again, it all works fine when the pov node is a child of sceneView.scene.rootNode, but not if it's a child of sceneView.pointOfView.
Am I doing something wrong or is this a limitation of collision detection?
Is there something else I can do to make this work, besides the workaround I already implemented?
Thanks!
Regarding the positioning of your cyliner:
instead to use the render update at time, you better use a position constraint for your cylinder node to move with the point of view. the result will be the same, as if it were a child of the point of view, but collisions will be detected, because you add it to the main rootnode scenegraph.
let constraint = SCNReplicatorConstraint(target: pointOfView) // must be a node
constraint.positionOffset = positionOffset // some SCNVector3
constraint.replicatesOrientation = false
constraint.replicatesScale = false
constraint.replicatesPosition = true
cylinder.constraints = [constraint]
There is also an influence factor you can configure. By default the influence is 100%, the position will immediatly follow.
I am stuck on a problem. I need to apply transformation (scale, rotation, position) right after i add model to my rootNode. Right after when i apply transformation on child model added to rootNode it shows fine on screen but when i apply transformation on rootNode it doesn't refresh. i experimented that as soon i touch screen UI updates. I also tried putting delay of 2,3 secs.
expected
UIView should update as soon i apply transformation to rootNode.
let res = SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0.5, z: 0, duration: 1))
// let res = SCNAction.sequence([SCNAction.wait(duration: 2000), SCNAction.rotateTo(x: CGFloat(180), y: CGFloat(90), z: CGFloat(0), duration: 1.0)])
self.rootNode.runAction(res)
i tried putting code in
RunLoop.main.perform {}
i tried using
scnView.preferredFramesPerSecond = 30
scnView.rendersContinuously = true
But none works. i am using sdk IOS 13.2. Any help please.
Edit:
var rootNode = SCNNode()
viewDidload(){
scnScene.rootNode.addChildNode(rootNode)
....
}
func initSceneWithModel(modelURL: URL) {
do {
try personModel = addModel(url: modelURL)
menuButton.setImage(UIImage.fontAwesomeIcon(name: .bars, style: .solid, textColor: .white, size: XConstants.FONT_AWSOME_SIZE), for: .normal)
selectedModel = personModel
centerPivot(for: personModel!)
moveNodeToCenter(node: personModel!)
setupEyeBlocker()
// selectedModel = eyeBlocker
updateFieldUI()
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.applyInitTransformations()
}
} catch let error {
Utilities.xalert(inView: self.view, desc: error.localizedDescription)
}
}
func applyInitTransformations() {
if let info = vm.physicialFile.extraInfo {
// personModel?.position = info.person.position
// personModel?.scale = info.person.scale
// personModel?.eulerAngles = info.person.rotation
var valueRotPos = SCNMatrix4Mult(SCNMatrix4MakeRotation(0,0,0,0), SCNMatrix4MakeTranslation(0,0,0))
var valueScale = SCNMatrix4MakeScale(7.0,7.0,7.0) // scales to 0.1 of original size
rootNode.transform = SCNMatrix4Mult(valueRotPos, valueScale)
// rootNode.position = info.root.position
// rootNode.scale = info.root.scale
// rootNode.eulerAngles = info.root.rotation
}
else {
applyEyeBlockerDefaultPosition()
}
}
Apple clearly says:
...
You should not modify the transform property of the root node.
...
(https://developer.apple.com/documentation/scenekit/scnscene/1524029-rootnode)
This might be causing the issues you have with your scene. Avoid SCNActions to be run on the rootNode. They are designed to run on the content of the rootNode (any SCNNode added to the rootNode).
You could probably take a common SCNNode, call it like myRootNode, add it to the real rootNode and add all your other content to myRootNode. Transformations should then apply correctly to all your sub-content, if this is your goal.
BTW: scnView.preferredFramesPerSecond = 30 never gave me more performence or any benefits. Leave it default. Scenekit switches automatically to lower framerates if required.
EDIT:
apply transformation like so:
// Precalculate the Rotation the Position and the Scale
var valueRotPos = SCNMatrix4Mult(SCNMatrix4MakeRotation(0,0,0,0), SCNMatrix4MakeTranslation(0,0,0))
var valueScale = SCNMatrix4MakeScale(0.1,0.1,0.1) // scales to 0.1 of original size
then you do:
myRootNode.transform = SCNMatrix4Mult(valueRotPos, valueScale)
(you could also try to use the worldTransform of the node or the other transform properties of the nodes presentation node-object)
I'm using ARKit to display 3D objects. I managed to place the nodes in the real world in front of the user (aka the camera). But I don't manage to make them to face the camera when I drop them.
let tap_point=CGPoint(x: x, y: y)
let results=arscn_view.hitTest(tap_point, types: .estimatedHorizontalPlane)
guard results.count>0 else{
return
}
guard let r=results.first else{
return
}
let hit_tf=SCNMatrix4(r.worldTransform)
let new_pos=SCNVector3Make(hit_tf.m41, hit_tf.m42+Float(0.2), hit_tf.m43)
guard let scene=SCNScene(named: file_name) else{
return
}
guard let node=scene.rootNode.childNode(withName: "Mesh", recursively: true) else{
return
}
node.position=new_pos
arscn_view.scene.rootNode.addChildNode(node)
The nodes are well positioned on the plane, in front of the camera. But they are all looking in the same direction. I guess I should rotate the SCNNode but I didn't manage to do this.
First, get the rotation matrix of the camera:
let rotate = simd_float4x4(SCNMatrix4MakeRotation(sceneView.session.currentFrame!.camera.eulerAngles.y, 0, 1, 0))
Then, combine the matrices:
let rotateTransform = simd_mul(r.worldTransform, rotate)
Lastly, apply a transform to your node, casting as SCNMatrix4:
node.transform = SCNMatrix4(rotateTransform)
Hope that helps
EDIT
here how you can create SCNMatrix4 from simd_float4x4
let rotateTransform = simd_mul(r.worldTransform, rotate)
node.transform = SCNMatrix4(m11: rotateTransform.columns.0.x, m12: rotateTransform.columns.0.y, m13: rotateTransform.columns.0.z, m14: rotateTransform.columns.0.w, m21: rotateTransform.columns.1.x, m22: rotateTransform.columns.1.y, m23: rotateTransform.columns.1.z, m24: rotateTransform.columns.1.w, m31: rotateTransform.columns.2.x, m32: rotateTransform.columns.2.y, m33: rotateTransform.columns.2.z, m34: rotateTransform.columns.2.w, m41: rotateTransform.columns.3.x, m42: rotateTransform.columns.3.y, m43: rotateTransform.columns.3.z, m44: rotateTransform.columns.3.w)
guard let frame = self.sceneView.session.currentFrame else {
return
}
node.eulerAngles.y = frame.camera.eulerAngles.y
here's my code for the SCNNode facing the camera..hope help for someone
let location = touches.first!.location(in: sceneView)
var hitTestOptions = [SCNHitTestOption: Any]()
hitTestOptions[SCNHitTestOption.boundingBoxOnly] = true
let hitResultsFeaturePoints: [ARHitTestResult] = sceneView.hitTest(location, types: .featurePoint)
let hitTestResults = sceneView.hitTest(location)
guard let node = hitTestResults.first?.node else {
if let hit = hitResultsFeaturePoints.first {
let rotate = simd_float4x4(SCNMatrix4MakeRotation(sceneView.session.currentFrame!.camera.eulerAngles.y, 0, 1, 0))
let finalTransform = simd_mul(hit.worldTransform, rotate)
sceneView.session.add(anchor: ARAnchor(transform: finalTransform))
}
return
}
Do you want the nodes to always face the camera, even as the camera moves? That's what SceneKit constraints are for. Either SCNLookAtConstraint or SCNBillboardConstraint can keep a node always pointing at the camera.
Do you want the node to face the camera when placed, but then hold still (so you can move the camera around and see the back of it)? There are a few ways to do that. Some involve fun math, but a simpler way to handle it might just be to design your 3D assets so that "front" is always in the positive Z-axis direction. Set a placed object's transform based on the camera transform, and its initial orientation will match the camera's.
Here's how I did it:
func faceCamera() {
guard constraints?.isEmpty ?? true else {
return
}
SCNTransaction.begin()
SCNTransaction.animationDuration = 5
SCNTransaction.completionBlock = { [weak self] in
self?.constraints = []
}
constraints = [billboardConstraint]
SCNTransaction.commit()
}
private lazy var billboardConstraint: SCNBillboardConstraint = {
let constraint = SCNBillboardConstraint()
constraint.freeAxes = [.Y]
return constraint
}()
As stated earlier a SCNBillboardConstraint will make the node always look at the camera. I am animating it so the node doesn't just immediately snap into place, this is optional. In the SCNTransaction.completionBlock I remove the constraint, also optional.
Also I set the SCNBillboardConstraint's freeAxes, which customizes on what axis the node follows the camera, again optional.
I want the node to face the camera when I place it then keep it here (and be able to move around). – Marie Dm
Blockquote
You can put object facing to camera, using this:
if let rotate = sceneView.session.currentFrame?.camera.transform {
node.simdTransform = rotate
}
This code will save you from gimbal lock and other troubles.
The four-component rotation vector specifies the direction of the rotation axis in the first three components and the angle of rotation (in radians) in the fourth. The default rotation is the zero vector, specifying no rotation. Rotation is applied relative to the node’s simdPivot property.
The simdRotation, simdEulerAngles, and simdOrientation properties all affect the rotational aspect of the node’s simdTransform property. Any change to one of these properties is reflected in the others.
https://developer.apple.com/documentation/scenekit/scnnode/2881845-simdrotation
https://developer.apple.com/documentation/scenekit/scnnode/2881843-simdtransform
I have a simple function call in my main class to create some instances from my Cube class but I cant seem to get my instances to be added to my scene. I tried returning self inside my Cube class but Swift wont let me do this inside init.
func addCubeLoop() {
for var i = 0; i <= 0; ++i {
cube = Cube(num: i, importedCube: importedCube1)
cubeArray.append(cube)
theScene.rootNode.addChildNode(cubeArray[i])
}
}
class Cube: SCNNode {
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(num: Int, importedCube: SCNNode) {
let _scale: Float = 60
let cube: SCNNode = importedCube.copy() as! SCNNode
super.init()
cube.scale = SCNVector3Make(_scale, _scale, _scale)
let node = SCNNode()
node.addChildNode(cube)
node.position = SCNVector3Make(5, 20, 3)
let collisionBox = SCNBox(width: 5.0, height: 5.0, length: 5.0, chamferRadius: 0)
node.physicsBody?.physicsShape = SCNPhysicsShape(geometry: collisionBox, options: nil)
node.physicsBody = SCNPhysicsBody.dynamicBody()
node.physicsBody?.mass = 0.1
node.physicsBody?.restitution = 0.8
node.physicsBody?.damping = 0.5
node.name = "dice" + String(num)
node.physicsBody?.allowsResting = true
}
}
The nodes created in the init of Cube are not added as child nodes of it.
I've simplified the your code below to illustrate the problem.
func addCubeLoop() {
for /* loop */ {
// 1. create cube
cube = Cube(num: i, importedCube: importedCube1)
// 6. add cube to the scene's root node
theScene.rootNode.addChildNode(cubeArray[i])
}
}
class Cube: SCNNode {
init(importedCube: SCNNode) {
// 2. copy importedCube
let cube: SCNNode = importedCube.copy() as! SCNNode
// configure cube
// ...
// 3. create node
let node = SCNNode()
// 4. add cube (the copy) to node
node.addChildNode(cube)
// configure node
// ...
// 5. End of init
}
}
For each run through the loop, this is what happens.
A new Cube instance is created, passing importedCube1
In the Cube initializer, the imported cube argument is copied. The node "cube" is now a copy of the argument.
Still in the initializer, a new node (called "node" is created).
Still in the initializer, "cube" (the copy) is added to "node". At this point, cube is a child node of "node", but the Cube instance itself (which is a node) had no child nodes.
Init completes.
The newly created Cube instance is added to the scene's root node.
At this point there are four relevant nodes:
the root node,
the cube instance node
the node called "node"
the imported copy
The cube instance node is a child of the root node. The imported copy is a child of the "node" node. However, The "node" node doesn't have a parent.
The fix is to make sure that the all nodes are part of the hierarchy by adding the "node" node to self inside the Cube instance initializer.
I can't seem to figure out how to constrain the Z-value of a node using SCNTransformConstraint. Here's what I have so far.
let constraint = SCNTransformConstraint(inWorldSpace: true, withBlock:{
node, matrix in
var newMatrix = matrix
let currentNode = node as SCNNode
if (currentNode.presentationNode().position.z > 0.0) {
newMatrix.m43 = 0.0
}
return newMatrix
})
ship.constraints = [constraint]
With the above constraint, ship doesn't move when I apply a force to its physicsBody. Any help would be greatly appreciated.
Yeah. This one stumped me for a bit, too.
The issue is with the matrix. According the the Developer documentation concerning the SCNMatrix4 (matrix) argument:
If the node is affected by an in-progress animation, this value reflects the currently visible state of the node during the animation (rather than its target state that will be visible when the animation completes).
Instead of this:
var newMatrix = matrix
You really want:
var newMatrix = node.transform
which appears to be the current transform about to be applied to the node.
I know this is an old question, but this was near the top of search results for SCNTransformConstraint. Hey, better late than never, right?
This worked for me to constrain a SCNCameraController to the boundaries of a grid:
let constraint = SCNTransformConstraint.positionConstraint(inWorldSpace: false, with: { (node, position) -> SCNVector3 in
var constrainedPosition = position
if position.x < gridMinX {constrainedPosition.x = gridMinX; node.position.x = gridMinX}
if position.x > gridMaxX {constrainedPosition.x = gridMaxX; node.position.x = gridMaxX}
if position.z < gridMinZ {constrainedPosition.z = gridMinZ; node.position.z = gridMinZ}
if position.z > gridMaxZ {constrainedPosition.z = gridMaxZ; node.position.z = gridMaxZ}
return constrainedPosition
})
sceneView.pointOfView?.constraints = [constraint]