MoveTo SKAction forever - swift

I have the following code to move an "enemy" to my "players" position.
let follow = SKAction.moveTo(player.position, duration: 2)
enemy.runAction(SKAction.repeatActionForever(action), withKey: "moving")
This code works fine. However, when I move the player, I would like the SKAction to "run again" so that the enemy is always moving towards the last location that the player was at. Therefore, if the player does not keep moving, they will eventually get caught.
How come the "repeatActionForever" is not working? The enemy moves to the initial location of the player, but when you move the player to a new location, the enemy does not move to that new location.
Thanks :D

repeatActionForever is working, it is not dynamic, it will only move to the player at the time the action was created, it does not know anything has changed.
You need to do runBlock for this:
let follow =
SKAction.sequence(
[
SKAction.runBlock(
{
//this will allow the enemy to constantly follow the player
enemy.runAction(SKAction.moveTo(player.position, duration: 2), withKey:"move")
}),
SKAction.waitForDuration(0.1)
]
)
enemy.runAction(SKAction.repeatActionForever(follow), withKey: "moving")
Now the problem with this approach is your enemy speed changes based on the distance of the player. Instead you want to do this:
//duration is expected time to reach the player
let follow = SKAction.customActionWithDuration(duration,
actionBlock:
{
node,elapsedTime in
//change the nodes velocity
//this formula is dependent on gameplay
})
enemy.runAction(SKAction.repeatActionForever(follow), withKey: "moving")

Related

Changing an object's position in Update() prevents the animation from playing

I'm making a turn-based grid-based game with a top-down perspective. Enemies move when the player moves, like a traditional roguelike.
Each character in the game (including the hero and enemies) has a futurePosition attribute that determines where they're supposed to move next. When the player presses a direction key, the hero's futurePosition is set to the direction indicated by the key, then every enemies' futurePosition is updated as well. The code in the Update() function of those characters looks like this :
public virtual void Update() {
if (MustMoveObject()) {
transform.position = Vector3.MoveTowards(transform.position, futurePosition, inverseMoveTime * Time.deltaTime);
} else if (attacking) {
Attack();
futurePosition = Vector3Int.RoundToInt(futurePosition);
}
}
Note: MustMoveObject() returns true if futurePosition and transform.position are not equal
If the game detects there's an enemy in the direction the player is going towards, it will attack it instead. The code looks like this:
MovingObject enemyToAttack = GameManager.instance.CheckForCreatureCurrentPosition(pos);
if (enemyToAttack != null)
{
target = enemyToAttack;
cameraFollow=false;
attacking=true;
animator.SetTrigger("attack");
futurePosition = Vector3.Lerp(transform.position, pos, 0.2f);
} else {
target=null;
cameraFollow=true;
futurePosition = pos;
}
As you can see in the code above, when attacking an enemy, the futurePosition attribute is set at about one fifth (or 20%) of the distance between the hero and the enemy, and "attacking" is set to true. When the player reaches that point in the Update() function, it attacks the enemy and then goes back to its original position. I hope this is clear so far.
Here's my problem : I recently added a test animation for the hero when it attacks, but the animation doesn't start when it should at animator.SetTrigger("attack");. It only activates after the movements of both the hero and the enemies is over. I know that this is the problem because if I replace the line transform.position = Vector3.MoveTowards(transform.position, futurePosition, inverseMoveTime * Time.deltaTime); with transform.position = futurePosition;, it makes the deplacement immediate and the animation triggers instantly. I want the animation to occur while the object is moving towards futurePosition.
Here's what the transition looks like in the inspector (I start from AnyState instead of IdleRight because I just wanted to test things out) :
Screenshot
As you can see, there's no exit time. There's also no delay in the animation itself, I've checked. Everything else is working as expected.
I'm also using Unity version 2019.4.37f1, I'm not sure whether that's relevant.
I hope I made my problem clear and that someone has an idea of what to do. I've searched for a solution, but it didn't seem like anyone had the same issue. If anything, it seemed most people had the opposite problem, where the object wouldn't move as long as the animation was active...
Thanks in advance for your help.
In your animation settings you can toggle loop and the box right under it, also add arguments when you're calling play like that: anim.play("thisAnim", 0, 0.25f) this way it will trigger the animation as soon as you call it, and not after your current anim is finished
Well, I figured it out... It was very dumb.
I needed to check "Has Exit Time" on for the transition between "Any State" and the various Idle States in the inspector. That was literally just it. :/

Why does SKNode() when assigned to a variable spawns an endless horde of enemies?

So I'm making a game where enemies fly past you and despawn when they die or leave the screen, and I was also making a play/pause button, and in order to do so I made a variable called worldNode and assigned it SKNode(). I made the variable inside no function but inside the GameScene class. Then every time I added something, I would do
worldNode.addChild(thing)
Since that allows me to pick and choose what gets paused and unpaused without freezing the whole game. But when I do it to these lines of code it spawns every enemy in at once, immediately killing the player. This code is from the update function.
if currentWave.enemies.isEmpty {
for (index, position) in positions.shuffled().enumerated() {
let enemy = EnemyNode(type: enemyTypes[enemyType], startPosition: CGPoint(x: enemyStartX, y: position), xOffset: enemyOffsetX * CGFloat(index * 3), moveStraight: true)
worldNode.addChild(enemy)
}
} else {
for enemy in currentWave.enemies {
let node = EnemyNode(type: enemyTypes[enemyType], startPosition: CGPoint(x: enemyStartX, y: positions[enemy.position]), xOffset: enemyOffsetX * enemy.xOffset, moveStraight: enemy.moveStraight)
worldNode.addChild(node)
}
}
only for the addChild(node) does this happen, and I can't leave it out because then they keep flying around the screen. What would I do to fix this?

SKAction Sequencing and Grouping Animations

I'm doing some death animations for a game, and wanted to ask for some help. I want my monster to disappear in a puff of smoke, but not before it animates a slash effect going across his body.
I have 3 animations that I want to use:
weaponSlash - a line that draws across the monster. Looks like you slashed him with a sword.
smoke - a puff of smoke that slowly expands out
monsterFalling - the monster falls back, startled
What I want to do is play it in this order:
Simultaneously, the slash appears & the monster starts to fall back
About 0.25s into the above animation, I want the cloud to start to appear
When the cloud is about to end (so maybe after 1s) I want the monster to disappear
Remove the smoke, the monster, the sword, etc, and drop some coins on the ground
I started like this, as a test that works somewhat: (ignore the above times for now)
//Cancel any current actions, like a monster attacking
monster.removeAllActions()
//since you can't play 3 animations on one node at the same time, you have to create 3 separate nodes for everything
let slash = SKSpriteNode()
let cloud = SKSpriteNode()
cloud.size = monster.size
slash.size = monster.size
monster.addChild(cloud)
monster.addChild(slash)
//Start the slash animation
slash.run(self.player.currentlyEquippedWeapon.attackAnimation())
//Start the cloud animation (how I get it is elsewhere and not relevant)
cloud.run(cloudAnimation)
//Run the monster death animation, followed by the cleanup/coin dropping
monster.run(SKAction.sequence([monster.deathAnimation(), SKAction.wait(forDuration: 1), postDeathActions]))
The variable PostDeathActions above simply removes the monster node and animates some coins falling.
WHERE I NEED SOME HELP
So the above code doesn't work so great in that the animations all run independently of each other. Based on this, you can see how regardless of whether the slash/cloud finish, the monster will run two actions: him falling back, followed by cleanup, which just removes the monster and spawns the coins. As you can see I tried to delay this by adding a 1s delay but this is all somewhat of a hack since I may have different monsters or attacks, etc, that are faster/slower. I'd rather guarantee that everything finishes before I despawn the monster.
I tried to group this into an SKAction.Run like so:
let preDeath = SKAction.run {
[unowned self] in
monster.run(monster.deathAnimation()
slash.run(self.player.currentlyEquippedWeapon.attackAnimation())
cloud.run(cloudAnimation)
}
but this runs everything at the same time again.
What I want to do is sequence it like this (pseudo code):
let preDeathAnimations = SKAction.Group([slash, cloud, monsterDeathAnimation])
])
SKAction.sequence([preDeathAnimations, postDeathActions])
So this way it'll run all 3 before running cleanup.
Is there a way to do something like this? I know Sequnce/Group need to be run against an SKNode, but I don't have 3 separate ones.
Thanks for your time reading this and any advice you can offer!
This is one idea that I had, but you could use threading + state + onCompletion blocks to take the math out of it. I didn't test it out fully but this general concept should work:
let slash = SKAction.fadeIn(withDuration: 0.5)
let fall = SKAction.fadeOut(withDuration: 0.25)
let puff = SKAction.fadeIn(withDuration: 0.1)
// Put in ALL of the actions from ALL parties that you want to happen prior to puff:
func findLongestTime(from actions: [SKAction]) -> TimeInterval {
var longestTime = TimeInterval(0)
for action in actions {
if action.duration > longestTime { longestTime = action.duration }
}
// Note, if you put a sequence into this function I don't know if it will work right..
// Might need another func like `findDurationOfSequence(_ sequence: SKAction) -> TimeInterval
return longestTime
}
// Note, if you have the monster doing more than falling prior to puff, then you will
// need to subtract those as well:
let monsterActionsPriorToPuff = [fall]
// Add the duration of all monster animations prior to puff:
var MAPTP_duration = TimeInterval(0)
for action in monsterActionsPriorToPuff {
MAPTP_duration += action.duration
}
// Calculate our final wait time, with no negative numbers:
var waitTime = findLongestTime(from: [slash, fall]) - MAPTP_duration
if waitTime < 0 { waitTime = 0 }
let wait = SKAction.wait(forDuration: waitTime)
// Our monster sequence (I forgot to add the disappear, just add after puff)
let monsterSequence = SKAction.sequence([fall, wait, puff])
// Player slashes:
SKSpriteNode().run(slash)
// Monster will wait 0.25 seconds after falling,
// for slash to finish before puffing:
SKSpriteNode().run(monsterSequence)
et me know if this idea isn't working I can try updating it.

How do run an SKAction on a group of SKSpriteNodes with the same name?

I'm making a game in Xcode 6 using spriteKit and swift. I have a plane on scene, and to make it look like its moving, I'm create clouds off the scene to the left of the screen. I then us an SKAction to move the cloud to the right side of the screen. This works great. You then click to jump off the plane, and the plane moves up off the scene. I then have it start making the clouds on the bottom of the scene, then they move up off the top of the scene, but the problem is, the already existing clouds still have to move to the right side of the screen. My question is, how do I make it so all of the existing clouds stop their action that moves them to the right, then begin to move up exactly where they are? How do I access the group of existing clouds all at the same time once they have been created? I also want the clouds to slow down after you have jumped when you tap the screen to open your parachute, but this should be able to be done by ending the SKAction that moves the clouds, then using another SKAction on them that moves them up slower, but I don't know how to access a group of SKSpriteNodes.
Here is the code that I have to make the clouds:
//This is how the cloud is first declared at the top of the .swift file
var cloud = SKSpriteNode()
//This is the function that runs every certain interval through an NSTimer
func createCloud()
{
cloud = SKSpriteNode(imageNamed: "cloud")
cloud.xScale = 1.25
cloud.yScale = cloud.xScale/2
cloud.zPosition = -1
self.addChild(cloud)
if personDidJump == false
{
let moveRight = SKAction.moveToX(CGRectGetWidth(self.frame) + CGRectGetWidth(cloud.frame), duration: cloudSpeed)
var apple = CGRectGetWidth(self.frame)
var randomNumber:CGFloat = CGFloat(arc4random_uniform(UInt32(apple)))
cloud.position = CGPointMake(-CGRectGetWidth(cloud.frame), randomNumber)
cloud.runAction(moveRight)
}
if personDidJump == true
{
let moveUp = SKAction.moveToY(CGRectGetHeight(self.frame) + CGRectGetWidth(cloud.frame), duration: cloudSpeed)
var apple = CGRectGetWidth(self.frame)
var randomNumber:CGFloat = CGFloat(arc4random_uniform(UInt32(apple)))
cloud.position = CGPointMake(randomNumber, -CGRectGetHeight(cloud.frame))
cloud.runAction(moveUp)
}
}
Also, should I be worried about deleting the clouds when they move off the scene? Or can I just leave them there because you can't see them, and from what I've seen, they don't lag you.
Any help would be greatly appreciated. Thank you.
-Callum-
Put your clouds in an array. When your player jumps (or whenever you need to move them) run through the array and run your action on each cloud.
stackoverflow.com/a/24213529/3402095
^ That is where I found my answere ^
When you make a node give it a name:
myNode.name = "A Node Name"
self.addChild(myNode)
If there are a lot of these nodes, later you can change properties or preform SKAcions on these nodes or do whatever you want to do by using enumerateChildNodesWithName:
self.enumerateChildNodesWithName("A Node Name")
{
myNode, stop in
myNode.runAction(SKAction.moveToY(CGRectGetHeight(self.Frame), duration: 1)
}
I hope this is useful to whoever may need it.

SKSpriteNode with physicsBody auto correct orientation

What would be the best way to make a Swift SKSpriteNode auto correct its orientation after it has had a couple of physicsBody knocks and is now not in the default orientation?
For example I have have a 2D stick-man who gets knocked to the side by another object and would like the stick-man to tilt and reorient itself to the default standing position.
The way I have done this in the past is to wait until the physics body comes to rest, turn off the physics engine for the stickman, reorient the stickman, turn the physics engine back on again.
Write yourself an action called standupAction which animates the stickman's reorientation, maybe just a rotate or a jump up. Then do something like the following.
Sorry this Objective-C not Swift, don't know swift, but should be easy to translate.
if (stickman.physicsBody.resting) {
stickman.physicsBody.dynamic = NO;
[stickman runAction:[SKAction sequence:#[
standupAction,
[SKAction runBlock:^{ stickman.physicsBody.dynamic = YES;}]
]]];
}
The only downside of this approach is that sometimes it can take a full second until the stickman comes comes to rest. Box2d takes quite a long time to settle down. But it looks very realistic if you wait.
Alternatively, you could just check the orientation of stickman and if he is >45degrees off balance then reorient him with impulses. I find impulses to be very difficult to know how much force to apply when you need precise movement like this.
Just to update, I couldn't afford to set the physicsBody to not be dynamic.
My scene updates each node, similarly to Apple's Adventure game, as such I implemented the rotation in the subclassed SKSpriteNode's update method as such:
if (isTouchingGround) {
if self.zRotation != 0 && !rotating {
let actions = [
SKAction.runBlock {
self.rotating = true
},
SKAction.rotateToAngle(0, duration: 1.0),
SKAction.runBlock {
self.rotating = false
}]
runAction(SKAction.sequence(actions))
}
}