How can I handle a touch as a consequence of a previous touch? (Swift, SpriteKit) - swift

This is a chess game in Swift using SpriteKit.
What I would like to happen is for the touched piece to move to the square touched on the next touch. When I run, the piece does not move when I try this. I know for certain that my code works up to and including 'case: "p"', and that the sprite is detecting the first touch.
In the image I've tried to implement this second touch using 'touch2', is this the correct thing to do? Thanks.
This is how I'm detecting the first touch and identifying the piece which has been touched. touchedPiece is the identified piece. moveset is an attribute determining the possible moves.
for touch in touches
{
let location = touch.location(in:self)
let firstTouchedNode = atPoint(location)
let touchedPiece:SKSpriteNode = (firstTouchedNode as? SKSpriteNode)!
switch touchedPiece.moveset {
case "p":
let locationToMove = touch.location(in:self)
let moveto = findpawnmoves(piece: touchedPiece)
spawnMoves(arr: moveto)
And here is the attempt to move the piece after locationToMove has been decided.
if moveto[0].contains(locationToMove){
move(piece: touchedPiece, location: moveto[0].position)
whiteMoved = true
deleteMoves(arr: moveto)
This is an example of how I am creating an array of possible squares to move to. I'll use the knight moves because it's the most concise.
func createMoveNode(piece: SKSpriteNode, up:Int, across:Int) -> SKSpriteNode{
let moveNode = SKSpriteNode(color: .clear, size: CGSize(width: CGFloat(cellsize), height: CGFloat(cellsize)))
moveNode.anchorPoint = CGPoint(x:0.5, y:0.5)
moveNode.position = CGPoint(x: piece.position.x + CGFloat(Double(across)*d), y: piece.position.y + CGFloat(Double(up)*d))
return moveNode
}
func findknightmoves(piece: SKSpriteNode) -> Array<SKSpriteNode>{
let move1 = createMoveNode(piece: piece, up:2, across:-1)
let move2 = createMoveNode(piece: piece, up:2, across:1)
let move3 = createMoveNode(piece: piece, up:-2, across:-1)
let move4 = createMoveNode(piece: piece, up:-2, across:1)
return [move1,move2,move3,move4]
}
func move(piece: SKSpriteNode, location: CGPoint){
piece.position = location
}
func spawnMoves(arr: Array<SKSpriteNode>){
for i in 0...arr.count-1{
arr[i].zPosition = 1
self.addChild(arr[i])
}
}
func deleteMoves(arr: Array<SKSpriteNode>){
for i in 0...arr.count-1{
arr[i].removeFromParent()
}
}

Related

Entity-Component in Swift

I am trying to build a simple iOS game using entity-component architecture similar to what is described here.
What I would like to achieve in my game is when a user touches the screen, detect where the touch occurred and move all entities of one type towards a specific direction (direction depends on where the user touched, right of screen = up, left of screen = down).
So far, the game is really simple and I am only getting started, but I am stuck in this simple functionality:
My issue is that an SKAction is supposed to run on all entities of a type, but happens at all.
Before I redesigned my game to an ECS approach, this worked fine.
Here is the GKEntity subclass that I declared in Lines.swift:
class Lines: GKEntity {
override init() {
super.init()
let LineSprite = SpriteComponent(color: UIColor.white, size: CGSize(width: 10.0, height: 300))
addComponent(LineSprite)
// Set physics body
if let sprite = component(ofType: SpriteComponent.self)?.node {
sprite.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: sprite.size.width, height: sprite.size.height))
sprite.physicsBody?.isDynamic = false
sprite.physicsBody?.restitution = 1.0
sprite.physicsBody?.friction = 0.0
sprite.physicsBody?.linearDamping = 0.0
sprite.physicsBody?.angularDamping = 0.0
sprite.physicsBody?.mass = 0.00
sprite.physicsBody?.affectedByGravity = false
sprite.physicsBody?.usesPreciseCollisionDetection = true
sprite.physicsBody?.categoryBitMask = 0b1
sprite.zPosition = 10
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In TouchesBegan I am calling the function Move(XAxisPoint: t.location(in: self)) which is declared in GameScene and here is what Move() does:
///Determines direction of movement based on touch location, calls MoveUpOrDown for movement
func move(XAxisPoint: CGPoint){
let Direction: SKAction
let Key: String
if XAxisPoint.x >= 0 {
Direction = SKAction.moveBy(x: 0, y: 3, duration: 0.01)
Key = "MovingUp"
} else {
Direction = SKAction.moveBy(x: 0, y: -3, duration: 0.01)
Key = "MovingDown"
}
moveUpOrDown(ActionDirection: Direction, ActionKey: Key)
}
///Moves sprite on touch
func moveUpOrDown(ActionDirection: SKAction, ActionKey: String) {
let Line = Lines()
if let sprite = Line.component(ofType: SpriteComponent.self)?.node {
if sprite.action(forKey: ActionKey) == nil {
stopMoving()
let repeatAction = SKAction.repeatForever(ActionDirection)
sprite.run(repeatAction, withKey: ActionKey)
}
}
}
///Stops movement
func stopMoving() {
let Line = Lines()
if let sprite = Line.component(ofType: SpriteComponent.self)?.node {
sprite.removeAllActions()
}
}
I am guessing there is some issue with this line of code Line.component(ofType: SpriteComponent.self)?.node but the compiler doesn't throw any errors and I am not sure where my mistake is.
Any help/guidance will be greatly appreciated!
The issue is the following line in MoveUpOrDown and StopMoving
let Line = Lines()
It's creating a new Lines object then telling it to run an action. Since it's new, it hasn't been added to the scene so it isn't drawn or acted on.
You should be getting an existing Lines object and modifying that instead of creating a new one.
As a side note, the common convention for naming methods and variables is to use camelCase which means MoveUpOrDown should be moveUpOrDown. On the other hand SnakeCase is used For classes structs and protocols so SpriteComponent is current. That allows you to know at a glance whether your working with a type or a variable.

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)
}
}

Move 2 different Spritenodes in the touchesMoved function

i'm trying to make a 2 player game in Swift, so far I can detect multiple touches and move each node individually, however when both players try to move their node at the same time one always takes control and moves both nodes at the same time, the code I have for when both nodes are being touched is the following:
if Touching && Touching2 {
for touch: AnyObject in touches {
let Loc = (touch as! UITouch).location(in: self)
let PrevLoc = (touch as! UITouch).previousLocation(in: self)
let Shell = self.childNode(withName: ShellCategory) as! SKSpriteNode
var NewPos = Shell.position.x + (Loc.x - PrevLoc.x)
NewPos = max(NewPos, Shell.size.width / 2)
NewPos = min(NewPos, self.size.width - Shell.size.width / 2)
Shell.position = CGPoint(x:NewPos, y: Shell.position.y)
let Loc2 = (touch as! UITouch).location(in: self)
let PrevLoc2 = (touch as! UITouch).previousLocation(in: self)
let Shell2 = self.childNode(withName: ShellCategory2) as! SKSpriteNode
var NewPos2 = Shell.position.x + (Loc2.x - PrevLoc2.x)
NewPos2 = max(NewPos2, Shell2.size.width / 2)
NewPos2 = min(NewPos2, self.size.width - Shell2.size.width / 2)
Shell2.position = CGPoint(x:NewPos2, y: Shell2.position.y)
}
}
Thanks beforehand.
Well, after some investigation I finally found what I needed, I'm leaving this here for future developers who might find themselves in similar situations.
Multi-touch gesture in Sprite Kit

ARKit – How to put 3D Object on QRCode?

I'm actually trying to put a 3D Object on QRCode with ARKit
For that I use a AVCaptureDevice to detect a QRCode and establish the area of the QRCode that gives me a CGRect.
Then, I make a hitTest on every point of the CGRect to get the average 3D coordinates like so :
positionGiven = SCNVector3(0, 0, 0)
for column in Int(qrZone.origin.x)...2*Int(qrZone.origin.x + qrZone.width) {
for row in Int(qrZone.origin.y)...2*Int(qrZone.origin.y + qrZone.height) {
for result in sceneView.hitTest(CGPoint(x: CGFloat(column)/2,y:CGFloat(row)/2), types: [.existingPlaneUsingExtent,.featurePoint]) {
positionGiven.x+=result.worldTransform.columns.3.x
positionGiven.y+=result.worldTransform.columns.3.y
positionGiven.z+=result.worldTransform.columns.3.z
cpts += 1
}
}
}
positionGiven.x=positionGiven.x/cpts
positionGiven.y=positionGiven.y/cpts
positionGiven.z=positionGiven.z/cpts
But the hitTest doesn't detect any result and freeze the camera while when I make a hitTest with a touch on screen it works.
Do you have any idea why it's not working ?
Do you have an other idea that can help me to achieve what I want to do ?
I already thought about 3D translation with CoreMotion that can give me the tilt of the device but that seems really tedious.
I also heard about ARWorldAlignmentCamera that can locked the scene coordinate to match the orientation of the camera but I don't know how to use it !
Edit : I try to move my 3D Object every time I touch the screen and the hitTest is positive, and it's pretty accurate ! I really don't understand why hitTest on an area of pixels doesn't work...
Edit 2 : Here is the code of the hitTest who works with 2-5 touches on the screen:
#objc func touch(sender : UITapGestureRecognizer) {
for result in sceneView.hitTest(CGPoint(x: sender.location(in: view).x,y: sender.location(in: view).y), types: [.existingPlaneUsingExtent,.featurePoint]) {
//Pop up message for testing
alert("\(sender.location(in: view))", message: "\(result.worldTransform.columns.3)")
//Moving the 3D Object to the new coordinates
let objectList = sceneView.scene.rootNode.childNodes
for object : SCNNode in objectList {
object.removeFromParentNode()
}
addObject(SCNVector3(result.worldTransform.columns.3.x,result.worldTransform.columns.3.y,result.worldTransform.columns.3.z))
}
}
Edit 3 :
I manage to resolve my problem partially.
I take the transform matrix of the camera (session.currentFrame.camera.transform) so that the object is in front of the camera.
Then I apply a translation on (x,y) with the position of the CGRect.
However i can't translate the z-axis because i don't have enough informations.
And I will probably need a estimation of z coordinate like the hitTest do..
Thanks in advance ! :)
You could use Apple's Vision API to detect the QR code and place an anchor.
To start detecting QR codes, use:
var qrRequests = [VNRequest]()
var detectedDataAnchor: ARAnchor?
var processing = false
func startQrCodeDetection() {
// Create a Barcode Detection Request
let request = VNDetectBarcodesRequest(completionHandler: self.requestHandler)
// Set it to recognize QR code only
request.symbologies = [.QR]
self.qrRequests = [request]
}
In ARSession's didUpdate Frame
public func session(_ session: ARSession, didUpdate frame: ARFrame) {
DispatchQueue.global(qos: .userInitiated).async {
do {
if self.processing {
return
}
self.processing = true
// Create a request handler using the captured image from the ARFrame
let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: frame.capturedImage,
options: [:])
// Process the request
try imageRequestHandler.perform(self.qrRequests)
} catch {
}
}
}
Handle the Vision QR request and trigger the hit test
func requestHandler(request: VNRequest, error: Error?) {
// Get the first result out of the results, if there are any
if let results = request.results, let result = results.first as? VNBarcodeObservation {
guard let payload = result.payloadStringValue else {return}
// Get the bounding box for the bar code and find the center
var rect = result.boundingBox
// Flip coordinates
rect = rect.applying(CGAffineTransform(scaleX: 1, y: -1))
rect = rect.applying(CGAffineTransform(translationX: 0, y: 1))
// Get center
let center = CGPoint(x: rect.midX, y: rect.midY)
DispatchQueue.main.async {
self.hitTestQrCode(center: center)
self.processing = false
}
} else {
self.processing = false
}
}
func hitTestQrCode(center: CGPoint) {
if let hitTestResults = self.latestFrame?.hitTest(center, types: [.featurePoint] ),
let hitTestResult = hitTestResults.first {
if let detectedDataAnchor = self.detectedDataAnchor,
let node = self.sceneView.node(for: detectedDataAnchor) {
let previousQrPosition = node.position
node.transform = SCNMatrix4(hitTestResult.worldTransform)
} else {
// Create an anchor. The node will be created in delegate methods
self.detectedDataAnchor = ARAnchor(transform: hitTestResult.worldTransform)
self.sceneView.session.add(anchor: self.detectedDataAnchor!)
}
}
}
Then handle adding the node when the anchor is added.
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
// If this is our anchor, create a node
if self.detectedDataAnchor?.identifier == anchor.identifier {
let sphere = SCNSphere(radius: 1.0)
sphere.firstMaterial?.diffuse.contents = UIColor.redColor()
let sphereNode = SCNNode(geometry: sphere)
sphereNode.transform = SCNMatrix4(anchor.transform)
return sphereNode
}
return nil
}
Source

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!