I'm working on a small ios game with this joystick library implemented onto it. My issue is after calculating the direction the joystick is going, I want the character to change to a running animation (I implement the animation using an .sks file). It almost works except after the animation has begun, it stops and doesn't finish until the player lets go at the stick. Some of my code is down below. Any help is appreciated.
Function to setup stick:
func setupJoystick() {
addChild(analogJoyStick)
analogJoyStick.trackingHandler = { [unowned self] data in
self.thePlayer.position = CGPoint(x: self.thePlayer.position.x + (data.velocity.x * 0.04), y: self.thePlayer.position.y + (data.velocity.y * 0.04))
let degrees = self.analogJoyStick.data.angular * 360 / (2 * .pi)
if degrees > 0 {
let walkAnimation:SKAction = SKAction(named: "WalkLeft")!
self.thePlayer.run(SKAction.repeatForever(walkAnimation), withKey: "animating")
} else if degrees < 0 {
let walkAnimation:SKAction = SKAction(named: "WalkRight")!
self.thePlayer.run(SKAction.repeatForever(walkAnimation), withKey: "animating")
}
}
analogJoyStick.beginHandler = { [unowned self] in
let degrees = self.analogJoyStick.data.angular * 360 / (2 * .pi)
if degrees > 0 {
let walkAnimation:SKAction = SKAction(named: "WalkLeft")!
self.thePlayer.run(SKAction.repeatForever(walkAnimation), withKey: "animating")
} else if degrees < 0 {
let walkAnimation:SKAction = SKAction(named: "WalkRight")!
self.thePlayer.run(SKAction.repeatForever(walkAnimation), withKey: "animating")
}
}
analogJoyStick.stopHandler = { [unowned self] in
self.thePlayer.removeAction(forKey: "animating")
}
}
Here is a visual of the coding:
Spritekit Demo
I read the joystick library instructions and saw two methods (handlers) you can use:
var beginHandler: (() -> Void)? // before move
var stopHandler: (() -> Void)? // after move
In beginHandler() add the (repeated) walk animation:
let walkAnimation: SKAction = SKAction(named: theAnimation)!
thePlayer.run(SKAction.repeatForever(walkAnimation), withKey: "animating")
And remove the action in stopHandler()
thePlayer.removeAction(forKey: "animating")
Or using the closures (also from the documentation):
joystick.beginHandler = { [unowned self] in
let walkAnimation: SKAction = SKAction(named: ".sks")!
self.thePlayer.run(SKAction.repeatForever(walkAnimation), withKey: "animating")
}
joystick.stopHandler = { [unowned self] in
self.thePlayer.removeAction(forKey: "animating")
}
Related
I need your help guys. I have game scene and func which allow to move camera using panGesture. Also i need pinchGesture to zoom in and out my SKScene. I found some code here, but it lags. Can plz someone help me to improve this code?
`
#objc private func didPinch(_ sender: UIPinchGestureRecognizer) {
guard let camera = self.camera else {return}
if sender.state == .changed {
previousCameraScale = camera.xScale
}
camera.setScale(previousCameraScale * 1 / sender.scale)
sender.scale = 1.0
}
`
try this pinch code.
//pinch -- simple version
#objc func pinch(_ recognizer:UIPinchGestureRecognizer) {
guard let camera = self.camera else { return } // The camera has a weak reference, so test it
if recognizer.state == .changed {
let deltaScale = (recognizer.scale - 1.0)*2
let convertedScale = recognizer.scale - deltaScale
let newScale = camera.xScale*convertedScale
camera.setScale(newScale)
//reset value for next time
recognizer.scale = 1.0
}
}
although i would recommend this slightly more complicated version which centers the pinch around the touch point. makes for a much nicer pinch in my experience.
//pinch around touch point
#objc func pinch(_ recognizer:UIPinchGestureRecognizer) {
guard let camera = self.camera else { return } // The camera has a weak reference, so test it
//cache location prior to scaling
let locationInView = recognizer.location(in: self.view)
let location = self.convertPoint(fromView: locationInView)
if recognizer.state == .changed {
let deltaScale = (recognizer.scale - 1.0)*2
let convertedScale = recognizer.scale - deltaScale
let newScale = camera.xScale*convertedScale
camera.setScale(newScale)
//zoom around touch point rather than center screen
let locationAfterScale = self.convertPoint(fromView: locationInView)
let locationDelta = location - locationAfterScale
let newPoint = camera.position + locationDelta
camera.position = newPoint
//reset value for next time
recognizer.scale = 1.0
}
}
//also need these extensions to add and subtract CGPoints
extension CGPoint {
static func + (a:CGPoint, b:CGPoint) -> CGPoint {
return CGPoint(x: a.x + b.x, y: a.y + b.y)
}
static func - (a:CGPoint, b:CGPoint) -> CGPoint {
return CGPoint(x: a.x - b.x, y: a.y - b.y)
}
}
This Game:
Game Screenshot
When I write this code.. Player disappears..
Variables:
var previousTimeInterval: TimeInterval = 1
var playerIsFacingRight = true
let playerSpeed = 4.0
Code:
extension GameScene {
override func update(_ currentTime: TimeInterval) {
let deltaTime = currentTime - previousTimeInterval
previousTimeInterval = currentTime
//Player Movement
guard let joystickKnob = joystickKnob else { return }
let xPosition = Double(joystickKnob.position.x)
let displacement = CGVector(dx: deltaTime * xPosition * playerSpeed, dy: 0)
let move = SKAction.move(by: displacement, duration: 0)
player?.run(move)
}
}
Game Screenshot
The update method runs many times.
It's highly probably was disappear by soo many actions for player movement.
You can debug inside the update and check player?.position
Check the player?.zPosition of player, if it the same of background can position behind.
You can use Xcode "Debug view hierarchy" to see where is the player.
I'm implementing custom transition using CABasicAnimation and UIView.animate both. Also need to implement a custom interactive transition using UIPercentDrivenInteractiveTransition which exactly copies the behavior of the native iOS swipe back. Animation without a back swipe gesture (when I'm pushing and popping by the back arrow) works fine and smoothly. Moreover, swipe back also works smoothly, except when the gesture velocity is more than 900
Gesture Recognition function:
#objc func handleBackGesture(_ gesture: UIScreenEdgePanGestureRecognizer) {
guard animationTransition != nil else { return }
switch gesture.state {
case .began:
interactionController = TransparentNavigationControllerTransitionInteractor(duration: anumationDuration)
popViewController(animated: true)
case .changed:
guard let view = gesture.view?.superview else { return }
let translation = gesture.translation(in: view)
var percentage = translation.x / view.bounds.size.width
percentage = min(1.0, max(0.0, percentage))
shouldCompleteTransition = percentage > 0.5
interactionController?.update(percentage)
case .cancelled, .failed, .possible:
if let interactionController = self.interactionController {
isInteractiveStarted = false
interactionController.cancel()
}
case .ended:
interactionController?.completionSpeed = 0.999
let greaterThanMaxVelocity = gesture.velocity(in: view).x > 800
let canFinish = shouldCompleteTransition || greaterThanMaxVelocity
canFinish ? interactionController?.finish() : interactionController?.cancel()
interactionController = nil
#unknown default: assertionFailure()
}
}
UIPercentDrivenInteractiveTransition class. Here I'm synchronizing layer animation.
final class TransparentNavigationControllerTransitionInteractor: UIPercentDrivenInteractiveTransition {
// MARK: - Private Properties
private var context: UIViewControllerContextTransitioning?
private var pausedTime: CFTimeInterval = 0
private let animationDuration: TimeInterval
// MARK: - Initialization
init(duration: TimeInterval) {
self.animationDuration = duration * 0.4 // I dk why but layer duration should be less
super.init()
}
// MARK: - Public Methods
override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
super.startInteractiveTransition(transitionContext)
context = transitionContext
pausedTime = transitionContext.containerView.layer.convertTime(CACurrentMediaTime(), from: nil)
transitionContext.containerView.layer.speed = 0
transitionContext.containerView.layer.timeOffset = pausedTime
}
override func finish() {
restart(isFinishing: true)
super.finish()
}
override func cancel() {
restart(isFinishing: false)
super.cancel()
}
override func update(_ percentComplete: CGFloat) {
super.update(percentComplete)
guard let transitionContext = context else { return }
let progress = CGFloat(animationDuration) * percentComplete
transitionContext.containerView.layer.timeOffset = pausedTime + Double(progress)
}
// MARK: - Private Methods
private func restart(isFinishing: Bool) {
guard let transitionLayer = context?.containerView.layer else { return }
transitionLayer.beginTime = transitionLayer.convertTime(CACurrentMediaTime(), from: nil)
transitionLayer.speed = isFinishing ? 1 : -1
}
}
And here is my Dismissal animation function in UIViewControllerAnimatedTransitioning class
private func runDismissAnimationFrom(
_ fromView: UIView,
to toView: UIView,
in transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to) else { return }
toView.frame = toView.frame.offsetBy(dx: -fromView.frame.width / 3, dy: 0)
let toViewFinalFrame = transitionContext.finalFrame(for: toViewController)
let fromViewFinalFrame = fromView.frame.offsetBy(dx: fromView.frame.width, dy: 0)
// Create mask to hide bottom view with sliding
let slidingMask = CAShapeLayer()
let initialMaskPath = UIBezierPath(rect: CGRect(
x: fromView.frame.width / 3,
y: 0,
width: 0,
height: toView.frame.height)
)
let finalMaskPath = UIBezierPath(rect: toViewFinalFrame)
slidingMask.path = initialMaskPath.cgPath
toView.layer.mask = slidingMask
toView.alpha = 0
let slidingAnimation = CABasicAnimation(keyPath: "path")
slidingAnimation.fromValue = initialMaskPath.cgPath
slidingAnimation.toValue = finalMaskPath.cgPath
slidingAnimation.timingFunction = .init(name: .linear)
slidingMask.path = finalMaskPath.cgPath
slidingMask.add(slidingAnimation, forKey: slidingAnimation.keyPath)
UIView.animate(
withDuration: duration,
delay: 0,
options: animationOptions,
animations: {
fromView.frame = fromViewFinalFrame
toView.frame = toViewFinalFrame
toView.alpha = 1
},
completion: { _ in
toView.layer.mask = nil
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
I note that glitch occurs only when a swipe has a grand velocity.
Here a video with the result of smooth animation at normal speed and not smooth at high speed - https://youtu.be/1d-kTPlhNvE
UPD:
I've already tried to use UIViewPropertyAnimator combine with
interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating
But the result is another type of glitching.
I've solved the issue, just change a part of restart function:
transitionLayer.beginTime =
transitionLayer.convertTime(CACurrentMediaTime(), from: nil) - transitionLayer.timeOffset
transitionLayer.speed = 1
I don't really understand why, but looks like timeOffset subtraction works!
I'm having some problems with a SKAction when clicking retry after the player dies. It's the shooting SKAction that I'm having difficulty with. It's shooting well when I run the game but when I click "Retry" to play again only one bullet fires. After that, the SKAction stops. All the other SKActions work when i play again, but not that one.
I know what the problem is. I have all the relevant functions in a func called initialize() that I add in the didMove. But I'm having difficulty calling some of the functions there since the functions have a parameter.
These are the shooting functions I have:
func fireMissile() {
let missile = SKSpriteNode(color: .yellow, size: CGSize(width: 20,
height: 5))
missile.name = "Missile"
missile.position = CGPoint(x: player.position.x + 28, y:
player.position.y + 10)
missile.zPosition = 2
missile.physicsBody = SKPhysicsBody(rectangleOf: missile.size)
missile.physicsBody?.isDynamic = false
missile.physicsBody?.categoryBitMask = ColliderType.Bullet
missile.physicsBody?.collisionBitMask = ColliderType.Enemy |
ColliderType.Boat
missile.physicsBody?.contactTestBitMask = ColliderType.Enemy |
ColliderType.Boat
let missileFlightTime = travelTime(to: missileDestination, from:
player.position, atSpeed: missileSpeed)
missile.zRotation = direction(to: missileDestination, from:
missile.position)
self.addChild(missile)
let missileMove = SKAction.move(to: missileDestination,
duration:
TimeInterval(missileFlightTime))
let missileRemove = SKAction.removeFromParent()
missile.run(SKAction.sequence([missileMove, missileRemove]))
}
func travelTime(to target : CGPoint, from : CGPoint, atSpeed speed :
CGFloat) -> TimeInterval {
let distance = sqrt(pow(abs(target.x - from.x),2) +
pow(abs(target.y - from.y),2))
return TimeInterval(distance/speed)
}
func direction(to target : CGPoint, from: CGPoint) -> CGFloat {
let x = target.x - from.x
let y = target.y - from.y
var angle = atan(y / x)
if x < 0 {
angle = angle + CGFloat.pi
}
return angle
}
And then we have the didMove function:
func initialize() {
score = 0
physicsWorld.contactDelegate = self
createPlayer()
createBG()
createWall()
spawnEnemiesLeft()
spawnEnemiesRight()
spawnBoat()
fireMissile()
createlabel()
}
When i try adding the "func travelTime()" and func "direction()", it requires for me to also add the CGPoint for the "to target" and "from", and I don't know what to type in there, I don't know if thats even the way to go, but it should be.
Here is my code:
func bombTowerTurnShoot() {
var prevDistance:CGFloat = 1000000
var closesetZombie = zombieArray[0]
self.enumerateChildNodes(withName: "bomb tower") {
node, stop in
if self.zombieArray.count > 0 {
for zombie in self.zombieArray {
if let bombTower = node as? SKSpriteNode {
let angle = atan2(closesetZombie.position.x - bombTower.position.x , closesetZombie.position.y - bombTower.position.y)
let actionTurn = SKAction.rotate(toAngle: -(angle - CGFloat(Double.pi/2)), duration: 0.2)
bombTower.run(actionTurn)
let turretBullet = SKSpriteNode(imageNamed: "Level 1 Turret Bullet")
turretBullet.position = bombTower.position
turretBullet.zPosition = 20
turretBullet.size = CGSize(width: 20, height: 20)
//turretBullet.setScale (frame.size.height / 5000)
turretBullet.physicsBody = SKPhysicsBody(circleOfRadius: max(turretBullet.size.width / 2, turretBullet.size.height / 2))
turretBullet.physicsBody?.affectedByGravity = false
turretBullet.physicsBody!.categoryBitMask = PhysicsCategories.Bullet //new contact
turretBullet.physicsBody!.collisionBitMask = PhysicsCategories.None
turretBullet.physicsBody!.contactTestBitMask = PhysicsCategories.Zombie
self.addChild(turretBullet)
var dx = CGFloat(closesetZombie.position.x - bombTower.position.x)
var dy = CGFloat(closesetZombie.position.y - bombTower.position.y)
let magnitude = sqrt(dx * dx + dy * dy)
dx /= magnitude
dy /= magnitude
let vector = CGVector(dx: 4.0 * dx, dy: 4.0 * dy)
func fire () {
turretBullet.physicsBody?.applyImpulse(vector)
}
func deleteBullet() {
turretBullet.removeFromParent()
}
turretBullet.run(SKAction.sequence([SKAction.wait(forDuration: 0), SKAction.run(fire), SKAction.wait(forDuration: 2.0), SKAction.run(deleteBullet) ]))
let distance = hypot(zombie.position.x - bombTower.position.x, zombie.position.y - bombTower.position.y)
if distance < prevDistance {
prevDistance = distance
closesetZombie = zombie
}
}
}
}
}
}
What this code does is turns a turret towards the closest zombie and shoot at it. As far as I can tell the turret is turn towards the closest zombie (if you can tell whether this code actually accomplishes that or not I would like to know). The bigger problem I am having is that the turrets sometimes shoot more than one bullet. I think it is because it is trying to fire at all zombies in the array not the specified one (the closest to the tower). How can I make it so that the turret only shoots the zombie that is closest?
class GameScene: SKScene, SKPhysicsContactDelegate {//new contact
var zombieArray:[SKSpriteNode] = []
...
...
}
And I append all the zombie to the array once they are added and remove them from the array once they die.
Basically, I don't know what you were doing wrong exactly. You had a ton of stuff going on, and trying to figure out the bug would probably have taken longer than rewriting it (for me at least). So that is what I did.
Here is a link to the project on github:
https://github.com/fluidityt/ShootClosestZombie/tree/master
For me, this was all about separating actions into somewhat distinct methods, and separating actions in general from logic.
You had so much going on, it was hard to test / see which parts were working correctly or not. This is where having somewhat smaller methods come in, as well as separating action from logic.. Your action may work fine, but perhaps it's not getting called due to a logic error.
So, how I implemented this was to just make your bomb turret it's own class.. that way we can have the bomb turret be in charge of most of its actions, and then let gameScene handle most of the implementation / and or logic.
The demo I've uploaded shows two turrets that auto-orient themselves to the closest zombie every frame, then shoot at them every second. Click the screen to add more zombies.
The turrets independently track the closest zombie to them so if you spawn a zombie on the left and the right, then the left turret will shoot at left zombie, and right turret will shoot at right zombie (and only once!).
class BombTower: SKSpriteNode {
static let bombName = "bomb tower"
var closestZombie: SKSpriteNode!
func updateClosestZombie() {
let gameScene = (self.scene! as! GameScene)
let zombieArray = gameScene.zombieArray
var prevDistance:CGFloat = 1000000
var closestZombie = zombieArray[0]
for zombie in zombieArray {
let distance = hypot(zombie.position.x - self.position.x, zombie.position.y - self.position.y)
if distance < prevDistance {
prevDistance = distance
closestZombie = zombie
}
}
self.closestZombie = closestZombie
}
func turnTowardsClosestZombie() {
let angle = atan2(closestZombie.position.x - self.position.x , closestZombie.position.y - self.position.y)
let actionTurn = SKAction.rotate(toAngle: -(angle - CGFloat(Double.pi/2)), duration: 0.2)
self.run(actionTurn)
}
private func makeTurretBullet() -> SKSpriteNode {
let turretBullet = SKSpriteNode(imageNamed: "Level 1 Turret Bullet")
turretBullet.position = self.position
turretBullet.zPosition = 20
turretBullet.size = CGSize(width: 20, height: 20)
//turretBullet.setScale (frame.size.height / 5000)
turretBullet.physicsBody = SKPhysicsBody(circleOfRadius: max(turretBullet.size.width / 2, turretBullet.size.height / 2))
turretBullet.physicsBody?.affectedByGravity = false
// turretBullet.physicsBody!.categoryBitMask = PhysicsCategories.Bullet //new contact
// turretBullet.physicsBody!.collisionBitMask = PhysicsCategories.None
// turretBullet.physicsBody!.contactTestBitMask = PhysicsCategories.Zombie
return turretBullet
}
private func fire(turretBullet: SKSpriteNode) {
var dx = CGFloat(closestZombie.position.x - self.position.x)
var dy = CGFloat(closestZombie.position.y - self.position.y)
let magnitude = sqrt(dx * dx + dy * dy)
dx /= magnitude
dy /= magnitude
let vector = CGVector(dx: 4.0 * dx, dy: 4.0 * dy)
turretBullet.physicsBody?.applyImpulse(vector)
}
func addBulletThenShootAtClosestZOmbie() {
let bullet = makeTurretBullet()
scene!.addChild(bullet)
fire(turretBullet: bullet)
}
}
// TODO: delete bullets, hit detection, and add SKConstraint for tracking instead of update.
// Also, I think that we are iterating too much looking for nodes. Should be able to reduce that.
// Also also, there are sure to be bugs if zombieArray is empty.
class GameScene: SKScene {
var zombieArray: [SKSpriteNode] = []
private func makeBombArray() -> [BombTower]? {
guard self.zombieArray.count > 0 else { return nil }
var towerArray: [BombTower] = []
self.enumerateChildNodes(withName: BombTower.bombName) { node, _ in towerArray.append(node as! BombTower) }
guard towerArray.count > 0 else { return nil }
return towerArray
}
private func towersShootEverySecond(towerArray: [BombTower]) {
let action = SKAction.run {
for bombTower in towerArray {
guard bombTower.closestZombie != nil else { continue } // I haven't tested this guard statement yet.
bombTower.addBulletThenShootAtClosestZOmbie()
}
}
self.run(.repeatForever(.sequence([.wait(forDuration: 1), action])))
}
override func didMove(to view: SKView) {
// Demo setup:
removeAllChildren()
makeTestZombie: do {
spawnZombie(at: CGPoint.zero)
}
makeTower1: do {
let tower = BombTower(color: .yellow, size: CGSize(width: 55, height: 55))
let turretGun = SKSpriteNode(color: .gray, size: CGSize(width: 25, height: 15))
turretGun.position.x = tower.frame.maxX + turretGun.size.height/2
tower.name = BombTower.bombName
tower.addChild(turretGun)
addChild(tower)
}
makeTower2: do {
let tower = BombTower(color: .yellow, size: CGSize(width: 55, height: 55))
let turretGun = SKSpriteNode(color: .gray, size: CGSize(width: 25, height: 15))
turretGun.position.x = tower.frame.maxX + turretGun.size.height/2
tower.addChild(turretGun)
tower.position.x += 200
tower.name = BombTower.bombName
addChild(tower)
}
guard let towerArray = makeBombArray() else { fatalError("couldn't make array!") }
towersShootEverySecond(towerArray: towerArray)
}
private func spawnZombie(at location: CGPoint) {
let zombie = SKSpriteNode(color: .blue, size: CGSize(width: 35, height: 50))
zombieArray.append(zombie)
zombie.position = location
zombie.run(.move(by: CGVector(dx: 3000, dy: -3000), duration: 50))
addChild(zombie)
}
// Just change this to touchesBegan for it to work on iOS:
override func mouseDown(with event: NSEvent) {
let location = event.location(in: self)
spawnZombie(at: location)
}
// I think this could be a constrain or action, but I couldn't get either to work right now.
private func keepTowersTrackingNearestZombie() {
guard let towerArray = makeBombArray() else { return }
for tower in towerArray {
tower.updateClosestZombie()
tower.turnTowardsClosestZombie()
}
}
override func update(_ currentTime: TimeInterval) {
keepTowersTrackingNearestZombie()
}
}