Mobile development
SpriteKit, SceneKit and Unity: what three engines taught me about mobile games
6 min read
SpriteKit, SceneKit and Unity: what three engines taught me about mobile games
During my master's in mobile development I spent a semester building small games with three different tools: SpriteKit for 2D on iOS, SceneKit for 3D on iOS, and Unity for cross-platform work. Some of those exercises are still in the archive section of this site — an air hockey clone, a maze, a tower defense. None of them is a commercial game, but building the same kind of thing three times taught me more about how games work than any single engine would have.
This is what I took away, organised by the problems every game has to solve.
The same skeleton under three APIs
Underneath the different class names, all three engines share one model.
- A scene is a tree of nodes. Every visible object is a node with a position, a scale, a rotation and children. Draw order follows the z-position, much like a z-index on the web.
- The game loop has two phases. Update reads input, moves characters and resolves interactions; render draws the result. Engines aim for 60 frames per second; for heavy games it is often better to target a stable 30 than an unstable 60.
- Time is explicit. Movement must be multiplied by the time elapsed since the previous frame, otherwise the game runs faster on faster devices. In SceneKit you compute that delta yourself in the renderer delegate:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
let deltaTime = time - (previousUpdateTime ?? time)
previousUpdateTime = time
// move nodes using deltaTime
}
Once I saw this skeleton, switching engines became mostly a matter of vocabulary.
Scenes and transitions
SpriteKit makes scene changes almost trivial: you load the next scene and hand the view a transition.
let transition = SKTransition.flipVertical(withDuration: 1)
if let scene = SKScene(fileNamed: "GameScene"), let view = self.view {
view.presentScene(scene, transition: transition)
}
In Unity the equivalent is SceneManager.LoadScene("GameScene"), with one trap I hit early: the scene has to be registered in Build Settings or loading fails at runtime. Unity also destroys every object when it changes scene, so to carry data across — credits, score, settings — I used a small singleton marked with DontDestroyOnLoad.
Physics: pick the simplest shape that works
The physics vocabulary is the same everywhere: a body (mass, friction, velocity), a shape for collisions, joints between bodies and a world with gravity. Bodies come in three flavours: dynamic ones obey physics, static ones never move, and kinematic ones are moved by code but still push others.
The most useful rule I learned is that the collision shape does not have to match the visual mesh. In SceneKit, a sphere is cheaper than a box, a box is cheaper than a capsule, and custom concave meshes should be avoided altogether. A ball that looks detailed can still collide as a perfect sphere.
Unity adds two settings worth knowing before you debug anything else: Interpolate, which removes jitter when a camera follows a physics object, and Collision Detection, which you raise to continuous for fast objects so they stop tunnelling through walls.
Collisions and raycasts
Both engines report contacts through callbacks. In SceneKit you adopt a contact delegate; since you do not know which body is A and which is B, you check both:
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
if contact.nodeA.name == "projectile" || contact.nodeB.name == "projectile" {
let damage = contact.collisionImpulse
// apply damage
}
}
Unity uses OnCollisionEnter, OnCollisionStay and OnCollisionExit, plus trigger variants for invisible zones — "the ball left the board" is just an OnTriggerExit. To pick objects with a finger, you cast a ray from the camera through the touch point:
Ray ray = Camera.main.ScreenPointToRay(touchPosition);
if (Physics.Raycast(ray, out RaycastHit hit, 100f)) {
hit.rigidbody?.AddForce(-hit.normal * 10f, ForceMode.Impulse);
}
Categories for "what collides with what" are bit masks in both Apple frameworks: each bit is a category, and a body declares which categories it hits.
UI is where the engines differ most
SpriteKit has no UI system for scene content. A button is something you build yourself: a sprite that tracks whether a touch began inside it and only fires if the touch also ended inside it. Writing that small class taught me how much a UI framework normally does for you.
Unity has a real UI layer on a Canvas, and the two concepts that finally made layouts behave were anchors — where an element is attached relative to its parent — and the pivot — which point of the element sits on that position. A canvas can also be placed in world space so that UI lives inside the 3D scene.
Input: touches and the accelerometer
Touch input is a list of active touches with positions and phases. The accelerometer is a three-component vector — Input.acceleration in Unity — which is enough for tilt controls like a rolling ball in a maze. Unity's virtual buttons ("Fire1" and friends) are useful for prototyping on desktop before moving to touch.
Screen sizes: decide what is allowed to be cropped
Phones and tablets have very different aspect ratios, so every game needs an explicit strategy:
- Aspect fill for games that scroll in two dimensions, making sure important content stays inside the always-visible area.
- Fixed width for vertical scrollers, fixed height for horizontal ones.
In SpriteKit I used a small extension that keeps the scene height and derives the width from the device's ratio. In Unity the same idea is applied through the camera's field of view. On top of that, HUD elements are anchored to the screen edges, and menus are centred, stacked vertically and sized for a fingertip.
Assets: sprite sheets and tilemaps
Two optimisations show up in every 2D game. Sprite sheets pack all animation frames into one texture (ideally with power-of-two dimensions), generated with tools like TexturePacker. Tilemaps build large backgrounds from small reusable tiles instead of one huge image; I designed them in Tiled, with pattern layers for the visuals and object layers to mark spawn points and invisible zones. Parallax — distant layers scrolling slower than near ones — then gives depth almost for free.
Achievements and leaderboards
Game Center on iOS and Google Play Games on Android cover achievements and leaderboards. The design advice that stuck with me: distinguish progress achievements, which push players to finish, from extra ones, which add replay value, and do not spend your whole achievement budget in the first release — watch how people actually play and add achievements later.
A short detour into VR
The last block of the course was VR in Unity. Google Cardboard only needs its SDK and a stereo camera. Gear VR required signing the build for the specific test device, identified through adb devices, before the headset would accept it — a reminder that half of mobile development is tooling and provisioning, not code.
What I would tell myself before starting
- Learn the shared model first — node tree, game loop, delta time, bodies and shapes — and every engine becomes easier.
- Choose collision shapes for performance, not looks.
- Decide your screen-adaptation strategy before drawing a single asset.
- Pick the engine by scope: SpriteKit for a focused 2D iOS game, SceneKit for lightweight 3D on Apple platforms, Unity when you need multiple platforms, a richer editor or VR.