Skip to content

Commit

Permalink
Add Auto Camera (#26)
Browse files Browse the repository at this point in the history
* initial failure to add auto-camera, need to rethink some UI stuff

* run swiftformat

* super basic implementation of auto-camera implemented

* reset default birthrate

* implement auto camera in mac and screensaver builds

* refactor most camera logic into a new class

* set up autocamera to track a body node throughout the tank

* adjust camera body movement speed

* run swiftformat

* fix main interface not coming back if disabling auto-camera by tank touch

* add more zoom levels and remove some unneeded code

* get auto-zoom working

* adjust debug log lines

* change minimum zoom level for auto-camera

* fix bug where camera body is not present in scene after creating a new tank

* cleanup

* fix mac/screensaver version

* fix tvos build

* fix some auto-camera related bugs

UI not being dismissed when deselecting creature
auto-camera snapping to a distant body when enabled
zoom not functioning correctly when selecting a creature when in auto-camera mode
auto-camera mode doesn't enable when zoomed on a specific creature

* fix bugs in tvos interface

* run swiftformat
  • Loading branch information
amiantos committed Feb 16, 2020
1 parent 5f5d6c6 commit 5494a06
Show file tree
Hide file tree
Showing 13 changed files with 429 additions and 95 deletions.
8 changes: 8 additions & 0 deletions Aeon Garden.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
22 changes: 21 additions & 1 deletion App/Aeon Garden.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
447C8BB52347C47C00DC7971 /* Structs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447C8BB22347C47700DC7971 /* Structs.swift */; };
447C8BB82347F72400DC7971 /* AeonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447C8BB72347F72400DC7971 /* AeonViewModel.swift */; };
447C8BB92347F72400DC7971 /* AeonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447C8BB72347F72400DC7971 /* AeonViewModel.swift */; };
4497925623F0904400FC2949 /* AeonCameraNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4497925523F0904400FC2949 /* AeonCameraNode.swift */; };
4497925723F0904400FC2949 /* AeonCameraNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4497925523F0904400FC2949 /* AeonCameraNode.swift */; };
4497925823F0904400FC2949 /* AeonCameraNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4497925523F0904400FC2949 /* AeonCameraNode.swift */; };
4497925923F0904400FC2949 /* AeonCameraNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4497925523F0904400FC2949 /* AeonCameraNode.swift */; };
4497925B23F0AF9D00FC2949 /* AeonCameraBodyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4497925A23F0AF9D00FC2949 /* AeonCameraBodyNode.swift */; };
4497925C23F0AF9D00FC2949 /* AeonCameraBodyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4497925A23F0AF9D00FC2949 /* AeonCameraBodyNode.swift */; };
4497925D23F0AF9D00FC2949 /* AeonCameraBodyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4497925A23F0AF9D00FC2949 /* AeonCameraBodyNode.swift */; };
4497925E23F0AF9D00FC2949 /* AeonCameraBodyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4497925A23F0AF9D00FC2949 /* AeonCameraBodyNode.swift */; };
449FF757226FD92900704D7C /* AeonFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449FF756226FD92900704D7C /* AeonFonts.swift */; };
449FF758226FD92900704D7C /* AeonFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449FF756226FD92900704D7C /* AeonFonts.swift */; };
44D73684227A68FF006A25B6 /* AeonButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B2D1F2226BD9A40050F14C /* AeonButton.swift */; };
Expand Down Expand Up @@ -225,6 +233,8 @@
44868F652231C7F5009F0460 /* AeonLimbNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeonLimbNode.swift; sourceTree = "<group>"; };
44868F672231E855009F0460 /* AeonUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeonUtils.swift; sourceTree = "<group>"; };
44868F6A223346E3009F0460 /* AeonCreatureBrainStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeonCreatureBrainStates.swift; sourceTree = "<group>"; };
4497925523F0904400FC2949 /* AeonCameraNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeonCameraNode.swift; sourceTree = "<group>"; };
4497925A23F0AF9D00FC2949 /* AeonCameraBodyNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeonCameraBodyNode.swift; sourceTree = "<group>"; };
449FF756226FD92900704D7C /* AeonFonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeonFonts.swift; sourceTree = "<group>"; };
44EC7E2B2282591A00F2BA3E /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
55106F82B8367E59A3F6FC58 /* Pods_Aeon_Garden.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Aeon_Garden.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -548,11 +558,13 @@
B4DEED3522470E400026604C /* Nodes */ = {
isa = PBXGroup;
children = (
B4DEED37224806850026604C /* AeonNodeProtocols.swift */,
44868F5F2230B6BA009F0460 /* AeonCreatureNode */,
B48DDACF1F81A1DE00DEA908 /* AeonFoodNode.swift */,
B4DEED37224806850026604C /* AeonNodeProtocols.swift */,
B4DEED39224817D80026604C /* AeonBubbleNode.swift */,
4431A05E22AB1CC900865489 /* AeonTextures.swift */,
4497925523F0904400FC2949 /* AeonCameraNode.swift */,
4497925A23F0AF9D00FC2949 /* AeonCameraBodyNode.swift */,
);
name = Nodes;
path = Core/Nodes;
Expand Down Expand Up @@ -895,8 +907,10 @@
4456E3BE234D776600A9E401 /* Logging.swift in Sources */,
B409E59B227E3A3300546184 /* AeonFoodNode.swift in Sources */,
B46CA6B8227F8F0800FAAAA6 /* AeonAssetGrabber.swift in Sources */,
4497925923F0904400FC2949 /* AeonCameraNode.swift in Sources */,
4431A06222AB1CC900865489 /* AeonTextures.swift in Sources */,
B409E59A227E3A3300546184 /* AeonCreatureBrainStates.swift in Sources */,
4497925E23F0AF9D00FC2949 /* AeonCameraBodyNode.swift in Sources */,
B409E59D227E3A3300546184 /* AeonBubbleNode.swift in Sources */,
B409E597227E3A3300546184 /* AeonCreatureNode.swift in Sources */,
B409E594227E3A2000546184 /* AeonColors.swift in Sources */,
Expand Down Expand Up @@ -945,7 +959,9 @@
444A981E234A638A00243BAB /* ManagedCreature+CoreDataProperties.swift in Sources */,
447C8BB32347C47700DC7971 /* Structs.swift in Sources */,
B413ABBC224D7C8100E8BDD1 /* AeonFoodNode.swift in Sources */,
4497925B23F0AF9D00FC2949 /* AeonCameraBodyNode.swift in Sources */,
B413ABBD224D7C8100E8BDD1 /* AeonNodeProtocols.swift in Sources */,
4497925623F0904400FC2949 /* AeonCameraNode.swift in Sources */,
444A9824234A638A00243BAB /* ManagedFood+CoreDataClass.swift in Sources */,
444A9828234A638A00243BAB /* ManagedTank+CoreDataClass.swift in Sources */,
B413ABBB224D7C8100E8BDD1 /* AeonCreatureBrainStates.swift in Sources */,
Expand Down Expand Up @@ -992,7 +1008,9 @@
444A981F234A638A00243BAB /* ManagedCreature+CoreDataProperties.swift in Sources */,
447C8BB42347C47700DC7971 /* Structs.swift in Sources */,
B413ABC4224D7C8200E8BDD1 /* AeonNodeProtocols.swift in Sources */,
4497925C23F0AF9D00FC2949 /* AeonCameraBodyNode.swift in Sources */,
B413ABC2224D7C8200E8BDD1 /* AeonCreatureBrainStates.swift in Sources */,
4497925723F0904400FC2949 /* AeonCameraNode.swift in Sources */,
444A9825234A638A00243BAB /* ManagedFood+CoreDataClass.swift in Sources */,
444A9829234A638A00243BAB /* ManagedTank+CoreDataClass.swift in Sources */,
B413ABC5224D7C8200E8BDD1 /* AeonBubbleNode.swift in Sources */,
Expand All @@ -1015,8 +1033,10 @@
4456E3BD234D76A800A9E401 /* Logging.swift in Sources */,
B413ABD9224D7C8C00E8BDD1 /* AeonNameGenerator.swift in Sources */,
B413ABC6224D7C8300E8BDD1 /* AeonCreatureNode.swift in Sources */,
4497925823F0904400FC2949 /* AeonCameraNode.swift in Sources */,
4431A06122AB1CC900865489 /* AeonTextures.swift in Sources */,
B413ABDE224D7C8C00E8BDD1 /* SKTextureGradient.swift in Sources */,
4497925D23F0AF9D00FC2949 /* AeonCameraBodyNode.swift in Sources */,
B413ABB7224D7C7300E8BDD1 /* AeonTankScene.swift in Sources */,
B413ABC8224D7C8300E8BDD1 /* AeonCreatureBrain.swift in Sources */,
B46CA6B7227F8F0800FAAAA6 /* AeonAssetGrabber.swift in Sources */,
Expand Down
12 changes: 6 additions & 6 deletions App/Core/Models/Core Data/CoreDataStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ extension CoreDataStore: DataStoreProtocol {

Log.info("Saved creature \(creature.uuid)")

if Log.logLevel == .debug {
self.databaseCounts()
}
// if Log.logLevel == .debug {
// self.databaseCounts()
// }

} catch {
Log.error("Creature failed to save to storage.")
Expand Down Expand Up @@ -265,9 +265,9 @@ extension CoreDataStore: DataStoreProtocol {

try self.mainManagedObjectContext.save()

if Log.logLevel == .debug {
self.databaseCounts()
}
// if Log.logLevel == .debug {
// self.databaseCounts()
// }

} catch {
Log.error("Tank failed to save to storage.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ extension ManagedTank {
}
}
var foods: [Food] = []
if let managedFoods = self.food?.allObjects as? [ManagedFood] {
if let managedFoods = food?.allObjects as? [ManagedFood] {
for managedFood in managedFoods {
foods.append(managedFood.toStruct())
}
Expand Down
110 changes: 110 additions & 0 deletions App/Core/Nodes/AeonCameraBodyNode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// AeonCameraBodyNode.swift
// Aeon Garden
//
// Created by Brad Root on 2/9/20.
// Copyright © 2020 Brad Root. All rights reserved.
//

import SpriteKit

class AeonCameraBodyNode: SKNode, Updatable {
var lastUpdateTime: TimeInterval = 0
var currentTarget: CGPoint?
var targetingTimer: Timer?
var targetTimeLimit: TimeInterval = 30
public var movementSpeed: CGFloat = 8
public var turnSpeed: CGFloat = 750

required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override init() {
super.init()

physicsBody = SKPhysicsBody(circleOfRadius: 20)
physicsBody?.categoryBitMask = 0
physicsBody?.collisionBitMask = 0
physicsBody?.allowsRotation = true
physicsBody?.affectedByGravity = false
physicsBody?.restitution = 0.8
physicsBody?.friction = 0.1
physicsBody?.mass = 1
physicsBody?.linearDamping = 0.5
physicsBody?.angularDamping = 1
zPosition = 1

// let body = SKSpriteNode(texture: foodTexture)
// body.size = CGSize(width: 40, height: 40)
// body.zPosition = 1
// body.name = "AeonCameraBodySprite"
// addChild(body)
}

func update(_ currentTime: TimeInterval) {
lastUpdateTime = currentTime
move()
}

@objc func pickRandomTarget() {
if let scene = scene as? AeonTankScene {
if let randomNode = scene.creatureNodes.randomElement() {
Log.debug("Picked new target for camera body: \(randomNode.position)")
currentTarget = randomNode.position

targetingTimer?.invalidate()

targetingTimer = Timer.scheduledTimer(
timeInterval: targetTimeLimit,
target: self,
selector: #selector(pickRandomTarget),
userInfo: nil,
repeats: false
)
}
}
}

// MARK: - Movement

func distance(point: CGPoint) -> CGFloat {
return CGFloat(hypotf(Float(point.x - position.x), Float(point.y - position.y)))
}

func angleBetween(pointOne: CGPoint, andPointTwo pointTwo: CGPoint) -> CGFloat {
let xdiff = (pointTwo.x - pointOne.x)
let ydiff = (pointTwo.y - pointOne.y)
let rad = atan2(ydiff, xdiff)
return rad - (CGFloat.pi / 2) // convert from atan's right-pointing zero to CG's up-pointing zero
}

func move() {
if let toCGPoint = currentTarget {
// Thrust
let radianFactor: CGFloat = CGFloat.pi / 180
let rotationInDegrees = zRotation / radianFactor
let newRotationDegrees = rotationInDegrees + 90
let newRotationRadians = newRotationDegrees * radianFactor

let thrustVector: CGVector = CGVector(
dx: cos(newRotationRadians) * movementSpeed,
dy: sin(newRotationRadians) * movementSpeed
)

physicsBody?.applyForce(thrustVector)

// Rotation
var goalAngle = angleBetween(pointOne: position, andPointTwo: toCGPoint)
var creatureAngle = atan2(physicsBody!.velocity.dy, physicsBody!.velocity.dx) - (CGFloat.pi / 2)

creatureAngle = convertRadiansToPi(creatureAngle)
goalAngle = convertRadiansToPi(goalAngle)

let angleDifference = convertRadiansToPi(goalAngle - creatureAngle)
let angleDivisor: CGFloat = turnSpeed

physicsBody?.applyTorque(angleDifference / angleDivisor)
}
}
}
137 changes: 137 additions & 0 deletions App/Core/Nodes/AeonCameraNode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//
// AeonCameraNode.swift
// Aeon Garden
//
// Created by Brad Root on 2/9/20.
// Copyright © 2020 Brad Root. All rights reserved.
//

import SpriteKit

class AeonCameraNode: SKCameraNode, Updatable {
var body: AeonCameraBodyNode = AeonCameraBodyNode()
var selectedNode: SKNode?
var autoCameraIsEnabled: Bool = false
let cameraMoveDuration: TimeInterval = 0.25
var lastUpdateTime: TimeInterval = 0
var zoomTimer: Timer?
var currentZoomState: zoomState = .zoomOut
weak var interfaceDelegate: AeonTankInterfaceDelegate?

enum zoomState {
case fullZoom
case threeQuartersZoom
case halfZoom
case quarterZoom
case zoomOut
}

// MARK: - Functions

func selectedNode(_ node: SKNode) {
if selectedNode == node {
deselectNode()
} else {
Log.info("📷 Selected Node")
if let currentCreature = selectedNode as? AeonCreatureNode, currentCreature != node {
currentCreature.hideSelectionRing()

if let cameraBody = node as? AeonCameraBodyNode {
// If previously selected node was a creature,
// and the new node is the camera body,
// move the camera body there for a soft transition
cameraBody.position = currentCreature.position
}
}

selectedNode = node
if let newCreature = selectedNode as? AeonCreatureNode {
newCreature.displaySelectionRing(withColor: .aeonBrightYellow)
interfaceDelegate?.creatureSelected(newCreature)
zoom(.fullZoom)
} else {
changeCameraZoomLevel()
}
}
}

func deselectNode(animated: Bool = true) {
Log.info("Deselected Node")
if let currentCreature = selectedNode as? AeonCreatureNode {
currentCreature.hideSelectionRing()
if animated {
interfaceDelegate?.creatureDeselected()
}
}
selectedNode = nil
if animated {
zoom(.zoomOut)
}
}

func zoom(_ state: zoomState, speed: TimeInterval = 1) {
removeAllActions()
switch state {
case .fullZoom:
run(SKAction.scale(to: 0.4, duration: speed))
currentZoomState = .fullZoom
case .threeQuartersZoom:
run(SKAction.scale(to: 0.55, duration: speed))
currentZoomState = .halfZoom
case .halfZoom:
run(SKAction.scale(to: 0.7, duration: speed))
currentZoomState = .halfZoom
case .quarterZoom:
run(SKAction.scale(to: 0.85, duration: speed))
currentZoomState = .halfZoom
case .zoomOut:
guard let scene = scene else { fatalError("Camera is not in a scene.") }
removeAllActions()
let scaleAction = SKAction.scale(to: 1, duration: speed)
let moveAction = SKAction.move(
to: CGPoint(x: scene.size.width / 2, y: scene.size.height / 2),
duration: 1
)
run(SKAction.group([scaleAction, moveAction]))
currentZoomState = .zoomOut
}
}

func update(_ currentTime: TimeInterval) {
if let selectedNode = self.selectedNode {
let cameraAction = SKAction.move(to: selectedNode.position, duration: cameraMoveDuration)
run(cameraAction)
}
body.update(currentTime)
lastUpdateTime = currentTime
}

func startAutoCamera() {
Log.debug("📷 Auto camera started...")

if let scene = scene, body.scene == nil {
Log.debug("📷 Creating camera body in scene.")
scene.addChild(body)
}

body.position = position

selectedNode(body)
body.pickRandomTarget()
}

func stopAutoCamera() {
Log.debug("📷 Auto camera stopped.")
}

@objc func changeCameraZoomLevel() {
Log.debug("📷 Camera auto-zoom updated.")

zoomTimer?.invalidate()

zoomTimer = Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(changeCameraZoomLevel), userInfo: nil, repeats: false)

let randomZoomLevels: [zoomState] = [.halfZoom, .threeQuartersZoom, .fullZoom]
zoom(randomZoomLevels.randomElement()!, speed: 20)
}
}
3 changes: 0 additions & 3 deletions App/Core/Nodes/AeonCreatureNode/AeonCreatureNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,9 +371,6 @@ class AeonCreatureNode: SKNode, Updatable {
run(SKAction.group([fadeOut, shrinkOut]), completion: {
if let mainScene = self.scene as? AeonTankScene {
mainScene.deathCount += 1
if mainScene.selectedCreature == self {
mainScene.selectedCreature = nil
}
}
if let scene = self.parent as? AeonTankScene {
scene.removeChild(self)
Expand Down
Loading

0 comments on commit 5494a06

Please sign in to comment.