Get the FREE Ultimate OpenClaw Setup Guide →

ax-3d-games

npx machina-cli add skill Kasempiternal/axiom-v2/ax-3d-games --openclaw
Files (1)
SKILL.md
27.7 KB

2D/3D Games & Spatial Content

Quick Patterns

SpriteKit 2D Scene (SwiftUI)

import SpriteKit
import SwiftUI

class GameScene: SKScene {
    override func didMove(to view: SKView) {
        physicsWorld.contactDelegate = self
        physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
        let player = SKSpriteNode(imageNamed: "player")
        player.position = CGPoint(x: size.width / 2, y: size.height / 2)
        player.physicsBody = SKPhysicsBody(circleOfRadius: player.size.width / 2)
        addChild(player)
    }
}

struct GameView: View {
    var body: some View {
        SpriteView(scene: GameScene(size: CGSize(width: 390, height: 844)))
            .ignoresSafeArea()
    }
}

RealityKit 3D Content (SwiftUI, iOS 18+)

import RealityKit
import SwiftUI

struct ContentView: View {
    var body: some View {
        RealityView { content in
            let box = ModelEntity(
                mesh: .generateBox(size: 0.1),
                materials: [SimpleMaterial(color: .blue, isMetallic: true)]
            )
            box.position = [0, 0.5, -1]
            box.components.set(InputTargetComponent())
            box.components.set(CollisionComponent(shapes: [.generateBox(size: [0.1, 0.1, 0.1])]))
            content.add(box)
        }
        .gesture(TapGesture().targetedToAnyEntity().onEnded { value in
            value.entity.position.y += 0.1
        })
    }
}

RealityKit Custom ECS Component + System

struct HealthComponent: Component {
    var current: Int = 100
    var max: Int = 100
}

struct DamageSystem: System {
    static let query = EntityQuery(where: .has(HealthComponent.self))

    init(scene: Scene) {}

    func update(context: SceneUpdateContext) {
        for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
            var health = entity.components[HealthComponent.self]!
            if health.current <= 0 { entity.removeFromParent() }
        }
    }
}

// Register: HealthComponent.registerComponent(); DamageSystem.registerSystem()

SceneKit 3D (Legacy -- deprecated iOS 26)

import SceneKit

let scene = SCNScene()
let box = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0))
box.geometry?.firstMaterial?.diffuse.contents = UIColor.blue
box.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
scene.rootNode.addChildNode(box)

Decision Tree

What are you building?
|
+-- 2D game / sprite-based?
|   -> SpriteKit
|   |
|   +-- SwiftUI host?       -> SpriteView(scene:)
|   +-- Physics?             -> SKPhysicsBody + bitmask discipline
|   +-- Particles?           -> SKEmitterNode (Xcode particle editor)
|   +-- Tile maps?           -> SKTileMapNode
|   +-- Metal hybrid?        -> SKRenderer (manual render loop)
|
+-- 3D content / AR / spatial?
|   -> RealityKit (modern, ECS)
|   |
|   +-- SwiftUI host?       -> RealityView { content in }
|   +-- Simple model display?-> Model3D(named:) (async load)
|   +-- AR plane anchoring?  -> AnchorEntity(.plane(.horizontal))
|   +-- Image anchoring?     -> AnchorEntity(.image(group:name:))
|   +-- Tap/drag interaction?-> InputTargetComponent + CollisionComponent + gesture
|   +-- Custom logic per frame? -> System protocol + EntityQuery
|   +-- Physics simulation?  -> PhysicsBodyComponent + CollisionComponent
|   +-- Multiplayer sync?    -> SynchronizationComponent + MultipeerConnectivityService
|   +-- Metal integration?   -> RealityRenderer (custom Metal pipeline)
|
+-- Existing SceneKit project?
|   -> Maintain or migrate to RealityKit
|   |
|   +-- Minor update?        -> Keep SceneKit, plan migration
|   +-- Major rewrite?       -> Migrate to RealityKit now
|   +-- AR features needed?  -> RealityKit (SceneKit+ARKit is legacy)
|   +-- Need concept mapping?-> See SceneKit-to-RealityKit table below
|
+-- Issue / not working?
    +-- SpriteKit?  -> SpriteKit Diagnostics
    +-- RealityKit? -> RealityKit Diagnostics
    +-- SceneKit?   -> SceneKit section (deprecated)

Anti-Patterns

SpriteKit

SKShapeNode in production

Each SKShapeNode creates its own draw call. 50 shapes = 50 draw calls. Pre-render to SKTexture or use sprite sheets instead.

// BAD: 50 draw calls
for _ in 0..<50 { addChild(SKShapeNode(circleOfRadius: 10)) }

// GOOD: 1 draw call per atlas page
for _ in 0..<50 { addChild(SKSpriteNode(imageNamed: "circle")) }

Forgetting bitmask discipline

Physics contacts silently fail without proper bitmask setup. Define ALL categories as powers of 2 and set categoryBitMask, contactTestBitMask, AND collisionBitMask explicitly.

struct PhysicsCategory {
    static let none:    UInt32 = 0
    static let player:  UInt32 = 0b0001
    static let enemy:   UInt32 = 0b0010
    static let bullet:  UInt32 = 0b0100
    static let wall:    UInt32 = 0b1000
}

Not removing completed actions and nodes

Offscreen nodes with running actions waste CPU. Remove nodes when they leave the screen.

Coordinate confusion (bottom-left origin)

SpriteKit uses bottom-left origin. UIKit uses top-left. anchorPoint defaults to (0.5, 0.5) for sprites, (0, 0) for scenes.

Processing touches on wrong node

Use nodes(at:) or atPoint() with node names, not just position checks.

RealityKit

Treating ECS like OOP inheritance

Components are value types. Don't subclass Entity for behavior -- add/remove components instead.

Forgetting read-modify-write for components

Components are value types. entity.components[T.self]?.property = x silently discards the change. Must copy, modify, write back.

// BAD: silently does nothing
entity.components[HealthComponent.self]?.current -= 10

// GOOD: read-modify-write
var health = entity.components[HealthComponent.self]!
health.current -= 10
entity.components.set(health)

Missing CollisionComponent for gestures

Gestures require BOTH InputTargetComponent AND CollisionComponent. Without collision shape, taps pass through.

Component churn in Systems

Creating/removing components every frame causes memory allocation. Use boolean flags or state enums inside existing components instead.

Using reference types as Components

Components must be value types (struct). Classes cause memory issues and break ECS guarantees.

SceneKit (Legacy)

Starting new SceneKit projects (deprecated iOS 26)

SceneKit is soft-deprecated. Use RealityKit for all new 3D work. SceneKit won't receive new features.

Modifying materials in tight loops

SCNMaterial changes trigger shader recompilation. Cache material variants, swap references instead.


Deep Patterns

SpriteKit

Coordinate System

Bottom-left origin (0,0). Y increases upward. Scene anchorPoint defaults to (0,0) (bottom-left corner). Sprite anchorPoint defaults to (0.5, 0.5) (center).

Scene Architecture

let scene = GameScene(size: CGSize(width: 390, height: 844))
scene.scaleMode = .aspectFill // .fill, .aspectFit, .resizeFill

Scale modes: .aspectFill (crops, no letterbox), .aspectFit (letterbox, no crop), .resizeFill (stretches to fill), .fill (scene resizes to view).

Camera node for scrolling worlds:

let camera = SKCameraNode()
scene.camera = camera
scene.addChild(camera)
// HUD nodes: add as children of camera (stay fixed on screen)

Layer organization with z-ordering:

enum Layer: CGFloat {
    case background = -1, gameplay = 0, player = 1, effects = 2, hud = 3
}
node.zPosition = Layer.player.rawValue

Physics Bitmask System

Three masks control physics behavior:

  • categoryBitMask: What this body IS
  • collisionBitMask: What this body BOUNCES off (default: all)
  • contactTestBitMask: What generates delegate callbacks (default: none)
// Player bounces off walls, generates contact events with enemies
player.physicsBody!.categoryBitMask = PhysicsCategory.player
player.physicsBody!.collisionBitMask = PhysicsCategory.wall
player.physicsBody!.contactTestBitMask = PhysicsCategory.enemy

// Contacts not firing checklist:
// 1. contactDelegate set on physicsWorld?
// 2. contactTestBitMask set (not just collisionBitMask)?
// 3. At least one body is dynamic?
// 4. Both nodes in scene tree?
// 5. Bitmask math correct (AND operation)?

Contact detection:

func didBegin(_ contact: SKPhysicsContact) {
    let sorted = [contact.bodyA, contact.bodyB].sorted { $0.categoryBitMask < $1.categoryBitMask }
    let (first, second) = (sorted[0], sorted[1])
    // Now first always has the lower category -- deterministic handling
}

Physics body types: .dynamic (full simulation), .static (immovable, never set velocity), .kinematic (moved by code, affects dynamic bodies).

Anti-tunneling for fast objects:

body.usesPreciseCollisionDetection = true // continuous detection, more expensive

SKAction System

Actions are copied when run -- safe to reuse templates:

let moveUp = SKAction.moveBy(x: 0, y: 100, duration: 0.5)
let fadeOut = SKAction.fadeOut(withDuration: 0.3)
let sequence = SKAction.sequence([moveUp, fadeOut, .removeFromParent()])
let forever = SKAction.repeatForever(SKAction.sequence([moveUp, moveUp.reversed()]))

// Named actions (can stop individually)
node.run(moveUp, withKey: "movement")
node.removeAction(forKey: "movement")

// Custom action (per-frame callback)
SKAction.customAction(withDuration: 1.0) { node, elapsed in
    node.alpha = 1.0 - (elapsed / 1.0)
}

All Node Types (Performance Notes)

NodePurposeDraw Calls
SKSpriteNodeTextured spritesBatched per atlas
SKShapeNodeVector shapes1 per node (expensive!)
SKLabelNodeText1 per node
SKEmitterNodeParticles1 per emitter
SKTileMapNodeTile gridsBatched
SKVideoNodeVideo playback1 per node
SKReferenceNode.sks file referenceVaries
SKCropNodeMaskingAdds passes
SKEffectNodeCIFilter/blurRasterizes subtree
SK3DNodeSceneKit in 2DFull 3D pipeline
SKCameraNodeViewport control0 (no rendering)
SKLightNode2D lightingAdds light pass
SKFieldNodePhysics fields0 (physics only)
SKAudioNodePositional audio0 (audio only)
SKTransformNode3D rotation for 2D0 (transform only)

Performance Optimization

// Texture atlases: batch draw calls
let atlas = SKTextureAtlas(named: "Sprites")
let texture = atlas.textureNamed("player_idle_01")

// Object pooling
class BulletPool {
    private var available: [SKSpriteNode] = []
    func get() -> SKSpriteNode {
        available.isEmpty ? createNew() : available.removeLast()
    }
    func recycle(_ node: SKSpriteNode) {
        node.removeFromParent(); node.removeAllActions()
        available.append(node)
    }
}

View diagnostics:

skView.showsFPS = true
skView.showsNodeCount = true
skView.showsDrawCount = true   // Most important -- target < 20
skView.showsPhysics = true     // Debug collision shapes

Game Loop Phases

update(_:) -> didEvaluateActions() -> didSimulatePhysics() -> didApplyConstraints() -> didFinishUpdate() -> render.

SwiftUI Integration

struct GameView: View {
    @StateObject var game = GameModel()

    var body: some View {
        SpriteView(scene: makeScene(), preferredFramesPerSecond: 60,
                   options: [.ignoresSiblingOrder], debugOptions: [.showsFPS])
    }
}

// @Observable bridge for SwiftUI <-> SpriteKit communication
@Observable class GameModel {
    var score = 0
    var scene: GameScene?
}

SKRenderer (Metal Hybrid)

let renderer = SKRenderer(device: MTLCreateSystemDefaultDevice()!)
renderer.scene = gameScene
// In Metal render loop:
renderer.update(atTime: currentTime)
renderer.render(withViewport: viewport, renderPassDescriptor: rpd,
                commandQueue: queue, renderCommandEncoder: encoder)

RealityKit

Entity-Component-System (ECS)

Entity: Identity container (has position via Transform, holds components). NOT subclassed for behavior. Component: Data (struct, value type). No logic. Examples: HealthComponent, VelocityComponent. System: Logic that runs every frame on entities matching a query.

Entity hierarchy:

let parent = Entity()
let child = ModelEntity(mesh: .generateSphere(radius: 0.1))
parent.addChild(child)
child.position = [0, 0.5, 0] // Relative to parent
child.setPosition([0, 1, 0], relativeTo: nil) // World space

// Find entities
entity.findEntity(named: "target")
entity.children.first(where: { $0.components.has(HealthComponent.self) })

Built-in Components Reference

ComponentPurpose
TransformPosition, rotation, scale
ModelComponentMesh + materials
CollisionComponentCollision shapes for physics and gestures
PhysicsBodyComponent.dynamic / .static / .kinematic physics
PhysicsMotionComponentVelocity and angular velocity
InputTargetComponentEnable gesture targeting
AnchoringComponentAR world anchoring
SynchronizationComponentMultiplayer sync
DirectionalLightComponentDirectional light source
PointLightComponentPoint light source
SpotLightComponentSpot light source
AccessibilityComponentVoiceOver for 3D content
OpacityComponentTransparency
GroundingShadowComponentDrop shadow on ground plane
HoverEffectComponentvisionOS hover highlight
ImageBasedLightComponentIBL environment lighting
ImageBasedLightReceiverComponentReceive IBL from another entity

RealityView (iOS 18+)

RealityView { content in
    // Called once. Add entities to content.
    let model = try? await ModelEntity(named: "Robot")
    if let model { content.add(model) }
} update: { content in
    // Called when @State changes. Modify existing entities.
} attachments: {
    // SwiftUI views attached to 3D space
    Attachment(id: "label") {
        Text("Hello").padding().glassBackgroundEffect()
    }
}

Model3D (Async Loading)

Model3D(named: "Robot") { model in
    model.resizable().scaledToFit()
} placeholder: {
    ProgressView()
}

AR Anchoring

// Plane anchoring
let anchor = AnchorEntity(.plane(.horizontal, classification: .floor, minimumBounds: [0.5, 0.5]))
anchor.addChild(model)

// Image anchoring (from AR Resource Group in asset catalog)
let anchor = AnchorEntity(.image(group: "ARResources", name: "poster"))

// SpatialTrackingSession (iOS 18+) for hand/world tracking
let config = SpatialTrackingSession.Configuration(tracking: [.hand, .world])
let session = SpatialTrackingSession()
let result = await session.run(config)

Gesture Interaction

Requirements: entity needs BOTH InputTargetComponent AND CollisionComponent.

entity.components.set(InputTargetComponent())
entity.components.set(CollisionComponent(shapes: [.generateBox(size: [0.1, 0.1, 0.1])]))

// In view:
RealityView { /* ... */ }
.gesture(TapGesture().targetedToAnyEntity().onEnded { value in
    print("Tapped: \(value.entity.name)")
})
.gesture(DragGesture().targetedToAnyEntity().onChanged { value in
    value.entity.position = value.convert(value.location3D, from: .local, to: .scene)
})

ManipulationComponent (visionOS): Adds built-in translate/rotate/scale with two-hand support.

Materials

MaterialUse Case
SimpleMaterialSolid color or basic texture, metallic/roughness
PhysicallyBasedMaterialFull PBR (baseColor, roughness, metallic, normal, AO, emissive)
UnlitMaterialNo lighting (UI overlays, always-bright)
OcclusionMaterialInvisible but hides content behind it (AR masking)
VideoMaterialPlay video on surface
ShaderGraphMaterialReality Composer Pro shader graphs
CustomMaterialMetal shader integration

PBR setup:

var material = PhysicallyBasedMaterial()
material.baseColor = .init(tint: .white, texture: .init(try .load(named: "albedo")))
material.roughness = .init(floatLiteral: 0.3)
material.metallic = .init(floatLiteral: 1.0)
material.normal = .init(texture: .init(try .load(named: "normal")))

Physics

// Dynamic body
entity.components.set(PhysicsBodyComponent(
    shapes: [.generateBox(size: [0.1, 0.1, 0.1])],
    mass: 1.0,
    material: .generate(staticFriction: 0.5, dynamicFriction: 0.3, restitution: 0.7),
    mode: .dynamic
))
entity.components.set(PhysicsMotionComponent(linearVelocity: [0, 5, 0]))

// Collision events
scene.subscribe(to: CollisionEvents.Began.self, on: entity) { event in
    let other = event.entityA == entity ? event.entityB : event.entityA
}

Modes: .dynamic (full simulation), .static (immovable), .kinematic (code-driven, affects dynamics).

Animation

// Transform animation
var transform = entity.transform
transform.translation.y += 0.5
entity.move(to: transform, relativeTo: entity.parent, duration: 1.0, timingFunction: .easeInOut)

// Play USDZ animation
if let animation = entity.availableAnimations.first {
    entity.playAnimation(animation.repeat())
}

// AnimationResource
let orbit = OrbitAnimation(duration: 3, axis: [0, 1, 0], startTransform: entity.transform, spinClockwise: true)
entity.playAnimation(try AnimationResource.generate(with: orbit))

Audio

entity.components.set(SpatialAudioComponent())
entity.components.set(AmbientAudioComponent())
let resource = try AudioFileResource.load(named: "sound.wav")
entity.playAudio(resource)

RealityRenderer (Metal Integration)

let renderer = try RealityRenderer()
let entity = ModelEntity(mesh: .generateSphere(radius: 0.1))
renderer.entities.append(entity)

// In Metal render loop:
try renderer.updateAndRender(deltaTime: dt, viewport: viewport,
    colorTexture: drawable.texture, depthTexture: depthTex,
    commandBuffer: commandBuffer)

Multiplayer

let service = try MultipeerConnectivityService(session: mcSession)
entity.components.set(SynchronizationComponent())
scene.synchronizationService = service
// Ownership: entity.requestOwnership { result in }
// SynchronizationComponent.isOwner for local authority check

SceneKit (Deprecated iOS 26 -- Maintenance Only)

Migration Status

SceneKit is soft-deprecated in iOS 26. No new features. Existing apps continue working. Plan migration to RealityKit for new work.

SceneKit-to-RealityKit Concept Mapping

SceneKitRealityKit
SCNSceneEntity (root)
SCNNodeEntity
SCNGeometryMeshResource
SCNMaterialMaterial protocol (SimpleMaterial, PBR)
SCNLightDirectionalLightComponent, PointLightComponent
SCNCameraPerspectiveCamera entity
SCNPhysicsBodyPhysicsBodyComponent
SCNPhysicsShapeShapeResource (CollisionComponent)
SCNActionTransform animations, entity.move(to:)
SCNTransactionNot needed (ECS handles updates)
SCNHitTestResultEntityTargetValue (gesture system)
SCNViewRealityView
ARSCNViewRealityView + ARKit anchoring
SCNNode.addChildNodeEntity.addChild
node.positionentity.position (SIMD3<Float>)
SCNVector3SIMD3<Float>
SCNQuaternionsimd_quatf

Key Architecture Differences

  • SceneKit: OOP scene graph (subclass nodes). RealityKit: ECS (compose with components).
  • SceneKit: SCNVector3, SCNQuaternion. RealityKit: SIMD3<Float>, simd_quatf.
  • SceneKit: Delegate-based updates. RealityKit: System protocol with EntityQuery.
  • SceneKit: Manual render loop via SCNRenderer. RealityKit: RealityRenderer.
  • SceneKit: SCNPhysicsContactDelegate. RealityKit: CollisionEvents subscription.

SceneKit Core API (For Maintenance)

// Scene setup
let scene = SCNScene(named: "scene.usdz")!
let scnView = SCNView(frame: .zero)
scnView.scene = scene
scnView.allowsCameraControl = true
scnView.autoenablesDefaultLighting = true

// Materials (6 lighting models: .physicallyBased, .blinn, .phong, .lambert, .constant, .shadowOnly)
let material = SCNMaterial()
material.lightingModel = .physicallyBased
material.diffuse.contents = UIColor.blue
material.metalness.contents = 0.8
material.roughness.contents = 0.2

// Animation
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
node.position = SCNVector3(0, 1, 0)
SCNTransaction.commit()

// Physics
node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: SCNPhysicsShape(geometry: node.geometry!))
node.physicsBody?.categoryBitMask = 1
node.physicsBody?.contactTestBitMask = 2

Constraints: SCNLookAtConstraint, SCNBillboardConstraint, SCNDistanceConstraint, SCNReplicatorConstraint, SCNAccelerationConstraint, SCNSliderConstraint, SCNAvoidOccluderConstraint.

SCNAction catalog mirrors SKAction: move, rotate, scale, fade, sequence, group, repeat, removeFromParent, run(block), customAction.


Diagnostics

SpriteKit Diagnostics

Root Causes (by frequency)

  1. Physics bitmask misconfiguration -- 35%
  2. Coordinate system confusion -- 20%
  3. Draw call explosion (SKShapeNode) -- 15%
  4. Memory leaks (retained actions/nodes) -- 15%
  5. Threading violations -- 15%

Symptom Table

SymptomLikely CauseFix
Contacts not firingcontactTestBitMask not set, delegate missing, or both bodies staticSet contactTestBitMask, assign contactDelegate, ensure >= 1 dynamic body
Objects tunnel through wallsFast small objects skip collisionusesPreciseCollisionDetection = true, thicker walls
FPS drops below 60Too many draw callsCheck showsDrawCount, replace SKShapeNode with sprites, use atlases
Touches not registeringisUserInteractionEnabled false, wrong node, node z-orderEnable interaction, check nodes(at:), verify zPosition
Memory grows over timeOffscreen nodes not removed, action references retainedRemove nodes leaving screen, use removeAllActions() on recycle
Sprites in wrong positionCoordinate origin confusionSpriteKit origin is bottom-left; UIKit is top-left; check anchorPoint
Scene transition crashRetaining references to old scene's nodesUse weak references, clean up in willMove(from:)
Physics jitterSetting position directly on dynamic bodyUse applyForce/applyImpulse, not .position =
Node not visibleWrong zPosition, outside scene bounds, alpha = 0Check zPosition, position, parent chain, alpha

Quick Diagnostic

// Enable all debug overlays
skView.showsFPS = true
skView.showsNodeCount = true
skView.showsDrawCount = true
skView.showsPhysics = true

// Dump scene tree
func dumpTree(_ node: SKNode, indent: Int = 0) {
    let prefix = String(repeating: "  ", count: indent)
    print("\(prefix)\(type(of: node)) '\(node.name ?? "")' z:\(node.zPosition) pos:\(node.position)")
    for child in node.children { dumpTree(child, indent: indent + 1) }
}

// Physics bitmask audit
scene.enumerateChildNodes(withName: "//*") { node, _ in
    if let body = node.physicsBody {
        print("\(node.name ?? "?"): cat=\(body.categoryBitMask) col=\(body.collisionBitMask) contact=\(body.contactTestBitMask)")
    }
}

RealityKit Diagnostics

Root Causes (by frequency)

  1. Missing components (InputTarget, Collision) -- 30%
  2. Component read-modify-write errors -- 25%
  3. Entity not in scene / not visible -- 20%
  4. AR anchor not tracking -- 15%
  5. Material/lighting issues -- 10%

Symptom Table

SymptomLikely CauseFix
Entity not visibleNot added to scene, scale 0, material transparent, behind cameraCheck entity.isEnabled, parent chain, position, material
Gesture not respondingMissing InputTargetComponent or CollisionComponentAdd BOTH components; collision shape must cover entity
Component change ignoredValue type not written backUse read-modify-write pattern: get, modify, entity.components.set()
Anchor not trackingInsufficient features, wrong classification, device unsupportedCheck ARWorldTrackingConfiguration.isSupported, improve lighting
Material looks wrongMissing normal map, wrong lighting, IBL not setAdd ImageBasedLightComponent to scene, check material properties
Physics not workingNo PhysicsBodyComponent, no CollisionComponent, wrong modeNeed both components; dynamic bodies for simulation
Multiplayer out of syncSynchronizationComponent missing, ownership conflictSet sync component, request ownership before modifying
Simulator crashGPU feature unsupported in simTest on device; simulator lacks full GPU support
System not runningComponent not registered, system not registeredCall Component.registerComponent() and System.registerSystem() at app launch
Poor performanceToo many unique meshes, component churnUse instancing (same MeshResource), avoid add/remove components per frame

Quick Diagnostic

// Check entity visibility chain
func diagnoseVisibility(_ entity: Entity) {
    print("Enabled: \(entity.isEnabled)")
    print("Position: \(entity.position)")
    print("Scale: \(entity.scale)")
    print("Has model: \(entity.components.has(ModelComponent.self))")
    print("Parent: \(entity.parent?.name ?? "none")")
    if let model = entity.components[ModelComponent.self] {
        print("Mesh bounds: \(model.mesh.bounds)")
    }
}

// Check gesture prerequisites
func diagnoseGesture(_ entity: Entity) {
    print("InputTarget: \(entity.components.has(InputTargetComponent.self))")
    print("Collision: \(entity.components.has(CollisionComponent.self))")
    if let collision = entity.components[CollisionComponent.self] {
        print("Collision shapes: \(collision.shapes.count)")
    }
}

Common Mistakes Table

MistakeImpactFix
Reference type componentMemory issues, ECS breaksUse struct for all components
Subclassing Entity for behaviorCan't swap behavior at runtimeUse components instead
Not registering Component/SystemSystem never runs, component ignoredRegister at app launch
Modifying entity from background threadRace conditions, crashesUse MainActor or scene's update
Loading USDZ synchronouslyUI freeze on loadUse ModelEntity(named:) with async/await

Related

  • ax-metal -- GPU programming and Metal shader migration
  • ax-camera -- Camera capture pipeline (AR camera feed)
  • ax-vision -- Computer vision (hand/body pose for gesture input)
  • WWDC: SpriteKit (2017-609), SceneKit (2017-604), RealityKit (2019-603, 2021-10074, 2023-10080, 2024-10103)

Framework Selection Guide

NeedFrameworkKey Advantage
2D game with physicsSpriteKitMature, integrated physics, particle editor
3D content, AR, spatialRealityKitModern ECS, AR-native, Apple's investment path
Existing 3D projectSceneKit (maintain)Already built, still runs, plan migration
Metal + 2D gameSKRendererSpriteKit scene in custom Metal pipeline
Metal + 3D contentRealityRendererRealityKit entities in custom Metal pipeline
Simple model viewerModel3DDeclarative SwiftUI, async loading

SpriteKit SKView Configuration Reference

skView.ignoresSiblingOrder = true    // Enable draw call batching (CRITICAL for performance)
skView.preferredFramesPerSecond = 60 // or 120 on ProMotion
skView.isAsynchronous = true         // Default: renders on its own thread
skView.shouldCullNonVisibleNodes = true // Default: skip offscreen nodes

API Availability

APIiOS
SpriteKit7+
SceneKit8+
RealityKit13+
RealityView18+
Model3D18+
SpriteView (SwiftUI)14+
ARView (deprecated)13+
SpatialTrackingSession18+
ManipulationComponentvisionOS 1+
RealityRenderer18+
SKRenderer11+
SceneKit deprecated26+

Source

git clone https://github.com/Kasempiternal/axiom-v2/blob/main/axiom-plugin/skills/ax-3d-games/SKILL.mdView on GitHub

Overview

ax-3d-games covers SpriteKit for 2D, SceneKit as a legacy 3D option, RealityKit for modern 3D/AR with ECS, and SwiftUI integration via SpriteView and RealityView. It also addresses physics, ECS architecture, and diagnostics to help you build, test, and migrate spatial content across platforms.

How This Skill Works

Choose SpriteKit for 2D scenes and embed them in SwiftUI with SpriteView, including physics setup. For 3D, use RealityKit with ModelEntity, components, and an ECS-style approach via Systems and EntityQuery, hosted in SwiftUI through RealityView. SceneKit remains available for legacy projects but is deprecated in favor of RealityKit.

When to Use It

  • Building a 2D sprite-based game inside a SwiftUI app
  • Creating modern 3D/AR content with RealityKit and ECS
  • Implementing a custom ECS pattern with components and systems
  • Maintaining or migrating a legacy SceneKit project
  • Adding physics, collisions, and interactive input to spatial apps

Quick Start

  1. Step 1: Decide 2D (SpriteKit) or 3D/AR (RealityKit) path and scaffold with SpriteView or RealityView
  2. Step 2: Create a basic scene/entity (e.g., SKSpriteNode with physics or a ModelEntity with a CollisionComponent)
  3. Step 3: Add simple interaction (tap to move or impulse; register a minimal ECS system) and iterate

Best Practices

  • Prefer RealityKit ECS for new 3D/AR apps and use components to compose behavior
  • Host SpriteKit in SwiftUI with SpriteView for quick 2D UI integration
  • Leverage PhysicsBodyComponent and CollisionComponent to manage physics interactions
  • Plan migration from SceneKit to RealityKit when updating legacy projects
  • Keep entities and systems modular; favor small, testable systems for per-frame logic

Example Use Cases

  • SpriteKit 2D game rendered inside a SwiftUI view via SpriteView
  • RealityKit 3D scene with tappable and movable entities using RealityView
  • Custom ECS demo with HealthComponent and DamageSystem for entity lifecycle
  • Legacy SceneKit project migrated gradually toward RealityKit architecture
  • AR content anchored to planes or images using AnchorEntity and input components

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers