Agent following path moves forward indefinitely - swift

I am trying to make a sprite move towards a point while avoiding some obstacles. The graph that I am using is a GKObstacleGraph, obtained from this scene:
For simplicity I decided not to use a circular physics body for the obstacles, but it's enough to use the sprites' bounds for now. So this is how I created the graph:
lazy var graph:GKObstacleGraph? =
{
guard let spaceship = self.spaceship else { return nil }
let obstacles = SKNode.obstacles(fromNodeBounds: self.rocks)
let graph = GKObstacleGraph(obstacles: obstacles, bufferRadius: Float(spaceship.size.width))
return graph
}()
When the user taps on any location of the scene, the spaceship should start moving toward that position by avoiding the obstacles:
func tap(locationInScene location:CGPoint)
{
guard let graph = self.graph else { return }
guard let spaceship = self.spaceship else { return }
let startNode = GKGraphNode2D(point: vector_float2(withPoint: spaceship.position))
let endNode = GKGraphNode2D(point: vector_float2(withPoint: location))
graph.connectUsingObstacles(node: startNode)
graph.connectUsingObstacles(node: endNode)
let path = graph.findPath(from: startNode, to: endNode)
print(path)
let goal = GKGoal(toFollow: GKPath(graphNodes: path, radius: Float(spaceship.size.width)) , maxPredictionTime: 1.0, forward: true)
let behavior = GKBehavior(goal: goal, weight: 1.0)
let agent = GKAgent2D()
agent.behavior = behavior
graph.remove([startNode, endNode])
spaceship.entity?.addComponent(agent)
// This is necessary. I don't know why, but if I don't do that, I
// end up having a duplicate entity in my scene's entities array.
self.entities = [spaceship.entity!]
}
But when I tap on a point on the scene, the spaceship starts moving indefinitely upwards. I tried to print the position of the GKAgent at every frame, and this is what I got:
With very low values, the spaceship still keeps moving upwards without stopping, ever.
This is my project on GitHub.

Related

How to move and rotate SCNode using ARKit and Gesture Recognizer?

I am working on an AR based iOS app using ARKit(SceneKit). I used the Apple sample code https://developer.apple.com/documentation/arkit/handling_3d_interaction_and_ui_controls_in_augmented_reality as base for this. Using this i am able to move or rotate the whole Virtual Object.
But i want to select and move/rotate a Child Node in Virtual object using user finger, similar to how we move/rotate the whole Virtual Object itself.
I tried the below two links but it is only moving the child node in particular axis but not freely moving anywhere as the user moves the finger.
ARKit - Drag a node along a specific axis (not on a plane)
Dragging SCNNode in ARKit Using SceneKit
Also i tried replacing the Virtual Object which is a SCNReferenceNode with SCNode so that whatever functionality present for existing Virtual Object applies to Child Node as well, it is not working.
Can anyone please help me on how to freely move/rotate not only the Virtual Object but also the child node of a Virtual Object?
Please find the code i am currently using below,
let tapPoint: CGPoint = gesture.location(in: sceneView)
let result = sceneView.hitTest(tapPoint, options: nil)
if result.count == 0 {
return
}
let scnHitResult: SCNHitTestResult? = result.first
movedObject = scnHitResult?.node //.parent?.parent
let hitResults = self.sceneView.hitTest(tapPoint, types: .existingPlane)
if !hitResults.isEmpty{
guard let hitResult = hitResults.last else { return }
movedObject?.position = SCNVector3Make(hitResult.worldTransform.columns.3.x, hitResult.worldTransform.columns.3.y, hitResult.worldTransform.columns.3.z)
}
To move an object:
Perform a hitTest to check where you have touched, and detect which plane you touched and get a position. Move your SCNNode to that position by changing the node.position value with an SCNVector3.
Code:
#objc func panDetected(recognizer: UIPanGestureRecognizer){
let hitResult = self.arSceneView.hitTest(loc, types: .existingPlane)
if !hitResult.isEmpty{
guard let hitResult = hitResult.last else { return }
self.yourNode.position = SCNVector3Make(hitResult.worldTransform.columns.3.x, hitResult.worldTransform.columns.3.y, hitResult.worldTransform.columns.3.z)
}
The above code is enough to move your node over a detected plane, anywhere you touch, and not just in a single axis.
Rotating a node according to your gesture is a very difficult task and I have worked on a solution for quite sometime, never reaching a perfect output.
But, I have come across this repository in GitHub which allows you to do just that with a very impressive result.
https://github.com/Xartec/ScreenSpaceRotationAndPan
The Swift version of the code you require to rotate your node using your gesture would be :
var previousLoc: CGPoint?
var touchCount: Int?
#objc func panDetected(recognizer: UIPanGestureRecognizer){
let loc = recognizer.location(in: self.view)
var delta = recognizer.translation(in: self.view)
if recognizer.state == .began {
previousLoc = loc
touchCount = recognizer.numberOfTouches
}
else if gestureRecognizer.state == .changed {
delta = CGPoint.init(x: 2 * (loc.x - previousLoc.x), y: 2 * (loc.y - previousLoc.y))
previousLoc = loc
if touchCount != recognizer.numberOfTouches {
return
}
var rotMatrix: SCNMatrix4!
let rotX = SCNMatrix4Rotate(SCNMatrix4Identity, Float((1.0/100) * delta.y), 1, 0, 0)
let rotY = SCNMatrix4Rotate(SCNMatrix4Identity, Float((1.0 / 100) * delta.x), 0, 1, 0)
rotMatrix = SCNMatrix4Mult(rotX, rotY)
let transMatrix = SCNMatrix4MakeTranslation(yourNode.position.x, yourNode.position.y, yourNode.position.z)
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, SCNMatrix4Invert(transMatrix))
let parentNoderanslationMatrix = SCNMatrix4MakeTranslation((self.yourNode.parent?.worldPosition.x)!, (self.yourNode.parent?.worldPosition.y)!, (self.yourNode.parent?.worldPosition.z)!)
let parentNodeMatWOTrans = SCNMatrix4Mult((self.yourNode.parent?.worldTransform)!, SCNMatrix4Invert(parentNoderanslationMatrix))
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, parentNodeMatWOTrans)
let camorbitNodeTransMat = SCNMatrix4MakeTranslation((self.arSceneView.pointOfView?.worldPosition.x)!, (self.arSceneView.pointOfView?.worldPosition.y)!, (self.arSceneView.pointOfView?.worldPosition.z)!)
let camorbitNodeMatWOTrans = SCNMatrix4Mult((self.arSceneView.pointOfView?.worldTransform)!, SCNMatrix4Invert(camorbitNodeTransMat))
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, SCNMatrix4Invert(camorbitNodeMatWOTrans))
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, rotMatrix)
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, camorbitNodeMatWOTrans)
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, SCNMatrix4Invert(parentNodeMatWOTrans))
self.yourNode.transform = SCNMatrix4Mult(self.yourNode.transform, transMatrix)
}
}

remove nodes from scene after use ARSCNView

I am making an app using ARKit to measure between two points. The goal is to be able to measure length, store that value, then measure width and store.
The problem I am having is disposing of the nodes after I get the measurement.
Steps so far:
1) added a button with a restartFunction. this worked to reset the measurements but did not remove the spheres from scene, and also made getting the next measurement clunky.
2) set a limit on > 2 nodes. This functionaly works best. But again the spheres just stay floating in the scene.
Here is a screen shot of the best result I have had.
#objc func handleTap(sender: UITapGestureRecognizer) {
let tapLocation = sender.location(in: sceneView)
let hitTestResults = sceneView.hitTest(tapLocation, types: .featurePoint)
if let result = hitTestResults.first {
let position = SCNVector3.positionFrom(matrix: result.worldTransform)
let sphere = SphereNode(position: position)
sceneView.scene.rootNode.addChildNode(sphere)
let tail = nodes.last
nodes.append(sphere)
if tail != nil {
let distance = tail!.position.distance(to: sphere.position)
infoLabel.text = String(format: "Size: %.2f inches", distance)
if nodes.count > 2 {
nodes.removeAll()
}
} else {
nodes.append(sphere)
}
}
}
I am new to Swift (coding in general) and most of my code has come from piecing together tutorials.
I think the issue here is that your are not actually removing the SCNNodes you have added to hierarchy.
Although you are removing the nodes from what I assume is an array of SCNNodes by calling: nodes.removeAll(), you first need to actually remove them from the scene hierarchy.
So what you need to do is call the following function on any node you wish to remove:
removeFromParentNode()
Which simply:
Removes the node from its parent’s array of child nodes.
As such you would do something like this which first removes the nodes from the hierarchy, and then removes them from the array:
for nodeAdded in nodesArray{
nodeAdded.removeFromParentNode()
}
nodesArray.removeAll()
So based on the code provided you could do the following:
if nodes.count > 2 {
for nodeAdded in nodes{
nodeAdded.removeFromParentNode()
}
nodes.removeAll()
}
For future reference, if you want to remove all SCNNodes from you hierarchy you can also call:
self.augmentedRealityView.scene.rootNode.enumerateChildNodes { (existingNode, _) in
existingNode.removeFromParentNode()
}
Whereby self.augmentedRealityView refers to the variable:
var augmentedRealityView: ARSCNView!
Here is a very basic working example based on (and modified from) the code you have provided:
/// Places A Marker Node At The Desired Tap Point
///
/// - Parameter sender: UITapGestureRecognizer
#objc func handleTap(_ sender: UITapGestureRecognizer) {
//1. Get The Current Tap Location
let currentTapLocation = sender.location(in: sceneView)
//2. Check We Have Hit A Feature Point
guard let hitTestResult = self.augmentedRealityView.hitTest(currentTapLocation, types: .featurePoint).first else { return }
//3. Get The World Position From The HitTest Result
let worldPosition = positionFromMatrix(hitTestResult.worldTransform)
//4. Create A Marker Node
createSphereNodeAt(worldPosition)
//5. If We Have Two Nodes Then Measure The Distance
if let distance = distanceBetweenNodes(){
print("Distance == \(distance)")
}
}
/// Creates A Marker Node
///
/// - Parameter position: SCNVector3
func createSphereNodeAt(_ position: SCNVector3){
//1. If We Have More Than 2 Nodes Remove Them All From The Array & Hierachy
if nodes.count >= 2{
nodes.forEach { (nodeToRemove) in
nodeToRemove.removeFromParentNode()
}
nodes.removeAll()
}
//2. Create A Marker Node With An SCNSphereGeometry & Add It To The Scene
let markerNode = SCNNode()
let markerGeometry = SCNSphere(radius: 0.01)
markerGeometry.firstMaterial?.diffuse.contents = UIColor.cyan
markerNode.geometry = markerGeometry
markerNode.position = position
sceneView.scene.rootNode.addChildNode(markerNode)
//3. Add It To The Nodes Array
nodes.append(markerNode)
}
/// Converts A matrix_float4x4 To An SCNVector3
///
/// - Parameter matrix: matrix_float4x4
/// - Returns: SCNVector3
func positionFromMatrix(_ matrix: matrix_float4x4) -> SCNVector3{
return SCNVector3(matrix.columns.3.x, matrix.columns.3.y, matrix.columns.3.z)
}
/// Calculates The Distance Between 2 Nodes
///
/// - Returns: Float?
func distanceBetweenNodes() -> Float? {
guard let firstNode = nodes.first, let endNode = nodes.last else { return nil }
let startPoint = GLKVector3Make(firstNode.position.x, firstNode.position.y, firstNode.position.z)
let endPoint = GLKVector3Make(endNode.position.x, endNode.position.y, endNode.position.z)
let distance = GLKVector3Distance(startPoint, endPoint)
return distance
}
For an example of a measuringApp which might help your development you can look here: ARKit Measuring Example
Hope it helps...
This looks like a logic issue. You're assigning nodes.last to tail just before checking if tail is not nil. So it will never be != nil so you'll never execute the nodes.append(sphere) in the else.
I agree with #dfd. Set a breakpoint to make sure the code is being executed before continuing.

GameplayKit find path to moving sprite

I want a sprite to follow the player and I tried to achieve this with the Pathfinding of GameplayKit. This is working, but there's problem. I get the path from the enemy sprite to the player sprite with the following code:
func moveEnemy(to playerPos: CGPoint){
guard !moving else { return }
moving = true
let startNode = GKGraphNode2D(point: float2(Float(self.position.x), Float(self.position.y)))
let endNode = GKGraphNode2D(point: float2(Float(playerPos.x), Float(playerPos.y)))
PhysicsHelper.graph.connectUsingObstacles(node: startNode)
PhysicsHelper.graph.connectUsingObstacles(node: endNode)
let path = PhysicsHelper.graph.findPath(from: startNode, to: endNode)
guard path.count > 0 else{ return }
var actions = [SKAction]()
for node in path{
if let point2d = node as? GKGraphNode2D{
let point = CGPoint(x:CGFloat(point2d.position.x) , y:CGFloat(point2d.position.y))
let action = SKAction.move(to: point, duration: 1)
actions.append(action)
}
}
let seq = SKAction.sequence(actions)
self.run(seq) {
self.moving = false
}
}
This is called in another function of my enemy in this block:
case .follow:
if allowMovement{
if self.action(forKey: "Hurt") == nil{
// orientEnemy(to: playerPos)
moveEnemy(to: playerPos)
// animateWalk()
}
}
and this function is called in the update function of my GameScene in this block:
if enemy.checkCircularIntersection(player: player, node: enemy, radius: 40){
print("Enemy is close to player")
enemy.update(playerPos: player.position)
}
The checkCircularIntersection function only checks, if the player is near the enemy.
My problem is that the enemy follows the player but when the player moves, the enemy moves to the point where the player stands before, then it stops for a second and then it moves again to the point where the player stood. But the player is already moved away.
Is there a way to let the enemy permanently follow the player and avoid obstacles without stopping?
Your code specifically guards against moving again until the existing move is completed.
guard !moving else { return }
moving = true
If you want to use paths you will have to recompute and reapply the path movement regularly to account for the target's movement.
However, to achieve your aim, Paths are not the most suitable GameKit tool to use. GKAgents are specifically designed for that kind of thing. It will require a significant refactoring of your code, but take a look at Agents, Goals and Behaviours.

ARKit dragging object changes scale

İ am trying to move the SCNNode object which i placed on to a surface. İt moves but the scale changes and it becomes smaller, when i first start to move.
This is what i did;
#IBAction func dragBanana(_ sender: UIPanGestureRecognizer) {
guard let _ = self.sceneView.session.currentFrame else {return}
if(sender.state == .began) {
let location = sender.location(in: self.sceneView)
let hitTestResult = sceneView.hitTest(location, options: nil)
if !hitTestResult.isEmpty {
guard let hitResult = hitTestResult.first else {return}
movedObject = hitResult.node
}
}
if (sender.state == .changed) {
if(movedObject != nil) {
let location = sender.location(in: self.sceneView)
let hitTestResult = sceneView.hitTest(location, types: .existingPlaneUsingExtent)
guard let hitResult = hitTestResult.first else {return}
let matrix = SCNMatrix4(hitResult.worldTransform)
let vector = SCNVector3Make(matrix.m41, matrix.m42, matrix.m43)
movedObject?.position = vector
}
}
if (sender.state == .ended) {
movedObject = nil
}
}
My answer is probably very late, but I faced this issue myself and it took me a while to kind of figure out why this might happen. I'll share my experience and maybe you can relate to it.
My problem was that I was trying to change the position of the node after changing its scale at runtime (most of my 3D assets were very large when added, I scale them down with a pinch gesture). I noticed changing the scale was the cause of the position change not working as expected.
I found a very simple solution to this. You simply need to change this line:
movedObject?.position = vector
to this:
movedObject?.worldPosition = vector
According to SCNNode documentation, the position property determines the position of the node relative to its parent. While worldPosition is the position of the node relative to the scene's root node (i.e. the world origin of ARSCNView)
I hope this answers your question.
It's because you're moving the object on the 3 axis and Z changes that's why it feels like it scales but it's only getting closer to you.

Add SCNNode after rotating rootNode

I'm trying to add a node (a sphere) to a body model but it doesn't work properly after I rotate the model through a pan gesture.
Here's how I'm adding the node (using a long tap gesture):
func addSphere(sender: UILongPressGestureRecognizer) {
switch sender.state {
case .Began:
let location = sender.locationInView(bodyView)
let hitResults = bodyView.hitTest(location, options: nil)
if hitResults.count > 0 {
let result = hitResults.first!
let secondSphereGeometry = SCNSphere(radius: 0.015)
secondSphereGeometry.firstMaterial?.diffuse.contents = UIColor.redColor()
let secondSphereNode = SCNNode(geometry: secondSphereGeometry)
let vpWithZ = SCNVector3(x: Float(result.worldCoordinates.x), y: Float(result.worldCoordinates.y), z: Float( result.worldCoordinates.z))
secondSphereNode.position = vpWithZ
bodyView.scene!.rootNode.addChildNode(secondSphereNode)
}
break
default:
break
}
}
Here is how I rotate the view:
func rotateGesture(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view)
var newZAngle = (Float)(translation.x)*(Float)(M_PI)/180.0
newZAngle += currentZAngle
bodyView.scene!.rootNode.transform = SCNMatrix4MakeRotation(newZAngle, 0, 0, 1)
if sender.state == .Ended {
currentZAngle = newZAngle
}
}
And to load the 3D model I just do:
bodyView.scene = SCNScene(named: "male_body.dae") // bodyView is a SCNView in the storyboard
I found something related to the worldTransform property and also the function convertPosition:toNode: but couldn't find an example that works well.
The problem is that, if I rotate the model, the sphere are not positioned properly. They're always positioned as if the model was in its initial state.
If I turn the body and add long tap his arm (on the side), the sphere is added somewhere floating in front of the body, as you can see above.
I don't know how to fix this. Appreciate if someone can help me. Thanks!