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