├── .gitignore ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ ├── RedECS-Package.xcscheme │ ├── RedECS.xcscheme │ ├── RedECSAppleSupport.xcscheme │ ├── RedECSBasicComponents.xcscheme │ ├── RedECSKit.xcscheme │ ├── RedECSUIComponents.xcscheme │ ├── RedECSWebSupport.xcscheme │ └── TiledInterpreter.xcscheme ├── Package.resolved ├── Package.swift ├── README.md ├── RedECSExample ├── RedECSExample Shared │ ├── Actions.sks │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── GameScene.sks │ └── GameScene.swift ├── RedECSExample iOS │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── GameViewController.swift │ └── Info.plist ├── RedECSExample macOS │ ├── AppDelegate.swift │ ├── Base.lproj │ │ └── Main.storyboard │ ├── GameViewController.swift │ ├── Info.plist │ └── RedECSExample_macOS.entitlements └── RedECSExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ └── xcschemes │ └── RedECSExample macOS.xcscheme ├── Sources ├── RedECS │ ├── Component │ │ ├── AnyComponent.swift │ │ └── GameComponent.swift │ ├── Entity │ │ ├── EntityEvent.swift │ │ ├── EntityRepository.swift │ │ └── GameEntity.swift │ ├── GameState.swift │ ├── Reducer │ │ ├── Reducer.swift │ │ └── Reducers │ │ │ ├── AnyReducer.swift │ │ │ ├── Filter.swift │ │ │ ├── Pullback.swift │ │ │ ├── Resending.swift │ │ │ ├── Throttle.swift │ │ │ └── Zip.swift │ ├── Rendering │ │ ├── Camera │ │ │ └── CameraComponent.swift │ │ ├── Color.swift │ │ ├── Label │ │ │ └── BitmapFont.swift │ │ ├── RenderTriangle.swift │ │ ├── RenderableComponent.swift │ │ ├── Renderer.swift │ │ ├── RenderingEnvironment.swift │ │ ├── Shape │ │ │ └── Shape+Rect.swift │ │ ├── Sprite │ │ │ ├── SpriteAnimatingReducer.swift │ │ │ ├── SpriteAnimationDictionary.swift │ │ │ ├── SpriteComponent.swift │ │ │ └── SpriteContext.swift │ │ ├── Texture │ │ │ ├── TextureId.swift │ │ │ ├── TextureMap.swift │ │ │ └── TextureReference.swift │ │ └── Transform │ │ │ └── TransformComponent.swift │ ├── ResourceManager.swift │ ├── Store │ │ ├── GameEffect.swift │ │ ├── GameStore.swift │ │ ├── PendingGameEffect.swift │ │ └── SystemAction.swift │ └── Utilities │ │ ├── Extensions │ │ └── Matrix3+Projection.swift │ │ └── Future.swift ├── RedECSAppleSupport │ ├── GameStore+JSONCoding.swift │ ├── MetalEnvironment.swift │ ├── MetalRenderer.swift │ ├── MetalResourceManager.swift │ ├── MetalViewController.swift │ └── Shaders.metal ├── RedECSBasicComponents │ ├── Input │ │ ├── KeyboardInputComponent.swift │ │ └── KeyboardInputReducer.swift │ ├── InteractionComponent.swift │ ├── Momentum │ │ ├── MomentumComponent.swift │ │ └── MomentumReducer.swift │ ├── Movement │ │ ├── MovementComponent.swift │ │ └── MovementReducer.swift │ ├── Operation │ │ ├── BasicOperationComponentContext.swift │ │ ├── Operation.swift │ │ ├── OperationComponent.swift │ │ ├── OperationReducer.swift │ │ ├── OperationType.swift │ │ └── OperationTypes │ │ │ ├── AnimateOperation.swift │ │ │ ├── CallOperation.swift │ │ │ ├── GroupOperation.swift │ │ │ ├── MoveOperation.swift │ │ │ ├── OpacityOperation.swift │ │ │ ├── RepeatOperation.swift │ │ │ ├── RotateOperation.swift │ │ │ ├── ScaleOperation.swift │ │ │ ├── SequenceOperation.swift │ │ │ ├── TimingOperation.swift │ │ │ ├── VisibilityOperation.swift │ │ │ └── WaitOperation.swift │ ├── Pathing │ │ ├── PathingComponent.swift │ │ └── PathingReducer.swift │ └── ResourceLoading │ │ ├── ResourceLoadingAction.swift │ │ └── ResourceLoadingReducer.swift ├── RedECSKit │ └── RedECSKit.swift ├── RedECSUIComponents │ └── HUD │ │ ├── HUDComponent.swift │ │ └── HUDRenderingContext.swift ├── RedECSWebSupport │ ├── WebBrowserKeyboardInput.swift │ ├── WebBrowserWindow.swift │ ├── WebEnvironment.swift │ ├── WebGL │ │ ├── Draw2DProgram.swift │ │ └── WebGLProgram.swift │ ├── WebHUDRenderingReducer.swift │ ├── WebRenderer.swift │ └── WebResourceManager.swift └── TiledInterpreter │ ├── TiledMap │ ├── TiledLayer.swift │ ├── TiledLayerType.swift │ ├── TiledMapJSON.swift │ ├── TiledObject.swift │ └── TiledText.swift │ └── TiledTileSet │ ├── Tile.swift │ ├── TiledTilesetJSON.swift │ └── TiledTilesetXML.swift ├── Tests ├── RedECSTests │ ├── RedECSTests.swift │ └── TestSystem │ │ └── TestSystem.swift ├── RenderingTests │ ├── BitmapFontTests.swift │ ├── CameraRenderingTests.swift │ ├── HitTestingTests.swift │ ├── MetalRenderingTests.swift │ ├── Resources │ │ ├── Media.xcassets │ │ │ ├── Contents.json │ │ │ └── pt-mono.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── pt-mono.png │ │ └── pt-mono.fnt │ ├── Utilities │ │ ├── EnqueueGrid.swift │ │ ├── MTKView+Snapshotting.swift │ │ ├── Point+Rounded.swift │ │ └── RenderingTestSystem.swift │ └── __Snapshots__ │ │ ├── BitmapFontTests │ │ ├── snapshotText-_.Bitmap-Font.png │ │ ├── snapshotText-_.Chars-_.png │ │ └── snapshotText-_.Welcome.png │ │ ├── CameraRenderingTests │ │ ├── testCameraRender.first-pass.png │ │ ├── testCameraRender.second-pass.png │ │ ├── testCameraRenderOffset.1.png │ │ ├── testCameraRenderZoom.1.png │ │ └── testCameraRenderZoom.temp.png │ │ ├── HitTestingTests │ │ ├── testCameraRenderZoomWithObjectTranslate.1.png │ │ ├── testShapeContainsPoint.1.png │ │ ├── testShapePointContainmentWhenTransformedFromCameraSpace.after-zoom.png │ │ ├── testShapePointContainmentWhenTransformedFromCameraSpace.before-zoom.png │ │ ├── testShapeTransformAndRotateContainsPoint.1.png │ │ ├── testShapeTransformAndRotateContainsPointAtCenter.1.png │ │ ├── testShapeTransformAndRotateContainsPointAtZero.1.png │ │ └── testShapeTransformAndRotateDoesNotContainPoint.1.png │ │ └── MetalRenderingTests │ │ ├── testProjectionMatrix.normal.png │ │ ├── testProjectionMatrix.scale-down.png │ │ ├── testProjectionMatrix.scaled-translated.png │ │ ├── testProjectionMatrix.translated.png │ │ ├── testTriangle.1.png │ │ ├── testTriangleRotatedAround0_0AnchorPoint.1.png │ │ ├── testTriangleRotatedAround0_5_0_5AnchorPoint.1.png │ │ └── testTriangleRotatedAround1_1AnchorPoint.1.png └── TiledInterpreterTests │ ├── TestMap.png │ ├── TestMap.tmj │ ├── TiledInterpreterTests.swift │ ├── __Snapshots__ │ └── TiledInterpreterTests │ │ └── testMapGeneration.1.png │ ├── dungeon.tsj │ └── tiles_dungeon.png ├── asteroids.gif ├── breakout.gif ├── getting-started.md ├── redecs-breakdown-1.png └── rpg.gif /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/RedECS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/RedECSAppleSupport.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/RedECSBasicComponents.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/RedECSKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/RedECSUIComponents.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/RedECSWebSupport.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/TiledInterpreter.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Geometry", 6 | "repositoryURL": "git@github.com:RedECSEngine/Geometry.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "7c9ba07e4ee2e09ab8454870c8ede2ad96e544f6", 10 | "version": "0.0.4" 11 | } 12 | }, 13 | { 14 | "package": "JavaScriptKit", 15 | "repositoryURL": "https://github.com/swiftwasm/JavaScriptKit", 16 | "state": { 17 | "branch": null, 18 | "revision": "9a3b7d5f316e6d1709bd25e3e0392efca357f525", 19 | "version": "0.14.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-collections", 24 | "repositoryURL": "git@github.com:apple/swift-collections.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", 28 | "version": "1.0.2" 29 | } 30 | }, 31 | { 32 | "package": "swift-numerics", 33 | "repositoryURL": "https://github.com/apple/swift-numerics.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "6583ac70c326c3ee080c1d42d9ca3361dca816cd", 37 | "version": "0.1.0" 38 | } 39 | }, 40 | { 41 | "package": "SnapshotTesting", 42 | "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "f8a9c997c3c1dab4e216a8ec9014e23144cbab37", 46 | "version": "1.9.0" 47 | } 48 | } 49 | ] 50 | }, 51 | "version": 1 52 | } 53 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "RedECS", 8 | platforms: [ 9 | .macOS(.v11), 10 | .iOS(.v14), 11 | .tvOS(.v14) 12 | ], 13 | products: [ 14 | // Products define the executables and libraries a package produces, and make them visible to other packages. 15 | .library(name: "RedECSKit", targets: ["RedECSKit"]), 16 | .library( 17 | name: "RedECS", 18 | targets: ["RedECS"] 19 | ), 20 | .library( 21 | name: "RedECSBasicComponents", 22 | targets: ["RedECSBasicComponents"] 23 | ), 24 | .library( 25 | name: "RedECSUIComponents", 26 | targets: ["RedECSUIComponents"] 27 | ), 28 | 29 | .library( 30 | name: "RedECSAppleSupport", 31 | targets: ["RedECSAppleSupport"] 32 | ), 33 | .library( 34 | name: "RedECSWebSupport", 35 | targets: ["RedECSWebSupport"] 36 | ), 37 | 38 | .library( 39 | name: "TiledInterpreter", 40 | targets: ["TiledInterpreter"] 41 | ), 42 | ], 43 | dependencies: [ 44 | .package( 45 | name: "JavaScriptKit", 46 | url: "https://github.com/swiftwasm/JavaScriptKit", 47 | from: "0.13.0" 48 | ), 49 | .package( 50 | url: "git@github.com:RedECSEngine/Geometry.git", 51 | from: "0.0.4" 52 | ), 53 | // .package(path: "../Geometry"), 54 | // .package(url: "git@github.com:RedECSEngine/Geometry.git", .branch("develop")), 55 | 56 | .package( 57 | url: "git@github.com:apple/swift-collections.git", 58 | from: "1.0.2" 59 | ), 60 | 61 | .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.9.0"), 62 | ], 63 | targets: [ 64 | .target( 65 | name: "RedECSKit", 66 | dependencies: [ 67 | "RedECS", 68 | "RedECSBasicComponents", 69 | "RedECSUIComponents" 70 | ] 71 | ), 72 | 73 | .target( 74 | name: "RedECS", 75 | dependencies: [ 76 | .product(name: "Geometry", package: "Geometry"), 77 | .product(name: "GeometryAlgorithms", package: "Geometry"), 78 | .product(name: "OrderedCollections", package: "swift-collections"), 79 | "TiledInterpreter", 80 | ] 81 | ), 82 | .target( 83 | name: "RedECSBasicComponents", 84 | dependencies: ["RedECS"] 85 | ), 86 | .target( 87 | name: "RedECSUIComponents", 88 | dependencies: ["RedECS", "RedECSBasicComponents"] 89 | ), 90 | 91 | .target( 92 | name: "RedECSAppleSupport", 93 | dependencies: ["RedECSKit"] 94 | ), 95 | .target( 96 | name: "RedECSWebSupport", 97 | dependencies: [ 98 | "RedECSKit", 99 | .product(name: "JavaScriptKit", package: "JavaScriptKit") 100 | ] 101 | ), 102 | 103 | .target( 104 | name: "TiledInterpreter", 105 | dependencies: [] 106 | ), 107 | 108 | .testTarget( 109 | name: "RedECSTests", 110 | dependencies: ["RedECS", "RedECSBasicComponents", "RedECSAppleSupport"] 111 | ), 112 | .testTarget( 113 | name: "RenderingTests", 114 | dependencies: [ 115 | "RedECS", 116 | "RedECSAppleSupport", 117 | .product(name: "SnapshotTesting", package: "swift-snapshot-testing") 118 | ], 119 | resources: [ 120 | .process("Resources") 121 | ] 122 | ), 123 | .testTarget( 124 | name: "TiledInterpreterTests", 125 | dependencies: [ 126 | "TiledInterpreter", 127 | .product(name: "SnapshotTesting", package: "swift-snapshot-testing") 128 | ] 129 | ), 130 | ] 131 | ) 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RedECS 2 | 3 | A Swift Entity Component System. Inspired by The Composable Architecture and focused on cross-platform. 4 | 5 | 6 | 7 | 8 | 9 | ## Current Supported Platforms 10 | 11 | - iOS, tvOS, macOS 12 | - Web 13 | 14 | ### Note: This is still under heavy development so documentation is sparse 15 | 16 | ## Features 17 | - Highly modular Entity Component System 18 | - Fully `Codable` game state 19 | - Separation of State and Game Logic through composable reducers. 20 | - Cross-platform, trying to have equivalents for all SpriteKit/GameplayKit capabilities (within reason) 21 | 22 | ## Who is this Game Engine for? 23 | - People who love Swift, first and foremost. There are a lot of options out there with far more advanced capabilities, so if you don't love Swift, you may not see a point 24 | - Hobby Game developers, at least while this is under development for a while 25 | - People who want to publish fun little things written in Swift to all platforms. The engine is likely to prioritize cross compatibility over performance or depth of capabilities. 26 | - People looking for a Swift game engine to tinker with, contribute to, help measure, build and turn this into something that maybe changes who this game engine is for entirely. How meta. 27 | 28 | ## Tutorials 29 | - [Getting Started](getting-started.md) 30 | - [Starter Template](https://github.com/RedECSEngine/starter-template) 31 | 32 | ## Architecture 33 | 34 | The engine's architecture is highly inspired by The Composable Architecture, but a gaming-focused flavour. 35 | 36 | 37 | 38 | 39 | ## Roadmap Ideas (unprioritized) 40 | 41 | - Review best capabilities of SpriteKit and Cocos2D-iPhone and determine what this engine should provide 42 | - Develop more components/reducers and algorithms to match GameplayKit capabilities and other common gaming problems 43 | - Investigate Windows support 44 | - CLI tooling 45 | - Significantly improve resource management 46 | - CodeGen to reduce boilerplate, help with templates 47 | - GUI Editor 48 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample Shared/Actions.sks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/RedECSExample/RedECSExample Shared/Actions.sks -------------------------------------------------------------------------------- /RedECSExample/RedECSExample Shared/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample Shared/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | }, 93 | { 94 | "idiom" : "mac", 95 | "scale" : "1x", 96 | "size" : "16x16" 97 | }, 98 | { 99 | "idiom" : "mac", 100 | "scale" : "2x", 101 | "size" : "16x16" 102 | }, 103 | { 104 | "idiom" : "mac", 105 | "scale" : "1x", 106 | "size" : "32x32" 107 | }, 108 | { 109 | "idiom" : "mac", 110 | "scale" : "2x", 111 | "size" : "32x32" 112 | }, 113 | { 114 | "idiom" : "mac", 115 | "scale" : "1x", 116 | "size" : "128x128" 117 | }, 118 | { 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "128x128" 122 | }, 123 | { 124 | "idiom" : "mac", 125 | "scale" : "1x", 126 | "size" : "256x256" 127 | }, 128 | { 129 | "idiom" : "mac", 130 | "scale" : "2x", 131 | "size" : "256x256" 132 | }, 133 | { 134 | "idiom" : "mac", 135 | "scale" : "1x", 136 | "size" : "512x512" 137 | }, 138 | { 139 | "idiom" : "mac", 140 | "scale" : "2x", 141 | "size" : "512x512" 142 | } 143 | ], 144 | "info" : { 145 | "author" : "xcode", 146 | "version" : 1 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample Shared/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample Shared/GameScene.sks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/RedECSExample/RedECSExample Shared/GameScene.sks -------------------------------------------------------------------------------- /RedECSExample/RedECSExample Shared/GameScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameScene.swift 3 | // RedECSExample Shared 4 | // 5 | // Created by Kyle Newsome on 2021-06-15. 6 | // 7 | 8 | import SpriteKit 9 | 10 | class GameScene: SKScene { 11 | 12 | 13 | fileprivate var label : SKLabelNode? 14 | fileprivate var spinnyNode : SKShapeNode? 15 | 16 | 17 | class func newGameScene() -> GameScene { 18 | // Load 'GameScene.sks' as an SKScene. 19 | guard let scene = SKScene(fileNamed: "GameScene") as? GameScene else { 20 | print("Failed to load GameScene.sks") 21 | abort() 22 | } 23 | 24 | // Set the scale mode to scale to fit the window 25 | scene.scaleMode = .aspectFill 26 | 27 | return scene 28 | } 29 | 30 | func setUpScene() { 31 | // Get label node from scene and store it for use later 32 | self.label = self.childNode(withName: "//helloLabel") as? SKLabelNode 33 | if let label = self.label { 34 | label.alpha = 0.0 35 | label.run(SKAction.fadeIn(withDuration: 2.0)) 36 | } 37 | 38 | // Create shape node to use during mouse interaction 39 | let w = (self.size.width + self.size.height) * 0.05 40 | self.spinnyNode = SKShapeNode.init(rectOf: CGSize.init(width: w, height: w), cornerRadius: w * 0.3) 41 | 42 | if let spinnyNode = self.spinnyNode { 43 | spinnyNode.lineWidth = 4.0 44 | spinnyNode.run(SKAction.repeatForever(SKAction.rotate(byAngle: CGFloat(Double.pi), duration: 1))) 45 | spinnyNode.run(SKAction.sequence([SKAction.wait(forDuration: 0.5), 46 | SKAction.fadeOut(withDuration: 0.5), 47 | SKAction.removeFromParent()])) 48 | 49 | #if os(watchOS) 50 | // For watch we just periodically create one of these and let it spin 51 | // For other platforms we let user touch/mouse events create these 52 | spinnyNode.position = CGPoint(x: 0.0, y: 0.0) 53 | spinnyNode.strokeColor = SKColor.red 54 | self.run(SKAction.repeatForever(SKAction.sequence([SKAction.wait(forDuration: 2.0), 55 | SKAction.run({ 56 | let n = spinnyNode.copy() as! SKShapeNode 57 | self.addChild(n) 58 | })]))) 59 | #endif 60 | } 61 | } 62 | 63 | #if os(watchOS) 64 | override func sceneDidLoad() { 65 | self.setUpScene() 66 | } 67 | #else 68 | override func didMove(to view: SKView) { 69 | self.setUpScene() 70 | } 71 | #endif 72 | 73 | func makeSpinny(at pos: CGPoint, color: SKColor) { 74 | if let spinny = self.spinnyNode?.copy() as! SKShapeNode? { 75 | spinny.position = pos 76 | spinny.strokeColor = color 77 | self.addChild(spinny) 78 | } 79 | } 80 | 81 | override func update(_ currentTime: TimeInterval) { 82 | // Called before each frame is rendered 83 | } 84 | } 85 | 86 | #if os(iOS) || os(tvOS) 87 | // Touch-based event handling 88 | extension GameScene { 89 | 90 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 91 | if let label = self.label { 92 | label.run(SKAction.init(named: "Pulse")!, withKey: "fadeInOut") 93 | } 94 | 95 | for t in touches { 96 | self.makeSpinny(at: t.location(in: self), color: SKColor.green) 97 | } 98 | } 99 | 100 | override func touchesMoved(_ touches: Set, with event: UIEvent?) { 101 | for t in touches { 102 | self.makeSpinny(at: t.location(in: self), color: SKColor.blue) 103 | } 104 | } 105 | 106 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 107 | for t in touches { 108 | self.makeSpinny(at: t.location(in: self), color: SKColor.red) 109 | } 110 | } 111 | 112 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 113 | for t in touches { 114 | self.makeSpinny(at: t.location(in: self), color: SKColor.red) 115 | } 116 | } 117 | 118 | 119 | } 120 | #endif 121 | 122 | #if os(OSX) 123 | // Mouse-based event handling 124 | extension GameScene { 125 | 126 | override func mouseDown(with event: NSEvent) { 127 | if let label = self.label { 128 | label.run(SKAction.init(named: "Pulse")!, withKey: "fadeInOut") 129 | } 130 | self.makeSpinny(at: event.location(in: self), color: SKColor.green) 131 | } 132 | 133 | override func mouseDragged(with event: NSEvent) { 134 | self.makeSpinny(at: event.location(in: self), color: SKColor.blue) 135 | } 136 | 137 | override func mouseUp(with event: NSEvent) { 138 | self.makeSpinny(at: event.location(in: self), color: SKColor.red) 139 | } 140 | 141 | } 142 | #endif 143 | 144 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RedECSExample iOS 4 | // 5 | // Created by Kyle Newsome on 2021-06-15. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample iOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample iOS/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample iOS/GameViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameViewController.swift 3 | // RedECSExample iOS 4 | // 5 | // Created by Kyle Newsome on 2021-06-15. 6 | // 7 | 8 | import UIKit 9 | import SpriteKit 10 | import GameplayKit 11 | 12 | class GameViewController: UIViewController { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | let scene = GameScene.newGameScene() 18 | 19 | // Present the scene 20 | let skView = self.view as! SKView 21 | skView.presentScene(scene) 22 | 23 | skView.ignoresSiblingOrder = true 24 | skView.showsFPS = true 25 | skView.showsNodeCount = true 26 | } 27 | 28 | override var shouldAutorotate: Bool { 29 | return true 30 | } 31 | 32 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask { 33 | if UIDevice.current.userInterfaceIdiom == .phone { 34 | return .allButUpsideDown 35 | } else { 36 | return .all 37 | } 38 | } 39 | 40 | override var prefersStatusBarHidden: Bool { 41 | return true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSupportsIndirectInputEvents 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIStatusBarHidden 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample macOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RedECSExample macOS 4 | // 5 | // Created by Kyle Newsome on 2021-06-15. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | 14 | 15 | func applicationDidFinishLaunching(_ aNotification: Notification) { 16 | // Insert code here to initialize your application 17 | } 18 | 19 | func applicationWillTerminate(_ aNotification: Notification) { 20 | // Insert code here to tear down your application 21 | } 22 | 23 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 24 | return true 25 | } 26 | 27 | 28 | } 29 | 30 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample macOS/GameViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameViewController.swift 3 | // RedECSExample macOS 4 | // 5 | // Created by Kyle Newsome on 2021-06-15. 6 | // 7 | 8 | import Cocoa 9 | import SpriteKit 10 | import GameplayKit 11 | import RedECSExamples 12 | import SwiftUI 13 | import SpriteKit 14 | 15 | struct SceneView: View { 16 | private var sceneType: T.Type 17 | @State private var isVisible: Bool = false 18 | 19 | init(sceneType: T.Type) { 20 | self.sceneType = sceneType 21 | } 22 | 23 | var body: some View { 24 | VStack { 25 | if isVisible { 26 | SpriteView( 27 | scene: sceneType.init(), 28 | options: [.shouldCullNonVisibleNodes, .ignoresSiblingOrder] 29 | ) 30 | } else { 31 | EmptyView() 32 | } 33 | } 34 | .onAppear { 35 | self.isVisible = true 36 | } 37 | .onDisappear { 38 | self.isVisible = false 39 | } 40 | } 41 | } 42 | 43 | struct AppView: View { 44 | var body: some View { 45 | NavigationView { 46 | List { 47 | NavigationLink( 48 | "ExampleScene1", 49 | destination: SceneView(sceneType: ExampleScene1.self) 50 | ) 51 | NavigationLink( 52 | "Separation", 53 | destination: SceneView(sceneType: SeparationExampleScene.self) 54 | ) 55 | NavigationLink( 56 | "Follow/Separation/Pathing", 57 | destination: SceneView(sceneType: FollowSeparationAndPathingExampleScene.self) 58 | ) 59 | NavigationLink( 60 | "Flocking", 61 | destination: SceneView(sceneType: FlockingExampleScene.self) 62 | ) 63 | NavigationLink( 64 | "Follow/Flock/Path", 65 | destination: SceneView(sceneType: FollowFlockingPathingExampleScene.self) 66 | ) 67 | NavigationLink( 68 | "Asteroids", 69 | destination: SceneView(sceneType: AsteroidsGameScene.self) 70 | ) 71 | } 72 | HStack { 73 | Spacer() 74 | Text("Select an Example").font(.largeTitle) 75 | Spacer() 76 | } 77 | } 78 | 79 | } 80 | } 81 | 82 | class GameViewController: NSViewController { 83 | 84 | let vc = NSHostingController(rootView: AppView()) 85 | 86 | override func viewDidLoad() { 87 | super.viewDidLoad() 88 | 89 | addChild(vc) 90 | view.addSubview(vc.view) 91 | 92 | 93 | // let scene = FlockingExampleScene() 94 | // 95 | // // Present the scene 96 | // let skView = self.view as! SKView 97 | // skView.presentScene(scene) 98 | // 99 | // skView.ignoresSiblingOrder = true 100 | // 101 | // skView.showsFPS = true 102 | // skView.showsNodeCount = true 103 | } 104 | 105 | override func viewWillLayout() { 106 | super.viewWillLayout() 107 | vc.view.frame = view.frame 108 | } 109 | 110 | } 111 | 112 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSMainStoryboardFile 26 | Main 27 | NSPrincipalClass 28 | NSApplication 29 | 30 | 31 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample macOS/RedECSExample_macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RedECSExample/RedECSExample.xcodeproj/xcshareddata/xcschemes/RedECSExample macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 61 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /Sources/RedECS/Component/AnyComponent.swift: -------------------------------------------------------------------------------- 1 | public typealias RegisteredComponentId = String 2 | 3 | public struct RegisteredComponentType: Identifiable { 4 | public let id: RegisteredComponentId 5 | public let onEntityDestroyed: (EntityId, inout S) -> Void 6 | 7 | public init(keyPath: WritableKeyPath) { 8 | id = String(describing: C.self) 9 | onEntityDestroyed = { entity, state in 10 | state[keyPath: keyPath][entity] = nil 11 | } 12 | } 13 | } 14 | 15 | extension RegisteredComponentType: Equatable { 16 | public static func == (lhs: RegisteredComponentType, rhs: RegisteredComponentType) -> Bool { 17 | lhs.id == rhs.id 18 | } 19 | } 20 | 21 | extension RegisteredComponentType: Hashable { 22 | public func hash(into hasher: inout Hasher) { 23 | hasher.combine(id) 24 | } 25 | } 26 | 27 | public struct AnyComponent { 28 | public let id: RegisteredComponentId 29 | public let onAdd: (EntityId, inout S) -> Void 30 | 31 | public init( 32 | _ component: C, 33 | into keyPath: WritableKeyPath 34 | ) { 35 | id = String(describing: C.self) 36 | onAdd = { entity, state in 37 | state[keyPath: keyPath][entity] = component 38 | } 39 | } 40 | 41 | public init( 42 | id: RegisteredComponentId, 43 | onAdd: @escaping (EntityId, inout S) -> Void 44 | ) { 45 | self.id = id 46 | self.onAdd = onAdd 47 | } 48 | 49 | public func map(_ stateTransform: WritableKeyPath) -> AnyComponent { 50 | return AnyComponent(id: id, onAdd: { entity, state in 51 | onAdd(entity, &state[keyPath: stateTransform]) 52 | }) 53 | } 54 | } 55 | 56 | extension AnyComponent: Equatable { 57 | public static func == (lhs: AnyComponent, rhs: AnyComponent) -> Bool { 58 | lhs.id == rhs.id 59 | } 60 | } 61 | 62 | extension AnyComponent: Hashable { 63 | public func hash(into hasher: inout Hasher) { 64 | hasher.combine(id) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/RedECS/Component/GameComponent.swift: -------------------------------------------------------------------------------- 1 | public protocol GameComponent: Codable, Equatable { 2 | var entity: EntityId { get } 3 | 4 | init(entity: EntityId) 5 | } 6 | -------------------------------------------------------------------------------- /Sources/RedECS/Entity/EntityEvent.swift: -------------------------------------------------------------------------------- 1 | public enum EntityEvent: Equatable { 2 | case added(EntityId) 3 | case removed(EntityId) 4 | } 5 | -------------------------------------------------------------------------------- /Sources/RedECS/Entity/EntityRepository.swift: -------------------------------------------------------------------------------- 1 | public struct EntityRepository: Equatable, Codable { 2 | public struct Constants { 3 | public static let rootTreeId = "Root" 4 | } 5 | 6 | public private(set) var entities: [EntityId: GameEntity] = [:] 7 | public private(set) var tags: [String: Set] = [:] 8 | public private(set) var tree: EntityTree = .init(id: Constants.rootTreeId) // TODO: use for rendering to power transform and show/hide capabilities 9 | 10 | public init() { } 11 | 12 | public subscript(index: EntityId) -> GameEntity? { 13 | get { 14 | entities[index] 15 | } 16 | } 17 | 18 | public var entityIds: Dictionary.Keys { 19 | entities.keys 20 | } 21 | } 22 | 23 | // MARK: - Entity Lifecycle 24 | 25 | public extension EntityRepository { 26 | mutating func addEntity(_ e: GameEntity) { 27 | assert(entities[e.id] == nil, "adding duplicate entity \(e.id)") 28 | var e = e 29 | e.tags.forEach { tag in 30 | tags[tag, default: []].insert(e.id) 31 | } 32 | 33 | var parentId = Constants.rootTreeId 34 | if let entityParent = e.parentId { 35 | parentId = entityParent 36 | } 37 | var tree = self.tree 38 | let result = insertEntity(e.id, intoTree: &tree, withParent: parentId) 39 | self.tree = tree 40 | assert(result, "Failed to find parent in tree '\(parentId)'") 41 | e.parentId = parentId 42 | entities[e.id] = e 43 | } 44 | 45 | mutating func removeEntity(_ id: EntityId) { 46 | // assert(entities[id] != nil, "removing already removed entity") 47 | entities[id]?.tags.forEach { tag in 48 | tags[tag]?.remove(id) 49 | } 50 | var tree = self.tree 51 | _ = removeEntity(id, fromTree: &tree) 52 | self.tree = tree 53 | entities[id] = nil 54 | } 55 | } 56 | 57 | // MARK: - Tree Management 58 | 59 | public extension EntityRepository { 60 | mutating func insertEntity( 61 | _ eId: EntityId, 62 | intoTree tree: inout EntityTree, 63 | withParent pId: EntityId 64 | ) -> Bool { 65 | if tree.id == pId { 66 | tree.addChild(EntityTree(id: eId)) 67 | return true 68 | } else { 69 | for i in 0.. Bool { 102 | for i in 0.. EntityId { 4 | let prefix = prefix.map({ "\($0)-" }) ?? "" 5 | return prefix + "\(Int.random(in: 0...Int.max))" + "\(Int.random(in: 0...Int.max))" // TODO: weak implementation of a uuid string, needs improvement 6 | } 7 | 8 | public struct GameEntity: Hashable, Identifiable, Codable { 9 | public var id: EntityId 10 | public var parentId: EntityId? 11 | public var tags: Set 12 | 13 | public init( 14 | id: EntityId, 15 | tags: Set 16 | ) { 17 | self.id = id 18 | self.tags = tags 19 | } 20 | } 21 | 22 | public struct EntityTree: Hashable, Codable, Identifiable { 23 | public var id: EntityId 24 | public var children: [EntityTree]? 25 | 26 | public var childCount: Int { children?.count ?? 0 } 27 | 28 | mutating func addChild(_ c: EntityTree) { 29 | if children == nil { 30 | children = [c] 31 | } else { 32 | children?.append(c) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/RedECS/GameState.swift: -------------------------------------------------------------------------------- 1 | public protocol GameState: Codable, Equatable { 2 | var entities: EntityRepository { get set } 3 | } 4 | 5 | public extension GameState { 6 | mutating func modify( 7 | _ keyPath: WritableKeyPath, 8 | ofEntity entityId: EntityId, 9 | modifyBlock: (inout C) -> Void 10 | ) { 11 | if var component = self[keyPath: keyPath][entityId] { 12 | modifyBlock(&component) 13 | self[keyPath: keyPath][entityId] = component 14 | } else { 15 | assertionFailure("Component not found \(C.self) for \(entityId)") 16 | } 17 | } 18 | 19 | mutating func modify( 20 | _ keyPath1: WritableKeyPath, 21 | _ keyPath2: WritableKeyPath, 22 | ofEntity entityId: EntityId, 23 | modifyBlock: (inout C1, inout C2) -> Void 24 | ) { 25 | if var component1 = self[keyPath: keyPath1][entityId], 26 | var component2 = self[keyPath: keyPath2][entityId] { 27 | modifyBlock(&component1, &component2) 28 | self[keyPath: keyPath1][entityId] = component1 29 | self[keyPath: keyPath2][entityId] = component2 30 | } else { 31 | assertionFailure("One or more components not found \(C1.self), \(C2.self) for \(entityId)") 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/RedECS/Reducer/Reducer.swift: -------------------------------------------------------------------------------- 1 | public protocol Reducer { 2 | associatedtype State: GameState 3 | associatedtype Action: Equatable 4 | associatedtype Environment 5 | 6 | func reduce(state: inout State, delta: Double, environment: Environment) -> GameEffect 7 | func reduce(state: inout State, action: Action, environment: Environment) -> GameEffect 8 | func reduce(state: inout State, entityEvent: EntityEvent, environment: Environment) -> GameEffect 9 | } 10 | 11 | public extension Reducer { 12 | func reduce(state: inout State, entityEvent: EntityEvent, environment: Environment) -> GameEffect { 13 | return .none 14 | } 15 | } 16 | 17 | 18 | public extension Reducer where Action == Never { 19 | func reduce(state: inout State, action: Action, environment: Environment) -> GameEffect { 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/RedECS/Reducer/Reducers/AnyReducer.swift: -------------------------------------------------------------------------------- 1 | public struct AnyReducer: Reducer { 2 | 3 | var reduceDelta: (inout State, Double, Environment) -> GameEffect 4 | var reduceAction: (inout State, Action, Environment) -> GameEffect 5 | var reduceEntityEvent: (inout State, EntityEvent, Environment) -> GameEffect 6 | 7 | public init( 8 | _ reduceDelta: @escaping (inout State, Double, Environment) -> GameEffect, 9 | _ reduceAction: @escaping (inout State, Action, Environment) -> GameEffect, 10 | _ reduceEntityEvent: @escaping (inout State, EntityEvent, Environment) -> GameEffect 11 | ) { 12 | self.reduceDelta = reduceDelta 13 | self.reduceAction = reduceAction 14 | self.reduceEntityEvent = reduceEntityEvent 15 | } 16 | 17 | public init(_ reducer: R) 18 | where R.State == State, 19 | R.Action == Action, 20 | R.Environment == Environment 21 | { 22 | self.reduceDelta = reducer.reduce(state:delta:environment:) 23 | self.reduceAction = reducer.reduce(state:action:environment:) 24 | self.reduceEntityEvent = reducer.reduce(state:entityEvent:environment:) 25 | } 26 | 27 | public static var noop: Self { 28 | AnyReducer({ _, _, _ in .none }, { _, _, _ in .none }, { _, _, _ in .none }) 29 | } 30 | 31 | public func reduce(state: inout State, delta: Double, environment: Environment) -> GameEffect { 32 | reduceDelta(&state, delta, environment) 33 | } 34 | 35 | public func reduce(state: inout State, action: Action, environment: Environment) -> GameEffect { 36 | reduceAction(&state, action, environment) 37 | } 38 | 39 | public func reduce(state: inout State, entityEvent: EntityEvent, environment: Environment) -> GameEffect { 40 | reduceEntityEvent(&state, entityEvent, environment) 41 | } 42 | } 43 | 44 | public extension Reducer { 45 | func eraseToAnyReducer() -> AnyReducer { 46 | AnyReducer(self) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/RedECS/Reducer/Reducers/Filter.swift: -------------------------------------------------------------------------------- 1 | public struct Filter< 2 | R: Reducer 3 | > : Reducer { 4 | var reducer: R 5 | var predicate: (R.State, R.Action?) -> Bool 6 | 7 | public init(reducer: R, predicate: @escaping (R.State, R.Action?) -> Bool) { 8 | self.reducer = reducer 9 | self.predicate = predicate 10 | } 11 | 12 | public func reduce( 13 | state: inout R.State, 14 | action: R.Action, 15 | environment: R.Environment 16 | ) -> GameEffect { 17 | guard predicate(state, action) else { return .none } 18 | return reducer.reduce( 19 | state: &state, 20 | action: action, 21 | environment: environment 22 | ) 23 | } 24 | 25 | public func reduce( 26 | state: inout R.State, 27 | delta: Double, 28 | environment: R.Environment 29 | ) -> GameEffect { 30 | guard predicate(state, nil) else { return .none } 31 | return reducer.reduce( 32 | state: &state, 33 | delta: delta, 34 | environment: environment 35 | ) 36 | } 37 | 38 | public func reduce( 39 | state: inout R.State, 40 | entityEvent: EntityEvent, 41 | environment: R.Environment 42 | ) -> GameEffect { 43 | guard predicate(state, nil) else { return .none } 44 | return reducer.reduce( 45 | state: &state, 46 | entityEvent: entityEvent, 47 | environment: environment 48 | ) 49 | } 50 | } 51 | 52 | public extension Reducer { 53 | func filter( 54 | _ predicate: @escaping (State, Action?) -> Bool 55 | ) -> Filter { 56 | return Filter(reducer: self, predicate: predicate) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/RedECS/Reducer/Reducers/Resending.swift: -------------------------------------------------------------------------------- 1 | public extension Reducer { 2 | func resending( 3 | _ transform: @escaping (Action) -> Action? 4 | ) -> AnyReducer { 5 | return AnyReducer( 6 | { _, _, _ in .none }, 7 | { state, action, env in 8 | let effects = self.reduce(state: &state, action: action, environment: env) 9 | if let resultingAction = transform(action) { 10 | return .many([ 11 | effects, 12 | .game(resultingAction) 13 | ]) 14 | } 15 | return effects 16 | }, 17 | { _,_,_ in .none } 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/RedECS/Reducer/Reducers/Throttle.swift: -------------------------------------------------------------------------------- 1 | public struct Throttle< 2 | R: Reducer 3 | > : Reducer { 4 | var reducer: AnyReducer 5 | var minimumDuration: Double 6 | 7 | public init(reducer: R, minimumDuration: Double) { 8 | var accumulatedDelta: Double = 0 9 | self.reducer = AnyReducer( 10 | { state, delta, env in 11 | accumulatedDelta += delta 12 | guard accumulatedDelta >= minimumDuration else { return .none } 13 | let nextDelta = accumulatedDelta 14 | accumulatedDelta = 0 15 | return reducer.reduce(state: &state, delta: nextDelta, environment: env) 16 | }, 17 | reducer.reduce(state:action:environment:), 18 | reducer.reduce(state:entityEvent:environment:) 19 | ) 20 | self.minimumDuration = minimumDuration 21 | } 22 | 23 | public func reduce( 24 | state: inout R.State, 25 | action: R.Action, 26 | environment: R.Environment 27 | ) -> GameEffect { 28 | reducer.reduce( 29 | state: &state, 30 | action: action, 31 | environment: environment 32 | ) 33 | } 34 | 35 | public func reduce( 36 | state: inout R.State, 37 | delta: Double, 38 | environment: R.Environment 39 | ) -> GameEffect { 40 | reducer.reduce( 41 | state: &state, 42 | delta: delta, 43 | environment: environment 44 | ) 45 | } 46 | 47 | public func reduce( 48 | state: inout R.State, 49 | entityEvent: EntityEvent, 50 | environment: R.Environment 51 | ) -> GameEffect { 52 | reducer.reduce( 53 | state: &state, 54 | entityEvent: entityEvent, 55 | environment: environment 56 | ) 57 | } 58 | } 59 | 60 | public extension Reducer { 61 | func throttle( 62 | _ minimumDuration: Double 63 | ) -> Throttle { 64 | return Throttle(reducer: self, minimumDuration: minimumDuration) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/Camera/CameraComponent.swift: -------------------------------------------------------------------------------- 1 | import Geometry 2 | import GeometryAlgorithms 3 | 4 | public struct CameraComponent: GameComponent { 5 | public let entity: EntityId 6 | public var zoom: Double = 1 7 | public var offset: Point = .zero 8 | public var isPrimaryCamera: Bool = true 9 | 10 | public init(entity: EntityId) { 11 | self = .init(entity: entity, zoom: 1) 12 | } 13 | 14 | public init( 15 | entity: EntityId, 16 | zoom: Double = 1, 17 | offset: Point = .zero, 18 | isPrimaryCamera: Bool = true 19 | ) { 20 | self.entity = entity 21 | self.zoom = zoom 22 | self.offset = offset 23 | self.isPrimaryCamera = isPrimaryCamera 24 | } 25 | 26 | public func matrix(withRect rect: Rect) -> Matrix3 { 27 | Matrix3.projection(rect: rect, zoom: zoom) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/Color.swift: -------------------------------------------------------------------------------- 1 | /// Values represented from 0 to 1 2 | public struct Color: Equatable, Codable { 3 | public var red: Double 4 | public var green: Double 5 | public var blue: Double 6 | public var alpha: Double 7 | 8 | public init( 9 | red: Double, 10 | green: Double, 11 | blue: Double, 12 | alpha: Double 13 | ) { 14 | self.red = red 15 | self.green = green 16 | self.blue = blue 17 | self.alpha = alpha 18 | } 19 | 20 | public var hexValue: Int { 21 | let rInt = Int(min(255, max(0, red * 255))) << 16 22 | let gInt = Int(min(255, max(0, green * 255))) << 8 23 | let bInt = Int(min(255, max(0, blue * 255))) 24 | return rInt + gInt + bInt 25 | } 26 | 27 | init(red: Int, green: Int, blue: Int) { 28 | assert(red >= 0 && red <= 255, "Invalid red component") 29 | assert(green >= 0 && green <= 255, "Invalid green component") 30 | assert(blue >= 0 && blue <= 255, "Invalid blue component") 31 | self.init( 32 | red: Double(red) / 255.0, 33 | green: Double(green) / 255.0, 34 | blue: Double(blue) / 255.0, 35 | alpha: 1.0 36 | ) 37 | } 38 | 39 | init(hex: Int) { 40 | self.init( 41 | red: (hex >> 16) & 0xFF, 42 | green: (hex >> 8) & 0xFF, 43 | blue: hex & 0xFF 44 | ) 45 | } 46 | } 47 | 48 | public extension Color { 49 | static let white: Color = .init(red: 1, green: 1, blue: 1, alpha: 1) 50 | static let grey: Color = .init(red: 0.5, green: 0.5, blue: 0.5, alpha: 1) 51 | static let black: Color = .init(red: 0, green: 0, blue: 0, alpha: 1) 52 | 53 | static let red: Color = .init(red: 1, green: 0, blue: 0, alpha: 1) 54 | static let green: Color = .init(red: 0, green: 1, blue: 0, alpha: 1) 55 | static let blue: Color = .init(red: 0, green: 0, blue: 1, alpha: 1) 56 | 57 | static let yellow: Color = .init(red: 1, green: 1, blue: 0, alpha: 1) 58 | static let pink: Color = .init(red: 1, green: 0, blue: 1, alpha: 1) 59 | static let cyan: Color = .init(red: 0, green: 1, blue: 1, alpha: 1) 60 | 61 | static let orange: Color = .init(hex: 0xff7700) 62 | 63 | static let clear: Color = .init(red: 0, green: 0, blue: 0, alpha: 0) 64 | 65 | static func random() -> Color { 66 | Color( 67 | red: .random(in: 0...1), 68 | green: .random(in: 0...1), 69 | blue: .random(in: 0...1), 70 | alpha: 1 71 | ) 72 | } 73 | 74 | func withAlpha(_ alpha: Double) -> Color { 75 | Color( 76 | red: red, 77 | green: green, 78 | blue: blue, 79 | alpha: alpha 80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/RenderTriangle.swift: -------------------------------------------------------------------------------- 1 | import Geometry 2 | import GeometryAlgorithms 3 | 4 | public struct RenderGroup { 5 | public enum FragmentType { 6 | case color(Color) 7 | case texture(TextureId) 8 | } 9 | 10 | public let triangles: [RenderTriangle] 11 | public let transformMatrix: Matrix3 12 | public let fragmentType: FragmentType 13 | public let zIndex: Int 14 | public let opacity: Double 15 | 16 | public init( 17 | triangles: [RenderTriangle], 18 | transformMatrix: Matrix3, 19 | fragmentType: FragmentType, 20 | zIndex: Int, 21 | opacity: Double = 1 22 | ) { 23 | self.triangles = triangles 24 | self.transformMatrix = transformMatrix 25 | self.fragmentType = fragmentType 26 | self.zIndex = zIndex 27 | self.opacity = opacity 28 | } 29 | } 30 | 31 | public extension RenderGroup { 32 | var textureId: TextureId? { 33 | switch fragmentType { 34 | case .texture(let id): 35 | return id 36 | case .color: 37 | return nil 38 | } 39 | } 40 | 41 | var color: Color? { 42 | switch fragmentType { 43 | case .texture: 44 | return nil 45 | case .color(let color): 46 | return color 47 | } 48 | } 49 | } 50 | 51 | public struct RenderTriangle { 52 | 53 | public let triangle: Triangle 54 | public let textureTriangle: Triangle? 55 | 56 | public init( 57 | triangle: Triangle, 58 | textureTriangle: Triangle? = nil 59 | ) { 60 | self.triangle = triangle 61 | self.textureTriangle = textureTriangle 62 | } 63 | } 64 | 65 | public extension RenderTriangle { 66 | static var noTextureTriangle: Triangle { 67 | Triangle(a: .init(x: -1, y: -1), b: .init(x: -1, y: -1), c: .init(x: -1, y: -1)) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/RenderableComponent.swift: -------------------------------------------------------------------------------- 1 | import Geometry 2 | import GeometryAlgorithms 3 | 4 | public protocol RenderableComponent { 5 | func renderGroups( 6 | cameraMatrix: Matrix3, 7 | transform: TransformComponent, 8 | resourceManager: ResourceManager 9 | ) -> [RenderGroup] 10 | } 11 | 12 | public protocol RenderableGameState: GameState { 13 | var transform: [EntityId: TransformComponent] { get } 14 | var camera: [EntityId: CameraComponent] { get } 15 | } 16 | 17 | public struct RenderableComponentType { 18 | var getRenderComponent: (EntityId, State) -> RenderableComponent? 19 | 20 | public init(keyPath: KeyPath) { 21 | getRenderComponent = { id, gameState in 22 | gameState[keyPath: keyPath][id] 23 | } 24 | } 25 | 26 | func renderComponent(entityId: EntityId, state: State) -> RenderableComponent? { 27 | getRenderComponent(entityId, state) 28 | } 29 | } 30 | 31 | public struct RenderingReducer: Reducer { 32 | public typealias State = ContextState 33 | public typealias Action = Never 34 | public typealias Environment = RenderingEnvironment 35 | 36 | var renderableComponentTypes: [RenderableComponentType] 37 | 38 | public init( 39 | renderableComponentTypes: [RenderableComponentType] 40 | ) { 41 | self.renderableComponentTypes = renderableComponentTypes 42 | } 43 | 44 | public func reduce( 45 | state: inout State, 46 | delta: Double, 47 | environment: RenderingEnvironment 48 | ) -> GameEffect { 49 | 50 | if let camera = state.camera.values.sorted(by: { $1.isPrimaryCamera ? false : true }).first, 51 | let transform = state.transform[camera.entity] { 52 | 53 | let renderer = environment.renderer 54 | let size = renderer.viewportSize 55 | let projectionMatrix = camera.matrix(withRect: Rect(center: transform.position, size: size)) 56 | renderer.setProjectionMatrix(projectionMatrix) 57 | 58 | state.entities.entities.forEach { id, entity in 59 | renderableComponentTypes.forEach { type in 60 | if let renderComponent = type.renderComponent(entityId: id, state: state), 61 | let transform = state.transform[id] { 62 | renderer.enqueue(renderComponent.renderGroups( 63 | cameraMatrix: projectionMatrix, 64 | transform: transform, 65 | resourceManager: environment.resourceManager 66 | )) 67 | } 68 | } 69 | } 70 | } 71 | return .none 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/Renderer.swift: -------------------------------------------------------------------------------- 1 | import Geometry 2 | import GeometryAlgorithms 3 | 4 | public protocol Renderer: AnyObject { 5 | var viewportSize: Size { get } 6 | var queuedWork: [RenderGroup] { get set } 7 | 8 | func setProjectionMatrix(_ matrix: Matrix3) 9 | func clearQueue() 10 | func enqueue(_ work: [RenderGroup]) 11 | } 12 | 13 | public enum RendererProgram { 14 | case color 15 | case texture 16 | } 17 | 18 | public extension Renderer { 19 | func clearQueue() { 20 | queuedWork.removeAll() 21 | } 22 | 23 | func enqueue(_ work: [RenderGroup]) { 24 | queuedWork.append(contentsOf: work) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/RenderingEnvironment.swift: -------------------------------------------------------------------------------- 1 | public protocol RenderingEnvironment { 2 | var renderer: Renderer { get } 3 | var resourceManager: ResourceManager { get } 4 | } 5 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/Shape/Shape+Rect.swift: -------------------------------------------------------------------------------- 1 | import Geometry 2 | import GeometryAlgorithms 3 | 4 | extension Shape { 5 | public var rect: Rect { 6 | switch self { 7 | case .rect(let r): 8 | return r 9 | case .triangle(let t): 10 | return GeometryAlgorithms.calculateContainingRect(of: t.points) 11 | case .circle(let c): 12 | return Rect(center: c.center, size: c.size) 13 | case .polygon(let p): 14 | return GeometryAlgorithms.calculateContainingRect(of: p.points) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/Sprite/SpriteAnimatingReducer.swift: -------------------------------------------------------------------------------- 1 | import Geometry 2 | 3 | public struct SpriteAnimationConfiguration: Codable, Equatable { 4 | public static var `default` = SpriteAnimationConfiguration() 5 | public var flipX: Bool 6 | public var flipY: Bool 7 | public init( 8 | flipX: Bool = false, 9 | flipY: Bool = false 10 | ) { 11 | self.flipX = flipX 12 | self.flipY = flipY 13 | } 14 | } 15 | 16 | public enum SpriteAnimatingAction: Equatable & Codable { 17 | case run( 18 | entityId: String, 19 | animationName: String, 20 | config: SpriteAnimationConfiguration = .default 21 | ) 22 | case runOnce( 23 | animationId: String, 24 | entityId: String, 25 | animationName: String, 26 | config: SpriteAnimationConfiguration = .default 27 | ) 28 | case animationComplete(String) 29 | } 30 | 31 | public struct SpriteAnimatingReducer: Reducer { 32 | public init() {} 33 | public func reduce( 34 | state: inout SpriteContext, 35 | delta: Double, 36 | environment: RenderingEnvironment 37 | ) -> GameEffect { 38 | var effects: [GameEffect] = [] 39 | state.sprite.forEach { (id, spriteComponent) in 40 | var sprite = spriteComponent 41 | if let completedAnimationId = sprite.applyDelta(delta) { 42 | effects.append(.game(.animationComplete(completedAnimationId))) 43 | } 44 | state.sprite[id] = sprite 45 | } 46 | return .many(effects) 47 | } 48 | 49 | public func reduce( 50 | state: inout SpriteContext, 51 | action: SpriteAnimatingAction, 52 | environment: RenderingEnvironment 53 | ) -> GameEffect { 54 | switch action { 55 | case let .run(entityId, animationName, config): 56 | guard let textureId = state.sprite[entityId]?.textureId, 57 | let animationDict = environment.resourceManager.animationsForTexture(textureId), 58 | let animation = animationDict[animationName] else { 59 | return .none 60 | } 61 | 62 | state.sprite[entityId]?.runAnimation(animation, animationId: nil, repeatsForever: true) 63 | state.transform[entityId]?.scale.x = config.flipX ? -1 : 1 64 | case let .runOnce(animationId, entityId, animationName, config): 65 | guard let textureId = state.sprite[entityId]?.textureId, 66 | let animationDict = environment.resourceManager.animationsForTexture(textureId), 67 | let animation = animationDict[animationName] else { 68 | return .none 69 | } 70 | state.sprite[entityId]?.runAnimation(animation, animationId: animationId, repeatsForever: false) 71 | state.transform[entityId]?.scale.x = config.flipX ? -1 : 1 72 | case .animationComplete: 73 | break 74 | } 75 | return .none 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/Sprite/SpriteAnimationDictionary.swift: -------------------------------------------------------------------------------- 1 | public struct SpriteAnimationDictionary: Codable { 2 | public struct Animation: Codable, Equatable { 3 | public struct Frame: Codable, Equatable { 4 | public let name: String 5 | public let duration: Double 6 | } 7 | public let name: String 8 | public let frames: [Frame] 9 | } 10 | 11 | public enum Error: Swift.Error { 12 | case textureMapDoesNotContainAnyAnimations 13 | } 14 | 15 | public let name: String 16 | public private(set) var dict: [String: Animation] 17 | 18 | public subscript(index: String) -> Animation? { 19 | dict[index] 20 | } 21 | 22 | public init(name: String, textureMap: TextureMap) throws { 23 | self.name = name 24 | guard textureMap.meta.frameTags?.isEmpty == false else { 25 | throw Error.textureMapDoesNotContainAnyAnimations 26 | } 27 | var dict: [String: Animation] = [:] 28 | if let frameTags = textureMap.meta.frameTags { 29 | for frameTag in frameTags { 30 | let startIndex = frameTag.from 31 | let endIndex = frameTag.to 32 | let frames = textureMap.frames[(startIndex...endIndex)].map { frame in 33 | Animation.Frame(name: frame.filename, duration: frame.duration ?? 0.16) 34 | } 35 | dict[frameTag.name] = Animation(name: frameTag.name, frames: frames) 36 | } 37 | } 38 | self.dict = dict 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/Sprite/SpriteContext.swift: -------------------------------------------------------------------------------- 1 | public struct SpriteContext: GameState { 2 | public var entities: EntityRepository = .init() 3 | public var transform: [EntityId: TransformComponent] 4 | public var sprite: [EntityId: SpriteComponent] 5 | 6 | public init( 7 | entities: EntityRepository = .init(), 8 | transform: [EntityId: TransformComponent], 9 | sprite: [EntityId: SpriteComponent] 10 | ) { 11 | self.entities = entities 12 | self.transform = transform 13 | self.sprite = sprite 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/Texture/TextureId.swift: -------------------------------------------------------------------------------- 1 | public typealias TextureId = String 2 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/Texture/TextureMap.swift: -------------------------------------------------------------------------------- 1 | public struct TextureMap: Codable { 2 | public struct TextureRect: Codable { 3 | public let x: Double 4 | public let y: Double 5 | public let w: Double 6 | public let h: Double 7 | } 8 | 9 | public struct TextureSize: Codable { 10 | public let w: Double 11 | public let h: Double 12 | } 13 | 14 | public struct Frame: Codable { 15 | public var filename: String 16 | public var frame: TextureRect 17 | public var rotated: Bool 18 | public var trimmed: Bool 19 | public var spriteSourceSize: TextureRect 20 | public var sourceSize: TextureSize 21 | public var duration: Double? 22 | } 23 | 24 | public struct Metadata: Codable { 25 | public var image: String? 26 | public var size: TextureSize 27 | public var format: String? 28 | public var frameTags: [FrameTag]? 29 | } 30 | 31 | public struct FrameTag: Codable { 32 | public var name: String 33 | public var from: Int 34 | public var to: Int 35 | public var direction: String 36 | } 37 | 38 | public var frames: [Frame] 39 | public var meta: Metadata 40 | } 41 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/Texture/TextureReference.swift: -------------------------------------------------------------------------------- 1 | public struct TextureReference: Equatable, Codable { 2 | public let textureId: TextureId 3 | public var frameId: String? 4 | 5 | public init(textureId: TextureId, frameId: String?) { 6 | self.textureId = textureId 7 | self.frameId = frameId 8 | } 9 | 10 | public static var empty: TextureReference = .init(textureId: "", frameId: nil) 11 | } 12 | -------------------------------------------------------------------------------- /Sources/RedECS/Rendering/Transform/TransformComponent.swift: -------------------------------------------------------------------------------- 1 | import Geometry 2 | import GeometryAlgorithms 3 | 4 | public struct TransformComponent: GameComponent { 5 | public let entity: EntityId 6 | public var position: Point = .zero 7 | public private(set) var anchorPoint: Point = .init(x: 0.5, y: 0.5) // From 0 to 1 8 | public var rotate: Double = 0 9 | public var scale: Point = Point(x: 1, y: 1) 10 | public var zIndex: Int = 0 11 | 12 | public var parentId: EntityId? // TODO: implement rendering implications 13 | public var isHidden: Bool = false // TODO: implement rendering implications 14 | 15 | public init(entity: EntityId) { 16 | self = .init(entity: entity, position: .zero) 17 | } 18 | 19 | public init( 20 | entity: EntityId, 21 | position: Point = .zero, 22 | anchorPoint: Point = .init(x: 0.5, y: 0.5), 23 | rotate: Double = 0, 24 | scale: Point = Point(x: 1, y: 1), 25 | zIndex: Int = 0, 26 | parentId: EntityId? = nil, 27 | isHidden: Bool = false 28 | ) { 29 | self.entity = entity 30 | self.position = position 31 | self.rotate = rotate 32 | self.scale = scale 33 | self.zIndex = zIndex 34 | self.parentId = parentId 35 | self.isHidden = isHidden 36 | 37 | self.setAnchorPoint(anchorPoint) 38 | } 39 | 40 | public func matrix(containerSize: Size = .zero) -> Matrix3 { 41 | Matrix3 42 | .identity 43 | .translatedBy(tx: position.x, ty: position.y) 44 | .rotatedBy(angleInRadians: -rotate.degreesToRadians()) 45 | .scaledBy(sx: scale.x, sy: scale.y) 46 | .translatedBy( 47 | tx: -anchorPoint.x * containerSize.width, 48 | ty: -anchorPoint.y * containerSize.height 49 | ) 50 | } 51 | 52 | /// Clamps values between 0 and 1. A value of 0.5 for both x and y means center 53 | public mutating func setAnchorPoint(_ anchorPoint: Point) { 54 | self.anchorPoint = .init( 55 | x: max(0, min(1, anchorPoint.x)), 56 | y: max(0, min(1, anchorPoint.y)) 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/RedECS/ResourceManager.swift: -------------------------------------------------------------------------------- 1 | import TiledInterpreter 2 | 3 | public enum Resource { 4 | case loading 5 | case failedToLoad(Error) 6 | case loaded(T) 7 | } 8 | 9 | public enum ResourceType: Equatable, Codable { 10 | case image 11 | case sound 12 | case tilemap 13 | case bitmapFont 14 | // TODO: preload sprite animation dictionary 15 | } 16 | 17 | public struct LoadableResource: Equatable, Codable { 18 | public var name: String 19 | public var type: ResourceType 20 | public init(name: String, type: ResourceType) { 21 | self.name = name 22 | self.type = type 23 | } 24 | } 25 | 26 | public protocol ResourceManager: AnyObject { 27 | var textures: [TextureId: Resource] { get } 28 | var animations: [TextureId: SpriteAnimationDictionary] { get set } 29 | var tileMaps: [String: TiledMapJSON] { get set } 30 | var tileSets: [String: TiledTilesetJSON] { get set } 31 | var fonts: [String: BitmapFont] { get set } 32 | 33 | func preload(_ assets: [LoadableResource]) -> Future 34 | 35 | @discardableResult 36 | func startTextureLoadIfNeeded(textureId: TextureId) -> Future 37 | 38 | func getTexture(textureId: TextureId) -> TextureMap? 39 | func animationsForTexture(_ textureId: TextureId) -> SpriteAnimationDictionary? 40 | 41 | func loadJSONFile(_ name: String, decodedAs: T.Type) -> Future 42 | func loadTiledMap(_ name: String) -> Future 43 | func loadBitmapFontTextFile(_ name: String) -> Future 44 | } 45 | 46 | public extension ResourceManager { 47 | func getTexture(textureId: TextureId) -> TextureMap? { 48 | guard let resource = textures[textureId] else { return nil } 49 | switch resource { 50 | case .loaded(let textureMap): 51 | return textureMap 52 | case .loading, .failedToLoad: 53 | return nil 54 | } 55 | } 56 | 57 | func animationsForTexture(_ textureId: TextureId) -> SpriteAnimationDictionary? { 58 | if let dict = animations[textureId] { 59 | return dict 60 | } 61 | guard let textureMap = getTexture(textureId: textureId) else { 62 | return nil 63 | } 64 | do { 65 | let dict = try SpriteAnimationDictionary(name: textureId, textureMap: textureMap) 66 | self.animations[textureId] = dict 67 | return dict 68 | } catch { 69 | print(error) 70 | return nil 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/RedECS/Store/GameStore.swift: -------------------------------------------------------------------------------- 1 | public final class GameStore { 2 | public private(set) var state: R.State 3 | public private(set) var environment: R.Environment 4 | private var reducer: R 5 | private var registeredComponentTypes: [String: RegisteredComponentType] = [:] 6 | private var awaitingEffects: [PendingGameEffect] = [] 7 | 8 | public init( 9 | state: R.State, 10 | environment: R.Environment, 11 | reducer: R, 12 | registeredComponentTypes: Set> 13 | ) { 14 | self.state = state 15 | self.environment = environment 16 | self.reducer = reducer 17 | self.registeredComponentTypes = registeredComponentTypes.reduce(into: [:]) { $0[$1.id] = $1 } 18 | } 19 | 20 | public func sendDelta(_ delta: Double) { 21 | assert(delta > 0, "Delta should be greater than 0") 22 | let effect = reducer.reduce(state: &state, delta: delta, environment: environment) 23 | handleEffect(effect) 24 | } 25 | 26 | public func sendAction(_ action: R.Action) { 27 | // print("[♦️] \(action)") 28 | var remainingAwaits: [PendingGameEffect] = [] 29 | for i in 0..) { 42 | switch effect { 43 | case .none: 44 | break 45 | case .game(let action): 46 | sendAction(action) 47 | case .system(let action): 48 | sendSystemAction(action) 49 | case .many(let effects): 50 | effects.forEach(handleEffect) 51 | case .deferred(let promise): 52 | promise.onDone { [weak self] effect in 53 | self?.handleEffect(effect) 54 | } 55 | case .waitFor(let pendingEffect): 56 | awaitingEffects.append(pendingEffect) 57 | } 58 | } 59 | 60 | public func sendSystemAction(_ action: SystemAction) { 61 | switch action { 62 | case .addEntity(let entityId, let tags): 63 | handleEffect(addEntity(entityId, tags: tags)) 64 | case .removeEntity(let entityId): 65 | handleEffect(removeEntity(entityId)) 66 | case .addComponent(let entityId, let componentRegistration): 67 | assert(isComponentTypeRegistered(id: componentRegistration.id), "Attempting to add a component type that is not registered \(String(describing: componentRegistration.id))") 68 | componentRegistration.onAdd(entityId, &state) 69 | // print("[♦️]: Added Component", componentRegistration.id, "for", entityId) 70 | case .removeComponent(let entityId, let registeredComponentId): 71 | // print("[♦️]: Removed Component", registeredComponentId, "for", entityId) 72 | registeredComponentTypes[registeredComponentId]?.onEntityDestroyed(entityId, &state) 73 | } 74 | } 75 | 76 | public func perform(_ reduceBlock: (inout R.State, R.Environment) -> GameEffect) { 77 | handleEffect(reduceBlock(&state, environment)) 78 | } 79 | 80 | public func addEntity(_ id: EntityId, tags: Set) -> GameEffect { 81 | state.entities.addEntity(GameEntity(id: id, tags: tags)) 82 | return reducer.reduce(state: &state, entityEvent: .added(id), environment: environment) 83 | } 84 | 85 | private func removeEntity(_ id: EntityId) -> GameEffect { 86 | registeredComponentTypes.values.forEach { componentType in 87 | componentType.onEntityDestroyed(id, &state) 88 | } 89 | state.entities.removeEntity(id) 90 | return reducer.reduce(state: &state, entityEvent: .removed(id), environment: environment) 91 | } 92 | 93 | public func addComponent( 94 | _ component: C, 95 | into keyPath: WritableKeyPath 96 | ) { 97 | let registration = AnyComponent(component, into: keyPath) 98 | assert(isComponentTypeRegistered(id: registration.id), "Attempting to add a component type that is not registered \(String(describing: registration.id))") 99 | registration.onAdd(component.entity, &state) 100 | } 101 | 102 | public func removeComponent( 103 | ofType type: C.Type, 104 | from keyPath: WritableKeyPath, 105 | forEntity entity: EntityId 106 | ) { 107 | state[keyPath: keyPath][entity] = nil 108 | } 109 | 110 | private func isComponentTypeRegistered(id: String) -> Bool { 111 | registeredComponentTypes[id] != nil 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /Sources/RedECS/Store/PendingGameEffect.swift: -------------------------------------------------------------------------------- 1 | public struct PendingGameEffect { 2 | public var outstandingActions: [Action] 3 | public let effect: GameEffect 4 | 5 | public init( 6 | outstandingActions: [Action], 7 | effect: GameEffect 8 | ) { 9 | self.outstandingActions = outstandingActions 10 | self.effect = effect 11 | } 12 | 13 | public init( 14 | outstandingAction: Action, 15 | effect: GameEffect 16 | ) { 17 | self.outstandingActions = [outstandingAction] 18 | self.effect = effect 19 | } 20 | 21 | public mutating func evaluateCompleteness(_ action: Action) -> Bool { 22 | guard let index = outstandingActions.firstIndex(where: { $0 == action }) else { return false } 23 | outstandingActions.remove(at: index) 24 | if outstandingActions.isEmpty { 25 | return true 26 | } else { 27 | return false 28 | } 29 | } 30 | 31 | public func map( 32 | stateTransform: WritableKeyPath, 33 | actionTransform: @escaping (Action) -> A 34 | ) -> PendingGameEffect { 35 | .init( 36 | outstandingActions: outstandingActions.map(actionTransform), 37 | effect: effect.map(stateTransform: stateTransform, actionTransform: actionTransform) 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/RedECS/Store/SystemAction.swift: -------------------------------------------------------------------------------- 1 | public enum SystemAction { 2 | case addEntity(EntityId, Set) 3 | case removeEntity(EntityId) 4 | case addComponent(EntityId, AnyComponent) 5 | case removeComponent(EntityId, RegisteredComponentId) 6 | 7 | public func map( 8 | _ stateTransform: WritableKeyPath 9 | ) -> SystemAction { 10 | switch self { 11 | case .addEntity(let e, let tags): 12 | return .addEntity(e, tags) 13 | case .removeEntity(let e): 14 | return .removeEntity(e) 15 | case .addComponent(let eId, let registeredComponent): 16 | return .addComponent(eId, registeredComponent.map(stateTransform)) 17 | case .removeComponent(let e, let registeredComponentId): 18 | return .removeComponent(e, registeredComponentId) 19 | } 20 | } 21 | 22 | public static func addComponent( 23 | _ component: C, 24 | into keyPath: WritableKeyPath 25 | ) -> Self { 26 | .addComponent(component.entity, AnyComponent(component, into: keyPath)) 27 | } 28 | 29 | public static func removeComponent( 30 | ofType type: C.Type, 31 | forEntity entity: EntityId 32 | ) -> Self { 33 | .removeComponent(entity, String(describing: C.self)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/RedECS/Utilities/Extensions/Matrix3+Projection.swift: -------------------------------------------------------------------------------- 1 | import Geometry 2 | import GeometryAlgorithms 3 | 4 | public extension Matrix3 { 5 | static func projection( 6 | rect: Rect, 7 | zoom: Double = 1 8 | ) -> Matrix3 { 9 | Matrix3.identity 10 | .scaledBy(sx: 1, sy: -1) // y flip 11 | .translatedBy(tx: -1, ty: 1) // translating from -1 to 1 12 | .scaledBy(sx: 2 / rect.size.width, sy: -2 / rect.size.height) // viewport size 13 | .scaledBy(sx: zoom, sy: zoom) // camera scale 14 | .translatedBy( 15 | tx: (rect.size.width/2 / zoom) - rect.center.x, 16 | ty: (rect.size.height/2 / zoom) - rect.center.y 17 | ) // camera position 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/RedECS/Utilities/Future.swift: -------------------------------------------------------------------------------- 1 | public struct Future { 2 | public typealias ResolutionBlock = (Result) -> Void 3 | public typealias ObserverCreationBlock = (@escaping ResolutionBlock) -> Void 4 | 5 | private var observer: (@escaping ResolutionBlock) -> Void 6 | 7 | public init(observer: @escaping ObserverCreationBlock) { 8 | self.observer = observer 9 | } 10 | 11 | public func subscribe(_ resolve: @escaping ResolutionBlock) { 12 | observer { result in 13 | resolve(result) 14 | } 15 | } 16 | 17 | public func map(_ transform: @escaping (T) -> A) -> Future { 18 | return .init { resolve in 19 | self.subscribe { resolve($0.map(transform)) } 20 | } 21 | } 22 | 23 | public func readValue(_ readClosure: @escaping (Result) -> Void) -> Future { 24 | return .init { resolve in 25 | self.subscribe { 26 | readClosure($0) 27 | resolve($0) 28 | } 29 | } 30 | } 31 | 32 | public func flatMap(_ transform: @escaping (T) -> Future) -> Future { 33 | return .init { resolve in 34 | self.subscribe { result in 35 | switch result { 36 | case .success(let value): 37 | transform(value).subscribe(resolve) 38 | case .failure(let e): 39 | resolve(.failure(e)) 40 | } 41 | } 42 | } 43 | } 44 | 45 | public func recoverError(_ transform: @escaping (E) -> T) -> Future { 46 | return .init { resolve in 47 | self.subscribe { result in 48 | resolve(result.flatMapError({ error in 49 | return .success(transform(error)) 50 | })) 51 | } 52 | } 53 | } 54 | 55 | } 56 | 57 | public extension Future { 58 | static func zip(_ a: Future, _ b: Future) -> Future<(A, B), E> { 59 | Future<(A, B), E> { resolver in 60 | var valueA: A? 61 | var valueB: B? 62 | 63 | func resolveIfComplete() { 64 | if let valueA = valueA, let valueB = valueB { 65 | resolver(.success((valueA, valueB))) 66 | } 67 | } 68 | 69 | a.subscribe { result in 70 | switch result { 71 | case .success(let aValue): 72 | valueA = aValue 73 | resolveIfComplete() 74 | case .failure(let e): 75 | resolver(.failure(e)) 76 | } 77 | } 78 | b.subscribe { result in 79 | switch result { 80 | case .success(let bValue): 81 | valueB = bValue 82 | resolveIfComplete() 83 | case .failure(let e): 84 | resolver(.failure(e)) 85 | } 86 | } 87 | } 88 | } 89 | 90 | static func zip(_ a: Future, _ b: Future, _ c: Future) -> Future<(A, B, C), E> { 91 | Future<(A, B, C), E> { resolver in 92 | zip(zip(a, b), c) 93 | .subscribe { result in 94 | switch result { 95 | case .success(let ab_c): 96 | resolver(.success((ab_c.0.0, ab_c.0.1, ab_c.1))) 97 | case .failure(let error): 98 | resolver(.failure(error)) 99 | } 100 | } 101 | } 102 | } 103 | 104 | static func zip(_ all: [Future]) -> Future<[A], E> { 105 | if all.isEmpty { 106 | return .just([]) 107 | } 108 | return Future<[A], E> { resolver in 109 | var cumulative: [(Int, A)] = [] 110 | var cumulativeCount = 0 111 | cumulative.reserveCapacity(all.count) 112 | 113 | func resolveIfComplete() { 114 | if cumulativeCount == all.count { 115 | let sorted = cumulative.sorted(by: { a, b in a.0 < b.0 }).map { $0.1 } 116 | resolver(.success(sorted)) 117 | } 118 | } 119 | 120 | all.enumerated().forEach { i, future in 121 | future.subscribe { result in 122 | switch result { 123 | case .success(let value): 124 | cumulative.append((i, value)) 125 | cumulativeCount += 1 126 | resolveIfComplete() 127 | case .failure(let e): 128 | resolver(.failure(e)) 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | func toVoid() -> Future { 136 | map { _ in () } 137 | } 138 | 139 | static func just(_ value: T) -> Future { 140 | Future(observer: { resolve in resolve(.success(value)) }) 141 | } 142 | 143 | static func fail(_ error: E) -> Future { 144 | Future(observer: { resolve in resolve(.failure(error)) }) 145 | } 146 | } 147 | 148 | public extension Future where E == Never { 149 | func onDone(_ completion: @escaping (T) -> Void) { 150 | self.subscribe { result in 151 | switch result { 152 | case .success(let value): 153 | completion(value) 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Sources/RedECSAppleSupport/GameStore+JSONCoding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RedECS 3 | 4 | public extension GameStore { 5 | 6 | convenience init( 7 | data: Data, 8 | environment: R.Environment, 9 | reducer: R, 10 | registeredComponentTypes: Set> 11 | ) throws { 12 | let state = try JSONDecoder().decode(R.State.self, from: data) 13 | self.init( 14 | state: state, 15 | environment: environment, 16 | reducer: reducer, 17 | registeredComponentTypes: registeredComponentTypes 18 | ) 19 | } 20 | 21 | func saveState() throws -> Data { 22 | try JSONEncoder().encode(state) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/RedECSAppleSupport/MetalEnvironment.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct MetalEnvironment: RenderingEnvironment { 4 | public var renderer: Renderer { metalRenderer } 5 | public var resourceManager: ResourceManager { metalResourceManager } 6 | 7 | public var metalRenderer: MetalRenderer 8 | public var metalResourceManager: MetalResourceManager 9 | 10 | public init( 11 | metalRenderer: MetalRenderer, 12 | metalResourceManager: MetalResourceManager 13 | ) { 14 | self.metalRenderer = metalRenderer 15 | self.metalResourceManager = metalResourceManager 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/RedECSAppleSupport/MetalViewController.swift: -------------------------------------------------------------------------------- 1 | import MetalKit 2 | 3 | #if os(OSX) 4 | import Cocoa 5 | 6 | public typealias AppleViewController = NSViewController 7 | public typealias AppleColor = NSColor 8 | #else 9 | import UIKit 10 | 11 | public typealias AppleViewController = UIViewController 12 | public typealias AppleColor = UIColor 13 | #endif 14 | 15 | public class MetalView: MTKView { 16 | #if os(OSX) 17 | public override var acceptsFirstResponder: Bool { true } 18 | #endif 19 | } 20 | 21 | open class MetalViewController: AppleViewController { 22 | public var renderer: MetalRenderer! 23 | public var resourceManager: MetalResourceManager! 24 | public var mtkView: MetalView! 25 | 26 | open override func loadView() { 27 | self.view = MetalView(frame: .init(origin: .zero, size: .init(width: 480, height: 480))) 28 | } 29 | 30 | open override func viewDidLoad() { 31 | super.viewDidLoad() 32 | 33 | guard let mtkView = self.view as? MetalView else { 34 | fatalError("View of Gameview controller is not an MTKView") 35 | } 36 | self.mtkView = mtkView 37 | 38 | // Select the device to render with. We choose the default device 39 | guard let defaultDevice = MTLCreateSystemDefaultDevice() else { 40 | fatalError("Metal is not supported") 41 | } 42 | 43 | mtkView.device = defaultDevice 44 | 45 | let resourceManager = MetalResourceManager(metalDevice: defaultDevice) 46 | guard let newRenderer = MetalRenderer( 47 | device: defaultDevice, 48 | pixelFormat: mtkView.colorPixelFormat, 49 | resourceManager: resourceManager 50 | ) else { 51 | print("Renderer cannot be initialized") 52 | return 53 | } 54 | 55 | self.resourceManager = resourceManager 56 | self.renderer = newRenderer 57 | 58 | renderer.mtkView(mtkView, drawableSizeWillChange: mtkView.drawableSize) 59 | 60 | mtkView.delegate = renderer 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/RedECSAppleSupport/Shaders.metal: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | Metal shaders used for this sample 6 | */ 7 | 8 | #include 9 | #include 10 | 11 | using namespace metal; 12 | 13 | typedef enum AAPLVertexInputIndex 14 | { 15 | AAPLVertexInputIndexVertices = 0, 16 | BufferIndexUniforms = 1, 17 | TextureCoordinates = 2 18 | } AAPLVertexInputIndex; 19 | 20 | typedef enum TextureIndex 21 | { 22 | TextureIndexColor = 0 23 | } TextureIndex; 24 | 25 | // This structure defines the layout of vertices sent to the vertex 26 | // shader. This header is shared between the .metal shader and C code, to guarantee that 27 | // the layout of the vertex array in the C code matches the layout that the .metal 28 | // vertex shader expects. 29 | typedef struct 30 | { 31 | vector_float2 position; 32 | vector_float4 color; 33 | } AAPLVertex; 34 | 35 | typedef struct 36 | { 37 | vector_float2 texCoord; 38 | vector_float2 texSize; 39 | } TextureInfo; 40 | 41 | typedef struct 42 | { 43 | matrix_float4x4 projectionMatrix; 44 | matrix_float4x4 modelViewMatrix; 45 | } Uniforms; 46 | 47 | // Vertex shader outputs and fragment shader inputs 48 | struct RasterizerData 49 | { 50 | // The [[position]] attribute of this member indicates that this value 51 | // is the clip space position of the vertex when this structure is 52 | // returned from the vertex function. 53 | float4 position [[position]]; 54 | 55 | // Since this member does not have a special attribute, the rasterizer 56 | // interpolates its value with the values of the other triangle vertices 57 | // and then passes the interpolated value to the fragment shader for each 58 | // fragment in the triangle. 59 | float4 color; 60 | 61 | float2 texCoord; 62 | }; 63 | 64 | vertex RasterizerData 65 | vertexShader(uint vertexID [[vertex_id]], 66 | constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]], 67 | constant TextureInfo *textureInfo [[buffer(TextureCoordinates)]], 68 | constant Uniforms & uniforms [[ buffer(BufferIndexUniforms) ]]) 69 | { 70 | RasterizerData out; 71 | float2 pixelSpacePosition = vertices[vertexID].position.xy; 72 | 73 | float4 position = float4(pixelSpacePosition, 0.0, 1.0); 74 | out.position = uniforms.projectionMatrix * uniforms.modelViewMatrix * position; 75 | 76 | vector_float2 texCoord = textureInfo[vertexID].texCoord; 77 | texCoord.y = textureInfo[vertexID].texSize.y - texCoord.y; 78 | texCoord = texCoord / textureInfo[vertexID].texSize; 79 | 80 | out.texCoord = float2(texCoord.x, texCoord.y); 81 | out.color = vertices[vertexID].color; 82 | 83 | return out; 84 | } 85 | 86 | fragment float4 fragmentShader(RasterizerData in [[stage_in]], 87 | texture2d colorMap [[ texture(TextureIndexColor) ]]) 88 | { 89 | if (colorMap.get_width() == 1 && colorMap.get_height() == 1) { 90 | return in.color; 91 | } 92 | 93 | constexpr sampler colorSampler(mip_filter::nearest, 94 | mag_filter::nearest, 95 | min_filter::nearest); 96 | 97 | half4 colorSample = colorMap.sample(colorSampler, in.texCoord.xy); 98 | if(colorSample.w == 0) { 99 | return float4(colorSample); 100 | } 101 | return float4(colorSample.x, colorSample.y, colorSample.z, in.color.w); 102 | } 103 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Input/KeyboardInputComponent.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public enum KeyboardInput: UInt16, Codable, Equatable { 4 | case a = 0 5 | case s = 1 6 | case d = 2 7 | case q = 12 8 | case w = 13 9 | case e = 14 10 | 11 | case enter = 36 12 | case space = 49 13 | case esc = 53 14 | 15 | case upKey = 126 16 | case downKey = 125 17 | case rightKey = 124 18 | case leftKey = 123 19 | } 20 | 21 | public struct KeyboardInputComponent: GameComponent { 22 | public struct Mapping: Equatable, Codable { 23 | public var keySet: Set 24 | public var action: Action 25 | public init(keySet: Set, action: Action) { 26 | self.keySet = keySet 27 | self.action = action 28 | } 29 | } 30 | 31 | public var entity: EntityId 32 | public var pressedKeys: [KeyboardInput: Bool] 33 | public var keyMap: [Mapping] 34 | 35 | public init(entity: EntityId) { 36 | self = .init(entity: entity, pressedKeys: [:], keyMap: []) 37 | } 38 | 39 | public init( 40 | entity: EntityId, 41 | pressedKeys: [KeyboardInput: Bool] = [:], 42 | keyMap: [(Set, Action)] = [] 43 | ) { 44 | self.entity = entity 45 | self.pressedKeys = pressedKeys 46 | self.keyMap = keyMap.map(Mapping.init) 47 | } 48 | 49 | public func isKeyPressed(_ key: KeyboardInput) -> Bool { 50 | pressedKeys[key] == true 51 | } 52 | 53 | public func isAnyKeyPressed(in keySet: Set) -> Bool { 54 | return keySet.contains(where: isKeyPressed) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Input/KeyboardInputReducer.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | 4 | public struct KeyboardInputReducerContext: GameState { 5 | public var entities: EntityRepository = .init() 6 | public var keyboardInput: [EntityId: KeyboardInputComponent] = [:] 7 | 8 | public init( 9 | entities: EntityRepository = .init(), 10 | keyboardInput: [EntityId: KeyboardInputComponent] = [:] 11 | ) { 12 | self.entities = entities 13 | self.keyboardInput = keyboardInput 14 | } 15 | } 16 | 17 | public enum KeyboardInputAction: Equatable & Codable { 18 | case keyDown(KeyboardInput) 19 | case keyUp(KeyboardInput) 20 | } 21 | 22 | public struct KeyboardKeyMapReducer: Reducer { 23 | public init() { } 24 | 25 | public func reduce(state: inout KeyboardInputReducerContext, action: Action, environment: ()) -> GameEffect, Action> { 26 | .none 27 | } 28 | 29 | public func reduce( 30 | state: inout KeyboardInputReducerContext, 31 | delta: Double, 32 | environment: Void 33 | ) -> GameEffect, Action> { 34 | var effects: [GameEffect, Action>] = [] 35 | for keyboard in state.keyboardInput.values { 36 | for mapping in keyboard.keyMap { 37 | if keyboard.isAnyKeyPressed(in: mapping.keySet) { 38 | effects.append(.game(mapping.action)) 39 | } 40 | } 41 | } 42 | return .many(effects) 43 | } 44 | } 45 | 46 | public struct KeyboardInputReducer: Reducer { 47 | 48 | public init() { } 49 | 50 | public func reduce(state: inout KeyboardInputReducerContext, delta: Double, environment: ()) -> GameEffect, KeyboardInputAction> { 51 | .none 52 | } 53 | 54 | public func reduce( 55 | state: inout KeyboardInputReducerContext, 56 | action: KeyboardInputAction, 57 | environment: Void 58 | ) -> GameEffect, KeyboardInputAction> { 59 | state.keyboardInput.forEach { (keyboardId, _) in 60 | switch action { 61 | case .keyDown(let keyboardInput): 62 | state.keyboardInput[keyboardId]?.pressedKeys[keyboardInput] = true 63 | case .keyUp(let keyboardInput): 64 | state.keyboardInput[keyboardId]?.pressedKeys.removeValue(forKey: keyboardInput) 65 | } 66 | } 67 | return .none 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/InteractionComponent.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct InteractionComponent: GameComponent { 4 | public enum InteractionType: String, Codable, Equatable { 5 | case proximity 6 | case selection 7 | } 8 | public var entity: EntityId 9 | public var interactionType: InteractionType? 10 | public var action: Action? 11 | public var radius: Double 12 | public var triggerTags: Set 13 | 14 | public init(entity: EntityId) { 15 | self = .init(entity: entity, interactionType: nil, action: nil, radius: 0, triggerTags: []) 16 | } 17 | 18 | public init( 19 | entity: EntityId, 20 | interactionType: InteractionType?, 21 | action: Action?, 22 | radius: Double, 23 | triggerTags: Set 24 | ) { 25 | self.entity = entity 26 | self.interactionType = interactionType 27 | self.action = action 28 | self.radius = radius 29 | self.triggerTags = triggerTags 30 | } 31 | } 32 | 33 | public struct InteractionContext: GameState { 34 | public var entities: EntityRepository 35 | public var transform: [EntityId: TransformComponent] 36 | public var interaction: [EntityId: InteractionComponent] 37 | public init( 38 | entities: EntityRepository, 39 | transform: [EntityId: TransformComponent], 40 | interaction: [EntityId: InteractionComponent] 41 | ) { 42 | self.entities = entities 43 | self.transform = transform 44 | self.interaction = interaction 45 | } 46 | } 47 | 48 | /// Triggers interaction components with a proximity interction type when the level's player is in range 49 | public struct InteractionWhenNearbyReducer: Reducer { 50 | public init() {} 51 | public func reduce( 52 | state: inout InteractionContext, 53 | action: Action, 54 | environment: () 55 | ) -> GameEffect, Action> { .none } 56 | 57 | public func reduce( 58 | state: inout InteractionContext, 59 | delta: Double, 60 | environment: Void 61 | ) -> GameEffect, Action> { 62 | var effects = [GameEffect, Action>]() 63 | state.interaction 64 | .filter { $1.interactionType == .proximity } 65 | .forEach { interactionId, interaction in 66 | guard let action = interaction.action, let interactionTransform = state.transform[interactionId] else { 67 | return 68 | } 69 | 70 | for triggerTag in interaction.triggerTags { 71 | state.entities.tags[triggerTag]?.forEach({ triggererEntityId in 72 | guard let triggererTransform = state.transform[triggererEntityId] 73 | else { 74 | assertionFailure("missing position component for triggerer") 75 | return 76 | } 77 | if interactionTransform.position.distanceFrom(triggererTransform.position) <= interaction.radius { 78 | effects.append(.game(action)) 79 | } 80 | }) 81 | } 82 | } 83 | if effects.isEmpty { 84 | return .none 85 | } else { 86 | return .many(effects) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Momentum/MomentumComponent.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | 4 | public struct MomentumComponent: GameComponent { 5 | public let entity: EntityId 6 | public var velocity: Point 7 | 8 | public init(entity: EntityId) { 9 | self = .init(entity: entity, velocity: .zero) 10 | } 11 | public init( 12 | entity: EntityId, 13 | velocity: Point 14 | ) { 15 | self.entity = entity 16 | self.velocity = velocity 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Momentum/MomentumReducer.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | 4 | public struct MomentumReducerContext: GameState { 5 | public var entities: EntityRepository = .init() 6 | public var momentum: [EntityId: MomentumComponent] = [:] 7 | public var movement: [EntityId: MovementComponent] = [:] 8 | 9 | public init( 10 | entities: EntityRepository = .init(), 11 | momentum: [EntityId : MomentumComponent] = [:], 12 | movement: [EntityId : MovementComponent] = [:] 13 | ) { 14 | self.entities = entities 15 | self.momentum = momentum 16 | self.movement = movement 17 | } 18 | } 19 | 20 | public struct MomentumReducer: Reducer { 21 | public init() { } 22 | public func reduce( 23 | state: inout MomentumReducerContext, 24 | delta: Double, 25 | environment: Void 26 | ) -> GameEffect { 27 | state.momentum.forEach { (id, momentum) in 28 | guard var move = state.movement[id] else { return } 29 | move.velocity += momentum.velocity 30 | state.movement[id] = move 31 | } 32 | return .none 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Movement/MovementComponent.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | 4 | public struct MovementComponent: GameComponent { 5 | public let entity: EntityId 6 | public var velocity: Point 7 | public var travelSpeed: Double 8 | public var recentVelocityHistory: [Point] = [] 9 | 10 | public init(entity: EntityId) { 11 | self = .init(entity: entity, velocity: .zero, travelSpeed: 1) 12 | } 13 | 14 | public init( 15 | entity: EntityId, 16 | velocity: Point, 17 | travelSpeed: Double 18 | ) { 19 | self.entity = entity 20 | self.velocity = velocity 21 | self.travelSpeed = travelSpeed 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Movement/MovementReducer.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | 4 | public struct MovementReducerContext: GameState { 5 | public var entities: EntityRepository = .init() 6 | public var transform: [EntityId: TransformComponent] = [:] 7 | public var movement: [EntityId: MovementComponent] = [:] 8 | 9 | public init( 10 | entities: EntityRepository = .init(), 11 | transform: [EntityId: TransformComponent] = [:], 12 | movement: [EntityId : MovementComponent] = [:] 13 | ) { 14 | self.entities = entities 15 | self.transform = transform 16 | self.movement = movement 17 | } 18 | } 19 | 20 | public struct MovementReducer: Reducer { 21 | public init() { } 22 | public func reduce( 23 | state: inout MovementReducerContext, 24 | delta: Double, 25 | environment: Void 26 | ) -> GameEffect { 27 | state.movement.forEach { (id, movement) in 28 | var movement = movement 29 | guard var point = state.transform[id]?.position else { return } 30 | let deltaVelocity = movement.velocity * delta 31 | 32 | movement.recentVelocityHistory.append(deltaVelocity) 33 | movement.recentVelocityHistory = Array(movement.recentVelocityHistory.suffix(1)) 34 | movement.velocity = .zero 35 | 36 | let avgVelocity = movement.recentVelocityHistory.reduce(Point.zero, +) / Double(movement.recentVelocityHistory.count) 37 | point += (avgVelocity * movement.travelSpeed) 38 | 39 | state.transform[id]?.position = point 40 | state.movement[id] = movement 41 | } 42 | return .none 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/BasicOperationComponentContext.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct BasicOperationComponentContext: GameState { 4 | public var entities: EntityRepository = .init() 5 | public var transform: [EntityId: TransformComponent] = [:] 6 | public var sprite: [EntityId: SpriteComponent] = [:] 7 | 8 | public init( 9 | entities: EntityRepository, 10 | transform: [EntityId: TransformComponent], 11 | sprite: [EntityId: SpriteComponent] 12 | ) { 13 | self.entities = entities 14 | self.transform = transform 15 | self.sprite = sprite 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/Operation.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | // TODO: Implement these: 4 | // Follow Path 5 | // speed increase/decrease 6 | 7 | public protocol Operation: Codable & Equatable { 8 | associatedtype Action: Equatable & Codable 9 | 10 | var currentTime: Double { get } 11 | var duration: Double { get } 12 | var isComplete: Bool { get } 13 | 14 | mutating func run( 15 | id: EntityId, 16 | state: inout BasicOperationComponentContext, 17 | delta: Double 18 | ) -> GameEffect 19 | 20 | mutating func reset() 21 | } 22 | 23 | public extension Operation { 24 | static var InstantDuration: Double { 0 } 25 | static var InfiniteDuration: Double { -1 } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/OperationComponent.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | import OrderedCollections 4 | 5 | public protocol OperationCapableGameState: GameState { 6 | associatedtype GameAction: Equatable & Codable 7 | 8 | var operation: [EntityId: OperationComponent] { get set } 9 | var transform: [EntityId: TransformComponent] { get set } 10 | var sprite: [EntityId: SpriteComponent] { get set } 11 | } 12 | 13 | extension OperationCapableGameState { 14 | public var operationContext: OperationComponentContext { 15 | get { 16 | OperationComponentContext( 17 | entities: entities, 18 | operation: operation, 19 | transform: transform, 20 | sprite: sprite 21 | ) 22 | } 23 | set { 24 | self.transform = newValue.transform 25 | self.operation = newValue.operation 26 | self.sprite = newValue.sprite 27 | } 28 | } 29 | 30 | var basicOperationComponentState: BasicOperationComponentContext { 31 | get { 32 | BasicOperationComponentContext( 33 | entities: entities, 34 | transform: transform, 35 | sprite: sprite 36 | ) 37 | } 38 | set { 39 | self.transform = newValue.transform 40 | self.sprite = newValue.sprite 41 | } 42 | } 43 | } 44 | 45 | public struct OperationComponent: GameComponent { 46 | public var entity: EntityId 47 | public var operations: OrderedDictionary> 48 | 49 | public init(entity: EntityId) { 50 | self = .init(entity: entity, operations: [:]) 51 | } 52 | 53 | public init ( 54 | entity: EntityId, 55 | operation: OperationType 56 | ) { 57 | self.init(entity: entity) 58 | self.newOperation(operation) 59 | } 60 | 61 | public init ( 62 | entity: EntityId, 63 | operations: OrderedDictionary> = [:] 64 | ) { 65 | self.entity = entity 66 | self.operations = operations 67 | } 68 | 69 | public mutating func newOperation(_ type: OperationType) { 70 | let name = newEntityId() 71 | newOperation(name: name, type) 72 | } 73 | 74 | public mutating func newOperation(name: String, _ type: OperationType) { 75 | operations[name] = type 76 | } 77 | 78 | public mutating func removeOperation(name: String) { 79 | operations[name] = nil 80 | } 81 | 82 | public mutating func removeAllOperations() { 83 | operations.removeAll() 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/OperationReducer.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct OperationComponentContext: GameState, OperationCapableGameState { 4 | public var entities: EntityRepository = .init() 5 | public var operation: [EntityId: OperationComponent] = [:] 6 | public var transform: [EntityId: TransformComponent] = [:] 7 | public var sprite: [EntityId : SpriteComponent] = [:] 8 | 9 | public init( 10 | entities: EntityRepository, 11 | operation: [EntityId : OperationComponent], 12 | transform: [EntityId: TransformComponent], 13 | sprite: [EntityId : SpriteComponent] 14 | ) { 15 | self.entities = entities 16 | self.operation = operation 17 | self.transform = transform 18 | self.sprite = sprite 19 | } 20 | } 21 | 22 | public struct OperationReducer: Reducer { 23 | public func reduce( 24 | state: inout OperationComponentContext, 25 | action: GameAction, 26 | environment: () 27 | ) -> GameEffect, GameAction> { 28 | .none 29 | } 30 | 31 | public init() { } 32 | public func reduce( 33 | state: inout OperationComponentContext, 34 | delta: Double, 35 | environment: Void 36 | ) -> GameEffect, GameAction> { 37 | var effects: [GameEffect, GameAction>] = [] 38 | state.operation.forEach { (id, operationComponent) in 39 | var operationComponent = operationComponent 40 | for (key, operation) in operationComponent.operations { 41 | var operation = operation 42 | let effect = operation.run(id: id, state: &state.basicOperationComponentState, delta: delta) 43 | effects.append(effect.map( 44 | stateTransform: \.basicOperationComponentState, 45 | actionTransform: { $0 } 46 | )) 47 | if operation.isComplete == true { 48 | operationComponent.operations[key] = nil 49 | } else { 50 | operationComponent.operations[key] = operation 51 | } 52 | } 53 | state.operation[id] = operationComponent 54 | } 55 | return .many(effects) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/OperationTypes/AnimateOperation.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct AnimateOperation: Operation { 4 | public struct FrameData: Equatable, Codable { 5 | public var texture: TextureReference 6 | public var duration: Double 7 | 8 | public init(texture: TextureReference, duration: Double) { 9 | self.texture = texture 10 | self.duration = duration 11 | } 12 | } 13 | 14 | public var currentTime: Double = 0 15 | public var currentFrameTime: Double = 0 16 | public var currentFrameIndex: Int = 0 17 | public var frames: [FrameData] 18 | public var isComplete: Bool = false 19 | public var duration: Double { 20 | frames.reduce(0) { $0 + $1.duration } 21 | } 22 | 23 | public init( 24 | frames: [FrameData] 25 | ) { 26 | self.frames = frames 27 | } 28 | 29 | public mutating func run( 30 | id: EntityId, 31 | state: inout BasicOperationComponentContext, 32 | delta: Double 33 | ) -> GameEffect { 34 | guard !isComplete, !frames.isEmpty else { 35 | isComplete = true 36 | return .none 37 | } 38 | 39 | if currentTime == 0 { 40 | state.sprite[id]?.setTexture(frames[0].texture) 41 | currentTime += delta 42 | return .none // first frame doesnt need frame delta applied on first tick 43 | } 44 | 45 | currentTime += delta 46 | currentFrameTime += delta 47 | 48 | guard currentFrameTime > frames[currentFrameIndex].duration else { 49 | return .none 50 | } 51 | 52 | currentFrameTime = 0 53 | currentFrameIndex += 1 54 | let isPastFinalFrame = (currentFrameIndex >= frames.count) 55 | if isPastFinalFrame { 56 | isComplete = true 57 | return .none 58 | } 59 | 60 | state.sprite[id]?.setTexture(frames[currentFrameIndex].texture) 61 | 62 | return .none 63 | } 64 | 65 | public mutating func reset() { 66 | currentTime = 0 67 | currentFrameTime = 0 68 | currentFrameIndex = 0 69 | isComplete = false 70 | } 71 | } 72 | 73 | public extension SpriteAnimationDictionary { 74 | func operation(for name: String) -> AnimateOperation? { 75 | guard let anim = self.dict[name] else { return nil } 76 | let frames = anim.frames.map { 77 | AnimateOperation.FrameData( 78 | texture: .init(textureId: self.name, frameId: $0.name), 79 | duration: $0.duration / 1000 80 | ) 81 | } 82 | return AnimateOperation(frames: frames) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/OperationTypes/CallOperation.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct CallOperation: Operation { 4 | public var currentTime: Double = 0 5 | public var duration: Double { Self.InstantDuration } 6 | 7 | public var action: GameAction 8 | public var isComplete: Bool = false 9 | 10 | public init( 11 | action: GameAction 12 | ) { 13 | self.action = action 14 | } 15 | 16 | public mutating func run( 17 | id: EntityId, 18 | state: inout BasicOperationComponentContext, 19 | delta: Double 20 | ) -> GameEffect { 21 | guard !isComplete else { return .none } 22 | isComplete = true 23 | return .game(action) 24 | } 25 | 26 | public mutating func reset() { 27 | currentTime = 0 28 | isComplete = false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/OperationTypes/GroupOperation.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct GroupOperation: Operation { 4 | public var currentTime: Double = 0 5 | public var operations: [OperationType] 6 | 7 | public var currentOperationCompletionCount: Int = 0 8 | public var isComplete: Bool { currentOperationCompletionCount >= operations.count } 9 | 10 | public var duration: Double { 11 | let max = operations.max(by: { $0.duration < $1.duration })?.duration 12 | return max ?? Self.InstantDuration 13 | } 14 | 15 | public init( 16 | operations: [OperationType] 17 | ) { 18 | self.operations = operations 19 | } 20 | 21 | public mutating func run( 22 | id: EntityId, 23 | state: inout BasicOperationComponentContext, 24 | delta: Double 25 | ) -> GameEffect { 26 | guard !operations.isEmpty, currentOperationCompletionCount < operations.count else { return .none } 27 | 28 | var effects: [GameEffect] = [] 29 | 30 | for i in 0..= duration } 18 | 19 | public init(strategy: Strategy, duration: Double, currentTime: Double = 0) { 20 | self.strategy = strategy 21 | self.duration = duration 22 | self.currentTime = currentTime 23 | } 24 | 25 | public mutating func run(id: EntityId, state: inout BasicOperationComponentContext, delta: Double) -> GameEffect { 26 | if currentTime == 0 { 27 | switch strategy { 28 | case .by(let amount): 29 | self.amount = amount 30 | case .to(let location): 31 | let currentPos = (state.transform[id]?.position ?? .zero) 32 | self.amount = location.diffOf(currentPos) 33 | } 34 | } 35 | 36 | let percentage = delta / duration 37 | let moveByIncrement = amount * percentage 38 | state.transform[id]?.position += moveByIncrement 39 | currentTime += delta 40 | 41 | return .none 42 | } 43 | 44 | public mutating func reset() { 45 | currentTime = 0 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/OperationTypes/OpacityOperation.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | 4 | public struct OpacityOperation: Operation { 5 | public typealias Action = Int 6 | 7 | public enum Strategy: Equatable, Codable { 8 | case by(Double) // degrees 9 | case to(Double) 10 | } 11 | 12 | public var strategy: Strategy 13 | public var amount: Double = 0 14 | public var duration: Double 15 | public var currentTime: Double = 0 16 | 17 | public var isComplete: Bool { currentTime >= duration } 18 | 19 | public init( 20 | strategy: Strategy, 21 | duration: Double, 22 | currentTime: Double = 0 23 | ) { 24 | self.strategy = strategy 25 | self.duration = duration 26 | self.currentTime = currentTime 27 | } 28 | 29 | public mutating func run( 30 | id: EntityId, 31 | state: inout BasicOperationComponentContext, 32 | delta: Double 33 | ) -> GameEffect { 34 | if currentTime == 0 { 35 | switch strategy { 36 | case .by(let amount): 37 | self.amount = amount 38 | case .to(let opacity): 39 | let current = (state.sprite[id]?.opacity ?? 0) 40 | if opacity > current { 41 | self.amount = opacity - current 42 | } else { 43 | self.amount = -(current - opacity) 44 | } 45 | } 46 | } 47 | 48 | let percentage = delta / duration 49 | let opacityIncrement = amount * percentage 50 | state.sprite[id]?.opacity += opacityIncrement // TODO: this doesnt work for shapes or labels 51 | currentTime += delta 52 | return .none 53 | } 54 | 55 | public mutating func reset() { 56 | currentTime = 0 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/OperationTypes/RepeatOperation.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct RepeatOperation: Operation { 4 | public enum Strategy: Equatable, Codable { 5 | case forever 6 | case times(Int) 7 | } 8 | 9 | public var strategy: Strategy 10 | public var operation: OperationType 11 | public var totalTime: Double = 0 12 | public var currentTime: Double = 0 13 | public var isComplete: Bool = false 14 | 15 | public var duration: Double { 16 | switch strategy { 17 | case .forever: 18 | return Self.InfiniteDuration 19 | case .times(let times): 20 | return operation.duration * Double(times) 21 | } 22 | } 23 | 24 | public init( 25 | strategy: Strategy, 26 | operation: OperationType 27 | ) { 28 | self.strategy = strategy 29 | self.operation = operation 30 | } 31 | 32 | public mutating func run( 33 | id: EntityId, 34 | state: inout BasicOperationComponentContext, 35 | delta: Double 36 | ) -> GameEffect { 37 | 38 | let effect = operation.run(id: id, state: &state, delta: delta) 39 | 40 | currentTime += delta 41 | totalTime += delta 42 | 43 | switch strategy { 44 | case .forever: break 45 | case .times(let count): 46 | if Int(totalTime / currentTime) >= count { 47 | isComplete = true 48 | } 49 | } 50 | 51 | if operation.isComplete { 52 | currentTime = 0 53 | operation.reset() 54 | } 55 | 56 | return effect 57 | } 58 | 59 | public mutating func reset() { 60 | currentTime = 0 61 | totalTime = 0 62 | operation.reset() 63 | isComplete = false 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/OperationTypes/RotateOperation.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | 4 | public struct RotateOperation: Operation { 5 | public typealias Action = Int 6 | 7 | public enum Strategy: Equatable, Codable { 8 | case by(Double) // degrees 9 | case to(Double) 10 | } 11 | 12 | public var strategy: Strategy 13 | public var amount: Double = 0 14 | public var duration: Double 15 | public var currentTime: Double = 0 16 | 17 | public var isComplete: Bool { currentTime >= duration } 18 | 19 | public init( 20 | strategy: Strategy, 21 | duration: Double, 22 | currentTime: Double = 0 23 | ) { 24 | self.strategy = strategy 25 | self.duration = duration 26 | self.currentTime = currentTime 27 | } 28 | 29 | public mutating func run( 30 | id: EntityId, 31 | state: inout BasicOperationComponentContext, 32 | delta: Double 33 | ) -> GameEffect { 34 | if currentTime == 0 { 35 | switch strategy { 36 | case .by(let amount): 37 | self.amount = amount 38 | case .to(let angle): 39 | let rotation = (state.transform[id]?.rotate ?? 0) 40 | if angle > rotation { 41 | self.amount = angle - rotation 42 | } else { 43 | self.amount = -(rotation - angle) 44 | } 45 | } 46 | } 47 | 48 | let percentage = delta / duration 49 | let rotateByIncrement = amount * percentage 50 | state.transform[id]?.rotate += rotateByIncrement 51 | currentTime += delta 52 | return .none 53 | } 54 | 55 | public mutating func reset() { 56 | currentTime = 0 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/OperationTypes/ScaleOperation.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | 4 | public struct ScaleOperation: Operation { 5 | public enum Strategy: Equatable, Codable { 6 | case by(Point) 7 | case to(Point) 8 | } 9 | 10 | public typealias Action = Int 11 | 12 | public var strategy: Strategy 13 | public var amount: Point = .zero 14 | public var duration: Double 15 | public var currentTime: Double = 0 16 | 17 | public var isComplete: Bool { currentTime >= duration } 18 | 19 | public init(strategy: Strategy, duration: Double, currentTime: Double = 0) { 20 | self.strategy = strategy 21 | self.duration = duration 22 | self.currentTime = currentTime 23 | } 24 | 25 | public mutating func run(id: EntityId, state: inout BasicOperationComponentContext, delta: Double) -> GameEffect { 26 | 27 | if currentTime == 0 { 28 | switch strategy { 29 | case .by(let point): 30 | self.amount = point 31 | case .to(let point): 32 | let scale = state.transform[id]?.scale ?? .zero 33 | self.amount = point - scale 34 | } 35 | } 36 | 37 | let percentage = delta / duration 38 | let scaleIncrement = amount * percentage 39 | state.transform[id]?.scale += scaleIncrement 40 | currentTime += delta 41 | return .none 42 | } 43 | 44 | public mutating func reset() { 45 | currentTime = 0 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/OperationTypes/SequenceOperation.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct SequenceOperation: Operation { 4 | public var currentTime: Double = 0 5 | public var operations: [OperationType] 6 | 7 | public var duration: Double { 8 | if operations.isEmpty { 9 | return Self.InstantDuration 10 | } else { 11 | return operations.reduce(0) { $0 + $1.duration } 12 | } 13 | } 14 | 15 | public var currentOperationIndex: Int = 0 16 | public var isComplete: Bool { currentOperationIndex >= operations.count } 17 | 18 | public init( 19 | operations: [OperationType] 20 | ) { 21 | self.operations = operations 22 | } 23 | 24 | public mutating func run( 25 | id: EntityId, 26 | state: inout BasicOperationComponentContext, 27 | delta: Double 28 | ) -> GameEffect { 29 | guard !operations.isEmpty, currentOperationIndex < operations.count else { return .none } 30 | let effect = operations[currentOperationIndex].run(id: id, state: &state, delta: delta) 31 | if operations[currentOperationIndex].isComplete { 32 | currentOperationIndex += 1 33 | } 34 | return effect 35 | } 36 | 37 | public mutating func reset() { 38 | currentTime = 0 39 | currentOperationIndex = 0 40 | for i in 0..: Operation { 5 | public typealias Action = GameAction 6 | 7 | public enum Strategy: Equatable, Codable { 8 | case easeIn 9 | case easeOut 10 | case easeInOut 11 | } 12 | 13 | public var strategy: Strategy 14 | public var operation: OperationType 15 | public var duration: Double 16 | public var currentTime: Double = 0 17 | 18 | public var previousPercentage: Double = 0 19 | 20 | public var isComplete: Bool { currentTime >= duration } 21 | 22 | public init(strategy: Strategy, operation: OperationType) { 23 | self.strategy = strategy 24 | self.operation = operation 25 | self.duration = operation.duration 26 | } 27 | 28 | public mutating func run(id: EntityId, state: inout BasicOperationComponentContext, delta: Double) -> GameEffect { 29 | 30 | let adjustedPercent = strategy.timing(currentTime / duration + delta / duration) 31 | let deltaPercent = adjustedPercent - previousPercentage 32 | let effect = operation.run(id: id, state: &state, delta: deltaPercent * duration) 33 | previousPercentage = adjustedPercent 34 | 35 | currentTime += delta 36 | 37 | return effect 38 | } 39 | 40 | public mutating func reset() { 41 | currentTime = 0 42 | previousPercentage = 0 43 | operation.reset() 44 | } 45 | } 46 | 47 | public extension TimingOperation.Strategy { 48 | func timing(_ t: Double) -> Double { 49 | switch self { 50 | case .easeIn: 51 | return easeIn(t) 52 | case .easeOut: 53 | return easeOut(t) 54 | case .easeInOut: 55 | return lerp(easeIn(t), easeOut(t), t) 56 | } 57 | } 58 | 59 | func easeIn(_ t: Double) -> Double { 60 | square(t) 61 | } 62 | 63 | func easeOut(_ t: Double) -> Double { 64 | flip(square(flip(t))) 65 | } 66 | 67 | func flip(_ t: Double) -> Double { 68 | 1 - t 69 | } 70 | 71 | func square(_ t: Double) -> Double { 72 | t * t 73 | } 74 | 75 | func lerp(_ a: Double, _ b: Double, _ t: Double) -> Double { 76 | ((b - a) * t) + a 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/OperationTypes/VisibilityOperation.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | 4 | public struct VisibilityOperation: Operation { 5 | public typealias Action = Int 6 | 7 | public enum Strategy: Equatable, Codable { 8 | case show 9 | case hide 10 | case toggle 11 | } 12 | 13 | public var strategy: Strategy 14 | public var currentTime: Double = 0 15 | public var duration: Double { Self.InstantDuration } 16 | public var isComplete: Bool = false 17 | 18 | public init(strategy: Strategy) { 19 | self.strategy = strategy 20 | } 21 | 22 | public mutating func run( 23 | id: EntityId, 24 | state: inout BasicOperationComponentContext, 25 | delta: Double 26 | ) -> GameEffect { 27 | guard !isComplete else { return .none } 28 | isComplete = true 29 | switch strategy { 30 | case .hide: 31 | state.transform[id]?.isHidden = true 32 | case .show: 33 | state.transform[id]?.isHidden = false 34 | case .toggle: 35 | state.transform[id]?.isHidden.toggle() 36 | } 37 | return .none 38 | } 39 | 40 | public mutating func reset() { 41 | currentTime = 0 42 | isComplete = false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Operation/OperationTypes/WaitOperation.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct WaitOperation: Operation { 4 | public var duration: Double 5 | public var currentTime: Double = 0 6 | 7 | public var isComplete: Bool { currentTime >= duration } 8 | 9 | public init(duration: Double) { 10 | self.duration = duration 11 | } 12 | 13 | public mutating func run( 14 | id: EntityId, 15 | state: inout BasicOperationComponentContext, 16 | delta: Double 17 | ) -> GameEffect { 18 | currentTime += delta 19 | return .none 20 | } 21 | 22 | public mutating func reset() { 23 | currentTime = 0 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Pathing/PathingComponent.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | 4 | public struct PathingComponent: GameComponent { 5 | public var entity: EntityId 6 | /// How far from the next target location is "close enough", 0 means precise location 7 | public var allowableProximityVariance: Double = 1 8 | public var travelPath: [Point] = [] 9 | 10 | public init(entity: EntityId) { 11 | self.entity = entity 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/Pathing/PathingReducer.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | 4 | public struct PathingReducerContext: GameState { 5 | public var entities: EntityRepository = .init() 6 | public var transform: [EntityId: TransformComponent] 7 | public var movement: [EntityId: MovementComponent] 8 | public var pathing: [EntityId: PathingComponent] 9 | 10 | public init( 11 | entities: EntityRepository = .init(), 12 | transform: [EntityId: TransformComponent], 13 | movement: [EntityId : MovementComponent], 14 | pathing: [EntityId : PathingComponent] 15 | ) { 16 | self.entities = entities 17 | self.transform = transform 18 | self.movement = movement 19 | self.pathing = pathing 20 | } 21 | } 22 | 23 | public enum PathingAction: Equatable, Codable { 24 | case setPath(EntityId, [Point]) 25 | case appendPath(EntityId, Point) 26 | case requestPathingCalculation(EntityId, to: Point) 27 | case pathingComplete(EntityId) 28 | } 29 | 30 | public struct PathingReducer: Reducer { 31 | public init() {} 32 | public func reduce( 33 | state: inout PathingReducerContext, 34 | delta: Double, 35 | environment: Void 36 | ) -> GameEffect { 37 | var effects: [GameEffect] = [] 38 | state.pathing.forEach { (id, pathing) in 39 | guard let firstLocation = pathing.travelPath.first, 40 | let position = state.transform[id] else { return } 41 | 42 | if position.position.distanceFrom(firstLocation) < pathing.allowableProximityVariance { 43 | state.pathing[id]?.travelPath.removeFirst() 44 | if state.pathing[id]?.travelPath.isEmpty == true { 45 | effects.append(.game(.pathingComplete(id))) 46 | } 47 | } else { 48 | var velocity: Point = .zero 49 | let diffPos = position.position.diffOf(firstLocation) 50 | let maxDirectionalDistance = max(abs(diffPos.x), abs(diffPos.y)) 51 | 52 | velocity.x -= diffPos.x != 0 ? max(min(diffPos.x / maxDirectionalDistance, 1) , -1) : 0 53 | velocity.y -= diffPos.y != 0 ? max(min(diffPos.y / maxDirectionalDistance, 1) , -1) : 0 54 | 55 | state.movement[id]?.velocity = velocity 56 | } 57 | } 58 | guard !effects.isEmpty else { return .none } 59 | return .many(effects) 60 | } 61 | 62 | public func reduce( 63 | state: inout PathingReducerContext, 64 | action: PathingAction, 65 | environment: () 66 | ) -> GameEffect { 67 | switch action { 68 | case .setPath(let entity, let points): 69 | assert(state.pathing[entity] != nil, "attempting to use pathing on an entity without a pathing component") 70 | state.pathing[entity]?.travelPath = points 71 | case .appendPath(let entity, let point): 72 | assert(state.pathing[entity] != nil, "attempting to use pathing on an entity without a pathing component") 73 | state.pathing[entity]?.travelPath.append(point) 74 | case .requestPathingCalculation(let entity, _): 75 | assert(state.pathing[entity] != nil, "attempting to use pathing on an entity without a pathing component") 76 | break // Needs implementing by another custom reducer 77 | case .pathingComplete: 78 | break 79 | } 80 | return .none 81 | } 82 | } 83 | 84 | public struct StraightLinePathingCalculatorReducer: Reducer { 85 | public init() {} 86 | public func reduce( 87 | state: inout PathingReducerContext, 88 | delta: Double, 89 | environment: Void 90 | ) -> GameEffect { 91 | return .none 92 | } 93 | 94 | public func reduce( 95 | state: inout PathingReducerContext, 96 | action: PathingAction, 97 | environment: () 98 | ) -> GameEffect { 99 | switch action { 100 | case .requestPathingCalculation(let eId, let point): 101 | return .game(.setPath(eId, [point])) 102 | default: 103 | break 104 | } 105 | return .none 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/ResourceLoading/ResourceLoadingAction.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public enum ResourceLoadingAction: Equatable, Codable { 4 | case load(groupName: String, resources: [LoadableResource]) 5 | case loadComplete(groupName: String) 6 | case loadingError(groupName: String, error: String) 7 | } 8 | -------------------------------------------------------------------------------- /Sources/RedECSBasicComponents/ResourceLoading/ResourceLoadingReducer.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct ResourceLoadingReducer: Reducer { 4 | public typealias State = S 5 | public typealias Action = ResourceLoadingAction 6 | public typealias Environment = RenderingEnvironment 7 | 8 | public init() {} 9 | 10 | public func reduce(state: inout State, delta: Double, environment: Environment) -> GameEffect { 11 | return .none 12 | } 13 | 14 | public func reduce(state: inout State, action: Action, environment: Environment) -> GameEffect { 15 | switch action { 16 | case .load(let groupName, let resources): 17 | print("⚙️ Load start, group: ", groupName) 18 | let future: Future, Never> = environment.resourceManager 19 | .preload(resources) 20 | .map({ _ in 21 | return .game(.loadComplete(groupName: groupName)) 22 | }) 23 | .recoverError({ error in 24 | .game(.loadingError(groupName: groupName, error: String(describing: error))) 25 | }) 26 | return .deferred(future) 27 | case .loadingError(let groupName, let error): 28 | print("💥 Preload Error :: \(groupName) :: \(String(describing: error))") 29 | assertionFailure() 30 | return .none 31 | case .loadComplete(let groupName): 32 | print("✅ Load complete, group: ", groupName) 33 | return .none 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/RedECSKit/RedECSKit.swift: -------------------------------------------------------------------------------- 1 | enum RedECSKit {} 2 | -------------------------------------------------------------------------------- /Sources/RedECSUIComponents/HUD/HUDComponent.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | import Geometry 3 | 4 | public enum HUDAction: Equatable & Codable { 5 | case inputDown(Point) 6 | case inputUp(Point) 7 | case onHUDElementInputDown(Formatter.ElementId) 8 | } 9 | 10 | public protocol HUDRenderingCapable: GameState where Formatter.State == Self { 11 | associatedtype Formatter: HUDElementFormattable 12 | var hud: [EntityId: HUDComponent] { get set } 13 | } 14 | 15 | public protocol HUDElementFormattable: Equatable & Codable { 16 | associatedtype ElementId: Hashable & Codable 17 | associatedtype State: GameState 18 | func format(_ elementId: ElementId, _ state: State) -> String 19 | } 20 | 21 | public struct HUDComponent: GameComponent { 22 | public var entity: EntityId 23 | public var children: [HUDElement] 24 | 25 | public init(entity: EntityId) { 26 | self = .init(entity: entity, children: []) 27 | } 28 | 29 | public init ( 30 | entity: EntityId, 31 | children: [HUDElement] 32 | ) { 33 | self.entity = entity 34 | self.children = children 35 | } 36 | } 37 | 38 | public struct HUDElement: Equatable & Codable { 39 | public var id: Formatter.ElementId 40 | public var position: Point 41 | public var type: HUDElementType 42 | 43 | public init( 44 | id: Formatter.ElementId, 45 | position: Point, 46 | type: HUDElementType 47 | ) { 48 | self.id = id 49 | self.position = position 50 | self.type = type 51 | } 52 | } 53 | 54 | public indirect enum HUDElementType: Equatable & Codable { 55 | case label(HUDLabel) 56 | case button(HUDButton) 57 | } 58 | 59 | public struct HUDLabel: Equatable & Codable { 60 | public var size: Double 61 | public var strategy: HUDLabelStrategy 62 | 63 | public init( 64 | size: Double, 65 | strategy: HUDLabelStrategy 66 | ) { 67 | self.size = size 68 | self.strategy = strategy 69 | } 70 | } 71 | 72 | public enum HUDLabelStrategy: Equatable & Codable { 73 | case fixed(String) 74 | case dynamic(Formatter) 75 | } 76 | 77 | public struct HUDButton: Equatable & Codable { 78 | public var shape: Shape 79 | public var fillColor: Color 80 | public var labelStrategy: HUDLabelStrategy? 81 | 82 | public init( 83 | shape: Shape, 84 | fillColor: Color, 85 | labelStrategy: HUDLabelStrategy? = nil 86 | ) { 87 | self.shape = shape 88 | self.fillColor = fillColor 89 | self.labelStrategy = labelStrategy 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/RedECSUIComponents/HUD/HUDRenderingContext.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct HUDRenderingContext< 4 | Formatter: HUDElementFormattable, 5 | Action: Equatable & Codable 6 | >: GameState { 7 | public var entities: EntityRepository = .init() 8 | public var hud: [EntityId: HUDComponent] 9 | 10 | public init( 11 | entities: EntityRepository = .init(), 12 | hud: [EntityId: HUDComponent] 13 | ) { 14 | self.entities = entities 15 | self.hud = hud 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/RedECSWebSupport/WebBrowserKeyboardInput.swift: -------------------------------------------------------------------------------- 1 | import RedECSBasicComponents 2 | 3 | public enum WebBrowserKeyboardInput: String, Codable { 4 | case a = "KeyA" 5 | case s = "KeyS" 6 | case d = "KeyD" 7 | case q = "KeyQ" 8 | case w = "KeyW" 9 | case e = "KeyE" 10 | 11 | case enter = "Enter" 12 | case space = "Space" 13 | case esc = "Escape" 14 | 15 | case upKey = "ArrowUp" 16 | case downKey = "ArrowDown" 17 | case rightKey = "ArrowRight" 18 | case leftKey = "ArrowLeft" 19 | } 20 | 21 | public extension WebBrowserKeyboardInput { 22 | var keyboardInput: KeyboardInput { 23 | switch self { 24 | case .a: 25 | return .a 26 | case .s: 27 | return .s 28 | case .d: 29 | return .d 30 | case .q: 31 | return .q 32 | case .w: 33 | return .w 34 | case .e: 35 | return .e 36 | case .enter: 37 | return .enter 38 | case .space: 39 | return .space 40 | case .esc: 41 | return .esc 42 | case .upKey: 43 | return .upKey 44 | case .downKey: 45 | return .downKey 46 | case .rightKey: 47 | return .rightKey 48 | case .leftKey: 49 | return .leftKey 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/RedECSWebSupport/WebEnvironment.swift: -------------------------------------------------------------------------------- 1 | import RedECS 2 | 3 | public struct WebEnvironment: RenderingEnvironment { 4 | public var renderer: Renderer { webRenderer } 5 | public var resourceManager: ResourceManager { webResourceManager } 6 | 7 | public var webRenderer: WebRenderer 8 | public var webResourceManager: WebResourceManager 9 | 10 | public init( 11 | webRenderer: WebRenderer, 12 | webResourceManager: WebResourceManager 13 | ) { 14 | self.webRenderer = webRenderer 15 | self.webResourceManager = webResourceManager 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/RedECSWebSupport/WebGL/WebGLProgram.swift: -------------------------------------------------------------------------------- 1 | import JavaScriptKit 2 | 3 | public protocol WebGLProgram { 4 | var vertexShader: String { get } 5 | var fragmentShader: String { get } 6 | 7 | func execute(with webRenderer: WebRenderer) throws 8 | } 9 | 10 | extension WebGLProgram { 11 | func createProgram(with webRenderer: WebRenderer) throws -> JSValue { 12 | let gl = webRenderer.glContext 13 | let shaders = try setUpShaders(with: webRenderer) 14 | return try createProgram(gl: gl, vertexShader: shaders.vertex, fragmentShader: shaders.fragment) 15 | } 16 | 17 | private func setUpShaders( 18 | with webRenderer: WebRenderer 19 | ) throws -> (vertex: JSValue, fragment: JSValue) { 20 | let gl = webRenderer.glContext 21 | let vertexShader = try createShader(gl: gl, type: gl.VERTEX_SHADER, source: vertexShader) 22 | let fragmentShader = try createShader(gl: gl, type: gl.FRAGMENT_SHADER, source: fragmentShader) 23 | return (vertexShader, fragmentShader) 24 | } 25 | 26 | private func createProgram( 27 | gl: JSValue, 28 | vertexShader: JSValue, 29 | fragmentShader: JSValue 30 | ) throws -> JSValue { 31 | let program = gl.createProgram() 32 | _ = gl.attachShader(program, vertexShader) 33 | _ = gl.attachShader(program, fragmentShader) 34 | _ = gl.linkProgram(program) 35 | 36 | let success = gl.getProgramParameter(program, gl.LINK_STATUS) 37 | if success.boolean == true { 38 | return program 39 | } 40 | 41 | throw WebGLError.couldNotCreateShader(gl.getProgramInfoLog(program).string) 42 | } 43 | 44 | private func createShader(gl: JSValue, type: JSValue, source: String) throws -> JSValue { 45 | let shader = gl.createShader(type) 46 | _ = gl.shaderSource(shader, source) 47 | _ = gl.compileShader(shader) 48 | 49 | let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS) 50 | if success.boolean == true { 51 | return shader 52 | } 53 | 54 | throw WebGLError.couldNotCreateShader(gl.getShaderInfoLog(shader).string) 55 | } 56 | } 57 | 58 | enum WebGLError: Error { 59 | case couldNotCreateShader(String?) 60 | case couldNotCreateProgram(String?) 61 | case couldNotCreateArray 62 | } 63 | -------------------------------------------------------------------------------- /Sources/RedECSWebSupport/WebRenderer.swift: -------------------------------------------------------------------------------- 1 | import JavaScriptKit 2 | import RedECS 3 | import Geometry 4 | import GeometryAlgorithms 5 | import RedECSUIComponents 6 | 7 | open class WebRenderer { 8 | public enum State { 9 | case loading 10 | case ready 11 | } 12 | 13 | public private(set) var size: Size 14 | public private(set) var canvasElement: JSValue = .undefined 15 | public private(set) var glContext: JSValue = .undefined 16 | 17 | public var webResourceManager: WebResourceManager 18 | 19 | public var queuedWork: [RenderGroup] = [] 20 | 21 | private(set) var projectionMatrix: Matrix3 = .identity 22 | 23 | lazy var drawProgram: Draw2DProgram = { 24 | Draw2DProgram( 25 | triangles: [], 26 | textureSize: .zero, 27 | image: .null, 28 | color: .clear, 29 | projectionMatrix: .identity, 30 | modelMatrix: .identity 31 | ) 32 | }() 33 | 34 | lazy var emptyImage: JSValue = { 35 | createEmptyImage(size: .init(width: 1, height: 1)) 36 | }() 37 | 38 | public init( 39 | size: Size, 40 | resourceLoader: WebResourceManager 41 | ) { 42 | self.size = size 43 | self.webResourceManager = resourceLoader 44 | setUp() 45 | } 46 | 47 | private func setUp() { 48 | let document = JSObject.global.document 49 | self.canvasElement = document.createElement("canvas") 50 | canvasElement.id = "webgl-canvas" 51 | canvasElement.width = size.width.jsValue 52 | canvasElement.height = size.height.jsValue 53 | _ = document.body.appendChild(canvasElement) 54 | glContext = webGLContext() 55 | } 56 | 57 | public func draw() { 58 | do { 59 | clearCanvas() 60 | for renderGroup in queuedWork.sorted(by: { $0.zIndex < $1.zIndex }) { 61 | switch renderGroup.fragmentType { 62 | case .color(let color): 63 | drawProgram.update( 64 | triangles: renderGroup.triangles, 65 | textureSize: .init(width: 1, height: 1), 66 | image: emptyImage, 67 | color: renderGroup.color ?? .clear, 68 | projectionMatrix: projectionMatrix, 69 | modelMatrix: renderGroup.transformMatrix 70 | ) 71 | try drawProgram.execute(with: self) 72 | case .texture(let textureId): 73 | if let image = webResourceManager.textureImages[textureId], 74 | let imageObject = image.object, 75 | let width = imageObject.width.number, 76 | let height = imageObject.height.number { 77 | drawProgram.update( 78 | triangles: renderGroup.triangles, 79 | textureSize: .init( 80 | width: width, 81 | height: height 82 | ), 83 | image: image, 84 | color: renderGroup.color ?? .init(red: 0, green: 0, blue: 0, alpha: renderGroup.opacity), 85 | projectionMatrix: projectionMatrix, 86 | modelMatrix: renderGroup.transformMatrix 87 | ) 88 | try drawProgram.execute(with: self) 89 | } else { 90 | print("no texture loaded for", textureId) 91 | webResourceManager.startTextureLoadIfNeeded(textureId: textureId) 92 | } 93 | } 94 | } 95 | } catch { 96 | print("⚠️ Draw error:", error) 97 | fatalError() 98 | } 99 | } 100 | 101 | private func webGLContext() -> JSValue { 102 | let document = JSObject.global.document 103 | let canvas = document.querySelector("#webgl-canvas") 104 | let gl = canvas.getContext("webgl") 105 | if gl.isNull { 106 | print("gl is null") 107 | fatalError() 108 | } 109 | return gl 110 | } 111 | 112 | private func clearCanvas() { 113 | // Clear the canvas 114 | _ = glContext.clearColor(0, 0, 0, 1) 115 | _ = glContext.clear(glContext.COLOR_BUFFER_BIT) 116 | } 117 | 118 | private func createEmptyImage(size: Size) -> JSValue { 119 | let document = JSObject.global.document 120 | var canvas = document.createElement("canvas") 121 | canvas.width = size.width.jsValue 122 | canvas.height = size.height.jsValue 123 | 124 | var ctx = canvas.getContext("2d") 125 | ctx.fillStyle = "rgba(0, 0, 0, 0)" 126 | _ = ctx.fillRect(0, 0, size.width.jsValue, size.height.jsValue) 127 | 128 | let img = JSObject.global.Image.function?.new(size.width.jsValue, size.height.jsValue) 129 | img?.src = canvas.toDataURL() 130 | return img?.jsValue ?? .null 131 | } 132 | } 133 | 134 | extension WebRenderer: Renderer { 135 | public var viewportSize: Size { 136 | size 137 | } 138 | 139 | public func setProjectionMatrix(_ matrix: Matrix3) { 140 | projectionMatrix = matrix 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Sources/TiledInterpreter/TiledMap/TiledLayer.swift: -------------------------------------------------------------------------------- 1 | public struct TiledLayer: Codable, Equatable { 2 | public var id: Int 3 | public var name: String 4 | public var type: TiledLayerType? 5 | 6 | public var data: [Int]? 7 | 8 | public var opacity: Double 9 | public var visible: Bool 10 | 11 | public var width: Int? 12 | public var height: Int? 13 | public var x: Double 14 | public var y: Double 15 | 16 | public var objects: [TiledObject]? 17 | } 18 | 19 | public extension TiledLayer { 20 | 21 | func tileDataAt(column: Int, row: Int, flipY: Bool = false) -> Int? { 22 | guard let data = data, let totalCols = width, let totalRows = width else { return nil } 23 | let c = column 24 | var r = row 25 | if flipY { 26 | r = (totalRows - 1) - row 27 | } 28 | let flatIndex = (totalCols * r) + c 29 | return data[flatIndex] 30 | } 31 | 32 | func flipYDataIterator() -> AnyIterator { 33 | guard let data = data, 34 | let totalCols = width, 35 | let totalRows = height else { 36 | return AnyIterator { nil } 37 | } 38 | var row = 0 39 | var col = 0 40 | return AnyIterator { 41 | if row == totalRows { 42 | return nil 43 | } 44 | 45 | let flippedYRow = (totalRows - 1) - row 46 | let flatIndex = (totalCols * flippedYRow) + col 47 | 48 | let value = data[flatIndex] 49 | 50 | col += 1 51 | if col == totalCols { 52 | col = 0 53 | row += 1 54 | } 55 | 56 | return value 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/TiledInterpreter/TiledMap/TiledLayerType.swift: -------------------------------------------------------------------------------- 1 | public enum TiledLayerType: String, Codable, Equatable { 2 | case tileLayer = "tilelayer" 3 | case objectGroup = "objectgroup" 4 | } 5 | -------------------------------------------------------------------------------- /Sources/TiledInterpreter/TiledMap/TiledMapJSON.swift: -------------------------------------------------------------------------------- 1 | public struct TileSetReference: Codable, Equatable { 2 | public var firstgid: Int 3 | public var source: String 4 | } 5 | 6 | public struct TiledMapJSON: Codable, Equatable { 7 | public enum CodingKeys: String, CodingKey { 8 | case tileWidth = "tilewidth" 9 | case tileHeight = "tileheight" 10 | case tileSets = "tilesets" 11 | case width 12 | case height 13 | case layers 14 | } 15 | 16 | public var tileWidth: Int 17 | public var tileHeight: Int 18 | public var width: Int 19 | public var height: Int 20 | 21 | public var layers: [TiledLayer] 22 | public var tileSets: [TileSetReference] 23 | 24 | public init( 25 | tileWidth: Int, 26 | tileHeight: Int, 27 | width: Int, 28 | height: Int, 29 | layers: [TiledLayer], 30 | tileSets: [TileSetReference] 31 | ) { 32 | self.tileWidth = tileWidth 33 | self.tileHeight = tileHeight 34 | self.width = width 35 | self.height = height 36 | self.layers = layers 37 | self.tileSets = tileSets 38 | } 39 | } 40 | 41 | public extension TiledMapJSON { 42 | var totalWidth: Double { 43 | Double(tileWidth * width) 44 | } 45 | var totalHeight: Double { 46 | Double(tileHeight * height) 47 | } 48 | var tileLayers: [TiledLayer] { 49 | layers.filter { $0.type == .tileLayer } 50 | } 51 | var objectLayers: [TiledLayer] { 52 | layers.filter { $0.type == .objectGroup } 53 | } 54 | 55 | func splitTileLayersToMaps() -> [TiledMapJSON] { 56 | (0.. [Int: Tile] { 51 | // we add 1 to the id to compensate for the difference between tile data ids (where 0 == nothing) and the tile id (where 0 is the first tile) 52 | tiles.reduce(into: [Int: Tile]()) { $0[$1.id + 1] = $1 } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/TiledInterpreter/TiledTileSet/TiledTilesetXML.swift: -------------------------------------------------------------------------------- 1 | public struct TiledTilesetXML: Codable, Equatable { 2 | enum CodingKeys: String, CodingKey { 3 | case name 4 | case image 5 | case tileWidth = "tilewidth" 6 | case tileHeight = "tileheight" 7 | case tileCount = "tilecount" 8 | case columns 9 | case tiles = "tile" 10 | } 11 | 12 | public var name: String 13 | public var tileWidth: Int 14 | public var tileHeight: Int 15 | public var tileCount: Int 16 | public var columns: Int 17 | public var image: Image 18 | public var tiles: [Tile]? 19 | 20 | public func makeTileInfoDictionary() -> [Int: Tile] { 21 | tiles.map { 22 | // we add 1 to the id to compensate for the difference between tile data ids (where 0 == nothing) and the tile id (where 0 is the first tile) 23 | $0.reduce(into: [Int: Tile]()) { $0[$1.id + 1] = $1 } 24 | } ?? [:] 25 | } 26 | } 27 | 28 | public extension TiledTilesetXML { 29 | struct Image: Codable, Equatable { 30 | public var source: String 31 | public var width: Int 32 | public var height: Int 33 | } 34 | } 35 | 36 | public extension TiledTilesetXML { 37 | func toJSONFormat() -> TiledTilesetJSON { 38 | return TiledTilesetJSON( 39 | name: name, 40 | image: image.source, 41 | imageWidth: image.width, 42 | imageHeight: image.height, 43 | tileWidth: tileWidth, 44 | tileHeight: tileHeight, 45 | tileCount: tileCount, 46 | columns: columns, 47 | tiles: tiles ?? [] 48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/RedECSTests/TestSystem/TestSystem.swift: -------------------------------------------------------------------------------- 1 | @testable import RedECS 2 | 3 | import Geometry 4 | import RedECSBasicComponents 5 | 6 | struct TestGlobalState: GameState { 7 | var entities: EntityRepository = .init() 8 | var count: Int32 = 0 9 | var text: String = "" 10 | 11 | var transform: [EntityId: TransformComponent] = [:] 12 | var movement: [EntityId: MovementComponent] = [:] 13 | 14 | var containedState: TestLocalState { 15 | get { 16 | TestLocalState(entities: entities, count: count) 17 | } 18 | set { 19 | entities = newValue.entities 20 | count = newValue.count 21 | } 22 | } 23 | 24 | var movementContext: MovementReducerContext { 25 | get { MovementReducerContext(entities: entities, transform: transform, movement: movement) } 26 | set { 27 | entities = newValue.entities 28 | transform = newValue.transform 29 | movement = newValue.movement 30 | } 31 | } 32 | } 33 | 34 | enum TestGlobalAction: Equatable { 35 | case incrementCount 36 | case updateVelocity(entity: EntityId, velocity: Point) 37 | case removeEntity(entity: EntityId) 38 | } 39 | 40 | final class TestGlobalEnvironment { 41 | 42 | } 43 | 44 | struct TestGlobalReducer: Reducer { 45 | func reduce(state: inout TestGlobalState, delta: Double, environment: TestGlobalEnvironment) -> GameEffect { 46 | .none 47 | } 48 | 49 | func reduce( 50 | state: inout TestGlobalState, 51 | action: TestGlobalAction, 52 | environment: TestGlobalEnvironment 53 | ) -> GameEffect { 54 | switch action { 55 | case .incrementCount: 56 | state.count += 1 57 | case .removeEntity(let entity): 58 | return .system(.removeEntity(entity)) 59 | default: break 60 | } 61 | return .none 62 | } 63 | } 64 | 65 | struct TestLocalState: GameState { 66 | var entities: EntityRepository 67 | var count: Int32 68 | } 69 | 70 | struct TestLocalReducer: Reducer { 71 | func reduce(state: inout TestLocalState, delta: Double, environment: TestGlobalEnvironment) -> GameEffect { 72 | .none 73 | } 74 | 75 | func reduce( 76 | state: inout TestLocalState, 77 | action: TestGlobalAction, 78 | environment: TestGlobalEnvironment 79 | ) -> GameEffect { 80 | switch action { 81 | case .incrementCount: 82 | state.count += 1 83 | default: break 84 | } 85 | return .none 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Tests/RenderingTests/BitmapFontTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SnapshotTesting 3 | import RedECS 4 | @testable import RedECSAppleSupport 5 | import MetalKit 6 | import CoreImage 7 | import CoreGraphics 8 | import Geometry 9 | import GeometryAlgorithms 10 | import RedECSBasicComponents 11 | 12 | class BitmapFontTests: XCTestCase { 13 | var mtkView: MTKView! 14 | var renderer: MetalRenderer! 15 | var store: GameStore>! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | 20 | let device = MTLCreateSystemDefaultDevice()! 21 | self.mtkView = MTKView( 22 | frame: .init(origin: .zero, size: .init(width: 600, height: 480)), 23 | device: device 24 | ) 25 | self.renderer = MetalRenderer( 26 | device: device, 27 | pixelFormat: mtkView.colorPixelFormat, 28 | resourceManager: MetalResourceManager(metalDevice: device) 29 | ) 30 | mtkView.delegate = renderer 31 | renderer.mtkView(mtkView, drawableSizeWillChange: .init(width: 600, height: 480)) 32 | 33 | let reducer: AnyReducer = 34 | ( 35 | RenderingReducer(renderableComponentTypes: [ 36 | .init(keyPath: \.sprite), 37 | .init(keyPath: \.label), 38 | .init(keyPath: \.shape) 39 | ]) 40 | .pullback( 41 | toLocalState: \.self, 42 | toLocalEnvironment: { $0 as RenderingEnvironment } 43 | ) 44 | + 45 | CameraReducer() 46 | .pullback( 47 | toLocalState: \.cameraContext, 48 | toLocalEnvironment: { $0 as RenderingEnvironment } 49 | ) 50 | ).eraseToAnyReducer() 51 | 52 | store = GameStore( 53 | state: RenderingTestState(), 54 | environment: RenderingTestEnvironment( 55 | metalRenderer: renderer, 56 | metalResourceManager: renderer.resourceManager 57 | ), 58 | reducer: reducer, 59 | registeredComponentTypes: [ 60 | .init(keyPath: \.transform), 61 | .init(keyPath: \.sprite), 62 | .init(keyPath: \.shape), 63 | .init(keyPath: \.label), 64 | .init(keyPath: \.camera), 65 | ]) 66 | } 67 | 68 | func testLoadFontFile() throws { 69 | let fontFile = URL(fileURLWithPath: #file) 70 | .deletingLastPathComponent() 71 | .appendingPathComponent("Resources") 72 | .appendingPathComponent("pt-mono.fnt") 73 | 74 | guard let fontData = FileManager.default.contents(atPath: fontFile.path), 75 | let fontString = String(data: fontData, encoding: .utf8) else { 76 | XCTFail() 77 | return 78 | } 79 | 80 | let font = try BitmapFont(fromString: fontString) 81 | 82 | XCTAssertEqual(font.info.face, "PT-Mono") 83 | XCTAssertEqual(font.page.file, "pt-mono.png") 84 | XCTAssertEqual(font.characters.count, 95) 85 | } 86 | 87 | func testBitmapFontRender() throws { 88 | snapshotText("Welcome") 89 | snapshotText("Bitmap Font") 90 | snapshotText("Chars--@!_=+?\"'\\") 91 | } 92 | 93 | func snapshotText(_ text: String) { 94 | let exp = expectation(description: "wait for async") 95 | renderer.resourceManager.resourceBundle = .module 96 | renderer.resourceManager.preload([("pt-mono.fnt", .bitmapFont)]) 97 | .subscribe { result in 98 | switch result { 99 | case .success: 100 | break 101 | case .failure(let err): 102 | XCTFail("\(err)") 103 | } 104 | exp.fulfill() 105 | } 106 | waitForExpectations(timeout: 2) 107 | 108 | let entityId = "TextEntity" 109 | let label = LabelComponent(entity: entityId, font: "PT-Mono", text: text) // _':~.,-! Cool you 110 | let transform = TransformComponent( 111 | entity: entityId, 112 | position: .zero, 113 | anchorPoint: .init(x: 0.5, y: 0) 114 | ) 115 | let camera = CameraComponent(entity: entityId) 116 | 117 | store.sendSystemAction(.removeEntity(entityId)) 118 | store.sendSystemAction(.addEntity(entityId, [])) 119 | store.sendSystemAction(.addComponent(transform, into: \.transform)) 120 | store.sendSystemAction(.addComponent(label, into: \.label)) 121 | store.sendSystemAction(.addComponent(camera, into: \.camera)) 122 | 123 | renderer.clearQueue() 124 | enqueueLine(into: renderer) 125 | store.sendDelta(1) 126 | assertSnapshot(matching: mtkView, as: .image(renderer: renderer), named: text) 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /Tests/RenderingTests/Resources/Media.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/RenderingTests/Resources/Media.xcassets/pt-mono.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pt-mono.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/RenderingTests/Resources/Media.xcassets/pt-mono.imageset/pt-mono.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/Resources/Media.xcassets/pt-mono.imageset/pt-mono.png -------------------------------------------------------------------------------- /Tests/RenderingTests/Utilities/EnqueueGrid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnqueueGrid.swift 3 | // 4 | // 5 | // Created by K N on 2022-08-19. 6 | // 7 | 8 | import Geometry 9 | import RedECS 10 | import RedECSAppleSupport 11 | 12 | func enqueueGrid(into renderer: MetalRenderer) { 13 | let increments = 48 14 | let stepBy = 20 15 | let majorIncrements = 6 16 | for i in 0.. Snapshotting { 15 | Snapshotting.image(precision: 1).pullback { mtkView in 16 | mtkView.framebufferOnly = false 17 | mtkView.drawableSize = mtkView.frame.size 18 | renderer.mtkView(mtkView, drawableSizeWillChange: mtkView.drawableSize) 19 | renderer.draw(in: mtkView) 20 | let texture = mtkView.currentDrawable!.texture 21 | let ciImage = CIImage(mtlTexture: texture)! 22 | let flipped = ciImage.transformed(by: CGAffineTransform(scaleX: 1, y: -1)) 23 | let opt = [CIContextOption.outputPremultiplied: true, 24 | CIContextOption.useSoftwareRenderer: false] 25 | let cont = CIContext(options: opt) 26 | let cgImage = cont.createCGImage(flipped, from: flipped.extent)! 27 | return NSImage(cgImage: cgImage, size: mtkView.frame.size) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/RenderingTests/Utilities/Point+Rounded.swift: -------------------------------------------------------------------------------- 1 | import Geometry 2 | 3 | extension Point { 4 | func rounded() -> Point { 5 | Point(x: x.rounded(), y: y.rounded()) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Tests/RenderingTests/Utilities/RenderingTestSystem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import RedECS 3 | import Geometry 4 | import RedECSBasicComponents 5 | import RedECSAppleSupport 6 | 7 | struct RenderingTestState: RenderableGameState { 8 | var entities: EntityRepository = .init() 9 | 10 | var transform: [EntityId: TransformComponent] = [:] 11 | var shape: [EntityId: ShapeComponent] = [:] 12 | var sprite: [EntityId: SpriteComponent] = [:] 13 | var label: [EntityId: LabelComponent] = [:] 14 | var camera: [EntityId: CameraComponent] = [:] 15 | 16 | var cameraContext: CameraReducerContext { 17 | get { 18 | CameraReducerContext(entities: entities, transform: transform, camera: camera) 19 | } 20 | set { 21 | self.transform = newValue.transform 22 | self.camera = newValue.camera 23 | } 24 | } 25 | } 26 | 27 | enum RenderingTestAction: Equatable { 28 | 29 | } 30 | 31 | struct RenderingTestEnvironment: RenderingEnvironment { 32 | var renderer: Renderer { metalRenderer } 33 | var resourceManager: ResourceManager { metalResourceManager } 34 | 35 | var metalRenderer: MetalRenderer 36 | var metalResourceManager: MetalResourceManager 37 | } 38 | -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/BitmapFontTests/snapshotText-_.Bitmap-Font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/BitmapFontTests/snapshotText-_.Bitmap-Font.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/BitmapFontTests/snapshotText-_.Chars-_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/BitmapFontTests/snapshotText-_.Chars-_.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/BitmapFontTests/snapshotText-_.Welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/BitmapFontTests/snapshotText-_.Welcome.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/CameraRenderingTests/testCameraRender.first-pass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/CameraRenderingTests/testCameraRender.first-pass.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/CameraRenderingTests/testCameraRender.second-pass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/CameraRenderingTests/testCameraRender.second-pass.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/CameraRenderingTests/testCameraRenderOffset.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/CameraRenderingTests/testCameraRenderOffset.1.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/CameraRenderingTests/testCameraRenderZoom.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/CameraRenderingTests/testCameraRenderZoom.1.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/CameraRenderingTests/testCameraRenderZoom.temp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/CameraRenderingTests/testCameraRenderZoom.temp.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/HitTestingTests/testCameraRenderZoomWithObjectTranslate.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/HitTestingTests/testCameraRenderZoomWithObjectTranslate.1.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapeContainsPoint.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapeContainsPoint.1.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapePointContainmentWhenTransformedFromCameraSpace.after-zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapePointContainmentWhenTransformedFromCameraSpace.after-zoom.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapePointContainmentWhenTransformedFromCameraSpace.before-zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapePointContainmentWhenTransformedFromCameraSpace.before-zoom.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapeTransformAndRotateContainsPoint.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapeTransformAndRotateContainsPoint.1.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapeTransformAndRotateContainsPointAtCenter.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapeTransformAndRotateContainsPointAtCenter.1.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapeTransformAndRotateContainsPointAtZero.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapeTransformAndRotateContainsPointAtZero.1.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapeTransformAndRotateDoesNotContainPoint.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/HitTestingTests/testShapeTransformAndRotateDoesNotContainPoint.1.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testProjectionMatrix.normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testProjectionMatrix.normal.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testProjectionMatrix.scale-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testProjectionMatrix.scale-down.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testProjectionMatrix.scaled-translated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testProjectionMatrix.scaled-translated.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testProjectionMatrix.translated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testProjectionMatrix.translated.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testTriangle.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testTriangle.1.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testTriangleRotatedAround0_0AnchorPoint.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testTriangleRotatedAround0_0AnchorPoint.1.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testTriangleRotatedAround0_5_0_5AnchorPoint.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testTriangleRotatedAround0_5_0_5AnchorPoint.1.png -------------------------------------------------------------------------------- /Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testTriangleRotatedAround1_1AnchorPoint.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/RenderingTests/__Snapshots__/MetalRenderingTests/testTriangleRotatedAround1_1AnchorPoint.1.png -------------------------------------------------------------------------------- /Tests/TiledInterpreterTests/TestMap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/TiledInterpreterTests/TestMap.png -------------------------------------------------------------------------------- /Tests/TiledInterpreterTests/TiledInterpreterTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TiledInterpreter 3 | import SpriteKit 4 | import SnapshotTesting 5 | //import XMLCoder 6 | 7 | enum TestTiledObjectType: String { 8 | case enemy 9 | case start 10 | case door 11 | case chest 12 | case unknown = "" 13 | } 14 | 15 | final class TiledInterpreterTests: XCTestCase { 16 | 17 | var mapData: Data! 18 | var tileMapImageData: Data! 19 | var tileSetData: Data! 20 | 21 | override func setUpWithError() throws { 22 | let mapDataPath = URL(fileURLWithPath: #file) 23 | .deletingLastPathComponent() 24 | .appendingPathComponent("TestMap.json") 25 | 26 | let tilesFilePath = URL(fileURLWithPath: #file) 27 | .deletingLastPathComponent() 28 | .appendingPathComponent("tiles_dungeon.png") 29 | 30 | let tileSetFilePath = URL(fileURLWithPath: #file) 31 | .deletingLastPathComponent() 32 | .appendingPathComponent("dungeon.tsx") 33 | 34 | guard let mapData = FileManager.default.contents(atPath: mapDataPath.path), 35 | let tilesData = FileManager.default.contents(atPath: tilesFilePath.path), 36 | let tileSetData = FileManager.default.contents(atPath: tileSetFilePath.path) else { 37 | XCTFail() 38 | return 39 | } 40 | 41 | self.mapData = mapData 42 | self.tileMapImageData = tilesData 43 | self.tileSetData = tileSetData 44 | } 45 | 46 | func testLoadingJSON() throws { 47 | let map = try JSONDecoder().decode(TiledMapJSON.self, from: mapData) 48 | 49 | XCTAssertEqual(map.layers.count, 3) 50 | XCTAssertEqual(map.layers.first?.type, .tileLayer) 51 | XCTAssertEqual(map.layers.last?.type, .objectGroup) 52 | XCTAssertEqual(map.layers.last?.objects?.first?.type, "") 53 | XCTAssertEqual(map.layers.last?.objects?[1].type, "start") 54 | } 55 | 56 | func testSplittingImage() { 57 | let dict = makeImageTileDictionary(name: "dungeon", imageData: tileMapImageData, tileWidth: 16, tileHeight: 16) 58 | XCTAssertEqual(dict?.count, 380) 59 | 60 | let atlas = makeTextureAtlas(name: "dungeon", imageData: tileMapImageData, tileWidth: 16, tileHeight: 16) 61 | XCTAssertEqual(atlas?.textureNames.count, 380) 62 | 63 | let tileset = makeTileset(name: "dungeon", imageData: tileMapImageData, tileWidth: 16, tileHeight: 16) 64 | XCTAssertEqual(tileset?.tileGroups.count, 380) 65 | XCTAssertEqual(tileset?.defaultTileSize, .init(width: 16, height: 16)) 66 | XCTAssertEqual(tileset?.name, "dungeon") 67 | } 68 | 69 | func testTileSetDataInterpreter() throws { 70 | let tileSetInfo = try XMLDecoder().decode(TiledTilesetXML.self, from: tileSetData) 71 | XCTAssertEqual(tileSetInfo.name, "dungeon") 72 | XCTAssertEqual(tileSetInfo.tiles?.count, 2) 73 | } 74 | 75 | func testMapGeneration() throws { 76 | let tiledMap = try TiledMap( 77 | mapData: mapData, 78 | tileSetImageData: tileMapImageData, 79 | tileSetData: tileSetData 80 | ) 81 | assertSnapshot(matching: try tiledMap.createScene(), as: .image, record: false) 82 | } 83 | 84 | } 85 | 86 | extension Snapshotting where Value == SKScene, Format == NSImage { 87 | static var image: Snapshotting { 88 | return Snapshotting.image.pullback { scene in 89 | let view = SKView(frame: .init(origin: .zero, size: scene.size)) 90 | view.presentScene(scene) 91 | return view 92 | } 93 | } 94 | } 95 | 96 | extension Snapshotting where Value == SKNode, Format == NSImage { 97 | static func image(sceneSize: CGSize) -> Snapshotting { 98 | return Snapshotting.image.pullback { node in 99 | let scene = SKScene(size: sceneSize) 100 | node.position = .init(x: sceneSize.width/2, y: sceneSize.height/2) 101 | scene.addChild(node) 102 | return scene 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Tests/TiledInterpreterTests/__Snapshots__/TiledInterpreterTests/testMapGeneration.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/TiledInterpreterTests/__Snapshots__/TiledInterpreterTests/testMapGeneration.1.png -------------------------------------------------------------------------------- /Tests/TiledInterpreterTests/dungeon.tsj: -------------------------------------------------------------------------------- 1 | { "columns":20, 2 | "editorsettings": 3 | { 4 | "export": 5 | { 6 | "format":"json", 7 | "target":"..\/..\/..\/RedECS-Tests\/Sources\/WebSupport\/Resources\/dungeon.tsj" 8 | } 9 | }, 10 | "image":"..\/..\/..\/RedECS-Tests\/Sources\/WebSupport\/Resources\/tiles_dungeon.png", 11 | "imageheight":304, 12 | "imagewidth":320, 13 | "margin":0, 14 | "name":"dungeon", 15 | "spacing":0, 16 | "tilecount":380, 17 | "tiledversion":"1.9.0", 18 | "tileheight":16, 19 | "tiles":[ 20 | { 21 | "class":"ground", 22 | "id":0 23 | }, 24 | { 25 | "class":"ground", 26 | "id":1 27 | }, 28 | { 29 | "class":"ground", 30 | "id":2 31 | }, 32 | { 33 | "class":"ground", 34 | "id":4 35 | }, 36 | { 37 | "class":"ground", 38 | "id":5 39 | }, 40 | { 41 | "class":"ground", 42 | "id":6 43 | }, 44 | { 45 | "class":"ground", 46 | "id":7 47 | }, 48 | { 49 | "class":"ground", 50 | "id":8 51 | }, 52 | { 53 | "class":"ground", 54 | "id":9 55 | }, 56 | { 57 | "class":"ground", 58 | "id":10 59 | }, 60 | { 61 | "class":"ground", 62 | "id":11 63 | }, 64 | { 65 | "class":"ground", 66 | "id":12 67 | }, 68 | { 69 | "class":"ground", 70 | "id":13 71 | }, 72 | { 73 | "class":"ground", 74 | "id":20 75 | }, 76 | { 77 | "class":"ground", 78 | "id":21 79 | }, 80 | { 81 | "class":"ground", 82 | "id":22 83 | }, 84 | { 85 | "class":"ground", 86 | "id":23 87 | }, 88 | { 89 | "class":"ground", 90 | "id":24 91 | }, 92 | { 93 | "class":"ground", 94 | "id":25 95 | }, 96 | { 97 | "class":"ground", 98 | "id":26 99 | }, 100 | { 101 | "class":"ground", 102 | "id":27 103 | }, 104 | { 105 | "class":"ground", 106 | "id":28 107 | }, 108 | { 109 | "class":"ground", 110 | "id":29 111 | }, 112 | { 113 | "class":"ground", 114 | "id":30 115 | }, 116 | { 117 | "class":"ground", 118 | "id":31 119 | }, 120 | { 121 | "class":"ground", 122 | "id":32 123 | }, 124 | { 125 | "class":"ground", 126 | "id":33 127 | }, 128 | { 129 | "class":"ground", 130 | "id":40 131 | }, 132 | { 133 | "class":"ground", 134 | "id":41 135 | }, 136 | { 137 | "class":"ground", 138 | "id":42 139 | }, 140 | { 141 | "class":"ground", 142 | "id":43 143 | }, 144 | { 145 | "class":"ground", 146 | "id":44 147 | }, 148 | { 149 | "class":"ground", 150 | "id":45 151 | }, 152 | { 153 | "class":"ground", 154 | "id":46 155 | }, 156 | { 157 | "class":"ground", 158 | "id":47 159 | }, 160 | { 161 | "class":"ground", 162 | "id":48 163 | }, 164 | { 165 | "class":"ground", 166 | "id":49 167 | }, 168 | { 169 | "class":"ground", 170 | "id":50 171 | }, 172 | { 173 | "class":"ground", 174 | "id":51 175 | }, 176 | { 177 | "class":"ground", 178 | "id":67 179 | }, 180 | { 181 | "class":"ground", 182 | "id":68 183 | }, 184 | { 185 | "class":"chest", 186 | "id":334 187 | }], 188 | "tilewidth":16, 189 | "type":"tileset", 190 | "version":"1.8" 191 | } -------------------------------------------------------------------------------- /Tests/TiledInterpreterTests/tiles_dungeon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/Tests/TiledInterpreterTests/tiles_dungeon.png -------------------------------------------------------------------------------- /asteroids.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/asteroids.gif -------------------------------------------------------------------------------- /breakout.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/breakout.gif -------------------------------------------------------------------------------- /redecs-breakdown-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/redecs-breakdown-1.png -------------------------------------------------------------------------------- /rpg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedECSEngine/RedECS/5208719165699be73bc1b48e09472bd8f9a99df0/rpg.gif --------------------------------------------------------------------------------