Add RollkofferSimulator iOS SpriteKit arcade game

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
This commit is contained in:
Claude
2025-12-19 16:47:49 +00:00
parent 8d4afcf0ad
commit 9e501cc4e8
22 changed files with 3228 additions and 0 deletions
@@ -0,0 +1,255 @@
//
// GameOverScene.swift
// RollkofferSimulator
//
// Created by Ingo K.
//
import SpriteKit
class GameOverScene: SKScene {
// MARK: - Properties
var finalScore: Int = 0
var dogsCollected: Int = 0
var humansCollected: Int = 0
var isNewHighScore: Bool = false
private var retryButton: SKShapeNode!
private var menuButton: SKShapeNode!
// MARK: - Scene Lifecycle
override func didMove(to view: SKView) {
setupBackground()
setupContent()
setupButtons()
startAnimations()
}
// MARK: - Setup
private func setupBackground() {
backgroundColor = SKColor(red: 0.15, green: 0.1, blue: 0.1, alpha: 1.0)
// Add dim pattern
for i in 0..<20 {
let x = CGFloat.random(in: 0...frame.width)
let y = CGFloat.random(in: 0...frame.height)
let size = CGFloat.random(in: 20...60)
let shape = SKShapeNode(circleOfRadius: size)
shape.position = CGPoint(x: x, y: y)
shape.fillColor = SKColor.red.withAlphaComponent(0.05)
shape.strokeColor = .clear
shape.zPosition = 0.1
addChild(shape)
}
}
private func setupContent() {
// Game Over title
let titleLabel = SKLabelNode(text: "💔 GAME OVER 💔")
titleLabel.fontName = "AvenirNext-Heavy"
titleLabel.fontSize = 42
titleLabel.fontColor = .red
titleLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.8)
titleLabel.zPosition = Constants.ZPosition.ui
addChild(titleLabel)
// Subtitle based on reason
let subtitleText: String
if dogsCollected < Constants.targetDogs || humansCollected < Constants.targetHumans {
subtitleText = "Zeit abgelaufen!"
} else {
subtitleText = "Keine Leben mehr!"
}
let subtitleLabel = SKLabelNode(text: subtitleText)
subtitleLabel.fontName = "AvenirNext-Medium"
subtitleLabel.fontSize = 22
subtitleLabel.fontColor = SKColor(white: 0.8, alpha: 1.0)
subtitleLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.72)
subtitleLabel.zPosition = Constants.ZPosition.ui
addChild(subtitleLabel)
// Score display
let scoreLabel = SKLabelNode(text: "Punkte: \(finalScore)")
scoreLabel.fontName = "AvenirNext-Bold"
scoreLabel.fontSize = 32
scoreLabel.fontColor = .white
scoreLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.58)
scoreLabel.zPosition = Constants.ZPosition.ui
addChild(scoreLabel)
// New high score indicator
if isNewHighScore {
let highScoreLabel = SKLabelNode(text: "🏆 NEUER HIGHSCORE! 🏆")
highScoreLabel.fontName = "AvenirNext-Heavy"
highScoreLabel.fontSize = 24
highScoreLabel.fontColor = SKColor.yellow
highScoreLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.65)
highScoreLabel.zPosition = Constants.ZPosition.ui
addChild(highScoreLabel)
// Animate high score
let scale = SKAction.sequence([
SKAction.scale(to: 1.1, duration: 0.5),
SKAction.scale(to: 1.0, duration: 0.5)
])
highScoreLabel.run(SKAction.repeatForever(scale))
}
// Stats display
let dogsText = "🐕 \(dogsCollected)/\(Constants.targetDogs)"
let humansText = "👤 \(humansCollected)/\(Constants.targetHumans)"
let statsLabel = SKLabelNode(text: "\(dogsText) | \(humansText)")
statsLabel.fontName = "AvenirNext-Medium"
statsLabel.fontSize = 24
statsLabel.fontColor = SKColor(white: 0.7, alpha: 1.0)
statsLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.48)
statsLabel.zPosition = Constants.ZPosition.ui
addChild(statsLabel)
// Progress indicators
let dogsProgress = min(1.0, CGFloat(dogsCollected) / CGFloat(Constants.targetDogs))
let humansProgress = min(1.0, CGFloat(humansCollected) / CGFloat(Constants.targetHumans))
addProgressBar(at: CGPoint(x: frame.midX - 60, y: frame.height * 0.42),
progress: dogsProgress, color: .orange, label: "Hunde")
addProgressBar(at: CGPoint(x: frame.midX + 60, y: frame.height * 0.42),
progress: humansProgress, color: .green, label: "Menschen")
// Sad suitcase
let sadSuitcase = PlayerNode()
sadSuitcase.position = CGPoint(x: frame.midX, y: frame.height * 0.25)
sadSuitcase.alpha = 0.6
sadSuitcase.setScale(0.8)
addChild(sadSuitcase)
// Sad face on suitcase area
let sadFace = SKLabelNode(text: "😢")
sadFace.fontSize = 30
sadFace.position = CGPoint(x: frame.midX, y: frame.height * 0.25 + 20)
sadFace.zPosition = Constants.ZPosition.ui
addChild(sadFace)
}
private func addProgressBar(at position: CGPoint, progress: CGFloat, color: SKColor, label: String) {
let barWidth: CGFloat = 80
let barHeight: CGFloat = 12
// Background
let bg = SKShapeNode(rect: CGRect(x: -barWidth / 2, y: 0, width: barWidth, height: barHeight),
cornerRadius: 6)
bg.position = position
bg.fillColor = SKColor(white: 0.3, alpha: 1.0)
bg.strokeColor = .clear
bg.zPosition = Constants.ZPosition.ui
addChild(bg)
// Progress fill
let fillWidth = barWidth * progress
if fillWidth > 0 {
let fill = SKShapeNode(rect: CGRect(x: -barWidth / 2, y: 0, width: fillWidth, height: barHeight),
cornerRadius: 6)
fill.position = position
fill.fillColor = progress >= 1.0 ? .green : color
fill.strokeColor = .clear
fill.zPosition = Constants.ZPosition.ui + 0.1
addChild(fill)
}
}
private func setupButtons() {
// Retry button
retryButton = createButton(text: "🔄 Nochmal", color: SKColor(red: 0.2, green: 0.6, blue: 0.2, alpha: 1.0))
retryButton.position = CGPoint(x: frame.midX, y: frame.height * 0.15)
retryButton.name = "retryButton"
addChild(retryButton)
// Menu button
menuButton = createButton(text: "🏠 Menü", color: SKColor(red: 0.3, green: 0.3, blue: 0.5, alpha: 1.0))
menuButton.position = CGPoint(x: frame.midX, y: frame.height * 0.08)
menuButton.name = "menuButton"
addChild(menuButton)
}
private func createButton(text: String, color: SKColor) -> SKShapeNode {
let buttonWidth: CGFloat = 180
let buttonHeight: CGFloat = 50
let button = SKShapeNode(rect: CGRect(x: -buttonWidth / 2, y: -buttonHeight / 2,
width: buttonWidth, height: buttonHeight),
cornerRadius: 12)
button.fillColor = color
button.strokeColor = color.withAlphaComponent(0.5)
button.lineWidth = 2
button.zPosition = Constants.ZPosition.ui
let label = SKLabelNode(text: text)
label.fontName = "AvenirNext-Bold"
label.fontSize = 22
label.fontColor = .white
label.verticalAlignmentMode = .center
label.zPosition = 1
button.addChild(label)
return button
}
private func startAnimations() {
// Fade in effect
alpha = 0
let fadeIn = SKAction.fadeIn(withDuration: 0.5)
run(fadeIn)
// Button pulse
let pulse = SKAction.sequence([
SKAction.scale(to: 1.05, duration: 0.8),
SKAction.scale(to: 1.0, duration: 0.8)
])
retryButton.run(SKAction.repeatForever(pulse))
}
// MARK: - Touch Handling
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
if retryButton.contains(location) {
retryGame()
} else if menuButton.contains(location) {
returnToMenu()
}
}
private func retryGame() {
let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
let pressUp = SKAction.scale(to: 1.0, duration: 0.1)
retryButton.run(SKAction.sequence([pressDown, pressUp])) { [weak self] in
guard let self = self else { return }
let gameScene = GameScene(size: self.size)
gameScene.scaleMode = self.scaleMode
let transition = SKTransition.fade(withDuration: 0.5)
self.view?.presentScene(gameScene, transition: transition)
}
}
private func returnToMenu() {
let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
let pressUp = SKAction.scale(to: 1.0, duration: 0.1)
menuButton.run(SKAction.sequence([pressDown, pressUp])) { [weak self] in
guard let self = self else { return }
let menuScene = MenuScene(size: self.size)
menuScene.scaleMode = self.scaleMode
let transition = SKTransition.fade(withDuration: 0.5)
self.view?.presentScene(menuScene, transition: transition)
}
}
}
+537
View File
@@ -0,0 +1,537 @@
//
// GameScene.swift
// RollkofferSimulator
//
// Created by Ingo K.
//
import SpriteKit
class GameScene: SKScene {
// MARK: - Game Objects
private var player: PlayerNode!
private var gameState: GameState!
// MARK: - Managers
private var spawnManager: SpawnManager!
private var collisionManager: CollisionManager!
// MARK: - UI Elements
private var livesLabel: SKLabelNode!
private var scoreLabel: SKLabelNode!
private var dogsLabel: SKLabelNode!
private var humansLabel: SKLabelNode!
private var timerLabel: SKLabelNode!
private var pauseButton: SKShapeNode!
private var pauseOverlay: SKNode?
// MARK: - Touch Handling
private var touchOffset: CGPoint = .zero
private var isDragging: Bool = false
// MARK: - Timing
private var lastUpdateTime: TimeInterval = 0
// MARK: - Background Scrolling
private var floorTiles: [SKShapeNode] = []
private let tileSize: CGFloat = 80
// MARK: - Scene Lifecycle
override func didMove(to view: SKView) {
setupGame()
}
// MARK: - Setup
private func setupGame() {
// Initialize game state
gameState = GameState()
gameState.reset()
// Setup physics
physicsWorld.gravity = .zero
collisionManager = CollisionManager()
collisionManager.delegate = self
collisionManager.setScene(self)
physicsWorld.contactDelegate = collisionManager
setupBackground()
setupPlayer()
setupUI()
setupSpawnManager()
spawnManager.startSpawning()
}
private func setupBackground() {
backgroundColor = Constants.Colors.backgroundColor
// Create scrolling floor tiles
let rows = Int(frame.height / tileSize) + 3
let cols = Int(frame.width / tileSize) + 1
for row in 0..<rows {
for col in 0..<cols {
let tile = SKShapeNode(rect: CGRect(x: 0, y: 0,
width: tileSize - 2, height: tileSize - 2))
let isEven = (row + col) % 2 == 0
tile.fillColor = isEven ?
SKColor(white: 0.78, alpha: 1.0) : SKColor(white: 0.72, alpha: 1.0)
tile.strokeColor = SKColor(white: 0.65, alpha: 0.5)
tile.lineWidth = 1
tile.position = CGPoint(x: CGFloat(col) * tileSize,
y: CGFloat(row) * tileSize)
tile.zPosition = Constants.ZPosition.floor
tile.name = "floorTile"
addChild(tile)
floorTiles.append(tile)
}
}
// Add airport markings
addAirportMarkings()
}
private func addAirportMarkings() {
// Center line
let lineWidth: CGFloat = 4
let dashLength: CGFloat = 30
let gapLength: CGFloat = 20
for y in stride(from: CGFloat(0), to: frame.height, by: dashLength + gapLength) {
let dash = SKShapeNode(rect: CGRect(x: frame.midX - lineWidth / 2, y: y,
width: lineWidth, height: dashLength))
dash.fillColor = SKColor.yellow.withAlphaComponent(0.6)
dash.strokeColor = .clear
dash.zPosition = Constants.ZPosition.floor + 0.5
dash.name = "floorMarking"
addChild(dash)
}
}
private func setupPlayer() {
player = PlayerNode()
player.position = CGPoint(x: frame.midX, y: frame.height * 0.15)
addChild(player)
}
private func setupUI() {
let uiY = frame.height - 50
let uiY2 = frame.height - 80
// Lives
livesLabel = createUILabel(text: gameState.formattedLives, fontSize: 24)
livesLabel.position = CGPoint(x: 60, y: uiY)
livesLabel.horizontalAlignmentMode = .left
addChild(livesLabel)
// Score
scoreLabel = createUILabel(text: gameState.formattedScore, fontSize: 20)
scoreLabel.position = CGPoint(x: frame.midX, y: uiY)
addChild(scoreLabel)
// Dogs counter
dogsLabel = createUILabel(text: gameState.formattedDogs, fontSize: 18)
dogsLabel.position = CGPoint(x: frame.width - 120, y: uiY)
addChild(dogsLabel)
// Humans counter
humansLabel = createUILabel(text: gameState.formattedHumans, fontSize: 18)
humansLabel.position = CGPoint(x: frame.width - 50, y: uiY)
addChild(humansLabel)
// Timer
timerLabel = createUILabel(text: "⏱️ \(gameState.formattedTime)", fontSize: 22)
timerLabel.position = CGPoint(x: frame.midX, y: uiY2)
addChild(timerLabel)
// Pause button
setupPauseButton()
// UI background bar
let uiBar = SKShapeNode(rect: CGRect(x: 0, y: frame.height - 100,
width: frame.width, height: 100))
uiBar.fillColor = SKColor.white.withAlphaComponent(0.9)
uiBar.strokeColor = SKColor.gray.withAlphaComponent(0.5)
uiBar.lineWidth = 1
uiBar.zPosition = Constants.ZPosition.ui - 1
addChild(uiBar)
}
private func createUILabel(text: String, fontSize: CGFloat) -> SKLabelNode {
let label = SKLabelNode(text: text)
label.fontName = "AvenirNext-Bold"
label.fontSize = fontSize
label.fontColor = .darkGray
label.zPosition = Constants.ZPosition.ui
label.verticalAlignmentMode = .center
return label
}
private func setupPauseButton() {
let buttonSize: CGFloat = 36
pauseButton = SKShapeNode(rect: CGRect(x: -buttonSize / 2, y: -buttonSize / 2,
width: buttonSize, height: buttonSize),
cornerRadius: 8)
pauseButton.fillColor = SKColor(white: 0.9, alpha: 1.0)
pauseButton.strokeColor = SKColor.gray
pauseButton.lineWidth = 2
pauseButton.position = CGPoint(x: 40, y: frame.height - 75)
pauseButton.zPosition = Constants.ZPosition.ui
pauseButton.name = "pauseButton"
addChild(pauseButton)
// Pause icon (two vertical bars)
let barWidth: CGFloat = 4
let barHeight: CGFloat = 16
let barGap: CGFloat = 5
let leftBar = SKShapeNode(rect: CGRect(x: -barGap - barWidth / 2, y: -barHeight / 2,
width: barWidth, height: barHeight))
leftBar.fillColor = .darkGray
leftBar.strokeColor = .clear
pauseButton.addChild(leftBar)
let rightBar = SKShapeNode(rect: CGRect(x: barGap - barWidth / 2, y: -barHeight / 2,
width: barWidth, height: barHeight))
rightBar.fillColor = .darkGray
rightBar.strokeColor = .clear
pauseButton.addChild(rightBar)
}
private func setupSpawnManager() {
spawnManager = SpawnManager(scene: self)
}
// MARK: - Update Loop
override func update(_ currentTime: TimeInterval) {
guard gameState.currentState == .playing else { return }
// Calculate delta time
let deltaTime = lastUpdateTime > 0 ? currentTime - lastUpdateTime : 0
lastUpdateTime = currentTime
// Update game time
gameState.updateTime(delta: deltaTime)
updateUI()
// Update spawn manager
spawnManager.update(deltaTime: deltaTime)
// Update floor scrolling
updateFloorScrolling(deltaTime: deltaTime)
// Check game end conditions
checkGameEndConditions()
}
private func updateFloorScrolling(deltaTime: TimeInterval) {
let scrollAmount = Constants.scrollSpeed * CGFloat(deltaTime)
for tile in floorTiles {
tile.position.y -= scrollAmount
// Reset tile position when it scrolls off screen
if tile.position.y < -tileSize {
tile.position.y += CGFloat(floorTiles.count / (Int(frame.width / tileSize) + 1)) * tileSize
}
}
// Also scroll floor markings
enumerateChildNodes(withName: "floorMarking") { node, _ in
node.position.y -= scrollAmount
if node.position.y < -30 {
node.position.y += self.frame.height + 50
}
}
}
private func updateUI() {
livesLabel.text = gameState.formattedLives
scoreLabel.text = gameState.formattedScore
dogsLabel.text = gameState.formattedDogs
humansLabel.text = gameState.formattedHumans
timerLabel.text = "⏱️ \(gameState.formattedTime)"
// Flash timer when low
if gameState.timeRemaining <= 10 {
timerLabel.fontColor = .red
if timerLabel.action(forKey: "flash") == nil {
let flash = SKAction.sequence([
SKAction.scale(to: 1.2, duration: 0.25),
SKAction.scale(to: 1.0, duration: 0.25)
])
timerLabel.run(SKAction.repeatForever(flash), withKey: "flash")
}
}
}
private func checkGameEndConditions() {
if gameState.hasWon {
handleVictory()
} else if gameState.hasLost {
handleGameOver()
}
}
// MARK: - Touch Handling
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
// Check pause button
if pauseButton.contains(location) {
togglePause()
return
}
// Check if touching player for dragging
if player.contains(location) {
isDragging = true
touchOffset = CGPoint(x: player.position.x - location.x,
y: player.position.y - location.y)
} else {
// Move player to touch location
isDragging = true
touchOffset = .zero
player.position = location
player.constrainToScreen(in: frame)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isDragging, gameState.currentState == .playing,
let touch = touches.first else { return }
let location = touch.location(in: self)
player.position = CGPoint(x: location.x + touchOffset.x,
y: location.y + touchOffset.y)
player.constrainToScreen(in: frame)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
isDragging = false
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
isDragging = false
}
// MARK: - Pause Handling
private func togglePause() {
if gameState.currentState == .playing {
pauseGame()
} else if gameState.currentState == .paused {
resumeGame()
}
}
private func pauseGame() {
gameState.setState(.paused)
spawnManager.stopSpawning()
isPaused = true
showPauseOverlay()
}
private func resumeGame() {
hidePauseOverlay()
gameState.setState(.playing)
spawnManager.startSpawning()
isPaused = false
lastUpdateTime = 0
}
private func showPauseOverlay() {
pauseOverlay = SKNode()
pauseOverlay?.zPosition = Constants.ZPosition.ui + 10
// Dimmed background
let dim = SKShapeNode(rect: frame)
dim.fillColor = SKColor.black.withAlphaComponent(0.6)
dim.strokeColor = .clear
pauseOverlay?.addChild(dim)
// Pause text
let pauseText = SKLabelNode(text: "⏸️ PAUSIERT")
pauseText.fontName = "AvenirNext-Heavy"
pauseText.fontSize = 40
pauseText.fontColor = .white
pauseText.position = CGPoint(x: frame.midX, y: frame.midY + 40)
pauseOverlay?.addChild(pauseText)
// Resume button
let resumeButton = createButton(text: "▶️ Weiter", at: CGPoint(x: frame.midX, y: frame.midY - 20))
resumeButton.name = "resumeButton"
pauseOverlay?.addChild(resumeButton)
// Menu button
let menuButton = createButton(text: "🏠 Menü", at: CGPoint(x: frame.midX, y: frame.midY - 80))
menuButton.name = "menuButton"
pauseOverlay?.addChild(menuButton)
addChild(pauseOverlay!)
}
private func createButton(text: String, at position: CGPoint) -> SKNode {
let container = SKNode()
container.position = position
let bg = SKShapeNode(rect: CGRect(x: -80, y: -25, width: 160, height: 50),
cornerRadius: 10)
bg.fillColor = SKColor(white: 0.2, alpha: 0.9)
bg.strokeColor = .white
bg.lineWidth = 2
container.addChild(bg)
let label = SKLabelNode(text: text)
label.fontName = "AvenirNext-Bold"
label.fontSize = 22
label.fontColor = .white
label.verticalAlignmentMode = .center
container.addChild(label)
return container
}
private func hidePauseOverlay() {
pauseOverlay?.removeFromParent()
pauseOverlay = nil
}
// MARK: - Handle pause overlay touches (override to handle when paused)
func handlePauseOverlayTouch(at location: CGPoint) {
guard let overlay = pauseOverlay else { return }
if let resumeButton = overlay.childNode(withName: "resumeButton"),
resumeButton.contains(location) {
resumeGame()
} else if let menuButton = overlay.childNode(withName: "menuButton"),
menuButton.contains(location) {
returnToMenu()
}
}
// Override touchesBegan to handle pause overlay
func handleTouchInPausedState(_ touches: Set<UITouch>) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
handlePauseOverlayTouch(at: location)
}
// MARK: - Game End Handling
private func handleDamage() {
guard !gameState.isInvincible else { return }
if gameState.loseLife() {
// Start invincibility
gameState.isInvincible = true
player.startBlinking()
// End invincibility after duration
let wait = SKAction.wait(forDuration: Constants.invincibilityDuration)
run(wait) { [weak self] in
self?.gameState.isInvincible = false
self?.player.stopBlinking()
}
}
}
private func handleVictory() {
gameState.setState(.victory)
spawnManager.stopSpawning()
// Record score
ScoreManager.shared.recordGameEnd(
score: gameState.score,
dogsCollected: gameState.dogsCollected,
humansCollected: gameState.humansCollected,
didWin: true
)
// Transition to victory scene
let victoryScene = VictoryScene(size: size)
victoryScene.scaleMode = scaleMode
victoryScene.finalScore = gameState.score
victoryScene.dogsCollected = gameState.dogsCollected
victoryScene.humansCollected = gameState.humansCollected
victoryScene.timeRemaining = gameState.timeRemaining
let transition = SKTransition.fade(withDuration: 0.5)
view?.presentScene(victoryScene, transition: transition)
}
private func handleGameOver() {
gameState.setState(.gameOver)
spawnManager.stopSpawning()
// Record score
ScoreManager.shared.recordGameEnd(
score: gameState.score,
dogsCollected: gameState.dogsCollected,
humansCollected: gameState.humansCollected,
didWin: false
)
// Transition to game over scene
let gameOverScene = GameOverScene(size: size)
gameOverScene.scaleMode = scaleMode
gameOverScene.finalScore = gameState.score
gameOverScene.dogsCollected = gameState.dogsCollected
gameOverScene.humansCollected = gameState.humansCollected
gameOverScene.isNewHighScore = ScoreManager.shared.isNewHighScore(gameState.score)
let transition = SKTransition.fade(withDuration: 0.5)
view?.presentScene(gameOverScene, transition: transition)
}
private func returnToMenu() {
let menuScene = MenuScene(size: size)
menuScene.scaleMode = scaleMode
let transition = SKTransition.fade(withDuration: 0.5)
view?.presentScene(menuScene, transition: transition)
}
}
// MARK: - CollisionManagerDelegate
extension GameScene: CollisionManagerDelegate {
func didCollectGoodDog(points: Int) {
gameState.addPoints(points)
gameState.collectDog()
// Show collection effect
showPointsEffect(points: points, at: player.position)
}
func didCollectGreenHuman(points: Int) {
gameState.addPoints(points)
gameState.collectHuman()
// Show collection effect
showPointsEffect(points: points, at: player.position)
}
func didHitHarmfulEntity() {
handleDamage()
}
private func showPointsEffect(points: Int, at position: CGPoint) {
let label = SKLabelNode(text: "+\(points)")
label.fontName = "AvenirNext-Bold"
label.fontSize = 24
label.fontColor = .green
label.position = CGPoint(x: position.x, y: position.y + 50)
label.zPosition = Constants.ZPosition.ui
addChild(label)
let moveUp = SKAction.moveBy(x: 0, y: 40, 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]))
}
}
+265
View File
@@ -0,0 +1,265 @@
//
// MenuScene.swift
// RollkofferSimulator
//
// Created by Ingo K.
//
import SpriteKit
class MenuScene: SKScene {
// MARK: - Properties
private var startButton: SKShapeNode!
private var titleLabel: SKLabelNode!
private var subtitleLabel: SKLabelNode!
private var highScoreLabel: SKLabelNode!
private var creditsLabel: SKLabelNode!
// MARK: - Decorative elements
private var decorativeSuitcase: PlayerNode!
private var decorativeDogs: [DogNode] = []
private var decorativeHumans: [HumanNode] = []
// MARK: - Scene Lifecycle
override func didMove(to view: SKView) {
setupBackground()
setupTitle()
setupStartButton()
setupHighScore()
setupDecorations()
setupCredits()
startAnimations()
}
// MARK: - Setup
private func setupBackground() {
backgroundColor = Constants.Colors.backgroundColor
// Create floor pattern
let floorHeight = frame.height * 0.6
let floor = SKShapeNode(rect: CGRect(x: 0, y: 0,
width: frame.width, height: floorHeight))
floor.fillColor = Constants.Colors.floorColor
floor.strokeColor = .clear
floor.zPosition = Constants.ZPosition.floor
addChild(floor)
// Add floor tiles pattern
let tileSize: CGFloat = 80
for x in stride(from: CGFloat(0), to: frame.width, by: tileSize) {
for y in stride(from: CGFloat(0), to: floorHeight, by: tileSize) {
let tile = SKShapeNode(rect: CGRect(x: x, y: y,
width: tileSize - 2, height: tileSize - 2))
tile.fillColor = (Int((x + y) / tileSize) % 2 == 0) ?
SKColor(white: 0.78, alpha: 1.0) : SKColor(white: 0.72, alpha: 1.0)
tile.strokeColor = SKColor(white: 0.65, alpha: 0.5)
tile.lineWidth = 1
tile.zPosition = Constants.ZPosition.floor + 0.1
addChild(tile)
}
}
}
private func setupTitle() {
// Main title
titleLabel = SKLabelNode(text: "🧳 Rollkoffer Simulator 🧳")
titleLabel.fontName = "AvenirNext-Heavy"
titleLabel.fontSize = 36
titleLabel.fontColor = SKColor(red: 0.3, green: 0.2, blue: 0.5, alpha: 1.0)
titleLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.85)
titleLabel.zPosition = Constants.ZPosition.ui
addChild(titleLabel)
// Subtitle
subtitleLabel = SKLabelNode(text: "Sammle Hunde & Menschen am Flughafen!")
subtitleLabel.fontName = "AvenirNext-Medium"
subtitleLabel.fontSize = 18
subtitleLabel.fontColor = SKColor.darkGray
subtitleLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.78)
subtitleLabel.zPosition = Constants.ZPosition.ui
addChild(subtitleLabel)
// Goal info
let goalLabel = SKLabelNode(text: "Ziel: 🐕 10 Hunde + 👤 5 Grüne Menschen")
goalLabel.fontName = "AvenirNext-Medium"
goalLabel.fontSize = 16
goalLabel.fontColor = SKColor.gray
goalLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.72)
goalLabel.zPosition = Constants.ZPosition.ui
addChild(goalLabel)
}
private func setupStartButton() {
// Button background
let buttonWidth: CGFloat = 200
let buttonHeight: CGFloat = 60
startButton = SKShapeNode(rect: CGRect(x: -buttonWidth / 2, y: -buttonHeight / 2,
width: buttonWidth, height: buttonHeight),
cornerRadius: 15)
startButton.fillColor = SKColor(red: 0.2, green: 0.7, blue: 0.3, alpha: 1.0)
startButton.strokeColor = SKColor(red: 0.15, green: 0.5, blue: 0.2, alpha: 1.0)
startButton.lineWidth = 3
startButton.position = CGPoint(x: frame.midX, y: frame.height * 0.5)
startButton.zPosition = Constants.ZPosition.ui
startButton.name = "startButton"
addChild(startButton)
// Button label
let buttonLabel = SKLabelNode(text: "▶ START")
buttonLabel.fontName = "AvenirNext-Bold"
buttonLabel.fontSize = 28
buttonLabel.fontColor = .white
buttonLabel.verticalAlignmentMode = .center
buttonLabel.position = .zero
buttonLabel.zPosition = 1
startButton.addChild(buttonLabel)
// Button pulse animation
let scaleUp = SKAction.scale(to: 1.05, duration: 0.8)
let scaleDown = SKAction.scale(to: 1.0, duration: 0.8)
let pulse = SKAction.sequence([scaleUp, scaleDown])
startButton.run(SKAction.repeatForever(pulse))
}
private func setupHighScore() {
let highScore = ScoreManager.shared.highScore
highScoreLabel = SKLabelNode(text: "🏆 High Score: \(highScore)")
highScoreLabel.fontName = "AvenirNext-DemiBold"
highScoreLabel.fontSize = 20
highScoreLabel.fontColor = SKColor(red: 0.8, green: 0.6, blue: 0.1, alpha: 1.0)
highScoreLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.38)
highScoreLabel.zPosition = Constants.ZPosition.ui
addChild(highScoreLabel)
// Statistics label
let victories = ScoreManager.shared.victories
let gamesPlayed = ScoreManager.shared.gamesPlayed
let statsText = "Siege: \(victories) | Spiele: \(gamesPlayed)"
let statsLabel = SKLabelNode(text: statsText)
statsLabel.fontName = "AvenirNext-Regular"
statsLabel.fontSize = 14
statsLabel.fontColor = SKColor.gray
statsLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.33)
statsLabel.zPosition = Constants.ZPosition.ui
addChild(statsLabel)
}
private func setupDecorations() {
// Decorative suitcase
decorativeSuitcase = PlayerNode()
decorativeSuitcase.position = CGPoint(x: frame.midX, y: frame.height * 0.2)
decorativeSuitcase.zPosition = Constants.ZPosition.entities
addChild(decorativeSuitcase)
// Add some decorative dogs
let dogTypes: [DogType] = [.smallGood, .bigGood, .bad]
let dogPositions: [CGPoint] = [
CGPoint(x: frame.width * 0.15, y: frame.height * 0.25),
CGPoint(x: frame.width * 0.85, y: frame.height * 0.22),
CGPoint(x: frame.width * 0.25, y: frame.height * 0.12)
]
for (index, type) in dogTypes.enumerated() {
let dog = DogNode(type: type)
dog.position = dogPositions[index]
dog.zPosition = Constants.ZPosition.entities
addChild(dog)
decorativeDogs.append(dog)
}
// Add decorative humans
let humanTypes: [HumanType] = [.green, .gray]
let humanPositions: [CGPoint] = [
CGPoint(x: frame.width * 0.75, y: frame.height * 0.15),
CGPoint(x: frame.width * 0.6, y: frame.height * 0.1)
]
for (index, type) in humanTypes.enumerated() {
let human = HumanNode(type: type)
human.position = humanPositions[index]
human.zPosition = Constants.ZPosition.entities
addChild(human)
decorativeHumans.append(human)
}
}
private func setupCredits() {
creditsLabel = SKLabelNode(text: "Created by Ingo K.")
creditsLabel.fontName = "AvenirNext-Italic"
creditsLabel.fontSize = 14
creditsLabel.fontColor = SKColor.gray
creditsLabel.position = CGPoint(x: frame.midX, y: 30)
creditsLabel.zPosition = Constants.ZPosition.ui
addChild(creditsLabel)
}
private func startAnimations() {
// Animate decorative suitcase
let moveLeft = SKAction.moveBy(x: -20, y: 0, duration: 1.5)
let moveRight = SKAction.moveBy(x: 40, y: 0, duration: 3.0)
let moveBack = SKAction.moveBy(x: -20, y: 0, duration: 1.5)
let suitcaseSequence = SKAction.sequence([moveLeft, moveRight, moveBack])
decorativeSuitcase.run(SKAction.repeatForever(suitcaseSequence))
// Animate decorative entities
for (index, dog) in decorativeDogs.enumerated() {
let delay = Double(index) * 0.3
let bounce = SKAction.sequence([
SKAction.wait(forDuration: delay),
SKAction.moveBy(x: 0, y: 10, duration: 0.4),
SKAction.moveBy(x: 0, y: -10, duration: 0.4)
])
dog.run(SKAction.repeatForever(bounce))
}
for (index, human) in decorativeHumans.enumerated() {
let delay = Double(index) * 0.4 + 0.2
let sway = SKAction.sequence([
SKAction.wait(forDuration: delay),
SKAction.rotate(byAngle: 0.05, duration: 0.5),
SKAction.rotate(byAngle: -0.1, duration: 1.0),
SKAction.rotate(byAngle: 0.05, duration: 0.5)
])
human.run(SKAction.repeatForever(sway))
}
// Title animation
let titleScale = SKAction.sequence([
SKAction.scale(to: 1.02, duration: 2.0),
SKAction.scale(to: 1.0, duration: 2.0)
])
titleLabel.run(SKAction.repeatForever(titleScale))
}
// MARK: - Touch Handling
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
if startButton.contains(location) {
startGame()
}
}
private func startGame() {
// Button press effect
let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
let pressUp = SKAction.scale(to: 1.0, duration: 0.1)
startButton.run(SKAction.sequence([pressDown, pressUp])) { [weak self] in
self?.transitionToGame()
}
}
private func transitionToGame() {
let gameScene = GameScene(size: size)
gameScene.scaleMode = scaleMode
let transition = SKTransition.fade(withDuration: 0.5)
view?.presentScene(gameScene, transition: transition)
}
}
@@ -0,0 +1,312 @@
//
// VictoryScene.swift
// RollkofferSimulator
//
// Created by Ingo K.
//
import SpriteKit
class VictoryScene: SKScene {
// MARK: - Properties
var finalScore: Int = 0
var dogsCollected: Int = 0
var humansCollected: Int = 0
var timeRemaining: TimeInterval = 0
private var playAgainButton: SKShapeNode!
private var menuButton: SKShapeNode!
// MARK: - Scene Lifecycle
override func didMove(to view: SKView) {
setupBackground()
setupContent()
setupButtons()
startCelebration()
}
// MARK: - Setup
private func setupBackground() {
backgroundColor = SKColor(red: 0.1, green: 0.2, blue: 0.1, alpha: 1.0)
// Add celebration particles
for _ in 0..<30 {
let confetti = createConfetti()
addChild(confetti)
}
}
private func createConfetti() -> SKShapeNode {
let size = CGFloat.random(in: 8...15)
let confetti = SKShapeNode(rectOf: CGSize(width: size, height: size * 1.5))
confetti.fillColor = [SKColor.red, SKColor.yellow, SKColor.green,
SKColor.blue, SKColor.orange, SKColor.purple].randomElement()!
confetti.strokeColor = .clear
confetti.position = CGPoint(x: CGFloat.random(in: 0...frame.width),
y: frame.height + CGFloat.random(in: 50...200))
confetti.zPosition = Constants.ZPosition.ui - 5
confetti.zRotation = CGFloat.random(in: 0...(.pi * 2))
// Falling animation
let fallDuration = Double.random(in: 3...6)
let fall = SKAction.moveTo(y: -50, duration: fallDuration)
let rotate = SKAction.rotate(byAngle: .pi * 4, duration: fallDuration)
let sway = SKAction.sequence([
SKAction.moveBy(x: 30, y: 0, duration: 0.5),
SKAction.moveBy(x: -30, y: 0, duration: 0.5)
])
let swayRepeat = SKAction.repeat(sway, count: Int(fallDuration))
let group = SKAction.group([fall, rotate, swayRepeat])
let reset = SKAction.run { [weak confetti, weak self] in
confetti?.position = CGPoint(x: CGFloat.random(in: 0...(self?.frame.width ?? 400)),
y: (self?.frame.height ?? 800) + 50)
}
let sequence = SKAction.sequence([group, reset])
confetti.run(SKAction.repeatForever(sequence))
return confetti
}
private func setupContent() {
// Victory title
let titleLabel = SKLabelNode(text: "🎉 GEWONNEN! 🎉")
titleLabel.fontName = "AvenirNext-Heavy"
titleLabel.fontSize = 46
titleLabel.fontColor = .yellow
titleLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.82)
titleLabel.zPosition = Constants.ZPosition.ui
addChild(titleLabel)
// Animate title
let titlePulse = SKAction.sequence([
SKAction.scale(to: 1.1, duration: 0.3),
SKAction.scale(to: 1.0, duration: 0.3)
])
titleLabel.run(SKAction.repeatForever(titlePulse))
// Subtitle
let subtitleLabel = SKLabelNode(text: "Du hast alle Ziele erreicht!")
subtitleLabel.fontName = "AvenirNext-Medium"
subtitleLabel.fontSize = 20
subtitleLabel.fontColor = .white
subtitleLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.74)
subtitleLabel.zPosition = Constants.ZPosition.ui
addChild(subtitleLabel)
// Score display
let scoreLabel = SKLabelNode(text: "Endpunktzahl: \(finalScore)")
scoreLabel.fontName = "AvenirNext-Bold"
scoreLabel.fontSize = 36
scoreLabel.fontColor = SKColor(red: 1.0, green: 0.85, blue: 0.0, alpha: 1.0)
scoreLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.62)
scoreLabel.zPosition = Constants.ZPosition.ui
addChild(scoreLabel)
// Time bonus display
let timeBonus = Int(timeRemaining) * 5
if timeBonus > 0 {
let timeBonusLabel = SKLabelNode(text: "⏱️ Zeitbonus: +\(timeBonus) (\(Int(timeRemaining))s übrig)")
timeBonusLabel.fontName = "AvenirNext-Medium"
timeBonusLabel.fontSize = 18
timeBonusLabel.fontColor = SKColor.cyan
timeBonusLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.55)
timeBonusLabel.zPosition = Constants.ZPosition.ui
addChild(timeBonusLabel)
}
// Stats display
let statsY = frame.height * 0.45
let dogsLabel = SKLabelNode(text: "🐕 Hunde: \(dogsCollected)")
dogsLabel.fontName = "AvenirNext-DemiBold"
dogsLabel.fontSize = 22
dogsLabel.fontColor = .white
dogsLabel.position = CGPoint(x: frame.midX - 80, y: statsY)
dogsLabel.zPosition = Constants.ZPosition.ui
addChild(dogsLabel)
let humansLabel = SKLabelNode(text: "👤 Menschen: \(humansCollected)")
humansLabel.fontName = "AvenirNext-DemiBold"
humansLabel.fontSize = 22
humansLabel.fontColor = .white
humansLabel.position = CGPoint(x: frame.midX + 80, y: statsY)
humansLabel.zPosition = Constants.ZPosition.ui
addChild(humansLabel)
// Happy suitcase with collected items
let happySuitcase = PlayerNode()
happySuitcase.position = CGPoint(x: frame.midX, y: frame.height * 0.28)
addChild(happySuitcase)
// Happy face
let happyFace = SKLabelNode(text: "😄")
happyFace.fontSize = 30
happyFace.position = CGPoint(x: frame.midX, y: frame.height * 0.28 + 20)
happyFace.zPosition = Constants.ZPosition.ui
addChild(happyFace)
// Add small dogs and humans around suitcase
let collectibles = [
("🐕", CGPoint(x: -50, y: 0)),
("🐕", CGPoint(x: 50, y: 0)),
("👤", CGPoint(x: -35, y: 30)),
("👤", CGPoint(x: 35, y: 30))
]
for (emoji, offset) in collectibles {
let label = SKLabelNode(text: emoji)
label.fontSize = 24
label.position = CGPoint(x: frame.midX + offset.x,
y: frame.height * 0.28 + offset.y)
label.zPosition = Constants.ZPosition.ui
addChild(label)
// Bounce animation
let bounce = SKAction.sequence([
SKAction.moveBy(x: 0, y: 5, duration: 0.3),
SKAction.moveBy(x: 0, y: -5, duration: 0.3)
])
label.run(SKAction.repeatForever(bounce))
}
// High score check
if ScoreManager.shared.isNewHighScore(finalScore) {
let highScoreLabel = SKLabelNode(text: "🏆 NEUER HIGHSCORE! 🏆")
highScoreLabel.fontName = "AvenirNext-Heavy"
highScoreLabel.fontSize = 26
highScoreLabel.fontColor = SKColor.yellow
highScoreLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.68)
highScoreLabel.zPosition = Constants.ZPosition.ui
addChild(highScoreLabel)
let glow = SKAction.sequence([
SKAction.fadeAlpha(to: 0.6, duration: 0.4),
SKAction.fadeAlpha(to: 1.0, duration: 0.4)
])
highScoreLabel.run(SKAction.repeatForever(glow))
}
}
private func setupButtons() {
// Play Again button
playAgainButton = createButton(text: "🎮 Nochmal spielen",
color: SKColor(red: 0.2, green: 0.7, blue: 0.3, alpha: 1.0))
playAgainButton.position = CGPoint(x: frame.midX, y: frame.height * 0.13)
playAgainButton.name = "playAgainButton"
addChild(playAgainButton)
// Menu button
menuButton = createButton(text: "🏠 Hauptmenü",
color: SKColor(red: 0.3, green: 0.3, blue: 0.6, alpha: 1.0))
menuButton.position = CGPoint(x: frame.midX, y: frame.height * 0.06)
menuButton.name = "menuButton"
addChild(menuButton)
}
private func createButton(text: String, color: SKColor) -> SKShapeNode {
let buttonWidth: CGFloat = 220
let buttonHeight: CGFloat = 50
let button = SKShapeNode(rect: CGRect(x: -buttonWidth / 2, y: -buttonHeight / 2,
width: buttonWidth, height: buttonHeight),
cornerRadius: 12)
button.fillColor = color
button.strokeColor = .white.withAlphaComponent(0.5)
button.lineWidth = 2
button.zPosition = Constants.ZPosition.ui
let label = SKLabelNode(text: text)
label.fontName = "AvenirNext-Bold"
label.fontSize = 20
label.fontColor = .white
label.verticalAlignmentMode = .center
label.zPosition = 1
button.addChild(label)
return button
}
private func startCelebration() {
// Screen flash
let flash = SKShapeNode(rect: frame)
flash.fillColor = .white
flash.strokeColor = .clear
flash.zPosition = Constants.ZPosition.ui + 100
flash.alpha = 0.8
addChild(flash)
let fadeOut = SKAction.fadeOut(withDuration: 0.5)
let remove = SKAction.removeFromParent()
flash.run(SKAction.sequence([fadeOut, remove]))
// Star burst effect
for _ in 0..<12 {
let star = SKLabelNode(text: "")
star.fontSize = CGFloat.random(in: 20...40)
star.position = CGPoint(x: frame.midX, y: frame.height * 0.82)
star.zPosition = Constants.ZPosition.ui - 1
star.alpha = 0
addChild(star)
let angle = CGFloat.random(in: 0...(.pi * 2))
let distance = CGFloat.random(in: 100...200)
let endPoint = CGPoint(x: frame.midX + cos(angle) * distance,
y: frame.height * 0.82 + sin(angle) * distance)
let fadeIn = SKAction.fadeIn(withDuration: 0.2)
let move = SKAction.move(to: endPoint, duration: 0.5)
let fadeOutStar = SKAction.fadeOut(withDuration: 0.3)
let removeStar = SKAction.removeFromParent()
let group = SKAction.group([move, SKAction.sequence([fadeIn,
SKAction.wait(forDuration: 0.2),
fadeOutStar])])
star.run(SKAction.sequence([SKAction.wait(forDuration: Double.random(in: 0...0.3)),
group, removeStar]))
}
}
// MARK: - Touch Handling
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
if playAgainButton.contains(location) {
playAgain()
} else if menuButton.contains(location) {
returnToMenu()
}
}
private func playAgain() {
let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
let pressUp = SKAction.scale(to: 1.0, duration: 0.1)
playAgainButton.run(SKAction.sequence([pressDown, pressUp])) { [weak self] in
guard let self = self else { return }
let gameScene = GameScene(size: self.size)
gameScene.scaleMode = self.scaleMode
let transition = SKTransition.fade(withDuration: 0.5)
self.view?.presentScene(gameScene, transition: transition)
}
}
private func returnToMenu() {
let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
let pressUp = SKAction.scale(to: 1.0, duration: 0.1)
menuButton.run(SKAction.sequence([pressDown, pressUp])) { [weak self] in
guard let self = self else { return }
let menuScene = MenuScene(size: self.size)
menuScene.scaleMode = self.scaleMode
let transition = SKTransition.fade(withDuration: 0.5)
self.view?.presentScene(menuScene, transition: transition)
}
}
}