Skip to the content.

OctopusKit OctopusKit Logo

A 2D game engine based on ECS and written in 100% Swift for iOS, macOS, tvOS and visionOS.

[!CAUTION] šŸ˜” This project is no longer updated by the sole maintainer.
šŸŒŸ I have moved to Godot. Check out Comedot ā† my components-based framework for 2D games in Godot!

[!IMPORTANT] ā• OctopusKit requires OctopusCore. Non-game functionality was split into a separate repository for use in general apps. For the last standalone version, see 4.0.0-beta-5

If youā€™ve tried making a game in Swift while sticking to the official APIs, this may be for you! OctopusKit wraps and extends Appleā€™s frameworks:

ā€¢ GameplayKit for a flexible Entity-Component-System architecture to dynamically compose game behavior.
ā€¢ SpriteKit for 2D graphics, physics and GPU shaders.
ā€¢ SwiftUI for quickly designing fluid, scalable HUDs with a declarative syntax.
ā€¢ Metal to ensure the best native performance under the hood.
ā€¢ OS-independent components let you handle mouse/touch or keyboard/gamepad input with the same code, and compile natively for iOS + macOS without needing Catalyst.

QuickStart Demo

  1. Examples
  2. Overview
  3. Design Goals
  4. Getting Started
  5. Etcetera

OctopusKit is a constant work in progress and Iā€™m still learning as I go, so it may change rapidly without maintaining backwards compatibility or updating the documentation.

This project is the result of my attempts to make games in pure Swift. I fell in love with the language but couldnā€™t find any engines that supported it or had the kind of architecture that I found intuitive, so I started making my own.

Feedback welcome! ā€“ ShinryakuTako

Examples

šŸš€ Eager to dive in? Add OctopusKit as a Swift Package Manager dependency to a SwiftUI project, and use the QuickStart template (which also serves as a little demo.)

šŸŽØ Using with SwiftUI

import SwiftUI
import OctopusKit

struct ContentView: View {

    // The coordinator object manages your game's scenes and global state.
    @StateObject var gameCoordinator = OKGameCoordinator(states: [
        MainMenu(),
        Lobby(),
        Gameplay() ])
    
    var body: some View {

        // The container view combines SpriteKit with SwiftUI,
        // and presents the coordinator's current scene.
        OKContainerView()
            .environmentObject(gameCoordinator)
            .statusBar(hidden: true)
    }
}

šŸ‘¾ Creating an animated sprite

var character = OKEntity(components: [
    
    // Start with a blank texture.
    NodeComponent(node: SKSpriteNode(color: .clear, size: CGSize(widthAndHeight: 42))),
    
    // Load texture resources.
    TextureDictionaryComponent(atlasName: "PlayerCharacter"),
    
    // Animate the sprite with textures whose names begin with the specified prefix.
    TextureAnimationComponent(initialAnimationTexturePrefix: "Idle") ])

šŸ•¹ Adding player control

// Add a component to the scene that will be updated with input events.
// Other components that handle player input will query this component.

// This lets us handle asynchronous events in sync with the frame-update cycle.
// A shared event stream is more efficient than forwarding events to every entity.

// PointerEventComponent is an OS-agnostic component for touch or mouse input.

let sharedPointerEventComponent = PointerEventComponent()
scene.entity?.addComponent(sharedPointerEventComponent)

character.addComponents([
    
    // A relay component adds a reference to a component from another entity,
    // and also fulfills the dependencies of other components in this entity.
    RelayComponent(for: sharedPointerEventComponent),
    
    // This component checks the entity's PointerEventComponent (provided here by a relay)
    // and syncs the entity's position to the touch or mouse location in every frame.
    PointerControlledPositioningComponent() ])

šŸ•¹ Dynamically removing player control or changing to a different input method

character.removeComponent(ofType: PointerControlledPositioningComponent.self)
    
character.addComponents([

    // Add a physics body to the sprite.
    PhysicsComponent(),
    
    RelayComponent(for: sharedKeyboardEventComponent),
    
    // Apply a force to the body based on keyboard input in each frame.
    KeyboardControlledForceComponent() ])

šŸ§© A custom game-specific component

class AngryEnemyComponent: OKComponent, RequiresUpdatesPerFrame {
    
    override func didAddToEntity(withNode node: SKNode) {
        node.colorTint = .angryMonster
    }
    
    override func update(deltaTime seconds: TimeInterval) {
        guard let behaviorComponent = coComponent(EnemyBehaviorComponent.self) else { return }
        behaviorComponent.regenerateHP()
        behaviorComponent.chasePlayerWithExtraFervor()
    }
    
    override func willRemoveFromEntity(withNode node: SKNode) {
        node.colorTint = .mildlyInconveniencedMonster
    }
}

šŸ›  Using a custom closure to change the animation based on player movement

// Add a component that executes the supplied closure every frame.
character.addComponent(RepeatingClosureComponent { component in
    
    // Check if the entity of this component has the required dependencies at runtime.
    // This approach allows dynamic behavior modification instead of halting the game.
    
    if  let physicsBody = component.coComponent(PhysicsComponent.self)?.physicsBody,
        let animationComponent = component.coComponent(TextureAnimationComponent.self)
    {
        // Change the animation depending on whether the body is stationary or mobile.
        animationComponent.textureDictionaryPrefix = physicsBody.isResting ? "Idle" : "Moving"
    }
})

// This behavior could be better encapsulated in a custom component,
// with many different game-specific animations depending on many conditions.

šŸŽŽ Loading a scene built in the Xcode Scene Editor and creating multiple entities from sprites identified by a shared name

// Load a ".sks" file as a child node.

if  let editorScene = SKReferenceNode(fileNamed: "EditorScene.sks") {
    scene.addChild(editorScene)
}

// Search the entire tree for all nodes named "Turret",
// and give them properties of "tower defense" turrets,
// and make them independently draggable by the player.

for turretNode in scene["//Turret"] {

    // Create a new entity for each node found.
    scene.addEntity(OKEntity(components: [
    
        NodeComponent(node: turretNode),
        RelayComponent(for: sharedPointerEventComponent),
                        
        // Hypothetical game-specific components.
        HealthComponent(),
        AttackComponent(),
        MonsterTargetingComponent(),
 
        // Track the first touch or mouse drag that begins inside the sprite.
        NodePointerStateComponent(),
                
        // Let the player select and drag a specific sprite.
        // This differs from the PointerControlledPositioningComponent in a previous example, 
        // which repositions nodes regardless of where the pointer began.
        PointerControlledDraggingComponent() ]))
}

// Once the first monster wave starts, you could replace PointerControlledDraggingComponent 
// with PointerControlledShootingComponent to make the turrets immovable but manually-fired.

Overview

OctopusKit uses an ā€œEntity-Component-Systemā€ architecture, where:

See the Architecture documentation for a detailed breakdown of the object hierarchy.

Your primary workflow will be writing component classes for each ā€œpartā€ of the graphics and gameplay, then combining them to build entities which appear onscreen, or abstract entities that handle data on the ā€œbackend.ā€

e.g. say a ParallaxBackgroundEntity containing a CloudsComponent, a HillsComponent and a TreesComponent, or a GameSessionEntity containing a WorldMapComponent and a MultiplayerSyncComponent.

Performance: Although extensive benchmarks have not been done yet, OK can display over 5000 sprites on an iPhone XS at 60 frames per second; each sprite represented by an entity with multiple components being updated every frame, and responding to touch input.

Design Goals

Getting Started

  1. Read the QuickStart and Usage Guide. You will need Xcode 12, iOS 14 and macOS Big Sur (though OK may work on older versions with some manual modifications.)

    Skill Level: Intermediate: Although OK is not presented in a form designed for absolute beginners, mostly because Iā€™m too lazy to write documentation from step zero, itā€™s not ā€œadvancedā€ level stuff either; if youā€™ve read the Swift Language Book and have attempted to make a SpriteKit game in Xcode, you are ready to use OK!

    You should also read about the ā€œComposition over inheritanceā€ and ā€œEntityā€“componentā€“systemā€ patterns if youā€™re not already familiar with those concepts, although OKā€™s implementation of these may be different than what you expect.

    Also see Appleā€™s tutorials for SwiftUI.

  2. For a detailed overview of the engineā€™s architecture, see Architecture.

  3. Stuck? See Tips & Troubleshooting.

  4. Wondering whether something was intentionally done the way it is, or why? Coding Conventions & Design Decisions may have an explanation.

  5. Want to keep tabs on whatā€™s coming or help out with the development of missing features? See the TODO & Roadmap.

Etcetera


OctopusKit Ā© 2023 Invading Octopus ā€¢ Apache License 2.0