9e501cc4e8
Complete implementation of a 2D top-down arcade collector game where players control a rolling suitcase through an airport, collecting good dogs and green people while avoiding bad dogs and gray people. Features: - Touch & drag controls for suitcase movement - Automatic scrolling airport floor with tile pattern - 4 entity types: good dogs (small/big), bad dogs, green/gray humans - Spawn system with configurable distribution rates - Collision detection with visual feedback effects - Score tracking with high score persistence - 90-second time limit with 10 dogs + 5 humans goal - 3 lives system with invincibility frames - Menu, Game, GameOver, and Victory scenes - German UI text (Created by Ingo K.) Technical: - iOS 15+ with SpriteKit framework - Modular architecture with Nodes, Managers, Scenes - Physics-based collision detection - UserDefaults for score persistence
198 lines
6.2 KiB
Swift
198 lines
6.2 KiB
Swift
//
|
|
// CollisionManager.swift
|
|
// RollkofferSimulator
|
|
//
|
|
// Created by Ingo K.
|
|
//
|
|
|
|
import SpriteKit
|
|
|
|
/// Protocol for collision event handling
|
|
protocol CollisionManagerDelegate: AnyObject {
|
|
func didCollectGoodDog(points: Int)
|
|
func didCollectGreenHuman(points: Int)
|
|
func didHitHarmfulEntity()
|
|
}
|
|
|
|
/// Manages collision detection and response
|
|
class CollisionManager: NSObject, SKPhysicsContactDelegate {
|
|
|
|
// MARK: - Properties
|
|
weak var delegate: CollisionManagerDelegate?
|
|
|
|
// MARK: - SKPhysicsContactDelegate
|
|
func didBegin(_ contact: SKPhysicsContact) {
|
|
let collision = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
|
|
|
|
// Check if suitcase is involved
|
|
guard collision & Constants.PhysicsCategory.suitcase != 0 else { return }
|
|
|
|
let otherBody: SKPhysicsBody
|
|
if contact.bodyA.categoryBitMask == Constants.PhysicsCategory.suitcase {
|
|
otherBody = contact.bodyB
|
|
} else {
|
|
otherBody = contact.bodyA
|
|
}
|
|
|
|
handleCollision(with: otherBody)
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
private func handleCollision(with body: SKPhysicsBody) {
|
|
guard let node = body.node else { return }
|
|
|
|
switch body.categoryBitMask {
|
|
case Constants.PhysicsCategory.goodDog:
|
|
handleGoodDogCollision(node: node)
|
|
|
|
case Constants.PhysicsCategory.badDog:
|
|
handleBadDogCollision(node: node)
|
|
|
|
case Constants.PhysicsCategory.greenHuman:
|
|
handleGreenHumanCollision(node: node)
|
|
|
|
case Constants.PhysicsCategory.grayHuman:
|
|
handleGrayHumanCollision(node: node)
|
|
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private func handleGoodDogCollision(node: SKNode) {
|
|
guard let dogNode = node as? DogNode else { return }
|
|
|
|
let points = dogNode.dogType.points
|
|
showCollectEffect(at: node.position, color: .green, text: "+\(points)")
|
|
node.removeFromParent()
|
|
delegate?.didCollectGoodDog(points: points)
|
|
}
|
|
|
|
private func handleBadDogCollision(node: SKNode) {
|
|
showDamageEffect(at: node.position)
|
|
node.removeFromParent()
|
|
delegate?.didHitHarmfulEntity()
|
|
}
|
|
|
|
private func handleGreenHumanCollision(node: SKNode) {
|
|
guard let humanNode = node as? HumanNode else { return }
|
|
|
|
let points = humanNode.humanType.points
|
|
showCollectEffect(at: node.position, color: .green, text: "+\(points)")
|
|
node.removeFromParent()
|
|
delegate?.didCollectGreenHuman(points: points)
|
|
}
|
|
|
|
private func handleGrayHumanCollision(node: SKNode) {
|
|
showDamageEffect(at: node.position)
|
|
node.removeFromParent()
|
|
delegate?.didHitHarmfulEntity()
|
|
}
|
|
|
|
// MARK: - Visual Effects
|
|
private func showCollectEffect(at position: CGPoint, color: SKColor, text: String) {
|
|
guard let scene = getScene() else { return }
|
|
|
|
// Particle burst
|
|
let emitter = SKEmitterNode()
|
|
emitter.particleTexture = nil
|
|
emitter.particleBirthRate = 50
|
|
emitter.numParticlesToEmit = 20
|
|
emitter.particleLifetime = 0.5
|
|
emitter.particleSpeed = 100
|
|
emitter.particleSpeedRange = 50
|
|
emitter.emissionAngleRange = .pi * 2
|
|
emitter.particleScale = 0.3
|
|
emitter.particleScaleRange = 0.2
|
|
emitter.particleColor = color
|
|
emitter.particleColorBlendFactor = 1.0
|
|
emitter.position = position
|
|
emitter.zPosition = Constants.ZPosition.ui - 1
|
|
|
|
// Create a simple circle shape for particles
|
|
let shape = SKShapeNode(circleOfRadius: 5)
|
|
shape.fillColor = color
|
|
shape.strokeColor = .clear
|
|
if let texture = scene.view?.texture(from: shape) {
|
|
emitter.particleTexture = texture
|
|
}
|
|
|
|
scene.addChild(emitter)
|
|
|
|
let waitAction = SKAction.wait(forDuration: 1.0)
|
|
let removeAction = SKAction.removeFromParent()
|
|
emitter.run(SKAction.sequence([waitAction, removeAction]))
|
|
|
|
// Floating text
|
|
let label = SKLabelNode(text: text)
|
|
label.fontName = "AvenirNext-Bold"
|
|
label.fontSize = 24
|
|
label.fontColor = color
|
|
label.position = position
|
|
label.zPosition = Constants.ZPosition.ui
|
|
|
|
scene.addChild(label)
|
|
|
|
let moveUp = SKAction.moveBy(x: 0, y: 50, duration: 0.5)
|
|
let fadeOut = SKAction.fadeOut(withDuration: 0.5)
|
|
let group = SKAction.group([moveUp, fadeOut])
|
|
let remove = SKAction.removeFromParent()
|
|
label.run(SKAction.sequence([group, remove]))
|
|
}
|
|
|
|
private func showDamageEffect(at position: CGPoint) {
|
|
guard let scene = getScene() else { return }
|
|
|
|
// Red flash
|
|
let flash = SKShapeNode(circleOfRadius: 30)
|
|
flash.fillColor = .red
|
|
flash.strokeColor = .clear
|
|
flash.alpha = 0.7
|
|
flash.position = position
|
|
flash.zPosition = Constants.ZPosition.ui - 1
|
|
|
|
scene.addChild(flash)
|
|
|
|
let scaleUp = SKAction.scale(to: 2.0, duration: 0.2)
|
|
let fadeOut = SKAction.fadeOut(withDuration: 0.2)
|
|
let group = SKAction.group([scaleUp, fadeOut])
|
|
let remove = SKAction.removeFromParent()
|
|
flash.run(SKAction.sequence([group, remove]))
|
|
|
|
// Floating text
|
|
let label = SKLabelNode(text: "-1 ❤️")
|
|
label.fontName = "AvenirNext-Bold"
|
|
label.fontSize = 24
|
|
label.fontColor = .red
|
|
label.position = position
|
|
label.zPosition = Constants.ZPosition.ui
|
|
|
|
scene.addChild(label)
|
|
|
|
let moveUp = SKAction.moveBy(x: 0, y: 50, duration: 0.5)
|
|
let labelFadeOut = SKAction.fadeOut(withDuration: 0.5)
|
|
let labelGroup = SKAction.group([moveUp, labelFadeOut])
|
|
let labelRemove = SKAction.removeFromParent()
|
|
label.run(SKAction.sequence([labelGroup, labelRemove]))
|
|
}
|
|
|
|
private func getScene() -> SKScene? {
|
|
// This would typically be set via dependency injection
|
|
// For simplicity, we'll use the notification pattern
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Scene Reference Extension
|
|
extension CollisionManager {
|
|
private static var sceneReference: SKScene?
|
|
|
|
func setScene(_ scene: SKScene) {
|
|
CollisionManager.sceneReference = scene
|
|
}
|
|
|
|
private func getSceneFromReference() -> SKScene? {
|
|
return CollisionManager.sceneReference
|
|
}
|
|
}
|