OctopusKit Usage Guide
Tutorials and examples for common tasks
- Adding OctopusKit to your project
- Xcode File Templates
- Player Input
- Logs
- Sharing Data
- Accessing Game State from SwiftUI Views
- Physics Collisions & Contact Detection
- Advanced Stuff
Related Documentation
Notes
-
This documentation assumes that the reader is using OctopusKit in a SwiftUI project, and has some prior experience with developing for Apple platforms in the Swift programming language.
-
Currently, API documentation (i.e. for types/methods/properties) is only provided via extensive source-code comments, which can be viewed in Xcode’s Quick Help.
This guide provides examples for common tasks and how to get started, but there is no standalone reference for the API, as I don’t have the time and energy to write that alongside developing the engine. (´・_・`)
The best way to learn may be to examine the engine source code.
Adding OctopusKit to your project
🍰 To begin from a template:
-
See one of the following files in the OctopusKit package/repository:
-
README QuickStart.md in the
QuickStart
folder. -
README Template.md in the
Templates/Game
folder.
-
🛠 To import OctopusKit into a new or existing project:
-
📦 Add OctopusKit as a Swift Package Manager Dependency.
Xcode File menu » Swift Packages » Add Package Dependency…
Enter the URL for the GitHub repository. Download the “develop” branch for the latest version.
-
Create an instance of
OKGameCoordinator
.let gameCoordinator = OKGameCoordinator( states: [OKGameState()], // A placeholder for now. initialStateClass: OKGameState.self)
The game coordinator is the top-level “controller” (in the Model-View-Controller sense) that manages the global state of your game.
If your game needs to share complex logic or data across multiple scenes, you may create a subclass of
OKGameCoordinator
.SwiftUI (iOS/iPadOS, macOS, tvOS): You may use
@StateObject var gameCoordinator
as an instance variable ofContentView
if you do not require it to be a global value. The@StateObject
property wrapper ensures that the coordinator is not recreated whenever the view hierarchy is updated. -
Presenting OctopusKit content in your view hierarchy requires different steps depending on whether you use SwiftUI or AppKit/UIKit:
-
SwiftUI (iOS/iPadOS, macOS, tvOS): Add a
OKContainerView
and pass the game coordinator as anenvironmentObject
to it:import OctopusKit struct ContentView: View { var body: some View { OKContainerView() .environmentObject(gameCoordinator) } }
The
OKContainerView
combines a SpriteKitSKView
with a SwiftUI overlay.❗️ If you created a subclass of
OKGameCoordinator
, then you must provide a generic type parameter:OKContainerView<MyGameCoordinator>()
💡 It’s best to pass the game coordinator
environmentObject
to the top level content view created in theSceneDelegate.swift
file, which will make it available to your entire view hierarchy. -
AppKit (macOS) or UIKit (iOS/iPadOS, tvOS): Your storyboard should have an
SKView
whose controller class is set toOKViewController
or its subclass.-
If you use
OKViewController
directly, then you must initialize OctopusKit early in your application launch cycle:func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { try! OctopusKit(gameCoordinator: coordinator) return true }
-
If you create your own subclass, it must implement these initializers:
required init?(coder aDecoder: NSCoder) { try! OctopusKit(gameCoordinator: gameCoordinator) super.init(coder: aDecoder) } required init(gameCoordinator: OKGameCoordinator? = nil) { try! super.init(gameCoordinator: gameCoordinator) }
❗️ If you are starting with Xcode’s SpriteKit Game template, you must delete the
GameViewController.viewDidLoad()
override, as that will prevent theOKViewController
from presenting your game coordinator’s scenes.
-
-
-
Code the states, scenes and UI for your game. The game coordinator must have at least one state that is associated with a scene, so your project must have custom classes which inherit from
OKGameState
andOKScene
.For an explanation of these classes, see the Architecture documentation.
If your scenes requires custom per-frame logic, you may override the
OKScene.shouldUpdateSystems(deltaTime:)
method.If your game state classes also perform per-frame updates, then you may also override the
OKScene.shouldUpdateGameCoordinator(deltaTime:)
method. -
Each of your game states can have a SwiftUI view associated with them to provide user interface elements like text and HUDs. The SwiftUI view is overlaid on top of the SpriteKit gameplay view. To let SwiftUI interact with your game’s state, make sure to pass an
.environmentObject(gameCoordinator)
to your SwiftUI view hierarchy.
Xcode File Templates
To save yourself from writing a lot of the same code for every new state, scene, component or method override, copy the contents of the Templates/Xcode
subfolder of the OK package to your ~/Library/Developer/Xcode/Templates/OctopusKit
.
This offers a section of templates for OctopusKit when you create a ⌘N New File in Xcode, including a very convenient template for creating a new game state class + its scene + UI in a single file, with just one click.
💡 You may create a symbolic link (with the ln
Terminal command) to keep the templates folders in sync whenever they’re updated.
Logs
OctopusKit uses the Xcode debug console to print
many kinds of events and status messages. Reading the logs can be very helpful in understanding the control flow and debugging your game. Pay special attention to warnings about incorrect usage of APIs that may cause unexpected behavior.
💡 Customize the logging system at the earliest point during application launch, e.g. in the AppDelegate
’s application(_:didFinishLaunchingWithOptions:)
method.
Set the formatting to your preferences:
OKLog.printEmptyLineBetweenFrames
OKLog.printTextOnSecondLine
OKLog.printEmptyLineBetweenEntries
OKLog.printAsCSV
Check
OctopusKit+Logs.swift
inSources/Core/Launch
to see the list of default logs. Disable unwanted logs to reduce clutter and improve performance:
OKLog.debug.isDisabled = true
OKLog.tips.isDisabled = true
💡 Set Conditional Compilation Flags in Tips & Troubleshooting to enable extra levels of verbose logging, such as player input events and physics collisions.
⚙️ Advanced: You may modify the OKLog
and OKLogEntry
code to emit entries for a different system, such as Apple’s Unified Logging (os_log
) or SwiftLog.
Player Input
In your scene’s override func createContents()
self.entity?.addComponents([
// Collect asynchronous events in a buffer for processing them in sync with the frame-update cycle:
sharedMouseOrTouchEventComponent, // iOS & macOS
// Translate mouse or touch input to an OS-agnostic format:
sharedPointerEventComponent, // iOS & macOS
sharedKeyboardEventComponent // macOS
])
yourPlayerEntity.addComponents([
// Use the scene's shared event stream:
RelayComponent(for: sharedPointerEventComponent),
// Filter the stream for events in the entity's sprite's bounds:
NodePointerStateComponent(),
PointerControlledDraggingComponent()
])
💡 See other components in OctopusKit/Components/Input
Sharing Data
You can share data between states, scenes and entities in many ways.
You may simply add custom properties to OctopusKit.shared.gameCoordinator
Or use data components like a DictionaryComponent
OctopusKit.shared.gameCoordinator.entity.addComponent(
DictionaryComponent <String, Any> ([
"playerNode": playerEntity.node as Any,
"playerStats": playerEntity[PlayerStatsComponent.self] as Any ]) )
To access that data:
if let globalData = OctopusKit.shared.gameCoordinator.entity[DictionaryComponent<String, Any>.self] {
let node = globalData["playerNode"] as? SKNode
let stats = globalData["playerStats"] as? PlayerStatsComponent
// ...
}
❕ Note that once an object is added to the dictionary, it holds a reference to the object.
💡 Instead of typo-prone String
keys, use TypeSafeIdentifiers
Accessing Game State from SwiftUI Views
class DataComponent: OKComponent, ObservableObject {
@Published public var secondsElapsed: TimeInterval = 0
override func update(deltaTime seconds: TimeInterval) {
secondsElapsed += seconds
}
}
struct DataComponentLabel: View {
@ObservedObject var component: DataComponent
var body: some View {
Text("Seconds elapsed: \(component.secondsElapsed)")
}
}
In a container view, pass the component as an argument to the label view:
var globalDataComponent: DataComponent? {
gameCoordinator.entity[DataComponent.self]
}
var body: some View {
if globalDataComponent != nil {
DataComponentLabel(component: globalDataComponent!)
}
}
💡 You may write a custom property wrapper like say @Component
to simplify accessing components from the current scene etc.
Physics Collisions
scene.entity?.addComponents([PhysicsWorldComponent(),
sharedPhysicsEventComponent])
scene.componentSystems.createSystem(forClass: PlayerContactComponent.self)
class PlayerContactComponent: PhysicsContactComponent {
override func didBegin(_ contact: SKPhysicsContact, in scene: OKScene?) {
// Handle the collision.
}
}
extension PhysicsCategories {
static let player = PhysicsCategories(1 << 0)
static let enemy = PhysicsCategories(1 << 1)
static let projectile = PhysicsCategories(1 << 2)
}
playerPhysicsBody
.categoryBitMask (.player)
.collisionBitMask (.enemy)
.contactTestBitMask ([.enemy, .projectile])
playerEntity.addComponents([PhysicsComponent(physicsBody: playerPhysicsBody),
RelayComponent(for: scene.sharedPhysicsEventComponent),
PlayerContactComponent())
-
A
PhysicsEventComponent
is like aTouchEventComponent
; it makes more sense to create just one and add it to the scene’s root entity, then share it with child entities viaRelayComponents
.
EveryOKScene
has asharedPhysicsEventComponent
property. Your child entities can access it with aRelayComponent(for: scene.sharedPhysicsEventComponent)
. -
A
PhysicsContactComponent
of an entity checks thePhysicsEventComponent
of the scene (via a relay) during itsupdate(deltaTime:)
method, and looks for events which involve thePhysicsComponent
body of its entity. -
If an entity’s physics body is involved in a contact event, then that entity’s
PhysicsContactComponent
calls itsdidBegin(_:in:)
ordidEnd(_:in:)
method, which are empty, abstract methods that must be customized by a game-specific subclass ofPhysicsContactComponent
. -
You must create a subclass of
PhysicsContactComponent
, for example aMonsterContactComponent
, and add it to all monster entities (and of course, remember to create a component system forMonsterContactComponent.self
). -
Your
PhysicsContactComponent
should override at least thedidBegin(_:in:)
method, and use it to decide what happens when a monster is involved in any physics collisions (after checking the arguments passed to that method to inspect the scene and other bodies).
❗️ Your scene’s
physicsWorld.contactDelegate
property must be set to the scene itself, otherwise the scene’sPhysicsEventComponent
will not receive any events! (The property is set automatically when you add aPhysicsWorldComponent
to the scene entity.)
❗️ Your physics bodies should have their
categoryBitMask
,collisionBitMask
andcontactTestBitMask
properties set according to which other bodies they should interact with and report events for. See Apple’s SpriteKit documentation for details.
❗️
PhysicsEventComponent
andPhysicsContactComponent
need to be updated every frame, so create component systems for them (and for their subclasses, if any)! Contact handling components must be updated after thePhysicsEventComponent
component, so be mindful of the order of component systems as well.
- 💡 Set the
LOGPHYSICS
custom compilation flag to see debugging information related to physics.
Advanced Stuff
Using the Xcode Scene Editor as the primary design tool
TODO: Incomplete section
Set the custom class of the scene as OKScene
or a subclass of it. Load the scene by calling OKGameCoordinator.loadAndPresentScene(fileNamed:withTransition:)
, e.g. during the didEnter.from(_:)
event of an OKGameState
.
Parallel Execution
You may be able to update multiple components simultaneously in parallel, if they do not depend upon each other and will not cause conflicts by accessing/modifying the same data.
For example, you may have multiple arrays of component systems with different dependency graphs, such as:
[MouseEventComponent, MouseControlledRotationComponent, MouseControlledShootingComponent]
[KeyboardEventComponent, KeyboardControlledMovementComponent]
If all these components are thread-safe, you could update those systems in parallel.
OctopusKit © 2021 Invading Octopus • Apache License 2.0