Camera follow falling ball - swift

I am trying to implement a scene which consists of a ball falling from a height and bouncing off the floor, i would also like the camera to follow the ball throughout its journey. Bellow is an attempt however the ball does not seem to be affected by gravity as println near the bottom of the code always outputs 100. however in reality the ball is on screen for a brief moment quickly falls off screen. What have i done wrong?
class obj {
var sn_node: SCNNode
init() {
let geom = SCNSphere(radius: 2.0)
sn_node = SCNNode(geometry: geom)
sn_node.position = SCNVector3Make(0, 100, 0)
sn_node.physicsBody = SCNPhysicsBody.dynamicBody()
}
}
var obj = obj()
class SceneKitController: NSViewController,SCNSceneRendererDelegate {
var _scene:SCNScene!
var _cameraNode:SCNNode!
var _floorNode:SCNNode!
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
let sceneView = view as SCNView
sceneView.backgroundColor = NSColor.blackColor()
setupScene()
sceneView.scene = _scene
sceneView.delegate = self
sceneView.jitteringEnabled = true
sceneView.autoenablesDefaultLighting = true
var newPos = obj.sn_node.position
newPos.z += 10
_cameraNode.position = newPos
sceneView.pointOfView = _cameraNode
}
func setupScene() {
_scene = SCNScene()
setupEnviroment()
setupSceneElements()
setupInitial()
}
func setupEnviroment() {
//create main camera
let camera = SCNCamera()
_cameraNode = SCNNode()
_cameraNode.camera = camera
//floor
var floor = SCNFloor()
floor.reflectivity = 0;
_floorNode = SCNNode(geometry: floor)
_floorNode.physicsBody = SCNPhysicsBody.staticBody()
_scene.rootNode.addChildNode(_floorNode)
}
func setupSceneElements() {
}
func setupInitial() {
_scene.rootNode.addChildNode(obj.sn_node)
}
func renderer(aRenderer: SCNSceneRenderer, didSimulatePhysicsAtTime time: NSTimeInterval){
var newPos = obj.sn_node.position
println(newPos.y)
}
}

obj.sn_node.presentatioNode.position will give you what you want.

Related

Detect intersect with masked image (not rectangular)

How to not get detected with intersects func when moving around masked image in UIImageView frame?
explanation image
Code that I am using to detect collision:
movingPerson.frame.intersects(camera.frame)
Where movingPerson is just a regular UIImage that I am moving around with touchesmoved func and camera is a masked Image.
I've tried .bounds instead of .frame but it's not working.
Is there any easy way?
If you want the "Very easy way" then UIDynamicItem and provide a path that bounds your image and let UIDynamicAnimator handle the collisions for you. If you do not know how to get the path for your artwork (yours is pretty easy and you should be able to extract the Bezier coordinates automatically in Paintcode, by hand in Illustrator, or from a SVG file) you can use SpriteKit instead since it will actually generate the bounding polygon for your sprite automatically.
EDIT Sample (note I didn't want to write a whole app for this so I took an existing playground and just added the colliders. the collision works, but the reset after collision doesn't):
import UIKit
import PlaygroundSupport
class ObstacleView: UIView {
override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .path
}
override var collisionBoundingPath: UIBezierPath {
return UIBezierPath(ovalIn: bounds)
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.width / 2
}
}
class ViewController: UIViewController {
var dynamicAnimator: UIDynamicAnimator!
var gravityBehavior: UIGravityBehavior!
var pushBehavior: UIPushBehavior!
var collisionBehavior: UICollisionBehavior!
lazy var player: UILabel = {
let label = UILabel()
label.text = "🐥"
label.sizeToFit()
label.isUserInteractionEnabled = true
label.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(label)
return label
}()
var isPanning = false
var anchor: CGPoint = .zero
let maxDistance = CGFloat(100)
lazy var obstacles: [UIView] = {
return (0..<2).map { index in
let view = ObstacleView(frame: CGRect(x:100 + CGFloat(index)*200,y:200, width: 40, height: 40))
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(view)
return view
}
}()
override func viewDidLoad() {
super.viewDidLoad()
dynamicAnimator = UIDynamicAnimator(referenceView: view)
dynamicAnimator.delegate = self
gravityBehavior = UIGravityBehavior(items: [player])
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(pan))
_ = obstacles
view.addGestureRecognizer(panGestureRecognizer)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
anchor = view.center
player.center = anchor
}
#objc private func pan(recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
guard player.bounds.contains(recognizer.location(in: player)) else {
isPanning = false
return
}
isPanning = true
case .changed:
guard isPanning else {
return
}
player.center = recognizer.location(in: view)
let dx = player.center.x - anchor.x
let dy = player.center.y - anchor.y
let distance = CGFloat(hypotf(Float(dx), Float(dy)))
if distance > maxDistance {
player.center.x = anchor.x + dx / distance * maxDistance
player.center.y = anchor.y + dy / distance * maxDistance
}
case .ended:
guard isPanning else {
return
}
isPanning = false
let dx = player.center.x - anchor.x
let dy = player.center.y - anchor.y
let distance = CGFloat(hypotf(Float(dx), Float(dy)))
guard distance > 10 else {
player.center = anchor
return
}
if pushBehavior != nil {
dynamicAnimator.removeBehavior(pushBehavior)
}
pushBehavior = UIPushBehavior(items: [player], mode: .instantaneous)
pushBehavior.pushDirection = CGVector(dx: -dx, dy: -dy)
pushBehavior.magnitude = distance / maxDistance * 0.75
dynamicAnimator.addBehavior(pushBehavior)
dynamicAnimator.addBehavior(gravityBehavior)
collisionBehavior = UICollisionBehavior(items: [player] + obstacles)
collisionBehavior.translatesReferenceBoundsIntoBoundary = true
collisionBehavior.collisionDelegate = self
dynamicAnimator.addBehavior(collisionBehavior)
case .cancelled:
isPanning = false
player.center = anchor
default:
break
}
}
}
extension ViewController: UIDynamicAnimatorDelegate {
func dynamicAnimatorDidPause(_ animator: UIDynamicAnimator) {
dynamicAnimator.removeAllBehaviors()
player.center = anchor
}
}
extension ViewController: UICollisionBehaviorDelegate {
func collisionBehavior(_ behavior: UICollisionBehavior, beganContactFor item1: UIDynamicItem, with item2: UIDynamicItem, at p: CGPoint) {
print("contact")
}
}
PlaygroundPage.current.liveView = ViewController()
PlaygroundPage.current.needsIndefiniteExecution = true

Smooth object rotation in ARSCNView

I'm trying to make my pan gesture to be as smooth as the jigspace app when rotating 3d objects in AR. Here's what I have right now:
#objc func rotateObject(sender: UIPanGestureRecognizer) {
let sceneView = sender.view as! ARSCNView
var currentAngleY: Float = 0.0
let translation = sender.translation(in: sceneView)
var newAngleY = Float(translation.x)*Float(Double.pi)/180
sceneView.scene.rootNode.enumerateChildNodes { (node, stop) in
if sender.state == .changed {
newAngleY -= currentAngleY
node.eulerAngles.y = newAngleY
} else if sender.state == .ended {
currentAngleY = newAngleY
node.removeAllActions()
}
}
}
There seems to be a delay when I'm using it and I'm trying to figure out how to make the rotation as smooth as possible, again, kinda like jigspace or the Ikea app.
I've also noticed that when I try to rotate the object when it's in a certain angle, it could get quite awkward.
Looking at your rotate object function it seems like some of the logic is not quite right.
Firstly, I believe that the var currentAngleY: Float = 0 should be outside of your function body.
Secondly you should be adding the currentAngleY to the newAngleY variable e.g:
/// Rotates The Models On Their YAxis
///
/// - Parameter gesture: UIPanGestureRecognizer
#objc func rotateModels(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: gesture.view!)
var newAngleY = (Float)(translation.x)*(Float)(Double.pi)/180.0
newAngleY += currentAngleY
DispatchQueue.main.async {
self.sceneView.scene.rootNode.enumerateChildNodes { (node, _) in
node.eulerAngles.y = newAngleY
}
}
if(gesture.state == .ended) { currentAngleY = newAngleY }
}
An example therefore of this in a working context would be like so:
class ViewController: UIViewController {
#IBOutlet var augmentedRealityView: ARSCNView!
var currentAngleY: Float = 0
//-----------------------
// MARK: - View LifeCycle
//-----------------------
override func viewDidLoad() {
super.viewDidLoad()
//1. Generate Our Three Box Nodes
generateBoxNodes()
//2. Create Our Rotation Gesture
let rotateGesture = UIPanGestureRecognizer(target: self, action: #selector(rotateModels(_:)))
self.view.addGestureRecognizer(rotateGesture)
//3. Run The Session
let configuration = ARWorldTrackingConfiguration()
augmentedRealityView.session.run(configuration)
}
//------------------------
// MARK: - Node Generation
//------------------------
/// Generates Three SCNNodes With An SCNBox Geometry
func generateBoxNodes(){
//1. Create An Array Of Colours For Each Face
let colours: [UIColor] = [.red, .green, .blue, .purple, .cyan, .black]
//2. Create An SCNNode Wih An SCNBox Geometry
let boxNode = SCNNode()
let boxGeometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.01)
boxNode.geometry = boxGeometry
//3. Create A Different Material For Each Face
var materials = [SCNMaterial]()
for i in 0..<5{
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = colours[i]
materials.append(faceMaterial)
}
//4. Set The Geometries Materials
boxNode.geometry?.materials = materials
//5. Create Two More Nodes By Cloning The First One
let secondBox = boxNode.flattenedClone()
let thirdBox = boxNode.flattenedClone()
//6. Position Them In A Line & Add To The Scene
boxNode.position = SCNVector3(-0.2, 0, -1.5)
secondBox.position = SCNVector3(0, 0, -1.5)
thirdBox.position = SCNVector3(0.2, 0, -1.5)
self.augmentedRealityView.scene.rootNode.addChildNode(boxNode)
self.augmentedRealityView.scene.rootNode.addChildNode(secondBox)
self.augmentedRealityView.scene.rootNode.addChildNode(thirdBox)
}
//----------------------
// MARK: - Node Rotation
//----------------------
/// Rotates The Models On Their YAxis
///
/// - Parameter gesture: UIPanGestureRecognizer
#objc func rotateModels(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: gesture.view!)
var newAngleY = (Float)(translation.x)*(Float)(Double.pi)/180.0
newAngleY += currentAngleY
DispatchQueue.main.async {
self.augmentedRealityView.scene.rootNode.enumerateChildNodes { (node, _) in
node.eulerAngles.y = newAngleY
}
}
if(gesture.state == .ended) { currentAngleY = newAngleY }
}
}
Hope it helps...

Color keying video with GPUImage on a SCNPlane in ARKit

I am trying to play a video, showing transparency in an ARSCNView. A SCNPlane is used as a projection space for the video and I am trying to color key this video with GPUImage.
I followed this example here. Unfortunately, I have not found a way to project that video back on my videoSpriteKitNode. Because the filter is rendered in a GPUImageView, and the SKVideoNode takes a AVPlayer.
I am not sure if it is possible at all, what I am trying to do, so if anyone could share their insight I'd be very thankful!
import UIKit
import ARKit
import GPUImage
class ARTransVC: UIViewController{
#IBOutlet weak var sceneView: ARSCNView!
let configuration = ARWorldTrackingConfiguration()
var movie: GPUImageMovie!
var filter: GPUImageChromaKeyBlendFilter!
var sourcePicture: GPUImagePicture!
var player = AVPlayer()
var gpuImageView: GPUImageView!
override func viewDidLoad() {
super.viewDidLoad()
self.sceneView.debugOptions = [ARSCNDebugOptions.showWorldOrigin, ARSCNDebugOptions.showFeaturePoints]
self.sceneView.session.run(configuration)
self.gpuImageView = GPUImageView()
self.gpuImageView.translatesAutoresizingMaskIntoConstraints = false
//a delay for ARKit to capture the surroundings
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// A SpriteKit scene to contain the SpriteKit video node
let spriteKitScene = SKScene(size: CGSize(width: self.sceneView.frame.width, height: self.sceneView.frame.height))
spriteKitScene.scaleMode = .aspectFit
// Create a video player, which will be responsible for the playback of the video material
guard let url = Bundle.main.url(forResource: "FY3A4278", withExtension: "mp4") else { return }
let playerItem = AVPlayerItem(url: url)
self.player.replaceCurrentItem(with: playerItem)
//trans
self.filter = GPUImageChromaKeyBlendFilter()
self.filter.thresholdSensitivity = 0.15
self.filter.smoothing = 0.3
self.filter.setColorToReplaceRed(0.322, green: 0.455, blue: 0.831)
self.movie = GPUImageMovie(playerItem: playerItem)
self.movie.playAtActualSpeed = true
self.movie.addTarget(self.filter)
self.movie.startProcessing()
let backgroundImage = UIImage(named: "transparent.png")
self.sourcePicture = GPUImagePicture(image: backgroundImage, smoothlyScaleOutput: true)!
self.sourcePicture.addTarget(self.filter)
self.sourcePicture.processImage()
///HERE DON'T KNOW HOW TO CONTINUE ?
self.filter.addTarget(self.gpuImageView)
// To make the video loop
self.player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(
self,
selector: #selector(ARTransVC.playerItemDidReachEnd),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: self.player.currentItem)
// Create the SpriteKit video node, containing the video player
let videoSpriteKitNode = SKVideoNode(avPlayer: self.player)
videoSpriteKitNode.position = CGPoint(x: spriteKitScene.size.width / 2.0, y: spriteKitScene.size.height / 2.0)
videoSpriteKitNode.size = spriteKitScene.size
videoSpriteKitNode.yScale = -1.0
videoSpriteKitNode.play()
spriteKitScene.addChild(videoSpriteKitNode)
// Create the SceneKit scene
let scene = SCNScene()
self.sceneView.scene = scene
self.sceneView.isPlaying = true
// Create a SceneKit plane and add the SpriteKit scene as its material
let background = SCNPlane(width: CGFloat(1), height: CGFloat(1))
background.firstMaterial?.diffuse.contents = spriteKitScene
let backgroundNode = SCNNode(geometry: background)
backgroundNode.geometry?.firstMaterial?.isDoubleSided = true
backgroundNode.position = SCNVector3(0,0,-2.0)
scene.rootNode.addChildNode(backgroundNode)
}
}
#objc func playerItemDidReachEnd(notification: NSNotification) {
if let playerItem: AVPlayerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: kCMTimeZero, completionHandler: nil)
}
}
}
So, I now managed to chroma-key and play a now transparent video in ARSCNView, but it still is a little patchy solution.
I stepped away from my former approach and implemented ChromaKeyMaterial from Lësha Turkowski!
Here it is, adjusted to the color I wanted to key:
import SceneKit
public class ChromaKeyMaterial: SCNMaterial {
public var backgroundColor: UIColor {
didSet { didSetBackgroundColor() }
}
public var thresholdSensitivity: Float {
didSet { didSetThresholdSensitivity() }
}
public var smoothing: Float {
didSet { didSetSmoothing() }
}
public init(backgroundColor: UIColor = .green, thresholdSensitivity: Float = 0.15, smoothing: Float = 0.0) {
self.backgroundColor = backgroundColor
self.thresholdSensitivity = thresholdSensitivity
self.smoothing = smoothing
super.init()
didSetBackgroundColor()
didSetThresholdSensitivity()
didSetSmoothing()
// chroma key shader is based on GPUImage
// https://github.com/BradLarson/GPUImage/blob/master/framework/Source/GPUImageChromaKeyFilter.m
let surfaceShader =
"""
uniform vec3 c_colorToReplace;
uniform float c_thresholdSensitivity;
uniform float c_smoothing;
#pragma transparent
#pragma body
vec3 textureColor = _surface.diffuse.rgb;
float maskY = 0.2989 * c_colorToReplace.r + 0.5866 * c_colorToReplace.g + 0.1145 * c_colorToReplace.b;
float maskCr = 0.7132 * (c_colorToReplace.r - maskY);
float maskCb = 0.5647 * (c_colorToReplace.b - maskY);
float Y = 0.2989 * textureColor.r + 0.5866 * textureColor.g + 0.1145 * textureColor.b;
float Cr = 0.7132 * (textureColor.r - Y);
float Cb = 0.5647 * (textureColor.b - Y);
float blendValue = smoothstep(c_thresholdSensitivity, c_thresholdSensitivity + c_smoothing, distance(vec2(Cr, Cb), vec2(maskCr, maskCb)));
float a = blendValue;
_surface.transparent.a = a;
"""
//_surface.transparent.a = a;
shaderModifiers = [
.surface: surfaceShader,
]
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//setting background color to be keyed out
private func didSetBackgroundColor() {
//getting pixel from background color
//let rgb = backgroundColor.cgColor.components!.map{Float($0)}
//let vector = SCNVector3(x: rgb[0], y: rgb[1], z: rgb[2])
let vector = SCNVector3(x: 0.216, y: 0.357, z: 0.663)
setValue(vector, forKey: "c_colorToReplace")
}
private func didSetSmoothing() {
setValue(smoothing, forKey: "c_smoothing")
}
private func didSetThresholdSensitivity() {
setValue(thresholdSensitivity, forKey: "c_thresholdSensitivity")
}
}
Here is the code that plays the keyed video in ARKit on a SCNPlane:
import UIKit
import ARKit
class ARTransVC: UIViewController{
#IBOutlet weak var arSceneView: ARSCNView!
let configuration = ARWorldTrackingConfiguration()
private var player: AVPlayer = {
guard let url = Bundle.main.url(forResource: "FY3A4278", withExtension: "mp4") else { fatalError() }
return AVPlayer(url: url)
}()
override func viewDidLoad() {
super.viewDidLoad()
self.arSceneView.debugOptions = [ARSCNDebugOptions.showWorldOrigin, ARSCNDebugOptions.showFeaturePoints]
self.arSceneView.session.run(configuration)
//a delay for ARKit to capture the surroundings
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// A SpriteKit scene to contain the SpriteKit video node
let spriteKitScene = SKScene(size: CGSize(width: self.arSceneView.frame.width, height: self.arSceneView.frame.height))
spriteKitScene.scaleMode = .aspectFit
spriteKitScene.backgroundColor = .clear
spriteKitScene.scaleMode = .aspectFit
//Create the SpriteKit video node, containing the video player
let videoSpriteKitNode = SKVideoNode(avPlayer: self.player)
videoSpriteKitNode.position = CGPoint(x: spriteKitScene.size.width / 2.0, y: spriteKitScene.size.height / 2.0)
videoSpriteKitNode.size = spriteKitScene.size
videoSpriteKitNode.yScale = -1.0
videoSpriteKitNode.play()
spriteKitScene.addChild(videoSpriteKitNode)
// To make the video loop
self.player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(
self,
selector: #selector(ARTransVC.playerItemDidReachEnd),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: self.player.currentItem)
// Create the SceneKit scene
let scene = SCNScene()
self.arSceneView.scene = scene
//Create a SceneKit plane and add the SpriteKit scene as its material
let background = SCNPlane(width: CGFloat(1), height: CGFloat(1))
background.firstMaterial?.diffuse.contents = spriteKitScene
let chromaKeyMaterial = ChromaKeyMaterial()
chromaKeyMaterial.diffuse.contents = self.player
let backgroundNode = SCNNode(geometry: background)
backgroundNode.geometry?.firstMaterial?.isDoubleSided = true
backgroundNode.geometry!.materials = [chromaKeyMaterial]
backgroundNode.position = SCNVector3(0,0,-2.0)
scene.rootNode.addChildNode(backgroundNode)
//video does not start without delaying the player
//playing the video before just results in [SceneKit] Error: Cannot get pixel buffer (CVPixelBufferRef)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.player.seek(to:CMTimeMakeWithSeconds(1, 1000))
self.player.play()
}
}
}
#objc func playerItemDidReachEnd(notification: NSNotification) {
if let playerItem: AVPlayerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: kCMTimeZero, completionHandler: nil)
}
}
I was getting a [SceneKit] Error: Cannot get pixel buffer (CVPixelBufferRef), which was apparently fixed in iOS 11.2. For now, I just found a rather patchy solution were I restarted the video after a one second delay. A better approach for that is greatly appreciated.
try clearing the background and set the scalemode with
backgroundColor = .clear
scaleMode = .aspectFit

Access Geometry Child Node Scenekit

I am trying to access the childnode: boxNode, and then rotate the node using the input from a pan gesture recognizer. How should I access this node so I can manipulate it? Should I declare the cube node in a global scope and modify that way? I am new to Swift so I apologize if this code looks sloppy. I would like to add rotation actions inside of my panGesture() function. Thanks!
import UIKit
import SceneKit
class GameViewController: UIViewController {
var scnView: SCNView!
var scnScene: SCNScene!
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupScene()
}
// override var shouldAutorotate: Bool {
// return true
// }
//
// override var prefersStatusBarHidden: Bool {
// return true
// }
func setupView() {
scnView = self.view as! SCNView
}
func setupScene() {
scnScene = SCNScene()
scnView.scene = scnScene
scnView.backgroundColor = UIColor.blueColor()
initCube()
initLight()
initCamera()
}
func initCube () {
var cubeGeometry:SCNGeometry
cubeGeometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0)
let boxNode = SCNNode(geometry: cubeGeometry)
scnScene.rootNode.addChildNode(boxNode)
}
func initLight () {
let light = SCNLight()
light.type = SCNLightTypeAmbient
let lightNode = SCNNode()
lightNode.light = light
light.castsShadow = false
lightNode.position = SCNVector3(x: 1.5, y: 1.5, z: 1.5)
scnScene.rootNode.addChildNode(lightNode)
}
func initCamera () {
let camera = SCNCamera()
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: 0.0, y: 0.0, z: 5.0)
scnScene.rootNode.addChildNode(cameraNode)
}
func initRecognizer () {
let panTestRecognizer = UIPanGestureRecognizer(target: self, action: #selector(GameViewController.panGesture(_:)))
scnView.addGestureRecognizer(panTestRecognizer)
}
//TAKE GESTURE INPUT AND ROTATE CUBE ACCORDINGLY
func panGesture(sender: UIPanGestureRecognizer) {
//let translation = sender.translationInView(sender.view!)
}
}
First of all you should update your Xcode. It looks like this is an older Swift version you are using. SCNLightTypeAmbient is .ambient in Swift 3.
So the way you should go is by giving your nodes names to identify them:
myChildNode.name = "FooChildBar"
and then you can call on your parent node
let theNodeYouAreLookingFor = parentNode.childNode(withName: "FooChildBar", recursively: true)
You can also use this on your scene's root node
let theNodeYouAreLookingFor = scene.rootNode.childNode(withName: "FooChildBar", recursively: true)
which will look at all nodes in your scene and return the one that you gave the name.

Animation atlas and dynamic physicsBody in Swift 3

I did a small test project using the "Hello World" Sprite-kit template where there is an atlas animation composed by these frames:
-
I want to show this knight and it's animation.
I want to set a DYNAMIC physics body.
So I've used a tool to separated single frames and I did an atlasc folder
so the code should be:
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
var textures : [SKTexture] = [SKTexture]()
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx:0, dy:-2)
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%#%02d",filename,i))
textures.append(genericAtlas.textureNamed(textureName))
}
if textures.count>0 {
knight = SKSpriteNode(texture:textures.first)
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
}
//
self.setPhysics()
let animation = SKAction.animate(with: textures, timePerFrame: 0.15, resize: true, restore: false)
knight.run(animation, withKey:"knight")
}
func setPhysics() {
knight.physicsBody = SKPhysicsBody.init(rectangleOf: knight.size)
knight.physicsBody?.isDynamic = false
}
}
The output is:
As you can see, the physicsBody is STATIC, don't respect the animation: this is normal because during the animation the texture change dimension / size and we don't change the physicsBody that remain the same during the action.
Following the sources there aren't methods that , during SKAction.animate, allow to change the physicsBody.
Although we use :
/**
Creates an compound body that is the union of the bodies used to create it.
*/
public /*not inherited*/ init(bodies: [SKPhysicsBody])
to create bodies for each frame of our animation, these bodies remain all together in the scene creating an ugly bizarre situation like this pic:
So, the correct way to do it should be to intercept frames during animation and change physicsBody on the fly.
We can use also the update() method from SKScene, but I was thinking about an extension.
My idea is to combine the animation action with a SKAction.group, making another custom action that check the execution of the first action, intercept frames that match the current knight.texture with the textures array and change the physicsBody launching an external method, in this case setPhysicsBody.
Then, I've write this one:
extension SKAction {
class func animateWithDynamicPhysicsBody(animate:SKAction, key:String, textures:[SKTexture], duration: TimeInterval, launchMethod: #escaping ()->()) ->SKAction {
let interceptor = SKAction.customAction(withDuration: duration) { node, _ in
if node is SKSpriteNode {
let n = node as! SKSpriteNode
guard n.action(forKey: key) != nil else { return }
if textures.contains(n.texture!) {
let frameNum = textures.index(of: n.texture!)
print("frame number: \(frameNum)")
// Launch a method to change physicBody or do other things with frameNum
launchMethod()
}
}
}
return SKAction.group([animate,interceptor])
}
}
Adding this extension, we change the animation part of the code with:
//
self.setPhysics()
let animation = SKAction.animate(with: textures, timePerFrame: 0.15, resize: true, restore: false)
let interceptor = SKAction.animateWithDynamicPhysicsBody(animate: animation, key: "knight", textures: textures, duration: 60.0, launchMethod: self.setPhysics)
knight.run(interceptor,withKey:"knight")
}
func setPhysics() {
knight.physicsBody = SKPhysicsBody.init(rectangleOf: knight.size)
knight.physicsBody?.isDynamic = false
}
This finally works, the output is:
Do you know a better way, or a more elegant method to obtain this result?
Like I mentioned in the comments, since you are doing boxed physics, add a child SKSpriteNode to your knight that will handle the contacts part of the physics, and just scale based on the knight's frame:
(Note: this is for demo purposes only, I am sure you can come up with a more elegant way to handle this across multiple sprites)
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
var textures : [SKTexture] = [SKTexture]()
private var child = SKSpriteNode(color:.clear,size:CGSize(width:1,height:1))
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx:0, dy:-2)
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%#%02d",filename,i))
textures.append(genericAtlas.textureNamed(textureName))
}
if textures.count>0 {
knight = SKSpriteNode(texture:textures.first)
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
}
//
self.setPhysics()
let animation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: true, restore: false))
knight.run(animation,withKey:"knight")
}
override func didEvaluateActions() {
child.xScale = knight.frame.size.width
child.yScale = knight.frame.size.height
}
func setPhysics() {
child.physicsBody = SKPhysicsBody.init(rectangleOf: child.size)
child.physicsBody?.isDynamic = false
knight.addChild(child)
}
}
To handle texture based bodies. I would write a custom animation to handle it:
extension SKAction
{
static func animate(withPhysicsTextures textures:[(texture:SKTexture,body:SKPhysicsBody)], timePerFrame:TimeInterval ,resize:Bool, restore:Bool) ->SKAction {
var originalTexture : SKTexture!;
let duration = timePerFrame * Double(textures.count);
return SKAction.customAction(withDuration: duration)
{
node,elapsedTime in
guard let sprNode = node as? SKSpriteNode
else
{
assert(false,"animatePhysicsWithTextures only works on members of SKSpriteNode");
return;
}
let index = Int((elapsedTime / CGFloat(duration)) * CGFloat(textures.count))
//If we havent assigned this yet, lets assign it now
if originalTexture == nil
{
originalTexture = sprNode.texture;
}
if(index < textures.count)
{
sprNode.texture = textures[index].texture
sprNode.physicsBody = textures[index].body
}
else if(restore)
{
sprNode.texture = originalTexture;
}
if(resize)
{
sprNode.size = sprNode.texture!.size();
}
}
}
}
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
var textures = [texture:SKTexture,body:SKPhysicsBody]()
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx:0, dy:-2)
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%#%02d",filename,i))
let texture = genericAtlas.textureNamed(textureName)
let body = SKPhysicsBody(texture:texture)
body.isDynamic = false
textures.append((texture:texture,body:body))
}
if textures.count>0 {
knight = SKSpriteNode(texture:textures.first.texture)
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
}
//
let animation = SKAction.animate(withPhysicsTextures: textures, timePerFrame: 0.15, resize: true, restore: false)
knight.run(animation, withKey:"knight")
}
}
Solution for isDynamic = false.
*Update to 2017 below
After days of test maded with my answer, Knight0fDragon answer's and some other ideas came from other SO answers (Confused and Whirlwind suggestions..) I've seen that there is a new problem : physicsBody can't propagate their properties to other bodies
adequately and correctly. In other words copy all properties from a body to another body it's not enough. That's because Apple restrict the access to some methods and properties of the physicsBody original class.
It may happen that when you launch a physicsBody.applyImpulse propagating adequately the velocity, the gravity isn't yet respected correctly. That's really orrible to see..and obviusly that's wrong.
So the main goal is: do not change the physicBody recreating it.In other words DON'T RECREATE IT!
I thought that, instead of creating sprite children, you could create a ghost sprites that do the work instead of the main sprite, and the main sprite takes advantage of the ghost changes but ONLY the main sprite have a physicsBody.
This seems to work!
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
private var ghostKnight:SKSpriteNode!
var textures : [SKTexture] = [SKTexture]()
var lastKnightTexture : SKTexture!
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector.zero
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%#%02d",filename,i))
textures.append(genericAtlas.textureNamed(textureName))
}
if textures.count>0 {
// Prepare the ghost
ghostKnight = SKSpriteNode(texture:textures.first)
addChild(ghostKnight)
ghostKnight.alpha = 0.2
ghostKnight.position = CGPoint(x:self.frame.midX,y:100)
lastKnightTexture = ghostKnight.texture
// Prepare my sprite
knight = SKSpriteNode(texture:textures.first,size:CGSize(width:1,height:1))
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
}
let ghostAnimation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: true, restore: false))
ghostKnight.run(ghostAnimation,withKey:"ghostAnimation")
let animation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: false, restore: false))
knight.run(animation,withKey:"knight")
}
override func didEvaluateActions() {
if ghostKnight.action(forKey: "ghostAnimation") != nil {
if ghostKnight.texture != lastKnightTexture {
setPhysics()
lastKnightTexture = ghostKnight.texture
}
}
}
func setPhysics() {
if let _ = knight.physicsBody{
knight.xScale = ghostKnight.frame.size.width
knight.yScale = ghostKnight.frame.size.height
} else {
knight.physicsBody = SKPhysicsBody.init(rectangleOf: knight.frame.size)
knight.physicsBody?.isDynamic = true
knight.physicsBody?.allowsRotation = false
knight.physicsBody?.affectedByGravity = true
}
}
}
Output:
Obviusly you can hide with alpha set to 0.0 and re-positioning the ghost as you wish to make it disappear.
Update 2017:
After hours of testing I've try to improve the code, finally I managed to remove the ghost sprite but, to work well, one condition is very important: you should not use SKAction.animate with resize in true. This because this method resize the sprites and don't respect the scale (I really don't understand why, hope to some future Apple improvements..). This is the best I've obtain for now:
NO CHILDS
NO OTHER GHOST SPRITES
NO EXTENSION
NO ANIMATE METHOD RECREATED
Code:
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
var textures : [SKTexture] = [SKTexture]()
var lastKnightSize: CGSize!
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector.zero
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%#%02d",filename,i))
textures.append(genericAtlas.textureNamed(textureName))
}
if textures.count>0 {
// Prepare my sprite
knight = SKSpriteNode(texture:textures.first,size:CGSize(width:1,height:1))
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
lastKnightSize = knight.texture?.size()
setPhysics()
}
let animation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: false, restore: false))
knight.run(animation,withKey:"knight")
}
override func didEvaluateActions() {
lastKnightSize = knight.texture?.size()
knight.xScale = lastKnightSize.width
knight.yScale = lastKnightSize.height
}
func setPhysics() {
knight.physicsBody = SKPhysicsBody.init(rectangleOf: knight.frame.size)
knight.physicsBody?.isDynamic = true
knight.physicsBody?.allowsRotation = false
knight.physicsBody?.affectedByGravity = true
}
}
Important detail:
About isDynamic = true that's not possible simply because , during the frequently changes of size, Apple reset also frequently the knight physicsBody but don't apply the inherit of the latest physicsBody properties to the new resetted physicsBody, this is a real shame, you can test it in update printing the knight.physicsBody?.velocity (is always zero but should change due to gravity...). This is probably the reason why Apple recommended to don't scale sprites during physics. To my point of view is a Sprite-kit limitation.
Another idea could be the suggestion about didEvaluateActions() to search a more general method to have a "variable" physicsBody that follow the real current knight texture OR physics settings as a rectangle body like this case:
Update: (thanks to Knight0fDragon and 0x141E interventions)
import SpriteKit
class GameScene: SKScene {
var knight: SKSpriteNode!
var textures : [SKTexture] = [SKTexture]()
var lastKnightTexture : SKTexture!
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx:0, dy:-2)
let plist = "knight.plist"
let genericAtlas = SKTextureAtlas(named:plist)
let filename : String! = NSURL(fileURLWithPath: plist).deletingPathExtension!.lastPathComponent
for i in 0 ..< genericAtlas.textureNames.count
{
let textureName = (String(format:"%#%02d",filename,i))
textures.append(genericAtlas.textureNamed(textureName))
}
if textures.count>0 {
knight = SKSpriteNode(texture:textures.first)
lastKnightTexture = knight.texture
knight.zPosition = 2
addChild(knight)
knight.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
}
let animation = SKAction.repeatForever(SKAction.animate(with: textures, timePerFrame: 0.15, resize: true, restore: false))
knight.run(animation,withKey:"knight")
}
override func didEvaluateActions() {
if knight.action(forKey: "knight") != nil {
if knight.texture != lastKnightTexture {
setPhysics()
lastKnightTexture = knight.texture
}
}
}
func setPhysics() {
knight.physicsBody = SKPhysicsBody.init(rectangleOf: knight.size)
knight.physicsBody?.isDynamic = false
}
}
I am getting ready to dive into some texture physics bodies and ran into this problem a while back.
After reading this I am thinking of an approach like so:
When animating, why not have a base node (center node), and then children that are are each the frames of animation. Then patterns of hiding and unhiding the child nodes, where each child node (which I will be doing) is using the alpha of the texture (all loaded) for their PBs. But just HIDE / UNHIDE (my answer).
I mean how much of a penalty would we incure having 12 nodes per se, of a base node that are the animation frames, versus loading and unloading images for animation.
Any penalty in my mind would be well worth being able to have the variety of various physics bodies.