├── .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
--------------------------------------------------------------------------------