<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged spritekit</title>
<link>https://coderlegion.com/tag/spritekit</link>
<description>Powered by Question2Answer</description>
<item>
<title>Tap-to-Match Marble Game in SpriteKit and SwiftUI: Physics, Cluster Matching, Lives and Power-Ups</title>
<link>https://coderlegion.com/32677/tap-match-marble-game-spritekit-and-swiftui-physics-cluster-matching-lives-and-power-ups</link>
<description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bestlatestapps.com/blog/building-a-tap-to-match-marble-game-with-spritekit-and-swiftui/&quot;&gt;https://bestlatestapps.com/blog/building-a-tap-to-match-marble-game-with-spritekit-and-swiftui/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A complete walkthrough of the game loop: marbles that settle under real physics, finding touching same-colour clusters, a pause-safe countdown, lives and levels, Lightning, Heart and Bomb power-ups, and particle effects, all hosted in SwiftUI with SpriteView&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bestlatestapps.com/assets/blog/building-a-tap-to-match-marble-game-with-spritekit-and-swiftui/marbles-popperz-icon-512.jpg&quot; alt=&quot;Marbles Popperz app icon&quot;&gt;&lt;/p&gt;
&lt;p&gt;Every game developer has a &quot;small&quot; game that taught them how games actually work. Mine is &lt;strong&gt;Marbles Popperz&lt;/strong&gt;, a puzzle game where you tap marbles of the same colour to match them and score points before time runs out. It has a lives system, levels that get harder, three power-ups (Lightning Flash, Heart and Bomb) and global leaderboards.&lt;/p&gt;
&lt;p&gt;On paper that's a weekend project. In practice it touches nearly every part of a real game: a physics world, hit-testing, a frame-driven clock that has to survive pausing, difficulty tuning, juicy feedback, and a SwiftUI interface laid over a SpriteKit scene.&lt;/p&gt;
&lt;p&gt;This article walks through how I'd build each piece with SpriteKit, Apple's 2D game framework. The code is mine, written for this article, and the design choices are labelled as the approach I'd use. Game Center leaderboards get their own article, my Speck Match piece, so here I'll stay on gameplay.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-why-spritekit-and-how-it-fits-with-swiftui&quot;&gt;1. Why SpriteKit, and how it fits with SwiftUI&lt;/h3&gt;
&lt;p&gt;For a 2D casual game on Apple platforms, SpriteKit is the shortest path. It gives you a scene graph (&lt;code&gt;SKScene&lt;/code&gt;, &lt;code&gt;SKSpriteNode&lt;/code&gt;), actions for animation (&lt;code&gt;SKAction&lt;/code&gt;), a built-in physics engine (&lt;code&gt;SKPhysicsBody&lt;/code&gt;) and particles (&lt;code&gt;SKEmitterNode&lt;/code&gt;), all rendered on the GPU, with no third-party engine to update.&lt;/p&gt;
&lt;p&gt;Since iOS 14, &lt;code&gt;SpriteView&lt;/code&gt; lets a SwiftUI view host a scene directly. So menus, the HUD and settings can be ordinary SwiftUI, and only the playfield is SpriteKit:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import SwiftUI
import SpriteKit

struct GameScreen: View {
    @StateObject private var state = GameState()
    @State private var scene: MarbleScene = {
        let s = MarbleScene(size: CGSize(width: 390, height: 700))
        s.scaleMode = .resizeFill
        return s
    }()

    var body: some View {
        ZStack(alignment: .top) {
            SpriteView(scene: scene, preferredFramesPerSecond: 60)
                .ignoresSafeArea()
                .onAppear { scene.state = state }

            HUD(score: state.score, lives: state.lives,
                timeLeft: state.timeLeft, level: state.level)
                .padding()
        }
    }
}

@MainActor
final class GameState: ObservableObject {
    @Published var score = 0
    @Published var lives = 3
    @Published var level = 1
    @Published var timeLeft: TimeInterval = 60
    @Published var isGameOver = false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rule I follow: &lt;strong&gt;the scene owns the simulation; SwiftUI owns the chrome.&lt;/strong&gt; The scene writes to a shared &lt;code&gt;GameState&lt;/code&gt;, and SwiftUI just renders it. Keep SwiftUI state changes to a few per second, not 60, or you'll pay for view updates every frame.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-a-playfield-of-marbles-that-settle-under-physics&quot;&gt;2. A playfield of marbles that settle under physics&lt;/h3&gt;
&lt;p&gt;Marbles should &lt;em&gt;behave&lt;/em&gt; like marbles. When some pop, the ones above should roll down and fill the gap. Giving each marble a circular physics body, and the scene an edge loop, gets you that for free:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import SpriteKit

enum MarbleColor: CaseIterable { case red, blue, green, yellow, purple }

final class Marble: SKSpriteNode {
    let color: MarbleColor
    static let radius: CGFloat = 22

    init(color: MarbleColor) {
        self.color = color
        let texture = SKTexture(imageNamed: &quot;marble-\(color)&quot;)    // from a texture atlas
        super.init(texture: texture, color: .clear,
                   size: CGSize(width: Self.radius * 2, height: Self.radius * 2))
        name = &quot;marble&quot;
        let body = SKPhysicsBody(circleOfRadius: Self.radius)
        body.restitution = 0.2          // a little bounce
        body.friction = 0.3
        body.allowsRotation = true
        physicsBody = body
    }
    required init?(coder: NSCoder) { fatalError() }
}

final class MarbleScene: SKScene {
    weak var state: GameState?
    var config = LevelConfig.forLevel(1)
    var remaining: TimeInterval = 60
    private var lastUpdate: TimeInterval = 0

    override func didMove(to view: SKView) {
        backgroundColor = .black
        physicsBody = SKPhysicsBody(edgeLoopFrom: frame)         // the walls of the jar
        physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
        view.ignoresSiblingOrder = true                          // lets SpriteKit batch draws
        fill(count: config.marbleCount)
    }

    func fill(count: Int) {
        let palette = Array(MarbleColor.allCases.prefix(config.colorCount))
        for i in 0..&amp;lt;count {
            let marble = Marble(color: palette.randomElement()!)
            marble.position = CGPoint(x: .random(in: 40...(size.width - 40)),
                                      y: size.height + CGFloat(i) * 8)   // drop in from above
            addChild(marble)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two cheap wins: put every marble texture in one &lt;strong&gt;texture atlas&lt;/strong&gt;, so all marbles are drawn in a few batched calls; and set &lt;code&gt;ignoresSiblingOrder&lt;/code&gt; so SpriteKit is free to reorder draws for batching.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-tap-to-match-finding-the-touching-cluster&quot;&gt;3. Tap-to-match: finding the touching cluster&lt;/h3&gt;
&lt;p&gt;The listing describes the core loop simply: &quot;Tap marbles of the same color to match them… The more marbles you match, the higher your score.&quot; The approach I'd use is &lt;strong&gt;cluster matching&lt;/strong&gt;. Tapping a marble pops it together with every same-colour marble it is touching, directly or through a chain. A lone marble doesn't pop. Bigger clusters score more, which rewards looking before you tap.&lt;/p&gt;
&lt;p&gt;Hit-testing uses &lt;code&gt;nodes(at:)&lt;/code&gt;. The cluster is a breadth-first search over &quot;touching&quot; neighbours:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;extension MarbleScene {
    override func touchesBegan(_ touches: Set&amp;lt;UITouch&amp;gt;, with event: UIEvent?) {
        guard let point = touches.first?.location(in: self),
              let tapped = nodes(at: point).compactMap({ $0 as? Marble }).first else { return }

        let cluster = cluster(from: tapped)
        guard cluster.count &amp;gt;= 2 else {
            tapped.run(.sequence([.moveBy(x: 6, y: 0, duration: 0.04),
                                  .moveBy(x: -12, y: 0, duration: 0.08),
                                  .moveBy(x: 6, y: 0, duration: 0.04)]))   // &quot;no match&quot; wiggle
            return
        }
        pop(cluster)
        state?.score += score(forClusterOf: cluster.count)
    }

    func cluster(from start: Marble) -&amp;gt; [Marble] {
        let all = children.compactMap { $0 as? Marble }.filter { $0.color == start.color }
        let touchDistance = Marble.radius * 2 + 4                  // small tolerance for resting contact
        var visited: Set&amp;lt;ObjectIdentifier&amp;gt; = [ObjectIdentifier(start)]
        var queue = [start], result: [Marble] = []

        while let current = queue.popLast() {
            result.append(current)
            for other in all where !visited.contains(ObjectIdentifier(other)) {
                if hypot(other.position.x - current.position.x,
                         other.position.y - current.position.y) &amp;lt;= touchDistance {
                    visited.insert(ObjectIdentifier(other))
                    queue.append(other)
                }
            }
        }
        return result
    }

    /// Quadratic reward: 2 marbles = 20, 5 marbles = 125, 10 marbles = 500.
    func score(forClusterOf n: Int) -&amp;gt; Int { n * n * 5 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With a few dozen marbles on screen, this brute-force neighbour check is trivially fast. If you ever have hundreds, bucket marbles into a grid by position first. Distance-based neighbours work better than physics contact callbacks here, because resting marbles don't keep reporting contacts.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-popping-with-juice&quot;&gt;4. Popping with juice&lt;/h3&gt;
&lt;p&gt;A match should feel &lt;em&gt;good&lt;/em&gt;: a quick scale-up, a fade, a particle burst and a sound. The physics then lets the marbles above fall into the gap.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;extension MarbleScene {
    func pop(_ marbles: [Marble]) {
        for (i, marble) in marbles.enumerated() {
            marble.physicsBody = nil                              // stop it interacting while it pops
            let delay = SKAction.wait(forDuration: Double(i) * 0.03)   // a ripple through the cluster
            let burst = SKAction.run { [weak self] in self?.spawnBurst(at: marble.position) }
            let vanish = SKAction.group([.scale(to: 1.4, duration: 0.12), .fadeOut(withDuration: 0.12)])
            marble.run(.sequence([delay, burst, vanish, .removeFromParent()]))
        }
        run(.playSoundFileNamed(&quot;pop.caf&quot;, waitForCompletion: false))
    }

    func spawnBurst(at point: CGPoint) {
        guard let emitter = SKEmitterNode(fileNamed: &quot;Pop.sks&quot;) else { return }   // designed in Xcode
        emitter.position = point
        emitter.numParticlesToEmit = 24
        addChild(emitter)
        emitter.run(.sequence([.wait(forDuration: 0.6), .removeFromParent()]))
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The 30 ms stagger is the cheapest &quot;juice&quot; there is. A cluster of eight pops as a ripple instead of all at once, and players read that as a chain reaction. Always remove finished emitters, or they pile up invisibly and eat frame time.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-a-countdown-that-survives-pausing&quot;&gt;5. A countdown that survives pausing&lt;/h3&gt;
&lt;p&gt;&quot;Watch the timer – you need to match quickly!&quot; The timer must stop when the game is paused, when a phone call comes in and when the app goes to the background. A &lt;code&gt;Timer&lt;/code&gt; or &lt;code&gt;Date&lt;/code&gt; arithmetic gets this wrong. The robust pattern is to &lt;strong&gt;accumulate frame deltas in &lt;code&gt;update(_:)&lt;/code&gt;&lt;/strong&gt;, and only while the game is actually running:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;extension MarbleScene {
    override func update(_ currentTime: TimeInterval) {
        defer { lastUpdate = currentTime }
        guard lastUpdate &amp;gt; 0, !isPaused, let state, !state.isGameOver else { return }

        // Clamp so a long hitch (or resuming from pause) can't eat seconds at once.
        let dt = min(currentTime - lastUpdate, 1.0 / 20.0)
        remaining -= dt

        // Push to SwiftUI only when the displayed whole second changes.
        let shown = max(0, remaining.rounded(.up))
        if shown != state.timeLeft { state.timeLeft = shown }

        if remaining &amp;lt;= 0 { timeUp() }
        if children.compactMap({ $0 as? Marble }).isEmpty { levelCleared() }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;remaining&lt;/code&gt; and &lt;code&gt;lastUpdate&lt;/code&gt; are the stored properties declared on the scene in section 2.) Three details carry the weight:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Clamp &lt;code&gt;dt&lt;/code&gt;.&lt;/strong&gt; After a pause, &lt;code&gt;currentTime&lt;/code&gt; jumps forward. Without the clamp, the first frame back would subtract all of the paused time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Guard on &lt;code&gt;isPaused&lt;/code&gt;.&lt;/strong&gt; Pausing the &lt;code&gt;SKView&lt;/code&gt; stops the frame loop entirely. Pausing just the scene stops its actions and physics, and the explicit guard makes sure the clock stops too, whichever you use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throttle UI updates.&lt;/strong&gt; Writing to &lt;code&gt;@Published&lt;/code&gt; state 60 times a second makes SwiftUI re-render the HUD 60 times a second. Write only when the displayed value changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pausing itself is one line, &lt;code&gt;scene.isPaused = true&lt;/code&gt;. Wire it to the scene phase going to &lt;code&gt;.inactive&lt;/code&gt; or &lt;code&gt;.background&lt;/code&gt; in SwiftUI.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-lives-and-levels&quot;&gt;6. Lives and levels&lt;/h3&gt;
&lt;p&gt;The listing has a &quot;Lives system – be careful not to run out!&quot; and &quot;Level progression – difficulty increases as you advance&quot;. The approach I'd use: running out of time on a level costs a life and restarts that level, and clearing the jar advances you. Difficulty lives in one small table, so tuning never means touching game logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;struct LevelConfig {
    let marbleCount: Int
    let colorCount: Int
    let seconds: TimeInterval
    let powerUpChance: Double

    static func forLevel(_ level: Int) -&amp;gt; LevelConfig {
        LevelConfig(
            marbleCount: min(30 + level * 4, 80),       // more marbles
            colorCount: min(3 + level / 3, 5),          // more colours = smaller clusters
            seconds: max(60 - Double(level) * 2, 30),   // less time
            powerUpChance: max(0.08 - Double(level) * 0.004, 0.03)
        )
    }
}

extension MarbleScene {
    func timeUp() {
        guard let state else { return }
        state.lives -= 1
        if state.lives &amp;lt;= 0 { state.isGameOver = true; isPaused = true }
        else { restartLevel() }
    }

    func restartLevel() {
        enumerateChildNodes(withName: &quot;marble&quot;) { node, _ in node.removeFromParent() }
        remaining = config.seconds
        fill(count: config.marbleCount)
    }

    func levelCleared() {
        state?.level += 1
        config = .forLevel(state?.level ?? 1)
        remaining = config.seconds
        fill(count: config.marbleCount)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding a colour is the most powerful difficulty knob in a colour-match game. Each extra colour shrinks the average cluster, which lowers scores and raises pressure without touching the clock.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-power-ups-lightning-heart-and-bomb&quot;&gt;7. Power-ups: Lightning, Heart and Bomb&lt;/h3&gt;
&lt;p&gt;The listing names three power-ups: &quot;Lightning Flash – Clear multiple marbles at once&quot;, &quot;Heart – Gain an extra life&quot; and &quot;Bomb – Explode surrounding marbles&quot;. I'd model them as special marbles that spawn occasionally and trigger when tapped:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;enum PowerUp { case lightning, heart, bomb }

extension MarbleScene {
    func trigger(_ powerUp: PowerUp, at point: CGPoint) {
        let marbles = children.compactMap { $0 as? Marble }
        switch powerUp {
        case .lightning:
            // Clear every marble of the most common colour.
            let counts = Dictionary(grouping: marbles, by: \.color)
            if let biggest = counts.max(by: { $0.value.count &amp;lt; $1.value.count })?.value {
                pop(biggest); state?.score += score(forClusterOf: biggest.count)
            }
        case .heart:
            state?.lives += 1
        case .bomb:
            let blastRadius: CGFloat = 110
            let hit = marbles.filter { hypot($0.position.x - point.x, $0.position.y - point.y) &amp;lt; blastRadius }
            pop(hit); state?.score += hit.count * 10
            // Push the survivors outwards for a satisfying shove.
            for m in marbles where !hit.contains(m) {
                let d = CGVector(dx: m.position.x - point.x, dy: m.position.y - point.y)
                let len = max(hypot(d.dx, d.dy), 1)
                m.physicsBody?.applyImpulse(CGVector(dx: d.dx / len * 30, dy: d.dy / len * 30))
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which marbles &quot;Lightning&quot; clears is my design choice; the listing only says it clears multiple at once. The bomb's impulse on survivors is where the physics world pays off a second time. The jar visibly reacts, and the new arrangement creates fresh clusters.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-real-world-example-marbles-popperz&quot;&gt;8. Real-world example: Marbles Popperz&lt;/h3&gt;
&lt;p&gt;This walkthrough is based on building &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/us/app/marbles-popperz/id1601511276&quot;&gt;Marbles Popperz&lt;/a&gt;&lt;/strong&gt;. The code here is the approach I'd use, not the shipped source.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bestlatestapps.com/assets/blog/building-a-tap-to-match-marble-game-with-spritekit-and-swiftui/marbles-popperz-screenshot-1.jpg&quot; alt=&quot;Marbles Popperz App Store screenshot 1&quot;&gt;&lt;/p&gt;
&lt;p&gt;At the time of writing, its App Store listing shows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free&lt;/strong&gt;, in &lt;strong&gt;Games&lt;/strong&gt; (Puzzle, Board)&lt;/li&gt;
&lt;li&gt;First released in &lt;strong&gt;December 2021&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3 ratings&lt;/strong&gt; in the US store, averaging about &lt;strong&gt;3.7 stars&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Supports &lt;strong&gt;iOS 16.0 and later&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How the listing maps to this article:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;What the listing says &lt;/th&gt;&lt;th&gt;Where it's covered&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&quot;Simple tap-to-match gameplay&quot; / &quot;Match marbles by color to remove them&quot; &lt;/td&gt;&lt;td&gt;Hit-testing and cluster matching (section 3)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Beautiful visual effects and animations&quot; &lt;/td&gt;&lt;td&gt;Staggered pops and particle bursts (section 4)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Watch the timer - you need to match quickly!&quot; &lt;/td&gt;&lt;td&gt;Pause-safe countdown in &lt;code&gt;update(_:)&lt;/code&gt; (section 5)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Lives system&quot; / &quot;Level progression - difficulty increases as you advance&quot; &lt;/td&gt;&lt;td&gt;Lives and a level table (section 6)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Lightning Flash&quot;, &quot;Heart&quot;, &quot;Bomb&quot; &lt;/td&gt;&lt;td&gt;Power-ups (section 7)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Global leaderboards - compete with players worldwide&quot; &lt;/td&gt;&lt;td&gt;Game Center, covered in my Speck Match article&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://bestlatestapps.com/assets/blog/building-a-tap-to-match-marble-game-with-spritekit-and-swiftui/marbles-popperz-screenshot-2.jpg&quot; alt=&quot;Marbles Popperz App Store screenshot 2&quot;&gt;&lt;/p&gt;
&lt;p&gt;The rating average is a useful honest signal for a small game: three ratings is too few to mean much. The way to learn what players struggle with is to watch someone play your first three levels without saying a word.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;9-before-you-ship-a-casual-game-a-checklist&quot;&gt;9. Before you ship a casual game: a checklist&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The timer pauses&lt;/strong&gt; on pause, on interruptions and in the background, and &lt;code&gt;dt&lt;/code&gt; is clamped on resume.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The UI isn't updated every frame&lt;/strong&gt;, only when displayed values change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Textures are in an atlas&lt;/strong&gt; and &lt;code&gt;ignoresSiblingOrder&lt;/code&gt; is on. Check the draw count with &lt;code&gt;showsDrawCount&lt;/code&gt; during development.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Finished emitters and actions are removed.&lt;/strong&gt; No invisible nodes pile up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The difficulty table is data&lt;/strong&gt;, and each level has been played by someone other than you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Taps feel fair.&lt;/strong&gt; The hit area isn't smaller than the visible marble, and a mis-tap gets clear feedback.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Colour isn't the only cue.&lt;/strong&gt; Add patterns or symbols to marbles for colour-blind players.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sound respects the silent switch&lt;/strong&gt;, and there's a mute option.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test on the oldest supported device&lt;/strong&gt; at 60 fps with the maximum marble count.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;A tap-to-match game is a perfect classroom. Physics gives you settling for free. A simple search gives you clusters. A clamped delta gives you an honest clock. A table gives you difficulty. And a few milliseconds of stagger turn &quot;it works&quot; into &quot;it feels good&quot;. SpriteKit and SwiftUI together cover all of it with nothing to install.&lt;/p&gt;
&lt;p&gt;Play the game, or explore my other work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Marbles Popperz (iOS):&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/us/app/marbles-popperz/id1601511276&quot;&gt;https://apps.apple.com/us/app/marbles-popperz/id1601511276&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All of my free iOS apps:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/us/developer/alok-singh/id1508795536&quot;&gt;https://apps.apple.com/us/developer/alok-singh/id1508795536&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;My portfolio:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://bestlatestapps.com/alok/&quot;&gt;https://bestlatestapps.com/alok/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What's your favourite bit of game &quot;juice&quot;? Share it in the comments.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;— Alok Kumar Singh&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bestlatestapps.com/blog/building-a-tap-to-match-marble-game-with-spritekit-and-swiftui/&quot;&gt;https://bestlatestapps.com/blog/building-a-tap-to-match-marble-game-with-spritekit-and-swiftui/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32677/tap-match-marble-game-spritekit-and-swiftui-physics-cluster-matching-lives-and-power-ups</guid>
<pubDate>Tue, 27 Oct 2026 12:39:50 +0000</pubDate>
</item>
</channel>
</rss>