diff --git a/TowerForge/TowerForge.xcodeproj/project.pbxproj b/TowerForge/TowerForge.xcodeproj/project.pbxproj index 788aed1a..f55f27f2 100644 --- a/TowerForge/TowerForge.xcodeproj/project.pbxproj +++ b/TowerForge/TowerForge.xcodeproj/project.pbxproj @@ -25,9 +25,17 @@ 3C9955C82BA5865C00D33FA5 /* ConcurrentEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9955C72BA5865C00D33FA5 /* ConcurrentEvent.swift */; }; 3C9955CA2BA5888F00D33FA5 /* SpawnEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9955C92BA5888F00D33FA5 /* SpawnEvent.swift */; }; 3C9955CC2BA5889800D33FA5 /* MoveEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9955CB2BA5889800D33FA5 /* MoveEvent.swift */; }; + 3CCF9CAF2BAB1A96004D170E /* SceneUpdateDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCF9CAE2BAB1A96004D170E /* SceneUpdateDelegate.swift */; }; + 3CCF9CB12BAB1BCE004D170E /* GameWorld.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCF9CB02BAB1BCE004D170E /* GameWorld.swift */; }; + 3CCF9CB32BAB1F42004D170E /* SystemManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCF9CB22BAB1F42004D170E /* SystemManager.swift */; }; + 3CCF9CB72BAB2877004D170E /* MainMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCF9CB62BAB2877004D170E /* MainMenuViewController.swift */; }; + 3CE9514B2BAC83FA008B2785 /* SpawnableEntities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE9514A2BAC83FA008B2785 /* SpawnableEntities.swift */; }; + 3CE9514F2BAC8936008B2785 /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE9514E2BAC8936008B2785 /* Renderer.swift */; }; + 3CE951512BAC8955008B2785 /* Renderable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE951502BAC8955008B2785 /* Renderable.swift */; }; + 3CE951562BACA0CF008B2785 /* Collidable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE951552BACA0CF008B2785 /* Collidable.swift */; }; 5200624E2BA8D597000DBA30 /* AiComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5200624D2BA8D597000DBA30 /* AiComponent.swift */; }; 520062522BA8DA09000DBA30 /* UnitGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520062512BA8DA09000DBA30 /* UnitGenerator.swift */; }; - 520062562BA8E026000DBA30 /* HasCost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520062552BA8E026000DBA30 /* HasCost.swift */; }; + 520062562BA8E026000DBA30 /* Spawnable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520062552BA8E026000DBA30 /* Spawnable.swift */; }; 520062582BA8ED73000DBA30 /* HomeComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520062572BA8ED73000DBA30 /* HomeComponent.swift */; }; 52578B822BA61AAF00B4D76C /* PositionComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52578B812BA61AAF00B4D76C /* PositionComponent.swift */; }; 52578B872BA6209700B4D76C /* DamageComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52578B862BA6209700B4D76C /* DamageComponent.swift */; }; @@ -101,9 +109,17 @@ 3C9955C72BA5865C00D33FA5 /* ConcurrentEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentEvent.swift; sourceTree = ""; }; 3C9955C92BA5888F00D33FA5 /* SpawnEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpawnEvent.swift; sourceTree = ""; }; 3C9955CB2BA5889800D33FA5 /* MoveEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveEvent.swift; sourceTree = ""; }; + 3CCF9CAE2BAB1A96004D170E /* SceneUpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneUpdateDelegate.swift; sourceTree = ""; }; + 3CCF9CB02BAB1BCE004D170E /* GameWorld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameWorld.swift; sourceTree = ""; }; + 3CCF9CB22BAB1F42004D170E /* SystemManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemManager.swift; sourceTree = ""; }; + 3CCF9CB62BAB2877004D170E /* MainMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuViewController.swift; sourceTree = ""; }; + 3CE9514A2BAC83FA008B2785 /* SpawnableEntities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpawnableEntities.swift; sourceTree = ""; }; + 3CE9514E2BAC8936008B2785 /* Renderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Renderer.swift; sourceTree = ""; }; + 3CE951502BAC8955008B2785 /* Renderable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Renderable.swift; sourceTree = ""; }; + 3CE951552BACA0CF008B2785 /* Collidable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collidable.swift; sourceTree = ""; }; 5200624D2BA8D597000DBA30 /* AiComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AiComponent.swift; sourceTree = ""; }; 520062512BA8DA09000DBA30 /* UnitGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitGenerator.swift; sourceTree = ""; }; - 520062552BA8E026000DBA30 /* HasCost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HasCost.swift; sourceTree = ""; }; + 520062552BA8E026000DBA30 /* Spawnable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spawnable.swift; sourceTree = ""; }; 520062572BA8ED73000DBA30 /* HomeComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeComponent.swift; sourceTree = ""; }; 52578B812BA61AAF00B4D76C /* PositionComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionComponent.swift; sourceTree = ""; }; 52578B862BA6209700B4D76C /* DamageComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamageComponent.swift; sourceTree = ""; }; @@ -121,7 +137,7 @@ 52DF5FA92BA32B2300135367 /* GameScene.sks */ = {isa = PBXFileReference; lastKnownFileType = file.sks; path = GameScene.sks; sourceTree = ""; }; 52DF5FAB2BA32B2300135367 /* Actions.sks */ = {isa = PBXFileReference; lastKnownFileType = file.sks; path = Actions.sks; sourceTree = ""; }; 52DF5FAD2BA32B2300135367 /* GameScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameScene.swift; sourceTree = ""; }; - 52DF5FAF2BA32B2300135367 /* GameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameViewController.swift; sourceTree = ""; }; + 52DF5FAF2BA32B2300135367 /* GameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GameViewController.swift; path = ../GameViewController.swift; sourceTree = ""; }; 52DF5FB22BA32B2300135367 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 52DF5FB42BA32B2600135367 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 52DF5FB72BA32B2600135367 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -173,14 +189,13 @@ 3C99559F2BA47D3E00D33FA5 /* Entities */ = { isa = PBXGroup; children = ( - 3C9955A02BA47DA500D33FA5 /* BaseTower.swift */, - 3C9955A22BA47DBB00D33FA5 /* BaseUnit.swift */, - 3C9955A42BA47DC600D33FA5 /* BaseProjectile.swift */, + 3CE9514C2BAC8668008B2785 /* Base Entities */, 3C9955AE2BA48FD200D33FA5 /* MeleeUnit.swift */, 3C9955B02BA4ACA100D33FA5 /* Arrow.swift */, 3C9955B32BA4B12000D33FA5 /* ArrowTower.swift */, 52578B8B2BA627B200B4D76C /* Team.swift */, 529F91872BA6D7A7009551D9 /* SoldierUnit.swift */, + 3CE9514A2BAC83FA008B2785 /* SpawnableEntities.swift */, ); path = Entities; sourceTree = ""; @@ -218,6 +233,33 @@ path = "Implemented Events"; sourceTree = ""; }; + 3CE9514C2BAC8668008B2785 /* Base Entities */ = { + isa = PBXGroup; + children = ( + 3C9955A02BA47DA500D33FA5 /* BaseTower.swift */, + 3C9955A22BA47DBB00D33FA5 /* BaseUnit.swift */, + 3C9955A42BA47DC600D33FA5 /* BaseProjectile.swift */, + ); + path = "Base Entities"; + sourceTree = ""; + }; + 3CE9514D2BAC8925008B2785 /* Rendering */ = { + isa = PBXGroup; + children = ( + 3CE9514E2BAC8936008B2785 /* Renderer.swift */, + 3CE951502BAC8955008B2785 /* Renderable.swift */, + ); + path = Rendering; + sourceTree = ""; + }; + 3CE951542BACA079008B2785 /* Collision */ = { + isa = PBXGroup; + children = ( + 3CE951552BACA0CF008B2785 /* Collidable.swift */, + ); + path = Collision; + sourceTree = ""; + }; 5200624C2BA8D574000DBA30 /* GameComponents */ = { isa = PBXGroup; children = ( @@ -268,8 +310,9 @@ 5295A2082BAAE14B005018A8 /* Controllers */ = { isa = PBXGroup; children = ( - 52DF5FAF2BA32B2300135367 /* GameViewController.swift */, 5295A20E2BAAE7CF005018A8 /* TeamController.swift */, + 52DF5FAF2BA32B2300135367 /* GameViewController.swift */, + 3CCF9CB62BAB2877004D170E /* MainMenuViewController.swift */, ); path = Controllers; sourceTree = ""; @@ -298,18 +341,21 @@ 52DF5FA62BA32B2300135367 /* TowerForge */ = { isa = PBXGroup; children = ( + 3CE9514D2BAC8925008B2785 /* Rendering */, 5295A2082BAAE14B005018A8 /* Controllers */, 5295A2052BAA0208005018A8 /* Nodes */, 5295A2002BA9FB97005018A8 /* Scenes */, 52DF5FE72BA33F8800135367 /* Protocols */, 52DF5FDF2BA3347C00135367 /* TFCore */, + 3CCF9CAE2BAB1A96004D170E /* SceneUpdateDelegate.swift */, + 5295A2012BA9FBD9005018A8 /* SceneManagerDelegate.swift */, 52DF5FDB2BA32CEF00135367 /* LevelManager */, 52DF5FA72BA32B2300135367 /* AppDelegate.swift */, 52DF5FAB2BA32B2300135367 /* Actions.sks */, + 3CCF9CB02BAB1BCE004D170E /* GameWorld.swift */, 52DF5FB12BA32B2300135367 /* Main.storyboard */, 52DF5FB42BA32B2600135367 /* Assets.xcassets */, 52DF5FB62BA32B2600135367 /* LaunchScreen.storyboard */, - 5295A2012BA9FBD9005018A8 /* SceneManagerDelegate.swift */, ); path = TowerForge; sourceTree = ""; @@ -335,12 +381,14 @@ 52DF5FDB2BA32CEF00135367 /* LevelManager */ = { isa = PBXGroup; children = ( + 3CE951542BACA079008B2785 /* Collision */, 5200624F2BA8D9E4000DBA30 /* Generators */, 3C9955C32BA585CD00D33FA5 /* Systems */, 3C9955B82BA5620A00D33FA5 /* Events */, 3C99559F2BA47D3E00D33FA5 /* Entities */, 52DF5FF02BA3519D00135367 /* Components */, 52DF5FDD2BA32D7E00135367 /* EntityManager.swift */, + 3CCF9CB22BAB1F42004D170E /* SystemManager.swift */, 52DF5FEC2BA34D0300135367 /* TFComponent.swift */, 3C9955AC2BA483B100D33FA5 /* TFSystem.swift */, 52DF5FEE2BA34EA000135367 /* TFEntity.swift */, @@ -371,7 +419,7 @@ isa = PBXGroup; children = ( 52DF5FE82BA33F9700135367 /* Animatable.swift */, - 520062552BA8E026000DBA30 /* HasCost.swift */, + 520062552BA8E026000DBA30 /* Spawnable.swift */, ); path = Protocols; sourceTree = ""; @@ -543,10 +591,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3CCF9CAF2BAB1A96004D170E /* SceneUpdateDelegate.swift in Sources */, 3C9955A12BA47DA500D33FA5 /* BaseTower.swift in Sources */, + 3CCF9CB32BAB1F42004D170E /* SystemManager.swift in Sources */, 5295A20F2BAAE7CF005018A8 /* TeamController.swift in Sources */, 3C9955A52BA47DC600D33FA5 /* BaseProjectile.swift in Sources */, 52578B8C2BA627B200B4D76C /* Team.swift in Sources */, + 3CCF9CB72BAB2877004D170E /* MainMenuViewController.swift in Sources */, 52DF5FFF2BA3656500135367 /* ShootingComponent.swift in Sources */, 3C9955CC2BA5889800D33FA5 /* MoveEvent.swift in Sources */, 5200624E2BA8D597000DBA30 /* AiComponent.swift in Sources */, @@ -558,17 +609,21 @@ 3C9955CA2BA5888F00D33FA5 /* SpawnEvent.swift in Sources */, 5295A2132BAAEA16005018A8 /* UnitNode.swift in Sources */, 52DF5FF32BA351E100135367 /* SpriteComponent.swift in Sources */, + 3CE9514B2BAC83FA008B2785 /* SpawnableEntities.swift in Sources */, 52578B872BA6209700B4D76C /* DamageComponent.swift in Sources */, 52DF5FB02BA32B2300135367 /* GameViewController.swift in Sources */, + 3CE951512BAC8955008B2785 /* Renderable.swift in Sources */, 529F91882BA6D7A7009551D9 /* SoldierUnit.swift in Sources */, 520062522BA8DA09000DBA30 /* UnitGenerator.swift in Sources */, 3C9955C02BA57E5500D33FA5 /* EventTarget.swift in Sources */, + 3CCF9CB12BAB1BCE004D170E /* GameWorld.swift in Sources */, 5295A2152BAAF335005018A8 /* UnitSelectionNode.swift in Sources */, 52DF5FF92BA35D2B00135367 /* MovableComponent.swift in Sources */, 52DF5FDE2BA32D7E00135367 /* EntityManager.swift in Sources */, 3C9955C82BA5865C00D33FA5 /* ConcurrentEvent.swift in Sources */, 3C9955AD2BA483B100D33FA5 /* TFSystem.swift in Sources */, 3C9955BE2BA57E4B00D33FA5 /* EventManager.swift in Sources */, + 3CE951562BACA0CF008B2785 /* Collidable.swift in Sources */, 52DF5FE62BA33AF300135367 /* TFSpriteNode.swift in Sources */, 3C9955B42BA4B12000D33FA5 /* ArrowTower.swift in Sources */, 52DF5FE92BA33F9700135367 /* Animatable.swift in Sources */, @@ -579,6 +634,7 @@ 52DF5FA82BA32B2300135367 /* AppDelegate.swift in Sources */, 3C769A742BA591BD00F454F9 /* SpawnSystem.swift in Sources */, 3C9955B12BA4ACA100D33FA5 /* Arrow.swift in Sources */, + 3CE9514F2BAC8936008B2785 /* Renderer.swift in Sources */, 3C769A722BA58DE700F454F9 /* MovementSystem.swift in Sources */, 52DF5FEB2BA3400C00135367 /* TFAnimatableNode.swift in Sources */, 5295A2042BA9FED4005018A8 /* MenuScene.swift in Sources */, @@ -586,7 +642,7 @@ 52DF5FED2BA34D0300135367 /* TFComponent.swift in Sources */, 527E3A242BA613F000FE1628 /* PlayerComponent.swift in Sources */, 3C9955C52BA585DD00D33FA5 /* HealthSystem.swift in Sources */, - 520062562BA8E026000DBA30 /* HasCost.swift in Sources */, + 520062562BA8E026000DBA30 /* Spawnable.swift in Sources */, 52DF5FFB2BA3601400135367 /* HealthComponent.swift in Sources */, 3C9955BC2BA563A800D33FA5 /* TFEvent.swift in Sources */, 5295A2072BAA02FD005018A8 /* TFButton.swift in Sources */, diff --git a/TowerForge/TowerForge/Base.lproj/Main.storyboard b/TowerForge/TowerForge/Base.lproj/Main.storyboard index 2cc971a4..aa8908b7 100644 --- a/TowerForge/TowerForge/Base.lproj/Main.storyboard +++ b/TowerForge/TowerForge/Base.lproj/Main.storyboard @@ -1,24 +1,71 @@ - + + - + + + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TowerForge/TowerForge/Controllers/MainMenuViewController.swift b/TowerForge/TowerForge/Controllers/MainMenuViewController.swift new file mode 100644 index 00000000..9e73ef1b --- /dev/null +++ b/TowerForge/TowerForge/Controllers/MainMenuViewController.swift @@ -0,0 +1,13 @@ +// +// MainMenuViewController.swift +// TowerForge +// +// Created by Zheng Ze on 20/3/24. +// + +import Foundation +import UIKit + +class MainMenuViewController: UIViewController { + +} diff --git a/TowerForge/TowerForge/Controllers/GameViewController.swift b/TowerForge/TowerForge/GameViewController.swift similarity index 51% rename from TowerForge/TowerForge/Controllers/GameViewController.swift rename to TowerForge/TowerForge/GameViewController.swift index e4b52d70..69ac74ae 100644 --- a/TowerForge/TowerForge/Controllers/GameViewController.swift +++ b/TowerForge/TowerForge/GameViewController.swift @@ -10,12 +10,18 @@ import SpriteKit import GameplayKit class GameViewController: UIViewController { + private var gameWorld: GameWorld? override func viewDidLoad() { super.viewDidLoad() showGameLevelScene(level: 1) // TODO : Change hardcoded level value } + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + gameWorld = nil + } + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UIDevice.current.userInterfaceIdiom == .phone { return .allButUpsideDown @@ -27,6 +33,36 @@ class GameViewController: UIViewController { override var prefersStatusBarHidden: Bool { true } + + private func setUpGameWorld(scene: GameScene) { + self.gameWorld = GameWorld(scene: scene) + } +} + +extension GameViewController: SceneUpdateDelegate { + func touch(at location: CGPoint) { + gameWorld?.spawnUnit(at: location) + } + + func update(deltaTime: TimeInterval) { + gameWorld?.update(deltaTime: deltaTime) + } + + func contactBegin(between nodeA: TFAnimatableNode, and nodeB: TFAnimatableNode) { + guard let nameA = nodeA.name, let nameB = nodeB.name, + let idA = UUID(uuidString: nameA), let idB = UUID(uuidString: nameB) else { + return + } + gameWorld?.handleContact(between: idA, and: idB) + } + + func contactEnd(between nodeA: TFAnimatableNode, and nodeB: TFAnimatableNode) { + guard let nameA = nodeA.name, let nameB = nodeB.name, + let idA = UUID(uuidString: nameA), let idB = UUID(uuidString: nameB) else { + return + } + gameWorld?.handleSeparation(between: idA, and: idB) + } } extension GameViewController: SceneManagerDelegate { @@ -39,10 +75,12 @@ extension GameViewController: SceneManagerDelegate { // TODO : to implement after Keith is done } func showGameLevelScene(level: Int) { - if let gameScene = SKScene(fileNamed: "GameScene") as? GameScene { + if let gameScene = GameScene(fileNamed: "GameScene") { // Present the scene gameScene.sceneManagerDelegate = self + gameScene.updateDelegate = self showScene(scene: gameScene) + setUpGameWorld(scene: gameScene) } } func showScene(scene: SKScene) { diff --git a/TowerForge/TowerForge/GameWorld.swift b/TowerForge/TowerForge/GameWorld.swift new file mode 100644 index 00000000..aa054cfc --- /dev/null +++ b/TowerForge/TowerForge/GameWorld.swift @@ -0,0 +1,92 @@ +// +// GameWorld.swift +// TowerForge +// +// Created by Zheng Ze on 20/3/24. +// + +import Foundation + +class GameWorld { + private unowned var scene: GameScene? + private var entityManager: EntityManager + private var systemManager: SystemManager + private var eventManager: EventManager + private var selectionNode: UnitSelectionNode + private var renderer: Renderer? + + init(scene: GameScene?) { + self.scene = scene + entityManager = EntityManager() + systemManager = SystemManager() + eventManager = EventManager() + selectionNode = UnitSelectionNode() + renderer = Renderer(target: self, scene: scene) + + self.setUpSelectionNode() + } + + func update(deltaTime: TimeInterval) { + systemManager.update(deltaTime) + eventManager.executeEvents(in: self) + entityManager.update(deltaTime) + renderer?.render() + } + + func spawnUnit(at location: CGPoint) { + selectionNode.unitNodeDidSpawn(location) + } + + func handleContact(between idA: UUID, and idB: UUID) { + guard let entityA = entityManager.entity(with: idA), let entityB = entityManager.entity(with: idB), + let event = entityA.collide(with: entityB) else { + return + } + + eventManager.add(event) + } + + func handleSeparation(between idA: UUID, and idB: UUID) { + guard let entityA = entityManager.entity(with: idA), let entityB = entityManager.entity(with: idB) else { + return + } + // TODO: Handle any separation logic here. + } + + private func setUpSelectionNode() { + selectionNode.delegate = self + scene?.addChild(selectionNode) + // Position unit selection node on the left side of the screen + selectionNode.position = CGPoint(x: selectionNode.frame.width / 2, y: scene?.frame.midY ?? 300) + + // Calculate vertical spacing between unit nodes + let verticalSpacing = selectionNode.frame.height + var verticalY = 10.0 + // Position unit nodes vertically aligned + for unitNode in selectionNode.unitNodes { + unitNode.position = CGPoint(x: selectionNode.frame.width / 2, + y: verticalY) + verticalY += verticalSpacing + } + } +} + +extension GameWorld: EventTarget { + func system(ofType type: T.Type) -> T? { + systemManager.system(ofType: type) + } +} + +extension GameWorld: UnitSelectionNodeDelegate { + func unitSelectionNodeDidSpawn(ofType type: T.Type, position: CGPoint) { + let unit = UnitGenerator.spawn(ofType: type, at: position, player: Player.ownPlayer, + entityManager: entityManager) + entityManager.add(unit) + } +} + +extension GameWorld: Renderable { + func entitiesToRender() -> [TFEntity] { + entityManager.entities + } +} diff --git a/TowerForge/TowerForge/LevelManager/Collision/Collidable.swift b/TowerForge/TowerForge/LevelManager/Collision/Collidable.swift new file mode 100644 index 00000000..bfadad2f --- /dev/null +++ b/TowerForge/TowerForge/LevelManager/Collision/Collidable.swift @@ -0,0 +1,14 @@ +// +// Collidable.swift +// TowerForge +// +// Created by Zheng Ze on 22/3/24. +// + +import Foundation + +protocol Collidable { + func collide(with other: Collidable) -> TFEvent? + func collide(with damageComponent: DamageComponent) -> TFEvent? + func collide(with healthComponent: HealthComponent) -> TFEvent? +} diff --git a/TowerForge/TowerForge/LevelManager/Components/AiComponent.swift b/TowerForge/TowerForge/LevelManager/Components/AiComponent.swift index 958153ee..0f33ee96 100644 --- a/TowerForge/TowerForge/LevelManager/Components/AiComponent.swift +++ b/TowerForge/TowerForge/LevelManager/Components/AiComponent.swift @@ -9,26 +9,19 @@ import Foundation class AiComponent: TFComponent { private var entityManager: EntityManager - private var chosenUnit: UnitType + private var chosenUnit: (BaseUnit & Spawnable).Type? init(entityManager: EntityManager) { self.entityManager = entityManager - self.chosenUnit = UnitType.possibleUnits.randomElement() ?? .melee + self.chosenUnit = SpawnableEntities.possibleUnits.randomElement() super.init() } override func update(deltaTime: TimeInterval) { - guard let homeComponent = entity?.component(ofType: HomeComponent.self) else { + guard let homeComponent = entity?.component(ofType: HomeComponent.self), let chosenUnit = chosenUnit else { return } -// if chosenUnit == .melee && homeComponent.points >= MeleeUnit.cost { -// // TODO: Remove hard code of CGPoints -// UnitGenerator.spawnMelee(at: CGPoint(x: 0, y: 10), player: .oppositePlayer, entityManager: entityManager) -// self.chosenUnit = UnitType.possibleUnits.randomElement() ?? .melee -// } -// if chosenUnit == .soldier && homeComponent.points >= SoldierUnit.cost { -// // TODO: Remove hard code of CGPoints -// UnitGenerator.spawnSoldier(at: CGPoint(x: 0, y: 10), player: .oppositePlayer, entityManager: entityManager) -// self.chosenUnit = UnitType.possibleUnits.randomElement() ?? .melee -// } + let unit = UnitGenerator.spawn(ofType: chosenUnit, at: CGPoint(x: 0, y: 10), player: .oppositePlayer, + entityManager: entityManager) + entityManager.add(unit) } } diff --git a/TowerForge/TowerForge/LevelManager/Components/BaseComponents/MovableComponent.swift b/TowerForge/TowerForge/LevelManager/Components/BaseComponents/MovableComponent.swift index 5a983bdd..b2c91ee6 100644 --- a/TowerForge/TowerForge/LevelManager/Components/BaseComponents/MovableComponent.swift +++ b/TowerForge/TowerForge/LevelManager/Components/BaseComponents/MovableComponent.swift @@ -21,7 +21,6 @@ class MovableComponent: TFComponent { override func update(deltaTime: TimeInterval) { guard let entity = entity, let positionComponent = entity.component(ofType: PositionComponent.self), - let spriteComponent = entity.component(ofType: SpriteComponent.self), let playerComponent = entity.component(ofType: PlayerComponent.self) else { return } @@ -31,6 +30,5 @@ class MovableComponent: TFComponent { let finalY = positionComponent.position.y + (velocity.dy * CGFloat(deltaTime) * directionVelocity.dy) positionComponent.changeTo(to: CGPoint(x: finalX, y: finalY)) - spriteComponent.node.position = positionComponent.position } } diff --git a/TowerForge/TowerForge/LevelManager/Components/BaseComponents/PositionComponent.swift b/TowerForge/TowerForge/LevelManager/Components/BaseComponents/PositionComponent.swift index 9681bf84..bb5fd9ff 100644 --- a/TowerForge/TowerForge/LevelManager/Components/BaseComponents/PositionComponent.swift +++ b/TowerForge/TowerForge/LevelManager/Components/BaseComponents/PositionComponent.swift @@ -17,9 +17,6 @@ class PositionComponent: TFComponent { } func changeTo(to position: CGPoint) { - guard let entity = entity, let spriteComponent = entity.component(ofType: SpriteComponent.self) else { - return - } self.position = position } } diff --git a/TowerForge/TowerForge/LevelManager/Components/BaseComponents/SpriteComponent.swift b/TowerForge/TowerForge/LevelManager/Components/BaseComponents/SpriteComponent.swift index 143447f2..4f4afdb1 100644 --- a/TowerForge/TowerForge/LevelManager/Components/BaseComponents/SpriteComponent.swift +++ b/TowerForge/TowerForge/LevelManager/Components/BaseComponents/SpriteComponent.swift @@ -8,12 +8,16 @@ import Foundation class SpriteComponent: TFComponent { - var node: TFAnimatableNode + var textures: TFTextures + var height: CGFloat + var width: CGFloat + var animatableKey: String init(textureNames: [String], height: CGFloat, width: CGFloat, position: CGPoint, animatableKey: String) { - let textures = TFTextures(textureNames: textureNames, textureAtlasName: "Sprites") - self.node = TFAnimatableNode(textures: textures, height: height, width: width, animatableKey: animatableKey) - self.node.position = position + textures = TFTextures(textureNames: textureNames, textureAtlasName: "Sprites") + self.height = height + self.width = width + self.animatableKey = animatableKey super.init() } } diff --git a/TowerForge/TowerForge/LevelManager/Components/GameComponents/DamageComponent.swift b/TowerForge/TowerForge/LevelManager/Components/GameComponents/DamageComponent.swift index 7009d179..8f98c4d3 100644 --- a/TowerForge/TowerForge/LevelManager/Components/GameComponents/DamageComponent.swift +++ b/TowerForge/TowerForge/LevelManager/Components/GameComponents/DamageComponent.swift @@ -32,38 +32,39 @@ class DamageComponent: TFComponent { let spriteComponent = entity.component(ofType: SpriteComponent.self) else { return } - + + // TODO: Shift damage logic to damage event and handled by health system. // Loop opposite team's entities - for entity in entityManager.entities { - guard let playerComponent = entity.component(ofType: PlayerComponent.self) else { - return - } - if playerComponent.player == .ownPlayer { - return - } - // Get opposite team's components - guard let oppositeSpriteComponent = entity.component(ofType: SpriteComponent.self), - let oppositeHealthComponent = entity.component(ofType: HealthComponent.self) else { - return - } - - // Check collision with opposite team sprite component - if oppositeSpriteComponent.node - .calculateAccumulatedFrame().intersects( - spriteComponent.node.calculateAccumulatedFrame()) { - - // Check if can attack - if CACurrentMediaTime() - lastAttackTime > attackRate { - lastAttackTime = CACurrentMediaTime() - oppositeHealthComponent.decreaseHealth(amount: attackPower) - } - - } - - // If only used once, then remove from entity - if temporary { - entityManager.removeEntity(with: entity.id) - } - } +// for entity in entityManager.entities { +// guard let playerComponent = entity.component(ofType: PlayerComponent.self) else { +// return +// } +// if playerComponent.player == .ownPlayer { +// return +// } +// // Get opposite team's components +// guard let oppositeSpriteComponent = entity.component(ofType: SpriteComponent.self), +// let oppositeHealthComponent = entity.component(ofType: HealthComponent.self) else { +// return +// } +// +// // Check collision with opposite team sprite component +// if oppositeSpriteComponent.node +// .calculateAccumulatedFrame().intersects( +// spriteComponent.node.calculateAccumulatedFrame()) { +// +// // Check if can attack +// if CACurrentMediaTime() - lastAttackTime > attackRate { +// lastAttackTime = CACurrentMediaTime() +// oppositeHealthComponent.decreaseHealth(amount: attackPower) +// } +// +// } +// +// // If only used once, then remove from entity +// if temporary { +// entityManager.removeEntity(with: entity.id) +// } +// } } } diff --git a/TowerForge/TowerForge/LevelManager/Entities/Arrow.swift b/TowerForge/TowerForge/LevelManager/Entities/Arrow.swift index e0a746cd..09546542 100644 --- a/TowerForge/TowerForge/LevelManager/Entities/Arrow.swift +++ b/TowerForge/TowerForge/LevelManager/Entities/Arrow.swift @@ -24,4 +24,23 @@ class Arrow: BaseProjectile { temporary: true, entityManager: entityManager)) } + + override func collide(with other: any Collidable) -> TFEvent? { + let superEvent = super.collide(with: other) + guard let damageComponent = self.component(ofType: DamageComponent.self) else { + return superEvent + } + if let superEvent = superEvent { + return superEvent.concurrentlyWith(other.collide(with: damageComponent)) + } + return other.collide(with: damageComponent) + } + + override func collide(with healthComponent: HealthComponent) -> TFEvent? { + guard let entityId = healthComponent.entity?.id, + let damageComponent = self.component(ofType: DamageComponent.self) else { + return nil + } + return DamageEvent(on: entityId, at: Date().timeIntervalSince1970, with: damageComponent.attackPower) + } } diff --git a/TowerForge/TowerForge/LevelManager/Entities/BaseProjectile.swift b/TowerForge/TowerForge/LevelManager/Entities/Base Entities/BaseProjectile.swift similarity index 99% rename from TowerForge/TowerForge/LevelManager/Entities/BaseProjectile.swift rename to TowerForge/TowerForge/LevelManager/Entities/Base Entities/BaseProjectile.swift index e5433f3d..27ec05cd 100644 --- a/TowerForge/TowerForge/LevelManager/Entities/BaseProjectile.swift +++ b/TowerForge/TowerForge/LevelManager/Entities/Base Entities/BaseProjectile.swift @@ -24,6 +24,7 @@ class BaseProjectile: TFEntity { animatableKey: key) self.addComponent(spriteComponent) } + private func createPositionComponent(position: CGPoint) { let positionComponent = PositionComponent(position: position) self.addComponent(positionComponent) diff --git a/TowerForge/TowerForge/LevelManager/Entities/BaseTower.swift b/TowerForge/TowerForge/LevelManager/Entities/Base Entities/BaseTower.swift similarity index 64% rename from TowerForge/TowerForge/LevelManager/Entities/BaseTower.swift rename to TowerForge/TowerForge/LevelManager/Entities/Base Entities/BaseTower.swift index 136019c9..d2a13767 100644 --- a/TowerForge/TowerForge/LevelManager/Entities/BaseTower.swift +++ b/TowerForge/TowerForge/LevelManager/Entities/Base Entities/BaseTower.swift @@ -21,6 +21,26 @@ class BaseTower: TFEntity { createPositionComponent(position: position) } + override func collide(with other: any Collidable) -> TFEvent? { + let superEvent = super.collide(with: other) + guard let healthComponent = self.component(ofType: HealthComponent.self) else { + return superEvent + } + + if let superEvent = superEvent { + return superEvent.concurrentlyWith(other.collide(with: healthComponent)) + } + return other.collide(with: healthComponent) + } + + override func collide(with damageComponent: DamageComponent) -> TFEvent? { + guard self.hasComponent(ofType: HealthComponent.self) else { + return nil + } + // No call to super here as super is done on collide with Collidable above. + return DamageEvent(on: self.id, at: Date().timeIntervalSince1970, with: damageComponent.attackPower) + } + private func createHealthComponent(maxHealth: CGFloat, entityManager: EntityManager) { let healthComponent = HealthComponent(maxHealth: maxHealth, entityManager: entityManager) self.addComponent(healthComponent) diff --git a/TowerForge/TowerForge/LevelManager/Entities/BaseUnit.swift b/TowerForge/TowerForge/LevelManager/Entities/Base Entities/BaseUnit.swift similarity index 74% rename from TowerForge/TowerForge/LevelManager/Entities/BaseUnit.swift rename to TowerForge/TowerForge/LevelManager/Entities/Base Entities/BaseUnit.swift index 02528a1f..7866cea4 100644 --- a/TowerForge/TowerForge/LevelManager/Entities/BaseUnit.swift +++ b/TowerForge/TowerForge/LevelManager/Entities/Base Entities/BaseUnit.swift @@ -32,18 +32,15 @@ enum UnitType { } } -class BaseUnit: TFEntity, HasCost { - var cost: Int +class BaseUnit: TFEntity { init(textureNames: [String], size: CGSize, key: String, position: CGPoint, maxHealth: CGFloat, entityManager: EntityManager, - cost: Int, velocity: CGVector, team: Team) { - self.cost = cost super.init() createHealthComponent(maxHealth: maxHealth, entityManager: entityManager) createSpriteComponent(textureNames: textureNames, size: size, key: key, position: position) @@ -52,6 +49,26 @@ class BaseUnit: TFEntity, HasCost { createPlayerComponent(team: team) } + override func collide(with other: any Collidable) -> TFEvent? { + let superEvent = super.collide(with: other) + guard let healthComponent = self.component(ofType: HealthComponent.self) else { + return superEvent + } + + if let superEvent = superEvent { + return superEvent.concurrentlyWith(other.collide(with: healthComponent)) + } + return other.collide(with: healthComponent) + } + + override func collide(with damageComponent: DamageComponent) -> TFEvent? { + guard self.hasComponent(ofType: HealthComponent.self) else { + return nil + } + // No call to super here as super is done on collide with Collidable above. + return DamageEvent(on: self.id, at: Date().timeIntervalSince1970, with: damageComponent.attackPower) + } + private func createHealthComponent(maxHealth: CGFloat, entityManager: EntityManager) { let healthComponent = HealthComponent(maxHealth: maxHealth, entityManager: entityManager) self.addComponent(healthComponent) diff --git a/TowerForge/TowerForge/LevelManager/Entities/MeleeUnit.swift b/TowerForge/TowerForge/LevelManager/Entities/MeleeUnit.swift index 5d48a0a8..31845af1 100644 --- a/TowerForge/TowerForge/LevelManager/Entities/MeleeUnit.swift +++ b/TowerForge/TowerForge/LevelManager/Entities/MeleeUnit.swift @@ -7,27 +7,48 @@ import Foundation -class MeleeUnit: BaseUnit { +class MeleeUnit: BaseUnit, Spawnable { + static let title: String = "melee" static let textureNames = ["melee-1", "melee-2"] static let size = CGSize(width: 100, height: 100) static let key = "melee" static let maxHealth = 100.0 static let damage = 10.0 - static let cost = 10 + static var cost = 10 + static let attackRate = 10.0 + static let velocity = CGVector(dx: 10.0, dy: 0.0) - init(position: CGPoint, entityManager: EntityManager, attackRate: TimeInterval, velocity: CGVector, team: Team) { + required init(position: CGPoint, entityManager: EntityManager, team: Team) { super.init(textureNames: MeleeUnit.textureNames, size: MeleeUnit.size, key: MeleeUnit.key, position: position, maxHealth: MeleeUnit.maxHealth, entityManager: entityManager, - cost: MeleeUnit.cost, - velocity: velocity, + velocity: MeleeUnit.velocity, team: team) - self.addComponent(DamageComponent(attackRate: attackRate, + self.addComponent(DamageComponent(attackRate: MeleeUnit.attackRate, attackPower: MeleeUnit.damage, temporary: false, entityManager: entityManager)) } + + override func collide(with other: any Collidable) -> (any TFEvent)? { + let superEvent = super.collide(with: other) + guard let damageComponent = self.component(ofType: DamageComponent.self) else { + return superEvent + } + if let superEvent = superEvent { + return superEvent.concurrentlyWith(other.collide(with: damageComponent)) + } + return other.collide(with: damageComponent) + } + + override func collide(with healthComponent: HealthComponent) -> (any TFEvent)? { + guard let entityId = healthComponent.entity?.id, + let damageComponent = self.component(ofType: DamageComponent.self) else { + return nil + } + return DamageEvent(on: entityId, at: Date().timeIntervalSince1970, with: damageComponent.attackPower) + } } diff --git a/TowerForge/TowerForge/LevelManager/Entities/SoldierUnit.swift b/TowerForge/TowerForge/LevelManager/Entities/SoldierUnit.swift index 5b6daad7..451c6404 100644 --- a/TowerForge/TowerForge/LevelManager/Entities/SoldierUnit.swift +++ b/TowerForge/TowerForge/LevelManager/Entities/SoldierUnit.swift @@ -7,26 +7,28 @@ import Foundation -class SoldierUnit: BaseUnit { +class SoldierUnit: BaseUnit, Spawnable { + static let title: String = "soldier" static let textureNames = ["Shooter-1", "Shooter-2"] static let size = CGSize(width: 100, height: 100) static let key = "shoot" static let maxHealth = 100.0 static let damage = 10.0 - static let cost = 5 + static var cost = 5 + static let attackRate = 10.0 + static let velocity = CGVector(dx: 10.0, dy: 0.0) - init(position: CGPoint, entityManager: EntityManager, attackRate: TimeInterval, velocity: CGVector, team: Team) { + required init(position: CGPoint, entityManager: EntityManager, team: Team) { super.init(textureNames: SoldierUnit.textureNames, size: SoldierUnit.size, key: SoldierUnit.key, position: position, maxHealth: SoldierUnit.maxHealth, entityManager: entityManager, - cost: SoldierUnit.cost, - velocity: velocity, + velocity: SoldierUnit.velocity, team: team) - self.addComponent(ShootingComponent(fireRate: attackRate, + self.addComponent(ShootingComponent(fireRate: SoldierUnit.attackRate, range: 1.0, entityManager: entityManager, attackPower: 10.0 diff --git a/TowerForge/TowerForge/LevelManager/Entities/SpawnableEntities.swift b/TowerForge/TowerForge/LevelManager/Entities/SpawnableEntities.swift new file mode 100644 index 00000000..05e3f77b --- /dev/null +++ b/TowerForge/TowerForge/LevelManager/Entities/SpawnableEntities.swift @@ -0,0 +1,13 @@ +// +// Entities.swift +// TowerForge +// +// Created by Zheng Ze on 21/3/24. +// + +import Foundation + +struct SpawnableEntities { + static let possibleUnits: [(BaseUnit & Spawnable).Type] = [MeleeUnit.self, + SoldierUnit.self] +} diff --git a/TowerForge/TowerForge/LevelManager/Events/EventTarget.swift b/TowerForge/TowerForge/LevelManager/Events/EventTarget.swift index a11a6718..cc7975cd 100644 --- a/TowerForge/TowerForge/LevelManager/Events/EventTarget.swift +++ b/TowerForge/TowerForge/LevelManager/Events/EventTarget.swift @@ -8,6 +8,5 @@ import Foundation protocol EventTarget: AnyObject { - func add(_ entity: TFEntity) func system(ofType type: T.Type) -> T? } diff --git a/TowerForge/TowerForge/LevelManager/Events/TFEvent.swift b/TowerForge/TowerForge/LevelManager/Events/TFEvent.swift index 1cd7cc79..f149865b 100644 --- a/TowerForge/TowerForge/LevelManager/Events/TFEvent.swift +++ b/TowerForge/TowerForge/LevelManager/Events/TFEvent.swift @@ -15,7 +15,10 @@ protocol TFEvent { } extension TFEvent { - func concurrentlyWith(_ otherEvent: TFEvent) -> TFEvent { - ConcurrentEvent(self, otherEvent) + func concurrentlyWith(_ otherEvent: TFEvent?) -> TFEvent { + guard let otherEvent = otherEvent else { + return self + } + return ConcurrentEvent(self, otherEvent) } } diff --git a/TowerForge/TowerForge/LevelManager/Generators/UnitGenerator.swift b/TowerForge/TowerForge/LevelManager/Generators/UnitGenerator.swift index a27a0199..e3239257 100644 --- a/TowerForge/TowerForge/LevelManager/Generators/UnitGenerator.swift +++ b/TowerForge/TowerForge/LevelManager/Generators/UnitGenerator.swift @@ -9,42 +9,9 @@ import Foundation import SpriteKit class UnitGenerator { - static func spawnUnit(ofType type: UnitType, at position: CGPoint, player: Player, entityManager: EntityManager, scene: SKScene) { - switch type { - case .melee: - spawnMelee(at: position, player: player, entityManager: entityManager, scene: scene) - case .soldier: - spawnSoldier(at: position, player: player, entityManager: entityManager, scene: scene) - } - } - static func spawnMelee(at position: CGPoint, player: Player, entityManager: EntityManager, scene: SKScene) { - // TODO: Change the default value and abstract as constant - let unit = MeleeUnit(position: position, - entityManager: entityManager, - attackRate: 10.0, - velocity: CGVector(dx: 10.0, dy: 0.0), - team: Team(player: player)) - let spriteComponent = unit.component(ofType: SpriteComponent.self) - spriteComponent?.node.position = position - entityManager.add(unit) - if let node = spriteComponent?.node { - scene.addChild(node) - node.playAnimation() - } - } - static func spawnSoldier(at position: CGPoint, player: Player, entityManager: EntityManager, scene: SKScene) { - // TODO: Change the default value and abstract as constant - let unit = SoldierUnit(position: position, - entityManager: entityManager, - attackRate: 10.0, - velocity: CGVector(dx: 10.0, dy: 0.0), - team: Team(player: player)) - let spriteComponent = unit.component(ofType: SpriteComponent.self) - spriteComponent?.node.position = position - entityManager.add(unit) - if let node = spriteComponent?.node { - scene.addChild(node) - node.playAnimation() - } + static func spawn(ofType type: T.Type, at position: CGPoint, + player: Player, entityManager: EntityManager) -> T { + let unit = type.init(position: position, entityManager: entityManager, team: Team(player: player)) + return unit } } diff --git a/TowerForge/TowerForge/LevelManager/SystemManager.swift b/TowerForge/TowerForge/LevelManager/SystemManager.swift new file mode 100644 index 00000000..ab19cfde --- /dev/null +++ b/TowerForge/TowerForge/LevelManager/SystemManager.swift @@ -0,0 +1,18 @@ +// +// SystemManager.swift +// TowerForge +// +// Created by Zheng Ze on 20/3/24. +// + +import Foundation + +class SystemManager { + func system(ofType type: T.Type) -> T? { + nil + } + + func update(_ deltaTime: TimeInterval) { + + } +} diff --git a/TowerForge/TowerForge/LevelManager/TFEntity.swift b/TowerForge/TowerForge/LevelManager/TFEntity.swift index e24979b7..4e7bc4f7 100644 --- a/TowerForge/TowerForge/LevelManager/TFEntity.swift +++ b/TowerForge/TowerForge/LevelManager/TFEntity.swift @@ -7,7 +7,7 @@ import Foundation -class TFEntity { +class TFEntity: Collidable { let id: UUID private(set) var components: [UUID: TFComponent] @@ -45,4 +45,17 @@ class TFEntity { componentToBeRemoved.willRemoveFromEntity() components.removeValue(forKey: componentToBeRemoved.id) } + + // To be overriden by sub classes as needed + func collide(with other: any Collidable) -> TFEvent? { + nil + } + + func collide(with damageComponent: DamageComponent) -> TFEvent? { + nil + } + + func collide(with healthComponent: HealthComponent) -> TFEvent? { + nil + } } diff --git a/TowerForge/TowerForge/Nodes/UnitNode.swift b/TowerForge/TowerForge/Nodes/UnitNode.swift index d9748b7a..92b3cdcd 100644 --- a/TowerForge/TowerForge/Nodes/UnitNode.swift +++ b/TowerForge/TowerForge/Nodes/UnitNode.swift @@ -13,7 +13,7 @@ protocol UnitNodeDelegate: AnyObject { } class UnitNode: TFSpriteNode { - var unitType: UnitType? + let type: (BaseUnit & Spawnable).Type weak var delegate: UnitNodeDelegate? var purchasable = true var teamController: TeamController? @@ -21,11 +21,11 @@ class UnitNode: TFSpriteNode { var unitCostLabel: SKLabelNode! var backgroundNode: SKSpriteNode! - convenience init(unitType: UnitType) { - self.init(imageName: unitType.title, height: 200.0, width: 140.0) - self.setupUnitCostLabel(cost: unitType.cost) - self.unitType = unitType + init(ofType type: T.Type) { + self.type = type + super.init(imageName: type.title, height: 200.0, width: 140.0) + setupUnitCostLabel(cost: type.cost) self.zPosition = 10.0 isUserInteractionEnabled = true @@ -33,6 +33,12 @@ class UnitNode: TFSpriteNode { backgroundNode.zPosition = -1 addChild(backgroundNode) } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + private func setupUnitCostLabel(cost amount: Int) { unitCostLabel = SKLabelNode() unitCostLabel.name = "unitLabel" @@ -45,11 +51,9 @@ class UnitNode: TFSpriteNode { self.addChild(unitCostLabel) } override func touchesBegan(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first else { return } - if !purchasable { + guard touches.first != nil, purchasable else { return } delegate?.unitNodeDidSelect(self) - } } diff --git a/TowerForge/TowerForge/Nodes/UnitSelectionNode.swift b/TowerForge/TowerForge/Nodes/UnitSelectionNode.swift index 575fa697..04e85e31 100644 --- a/TowerForge/TowerForge/Nodes/UnitSelectionNode.swift +++ b/TowerForge/TowerForge/Nodes/UnitSelectionNode.swift @@ -9,7 +9,7 @@ import Foundation import UIKit protocol UnitSelectionNodeDelegate: AnyObject { - func unitSelectionNodeDidSpawn(unitType: UnitType, position: CGPoint) + func unitSelectionNodeDidSpawn(ofType type: T.Type, position: CGPoint) } class UnitSelectionNode: TFSpriteNode, UnitNodeDelegate { @@ -21,33 +21,37 @@ class UnitSelectionNode: TFSpriteNode, UnitNodeDelegate { } var unitNodes: [UnitNode] = [] var selectedNode: UnitNode? + init() { super.init(textures: nil, height: 200.0, width: 100.0) isUserInteractionEnabled = true - for unit in UnitType.possibleUnits { - let unitNode = UnitNode(unitType: unit) + let possibleUnits: [(BaseUnit & Spawnable).Type] = SpawnableEntities.possibleUnits + for type in possibleUnits { + let unitNode = UnitNode(ofType: type) unitNodes.append(unitNode) unitNode.delegate = self addChild(unitNode) } } + + @available(*, unavailable) required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + private func updateUnitAlphas() { for unitNode in unitNodes { - if let unit = unitNode.unitType { - if unit.cost <= availablePoints { - unitNode.alpha = 1.0 - unitNode.purchasable = true - } else { - unitNode.alpha = 0.5 - unitNode.purchasable = false - } + if unitNode.type.cost <= availablePoints { + unitNode.alpha = 1.0 + unitNode.purchasable = true + } else { + unitNode.alpha = 0.5 + unitNode.purchasable = false } } } + func unitNodeDidSelect(_ unitNode: UnitNode) { if unitNode.purchasable { selectedNode = unitNode @@ -55,9 +59,9 @@ class UnitSelectionNode: TFSpriteNode, UnitNodeDelegate { } func unitNodeDidSpawn(_ position: CGPoint) { - guard let selectedUnitType = self.selectedNode?.unitType else { + guard let selectedType = self.selectedNode?.type else { return } - delegate?.unitSelectionNodeDidSpawn(unitType: selectedUnitType, position: position) + delegate?.unitSelectionNodeDidSpawn(ofType: selectedType, position: position) } } diff --git a/TowerForge/TowerForge/Protocols/HasCost.swift b/TowerForge/TowerForge/Protocols/HasCost.swift deleted file mode 100644 index 5ad04a5e..00000000 --- a/TowerForge/TowerForge/Protocols/HasCost.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// HasCost.swift -// TowerForge -// -// Created by Vanessa Mae on 19/03/24. -// - -import Foundation - -protocol HasCost { - var cost: Int { get set } -} diff --git a/TowerForge/TowerForge/Protocols/Spawnable.swift b/TowerForge/TowerForge/Protocols/Spawnable.swift new file mode 100644 index 00000000..3b570f3c --- /dev/null +++ b/TowerForge/TowerForge/Protocols/Spawnable.swift @@ -0,0 +1,14 @@ +// +// Spawnable.swift +// TowerForge +// +// Created by Vanessa Mae on 19/03/24. +// + +import Foundation + +protocol Spawnable { + init(position: CGPoint, entityManager: EntityManager, team: Team) + static var cost: Int { get set } + static var title: String { get } +} diff --git a/TowerForge/TowerForge/Rendering/Renderable.swift b/TowerForge/TowerForge/Rendering/Renderable.swift new file mode 100644 index 00000000..400a23d8 --- /dev/null +++ b/TowerForge/TowerForge/Rendering/Renderable.swift @@ -0,0 +1,12 @@ +// +// Renderable.swift +// TowerForge +// +// Created by Zheng Ze on 21/3/24. +// + +import Foundation + +protocol Renderable: AnyObject { + func entitiesToRender() -> [TFEntity] +} diff --git a/TowerForge/TowerForge/Rendering/Renderer.swift b/TowerForge/TowerForge/Rendering/Renderer.swift new file mode 100644 index 00000000..4289a366 --- /dev/null +++ b/TowerForge/TowerForge/Rendering/Renderer.swift @@ -0,0 +1,71 @@ +// +// Renderer.swift +// TowerForge +// +// Created by Zheng Ze on 21/3/24. +// + +import Foundation +import SpriteKit + +class Renderer { + private unowned var target: Renderable + private unowned var scene: SKScene? + + private var renderedNodes: [UUID: TFAnimatableNode] = [:] + + init(target: Renderable, scene: GameScene?) { + self.target = target + self.scene = scene + } + + func render() { + var nodesToBeRemoved = renderedNodes + + for entity in target.entitiesToRender() { + guard nodesToBeRemoved[entity.id] != nil else { + addAndCache(entity: entity) + continue + } + + nodesToBeRemoved.removeValue(forKey: entity.id) + update(entity: entity) + } + + for entityId in nodesToBeRemoved.keys { + removeAndUncache(with: entityId) + } + } + + private func update(entity: TFEntity) { + guard let positionComponent = entity.component(ofType: PositionComponent.self), + let node = renderedNodes[entity.id] else { + return + } + + node.position = positionComponent.position + } + + private func addAndCache(entity: TFEntity) { + guard let spriteComponent = entity.component(ofType: SpriteComponent.self), + let positionComponent = entity.component(ofType: PositionComponent.self) else { + return + } + + let node = TFAnimatableNode(textures: spriteComponent.textures, + height: spriteComponent.height, + width: spriteComponent.width, + animatableKey: spriteComponent.animatableKey) + node.position = positionComponent.position + node.name = entity.id.uuidString + renderedNodes[entity.id] = node + scene?.addChild(node) + } + + private func removeAndUncache(with id: UUID) { + guard let node = renderedNodes.removeValue(forKey: id) else { + return + } + node.removeFromParent() + } +} diff --git a/TowerForge/TowerForge/SceneManagerDelegate.swift b/TowerForge/TowerForge/SceneManagerDelegate.swift index 7b07d484..5d5f09a8 100644 --- a/TowerForge/TowerForge/SceneManagerDelegate.swift +++ b/TowerForge/TowerForge/SceneManagerDelegate.swift @@ -7,7 +7,7 @@ import Foundation -protocol SceneManagerDelegate { +protocol SceneManagerDelegate: AnyObject { func showMenuScene() func showLevelScene() func showGameLevelScene(level: Int) diff --git a/TowerForge/TowerForge/SceneUpdateDelegate.swift b/TowerForge/TowerForge/SceneUpdateDelegate.swift new file mode 100644 index 00000000..39e6b7a7 --- /dev/null +++ b/TowerForge/TowerForge/SceneUpdateDelegate.swift @@ -0,0 +1,15 @@ +// +// SceneUpdateDelegate.swift +// TowerForge +// +// Created by Zheng Ze on 20/3/24. +// + +import Foundation + +protocol SceneUpdateDelegate: AnyObject { + func update(deltaTime: TimeInterval) + func touch(at location: CGPoint) + func contactBegin(between nodeA: TFAnimatableNode, and nodeB: TFAnimatableNode) + func contactEnd(between nodeA: TFAnimatableNode, and nodeB: TFAnimatableNode) +} diff --git a/TowerForge/TowerForge/Scenes/GameScene.swift b/TowerForge/TowerForge/Scenes/GameScene.swift index adbf027a..d5ac5424 100644 --- a/TowerForge/TowerForge/Scenes/GameScene.swift +++ b/TowerForge/TowerForge/Scenes/GameScene.swift @@ -8,62 +8,50 @@ import SpriteKit import GameplayKit -class GameScene: SKScene, UnitSelectionNodeDelegate { - var sceneManagerDelegate: SceneManagerDelegate? - +class GameScene: SKScene { private var lastUpdatedTimeInterval = TimeInterval(0) - private var entityManager: EntityManager? - private var selectionNode: UnitSelectionNode? - - override func didMove(to view: SKView) { - entityManager = EntityManager() - guard let entityManager = entityManager else { - return - } - selectionNode = UnitSelectionNode() - guard var selectionNode = selectionNode else { - return - } - selectionNode.delegate = self - addChild(selectionNode) - - // Position unit selection node on the left side of the screen - selectionNode.position = CGPoint(x: selectionNode.frame.width / 2, y: frame.midY) + unowned var updateDelegate: SceneUpdateDelegate? + unowned var sceneManagerDelegate: SceneManagerDelegate? - // Calculate vertical spacing between unit nodes - let verticalSpacing = selectionNode.frame.height - var verticalY = 10.0 - // Position unit nodes vertically aligned - for (index, unitNode) in selectionNode.unitNodes.enumerated() { - unitNode.position = CGPoint(x: selectionNode.frame.width / 2, - y: verticalY) - print(unitNode.position) - verticalY += verticalSpacing - } + override func sceneDidLoad() { + super.sceneDidLoad() + physicsWorld.contactDelegate = self } + override func touchesBegan(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first, let selectionNode = selectionNode else { + guard let touch = touches.first else { return } let location = touch.location(in: self) - selectionNode.unitNodeDidSpawn(location) + updateDelegate?.touch(at: location) } override func update(_ currentTime: TimeInterval) { - guard let entityManager = entityManager else { - return - } if lastUpdatedTimeInterval == TimeInterval(0) { lastUpdatedTimeInterval = currentTime } + let changeInTime = currentTime - lastUpdatedTimeInterval lastUpdatedTimeInterval = currentTime - entityManager.update(changeInTime) + updateDelegate?.update(deltaTime: changeInTime) } - func unitSelectionNodeDidSpawn(unitType: UnitType, position: CGPoint) { - guard var entityManager = entityManager else { +} + +extension GameScene: SKPhysicsContactDelegate { + public func didBegin(_ contact: SKPhysicsContact) { + guard let nodeA = contact.bodyA.node as? TFAnimatableNode, + let nodeB = contact.bodyB.node as? TFAnimatableNode else { return } - UnitGenerator.spawnUnit(ofType: unitType, at: position, player: Player.ownPlayer, entityManager: entityManager, scene: self) + updateDelegate?.contactBegin(between: nodeA, and: nodeB) + } + + public func didEnd(_ contact: SKPhysicsContact) { + guard let nodeA = contact.bodyA.node as? TFAnimatableNode, + let nodeB = contact.bodyB.node as? TFAnimatableNode else { + return + } + + updateDelegate?.contactEnd(between: nodeA, and: nodeB) } } diff --git a/TowerForge/TowerForge/TFCore/TFAnimatableNode.swift b/TowerForge/TowerForge/TFCore/TFAnimatableNode.swift index ba80590c..3581159b 100644 --- a/TowerForge/TowerForge/TFCore/TFAnimatableNode.swift +++ b/TowerForge/TowerForge/TFCore/TFAnimatableNode.swift @@ -17,6 +17,7 @@ class TFAnimatableNode: TFSpriteNode, Animatable { super.init(textures: textures, height: height, width: width) } + @available(*, unavailable) required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/TowerForge/TowerForge/TFCore/TFButton.swift b/TowerForge/TowerForge/TFCore/TFButton.swift index f2953ade..51b965e4 100644 --- a/TowerForge/TowerForge/TFCore/TFButton.swift +++ b/TowerForge/TowerForge/TFCore/TFButton.swift @@ -22,6 +22,7 @@ class TFButton: SKSpriteNode { addChild(defaultButton) } + @available(*, unavailable) required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/TowerForge/TowerForge/TFCore/TFSpriteNode.swift b/TowerForge/TowerForge/TFCore/TFSpriteNode.swift index c10f7627..59997dd3 100644 --- a/TowerForge/TowerForge/TFCore/TFSpriteNode.swift +++ b/TowerForge/TowerForge/TFCore/TFSpriteNode.swift @@ -21,13 +21,25 @@ class TFSpriteNode: SKSpriteNode { self.width = width self.height = height super.init(texture: textures?.mainTexture, color: .clear, size: CGSize(width: width, height: height)) + setUpPhysicsBody() } + init(imageName: String, height: CGFloat, width: CGFloat) { self.width = width self.height = height super.init(texture: SKTexture(imageNamed: imageName), color: .clear, size: CGSize(width: width, height: height)) } + + @available(*, unavailable) required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + // Only for collision detection + private func setUpPhysicsBody() { + self.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: width, height: height)) + self.physicsBody?.affectedByGravity = false + self.physicsBody?.collisionBitMask = .min + self.physicsBody?.contactTestBitMask = .max + } }