Search code examples
swiftsprite-kitskspritenode

Tower defense: turret tracking enemy and shooting issues


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.


Solution

  • 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!).

    enter image description here

    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()
      }
    }