├── .gitignore ├── .swift-version ├── .swiftlint.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Configs ├── ImagineEngine.plist └── ImagineEngineTests.plist ├── Dangerfile ├── Documentation ├── Guides │ ├── CustomEvents.md │ └── README.md ├── README.md └── Tutorials │ ├── 1-AsteroidBlaster │ ├── AsteroidBlaster.xcworkspace │ │ ├── Playground.playground │ │ │ ├── Contents.swift │ │ │ ├── Resources │ │ │ │ ├── Asteroid │ │ │ │ │ └── 0@2x.png │ │ │ │ ├── Explosion │ │ │ │ │ ├── 0@2x.png │ │ │ │ │ ├── 1@2x.png │ │ │ │ │ ├── 2@2x.png │ │ │ │ │ ├── 3@2x.png │ │ │ │ │ ├── 4@2x.png │ │ │ │ │ ├── 5@2x.png │ │ │ │ │ └── 6@2x.png │ │ │ │ ├── Ground │ │ │ │ │ ├── bottom@2x.png │ │ │ │ │ ├── bottomLeft@2x.png │ │ │ │ │ ├── bottomRight@2x.png │ │ │ │ │ ├── center@2x.png │ │ │ │ │ ├── left@2x.png │ │ │ │ │ ├── right@2x.png │ │ │ │ │ ├── top@2x.png │ │ │ │ │ ├── topLeft@2x.png │ │ │ │ │ └── topRight@2x.png │ │ │ │ └── House │ │ │ │ │ └── 0@2x.png │ │ │ └── contents.xcplayground │ │ └── contents.xcworkspacedata │ ├── FinalCode.swift │ ├── README.md │ └── Screenshots │ │ ├── Asteroids.png │ │ ├── Explosions.png │ │ ├── Finished.png │ │ ├── Ground.png │ │ └── Houses.png │ ├── 2-Walkabout │ ├── FinalCode.swift │ ├── README.md │ ├── Screenshots │ │ ├── Edge.png │ │ ├── Finished.png │ │ ├── Grass.png │ │ └── Player.png │ └── Walkabout.xcworkspace │ │ ├── Playground.playground │ │ ├── Contents.swift │ │ ├── Resources │ │ │ ├── Ground@2x.png │ │ │ ├── Obstacle@2x.png │ │ │ └── Player │ │ │ │ ├── Idle │ │ │ │ └── 0@2x.png │ │ │ │ └── Walking │ │ │ │ ├── Down@2x.png │ │ │ │ ├── Left@2x.png │ │ │ │ ├── Right@2x.png │ │ │ │ └── Up@2x.png │ │ └── contents.xcplayground │ │ └── contents.xcworkspacedata │ └── README.md ├── Gemfile ├── ImagineEngine.podspec ├── ImagineEngine.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── ImagineEngine-iOS.xcscheme │ ├── ImagineEngine-macOS.xcscheme │ └── ImagineEngine-tvOS.xcscheme ├── LICENSE ├── Logo.png ├── README.md ├── Sources ├── Core │ ├── API │ │ ├── Action.swift │ │ ├── ActionPerformer.swift │ │ ├── ActionToken.swift │ │ ├── Actor.swift │ │ ├── ActorEventCollection.swift │ │ ├── Animation.swift │ │ ├── AnimationAction.swift │ │ ├── Block.swift │ │ ├── BlockTextureCollection.swift │ │ ├── Camera.swift │ │ ├── CameraEventCollection.swift │ │ ├── CancellationToken.swift │ │ ├── ClosureAction.swift │ │ ├── Constraint.swift │ │ ├── Coordinate.swift │ │ ├── Event.swift │ │ ├── EventCollection.swift │ │ ├── EventToken.swift │ │ ├── FadeAction.swift │ │ ├── Fadeable.swift │ │ ├── Game.swift │ │ ├── Group.swift │ │ ├── InstanceHashable.swift │ │ ├── Label.swift │ │ ├── LabelEventCollection.swift │ │ ├── MetricAction.swift │ │ ├── Mirroring.swift │ │ ├── Movable.swift │ │ ├── MoveAction.swift │ │ ├── Node.swift │ │ ├── Pluggable.swift │ │ ├── Plugin.swift │ │ ├── RepeatAction.swift │ │ ├── RepeatMode.swift │ │ ├── Rotatable.swift │ │ ├── RotateAction.swift │ │ ├── Scalable.swift │ │ ├── ScaleAction.swift │ │ ├── Scene.swift │ │ ├── SceneEventCollection.swift │ │ ├── Shadow.swift │ │ ├── SpriteSheet.swift │ │ ├── Texture.swift │ │ ├── TextureFormat.swift │ │ ├── TextureImageLoader.swift │ │ ├── TextureManager.swift │ │ ├── Timeline.swift │ │ ├── Typealiases.swift │ │ └── ZIndexed.swift │ └── Internal │ │ ├── ActionManager.swift │ │ ├── ActionWrapper.swift │ │ ├── Activatable.swift │ │ ├── AnyNode.swift │ │ ├── BundleProtocol.swift │ │ ├── BundleTextureImageLoader.swift │ │ ├── CALayer+Transform.swift │ │ ├── ClickGestureRecognizer-macOS.swift │ │ ├── ClickPlugin.swift │ │ ├── ClosureUpdatable.swift │ │ ├── DisplayLink-iOS+tvOS.swift │ │ ├── DisplayLink-macOS.swift │ │ ├── DisplayLinkProtocol.swift │ │ ├── EdgeInsets-macOS.swift │ │ ├── Font+Default.swift │ │ ├── GameView.swift │ │ ├── Grid.swift │ │ ├── GridPlaceable.swift │ │ ├── Image-macOS.swift │ │ ├── Layer.swift │ │ ├── LoadedTexture.swift │ │ ├── Optional+GetOrSet.swift │ │ ├── PluginManager.swift │ │ ├── PluginWrapper.swift │ │ ├── ReplicatorLayer.swift │ │ ├── Screen-iOS.swift │ │ ├── Screen-macOS.swift │ │ ├── TextLayer.swift │ │ ├── TextureErrorHandler.swift │ │ ├── Updatable.swift │ │ ├── UpdatableCollection.swift │ │ ├── UpdatableWrapper.swift │ │ ├── UpdateOutcome.swift │ │ └── View+MakeLayer.swift └── Integrations │ ├── AppKit │ ├── GameViewController-macOS.swift │ └── GameWindowController.swift │ └── UIKit │ ├── GameViewController-iOS+tvOS.swift │ └── GameWindow.swift ├── Tests └── ImagineEngineTests │ ├── ActionTests.swift │ ├── ActorTests.swift │ ├── BlockTests.swift │ ├── BundleTextureImageLoaderTests.swift │ ├── CameraTests.swift │ ├── EventTests.swift │ ├── LabelTests.swift │ ├── Mocks │ ├── ActionMock.swift │ ├── BundleMock.swift │ ├── ClickGestureRecognizerMock.swift │ ├── DisplayLinkMock.swift │ ├── GameMock.swift │ ├── GameViewMock.swift │ ├── PluginMock.swift │ ├── TextureErrorHandlerMock.swift │ └── TextureImageLoaderMock.swift │ ├── Resources │ ├── sample.jpg │ └── sample.png │ ├── SceneTests.swift │ ├── SpriteSheetTests.swift │ ├── TextureManagerTests.swift │ ├── TimelineTests.swift │ └── Utilities │ ├── Assert.swift │ ├── ImageMockFactory.swift │ ├── TimeTraveler.swift │ └── XCTAssertEqual+CATransform3D.swift ├── XcodeTemplates ├── Imagine Engine │ └── iOS Game.xctemplate │ │ ├── AppDelegate.swift │ │ ├── Podfile │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ ├── TemplateInfo.plist │ │ └── ___PACKAGENAME___Scene.swift └── README.md └── fastlane ├── Fastfile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | Gemfile.lock 69 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - XcodeTemplates 3 | 4 | function_body_length: 50 5 | identifier_name: 6 | excluded: 7 | - x 8 | - y 9 | 10 | disabled_rules: 11 | - colon 12 | - cyclomatic_complexity 13 | - file_length 14 | - for_where 15 | - operator_whitespace 16 | - type_body_length 17 | 18 | custom_rules: 19 | empty_line_after_guard_statement: 20 | included: ".*\\.swift" 21 | name: "Empty line after guard statement" 22 | regex: "((?<=\n)([ ]*)guard[^\\}]*?\\}\n\\2[^\n])" # Follow https://regex101.com/r/i1IaQH/1 for the explanation on the regex 23 | message: "Add a newline after guard statement to make it easier to read" 24 | severity: warning 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10.2 3 | script: 4 | - bundle exec fastlane tests 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Imagine Engine Code of Conduct 2 | 3 | Below is the Code of Conduct that all contributors and participants in the Imagine Engine community are expected to adhere to. 4 | It's adopted from the [Contributor Covenant Code of Conduct][homepage]. 5 | 6 | ## Our Pledge 7 | 8 | In the interest of fostering an open and welcoming environment, we as 9 | contributors and maintainers pledge to making participation in our project and 10 | our community a harassment-free experience for everyone, regardless of age, body 11 | size, disability, ethnicity, gender identity and expression, level of experience, 12 | nationality, personal appearance, race, religion, or sexual identity and 13 | orientation. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to creating a positive environment 18 | include: 19 | 20 | * Using welcoming and inclusive language 21 | * Being respectful of differing viewpoints and experiences 22 | * Gracefully accepting constructive criticism 23 | * Focusing on what is best for the community 24 | * Showing empathy towards other community members 25 | 26 | Examples of unacceptable behavior by participants include: 27 | 28 | * The use of sexualized language or imagery and unwelcome sexual attention or 29 | advances 30 | * Trolling, insulting/derogatory comments, and personal or political attacks 31 | * Public or private harassment 32 | * Publishing others' private information, such as a physical or electronic 33 | address, without explicit permission 34 | * Other conduct which could reasonably be considered inappropriate in a 35 | professional setting 36 | 37 | ## Our Responsibilities 38 | 39 | Project maintainers are responsible for clarifying the standards of acceptable 40 | behavior and are expected to take appropriate and fair corrective action in 41 | response to any instances of unacceptable behavior. 42 | 43 | Project maintainers have the right and responsibility to remove, edit, or 44 | reject comments, commits, code, wiki edits, issues, and other contributions 45 | that are not aligned to this Code of Conduct, or to ban temporarily or 46 | permanently any contributor for other behaviors that they deem inappropriate, 47 | threatening, offensive, or harmful. 48 | 49 | ## Scope 50 | 51 | This Code of Conduct applies both within project spaces and in public spaces 52 | when an individual is representing the project or its community. Examples of 53 | representing a project or community include using an official project e-mail 54 | address, posting via an official social media account, or acting as an appointed 55 | representative at an online or offline event. Representation of a project may be 56 | further defined and clarified by project maintainers. 57 | 58 | ## Enforcement 59 | 60 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 61 | reported by contacting the project leader at john@sundell.co. All 62 | complaints will be reviewed and investigated and will result in a response that 63 | is deemed necessary and appropriate to the circumstances. The project team is 64 | obligated to maintain confidentiality with regard to the reporter of an incident. 65 | Further details of specific enforcement policies may be posted separately. 66 | 67 | Project maintainers who do not follow or enforce the Code of Conduct in good 68 | faith may face temporary or permanent repercussions as determined by other 69 | members of the project's leadership. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 74 | available at [http://contributor-covenant.org/version/1/4][version] 75 | 76 | [homepage]: http://contributor-covenant.org 77 | [version]: http://contributor-covenant.org/version/1/4/ 78 | -------------------------------------------------------------------------------- /Configs/ImagineEngine.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2017 John Sundell. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Configs/ImagineEngineTests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | # Display a friendly welcoming message to non-contributors 2 | contributors = github.api.contributors("JohnSundell/ImagineEngine").map { |user| user.login } 3 | 4 | unless contributors.include? github.pr_author 5 | message "Hi @#{github.pr_author} 👋! Thank you for contributing to Imagine Engine! I'm the CI Bot for this project, and will assist you in getting your PR merged 👍" 6 | end 7 | 8 | # Show SwiftLint warnings inline in the diff 9 | swiftlint.lint_files inline_mode: true 10 | 11 | # Warning to discourage big PRs 12 | if git.lines_of_code > 500 13 | warn "Your PR has over 500 lines of code 😱 Try to break it up into separate PRs if possible 👍" 14 | end 15 | 16 | # Warning to encourage a PR description 17 | if github.pr_body.length == 0 18 | warn "Please add a decription to your PR to make it easier to review 👌" 19 | end 20 | 21 | # Encourage rebases instead of including merge commits 22 | if git.commits.any? { |c| c.message =~ /^Merge branch 'master'/ } 23 | warn "Please rebase to get rid of the merge commits in this PR 🙏" 24 | end 25 | 26 | # If changes have been made in sources, encourage tests 27 | if !git.modified_files.grep(/Sources/).empty? && git.modified_files.grep(/Tests/).empty? 28 | warn "Remember to write tests in case you have added a new API or fixed a bug. Feel free to ask for help if you need it 👍" 29 | end 30 | -------------------------------------------------------------------------------- /Documentation/Guides/CustomEvents.md: -------------------------------------------------------------------------------- 1 | # Custom Events 2 | 3 | You can easily extend Imagine Engine's default suite of events with your own. This is super useful for communicating between different parts of your game code, such as between a scene and a plugin. 4 | 5 | ## Starting point 6 | 7 | Let's say that we're building an asteroid game, and each time an asteroid is added to the scene, we want to trigger a custom event. To add an asteroid every 10 seconds, we use the `Timeline` API in our scene, like this: 8 | 9 | ```swift 10 | class AsteroidScene: Scene { 11 | override func setup() { 12 | timeline.repeat(withInterval: 10, using: self) { scene in 13 | let asteroid = Actor(textureNamed: "Asteroid") 14 | asteroid.position = scene.center 15 | scene.add(asteroid) 16 | } 17 | } 18 | } 19 | ``` 20 | 21 | ## Defining an event 22 | 23 | To be able to trigger a custom event, we have to start by defining it. To do that, we start by adding an extension on `SceneEventCollection` and define our event as a property. Then all we have to do is to call `makeEvent()` from within that property definition, like this: 24 | 25 | ```swift 26 | extension SceneEventCollection { 27 | var asteroidAdded: Event { 28 | return makeEvent() 29 | } 30 | } 31 | ``` 32 | 33 | *If you wanted to add a custom event for actors, you'd instead use `ActorEventCollection` as the class you're extending.* 34 | 35 | ## Triggering event 36 | 37 | You can now observe and trigger the event just like a built-in one. For example, right after we add an asteroid to the scene we can simply add this line of code to trigger our new custom event: 38 | 39 | ```swift 40 | scene.events.asteroidAdded.trigger(with: asteroid) 41 | ``` 42 | 43 | We can now observe our new event to drive our game logic, for example to display a warning text whenever a new asteroid is incoming: 44 | 45 | ```swift 46 | scene.events.asteroidAdded.observe { scene, asteroid in 47 | let label = Label(text: "Warning! Incoming asteroid!") 48 | label.position = scene.center 49 | scene.add(label) 50 | } 51 | ``` -------------------------------------------------------------------------------- /Documentation/Guides/README.md: -------------------------------------------------------------------------------- 1 | ## Imagine Engine Programming Guides 2 | 3 | Welcome to Imagine Engine's programming guides section! 4 | 5 | While it's currently quite empty, this section will soon be filled up with targeted programming guides for each of the engine's APIs and features. If you want to help out, feel free to submit a PR adding a new guide! 👍 6 | 7 | ## Available guides 8 | 9 | **[Custom Events](CustomEvents.md):** Learn how to use custom events to communicate between different parts of your game code. -------------------------------------------------------------------------------- /Documentation/README.md: -------------------------------------------------------------------------------- 1 | ## Imagine Engine Documentation 2 | 3 | Welcome to the Imagine Engine documentation portal! Here you can navigate to the available documentation sections: 4 | 5 | **🚀 [Tutorials](Tutorials)** 6 | 7 | Learn how to use Imagine Engine by completing step-by-step tutorials using Swift playgrounds. 8 | 9 | **📖 [Guides](Guides)** 10 | 11 | Browse various guides that each explain how a certain aspect or feature of Imagine Engine works. 12 | 13 | 14 | -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PlaygroundSupport 3 | -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Asteroid/0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Asteroid/0@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/0@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/1@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/2@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/3@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/3@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/4@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/4@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/5@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/6@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Explosion/6@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/bottom@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/bottom@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/bottomLeft@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/bottomLeft@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/bottomRight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/bottomRight@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/center@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/center@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/left@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/left@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/right@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/right@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/top@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/top@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/topLeft@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/topLeft@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/topRight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/Ground/topRight@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/House/0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/Resources/House/0@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/Playground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/AsteroidBlaster.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/FinalCode.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | import PlaygroundSupport 9 | import ImagineEngine 10 | 11 | class AsteroidBlasterScene: Scene { 12 | override func setup() { 13 | backgroundColor = Color(red: 0, green: 0, blue: 0.3, alpha: 1) 14 | 15 | let housesGroup = Group.name("Houses") 16 | let groundGroup = Group.name("Ground") 17 | 18 | let groundSize = Size(width: size.width, height: 100) 19 | let ground = Block(size: groundSize, textureCollectionName: "Ground") 20 | ground.position.x = center.x 21 | ground.position.y = size.height - groundSize.height / 2 22 | ground.group = groundGroup 23 | add(ground) 24 | 25 | for x in stride(from: center.x - 100, to: center.x + 150, by: 50) { 26 | let house = Actor() 27 | house.animation = Animation(name: "House", frameCount: 1, frameDuration: 0) 28 | house.group = housesGroup 29 | add(house) 30 | 31 | house.position.x = x 32 | house.position.y = ground.rect.minY - house.size.height / 2 33 | } 34 | 35 | timeline.repeat(withInterval: 2) { [weak self] in 36 | guard let scene = self else { 37 | return 38 | } 39 | 40 | let asteroid = Actor() 41 | asteroid.animation = Animation(name: "Asteroid", frameCount: 1, frameDuration: 0) 42 | scene.add(asteroid) 43 | 44 | let positionRange = scene.size.width - asteroid.size.width 45 | let randomPosition = Metric(arc4random() % UInt32(positionRange)) 46 | asteroid.position.x = asteroid.size.width / 2 + randomPosition 47 | 48 | asteroid.velocity.dy = 100 49 | 50 | asteroid.events.collided(withBlockInGroup: groundGroup).observe { asteroid in 51 | asteroid.explode() 52 | } 53 | 54 | asteroid.events.collided(withActorInGroup: housesGroup).observe { asteroid, house in 55 | asteroid.explode() 56 | 57 | house.explode().then { 58 | guard let scene = self else { 59 | return 60 | } 61 | 62 | for actor in scene.actors { 63 | if actor.group == housesGroup { 64 | return 65 | } 66 | } 67 | 68 | scene.reset() 69 | } 70 | } 71 | 72 | asteroid.events.clicked.observe { asteroid in 73 | asteroid.explode() 74 | } 75 | } 76 | } 77 | } 78 | 79 | extension Actor { 80 | @discardableResult func explode() -> ActionToken { 81 | velocity = .zero 82 | 83 | let explosionAnimation = Animation( 84 | name: "Explosion", 85 | frameCount: 7, 86 | frameDuration: 0.07, 87 | repeatMode: .never 88 | ) 89 | 90 | return playAnimation(explosionAnimation).then { 91 | self.remove() 92 | } 93 | } 94 | } 95 | 96 | let sceneSize = Size(width: 375, height: 667) 97 | let scene = AsteroidBlasterScene(size: sceneSize) 98 | PlaygroundPage.current.liveView = GameViewController(scene: scene) 99 | -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/Screenshots/Asteroids.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/Screenshots/Asteroids.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/Screenshots/Explosions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/Screenshots/Explosions.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/Screenshots/Finished.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/Screenshots/Finished.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/Screenshots/Ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/Screenshots/Ground.png -------------------------------------------------------------------------------- /Documentation/Tutorials/1-AsteroidBlaster/Screenshots/Houses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/1-AsteroidBlaster/Screenshots/Houses.png -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/FinalCode.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | import PlaygroundSupport 9 | import ImagineEngine 10 | 11 | class WalkaboutScene: Scene { 12 | override func setup() { 13 | let ground = Block(size: size, spriteSheetName: "Ground") 14 | ground.position = center 15 | add(ground) 16 | 17 | let player = Actor() 18 | player.position = center 19 | add(player) 20 | 21 | player.textureNamePrefix = "Player/" 22 | 23 | let idleAnimation = Animation(name: "Idle", frameCount: 1, frameDuration: 1) 24 | player.animation = idleAnimation 25 | 26 | var moveToken: ActionToken? 27 | 28 | events.clicked.observe { _, point in 29 | moveToken?.cancel() 30 | 31 | let speed: Metric = 100 32 | let horizontalTarget = Point(x: point.x, y: player.position.y) 33 | let horizontalDuration = TimeInterval(abs(player.position.x - point.x) / speed) 34 | let verticalTarget = Point(x: point.x, y: point.y) 35 | let verticalDuration = TimeInterval(abs(player.position.y - point.y) / speed) 36 | 37 | moveToken = player.move(to: horizontalTarget, duration: horizontalDuration) 38 | .then(player.move(to: verticalTarget, duration: verticalDuration)) 39 | .then(player.playAnimation(idleAnimation)) 40 | } 41 | 42 | player.events.moved.addObserver(self) { scene, player, positions in 43 | let directionName: String 44 | 45 | if positions.new.x < positions.old.x { 46 | directionName = "Left" 47 | } else if positions.new.x > positions.old.x { 48 | directionName = "Right" 49 | } else if positions.new.y > positions.old.y { 50 | directionName = "Down" 51 | } else { 52 | directionName = "Up" 53 | } 54 | 55 | player.animation = Animation( 56 | spriteSheetNamed: "Walking/\(directionName)", 57 | frameCount: 4, 58 | rowCount: 1, 59 | frameDuration: 0.15 60 | ) 61 | 62 | scene.camera.position = positions.new 63 | } 64 | 65 | player.constraints = [.scene] 66 | camera.constrainedToScene = true 67 | } 68 | } 69 | 70 | let sceneSize = Size(width: 768, height: 768) 71 | let scene = WalkaboutScene(size: sceneSize) 72 | PlaygroundPage.current.liveView = GameViewController(scene: scene) 73 | -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Screenshots/Edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/2-Walkabout/Screenshots/Edge.png -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Screenshots/Finished.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/2-Walkabout/Screenshots/Finished.png -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Screenshots/Grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/2-Walkabout/Screenshots/Grass.png -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Screenshots/Player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/2-Walkabout/Screenshots/Player.png -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PlaygroundSupport 3 | -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Ground@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Ground@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Obstacle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Obstacle@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Player/Idle/0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Player/Idle/0@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Player/Walking/Down@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Player/Walking/Down@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Player/Walking/Left@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Player/Walking/Left@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Player/Walking/Right@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Player/Walking/Right@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Player/Walking/Up@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/Resources/Player/Walking/Up@2x.png -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/Playground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Documentation/Tutorials/2-Walkabout/Walkabout.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Documentation/Tutorials/README.md: -------------------------------------------------------------------------------- 1 | ## Imagine Engine Tutorials 2 | 3 | Welcome to the tutorials section! All tutorials feature a detailed walkthrough, as well as a playground that lets you code along it. Currently, the following tutorials are available: 4 | 5 | ### [1. Asteroid Blaster](1-AsteroidBlaster) 6 | 7 | Learn how to get started with Imagine Engine by building a simple game in which the player has to defend a handful of houses by destroying asteroids falling from the sky. 8 | 9 | ### [2. Walkabout](2-Walkabout) 10 | 11 | Learn more about using sprite sheets, camera control, constrains and more, by building a simple game in which a character can be moved around a 2D scene. -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'danger' 4 | gem 'danger-swiftlint' 5 | gem 'xcpretty' 6 | gem 'fastlane' 7 | -------------------------------------------------------------------------------- /ImagineEngine.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ImagineEngine" 3 | s.version = "0.10.0" 4 | s.summary = "A Swift game engine based on Core Animation" 5 | s.description = <<-DESC 6 | Imagine Engine is an ongoing project that aims to create a fast, high-performace Swift 2D game engine for Apple's platforms that is also a joy to use. 7 | While there are still ways to go, things to fix and new capabilities to add, you are invited to participate in this new community to build a tool with 8 | an ambitious but clear goal - to enable you to easily build any game that you can imagine. 9 | DESC 10 | s.homepage = "https://github.com/JohnSundell/ImagineEngine" 11 | s.license = { :type => "MIT", :file => "LICENSE" } 12 | s.author = { "John Sundell" => "john@sundell.co" } 13 | s.social_media_url = "https://twitter.com/johnsundell" 14 | s.ios.deployment_target = "9.0" 15 | s.osx.deployment_target = "10.12" 16 | s.tvos.deployment_target = "10.0" 17 | s.source = { :git => "https://github.com/JohnSundell/ImagineEngine.git", :tag => s.version.to_s } 18 | s.source_files = "Sources/Core/**/*.swift" 19 | s.ios.source_files = "Sources/Integrations/UIKit/*.swift" 20 | s.ios.exclude_files = [ 21 | "Sources/Core/Internal/ClickGestureRecognizer-macOS.swift", 22 | "Sources/Core/Internal/DisplayLink-macOS.swift", 23 | "Sources/Core/Internal/EdgeInsets-macOS.swift", 24 | "Sources/Core/Internal/Image-macOS.swift", 25 | "Sources/Core/Internal/Screen-macOS.swift" 26 | ] 27 | s.osx.source_files = "Sources/Integrations/AppKit/*.swift" 28 | s.osx.exclude_files = [ 29 | "Sources/Core/Internal/DisplayLink-iOS+tvOS.swift", 30 | "Sources/Core/Internal/Screen-iOS.swift" 31 | ] 32 | s.tvos.source_files = "Sources/Integrations/UIKit/*.swift" 33 | s.tvos.exclude_files = [ 34 | "Sources/Core/Internal/ClickGestureRecognizer-macOS.swift", 35 | "Sources/Core/Internal/DisplayLink-macOS.swift", 36 | "Sources/Core/Internal/EdgeInsets-macOS.swift", 37 | "Sources/Core/Internal/Image-macOS.swift", 38 | "Sources/Core/Internal/Screen-macOS.swift" 39 | ] 40 | s.frameworks = "Foundation", "CoreGraphics", "QuartzCore" 41 | end 42 | -------------------------------------------------------------------------------- /ImagineEngine.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ImagineEngine.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ImagineEngine.xcodeproj/xcshareddata/xcschemes/ImagineEngine-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /ImagineEngine.xcodeproj/xcshareddata/xcschemes/ImagineEngine-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /ImagineEngine.xcodeproj/xcshareddata/xcschemes/ImagineEngine-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 John Sundell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | The above license covers Imagine Engine itself. It excludes the graphics from 24 | OpenGameArt.org that are used in the tutorials. For the licenses for those files, 25 | please refer to their respective licenses by following the links included in the 26 | tutorials. 27 | 28 | -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnSundell/ImagineEngine/51651193cffb9762f92b99181881ea98e869431e/Logo.png -------------------------------------------------------------------------------- /Sources/Core/API/Action.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /** 10 | * Object representing an action that can be performed by an Imagine Engine game object 11 | * 12 | * By making an Actor or Camera perform an action, by can change one of its properties 13 | * over time. Actions have a set duration during which they are performed, and get updated 14 | * every frame. You can use actions to, for example, move objects around, resize them 15 | * or fade them in or out. 16 | * 17 | * Imagine Engine ships with a default set of actions that are ready to use, and you 18 | * can also easily define your own by subclassing this class and overriding one of the 19 | * override points to perform your own custom action logic. 20 | * 21 | * Here's an example of how an Action can be used to move an Actor: 22 | * 23 | * ``` 24 | * let point = Point(x: 200, y: 300) 25 | * actor.perform(MoveAction(destination: point, duration: 5)) 26 | * ``` 27 | * 28 | * When actions are performed, you get an `ActionToken` back as the result. These tokens 29 | * enable you to both cancel an ongoing action, as well as to observe when it has been 30 | * completed and chain it to other actions. 31 | */ 32 | open class Action { 33 | internal let duration: TimeInterval 34 | internal let token = ActionToken() 35 | 36 | private var startTime: TimeInterval? 37 | private var lastUpdateTime: TimeInterval? 38 | 39 | // MARK: - Initializer 40 | 41 | /// Initialize an instance of this class with a duration 42 | public init(duration: TimeInterval) { 43 | self.duration = duration 44 | } 45 | 46 | // MARK: - Override points 47 | 48 | /// Called on your action whenever it was started for an object 49 | open func start(for object: Object) {} 50 | 51 | /// Called on each frame for as long as the action is active, with a context object 52 | /// that can be used to drive your action's logic 53 | open func update(with context: UpdateContext) {} 54 | 55 | /// Called whenever the action was cancelled, with the object it was attached to 56 | open func cancel(for object: Object) {} 57 | 58 | /// Called whenever the action finished, with the object it was attached to 59 | open func finish(for object: Object) {} 60 | 61 | // MARK: - Internal 62 | 63 | internal func reset() { 64 | startTime = nil 65 | lastUpdateTime = nil 66 | } 67 | 68 | internal func update(for object: Object, currentTime: TimeInterval) -> UpdateOutcome { 69 | let startTime = self.startTime.get(orSet: currentTime) 70 | let timeElapsed = currentTime - startTime 71 | let completionRatio = min(Metric(timeElapsed) / Metric(duration), 1) 72 | let timeSinceLastUpdate = currentTime - (lastUpdateTime ?? currentTime) 73 | 74 | let context = Action.UpdateContext( 75 | object: object, 76 | timeElapsed: timeElapsed, 77 | timeSinceLastUpdate: timeSinceLastUpdate, 78 | completionRatio: completionRatio 79 | ) 80 | 81 | update(with: context) 82 | lastUpdateTime = currentTime 83 | 84 | if context.completionRatio < 1 { 85 | return .continueAfter(0) 86 | } 87 | 88 | return .finished 89 | } 90 | } 91 | 92 | public extension Action { 93 | struct UpdateContext { 94 | public let object: Object 95 | public let timeElapsed: TimeInterval 96 | public let timeSinceLastUpdate: TimeInterval 97 | public let completionRatio: Metric 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/Core/API/ActionPerformer.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /** 10 | * Protocol adopted by objects that are able to perform actions 11 | * 12 | * You don't conform to this protocol yourself, instead `Actor` & `Camera` already 13 | * conform to this protocol, making them ready to perform actions. 14 | */ 15 | public protocol ActionPerformer: class { 16 | /// Perform an action 17 | /// The action will be started on the next frame, the returned `ActionToken` 18 | /// can be used to cancel the action, or chain it to other ones. 19 | @discardableResult func perform(_ action: Action) -> ActionToken 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Core/API/ActionToken.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | * Token returned when an action was performed 5 | * 6 | * You can use an action token to either cancel an action, or to observe when 7 | * it was finished. You can also use it to chain an ongoing action to another 8 | * one, making it automatically performed once the current action has finished. 9 | * 10 | * Here's an example of how to cancel an action: 11 | * 12 | * ``` 13 | * let token = actor.move(byX: 50, y: 300, duration: 5) 14 | * token.cancel() 15 | * ``` 16 | * 17 | * Here's how you can run a closure whenever an action was finished: 18 | * 19 | * ``` 20 | * actor.move(byX: 50, y: 300, duration: 5).then { 21 | * print("Move finished!") 22 | * } 23 | * ``` 24 | * 25 | * Here's how an action can be chained into another: 26 | * 27 | * ``` 28 | * actor.move(byX: 50, y: 300, duration: 5) 29 | * .then(actor.fadeOut(withDuration: 3)) 30 | * ``` 31 | * 32 | * Finally, here's how two actions can be linked togher to be performed at the same time: 33 | * 34 | * ``` 35 | * actor.move(byX: 50, y: 300, duration: 5) 36 | * .also(actor.fadeOut(withDuration: 3)) 37 | * ``` 38 | */ 39 | public final class ActionToken: CancellationToken { 40 | internal private(set) lazy var linkedTokens = [ActionToken]() 41 | internal private(set) lazy var chain = [ChainItem]() 42 | internal var isPending = false 43 | private weak var precedingToken: ActionToken? 44 | 45 | // MARK: - API 46 | 47 | /// Link the action that this token is for to another one 48 | /// You can use this API to perform two actions in parallel 49 | @discardableResult public func also(_ token: ActionToken) -> ActionToken { 50 | token.isPending = true 51 | token.precedingToken = self 52 | linkedTokens.append(token) 53 | return token 54 | } 55 | 56 | /// Chain the action that this token is for to another one 57 | /// You can use this API to perform two actions in sequence 58 | @discardableResult public func then(_ token: ActionToken) -> ActionToken { 59 | token.isPending = true 60 | token.precedingToken = self 61 | chain.append(.token(token)) 62 | return token 63 | } 64 | 65 | /// Run any function after the action that this token is for has finished 66 | @discardableResult public func then(_ closure: @escaping @autoclosure () -> Void) -> ActionToken { 67 | chain.append(.closure(closure)) 68 | return self 69 | } 70 | 71 | /// Run any closure after the action that this token is for has finished 72 | @discardableResult public func then(_ closure: @escaping () -> Void) -> ActionToken { 73 | chain.append(.closure(closure)) 74 | return self 75 | } 76 | 77 | // MARK: - CancellationToken 78 | 79 | public override func cancel() { 80 | super.cancel() 81 | precedingToken?.cancel() 82 | } 83 | 84 | // MARK: - Internal 85 | 86 | internal func performChaining() { 87 | for item in chain { 88 | switch item { 89 | case .token(let token): 90 | token.isPending = false 91 | case .closure(let closure): 92 | closure() 93 | } 94 | } 95 | } 96 | } 97 | 98 | public extension ActionToken { 99 | /// Run a closure after the action that this token is for has finished, passing in a given object 100 | /// to the closure (the object won't be retained and the closure won't be run if it's deallocated) 101 | @discardableResult func then(using object: T, run closure: @escaping (T) -> Void) -> ActionToken { 102 | return then { [weak object] in 103 | object.map(closure) 104 | } 105 | } 106 | } 107 | 108 | internal extension ActionToken { 109 | enum ChainItem { 110 | case token(ActionToken) 111 | case closure(() -> Void) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/Core/API/ActorEventCollection.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Events that can be used to observe an actor 10 | public final class ActorEventCollection: EventCollection { 11 | /// Event triggered when the actor was moved 12 | public private(set) lazy var moved = Event(object: self.object) 13 | /// Event triggered when the actor was resized 14 | public private(set) lazy var resized = Event(object: self.object) 15 | /// Event triggered when the actor was rotated 16 | public private(set) lazy var rotated = Event(object: self.object) 17 | /// Event triggered when the actor's rectangle changed (either by position or size) 18 | public private(set) lazy var rectChanged = Event(object: self.object) 19 | /// Event triggered when the actor's velocity changed 20 | public private(set) lazy var velocityChanged = Event(object: self.object) 21 | /// Event triggered when actor entered its scene 22 | public private(set) lazy var enteredScene = Event(object: self.object) 23 | /// Event triggered when the actor exited its scene 24 | public private(set) lazy var leftScene = Event(object: self.object) 25 | /// Event triggered when the actor was clicked (on macOS) or tapped (on iOS/tvOS) 26 | public private(set) lazy var clicked = Event(object: object) 27 | 28 | /// Event triggered when the actor collided with another actor 29 | public func collided(with actor: Actor) -> Event { 30 | object?.isCollisionDetectionActive = true 31 | actor.isCollisionDetectionActive = true 32 | return makeEvent(withSubject: actor) 33 | } 34 | 35 | /// Event triggered when the actor collided with an actor in a given group 36 | public func collided(withActorInGroup group: Group) -> Event { 37 | object?.isCollisionDetectionActive = true 38 | return makeEvent(withSubjectIdentifier: group.identifier) 39 | } 40 | 41 | /// Event triggered when the actor collided with a block in a given group 42 | public func collided(withBlockInGroup group: Group) -> Event { 43 | object?.isCollisionDetectionActive = true 44 | return makeEvent(withSubjectIdentifier: group.identifier) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Core/API/AnimationAction.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Action used to make an actor play an animation 10 | public final class AnimationAction: Action { 11 | private let animation: Animation 12 | private let triggeredByActor: Bool 13 | private var frameIndex = 0 14 | private var repeatCount = 0 15 | 16 | // MARK: - Initializers 17 | 18 | /// Initialize an instance of this action with an animation 19 | public init(animation: Animation) { 20 | self.animation = animation 21 | self.triggeredByActor = false 22 | super.init(duration: animation.totalDuration) 23 | } 24 | 25 | internal init(animation: Animation, triggeredByActor: Bool) { 26 | self.animation = animation 27 | self.triggeredByActor = triggeredByActor 28 | super.init(duration: animation.totalDuration) 29 | } 30 | 31 | // MARK: - Action 32 | 33 | public override func start(for actor: Actor) { 34 | if !triggeredByActor { 35 | actor.animation = animation 36 | } 37 | } 38 | 39 | internal override func update(for actor: Actor, currentTime: TimeInterval) -> UpdateOutcome { 40 | let frame = animation.frame(at: frameIndex) 41 | frameIndex += 1 42 | 43 | actor.render(frame: frame, 44 | scale: animation.textureScale, 45 | resize: animation.autoResize, 46 | ignoreNamePrefix: animation.ignoreTextureNamePrefix) 47 | 48 | if animation.frameCount < 2 { 49 | return .finished 50 | } 51 | 52 | if frameIndex == animation.frameCount { 53 | switch animation.repeatMode { 54 | case .times(let count): 55 | guard repeatCount < count else { 56 | actor.animation = nil 57 | return .finished 58 | } 59 | case .forever: 60 | break 61 | } 62 | 63 | repeatCount += 1 64 | frameIndex = 0 65 | } 66 | 67 | return .continueAfter(animation.frameDuration) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Core/API/BlockTextureCollection.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A collection of textures that a Block can render 10 | public struct BlockTextureCollection { 11 | /// The top texture, tiled horizontally if needed 12 | public var top: Texture? 13 | /// The top left texture 14 | public var topLeft: Texture? 15 | /// The top right texture 16 | public var topRight: Texture? 17 | /// The right texture, tiled vertically if needed 18 | public var right: Texture? 19 | /// The left texture, tiled vertically if needed 20 | public var left: Texture? 21 | /// The center texture, tiled both horizontally & vertically if needed 22 | public var center: Texture? 23 | /// The bottom texture, tiled horizontally if needed 24 | public var bottom: Texture? 25 | /// The bottom left texture 26 | public var bottomLeft: Texture? 27 | /// The bottom right texture 28 | public var bottomRight: Texture? 29 | 30 | /// Initialize an instance, optionally with a name 31 | /// If a name is given, then textures will automatically be assigned to 32 | /// all properties using the property name as a suffix for the texture's 33 | /// name. This enables you to create a folder containing textures for a 34 | /// block and simply reference them using the folder's name. 35 | public init(name: String? = nil, textureFormat: TextureFormat? = nil) { 36 | guard let name = name else { 37 | return 38 | } 39 | 40 | top = Texture(name: "\(name)/top", format: textureFormat) 41 | topLeft = Texture(name: "\(name)/topLeft", format: textureFormat) 42 | topRight = Texture(name: "\(name)/topRight", format: textureFormat) 43 | right = Texture(name: "\(name)/right", format: textureFormat) 44 | left = Texture(name: "\(name)/left", format: textureFormat) 45 | center = Texture(name: "\(name)/center", format: textureFormat) 46 | bottom = Texture(name: "\(name)/bottom", format: textureFormat) 47 | bottomLeft = Texture(name: "\(name)/bottomLeft", format: textureFormat) 48 | bottomRight = Texture(name: "\(name)/bottomRight", format: textureFormat) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Core/API/Camera.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /** 10 | * Class that represents a camera in a scene 11 | * 12 | * You use a scene's camera to move the viewport of your game around. Initially 13 | * all cameras start out at the center of their scene. A camera gets its size 14 | * from the game that its scene is presented in. 15 | */ 16 | public final class Camera: ActionPerformer, Pluggable, Movable, Activatable { 17 | /// The position of the camera within its scene 18 | public var position = Point() { didSet { positionDidChange(from: oldValue) } } 19 | /// The size of the camera's viewport. Set as soon as its presented in a game. 20 | public internal(set) var size = Size() { didSet { sizeDidChange(from: oldValue) }} 21 | /// The current rectangle of the camera's viewport. 22 | public private(set) var rect = Rect() 23 | /// A collection of events that can be used to observe the camera. 24 | public private(set) lazy var events = CameraEventCollection(object: self) 25 | /// Whether the camera is constrained to the scene or can move outside of it (default = false) 26 | public var constrainedToScene = false { didSet { update() } } 27 | 28 | private let pluginManager = PluginManager() 29 | private lazy var actionManager = ActionManager(object: self) 30 | private weak var scene: Scene? 31 | private let layer: Layer 32 | 33 | // MARK: - Initializer 34 | 35 | internal init(scene: Scene, layer: Layer) { 36 | self.scene = scene 37 | self.layer = layer 38 | 39 | scene.events.resized.addObserver(self) { camera in 40 | camera.update() 41 | } 42 | } 43 | 44 | // MARK: - ActionPerformer 45 | 46 | @discardableResult public func perform(_ action: Action) -> ActionToken { 47 | return actionManager.add(action) 48 | } 49 | 50 | // MARK: - Pluggable 51 | 52 | @discardableResult public func add(_ plugin: @autoclosure () -> P, 53 | reuseExistingOfSameType: Bool) -> P where P.Object == Camera { 54 | return pluginManager.add(plugin, for: self, reuseExistingOfSameType: reuseExistingOfSameType) 55 | } 56 | 57 | public func plugins(ofType type: P.Type) -> [P] where P.Object == Camera { 58 | return pluginManager.plugins(ofType: type) 59 | } 60 | 61 | public func remove(_ plugin: P) where P.Object == Camera { 62 | pluginManager.remove(plugin, from: self) 63 | } 64 | 65 | public func removePlugins(ofType type: P.Type) where P.Object == Camera { 66 | pluginManager.removePlugins(ofType: type, from: self) 67 | } 68 | 69 | // MARK: - Activatable 70 | 71 | internal func activate(in game: Game) { 72 | size = game.view.frame.size 73 | pluginManager.activate(in: game) 74 | actionManager.activate(in: game) 75 | } 76 | 77 | internal func deactivate() { 78 | pluginManager.deactivate() 79 | actionManager.deactivate() 80 | } 81 | 82 | // MARK: - Internal 83 | 84 | internal func handleClick(at point: Point) { 85 | events.clicked.trigger(with: point) 86 | } 87 | 88 | // MARK: - Private 89 | 90 | private func positionDidChange(from oldValue: Point) { 91 | if position != oldValue { 92 | update() 93 | events.moved.trigger(with: (oldValue, position)) 94 | } 95 | } 96 | 97 | private func sizeDidChange(from oldValue: Size) { 98 | if size != oldValue { 99 | update() 100 | events.resized.trigger() 101 | } 102 | } 103 | 104 | private func update() { 105 | guard let scene = scene else { 106 | return 107 | } 108 | 109 | var newRect = Rect(origin: position, size: size) 110 | newRect.origin.x -= size.width / 2 111 | newRect.origin.y -= size.height / 2 112 | rect = newRect 113 | 114 | if constrainedToScene { 115 | if scene.size.width >= size.width { 116 | guard newRect.minX >= 0 else { 117 | position.x = size.width / 2 118 | return 119 | } 120 | 121 | guard newRect.maxX <= scene.size.width else { 122 | position.x = scene.size.width - size.width / 2 123 | return 124 | } 125 | } 126 | 127 | if scene.size.height >= size.height { 128 | guard newRect.minY >= 0 else { 129 | position.y = size.height / 2 130 | return 131 | } 132 | 133 | guard newRect.maxY <= scene.size.height else { 134 | position.y = scene.size.height - size.height / 2 135 | return 136 | } 137 | } 138 | } 139 | 140 | layer.frame.origin = Point( 141 | x: size.width / 2 - position.x, 142 | y: size.height / 2 - position.y 143 | ) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Sources/Core/API/CameraEventCollection.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Events that can be used to observe a camera 10 | public final class CameraEventCollection: EventCollection { 11 | /// Event triggered when the camera was moved 12 | public private(set) lazy var moved = Event(object: object) 13 | /// Event triggered when the camera was resized 14 | public private(set) lazy var resized = Event(object: object) 15 | /// Event triggered when the camera was clicked (on macOS) or tapped (on iOS/tvOS) 16 | public private(set) lazy var clicked = Event(object: object) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Core/API/CancellationToken.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /** 10 | * Class used to cancel an operation that takes place over time 11 | * 12 | * Whenever you perform an action, schedule an event on a timeline 13 | * or do something else that will either be performed later or 14 | * during a longer period of time, Imagine Engine returns a token 15 | * to you that can be used to cancel that operation. 16 | */ 17 | public class CancellationToken: InstanceHashable { 18 | internal private(set) var isCancelled = false 19 | 20 | /// Cancel the operation that this token is for 21 | public func cancel() { 22 | isCancelled = true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Core/API/ClosureAction.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /** 10 | * Class that can be used to implement custom actions using a closure 11 | * 12 | * This class provides a simple way to define your own custom actions, 13 | * by simply using a closure that gets passed the current update context 14 | * whenever the action is updated. 15 | */ 16 | public final class ClosureAction: Action { 17 | private let closure: (UpdateContext) -> Void 18 | 19 | /// Initialize an instance with a given duration and a closure that will 20 | /// be run on every update of the action. 21 | public init(duration: TimeInterval, closure: @escaping (UpdateContext) -> Void) { 22 | self.closure = closure 23 | super.init(duration: duration) 24 | } 25 | 26 | public override func update(with context: UpdateContext) { 27 | closure(context) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Core/API/Constraint.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Enum defining various constraints that can be applied to Imagine Engine 10 | /// game objects, in order to restrict their movement in various ways. 11 | public enum Constraint: Hashable { 12 | /// Constraint the object to its scene. When this constraint it set the 13 | /// object won't be able to leave its scene. 14 | case scene 15 | /// Prevent the object from overlaping a block in a given group. 16 | case neverOverlapBlockInGroup(Group) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Core/API/Coordinate.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2018 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Type that can be used to express a non-fractional coordinate within a space 10 | /// In Imagine Engine itself this is mostly used to slice sprite sheets, but you 11 | /// can also use it in your own code for maps and other coordinate systems. 12 | /// 13 | /// Note on comparability: A coordinate is considered to be "lower than" another 14 | /// when either its x or y component has a lower value than the other coordinate. 15 | public struct Coordinate { 16 | /// The coordinate's position on the horizontal axis 17 | public var x: Int 18 | /// The coordinate's position on the vertical axis 19 | public var y: Int 20 | 21 | /// Initialize an instance with given x & y values 22 | public init(x: Int = 0, y: Int = 0) { 23 | self.x = x 24 | self.y = y 25 | } 26 | } 27 | 28 | extension Coordinate: Comparable { 29 | public static func ==(lhs: Coordinate, rhs: Coordinate) -> Bool { 30 | return lhs.x == rhs.x && lhs.y == rhs.y 31 | } 32 | 33 | public static func <(lhs: Coordinate, rhs: Coordinate) -> Bool { 34 | return lhs.x < rhs.x || lhs.y < rhs.y 35 | } 36 | } 37 | 38 | public extension Coordinate { 39 | /// A coordinate value that has both its x & y components set to 0 40 | static var zero: Coordinate { 41 | return Coordinate() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Core/API/Event.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /** 10 | * Class that represents an event that can occur in a game 11 | * 12 | * You use this class to observe and trigger events that can happen 13 | * in your game, and is the recommended way to drive most of your 14 | * game logic. 15 | * 16 | * To observe an event, simply call the `observe` method on it: 17 | * 18 | * ``` 19 | * actor.events.moved.observe { actor in 20 | * print("This actor was moved: \(actor)") 21 | * } 22 | * ``` 23 | * 24 | * To define a custom event, create an extension on `EventCollection` 25 | * that defines a computed property for your event, and simply call 26 | * `makeEvent()` within its implementation, like this: 27 | * 28 | * ``` 29 | * extension EventCollection where Object == Actor { 30 | * var myEvent: Event { 31 | * return makeEvent() 32 | * } 33 | * } 34 | * ``` 35 | * 36 | * You can then trigger the event like this: 37 | * 38 | * ``` 39 | * actor.events.myEvent.trigger() 40 | * ``` 41 | */ 42 | public final class Event { 43 | private weak var object: Object? 44 | private lazy var observations = [ObservationKey : Observation]() 45 | 46 | // MARK: - Initializer 47 | 48 | internal init(object: Object?) { 49 | self.object = object 50 | } 51 | 52 | // MARK: - Public 53 | 54 | /// Trigger the event with a subject value 55 | /// A subject is the object or value that the event happened with, for 56 | /// example in a collision the subject will be the object collided with. 57 | public func trigger(with subject: Subject) { 58 | guard let object = object else { 59 | return 60 | } 61 | 62 | for (key, observation) in observations { 63 | switch key { 64 | case .objectIdentifier: 65 | break 66 | case .token(let token): 67 | guard !token.isCancelled else { 68 | observations[key] = nil 69 | continue 70 | } 71 | } 72 | 73 | observation.closure(object, subject) 74 | } 75 | } 76 | 77 | /// Observe the event using a closure 78 | @discardableResult public func observe(using closure: @escaping (Object, Subject) -> Void) -> EventToken { 79 | let token = EventToken() 80 | observations[.token(token)] = Observation(closure: closure) 81 | return token 82 | } 83 | 84 | /// Add an observer to the event, that will be passed into the observation closure 85 | public func addObserver(_ observer: T, closure: @escaping (T, Object, Subject) -> Void) { 86 | let identifier = ObjectIdentifier(observer) 87 | 88 | observations[.objectIdentifier(identifier)] = Observation { [weak self, weak observer] object, subject in 89 | guard let observer = observer else { 90 | self?.observations[.objectIdentifier(identifier)] = nil 91 | return 92 | } 93 | 94 | closure(observer, object, subject) 95 | } 96 | } 97 | 98 | /// Remove an observer from the event 99 | public func removeObserver(_ observer: T) { 100 | let identifier = ObjectIdentifier(observer) 101 | observations[.objectIdentifier(identifier)] = nil 102 | } 103 | } 104 | 105 | public extension Event { 106 | /// Observe the event using a closure that only gets passed the event's object 107 | @discardableResult func observe(using closure: @escaping (Object) -> Void) -> EventToken { 108 | return observe { object, _ in 109 | closure(object) 110 | } 111 | } 112 | 113 | /// Observe the event using a closure that doesn't take any arguments 114 | @discardableResult func observe(using closure: @escaping () -> Void) -> EventToken { 115 | return observe { _, _ in 116 | closure() 117 | } 118 | } 119 | 120 | /// Add an observer to the event using a closure that only gets passed the event's object 121 | func addObserver(_ observer: T, closure: @escaping (T, Object) -> Void) { 122 | addObserver(observer) { observer, object, _ in 123 | closure(observer, object) 124 | } 125 | } 126 | 127 | /// Add an observer to the event using a closure that only gets passed the observer 128 | func addObserver(_ observer: T, closure: @escaping (T) -> Void) { 129 | addObserver(observer) { observer, _, _ in 130 | closure(observer) 131 | } 132 | } 133 | } 134 | 135 | public extension Event where Subject == Void { 136 | /// Trigger a Void-based event without a subject 137 | func trigger() { 138 | trigger(with: Void()) 139 | } 140 | } 141 | 142 | private extension Event { 143 | enum ObservationKey: Hashable { 144 | case token(EventToken) 145 | case objectIdentifier(ObjectIdentifier) 146 | } 147 | 148 | struct Observation { 149 | let closure: (Object, Subject) -> Void 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/Core/API/EventCollection.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /** 10 | * Class that defines a collection of events 11 | * 12 | * Events are used to drive most of the game logic in an Imagine Engine 13 | * game. Objects like Actors and Scenes define an event collection that 14 | * both comes with built-in events, and enables you to define your own. 15 | */ 16 | public class EventCollection { 17 | internal private(set) weak var object: Object? 18 | private lazy var events = [String : AnyObject]() 19 | 20 | internal init(object: Object) { 21 | self.object = object 22 | } 23 | 24 | /// Make a new event with an identifier for the event's subject 25 | /// Call this method within an extension defining a custom event. 26 | /// For more information, see the documentation for `Event`. 27 | public func makeEvent(named name: StaticString = #function, 28 | withSubjectIdentifier subjectIdentifier: String) -> Event { 29 | let name = "\(name)-\(subjectIdentifier)" 30 | 31 | if let event = events[name] { 32 | // swiftlint:disable:next force_cast 33 | return event as! Event 34 | } 35 | 36 | let event = Event(object: object) 37 | events[name] = event 38 | return event 39 | } 40 | 41 | /// Make a new event with a subject 42 | /// Call this method within an extension defining a custom event. 43 | /// For more information, see the documentation for `Event`. 44 | public func makeEvent(named name: StaticString = #function, 45 | withSubject subject: Subject) -> Event { 46 | return makeEvent(named: name, withSubjectIdentifier: String(describing: ObjectIdentifier(subject))) 47 | } 48 | 49 | /// Make a new event that is not bound to a specific subject 50 | /// Call this method within an extension defining a custom event. 51 | /// For more information, see the documentation for `Event`. 52 | public func makeEvent(named name: StaticString = #function) -> Event { 53 | return makeEvent(named: name, withSubjectIdentifier: "Void") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Core/API/EventToken.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Token that can be used to cancel an event observation 10 | public final class EventToken: CancellationToken { 11 | internal let identifier = UUID() 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Core/API/FadeAction.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Action that can be used to fade objects that are fadable over time 10 | public final class FadeAction: MetricAction { 11 | /// Initialize an instance with a target opacity & duration 12 | public init(opacity: Metric, duration: TimeInterval) { 13 | super.init( 14 | mode: .target(opacity), 15 | duration: duration, 16 | getClosure: { $0.opacity }, 17 | setClosure: { $0.opacity = $1 } 18 | ) 19 | } 20 | 21 | /// Initialize an instance with a delta opacity & duration 22 | public init(delta: Metric, duration: TimeInterval) { 23 | super.init( 24 | mode: .delta(delta), 25 | duration: duration, 26 | getClosure: { $0.opacity }, 27 | setClosure: { $0.opacity = $1 } 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Core/API/Fadeable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol adopted by objects that can have their opacity changed 10 | public protocol Fadeable: class { 11 | /// The opacity of the object, ranging from 0 (invisible) - 1 (opaque) 12 | var opacity: Metric { get set } 13 | } 14 | 15 | public extension Fadeable where Self: ActionPerformer { 16 | /// Fade in the object (to opacity = 1) with a given duration 17 | @discardableResult func fadeIn(withDuration duration: TimeInterval) -> ActionToken { 18 | return perform(FadeAction(opacity: 1, duration: duration)) 19 | } 20 | 21 | /// Fade the object to a given opacity with a duration 22 | @discardableResult func fade(to opacity: Metric, withDuration duration: TimeInterval) -> ActionToken { 23 | return perform(FadeAction(opacity: opacity, duration: duration)) 24 | } 25 | 26 | /// Fade out the object (to opacity = 0) with a given duration 27 | @discardableResult func fadeOut(withDuration duration: TimeInterval) -> ActionToken { 28 | return perform(FadeAction(opacity: 0, duration: duration)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Core/API/Game.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /** 10 | * Class used to create an Imagine Engine game 11 | * 12 | * A Game is the root object any Imagine Engine game and provides a view 13 | * that can be attached to an app's view hierarchy. To present game content 14 | * create a `Scene` and attach it to your game. 15 | */ 16 | open class Game { 17 | /// The game's view, attach it to one of your app's views to start the game 18 | public let view: View 19 | /// The current active scene that the game is presenting 20 | public var scene: Scene { didSet { sceneDidChange(from: oldValue) } } 21 | 22 | internal private(set) var currentTime: TimeInterval 23 | 24 | private let displayLink: DisplayLinkProtocol 25 | private let dateProvider: () -> Date 26 | private var isActive = false 27 | 28 | // MARK: - Initializers 29 | 30 | /// Initialize an instance with a certain viewport size & an initial scene 31 | public convenience init(size: Size, scene: Scene) { 32 | let view = GameView(frame: Rect(origin: .zero, size: size)) 33 | self.init(view: view, scene: scene, displayLink: DisplayLink()) 34 | } 35 | 36 | internal init(view: GameView, 37 | scene: Scene, 38 | displayLink: DisplayLinkProtocol, 39 | dateProvider: @escaping () -> Date = Date.init) { 40 | self.view = view 41 | self.scene = scene 42 | self.displayLink = displayLink 43 | self.dateProvider = dateProvider 44 | currentTime = dateProvider().timeIntervalSinceReferenceDate 45 | 46 | view.game = self 47 | sceneDidChange(from: nil) 48 | } 49 | 50 | // MARK: - Internal 51 | 52 | internal func updateActivationStatus() { 53 | guard view.superview != nil else { 54 | isActive = false 55 | return 56 | } 57 | 58 | guard view.frame.size != .zero else { 59 | isActive = false 60 | return 61 | } 62 | 63 | guard !isActive else { 64 | return 65 | } 66 | 67 | isActive = true 68 | scene.activate(in: self) 69 | displayLink.activate() 70 | } 71 | 72 | // MARK: - Private 73 | 74 | private func sceneDidChange(from previousScene: Scene?) { 75 | previousScene?.deactivate() 76 | 77 | if isActive { 78 | scene.activate(in: self) 79 | } 80 | 81 | displayLink.callback = { [weak self] in 82 | guard let `self` = self else { 83 | return 84 | } 85 | 86 | self.currentTime = self.dateProvider().timeIntervalSinceReferenceDate 87 | self.scene.timeline.update(currentTime: self.currentTime) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Core/API/Group.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Enum used to define logical groups that game objects can belong to 10 | public enum Group: Hashable { 11 | /// A group that is identified by a string-based name 12 | case name(String) 13 | /// A group that is identifier by a number 14 | case number(Int) 15 | } 16 | 17 | public extension Group { 18 | /// Create a group using a member of a string-based enum 19 | static func enumValue(_ value: R) -> Group where R.RawValue == String { 20 | return .name(value.rawValue) 21 | } 22 | 23 | /// Create a group using a member of an int-based enum 24 | static func enumValue(_ value: R) -> Group where R.RawValue == Int { 25 | return .number(value.rawValue) 26 | } 27 | } 28 | 29 | internal extension Group { 30 | var identifier: String { 31 | switch self { 32 | case .name(let name): 33 | return "name-\(name)" 34 | case .number(let number): 35 | return "number-\(number)" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Core/API/InstanceHashable.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol adopted by objects that have their hash value calculated 10 | /// based on their instance (object) identifier 11 | public protocol InstanceHashable: class, Hashable {} 12 | 13 | extension InstanceHashable { 14 | public static func ==(lhs: Self, rhs: Self) -> Bool { 15 | return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) 16 | } 17 | public func hash(into hasher: inout Hasher) { 18 | hasher.combine(ObjectIdentifier(self)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Core/API/LabelEventCollection.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine Engine 3 | * Copyright (c) John Sundell 2017 4 | * See LICENSE file for license 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Events that can be used to observe an label 10 | public final class LabelEventCollection: EventCollection