SCNBox make the front side rotated to the camera in ARKit - swift

I have a simple ARKit app. When the user touches the screen
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let result = sceneView.hitTest(touch.location(in: sceneView), types: [ARHitTestResult.ResultType.featurePoint])
guard let hitResult = result.last else { return }
let hitTrasform = SCNMatrix4(hitResult.worldTransform)
let hitVector = SCNVector3Make(hitTrasform.m41, hitTrasform.m42, hitTrasform.m43)
createBox(position: hitVector)
}
I add a cube
func createBox(position: SCNVector3) {
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 1)
box.firstMaterial?.diffuse.contents = UIColor.black
let sides = [
UIColor.red, // Front
UIColor.black, // Right
UIColor.black, // Back
UIColor.black, // Left
UIColor.black, // Top
UIColor.black // Bottom
]
let materials = sides.map { (side) -> SCNMaterial in
let material = SCNMaterial()
material.diffuse.contents = side
material.locksAmbientWithDiffuse = true
return material
}
box.materials = materials
let boxNode = SCNNode(geometry: box)
boxNode.position = position
sceneView.scene.rootNode.addChildNode(boxNode)
}
When I add it for the first time, I see the front side and a bit of the right side. if I go around the cube on the other side and add another cube, I see the back side of the cube. So, can I do it so that the user sees only the front side whatever how the user goes around.
Thank you.

You can use SCNBillboardConstraint to make a given node face the camera. Just add boxNode.constraints = [SCNBillboardConstraint()] before adding it to the rootNode.

Related

RealityKit – Convert CGPoint to a SIMD<Float>

I am using RealityKit and when I tap on the screen, I just want to add a box on that place where I tapped. It can be on the middle of the screen or anywhere. I will adjust Z axis so the object is placed 1 meter away from me. But I am having a hard time converting CGPoint to relevant RealityKit coordinates.
#objc func handleTap(_ recognizer: UITapGestureRecognizer) {
guard let view = view else { return }
let location = recognizer.location(in: view)
// location to SIMD3<Float> so I can create an anchor
}
Any ideas?
Model on a detected plane
CGPoint is a XY-point on iPhone's screen, so there's no need to convert it to XYZ-point. If you need a 3D point on a detected plane for accomodating your model, all you have to do is to generate an object from ARRaycastResult (or ARHitTestResult in case you're using ARKit+SceneKit).
import RealityKit
import ARKit
var arView = ARView(frame: .zero)
#objc func tappingScreen(_ sender: UITapGestureRecognizer) {
let results: [ARRaycastResult] = arView.raycast(from: arView.center,
allowing: .estimatedPlane,
alignment: .horizontal)
if let result: ARRaycastResult = results.first {
let model = ModelEntity(mesh: .generateSphere(radius: 0.02))
let anchor = AnchorEntity(world: result.worldTransform)
anchor.addChild(model)
arView.scene.anchors.append(anchor)
}
}
Model at camera's position
Creating a model exactly where ARCamera is located in the current frame is super easy:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let model = ModelEntity(mesh: .generateSphere(radius: 0.02))
let anchor = AnchorEntity(world: arView.cameraTransform.matrix)
anchor.addChild(model)
model.position.z = -0.5 // 50 cm offset
arView.scene.anchors.append(anchor)
}
Also, you can multiply camera's transform by desired offset:
var translation = matrix_identity_float4x4
translation.columns.3.z = -0.5 // 50 cm offset
// Multiplying camera matrix by offset matrix
let transform = simd_mul(arView.cameraTransform.matrix, translation)
model.transform.matrix = transform
Additional info
This post is also helpful.

Collision between two nodes not detected ARKit

I created two nodes: a sphere and a box:
var sphere = SCNNode(geometry: SCNSphere(radius: 0.005))
//I get the box node from scn file
let boxScene = SCNScene(named: "art.scnassets/world.scn")!
var boxNode: SCNNode?
I want two nodes or physicsBody's to interact, so I created a category for categoryBitMask and contactTestBitMask:
struct CollisionCategory: OptionSet {
let rawValue: Int
static let box = CollisionCategory(rawValue: 1)
static let sphere = CollisionCategory(rawValue: 2)
}
Here I set the box node as a physics body:
self.boxScene.rootNode.enumerateChildNodes { (node, _) in
if node.name == "box" {
boxNode = node
let boxBodyShape = SCNPhysicsShape(geometry: SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.1), options: nil)
let physicsBody = SCNPhysicsBody(type: .static, shape: boxBodyShape)
boxNode!.physicsBody = physicsBody
boxNode!.physicsBody?.categoryBitMask = CollisionCategory.box.rawValue
boxNode!.physicsBody?.contactTestBitMask = CollisionCategory.sphere.rawValue
boxNode!.physicsBody?.collisionBitMask = boxNode!.physicsBody!.contactTestBitMask
}
}
Here I set the sphere node in the render function, which you can move around the view:
func setUpSphere() {
let sphereBodySphere = SCNPhysicsShape(geometry: SCNSphere(radius: 0.005))
let physicsBody = SCNPhysicsBody(type: .kinematic, shape: sphereBodySphere)
sphere.physicsBody = physicsBody
sphere.physicsBody?.categoryBitMask = CollisionCategory.sphere.rawValue
sphere.physicsBody?.contactTestBitMask = CollisionCategory.box.rawValue
sphere.geometry?.firstMaterial?.diffuse.contents = UIColor.blue
sphere.physicsBody?.collisionBitMask = sphere.physicsBody!.contactTestBitMask
previousPoint = currentPosition
}
///It Adds a sphere and changes his position
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
guard let pointOfView = sceneView.pointOfView else { return }
let mat = pointOfView.transform
let dir = SCNVector3(-1 * mat.m31, -1 * mat.m32, -1 * mat.m33)
let currentPosition = pointOfView.position + (dir * 0.185)
if buttonPressed {
if let previousPoint = previousPoint {
sphere.position = currentPosition
sceneView.scene.rootNode.addChildNode(sphere)
}
}
}
I added the protocol SCNPhysicsContactDelegate to the ViewController,
and I set in ViewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.scene.physicsWorld.contactDelegate = self
///I correctly see the shapes of the sphere and the box physics bodies using
sceneView.debugOptions = .showPhysicsShapes
createBox()
setUpSphere()
sceneView.scene = boxScene
sceneView.scene.physicsWorld.contactDelegate = self
}
Then I added that function:
func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
print("Collision!")
}
This is what happens.
When the two nodes collide nothing happens, so I can't know if the two bodies are touching. Could the problem be about .kinematic, .static or about the function render()?
I followed step by step different tutorials about collisions in ARKit: Tutorial 1, Tutorial 2.
I have no idea why it doesn't work like expected.
Is there something wrong in my code?
Download file code link: https://ufile.io/20sla
willRenderScene is called uptown 60 times a second for every time the scene is going to be rendered. Since you're recreating the physics body every time it's probably messing up the physics engine determine collisions.
Try changing your code to only create the physics body once during setup.

How to detect touch and show new SCNPlane using ARKit?

Now I am able to show different SCNPlane, when card detected. After displaying SCNPlanes, the user touches any plane to show new SCNPlane. But right now touch is working properly but new SCNPlane is not showing.
Here is the code I've tried:
var cake_1_PlaneNode : SCNNode? = nil
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else { return }
if let imageName = imageAnchor.referenceImage.name {
print(imageName)
if imageName == "menu" {
// Check To See The Detected Size Of Our menu Card (Should By 5cm*3cm)
let menuCardWidth = imageAnchor.referenceImage.physicalSize.width
let menuCardHeight = imageAnchor.referenceImage.physicalSize.height
print(
"""
We Have Detected menu Card With Name \(imageName)
\(imageName)'s Width Is \(menuCardWidth)
\(imageName)'s Height Is \(menuCardHeight)
""")
//raspberry
//cake 1
let cake_1_Plane = SCNPlane(width: 0.045, height: 0.045)
cake_1_Plane.firstMaterial?.diffuse.contents = UIImage(named: "france")
cake_1_Plane.cornerRadius = 0.01
let cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
self.cake_1_PlaneNode = cake_1_PlaneNode
cake_1_PlaneNode.eulerAngles.x = -.pi/2
cake_1_PlaneNode.runAction(SCNAction.moveBy(x: 0.15, y: 0, z: -0.125, duration: 0.75))
node.addChildNode(cake_1_PlaneNode)
self.sceneView.scene.rootNode.addChildNode(node)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first as! UITouch
if(touch.view == self.sceneView){
//print("touch working")
let viewTouchLocation:CGPoint = touch.location(in: sceneView)
guard let result = sceneView.hitTest(viewTouchLocation, options: nil).first else {
return
}
if let planeNode = cake_1_PlaneNode, cake_1_PlaneNode == result.node{
print("match")
cake_1()
}
}
}
func cake_1() {
let plane = SCNPlane(width: 0.15 , height: 0.15)
plane.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)
let planeNodee = SCNNode(geometry: plane)
planeNodee.eulerAngles.x = -.pi / 2
planeNodee.runAction(SCNAction.moveBy(x: 0.21, y: 0, z: 0, duration: 0))
} //cake_1
Follow this link: Detect touch on SCNNode in ARKit.
Looking at your code I can see several issues (not to mention the naming conventions for your variables and methods).
Firstly, you are creating a Global Variable which you have declared like so:
var cake_1_PlaneNode : SCNNode? = nil
However you use both a Local and Global Variable for your cake_1_PlaneNode in yourDelegate Callback:
let cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
self.cake_1_PlaneNode = cake_1_PlaneNode
Which should simply read like so:
self.cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
Secondly, you are adding your cake_1_PlaneNode to the rootNode of your ARSCNView rather than your detected ARImageAnchor which is probably what you don't want to do, since when an ARAnchor is detected:
You can provide visual content for the anchor by attaching geometry
(or other SceneKit features) to this node or by adding child nodes.
As such, this method (unless you actually want to do it like this) is unnecessary.
The remaining issues lie within your cake_1 function itself.
Firstly you are not actually adding your planeNodee to your sceneHierachy.
Since you haven't specified whether or not the newly initialised planeNode should be added directly to your ARSCNView or as a childNode of your cake_1_planeNode your function should include one of the following:
self.sceneView.scene.rootNode.addChildNode(planeNodee)
self.cake_1_planeNode.addChildNode(planeNodee)
In addition there is probably also no need to rotate your planeNodee since by default an SCNPlane is rendered vertically.
Since you haven't stipulated where you will be placing your content, it could be that using -.pi / 2 is unnecessary, since this could make it virtually invisible to the naked eye.
One other issue which could also account for you not seeing your node, when you actually add it, is the Z position.
If you set 2 nodes at the same position you will likely experience an issue know as Z-fighting (which you can read more about here). As such you should probably move your added node forward slightly when adding it e.g. SCNVector3 (0,0,0.001)to account for this.
Based on all of these points, I have provided a fully working and commented example below:
import UIKit
import ARKit
//-------------------------
//MARK: - ARSCNViewDelegate
//-------------------------
extension ViewController: ARSCNViewDelegate {
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. Check We Have An ARImageAnchor, Then Get It's Reference Image & Name
guard let imageAnchor = anchor as? ARImageAnchor else { return }
let detectedTarget = imageAnchor.referenceImage
guard let detectedTargetName = detectedTarget.name else { return }
//2. If We Have Detected Our Virtual Menu Then Add The CakeOnePlane
if detectedTargetName == "cakeMenu" {
let cakeOnePlaneGeometry = SCNPlane(width: 0.045, height: 0.045)
cakeOnePlaneGeometry.firstMaterial?.diffuse.contents = UIColor.cyan
cakeOnePlaneGeometry.cornerRadius = 0.01
let cakeOnPlaneNode = SCNNode(geometry: cakeOnePlaneGeometry)
cakeOnPlaneNode.eulerAngles.x = -.pi/2
//3. To Allow Us To Easily Keep Track Our Our Currently Added Node We Will Assign It A Unique Name
cakeOnPlaneNode.name = "Strawberry Cake"
node.addChildNode(cakeOnPlaneNode)
cakeOnPlaneNode.runAction(SCNAction.moveBy(x: 0.15, y: 0, z: 0, duration: 0.75))
}
}
}
class ViewController: UIViewController {
#IBOutlet var augmentedRealityView: ARSCNView!
let augmentedRealitySession = ARSession()
let configuration = ARWorldTrackingConfiguration()
//------------------
//MARK: - Life Cycle
//------------------
override func viewDidLoad() {
super.viewDidLoad()
setupARSession()
}
//-----------------
//MARK: - ARSession
//-----------------
/// Runs The ARSession
func setupARSession(){
//1. Load Our Detection Images
guard let detectionImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else { return }
//2. Configure & Run Our ARSession
augmentedRealityView.session = augmentedRealitySession
augmentedRealityView.delegate = self
configuration.detectionImages = detectionImages
augmentedRealitySession.run(configuration, options: [.resetTracking, .removeExistingAnchors])
}
//--------------------
//MARK: - Interaction
//--------------------
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//1. Get The Current Touch Location & Perform An ARSCNHitTest To Check For Any Hit SCNNode's
guard let currentTouchLocation = touches.first?.location(in: self.augmentedRealityView),
let hitTestNode = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first?.node else { return }
//2. If We Have Hit Our Strawberry Cake Then We Call Our makeCakeOnNode Function
if let cakeID = hitTestNode.name, cakeID == "Strawberry Cake"{
makeCakeOnNode(hitTestNode)
}
}
/// Adds An SCNPlane To A Detected Cake Target
///
/// - Parameter node: SCNNode
func makeCakeOnNode(_ node: SCNNode){
let planeGeometry = SCNPlane(width: 0.15 , height: 0.15)
planeGeometry.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)
let planeNode = SCNNode(geometry: planeGeometry)
planeNode.position = SCNVector3(0, 0, 0.001)
planeNode.runAction(SCNAction.moveBy(x: 0.21, y: 0, z: 0, duration: 0))
node.addChildNode(planeNode)
}
}
Which yields the following on my device:
For your information, this seems to show that your calculations for placing your content are off (unless of course this is the desired result).
As you can see, all of content rendered correctly, however the spacing of these was quite large, and as such you will likely need to pan your device somewhat to see it all when testing and developing further.
Hope it helps...
***Please use descriptive and clear names for your variables and functions, it is very hard to read and understand your code. You can read more about swift styling guidelines here: https://github.com/raywenderlich/swift-style-guide#naming
You are creating a new plane when the user touches the screen, but you are not adding that plane to the scene, therefore your "cake_1()" function only creates a new plane.
When ARKit detects an image, it automatically creates an empty node and adds it to our scene, at the center of the detected image. We must first keep a reference to the node ARKit has added for us when the image is detected.
Add this variable to the top of your class:
var detectedImageNode: SCNNode?
Then in func renderer(renderer: didAdd node:, for anchor:) add the following line:
detectedImageNode = node
Now that we have a reference to the node, we can easily add and remove other nodes.
Add the following line at the end of cake_1():
if let detectedImageNode = detectedImageNode {
cake_1_PlaneNode?.removeFromParentNode()
detectedImageNode.addChildNode(planeNodee)
}
Your final code should look like this:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else { return }
if let imageName = imageAnchor.referenceImage.name {
print(imageName)
if imageName == "menu" {
let cake_1_Plane = SCNPlane(width: 0.045, height: 0.045)
cake_1_Plane.firstMaterial?.diffuse.contents = UIImage(named: "france")
cake_1_Plane.cornerRadius = 0.01
let cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
self.cake_1_PlaneNode = cake_1_PlaneNode
cake_1_PlaneNode.eulerAngles.x = -.pi/2
cake_1_PlaneNode.runAction(SCNAction.moveBy(x: 0.15, y: 0, z: -0.125, duration: 0.75))
node.addChildNode(cake_1_PlaneNode)
// No need to add the following line. The node is already added to the scene
//self.sceneView.scene.rootNode.addChildNode(node)
detectedImageNode = node
}
}
}
func cake_1() {
let plane = SCNPlane(width: 0.15 , height: 0.15)
plane.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)
let planeNodee = SCNNode(geometry: plane)
planeNodee.eulerAngles.x = -.pi / 2
if let detectedImageNode = detectedImageNode {
cake_1_PlaneNode?.removeFromParentNode()
detectedImageNode.addChildNode(planeNodee)
}
}
Alternative solution
If you are just trying to change the image of the plane then an easier way to approach this is to just change the texture of the plane.
Replace the contents of cake_1() with:
if let planeGeometry = cake_1_PlaneNode?.geometry {
planeGeometry.firstMaterial?.diffuse.contents = UIImage(named: "newImage")
}

Swift: ARKit place a rectangle based on 2 nodes

My idea is that i want to place 2 sphere nodes at selected locations. From that point i basically want to draw a rectangle that will adjust the height with a slider. Basically that means that the 2 spheres will represent all 4 corners in the beginning. But when testing the code i use 1 meter height as a test.
The problem i have is that i cant seem to place the rectangle at the correct location as illustrated in the image below:
the rectangle in the image has a higher y-point than the points, it's rotated slightly and its center point is above node 2 and not in between the nodes. I don't want to use node.rotation as it wont work dynamically.
This is the code i use to place the 2 nodes and draw + add the rectangle.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let location = touches.first?.location(in: sceneView) else {return}
let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])
guard let result = hitTest.last else {return}
// Converts the matrix_float4x4 to an SCNMatrix4 to be used with SceneKit
let transform = SCNMatrix4.init(result.worldTransform)
// Creates an SCNVector3 with certain indexes in the matrix
let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)
let node = addSphere(withPosition: vector)
nodeArray.append(node)
sceneView.scene.rootNode.addChildNode(node)
if nodeArray.count == 2 {
let node1 = sceneView.scene.rootNode.childNodes[0]
let node2 = sceneView.scene.rootNode.childNodes[1]
let bezeierPath = UIBezierPath()
bezeierPath.lineWidth = 0.01
bezeierPath.move(to: CGPoint(x: CGFloat(node1.position.x), y: CGFloat(node1.position.y)))
bezeierPath.addLine(to: CGPoint(x: CGFloat(node2.position.x), y: CGFloat(node2.position.y)))
bezeierPath.addLine(to: CGPoint(x: CGFloat(node2.position.x), y: CGFloat(node2.position.y+1.0)))
bezeierPath.addLine(to: CGPoint(x: CGFloat(node1.position.x), y: CGFloat(node1.position.y+1.0)))
bezeierPath.close()
bezeierPath.fill()
let shape = SCNShape(path: bezeierPath, extrusionDepth: 0.02)
shape.firstMaterial?.diffuse.contents = UIColor.red.withAlphaComponent(0.8)
let node = SCNNode.init(geometry: shape)
node.position = SCNVector3(CGFloat(abs(node1.position.x-node2.position.x)/2), CGFloat(abs((node1.position.y)-(node2.position.y))/2), CGFloat(node1.position.z))
sceneView.scene.rootNode.addChildNode(node)
}
}
Also note that this is not my final code. It will be refactored once i get everything working :).
This is currently working. I used the wrong calculation to set it in the middle of the 2 points.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check if a window is placed already and fetch location in sceneView
guard debugMode, let location = touches.first?.location(in: sceneView) else {return}
// Fetch targets at the current location
let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])
guard let result = hitTest.last else {return}
// Create sphere are position. getPosition() is an extension
createSphere(withPosition: result.getPosition())
// When 2 nodes has been placed, create a window and hide the spheres
if nodeArray.count == 2 {
let firstNode = nodeArray[nodeArray.count-1]
let secondNode = nodeArray[nodeArray.count-2]
firstNode.isHidden = true
secondNode.isHidden = true
createWindow(firstNode: firstNode, secondNode: secondNode)
sceneView.debugOptions = []
}
}
func createSphere(withPosition position: SCNVector3) {
// Create an geometry which you will apply materials to and convert to a node
let object = SCNSphere(radius: 0.01)
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
object.materials = [material]
let node = SCNNode(geometry: object)
node.position = position
nodeArray.append(node)
sceneView.scene.rootNode.addChildNode(node)
}
func createWindow(firstNode: SCNNode, secondNode: SCNNode) {
// Create an geometry which you will apply materials to and convert to a node
let shape = SCNBox(width: CGFloat(abs(firstNode.worldPosition.x-secondNode.worldPosition.x)), height: 0.01, length: 0.02, chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor.red.withAlphaComponent(0.8)
// Each side of a cube can have different materials
// https://stackoverflow.com/questions/27509092/scnbox-different-colour-or-texture-on-each-face
shape.materials = [material]
let node = SCNNode(geometry: shape)
let minX = min(firstNode.worldPosition.x, secondNode.worldPosition.x)
let maxX = max(firstNode.worldPosition.x, secondNode.worldPosition.x)
// The nodes pivot Y-axis should be split in half of the nodes height, this will cause the node to
// only scale in one direction, when scaling it
// https://stackoverflow.com/questions/42568420/scenekit-understanding-the-pivot-property-of-scnnode/42613002#42613002
node.position = SCNVector3(((maxX-minX)/2)+minX, firstNode.worldPosition.y, firstNode.worldPosition.z)
node.pivot = SCNMatrix4MakeTranslation(0, 0.005, 0)
node.scale = SCNVector3Make(1, -50, 1)
node.name = "window"
sceneView.scene.rootNode.addChildNode(node)
}
Also added:
#objc func resetSceneView() {
// Clear all the nodes
sceneView.scene.rootNode.enumerateHierarchy { (node, stop) in
node.childNodes.forEach({
$0.removeFromParentNode()
})
node.removeFromParentNode()
}
// Reset the session
sceneView.session.pause()
sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints]
sceneView.session.run(configuration, options: .resetTracking)
let matrix = sceneView.session.currentFrame!.camera.transform
sceneView.session.setWorldOrigin(relativeTransform: matrix)
nodeArray = []
}
To make sure that the world alignment is reset to your camera position

How do I rotate a colour sprite by moving finger along y-axis in the BOTTOM HALF of the screen?

Code:
for touch in touches{
let location = touch.location(in: self)
let maxTouchHeight = self.frame.midY
if currentGameType == .wobble{
main.zRotation = (location.y)/512
if location.y > maxTouchHeight { return }
}
}
}
So I am trying to rotate a paddle depending on where your finger is on the y-axis in a type of pong game. However, I have been unable to make it so that it only works in the bottom half of the screen i.e. the two extremities of rotation are at the very top and very bottom of the screen but I want the top extremity to be at the top of the bottom half of the screen. (main is a pong paddle).
Any help is appreciated :)
After followed discussion from comments and repeated answer updates, I have a solution for you. Please let me know if questions:
class GameScene: SKScene {
let paddle = SKSpriteNode()
let maximumRotation = CGFloat(45)
override func didMove(to view: SKView) {
paddle.color = .blue
paddle.size = CGSize(width: 200, height: 15)
let deg45 = CGFloat(0.785398)
let degNeg45 = -deg45
let constraint = [SKConstraint.zRotation(SKRange(lowerLimit: degNeg45, upperLimit: deg45))]
paddle.constraints = constraint
addChild(paddle)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let location = touches.first?.location(in: self) else { return }
guard let lastLocation = touches.first?.previousLocation(in: self) else { return }
guard location.y < self.frame.midY else { return }
// Our vertical plots:
let yVal = location.y
let lastY = lastLocation.y
// How much we moved in a certain direction this frame:
let deltaY = yVal - lastY
// This value represents 100% of a 45deg angle:
let oneHundredPercent = self.frame.height/2
assert(oneHundredPercent != 0)
// The % of 100%Val (45degrees) that we moved (in radians):
let absY = abs(deltaY)
let radToDegFactor = CGFloat(0.01745329252)
let multiplier = (absY / oneHundredPercent) * radToDegFactor
// I suggest a sensitivity of 2-4:
let sensitivity = CGFloat(3)
let amountToRotate = maximumRotation * (multiplier * sensitivity)
// Rotate the correct amount in the correct direction:
if deltaY > 0 {
// Rotate counter-clockwise:
paddle.run(.rotate(byAngle: amountToRotate, duration: 0))
} else {
// Rotate clockwise:
paddle.run(.rotate(byAngle: -amountToRotate, duration: 0))
}
}
}
Just open up a new project and try it out!
There may be an easier way to do this, but this is what popped in my head. It's a little verbose too so hopefully you can see each process step-by-step.
As far as doing something using the bottom half only, you can do something like this
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let fingerPosition = touch.location(in: self)
//this is the bottom
if fingerPosition.y < self.size.height/2 {
//just replace print with your own code
print("bottom half")
}
//this is the top half
if fingerPosition.y > self.size.width/2 {
//you can just leave this as it is or if ever you want to do something with the top part, just add your own code!
print("not bottom half")
}
}
}
Hopefully this helps.
put this inside of your touches functions:
for touch in touches{
let location = touch.location(in: self)
let maxTouchHeight = self.frame.midY
if location.y > maxTouchHeight { return }
if currentGameType == .wobble{
main.zRotation = (location.y)/512
}
}