From 25a2553618d8afa542716c302de75c758b843725 Mon Sep 17 00:00:00 2001 From: zheng-ze Date: Fri, 22 Mar 2024 16:29:36 +0800 Subject: [PATCH] Fix bug where contact is only detected once. Update damage event generation to respect the component's attack rate. --- .../TowerForge.xcodeproj/project.pbxproj | 4 +++ .../TowerForge/GameViewController.swift | 9 ++--- TowerForge/TowerForge/GameWorld.swift | 34 ++++++++++++++++--- .../LevelManager/Collision/TFContact.swift | 18 ++++++++++ .../GameComponents/DamageComponent.swift | 14 +++++++- .../LevelManager/Entities/Arrow.swift | 5 ++- .../LevelManager/Entities/MeleeUnit.swift | 7 ++-- .../TowerForge/SceneUpdateDelegate.swift | 4 +-- TowerForge/TowerForge/Scenes/GameScene.swift | 8 ++--- .../TowerForge/TFCore/TFSpriteNode.swift | 2 +- 10 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 TowerForge/TowerForge/LevelManager/Collision/TFContact.swift diff --git a/TowerForge/TowerForge.xcodeproj/project.pbxproj b/TowerForge/TowerForge.xcodeproj/project.pbxproj index f55f27f2..f3b66c8e 100644 --- a/TowerForge/TowerForge.xcodeproj/project.pbxproj +++ b/TowerForge/TowerForge.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 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 */; }; + 3CE951582BAD724D008B2785 /* TFContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE951572BAD724D008B2785 /* TFContact.swift */; }; 5200624E2BA8D597000DBA30 /* AiComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5200624D2BA8D597000DBA30 /* AiComponent.swift */; }; 520062522BA8DA09000DBA30 /* UnitGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520062512BA8DA09000DBA30 /* UnitGenerator.swift */; }; 520062562BA8E026000DBA30 /* Spawnable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520062552BA8E026000DBA30 /* Spawnable.swift */; }; @@ -117,6 +118,7 @@ 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 = ""; }; + 3CE951572BAD724D008B2785 /* TFContact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TFContact.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 /* Spawnable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spawnable.swift; sourceTree = ""; }; @@ -256,6 +258,7 @@ isa = PBXGroup; children = ( 3CE951552BACA0CF008B2785 /* Collidable.swift */, + 3CE951572BAD724D008B2785 /* TFContact.swift */, ); path = Collision; sourceTree = ""; @@ -607,6 +610,7 @@ 3C9955A32BA47DBB00D33FA5 /* BaseUnit.swift in Sources */, 3C9955BA2BA5637200D33FA5 /* DamageEvent.swift in Sources */, 3C9955CA2BA5888F00D33FA5 /* SpawnEvent.swift in Sources */, + 3CE951582BAD724D008B2785 /* TFContact.swift in Sources */, 5295A2132BAAEA16005018A8 /* UnitNode.swift in Sources */, 52DF5FF32BA351E100135367 /* SpriteComponent.swift in Sources */, 3CE9514B2BAC83FA008B2785 /* SpawnableEntities.swift in Sources */, diff --git a/TowerForge/TowerForge/GameViewController.swift b/TowerForge/TowerForge/GameViewController.swift index 69ac74ae..e38a6ec5 100644 --- a/TowerForge/TowerForge/GameViewController.swift +++ b/TowerForge/TowerForge/GameViewController.swift @@ -48,20 +48,20 @@ extension GameViewController: SceneUpdateDelegate { gameWorld?.update(deltaTime: deltaTime) } - func contactBegin(between nodeA: TFAnimatableNode, and nodeB: TFAnimatableNode) { + func contactBegin(between nodeA: TFSpriteNode, and nodeB: TFSpriteNode) { 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) + gameWorld?.contactDidBegin(between: idA, and: idB) } - func contactEnd(between nodeA: TFAnimatableNode, and nodeB: TFAnimatableNode) { + func contactEnd(between nodeA: TFSpriteNode, and nodeB: TFSpriteNode) { 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) + gameWorld?.contactDidEnd(between: idA, and: idB) } } @@ -90,6 +90,7 @@ extension GameViewController: SceneManagerDelegate { view.ignoresSiblingOrder = true // to render nodes more efficiently view.showsFPS = true view.showsNodeCount = true + view.showsPhysics = true } } } diff --git a/TowerForge/TowerForge/GameWorld.swift b/TowerForge/TowerForge/GameWorld.swift index aa054cfc..79fd7041 100644 --- a/TowerForge/TowerForge/GameWorld.swift +++ b/TowerForge/TowerForge/GameWorld.swift @@ -14,6 +14,7 @@ class GameWorld { private var eventManager: EventManager private var selectionNode: UnitSelectionNode private var renderer: Renderer? + private var entitiesInContact: Set = [] init(scene: GameScene?) { self.scene = scene @@ -27,6 +28,10 @@ class GameWorld { } func update(deltaTime: TimeInterval) { + for contact in entitiesInContact { + handleContact(between: contact.entityIdA, and: contact.entityIdB) + } + systemManager.update(deltaTime) eventManager.executeEvents(in: self) entityManager.update(deltaTime) @@ -37,16 +42,37 @@ class GameWorld { 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 { + // TODO: Move contact handling to a system + func contactDidBegin(between idA: UUID, and idB: UUID) { + guard idA != idB, entityManager.entity(with: idA) != nil, entityManager.entity(with: idB) != nil else { + return + } + + entitiesInContact.insert(TFContact(entityIdA: idA, entityIdB: idB)) + } + + func contactDidEnd(between idA: UUID, and idB: UUID) { + guard entitiesInContact.remove(TFContact(entityIdA: idA, entityIdB: idB)) != nil else { + return + } + + handleSeparation(between: idA, and: idB) + } + + private func handleContact(between idA: UUID, and idB: UUID) { + guard let entityA = entityManager.entity(with: idA), let entityB = entityManager.entity(with: idB) else { + entitiesInContact.remove(TFContact(entityIdA: idA, entityIdB: idB)) + return + } + + guard let event = entityA.collide(with: entityB) else { return } eventManager.add(event) } - func handleSeparation(between idA: UUID, and idB: UUID) { + private func handleSeparation(between idA: UUID, and idB: UUID) { guard let entityA = entityManager.entity(with: idA), let entityB = entityManager.entity(with: idB) else { return } diff --git a/TowerForge/TowerForge/LevelManager/Collision/TFContact.swift b/TowerForge/TowerForge/LevelManager/Collision/TFContact.swift new file mode 100644 index 00000000..5177c9e3 --- /dev/null +++ b/TowerForge/TowerForge/LevelManager/Collision/TFContact.swift @@ -0,0 +1,18 @@ +// +// TFContact.swift +// TowerForge +// +// Created by Zheng Ze on 22/3/24. +// + +import Foundation + +struct TFContact: Hashable { + let entityIdA: UUID + let entityIdB: UUID + + static func == (lhs: TFContact, rhs: TFContact) -> Bool { + (lhs.entityIdA == rhs.entityIdA && lhs.entityIdB == rhs.entityIdB) + || (lhs.entityIdA == rhs.entityIdB && lhs.entityIdB == rhs.entityIdA) + } +} diff --git a/TowerForge/TowerForge/LevelManager/Components/GameComponents/DamageComponent.swift b/TowerForge/TowerForge/LevelManager/Components/GameComponents/DamageComponent.swift index 8f98c4d3..30583c95 100644 --- a/TowerForge/TowerForge/LevelManager/Components/GameComponents/DamageComponent.swift +++ b/TowerForge/TowerForge/LevelManager/Components/GameComponents/DamageComponent.swift @@ -24,6 +24,18 @@ class DamageComponent: TFComponent { super.init() } + var canDamage: Bool { + CACurrentMediaTime() - lastAttackTime >= attackRate + } + + func damage(_ healthComponent: HealthComponent) -> DamageEvent? { + guard canDamage, let entityId = healthComponent.entity?.id else { + return nil + } + lastAttackTime = CACurrentMediaTime() + return DamageEvent(on: entityId, at: lastAttackTime, with: attackPower) + } + override func update(deltaTime: TimeInterval) { super.update(deltaTime: deltaTime) @@ -32,7 +44,7 @@ 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 { diff --git a/TowerForge/TowerForge/LevelManager/Entities/Arrow.swift b/TowerForge/TowerForge/LevelManager/Entities/Arrow.swift index 09546542..05b8e3b4 100644 --- a/TowerForge/TowerForge/LevelManager/Entities/Arrow.swift +++ b/TowerForge/TowerForge/LevelManager/Entities/Arrow.swift @@ -37,10 +37,9 @@ class Arrow: BaseProjectile { } override func collide(with healthComponent: HealthComponent) -> TFEvent? { - guard let entityId = healthComponent.entity?.id, - let damageComponent = self.component(ofType: DamageComponent.self) else { + guard let damageComponent = self.component(ofType: DamageComponent.self) else { return nil } - return DamageEvent(on: entityId, at: Date().timeIntervalSince1970, with: damageComponent.attackPower) + return damageComponent.damage(healthComponent) } } diff --git a/TowerForge/TowerForge/LevelManager/Entities/MeleeUnit.swift b/TowerForge/TowerForge/LevelManager/Entities/MeleeUnit.swift index 31845af1..5e0f60a8 100644 --- a/TowerForge/TowerForge/LevelManager/Entities/MeleeUnit.swift +++ b/TowerForge/TowerForge/LevelManager/Entities/MeleeUnit.swift @@ -15,7 +15,7 @@ class MeleeUnit: BaseUnit, Spawnable { static let maxHealth = 100.0 static let damage = 10.0 static var cost = 10 - static let attackRate = 10.0 + static let attackRate = 1.0 static let velocity = CGVector(dx: 10.0, dy: 0.0) required init(position: CGPoint, entityManager: EntityManager, team: Team) { @@ -45,10 +45,9 @@ class MeleeUnit: BaseUnit, Spawnable { } override func collide(with healthComponent: HealthComponent) -> (any TFEvent)? { - guard let entityId = healthComponent.entity?.id, - let damageComponent = self.component(ofType: DamageComponent.self) else { + guard let damageComponent = self.component(ofType: DamageComponent.self) else { return nil } - return DamageEvent(on: entityId, at: Date().timeIntervalSince1970, with: damageComponent.attackPower) + return damageComponent.damage(healthComponent) } } diff --git a/TowerForge/TowerForge/SceneUpdateDelegate.swift b/TowerForge/TowerForge/SceneUpdateDelegate.swift index 39e6b7a7..17e973e0 100644 --- a/TowerForge/TowerForge/SceneUpdateDelegate.swift +++ b/TowerForge/TowerForge/SceneUpdateDelegate.swift @@ -10,6 +10,6 @@ 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) + func contactBegin(between nodeA: TFSpriteNode, and nodeB: TFSpriteNode) + func contactEnd(between nodeA: TFSpriteNode, and nodeB: TFSpriteNode) } diff --git a/TowerForge/TowerForge/Scenes/GameScene.swift b/TowerForge/TowerForge/Scenes/GameScene.swift index d5ac5424..c4757864 100644 --- a/TowerForge/TowerForge/Scenes/GameScene.swift +++ b/TowerForge/TowerForge/Scenes/GameScene.swift @@ -39,16 +39,16 @@ class GameScene: SKScene { extension GameScene: SKPhysicsContactDelegate { public func didBegin(_ contact: SKPhysicsContact) { - guard let nodeA = contact.bodyA.node as? TFAnimatableNode, - let nodeB = contact.bodyB.node as? TFAnimatableNode else { + guard let nodeA = contact.bodyA.node as? TFSpriteNode, + let nodeB = contact.bodyB.node as? TFSpriteNode else { return } 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 { + guard let nodeA = contact.bodyA.node as? TFSpriteNode, + let nodeB = contact.bodyB.node as? TFSpriteNode else { return } diff --git a/TowerForge/TowerForge/TFCore/TFSpriteNode.swift b/TowerForge/TowerForge/TFCore/TFSpriteNode.swift index 59997dd3..1f283281 100644 --- a/TowerForge/TowerForge/TFCore/TFSpriteNode.swift +++ b/TowerForge/TowerForge/TFCore/TFSpriteNode.swift @@ -34,7 +34,7 @@ class TFSpriteNode: SKSpriteNode { 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))