I'm a swift newbie trying to loop an A to B positional animation. I'm not sure how to reset the position so the animation can loop. Any help appreciated.
import SpriteKit
class GameScene: SKScene {
let Cloud1 = SKSpriteNode(imageNamed:"Cloud_01.png")
override func didMoveToView(view: SKView) {
view.scene!.anchorPoint = CGPoint(x: 0.5,y: 0.5)
Cloud1.position = CGPoint(x: -800,y: 0)
Cloud1.xScale = 0.5
Cloud1.yScale = 0.5
self.addChild(Cloud1)
//DEFINING SPRITE ACTION & REPEAT
let animateCloud1 = SKAction.moveToX(800, duration: 1.4);
let repeatCloud1 = SKAction.repeatActionForever(animateCloud1)
let group = SKAction.group([ animateCloud1,repeatCloud1]);
//RUNNING ACTION
self.Cloud1.runAction(group);
}
override func update(currentTime: NSTimeInterval) {
if(Cloud1.position.x == 800){
Cloud1.position.x = -800
}
}
}
If I understand your question correctly, you want the Sprite to move back and forth between its current location and the new location you specified.
If so, a way to do this would be to create two animations and put them in a sequence. Then repeat the sequence forever.
let animateCloud = SKAction.moveToX(800, duration: 1.4)
let animateCloudBackwards = SKAction.moveToX(Cloud1.position.x, duration: 0)
// Sequences run each action one after another, whereas groups run
// each action in parallel
let sequence = SKAction.sequence([animateCloud, animateCloudBackwards])
let repeatedSequence = SKAction.repeatActionForever(sequence)
Cloud1.runAction(repeatedSequence)
Related
I place 3d object in the world space. After that I try to move camera randomly. Then right now I need to know after I knew object has became inside frustum by isNode method, if the object is in center, top or bottom of camera view.
For a solution that's not a hack you can use the projectPoint: API.
It's probably better to work with pixel coordinates because this method uses the actual camera's settings to determine where the object appears on screen.
let projectedPoint = sceneView.projectPoint(self.sphereNode.worldPosition)
let xOffset = projectedPoint.x - screenCenter.x;
let yOffset = projectedPoint.y - screenCenter.y;
if xOffset * xOffset + yOffset * yOffset < R_squared {
// inside a disc of radius 'R' at the center of the screen
}
Solution
To achieve this you need to use a trick. Create new SCNCamera, make it a child of pointOfView default camera and set its FoV to approximately 10 degrees.
Then inside renderer(_:updateAtTime:) instance method use isNode(:insideFrustumOf:) method.
Here's working code:
import ARKit
class ViewController: UIViewController,
ARSCNViewDelegate,
SCNSceneRendererDelegate {
#IBOutlet var sceneView: ARSCNView!
#IBOutlet var label: UILabel!
let cameraNode = SCNNode()
let sphereNode = SCNNode()
let config = ARWorldTrackingConfiguration()
public func renderer(_ renderer: SCNSceneRenderer,
updateAtTime time: TimeInterval) {
DispatchQueue.main.async {
if self.sceneView.isNode(self.sphereNode,
insideFrustumOf: self.cameraNode) {
self.label.text = "In the center..."
} else {
self.label.text = "Out OF CENTER"
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.allowsCameraControl = true
let scene = SCNScene()
sceneView.scene = scene
cameraNode.camera = SCNCamera()
cameraNode.camera?.fieldOfView = 10
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.sceneView.pointOfView!.addChildNode(self.cameraNode)
}
sphereNode.geometry = SCNSphere(radius: 0.05)
sphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red
sphereNode.position.z = -1.0
sceneView.scene.rootNode.addChildNode(sphereNode)
sceneView.session.run(config)
}
}
Also, in this solution you may turn on an orthographic projection for child camera, instead of perspective one. It helps when a model is far from the camera.
cameraNode.camera?.usesOrthographicProjection = true
Here's how your screen might look like:
Next steps
The same way you can append two additional SCNCameras, place them above and below central SCNCamera, and test your object with two extra isNode(:insideFrustumOf:) instance methods.
I solved problem with another way:
let results = self.sceneView.hitTest(screenCenter!, options: [SCNHitTestOption.rootNode: parentnode])
where parentnode is the parent of target node, because I have multiple nodes.
func nodeInCenter() -> SCNNode? {
let x = (Int(sceneView.projectPoint(sceneView.pointOfView!.worldPosition).x - sceneView.projectPoint(sphereNode.worldPosition).x) ^^ 2) < 9
let y = (Int(sceneView.projectPoint(sceneView.pointOfView!.worldPosition).y - sceneView.projectPoint(sphereNode.worldPosition).y) ^^ 2) < 9
if x && y {
return node
}
return nil
}
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.
I'm adding a SKErmitterNode from the update() function. I want to remove the SKErmitterNode after 2 seconds, therefore I made a sequence but in the sequence, I can´t add Nodes. And if I add the Node outside of the sequence it gets added over and over again(because I´m doing all of this in the update function) Does someone know a better way to do this?
Here is my Code from the update function:
override func update(_ currentTime: CFTimeInterval) {
if player.position.y <= player.size.height / 2{
self.player.removeFromParent()
if let particles = SKEmitterNode(fileNamed: "MyParticle.sks") {
particles.position = player.position
let addParticle = addChild(particles)
let wait = SKAction.wait(forDuration: 2.0)
let removeParticle = SKAction.removeFromParent()
let particleSequence = SKAction.sequence([addParticle, wait, removeParticle]) //Error ->Cannot convert value of type 'Void' to expected element type 'SKAction'
self.run(SKAction.run(particleSequence))
}
}
So what I recommend for you to do is to create a function like the following
func myExplosion (explosionPosition: CGPoint){
let explosion = SKEmitterNode(fileNamed: "MyParticle")// borrowed this from you
explosion?.position = explosionPosition
explosion?.zPosition = 3
self.addChild(explosion!)
self.run(SKAction.wait(forDuration: 2)){//you can always change the duration to whatever you want
explosion?.removeFromParent()
}
}
then when it is time to use this function, use it like so
myExplosion(explosionPosition: player.position)
Hope this can help you out.
Here's my code:
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let origin = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
let delay = SKAction.waitForDuration(2.0)
for (var i = 0; i < 6; i++) {
//generate a random point on a circle centered at origin with radius of 500
let point = randomPointOnCircle(500, center: origin)
let chanceToSpawn = arc4random_uniform(100)
switch chanceToSpawn {
case 0...60:
self.runAction(delay){
let smallBall = SKSpriteNode(imageNamed: "smallBall”)
self.addChild(smallBall)
smallBall.xScale = 0.05
smallBall.yScale = 0.05
smallBall.position = point
let finalDest = CGPointMake(origin.x, origin.y)
let moveAction = SKAction.moveTo(finalDest, duration: 5.0)
smallBall.runAction(SKAction.sequence([SKAction.waitForDuration(0.1), moveAction, SKAction.runBlock({
smallBall.removeFromParent()
})]))
delay
}
break
case 61...80:
//same as case 0…60, smallBall changed to “mediumBall”
break
case 81...99:
//same as case 0…60, smallBall changed to “largeBall”
break
default:
break
} // ends switch statement
}//ends for loop
}//end didMoveToView
Basically I have balls that spawn on a random point on a circle and move towards the center. But the problem I am having is that no matter how I rearrange things (or rewrite things, i.e., trying different loops or defining functions for spawning the ball and the movement of the ball and simply calling them from inside the switch statement) all of the balls spawn at the same time. I am trying to make it so they spawn one after the other (meaning a ball spawns, the thread waits a few seconds, then spawns another ball.), and I can't seem to figure out why each ball is spawning at the same time as all the other balls.
I'm fairly new to Swift/Spritekit programming and I feel like I'm overlooking some small detail and this is really bugging me. Any help appreciated!
For loops are executed faster than you think! Your for loop is probably executed instantly.
"Why is that? I have already called self.runAction(delay) to delay the for loop!" you asked. Well, the delay action doesn't actually delay the for loop, unfortunately. It just delays the other actions you run on self.
To fix this, try delaying different amounts, dependind on the for loop counter:
self.runAction(SKAction.waitForDuration(i * 2 + 2)) { ... }
This way, balls will appear every two seconds.
try this, may be due to same time u are adding the actions for each balls,
smallBall.runAction(SKAction.sequence([SKAction.waitForDuration(0.1 + (i * 0.35/2)), moveAction, SKAction.runBlock({
smallBall.removeFromParent()
})])) // delay added (0.1 + (increment the daly))
try this
Your problem is you are calling runAction on each iteration of your for loop. This means that all your actions will run concurrently.
What you want to do is create an array of actions. then after the for loop, run a sequence of actions.
(Note: I took the liberty of cleaning up your code)
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let origin = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
let delay = SKAction.waitForDuration(2.0)
var sequence = [SKAction]()
for (var i = 0; i < 6; i++) {
//generate a random point on a circle centered at origin with radius of 500
let point = randomPointOnCircle(500, center: origin)
let chanceToSpawn = arc4random_uniform(100)
var ballname = "smallBall”
switch chanceToSpawn {
case 61...80:
//same as case 0…60, smallBall changed to “mediumBall”
ballname = "mediumBall”
case 81...99:
//same as case 0…60, smallBall changed to “largeBall”
ballname = "largeBall”
default: ()
} // ends switch statement
sequence.append(delay)
sequence.append(
SKAction.run()
{
[unowned self] in
let ball = SKSpriteNode(imageNamed: ballName)
self.addChild(ball)
ball.xScale = 0.05
ball.yScale = 0.05
ball.position = point
let finalDest = CGPointMake(origin.x, origin.y)
let moveAction = SKAction.moveTo(finalDest, duration: 5.0)
let briefWait = SKAction.waitForDuration(0.1)
let remove = SKAction.removeFromParent()
let moveSeq = SKAction.sequence([briefWait ,moveAction,remove])
ball.runAction(moveSeq)
}
)
}//ends for loop
self.run(SKAction.sequence(sequence))
}//end didMoveToView
I am new to swift and looking to build my first basic game. The game I have in mind involves sprites generating at random and then disappearing based on time or a click if the click is within the time allocated. So far I have created the basic framework and am still messing around with design. My problem comes in where I can't seem to remove the sprite based on time (its generating fine). Any help is appreciated and thanks in advance😊
Below is the framework I've built up so far.
import SpriteKit
var one = SKSpriteNode()
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let myFunction = SKAction.runBlock({()in self.addOne()})
let wait = SKAction.waitForDuration(5)
let remove = SKAction.runBlock({() in self.removeOne()})
self.runAction(SKAction.sequence([myFunction, wait, remove]))
}
func addOne() {
let oneTexture = SKTexture(imageNamed: "blue button 10.png")
let one = SKSpriteNode(texture: oneTexture)
one.position = CGPoint(x: CGRectGetMidX(self.frame) - 100, y: CGRectGetMidY(self.frame) + 250)
one.zPosition = 1
self.addChild(one)
}
func removeOne() {
one.removeFromParent()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
It doesn't disappear because your create a new SpiteNode, but try to remove the old one, do it like this:
var one : SKSpriteNode! //instead of creating it without data, just define the type(not necessary, but I would do it)
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let myFunction = SKAction.runBlock({()in self.addOne()})
let wait = SKAction.waitForDuration(5)
let remove = SKAction.runBlock({() in self.removeOne()})
self.runAction(SKAction.sequence([myFunction, wait, remove]))
}
func addOne() {
let oneTexture = SKTexture(imageNamed: "blue button 10.png")
one = SKSpriteNode(texture: oneTexture) //removed the let, so you dont create a new "one"
one.position = CGPoint(x: CGRectGetMidX(self.frame) - 100, y: CGRectGetMidY(self.frame) + 250)
one.zPosition = 1
self.addChild(one)
}
func removeOne() {
one.removeFromParent()
}
}