Skip to content
Back to the quest

2D Environmental Adventure Game

Finding Tuki

An ocean adventure where a mother turtle swims through plastic-polluted waters to find her baby.

Role
Game Developer / iOS Developer
Duration
≈ 3 weeks
Year
2026
Category
iOS Development, Game Development

Project Overview

Finding Tuki is a 2D ocean adventure game built with SpriteKit. Mom Turtle swims up-screen collecting jellyfish for energy while dodging trash, bombs, and six-pack rings to find her baby, Tuki. The game combines environmental storytelling, a guided tutorial, energy management, difficulty ramping, and haptic feedback — all in a pixel-art style with EN/ID localization.

The Problem

Ocean plastic is an abstract problem for most players. The team wanted a game where the pollution isn't a backdrop — it is the obstacle system itself, felt through gameplay rather than explained through text.

The Solution

An endless-runner loop where every hazard is a real category of marine waste. A short net-capture cutscene sets the emotional stakes, a guided tutorial teaches each mechanic one hazard at a time, and an energy system turns survival into constant risk-reward decisions.

Mom Turtle player sprite from Finding Tuki
Tuki, the baby turtle sprite
Golden jellyfish collectible sprite
Bomb obstacle sprite
Six-pack plastic ring obstacle sprite
Pixel-art energy bar frame from the game HUD

What I Owned

  • Implemented core gameplay logic and the GamePhase state machine (pre-game → cutscene → tutorial → playing → paused → game over) inside a single persistent SpriteKit scene
  • Built the energy system and wired it to the HUD through a change-callback API
  • Wrote the collision routing layer (ContactHandler) that maps physics contacts to typed gameplay closures
  • Solved responsive layout for iPhone and iPad by computing the true visible frame under .aspectFill
  • Implemented the pause flow and game-state transitions
  • Integrated game assets and rebuilt physics hitboxes to match display size
  • Participated in debugging and playtesting across device sizes

What the Team Owned

  • Hanum — cutscene direction, localization (EN/ID), background music, settings screen, and haptic feedback engine
  • Khalis — Game Center integration and the difficulty speed-ramp system
  • Team — art direction, pixel-art assets, level balancing, and playtesting

Challenge I Solved

The scene is authored at 750×1334 and presented with .aspectFill, which silently crops about 70 points from each horizontal edge on iPhone. Any HUD element anchored to the scene's own frame edges rendered off-screen.

I fixed it by computing the genuinely visible rectangle from the view bounds and the aspect-fill scale factor, then anchoring all HUD elements to that rect with proportional margins. The same code now lays out correctly on iPhone SE, Pro Max, and iPad without per-device branches.

A Trap Worth Documenting

Calling Trash() with no arguments compiled fine but resolved to the plain SKSpriteNode initializer — producing an invisible node with no texture and no physics. The bug surfaced as “trash sometimes doesn't spawn.” The fix (and the convention we documented) is to always construct obstacles through their designated initializers.

Similarly, physics bodies are built from texture size, not display size. Tutorial trash uses a 479px texture shown at 95pt, so collisions fired far too early until the body was rebuilt at the visual size.

Code Highlights

Gameplay/Systems/EnergySystem.swift
final class EnergySystem {

    let maximumEnergy: Int
    let startingEnergy: Int

    private(set) var currentEnergy: Int

    var onEnergyChanged: ((Int) -> Void)?

    var isEmpty: Bool { currentEnergy == 0 }

    func addEnergy(_ amount: Int) {
        guard amount > 0 else { return }
        updateEnergy(to: currentEnergy + amount)
    }

    func reduceEnergy(_ amount: Int) {
        guard amount > 0 else { return }
        updateEnergy(to: currentEnergy - amount)
    }

    private func updateEnergy(to newValue: Int) {
        let safeValue = min(max(0, newValue), maximumEnergy)
        guard safeValue != currentEnergy else { return }

        currentEnergy = safeValue
        onEnergyChanged?(currentEnergy)
    }
}

Energy System: The turtle's stamina. Jellyfish restore energy, trash and bombs drain it; when it hits zero, the run ends. The rest of the game only needs to observe one value. A small, single-purpose class with a clamped setter and a change callback. GameScene subscribes with onEnergyChanged and forwards the value to the HUD, so gameplay logic never touches UI nodes directly.

Scenes/GameScene.swift
enum GamePhase {
    case preGame    // title screen, two turtles swimming
    case cutscene   // Tuki gets caught by the net
    case tutorial   // guided practice before real gameplay
    case playing
    case paused
    case gameOver
}

final class GameScene: SKScene {
    private(set) var phase: GamePhase = .preGame
    // Each update() and touch handler branches on `phase`,
    // so tutorial objects, spawners, and HUD can never
    // run in the wrong state.
}

Game Phase Management: Pre-game screen, cutscene, guided tutorial, gameplay, pause, and game over all live inside one SpriteKit scene. A phase enum keeps them from stepping on each other. Instead of presenting six separate SKScenes (and losing shared state on every transition), one GamePhase enum drives what update(), touch handling, and the spawner are allowed to do in each phase.

UI/HUD.swift
private func visibleFrame(in scene: SKScene) -> CGRect {
    guard let view = scene.view,
          scene.size.width > 0,
          scene.size.height > 0 else {
        return scene.frame
    }

    let scale = max(
        view.bounds.width / scene.size.width,
        view.bounds.height / scene.size.height
    )

    let visibleWidth = view.bounds.width / scale
    let visibleHeight = view.bounds.height / scale

    return CGRect(
        x: scene.frame.midX - visibleWidth / 2,
        y: scene.frame.midY - visibleHeight / 2,
        width: visibleWidth,
        height: visibleHeight
    )
}

Responsive Layout (visibleFrame): The scene is 750×1334 presented with .aspectFill, so roughly 70pt on each side is cropped on iPhone. Anything anchored to frame.minX/maxX simply disappears off-screen. Compute the truly visible rectangle from the view bounds and the aspect-fill scale factor, then anchor every HUD element to that rect with proportional margins instead of the raw scene frame.

Physics/ContactHandler.swift
final class ContactHandler: NSObject, SKPhysicsContactDelegate {

    var onTurtleHitBomb: ((Bomb) -> Void)?
    var onTurtleHitTrash: ((SKSpriteNode) -> Void)?
    var onTurtleCollectJellyfish: ((Jellyfish) -> Void)?
    var onTurtleHitSixthRing: ((SixthRing) -> Void)?

    func didBegin(_ contact: SKPhysicsContact) {
        if containsPair(contact, PhysicsCategory.turtle,
                        PhysicsCategory.bomb) {
            handleBombContact(contact)
            return
        }
        if containsPair(contact, PhysicsCategory.turtle,
                        PhysicsCategory.trash) {
            handleTrashContact(contact)
            return
        }
        // ...jellyfish and six-pack ring follow
    }
}

Collision Routing: Every physics contact — turtle vs. bomb, trash, jellyfish, or six-pack ring — needs to trigger different gameplay reactions without coupling physics code to game rules. A dedicated SKPhysicsContactDelegate that only classifies contacts and fires typed closures. GameScene wires the closures to score, energy, and feedback systems at setup time.

Key Learning

State machines beat scene-swapping. Keeping every phase inside one persistent scene made transitions seamless, but it demanded discipline: every touch handler and update loop has to ask “which phase am I in?” before acting.

What I Would Improve

  • Extract more reusable game systems instead of GameScene extensions
  • Clearer separation between gameplay and UI logic
  • Expanded accessibility options (motion, contrast, one-handed play)
  • More structured level balancing data instead of tuned constants
  • Additional automated testing across device sizes

Technology Stack

SwiftSpriteKitGameplayKitCore HapticsSwiftDataGame CenterXcodeGit & GitHub