How to keep child from transforming with parent - swift

Im creating a function to shoot a bullet out of a guns barrel. Im keeping the bullet as a child of the gun in order to have it place at the right position regardless of where the player is facing. However if I shoot while the player is moving the bullet will move relative to the player. How can I remove the relative reference when the bullet has spawn.
func creatBullet() {
let bullet = SKSpriteNode(imageNamed: "bullet")
bullet.zPosition = 4
bullet.position = CGPoint(x: gun.position.x-1, y: gun.position.y-20)
//add physics
gun.addChild(bullet)
let xDirection = CGFloat(bullet.position.x)
let yDirection = CGFloat(bullet.position.y + 150)
let bulletMove = SKAction.moveBy(x: xDirection, y: yDirection, duration: 2)
let sequence = SKAction.sequence([bulletMove, SKAction.removeFromParent()])
bullet.run(sequence)
}

from Apple...
Converts a point in this node’s coordinate system to the coordinate
system of another node in the node tree. Declaration
func convert(_ point: CGPoint, to node: SKNode) -> CGPoint
Parameters
point A point in this node’s coordinate system.
node Another node in the same node tree as this node.
Returns The same point converted to the other node’s coordinate
system.
so I would just use (assuming self is scene)
let pos = convert(gun.position, to self)
bullet.position = pos
addChild(bullet)
EDIT
if your object is a child of another object you may have to use convert from and convert to to get the absolute portion
pos = convert(convert(gun.position, from: gun.parent!), to: self)

Related

How to get the SCNVector3 position of the camera in relation to it's direction ARKit Swift

I am trying to attach an object in front of the camera, but the issue is that it is always in relation to the initial camera direction. How can I adjust/get the SCNVector3 position to place the object in front, even if the direction of the camera is up or down?
This is how I do it now:
let ballShape = SCNSphere(radius: 0.03)
let ballNode = SCNNode(geometry: ballShape)
let viewPosition = sceneView.pointOfView!.position
ballNode.position = SCNVector3Make(viewPosition.x, viewPosition.y, viewPosition.z - 0.4)
sceneView.scene.rootNode.addChildNode(ballNode)
Edited to better answer the question now that it's clarified in a comment
New Answer:
You are using only the position of the camera, so if the camera is rotated, it doesn't affect the ball.
What you can do is get the transform matrix of the ball and multiply it by the transform matrix of the camera, that way the ball position will be relative to the full transformation of the camera, including rotation.
e.g.
let ballShape = SCNSphere(radius: 0.03)
let ballNode = SCNNode(geometry: ballShape)
ballNode.position = SCNVector3Make(0.0, 0.0, -0.4)
let ballMatrix = ballNode.transform
let cameraMatrix = sceneView.pointOfView!.transform
let newBallMatrix = SCNMatrix4Mult(ballMatrix, cameraMatrix)
ballNode.transform = newBallMatrix
sceneView.scene.rootNode.addChildNode(ballNode)
Or if you only want the SCNVector3 position, to answer exactly to your question (this way the ball will not rotate):
...
let newBallMatrix = SCNMatrix4Mult(ballMatrix, cameraMatrix)
let newBallPosition = SCNVector3Make(newBallMatrix.m41, newBallMatrix.m42, newBallMatrix.m43)
ballNode.position = newBallPosition
sceneView.scene.rootNode.addChildNode(ballNode)
Old Answer:
You are using only the position of the camera, so when the camera rotates, it doesn't affect the ball.
SceneKit uses a hierarchy of nodes, so when a node is "child" of another node, it follows the position, rotation and scale of its "parent". The proper way of attaching an object to another object, in this case the camera, is to make it "child" of the camera.
Then, when you set the position, rotation or any other aspect of the transform of the "child" node, you are setting it relative to its parent. So if you set the position to SCNVector3Make(0.0, 0.0, -0.4), it's translated -0.4 units in Z on top of its "parent" translation.
So to make what you want, it should be:
let ballShape = SCNSphere(radius: 0.03)
let ballNode = SCNNode(geometry: ballShape)
ballNode.position = SCNVector3Make(0.0, 0.0, -0.4)
let cameraNode = sceneView.pointOfView
cameraNode?.addChildNode(ballNode)
This way, when the camera rotates, the ball follows exactly its rotation, but separated -0.4 units from the camera.

ARSCNView unprojectPoint

I need to convert a point in the 2d coordinate space of my ARSCNView to a coordinate in 3d space. Basically a ray from the point of view to the touched location (up to a set distance away).
I wanted to use arView.unprojectPoint(vec2d) for that, but the point returned always seems to be located in the center of the view
vec2d is a SCNVector3 created from a 2d coordinate like this
SCNVector3(x, y, 0) // 0 specifies camera near plane
What am I doing wrong? How do I get the desired result?
I think you have at least 2 possible solutions:
First
Use hitTest(_:types:) instance method:
This method searches for real-world objects or AR anchors in the captured camera image corresponding to a point in the SceneKit view.
let sceneView = ARSCNView()
func calculateVector(point: CGPoint) -> SCNVector3? {
let hitTestResults = sceneView.hitTest(point,
types: [.existingPlane])
if let result = hitTestResults.first {
return SCNVector3.init(SIMD3(result.worldTransform.columns.3.x,
result.worldTransform.columns.3.y,
result.worldTransform.columns.3.z))
}
return nil
}
calculateVector(point: yourPoint)
Second
Use unprojectPoint(_:ontoPlane:) instance method:
This method returns the projection of a point from 2D view onto a plane in the 3D world space detected by ARKit.
#nonobjc func unprojectPoint(_ point: CGPoint,
ontoPlane planeTransform: simd_float4x4) -> simd_float3?
or:
let point = CGPoint()
var planeTransform = simd_float4x4()
sceneView.unprojectPoint(point,
ontoPlane: planeTransform)
Add a empty node infront of camera at 'x' cm offset and making it the child of camera.
//Add a node in front of camera just after creating scene
hitNode = SCNNode()
hitNode!.position = SCNVector3Make(0, 0, -0.25) //25 cm offset
sceneView.pointOfView?.addChildNode(hitNode!)
func unprojectedPosition(touch: CGPoint) -> SCNVector3 {
guard let hitNode = self.hitNode else {
return SCNVector3Zero
}
let projectedOrigin = sceneView.projectPoint(hitNode.worldPosition)
let offset = sceneView.unprojectPoint(SCNVector3Make(Float(touch.x), Float(touch.y), projectedOrigin.z))
return offset
}
See the Justaline GitHub implementation of the code here

How to change node value

func createBall(){
ballS = SKNode()
let ball = SKSpriteNode(imageNamed: "Ball")
saw.zPosition = 2
ballS.addChild(ball)
self.addChild(ballS)
}
This creates a Ball node with z position 2. But while running the game, I want to create another ball that has a z position at 1. How can I do this, or do I have to make a whole new function that makes a ball node with z position 1?
If each ball sprite has to be a child of the ballS node, then you could do the following.
Define ballS as a property (i.e. after the class definition but before any functions)
let balls = SKNode()
in didMove(to:), add the ballS Node:
addChild(ballS)
Create a addBall function: (don't call it createBall unless all it does create the ball)
func addBall(withZPosition zPos: Int) {
let newBall = SKSpriteNode(imageNamed: "Ball")
newBall.zPosition = zPos
ballS.addChild(newBall)
}
Then simply call addBall(withZPosition:) to create and add a new ball:
addBall(withZPosition: 2)
If you want more control over the ball that is created (e.g. you want to change some of it's properties before adding it to the scene), you can make a createBall function that creates a new ball and returns the new node:
func createBall(withZPosition zPos: Int) -> SKSpriteNode {
let newBall = SKSpriteNode(imageNamed: "Ball")
newBall.zPosition = zPos
return newBall
}
and use this as follows:
let newBall = createBall(withZPosition: 2)
newBall.size = CGSize(x: 50, y:50)
ballS,addchild(newBall)
U can have a global variable so that whenever you want to change u set it and create the ball
so create a variable
var myZposition = 2
and create a ball at the start in didmove to view by calling createBall()
func createBall(){
ballS = SKNode()
let ball = SKSpriteNode(imageNamed: "Ball")
saw.zPosition = myZPosition
ballS.addChild(ball)
self.addChild(ballS)
}
then after these if you want to change the zPosition just set it at the end of didmove to view
myZposition = 1
Then whenever u create balls from here your zPosition would be 1. This would be an easy setup if you have lots of balls and implement a change in zPosition for reaching a specific area of the game
Hope this helped
I'd suggest to use an argument, like:
func createBall(z: Int) {
ballS = SKNode()
let ball = SKSpriteNode(imageNamed: "Ball")
saw.zPosition = z
ballS.addChild(ball)
self.addChild(ballS)
}
And use it as:
createBall(z: 2)

Why are objects in the same SKNode layer not interacting with each other?

I have less than 1 year using SpriteKit so I didn't use SKNodes as layers before until recently.
I have an SKNode layer that holds all of the fish and the user's position, for example:
var layerMainGame = SKNode()
layerMainGame.zPosition = 50
layerMainGame.addChild(userPosition)
layerMainGame.addChild(pipFish)
addChild(layerMainGame)
The interaction whether the user touched a fish or not is handled with this function, which is basically checking if their frames crossed:
if CGRectIntersectsRect(CGRectInset(node.frame, delta.dx, delta.dy), self.userPosition.frame) {
print("You got hit by \(name).")
gameOver()
}
It works. The interaction between the userPosition and pipFish works. What doesn't work is fish that are added as the game progresses. I have a function spawning different types of fish in intervals like this:
func spawnNew(fish: SKSpriteNode) {
layerMainGame.addChild(fish)
}
The interaction between the user and those fish that get added to the same layer later in the game does not work. I can pass right through them and no game over happens. When I completely remove the entire layerMainGame variable and just add them to the scene like normal, all the interactions work. Adding them all to the same SKNode layer doesn't work.
This is the function that creates a hit collision for every fish.
func createHitCollisionFor(name: String, GameOver gameOver: String!, delta: (dx: CGFloat, dy: CGFloat), index: Int = -1) {
enumerateChildNodesWithName(name) { [unowned me = self] node, _ in
if CGRectIntersectsRect(CGRectInset(node.frame, delta.dx, delta.dy), self.userPosition.frame) {
me.gameOverImage.texture = SKTexture(imageNamed: gameOver)
didGetHitActions()
me.runAction(audio.playSound(hit)!)
if index != -1 {
me.trophySet.encounterTrophy.didEncounter[index] = true
}
print("You got hit by \(name).")
}
}
}
And I call it like this:
createHitCollisionFor("GoldPiranha", GameOver: model.gameOverImage["Gold"], delta: (dx: 50, dy: 50), index: 1)
It works when the fish are not in the layer, but doesn't work when they are added to the layer.
When a node is placed in the node tree, its position property places it within a coordinate system provided by its parent.
Sprite Kit uses a coordinate orientation that starts from the bottom left corner of the screen (0, 0), and the x and y values increase as you move up and to the right.
For SKScene, the default value of the origin – anchorPoint is (0, 0), which corresponds to the lower-left corner of the view’s frame rectangle. To change it to center you can specify (0.5, 0.5)
For SKNode, the coordinate system origin is defined by its anchorPoint which by default is (0.5, 0.5) which is center of the node.
In your project you have layerMainGame added for example to the scene, his anchorPoint is by default (0.5,0.5) so the origin for the children like your fish is the center, you can see it if you change the fish positions like:
func spawnNew(fish: SKSpriteNode) {
layerMainGame.addChild(fish)
fish.position = CGPointZero // position 0,0 = parent center
}
Hope it help to understand how to solve your issue.
Update: (after your changes to the main question)
To help you better understand what happens I will give an example right away:
override func didMoveToView(view: SKView) {
var layerMainGame = SKNode()
addChild(layerMainGame)
let pipFish = SKSpriteNode(color: UIColor.yellowColor(), size: CGSizeMake(50,50))
pipFish.name = "son"
self.addChild(pipFish)
let layerPipFish = SKSpriteNode(color: UIColor.yellowColor(), size: CGSizeMake(50,50))
layerPipFish.name = "son"
layerMainGame.addChild(layerPipFish)
enumerateChildNodesWithName("son") { [unowned me = self] node, _ in
print(node)
}
}
Output:
Now I will simply change the line:
layerMainGame.addChild(layerPipFish)
with:
self.addChild(layerPipFish)
Output:
What happened?
As you can see enumerateChildNodesWithName written as your and my code print only childs directly added to self (because actually we launch enumerateChildNodesWithName which it is equal to launch self.enumerateChildNodesWithName )
How can I search in the full node tree?
If you have a node named "GoldPiranha" then you can search through all descendants by putting a // before the name. So you would search for "//GoldPiranha":
enumerateChildNodesWithName("//GoldPiranha") { [unowned me = self] ...

How to move enemy towards a moving player?

I am creating a simple sprite kit game that will position a player on the left side of the screen, while enemies approach from the right. Since the player can be moved up and down, I want the enemies to "smartly" adjust their path towards the player.
I tried removing and re-adding the SKAction sequence whenever the player moves, but the below code causes the enemies to not show at all, probably because its just adding and removing each action on every frame update, so they never have a chance to move.
Hoping to get a little feedback about the best practice of creating "smart" enemies that will move towards a player's position at any time.
Here is my code:
func moveEnemy(enemy: Enemy) {
let moveEnemyAction = SKAction.moveTo(CGPoint(x:self.player.position.x, y:self.player.position.y), duration: 1.0)
moveEnemyAction.speed = 0.2
let removeEnemyAction = SKAction.removeFromParent()
enemy.runAction(SKAction.sequence([moveEnemyAction,removeEnemyAction]), withKey: "moveEnemyAction")
}
func updateEnemyPath() {
for enemy in self.enemies {
if let action = enemy.actionForKey("moveEnemyAction") {
enemy.removeAllActions()
self.moveEnemy(enemy)
}
}
}
override func update(currentTime: NSTimeInterval) {
self. updateEnemyPath()
}
You have to update enemy position and zRotation property in each update: method call.
Seeker and a Target
Okay, so lets add some nodes to the scene. We need a seeker and a target. Seeker would be a missile, and target would be a touch location. I said you should do this inside of a update: method, but I will use touchesMoved method to make a better example. Here is how you should setup the scene:
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
let missile = SKSpriteNode(imageNamed: "seeking_missile")
let missileSpeed:CGFloat = 3.0
override func didMoveToView(view: SKView) {
missile.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(missile)
}
}
Aiming
To implement the aiming you have to calculate the how much you have to rotate a sprite based on its target. In this example I will use a missile and make it point towards the touch location. To accomplish this, you should use atan2 function, like this ( inside touchesMoved: method):
if let touch = touches.first {
let location = touch.locationInNode(self)
//Aim
let dx = location.x - missile.position.x
let dy = location.y - missile.position.y
let angle = atan2(dy, dx)
missile.zRotation = angle
}
Note that atan2 accepts parameters in y,x order, rather than x,y.
So right now, we have an angle in which missile should go. Now lets update its position based on that angle (add this inside touchesMoved: method right below the aiming part):
//Seek
let vx = cos(angle) * missileSpeed
let vy = sin(angle) * missileSpeed
missile.position.x += vx
missile.position.y += vy
And that would be it. Here is the result:
Note that in Sprite-Kit the angle of 0 radians specifies the positive x axis. And the positive angle is in the counterclockwise direction:
Read more here.
This means that you should orient your missile to the right rather than upwards . You can use the upwards oriented image as well, but you will have to do something like this:
missile.zRotation = angle - CGFloat(M_PI_2)