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:
@@ -0,0 +1,162 @@
|
||||
//
|
||||
// DogNode.swift
|
||||
// RollkofferSimulator
|
||||
//
|
||||
// Created by Ingo K.
|
||||
//
|
||||
|
||||
import SpriteKit
|
||||
|
||||
/// A dog entity node
|
||||
class DogNode: SKNode {
|
||||
|
||||
// MARK: - Properties
|
||||
let dogType: DogType
|
||||
private let bodyNode: SKShapeNode
|
||||
|
||||
// MARK: - Initialization
|
||||
init(type: DogType) {
|
||||
self.dogType = type
|
||||
|
||||
let size: CGSize
|
||||
let color: SKColor
|
||||
let hasRedOutline: Bool
|
||||
|
||||
switch type {
|
||||
case .smallGood:
|
||||
size = Constants.smallDogSize
|
||||
color = Constants.Colors.goodDogColor
|
||||
hasRedOutline = false
|
||||
case .bigGood:
|
||||
size = Constants.bigDogSize
|
||||
color = Constants.Colors.goodDogColor
|
||||
hasRedOutline = false
|
||||
case .bad:
|
||||
size = Constants.badDogSize
|
||||
color = Constants.Colors.goodDogColor
|
||||
hasRedOutline = true
|
||||
}
|
||||
|
||||
// Create dog body (simple oval shape)
|
||||
let bodyPath = CGMutablePath()
|
||||
bodyPath.addEllipse(in: CGRect(x: -size.width / 2, y: -size.height / 2,
|
||||
width: size.width, height: size.height * 0.7))
|
||||
bodyNode = SKShapeNode(path: bodyPath)
|
||||
bodyNode.fillColor = color
|
||||
bodyNode.strokeColor = hasRedOutline ? Constants.Colors.badDogColor : SKColor(white: 0.3, alpha: 1.0)
|
||||
bodyNode.lineWidth = hasRedOutline ? 4 : 2
|
||||
|
||||
super.init()
|
||||
|
||||
addChild(bodyNode)
|
||||
|
||||
// Add head
|
||||
let headSize = size.width * 0.5
|
||||
let head = SKShapeNode(circleOfRadius: headSize / 2)
|
||||
head.position = CGPoint(x: 0, y: size.height * 0.25)
|
||||
head.fillColor = color
|
||||
head.strokeColor = hasRedOutline ? Constants.Colors.badDogColor : SKColor(white: 0.3, alpha: 1.0)
|
||||
head.lineWidth = hasRedOutline ? 3 : 1.5
|
||||
addChild(head)
|
||||
|
||||
// Add ears
|
||||
let earSize = headSize * 0.4
|
||||
for xOffset in [-headSize * 0.4, headSize * 0.4] {
|
||||
let ear = SKShapeNode(ellipseOf: CGSize(width: earSize, height: earSize * 1.5))
|
||||
ear.position = CGPoint(x: xOffset, y: size.height * 0.25 + headSize * 0.35)
|
||||
ear.fillColor = color.withAlphaComponent(0.8)
|
||||
ear.strokeColor = hasRedOutline ? Constants.Colors.badDogColor : SKColor(white: 0.3, alpha: 1.0)
|
||||
ear.lineWidth = hasRedOutline ? 2 : 1
|
||||
addChild(ear)
|
||||
}
|
||||
|
||||
// Add eyes
|
||||
let eyeSize: CGFloat = headSize * 0.15
|
||||
for xOffset in [-headSize * 0.15, headSize * 0.15] {
|
||||
let eye = SKShapeNode(circleOfRadius: eyeSize)
|
||||
eye.position = CGPoint(x: xOffset, y: size.height * 0.28)
|
||||
eye.fillColor = hasRedOutline ? .red : .black
|
||||
eye.strokeColor = .clear
|
||||
addChild(eye)
|
||||
}
|
||||
|
||||
// Add nose
|
||||
let nose = SKShapeNode(circleOfRadius: headSize * 0.1)
|
||||
nose.position = CGPoint(x: 0, y: size.height * 0.18)
|
||||
nose.fillColor = .black
|
||||
nose.strokeColor = .clear
|
||||
addChild(nose)
|
||||
|
||||
// Add tail
|
||||
let tailPath = CGMutablePath()
|
||||
tailPath.move(to: CGPoint(x: 0, y: -size.height * 0.25))
|
||||
tailPath.addQuadCurve(to: CGPoint(x: size.width * 0.3, y: -size.height * 0.1),
|
||||
control: CGPoint(x: size.width * 0.4, y: -size.height * 0.3))
|
||||
let tail = SKShapeNode(path: tailPath)
|
||||
tail.strokeColor = color
|
||||
tail.lineWidth = size.width * 0.1
|
||||
tail.lineCap = .round
|
||||
addChild(tail)
|
||||
|
||||
// Add legs
|
||||
let legWidth = size.width * 0.12
|
||||
let legHeight = size.height * 0.25
|
||||
let legPositions: [CGFloat] = [-size.width * 0.25, -size.width * 0.1,
|
||||
size.width * 0.1, size.width * 0.25]
|
||||
for xPos in legPositions {
|
||||
let leg = SKShapeNode(rect: CGRect(x: xPos - legWidth / 2,
|
||||
y: -size.height * 0.35 - legHeight,
|
||||
width: legWidth, height: legHeight),
|
||||
cornerRadius: legWidth / 2)
|
||||
leg.fillColor = color
|
||||
leg.strokeColor = hasRedOutline ? Constants.Colors.badDogColor : SKColor(white: 0.3, alpha: 1.0)
|
||||
leg.lineWidth = hasRedOutline ? 2 : 1
|
||||
addChild(leg)
|
||||
}
|
||||
|
||||
// Add angry expression for bad dogs
|
||||
if hasRedOutline {
|
||||
let browPath = CGMutablePath()
|
||||
browPath.move(to: CGPoint(x: -headSize * 0.3, y: size.height * 0.35))
|
||||
browPath.addLine(to: CGPoint(x: -headSize * 0.05, y: size.height * 0.32))
|
||||
let leftBrow = SKShapeNode(path: browPath)
|
||||
leftBrow.strokeColor = .black
|
||||
leftBrow.lineWidth = 2
|
||||
addChild(leftBrow)
|
||||
|
||||
let browPath2 = CGMutablePath()
|
||||
browPath2.move(to: CGPoint(x: headSize * 0.3, y: size.height * 0.35))
|
||||
browPath2.addLine(to: CGPoint(x: headSize * 0.05, y: size.height * 0.32))
|
||||
let rightBrow = SKShapeNode(path: browPath2)
|
||||
rightBrow.strokeColor = .black
|
||||
rightBrow.lineWidth = 2
|
||||
addChild(rightBrow)
|
||||
}
|
||||
|
||||
setupPhysics(size: size)
|
||||
self.zPosition = Constants.ZPosition.entities
|
||||
self.name = type.isHarmful ? "badDog" : "goodDog"
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Physics Setup
|
||||
private func setupPhysics(size: CGSize) {
|
||||
let radius = min(size.width, size.height) / 2
|
||||
physicsBody = SKPhysicsBody(circleOfRadius: radius)
|
||||
physicsBody?.isDynamic = true
|
||||
physicsBody?.affectedByGravity = false
|
||||
physicsBody?.allowsRotation = false
|
||||
|
||||
if dogType.isHarmful {
|
||||
physicsBody?.categoryBitMask = Constants.PhysicsCategory.badDog
|
||||
} else {
|
||||
physicsBody?.categoryBitMask = Constants.PhysicsCategory.goodDog
|
||||
}
|
||||
|
||||
physicsBody?.contactTestBitMask = Constants.PhysicsCategory.suitcase
|
||||
physicsBody?.collisionBitMask = Constants.PhysicsCategory.none
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
//
|
||||
// HumanNode.swift
|
||||
// RollkofferSimulator
|
||||
//
|
||||
// Created by Ingo K.
|
||||
//
|
||||
|
||||
import SpriteKit
|
||||
|
||||
/// A human entity node
|
||||
class HumanNode: SKNode {
|
||||
|
||||
// MARK: - Properties
|
||||
let humanType: HumanType
|
||||
private let bodyNode: SKShapeNode
|
||||
|
||||
// MARK: - Initialization
|
||||
init(type: HumanType) {
|
||||
self.humanType = type
|
||||
|
||||
let size = Constants.humanSize
|
||||
let color: SKColor
|
||||
|
||||
switch type {
|
||||
case .green:
|
||||
color = Constants.Colors.greenHumanColor
|
||||
case .gray:
|
||||
color = Constants.Colors.grayHumanColor
|
||||
}
|
||||
|
||||
// Create body (torso)
|
||||
let torsoHeight = size.height * 0.4
|
||||
let torsoWidth = size.width * 0.6
|
||||
let torsoRect = CGRect(x: -torsoWidth / 2, y: -size.height * 0.1,
|
||||
width: torsoWidth, height: torsoHeight)
|
||||
bodyNode = SKShapeNode(rect: torsoRect, cornerRadius: 5)
|
||||
bodyNode.fillColor = color
|
||||
bodyNode.strokeColor = color.withAlphaComponent(0.7)
|
||||
bodyNode.lineWidth = 2
|
||||
|
||||
super.init()
|
||||
|
||||
addChild(bodyNode)
|
||||
|
||||
// Add head
|
||||
let headRadius = size.width * 0.25
|
||||
let head = SKShapeNode(circleOfRadius: headRadius)
|
||||
head.position = CGPoint(x: 0, y: size.height * 0.35)
|
||||
head.fillColor = SKColor(red: 1.0, green: 0.87, blue: 0.77, alpha: 1.0) // Skin tone
|
||||
head.strokeColor = SKColor(red: 0.9, green: 0.75, blue: 0.65, alpha: 1.0)
|
||||
head.lineWidth = 1
|
||||
addChild(head)
|
||||
|
||||
// Add eyes
|
||||
let eyeSize: CGFloat = 3
|
||||
for xOffset in [-headRadius * 0.35, headRadius * 0.35] {
|
||||
let eye = SKShapeNode(circleOfRadius: eyeSize)
|
||||
eye.position = CGPoint(x: xOffset, y: size.height * 0.37)
|
||||
eye.fillColor = .black
|
||||
eye.strokeColor = .clear
|
||||
addChild(eye)
|
||||
}
|
||||
|
||||
// Add mouth/expression
|
||||
if type == .green {
|
||||
// Happy smile
|
||||
let smilePath = CGMutablePath()
|
||||
smilePath.addArc(center: CGPoint(x: 0, y: size.height * 0.32),
|
||||
radius: headRadius * 0.3,
|
||||
startAngle: .pi * 0.2,
|
||||
endAngle: .pi * 0.8,
|
||||
clockwise: true)
|
||||
let smile = SKShapeNode(path: smilePath)
|
||||
smile.strokeColor = .black
|
||||
smile.lineWidth = 2
|
||||
smile.lineCap = .round
|
||||
addChild(smile)
|
||||
} else {
|
||||
// Neutral/frowning expression
|
||||
let frownPath = CGMutablePath()
|
||||
frownPath.move(to: CGPoint(x: -headRadius * 0.25, y: size.height * 0.3))
|
||||
frownPath.addLine(to: CGPoint(x: headRadius * 0.25, y: size.height * 0.3))
|
||||
let frown = SKShapeNode(path: frownPath)
|
||||
frown.strokeColor = .black
|
||||
frown.lineWidth = 2
|
||||
addChild(frown)
|
||||
}
|
||||
|
||||
// Add arms
|
||||
let armWidth: CGFloat = 6
|
||||
let armLength = size.height * 0.3
|
||||
for xOffset in [-torsoWidth / 2 - armWidth / 2, torsoWidth / 2 + armWidth / 2] {
|
||||
let arm = SKShapeNode(rect: CGRect(x: xOffset - armWidth / 2,
|
||||
y: size.height * 0.05,
|
||||
width: armWidth, height: armLength),
|
||||
cornerRadius: armWidth / 2)
|
||||
arm.fillColor = color
|
||||
arm.strokeColor = color.withAlphaComponent(0.7)
|
||||
arm.lineWidth = 1
|
||||
addChild(arm)
|
||||
|
||||
// Add hand
|
||||
let hand = SKShapeNode(circleOfRadius: armWidth * 0.8)
|
||||
hand.position = CGPoint(x: xOffset, y: size.height * 0.05)
|
||||
hand.fillColor = SKColor(red: 1.0, green: 0.87, blue: 0.77, alpha: 1.0)
|
||||
hand.strokeColor = .clear
|
||||
addChild(hand)
|
||||
}
|
||||
|
||||
// Add legs
|
||||
let legWidth: CGFloat = 10
|
||||
let legHeight = size.height * 0.35
|
||||
for xOffset in [-torsoWidth * 0.25, torsoWidth * 0.25] {
|
||||
let leg = SKShapeNode(rect: CGRect(x: xOffset - legWidth / 2,
|
||||
y: -size.height * 0.45,
|
||||
width: legWidth, height: legHeight),
|
||||
cornerRadius: legWidth / 2)
|
||||
leg.fillColor = SKColor(red: 0.2, green: 0.2, blue: 0.4, alpha: 1.0) // Pants color
|
||||
leg.strokeColor = SKColor(red: 0.15, green: 0.15, blue: 0.3, alpha: 1.0)
|
||||
leg.lineWidth = 1
|
||||
addChild(leg)
|
||||
|
||||
// Add shoe
|
||||
let shoe = SKShapeNode(rect: CGRect(x: xOffset - legWidth * 0.6,
|
||||
y: -size.height * 0.48,
|
||||
width: legWidth * 1.2, height: 6),
|
||||
cornerRadius: 2)
|
||||
shoe.fillColor = .black
|
||||
shoe.strokeColor = .clear
|
||||
addChild(shoe)
|
||||
}
|
||||
|
||||
// Add indicator icon for type
|
||||
if type == .green {
|
||||
let checkmark = SKLabelNode(text: "✓")
|
||||
checkmark.fontSize = 16
|
||||
checkmark.fontColor = .white
|
||||
checkmark.position = CGPoint(x: 0, y: size.height * 0.1)
|
||||
checkmark.verticalAlignmentMode = .center
|
||||
addChild(checkmark)
|
||||
} else {
|
||||
let xMark = SKLabelNode(text: "✗")
|
||||
xMark.fontSize = 16
|
||||
xMark.fontColor = .white
|
||||
xMark.position = CGPoint(x: 0, y: size.height * 0.1)
|
||||
xMark.verticalAlignmentMode = .center
|
||||
addChild(xMark)
|
||||
}
|
||||
|
||||
setupPhysics(size: size)
|
||||
self.zPosition = Constants.ZPosition.entities
|
||||
self.name = type.isHarmful ? "grayHuman" : "greenHuman"
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Physics Setup
|
||||
private func setupPhysics(size: CGSize) {
|
||||
physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: size.width * 0.6,
|
||||
height: size.height * 0.8))
|
||||
physicsBody?.isDynamic = true
|
||||
physicsBody?.affectedByGravity = false
|
||||
physicsBody?.allowsRotation = false
|
||||
|
||||
if humanType.isHarmful {
|
||||
physicsBody?.categoryBitMask = Constants.PhysicsCategory.grayHuman
|
||||
} else {
|
||||
physicsBody?.categoryBitMask = Constants.PhysicsCategory.greenHuman
|
||||
}
|
||||
|
||||
physicsBody?.contactTestBitMask = Constants.PhysicsCategory.suitcase
|
||||
physicsBody?.collisionBitMask = Constants.PhysicsCategory.none
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// PlayerNode.swift
|
||||
// RollkofferSimulator
|
||||
//
|
||||
// Created by Ingo K.
|
||||
//
|
||||
|
||||
import SpriteKit
|
||||
|
||||
/// The player-controlled suitcase node
|
||||
class PlayerNode: SKNode {
|
||||
|
||||
// MARK: - Properties
|
||||
private let suitcaseBody: SKShapeNode
|
||||
private let handle: SKShapeNode
|
||||
private let wheels: [SKShapeNode]
|
||||
|
||||
// MARK: - Initialization
|
||||
override init() {
|
||||
let size = Constants.suitcaseSize
|
||||
|
||||
// Create suitcase body (rounded rectangle)
|
||||
let bodyRect = CGRect(x: -size.width / 2, y: -size.height / 2 + 10,
|
||||
width: size.width, height: size.height - 15)
|
||||
suitcaseBody = SKShapeNode(rect: bodyRect, cornerRadius: 8)
|
||||
suitcaseBody.fillColor = Constants.Colors.suitcaseColor
|
||||
suitcaseBody.strokeColor = SKColor(white: 0.2, alpha: 1.0)
|
||||
suitcaseBody.lineWidth = 2
|
||||
|
||||
// Create handle
|
||||
let handlePath = CGMutablePath()
|
||||
handlePath.move(to: CGPoint(x: -10, y: size.height / 2 - 5))
|
||||
handlePath.addLine(to: CGPoint(x: -10, y: size.height / 2 + 15))
|
||||
handlePath.addLine(to: CGPoint(x: 10, y: size.height / 2 + 15))
|
||||
handlePath.addLine(to: CGPoint(x: 10, y: size.height / 2 - 5))
|
||||
handle = SKShapeNode(path: handlePath)
|
||||
handle.strokeColor = SKColor(white: 0.3, alpha: 1.0)
|
||||
handle.lineWidth = 4
|
||||
handle.lineCap = .round
|
||||
|
||||
// Create wheels
|
||||
var tempWheels: [SKShapeNode] = []
|
||||
let wheelPositions = [
|
||||
CGPoint(x: -size.width / 2 + 8, y: -size.height / 2 + 5),
|
||||
CGPoint(x: size.width / 2 - 8, y: -size.height / 2 + 5)
|
||||
]
|
||||
for pos in wheelPositions {
|
||||
let wheel = SKShapeNode(circleOfRadius: 6)
|
||||
wheel.position = pos
|
||||
wheel.fillColor = SKColor.darkGray
|
||||
wheel.strokeColor = SKColor.black
|
||||
wheel.lineWidth = 1
|
||||
tempWheels.append(wheel)
|
||||
}
|
||||
wheels = tempWheels
|
||||
|
||||
super.init()
|
||||
|
||||
// Add decorative stripes
|
||||
let stripe1 = SKShapeNode(rect: CGRect(x: -size.width / 2 + 5, y: 0,
|
||||
width: size.width - 10, height: 3))
|
||||
stripe1.fillColor = SKColor(white: 0.3, alpha: 0.5)
|
||||
stripe1.strokeColor = .clear
|
||||
|
||||
let stripe2 = SKShapeNode(rect: CGRect(x: -size.width / 2 + 5, y: -15,
|
||||
width: size.width - 10, height: 3))
|
||||
stripe2.fillColor = SKColor(white: 0.3, alpha: 0.5)
|
||||
stripe2.strokeColor = .clear
|
||||
|
||||
addChild(suitcaseBody)
|
||||
addChild(handle)
|
||||
addChild(stripe1)
|
||||
addChild(stripe2)
|
||||
for wheel in wheels {
|
||||
addChild(wheel)
|
||||
}
|
||||
|
||||
setupPhysics()
|
||||
self.zPosition = Constants.ZPosition.player
|
||||
self.name = "player"
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Physics Setup
|
||||
private func setupPhysics() {
|
||||
let size = Constants.suitcaseSize
|
||||
physicsBody = SKPhysicsBody(rectangleOf: size)
|
||||
physicsBody?.isDynamic = true
|
||||
physicsBody?.affectedByGravity = false
|
||||
physicsBody?.allowsRotation = false
|
||||
physicsBody?.categoryBitMask = Constants.PhysicsCategory.suitcase
|
||||
physicsBody?.contactTestBitMask = Constants.PhysicsCategory.all
|
||||
physicsBody?.collisionBitMask = Constants.PhysicsCategory.none
|
||||
}
|
||||
|
||||
// MARK: - Visual Effects
|
||||
func startBlinking() {
|
||||
let fadeOut = SKAction.fadeAlpha(to: 0.3, duration: Constants.blinkDuration)
|
||||
let fadeIn = SKAction.fadeAlpha(to: 1.0, duration: Constants.blinkDuration)
|
||||
let blink = SKAction.sequence([fadeOut, fadeIn])
|
||||
let blinkRepeat = SKAction.repeat(blink, count: Constants.blinkCount)
|
||||
run(blinkRepeat, withKey: "blink")
|
||||
}
|
||||
|
||||
func stopBlinking() {
|
||||
removeAction(forKey: "blink")
|
||||
alpha = 1.0
|
||||
}
|
||||
|
||||
// MARK: - Movement
|
||||
func constrainToScreen(in frame: CGRect) {
|
||||
let halfWidth = Constants.suitcaseSize.width / 2
|
||||
let halfHeight = Constants.suitcaseSize.height / 2
|
||||
|
||||
let minX = frame.minX + halfWidth + 10
|
||||
let maxX = frame.maxX - halfWidth - 10
|
||||
let minY = frame.minY + halfHeight + 10
|
||||
let maxY = frame.maxY - halfHeight - 100 // Leave space for UI
|
||||
|
||||
position.x = max(minX, min(maxX, position.x))
|
||||
position.y = max(minY, min(maxY, position.y))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user