I'm playing with the tile editor, with which I've created a simple, somewhat large map like the following. The tile size is 64 x 64. And the map size is 58 x 32.
And I want to know the current position of the game character. Let me call it player (as SKNode). I want to know this guy's current position so that I can find out whether any of its adjascent tile is NOT an obstacle. The following lines of code gives me (7, -3).
background = self.childNode(withName: "World") as! SKTileMapNode
let position = background.convert(player.position, from: player)
let column = background.tileColumnIndex(fromPosition: position)
let row = background.tileRowIndex(fromPosition: position)
print(column, row)
, which suggests that these coordinates point to the center of the camera, indicated by the red circle. The following gives me (10, 2).
let position = background.convert(player.position, from: self)
let column = background.tileColumnIndex(fromPosition: position)
let row = background.tileRowIndex(fromPosition: position)
That's the position from the bottom-left corner.
Now, if I want to move the game character up by 1 tile, writing...
let position = background.convert(player.position, from: player)
let column = background.tileColumnIndex(fromPosition: position)
let row = background.tileRowIndex(fromPosition: position)
let destination = background.centerOfTile(atColumn: column, row: row + 1)
let action = SKAction.move(to: destination, duration: 1)
player.run(action)
, the game character will go missing in action, disappearing from the face of the map. So how can I move the node by one tile?
Maybe try this:
let currentPlayerPosition = background.convert(player.position, from: player)
let column = background.tileColumnIndex(fromPosition: currentPlayerPosition)
let row = background.tileRowIndex(fromPosition: currentPlayerPosition)
let destination = background.centerOfTile(atColumn: column, row: row + 1)
// the new line I added
let newPlayerPosition = background.convert(destination, to: player)
let action = SKAction.move(to: newPlayerPosition, duration: 1)
player.run(action)
Related
I'm trying to detect when the camera is facing my object that I've placed in ARSKView. Here's the code:
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
guard let sceneView = self.view as? ARSKView else {
return
}
if let currentFrame = sceneView.session.currentFrame {
//let cameraZ = currentFrame.camera.transform.columns.3.z
for anchor in currentFrame.anchors {
if let spriteNode = sceneView.node(for: anchor), spriteNode.name == "token", intersects(spriteNode) {
// token is within the camera view
let distance = simd_distance(anchor.transform.columns.3,
currentFrame.camera.transform.columns.3)
//print("DISTANCE BETWEEN CAMERA AND TOKEN: \(distance)")
if distance <= captureDistance {
// token is within the camera view and within capture distance
print("token is within the camera view and within capture distance")
}
}
}
}
}
The problem is that the intersects method is returning true both when the object is directly in front of the camera, as well as directly behind you. How can I update this code so it only detects when the spriteNode is in the current camera viewfinder? I'm using SpriteKit by the way, not SceneKit.
Here's the code I'm using to actually create the anchor:
self.captureDistance = captureDistance
guard let sceneView = self.view as? ARSKView else {
return
}
// Create anchor using the camera's current position
if sceneView.session.currentFrame != nil {
print("token dropped at \(distance) meters and bearing: \(bearing)")
// Add a new anchor to the session
let transform = getTransformGiven(bearing: bearing, distance: distance)
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
}
func getTransformGiven(bearing: Float, distance: Float) -> matrix_float4x4 {
let origin = MatrixHelper.translate(x: 0, y: 0, z: Float(distance * -1))
let bearingTransform = MatrixHelper.rotateMatrixAroundY(degrees: bearing * -1, matrix: origin)
return bearingTransform
}
I have spent a while looking at this, and have come to the conclusion that trying to get the distance between the currentFrame.camera and the anchor doesn't work simply because it returns similar values irregardless of whether the anchor is infront of, or behind the camera. By this I mean that if we assume that our anchor is at point x, and we move forwards 1meter or backwards 1 meter, the distance from the camera and the anchor is still 1 meter.
As such after some experimenting I believe we need to look at the following variables and functions to help us detect whether our SKNode is infront of the camera:
(a) The zPosition of the SpriteNode which refers to:
The z-order of the node (used for ordering). Negative z is "into" the screen, Positive z is "out" of the screen
(b) open func intersects(_ node: SKNode) -> Bool which:
Returns true if the bounds of this node intersects with the
transformed bounds of the other node, otherwise false.
As such the following seems to do exactly what you need:
override func update(_ currentTime: TimeInterval) {
//1. Get The Current ARSKView & Current Frame
guard let sceneView = self.view as? ARSKView, let currentFrame = sceneView.session.currentFrame else { return }
//3. Iterate Through Our Anchors & Check For Our Token Node
for anchor in currentFrame.anchors {
if let spriteNode = sceneView.node(for: anchor), spriteNode.name == "token"{
/*
If The ZPosition Of The SpriteNode Is Negative It Can Be Seen As Into The Screen Whereas Positive Is Out Of The Screen
However We Also Need To Know Whether The Actual Frostrum (SKScene) Intersects Our Object
If Our ZPosition Is Negative & The SKScene Doesnt Intersect Our Node Then We Can Assume It Isnt Visible
*/
if spriteNode.zPosition <= 0 && intersects(spriteNode){
print("Infront Of Camera")
}else{
print("Not InFront Of Camera")
}
}
}
}
Hope it helps...
You can also use this function to check the camera's position :-
- (void)session:(ARSession *)session didUpdateFrame:(ARFrame *)frame; {
simd_float4x4 transform = session.currentFrame.camera.transform;
SCNVector3 position = SCNVector3Make(transform.columns[3].x,
transform.columns[3].y,
transform.columns[3].z);
// Call any function to check the Position.
}
I would give you a clue. Check the ZPosition like this.
if let spriteNode = sceneView.node(for: anchor),
spriteNode.name == "token",
intersects(spriteNode) && spriteNode.zPosition < 0 {....}
I'm creating my anchor and 2d node in ARSKView like so:
func displayToken(distance: Float) {
print("token dropped at: \(distance)")
guard let sceneView = self.view as? ARSKView else {
return
}
// Create anchor using the camera's current position
if let currentFrame = sceneView.session.currentFrame {
removeToken()
// Create a transform with a translation of x meters in front of the camera
var translation = matrix_identity_float4x4
translation.columns.3.z = -distance
let transform = simd_mul(currentFrame.camera.transform, translation)
// Add a new anchor to the session
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
}
}
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
// Create and configure a node for the anchor added to the view's session.
if let image = tokenImage {
let texture = SKTexture(image: image)
let tokenImageNode = SKSpriteNode(texture: texture)
tokenImageNode.name = "token"
return tokenImageNode
} else {
return nil
}
}
This places it exactly in front of the camera at a given distance (z). What I want to also do is take a latitude/longitude for an object, calculate an angle or heading in degrees, and initially drop the anchor/node at this angle from the camera. I'm currently getting the heading by using the GMSGeometryHeading method, which takes users current location, and the target location to determine the heading. So when dropping the anchor, I want to put it in the right direction towards the target location's lat/lon. How can I achieve this with SpriteKit/ARKit?
Can you clarify your question a bit please?
Perhaps the following lines can help you as example. There the cameraNode moves using a basic geometry-formula, moving obliquely depending on the angle (in Euler) in both x and y coordinates
var angleEuler = 0.1
let rotateX = Double(cameraNode.presentation.position.x) - sin(degrees(radians: Double(angleEuler)))*10
let rotateZ = Double(cameraNode.presentation.position.z) - abs(cos(degrees(radians: Double(angleEuler))))*10
cameraNode.position = SCNVector3(x:Float(rotateX), y:0, z:Float(rotateX))
If you want an object fall in front of the camera, and the length of the fall depend on a degree just calculate the value of "a" in the geometry-formula "Tan A = a/b" and update the node's "presentation.position.y"
I hope this helps
Is there anyway to center the pivot of a model? Currently (by default) it is set to the bottom left. I want to set to the center. How can I do that?
Here is the image:
UPDATE: I added another node and added the chair as a child.
So, now it works better but it does not rotate all the way and it resets the position if I continue to rotate. Here is the code for the panned operation:
#objc func panned(recognizer :UIPanGestureRecognizer) {
var newAngleY :Float = 0.0
if recognizer.state == .changed {
let sceneView = recognizer.view as! ARSCNView
let touchPoint = recognizer.location(in: sceneView)
let translation = recognizer.translation(in: sceneView)
print(translation.x)
let scnHitTestResults = self.sceneView.hitTest(touchPoint, options: nil)
if let hitTestResult = scnHitTestResults.first {
if let parentNode = hitTestResult.node.parent {
newAngleY = (Float)(translation.x)*(Float)(Double.pi)/180
newAngleY += currentAngleY
parentNode.eulerAngles.y = newAngleY
}
}
}
else if recognizer.state == .ended {
currentAngleY = newAngleY
}
}
You can change a pivot of the Chair using the pivotproperty of the SCNNode (Apple Documentation)
You can change this using an SCNMatrix4MakeTranslation e.g:
nodeToAdd.pivot = SCNMatrix4MakeTranslation(0,0,0)
This may solve the problem:
//1. Get The Bounding Box Of The Node
let minimum = float3(nodeToAdd.boundingBox.min)
let maximum = float3(nodeToAdd.boundingBox.max)
//2. Set The Translation To Be Half Way Between The Vector
let translation = (maximum + minimum) * 0.5
//3. Set The Pivot
nodeToAdd.pivot = SCNMatrix4MakeTranslation(translation.x, translation.y, translation.z)
You could also change it within within a program like Blender or Maya.
Alternatively, and probably much easier, is to edit the model in the SceneKit Editor itself.
If you cant fix it's pivot, then what you can do is to create an Empty Node, and then add the Chair as a child of this, ensuring that it is centered within that.
This way you should be able to transform it's rotation for example more accurately.
I have two view controllers. One allows the user to enter their own sightings of things:
The other is a UITableViewController that allows them to see their submitted sightings:
What I want is for the user to be able to click on their sighting in the 2nd VC and for it to populate the 1st VC with the relevant details. I have that working for the textFields using :
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let pvc = self.navigationController?.previousViewController() as! SubmitSightingVC
let sighting = sightingsArray[indexPath.row]
pvc.titleTextField.text = sighting["title"] as! String?
pvc.locationTextField.text = sighting["location"] as! String?
pvc.dateTextField.text = sighting["date"] as! String?
pvc.descriptionTextView.text = sighting["sightingDesc"] as! String?
let pinArray = ["1", "2", "3", "4", "5"]
let pinTextArray = ["1text", "2text", "3text", "4text", "5text"]
var pinImageArray = [#imageLiteral(resourceName: "1"), #imageLiteral(resourceName: "2"), #imageLiteral(resourceName: "3"), #imageLiteral(resourceName: "4"), #imageLiteral(resourceName: "5")]
let pinIconString = sighting["pinIcon"] as! String
let index = pinArray.index(of: pinIconString)
pvc.pinImageView.image = pinImageArray[index!]
pvc.pinTextField.text = pinTextArray[index!]
// Bit I'm struggling with:
var coordinates = CLLocationCoordinate2D(latitude: sighting["latitude"] as! Double, longitude: sighting["longitude"] as! Double)
pvc.mapView.centerCoordinate = coordinates
self.navigationController!.popViewController(animated: true)
}
The problem is that because the centre of the map is behind the blurred view it is offset. The pin on the map isn't an actual pin but a UIImage (pinImageView). How can I move the map so that the coordinates are under this pinImage instead of in the centre of the mapView?
As mentioned above you can use the MKMapCamera to position the view of the map or you can also use setVisibleMapRect and add some padding to position the map like this:
self.mapView.setVisibleMapRect(self.mapView.visibleMapRect, edgePadding: UIEdgeInsetsMake(200.0, 0.0, 0.0, 0.0), animated: true)
This will offset the center by 200 screen pixels on the top.
From the excellent #rmp answer, the general idea is
self.map.setVisibleMapRect(self.map.visibleMapRect,
edgePadding: UIEdgeInsets(top: -200, left: 0, bottom: 0, right: 0),
animated: true)
to move it "upwards 200". However, note that the arithmetic is not really correct.
To move a "box upwards" you have to negative inset the top, and positive inset the bottom. The literal above code will sort of stretch and zoom the box (the center would move up, but it would move up a random height less than 200).
So what you have to do is this:
let UP = 170 // move it up 170
let LEFT = 50 // move it left 50
self.map.setVisibleMapRect(self.map.visibleMapRect,
edgePadding: UIEdgeInsets(top: -UP, left: -LEFT, bottom: UP, right: LEFT),
animated: true)
That will work.
Here's a total formula for getting a point on the map, perhaps a town or house coords and
1. nicely centering it, regardless of current camera position, angle, zoom etc, and then
2. move it, let's say upwards, to an exact point on the screen (to allow for an input display or whatever)
let coords = .. the coords of the town, house etc
// for example, if you let someone tap on the map, something like...
let coords = map.convert(g.location(in: map), toCoordinateFrom: map)
// we'll need the W/H of the map on the device:
let W = self.map.bounds.width
let H = self.map.bounds.height
// here's the formula to tidily and safely move/zoom
// to center a coords on the screen
let thatRegion = MKCoordinateRegion(
center: coords,
latitudinalMeters: 5000 * Double((H/W)),
longitudinalMeters: 5000)
MKMapView.animate(withDuration: 2, animations: { [weak self] in
self?.map.region = thatRegion
}, completion: {completed in
print("part 1 done")
// so now the coords is centered. but we want to move it
// so that we can see something else we're adding to the
// screen on top, such as an input display
.. so that's the first part.
Now you want to continue animating the move to nudge it out of the way of your UX:
MKMapView.animate(withDuration: 2, animations: { [weak self] in
guard let self = self else { return }
// in this example, we'll move the point,
// to the top-left of the screen, just below the notch:
let UP = H/2 - 20
let LEFT = W/4
// here's the exact formula for moving
// the center of the map a certain amount UP/LEFT:
self.map.setVisibleMapRect(self.map.visibleMapRect,
edgePadding: UIEdgeInsets(
top: -UP, left: -LEFT, bottom: UP, right: LEFT),
animated: true)
}, completion: {completed in
print("part 2 done")
})
})
That will always do it.
Also, recall that unfortunately MapKit is inherently sloppy. It is, simply, not possible to position a geographic point absolutely in the center of the screen; there's always a little variation. (Say the odd few pixels one way or the other.) It's just how MapKit works. Also, animations (such as the two above) can be thrown off a bit if the texture data is still loading.
For simplicity, let's say I have an SKSpriteNode and an SKTileMapNode like so
var background = childNode(withName: "Background") as! SKTileMapNode
var player = SKSpriteNode(imageNamed: "player") as! SKSpriteNode
I want to move player to the row position 5 and column position 8 in background, whats the best way to go about this?
To get started, you could retrieve the position of the specified tile and create a move action to run on the player node.
let destination = background.centerOfTile(atColumn: 8, row: 5)
let action = SKAction.move(to: destination, duration: 5)
player.run(action)
Then you might look at UITapGestureRecognizer so the player can move to where you tap on the map.