OctopusKit Usage Guide
August 8, 2023 Ā· View on GitHub
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. (“d_d`)
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
QuickStartfolder. -
README Template.md in the
Templates/Gamefolder.
-
š 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 gameCoordinatoras an instance variable ofContentViewif you do not require it to be a global value. The@StateObjectproperty 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
OKContainerViewand pass the game coordinator as anenvironmentObjectto it:import OctopusKit struct ContentView: View { var body: some View { OKContainerView() .environmentObject(gameCoordinator) } }The
OKContainerViewcombines a SpriteKitSKViewwith 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
environmentObjectto the top level content view created in theSceneDelegate.swiftfile, which will make it available to your entire view hierarchy. -
AppKit (macOS) or UIKit (iOS/iPadOS, tvOS): Your storyboard should have an
SKViewwhose controller class is set toOKViewControlleror its subclass.-
If you use
OKViewControllerdirectly, 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 theOKViewControllerfrom 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
OKGameStateandOKScene.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.swiftinSources/Core/Launchto 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
PhysicsEventComponentis 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.
EveryOKScenehas asharedPhysicsEventComponentproperty. Your child entities can access it with aRelayComponent(for: scene.sharedPhysicsEventComponent). -
A
PhysicsContactComponentof an entity checks thePhysicsEventComponentof the scene (via a relay) during itsupdate(deltaTime:)method, and looks for events which involve thePhysicsComponentbody of its entity. -
If an entity's physics body is involved in a contact event, then that entity's
PhysicsContactComponentcalls 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
PhysicsContactComponentshould 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.contactDelegateproperty must be set to the scene itself, otherwise the scene'sPhysicsEventComponentwill not receive any events! (The property is set automatically when you add aPhysicsWorldComponentto the scene entity.)
āļø Your physics bodies should have their
categoryBitMask,collisionBitMaskandcontactTestBitMaskproperties set according to which other bodies they should interact with and report events for. See Apple's SpriteKit documentation for details.
āļø
PhysicsEventComponentandPhysicsContactComponentneed to be updated every frame, so create component systems for them (and for their subclasses, if any)! Contact handling components must be updated after thePhysicsEventComponentcomponent, so be mindful of the order of component systems as well.
- š” Set the
LOGPHYSICScustom 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