A fully immersive first-person wave shooter for Apple Vision Pro. Defend yourself against waves of geometric enemies using hand tracking and spatial awareness.
Spatial Defense is a tower defense game where YOU are the tower. Enemies spawn in 360° around you and move toward your position. Use hand tracking to aim and pinch gestures to shoot them down before they reach you.
- 🖐️ Hand Tracking: Point and pinch to shoot with both hands
- 🌍 Full Space Immersion: 360° gameplay in volumetric space
- 💥 Particle Effects: Bullet trails, explosions, and power-up effects
- 🎮 Wave-Based Progression: Increasing difficulty with enemy variety
- 🔊 Spatial Audio: 3D sound positioning for immersive feedback
- ⚡ Power-Ups: Collectible upgrades like rapid fire and spread shot
- 🧠 AI-Powered Adaptation: Smart power-up drops based on your playstyle (uses Apple Intelligence)
This project is designed to teach:
- visionOS 26 app architecture
- ImmersiveSpace and Full Space mode
- RealityKit Entity-Component-System (ECS)
- Hand tracking with ARKit
- Particle systems and visual effects
- Game loop implementation
- Spatial audio integration
- 3D math basics (vectors, raycasting)
- Apple Intelligence integration for adaptive gameplay
- CoreML for on-device player behavior prediction
- Xcode: 26 or later
- visionOS SDK: 26 or later
- Swift: 6.2+
- Device: Apple Vision Pro (or visionOS Simulator)
- SwiftUI: App structure and UI
- RealityKit: 3D rendering and Entity-Component-System
- ARKit: Hand tracking provider
- AVFoundation: Spatial audio
- Combine: Reactive state management
- Apple Intelligence: AI-powered gameplay adaptation (iOS 26+)
- CoreML: On-device machine learning for player behavior analysis
SpatialDefense/
├── SpatialDefenseApp.swift # Main app entry point
├── Views/
│ ├── GameView.swift # Main immersive game view
│ ├── MenuView.swift # Start menu and settings
│ └── HUDView.swift # Score, health, wave counter
├── Systems/
│ ├── EnemySpawnSystem.swift # Spawns enemies in waves
│ ├── MovementSystem.swift # Moves enemies toward player
│ ├── ShootingSystem.swift # Handles aiming and firing
│ ├── CollisionSystem.swift # Detects bullet-enemy hits
│ ├── ParticleSystem.swift # Manages visual effects
│ ├── WaveSystem.swift # Controls wave progression
│ ├── ScoreSystem.swift # Tracks score and health
│ └── AIAdaptationSystem.swift # AI-powered gameplay adaptation
├── Components/
│ ├── EnemyComponent.swift # Enemy data (health, type, speed)
│ ├── BulletComponent.swift # Bullet data (damage, lifetime)
│ ├── PowerUpComponent.swift # Power-up data (type, effect)
│ └── HealthComponent.swift # Health tracking
├── Entities/
│ ├── EnemyFactory.swift # Creates enemy entities
│ ├── BulletFactory.swift # Creates bullet entities
│ ├── PowerUpFactory.swift # Creates power-up entities
│ └── ParticleFactory.swift # Creates particle effects
├── Utilities/
│ ├── HandTrackingManager.swift # Manages hand tracking state
│ ├── SpatialAudioManager.swift # Handles 3D audio
│ ├── AIBehaviorAnalyzer.swift # Analyzes player patterns for AI
│ ├── GameConstants.swift # Game balance constants
│ └── Extensions.swift # Utility extensions
└── Resources/
├── Sounds/ # Audio files
└── Assets.xcassets # App icons and images
| Enemy Type | Shape | Color | Speed | Health | Points |
|---|---|---|---|---|---|
| Basic | Cube | Red | 1.0 | 1 | 10 |
| Fast | Small Sphere | Yellow | 2.5 | 1 | 25 |
| Tank | Large Cylinder | Gray | 0.5 | 5 | 50 |
| Power-Up | Effect | Duration | Visual |
|---|---|---|---|
| Rapid Fire | 3x fire rate | 10s | Blue rotating sphere |
| Spread Shot | 5 bullets per shot | 8s | Orange rotating sphere |
| Freeze Time | 50% enemy speed | 6s | Cyan rotating sphere |
| Shield | +1 health | Instant | Green rotating sphere |
Wave 1: 5 Basic enemies
Wave 2: 8 Basic enemies
Wave 3: 6 Basic + 2 Fast enemies
Wave 4: 10 Basic + 3 Fast enemies
Wave 5: 8 Basic + 4 Fast + 1 Tank
...
Every 5 waves: Boss wave (10 Tanks)
The game uses Apple Intelligence and CoreML to analyze your playstyle in real-time and adapt the experience to keep you engaged. This is on-device ML - no data leaves your Vision Pro.
| Metric | What It Means | How It's Used |
|---|---|---|
| Accuracy Rate | % of shots that hit enemies | Low accuracy → Spawn Spread Shot power-up |
| Reaction Time | Average time to shoot at new enemies | Slow reaction → Reduce enemy speed slightly |
| Overwhelm Score | How many enemies get close to you | High overwhelm → Spawn Freeze Time or Shield |
| Engagement Pattern | Which directions you focus on | Spawn enemies in neglected areas |
| Power-Up Usage | Which power-ups you collect/ignore | Spawn more of what you use |
| Survival Time | How long you last in waves | Adjust difficulty curve |
The AI decides which power-up to spawn based on your current needs:
// Example AI decision logic
if player.accuracy < 0.4 && player.shotsFired > 20 {
// Player is struggling with aim
spawnPowerUp(.spreadShot, priority: .high)
} else if player.nearMissCount > 5 {
// Enemies getting too close
spawnPowerUp(.freezeTime, priority: .urgent)
} else if player.currentStreak > 10 {
// Player is doing well, reward them
spawnPowerUp(.rapidFire, priority: .normal)
}Player Actions → Behavior Analyzer → CoreML Model → Adaptation Decisions
↓ ↓
Metrics DB ←──────────────────────────────── Update Game Parameters
Components:
- AIBehaviorAnalyzer: Collects gameplay metrics every frame
- PlayerProfileModel: CoreML model trained to classify player skill/style
- AIAdaptationSystem: Makes real-time decisions based on AI predictions
- FeedbackLoop: Updates model based on player response to adaptations
- ✅ 100% on-device: No data sent to servers
- ✅ Lightweight: CoreML model < 5MB
- ✅ Efficient: Runs inference once per second (not per frame)
- ✅ Optional: Players can disable AI adaptation in settings
We'll use Create ML to build a simple classifier:
Input Features:
- Accuracy (Float)
- Reaction time (Float)
- Enemies killed per wave (Int)
- Health remaining (Int)
- Time survived (Float)
Output:
- Recommended power-up (Category: spread_shot, rapid_fire, freeze_time, shield, none)
Training:
- We'll create synthetic training data based on game design principles
- Model size: ~3MB
- Inference time: < 10ms
Scenario 1: Struggling Player
Metrics: accuracy=25%, enemiesClose=4, health=1
AI Decision: Spawn Shield (urgent) + Freeze Time
Result: Player gets breathing room to improve
Scenario 2: Skilled Player
Metrics: accuracy=80%, killStreak=15, health=3
AI Decision: Increase enemy spawn rate + Spawn Rapid Fire
Result: Challenge matches skill level
Scenario 3: Defensive Player
Metrics: shotDelay=0.8s, favorsBackside=true
AI Decision: Spawn enemies from front + Rapid Fire power-up
Result: Encourage more aggressive play
- Create visionOS project
- Set up ImmersiveSpace
- Add first 3D cube
- Implement basic app navigation
- Set up HandTrackingProvider
- Track hand positions in 3D space
- Calculate aim direction from index finger
- Detect pinch gestures
- Create EnemyComponent
- Implement EnemyFactory with simple geometry
- Build EnemySpawnSystem (spawn in circle around player)
- Build MovementSystem (move toward center)
- Add health and damage system
- Build ShootingSystem with raycasting
- Create BulletFactory with particle trails
- Implement CollisionSystem
- Add explosion particle effects
- Integrate spatial audio for shooting/explosions
- Implement WaveSystem
- Build ScoreSystem (points, combos, health)
- Create HUD with SwiftUI overlay
- Add game over and restart logic
- Create power-up spawning and collection
- Add visual feedback (hit flashes, screen shake)
- Implement difficulty scaling
- Add menu system
- Create settings (difficulty, volume)
- Optimize performance
- Add haptic feedback (if available)
- Create AIBehaviorAnalyzer to track metrics
- Build synthetic training dataset
- Train CoreML model with Create ML
- Implement AIAdaptationSystem
- Test AI decisions across player types
- Add AI toggle in settings
RealityKit uses ECS architecture:
- Entity: A thing in your game (enemy, bullet, power-up)
- Component: Data attached to entities (position, health, speed)
- System: Logic that processes entities with specific components
Example:
// Entity
let enemy = ModelEntity(mesh: .generateBox(size: 0.3))
// Component
struct EnemyComponent: Component {
var health: Int
var speed: Float
var type: EnemyType
}
// System (runs every frame)
func updateMovement(deltaTime: Float) {
for entity in enemyEntities {
if let enemy = entity.components[EnemyComponent.self] {
// Move enemy toward player
}
}
}// Get hand position
let handAnchor = handTracking.handAnchors[.right]
let indexTip = handAnchor.skeleton.joint(.indexFingerTip)
let thumbTip = handAnchor.skeleton.joint(.thumbTip)
// Calculate aim direction
let aimDirection = normalize(indexTip.position - headPosition)
// Detect pinch
let isPinching = distance(indexTip.position, thumbTip.position) < 0.02var particleEmitter = ParticleEmitterComponent()
particleEmitter.emitterShape = .sphere
particleEmitter.birthRate = 100
particleEmitter.lifeSpan = 0.5
particleEmitter.speed = 2.0
entity.components.set(particleEmitter)import CoreML
class AIBehaviorAnalyzer {
private var shotsFired: Int = 0
private var shotsHit: Int = 0
private var model: PlayerProfileModel?
func recordShot(hit: Bool) {
shotsFired += 1
if hit { shotsHit += 1 }
}
func getRecommendedPowerUp() async -> PowerUpType {
let accuracy = Float(shotsHit) / Float(max(shotsFired, 1))
// Create input for CoreML model
let input = PlayerProfileInput(
accuracy: accuracy,
reactionTime: getAverageReactionTime(),
enemiesKilled: getEnemiesKilledThisWave(),
healthRemaining: getPlayerHealth(),
timeSurvived: getCurrentWaveTime()
)
// Run prediction
guard let prediction = try? await model?.prediction(from: input) else {
return .none
}
return PowerUpType(rawValue: prediction.recommendedPowerUp) ?? .none
}
}git clone https://github.com/shortcut/SpacialDefender
cd spatial-defenseopen SpatialDefense.xcodeproj- Select project in navigator
- Go to "Signing & Capabilities"
- Select your development team
- Xcode will automatically configure provisioning
- Select "Apple Vision Pro" simulator or your device
- Press
Cmd + Rto build and run
- Use Swift naming conventions (camelCase for variables, PascalCase for types)
- Keep functions small and focused (< 50 lines)
- Comment complex game logic
- Use meaningful variable names (
enemyHealthnoteh)
- Frame Rate: 90 FPS minimum (visionOS requirement)
- Particle Count: < 1000 active particles
- Entity Count: < 200 active entities
- Draw Calls: Minimize by batching similar geometry
- Test with both hands
- Test with one hand
- Test enemy spawning at all angles
- Test wave progression
- Test power-up collection
- Test game over state
- Test performance (monitor FPS)
Solution: Simulator has limited hand tracking. Use keyboard shortcuts:
- Hold
Option+ move mouse to simulate hand - Click to simulate pinch
Solution: Check:
- Entity has material with color/texture
- Lighting is present in scene
- Entity is within camera view frustum
- Scale is appropriate (not too small/large)
Solution:
- Reduce particle emission rates
- Remove entities that are far from player
- Use object pooling for bullets/enemies
- Profile with Instruments
Solution: Ensure:
- Entities have collision components
- Collision shapes match visual geometry
- Raycast direction is correct
- Collision masks are properly set
- visionOS Documentation
- RealityKit
- ARKit Hand Tracking
- SwiftUI for visionOS
- CoreML
- Create ML
- Apple Intelligence
- Apple's Sample Projects
- WWDC 2025 Sessions
- "Build immersive experiences with RealityKit"
- "Explore hand tracking in visionOS"
- "Integrate Apple Intelligence into your apps"
- "Build ML models with Create ML"
// Located in GameConstants.swift
struct GameConstants {
// Player
static let playerStartHealth = 3
static let playerShootCooldown: Float = 0.2 // seconds
// Enemies
static let basicEnemySpeed: Float = 1.0
static let fastEnemySpeed: Float = 2.5
static let tankEnemySpeed: Float = 0.5
// Spawning
static let spawnRadius: Float = 5.0 // meters from player
static let waveCooldown: Float = 12.0 // seconds between waves
// Scoring
static let basicEnemyPoints = 10
static let fastEnemyPoints = 25
static let tankEnemyPoints = 50
static let comboMultiplier = 1.5
// Power-ups
static let powerUpSpawnChance: Float = 0.15 // 15% per wave
static let rapidFireDuration: Float = 10.0
static let spreadShotDuration: Float = 8.0
static let freezeTimeDuration: Float = 6.0
}- Archive build in Xcode
- Upload to App Store Connect
- Create TestFlight build
- Invite beta testers
- Requires Apple Vision Pro device for testing
- Submit app review with demo video
- Provide privacy policy
- Set pricing and availability
This is a learning project! Contributions welcome:
- Fork the repository
- Create feature branch (
git checkout -b feature/new-enemy-type) - Commit changes (
git commit -m 'Add laser beam enemy') - Push to branch (
git push origin feature/new-enemy-type) - Open Pull Request
MIT License - See LICENSE file for details
- Built for Apple Vision Pro
- Created as a comprehensive learning project for visionOS development
- Special thanks to the visionOS developer community
Questions? Issues?
- Open an issue on GitHub
Good luck, and happy spatial coding! 🥽✨