├── .gitignore ├── _config.yml ├── CODE_OF_CONDUCT.md ├── Assets └── Images │ ├── SinclairZXSpectrumOK.png │ └── SinclairZXSpectrumOK-Slim.png ├── Sources └── OctopusKit │ ├── Assets │ ├── OctopusKit.xcassets │ │ ├── Contents.json │ │ └── OctopusKit │ │ │ ├── Contents.json │ │ │ ├── Colors │ │ │ ├── Contents.json │ │ │ ├── Sinclair Spectrum │ │ │ │ ├── Contents.json │ │ │ │ ├── Black.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── Blue.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── Cyan.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── Green.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── Red.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── White.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── BlueBright.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── CyanBright.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── Magenta.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── RedBright.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── Yellow.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── GreenBright.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── MagentaBright.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── WhiteBright.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── YellowBright.colorset │ │ │ │ │ └── Contents.json │ │ │ └── OKPurple.colorset │ │ │ │ └── Contents.json │ │ │ └── Images │ │ │ └── Contents.json │ ├── Bundle+OctopusKit.swift │ └── Shaders │ │ └── ShaderKit │ │ └── LICENSE │ ├── Apple API Extensions │ ├── SwiftUI │ │ └── OctopusUI.md │ ├── SpriteKit │ │ ├── SKRange+OctopusKit.swift │ │ ├── SKConstraint+OctopusKit.swift │ │ ├── SKNodeWithAnchor.swift │ │ ├── SKPhysicsContact+OctopusKit.swift │ │ ├── SKNode+OctopusAnimations.swift │ │ ├── SKNodeWithShader.swift │ │ ├── SKNodeWithBlendMode.swift │ │ ├── SKShapeNode+OctopusKit.swift │ │ ├── SKNodeWithSize.swift │ │ ├── SKNodeWithColor.swift │ │ ├── SKAttributeValue+OctopusKit.swift │ │ ├── SKTexture+OctopusKit.swift │ │ └── SKView+OctopusKit.swift │ ├── GameplayKit │ │ └── GKSKNodeComponent+OctopusKit.swift │ ├── OSAgnosticTypeAliases.swift │ ├── UIKit │ │ └── UIGestureRecognizer+OctopusKit.swift │ ├── AppKit │ │ └── NSGestureRecognizer+OctopusKit.swift │ └── Foundation │ │ └── Collection+OctopusKit.swift │ ├── SwiftUI │ ├── OctopusUI.swift │ ├── OKUIOverlay.swift │ └── OKContainerView.swift │ ├── Support & Utility │ ├── Protocols │ │ ├── Nameable.swift │ │ ├── Pseudocomponent.swift │ │ ├── RequiresUpdatesPerFrame.swift │ │ └── TurnBased.swift │ ├── OKLoader.swift │ ├── OKUtility+Graphics.swift │ ├── Enums │ │ ├── PhysicsMovementType.swift │ │ └── TimeStep.swift │ └── Data Structures │ │ ├── PhysicsCategories.swift │ │ └── LightCategories.swift │ ├── Components │ ├── Graphics │ │ ├── OKSprite │ │ │ └── OKSprite.swift │ │ ├── NoiseComponent.swift │ │ ├── ParticleEmitterComponent.swift │ │ ├── SceneComponent.swift │ │ ├── NodeActionComponent.swift │ │ └── OKShadow.swift │ ├── Turn-Based │ │ ├── TurnCounterComponent.swift │ │ └── TurnBasedTileBasedPositionComponent.swift │ ├── Abstract │ │ ├── SingleUseComponent.swift │ │ ├── macOSExclusiveComponent.swift │ │ ├── iOSExclusiveComponent.swift │ │ ├── DictionaryComponent.swift │ │ └── ValueComponent.swift │ ├── Input │ │ ├── Motion │ │ │ ├── MotionControlledParallaxComponent.swift │ │ │ ├── MotionManagerComponent.swift │ │ │ ├── MotionControlledGravityComponent.swift │ │ │ └── MotionControlledThrustComponent.swift │ │ ├── ImpactVibrationComponent.swift │ │ ├── VibrationComponent.swift │ │ ├── Touch Gestures │ │ │ ├── ClickGestureRecognizerComponent.swift │ │ │ └── TapGestureRecognizerComponent.swift │ │ ├── Pointer │ │ │ └── NodePointerState.swift │ │ └── Touch │ │ │ ├── NodeTouchState.swift │ │ │ └── TouchControlledPositioningComponent.swift │ ├── Gameplay │ │ ├── Agents & Goals │ │ │ └── WanderingGoalComponent.swift │ │ └── RandomizationComponent.swift │ ├── Audio │ │ └── AudioComponent.swift │ └── Physics │ │ ├── ThrustComponent.swift │ │ └── PhysicsWorldComponent.swift │ ├── Miscellaneous │ ├── OctopusKitErrors.swift │ └── Lab │ │ ├── Entity.swift │ │ └── Component.swift │ ├── Core │ ├── Launch │ │ ├── OctopusKit+Constants.swift │ │ ├── OctopusKit+Logs.swift │ │ └── OKConfiguration.swift │ └── Base │ │ ├── OKStateMachine.swift │ │ ├── OKScene+Keyboard.swift │ │ ├── OKSubscene+Keyboard.swift │ │ ├── OKTurnBasedEntity.swift │ │ ├── OKComponentSystem.swift │ │ └── OKState.swift │ └── Scene Templates │ └── OKLogoScene.swift ├── QuickStart ├── Images │ └── OctopusKitQuickStartDemo.gif └── Universal │ ├── 2 - MyGameCoordinator.swift │ ├── Components │ ├── GlobalDataComponent.swift │ └── TitleEffectsComponent.swift │ ├── 1 - OKQuickStartView.swift │ ├── Game States │ ├── 5 - TitleState │ │ ├── 5A - TitleState.swift │ │ └── 5C - TitleUI.swift │ ├── 7 - PausedState │ │ └── 7 - PausedState.swift │ ├── 8 - GameOverState │ │ └── 8 - GameOverState.swift │ ├── 4 - LogoState │ │ └── 4 - LogoState.swift │ └── 6 - PlayState │ │ └── 6A - PlayState.swift │ ├── Shared UI │ └── ButtonStyles.swift │ └── 3 - MyGameViewController.swift ├── Templates ├── Xcode │ ├── OctopusKit Scene.xctemplate │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ ├── TemplateInfo.plist │ │ └── ___FILEBASENAME___.swift │ ├── OctopusKit Entity.xctemplate │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ ├── ___FILEBASENAME___.swift │ │ └── TemplateInfo.plist │ ├── OctopusKit Component.xctemplate │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ ├── ___FILEBASENAME___.swift │ │ └── TemplateInfo.plist │ ├── OctopusKit Game State.xctemplate │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ ├── TemplateInfo.plist │ │ └── ___FILEBASENAME___.swift │ ├── OctopusKit Entity State.xctemplate │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ ├── TemplateInfo.plist │ │ └── ___FILEBASENAME___.swift │ ├── OctopusKit SwiftUI View.xctemplate │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ ├── ___FILEBASENAME___.swift │ │ └── TemplateInfo.plist │ ├── OctopusKit UI Component.xctemplate │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ ├── TemplateInfo.plist │ │ └── ___FILEBASENAME___.swift │ ├── OctopusKit Multistate Entity.xctemplate │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ └── TemplateInfo.plist │ ├── OctopusKit Multistate Scene.xctemplate │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ └── TemplateInfo.plist │ ├── OctopusKit Game State+Scene+UI.xctemplate │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ └── TemplateInfo.plist │ └── OctopusKit Turn-based Component.xctemplate │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ ├── ___FILEBASENAME___.swift │ │ └── TemplateInfo.plist ├── Game │ ├── Shared │ │ ├── Components │ │ │ ├── PlayerStatsComponent.swift │ │ │ └── UIViewModelComponent.swift │ │ ├── Game States & Scenes │ │ │ ├── Gameplay │ │ │ │ ├── PausedUI.swift │ │ │ │ ├── PlayState.swift │ │ │ │ ├── PausedState.swift │ │ │ │ └── PlayUI.swift │ │ │ └── Main Menu │ │ │ │ ├── MainMenuState.swift │ │ │ │ ├── MainMenuScene.swift │ │ │ │ └── MainMenuUI.swift │ │ ├── Core │ │ │ ├── GameCoordinator.swift │ │ │ └── GameContentView.swift │ │ └── Views │ │ │ └── PlayerStatsView.swift │ └── README Template.md └── Release Notes Template.md ├── Tests └── OctopusKitTests │ ├── TestsTemplate.swift │ ├── XCTestManifests.swift │ ├── Core Tests │ ├── EntityContainerTests.swift │ └── OctopusKitLaunchTests.swift │ └── Apple API Extensions Tests │ └── StringTests.swift ├── CONTRIBUTING.md ├── .github ├── pull_request_template.md ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── THANKS.md └── Package.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | /.swiftpm 6 | 7 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | plugins: 3 | - jekyll-redirect-from 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Don't be a doodoo 2 | 3 | Report doodoo to 🅾ctopus🅺it@🅘nvading🅞ctopus.ⓘⓞ 4 | -------------------------------------------------------------------------------- /Assets/Images/SinclairZXSpectrumOK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Assets/Images/SinclairZXSpectrumOK.png -------------------------------------------------------------------------------- /Assets/Images/SinclairZXSpectrumOK-Slim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Assets/Images/SinclairZXSpectrumOK-Slim.png -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /QuickStart/Images/OctopusKitQuickStartDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/QuickStart/Images/OctopusKitQuickStartDemo.gif -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Scene.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Scene.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Entity.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Entity.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Scene.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Scene.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Component.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Component.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Entity.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Entity.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Game State.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Game State.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Component.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Component.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Entity State.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Entity State.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Game State.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Game State.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit SwiftUI View.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit SwiftUI View.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit UI Component.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit UI Component.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Entity State.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Entity State.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit SwiftUI View.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit SwiftUI View.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit UI Component.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit UI Component.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Multistate Entity.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Multistate Entity.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Multistate Scene.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Multistate Scene.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Game State+Scene+UI.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Game State+Scene+UI.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Multistate Entity.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Multistate Entity.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Multistate Scene.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Multistate Scene.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Turn-based Component.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Turn-based Component.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Game State+Scene+UI.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Game State+Scene+UI.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Turn-based Component.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InvadingOctopus/octopuskit/HEAD/Templates/Xcode/OctopusKit Turn-based Component.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Images/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SwiftUI/OctopusUI.md: -------------------------------------------------------------------------------- 1 | SwiftUI-related extensions are in the separate **OctopusUI** package: 2 | 3 | 4 | 5 | These extensions are not necessary for all games, and non-game applications that don't require OctopusKit may import OctopusUI only. -------------------------------------------------------------------------------- /Sources/OctopusKit/SwiftUI/OctopusUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OctopusUI.swift 3 | // 4 | // Created by ShinryakuTako@invadingoctopus.io on 2019/11/20 5 | // 6 | 7 | // For a library of open-source SwiftUI controls and extensions, see the OctopusUI repository: 8 | // 9 | // https://github.com/InvadingOctopus/octopusui 10 | -------------------------------------------------------------------------------- /Tests/OctopusKitTests/TestsTemplate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tests.swift 3 | // OctopusKitTests 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/MM/DD. 6 | // 7 | 8 | import XCTest 9 | @testable import OctopusKit 10 | 11 | final class Tests: XCTestCase { 12 | 13 | func test() { 14 | 15 | } 16 | 17 | static var allTests = [ 18 | ("Test", test) 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please check the [TODO & Roadmap][todo] to see what this project is currently missing and where you could help out. 2 | 3 | If you have an idea, suggestion, code or any other contribution, please open a new issue or pull request on the [OctopusKit repository][repository]. 4 | 5 | [repository]: https://github.com/invadingoctopus/octopuskit 6 | [todo]: https://invadingoctopus.io/octopuskit/documentation/todo.html 7 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKRange+OctopusKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKRange+OctopusKit.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/04/17. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | public extension SKRange { 12 | 13 | static let zero = SKRange(constantValue: 0) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Support & Utility/Protocols/Nameable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Nameable.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/04/22. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import OctopusCore 11 | 12 | extension OKEntity: Nameable {} 13 | extension SKNode: Nameable {} 14 | extension SKTileGroup: Nameable {} 15 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/GameplayKit/GKSKNodeComponent+OctopusKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GKSKNodeComponent+OctopusKit.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/10/11. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | private extension GKSKNodeComponent { 12 | 13 | /// Moved to `NodeComponent` 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Templates/Game/Shared/Components/PlayerStatsComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerStatsComponent.swift 3 | // OctopusKit Project Template 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import GameplayKit 11 | import OctopusKit 12 | 13 | final class PlayerStatsComponent: OKComponent { 14 | 15 | private(set) var score: Int = 0 16 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /Tests/OctopusKitTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(NumericsTests.allTests), 7 | testCase(CGPointTests.allTests), 8 | testCase(StringTests.allTests), 9 | 10 | testCase(OctopusKitLaunchTests.allTests), 11 | testCase(OKLogTests.allTests), 12 | testCase(ComponentTests.allTests), 13 | 14 | testCase(ContiguousArray2DTests.allTests) 15 | ] 16 | } 17 | #endif 18 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Graphics/OKSprite/OKSprite.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKSprite.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/03/16. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // CHECK: Rename to `OKTouchEventComponentSprite`? :P 10 | 11 | import SpriteKit 12 | import GameplayKit 13 | 14 | /// A subclass of `SKSpriteNode` that redirects input events to a `TouchEventComponent`. 15 | public final class OKSprite: SKSpriteNode { 16 | } 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull request 3 | about: An awesome contribution 4 | 5 | --- 6 | 7 | - Category: 'API/Functionality/Component/Language/Style/other' 8 | - Target Platform: 'iOS/macOS/tvOS/Universal' 9 | - Any other info? 10 | 11 | **Is your pull request related to an issue?** 12 | A link to the issue. 13 | 14 | **Describe the solution offered by your pull request** 15 | A clear description of your commit(s). 16 | 17 | **Additional context** 18 | Add any other context or screenshots etc. related to your pull request here. 19 | -------------------------------------------------------------------------------- /Tests/OctopusKitTests/Core Tests/EntityContainerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityContainerTests.swift 3 | // OctopusKitTests 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/24. 6 | // 7 | 8 | import XCTest 9 | import GameplayKit 10 | @testable import OctopusKit 11 | 12 | /// Tests for the core Entity Container (e.g. scene) logic. 13 | final class EntityContainerTests: XCTestCase { 14 | 15 | // MARK: - Types 16 | 17 | // MARK: - Tests 18 | 19 | // static var allTests = [ 20 | // ("Test entity search", testEntitySearch) 21 | // ] 22 | } 23 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/Bundle+OctopusKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+OctopusKit.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020-06-30 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Bundle { 12 | 13 | /// The bundle containing the assets and resources which are included with OctopusKit. 14 | /// 15 | /// **Example:** `Color("OctopusKit/Colors/OKPurple", bundle: Bundle.octopusKit)` 16 | static var octopusKit: Bundle { 17 | return Bundle.module 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Miscellaneous/OctopusKitErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OctopusKitErrors.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/10/29. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import OctopusCore 10 | 11 | public extension OKError { 12 | 13 | /// This error is raised when the core objects for OctopusKit have not been initialized correctly during the application launch cycle. 14 | /// 15 | /// Used by `OKGameCoordinator` and `OKViewController`. 16 | // case invalidConfiguration(String) 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Turn-based Component.xctemplate/___FILEBASENAME___.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SpriteKit 4 | import GameplayKit 5 | import OctopusKit 6 | 7 | final class ___FILEBASENAMEASIDENTIFIER___: OKTurnBasedComponent { 8 | 9 | override var requiredComponents: [GKComponent.Type]? { 10 | [] 11 | } 12 | 13 | override func beginTurn(delta turns: Int = 1) { 14 | 15 | } 16 | 17 | override func updateTurn(delta turns: Int = 1) { 18 | 19 | } 20 | 21 | override func endTurn(delta turns: Int = 1) { 22 | 23 | } 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Component.xctemplate/___FILEBASENAME___.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SpriteKit 4 | import GameplayKit 5 | import OctopusKit 6 | 7 | final class ___FILEBASENAMEASIDENTIFIER___: OKComponent { 8 | 9 | override var requiredComponents: [GKComponent.Type]? { 10 | [] 11 | } 12 | 13 | override func didAddToEntity(withNode node: SKNode) { 14 | 15 | } 16 | 17 | override func update(deltaTime seconds: TimeInterval) { 18 | 19 | } 20 | 21 | override func willRemoveFromEntity(withNode node: SKNode) { 22 | 23 | } 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Entity.xctemplate/___FILEBASENAME___.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SpriteKit 4 | import GameplayKit 5 | import OctopusKit 6 | 7 | final class ___FILEBASENAMEASIDENTIFIER___: OKEntity { 8 | 9 | init() { 10 | 11 | super.init(name: "___FILEBASENAME___") 12 | 13 | // Add components in the order of dependency, creating and configuring them first if necessary. 14 | 15 | self.addComponents([ 16 | // Customize 17 | ]) 18 | 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit SwiftUI View.xctemplate/___FILEBASENAME___.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | import OctopusKit 5 | 6 | struct ___FILEBASENAMEASIDENTIFIER___: View { 7 | 8 | @EnvironmentObject var gameCoordinator: OKGameCoordinator 9 | 10 | var body: some View { 11 | <#Text("___FILEBASENAMEASIDENTIFIER___").font(.largeTitle).foregroundColor(.gray)#> 12 | } 13 | 14 | } 15 | 16 | struct ___FILEBASENAMEASIDENTIFIER____Previews: PreviewProvider { 17 | static var previews: some View { 18 | ___FILEBASENAMEASIDENTIFIER___() 19 | // .environmentObject(gameCoordinator) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Templates/Game/Shared/Game States & Scenes/Gameplay/PausedUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PausedUI.swift 3 | // OctopusKit Project Template 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct PausedUI: View { 12 | var body: some View { 13 | VStack { 14 | 15 | Text("PAUSED") 16 | .font(.title) 17 | .foregroundColor(.primary) 18 | 19 | Spacer() 20 | } 21 | } 22 | } 23 | 24 | struct PausedUI_Previews: PreviewProvider { 25 | static var previews: some View { 26 | PausedUI() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: invadingoctopus 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /Templates/Game/Shared/Core/GameCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameCoordinator.swift 3 | // OctopusKit Project Template 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import Foundation 10 | import OctopusKit 11 | 12 | class GameCoordinator: OKGameCoordinator { 13 | 14 | init() { 15 | super.init(states: [MainMenuState(), 16 | PlayState(), 17 | PausedState()], 18 | initialStateClass: MainMenuState.self) 19 | } 20 | 21 | override func willEnterInitialState() { 22 | self.entity.addComponents([]) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Support & Utility/Protocols/Pseudocomponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pseudocomponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/12/10. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | public typealias OKPseudocomponent = Pseudocomponent 12 | 13 | /// A protocol for types that hold a reference to an `OKEntity` and can perform per-frame updates. Such types may be used as the properties of a component, but otherwise cannot be added to an entity or component system. 14 | public protocol Pseudocomponent: UpdatablePerFrame { 15 | var entity: OKEntity? { get } 16 | func update(deltaTime seconds: TimeInterval) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Turn-Based/TurnCounterComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TurnCounterComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/03/04. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | public final class TurnCounterComponent: OKTurnBasedComponent { 12 | 13 | public fileprivate(set) var currentTurn: Int = 0 14 | public fileprivate(set) var turnsElapsed: Int = 0 15 | 16 | public override func beginTurn(delta turns: Int = 1) { 17 | currentTurn += turns 18 | } 19 | 20 | public override func endTurn(delta turns: Int = 1) { 21 | turnsElapsed += turns 22 | } 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an awesome idea 4 | 5 | --- 6 | 7 | - Category: 'API/Functionality/Component/Language/Style/other' 8 | - Target Platform: 'iOS/macOS/tvOS/Universal' 9 | - Any other info? 10 | 11 | **Is your feature request related to a problem? Please describe.** 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /Templates/Release Notes Template.md: -------------------------------------------------------------------------------- 1 | Summary! 2 | 3 | [Compare with previous release](https://github.com/InvadingOctopus/octopuskit/compare/[PREVIOUSRELEASE]...[CURRENTREASE]) or [latest development branch](https://github.com/InvadingOctopus/octopuskit/compare/[CURRENTRELEASE]...develop) 4 | 5 | Requires Xcode 1x, iOS 1x and macOS 10.1x Name. 6 | 7 | #### ⭐️ Major New Stuff: 8 | 9 | - **First Major Feature!** 10 | - Second 11 | - More! 12 | 13 | #### ⚡️ Massive Improvements: 14 | 15 | - This! 16 | - That! 17 | 18 | #### ✨ Fixed & Polished: 19 | 20 | - beep 21 | - boop 22 | 23 | #### 🧩 Components: 24 | 25 | - Component 26 | 27 | #### 🍱 Miscellaneous: 28 | 29 | - Also 30 | 31 | ##### Notes 32 | 33 | And finally ^—^ 34 | 35 | Current focus, stuff left to do, priorities for next release. -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKConstraint+OctopusKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKConstraint+OctopusKit.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/04/18. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | extension SKConstraint { 12 | 13 | /// Creates a constraint that restricts both coordinates of a node's position inside the specified rectangle. 14 | @inlinable 15 | public class func bounds(_ rect: CGRect) -> SKConstraint { 16 | let xRange = SKRange(lowerLimit: rect.minX, upperLimit: rect.maxX) 17 | let yRange = SKRange(lowerLimit: rect.minY, upperLimit: rect.maxY) 18 | return positionX(xRange, y: yRange) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Entity.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | An OctopusKit entity. 9 | Summary 10 | An OctopusKit entity 11 | SortOrder 12 | 40 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | Entity 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | 22 | 23 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Component.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | An OctopusKit component. 9 | Summary 10 | An OctopusKit component 11 | SortOrder 12 | 50 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | Component 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | 22 | 23 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Game State.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | A state for an OctopusKit game. 9 | Summary 10 | A state for an OctopusKit game 11 | SortOrder 12 | 20 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | GameState 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | 22 | 23 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Scene.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | An OctopusKit components-based scene. 9 | Summary 10 | An OctopusKit components-based scene 11 | SortOrder 12 | 30 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | Scene 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | 22 | 23 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Entity State.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | A state for an OctopusKit entity. 9 | Summary 10 | A state for an OctopusKit entity 11 | SortOrder 12 | 45 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | EntityState 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | 22 | 23 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit SwiftUI View.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | A SwiftUI view for an OctopusKit game state. 9 | Summary 10 | A SwiftUI view for an OctopusKit game state 11 | SortOrder 12 | 35 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | UI 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | 22 | 23 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Turn-based Component.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | An OctopusKit turn-based component. 9 | Summary 10 | An OctopusKit turn-based component 11 | SortOrder 12 | 52 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | Component 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | 22 | 23 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Multistate Entity.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | An OctopusKit entity with multiple states. 9 | Summary 10 | An OctopusKit entity with multiple states 11 | SortOrder 12 | 42 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | Entity 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | 22 | 23 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Game State.xctemplate/___FILEBASENAME___.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SpriteKit 4 | import GameplayKit 5 | import OctopusKit 6 | 7 | final class ___FILEBASENAMEASIDENTIFIER___: OKGameState { 8 | 9 | init() { 10 | // NOTE: Game state classes are initialized when the game coordinator is initialized: on game launch. 11 | super.init(associatedSceneClass: <#SceneForThisState#>.self, 12 | associatedSwiftUIView: <#UIForThisState#>()) 13 | } 14 | 15 | override var validNextStates: [OKState.Type] { 16 | // Customize: Specify the valid states that this state can transition to. 17 | // NOTE: Do not perform any logic to conditionally control state transitions here. See `OKState` documentation. 18 | [] // Default: allow all states. 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit UI Component.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | An OctopusKit component with an associated SwiftUI view. 9 | Summary 10 | An OctopusKit component with an associated SwiftUI view 11 | SortOrder 12 | 52 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | Component 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/OKPurple.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.881", 9 | "green" : "0.416", 10 | "red" : "0.670" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.855", 22 | "green" : "0.428", 23 | "red" : "0.635" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/Black.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.000", 22 | "green" : "0.000", 23 | "red" : "0.000" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/Blue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.843", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.808", 22 | "green" : "0.000", 23 | "red" : "0.020" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/Cyan.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.843", 9 | "green" : "0.843", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.835", 22 | "green" : "0.831", 23 | "red" : "0.384" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/Green.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.843", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.247", 22 | "green" : "0.831", 23 | "red" : "0.380" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/Red.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.843" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.110", 22 | "green" : "0.161", 23 | "red" : "0.773" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/White.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.843", 9 | "green" : "0.843", 10 | "red" : "0.843" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.843", 22 | "green" : "0.843", 23 | "red" : "0.843" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Game State+Scene+UI.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | A state, scene and UI for an OctopusKit game, in a single file. 9 | Summary 10 | A state, scene and UI for an OctopusKit game, in a single file 11 | SortOrder 12 | 10 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | GameState 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/BlueBright.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.961", 22 | "green" : "0.000", 23 | "red" : "0.027" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/CyanBright.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.992", 22 | "green" : "0.984", 23 | "red" : "0.459" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/Magenta.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.843", 9 | "green" : "0.000", 10 | "red" : "0.843" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.816", 22 | "green" : "0.165", 23 | "red" : "0.773" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/RedBright.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.137", 22 | "green" : "0.200", 23 | "red" : "0.918" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/Yellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.843", 10 | "red" : "0.843" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.275", 22 | "green" : "0.843", 23 | "red" : "0.843" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Templates/Game/Shared/Views/PlayerStatsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerStatsView.swift 3 | // OctopusKit Project Template 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct PlayerStatsView: View { 12 | 13 | @EnvironmentObject var viewModelComponent: UIViewModelComponent 14 | 15 | var body: some View { 16 | VStack { 17 | Text("Score: \(viewModelComponent.playerScore)") 18 | Text("Angle: \(viewModelComponent.playerRotation)") 19 | } 20 | .foregroundColor(.accentColor) 21 | } 22 | } 23 | 24 | struct PlayerStatsView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | PlayerStatsView() 27 | .environmentObject(UIViewModelComponent()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/GreenBright.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "1.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.298", 22 | "green" : "0.984", 23 | "red" : "0.455" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/MagentaBright.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "0.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.969", 22 | "green" : "0.200", 23 | "red" : "0.918" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/WhiteBright.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "1.000", 22 | "green" : "1.000", 23 | "red" : "1.000" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/OctopusKit.xcassets/OctopusKit/Colors/Sinclair Spectrum/YellowBright.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "display-gamut" : "sRGB", 14 | "idiom" : "universal" 15 | }, 16 | { 17 | "color" : { 18 | "color-space" : "display-p3", 19 | "components" : { 20 | "alpha" : "1.000", 21 | "blue" : "0.329", 22 | "green" : "1.000", 23 | "red" : "1.000" 24 | } 25 | }, 26 | "display-gamut" : "display-P3", 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Multistate Scene.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | An OctopusKit components-based scene that handles multiple game states. 9 | Summary 10 | An OctopusKit components-based scene that handles multiple game states 11 | SortOrder 12 | 60 13 | AllowedTypes 14 | 15 | public.swift-source 16 | 17 | DefaultCompletionName 18 | Scene 19 | MainTemplateFile 20 | ___FILEBASENAME___.swift 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/OctopusKit/SwiftUI/OKUIOverlay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKUIOverlay.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019-10-20 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | public typealias OctopusUIOverlay = OKUIOverlay 13 | 14 | /// Displays the SwiftUI overlay for the `OKGameCoordinatorType`'s current `OKGameState`. 15 | public struct OKUIOverlay : View 16 | where OKGameCoordinatorType: OKGameCoordinator 17 | { 18 | 19 | @EnvironmentObject var gameCoordinator: OKGameCoordinatorType 20 | 21 | public var body: some View { 22 | Group { 23 | if gameCoordinator.currentGameState != nil { 24 | gameCoordinator.currentGameState!.associatedSwiftUIView 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Abstract/SingleUseComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleUseComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/11/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | /// An abstract base class for components that perform their task only once at the moment when they're added to an entity, and then remove themselves from their entity. 12 | /// 13 | /// - Important: Subclasses must call `super.didAddToEntity()` or `super.didAddToEntity(withNode:)` *after* they hve performed their task in their override of those methods. 14 | open class SingleUseComponent: OKComponent { 15 | 16 | open override func didAddToEntity() { 17 | super.didAddToEntity() // Will also call `didAddToEntity(withNode:)` on the subclass. 18 | self.entity?.removeComponent(ofType: type(of: self)) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Templates/Game/Shared/Game States & Scenes/Gameplay/PlayState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayState.swift 3 | // OctopusKit Project Template 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import OctopusKit 10 | 11 | final class PlayState: OKGameState { 12 | 13 | init() { 14 | // NOTE: Game state classes are initialized when the game coordinator is initialized: on game launch. 15 | super.init(associatedSceneClass: PlayScene.self, 16 | associatedSwiftUIView: PlayUI()) 17 | } 18 | 19 | override var validNextStates: [OKState.Type] { 20 | // Customize: Specify the valid states that this state can transition to. 21 | // NOTE: Do not perform any logic to conditionally control state transitions here. See `OKGameState` documentation. 22 | [PausedState.self] 23 | } 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Abstract/macOSExclusiveComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // macOSExclusiveComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/11/2. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | #if os(macOS) 12 | 13 | /// A "dummy" base class for components that are not compatible with iOS. 14 | public typealias macOSExclusiveComponent = OKComponent 15 | 16 | #else 17 | 18 | /// A "dummy" base class for components that are not compatible with iOS. 19 | open class macOSExclusiveComponent: OKComponent { 20 | 21 | public override init() { 22 | // TO DECIDE: Error or warning? 23 | OKLog.errors.debug("\(📜("\(type(of: self)) is for macOS only!"))") 24 | super.init() 25 | } 26 | 27 | public required init?(coder aDecoder: NSCoder) { fatalError("\(Self.self)) is for macOS only!") } 28 | } 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /Templates/Game/Shared/Game States & Scenes/Gameplay/PausedState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PausedState.swift 3 | // OctopusKit Project Template 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import OctopusKit 10 | 11 | final class PausedState: OKGameState { 12 | 13 | init() { 14 | // NOTE: Game state classes are initialized when the game coordinator is initialized: on game launch. 15 | super.init(associatedSceneClass: PlayScene.self, 16 | associatedSwiftUIView: PausedUI()) 17 | } 18 | 19 | override var validNextStates: [OKState.Type] { 20 | // Customize: Specify the valid states that this state can transition to. 21 | // NOTE: Do not perform any logic to conditionally control state transitions here. See `OKGameState` documentation. 22 | [PlayState.self, 23 | MainMenuState.self] 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Abstract/iOSExclusiveComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iOSExclusiveComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/03/21. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import OctopusCore 10 | import GameplayKit 11 | 12 | #if os(iOS) 13 | 14 | /// A "dummy" base class for components that are not compatible with macOS or tvOS. 15 | public typealias iOSExclusiveComponent = OKComponent 16 | 17 | #else 18 | 19 | /// A "dummy" base class for components that are not compatible with macOS or tvOS. 20 | open class iOSExclusiveComponent: OKComponent { 21 | 22 | public override init() { 23 | // TO DECIDE: Error or warning? 24 | OKLog.errors.debug("\(📜("\(type(of: self)) is for iOS only!"))") 25 | super.init() 26 | } 27 | 28 | public required init?(coder aDecoder: NSCoder) { fatalError("\(Self.self)) is for iOS only!") } 29 | } 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /Templates/Game/Shared/Game States & Scenes/Main Menu/MainMenuState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainMenuState.swift 3 | // OctopusKit Project Template 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import GameplayKit 11 | import OctopusKit 12 | 13 | final class MainMenuState: OKGameState { 14 | 15 | init() { 16 | // NOTE: Game state classes are initialized when the game coordinator is initialized: on game launch. 17 | super.init(associatedSceneClass: MainMenuScene.self, 18 | associatedSwiftUIView: MainMenuUI()) 19 | } 20 | 21 | override var validNextStates: [OKState.Type] { 22 | // Customize: Specify the valid states that this state can transition to. 23 | // NOTE: Do not perform any logic to conditionally control state transitions here. See `OKGameState` documentation. 24 | [PlayState.self] 25 | } 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Templates/Game/Shared/Core/GameContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameContentView.swift 3 | // OctopusKit Project Template 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SwiftUI 10 | import OctopusKit 11 | 12 | /// Set this as the root view of your SwiftUI project's `ContentView`. 13 | struct GameContentView: View { 14 | 15 | @StateObject private var gameCoordinator = GameCoordinator() 16 | 17 | var body: some View { 18 | OKContainerView() 19 | .environmentObject(gameCoordinator) 20 | .statusBar(hidden: true) 21 | // .edgesIgnoringSafeArea(.all) // Uncomment this to allow all of your UI views to encompass the entire screen (e.g. including the iPhone notch). 22 | } 23 | } 24 | 25 | struct GameContentView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | GameContentView() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Templates/Game/Shared/Game States & Scenes/Gameplay/PlayUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayUI.swift 3 | // OctopusKit Project Template 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SwiftUI 10 | import OctopusKit 11 | 12 | struct PlayUI: View { 13 | 14 | var viewModelComponent: UIViewModelComponent? { 15 | OctopusKit.shared.currentScene?.entity?[UIViewModelComponent.self] 16 | } 17 | 18 | var body: some View { 19 | VStack { 20 | 21 | if let viewModelComponent = self.viewModelComponent { 22 | PlayerStatsView() 23 | .environmentObject(viewModelComponent) 24 | .font(.title) 25 | .foregroundColor(.primary) 26 | } 27 | 28 | Spacer() 29 | } 30 | } 31 | } 32 | 33 | struct PlayUI_Previews: PreviewProvider { 34 | static var previews: some View { 35 | PlayUI() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKNodeWithAnchor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKNodeWithAnchor.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/30. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | /// A protocol for types that have an `anchorPoint` property. 12 | /// 13 | /// This allows different `SKNode` subclasses to be handled together when processing their anchor points. 14 | public protocol SKNodeWithAnchor { // where Self: SKNode { // ⚠️ Crashes. 15 | // TODO: Change name to an adjective? 16 | 17 | // https://developer.apple.com/documentation/spritekit/skspritenode/using_the_anchor_point_to_move_a_sprite 18 | 19 | var anchorPoint: CGPoint { get set } 20 | } 21 | 22 | extension SKScene: SKNodeWithAnchor {} 23 | extension SKSpriteNode: SKNodeWithAnchor {} 24 | extension SKVideoNode: SKNodeWithAnchor {} 25 | extension SKTileMapNode: SKNodeWithAnchor {} 26 | 27 | extension SKLabelNode: SKNodeWithAnchor {} // Conformance added via SKLabelNode+OctopusKit extensions 28 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Core/Launch/OctopusKit+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OctopusKit+Constants.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/04/15. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import Foundation 10 | 11 | extension OctopusKit { 12 | 13 | /// Encapsulates global constants that may be used by various parts of the OctopusKit. 14 | public enum Constants { 15 | 16 | /// A collection of strings for the names of entities etc., to avoid hard-coding text and prevent typos during searches or comparisons etc. 17 | public enum Strings { 18 | public static let gameCoordinatorEntityName = "Game Coordinator Entity" 19 | } 20 | 21 | /// Various timings and durations. 22 | public enum Time { 23 | /// Represents the ideal time (in fractions of a second) per frame, in a 60 frames-per-second system: `1 / 60 = 0.01666666667` 24 | public static let timePerFrameIn60FPS: TimeInterval = 0.01666666667 // 1 / 60 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Assets/Shaders/ShaderKit/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Paul Hudson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Input/Motion/MotionControlledParallaxComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotionControlledParallaxComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/11/16. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // TODO: Implement 10 | 11 | import SpriteKit 12 | import GameplayKit 13 | 14 | #if os(iOS) 15 | 16 | import CoreMotion 17 | 18 | /// Adds a shift in the position of the entity's `NodeComponent` node every frame, based on the device's motion. 19 | /// 20 | /// **Dependencies:** `MotionManagerComponent`, `NodeComponent` 21 | public class MotionControlledParallaxComponent: OKComponent, RequiresUpdatesPerFrame { 22 | 23 | public override var requiredComponents: [GKComponent.Type]? { 24 | [NodeComponent.self, 25 | MotionManagerComponent.self] 26 | } 27 | 28 | public override func update(deltaTime seconds: TimeInterval) { 29 | super.update(deltaTime: seconds) 30 | 31 | } 32 | } 33 | 34 | #else 35 | 36 | public final class MotionControlledParallaxComponent: iOSExclusiveComponent {} 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/OSAgnosticTypeAliases.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSAgnosticTypeAliases.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/11/1. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // Wherever the AppKit (macOS) "NS-" and UIKit (iOS, iPadOS, tvOS) "UI-" variants of an object can be interchanged, OctopusKit uses an OS-agnostic type alias to reduce the amount of duplicated code. 10 | 11 | // TODO: Check with Catalyst. 12 | // TODO: Use `GCEventViewController` for game controller input. 13 | 14 | import Foundation 15 | 16 | #if canImport(AppKit) 17 | 18 | import AppKit 19 | 20 | public typealias OSMouseOrTouchEventComponent = MouseEventComponent 21 | public typealias OSClickOrTapGestureRecognizerComponent = ClickGestureRecognizerComponent 22 | 23 | #elseif canImport(UIKit) 24 | 25 | import UIKit 26 | 27 | public typealias OSMouseOrTouchEventComponent = TouchEventComponent 28 | 29 | #endif 30 | 31 | #if os(iOS) // Not available on tvOS 32 | 33 | public typealias OSClickOrTapGestureRecognizerComponent = TapGestureRecognizerComponent 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Core/Launch/OctopusKit+Logs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OctopusKit+Logs.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/02/12. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // Logs are in a separate extension for convenience, e.g. so that a project may replace them with its own versions. 10 | 11 | import OctopusCore 12 | import OSLog 13 | 14 | public extension OKLog { 15 | 16 | // MARK: Global Game-related Logs 17 | 18 | /// A log for transitions within game states and entity states. 19 | static let states = Logger(subsystem: OctopusCore.OctopusKit.Constants.Strings.octopusKitBundleID, category: "🚦 States") 20 | 21 | /// A log for the components architecture, including entities and component systems. 22 | static let components = Logger(subsystem: OctopusCore.OctopusKit.Constants.Strings.octopusKitBundleID, category: "🧩 ECS") 23 | 24 | /// A log for the cycle of turn updates in a turn-based game. 25 | static let turns = Logger(subsystem: OctopusCore.OctopusKit.Constants.Strings.octopusKitBundleID, category: "🔄 Turns") 26 | 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a compilation error, runtime crash or weird behavior 4 | 5 | --- 6 | 7 | **Category:** 'Compilation/Warning/Crash/Behavior' 8 | 9 | **Describe the bug** 10 | A clear and concise description of the bug. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Start with a new 'SwiftUI/AppKit/UIKit' project 15 | 2. Edit or create this file: `…` 16 | 3. Add the following code: `…` 17 | 4. Build for 'iOS/macOS/tvOS' 18 | 5. Run and perform these actions: '…' 19 | 6. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help illustrate the problem. 26 | 27 | **Build Environment (what you're developing on)** 28 | - OctopusKit Version: 'x.x.x or branch or fork' 29 | - macOS Version: '10.x.x (build)' 30 | - Xcode Version: 'x.x.x (build)' 31 | - Swift Version: 'x.x' 32 | 33 | **Target Device (what you're compiling for)** 34 | - Device: 'e.g. iPhone12, Simulator?' 35 | - Target OS Version: 'e.g. iOS14.0' 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Input/ImpactVibrationComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImpactVibrationComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/10/28. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import GameplayKit 11 | 12 | #if os(iOS) 13 | 14 | public final class ImpactVibrationComponent: VibrationComponent { 15 | 16 | public var style: UIImpactFeedbackGenerator.FeedbackStyle 17 | 18 | public init(style: UIImpactFeedbackGenerator.FeedbackStyle = .light) { 19 | self.style = style 20 | super.init() 21 | } 22 | 23 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 24 | 25 | public override func createGenerator() { 26 | self.feedbackGenerator = UIImpactFeedbackGenerator(style: self.style) 27 | } 28 | 29 | public override func vibrate() { 30 | self.feedbackGenerator?.impactOccurred() 31 | } 32 | 33 | } 34 | 35 | #else 36 | 37 | public final class ImpactVibrationComponent: iOSExclusiveComponent {} 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Miscellaneous/Lab/Entity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entity.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/21. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import Foundation 10 | import GameplayKit 11 | 12 | #if UseNewProtocols // ℹ️ Not currently in use; This is mostly preparation for future independence from GameplayKit, if needed. 13 | 14 | public protocol Entity: 15 | /// class, // Already on `ComponentContainer` 16 | Nameable, 17 | ComponentContainer, 18 | UpdatablePerFrame 19 | { 20 | var delegate: EntityDelegate? { get } 21 | 22 | // init() 23 | 24 | func removeFromDelegate() 25 | } 26 | 27 | /// A protocol for types that manage entities, such as `OKScene`. 28 | public protocol EntityDelegate: class { 29 | 30 | func entity(_ entity: Entity, didAddComponent component: Component) 31 | func entity(_ entity: Entity, willRemoveComponent component: Component) 32 | 33 | @discardableResult 34 | func entity(_ entity: Entity, didSpawn spawnedEntity: Entity) -> Bool 35 | 36 | func entityDidRequestRemoval(_ entity: Entity) 37 | } 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Support & Utility/OKLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKLoader.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2014-10-31 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // TODO: Implement 10 | 11 | import Foundation 12 | 13 | public typealias OctopusLoader = OKLoader 14 | 15 | public final class OKLoader { 16 | 17 | /// Define a startupLoader() function or variable at the global scope. 18 | public class func loadResourcesWithCompletionHandler(completionHandler: () -> Void) { 19 | // CREDIT: Apple's Adventure Sample 20 | // 21 | // let queue = dispatch_get_main_queue() 22 | // 23 | // let backgroundQueue = dispatch_get_global_queue(CLong(DISPATCH_QUEUE_PRIORITY_HIGH), 0) 24 | // dispatch_async(backgroundQueue) { 25 | // 26 | // if let loader = OctopusKit.startupLoader { 27 | // loader() 28 | // } else { 29 | // OKLog.warnings.debug("\(📜("No startupLoader specified in OctopusEnvironmnet"))") 30 | // } 31 | // 32 | // dispatch_async(queue, completionHandler) 33 | // } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Templates/Game/Shared/Game States & Scenes/Main Menu/MainMenuScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainMenuScene.swift 3 | // OctopusKit Project Template 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import GameplayKit 11 | import OctopusKit 12 | 13 | final class MainMenuScene: OKScene { 14 | 15 | override func setName() -> String? { "MainMenuScene" } 16 | 17 | override func createComponentSystems() -> [GKComponent.Type] { 18 | // Customize. Each component must be listed after the components it depends on (as per its `requiredComponents` property.) 19 | // See OKScene.createComponentSystems() for the default set of commonly-used systems. 20 | super.createComponentSystems() 21 | } 22 | 23 | override func createContents() { 24 | // Customize: This is where you construct entities to add to your scene. 25 | 26 | self.entity? += ([sharedMouseOrTouchEventComponent, 27 | sharedPointerEventComponent]) 28 | 29 | addEntity(OKEntity(name: "", components: [ 30 | // Customize 31 | ])) 32 | } 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Core/Launch/OKConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKConfiguration.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020-03-31 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import OctopusCore 10 | 11 | public struct OKConfiguration { 12 | 13 | #if os(macOS) 14 | 15 | /// If `true` (default), the application's default menu bar is modified to have menu names and items suitable for games. 16 | /// 17 | /// This setting is read when `OKViewController` presents its view. 18 | public var modifyDefaultMenuBar: Bool = true 19 | 20 | #endif 21 | 22 | /// A dictionary of flags for SpriteKit, mainly for debugging. 23 | /// 24 | /// `debugDrawStats_SKContextType`: When `true`, prints "Metal" or "OpenGL" in the corner of the `SKView` depending on which renderer is active. 25 | /// 26 | /// See Apple Technical Note TN2451: SpriteKit Debugging Guide: https://developer.apple.com/library/archive/technotes/tn2451/_index.html#//apple_ref/doc/uid/DTS40017609-CH1-SHADERCOMPILATION 27 | @OKUserDefault(key: "SKDefaults", defaultValue: [:]) 28 | public static var flagsForSpriteKit: [String: Any] 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Graphics/NoiseComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoiseComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/03/15. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import GameplayKit 11 | 12 | /// Encapsulates a `GKNoise` object and provides the foundation for a `NoiseMapComponent`. 13 | open class NoiseComponent: OKComponent { 14 | 15 | // ℹ️ Hierarchy: Noise Source » Noise » Noise Map 16 | // https://developer.apple.com/documentation/gameplaykit/gknoisesource 17 | // https://developer.apple.com/documentation/gameplaykit/gknoise 18 | // https://developer.apple.com/documentation/gameplaykit/gknoisemap 19 | 20 | open var noise: GKNoise 21 | 22 | /// - Parameter noise: A representation of procedural noise, generated by a noise source, that you can use to process, transform, or combine noise. Default: A single `GKPerlinNoiseSource`. 23 | public init(noise: GKNoise = GKNoise(GKPerlinNoiseSource())) { 24 | self.noise = noise 25 | super.init() 26 | } 27 | 28 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Scene.xctemplate/___FILEBASENAME___.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SpriteKit 4 | import GameplayKit 5 | import OctopusKit 6 | 7 | final class ___FILEBASENAMEASIDENTIFIER___: OKScene { 8 | 9 | // MARK: - Life Cycle 10 | 11 | override func createComponentSystems() -> [GKComponent.Type] { 12 | // Customize. Each component must be listed after the components it depends on (as per its `requiredComponents` property.) 13 | // See OKScene.createComponentSystems() for the default set of commonly-used systems. 14 | super.createComponentSystems() 15 | } 16 | 17 | override func createContents() { 18 | // Customize: This is where you construct entities to add to your scene. 19 | 20 | // Access these shared components from child entities with `RelayComponent(for:)` 21 | self.entity?.addComponents([sharedMouseOrTouchEventComponent, 22 | sharedPointerEventComponent]) 23 | 24 | self.anchorPoint = .half // This places nodes with a position of (0,0) at the center of the scene. 25 | 26 | addEntity(OKEntity(name: "", components: [ 27 | // Customize 28 | ])) 29 | } 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKPhysicsContact+OctopusKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKPhysicsContact+OctopusKit.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/27. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | public extension SKPhysicsContact { 12 | 13 | // TODO: Tests 14 | 15 | /// Returns an array of the bodies whose `categoryBitMask` contains all of the specified flags. The returned bodies may have more flags besides the search criteria. If neither body has the specified categories, an empty array is returned. 16 | @inlinable 17 | final func bodiesMatchingCategories(_ categories: PhysicsCategories) -> [SKPhysicsBody] { 18 | 19 | var matchingBodies: [SKPhysicsBody] = [] 20 | 21 | // Loops may not be as efficient as just checking twice :P 22 | 23 | if PhysicsCategories(self.bodyA.categoryBitMask).contains(categories) { 24 | matchingBodies.append(bodyA) 25 | } 26 | 27 | if PhysicsCategories(self.bodyB.categoryBitMask).contains(categories) { 28 | matchingBodies.append(bodyB) 29 | } 30 | 31 | return matchingBodies 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKNode+OctopusAnimations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKNode+OctopusAnimations.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/11/09. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | extension SKNode { 12 | 13 | /// Cycles the node's `isHidden` state between `true` and `false` for the specified number of times, optionally removing the node from its parent at the end. 14 | @inlinable 15 | public func blink(times: Int, 16 | withDelay delay: TimeInterval = 0.1, 17 | removeFromParentOnCompletion: Bool = false) 18 | { 19 | guard times > 0 else { return } 20 | 21 | let blink = SKAction.repeat( 22 | SKAction.blink(withDelay: delay), 23 | count: times) 24 | 25 | var sequence: SKAction 26 | 27 | if removeFromParentOnCompletion { 28 | sequence = SKAction.sequence([ 29 | blink, 30 | .removeFromParent()]) 31 | 32 | } else { 33 | sequence = blink 34 | } 35 | 36 | self.run(sequence, withKey: SKAction.OKAnimationKeys.blink) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/UIKit/UIGestureRecognizer+OctopusKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIGestureRecognizer+OctopusKit.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/04/19. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | #if canImport(UIKit) 10 | 11 | import UIKit 12 | 13 | public extension UIGestureRecognizer { 14 | 15 | /// Specifies whether the recognizer is currently processing a gesture. 16 | /// 17 | /// Returns `true` when the gesture recognizer's `state` is `.began` or `.changed`. 18 | /// 19 | /// Returns `false` when the `state` is `.possible`, `.cancelled`, `.failed` or `.ended`. 20 | /// 21 | /// Use this flag to avoid unnecessary processing in gesture-controlled objects. 22 | @inlinable 23 | var isHandlingGesture: Bool { 24 | switch self.state { 25 | // CHECK: Is this all the correct states? 26 | 27 | case .began, .changed: // CHECK: Should this include `.ended?` 28 | return true 29 | 30 | case .possible, .cancelled, .failed, .ended: 31 | return false 32 | 33 | @unknown default: 34 | return false // CHECK: Is this the correct way to handle this, or 'fatalError()'? 35 | } 36 | } 37 | } 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit Entity State.xctemplate/___FILEBASENAME___.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SpriteKit 4 | import GameplayKit 5 | import OctopusKit 6 | 7 | final class ___FILEBASENAMEASIDENTIFIER___: OKEntityState { 8 | 9 | override init(entity: OKEntity) { 10 | super.init(entity: entity) 11 | 12 | self.componentsToAddOnEntry = [] // Customize 13 | 14 | self.componentTypesToRemoveOnExit = [] // Customize 15 | } 16 | 17 | required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } 18 | 19 | override func didEnter(from previousState: GKState?) { 20 | super.didEnter(from: previousState) 21 | 22 | switch previousState { 23 | 24 | case is OKEntityState: // Customize 25 | break 26 | 27 | default: break 28 | } 29 | } 30 | 31 | override func willExit(to nextState: GKState) { 32 | super.willExit(to: nextState) 33 | 34 | switch nextState { 35 | 36 | case is OKEntityState: // Customize 37 | break 38 | 39 | default: break 40 | } 41 | } 42 | 43 | override func isValidNextState(_ stateClass: AnyClass) -> Bool { 44 | return stateClass is OKEntityState.Type // Customize 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Support & Utility/Protocols/RequiresUpdatesPerFrame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequiresUpdatesPerFrame.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/28. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | public typealias OctopusUpdatableComponent = RequiresUpdatesPerFrame 12 | public typealias OKUpdatableComponent = RequiresUpdatesPerFrame 13 | 14 | /// A protocol for objects that can be updated every frame. 15 | public protocol UpdatablePerFrame { 16 | func update(deltaTime seconds: TimeInterval) 17 | } 18 | 19 | /// A protocol for components that must be updated every frame to correctly perform their functions. 20 | /// 21 | /// The component must be updated every frame during the scene's `update(_:)` method, by directly calling the component's `update(deltaTime:)` method, updating the component's entity, or updating the component system which this component is registered with. 22 | /// 23 | /// When a component with this protocol is added to a scene but the scene does not the relevant component system, a warning is logged to help reduce bugs and incorrect behaviors that result from missing systems. 24 | public protocol RequiresUpdatesPerFrame: UpdatablePerFrame { 25 | // func update(deltaTime seconds: TimeInterval) 26 | } 27 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Support & Utility/OKUtility+Graphics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKUtility+Graphics.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2014-10-09 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import OctopusCore 10 | import SpriteKit 11 | 12 | // MARK: - SpriteKit Utilities 13 | 14 | public extension OKUtility { 15 | 16 | static func loadFramesFromAtlas(named atlasName: String) -> [SKTexture] { 17 | // CREDIT: Apple Adventure Sample 18 | let atlas = SKTextureAtlas(named: atlasName) 19 | return (atlas.textureNames ).sorted().map { atlas.textureNamed($0) } 20 | } 21 | 22 | static func runOneShotEmitter(emitter: SKEmitterNode, withDuration duration: CGFloat) { 23 | // CREDIT: Apple Adventure Sample 24 | let waitAction = SKAction.wait(forDuration: TimeInterval(duration)) 25 | let birthRateSet = SKAction.run { emitter.particleBirthRate = 0.0 } 26 | let waitAction2 = SKAction.wait(forDuration: TimeInterval(emitter.particleLifetime + emitter.particleLifetimeRange)) 27 | let removeAction = SKAction.removeFromParent() 28 | 29 | let sequence = [waitAction, birthRateSet, waitAction2, removeAction] // Correction: var changed to let 30 | emitter.run(SKAction.sequence(sequence)) 31 | } 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Input/Motion/MotionManagerComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotionManagerComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/10/13. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | #if os(iOS) // CHECK: Include tvOS? 12 | 13 | import CoreMotion 14 | 15 | /// Retains a reference to a `CMMotionManager` to be used by other components. 16 | public final class MotionManagerComponent: OKComponent { 17 | 18 | public var motionManager: CMMotionManager? // CHECK: Should this be weak? 19 | 20 | /// If the `motionManager` argument is `nil` then `OctopusKit.motionManager` will be used. 21 | /// 22 | /// - Important: As per Apple documentation: An app should create only a single instance of the `CMMotionManager` class, as multiple instances of this class can affect the rate at which data is received from the accelerometer and gyroscope. 23 | public init(motionManager: CMMotionManager? = nil) { 24 | self.motionManager = motionManager ?? OctopusKit.motionManager 25 | super.init() 26 | } 27 | 28 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 29 | 30 | } 31 | 32 | #else 33 | 34 | public final class MotionManagerComponent: iOSExclusiveComponent {} 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKNodeWithShader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKNodeWithShader.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/10/24. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | /// A protocol for SpriteKit node types that have a `shader` and related properties. 12 | /// 13 | /// This allows different `SKNode` subclasses to be handled together when processing shaders. 14 | public protocol SKNodeWithShader: SKNode { // where Self: SKNode { // ⚠️ Crashes. 15 | 16 | // TODO: CHECK: Change protocol name to an adjective? 17 | 18 | // Applying Shaders to a Sprite: https://developer.apple.com/documentation/spritekit/skspritenode/applying_shaders_to_a_sprite 19 | 20 | var shader: SKShader? { get set } 21 | var attributeValues: [String : SKAttributeValue] { get set } 22 | 23 | func setValue(_: SKAttributeValue, forAttribute: String) 24 | func value(forAttributeNamed: String) -> SKAttributeValue? 25 | } 26 | 27 | // NOTE: `public' modifier cannot be used with extensions that declare protocol conformances :) 28 | 29 | extension SKEffectNode: SKNodeWithShader {} 30 | extension SKSpriteNode: SKNodeWithShader {} 31 | extension SKTileMapNode: SKNodeWithShader {} 32 | 33 | // extension SKScene: SKNodeWithShader {} // Unnecessary because SKScene inherits from SKEffectNode 34 | -------------------------------------------------------------------------------- /Templates/Game/Shared/Game States & Scenes/Main Menu/MainMenuUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainMenuUI.swift 3 | // OctopusKit Project Template 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SwiftUI 10 | import OctopusKit 11 | // import OctopusUI 12 | 13 | struct MainMenuUI: View { 14 | 15 | @EnvironmentObject var gameCoordinator: GameCoordinator 16 | 17 | var body: some View { 18 | VStack { 19 | 20 | title 21 | 22 | Spacer() 23 | 24 | menu.font(.title) 25 | 26 | Spacer() 27 | } 28 | } 29 | 30 | var title: some View { 31 | Text("\(OctopusKit.shared.appName)") 32 | .fontWeight(.bold) 33 | .font(.largeTitle) 34 | .foregroundColor(.primary) 35 | } 36 | 37 | var menu: some View { 38 | VStack { 39 | Button(action: { self.gameCoordinator.enter(PlayState.self) }) { 40 | Text("START") 41 | .fontWeight(.bold) 42 | .foregroundColor(.green) 43 | } 44 | // .buttonStyle(FatButtonStyle()) 45 | } 46 | } 47 | 48 | } 49 | 50 | struct MainMenuUI_Previews: PreviewProvider { 51 | static var previews: some View { 52 | MainMenuUI() 53 | .environmentObject(GameCoordinator()) 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Core/Base/OKStateMachine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKStateMachine.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/01. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | /// A finite-state machine. 12 | /// 13 | /// Adds convenience features to `GKStateMachine`, such as remembering the previous state. 14 | open class OKStateMachine: GKStateMachine { 15 | 16 | /// The previous state, if any. Set upon a successful state transition when `enter(_:)` is called. 17 | public private(set) weak var previousState: GKState? // CHECK: Should this be `weak`? 18 | 19 | @inlinable 20 | public var previousStateClass: GKState.Type? { 21 | if let previousState = self.previousState { 22 | return type(of: previousState) 23 | } else { 24 | return nil 25 | } 26 | } 27 | 28 | /// Attempts to transition the state machine from its current state to a state of the specified class, and sets the `previousState` property if the transition was successful. 29 | open override func enter(_ stateClass: AnyClass) -> Bool { 30 | let previousState = self.currentState 31 | let didEnter = super.enter(stateClass) 32 | 33 | if didEnter, 34 | previousState != self.currentState { 35 | self.previousState = previousState 36 | } 37 | 38 | return didEnter 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKNodeWithBlendMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKNodeWithBlendMode.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/05. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | /// A protocol for nodes that have a `blendMode` property. 12 | /// 13 | /// This allows different `SKNode` subclasses to be handled together when processing blending modes. 14 | public protocol SKNodeWithBlendMode: AnyObject { // where Self: SKNode { // ⚠️ Crashes. 15 | // TODO: Change name to an adjective? 16 | 17 | // Blending a Sprite with Different Interpretations of Alpha: https://developer.apple.com/documentation/spritekit/skspritenode/blending_a_sprite_with_different_interpretations_of_alpha 18 | 19 | var blendMode: SKBlendMode { get set } 20 | } 21 | 22 | extension SKEffectNode: SKNodeWithBlendMode {} 23 | extension SKSpriteNode: SKNodeWithBlendMode {} 24 | extension SKLabelNode: SKNodeWithBlendMode {} 25 | extension SKShapeNode: SKNodeWithBlendMode {} 26 | extension SKTileMapNode: SKNodeWithBlendMode {} 27 | 28 | extension SKNodeWithBlendMode { 29 | 30 | // MARK: - Modifiers 31 | // As in SwiftUI. 32 | 33 | /// Returns this node after setting its blending mode. 34 | @inlinable 35 | public func blendMode(_ blendMode: SKBlendMode) -> Self { 36 | self.blendMode = blendMode 37 | return self 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Gameplay/Agents & Goals/WanderingGoalComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WanderingGoalComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/03/31. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // TODO: Add stopping behavior? 10 | 11 | import GameplayKit 12 | 13 | /// Sets a `GKGoal` on the entity's `AgentComponent` component to wander around. 14 | /// 15 | /// **Dependencies:** `AgentComponent` 16 | public final class WanderingGoalComponent: AgentGoalComponent { 17 | 18 | /// The forward speed for the agent to maintain while turning at random. 19 | /// 20 | /// When this value is modified, a new goal is created with the new speed. 21 | public var speedToMaintain: Float { 22 | didSet { 23 | if speedToMaintain != oldValue { // Avoid redundancy 24 | recreateAndReapplyGoal() 25 | } 26 | } 27 | } 28 | 29 | public init(speedToMaintain: Float = 5.0, 30 | goalWeight: Float = 1.0, 31 | isPaused: Bool = false) 32 | { 33 | self.speedToMaintain = speedToMaintain 34 | super.init(goalWeight: goalWeight, isPaused: isPaused) 35 | } 36 | 37 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 38 | 39 | public override func createGoal() -> GKGoal? { 40 | return GKGoal(toWander: speedToMaintain) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Input/Motion/MotionControlledGravityComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotionControlledGravityComponent 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/10/24. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | #if os(iOS) 12 | 13 | /// Modifies the `gravity` of a scene's `PhysicsWorldComponent` based on the input from a `MotionManagerComponent`. 14 | /// 15 | /// **Dependencies:** `MotionManagerComponent`, `PhysicsWorldComponent` 16 | public class MotionControlledGravityComponent: OKComponent, RequiresUpdatesPerFrame { 17 | 18 | public override var requiredComponents: [GKComponent.Type]? { 19 | [PhysicsWorldComponent.self, 20 | MotionManagerComponent.self] 21 | } 22 | 23 | public override func update(deltaTime seconds: TimeInterval) { 24 | guard 25 | let physicsWorld = coComponent(PhysicsWorldComponent.self)?.physicsWorld, 26 | let motionManagerComponent = coComponent(MotionManagerComponent.self) 27 | else { return } 28 | 29 | if let motion = motionManagerComponent.motionManager?.deviceMotion { 30 | let vector = CGVector(dx: CGFloat(motion.gravity.x), dy: CGFloat(motion.gravity.y)) 31 | physicsWorld.gravity = vector 32 | } 33 | } 34 | } 35 | 36 | #else 37 | 38 | public final class MotionControlledGravityComponent: iOSExclusiveComponent {} 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /THANKS.md: -------------------------------------------------------------------------------- 1 | # OctopusKit Contributors & Supporters 2 | 3 | Awesome people, past & present. If someone is missing, or if you wish your name to not be included here, please drop a message or open an issue on the [repository]. 4 | 5 | ## Contributors ⌨️ 6 | 7 | * [Pradeep Roark](https://github.com/pradeeproark) 8 | 9 | ## [Patrons][patreon] ⭐️ 10 | 11 | * Cameron Garnham 12 | * Collin Bell 13 | * Dave Reed 14 | * Jim Reis 15 | * Rob Silverman 16 | * TheMadBug 17 | 18 | ## Resources 📖 19 | 20 | * Apple's Samples 21 | * [John Sundell](https://github.com/JohnSundell/SwiftTips) 22 | * [Paul Hudson (twostraws) – ShaderKit](https://github.com/twostraws/ShaderKit) 23 | * [Ray Wenderlich](https://www.raywenderlich.com) 24 | * [Swift With Majid](https://swiftwithmajid.com) 25 | * [SwiftUI Lab](https://swiftui-lab.com) 26 | * All the helpful people on Stack Overflow & the rest of the Stack Exchange Network. 27 | 28 | ## Personal Assistance & Inspiration 💕 29 | 30 | * All the indie devs & artists out there adding beauty to this world. 31 | * My aunts & uncles, who raised me better than any biological parents. *– ShinryakuTako* 32 | 33 | ---- 34 | 35 | [OctopusKit][repository] © 2021 [Invading Octopus][website] • [Apache License 2.0][license] 36 | 37 | [repository]: https://github.com/invadingoctopus/octopuskit 38 | [website]: https://invadingoctopus.io 39 | [license]: https://www.apache.org/licenses/LICENSE-2.0.html 40 | [twitter]: https://twitter.com/invadingoctopus 41 | [discord]: https://discord.gg/y3har7D 42 | [patreon]: https://www.patreon.com/invadingoctopus 43 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Abstract/DictionaryComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/04/18. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | /// A component for storing a dictionary of arbitrary types and values. May be used for sharing data between other components. 12 | /// 13 | /// - Note: If you need to share a lot of data or properties between components, consider writing a custom data component specific to your game. 14 | public final class DictionaryComponent: OKComponent { 15 | 16 | public var dictionary: [KeyType: ValueType] 17 | 18 | /// Sets or returns the value for `key` from the dictionary. 19 | public subscript(key: KeyType) -> ValueType? { 20 | get { return dictionary[key] } 21 | set { dictionary[key] = newValue } 22 | } 23 | 24 | /// Creates a `DictionaryComponent` with an empty (non-`nil`) dictionary. 25 | public override init() { 26 | self.dictionary = [:] 27 | super.init() 28 | } 29 | 30 | /// Creates a `DictionaryComponent` and initializes it with the specified dictionary. 31 | public init(_ dictionary: [KeyType: ValueType]) { 32 | self.dictionary = dictionary 33 | super.init() 34 | } 35 | 36 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Templates/Game/Shared/Components/UIViewModelComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewModelComponent.swift 3 | // OctopusKit Project Template 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/07/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import GameplayKit 11 | import OctopusKit 12 | 13 | final class UIViewModelComponent: OKComponent, 14 | RequiresUpdatesPerFrame, 15 | ObservableObject 16 | { 17 | 18 | @Published private(set) var playerScore: Int = 0 19 | @Published private(set) var playerRotation: CGFloat = 0 20 | 21 | var updateRateInFrames: Int = 10 // 6 times a second. 22 | private(set) var frameCount: Int = 0 23 | 24 | override var requiredComponents: [GKComponent.Type]? { 25 | [PlayerStatsComponent.self] 26 | } 27 | 28 | @inlinable 29 | override func update(deltaTime seconds: TimeInterval) { 30 | 31 | // TODO: PERFORMANCE: Replace with Combine-based reactive updates. 32 | 33 | frameCount += 1 34 | 35 | guard frameCount !< updateRateInFrames else { return } 36 | 37 | if let playerStatsComponent = coComponent(PlayerStatsComponent.self) { 38 | self.playerScore = playerStatsComponent.score 39 | } 40 | 41 | if let node = self.entity?.node { 42 | self.playerRotation = node.zRotation 43 | } 44 | 45 | frameCount = 0 46 | } 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Support & Utility/Enums/PhysicsMovementType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhysicsMovementType.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/8/4. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | /// Defines an option between forces (continuous) or impulses (instantaneous) for components that handle physics-based movement. 12 | /// 13 | /// - Note: See the SpriteKit documentation on **Making Physics Bodies Move** for details. 14 | public enum PhysicsMovementType { 15 | 16 | // 📖 https://developer.apple.com/documentation/spritekit/skphysicsbody/making_physics_bodies_move 17 | 18 | /// A continuous force to a physics body that must be re-applied on every frame update to maintain movement. 19 | /// 20 | /// A *force* is applied for a length of time based on the amount of simulation time that passes between when you apply the force and when the next frame of the simulation is processed. So, to apply a continuous force to an body, you need to make the appropriate method calls each time a new frame is processed. Forces are usually used for continuous effects. 21 | case force 22 | 23 | /// An instantaneous change to a physics body’s velocity, generally applied only once or in discrete steps. 24 | /// 25 | /// An *impulse* makes an instantaneous change to the body’s velocity that is independent of the amount of simulation time that has passed. Impulses are usually used for immediate changes to a body’s velocity. 26 | case impulse 27 | } 28 | -------------------------------------------------------------------------------- /QuickStart/Universal/2 - MyGameCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyGameCoordinator.swift 3 | // OctopusKitQuickStart 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/02/10. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // 🔶 STEP 2: The OKGameCoordinator is the master coordinator for an OctopusKit game. 10 | // 11 | // It lists all the valid states for your game and specifies the initial state. 12 | // 13 | // In the MVC hierarchy, it's a "controller" (as in "ViewController" etc.) 14 | // 15 | // You may also use the game coordinator to store objects or properties that must be shared across all states and scenes, such as the game world, player data and network connections etc. 16 | // 17 | // Each OKGameState has a scene and UI overlay associated with it; scenes present the content for each state. 18 | // 19 | // Creating a subclass of OKGameCoordinator is not necessary for basic OctopusKit projects so you may simply use OKGameCoordinator(states:initialStateClass:) 20 | // 21 | // Complex games may require custom coordinators to manage different kinds of global state and external connections. 22 | 23 | import OctopusKit 24 | 25 | final class MyGameCoordinator: OKGameCoordinator { 26 | 27 | init() { 28 | super.init(states: [LogoState(), 29 | TitleState(), 30 | PlayState(), 31 | PausedState(), 32 | GameOverState()], 33 | initialStateClass: LogoState.self) 34 | } 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Graphics/ParticleEmitterComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticleEmitterComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/10/16. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | /// Adds a `SKEmitterNode` to the entity's `NodeComponent` node. 12 | /// 13 | /// **Dependencies:** `NodeComponent` 14 | public final class ParticleEmitterComponent: NodeAttachmentComponent { 15 | 16 | public override var requiredComponents: [GKComponent.Type]? { 17 | [NodeComponent.self] 18 | } 19 | 20 | public var emitterNode: SKEmitterNode 21 | 22 | public init(emitterNode: SKEmitterNode) { 23 | self.emitterNode = emitterNode 24 | super.init() 25 | self.attachment = self.emitterNode 26 | } 27 | 28 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 29 | 30 | public override func didAddToEntity(withNode node: SKNode) { 31 | super.didAddToEntity(withNode: node) 32 | 33 | // ℹ️ If the emitter's parent node has a parent, set the grandparent as the emitter's target node (but don't let the target node be set to `nil` in case it was already set to something). This ensures that this component will function correctly even if it's added to an `SKScene`, which has no parent. 34 | 35 | if let parent = node.parent { 36 | emitterNode.targetNode = parent 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Input/Motion/MotionControlledThrustComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotionControlledThrustComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/12/06. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // TODO: Other types of motion input sources (e.g. besides deviceMotion) 10 | 11 | import GameplayKit 12 | 13 | #if os(iOS) 14 | 15 | /// Controls the entity's `ThrustComponent` based on the motion data from a `MotionManagerComponent`. 16 | /// 17 | /// **Dependencies:** `MotionManagerComponent`, `ThrustComponent` 18 | public final class MotionControlledThrustComponent: OKComponent, RequiresUpdatesPerFrame { 19 | 20 | public override var requiredComponents: [GKComponent.Type]? { 21 | [MotionManagerComponent.self, 22 | ThrustComponent.self] 23 | } 24 | 25 | public override func update(deltaTime seconds: TimeInterval) { 26 | guard let motionManagerComponent = coComponent(MotionManagerComponent.self) else { return } 27 | 28 | // Tilt to move/thrust. 29 | 30 | if let motion = motionManagerComponent.motionManager?.deviceMotion, 31 | let thrustComponent = coComponent(ThrustComponent.self) 32 | { 33 | thrustComponent.thrustVector = CGVector(dx: CGFloat(motion.gravity.x), 34 | dy: CGFloat(motion.gravity.y)) 35 | } 36 | } 37 | } 38 | 39 | #else 40 | 41 | public final class MotionControlledThrustComponent: iOSExclusiveComponent {} 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /QuickStart/Universal/Components/GlobalDataComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalDataComponent.swift 3 | // OctopusKitQuickStart 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/07/27. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import GameplayKit 11 | import OctopusCore 12 | import OctopusKit 13 | 14 | /// A custom component for the QuickStart project that holds some simple data to be shared across multiple game states and scenes. 15 | final class GlobalDataComponent: OKComponent, RequiresUpdatesPerFrame, ObservableObject { 16 | 17 | public var secondsElapsed: TimeInterval = 0 18 | 19 | /// A more slowly-updated version of `secondsElapsed`. Should reduce strain on SwiftUI updates? :) 20 | public var secondsElapsedRounded: Int = 0 { 21 | willSet { 22 | // ℹ️ We don't use @Published here because that causes a SwiftUI update every frame, even when this value does not change between seconds. 23 | if newValue != secondsElapsedRounded { 24 | self.objectWillChange.send() 25 | } 26 | } 27 | } 28 | 29 | @Published 30 | public var emojiCount: Int = 0 { 31 | didSet { 32 | emojiHighScore = max(emojiCount, emojiHighScore) 33 | } 34 | } 35 | 36 | @OKUserDefault(key: "emojiHighScore", defaultValue: 50) public var emojiHighScore: Int 37 | 38 | override func update(deltaTime seconds: TimeInterval) { 39 | secondsElapsed += seconds 40 | secondsElapsedRounded = Int(secondsElapsed.rounded(.down)) 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Tests/OctopusKitTests/Apple API Extensions Tests/StringTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringTests.swift 3 | // OctopusKitTests 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/11/12. 6 | // 7 | 8 | import XCTest 9 | @testable import OctopusKit 10 | 11 | class StringTests: XCTestCase { 12 | 13 | func testString() { 14 | 15 | var string: String 16 | 17 | let optionalInt: Int? = 1234 18 | let nilInt: Int? = nil 19 | 20 | // #1: Optional Interpolation should raise no warnings 21 | // like "String interpolation produces a debug description for an optional value; did you mean to make this explicit?" 22 | // and the named argument version and the unlabeled argument version should be the same. 23 | 24 | string = "\(optionalInt)" 25 | print (string) 26 | XCTAssertEqual (string, "1234") 27 | XCTAssertEqual (string, "\(optional: optionalInt)") 28 | 29 | string = "\(nilInt)" 30 | print (string) 31 | XCTAssertEqual (string, "nil") 32 | XCTAssertEqual (string, "\(optional: nilInt)") 33 | 34 | // #2: We should be able to get the default behavior back. 35 | 36 | string = String (describing: optionalInt) 37 | print (string) 38 | XCTAssertEqual (string, "Optional(1234)") 39 | 40 | string = String (describing: nilInt) 41 | print (string) 42 | XCTAssertEqual (string, "nil") 43 | 44 | } 45 | 46 | static var allTests = [ 47 | ("Test String", testString) 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKShapeNode+OctopusKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKShapeNode+OctopusKit.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/06. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | public extension SKShapeNode { 12 | 13 | // MARK: - Modifiers 14 | // As in SwiftUI. 15 | // DESIGN: No `@discardableResult` because they *should* raise a warning when used in place of more-efficient direct properties (i.e. most modifiers should only be used when creating an object as an argument for another initializer). 16 | 17 | /// Returns this shape after setting the color of its fill. 18 | @inlinable 19 | final func fillColor(_ color: SKColor) -> Self { 20 | self.fillColor = color 21 | return self 22 | } 23 | 24 | /// Returns this shape after setting the width of its glow. 25 | @inlinable 26 | final func glowWidth(_ width: CGFloat) -> Self { 27 | self.glowWidth = width 28 | return self 29 | } 30 | 31 | /// Returns this shape after setting the width of its stroke. A width larger than `2.0` may cause rendering artifacts. Default: `1.0` 32 | @inlinable 33 | final func lineWidth(_ width: CGFloat) -> Self { 34 | self.lineWidth = width 35 | return self 36 | } 37 | 38 | /// Returns this shape after setting the color of its stroke. 39 | @inlinable 40 | final func strokeColor(_ color: SKColor) -> Self { 41 | self.strokeColor = color 42 | return self 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Core/Base/OKScene+Keyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKScene+Keyboard.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/11/17. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | #if canImport(AppKit) 10 | 11 | import AppKit 12 | 13 | extension OKScene: KeyboardEventProvider { 14 | 15 | // TODO: Eliminate code duplication between OKScene+Keyboard and OKSubscene+Keyboard 16 | 17 | // MARK: - Player Input (macOS Keyboard) 18 | 19 | /// Relays keyboard-input events to the scene's `KeyboardEventComponent`. 20 | open override func keyDown(with event: NSEvent) { 21 | #if LOGINPUTEVENTS 22 | debugLog() 23 | #endif 24 | 25 | self.entity?[KeyboardEventComponent.self]?.keyDown = KeyboardEventComponent.KeyboardEvent(event: event, node: self) 26 | } 27 | 28 | /// Relays keyboard-input events to the scene's `KeyboardEventComponent`. 29 | open override func keyUp(with event: NSEvent) { 30 | #if LOGINPUTEVENTS 31 | debugLog() 32 | #endif 33 | 34 | self.entity?[KeyboardEventComponent.self]?.keyUp = KeyboardEventComponent.KeyboardEvent(event: event, node: self) 35 | } 36 | 37 | /// Relays keyboard-input events to the scene's `KeyboardEventComponent`. 38 | open override func flagsChanged(with event: NSEvent) { 39 | #if LOGINPUTEVENTS 40 | debugLog() 41 | #endif 42 | 43 | self.entity?[KeyboardEventComponent.self]?.flagsChanged = KeyboardEventComponent.KeyboardEvent(event: event, node: self) 44 | } 45 | 46 | } 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Input/VibrationComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VibrationComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/10/28. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | #if os(iOS) 12 | 13 | /// Abstract. Use `ImpactVibrationComponent`. 14 | open class VibrationComponent: OKComponent, RequiresUpdatesPerFrame { 15 | 16 | public var feedbackGenerator: FeedbackGeneratorType? // CHECK: Should this be weak? 17 | 18 | public var shouldVibrateOnNextFrame = false 19 | private var vibrateOnce = false 20 | 21 | open override func update(deltaTime seconds: TimeInterval) { 22 | 23 | if vibrateOnce { 24 | vibrate() 25 | vibrateOnce = false 26 | shouldVibrateOnNextFrame = false 27 | } 28 | 29 | if shouldVibrateOnNextFrame { 30 | 31 | createGenerator() 32 | 33 | if self.feedbackGenerator != nil { 34 | vibrateOnce = true 35 | } 36 | 37 | shouldVibrateOnNextFrame = false 38 | 39 | } else { 40 | feedbackGenerator = nil 41 | } 42 | 43 | } 44 | 45 | /// Abstract; must be implemented by subclass. 46 | open func createGenerator() {} 47 | 48 | /// Abstract; must be implemented by subclass. 49 | open func vibrate() {} 50 | } 51 | 52 | #endif 53 | 54 | #if !os(iOS) 55 | public final class VibrationComponent: iOSExclusiveComponent {} 56 | #endif 57 | -------------------------------------------------------------------------------- /QuickStart/Universal/1 - OKQuickStartView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKQuickStartView.swift 3 | // OctopusKitQuickStart 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/10/16. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // 🔶 STEP 1: A SwiftUI view which displays the OctopusKit QuickStart "game". 10 | // 11 | // Add this view in the `body` property of your SwiftUI project's `ContentView.swift` file. 12 | 13 | import SwiftUI 14 | import OctopusKit 15 | import Combine 16 | 17 | typealias OctopusKitQuickStartView = OKQuickStartView // In case you prefer the longer prefix :) 18 | 19 | struct OKQuickStartView: View { 20 | 21 | var body: some View { 22 | 23 | #if os(iOS) 24 | 25 | return OKContainerView() 26 | .environmentObject(MyGameCoordinator()) 27 | .edgesIgnoringSafeArea(.all) 28 | .statusBar(hidden: true) 29 | 30 | #elseif os(macOS) 31 | 32 | return OKContainerView() 33 | .environmentObject(MyGameCoordinator()) 34 | .frame(width: 375, height: 812) 35 | .fixedSize() 36 | 37 | #elseif os(tvOS) 38 | 39 | return OKContainerView() 40 | .environmentObject(MyGameCoordinator()) 41 | .edgesIgnoringSafeArea(.all) 42 | 43 | #endif 44 | 45 | } 46 | 47 | } 48 | 49 | struct OKQuickStartView_Previews: PreviewProvider { 50 | static var previews: some View { 51 | Text("See the TitleUI preview.") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Core/Base/OKSubscene+Keyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKSubscene+Keyboard.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/11/18. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | #if canImport(AppKit) 10 | 11 | import AppKit 12 | 13 | extension OKSubscene: KeyboardEventProvider { 14 | 15 | // TODO: Eliminate code duplication between OKScene+Keyboard and OKSubscene+Keyboard 16 | 17 | // MARK: - Player Input (macOS Keyboard) 18 | 19 | /// Relays keyboard-input events to the subscene's `KeyboardEventComponent`. 20 | open override func keyDown(with event: NSEvent) { 21 | #if LOGINPUTEVENTS 22 | debugLog() 23 | #endif 24 | 25 | self.entity?[KeyboardEventComponent.self]?.keyDown = KeyboardEventComponent.KeyboardEvent(event: event, node: self) 26 | } 27 | 28 | /// Relays keyboard-input events to the subscene's `KeyboardEventComponent`. 29 | open override func keyUp(with event: NSEvent) { 30 | #if LOGINPUTEVENTS 31 | debugLog() 32 | #endif 33 | 34 | self.entity?[KeyboardEventComponent.self]?.keyUp = KeyboardEventComponent.KeyboardEvent(event: event, node: self) 35 | } 36 | 37 | /// Relays keyboard-input events to the subscene's `KeyboardEventComponent`. 38 | open override func flagsChanged(with event: NSEvent) { 39 | #if LOGINPUTEVENTS 40 | debugLog() 41 | #endif 42 | 43 | self.entity?[KeyboardEventComponent.self]?.flagsChanged = KeyboardEventComponent.KeyboardEvent(event: event, node: self) 44 | } 45 | 46 | } 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /QuickStart/Universal/Game States/5 - TitleState/5A - TitleState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleState.swift 3 | // OctopusKitQuickStart 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/02/10. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // 🔶 STEP 5A: The title state displays the title scene for the QuickStart project, then segues into the PlayState. 10 | 11 | import GameplayKit 12 | import OctopusKit 13 | import SwiftUI 14 | 15 | final class TitleState: OKGameState { 16 | 17 | init() { 18 | 19 | // 🔶 STEP 5A.1: Associates a scene and SwiftUI overlay with this state. 20 | // 21 | // The "scene" displays the SpriteKit/SceneKit/Metal content, and the SwiftUI view is overlaid on top of the gameplay. 22 | // 23 | // This hybridization lets you use an Entity-Component-System architecture for your gameplay, with a convenient declarative syntax for your user interface. 24 | 25 | super.init(associatedSceneClass: TitleScene.self, 26 | associatedSwiftUIView: TitleUI()) 27 | } 28 | 29 | @discardableResult override func octopusSceneDidChooseNextGameState(_ scene: OKScene) -> Bool { 30 | 31 | // 🔶 STEP 5A.2: This method will be called by the TitleScene when the "Cycle Game States" button is tapped. 32 | 33 | return stateMachine?.enter(PlayState.self) ?? false 34 | } 35 | 36 | override var validNextStates: [OKState.Type] { 37 | 38 | // 🔶 STEP 5A.3: This property lists all the valid states which this state is allowed to transition to. 39 | 40 | [PlayState.self] 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Graphics/SceneComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/10/28. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // CHECK: Should this component be just replaced with `NodeComponent`? 10 | 11 | import OctopusCore 12 | import GameplayKit 13 | 14 | public typealias SpriteKitSceneComponent = SceneComponent 15 | 16 | /// An abstraction layer for accessing SpriteKit scene features via a component. This component should only be added to an `SKScene.entity` or `OKScene.entity` and is used to identify the entity as a scene to other components. 17 | public final class SceneComponent: OKComponent { 18 | 19 | public let scene: OKScene 20 | 21 | public init(scene: OKScene) { 22 | self.scene = scene 23 | super.init() 24 | } 25 | 26 | public override func didAddToEntity(withNode node: SKNode) { 27 | super.didAddToEntity(withNode: node) 28 | 29 | // Remove ourselves if our node is not a scene 30 | // ⚠️ NOTE: This does not prevent this component from being added to entities WITHOUT an `NodeComponent`/`GKSKNodeComponent` 31 | 32 | guard node is SKScene || node is OKScene // CHECK: Is checking for subclass redundant? 33 | else { 34 | OKLog.errors.debug("\(📜("\(node) is not a scene — Detaching from entity"))") 35 | self.removeFromEntity() 36 | return 37 | } 38 | } 39 | 40 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Templates/Xcode/OctopusKit UI Component.xctemplate/___FILEBASENAME___.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SpriteKit 4 | import GameplayKit 5 | import OctopusKit 6 | import SwiftUI 7 | 8 | /// A component with an associated SwiftUI view. 9 | final class ___FILEBASENAMEASIDENTIFIER___: OKComponent, UpdatedPerFrame, ObservableObject { 10 | 11 | override var requiredComponents: [GKComponent.Type]? { 12 | [] 13 | } 14 | 15 | // MARK: Observed Properties 16 | 17 | @Published var frame: Int = 0 // CUSTOMIZE 18 | 19 | // MARK: Life Cycle 20 | 21 | override func didAddToEntity(withNode node: SKNode) { 22 | 23 | } 24 | 25 | override func update(deltaTime seconds: TimeInterval) { 26 | self.frame += 1 // CUSTOMIZE 27 | } 28 | 29 | override func willRemoveFromEntity(withNode node: SKNode) { 30 | 31 | } 32 | 33 | // MARK: - UI 34 | 35 | /// Creates and returns a copy of this component's associated SwiftUI view, with this instance of the component as the view's observed property. 36 | var ui: ___FILEBASENAMEASIDENTIFIER___.UI { 37 | // NOTE: Returning a singleton may not make a difference in performance, as SwiftUI Views are value types (that would be copied anyway). 38 | ___FILEBASENAMEASIDENTIFIER___.UI(component: self) 39 | } 40 | 41 | /// A SwiftUI view that displays UI based on the data of a ___FILEBASENAMEASIDENTIFIER___. 42 | struct UI: View { 43 | 44 | @ObservedObject var component: ___FILEBASENAMEASIDENTIFIER___ 45 | 46 | var body: some View { 47 | Text("\(component.frame)") // CUSTOMIZE 48 | } 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /QuickStart/Universal/Game States/7 - PausedState/7 - PausedState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PausedState.swift 3 | // OctopusKitQuickStart 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/02/10. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // 🔶 STEP 7: The paused state for the QuickStart project. 10 | // 11 | // This state does not provide a scene or UI, as it is represented by the PlayScene (which also displays the content for the PlayState and GameOverState.) 12 | 13 | import GameplayKit 14 | import OctopusKit 15 | import SwiftUI 16 | 17 | final class PausedState: OKGameState { 18 | 19 | init() { 20 | 21 | // 🔶 STEP 7.1: Associates a scene and UI with this state. 22 | // The PlayScene and PlayUI are also associated with the PlayState and GamerOverState. 23 | 24 | super.init(associatedSceneClass: PlayScene.self, 25 | associatedSwiftUIView: PlayUI()) 26 | } 27 | 28 | @discardableResult override func octopusSceneDidChooseNextGameState(_ scene: OKScene) -> Bool { 29 | 30 | // 🔶 STEP 7.2: This method will be called by the PlayScene when the "Cycle Game States" button is tapped. 31 | 32 | return stateMachine?.enter(GameOverState.self) ?? false 33 | } 34 | 35 | override var validNextStates: [OKState.Type] { 36 | 37 | // 🔶 STEP 7.3: This property lists all the valid states which this state is allowed to transition to. 38 | // 39 | // The PausedState can lead to the PlayState, the GameOverState or the TitleState. 40 | 41 | [PlayState.self, GameOverState.self, TitleState.self] 42 | } 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /QuickStart/Universal/Game States/8 - GameOverState/8 - GameOverState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameOverState.swift 3 | // OctopusKitQuickStart 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/02/10. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // 🔶 STEP 8: The "game over" state for the QuickStart project. 10 | // 11 | // This state does not provide a scene or UI, as it is represented by the PlayScene (which also displays the content for the PlayState and GameOverState.) 12 | // 13 | // Cycles back to STEP 6 (PlayState). 14 | 15 | import GameplayKit 16 | import OctopusKit 17 | import SwiftUI 18 | 19 | final class GameOverState: OKGameState { 20 | 21 | init() { 22 | 23 | // 🔶 STEP 8.1: Associates a scene and UI with this state. 24 | // The PlayScene and PlayUI are also associated with the PlayState and PausedState. 25 | 26 | super.init(associatedSceneClass: PlayScene.self, 27 | associatedSwiftUIView: PlayUI()) 28 | } 29 | 30 | @discardableResult override func octopusSceneDidChooseNextGameState(_ scene: OKScene) -> Bool { 31 | 32 | // 🔶 STEP 8.2: This method will be called by the PlayScene when the "Cycle Game States" button is tapped. 33 | 34 | return stateMachine?.enter(TitleState.self) ?? false 35 | } 36 | 37 | override var validNextStates: [OKState.Type] { 38 | 39 | // 🔶 STEP 8.3: This property lists all the valid states which this state is allowed to transition to. 40 | // 41 | // The GameOverState can cycle back to either the TitleState or the PlayState. 42 | 43 | [TitleState.self, PlayState.self] 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Abstract/ValueComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValueComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/28. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | /// A base class for components that represent a single value and reports its changes for subclasses to act upon. 12 | /// 13 | /// For example, a subclass may call a `BubbleEmitterComponent` to display changes in the value. 14 | open class ValueComponent : OKComponent 15 | where ValueType: Comparable 16 | { 17 | 18 | // CHECK: Should there be a `willChange(to:)` or would that just be unnecessary and reduce performance? 19 | 20 | /// The value represented by this component. `didChange(from:)` is called when this property changes to a different value. 21 | open var value: ValueType { 22 | didSet { 23 | if value != oldValue { 24 | 25 | #if LOGCHANGES 26 | debugLog("\(oldValue) → \(value)", topic: "\(self)") 27 | #endif 28 | 29 | self.didChange(from: oldValue) 30 | } 31 | } 32 | } 33 | 34 | public init(initial: ValueType) 35 | { 36 | self.value = initial 37 | super.init() 38 | } 39 | 40 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 41 | 42 | // MARK: - Abstract 43 | 44 | /// Abstract; Subclasses may implement this method to respond to changes. Called by the `value` property's `didSet` observer. 45 | @inlinable 46 | open func didChange(from oldValue: ValueType) {} 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Input/Touch Gestures/ClickGestureRecognizerComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClickGestureRecognizerComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/12/22. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import GameplayKit 11 | 12 | #if canImport(AppKit) 13 | 14 | /// Creates a `NSClickGestureRecognizer` and attaches it to the `SceneComponent` `SKView` when this component is added to the scene entity. 15 | /// 16 | /// - Important: The player must click the specified mouse button the required number of times without dragging the mouse for the gesture to be recognized. 17 | /// 18 | /// **Dependencies:** `SceneComponent` 19 | @available(macOS 10.15, *) 20 | public final class ClickGestureRecognizerComponent: OKGestureRecognizerComponent { 21 | 22 | // https://developer.apple.com/documentation/appkit/nsclickgesturerecognizer 23 | 24 | public init(numberOfClicksRequired: Int = 1, 25 | numberOfTouchesRequired: Int = 1, 26 | buttonMask: Int = 0x1) 27 | { 28 | super.init() 29 | self.gestureRecognizer.numberOfClicksRequired = numberOfClicksRequired 30 | self.gestureRecognizer.numberOfTouchesRequired = numberOfTouchesRequired 31 | self.gestureRecognizer.buttonMask = buttonMask 32 | } 33 | 34 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 35 | } 36 | 37 | #endif 38 | 39 | #if os(iOS) 40 | @available(iOS, unavailable, message: "Use TapGestureRecognizerComponent") 41 | public final class ClickGestureRecognizerComponent: macOSExclusiveComponent {} 42 | #endif 43 | -------------------------------------------------------------------------------- /Tests/OctopusKitTests/Core Tests/OctopusKitLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OctopusKitLaunchTests.swift 3 | // OctopusKitTests 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/10/30. 6 | // 7 | 8 | import XCTest 9 | @testable import OctopusKit 10 | 11 | final class OctopusKitLaunchTests: XCTestCase { 12 | 13 | func testLaunch() { 14 | 15 | // 1: `verifyConfiguration` should fail if there's nothing configured. 16 | 17 | XCTAssertThrowsError(try OctopusKit.verifyConfiguration()) 18 | 19 | // 2: `OKViewController` should not initialize without an `OKGameCoordinator`. 20 | 21 | XCTAssertThrowsError(try OKViewController()) 22 | 23 | // 3: OctopusKit should be initialized more than once. 24 | 25 | let gameCoordinator = OKGameCoordinator(states: [OKGameState()]) 26 | 27 | XCTAssertNoThrow(try OctopusKit(gameCoordinator: gameCoordinator)) 28 | 29 | XCTAssertThrowsError(try OctopusKit(gameCoordinator: gameCoordinator)) 30 | 31 | // 4: `OKViewController` should allow multiple instances. 32 | 33 | var viewController1, viewController2: OKViewController? 34 | 35 | XCTAssertNoThrow(viewController1 = try OKViewController()) 36 | XCTAssertNoThrow(viewController2 = try OKViewController()) 37 | 38 | XCTAssertEqual(viewController1!.gameCoordinator, viewController2!.gameCoordinator) 39 | 40 | // TODO: Test `OKViewControllerRepresentable` 41 | // let viewControllerRepresentable = OKViewControllerRepresentable() 42 | // XCTAssertThrowsError(viewControllerRepresentable.makeCoordinator()) 43 | 44 | } 45 | 46 | static var allTests = [ 47 | ("Test Launch Process", testLaunch) 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Core/Base/OKTurnBasedEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKTurnBasedEntity.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/04/07. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import Foundation 10 | 11 | /// A base class for entities in a turn-based game. 12 | open class OKTurnBasedEntity: OKEntity, TurnBased { 13 | 14 | /// Calls `beginTurn(delta:)` on each turn-based component. Use this to perform any tasks that must occur at the beginning of each turn, *before* `updateTurn(delta:)`, such as health regeneration effects. 15 | /// 16 | /// - Parameter turns: The number of turns passed since the previous update. 17 | open func beginTurn(delta turns: Int) { 18 | for case let turnBasedComponent as TurnBased in self.components { 19 | turnBasedComponent.beginTurn(delta: turns) 20 | } 21 | } 22 | 23 | /// Calls `updateTurn(delta:)` on each turn-based component. 24 | /// 25 | /// - Parameter turns: The number of turns passed since the previous update. 26 | open func updateTurn(delta turns: Int) { 27 | for case let turnBasedComponent as TurnBased in self.components { 28 | turnBasedComponent.updateTurn(delta: turns) 29 | } 30 | } 31 | 32 | /// Calls `endTurn(delta:)` on each turn-based component. Use this to perform any tasks that must occur at the end of each turn, *after* `updateTurn(delta:)`, such as damage-over-time effects. 33 | /// 34 | /// - Parameter turns: The number of turns passed since the previous update. 35 | open func endTurn(delta turns: Int) { 36 | for case let turnBasedComponent as TurnBased in self.components { 37 | turnBasedComponent.endTurn(delta: turns) 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKNodeWithSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKNodeWithSize.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/03/18. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | public typealias SKNodeWithDimensions = SKNodeWithSize 12 | 13 | /// A protocol for types that have `width` and `height` properties. 14 | /// 15 | /// This allows different `SKNode` subclasses to be handled together when processing width or height. 16 | public protocol SKNodeWithSize { // where Self: SKNode { // ⚠️ Crashes. 17 | // TODO: Change name to an adjective? 18 | var size: CGSize { get } 19 | } 20 | 21 | // NOTE: `public' modifier cannot be used with extensions that declare protocol conformances :) 22 | 23 | extension SKCameraNode: SKNodeWithSize { 24 | 25 | /// Returns the `size` of the parent (scene.) 26 | public var size: CGSize { 27 | // TODO: Verify and check compatibility with scaling etc. 28 | if let parent = self.parent as? SKNodeWithSize { 29 | return parent.size 30 | } else { 31 | return CGSize.zero 32 | } 33 | } 34 | } 35 | 36 | //extension SKEffectNode: SKNodeWithSize { // Includes SKScene 37 | /// TODO: Causing conflict with `SKScene.size` in Swift 5.3 2020-06-24 38 | // public var size: CGSize { 39 | // // CHECK: PERFORMANCE: Is this efficient? Necessary? 40 | // self.calculateAccumulatedFrame().size 41 | // } 42 | //} 43 | 44 | // extension SKScene: SKNodeWithSize {} // Included in SKEffectNode 45 | 46 | extension SKSpriteNode: SKNodeWithSize {} 47 | 48 | extension SKVideoNode: SKNodeWithSize {} 49 | 50 | extension SKTileMapNode: SKNodeWithSize { 51 | public var size: CGSize { self.mapSize } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Miscellaneous/Lab/Component.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Component.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/21. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import Foundation 10 | import GameplayKit 11 | 12 | #if UseNewProtocols // ℹ️ Not currently in use; This is mostly preparation for future independence from GameplayKit, if needed. 13 | 14 | public protocol Component: class, UpdatablePerFrame { 15 | 16 | // MARK: Properties 17 | 18 | var entity: Entity? { get } 19 | var requiredComponents: [Component.Type]? { get } 20 | 21 | var componentType: Component.Type { get } 22 | 23 | var shouldRemoveFromEntityOnDeinit: Bool { get } 24 | var shouldWarnIfDeinitWithoutRemoving: Bool { get } 25 | var disableDependencyChecks: Bool { get set } 26 | 27 | // MARK: Life Cycle 28 | 29 | // init() 30 | 31 | @discardableResult 32 | func disableDependencyChecks(_ newValue: Bool) -> Self 33 | 34 | func didAddToEntity() 35 | func didAddToEntity(withNode node: SKNode) 36 | 37 | func removeFromEntity() 38 | 39 | func willRemoveFromEntity() 40 | func willRemoveFromEntity(withNode node: SKNode) 41 | 42 | // MARK: Queries 43 | 44 | func coComponent ( 45 | ofType componentClass: ComponentType.Type, 46 | ignoreRelayComponents: Bool) 47 | -> ComponentType? where ComponentType: Component 48 | 49 | func coComponent ( 50 | _ componentClass: ComponentType.Type, 51 | ignoreRelayComponents: Bool) 52 | -> ComponentType? where ComponentType: Component 53 | 54 | @discardableResult 55 | func checkEntityForRequiredComponents() -> Bool 56 | } 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /QuickStart/Universal/Game States/5 - TitleState/5C - TitleUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleUI.swift 3 | // OctopusKitQuickStart 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/10/20. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // 🔶 STEP 5C: The user interface overlay for the title screen of the QuickStart project. 10 | // 11 | // This view displays a button which signals the game coordinator to enter the next playable game state. 12 | // 13 | // Once you understand how everything works, you may delete this file and replace it with your own UI. 14 | 15 | import SwiftUI 16 | import OctopusKit 17 | 18 | struct TitleUI: View { 19 | 20 | @EnvironmentObject var gameCoordinator: MyGameCoordinator 21 | 22 | var body: some View { 23 | 24 | VStack { 25 | Spacer() 26 | startButton 27 | } 28 | .tvOSExcluded { $0.padding(.bottom, 50) } 29 | .tvOS { $0.padding(.bottom, 100) } // BUG? APPLEBUG? This seems necessary to prevent the bottom edge of the button from stretching. 30 | } 31 | 32 | var startButton: some View { 33 | Button(action: startGame) { 34 | Text("START").fontWeight(.bold) 35 | } 36 | .tvOSExcluded { $0.buttonStyle(FatButtonStyle(color: .green)) } 37 | } 38 | 39 | func startGame() { 40 | gameCoordinator.enter(PlayState.self) 41 | } 42 | } 43 | 44 | // MARK: - Preview 45 | 46 | struct TitleUI_Previews: PreviewProvider { 47 | static var previews: some View { 48 | TitleUI() 49 | .environmentObject(MyGameCoordinator()) 50 | .frame(maxWidth: .infinity, maxHeight: .infinity) 51 | .foregroundColor(.red) 52 | .background(Color(red: 0.2, green: 0.1, blue: 0.5)) 53 | .edgesIgnoringSafeArea(.all) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/OctopusKit/SwiftUI/OKContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKContainerView.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019-10-20 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SwiftUI 10 | 11 | public typealias OctopusContainerView = OKContainerView 12 | 13 | /// The primary view for an OctopusKit game in a SwiftUI view hierarchy. 14 | /// 15 | /// Combines an `OKViewControllerRepresentable` with a SwiftUI overlay, for presenting SpriteKit content in a SwiftUI application along with the game user interface controls related to the current game state. 16 | /// 17 | /// The views are combined in a static ZStack, so the UI layer is always displayed on top of the 2D sprite layer. 18 | /// 19 | /// To use the entire screen on iOS, add the following view modifiers: 20 | /// 21 | /// .edgesIgnoringSafeArea(.all) 22 | /// .statusBar(hidden: true) 23 | public struct OKContainerView : View 24 | where OKGameCoordinatorType: OKGameCoordinator, 25 | OKViewControllerType: OKViewController 26 | { 27 | 28 | @EnvironmentObject var gameCoordinator: OKGameCoordinatorType 29 | 30 | public init() {} 31 | 32 | public var body: some View { 33 | 34 | ZStack { // CHECK: Group or ZStack? 35 | 36 | OKViewControllerRepresentable() 37 | .edgesIgnoringSafeArea(.all) 38 | 39 | if gameCoordinator.currentGameState != nil { 40 | gameCoordinator.currentGameState!.associatedSwiftUIView 41 | } 42 | 43 | // Use `OKUIOverlay()` if more complex UI overlay code is needed. 44 | // CHECK: Should the UI be an `.overlay` modifier for `OKViewControllerRepresentable`? 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /QuickStart/Universal/Components/TitleEffectsComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleEffectsComponent.swift 3 | // OctopusKitQuickStart 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/10/22. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import GameplayKit 11 | import OctopusKit 12 | 13 | /// A demonstration component that creates random effects for the title/main menu scene. 14 | final class TitleEffectsComponent: OKComponent, RequiresUpdatesPerFrame { 15 | 16 | override func update(deltaTime seconds: TimeInterval) { 17 | 18 | guard 19 | let parent = entityNode, 20 | (parent.scene as? OKScene)?.currentFrameNumber.isMultiple(of: 3) ?? false, 21 | let parentSize = (entityNode as? SKNodeWithDimensions)?.size 22 | else { return } 23 | 24 | let line = createRandomLine(parentSize: parentSize) 25 | parent.addChild(line) 26 | } 27 | 28 | func createRandomLine(parentSize: CGSize) -> SKShapeNode { 29 | 30 | let line = SKShapeNode(rectOf: CGSize(width: parentSize.width, height: 2)) 31 | 32 | line.lineWidth = CGFloat(Int.random(in: 1...10)) 33 | line.strokeColor = SKColor.brightColors.randomElement()! 34 | line.alpha = CGFloat([0.25, 0.5].randomElement()!) 35 | line.blendMode = .screen 36 | 37 | line.run( 38 | .group([ 39 | .scaleY(to: 3, duration: 0.5), 40 | .sequence([ 41 | .wait(forDuration: 0.1), 42 | .fadeOut(withDuration: 0.3), 43 | .removeFromParent()]) 44 | ])) 45 | 46 | line.position = CGPoint(x: 0, 47 | y: CGFloat(Int.random(in: -Int(parentSize.height) ... Int(parentSize.height)))) 48 | 49 | return line 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Support & Utility/Data Structures/PhysicsCategories.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhysicsCategories.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/26. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | /// A convenient alternative to specifying the categories of an `SKPhysicsBody` with an `OptionSet` instead of a direct `UInt32` value. 12 | /// 13 | /// 14 | /// To use this in your game, create an extension for this structure and add static instances of `PhysicsCategories` to it, then call the `categoryBitMask(_:)` etc. modifiers on `SKPhysicsBody` which accept a `PhysicsCategories` argument. 15 | /// 16 | /// **Example** 17 | /// 18 | /// extension PhysicsCategories { 19 | /// static let player = PhysicsCategories(1 << 0) 20 | /// static let enemy = PhysicsCategories(1 << 1) 21 | /// static let projectile = PhysicsCategories(1 << 2) 22 | /// } 23 | /// 24 | /// projectileBody 25 | /// .categoryBitMask (.projectile) 26 | /// .collisionBitMask ([.player, .enemy]) 27 | /// .contactTestBitMask ([.player, .enemy]) 28 | /// 29 | /// PhysicsCategories(projectileBody.categoryBitMask).contains(.projectile) 30 | public struct PhysicsCategories: OptionSet { 31 | 32 | public let rawValue: UInt32 33 | 34 | /// Creates a physics category with the specified bit mask. 35 | public init(rawValue: UInt32) { 36 | self.rawValue = rawValue 37 | } 38 | 39 | /// A convenience initializer which reduces text clutter by omitting the argument name. 40 | public init(_ rawValue: UInt32) { 41 | self.rawValue = rawValue 42 | } 43 | 44 | /// Indicates that this physics body will not interact with any other bodies for the particular property. 45 | public static let none = PhysicsCategories([]) 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Input/Pointer/NodePointerState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodePointerState.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/11/4. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import Foundation 10 | 11 | /// The state of a pointer-based interaction (touch or mouse) in relation to a `NodeComponent` node, as tracked by components which handle player input, such as `NodePointerStateComponent`. 12 | public enum NodePointerState: String, CustomStringConvertible { 13 | 14 | // The enum has a `rawValue` of `String` to assist with `description` for `CustomStringConvertible`. 15 | 16 | /// The default state, when the node is ready to accept input. 17 | case ready 18 | 19 | /// When the player touches or clicks the node. 20 | case pointing 21 | 22 | /// May occur after a `pointing` state, when the player keeps the finger or mouse pressed after touching or clicking the node, then moves the finger or cursor outside the node's bounds without lifting the finger or mouse button. 23 | case pointingOutside 24 | 25 | /// May occur after a `pointing` state, when the player lifts the finger or mouse button inside the node's bounds. 26 | /// 27 | /// - IMPORTANT: This state only persists for a single frame, then the state is immediately set to `ready` on the next frame. 28 | case tapped // CHECK: Rename to tappedOrClicked? 29 | 30 | /// May occur after a `pointing` state, when the player lifts the finger or mouse button *outside* the node's bounds. 31 | /// 32 | /// - IMPORTANT: This state only persists for a single frame, then the state is immediately set to `ready` on the next frame. 33 | case endedOutside 34 | 35 | /// When the component is inactive and the node is not accepting input. 36 | case disabled 37 | 38 | public var description: String { return self.rawValue } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Turn-Based/TurnBasedTileBasedPositionComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TurnBasedTileBasedPositionComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/02. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import OctopusCore 10 | import GameplayKit 11 | 12 | /// Updates the coordinates of the entity's `TileBasedPositionComponent` on a new game turn, ultimately setting the position of the entity's `NodeComponent` node. 13 | /// 14 | /// **Dependencies:** `TileBasedPositionComponent` 15 | open class TurnBasedTileBasedPositionComponent: OKTurnBasedComponent { 16 | 17 | open override var requiredComponents: [GKComponent.Type]? { 18 | [TileBasedPositionComponent.self] 19 | } 20 | 21 | /// The coordinates to set on the entity's `TileBasedPositionComponent` in `updateTurn(delta:)`. 22 | open var pendingCoordinates: Point? 23 | 24 | /// Set before `pendingCoordinates` are applied, and cleared at the start of a new turn. 25 | public fileprivate(set) var previousCoordinates: Point? 26 | 27 | open override func beginTurn(delta turns: Int = 1) { 28 | self.previousCoordinates = nil 29 | } 30 | 31 | open override func updateTurn(delta turns: Int) { 32 | guard 33 | let pendingCoordinates = self.pendingCoordinates, 34 | let tileBasedPositionComponent = coComponent(TileBasedPositionComponent.self) 35 | else { return } 36 | 37 | // CHECK: Validate coordinates here or is that the TileBasedPositionComponent's job? 38 | 39 | self.previousCoordinates = tileBasedPositionComponent.coordinates 40 | 41 | tileBasedPositionComponent.coordinates = pendingCoordinates 42 | 43 | // Clear the pending coordinates so they don't get reapplied. 44 | 45 | self.pendingCoordinates = nil 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Graphics/NodeActionComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeActionComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/11/06. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import OctopusCore 10 | import SpriteKit 11 | import GameplayKit 12 | 13 | public typealias SpriteKitActionComponent = NodeActionComponent 14 | 15 | /// Adds an action to the entity's `NodeComponent` node with an automatically generated `key`, and removes any action associated with that key when this component is removed from that entity. 16 | /// 17 | /// **Dependencies:** `NodeComponent` 18 | public class NodeActionComponent: OKComponent { 19 | 20 | public override var requiredComponents: [GKComponent.Type]? { 21 | [NodeComponent.self] 22 | } 23 | 24 | public let action: SKAction 25 | public let key: String 26 | 27 | public init(action: SKAction) { 28 | self.action = action 29 | self.key = "\(NodeActionComponent.self)\(action.debugDescription)@\(Date().timeIntervalSince1970)" 30 | super.init() 31 | } 32 | 33 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 34 | 35 | public override func didAddToEntity(withNode node: SKNode) { 36 | 37 | if let action = node.action(forKey: key) { 38 | OKLog.warnings.debug("\(📜("\(node) already has \(action) with key \"\(key)\" — Replacing"))") 39 | node.removeAction(forKey: key) 40 | } 41 | 42 | node.run(action, withKey: key) 43 | } 44 | 45 | public override func willRemoveFromEntity(withNode node: SKNode) { 46 | 47 | if node.action(forKey: key) == nil { 48 | OKLog.warnings.debug("\(📜("\(node) has no action with key \"\(key)\""))") 49 | } 50 | else { 51 | node.removeAction(forKey: key) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /QuickStart/Universal/Game States/4 - LogoState/4 - LogoState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoState.swift 3 | // OctopusKitQuickStart 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/02/10. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // 🔶 STEP 4: This is the initial game state for the OctopusKit QuickStart project. 10 | // 11 | // It displays the OKLogoScene which is provided by OctopusKit. When the logo scene finishes its animations, it tells the game coordinator to transition to the next state, which for this project is the TitleState. 12 | 13 | import GameplayKit 14 | import OctopusKit 15 | import SwiftUI 16 | 17 | final class LogoState: OKGameState { 18 | 19 | init() { 20 | 21 | // 🔶 STEP 4.1: Associates a scene with this state. 22 | // Each state may have only one scene, but a scene may represent multiple states (such as playing and paused.) 23 | // 24 | // Note that the LogoState has no associated SwiftUI overlay, which we will see in the upcoming TitleState. 25 | 26 | super.init(associatedSceneClass: OKLogoScene.self) 27 | } 28 | 29 | @discardableResult override func octopusSceneDidChooseNextGameState(_ scene: OKScene) -> Bool { 30 | 31 | // 🔶 STEP 4.2: This method will be called by the OKLogoScene after it finishes animating. 32 | 33 | return stateMachine?.enter(TitleState.self) ?? false 34 | } 35 | 36 | override var validNextStates: [OKState.Type] { 37 | 38 | // 🔶 STEP 4.3: This property lists all the valid states which this state is allowed to transition to. 39 | // 40 | // ❕ NOTE: Conditional logic to restrict state transitions should NOT be performed here. This property describes the static relationships between state classes that determine the set of edges in the state graph of OKGameCoordinator's superclass GKStateMachine. 41 | 42 | [TitleState.self] 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Support & Utility/Data Structures/LightCategories.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LightCategories.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/06/05. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | /// A convenient alternative to specifying the categories of a lighting-capable node (like `SKLightNode` or `SKSpriteNode`) with an `OptionSet` instead of a direct `UInt32` value. 12 | /// 13 | /// To use this in your game, create an extension for this structure and add static instances of `LightCategories` to it, then call the `lightingBitMask(_:)` etc. modifiers on lighting-compatible nodes which accept a `LightCategories` argument. 14 | /// 15 | /// **Example** 16 | /// 17 | /// extension LightCategories { 18 | /// static let droid = LightCategories(1 << 1) 19 | /// static let laser = LightCategories(1 << 2) 20 | /// static let terrain = LightCategories(1 << 3) 21 | /// } 22 | /// 23 | /// laserLight.categoryBitMask(.laser) 24 | /// tileMap.lightingBitMask([.terrain, .laser]) 25 | /// 26 | /// LightCategories(laserLight.categoryBitMask).contains(.laser) 27 | public struct LightCategories: OptionSet { 28 | 29 | // CHECK: Should this be named `LightingCategories`? 30 | 31 | public let rawValue: UInt32 32 | 33 | /// Creates a lighting category with the specified bit mask. 34 | public init(rawValue: UInt32) { 35 | self.rawValue = rawValue 36 | } 37 | 38 | /// A convenience initializer which reduces text clutter by omitting the argument name. 39 | public init(_ rawValue: UInt32) { 40 | self.rawValue = rawValue 41 | } 42 | 43 | /// Indicates that this node is not included in the lighting system. 44 | public static let none = LightCategories([]) 45 | 46 | /// The default category for a new `SKLightNode`. 47 | public static let `default` = LightCategories(1 << 0) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /QuickStart/Universal/Shared UI/ButtonStyles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OctopusKitQuickStart.swift 3 | // OctopusUI 4 | // https://github.com/InvadingOctopus/octopusui 5 | // 6 | // Created by ShinryakuTako@invadingoctopus.io on 2019/11/3. 7 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 8 | // 9 | 10 | // ❕ This code has been copied from the OctopusUI package to simplify the OctopusKit QuickStart tutorial and to keep OctopusKit self-contained (without dependencies on other packages). It may be an older version than its counterpart in OctopusUI. 11 | // ❗️ Exclude this file from your project if you import OctopusUI, otherwise using one of these extensions may cause an ambiguity conflict and prevent compilation. 12 | 13 | import SwiftUI 14 | 15 | struct FatButtonStyle: ButtonStyle { 16 | 17 | var color: Color = .accentColor 18 | 19 | func makeBody(configuration: Configuration) -> some View { 20 | configuration.label 21 | .foregroundColor(.white) 22 | .padding() 23 | .background( 24 | RoundedRectangle(cornerRadius: 10) 25 | .foregroundColor(color) 26 | .opacity(0.85) 27 | .shadow(color: .black, 28 | radius: configuration.isPressed ? 5 : 10, 29 | x: 0, 30 | y: configuration.isPressed ? -2 : -10) 31 | .brightness(configuration.isPressed ? 0.2 : 0) 32 | .animation(.easeOut(duration: 0.2), value: configuration.isPressed) 33 | ) 34 | } 35 | } 36 | 37 | /// Preview in live mode to test interactivity and animations. 38 | struct FatButtonStyle_Previews: PreviewProvider { 39 | static var previews: some View { 40 | Button(action: {}) { 41 | Text("Fat Button Style") 42 | } 43 | .buttonStyle(FatButtonStyle()) 44 | .padding() 45 | .background(Color.white) 46 | .previewLayout(.sizeThatFits) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/AppKit/NSGestureRecognizer+OctopusKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSGestureRecognizer+OctopusKit.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/12/22. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | #if canImport(AppKit) 10 | 11 | import AppKit 12 | 13 | extension NSGestureRecognizer { 14 | 15 | /// Replaces `target` and `action`. 16 | /// 17 | /// Emulates the `UIGestureRecognizer` method to support code shared with iOS. 18 | public func addTarget(_ target: AnyObject, action: Selector) { 19 | self.target = target 20 | self.action = action 21 | } 22 | 23 | /// Removes `target` and `action` if the current properties match the arguments. If an argument is `nil`, then the corresponding property is also set to `nil`. 24 | /// 25 | /// Emulates the `UIGestureRecognizer` method to support code shared with iOS. 26 | public func removeTarget(_ target: AnyObject?, action: Selector?) { 27 | // https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624226-removetarget 28 | 29 | if target == nil { self.target = nil } 30 | if action == nil { self.action = nil } 31 | 32 | if let target = target, 33 | let currentTarget = self.target, 34 | currentTarget === target 35 | { 36 | self.target = nil 37 | } 38 | 39 | if let action = action, 40 | let currentAction = self.action, 41 | currentAction == action 42 | { 43 | self.action = nil 44 | } 45 | } 46 | 47 | /// A dummy property on macOS to support code shared with iOS. 48 | // open var cancelsTouchesInView: Bool { 49 | // get { return true } // To mimic the default on iOS: https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624218-cancelstouchesinview 50 | // set {} 51 | // } 52 | } 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Audio/AudioComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/10/15. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // TODO: Tests 10 | 11 | // TODO: A way to add multiple `AudioComponent`s to an entity, as GameplayKit replaces older components of the same type. 12 | 13 | import GameplayKit 14 | 15 | /// Creates an `SKAudioNode` from the specified filename and plays it when this component is added to an entity, adding the audio node to the entity's `NodeComponent` node. 16 | /// 17 | /// **Dependencies:** `NodeComponent` 18 | public final class AudioComponent: NodeAttachmentComponent { 19 | 20 | // ℹ️ DESIGN: As we have to setup the audio in our initialization, and play it after it has been added to a parent node, we do not use `createAttachment(for:)` and just set `self.attachment` directly. 21 | 22 | public let audioNode: SKAudioNode 23 | 24 | public init(fileNamed fileName: String, 25 | volume: Float? = nil) 26 | { 27 | // TODO: Error-handling for missing files. 28 | self.audioNode = SKAudioNode(fileNamed: fileName) 29 | audioNode.autoplayLooped = false 30 | audioNode.isPositional = true 31 | 32 | if let volume = volume { 33 | audioNode.run(.changeVolume(to: volume, duration: 0)) 34 | } 35 | 36 | super.init(self.audioNode) 37 | } 38 | 39 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 40 | 41 | public override func didAddToEntity(withNode node: SKNode) { 42 | super.didAddToEntity(withNode: node) 43 | audioNode.run(SKAction.play()) 44 | } 45 | 46 | public override func willRemoveFromEntity(withNode node: SKNode) { 47 | audioNode.run(SKAction.stop()) // CHECK: Necessary? 48 | super.willRemoveFromEntity(withNode: node) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/Foundation/Collection+OctopusKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collection+OctopusKit.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/05/03. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | 13 | // MARK: - Randomization 14 | // Created by ShinryakuTako@invadingoctopus.io on 2017/10/07. 15 | 16 | import GameplayKit 17 | 18 | public extension Collection where Index == Int { 19 | 20 | 21 | /// Returns a random element from this array whose index is not in `skippingIndices` and generated by the `randomDistribution` source provided.. 22 | /// 23 | /// If the array is empty or if the list of exclusions prevents any acceptable value within `maximumAttempts`, `nil` is returned. 24 | /// 25 | /// If the array has only 1 element, then that is returned without generating any random indices. 26 | /// 27 | /// Can be used with array literals, e.g.: `[4, 8, 16, 42].randomElement(usingDistribution: GKRandomDistribution.d20(), skippingIndices: [0, 1, 2])` 28 | @inlinable 29 | func randomElement(usingDistribution randomDistribution: GKRandomDistribution, 30 | skippingIndices exclusions: Set? = nil, 31 | maximumAttempts: UInt = 100) -> Element? 32 | { 33 | guard self.count > 0 else { return nil } 34 | guard self.count > 1 else { return self[0] } 35 | 36 | var randomIndex: Int? 37 | 38 | if let exclusions = exclusions { 39 | randomIndex = randomDistribution.nextInt(upperBound: self.count, 40 | skipping: exclusions, 41 | maximumAttempts: maximumAttempts) 42 | } else { 43 | randomIndex = randomDistribution.nextInt(upperBound: self.count) 44 | } 45 | 46 | if let randomIndex = randomIndex { 47 | return self[randomIndex] 48 | } else { 49 | return nil 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKNodeWithColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKNodeWithColor.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/05. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | /// A protocol for nodes that have `color` and `colorBlendFactor` properties. 12 | /// 13 | /// This allows different `SKNode` subclasses to be handled together when processing color and tint. 14 | public protocol SKNodeWithColor: AnyObject { // where Self: SKNode { // ⚠️ Crashes. 15 | // TODO: Change name to an adjective? 16 | 17 | // Tinting a Sprite: https://developer.apple.com/documentation/spritekit/skspritenode/tinting_a_sprite 18 | 19 | var color: SKColor { get set } 20 | var colorBlendFactor: CGFloat { get set } 21 | } 22 | 23 | extension SKSpriteNode: SKNodeWithColor {} 24 | 25 | extension SKLabelNode: SKNodeWithColor { 26 | public var color: SKColor { 27 | get { self.fontColor ?? .clear } 28 | set { self.fontColor = newValue } 29 | } 30 | } 31 | 32 | extension SKShapeNode: SKNodeWithColor { 33 | // CHECK: Should this just be removed from `SKShapeNode`? 34 | 35 | public var color: SKColor { 36 | get { self.fillColor } 37 | set { self.fillColor = newValue } 38 | } 39 | 40 | /// Ignored on `SKShapeNode`. 41 | public var colorBlendFactor: CGFloat { 42 | get { 1.0 } // The `color` property will always be applied 100% 43 | set {} // Nothing to set 44 | } 45 | } 46 | 47 | extension SKNodeWithColor { 48 | 49 | // MARK: - Modifiers 50 | // As in SwiftUI. 51 | 52 | /// Returns this node after setting its color. 53 | @inlinable 54 | public func color(_ color: SKColor) -> Self { 55 | self.color = color 56 | return self 57 | } 58 | 59 | /// Returns this node after setting its color blending factor. 60 | @inlinable 61 | public func colorBlendFactor(_ colorBlendFactor: CGFloat) -> Self { 62 | self.colorBlendFactor = colorBlendFactor 63 | return self 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Input/Touch/NodeTouchState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeTouchState.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/05/17. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import Foundation 10 | 11 | /// The state of a touch-based interaction in relation to a `NodeComponent` node, as tracked by components which handle player input, such as `NodeTouchStateComponent`. 12 | @available(iOS 13.0, *) 13 | @available(macOS, unavailable, message: "Use NodePointerState") 14 | public enum NodeTouchState: String, CustomStringConvertible { 15 | 16 | // CHECK: Should there be a new `cancelled` state that's only set on an actual `touchesCancelled` event? 17 | 18 | // The enum has a `rawValue` of `String` to assist with `description` for `CustomStringConvertible`. 19 | 20 | /// The default state, when the node is ready to accept input. 21 | case ready 22 | 23 | /// When the player touches the node. 24 | case touching 25 | 26 | /// May occur after a `touching` state, when the player keeps the finger pressed on the screen after touching the node, then moves it outside the node's bounds without lifting the finger. 27 | case touchingOutside 28 | 29 | /// May occur after a `touching` state, when the player lifts the finger from the screen inside the node's bounds. 30 | /// 31 | /// - IMPORTANT: This state only persists for a single frame, then the state is immediately set to `ready` on the next frame. 32 | case tapped 33 | 34 | /// May occur after a `touching` state, when the player lifts the finger from the screen *outside* the node's bounds, or when the touch is cancelled for other reasons, such as a system interruption. 35 | /// 36 | /// - IMPORTANT: This state only persists for a single frame, then the state is immediately set to `ready` on the next frame. 37 | case endedOutside // CHECK: Rename to `liftedOutside`? 38 | 39 | /// When the component is inactive and the node is not accepting input. 40 | case disabled 41 | 42 | public var description: String { return self.rawValue } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Gameplay/RandomizationComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RandomizationComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/03/09. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | /// Encapsulates a `GKRandom` object. 12 | open class RandomizationComponent: OKComponent { 13 | 14 | open var source: GKRandom 15 | 16 | public init(source: GKRandom = GKRandomSource()) { 17 | self.source = source 18 | super.init() 19 | } 20 | 21 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 22 | } 23 | 24 | extension RandomizationComponent: RandomNumberGenerator { 25 | 26 | // TODO: Verify & Test 27 | 28 | // ⚠️ WARNING: GKRandomDistribution does not currently work when accessed as a RandomNumberGenerator 29 | 30 | /// Calls `source.next()` if `source` conforms to `RandomNumberGenerator`, otherwise calls `source.nextInt()` twice to generate an unsigned 64-bit integer. 31 | /// 32 | /// - Returns: An unsigned 64-bit random value. 33 | public func next() -> UInt64 { 34 | if var source = self.source as? RandomNumberGenerator { 35 | return source.next() 36 | } else { 37 | // CREDIT: Grigory Entin https://stackoverflow.com/users/1859783/grigory-entin 38 | // https://stackoverflow.com/a/57370987/1948215 39 | // GKRandom produces values in the `INT32_MIN...INT32_MAX` range; hence we need two numbers to produce 64-bit value. 40 | let next1 = UInt64(bitPattern: Int64(self.source.nextInt())) 41 | let next2 = UInt64(bitPattern: Int64(self.source.nextInt())) 42 | return next1 ^ (next2 << 32) 43 | } 44 | } 45 | 46 | /* 47 | /// Calls `source.next(upperBound:)` 48 | /// 49 | /// - Returns - A random value of T in the range 0..(upperBound: T) -> T 51 | where T: FixedWidthInteger, T: UnsignedInteger 52 | { 53 | return T(abs(self.source.nextInt(upperBound: Int(upperBound)))) 54 | } 55 | */ 56 | } 57 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Support & Utility/Enums/TimeStep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeStep.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2019/11/28. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import Foundation 10 | 11 | /// Specifies the time step for time-dependent components. 12 | public enum TimeStep { 13 | 14 | // CHECK: Add "Between frame" updates? 15 | // http://expiredpopsicle.com/2014/09/16/Variable_Timesteps_and_Holy_Crap_Math_is_Hard.html 16 | // http://expiredpopsicle.com/2014/09/18/Tick_Time_Debt.html 17 | 18 | /// Fixed time step; applies a constant `…perUpdate` change to the affected values in `update(deltaTime:)` every frame. 19 | /// 20 | /// Use this when slower gameplay is preferred to losing frames. 21 | case perFrame 22 | 23 | /// Variable time step; multiples each `…perUpdate` change by `deltaTime` in `update(deltaTime:)` every frame. 24 | /// 25 | /// Use this when losing frames is preferred to slower gameplay. 26 | case perSecond 27 | 28 | /// Shorthand for `(timeStep == .perFrame) ? change : change * deltaTime` 29 | /// 30 | /// **Example:** `let acceleratedMagnitude = timeStep.applying(acceleration, deltaTime: CGFloat(seconds))` 31 | /// 32 | /// - Returns: `change` if the time step is `perFrame`, or `change * deltaTime` if the time step is `perSecond`. 33 | @inlinable 34 | public func applying (_ change: Number, 35 | deltaTime: Number) -> Number 36 | where Number: Numeric 37 | { 38 | return (self == .perFrame) ? change : change * deltaTime 39 | } 40 | 41 | /// Shorthand for `value += (timeStep == .perFrame) ? change : change * deltaTime` 42 | /// 43 | /// **Example:** `timeStep.apply(acceleration, to: &acceleratedMagnitude, deltaTime: CGFloat(seconds))` 44 | @inlinable 45 | public func apply (_ change: Number, 46 | to value: inout Number, 47 | deltaTime: Number) 48 | where Number: Numeric 49 | { 50 | value += (self == .perFrame) ? change : change * deltaTime 51 | } 52 | 53 | } 54 | 55 | // Prompted by a discussion on the Reddit /r/GameDev Discord. :) 56 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKAttributeValue+OctopusKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKAttributeValue+OctopusKit.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/05/13. 6 | // ORIGINAL: SOURCE: https://github.com/twostraws/ShaderKit/blob/master/ShaderKitExtensions.swift 7 | // ORIGINAL: CREDIT: Copyright © 2019 Paul Hudson. Licensed under MIT License (see below) 8 | // UPDATES: Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 9 | // 10 | 11 | // MIT License 12 | // 13 | // Copyright (c) 2019 Paul Hudson 14 | // https://www.github.com/twostraws/ShaderKit 15 | // 16 | // Permission is hereby granted, free of charge, to any person obtaining a copy 17 | // of this software and associated documentation files (the "Software"), to deal 18 | // in the Software without restriction, including without limitation the rights 19 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | // copies of the Software, and to permit persons to whom the Software is 21 | // furnished to do so, subject to the following conditions: 22 | // 23 | // The above copyright notice and this permission notice shall be included in all 24 | // copies or substantial portions of the Software. 25 | // 26 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | // SOFTWARE. 33 | 34 | import SpriteKit 35 | 36 | public extension SKAttributeValue { 37 | 38 | /// Creates and initializes a new attribute value object that converts a `CGSize` to a vector of two floating point numbers. 39 | /// 40 | /// - Parameters: 41 | /// - size: The input size, i.e. the node's size. For an `SKTileMapNode` this is the `tileSize`. 42 | convenience init(size: CGSize) { 43 | let size = vector_float2(x: Float(size.width), 44 | y: Float(size.height)) 45 | self.init(vectorFloat2: size) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Input/Touch Gestures/TapGestureRecognizerComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TapGestureRecognizerComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/04/19. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import GameplayKit 11 | 12 | #if os(iOS) // CHECK: Add tvOS support? 13 | 14 | /// Creates a `UITapGestureRecognizer` and attaches it to the `SceneComponent` `SKView` when this component is added to the scene entity. 15 | /// 16 | /// - Important: Although taps are discrete gestures, they are discrete for each state of the gesture recognizer; thus the associated action message is sent when the gesture begins and is sent for each intermediate state until (and including) the ending state of the gesture. 17 | /// 18 | /// - Note: Adding a gesture recognizer to the scene's view may prevent touches from being delivered to the scene and its nodes. To allow gesture-based components to cooperate with touch-based components, set properties such as `gestureRecognizer.cancelsTouchesInView` to `false` for this component. 19 | /// 20 | /// **Dependencies:** `SceneComponent` 21 | @available(iOS 13.0, *) 22 | public final class TapGestureRecognizerComponent: OKGestureRecognizerComponent { 23 | 24 | // ⚠️ NOTE: https://developer.apple.com/documentation/uikit/uitapgesturerecognizer 25 | 26 | public init(numberOfTapsRequired: Int = 1, 27 | numberOfTouchesRequired: Int = 1, 28 | cancelsTouchesInView: Bool = true) 29 | { 30 | super.init() // CHECK: PERFORMANCE: Is it faster to not call the `super.init(cancelsTouchesInView:)` convenience? 31 | self.gestureRecognizer.cancelsTouchesInView = cancelsTouchesInView 32 | self.gestureRecognizer.numberOfTapsRequired = numberOfTapsRequired 33 | self.gestureRecognizer.numberOfTouchesRequired = numberOfTouchesRequired 34 | } 35 | 36 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 37 | } 38 | 39 | #endif 40 | 41 | #if os(macOS) 42 | @available(macOS, unavailable, message: "Use ClickGestureRecognizerComponent") 43 | public final class TapGestureRecognizerComponent: iOSExclusiveComponent {} 44 | #endif 45 | -------------------------------------------------------------------------------- /Templates/Game/README Template.md: -------------------------------------------------------------------------------- 1 | # OctopusKit Game Project Template 2 | 3 | **Template and tutorial for the [OctopusKit][repository] game engine.** 4 | 5 | 1. 🆕 In Xcode 12 or later, create a new **App** project, and choose **Interface: SwiftUI**. 6 | 7 | > ❗️ **Do NOT** create a "Game" project, because that template does not use SwiftUI. 8 | 9 | > OctopusKit targets iOS/iPadOS/tvOS 14 and macOS Big Sur, but you may be able to get it to run with Xcode 11, iOS 13 and Catalina. 10 | 11 | 2. 📦 Add OctopusKit as a **Swift Package Manager** Dependency. 12 | 13 | > *Xcode File menu » Swift Packages » Add Package Dependency...* 14 | 15 | > Enter the URL for the GitHub [repository][repository]. 16 | 17 | > Download the "develop" branch for the latest version. 18 | 19 | 3. 📥 **Copy and include** all the *subfolders* from the `Templates/Game/Shared` folder in your project. 20 | 21 | > In the Xcode Project Navigator, select the `OctopusKit/Templates/Game` folder, then in the File Inspector, click the arrow next to "Full Path", then drag all the *subfolders* (`Components, Core, Entities` etc.) from the `Shared` subfolder into your project's `Shared` folder in the Xcode navigator. 22 | 23 | 4. 🖼 Add the `GameContentView` to the `ContentView.swift` file: 24 | 25 | ``` 26 | var body: some View { 27 | GameContentView() 28 | } 29 | ``` 30 | 31 | 5. 🚀 Build and run the project to verify that the template works. 32 | 33 | 6. 👓 Examine the various files to see how everything works, then modify the template for your specific game. 34 | 35 | 7. Check the documentation: 36 | 37 | * For a detailed explanation of the engine architecture, see [Architecture.md][architecture] in the `Documentation` folder of the OctopusKit package/repository. 38 | 39 | * **To see how to do common tasks, refer to [Usage Guide.md][guide]** 40 | 41 | ---- 42 | 43 | [OctopusKit][repository] © 2020 [Invading Octopus][website] • [Apache License 2.0][license] 44 | 45 | [repository]: https://github.com/invadingoctopus/octopuskit 46 | [website]: https://invadingoctopus.io 47 | [license]: https://www.apache.org/licenses/LICENSE-2.0.html 48 | 49 | [guide]: https://github.com/InvadingOctopus/octopuskit/blob/master/Documentation/Usage%20Guide.md 50 | [architecture]: https://github.com/InvadingOctopus/octopuskit/blob/master/Documentation/Architecture.md 51 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Physics/ThrustComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThrustComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/11/11. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import OctopusCore 10 | import SpriteKit 11 | import GameplayKit 12 | 13 | /// Applies a `thrustVector` to a `PhysicsComponent` every frame. 14 | /// 15 | /// **Dependencies:** `PhysicsComponent` 16 | public final class ThrustComponent: OKComponent, RequiresUpdatesPerFrame { 17 | 18 | // TODO: CHECK: Use deltaTime to determine thrust per update, instead of a fixed value? 19 | // CHECK: Should it be renamed to ForceComponent? 20 | 21 | public override var requiredComponents: [GKComponent.Type]? { 22 | [PhysicsComponent.self] 23 | } 24 | 25 | /// The scalar that `thrustVector` will be multiplied by. 26 | public var thrustBoostFactor: CGFloat = 1.0 27 | 28 | /// The scalar to clamp the `thrustVector` to, after applying the `thrustBoostFactor`. 29 | public var maximumThrust: CGFloat? 30 | 31 | public var thrustVector: CGVector? 32 | 33 | public init(thrustBoostFactor: CGFloat = 1.0, 34 | maximumThrust: CGFloat? = nil) 35 | { 36 | self.thrustBoostFactor = thrustBoostFactor 37 | self.maximumThrust = maximumThrust 38 | 39 | super.init() 40 | } 41 | 42 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 43 | 44 | @inlinable 45 | public override func update(deltaTime seconds: TimeInterval) { 46 | super.update(deltaTime: seconds) 47 | 48 | guard 49 | var thrustVector = self.thrustVector, 50 | let physicsBody = coComponent(PhysicsComponent.self)?.physicsBody 51 | else { return } 52 | 53 | // Multiply the thrust by the boost factor, 54 | thrustVector *= thrustBoostFactor 55 | 56 | // then multiply it by the time that has passed since the last update? 57 | // thrustVector *= CGFloat(seconds) 58 | 59 | if let maximumThrust = self.maximumThrust { 60 | thrustVector.clampMagnitude(to: maximumThrust) 61 | } 62 | 63 | physicsBody.applyForce(thrustVector) 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /QuickStart/Universal/3 - MyGameViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyGameViewController.swift 3 | // OctopusKitQuickStart 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/06/04. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // 🔶 STEP 3: The view controller for the SpriteKit view (SKView) that displays your game. 10 | // 11 | // Creating a subclass of OKViewController is not necessary for a basic OctopusKit project, but complex games may require some view controller customization. This subclass does no customization but is provided for illustration. 12 | // 13 | // In SwiftUI, an OKViewController is encapsulated by OKViewControllerRepresentable. 14 | // 15 | // However, to use OctopusKit in a SwiftUI project, use: 16 | // 17 | // OKContainerView() 18 | // .environmentObject(MyGameCoordinator()) 19 | // 20 | // The OKContainerView presents SpriteKit and SwiftUI content together. 21 | // 22 | // If you are using AppKit or UIKit, then the view controller for the SKView in your main storyboard must be the OKViewController class, or a subclass of it that is specific to your game, like the MyGameViewController in this project. 23 | 24 | import OctopusKit 25 | 26 | final class MyGameViewController: OKViewController { 27 | 28 | override func viewDidLoad() { 29 | 30 | // 🔶 STEP 3.1: You may customize some screen-related settings here, such as the device orientations allowed in your game and status bar visibility etc. 31 | 32 | super.viewDidLoad() // ❗️ Required. You must call super.viewDidLoad() before any other code in your overriding implementation. 33 | 34 | #if os(iOS) 35 | 36 | supportedInterfaceOrientationsOverride[.phone] = .allButUpsideDown 37 | 38 | // prefersStatusBarHiddenOverride = true 39 | // prefersHomeIndicatorAutoHiddenOverride = true 40 | // shouldAutorotateOverride = false 41 | 42 | #endif 43 | 44 | } 45 | 46 | #if canImport(UIKit) // iOS & tvOS 47 | 48 | override func didReceiveMemoryWarning() { 49 | // Customize this method to release any cached game-specific data, images, etc. that aren't in use, so that the operating system can free up some memory. 50 | super.didReceiveMemoryWarning() 51 | } 52 | 53 | #endif 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKTexture+OctopusKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKTexture+OctopusKit.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/03/23. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // TODO: Tests 10 | 11 | import OctopusCore 12 | import SpriteKit 13 | 14 | public extension SKTexture { 15 | 16 | /// Creates a gradient texture. 17 | convenience init(size: CGSize, 18 | startColor: SKColor, 19 | endColor: SKColor, 20 | direction: OKDirection = .up) 21 | { 22 | // CREDIT: https://theswiftdev.com/2017/10/24/spritekit-best-practices/ 23 | 24 | // TODO: Add diagonals 25 | // TODO: Remove forced unwraps! 26 | // CHECK: Make init failable? 27 | 28 | let context = CIContext(options: nil) 29 | let filter = CIFilter(name: "CILinearGradient")! 30 | let startVector: CIVector 31 | let endVector: CIVector 32 | 33 | filter.setDefaults() 34 | 35 | switch direction { 36 | case .up: 37 | startVector = CIVector(x: size.width/2, y: 0) 38 | endVector = CIVector(x: size.width/2, y: size.height) 39 | case .down: 40 | startVector = CIVector(x: size.width/2, y: size.height) 41 | endVector = CIVector(x: size.width/2, y: 0) 42 | case .left: 43 | startVector = CIVector(x: size.width, y: size.height/2) 44 | endVector = CIVector(x: 0, y: size.height/2) 45 | case .right: 46 | startVector = CIVector(x: 0, y: size.height/2) 47 | endVector = CIVector(x: size.width, y: size.height/2) 48 | default: 49 | OKLog.warnings.debug("\(📜("Unsupported gradient direction"))") 50 | startVector = CIVector(x: 0, y: 0) 51 | endVector = startVector 52 | } 53 | 54 | filter.setValue(startVector, forKey: "inputPoint0") 55 | filter.setValue(endVector, forKey: "inputPoint1") 56 | filter.setValue(CIColor(color: startColor), forKey: "inputColor0") 57 | filter.setValue(CIColor(color: endColor), forKey: "inputColor1") 58 | 59 | let image = context.createCGImage(filter.outputImage!, from: CGRect(origin: .zero, size: size)) 60 | 61 | self.init(cgImage: image!) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | // https://github.com/InvadingOctopus/octopuskit 5 | 6 | import PackageDescription 7 | 8 | let package = Package( 9 | name: "OctopusKit", 10 | defaultLocalization: "en", 11 | platforms: [ 12 | .iOS(.v17), 13 | .macOS(.v14), 14 | .tvOS(.v17) 15 | ], 16 | products: [ 17 | // Products define the executables and libraries a package produces, making them visible to other packages. 18 | .library( 19 | name: "OctopusKit", 20 | targets: ["OctopusKit"]), 21 | ], 22 | dependencies: [ 23 | // Dependencies declare other packages that this package depends on. 24 | // .package(url: /* package url */, from: "1.0.0"), 25 | // .package(url: "https://github.com/InvadingOctopus/octopuscore.git", from: "1.0.0") 26 | .package(path: "../OctopusCore") 27 | ], 28 | targets: [ 29 | // Targets are the basic building blocks of a package, defining a module or a test suite. 30 | // Targets can depend on other targets in this package and products from dependencies. 31 | .target( 32 | name: "OctopusKit", 33 | dependencies: ["OctopusCore"], 34 | exclude: [ 35 | "Apple API Extensions/SwiftUI/OctopusUI.md"], 36 | resources: [ 37 | .copy("Assets/Shaders/ShaderKit/LICENSE")] 38 | // , swiftSettings: [ // MARK: - Conditional Compilation Flags 39 | // .define("LOGECSVERBOSE"), // Log detailed ECS core events. ⚠️ May decrease performance. 40 | // .define("LOGECSDEBUG"), // Log ECS debugging info. ⚠️ Will decrease performance. 41 | // .define("LOGINPUTEVENTS"), // Log detailed mouse/touch/pointer input events. ⚠️ May decrease performance. 42 | // .define("LOGPHYSICS"), // Log physics contact/collision events. ⚠️ May decrease performance. 43 | // .define("LOGTURNBASED") // Log each begin/update/end cycle for turn-based components. ⚠️ May decrease performance. 44 | // ] // Remember to uncomment this if you uncomment any of the lines above ^^ 45 | ), 46 | .testTarget( 47 | name: "OctopusKitTests", 48 | dependencies: ["OctopusKit"]), 49 | ], 50 | swiftLanguageVersions: [.v5] 51 | ) 52 | 53 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Core/Base/OKComponentSystem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKComponentSystem.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/10/13. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import OctopusCore 10 | import GameplayKit 11 | 12 | public typealias OctopusComponentSystem = OKComponentSystem 13 | 14 | /// Manages component objects of a single specific class. Adds validation and convenience methods for common tasks to `GKComponentSystem`. 15 | public final class OKComponentSystem: GKComponentSystem { 16 | 17 | public override init(componentClass cls: AnyClass) { 18 | 19 | // Warn about unnecessary systems. 20 | 21 | if !(cls.self is RequiresUpdatesPerFrame.Type) 22 | && !(cls.self is TurnBased.Type) 23 | { 24 | OKLog.warnings.debug("\(📜("System created for component that is not marked as RequiresUpdatesPerFrame: \(cls)"))") 25 | OKLog.tips.debug("\(📜("Either this system is unnecessary, or you forgot to add RequiresUpdatesPerFrame protocol conformance to \(cls)"))") 26 | } 27 | 28 | super.init(componentClass: cls) 29 | } 30 | 31 | /// Adds an applicable component to this system, ignoring duplicates. 32 | public final override func addComponent(_ component: GKComponent) { 33 | 34 | guard !(components.contains(component)) else { 35 | OKLog.debug.debug("\(📜("\(component) already in system – Skipping"))") 36 | return 37 | } 38 | 39 | OKLog.components.debug("\(📜("\(component) — Object: \(self)"))") 40 | 41 | super.addComponent(component) 42 | } 43 | 44 | /// Adds all applicable components from the specified entity to this system, ignoring duplicates. 45 | public final override func addComponent(foundIn entity: GKEntity) { 46 | super.addComponent(foundIn: entity) 47 | } 48 | 49 | /// Adds all applicable components from the specified entities to this system, ignoring duplicates. 50 | public final func addComponents(foundIn entities: [GKEntity]) { 51 | // ⚠️ NOTE: Cannot add this method as an extension to `GKComponentSystem` as of 2017/10/14, because "Extension of a generic Objective-C class cannot access the class's generic parameters at runtime" 52 | 53 | for entity in entities { 54 | self.addComponent(foundIn: entity) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Input/Touch/TouchControlledPositioningComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TouchControlledPositioningComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/05/19. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import GameplayKit 11 | 12 | #if canImport(UIKit) 13 | 14 | /// Sets the position of the entity's `NodeComponent` node to the location of the first or latest touch received by the entity's `TouchEventComponent`. 15 | /// 16 | /// **Dependencies:** `NodeComponent`, `TouchEventComponent` 17 | @available(iOS 13.0, *) 18 | public final class TouchControlledPositioningComponent: OKComponent, RequiresUpdatesPerFrame { 19 | 20 | public override var requiredComponents: [GKComponent.Type]? { 21 | [NodeComponent.self, 22 | TouchEventComponent.self] 23 | } 24 | 25 | public var trackLatestTouchInsteadOfFirst: Bool = false 26 | 27 | public var trackedTouch: UITouch? { 28 | return (trackLatestTouchInsteadOfFirst ? coComponent(ofType: TouchEventComponent.self)?.latestTouch : coComponent(ofType: TouchEventComponent.self)?.firstTouch) 29 | } 30 | 31 | public init(trackLatestTouchInsteadOfFirst: Bool = false) { 32 | self.trackLatestTouchInsteadOfFirst = trackLatestTouchInsteadOfFirst 33 | super.init() 34 | } 35 | 36 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 37 | 38 | public override func update(deltaTime seconds: TimeInterval) { 39 | 40 | guard 41 | let node = self.entityNode, 42 | let parent = node.parent, 43 | let trackedTouch = self.trackedTouch 44 | else { return } 45 | 46 | node.position = trackedTouch.location(in: parent) 47 | 48 | // Update the state of a `NodeTouchStateComponent`, if present, for the new position. 49 | 50 | if let nodeTouchComponent = coComponent(NodeTouchStateComponent.self) { 51 | nodeTouchComponent.updateState(suppressTappedState: true, 52 | suppressCancelledState: true) 53 | } 54 | 55 | } 56 | } 57 | 58 | #endif 59 | 60 | #if !canImport(UIKit) 61 | @available(macOS, unavailable, message: "Use PointerControlledPositioningComponent") 62 | public final class TouchControlledPositioningComponent: iOSExclusiveComponent {} 63 | #endif 64 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Core/Base/OKState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKState.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/06/01. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import GameplayKit 10 | 11 | /// Represents a state in a state machine. 12 | /// 13 | /// Adds convenience features to `GKState`, such as an array of states it can transition to. The base class for `OKGameState` and `OKEntityState`. 14 | open class OKState: GKState { 15 | 16 | /// Specifies the possible states that this state may transition to. If this array is empty, then all states are allowed (the default.) 17 | /// 18 | /// Checked by `isValidNextState(_:)`. 19 | /// 20 | /// - Important: This property should describe the **static** relationships between state classes that determine the set of edges in a state machine’s state graph; Do **not** perform conditional logic in this property to conditionally control state transitions. Check conditions before attempting to transition to a different state. 21 | @inlinable 22 | open var validNextStates: [OKState.Type] { 23 | [] 24 | } 25 | 26 | /// Returns `true` if the `validNextStates` property contains `stateClass` or is an empty array (which means all states are allowed.) 27 | /// 28 | /// - Important: Do not override this method to conditionally control state transitions. Instead, perform such conditional logic before transitioning to a different state. 29 | open override func isValidNextState(_ stateClass: AnyClass) -> Bool { 30 | // https://developer.apple.com/documentation/gameplaykit/gkstate/1501221-isvalidnextstate 31 | // Your implementation of this method should describe the static relationships between state classes that determine the set of edges in a state machine’s state graph. 32 | /// ⚠️ Do **not** use this method to conditionally control state transitions—instead, perform such conditional logic before calling a state machine’s `enter(_:)` method. 33 | // By restricting the set of valid state transitions, you can use a state machine to enforce invariant conditions in your code. For example, if one state class can be entered only after a state machine has passed through a series of other states, code in that state class can safely assume that any actions performed by those other states have already occurred. 34 | 35 | validNextStates.isEmpty || validNextStates.contains { stateClass == $0 } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Physics/PhysicsWorldComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhysicsWorldComponent.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/10/27. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import OctopusCore 10 | import GameplayKit 11 | 12 | /// Retains a reference to the `physicsWorld` of an `SKScene` to be used by other components. Must be assigned to an entity with an `SceneComponent`. If the `physicsWorld` has no `contactDelegate` then it's set to the scene, if the scene conforms to `SKPhysicsContactDelegate` (as `OKScene` always does.) 13 | /// 14 | /// **Dependencies:** `SceneComponent` 15 | public final class PhysicsWorldComponent: OKComponent { 16 | 17 | public override var requiredComponents: [GKComponent.Type]? { 18 | [SceneComponent.self] 19 | } 20 | 21 | public weak var physicsWorld: SKPhysicsWorld? // CHECK: Should this be weak? 22 | 23 | public override func didAddToEntity() { 24 | super.didAddToEntity() 25 | 26 | guard let scene = coComponent(SceneComponent.self)?.scene else { 27 | OKLog.warnings.debug("\(📜("\(entity) missing SceneComponent – Cannot assign physicsWorld"))") 28 | return 29 | } 30 | 31 | self.physicsWorld = scene.physicsWorld 32 | 33 | if scene.physicsWorld.contactDelegate == nil { 34 | scene.physicsWorld.contactDelegate = scene // as? SKPhysicsContactDelegate 35 | } 36 | else if scene.physicsWorld.contactDelegate !== scene { 37 | OKLog.warnings.debug("\(📜("\(scene.physicsWorld) has a contactDelegate that is not \(scene)"))") 38 | } 39 | 40 | // If the physics simulation is paused, start it, as this would be the expected behavior upon adding a `PhysicsWorld` component. 41 | // NOTE: If the world is NOT paused, then leave the speed as is, in case it was directly slowed down or sped up before this component was added. 42 | 43 | if let physicsWorld = self.physicsWorld, physicsWorld.speed <= 0 { 44 | physicsWorld.speed = 1 45 | } 46 | } 47 | 48 | public override func willRemoveFromEntity() { 49 | super.willRemoveFromEntity() 50 | // Stop the physics simulation as this would be the expected behavior upon removing a `PhysicsWorld` component. 51 | self.physicsWorld?.speed = 0 52 | self.physicsWorld = nil 53 | } 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /QuickStart/Universal/Game States/6 - PlayState/6A - PlayState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayableState.swift 3 | // OctopusKitQuickStart 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2018/02/10. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // 🔶 STEP 6A: The "gameplay" state for the QuickStart project, represented by the PlayScene (which also displays the content for the PausedState and GameOverState.) 10 | 11 | import GameplayKit 12 | import OctopusKit 13 | import SwiftUI 14 | 15 | final class PlayState: OKGameState { 16 | 17 | init() { 18 | 19 | // 🔶 STEP 6A.1: Associates a scene and UI with this state. 20 | // The PlayScene is also associated with the PausedState and GamerOverState. 21 | 22 | super.init(associatedSceneClass: PlayScene.self, 23 | associatedSwiftUIView: PlayUI()) 24 | } 25 | 26 | override func didEnter(from previousState: GKState?) { 27 | 28 | // 🔶 STEP 6A.2: This method is called when a state begins. 29 | // 30 | // Here we add a component to the global game coordinator entity (a property of OKGameCoordinator and its subclasses) which is available to all states and all scenes, to demonstrate how to hold data which will persist throughout the game. 31 | 32 | if OctopusKit.shared.gameCoordinator.entity.component(ofType: GlobalDataComponent.self) == nil { 33 | OctopusKit.shared.gameCoordinator.entity.addComponent(GlobalDataComponent()) 34 | } 35 | 36 | // Note that we pass control to the OKGameState superclass AFTER we've added the global component, so that it will be available to the PlayScene when it's presented by the code in the superclass. 37 | 38 | super.didEnter(from: previousState) 39 | } 40 | 41 | @discardableResult override func octopusSceneDidChooseNextGameState(_ scene: OKScene) -> Bool { 42 | 43 | // 🔶 STEP 6A.3: This method will be called by the PlayScene when the "Cycle Game States" button is tapped. 44 | 45 | return stateMachine?.enter(PausedState.self) ?? false 46 | } 47 | 48 | override var validNextStates: [OKState.Type] { 49 | 50 | // 🔶 STEP 6A.4: This property lists all the valid states which this state is allowed to transition to. 51 | // 52 | // The PlayState can lead to either the PausedState or the GameOverState. 53 | 54 | [PausedState.self, GameOverState.self] 55 | } 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Scene Templates/OKLogoScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKLogoScene.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2014-10-23 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | import AVFoundation 11 | 12 | public typealias OctopusLogoScene = OKLogoScene 13 | 14 | /// A template for presenting the OctopusKit logo. 15 | public final class OKLogoScene: OKScene { 16 | 17 | public override func setName() -> String? { "OctopusKit Logo Scene" } 18 | 19 | public override func createContents() { 20 | self.backgroundColor = .black 21 | self.isUserInteractionEnabled = false 22 | beginAct1() 23 | } 24 | 25 | public func beginAct1() { 26 | createLogo(text: "👾", completion: beginAct2) 27 | // audioEngine.playUISound("BeepLong") 28 | } 29 | 30 | public func beginAct2() { 31 | createLogo(text: "🐙", completion: beginAct3) 32 | // audioEngine.playUISound("BeepLong") 33 | } 34 | 35 | public func beginAct3() { 36 | introSceneDidFinish() 37 | } 38 | 39 | /// Signals the delegate to present the next scene (effectively the first scene of the game, after the logo.) 40 | public func introSceneDidFinish() { 41 | octopusSceneDelegate?.octopusSceneDidFinish(self) 42 | } 43 | 44 | /// Creates and animates the specified text. 45 | @discardableResult private func createLogo(text: String, completion: (() -> Void)? = nil) -> SKNode { 46 | 47 | let logo = SKLabelNode(text: text) 48 | logo.alignment = (.center, .center) 49 | logo.position = CGPoint( 50 | x: self.frame.midX, 51 | y: self.frame.midY) 52 | logo.alpha = 0 53 | logo.setScale(10.0) 54 | 55 | let animationDuration = 0.5 56 | let wait = 0.75 57 | let shrink = SKAction.scale(to: 1.0, duration: animationDuration).timingMode(.easeIn) 58 | let fadeIn = SKAction.fadeIn(withDuration: animationDuration) 59 | let shrinkAndFadeIn = SKAction.group([shrink, fadeIn]) 60 | 61 | self.addChild(logo) 62 | 63 | if let completion = completion { 64 | logo.run(.sequence([ 65 | shrinkAndFadeIn, 66 | .wait(forDuration: wait), 67 | .removeFromParent()]), 68 | completion: completion 69 | ) 70 | } 71 | 72 | return logo 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Components/Graphics/OKShadow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OKShadow.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2014-11-28 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | // TODO: Test 10 | // TODO: Improve 11 | // TODO: macOS support 12 | 13 | import SpriteKit 14 | 15 | public struct OKShadow { 16 | // CHECK: struct or class? 17 | 18 | public var shadowColor: CGColor? 19 | public var shadowOpacity: Float? 20 | public var shadowOffset: CGSize? 21 | public var shadowPath: CGPath? 22 | public var shadowRadius: CGFloat? 23 | 24 | public init(shadowColor: CGColor? = nil, 25 | shadowOpacity: Float? = nil, 26 | shadowOffset: CGSize? = nil, 27 | shadowPath: CGPath? = nil, 28 | shadowRadius: CGFloat? = nil) { 29 | self.shadowColor = shadowColor 30 | self.shadowOpacity = shadowOpacity 31 | self.shadowOffset = shadowOffset 32 | self.shadowPath = shadowPath 33 | self.shadowRadius = shadowRadius 34 | } 35 | 36 | public init(fromLayer layer: CALayer) { 37 | self.init(shadowColor: layer.shadowColor, 38 | shadowOpacity: layer.shadowOpacity, 39 | shadowOffset: layer.shadowOffset, 40 | shadowPath: layer.shadowPath, 41 | shadowRadius: layer.shadowRadius) 42 | } 43 | 44 | // open func apply(toView view: NSView) { 45 | // apply(toLayer: view.layer) 46 | // } 47 | 48 | #if os(iOS) 49 | 50 | public func apply(toView view: UIView) { 51 | apply(toLayer: view.layer) 52 | } 53 | 54 | #endif 55 | 56 | public func apply(toLayer layer: CALayer) { 57 | // nil-able properties... 58 | layer.shadowColor = shadowColor 59 | layer.shadowPath = shadowPath 60 | 61 | // Non-nil properties. 62 | if let shadowOpacity = self.shadowOpacity { layer.shadowOpacity = shadowOpacity } 63 | if let shadowOffset = self.shadowOffset { layer.shadowOffset = shadowOffset } 64 | if let shadowRadius = self.shadowRadius { layer.shadowRadius = shadowRadius } 65 | } 66 | 67 | public mutating func copy(fromLayer layer: CALayer) { 68 | shadowColor = layer.shadowColor 69 | shadowOpacity = layer.shadowOpacity 70 | shadowOffset = layer.shadowOffset 71 | shadowPath = layer.shadowPath 72 | shadowRadius = layer.shadowRadius 73 | } 74 | } 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Apple API Extensions/SpriteKit/SKView+OctopusKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SKView+OctopusKit.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2017/10/07. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import SpriteKit 10 | 11 | extension SKView { 12 | 13 | /// Sets the visibility of all debugging-related information and overlays. 14 | @inlinable 15 | public func setAllDebugStatsVisibility(to visibility: Bool) { 16 | self.showsFPS = visibility 17 | self.showsDrawCount = visibility 18 | self.showsFields = visibility 19 | self.showsNodeCount = visibility 20 | self.showsPhysics = visibility 21 | self.showsQuadCount = visibility 22 | } 23 | } 24 | 25 | #if os(tvOS) // MARK: - tvOS 26 | 27 | extension SKView { 28 | 29 | // This extension forwards focus and press-related events from the view to the scene, to ensure SpriteKit interaction is correctly handled within a SwiftUI view hierarchy. 30 | // SKView was found to be the appropriate object for these instead of OKViewController 31 | 32 | // CHECK: Should this be applied for all operating systems to handle game controller input as well? 33 | 34 | /// Defers to the `scene` or returns `false` if `scene` is `nil`. 35 | open override var canBecomeFocused: Bool { 36 | scene?.canBecomeFocused ?? false 37 | } 38 | 39 | /// Defers to the `scene` or returns `[parentFocusEnvironment ?? self]` if `scene` is `nil`. 40 | open override var preferredFocusEnvironments: [UIFocusEnvironment] { 41 | scene?.preferredFocusEnvironments ?? [self.parentFocusEnvironment ?? self] 42 | } 43 | 44 | /// Forwards the event to the `scene`. 45 | open override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { 46 | scene?.pressesBegan(presses, with: event) 47 | } 48 | 49 | /// Forwards the event to the `scene`. 50 | open override func pressesEnded(_ presses: Set, with event: UIPressesEvent?) { 51 | scene?.pressesEnded(presses, with: event) 52 | } 53 | 54 | /// Forwards the event to the `scene`. 55 | open override func pressesChanged(_ presses: Set, with event: UIPressesEvent?) { 56 | self.scene?.pressesChanged(presses, with: event) 57 | } 58 | 59 | /// Forwards the event to the `scene`. 60 | open override func pressesCancelled(_ presses: Set, with event: UIPressesEvent?) { 61 | self.scene?.pressesCancelled(presses, with: event) 62 | } 63 | } 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /Sources/OctopusKit/Support & Utility/Protocols/TurnBased.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TurnBased.swift 3 | // OctopusKit 4 | // 5 | // Created by ShinryakuTako@invadingoctopus.io on 2020/04/07. 6 | // Copyright © 2020 Invading Octopus. Licensed under Apache License v2.0 (see LICENSE.txt) 7 | // 8 | 9 | import Foundation 10 | 11 | /// A protocol for objects in a turn-based system. 12 | public protocol TurnBased { 13 | 14 | /// A flag that may be implemented by subclasses, e.g. to prevent multiple calls of `beginTurn()` during a single cycle. 15 | /// 16 | /// - Note: Using `OKGameState` may be more suitable for managing the begin/update/end turn cycle. See `OKTurnBasedComponent` comments for tips. 17 | var disallowBeginTurn: Bool { get } 18 | 19 | /// A flag that may be implemented by subclasses, e.g. to prevent multiple calls of `updateTurn()` during a single cycle. 20 | /// 21 | /// - Note: Using `OKGameState` may be more suitable for managing the begin/update/end turn cycle. See `OKTurnBasedComponent` comments for tips. 22 | var disallowUpdateTurn: Bool { get } 23 | 24 | /// A flag that may be implemented by subclasses, e.g. to prevent multiple calls of `endTurn()` during a single cycle. 25 | /// 26 | /// - Note: Using `OKGameState` may be more suitable for managing the begin/update/end turn cycle. See `OKTurnBasedComponent` comments for tips. 27 | var disallowEndTurn: Bool { get } 28 | 29 | // *Abstract; override in subclass.* Use this to perform any tasks that must occur at the beginning of each turn, *before* `updateTurn(delta:)`, such as health regeneration effects. 30 | /// 31 | /// - Parameter turns: The number of turns passed since the previous update. 32 | func beginTurn(delta turns: Int) 33 | 34 | /// *Abstract; override in subclass.* 35 | /// 36 | /// - Parameter turns: The number of turns passed since the previous update. 37 | func updateTurn(delta turns: Int) 38 | 39 | /// *Abstract; override in subclass.* Use this to perform any tasks that must occur at the end of each turn, *after* `updateTurn(delta:)`, such as damage-over-time effects. 40 | /// 41 | /// - Parameter turns: The number of turns passed since the previous update. 42 | func endTurn(delta turns: Int) 43 | } 44 | 45 | public extension TurnBased { 46 | 47 | /// ℹ️ This extension basically allows conforming types to skip the implementation of these flags, like `OKTurnBasedEntity`. 48 | 49 | var disallowBeginTurn: Bool { false } 50 | 51 | var disallowUpdateTurn: Bool { false } 52 | 53 | var disallowEndTurn: Bool { false } 54 | } 55 | --------------------------------------------------------------------------------