Action not working correctly in SpriteKit - ios

I'm new to iOS programing and I'm experimenting to learn trying to create a game in swift using Sprite Kit.
What I'm trying to achieve is having a constant flow of blocks being created and moving rightwards on the screen.
I start by creating a set which contains all the initial blocks, then an action "constant movement" is added to each one, which makes them move slowly to the right. What I'm having trouble is adding new blocks to the screen.
The last column of blocks has an "isLast" boolean set to true, when it passes a certain threshold it is supposed to switch to false and add a new column of blocks to the set which now have "isLast" set to true.
Each block in the set has the "constantMovement" action added which makes them move slowly to the right, the new blocks have it added as well, but they don't work as the original ones.
Not all of the move, even tho if I print "hasActions()" it says they do, and the ones that do move stop doing so when they get to the middle of the screen. I have no idea why this happens, can somebody experienced give me a hint please?
This is the update function:
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
let constantMovement = SKAction.moveByX(-1, y: 0, duration: 10);
background.runAction(SKAction.repeatActionForever(constantMovement));
let removeBlock = SKAction.removeFromParent();
let frame = self.frame;
var currentBlockSprite:SKSpriteNode;
var newBlock: Block;
for block in blocks {
currentBlockSprite = block.sprite!;
currentBlockSprite.runAction(constantMovement);
if(block.column == NumColumns - 1) {
block.isLast = true;
}
if(block.isNew) {
println("position \(currentBlockSprite.position.x) has actions \(currentBlockSprite.hasActions())");
}
if(block.isLast && currentBlockSprite.position.x < frame.maxX - 50) {
println("the block that hits is " + block.description);
println("HITS AT \(currentBlockSprite.position.x)");
block.isLast = false;
for row in 0..<NumRows {
newBlock = Block(column: NumColumns - 1, row: row, blockType: BlockType.random(), isLast: true, isNew: true);
blocks.addElement(newBlock);
addBlockSprite(newBlock);
println("new block: " + newBlock.description + "position \(newBlock.sprite?.position.x)");
}
}
if(currentBlockSprite.position.x < frame.minX) {
currentBlockSprite.runAction(removeBlock);
blocks.removeElement(block);
}
}
}
My whole project is in here: https://github.com/thanniaB/JumpingGame/tree/master/Experimenting
but keep in mind that since I'm new to this it might be full of cringeworthy bad practices.

I would remove any SKAction code from the update function as that's kind of a bad idea. Instead I would just apply the SKAction when you add your block sprite to the scene, like this.
func addBlockSprite(block: Block) {
let blockSprite = SKSpriteNode(imageNamed: "block");
blockSprite.position = pointForColumn(block.column, row:block.row);
if(block.blockType != BlockType.Empty) {
addChild(blockSprite);
let constantMovement = SKAction.moveByX(-10, y: 0, duration: 1)
var currentBlockSprite:SKSpriteNode
let checkPosition = SKAction.runBlock({ () -> Void in
if(blockSprite.position.x < -512){
blockSprite.removeAllActions()
blockSprite.removeFromParent()
}
})
let movementSequence = SKAction.sequence([constantMovement, checkPosition])
let constantlyCheckPosition = SKAction.repeatActionForever(movementSequence)
blockSprite.runAction(constantlyCheckPosition)
}
block.sprite = blockSprite;
}
That would then allow you to simply add a new block whenever you see fit and it will have the appropriate action when it's added.
I've used 512 as thats the size of the iPhone 5 screen but you could swap this out for another screen size or what would be better would be a variable that dynamically reflects the screen size.

Related

SCNView not refreshing but after tap on screen

I am stuck on a problem. I need to apply transformation (scale, rotation, position) right after i add model to my rootNode. Right after when i apply transformation on child model added to rootNode it shows fine on screen but when i apply transformation on rootNode it doesn't refresh. i experimented that as soon i touch screen UI updates. I also tried putting delay of 2,3 secs.
expected
UIView should update as soon i apply transformation to rootNode.
let res = SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0.5, z: 0, duration: 1))
// let res = SCNAction.sequence([SCNAction.wait(duration: 2000), SCNAction.rotateTo(x: CGFloat(180), y: CGFloat(90), z: CGFloat(0), duration: 1.0)])
self.rootNode.runAction(res)
i tried putting code in
RunLoop.main.perform {}
i tried using
scnView.preferredFramesPerSecond = 30
scnView.rendersContinuously = true
But none works. i am using sdk IOS 13.2. Any help please.
Edit:
var rootNode = SCNNode()
viewDidload(){
scnScene.rootNode.addChildNode(rootNode)
....
}
func initSceneWithModel(modelURL: URL) {
do {
try personModel = addModel(url: modelURL)
menuButton.setImage(UIImage.fontAwesomeIcon(name: .bars, style: .solid, textColor: .white, size: XConstants.FONT_AWSOME_SIZE), for: .normal)
selectedModel = personModel
centerPivot(for: personModel!)
moveNodeToCenter(node: personModel!)
setupEyeBlocker()
// selectedModel = eyeBlocker
updateFieldUI()
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.applyInitTransformations()
}
} catch let error {
Utilities.xalert(inView: self.view, desc: error.localizedDescription)
}
}
func applyInitTransformations() {
if let info = vm.physicialFile.extraInfo {
// personModel?.position = info.person.position
// personModel?.scale = info.person.scale
// personModel?.eulerAngles = info.person.rotation
var valueRotPos = SCNMatrix4Mult(SCNMatrix4MakeRotation(0,0,0,0), SCNMatrix4MakeTranslation(0,0,0))
var valueScale = SCNMatrix4MakeScale(7.0,7.0,7.0) // scales to 0.1 of original size
rootNode.transform = SCNMatrix4Mult(valueRotPos, valueScale)
// rootNode.position = info.root.position
// rootNode.scale = info.root.scale
// rootNode.eulerAngles = info.root.rotation
}
else {
applyEyeBlockerDefaultPosition()
}
}
Apple clearly says:
...
You should not modify the transform property of the root node.
...
(https://developer.apple.com/documentation/scenekit/scnscene/1524029-rootnode)
This might be causing the issues you have with your scene. Avoid SCNActions to be run on the rootNode. They are designed to run on the content of the rootNode (any SCNNode added to the rootNode).
You could probably take a common SCNNode, call it like myRootNode, add it to the real rootNode and add all your other content to myRootNode. Transformations should then apply correctly to all your sub-content, if this is your goal.
BTW: scnView.preferredFramesPerSecond = 30 never gave me more performence or any benefits. Leave it default. Scenekit switches automatically to lower framerates if required.
EDIT:
apply transformation like so:
// Precalculate the Rotation the Position and the Scale
var valueRotPos = SCNMatrix4Mult(SCNMatrix4MakeRotation(0,0,0,0), SCNMatrix4MakeTranslation(0,0,0))
var valueScale = SCNMatrix4MakeScale(0.1,0.1,0.1) // scales to 0.1 of original size
then you do:
myRootNode.transform = SCNMatrix4Mult(valueRotPos, valueScale)
(you could also try to use the worldTransform of the node or the other transform properties of the nodes presentation node-object)

Why are my UI elements not resetting correctly after being animated/scaled off screen?

So I'll give as much information about this project as I can right up front. Here is an image of a section of the storyboard that is relevant to the issue:
And here is the flow of the code:
1) A user plays the game. This scrambles up the emoji that are displayed and will eventually hide all of the emoji on the right side.
2) When someone wins the game, it calls
performSegue(withIdentifier: "ShowWinScreenSegue", sender: self)
Which will perform the segue the red arrow is pointing to. This segue is a modal segue, over current content, cross dissolve.
3) Stuff goes on here, and then I try to get back to the game screen so the user can play another game. Here is my current code for that
// self.delegate is the GameController that called the segue
// it's set somewhere else in the code so I can call these reset functions
GameController.gs = GameState()
guard let d = self.delegate else {
return
}
d.resetGameToMatchState()
dismiss(animated: true, completion: {
print("Modal dismiss completed")
GameController.gs = GameState()
self.delegate?.resetGameToMatchState()
})
So here's where the issue is. You can see that I have to call delegate?.resetGameToMatchState() twice for anything to actually happen. If I remove the top one, nothing happens when I call the second one and vice-versa. What makes this so annoying is that the user will see a weird jump where all the ui goes from the old state to the new state because it's updating so late and spastically.
What I've tried so far
So this whole issue has made me really confused on how the UI system works.
My first thought was that maybe the function is trying to update the UI in a thread that's executing too early for the UI thread. So I put the whole body of resetGameToMatchState in a DispatchQueue.main.async call. This didn't do anything.
Then I thought that it was working before because when the WinScreenSegue was being dismissed before (when it was a "show" segue) it was calling GameController's ViewDidAppear. I tried manually calling this function in the dismiss callback, but that didn't work either and feels really hacky.
And now I'm stuck :( Any help would be totally appreciated. Even if it's just a little info that can clear up how the UI system works.
Here is my resetGameToMatchState():
//reset all emoji labels
func resetGameToMatchState() {
DispatchQueue.main.async {
let tier = GameController.gs.tier
var i = 0
for emoji in self.currentEmojiLabels! {
emoji.frame = self.currentEmojiLabelInitFrames[i]
emoji.isHidden = false
emoji.transform = CGAffineTransform(scaleX: 1, y: 1);
i+=1
}
i=0
for emoji in self.goalEmojiLabels! {
emoji.frame = self.goalEmojiLabelInitFrames[i]
emoji.isHidden = false
emoji.transform = CGAffineTransform(scaleX: 1, y: 1);
i+=1
}
//match state
for i in 1...4 {
if GameController.gs.currentEmojis[i] == GameController.gs.goalEmojis[i] {
self.currentEmojiLabels?.findByTag(tag: i)?.isHidden = true
}
}
//reset highlight
let f = self.highlightBarInitFrame
let currentLabel = self.goalEmojiLabels?.findByTag(tag: tier)
let newSize = CGRect(x: f.origin.x, y: (currentLabel?.frame.origin.y)!, width: f.width, height: (currentLabel?.frame.height)! )
self.highlightBarImageView.frame = newSize
//update taps
self.updateTapUI()
//update goal and current emojis to show what the current goal/current selected emoji is
self.updateGoalEmojiLabels()
self.updateCurrentEmojiLabels()
}
}
UPDATE
So I just found this out. The only thing that isn't working when I try to reset the UI is resetting the right side emoji to their original positions. What I do is at the start of the app (in viewDidLoad) I run this:
for emoji in currentEmojiLabels! {
currentEmojiLabelInitFrames.append(emoji.frame)
}
This saves their original positions to be used later. I do this because I animate them to the side of the screen before hiding them.
Now when I want to reset their positions, I do this:
var i = 0
for emoji in self.currentEmojiLabels! {
emoji.frame = self.currentEmojiLabelInitFrames[i]
emoji.isHidden = false
emoji.transform = CGAffineTransform(scaleX: 1, y: 1);
i+=1
}
this should set them all to their original frame and scale, but it doesn't set the position correctly. It DOES reset the scale though. What's weird is that I can see a tiny bit of one of the emoji off to the left of the screen and when they animate, they animate from far off on the left. I'm trying to think of why the frames are so off...
UPDATE 2
So I tried changing the frame reset code to this:
emoji.frame = CGRect(x: 25, y: 25, width: 25, height: 25)
Which I thought should reset them correctly to the top left, but it STILL shoves them off to the left. This should prove that the currentEmojiLabelInitFrames are not the issue and that it has something to do with when I'm setting them. Maybe the constraints are getting reset or messed up?
Your first screen, GameController, should receive a viewWillAppear callback from UIKit when the modal WinScreenController is being dismissed.
So its resetGameToMatchState function could simply set a property to true, then your existing resetGameToMatchState could move into viewWillAppear, checking first if the property is being set.
var resetNeeded: Bool = false
func resetGameToMatchState() {
resetNeeded = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// reset code here
}
TLDR; Reset an element's scale BEFORE resetting it's frame or else it will scale/position incorrectly
Finally figured this out. Here's a bit more background. When an emoji is animated off the screen, this is called:
UIView.animate(withDuration: 1.5, animations: {
let newFrame = self.profileButton.frame
prevLabel?.frame = newFrame
prevLabel?.transform = CGAffineTransform(scaleX: 0.1, y: 0.1);
}) { (finished) in
prevLabel?.isHidden = true
}
So this sets the frame to the top left of the screen and then scales it down. What I didn't realize is that when I want to reset the element, I NEED to set the scale back to normal before setting the frame. Here is the new reset code:
for emoji in self.currentEmojiLabels! {
emoji.transform = CGAffineTransform(scaleX: 1, y: 1) //this needs to be first
emoji.frame = self.currentEmojiLabelInitFrames[i] //this needs to be after the scale
emoji.isHidden = false
i+=1
}

Updating parameter of an SKAction that is repeating forever swift

I have a function that animates a ball in a game that I am designing right now. However, I want the animation speed to change with the balls actual velocity, which I have achieved but only after each iteration of the animation. That makes it come out a little choppy, I am looking for a more elegant solution that could update the timePerFrame argument mid-action. I originally had it set to repeatforever but realized that timePerFrame wouldn't update once the action had started. Is there a way to make this animation speed change more smoothly?
func animBall() {
//This is the general runAction method to make our ball change color
var animSpeedNow = self.updateAnimSpeed()
println(animSpeedNow)
var animBallAction = SKAction.animateWithTextures(ballColorFrames, timePerFrame: animSpeedNow,resize: false, restore: true)
self.runAction(animBallAction, completion: {() -> Void in
self.animBall()
})
}
func updateAnimSpeed() -> NSTimeInterval{
// Update the animation speed based on the velocity to syncrhonize animation with ball velocity
var velocX = self.physicsBody!.velocity.dx
var velocY = self.physicsBody!.velocity.dy
if abs(velocX) > 0 || abs(velocY) > 0 {
var veloc = sqrt(velocX*velocX + velocY*velocY)
var animSpeedNow: NSTimeInterval = NSTimeInterval(35/veloc)
let minAS = NSTimeInterval(0.017)
let maxAS = NSTimeInterval(0.190)
if animSpeedNow < minAS {
return maxAS
}
else if animSpeedNow > maxAS {
return minAS
}
else {
return animSpeedNow
}
}
else {
return NSTimeInterval(0.15)
}
}
If it isn't possible to directly manipulate a parameter of an SKAction that is running forever, I supposed I
In your case, a simpler way would be to remove the old animation action and replace it with a new one.

Keeping an object within the screen. Swift SpriteKit

I am new to swift a Sprite kit. In the app I am trying to make I have a submarine moving through the ocean. Every time the user clicks the screen the gravity starts pulling the sub in the opposite direction. My problem is that i can't find a way to keep the sub from leaving the screen. I have tried to solve it by making a physicsBody around the screen, but the sub still leaves the screen. I have also tried the following code in the updateCurrentTime fund.
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
self.physicsWorld.gravity = CGVectorMake(0,gravity)
if (sub.position.y >= self.size.height - sub.size.height / 2){
sub.position.y = self.size.height - self.sub.size.height / 2
}
if (sub.position.y <= sub.size.height / 2) {
sub.position.y = self.sub.size.height / 2
}
}
But this doesn't do anything either.
Any help would be greatly appreciated!!!!! thanks in advance!
P.S. I can't believe that it is that hard to keep things on the screen!!!
frustrating!
Try SKConstraint - it doesn't require a physicsBody. The code would look something like this, and would constrain the sub sprite to the scene:
let width2 = sub.size.width/2
let height2 = sub.size.height/2
let xRange = SKRange(lowerLimit:0+width2,upperLimit:size.width-width2)
let yRange = SKRange(lowerLimit:0+height2,upperLimit:size.height-height2)
sub.constraints = [SKConstraint.positionX(xRange,Y:yRange)]
Try this in the update:
if sub.frame.maxY >= view!.frame.height {
sub.position.y = view!.frame.height - sub.size.height / 2
sub.physicsBody!.affectedByGravity = false
}
if sub.frame.minY <= 0 {
sub.position.y = sub.size.height / 2
sub.physicsBody!.affectedByGravity = false
}
And then inside of the event where you want to reverse gravity don't forget to do this:
sub.physicsBody!.affectedByGravity = true
Alternatively, instead of using gravity you could use this which is a better option in my opinion:
// This moves the object to the top of the screen
let action = SKAction.moveToY(view!.frame.height - character.size.height / 2, duration: 5.0) // Or however much time you want to the action to run.
action.timingMode = .EaseInEaseOut // Or something else
character.runAction(action)
// Change view!.frame.height - character.size.height / 2 to just character.size.height / 2 to move to the bottom.

Animate object size within GameScenes.swift (Swift)

We need to animate an object's size within GameScene.swift. Other Stack Overflow posts suggest using UIView.animateWithDuration, but this isn't available inside GameScene.swift. We need to animate inside GameScene.swift because we also need access to SKAction to run an action forever.
Right now, we are using the code below, but it is too clunky. The hope is animation will smooth out the appearance of the object as it shrinks.
runAction(SKAction.repeatActionForever(
SKAction.sequence([
SKAction.runBlock(shrinkItem),
SKAction.waitForDuration(0.5)
])
))
func shrinkItem() {
let curWidth = item.size.width
if curWidth < 15 {
return
}
item.size = CGSize( width: CGFloat(item.size.width - 20 ), height: CGFloat(bird.size.height - 20) )
}
What you are trying to do is to shrink your bird, right ? That correspond to scale it down.
Why are you doing it manually inside a runBlock ?
You might want to give a look at the SKAction Class Reference : http://goo.gl/ycPYcF.
Inside which you'll see all the possible actions, and scaleBy:duration: (or another one) might be what you're looking for.
let shrinkAction = SKAction.scaleBy(0.5, duration: 1.0)
let waitAction = SKAction.waitForDuration(0.5)
let sequenceAction = SKAction.sequence([shrinkAction, waitAction])
let repeatAction = SKAction.repeatActionForever(sequenceAction)
self.yourBirdNode.runAction(repeatAction)
Depending on what you'll do next, take note that some action are automatically reversible (through reversedAction) and some aren't.

Resources