diff --git a/AUTHORS b/AUTHORS index 3da7b5f40..1448b6b65 100644 --- a/AUTHORS +++ b/AUTHORS @@ -5,3 +5,4 @@ Google Inc. Abhijeeth Padarthi +Filip Hracek diff --git a/game_template/.gitignore b/game_template/.gitignore new file mode 100644 index 000000000..a10187af6 --- /dev/null +++ b/game_template/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks that +# you configure in VS Code that you might want include in version control, +# so this line is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +.fvm/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio places build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/game_template/.metadata b/game_template/.metadata new file mode 100644 index 000000000..166a9984c --- /dev/null +++ b/game_template/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: c860cba910319332564e1e9d470a17074c1f2dfd + channel: stable + +project_type: app diff --git a/game_template/README.md b/game_template/README.md new file mode 100644 index 000000000..7fa92a079 --- /dev/null +++ b/game_template/README.md @@ -0,0 +1,538 @@ +A starter game in Flutter with all the bells and whistles +of a mobile (iOS & Android) game including the following features: + +- sound +- music +- main menu screen +- settings +- ads (AdMob) +- in-app purchases +- games services (Game Center & Google Play Games Services) +- crash reporting (Firebase Crashlytics) + + +# Getting started + +The game compiles and works out of the box. It comes with things +like a main menu, a router, a settings screen, and audio. +When building a new game, this is likely everything you first need. + +When you're ready to enable more advanced integrations, like ads +and in-app payments, read the _Integrations_ section below. + + +# Development + +To run the app in debug mode: + + flutter run + +This assumes you have an Android emulator, +iOS Simulator, or an attached physical device. + +It is often convenient to develop your game as a desktop app. +For example, you can run `flutter run -d macOS`, and get the same UI +in a desktop window on a Mac. That way, you don't need to use a +simulator/emulator or attach a mobile device. This template supports +desktop development by disabling integrations like AdMob for desktop. + + +## Code organization + +Code is organized in a loose and shallow feature-first fashion. +In `lib/src`, you'll therefore find directories such as `ads`, `audio` +or `main_menu`. Nothing fancy, but usable. + +``` +lib +├── src +│   ├── ads +│   ├── app_lifecycle +│   ├── audio +│   ├── crashlytics +│   ├── game_internals +│   ├── games_services +│   ├── in_app_purchase +│   ├── level_selection +│   ├── main_menu +│   ├── play_session +│   ├── player_progress +│   ├── settings +│   ├── style +│   └── win_game +├── ... +└── main.dart +``` + +The state management approach is intentionally low-level. That way, it's easy to +take this project and run with it, without having to learn new paradigms, or having +to remember to run `flutter pub run build_runner watch`. You are, +of course, encouraged to use whatever paradigm, helper package or code generation +scheme that you prefer. + + +## Building for production + +To build the app for iOS (and open Xcode when finished): + +```bash +flutter build ipa && open build/ios/archive/Runner.xcarchive +``` + +To build the app for Android (and open the folder with the bundle when finished): + +```bash +flutter build appbundle && open build/app/outputs/bundle/release +``` + +While the template is meant for mobile games, you can also publish +for the web. This might be useful for web-based demos, for example, +or for rapid play-testing. The following command requires installing +[`peanut`](https://pub.dev/packages/peanut/install). + +```bash +flutter pub global run peanut \ +--web-renderer canvaskit \ +--extra-args "--base-href=/name_of_your_github_repo/" \ +&& git push origin --set-upstream gh-pages +``` + +The last line of the command above automatically pushes +your newly built web game to GitHub pages, assuming that you have +that set up. + + +# Integrations + +The more advanced integrations are disabled by default. For example, +achievements aren't enabled at first because you, the developer, +have to set them up (the achievements need to exist in App Store Connect +and Google Play Console before they can be used in the code). + +This section includes instructions on how to enable +any given integration. + +Some general notes: + +- Change the package name of your game + before you start any of the deeper integrations. + [StackOverflow has instructions](https://stackoverflow.com/a/51550358/1416886) + for this, and the [`rename`](https://pub.dev/packages/rename) tool + (on pub.dev) automates the process. +- The guides below all assume you already have your game + registered in [Google Play Console][] and in Apple's + [App Store Connect][]. + + +## Ads + +Ads are implemented using the official `google_mobile_ads` package +and are disabled by default. + +```dart +// TODO: When ready, uncomment the following lines to enable integrations. + +AdsController? adsController; +// if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) { +// /// Prepare the google_mobile_ads plugin so that the first ad loads +// /// faster. This can be done later or with a delay if startup +// /// experience suffers. +// adsController = AdsController(MobileAds.instance); +// adsController.initialize(); +// } +``` + +The `AdsController` code in`lib/main.dart` is `null` by default, +so the template gracefully falls back to not showing ads +on desktop. + +You can find the code relating to ads in `lib/src/ads/`. + +To enable ads in your game: + +1. Go to [AdMob][] and set up an account. + This could take a significant amount of time because you need to provide + banking information, sign contracts, and so on. +2. Create two _Apps_ in [AdMob][]: one for Android and one for iOS. +3. Get the AdMob _App IDs_ for both the Android app and the iOS app. + You can find these in the _App settings_ section. They look + something like `ca-app-pub-1234567890123456~1234567890` + (note the tilde between the two numbers). +4. Open `android/app/src/main/AndroidManifest.xml`, find the `` + entry called `com.google.android.gms.ads.APPLICATION_ID`, + and update the value with the _App ID_ of the Android AdMob app + that you obtained in the previous step. + + ```xml + + ``` +5. Open `ios/Runner/Info.plist`, find the + entry called `GADApplicationIdentifier`, + and update the value with the _App ID_ of the iOS AdMob app. + + ```xml + GADApplicationIdentifier + ca-app-pub-1234567890123456~0987654321 + ``` +6. Back in [AdMob][], create an _Ad unit_ for each of the AdMob apps. + This asks for the Ad unit's format (Banner, Interstitial, Rewarded). + The template is set up for a Banner ad unit, so select that if you + want to avoid making changes to the code in `lib/src/ads`. +7. Get the _Ad unit IDs_ for both the Android app and the iOS app. + You can find these in the _Ad units_ section. They look + something like `ca-app-pub-1234567890123456/1234567890` + (yes, the format is very similar to _App ID_; note the slash + between the two numbers). +8. Open `lib/src/ads/ads_controller.dart` and update the values + of the _Ad unit_ IDs there. + + ```dart + final adUnitId = defaultTargetPlatform == TargetPlatform.android + ? 'ca-app-pub-1234567890123456/1234567890' + : 'ca-app-pub-1234567890123456/0987654321'; + ``` +9. Uncomment the code relating to ads in `lib/main.dart`, + and add the following two imports: + + ```dart + import 'dart:io'; + import 'package:google_mobile_ads/google_mobile_ads.dart'; + ``` +10. Register your test devices + in [AdMob][]'s _Settings_ → _Test devices_ section. + +[AdMob]: https://admob.google.com/ + +The game template defines a sample AdMob _app ID_ and two sample _Ad unit ID_s. +These allow you to test your code without getting real +IDs from AdMob, but this "feature" is sparsely documented and only meant +for hello world projects. +The sample IDs **won't** work for published games. + +If you feel lost at any point, a full [AdMob for Flutter walk-through][] +is available on Google AdMob's documentation site. + +[AdMob for Flutter walk-through]: https://developers.google.com/admob/flutter/quick-start + +If you want to implement more AdMob formats (such as Interstitial ads), +a good place to start are the examples in +[`package:google_mobile_ads`](https://pub.dev/packages/google_mobile_ads). + +## Audio + +Audio is enabled by default and ready to go. You can modify code +in `lib/src/audio/` to your liking. + +You can find some music +tracks in `assets/music` — these are Creative Commons Attribution (CC-BY) +licensed, and are included in this repository with permission. If you decide +to keep these tracks in your game, please don't forget to give credit +to the musician, [Mr Smith][]. + +[Mr Smith]: https://freemusicarchive.org/music/mr-smith + +The repository also includes a few sound effect samples in `assets/sfx`. +These are public domain (CC0) and you will almost surely want to replace +them because they're just recordings of a developer doing silly sounds +with their mouth. + +## Crashlytics + +Crashlytics integration is disabled by default. But even if you don't +enable it, you might find code in `lib/src/crashlytics` helpful. +It gathers all log messages and errors, so that you can, at the very least, +print them to the console. + +When enabled, this integration is a lot more powerful: + +- Any crashes of your app are sent to the Firebase Crashlytics console. +- Any uncaught exception thrown anywhere in your code is captured + and sent to the Firebase Crashlytics console. +- Each of these reports includes the following information: + - Error message + - Stack trace + - Device model, orientation, RAM free, disk free + - Operating system version + - App version +- In addition, log messages generated anywhere in your app + (and from packages you use) are recorded in memory, + and are sent alongside the reports. This means that you can + learn what happened before the crash or exception + occurred. +- Also, any generated log message with `Level.severe` or above + is also sent to Crashlytics. +- You can customize these behaviors in `lib/src/crashlytics`. + +To enable Firebase Crashlytics, do the following: + +1. Create a new project in + [console.firebase.google.com](https://console.firebase.google.com/). + Call the Firebase project whatever you like; just **remember the name**. + You don't need to enable Analytics in the project if you don't want to. +2. [Install `firebase-tools`](https://firebase.google.com/docs/cli?authuser=0#setup_update_cli) + on your machine. +3. [Install `flutterfire` CLI](https://firebase.flutter.dev/docs/cli#installation) + on your machine. +4. In the root of this project (the directory containing `pubspec.yaml`), + run the following: + - `flutterfire configure` + - This command asks you for the name of the Firebase project + that you created earlier, and the list of target platforms you support. + As of April 2022, only `android` and `ios` are fully + supported by Crashlytics. + - The command rewrites `lib/firebase_options.dart` with + the correct code. +5. Go to `lib/main.dart` and uncomment the lines that relate to Crashlytics. + +You should now be able to see crashes, errors, and +severe log messages in +[console.firebase.google.com](https://console.firebase.google.com/). +To test, add a button to your project, and throw whatever +exception you like when the player presses it. + +```dart +TextButton( + onPressed: () => throw StateError('whoa!'), + child: Text('Test Crashlytics'), +) +``` + + +## Games Services (Game Center & Play Games Services) + +Games Services (like achievements and leaderboards) are implemented by the +[`games_services`](https://pub.dev/packages/games_services) package, +and are disabled by default. + +To enable games services, first set up _Game Center_ on iOS +and _Google Play Games Services_ on Android. + +To enable _Game Center_ (GameKit) on iOS: + +1. Open your Flutter project in Xcode (`open ios/Runner.xcodeproj`). +2. Select the root _Runner_ project and go to the _Signing & Capabilities_ tab. +3. Click the `+` button to add _Game Center_ as a capability. + You can close Xcode now. +4. Go to your app in [App Store Connect][] and set up _Game Center_ + in the _Features_ section. For example, you might want to set up + a leaderboard and several achievements. + Take note of the IDs of the leaderboards and achievements you create. + +[App Store Connect]: https://appstoreconnect.apple.com/ + +To enable _Play Games Services_ on Android: + +1. Go to your app in [Google Play Console][]. +2. Select _Play Games Services_ → _Setup and management_ → + _Configuration_ from the navigation menu and follow their instructions. + * This takes a significant amount of time and patience. + Among other things, you'll need to set up an OAuth consent + screen in Google Cloud Console. + If at any point you feel lost, + consult the official [Play Games Services guide][]. +3. When done, you can start adding leaderboards and achievements + in _Play Games Services_ → _Setup and management_. + Create the exact same set as you did on the iOS side. + Make note of IDs. +4. Go to _Play Games Services_ → _Setup and management_ → + Publishing, and click _'Publish'_. Don't worry, this doesn't + actually publish your game. It only publishes the achievements + and leaderboard. Once a leaderboard, for example, is published + this way, it cannot be unpublished. +5. Go to _Play Games Services_ → + _Setup and management_ → _Configuration_ → + _Credentials_. Find a button that says _'Get resources'_. + You get an XML file with the _Play Games Services_ IDs. + + ```xml + + + + + 424242424242 + + dev.flutter.tictactoe + + sOmEiDsTrInG + + sOmEiDsTrInG + + ``` +6. Replace the file at `android/app/src/main/res/values/games-ids.xml` + with the XML you received in the previous step. + +[Google Play Console]: https://play.google.com/console/ +[Play Games Services guide]: https://developers.google.com/games/services/console/enabling + +Now that you have set up _Game Center_ and _Play Games Services_, +and have your achievement & leaderboard IDs ready, it's finally Dart time. + +1. Open `lib/src/games_services/games_services.dart` and edit the leaderboard + IDs in the `showLeaderboard()` function. + + ```dart + // TODO: When ready, change both these leaderboard IDs. + iOSLeaderboardID: "some_id_from_app_store", + androidLeaderboardID: "sOmE_iD_fRoM_gPlAy", + ``` +2. The `awardAchievement()` function in the same file takes the IDs + as arguments. You can therefore call it from anywhere + in your game like this: + + ```dart + final gamesServicesController = context.read(); + await gamesServicesController?.awardAchievement( + iOS: 'an_achievement_id', + android: 'aNaChIeVeMenTiDfRoMgPlAy', + ); + ``` + + You might want to attach the achievement IDs to levels, enemies, + places, items, and so on. For example, the template has levels + defined in `lib/src/level_selection/levels.dart` like so: + + ```dart + GameLevel( + number: 1, + difficulty: 5, + achievementIdIOS: 'first_win', + achievementIdAndroid: 'sOmEtHinG', + ), + ``` + + That way, after the player reaches a level, we check if the level + has non-null achievement IDs, and if so, we call `awardAchievement()` + with those IDs. +3. Uncomment the code relating to games services in `lib/main.dart`. + + ```dart + // TODO: When ready, uncomment the following lines. + + GamesServicesController? gamesServicesController; + // if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) { + // gamesServicesController = GamesServicesController() + // // Attempt to log the player in. + // ..initialize(); + // } + ``` + +If at any point you feel lost, there's a [How To][] guide written by the author +of `package:games_services`. Some of the guide's instructions and screenshots +are slightly outdated (for example, _iTunes Connect_ has been renamed to _App Store Connect_ +after the article was published) but it's still an excellent resource. + +[How To]: https://itnext.io/how-to-integrate-gamekit-and-google-play-services-flutter-4d3f4a4a2f77 + + +## In-app purchases + +In-app purchases are implemented using the official +[`in_app_purchase`](https://pub.dev/packages/in_app_purchase) package. +The integration is disabled by default. + +To enable in-app purchases on Android: + +1. Upload the game to [Google Play Console][], + to the Closed Testing track. + - Since the game already + depends on `package:in_app_purchase`, it signals itself to the + Play Store as a project with in-app purchases. + - Releasing to Closed Testing triggers a review process, + which is a prerequisite for in-app purchases to work. + The review process can take several days and until it's complete, + you can't move on with the Android side of things. +2. Add an in-app product in _Play Console_ → _Monetize_ → + _In-app products_. Come up with a product ID (for example, + `ad_removal`). +3. While still in Play Console, _activate_ the in-app product. + +To enable in-app purchases on iOS: + +1. Make sure you have signed the _Paid Apps Agreement_ + in [App Store Connect][]. +2. While still in App Store Connect, go to _Features_ → + _In-App Purchases_, and add a new in-app purchase + by clicking the `+` button. + Use the same product ID you used on the Android side. +3. Follow instructions on how to get the in-app purchase approved. + +Now everything is ready to enable the integration in your Dart code: + +1. Open `lib/src/in_app_purchase/ad_removal.dart` and change `productId` + to the product ID you entered in Play Console and App Store Connect. + + ```dart + /// The representation of this product on the stores. + static const productId = 'remove_ads'; + ``` + + - If your in-app purchase is not an ad removal, then create a class + similar to the template's `AdRemovalPurchase`. + - If you created several in-app purchases, you need to modify + the code in `lib/src/in_app_purchase/in_app_purchase.dart`. + By default, the template only supports one in-app purchase. +2. Uncomment the code relating to in-app purchases in `lib/main.dart`. + + ```dart + // TODO: When ready, uncomment the following lines. + + InAppPurchaseController? inAppPurchaseController; + // if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) { + // inAppPurchaseController = InAppPurchaseController(InAppPurchase.instance) + // // Subscribing to [InAppPurchase.instance.purchaseStream] as soon + // // as possible in order not to miss any updates. + // ..subscribe(); + // // Ask the store what the player has bought already. + // inAppPurchaseController.restorePurchases(); + // } + ``` + + +If at any point you feel lost, check out the official +[Adding in-app purchases to your Flutter app](https://codelabs.developers.google.com/codelabs/flutter-in-app-purchases#0) +codelab. + + +## Settings + +The settings page is enabled by default, and accessible both +from the main menu and the "gear" button in the play session screen. + +Settings are saved to local storage using the `package:shared_preferences`. +To change what preferences are saved and how, edit files in +`lib/src/settings/persistence`. + +```dart +abstract class SettingsPersistence { + Future getMusicOn(); + + Future getMuted({required bool defaultValue}); + + Future getPlayerName(); + + Future getSoundsOn(); + + Future saveMusicOn(bool value); + + Future saveMuted(bool value); + + Future savePlayerName(String value); + + Future saveSoundsOn(bool value); +} +``` + +# Icon + +To update the launcher icon, first change the files +`assets/icon-adaptive-foreground.png` and `assets/icon.png`. +Then, run the following: + +```bash +flutter pub run flutter_launcher_icons:main +``` + +You can [configure](https://github.com/fluttercommunity/flutter_launcher_icons#book-guide) +the look of the icon in the `flutter_icons:` section of `pubspec.yaml`. diff --git a/game_template/analysis_options.yaml b/game_template/analysis_options.yaml new file mode 100644 index 000000000..379ca2294 --- /dev/null +++ b/game_template/analysis_options.yaml @@ -0,0 +1,9 @@ +include: ../analysis_options.yaml + +linter: + rules: + # Remove or force lint rules by adding lines like the following. + # The lints below are disabled in order to make things smoother in early + # development. Consider enabling them once development is further along. + prefer_const_constructors: false # Annoying in early development + prefer_single_quotes: false # Annoying in early development diff --git a/game_template/android/.gitignore b/game_template/android/.gitignore new file mode 100644 index 000000000..6f568019d --- /dev/null +++ b/game_template/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/game_template/android/app/build.gradle b/game_template/android/app/build.gradle new file mode 100644 index 000000000..03238d4e2 --- /dev/null +++ b/game_template/android/app/build.gradle @@ -0,0 +1,68 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.game_template" + minSdkVersion 21 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/game_template/android/app/src/debug/AndroidManifest.xml b/game_template/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..e720d43d4 --- /dev/null +++ b/game_template/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/game_template/android/app/src/main/AndroidManifest.xml b/game_template/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..088728e42 --- /dev/null +++ b/game_template/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/game_template/android/app/src/main/kotlin/com/example/game_template/MainActivity.kt b/game_template/android/app/src/main/kotlin/com/example/game_template/MainActivity.kt new file mode 100644 index 000000000..333ec8c2e --- /dev/null +++ b/game_template/android/app/src/main/kotlin/com/example/game_template/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.game_template + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/game_template/android/app/src/main/res/drawable-v21/launch_background.xml b/game_template/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000..f74085f3f --- /dev/null +++ b/game_template/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/game_template/android/app/src/main/res/drawable/launch_background.xml b/game_template/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000..304732f88 --- /dev/null +++ b/game_template/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/game_template/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/game_template/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..db77bb4b7 Binary files /dev/null and b/game_template/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/game_template/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/game_template/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..17987b79b Binary files /dev/null and b/game_template/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/game_template/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/game_template/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..09d439148 Binary files /dev/null and b/game_template/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/game_template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/game_template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d5f1c8d34 Binary files /dev/null and b/game_template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/game_template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/game_template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/game_template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/game_template/android/app/src/main/res/values-night/styles.xml b/game_template/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000..3db14bb53 --- /dev/null +++ b/game_template/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/game_template/android/app/src/main/res/values/games-ids.xml b/game_template/android/app/src/main/res/values/games-ids.xml new file mode 100644 index 000000000..59f0b1a8e --- /dev/null +++ b/game_template/android/app/src/main/res/values/games-ids.xml @@ -0,0 +1,17 @@ + + + + + 123456789012 + + name.of.your.game + + sOmEtHiNg + diff --git a/game_template/android/app/src/main/res/values/styles.xml b/game_template/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..d460d1e92 --- /dev/null +++ b/game_template/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/game_template/android/app/src/profile/AndroidManifest.xml b/game_template/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000..e720d43d4 --- /dev/null +++ b/game_template/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/game_template/android/build.gradle b/game_template/android/build.gradle new file mode 100644 index 000000000..4256f9173 --- /dev/null +++ b/game_template/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/game_template/android/gradle.properties b/game_template/android/gradle.properties new file mode 100644 index 000000000..94adc3a3f --- /dev/null +++ b/game_template/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/game_template/android/gradle/wrapper/gradle-wrapper.properties b/game_template/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..bc6a58afd --- /dev/null +++ b/game_template/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/game_template/android/settings.gradle b/game_template/android/settings.gradle new file mode 100644 index 000000000..44e62bcf0 --- /dev/null +++ b/game_template/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/game_template/assets/Permanent_Marker/LICENSE.txt b/game_template/assets/Permanent_Marker/LICENSE.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/game_template/assets/Permanent_Marker/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/game_template/assets/Permanent_Marker/PermanentMarker-Regular.ttf b/game_template/assets/Permanent_Marker/PermanentMarker-Regular.ttf new file mode 100644 index 000000000..6541e9d87 Binary files /dev/null and b/game_template/assets/Permanent_Marker/PermanentMarker-Regular.ttf differ diff --git a/game_template/assets/icon-adaptive-foreground.png b/game_template/assets/icon-adaptive-foreground.png new file mode 100644 index 000000000..6fe945591 Binary files /dev/null and b/game_template/assets/icon-adaptive-foreground.png differ diff --git a/game_template/assets/icon.png b/game_template/assets/icon.png new file mode 100644 index 000000000..a8f5c3079 Binary files /dev/null and b/game_template/assets/icon.png differ diff --git a/game_template/assets/images/2x/back.png b/game_template/assets/images/2x/back.png new file mode 100644 index 000000000..c2daa283d Binary files /dev/null and b/game_template/assets/images/2x/back.png differ diff --git a/game_template/assets/images/2x/restart.png b/game_template/assets/images/2x/restart.png new file mode 100644 index 000000000..360c109fd Binary files /dev/null and b/game_template/assets/images/2x/restart.png differ diff --git a/game_template/assets/images/2x/settings.png b/game_template/assets/images/2x/settings.png new file mode 100644 index 000000000..8780f9437 Binary files /dev/null and b/game_template/assets/images/2x/settings.png differ diff --git a/game_template/assets/images/3.5x/back.png b/game_template/assets/images/3.5x/back.png new file mode 100644 index 000000000..5961b2292 Binary files /dev/null and b/game_template/assets/images/3.5x/back.png differ diff --git a/game_template/assets/images/3.5x/restart.png b/game_template/assets/images/3.5x/restart.png new file mode 100644 index 000000000..3f89e1981 Binary files /dev/null and b/game_template/assets/images/3.5x/restart.png differ diff --git a/game_template/assets/images/3.5x/settings.png b/game_template/assets/images/3.5x/settings.png new file mode 100644 index 000000000..33d4326ab Binary files /dev/null and b/game_template/assets/images/3.5x/settings.png differ diff --git a/game_template/assets/images/3x/back.png b/game_template/assets/images/3x/back.png new file mode 100644 index 000000000..2ac088bc7 Binary files /dev/null and b/game_template/assets/images/3x/back.png differ diff --git a/game_template/assets/images/3x/restart.png b/game_template/assets/images/3x/restart.png new file mode 100644 index 000000000..3578b927b Binary files /dev/null and b/game_template/assets/images/3x/restart.png differ diff --git a/game_template/assets/images/3x/settings.png b/game_template/assets/images/3x/settings.png new file mode 100644 index 000000000..a7dbd260e Binary files /dev/null and b/game_template/assets/images/3x/settings.png differ diff --git a/game_template/assets/images/back.png b/game_template/assets/images/back.png new file mode 100644 index 000000000..65ab3eefe Binary files /dev/null and b/game_template/assets/images/back.png differ diff --git a/game_template/assets/images/restart.png b/game_template/assets/images/restart.png new file mode 100644 index 000000000..bd22efedf Binary files /dev/null and b/game_template/assets/images/restart.png differ diff --git a/game_template/assets/images/settings.png b/game_template/assets/images/settings.png new file mode 100644 index 000000000..48863f4c7 Binary files /dev/null and b/game_template/assets/images/settings.png differ diff --git a/game_template/assets/music/Mr_Smith-Azul.mp3 b/game_template/assets/music/Mr_Smith-Azul.mp3 new file mode 100644 index 000000000..d454d1aee Binary files /dev/null and b/game_template/assets/music/Mr_Smith-Azul.mp3 differ diff --git a/game_template/assets/music/Mr_Smith-Sonorus.mp3 b/game_template/assets/music/Mr_Smith-Sonorus.mp3 new file mode 100644 index 000000000..28a01d7a3 Binary files /dev/null and b/game_template/assets/music/Mr_Smith-Sonorus.mp3 differ diff --git a/game_template/assets/music/Mr_Smith-Sunday_Solitude.mp3 b/game_template/assets/music/Mr_Smith-Sunday_Solitude.mp3 new file mode 100644 index 000000000..0eec63924 Binary files /dev/null and b/game_template/assets/music/Mr_Smith-Sunday_Solitude.mp3 differ diff --git a/game_template/assets/music/README.md b/game_template/assets/music/README.md new file mode 100644 index 000000000..c054c0727 --- /dev/null +++ b/game_template/assets/music/README.md @@ -0,0 +1,6 @@ +Music in the template is by Mr Smith, and licensed under Creative Commons +Attribution 4.0 International (CC BY 4.0). + +https://freemusicarchive.org/music/mr-smith + +Mr Smith's music is used in this template project with his explicit permission. diff --git a/game_template/assets/sfx/README.md b/game_template/assets/sfx/README.md new file mode 100644 index 000000000..90099092b --- /dev/null +++ b/game_template/assets/sfx/README.md @@ -0,0 +1 @@ +Sounds in this folder are made by Filip Hracek and are CC0 (Public Domain). diff --git a/game_template/assets/sfx/dsht1.mp3 b/game_template/assets/sfx/dsht1.mp3 new file mode 100644 index 000000000..8ce3ac577 Binary files /dev/null and b/game_template/assets/sfx/dsht1.mp3 differ diff --git a/game_template/assets/sfx/ehehee1.mp3 b/game_template/assets/sfx/ehehee1.mp3 new file mode 100644 index 000000000..73d6ccf26 Binary files /dev/null and b/game_template/assets/sfx/ehehee1.mp3 differ diff --git a/game_template/assets/sfx/fwfwfwfw1.mp3 b/game_template/assets/sfx/fwfwfwfw1.mp3 new file mode 100644 index 000000000..2784578de Binary files /dev/null and b/game_template/assets/sfx/fwfwfwfw1.mp3 differ diff --git a/game_template/assets/sfx/fwfwfwfwfw1.mp3 b/game_template/assets/sfx/fwfwfwfwfw1.mp3 new file mode 100644 index 000000000..f84770103 Binary files /dev/null and b/game_template/assets/sfx/fwfwfwfwfw1.mp3 differ diff --git a/game_template/assets/sfx/hash1.mp3 b/game_template/assets/sfx/hash1.mp3 new file mode 100644 index 000000000..5e4bb7ca0 Binary files /dev/null and b/game_template/assets/sfx/hash1.mp3 differ diff --git a/game_template/assets/sfx/hash2.mp3 b/game_template/assets/sfx/hash2.mp3 new file mode 100644 index 000000000..1068ce4d9 Binary files /dev/null and b/game_template/assets/sfx/hash2.mp3 differ diff --git a/game_template/assets/sfx/hash3.mp3 b/game_template/assets/sfx/hash3.mp3 new file mode 100644 index 000000000..916ddda7c Binary files /dev/null and b/game_template/assets/sfx/hash3.mp3 differ diff --git a/game_template/assets/sfx/haw1.mp3 b/game_template/assets/sfx/haw1.mp3 new file mode 100644 index 000000000..7d25af2e4 Binary files /dev/null and b/game_template/assets/sfx/haw1.mp3 differ diff --git a/game_template/assets/sfx/hh1.mp3 b/game_template/assets/sfx/hh1.mp3 new file mode 100644 index 000000000..8ca434426 Binary files /dev/null and b/game_template/assets/sfx/hh1.mp3 differ diff --git a/game_template/assets/sfx/hh2.mp3 b/game_template/assets/sfx/hh2.mp3 new file mode 100644 index 000000000..b11cb0a9a Binary files /dev/null and b/game_template/assets/sfx/hh2.mp3 differ diff --git a/game_template/assets/sfx/k1.mp3 b/game_template/assets/sfx/k1.mp3 new file mode 100644 index 000000000..757e21ee3 Binary files /dev/null and b/game_template/assets/sfx/k1.mp3 differ diff --git a/game_template/assets/sfx/k2.mp3 b/game_template/assets/sfx/k2.mp3 new file mode 100644 index 000000000..1e45f8bd3 Binary files /dev/null and b/game_template/assets/sfx/k2.mp3 differ diff --git a/game_template/assets/sfx/kch1.mp3 b/game_template/assets/sfx/kch1.mp3 new file mode 100644 index 000000000..cfe79c378 Binary files /dev/null and b/game_template/assets/sfx/kch1.mp3 differ diff --git a/game_template/assets/sfx/kss1.mp3 b/game_template/assets/sfx/kss1.mp3 new file mode 100644 index 000000000..e7fd20a29 Binary files /dev/null and b/game_template/assets/sfx/kss1.mp3 differ diff --git a/game_template/assets/sfx/lalala1.mp3 b/game_template/assets/sfx/lalala1.mp3 new file mode 100644 index 000000000..85dcd801f Binary files /dev/null and b/game_template/assets/sfx/lalala1.mp3 differ diff --git a/game_template/assets/sfx/oo1.mp3 b/game_template/assets/sfx/oo1.mp3 new file mode 100644 index 000000000..172ca8256 Binary files /dev/null and b/game_template/assets/sfx/oo1.mp3 differ diff --git a/game_template/assets/sfx/p1.mp3 b/game_template/assets/sfx/p1.mp3 new file mode 100644 index 000000000..65755465a Binary files /dev/null and b/game_template/assets/sfx/p1.mp3 differ diff --git a/game_template/assets/sfx/p2.mp3 b/game_template/assets/sfx/p2.mp3 new file mode 100644 index 000000000..7d7a7fd4c Binary files /dev/null and b/game_template/assets/sfx/p2.mp3 differ diff --git a/game_template/assets/sfx/sh1.mp3 b/game_template/assets/sfx/sh1.mp3 new file mode 100644 index 000000000..5059dd57e Binary files /dev/null and b/game_template/assets/sfx/sh1.mp3 differ diff --git a/game_template/assets/sfx/sh2.mp3 b/game_template/assets/sfx/sh2.mp3 new file mode 100644 index 000000000..6425d99ac Binary files /dev/null and b/game_template/assets/sfx/sh2.mp3 differ diff --git a/game_template/assets/sfx/spsh1.mp3 b/game_template/assets/sfx/spsh1.mp3 new file mode 100644 index 000000000..34065c857 Binary files /dev/null and b/game_template/assets/sfx/spsh1.mp3 differ diff --git a/game_template/assets/sfx/swishswish1.mp3 b/game_template/assets/sfx/swishswish1.mp3 new file mode 100644 index 000000000..2d030aa00 Binary files /dev/null and b/game_template/assets/sfx/swishswish1.mp3 differ diff --git a/game_template/assets/sfx/wehee1.mp3 b/game_template/assets/sfx/wehee1.mp3 new file mode 100644 index 000000000..8f7bc8446 Binary files /dev/null and b/game_template/assets/sfx/wehee1.mp3 differ diff --git a/game_template/assets/sfx/ws1.mp3 b/game_template/assets/sfx/ws1.mp3 new file mode 100644 index 000000000..0daf26a48 Binary files /dev/null and b/game_template/assets/sfx/ws1.mp3 differ diff --git a/game_template/assets/sfx/wssh1.mp3 b/game_template/assets/sfx/wssh1.mp3 new file mode 100644 index 000000000..9f455d952 Binary files /dev/null and b/game_template/assets/sfx/wssh1.mp3 differ diff --git a/game_template/assets/sfx/wssh2.mp3 b/game_template/assets/sfx/wssh2.mp3 new file mode 100644 index 000000000..b5c5c3b1a Binary files /dev/null and b/game_template/assets/sfx/wssh2.mp3 differ diff --git a/game_template/assets/sfx/yay1.mp3 b/game_template/assets/sfx/yay1.mp3 new file mode 100644 index 000000000..51102cbf2 Binary files /dev/null and b/game_template/assets/sfx/yay1.mp3 differ diff --git a/game_template/ios/.gitignore b/game_template/ios/.gitignore new file mode 100644 index 000000000..7a7f9873a --- /dev/null +++ b/game_template/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/game_template/ios/Flutter/AppFrameworkInfo.plist b/game_template/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000..8d4492f97 --- /dev/null +++ b/game_template/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/game_template/ios/Flutter/Debug.xcconfig b/game_template/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000..ec97fc6f3 --- /dev/null +++ b/game_template/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/game_template/ios/Flutter/Release.xcconfig b/game_template/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000..c4855bfe2 --- /dev/null +++ b/game_template/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/game_template/ios/Podfile b/game_template/ios/Podfile new file mode 100644 index 000000000..1e8c3c90a --- /dev/null +++ b/game_template/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/game_template/ios/Podfile.lock b/game_template/ios/Podfile.lock new file mode 100644 index 000000000..183123d6f --- /dev/null +++ b/game_template/ios/Podfile.lock @@ -0,0 +1,174 @@ +PODS: + - audioplayers (0.0.1): + - Flutter + - Firebase/CoreOnly (8.15.0): + - FirebaseCore (= 8.15.0) + - Firebase/Crashlytics (8.15.0): + - Firebase/CoreOnly + - FirebaseCrashlytics (~> 8.15.0) + - firebase_core (1.15.0): + - Firebase/CoreOnly (= 8.15.0) + - Flutter + - firebase_crashlytics (2.6.3): + - Firebase/Crashlytics (= 8.15.0) + - firebase_core + - Flutter + - FirebaseCore (8.15.0): + - FirebaseCoreDiagnostics (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - FirebaseCoreDiagnostics (8.15.0): + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - nanopb (~> 2.30908.0) + - FirebaseCrashlytics (8.15.0): + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/Environment (~> 7.7) + - nanopb (~> 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - FirebaseInstallations (8.15.0): + - FirebaseCore (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - PromisesObjC (< 3.0, >= 1.2) + - Flutter (1.0.0) + - games_services (0.0.1): + - Flutter + - Google-Mobile-Ads-SDK (8.13.0): + - GoogleAppMeasurement (< 9.0, >= 7.0) + - GoogleUserMessagingPlatform (>= 1.1) + - google_mobile_ads (0.0.1): + - Flutter + - Google-Mobile-Ads-SDK (= 8.13.0) + - GoogleAppMeasurement (8.12.0): + - GoogleAppMeasurement/AdIdSupport (= 8.12.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - GoogleAppMeasurement/AdIdSupport (8.12.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 8.12.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - GoogleAppMeasurement/WithoutAdIdSupport (8.12.0): + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - GoogleDataTransport (9.1.2): + - GoogleUtilities/Environment (~> 7.2) + - nanopb (~> 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUserMessagingPlatform (2.0.0) + - GoogleUtilities/AppDelegateSwizzler (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.7.0): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/MethodSwizzler (7.7.0): + - GoogleUtilities/Logger + - GoogleUtilities/Network (7.7.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.7.0)" + - GoogleUtilities/Reachability (7.7.0): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (7.7.0): + - GoogleUtilities/Logger + - in_app_purchase_storekit (0.0.1): + - Flutter + - nanopb (2.30908.0): + - nanopb/decode (= 2.30908.0) + - nanopb/encode (= 2.30908.0) + - nanopb/decode (2.30908.0) + - nanopb/encode (2.30908.0) + - path_provider_ios (0.0.1): + - Flutter + - PromisesObjC (2.0.0) + - shared_preferences_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - audioplayers (from `.symlinks/plugins/audioplayers/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) + - Flutter (from `Flutter`) + - games_services (from `.symlinks/plugins/games_services/ios`) + - google_mobile_ads (from `.symlinks/plugins/google_mobile_ads/ios`) + - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/ios`) + - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + +SPEC REPOS: + trunk: + - Firebase + - FirebaseCore + - FirebaseCoreDiagnostics + - FirebaseCrashlytics + - FirebaseInstallations + - Google-Mobile-Ads-SDK + - GoogleAppMeasurement + - GoogleDataTransport + - GoogleUserMessagingPlatform + - GoogleUtilities + - nanopb + - PromisesObjC + +EXTERNAL SOURCES: + audioplayers: + :path: ".symlinks/plugins/audioplayers/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_crashlytics: + :path: ".symlinks/plugins/firebase_crashlytics/ios" + Flutter: + :path: Flutter + games_services: + :path: ".symlinks/plugins/games_services/ios" + google_mobile_ads: + :path: ".symlinks/plugins/google_mobile_ads/ios" + in_app_purchase_storekit: + :path: ".symlinks/plugins/in_app_purchase_storekit/ios" + path_provider_ios: + :path: ".symlinks/plugins/path_provider_ios/ios" + shared_preferences_ios: + :path: ".symlinks/plugins/shared_preferences_ios/ios" + +SPEC CHECKSUMS: + audioplayers: 455322b54050b30ea4b1af7cd9e9d105f74efa8c + Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d + firebase_core: fa19947d8db1c0a62d8872c45039b3113829cd2e + firebase_crashlytics: bf47208bdd39b16c90a0beed3fed09786081bcba + FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1 + FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb + FirebaseCrashlytics: feb07e4e9187be3c23c6a846cce4824e5ce2dd0b + FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + games_services: 6c9a23d55bc6ae0129b24507e008643dcb02f341 + Google-Mobile-Ads-SDK: 05e5d68bb42a61b2e5bef336a52789785605aa22 + google_mobile_ads: 36eaaacc5c08ca2b9c4878232ab98152b11805cc + GoogleAppMeasurement: ae033c3aad67e68294369373056b4d74cc8ae0d6 + GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 + GoogleUserMessagingPlatform: ab890ce5f6620f293a21b6bdd82e416a2c73aeca + GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 + in_app_purchase_storekit: d7fcf4646136ec258e237872755da8ea6c1b6096 + nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 + shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad + +PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c + +COCOAPODS: 1.11.2 diff --git a/game_template/ios/Runner.xcodeproj/project.pbxproj b/game_template/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..bfd10f0f0 --- /dev/null +++ b/game_template/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,555 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 5242776D1CC76A22FF8149EF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27EE1002C41F0C2E3F9CEBC3 /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 27EE1002C41F0C2E3F9CEBC3 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5BF194CD332DE1CAC09BD6F0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8BB71794779AD8B569C40F2E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9FDE944BCE54149A93F91707 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5242776D1CC76A22FF8149EF /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7D1D08B0328D0767484FE036 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 27EE1002C41F0C2E3F9CEBC3 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + BF9564A47EC3FE06EBF66DC3 /* Pods */, + 7D1D08B0328D0767484FE036 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + BF9564A47EC3FE06EBF66DC3 /* Pods */ = { + isa = PBXGroup; + children = ( + 9FDE944BCE54149A93F91707 /* Pods-Runner.debug.xcconfig */, + 8BB71794779AD8B569C40F2E /* Pods-Runner.release.xcconfig */, + 5BF194CD332DE1CAC09BD6F0 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + E16480A248DF2F6B44364560 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 272B410CAABABCB1CE1B1183 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 272B410CAABABCB1CE1B1183 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + E16480A248DF2F6B44364560 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 3QUFHCJT7P; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.gameTemplate; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 3QUFHCJT7P; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.gameTemplate; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 3QUFHCJT7P; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.gameTemplate; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/game_template/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/game_template/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/game_template/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/game_template/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/game_template/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/game_template/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/game_template/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/game_template/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/game_template/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/game_template/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/game_template/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..c87d15a33 --- /dev/null +++ b/game_template/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game_template/ios/Runner.xcworkspace/contents.xcworkspacedata b/game_template/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/game_template/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/game_template/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/game_template/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/game_template/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/game_template/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/game_template/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/game_template/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/game_template/ios/Runner/AppDelegate.swift b/game_template/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000..70693e4a8 --- /dev/null +++ b/game_template/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..d36b1fab2 --- /dev/null +++ b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000..dc9ada472 Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000..28c6bf030 Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000..2ccbfd967 Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000..f091b6b0b Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000..4cde12118 Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000..d0ef06e7e Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000..dcdc2306c Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000..2ccbfd967 Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000..c8f9ed8f5 Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000..a6d6b8609 Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000..a6d6b8609 Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000..75b2d164a Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000..c4df70d39 Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000..6a84f41e1 Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000..d0e1f5853 Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000..0bedcf2fd --- /dev/null +++ b/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000..89c2725b7 --- /dev/null +++ b/game_template/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/game_template/ios/Runner/Base.lproj/LaunchScreen.storyboard b/game_template/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..f2e259c7c --- /dev/null +++ b/game_template/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game_template/ios/Runner/Base.lproj/Main.storyboard b/game_template/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000..f3c28516f --- /dev/null +++ b/game_template/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game_template/ios/Runner/Info.plist b/game_template/ios/Runner/Info.plist new file mode 100644 index 000000000..d86409fc1 --- /dev/null +++ b/game_template/ios/Runner/Info.plist @@ -0,0 +1,52 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Game Template + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + game_template + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + GADApplicationIdentifier + ca-app-pub-3940256099942544~1458002511 + + diff --git a/game_template/ios/Runner/Runner-Bridging-Header.h b/game_template/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000..308a2a560 --- /dev/null +++ b/game_template/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/game_template/lib/firebase_options.dart b/game_template/lib/firebase_options.dart new file mode 100644 index 000000000..0a1a62e2b --- /dev/null +++ b/game_template/lib/firebase_options.dart @@ -0,0 +1,12 @@ +// File normally generated by FlutterFire CLI. This is a stand-in. +// See README.md for details. +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; + +class DefaultFirebaseOptions { + @Deprecated('Run `flutterfire configure` to re-generate this file') + static FirebaseOptions get currentPlatform { + throw UnimplementedError( + 'Generate this file by running `flutterfire configure`. ' + 'See README.md for details.'); + } +} diff --git a/game_template/lib/main.dart b/game_template/lib/main.dart new file mode 100644 index 000000000..a442385bb --- /dev/null +++ b/game_template/lib/main.dart @@ -0,0 +1,261 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; + +import 'src/ads/ads_controller.dart'; +import 'src/app_lifecycle/app_lifecycle.dart'; +import 'src/audio/audio_controller.dart'; +import 'src/crashlytics/crashlytics.dart'; +import 'src/games_services/games_services.dart'; +import 'src/games_services/score.dart'; +import 'src/in_app_purchase/in_app_purchase.dart'; +import 'src/level_selection/level_selection_screen.dart'; +import 'src/level_selection/levels.dart'; +import 'src/main_menu/main_menu_screen.dart'; +import 'src/play_session/play_session_screen.dart'; +import 'src/player_progress/persistence/local_storage_player_progress_persistence.dart'; +import 'src/player_progress/persistence/player_progress_persistence.dart'; +import 'src/player_progress/player_progress.dart'; +import 'src/settings/persistence/local_storage_settings_persistence.dart'; +import 'src/settings/persistence/settings_persistence.dart'; +import 'src/settings/settings.dart'; +import 'src/settings/settings_screen.dart'; +import 'src/style/my_transition.dart'; +import 'src/style/palette.dart'; +import 'src/style/snack_bar.dart'; +import 'src/win_game/win_game_screen.dart'; + +Future main() async { + // Uncomment the following lines to enable Firebase Crashlytics. + // See lib/src/crashlytics/README.md for details. + + FirebaseCrashlytics? crashlytics; + // if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) { + // try { + // WidgetsFlutterBinding.ensureInitialized(); + // await Firebase.initializeApp( + // options: DefaultFirebaseOptions.currentPlatform, + // ); + // crashlytics = FirebaseCrashlytics.instance; + // } catch (e) { + // debugPrint("Firebase couldn't be initialized: $e"); + // } + // } + + await guardWithCrashlytics( + guardedMain, + crashlytics: crashlytics, + ); +} + +/// Without logging and crash reporting, this would be `void main()`. +void guardedMain() { + if (kReleaseMode) { + // Don't log anything below warnings in production. + Logger.root.level = Level.WARNING; + } + Logger.root.onRecord.listen((record) { + debugPrint('${record.level.name}: ${record.time}: ' + '${record.loggerName}: ' + '${record.message}'); + }); + + WidgetsFlutterBinding.ensureInitialized(); + + _log.info('Going full screen'); + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.edgeToEdge, + ); + + // TODO: When ready, uncomment the following lines to enable integrations. + // Read the README for more info on each integration. + + AdsController? adsController; + // if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) { + // /// Prepare the google_mobile_ads plugin so that the first ad loads + // /// faster. This can be done later or with a delay if startup + // /// experience suffers. + // adsController = AdsController(MobileAds.instance); + // adsController.initialize(); + // } + + GamesServicesController? gamesServicesController; + // if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) { + // gamesServicesController = GamesServicesController() + // // Attempt to log the player in. + // ..initialize(); + // } + + InAppPurchaseController? inAppPurchaseController; + // if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) { + // inAppPurchaseController = InAppPurchaseController(InAppPurchase.instance) + // // Subscribing to [InAppPurchase.instance.purchaseStream] as soon + // // as possible in order not to miss any updates. + // ..subscribe(); + // // Ask the store what the player has bought already. + // inAppPurchaseController.restorePurchases(); + // } + + runApp( + MyApp( + settingsPersistence: LocalStorageSettingsPersistence(), + playerProgressPersistence: LocalStoragePlayerProgressPersistence(), + inAppPurchaseController: inAppPurchaseController, + adsController: adsController, + gamesServicesController: gamesServicesController, + ), + ); +} + +Logger _log = Logger('main.dart'); + +class MyApp extends StatelessWidget { + static final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => + const MainMenuScreen(key: Key('main menu')), + routes: [ + GoRoute( + path: 'play', + pageBuilder: (context, state) => buildMyTransition( + child: const LevelSelectionScreen( + key: Key('level selection')), + color: context.watch().backgroundLevelSelection, + ), + routes: [ + GoRoute( + path: 'session/:level', + pageBuilder: (context, state) { + final levelNumber = int.parse(state.params['level']!); + final level = gameLevels + .singleWhere((e) => e.number == levelNumber); + return buildMyTransition( + child: PlaySessionScreen( + level, + key: const Key('play session'), + ), + color: context.watch().backgroundPlaySession, + ); + }, + ), + GoRoute( + path: 'won', + pageBuilder: (context, state) { + final map = state.extra! as Map; + final score = map['score'] as Score; + + return buildMyTransition( + child: WinGameScreen( + score: score, + key: const Key('win game'), + ), + color: context.watch().backgroundPlaySession, + ); + }, + ) + ]), + GoRoute( + path: 'settings', + builder: (context, state) => + const SettingsScreen(key: Key('settings')), + ), + ]), + ], + ); + + final PlayerProgressPersistence playerProgressPersistence; + + final SettingsPersistence settingsPersistence; + + final GamesServicesController? gamesServicesController; + + final InAppPurchaseController? inAppPurchaseController; + + final AdsController? adsController; + + const MyApp({ + required this.playerProgressPersistence, + required this.settingsPersistence, + required this.inAppPurchaseController, + required this.adsController, + required this.gamesServicesController, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppLifecycleObserver( + child: MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) { + var progress = PlayerProgress(playerProgressPersistence); + progress.getLatestFromStore(); + return progress; + }, + ), + Provider.value( + value: gamesServicesController), + Provider.value(value: adsController), + ChangeNotifierProvider.value( + value: inAppPurchaseController), + Provider( + lazy: false, + create: (context) => SettingsController( + persistence: settingsPersistence, + )..loadStateFromPersistence(), + ), + ProxyProvider2, + AudioController>( + // Ensures that the AudioController is created on startup, + // and not "only when it's needed", as is default behavior. + // This way, music starts immediately. + lazy: false, + create: (context) => AudioController()..initialize(), + update: (context, settings, lifecycleNotifier, audio) { + if (audio == null) throw ArgumentError.notNull(); + audio.attachSettings(settings); + audio.attachLifecycleNotifier(lifecycleNotifier); + return audio; + }, + dispose: (context, audio) => audio.dispose(), + ), + Provider( + create: (context) => Palette(), + ), + ], + child: Builder(builder: (context) { + final palette = context.watch(); + + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData.from( + colorScheme: ColorScheme.fromSeed( + seedColor: palette.darkPen, + background: palette.backgroundMain, + ), + textTheme: TextTheme( + bodyText2: TextStyle( + color: palette.ink, + ), + ), + ), + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + scaffoldMessengerKey: scaffoldMessengerKey, + ); + }), + ), + ); + } +} diff --git a/game_template/lib/src/ads/ads_controller.dart b/game_template/lib/src/ads/ads_controller.dart new file mode 100644 index 000000000..eb9c44c6e --- /dev/null +++ b/game_template/lib/src/ads/ads_controller.dart @@ -0,0 +1,62 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:google_mobile_ads/google_mobile_ads.dart'; + +import 'preloaded_banner_ad.dart'; + +/// Allows showing ads. A facade for `package:google_mobile_ads`. +class AdsController { + final MobileAds _instance; + + PreloadedBannerAd? _preloadedAd; + + /// Creates an [AdsController] that wraps around a [MobileAds] [instance]. + /// + /// Example usage: + /// + /// var controller = AdsController(MobileAds.instance); + AdsController(MobileAds instance) : _instance = instance; + + void dispose() { + _preloadedAd?.dispose(); + } + + /// Initializes the injected [MobileAds.instance]. + Future initialize() async { + await _instance.initialize(); + } + + /// Starts preloading an ad to be used later. + /// + /// The work doesn't start immediately so that calling this doesn't have + /// adverse effects (jank) during start of a new screen. + void preloadAd() { + // TODO: When ready, change this to the Ad Unit IDs provided by AdMob. + // The current values are AdMob's sample IDs. + final adUnitId = defaultTargetPlatform == TargetPlatform.android + ? 'ca-app-pub-3940256099942544/6300978111' + // iOS + : 'ca-app-pub-3940256099942544/2934735716'; + _preloadedAd = + PreloadedBannerAd(size: AdSize.mediumRectangle, adUnitId: adUnitId); + + // Wait a bit so that calling at start of a new screen doesn't have + // adverse effects on performance. + Future.delayed(const Duration(seconds: 1)).then((_) { + return _preloadedAd!.load(); + }); + } + + /// Allows caller to take ownership of a [PreloadedBannerAd]. + /// + /// If this method returns a non-null value, then the caller is responsible + /// for disposing of the loaded ad. + PreloadedBannerAd? takePreloadedAd() { + final ad = _preloadedAd; + _preloadedAd = null; + return ad; + } +} diff --git a/game_template/lib/src/ads/banner_ad_widget.dart b/game_template/lib/src/ads/banner_ad_widget.dart new file mode 100644 index 000000000..39301c7d5 --- /dev/null +++ b/game_template/lib/src/ads/banner_ad_widget.dart @@ -0,0 +1,205 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:google_mobile_ads/google_mobile_ads.dart'; +import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; + +import 'ads_controller.dart'; +import 'preloaded_banner_ad.dart'; + +/// Displays a banner ad that conforms to the widget's size in the layout, +/// and reloads the ad when the user changes orientation. +/// +/// Do not use this widget on platforms that AdMob currently doesn't support. +/// For example: +/// +/// ```dart +/// if (kIsWeb) { +/// return Text('No ads here! (Yet.)'); +/// } else { +/// return MyBannerAd(); +/// } +/// ``` +/// +/// This widget is adapted from pkg:google_mobile_ads's example code, +/// namely the `anchored_adaptive_example.dart` file: +/// https://github.com/googleads/googleads-mobile-flutter/blob/main/packages/google_mobile_ads/example/lib/anchored_adaptive_example.dart +class BannerAdWidget extends StatefulWidget { + const BannerAdWidget({Key? key}) : super(key: key); + + @override + _BannerAdWidgetState createState() => _BannerAdWidgetState(); +} + +class _BannerAdWidgetState extends State { + static final _log = Logger('BannerAdWidget'); + + static const useAnchoredAdaptiveSize = false; + BannerAd? _bannerAd; + _LoadingState _adLoadingState = _LoadingState.initial; + + late Orientation _currentOrientation; + + @override + Widget build(BuildContext context) { + return OrientationBuilder( + builder: (context, orientation) { + if (_currentOrientation == orientation && + _bannerAd != null && + _adLoadingState == _LoadingState.loaded) { + _log.info(() => 'We have everything we need. Showing the ad ' + '${_bannerAd.hashCode} now.'); + return SizedBox( + width: _bannerAd!.size.width.toDouble(), + height: _bannerAd!.size.height.toDouble(), + child: AdWidget(ad: _bannerAd!), + ); + } + // Reload the ad if the orientation changes. + if (_currentOrientation != orientation) { + _log.info('Orientation changed'); + _currentOrientation = orientation; + _loadAd(); + } + return const SizedBox(); + }, + ); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _currentOrientation = MediaQuery.of(context).orientation; + } + + @override + void dispose() { + _log.info('disposing ad'); + _bannerAd?.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + + final adsController = context.read(); + final ad = adsController.takePreloadedAd(); + if (ad != null) { + _log.info("A preloaded banner was supplied. Using it."); + _showPreloadedAd(ad); + } else { + _loadAd(); + } + } + + /// Load (another) ad, disposing of the current ad if there is one. + Future _loadAd() async { + if (!mounted) return; + _log.info('_loadAd() called.'); + if (_adLoadingState == _LoadingState.loading || + _adLoadingState == _LoadingState.disposing) { + _log.info('An ad is already being loaded or disposed. Aborting.'); + return; + } + _adLoadingState = _LoadingState.disposing; + await _bannerAd?.dispose(); + _log.fine('_bannerAd disposed'); + setState(() { + _bannerAd = null; + _adLoadingState = _LoadingState.loading; + }); + + AdSize size; + + if (useAnchoredAdaptiveSize) { + final AnchoredAdaptiveBannerAdSize? adaptiveSize = + await AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize( + MediaQuery.of(context).size.width.truncate()); + + if (adaptiveSize == null) { + _log.warning('Unable to get height of anchored banner.'); + size = AdSize.banner; + } else { + size = adaptiveSize; + } + } else { + size = AdSize.mediumRectangle; + } + + assert(Platform.isAndroid || Platform.isIOS, + 'AdMob currently does not support ${Platform.operatingSystem}'); + _bannerAd = BannerAd( + // This is a test ad unit ID from + // https://developers.google.com/admob/android/test-ads. When ready, + // you replace this with your own, production ad unit ID, + // created in https://apps.admob.com/. + adUnitId: Theme.of(context).platform == TargetPlatform.android + ? 'ca-app-pub-3940256099942544/6300978111' + : 'ca-app-pub-3940256099942544/2934735716', + size: size, + request: const AdRequest(), + listener: BannerAdListener( + onAdLoaded: (ad) { + _log.info(() => 'Ad loaded: ${ad.responseInfo}'); + setState(() { + // When the ad is loaded, get the ad size and use it to set + // the height of the ad container. + _bannerAd = ad as BannerAd; + _adLoadingState = _LoadingState.loaded; + }); + }, + onAdFailedToLoad: (ad, error) { + _log.warning('Banner failedToLoad: $error'); + ad.dispose(); + }, + onAdImpression: (ad) { + _log.info('Ad impression registered'); + }, + onAdClicked: (ad) { + _log.info('Ad click registered'); + }, + ), + ); + return _bannerAd!.load(); + } + + Future _showPreloadedAd(PreloadedBannerAd ad) async { + // It's possible that the banner is still loading (even though it started + // preloading at the start of the previous screen). + _adLoadingState = _LoadingState.loading; + try { + _bannerAd = await ad.ready; + } on LoadAdError catch (error) { + _log.severe('Error when loading preloaded banner: $error'); + unawaited(_loadAd()); + return; + } + if (!mounted) return; + + setState(() { + _adLoadingState = _LoadingState.loaded; + }); + } +} + +enum _LoadingState { + /// The state before we even start loading anything. + initial, + + /// The ad is being loaded at this point. + loading, + + /// The previous ad is being disposed of. After that is done, the next + /// ad will be loaded. + disposing, + + /// An ad has been loaded already. + loaded, +} diff --git a/game_template/lib/src/ads/preloaded_banner_ad.dart b/game_template/lib/src/ads/preloaded_banner_ad.dart new file mode 100644 index 000000000..ac4b25df1 --- /dev/null +++ b/game_template/lib/src/ads/preloaded_banner_ad.dart @@ -0,0 +1,71 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'package:google_mobile_ads/google_mobile_ads.dart'; +import 'package:logging/logging.dart'; + +class PreloadedBannerAd { + static final _log = Logger('PreloadedBannerAd'); + + /// Something like [AdSize.mediumRectangle]. + final AdSize size; + + final AdRequest _adRequest; + + BannerAd? _bannerAd; + + final String adUnitId; + + final _adCompleter = Completer(); + + PreloadedBannerAd({ + required this.size, + required this.adUnitId, + AdRequest? adRequest, + }) : _adRequest = adRequest ?? const AdRequest(); + + Future get ready => _adCompleter.future; + + Future load() { + assert(Platform.isAndroid || Platform.isIOS, + 'AdMob currently does not support ${Platform.operatingSystem}'); + + _bannerAd = BannerAd( + // This is a test ad unit ID from + // https://developers.google.com/admob/android/test-ads. When ready, + // you replace this with your own, production ad unit ID, + // created in https://apps.admob.com/. + adUnitId: adUnitId, + size: size, + request: _adRequest, + listener: BannerAdListener( + onAdLoaded: (ad) { + _log.info(() => 'Ad loaded: ${_bannerAd.hashCode}'); + _adCompleter.complete(_bannerAd); + }, + onAdFailedToLoad: (ad, error) { + _log.warning('Banner failedToLoad: $error'); + _adCompleter.completeError(error); + ad.dispose(); + }, + onAdImpression: (ad) { + _log.info('Ad impression registered'); + }, + onAdClicked: (ad) { + _log.info('Ad click registered'); + }, + ), + ); + + return _bannerAd!.load(); + } + + void dispose() { + _log.info('preloaded banner ad being disposed'); + _bannerAd?.dispose(); + } +} diff --git a/game_template/lib/src/app_lifecycle/app_lifecycle.dart b/game_template/lib/src/app_lifecycle/app_lifecycle.dart new file mode 100644 index 000000000..90b93d949 --- /dev/null +++ b/game_template/lib/src/app_lifecycle/app_lifecycle.dart @@ -0,0 +1,63 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; + +class AppLifecycleObserver extends StatefulWidget { + final Widget child; + + const AppLifecycleObserver({required this.child, Key? key}) : super(key: key); + + @override + _AppLifecycleObserverState createState() => _AppLifecycleObserverState(); +} + +class _AppLifecycleObserverState extends State + with WidgetsBindingObserver { + static final _log = Logger('AppLifecycleObserver'); + + final ValueNotifier lifecycleListenable = + ValueNotifier(AppLifecycleState.inactive); + + @override + Widget build(BuildContext context) { + // Using InheritedProvider because we don't want to use Consumer + // or context.watch or anything like that to listen to this. We want + // to manually add listeners. We're interested in the _events_ of lifecycle + // state changes, and not so much in the state itself. (For example, + // we want to stop sound when the app goes into the background, and + // restart sound again when the app goes back into focus. We're not + // rebuilding any widgets.) + // + // Provider, by default, throws when one + // is trying to provide a Listenable (such as ValueNotifier) without using + // something like ValueListenableProvider. InheritedProvider is more + // low-level and doesn't have this problem. + return InheritedProvider>.value( + value: lifecycleListenable, + child: widget.child, + ); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + _log.info(() => 'didChangeAppLifecycleState: $state'); + lifecycleListenable.value = state; + } + + @override + void dispose() { + WidgetsBinding.instance!.removeObserver(this); + super.dispose(); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance!.addObserver(this); + _log.info('Subscribed to app lifecycle updates'); + } +} diff --git a/game_template/lib/src/audio/audio_controller.dart b/game_template/lib/src/audio/audio_controller.dart new file mode 100644 index 000000000..8cfd6cf3a --- /dev/null +++ b/game_template/lib/src/audio/audio_controller.dart @@ -0,0 +1,271 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:collection'; +import 'dart:math'; + +import 'package:audioplayers/audioplayers.dart' hide Logger; +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; + +import '../settings/settings.dart'; +import 'songs.dart'; +import 'sounds.dart'; + +/// Allows playing music and sound. A facade to `package:audioplayers`. +class AudioController { + static final _log = Logger('AudioController'); + + late AudioCache _musicCache; + + late AudioCache _sfxCache; + + final AudioPlayer _musicPlayer; + + /// This is a list of [AudioPlayer] instances which are rotated to play + /// sound effects. + /// + /// Normally, we would just call [AudioCache.play] and let it procure its + /// own [AudioPlayer] every time. But this seems to lead to errors and + /// bad performance on iOS devices. + final List _sfxPlayers; + + int _currentSfxPlayer = 0; + + final Queue _playlist; + + final Random _random = Random(); + + SettingsController? _settings; + + ValueNotifier? _lifecycleNotifier; + + /// Creates an instance that plays music and sound. + /// + /// Use [polyphony] to configure the number of sound effects (SFX) that can + /// play at the same time. A [polyphony] of `1` will always only play one + /// sound (a new sound will stop the previous one). See discussion + /// of [_sfxPlayers] to learn why this is the case. + /// + /// Background music does not count into the [polyphony] limit. Music will + /// never be overridden by sound effects. + AudioController({int polyphony = 2}) + : assert(polyphony >= 1), + _musicPlayer = AudioPlayer(playerId: 'musicPlayer'), + _sfxPlayers = Iterable.generate( + polyphony, + (i) => AudioPlayer( + playerId: 'sfxPlayer#$i', + mode: PlayerMode.LOW_LATENCY)).toList(growable: false), + _playlist = Queue.of(List.of(songs)..shuffle()) { + _musicCache = AudioCache( + fixedPlayer: _musicPlayer, + prefix: 'assets/music/', + ); + _sfxCache = AudioCache( + fixedPlayer: _sfxPlayers.first, + prefix: 'assets/sfx/', + ); + + _musicPlayer.onPlayerCompletion.listen(_changeSong); + } + + /// Enables the [AudioController] to listen to [AppLifecycleState] events, + /// and therefore do things like stopping playback when the game + /// goes into the background. + void attachLifecycleNotifier( + ValueNotifier lifecycleNotifier) { + _lifecycleNotifier?.removeListener(_handleAppLifecycle); + + lifecycleNotifier.addListener(_handleAppLifecycle); + _lifecycleNotifier = lifecycleNotifier; + } + + /// Enables the [AudioController] to track changes to settings. + /// Namely, when any of [SettingsController.muted], + /// [SettingsController.musicOn] or [SettingsController.soundsOn] changes, + /// the audio controller will act accordingly. + void attachSettings(SettingsController settingsController) { + if (_settings == settingsController) { + // Already attached to this instance. Nothing to do. + return; + } + + // Remove handlers from the old settings controller if present + final oldSettings = _settings; + if (oldSettings != null) { + oldSettings.muted.removeListener(_mutedHandler); + oldSettings.musicOn.removeListener(_musicOnHandler); + oldSettings.soundsOn.removeListener(_soundsOnHandler); + } + + _settings = settingsController; + + // Add handlers to the new settings controller + settingsController.muted.addListener(_mutedHandler); + settingsController.musicOn.addListener(_musicOnHandler); + settingsController.soundsOn.addListener(_soundsOnHandler); + + if (!settingsController.muted.value && settingsController.musicOn.value) { + _startMusic(); + } + } + + void dispose() { + _lifecycleNotifier?.removeListener(_handleAppLifecycle); + _stopAllSound(); + _musicPlayer.dispose(); + for (final player in _sfxPlayers) { + player.dispose(); + } + } + + /// Preloads all sound effects. + Future initialize() async { + _log.info('Preloading sound effects'); + // This assumes there is only a limited number of sound effects in the game. + // If there are hundreds of long sound effect files, it's better + // to be more selective when preloading. + await _sfxCache + .loadAll(SfxType.values.expand(soundTypeToFilename).toList()); + } + + /// Plays a single sound effect, defined by [type]. + /// + /// The controller will ignore this call when the attached settings' + /// [SettingsController.muted] is `true` or if its + /// [SettingsController.soundsOn] is `false`. + void playSfx(SfxType type) { + final muted = _settings?.muted.value ?? true; + if (muted) { + _log.info(() => 'Ignoring playing sound ($type) because audio is muted.'); + return; + } + final soundsOn = _settings?.soundsOn.value ?? false; + if (!soundsOn) { + _log.info(() => + 'Ignoring playing sound ($type) because sounds are turned off.'); + return; + } + + _log.info(() => 'Playing sound: $type'); + final options = soundTypeToFilename(type); + final filename = options[_random.nextInt(options.length)]; + _log.info(() => '- Chosen filename: $filename'); + _sfxCache.play(filename, volume: soundTypeToVolume(type)); + _currentSfxPlayer = (_currentSfxPlayer + 1) % _sfxPlayers.length; + _sfxCache.fixedPlayer = _sfxPlayers[_currentSfxPlayer]; + } + + void _changeSong(void _) { + _log.info('Last song finished playing.'); + // Put the song that just finished playing to the end of the playlist. + _playlist.addLast(_playlist.removeFirst()); + // Play the next song. + _log.info(() => 'Playing ${_playlist.first} now.'); + _musicCache.play(_playlist.first.filename); + } + + void _handleAppLifecycle() { + switch (_lifecycleNotifier!.value) { + case AppLifecycleState.paused: + case AppLifecycleState.detached: + _stopAllSound(); + break; + case AppLifecycleState.resumed: + if (!_settings!.muted.value && _settings!.musicOn.value) { + _resumeMusic(); + } + break; + case AppLifecycleState.inactive: + // No need to react to this state change. + break; + } + } + + void _musicOnHandler() { + if (_settings!.musicOn.value) { + // Music got turned on. + if (!_settings!.muted.value) { + _resumeMusic(); + } + } else { + // Music got turned off. + _stopMusic(); + } + } + + void _mutedHandler() { + if (_settings!.muted.value) { + // All sound just got muted. + _stopAllSound(); + } else { + // All sound just got un-muted. + if (_settings!.musicOn.value) { + _resumeMusic(); + } + } + } + + Future _resumeMusic() async { + _log.info('Resuming music'); + switch (_musicPlayer.state) { + case PlayerState.PAUSED: + _log.info('Calling _musicPlayer.resume()'); + try { + await _musicPlayer.resume(); + } catch (e) { + // Sometimes, resuming fails with an "Unexpected" error. + _log.severe(e); + await _musicCache.play(_playlist.first.filename); + } + break; + case PlayerState.STOPPED: + _log.info("resumeMusic() called when music is stopped. " + "This probably means we haven't yet started the music. " + "For example, the game was started with sound off."); + await _musicCache.play(_playlist.first.filename); + break; + case PlayerState.PLAYING: + _log.warning('resumeMusic() called when music is playing. ' + 'Nothing to do.'); + break; + case PlayerState.COMPLETED: + _log.warning('resumeMusic() called when music is completed. ' + "Music should never be 'completed' as it's either not playing " + "or looping forever."); + await _musicCache.play(_playlist.first.filename); + break; + } + } + + void _soundsOnHandler() { + for (final player in _sfxPlayers) { + if (player.state == PlayerState.PLAYING) { + player.stop(); + } + } + } + + void _startMusic() { + _log.info('starting music'); + _musicCache.play(_playlist.first.filename); + } + + void _stopAllSound() { + if (_musicPlayer.state == PlayerState.PLAYING) { + _musicPlayer.pause(); + } + for (final player in _sfxPlayers) { + player.stop(); + } + } + + void _stopMusic() { + _log.info('Stopping music'); + if (_musicPlayer.state == PlayerState.PLAYING) { + _musicPlayer.pause(); + } + } +} diff --git a/game_template/lib/src/audio/songs.dart b/game_template/lib/src/audio/songs.dart new file mode 100644 index 000000000..dd542bd1b --- /dev/null +++ b/game_template/lib/src/audio/songs.dart @@ -0,0 +1,24 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +const Set songs = { + // Filenames with whitespace break package:audioplayers on iOS + // (as of February 2022), so we use no whitespace. + Song('Mr_Smith-Azul.mp3', 'Azul', artist: 'Mr Smith'), + Song('Mr_Smith-Sonorus.mp3', 'Sonorus', artist: 'Mr Smith'), + Song('Mr_Smith-Sunday_Solitude.mp3', 'SundaySolitude', artist: 'Mr Smith'), +}; + +class Song { + final String filename; + + final String name; + + final String? artist; + + const Song(this.filename, this.name, {this.artist}); + + @override + String toString() => 'Song<$filename>'; +} diff --git a/game_template/lib/src/audio/sounds.dart b/game_template/lib/src/audio/sounds.dart new file mode 100644 index 000000000..139ac2132 --- /dev/null +++ b/game_template/lib/src/audio/sounds.dart @@ -0,0 +1,71 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +List soundTypeToFilename(SfxType type) { + switch (type) { + case SfxType.huhsh: + return const [ + 'hash1.mp3', + 'hash2.mp3', + 'hash3.mp3', + ]; + case SfxType.wssh: + return const [ + 'wssh1.mp3', + 'wssh2.mp3', + 'dsht1.mp3', + 'ws1.mp3', + 'spsh1.mp3', + 'hh1.mp3', + 'hh2.mp3', + 'kss1.mp3', + ]; + case SfxType.buttonTap: + return const [ + 'k1.mp3', + 'k2.mp3', + 'p1.mp3', + 'p2.mp3', + ]; + case SfxType.congrats: + return const [ + 'yay1.mp3', + 'wehee1.mp3', + 'oo1.mp3', + ]; + case SfxType.erase: + return const [ + 'fwfwfwfwfw1.mp3', + 'fwfwfwfw1.mp3', + ]; + case SfxType.swishSwish: + return const [ + 'swishswish1.mp3', + ]; + } +} + +/// Allows control over loudness of different SFX types. +double soundTypeToVolume(SfxType type) { + switch (type) { + case SfxType.huhsh: + return 0.4; + case SfxType.wssh: + return 0.2; + case SfxType.buttonTap: + case SfxType.congrats: + case SfxType.erase: + case SfxType.swishSwish: + return 1.0; + } +} + +enum SfxType { + huhsh, + wssh, + buttonTap, + congrats, + erase, + swishSwish, +} diff --git a/game_template/lib/src/crashlytics/crashlytics.dart b/game_template/lib/src/crashlytics/crashlytics.dart new file mode 100644 index 000000000..9fb5dd1b5 --- /dev/null +++ b/game_template/lib/src/crashlytics/crashlytics.dart @@ -0,0 +1,103 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:isolate'; + +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; + +/// Runs [mainFunction] in a guarded [Zone]. +/// +/// If a non-null [FirebaseCrashlytics] instance is provided through +/// [crashlytics], then all errors will be reported through it. +/// +/// These errors will also include latest logs from anywhere in the app +/// that use `package:logging`. +Future guardWithCrashlytics( + void Function() mainFunction, { + required FirebaseCrashlytics? crashlytics, +}) async { + // Running the initialization code and [mainFunction] inside a guarded + // zone, so that all errors (even those occurring in callbacks) are + // caught and can be sent to Crashlytics. + await runZonedGuarded>(() async { + if (kDebugMode) { + // Log more when in debug mode. + Logger.root.level = Level.FINE; + } + // Subscribe to log messages. + Logger.root.onRecord.listen((record) { + final message = '${record.level.name}: ${record.time}: ' + '${record.loggerName}: ' + '${record.message}'; + + debugPrint(message); + // Add the message to the rotating Crashlytics log. + crashlytics?.log(message); + + if (record.level >= Level.SEVERE) { + crashlytics?.recordError(message, filterStackTrace(StackTrace.current)); + } + }); + + // Pass all uncaught errors from the framework to Crashlytics. + if (crashlytics != null) { + WidgetsFlutterBinding.ensureInitialized(); + FlutterError.onError = crashlytics.recordFlutterError; + } + + if (!kIsWeb) { + // To catch errors outside of the Flutter context, we attach an error + // listener to the current isolate. + Isolate.current.addErrorListener(RawReceivePort((dynamic pair) async { + final errorAndStacktrace = pair as List; + await crashlytics?.recordError( + errorAndStacktrace.first, + errorAndStacktrace.last as StackTrace?, + ); + }).sendPort); + } + + // Run the actual code. + mainFunction(); + }, (error, stack) { + // This sees all errors that occur in the runZonedGuarded zone. + debugPrint('ERROR: $error\n\n' + 'STACK:$stack'); + crashlytics?.recordError(error, stack); + }); +} + +/// Takes a [stackTrace] and creates a new one, but without the lines that +/// have to do with this file and logging. This way, Crashlytics won't group +/// all messages that come from this file into one big heap just because +/// the head of the StackTrace is identical. +/// +/// See this: +/// https://stackoverflow.com/questions/47654410/how-to-effectively-group-non-fatal-exceptions-in-crashlytics-fabrics. +@visibleForTesting +StackTrace filterStackTrace(StackTrace stackTrace) { + try { + final lines = stackTrace.toString().split('\n'); + final buf = StringBuffer(); + for (final line in lines) { + if (line.contains('crashlytics.dart') || + line.contains('_BroadcastStreamController.java') || + line.contains('logger.dart')) { + continue; + } + buf.writeln(line); + } + return StackTrace.fromString(buf.toString()); + } catch (e) { + debugPrint('Problem while filtering stack trace: $e'); + } + + // If there was an error while filtering, + // return the original, unfiltered stack track. + return stackTrace; +} diff --git a/game_template/lib/src/game_internals/level_state.dart b/game_template/lib/src/game_internals/level_state.dart new file mode 100644 index 000000000..0d765f001 --- /dev/null +++ b/game_template/lib/src/game_internals/level_state.dart @@ -0,0 +1,32 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +/// An extremely silly example of a game state. +/// +/// Tracks only a single variable, [progress], and calls [onWin] when +/// the value of [progress] reaches [goal]. +class LevelState extends ChangeNotifier { + final VoidCallback onWin; + + final int goal; + + LevelState({required this.onWin, this.goal = 100}); + + int _progress = 0; + + int get progress => _progress; + + void setProgress(int value) { + _progress = value; + notifyListeners(); + } + + void evaluate() { + if (_progress >= goal) { + onWin(); + } + } +} diff --git a/game_template/lib/src/games_services/games_services.dart b/game_template/lib/src/games_services/games_services.dart new file mode 100644 index 000000000..b4b10eb98 --- /dev/null +++ b/game_template/lib/src/games_services/games_services.dart @@ -0,0 +1,119 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:games_services/games_services.dart' as gs; +import 'package:logging/logging.dart'; + +import 'score.dart'; + +/// Allows awarding achievements and leaderboard scores, +/// and also showing the platforms' UI overlays for achievements +/// and leaderboards. +/// +/// A facade of `package:games_services`. +class GamesServicesController { + static final Logger _log = Logger('GamesServicesController'); + + final Completer _signedInCompleter = Completer(); + + Future get signedIn => _signedInCompleter.future; + + /// Unlocks an achievement on Game Center / Play Games. + /// + /// You must provide the achievement ids via the [iOS] and [android] + /// parameters. + /// + /// Does nothing when the game isn't signed into the underlying + /// games service. + Future awardAchievement( + {required String iOS, required String android}) async { + if (!await signedIn) { + _log.warning('Trying to award achievement when not logged in.'); + return; + } + + try { + await gs.GamesServices.unlock( + achievement: gs.Achievement( + androidID: android, + iOSID: iOS, + ), + ); + } catch (e) { + _log.severe('Cannot award achievement: $e'); + } + } + + /// Signs into the underlying games service. + Future initialize() async { + try { + await gs.GamesServices.signIn(); + // The API is unclear so we're checking to be sure. The above call + // returns a String, not a boolean, and there's no documentation + // as to whether every non-error result means we're safely signed in. + final signedIn = await gs.GamesServices.isSignedIn; + _signedInCompleter.complete(signedIn); + } catch (e) { + _log.severe('Cannot log into GamesServices: $e'); + _signedInCompleter.complete(false); + } + } + + /// Launches the platform's UI overlay with achievements. + Future showAchievements() async { + if (!await signedIn) { + _log.severe('Trying to show achievements when not logged in.'); + return; + } + + try { + await gs.GamesServices.showAchievements(); + } catch (e) { + _log.severe('Cannot show achievements: $e'); + } + } + + /// Launches the platform's UI overlay with leaderboard(s). + Future showLeaderboard() async { + if (!await signedIn) { + _log.severe('Trying to show leaderboard when not logged in.'); + return; + } + + try { + await gs.GamesServices.showLeaderboards( + // TODO: When ready, change both these leaderboard IDs. + iOSLeaderboardID: "some_id_from_app_store", + androidLeaderboardID: "sOmE_iD_fRoM_gPlAy", + ); + } catch (e) { + _log.severe('Cannot show leaderboard: $e'); + } + } + + /// Submits [score] to the leaderboard. + Future submitLeaderboardScore(Score score) async { + if (!await signedIn) { + _log.warning('Trying to submit leaderboard when not logged in.'); + return; + } + + _log.info('Submitting $score to leaderboard.'); + + try { + await gs.GamesServices.submitScore( + score: gs.Score( + // TODO: When ready, change these leaderboard IDs. + iOSLeaderboardID: 'some_id_from_app_store', + androidLeaderboardID: 'sOmE_iD_fRoM_gPlAy', + value: score.score, + ), + ); + } catch (e) { + _log.severe('Cannot submit leaderboard score: $e'); + } + } +} diff --git a/game_template/lib/src/games_services/score.dart b/game_template/lib/src/games_services/score.dart new file mode 100644 index 000000000..c4ecef561 --- /dev/null +++ b/game_template/lib/src/games_services/score.dart @@ -0,0 +1,48 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +/// Encapsulates a score and the arithmetic to compute it. +@immutable +class Score { + final int score; + + final Duration duration; + + final int level; + + factory Score(int level, int difficulty, Duration duration) { + // The higher the difficulty, the higher the score. + var score = difficulty; + // The lower the time to beat the level, the higher the score. + score *= 10000 ~/ (duration.inSeconds.abs() + 1); + return Score._(score, duration, level); + } + + const Score._(this.score, this.duration, this.level); + + String get formattedTime { + final buf = StringBuffer(); + if (duration.inHours > 0) { + buf.write('${duration.inHours}'); + buf.write(':'); + } + final minutes = duration.inMinutes % Duration.minutesPerHour; + if (minutes > 9) { + buf.write('$minutes'); + } else { + buf.write('0'); + buf.write('$minutes'); + } + buf.write(':'); + buf.write((duration.inSeconds % Duration.secondsPerMinute) + .toString() + .padLeft(2, '0')); + return buf.toString(); + } + + @override + String toString() => 'Score<$score,$formattedTime,$level>'; +} diff --git a/game_template/lib/src/in_app_purchase/ad_removal.dart b/game_template/lib/src/in_app_purchase/ad_removal.dart new file mode 100644 index 000000000..5148123c6 --- /dev/null +++ b/game_template/lib/src/in_app_purchase/ad_removal.dart @@ -0,0 +1,41 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Represents the state of an in-app purchase of ad removal such as +/// [AdRemovalPurchase.notStarted()] or [AdRemovalPurchase.active()]. +class AdRemovalPurchase { + /// The representation of this product on the stores. + static const productId = 'remove_ads'; + + /// This is `true` if the `remove_ad` product has been purchased and verified. + /// Do not show ads if so. + final bool active; + + /// This is `true` when the purchase is pending. + final bool pending; + + /// If there was an error with the purchase, this field will contain + /// that error. + final Object? error; + + const AdRemovalPurchase.active() : this._(true, false, null); + + const AdRemovalPurchase.error(Object error) : this._(false, false, error); + + const AdRemovalPurchase.notStarted() : this._(false, false, null); + + const AdRemovalPurchase.pending() : this._(false, true, null); + + const AdRemovalPurchase._(this.active, this.pending, this.error); + + @override + int get hashCode => Object.hash(active, pending, error); + + @override + bool operator ==(Object other) => + other is AdRemovalPurchase && + other.active == active && + other.pending == pending && + other.error == error; +} diff --git a/game_template/lib/src/in_app_purchase/in_app_purchase.dart b/game_template/lib/src/in_app_purchase/in_app_purchase.dart new file mode 100644 index 000000000..b2c86fcb4 --- /dev/null +++ b/game_template/lib/src/in_app_purchase/in_app_purchase.dart @@ -0,0 +1,193 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:logging/logging.dart'; + +import '../style/snack_bar.dart'; +import 'ad_removal.dart'; + +/// Allows buying in-app. Facade of `package:in_app_purchase`. +class InAppPurchaseController extends ChangeNotifier { + static final Logger _log = Logger('InAppPurchases'); + + StreamSubscription>? _subscription; + + InAppPurchase inAppPurchaseInstance; + + AdRemovalPurchase _adRemoval = const AdRemovalPurchase.notStarted(); + + /// Creates a new [InAppPurchaseController] with an injected + /// [InAppPurchase] instance. + /// + /// Example usage: + /// + /// var controller = InAppPurchaseController(InAppPurchase.instance); + InAppPurchaseController(this.inAppPurchaseInstance); + + /// The current state of the ad removal purchase. + AdRemovalPurchase get adRemoval => _adRemoval; + + /// Launches the platform UI for buying an in-app purchase. + /// + /// Currently, the only supported in-app purchase is ad removal. + /// To support more, ad additional classes similar to [AdRemovalPurchase] + /// and modify this method. + Future buy() async { + if (!await inAppPurchaseInstance.isAvailable()) { + _reportError('InAppPurchase.instance not available'); + return; + } + + _adRemoval = const AdRemovalPurchase.pending(); + notifyListeners(); + + _log.info('Querying the store with queryProductDetails()'); + final response = await inAppPurchaseInstance + .queryProductDetails({AdRemovalPurchase.productId}); + + if (response.error != null) { + _reportError('There was an error when making the purchase: ' + '${response.error}'); + return; + } + + if (response.productDetails.length != 1) { + _log.info( + 'Products in response: ' + '${response.productDetails.map((e) => '${e.id}: ${e.title}, ').join()}', + ); + _reportError('There was an error when making the purchase: ' + 'product ${AdRemovalPurchase.productId} does not exist?'); + return; + } + final productDetails = response.productDetails.single; + + _log.info('Making the purchase'); + final purchaseParam = PurchaseParam(productDetails: productDetails); + try { + final success = await inAppPurchaseInstance.buyNonConsumable( + purchaseParam: purchaseParam); + _log.info('buyNonConsumable() request was sent with success: $success'); + // The result of the purchase will be reported in the purchaseStream, + // which is handled in [_listenToPurchaseUpdated]. + } catch (e) { + _log.severe( + 'Problem with calling inAppPurchaseInstance.buyNonConsumable(): ' + '$e'); + } + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } + + /// Asks the underlying platform to list purchases that have been already + /// made (for example, in a previous session of the game). + Future restorePurchases() async { + if (!await inAppPurchaseInstance.isAvailable()) { + _reportError('InAppPurchase.instance not available'); + return; + } + + try { + await inAppPurchaseInstance.restorePurchases(); + } catch (e) { + _log.severe('Could not restore in-app purchases: $e'); + } + _log.info('In-app purchases restored'); + } + + /// Subscribes to the [inAppPurchaseInstance.purchaseStream]. + void subscribe() { + _subscription?.cancel(); + _subscription = + inAppPurchaseInstance.purchaseStream.listen((purchaseDetailsList) { + _listenToPurchaseUpdated(purchaseDetailsList); + }, onDone: () { + _subscription?.cancel(); + }, onError: (dynamic error) { + _log.severe('Error occurred on the purchaseStream: $error'); + }); + } + + Future _listenToPurchaseUpdated( + List purchaseDetailsList) async { + for (final purchaseDetails in purchaseDetailsList) { + _log.info(() => 'New PurchaseDetails instance received: ' + 'productID=${purchaseDetails.productID}, ' + 'status=${purchaseDetails.status}, ' + 'purchaseID=${purchaseDetails.purchaseID}, ' + 'error=${purchaseDetails.error}, ' + 'pendingCompletePurchase=${purchaseDetails.pendingCompletePurchase}'); + + if (purchaseDetails.productID != AdRemovalPurchase.productId) { + _log.severe("The handling of the product with id " + "'${purchaseDetails.productID}' is not implemented."); + _adRemoval = const AdRemovalPurchase.notStarted(); + notifyListeners(); + continue; + } + + switch (purchaseDetails.status) { + case PurchaseStatus.pending: + _adRemoval = const AdRemovalPurchase.pending(); + notifyListeners(); + break; + case PurchaseStatus.purchased: + case PurchaseStatus.restored: + bool valid = await _verifyPurchase(purchaseDetails); + if (valid) { + _adRemoval = const AdRemovalPurchase.active(); + if (purchaseDetails.status == PurchaseStatus.purchased) { + showSnackBar('Thank you for your support!'); + } + notifyListeners(); + } else { + _log.severe('Purchase verification failed: $purchaseDetails'); + _adRemoval = AdRemovalPurchase.error( + StateError('Purchase could not be verified')); + notifyListeners(); + } + break; + case PurchaseStatus.error: + _log.severe('Error with purchase: ${purchaseDetails.error}'); + _adRemoval = AdRemovalPurchase.error(purchaseDetails.error!); + notifyListeners(); + break; + case PurchaseStatus.canceled: + _adRemoval = const AdRemovalPurchase.notStarted(); + notifyListeners(); + break; + } + + if (purchaseDetails.pendingCompletePurchase) { + // Confirm purchase back to the store. + await inAppPurchaseInstance.completePurchase(purchaseDetails); + } + } + } + + void _reportError(String message) { + _log.severe(message); + showSnackBar(message); + _adRemoval = AdRemovalPurchase.error(message); + notifyListeners(); + } + + Future _verifyPurchase(PurchaseDetails purchaseDetails) async { + _log.info('Verifying purchase: ${purchaseDetails.verificationData}'); + // TODO: verify the purchase. + // See the info in [purchaseDetails.verificationData] to learn more. + // There's also a codelab that explains purchase verification + // on the backend: + // https://codelabs.developers.google.com/codelabs/flutter-in-app-purchases#9 + return true; + } +} diff --git a/game_template/lib/src/level_selection/level_selection_screen.dart b/game_template/lib/src/level_selection/level_selection_screen.dart new file mode 100644 index 000000000..2cec06020 --- /dev/null +++ b/game_template/lib/src/level_selection/level_selection_screen.dart @@ -0,0 +1,71 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../audio/audio_controller.dart'; +import '../audio/sounds.dart'; +import '../player_progress/player_progress.dart'; +import '../style/palette.dart'; +import '../style/responsive_screen.dart'; +import 'levels.dart'; + +class LevelSelectionScreen extends StatelessWidget { + const LevelSelectionScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final palette = context.watch(); + final playerProgress = context.watch(); + + return Scaffold( + backgroundColor: palette.backgroundLevelSelection, + body: ResponsiveScreen( + squarishMainArea: Column( + children: [ + const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: Text( + 'Select level', + style: + TextStyle(fontFamily: 'Permanent Marker', fontSize: 30), + ), + ), + ), + const SizedBox(height: 50), + Expanded( + child: ListView( + children: [ + for (final level in gameLevels) + ListTile( + enabled: playerProgress.highestLevelReached >= + level.number - 1, + onTap: () { + final audioController = context.read(); + audioController.playSfx(SfxType.buttonTap); + + GoRouter.of(context) + .go('/play/session/${level.number}'); + }, + leading: Text(level.number.toString()), + title: Text('Level #${level.number}'), + ) + ], + ), + ), + ], + ), + rectangularMenuArea: ElevatedButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('Back'), + ), + ), + ); + } +} diff --git a/game_template/lib/src/level_selection/levels.dart b/game_template/lib/src/level_selection/levels.dart new file mode 100644 index 000000000..c8ef578f2 --- /dev/null +++ b/game_template/lib/src/level_selection/levels.dart @@ -0,0 +1,49 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +const gameLevels = [ + GameLevel( + number: 1, + difficulty: 5, + // TODO: When ready, change these achievement IDs. + // You configure this in App Store Connect. + achievementIdIOS: 'first_win', + // You get this string when you configure an achievement in Play Console. + achievementIdAndroid: 'NhkIwB69ejkMAOOLDb', + ), + GameLevel( + number: 2, + difficulty: 42, + ), + GameLevel( + number: 3, + difficulty: 100, + achievementIdIOS: 'finished', + achievementIdAndroid: 'CdfIhE96aspNWLGSQg', + ), +]; + +class GameLevel { + final int number; + + final int difficulty; + + /// The achievement to unlock when the level is finished, if any. + final String? achievementIdIOS; + + final String? achievementIdAndroid; + + bool get awardsAchievement => achievementIdAndroid != null; + + const GameLevel({ + required this.number, + required this.difficulty, + this.achievementIdIOS, + this.achievementIdAndroid, + }) : assert( + (achievementIdAndroid != null && achievementIdIOS != null) || + (achievementIdAndroid == null && achievementIdIOS == null), + 'Either both iOS and Android achievement ID must be provided, ' + 'or none'); +} diff --git a/game_template/lib/src/main_menu/main_menu_screen.dart b/game_template/lib/src/main_menu/main_menu_screen.dart new file mode 100644 index 000000000..00459eadd --- /dev/null +++ b/game_template/lib/src/main_menu/main_menu_screen.dart @@ -0,0 +1,123 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../audio/audio_controller.dart'; +import '../audio/sounds.dart'; +import '../games_services/games_services.dart'; +import '../settings/settings.dart'; +import '../style/palette.dart'; +import '../style/responsive_screen.dart'; + +class MainMenuScreen extends StatelessWidget { + const MainMenuScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final palette = context.watch(); + final gamesServicesController = context.watch(); + final settingsController = context.watch(); + final audioController = context.watch(); + + return Scaffold( + backgroundColor: palette.backgroundMain, + body: ResponsiveScreen( + mainAreaProminence: 0.45, + squarishMainArea: Center( + child: Transform.rotate( + angle: -0.1, + child: const Text( + 'Flutter Game Template!', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 55, + height: 1, + ), + ), + ), + ), + rectangularMenuArea: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () { + audioController.playSfx(SfxType.buttonTap); + GoRouter.of(context).go('/play'); + }, + child: const Text('Play'), + ), + _gap, + if (gamesServicesController != null) ...[ + _hideUntilReady( + ready: gamesServicesController.signedIn, + child: ElevatedButton( + onPressed: () => gamesServicesController.showAchievements(), + child: const Text('Achievements'), + ), + ), + _gap, + _hideUntilReady( + ready: gamesServicesController.signedIn, + child: ElevatedButton( + onPressed: () => gamesServicesController.showLeaderboard(), + child: const Text('Leaderboard'), + ), + ), + _gap, + ], + ElevatedButton( + onPressed: () => GoRouter.of(context).go('/settings'), + child: const Text('Settings'), + ), + _gap, + Padding( + padding: const EdgeInsets.only(top: 32), + child: ValueListenableBuilder( + valueListenable: settingsController.muted, + builder: (context, muted, child) { + return IconButton( + onPressed: () => settingsController.toggleMuted(), + icon: Icon(muted ? Icons.volume_off : Icons.volume_up), + ); + }, + ), + ), + _gap, + const Text('Music by Mr Smith'), + _gap, + ], + ), + ), + ); + } + + /// Prevents the game from showing game-services-related menu items + /// until we're sure the player is signed in. + /// + /// This normally happens immediately after game start, so players will not + /// see any flash. The exception is folks who decline to use Game Center + /// or Google Play Game Services, or who haven't yet set it up. + Widget _hideUntilReady({required Widget child, required Future ready}) { + return FutureBuilder( + future: ready, + builder: (context, snapshot) { + // Use Visibility here so that we have the space for the buttons + // ready. + return Visibility( + visible: snapshot.data ?? false, + maintainState: true, + maintainSize: true, + maintainAnimation: true, + child: child, + ); + }, + ); + } + + static const _gap = SizedBox(height: 10); +} diff --git a/game_template/lib/src/play_session/play_session_screen.dart b/game_template/lib/src/play_session/play_session_screen.dart new file mode 100644 index 000000000..66bbd9396 --- /dev/null +++ b/game_template/lib/src/play_session/play_session_screen.dart @@ -0,0 +1,180 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart' hide Level; +import 'package:provider/provider.dart'; + +import '../ads/ads_controller.dart'; +import '../audio/audio_controller.dart'; +import '../audio/sounds.dart'; +import '../game_internals/level_state.dart'; +import '../games_services/games_services.dart'; +import '../games_services/score.dart'; +import '../in_app_purchase/in_app_purchase.dart'; +import '../level_selection/levels.dart'; +import '../player_progress/player_progress.dart'; +import '../style/confetti.dart'; +import '../style/palette.dart'; + +class PlaySessionScreen extends StatefulWidget { + final GameLevel level; + + const PlaySessionScreen(this.level, {Key? key}) : super(key: key); + + @override + State createState() => _PlaySessionScreenState(); +} + +class _PlaySessionScreenState extends State { + static final _log = Logger('PlaySessionScreen'); + + static const _celebrationDuration = Duration(milliseconds: 2000); + + static const _preCelebrationDuration = Duration(milliseconds: 500); + + bool _duringCelebration = false; + + late DateTime _startOfPlay; + + @override + Widget build(BuildContext context) { + final palette = context.watch(); + + return MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => LevelState( + goal: widget.level.difficulty, + onWin: _playerWon, + ), + ), + ], + child: IgnorePointer( + ignoring: _duringCelebration, + child: Scaffold( + backgroundColor: palette.backgroundPlaySession, + body: Stack( + children: [ + Center( + // This is the entirety of the "game". + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Align( + alignment: Alignment.centerRight, + child: InkResponse( + onTap: () => GoRouter.of(context).push('/settings'), + child: Image.asset( + 'assets/images/settings.png', + semanticLabel: 'Settings', + ), + ), + ), + const Spacer(), + Text('Drag the slider to ${widget.level.difficulty}%' + ' or above!'), + Consumer( + builder: (context, levelState, child) => Slider( + label: 'Level Progress', + autofocus: true, + value: levelState.progress / 100, + onChanged: (value) => + levelState.setProgress((value * 100).round()), + onChangeEnd: (value) => levelState.evaluate(), + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => GoRouter.of(context).pop(), + child: const Text('Back'), + ), + ), + ), + ], + ), + ), + SizedBox.expand( + child: Visibility( + visible: _duringCelebration, + child: IgnorePointer( + child: Confetti( + isStopped: !_duringCelebration, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + @override + void initState() { + super.initState(); + + _startOfPlay = DateTime.now(); + + // Preload ad for the win screen. + final adsRemoved = + context.read()?.adRemoval.active ?? false; + if (!adsRemoved) { + final adsController = context.read(); + adsController?.preloadAd(); + } + } + + Future _playerWon() async { + _log.info('Level ${widget.level.number} won'); + + final score = Score( + widget.level.number, + widget.level.difficulty, + DateTime.now().difference(_startOfPlay), + ); + + final playerProgress = context.read(); + playerProgress.setLevelReached(widget.level.number); + + // Let the player see the game just after winning for a bit. + await Future.delayed(_preCelebrationDuration); + if (!mounted) return; + + setState(() { + _duringCelebration = true; + }); + + final audioController = context.read(); + audioController.playSfx(SfxType.congrats); + + final gamesServicesController = context.read(); + if (gamesServicesController != null) { + // Award achievement. + if (widget.level.awardsAchievement) { + await gamesServicesController.awardAchievement( + android: widget.level.achievementIdAndroid!, + iOS: widget.level.achievementIdIOS!, + ); + } + + // Send score to leaderboard. + await gamesServicesController.submitLeaderboardScore(score); + } + + /// Give the player some time to see the celebration animation. + await Future.delayed(_celebrationDuration); + if (!mounted) return; + + GoRouter.of(context).go('/play/won', extra: {'score': score}); + } +} diff --git a/game_template/lib/src/player_progress/persistence/local_storage_player_progress_persistence.dart b/game_template/lib/src/player_progress/persistence/local_storage_player_progress_persistence.dart new file mode 100644 index 000000000..66c0d35f4 --- /dev/null +++ b/game_template/lib/src/player_progress/persistence/local_storage_player_progress_persistence.dart @@ -0,0 +1,26 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:shared_preferences/shared_preferences.dart'; + +import 'player_progress_persistence.dart'; + +/// An implementation of [PlayerProgressPersistence] that uses +/// `package:shared_preferences`. +class LocalStoragePlayerProgressPersistence extends PlayerProgressPersistence { + final Future instanceFuture = + SharedPreferences.getInstance(); + + @override + Future getHighestLevelReached() async { + final prefs = await instanceFuture; + return prefs.getInt('highestLevelReached') ?? 0; + } + + @override + Future saveHighestLevelReached(int level) async { + final prefs = await instanceFuture; + await prefs.setInt('highestLevelReached', level); + } +} diff --git a/game_template/lib/src/player_progress/persistence/memory_player_progress_persistence.dart b/game_template/lib/src/player_progress/persistence/memory_player_progress_persistence.dart new file mode 100644 index 000000000..091370588 --- /dev/null +++ b/game_template/lib/src/player_progress/persistence/memory_player_progress_persistence.dart @@ -0,0 +1,23 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'player_progress_persistence.dart'; + +/// An in-memory implementation of [PlayerProgressPersistence]. +/// Useful for testing. +class MemoryOnlyPlayerProgressPersistence implements PlayerProgressPersistence { + int level = 0; + + @override + Future getHighestLevelReached() async { + await Future.delayed(const Duration(milliseconds: 500)); + return level; + } + + @override + Future saveHighestLevelReached(int level) async { + await Future.delayed(const Duration(milliseconds: 500)); + this.level = level; + } +} diff --git a/game_template/lib/src/player_progress/persistence/player_progress_persistence.dart b/game_template/lib/src/player_progress/persistence/player_progress_persistence.dart new file mode 100644 index 000000000..123027a46 --- /dev/null +++ b/game_template/lib/src/player_progress/persistence/player_progress_persistence.dart @@ -0,0 +1,13 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// An interface of persistence stores for the player's progress. +/// +/// Implementations can range from simple in-memory storage through +/// local preferences to cloud saves. +abstract class PlayerProgressPersistence { + Future getHighestLevelReached(); + + Future saveHighestLevelReached(int level); +} diff --git a/game_template/lib/src/player_progress/player_progress.dart b/game_template/lib/src/player_progress/player_progress.dart new file mode 100644 index 000000000..6f746da9a --- /dev/null +++ b/game_template/lib/src/player_progress/player_progress.dart @@ -0,0 +1,57 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import 'persistence/player_progress_persistence.dart'; + +/// Encapsulates the player's progress. +class PlayerProgress extends ChangeNotifier { + static const maxHighestScoresPerPlayer = 10; + + final PlayerProgressPersistence _store; + + int _highestLevelReached = 0; + + /// Creates an instance of [PlayerProgress] backed by an injected + /// persistence [store]. + PlayerProgress(PlayerProgressPersistence store) : _store = store; + + /// The highest level that the player has reached so far. + int get highestLevelReached => _highestLevelReached; + + /// Fetches the latest data from the backing persistence store. + Future getLatestFromStore() async { + final level = await _store.getHighestLevelReached(); + if (level > _highestLevelReached) { + _highestLevelReached = level; + notifyListeners(); + } else if (level < _highestLevelReached) { + await _store.saveHighestLevelReached(_highestLevelReached); + } + } + + /// Resets the player's progress so it's like if they just started + /// playing the game for the first time. + void reset() { + _highestLevelReached = 0; + notifyListeners(); + _store.saveHighestLevelReached(_highestLevelReached); + } + + /// Registers [level] as reached. + /// + /// If this is higher than [highestLevelReached], it will update that + /// value and save it to the injected persistence store. + void setLevelReached(int level) { + if (level > _highestLevelReached) { + _highestLevelReached = level; + notifyListeners(); + + unawaited(_store.saveHighestLevelReached(level)); + } + } +} diff --git a/game_template/lib/src/settings/custom_name_dialog.dart b/game_template/lib/src/settings/custom_name_dialog.dart new file mode 100644 index 000000000..405f6f66e --- /dev/null +++ b/game_template/lib/src/settings/custom_name_dialog.dart @@ -0,0 +1,75 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:game_template/src/settings/settings.dart'; +import 'package:provider/provider.dart'; + +void showCustomNameDialog(BuildContext context) { + showGeneralDialog( + context: context, + pageBuilder: (context, animation, secondaryAnimation) => + CustomNameDialog(animation: animation)); +} + +class CustomNameDialog extends StatefulWidget { + final Animation animation; + + const CustomNameDialog({required this.animation, Key? key}) : super(key: key); + + @override + State createState() => _CustomNameDialogState(); +} + +class _CustomNameDialogState extends State { + final TextEditingController _controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return ScaleTransition( + scale: CurvedAnimation( + parent: widget.animation, + curve: Curves.easeOutCubic, + ), + child: SimpleDialog( + title: const Text('Change name'), + children: [ + TextField( + controller: _controller, + autofocus: true, + maxLength: 12, + maxLengthEnforcement: MaxLengthEnforcement.enforced, + textAlign: TextAlign.center, + textCapitalization: TextCapitalization.words, + textInputAction: TextInputAction.done, + onChanged: (value) { + context.read().setPlayerName(value); + }, + onSubmitted: (value) { + // Player tapped 'Submit'/'Done' on their keyboard. + Navigator.pop(context); + }, + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + } + + @override + void didChangeDependencies() { + _controller.text = context.read().playerName.value; + super.didChangeDependencies(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/game_template/lib/src/settings/persistence/local_storage_settings_persistence.dart b/game_template/lib/src/settings/persistence/local_storage_settings_persistence.dart new file mode 100644 index 000000000..1eef3c1be --- /dev/null +++ b/game_template/lib/src/settings/persistence/local_storage_settings_persistence.dart @@ -0,0 +1,62 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:shared_preferences/shared_preferences.dart'; + +import 'settings_persistence.dart'; + +/// An implementation of [SettingsPersistence] that uses +/// `package:shared_preferences`. +class LocalStorageSettingsPersistence extends SettingsPersistence { + final Future instanceFuture = + SharedPreferences.getInstance(); + + @override + Future getMusicOn() async { + final prefs = await instanceFuture; + return prefs.getBool('musicOn') ?? true; + } + + @override + Future getMuted({required bool defaultValue}) async { + final prefs = await instanceFuture; + return prefs.getBool('mute') ?? defaultValue; + } + + @override + Future getPlayerName() async { + final prefs = await instanceFuture; + return prefs.getString('playerName') ?? 'Player'; + } + + @override + Future getSoundsOn() async { + final prefs = await instanceFuture; + return prefs.getBool('soundsOn') ?? true; + } + + @override + Future saveMusicOn(bool value) async { + final prefs = await instanceFuture; + await prefs.setBool('musicOn', value); + } + + @override + Future saveMuted(bool value) async { + final prefs = await instanceFuture; + await prefs.setBool('mute', value); + } + + @override + Future savePlayerName(String value) async { + final prefs = await instanceFuture; + await prefs.setString('playerName', value); + } + + @override + Future saveSoundsOn(bool value) async { + final prefs = await instanceFuture; + await prefs.setBool('soundsOn', value); + } +} diff --git a/game_template/lib/src/settings/persistence/memory_settings_persistence.dart b/game_template/lib/src/settings/persistence/memory_settings_persistence.dart new file mode 100644 index 000000000..30015e665 --- /dev/null +++ b/game_template/lib/src/settings/persistence/memory_settings_persistence.dart @@ -0,0 +1,41 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:game_template/src/settings/persistence/settings_persistence.dart'; + +/// An in-memory implementation of [SettingsPersistence]. +/// Useful for testing. +class MemoryOnlySettingsPersistence implements SettingsPersistence { + bool musicOn = true; + + bool soundsOn = true; + + bool muted = false; + + String playerName = 'Player'; + + @override + Future getMusicOn() async => musicOn; + + @override + Future getMuted({required bool defaultValue}) async => muted; + + @override + Future getPlayerName() async => playerName; + + @override + Future getSoundsOn() async => soundsOn; + + @override + Future saveMusicOn(bool value) async => musicOn = value; + + @override + Future saveMuted(bool value) async => muted = value; + + @override + Future savePlayerName(String value) async => playerName = value; + + @override + Future saveSoundsOn(bool value) async => soundsOn = value; +} diff --git a/game_template/lib/src/settings/persistence/settings_persistence.dart b/game_template/lib/src/settings/persistence/settings_persistence.dart new file mode 100644 index 000000000..46d1be051 --- /dev/null +++ b/game_template/lib/src/settings/persistence/settings_persistence.dart @@ -0,0 +1,25 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// An interface of persistence stores for settings. +/// +/// Implementations can range from simple in-memory storage through +/// local preferences to cloud-based solutions. +abstract class SettingsPersistence { + Future getMusicOn(); + + Future getMuted({required bool defaultValue}); + + Future getPlayerName(); + + Future getSoundsOn(); + + Future saveMusicOn(bool value); + + Future saveMuted(bool value); + + Future savePlayerName(String value); + + Future saveSoundsOn(bool value); +} diff --git a/game_template/lib/src/settings/settings.dart b/game_template/lib/src/settings/settings.dart new file mode 100644 index 000000000..56c69d1e2 --- /dev/null +++ b/game_template/lib/src/settings/settings.dart @@ -0,0 +1,62 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +import 'persistence/settings_persistence.dart'; + +/// An class that holds settings like [playerName] or [musicOn], +/// and saves them to an injected persistence store. +class SettingsController { + final SettingsPersistence _persistence; + + /// Whether or not the sound is on at all. This overrides both music + /// and sound. + ValueNotifier muted = ValueNotifier(false); + + ValueNotifier playerName = ValueNotifier('Player'); + + ValueNotifier soundsOn = ValueNotifier(false); + + ValueNotifier musicOn = ValueNotifier(false); + + /// Creates a new instance of [SettingsController] backed by [persistence]. + SettingsController({required SettingsPersistence persistence}) + : _persistence = persistence; + + /// Asynchronously loads values from the injected persistence store. + Future loadStateFromPersistence() async { + await Future.wait([ + _persistence + // On the web, sound can only start after user interaction, so + // we start muted there. + // On any other platform, we start unmuted. + .getMuted(defaultValue: kIsWeb) + .then((value) => muted.value = value), + _persistence.getSoundsOn().then((value) => soundsOn.value = value), + _persistence.getMusicOn().then((value) => musicOn.value = value), + _persistence.getPlayerName().then((value) => playerName.value = value), + ]); + } + + void setPlayerName(String name) { + playerName.value = name; + _persistence.savePlayerName(playerName.value); + } + + void toggleMusicOn() { + musicOn.value = !musicOn.value; + _persistence.saveMusicOn(musicOn.value); + } + + void toggleMuted() { + muted.value = !muted.value; + _persistence.saveMuted(muted.value); + } + + void toggleSoundsOn() { + soundsOn.value = !soundsOn.value; + _persistence.saveSoundsOn(soundsOn.value); + } +} diff --git a/game_template/lib/src/settings/settings_screen.dart b/game_template/lib/src/settings/settings_screen.dart new file mode 100644 index 000000000..722e74f5c --- /dev/null +++ b/game_template/lib/src/settings/settings_screen.dart @@ -0,0 +1,187 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../in_app_purchase/in_app_purchase.dart'; +import '../player_progress/player_progress.dart'; +import '../style/palette.dart'; +import '../style/responsive_screen.dart'; +import 'custom_name_dialog.dart'; +import 'settings.dart'; + +class SettingsScreen extends StatelessWidget { + const SettingsScreen({Key? key}) : super(key: key); + + static const _gap = SizedBox(height: 60); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final palette = context.watch(); + + return Scaffold( + backgroundColor: palette.backgroundSettings, + body: ResponsiveScreen( + squarishMainArea: ListView( + children: [ + _gap, + const Text( + 'Settings', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 55, + height: 1, + ), + ), + _gap, + const _NameChangeLine( + 'Name', + ), + ValueListenableBuilder( + valueListenable: settings.soundsOn, + builder: (context, soundsOn, child) => _SettingsLine( + 'Sound FX', + Icon(soundsOn ? Icons.graphic_eq : Icons.volume_off), + onSelected: () => settings.toggleSoundsOn(), + ), + ), + ValueListenableBuilder( + valueListenable: settings.musicOn, + builder: (context, musicOn, child) => _SettingsLine( + 'Music', + Icon(musicOn ? Icons.music_note : Icons.music_off), + onSelected: () => settings.toggleMusicOn(), + ), + ), + Consumer( + builder: (context, inAppPurchase, child) { + if (inAppPurchase == null) { + // In-app purchases are not supported yet. + // Go to lib/main.dart and uncomment the lines that create + // the InAppPurchaseController. + return const SizedBox.shrink(); + } + + Widget icon; + VoidCallback? callback; + if (inAppPurchase.adRemoval.active) { + icon = const Icon(Icons.check); + } else if (inAppPurchase.adRemoval.pending) { + icon = const CircularProgressIndicator(); + } else { + icon = const Icon(Icons.ad_units); + callback = () { + inAppPurchase.buy(); + }; + } + return _SettingsLine( + 'Remove ads', + icon, + onSelected: callback, + ); + }), + _SettingsLine( + 'Reset progress', + const Icon(Icons.delete), + onSelected: () { + context.read().reset(); + + final messenger = ScaffoldMessenger.of(context); + messenger.showSnackBar( + const SnackBar( + content: Text('Player progress has been reset.')), + ); + }, + ), + _gap, + ], + ), + rectangularMenuArea: ElevatedButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('Back'), + ), + ), + ); + } +} + +class _NameChangeLine extends StatelessWidget { + final String title; + + const _NameChangeLine(this.title, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + + return InkResponse( + highlightShape: BoxShape.rectangle, + onTap: () => showCustomNameDialog(context), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(title, + style: const TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 30, + )), + const Spacer(), + ValueListenableBuilder( + valueListenable: settings.playerName, + builder: (context, name, child) => Text( + '‘$name’', + style: const TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 30, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _SettingsLine extends StatelessWidget { + final String title; + + final Widget icon; + + final VoidCallback? onSelected; + + const _SettingsLine(this.title, this.icon, {this.onSelected, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return InkResponse( + highlightShape: BoxShape.rectangle, + onTap: onSelected, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(title, + style: const TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 30, + )), + const Spacer(), + icon, + ], + ), + ), + ); + } +} diff --git a/game_template/lib/src/style/confetti.dart b/game_template/lib/src/style/confetti.dart new file mode 100644 index 000000000..ea80387ca --- /dev/null +++ b/game_template/lib/src/style/confetti.dart @@ -0,0 +1,234 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:collection'; +import 'dart:math'; + +import 'package:flutter/widgets.dart'; + +/// Shows a confetti (celebratory) animation: paper snippings falling down. +/// +/// The widget fills the available space (like [SizedBox.expand] would). +/// +/// When [isStopped] is `true`, the animation will not run. This is useful +/// when the widget is not visible yet, for example. Provide [colors] +/// to make the animation look good in context. +/// +/// This is a partial port of this CodePen by Hemn Chawroka: +/// https://codepen.io/iprodev/pen/azpWBr +class Confetti extends StatefulWidget { + static const _defaultColors = [ + Color(0xffd10841), + Color(0xff1d75fb), + Color(0xff0050bc), + Color(0xffa2dcc7), + ]; + + final bool isStopped; + + final List colors; + + const Confetti({ + this.colors = _defaultColors, + this.isStopped = false, + Key? key, + }) : super(key: key); + + @override + State createState() => _ConfettiState(); +} + +class ConfettiPainter extends CustomPainter { + final defaultPaint = Paint(); + + final int snippingsCount = 200; + + late final List<_PaperSnipping> _snippings; + + Size? _size; + + DateTime _lastTime = DateTime.now(); + + final UnmodifiableListView colors; + + ConfettiPainter( + {required Listenable animation, required Iterable colors}) + : colors = UnmodifiableListView(colors), + super(repaint: animation); + + @override + void paint(Canvas canvas, Size size) { + if (_size == null) { + // First time we have a size. + _snippings = List.generate( + snippingsCount, + (i) => _PaperSnipping( + frontColor: colors[i % colors.length], + bounds: size, + )); + } + + final didResize = _size != null && _size != size; + final now = DateTime.now(); + final dt = now.difference(_lastTime); + for (final snipping in _snippings) { + if (didResize) { + snipping.updateBounds(size); + } + snipping.update(dt.inMilliseconds / 1000); + snipping.draw(canvas); + } + + _size = size; + _lastTime = now; + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} + +class _ConfettiState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: ConfettiPainter( + colors: widget.colors, + animation: _controller, + ), + willChange: true, + child: const SizedBox.expand(), + ); + } + + @override + void didUpdateWidget(covariant Confetti oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.isStopped && !widget.isStopped) { + _controller.repeat(); + } else if (!oldWidget.isStopped && widget.isStopped) { + _controller.stop(canceled: false); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _controller = AnimationController( + // We don't really care about the duration, since we're going to + // use the controller on loop anyway. + duration: const Duration(seconds: 1), + vsync: this, + ); + + if (!widget.isStopped) { + _controller.repeat(); + } + } +} + +class _PaperSnipping { + static final Random _random = Random(); + + static const degToRad = pi / 180; + + static const backSideBlend = Color(0x70EEEEEE); + + Size _bounds; + + late final _Vector position = _Vector( + _random.nextDouble() * _bounds.width, + _random.nextDouble() * _bounds.height, + ); + + final double rotationSpeed = 800 + _random.nextDouble() * 600; + + final double angle = _random.nextDouble() * 360 * degToRad; + + double rotation = _random.nextDouble() * 360 * degToRad; + + double cosA = 1.0; + + final double size = 7.0; + + final double oscillationSpeed = 0.5 + _random.nextDouble() * 1.5; + + final double xSpeed = 40; + + final double ySpeed = 50 + _random.nextDouble() * 60; + + late List<_Vector> corners = List.generate(4, (i) { + final angle = this.angle + degToRad * (45 + i * 90); + return _Vector(cos(angle), sin(angle)); + }); + + double time = _random.nextDouble(); + + final Color frontColor; + + late final Color backColor = Color.alphaBlend(backSideBlend, frontColor); + + final paint = Paint()..style = PaintingStyle.fill; + + _PaperSnipping({ + required this.frontColor, + required Size bounds, + }) : _bounds = bounds; + + void draw(Canvas canvas) { + if (cosA > 0) { + paint.color = frontColor; + } else { + paint.color = backColor; + } + + final path = Path() + ..addPolygon( + List.generate( + 4, + (index) => Offset( + position.x + corners[index].x * size, + position.y + corners[index].y * size * cosA, + )), + true, + ); + canvas.drawPath(path, paint); + } + + void update(double dt) { + time += dt; + rotation += rotationSpeed * dt; + cosA = cos(degToRad * rotation); + position.x += cos(time * oscillationSpeed) * xSpeed * dt; + position.y += ySpeed * dt; + if (position.y > _bounds.height) { + // Move the snipping back to the top. + position.x = _random.nextDouble() * _bounds.width; + position.y = 0; + } + } + + void updateBounds(Size newBounds) { + if (!newBounds.contains(Offset(position.x, position.y))) { + position.x = _random.nextDouble() * newBounds.width; + position.y = _random.nextDouble() * newBounds.height; + } + _bounds = newBounds; + } +} + +class _Vector { + double x, y; + _Vector(this.x, this.y); +} diff --git a/game_template/lib/src/style/my_transition.dart b/game_template/lib/src/style/my_transition.dart new file mode 100644 index 000000000..7d9050199 --- /dev/null +++ b/game_template/lib/src/style/my_transition.dart @@ -0,0 +1,124 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; + +CustomTransitionPage buildMyTransition({ + required Widget child, + required Color color, + String? name, + Object? arguments, + String? restorationId, + LocalKey? key, +}) { + return CustomTransitionPage( + child: child, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return _MyReveal( + animation: animation, + color: color, + child: child, + ); + }, + key: key, + name: name, + arguments: arguments, + restorationId: restorationId, + transitionDuration: const Duration(milliseconds: 700), + ); +} + +class _MyReveal extends StatefulWidget { + final Widget child; + + final Animation animation; + + final Color color; + + const _MyReveal({ + required this.child, + required this.animation, + required this.color, + Key? key, + }) : super(key: key); + + @override + State<_MyReveal> createState() => _MyRevealState(); +} + +class _MyRevealState extends State<_MyReveal> { + static final _log = Logger('_InkRevealState'); + + bool _finished = false; + + final _tween = Tween(begin: const Offset(0, -1), end: Offset.zero); + + @override + void initState() { + super.initState(); + + widget.animation.addStatusListener(_statusListener); + } + + @override + void didUpdateWidget(covariant _MyReveal oldWidget) { + if (oldWidget.animation != widget.animation) { + oldWidget.animation.removeStatusListener(_statusListener); + widget.animation.addStatusListener(_statusListener); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + widget.animation.removeStatusListener(_statusListener); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + SlideTransition( + position: _tween.animate( + CurvedAnimation( + parent: widget.animation, + curve: Curves.easeOutCubic, + reverseCurve: Curves.easeOutCubic, + ), + ), + child: Container( + color: widget.color, + ), + ), + AnimatedOpacity( + opacity: _finished ? 1 : 0, + duration: const Duration(milliseconds: 300), + child: widget.child, + ), + ], + ); + } + + void _statusListener(AnimationStatus status) { + _log.fine(() => 'status: $status'); + switch (status) { + case AnimationStatus.completed: + setState(() { + _finished = true; + }); + break; + case AnimationStatus.forward: + case AnimationStatus.dismissed: + case AnimationStatus.reverse: + setState(() { + _finished = false; + }); + break; + } + } +} diff --git a/game_template/lib/src/style/palette.dart b/game_template/lib/src/style/palette.dart new file mode 100644 index 000000000..84de8c409 --- /dev/null +++ b/game_template/lib/src/style/palette.dart @@ -0,0 +1,37 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// A palette of colors to be used in the game. +/// +/// The reason we're not going with something like Material Design's +/// `Theme` is simply that this is simpler to work with and yet gives +/// us everything we need for a game. +/// +/// Games generally have more radical color palettes than apps. For example, +/// every level of a game can have radically different colors. +/// At the same time, games rarely support dark mode. +/// +/// Colors taken from this fun palette: +/// https://lospec.com/palette-list/crayola84 +/// +/// Colors here are implemented as getters so that hot reloading works. +/// In practice, we could just as easily implement the colors +/// as `static const`. But this way the palette is more malleable: +/// we could allow players to customize colors, for example, +/// or even get the colors from the network. +class Palette { + Color get pen => const Color(0xff1d75fb); + Color get darkPen => const Color(0xFF0050bc); + Color get redPen => const Color(0xFFd10841); + Color get inkFullOpacity => const Color(0xff352b42); + Color get ink => const Color(0xee352b42); + Color get backgroundMain => const Color(0xffffffd1); + Color get backgroundLevelSelection => const Color(0xffa2dcc7); + Color get backgroundPlaySession => const Color(0xffffebb5); + Color get background4 => const Color(0xffffd7ff); + Color get backgroundSettings => const Color(0xffbfc8e3); + Color get trueWhite => const Color(0xffffffff); +} diff --git a/game_template/lib/src/style/responsive_screen.dart b/game_template/lib/src/style/responsive_screen.dart new file mode 100644 index 000000000..cd33c7583 --- /dev/null +++ b/game_template/lib/src/style/responsive_screen.dart @@ -0,0 +1,125 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// A widget that makes it easy to create a screen with a square-ish +/// main area, a smaller menu area, and a small area for a message on top. +/// It works in both orientations on mobile- and tablet-sized screens. +class ResponsiveScreen extends StatelessWidget { + /// This is the "hero" of the screen. It's more or less square, and will + /// be placed in the visual "center" of the screen. + final Widget squarishMainArea; + + /// The second-largest area after [squarishMainArea]. It can be narrow + /// or wide. + final Widget rectangularMenuArea; + + /// An area reserved for some static text close to the top of the screen. + final Widget topMessageArea; + + /// How much bigger should the [squarishMainArea] be compared to the other + /// elements. + final double mainAreaProminence; + + const ResponsiveScreen({ + required this.squarishMainArea, + required this.rectangularMenuArea, + this.topMessageArea = const SizedBox.shrink(), + this.mainAreaProminence = 0.8, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + // This widget wants to fill the whole screen. + final size = constraints.biggest; + final padding = EdgeInsets.all(size.shortestSide / 30); + + if (size.height >= size.width) { + // "Portrait" / "mobile" mode. + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SafeArea( + bottom: false, + child: Padding( + padding: padding, + child: topMessageArea, + ), + ), + Expanded( + flex: (mainAreaProminence * 100).round(), + child: SafeArea( + top: false, + bottom: false, + minimum: padding, + child: squarishMainArea, + ), + ), + SafeArea( + top: false, + maintainBottomViewPadding: true, + child: Padding( + padding: padding, + child: rectangularMenuArea, + ), + ), + ], + ); + } else { + // "Landscape" / "tablet" mode. + final isLarge = size.width > 900; + + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + flex: isLarge ? 7 : 5, + child: SafeArea( + right: false, + maintainBottomViewPadding: true, + minimum: padding, + child: squarishMainArea, + ), + ), + Expanded( + flex: 3, + child: Column( + children: [ + SafeArea( + bottom: false, + left: false, + maintainBottomViewPadding: true, + child: Padding( + padding: padding, + child: topMessageArea, + ), + ), + Expanded( + child: SafeArea( + top: false, + left: false, + maintainBottomViewPadding: true, + child: Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: padding, + child: rectangularMenuArea, + ), + ), + ), + ) + ], + ), + ), + ], + ); + } + }, + ); + } +} diff --git a/game_template/lib/src/style/snack_bar.dart b/game_template/lib/src/style/snack_bar.dart new file mode 100644 index 000000000..851cb2141 --- /dev/null +++ b/game_template/lib/src/style/snack_bar.dart @@ -0,0 +1,18 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Shows [message] in a snack bar as long as a [ScaffoldMessengerState] +/// with global key [scaffoldMessengerKey] is anywhere in the widget tree. +void showSnackBar(String message) { + final messenger = scaffoldMessengerKey.currentState; + messenger?.showSnackBar( + SnackBar(content: Text(message)), + ); +} + +/// Use this when creating [MaterialApp] if you want [showSnackBar] to work. +final GlobalKey scaffoldMessengerKey = + GlobalKey(debugLabel: 'scaffoldMessengerKey'); diff --git a/game_template/lib/src/win_game/win_game_screen.dart b/game_template/lib/src/win_game/win_game_screen.dart new file mode 100644 index 000000000..4c8be0866 --- /dev/null +++ b/game_template/lib/src/win_game/win_game_screen.dart @@ -0,0 +1,73 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../ads/ads_controller.dart'; +import '../ads/banner_ad_widget.dart'; +import '../games_services/score.dart'; +import '../in_app_purchase/in_app_purchase.dart'; +import '../style/palette.dart'; +import '../style/responsive_screen.dart'; + +class WinGameScreen extends StatelessWidget { + final Score score; + + const WinGameScreen({ + Key? key, + required this.score, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final adsControllerAvailable = context.watch() != null; + final adsRemoved = + context.watch()?.adRemoval.active ?? false; + final palette = context.watch(); + + const gap = SizedBox(height: 10); + + return Scaffold( + backgroundColor: palette.backgroundPlaySession, + body: ResponsiveScreen( + squarishMainArea: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (adsControllerAvailable && !adsRemoved) ...[ + const Expanded( + child: Center( + child: BannerAdWidget(), + ), + ), + ], + gap, + const Center( + child: Text( + 'You won!', + style: TextStyle(fontFamily: 'Permanent Marker', fontSize: 50), + ), + ), + gap, + Center( + child: Text( + 'Score: ${score.score}\n' + 'Time: ${score.formattedTime}', + style: const TextStyle( + fontFamily: 'Permanent Marker', fontSize: 20), + ), + ), + ], + ), + rectangularMenuArea: ElevatedButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('Continue'), + ), + ), + ); + } +} diff --git a/game_template/macos/.gitignore b/game_template/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/game_template/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/game_template/macos/Flutter/Flutter-Debug.xcconfig b/game_template/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..4b81f9b2d --- /dev/null +++ b/game_template/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/game_template/macos/Flutter/Flutter-Release.xcconfig b/game_template/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..5caa9d157 --- /dev/null +++ b/game_template/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/game_template/macos/Flutter/GeneratedPluginRegistrant.swift b/game_template/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 000000000..5032c602a --- /dev/null +++ b/game_template/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,22 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import audioplayers +import firebase_core +import firebase_crashlytics +import games_services +import path_provider_macos +import shared_preferences_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioplayersPlugin.register(with: registry.registrar(forPlugin: "AudioplayersPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) + GamesServicesPlugin.register(with: registry.registrar(forPlugin: "GamesServicesPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) +} diff --git a/game_template/macos/Podfile b/game_template/macos/Podfile new file mode 100644 index 000000000..f9ebb8dcb --- /dev/null +++ b/game_template/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/game_template/macos/Podfile.lock b/game_template/macos/Podfile.lock new file mode 100644 index 000000000..64ef681bb --- /dev/null +++ b/game_template/macos/Podfile.lock @@ -0,0 +1,119 @@ +PODS: + - audioplayers (0.0.1): + - FlutterMacOS + - Firebase/CoreOnly (8.15.0): + - FirebaseCore (= 8.15.0) + - Firebase/Crashlytics (8.15.0): + - Firebase/CoreOnly + - FirebaseCrashlytics (~> 8.15.0) + - firebase_core (1.15.0): + - Firebase/CoreOnly (~> 8.15.0) + - FlutterMacOS + - firebase_crashlytics (2.6.3): + - Firebase/CoreOnly (~> 8.15.0) + - Firebase/Crashlytics (~> 8.15.0) + - firebase_core + - FlutterMacOS + - FirebaseCore (8.15.0): + - FirebaseCoreDiagnostics (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - FirebaseCoreDiagnostics (8.15.0): + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - nanopb (~> 2.30908.0) + - FirebaseCrashlytics (8.15.0): + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/Environment (~> 7.7) + - nanopb (~> 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - FirebaseInstallations (8.15.0): + - FirebaseCore (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - PromisesObjC (< 3.0, >= 1.2) + - FlutterMacOS (1.0.0) + - games_services (0.0.1): + - FlutterMacOS + - GoogleDataTransport (9.1.2): + - GoogleUtilities/Environment (~> 7.2) + - nanopb (~> 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Environment (7.7.0): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/UserDefaults (7.7.0): + - GoogleUtilities/Logger + - nanopb (2.30908.0): + - nanopb/decode (= 2.30908.0) + - nanopb/encode (= 2.30908.0) + - nanopb/decode (2.30908.0) + - nanopb/encode (2.30908.0) + - path_provider_macos (0.0.1): + - FlutterMacOS + - PromisesObjC (2.0.0) + - shared_preferences_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - audioplayers (from `Flutter/ephemeral/.symlinks/plugins/audioplayers/macos`) + - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) + - firebase_crashlytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_crashlytics/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - games_services (from `Flutter/ephemeral/.symlinks/plugins/games_services/macos`) + - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) + +SPEC REPOS: + trunk: + - Firebase + - FirebaseCore + - FirebaseCoreDiagnostics + - FirebaseCrashlytics + - FirebaseInstallations + - GoogleDataTransport + - GoogleUtilities + - nanopb + - PromisesObjC + +EXTERNAL SOURCES: + audioplayers: + :path: Flutter/ephemeral/.symlinks/plugins/audioplayers/macos + firebase_core: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos + firebase_crashlytics: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_crashlytics/macos + FlutterMacOS: + :path: Flutter/ephemeral + games_services: + :path: Flutter/ephemeral/.symlinks/plugins/games_services/macos + path_provider_macos: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + shared_preferences_macos: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos + +SPEC CHECKSUMS: + audioplayers: 8b48e22684b6e0d9df143b2d1bbd61dca9dab6b4 + Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d + firebase_core: c282e8c1f01967ba4ae24d4872e8a3f6c3163730 + firebase_crashlytics: 966553735070e9a246c74ad764920f350d7ceeed + FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1 + FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb + FirebaseCrashlytics: feb07e4e9187be3c23c6a846cce4824e5ce2dd0b + FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd + FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + games_services: eb1f60c78e6b358d8e0c7bd0f8c8c7303f8971da + GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 + GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 + nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + path_provider_macos: 160cab0d5461f0c0e02995469a98f24bdb9a3f1f + PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 + shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 + +PODFILE CHECKSUM: 8d40c19d3cbdb380d870685c3a564c989f1efa52 + +COCOAPODS: 1.11.2 diff --git a/game_template/macos/Runner.xcodeproj/project.pbxproj b/game_template/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..19a3dc0cd --- /dev/null +++ b/game_template/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,634 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 31231BC7C0070571A3F5F3DC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BEBC2261DB206682FD81D34 /* Pods_Runner.framework */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* game_template.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = game_template.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 3B416D2ADC36926A91D87A31 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 4BEBC2261DB206682FD81D34 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6BE93D89615A3A0FAB9D6A00 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + CA6976AA34ACFE4C9E50A0C3 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 31231BC7C0070571A3F5F3DC /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 86164A4277B2E67BDE507088 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* game_template.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 86164A4277B2E67BDE507088 /* Pods */ = { + isa = PBXGroup; + children = ( + 3B416D2ADC36926A91D87A31 /* Pods-Runner.debug.xcconfig */, + 6BE93D89615A3A0FAB9D6A00 /* Pods-Runner.release.xcconfig */, + CA6976AA34ACFE4C9E50A0C3 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4BEBC2261DB206682FD81D34 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 0C91FFE2D4FD89B15D7A2457 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 28A820B82E74D7D11FBAB15D /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* game_template.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0C91FFE2D4FD89B15D7A2457 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 28A820B82E74D7D11FBAB15D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/game_template/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/game_template/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/game_template/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/game_template/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/game_template/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..5acc3402d --- /dev/null +++ b/game_template/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game_template/macos/Runner.xcworkspace/contents.xcworkspacedata b/game_template/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/game_template/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/game_template/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/game_template/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/game_template/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/game_template/macos/Runner/AppDelegate.swift b/game_template/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..d53ef6437 --- /dev/null +++ b/game_template/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..a2ec33f19 --- /dev/null +++ b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000..3c4935a7c Binary files /dev/null and b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000..ed4cc1642 Binary files /dev/null and b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000..483be6138 Binary files /dev/null and b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000..bcbf36df2 Binary files /dev/null and b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000..9c0a65286 Binary files /dev/null and b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000..e71a72613 Binary files /dev/null and b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000..8a31fe2dd Binary files /dev/null and b/game_template/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/game_template/macos/Runner/Base.lproj/MainMenu.xib b/game_template/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..80e867a4e --- /dev/null +++ b/game_template/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game_template/macos/Runner/Configs/AppInfo.xcconfig b/game_template/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..02531bcd2 --- /dev/null +++ b/game_template/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = game_template + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.gameTemplate + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved. diff --git a/game_template/macos/Runner/Configs/Debug.xcconfig b/game_template/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/game_template/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/game_template/macos/Runner/Configs/Release.xcconfig b/game_template/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/game_template/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/game_template/macos/Runner/Configs/Warnings.xcconfig b/game_template/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/game_template/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/game_template/macos/Runner/DebugProfile.entitlements b/game_template/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000..dddb8a30c --- /dev/null +++ b/game_template/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/game_template/macos/Runner/Info.plist b/game_template/macos/Runner/Info.plist new file mode 100644 index 000000000..4789daa6a --- /dev/null +++ b/game_template/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/game_template/macos/Runner/MainFlutterWindow.swift b/game_template/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..2722837ec --- /dev/null +++ b/game_template/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/game_template/macos/Runner/Release.entitlements b/game_template/macos/Runner/Release.entitlements new file mode 100644 index 000000000..852fa1a47 --- /dev/null +++ b/game_template/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/game_template/pubspec.lock b/game_template/pubspec.lock new file mode 100644 index 000000000..2adfbcd0e --- /dev/null +++ b/game_template/pubspec.lock @@ -0,0 +1,733 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "31.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.0" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.2" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + url: "https://pub.dartlang.org" + source: hosted + version: "0.20.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + coverage: + dependency: transitive + description: + name: coverage + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.5" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + firebase_crashlytics: + dependency: "direct main" + description: + name: firebase_crashlytics + url: "https://pub.dartlang.org" + source: hosted + version: "2.6.3" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.4" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.2" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + games_services: + dependency: "direct main" + description: + name: games_services + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" + games_services_platform_interface: + dependency: transitive + description: + name: games_services_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + go_router: + dependency: "direct main" + description: + name: go_router + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + google_mobile_ads: + dependency: "direct main" + description: + name: google_mobile_ads + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + in_app_purchase: + dependency: "direct main" + description: + name: in_app_purchase + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + in_app_purchase_android: + dependency: transitive + description: + name: in_app_purchase_android + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.2+2" + in_app_purchase_platform_interface: + dependency: transitive + description: + name: in_app_purchase_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + in_app_purchase_storekit: + dependency: transitive + description: + name: in_app_purchase_storekit + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0+4" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + logging: + dependency: "direct main" + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + path_to_regexp: + dependency: transitive + description: + name: path_to_regexp + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.10" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.19.5" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.8" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "7.5.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+1" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "5.3.1" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" +sdks: + dart: ">=2.16.0 <3.0.0" + flutter: ">=2.8.0" diff --git a/game_template/pubspec.yaml b/game_template/pubspec.yaml new file mode 100644 index 000000000..22816e177 --- /dev/null +++ b/game_template/pubspec.yaml @@ -0,0 +1,58 @@ +name: game_template +description: A mobile game built in Flutter. + +# Prevent accidental publishing to pub.dev. +publish_to: 'none' + +version: 0.0.1+1 + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + audioplayers: ^0.20.1 + cupertino_icons: ^1.0.2 + go_router: ^3.0.1 + logging: ^1.0.2 + provider: ^6.0.2 + shared_preferences: ^2.0.13 + + # If you don't need one of the following dependencies, + # delete the relevant line below, and get rid of any Dart code + # that references the dependency. + firebase_core: ^1.15.0 # Needed for Crashlytics below + firebase_crashlytics: ^2.6.3 # Error reporting + games_services: ^2.0.7 # Achievements and leaderboards + google_mobile_ads: ^1.1.0 # Ads + in_app_purchase: ^3.0.1 # In-app purchases + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_launcher_icons: ^0.9.2 + flutter_lints: ^1.0.0 + test: ^1.19.0 + +flutter: + uses-material-design: true + + assets: + - assets/images/ + - assets/music/ + - assets/sfx/ + + fonts: + - family: Permanent Marker + fonts: + - asset: assets/Permanent_Marker/PermanentMarker-Regular.ttf + +flutter_icons: + android: true + ios: true + image_path: "assets/icon.png" + adaptive_icon_background: "#FFFFFF" + adaptive_icon_foreground: "assets/icon-adaptive-foreground.png" diff --git a/game_template/test/crashlytics_test.dart b/game_template/test/crashlytics_test.dart new file mode 100644 index 000000000..9a08dcd44 --- /dev/null +++ b/game_template/test/crashlytics_test.dart @@ -0,0 +1,39 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:game_template/src/crashlytics/crashlytics.dart'; +import 'package:test/test.dart'; + +void main() { + group('filterStackTrace', () { + test('keeps current stacktrace intact', () { + final original = StackTrace.current; + final filtered = filterStackTrace(original).toString().trim(); + + expect(filtered, equals(original.toString().trim())); + }); + + test('parses an empty stacktrace', () { + const original = StackTrace.empty; + final filtered = filterStackTrace(original).toString().trim(); + + expect(filtered, equals(original.toString().trim())); + }); + + test('removes the head of an example stacktrace', () { + final original = StackTrace.fromString( + ''' at guardWithCrashlytics..(crashlytics.dart:32) + at _BroadcastStreamController.add(_BroadcastStreamController.java) + at Logger._publish(logger.dart:276) + at Logger.log(logger.dart:200) + at Logger.severe(logger.dart:258) + at GamesServicesController.initialize(games_services.dart:23)'''); + final filtered = filterStackTrace(original).toString().trim(); + + expect(filtered, isNot(original.toString().trim())); + expect(filtered, isNot(contains('at guardWithCrashlytics'))); + expect(filtered, contains('at GamesServicesController')); + }); + }); +} diff --git a/game_template/test/smoke_test.dart b/game_template/test/smoke_test.dart new file mode 100644 index 000000000..77e39ce8d --- /dev/null +++ b/game_template/test/smoke_test.dart @@ -0,0 +1,48 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:game_template/main.dart'; +import 'package:game_template/src/player_progress/persistence/memory_player_progress_persistence.dart'; +import 'package:game_template/src/settings/persistence/memory_settings_persistence.dart'; + +void main() { + testWidgets('smoke test', (tester) async { + // Build our game and trigger a frame. + await tester.pumpWidget(MyApp( + settingsPersistence: MemoryOnlySettingsPersistence(), + playerProgressPersistence: MemoryOnlyPlayerProgressPersistence(), + adsController: null, + gamesServicesController: null, + inAppPurchaseController: null, + )); + + // Verify that the 'Play' button is shown. + expect(find.text('Play'), findsOneWidget); + + // Verify that the 'Settings' button is shown. + expect(find.text('Settings'), findsOneWidget); + + // Go to 'Settings'. + await tester.tap(find.text('Settings')); + await tester.pumpAndSettle(); + expect(find.text('Music'), findsOneWidget); + + // Go back to main menu. + await tester.tap(find.text('Back')); + await tester.pumpAndSettle(); + + // Tap 'Play'. + await tester.tap(find.text('Play')); + await tester.pumpAndSettle(); + expect(find.text('Select level'), findsOneWidget); + + // Tap level 1. + await tester.tap(find.text('Level #1')); + await tester.pumpAndSettle(); + + // Find the first level's "tutorial" text. + expect(find.text('Drag the slider to 5% or above!'), findsOneWidget); + }); +} diff --git a/game_template/web/favicon.png b/game_template/web/favicon.png new file mode 100644 index 000000000..8aaa46ac1 Binary files /dev/null and b/game_template/web/favicon.png differ diff --git a/game_template/web/icons/Icon-192.png b/game_template/web/icons/Icon-192.png new file mode 100644 index 000000000..b749bfef0 Binary files /dev/null and b/game_template/web/icons/Icon-192.png differ diff --git a/game_template/web/icons/Icon-512.png b/game_template/web/icons/Icon-512.png new file mode 100644 index 000000000..88cfd48df Binary files /dev/null and b/game_template/web/icons/Icon-512.png differ diff --git a/game_template/web/icons/Icon-maskable-192.png b/game_template/web/icons/Icon-maskable-192.png new file mode 100644 index 000000000..eb9b4d76e Binary files /dev/null and b/game_template/web/icons/Icon-maskable-192.png differ diff --git a/game_template/web/icons/Icon-maskable-512.png b/game_template/web/icons/Icon-maskable-512.png new file mode 100644 index 000000000..d69c56691 Binary files /dev/null and b/game_template/web/icons/Icon-maskable-512.png differ diff --git a/game_template/web/index.html b/game_template/web/index.html new file mode 100644 index 000000000..3a441e1e9 --- /dev/null +++ b/game_template/web/index.html @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + game_template + + + + + + + diff --git a/game_template/web/manifest.json b/game_template/web/manifest.json new file mode 100644 index 000000000..a6e619acd --- /dev/null +++ b/game_template/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "game_template", + "short_name": "game_template", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/game_template/windows/.gitignore b/game_template/windows/.gitignore new file mode 100644 index 000000000..d492d0d98 --- /dev/null +++ b/game_template/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/game_template/windows/CMakeLists.txt b/game_template/windows/CMakeLists.txt new file mode 100644 index 000000000..5a3641da1 --- /dev/null +++ b/game_template/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(game_template LANGUAGES CXX) + +set(BINARY_NAME "game_template") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/game_template/windows/flutter/CMakeLists.txt b/game_template/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000..b2e4bd8d6 --- /dev/null +++ b/game_template/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/game_template/windows/flutter/generated_plugin_registrant.cc b/game_template/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..8b6d4680a --- /dev/null +++ b/game_template/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/game_template/windows/flutter/generated_plugin_registrant.h b/game_template/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..dc139d85a --- /dev/null +++ b/game_template/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/game_template/windows/flutter/generated_plugins.cmake b/game_template/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000..4d10c2518 --- /dev/null +++ b/game_template/windows/flutter/generated_plugins.cmake @@ -0,0 +1,15 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/game_template/windows/runner/CMakeLists.txt b/game_template/windows/runner/CMakeLists.txt new file mode 100644 index 000000000..de2d8916b --- /dev/null +++ b/game_template/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/game_template/windows/runner/Runner.rc b/game_template/windows/runner/Runner.rc new file mode 100644 index 000000000..56bcef7a5 --- /dev/null +++ b/game_template/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "game_template" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "game_template" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "game_template.exe" "\0" + VALUE "ProductName", "game_template" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/game_template/windows/runner/flutter_window.cpp b/game_template/windows/runner/flutter_window.cpp new file mode 100644 index 000000000..b43b9095e --- /dev/null +++ b/game_template/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/game_template/windows/runner/flutter_window.h b/game_template/windows/runner/flutter_window.h new file mode 100644 index 000000000..6da0652f0 --- /dev/null +++ b/game_template/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/game_template/windows/runner/main.cpp b/game_template/windows/runner/main.cpp new file mode 100644 index 000000000..409924e9a --- /dev/null +++ b/game_template/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"game_template", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/game_template/windows/runner/resource.h b/game_template/windows/runner/resource.h new file mode 100644 index 000000000..66a65d1e4 --- /dev/null +++ b/game_template/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/game_template/windows/runner/resources/app_icon.ico b/game_template/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000..c04e20caf Binary files /dev/null and b/game_template/windows/runner/resources/app_icon.ico differ diff --git a/game_template/windows/runner/runner.exe.manifest b/game_template/windows/runner/runner.exe.manifest new file mode 100644 index 000000000..c977c4a42 --- /dev/null +++ b/game_template/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/game_template/windows/runner/utils.cpp b/game_template/windows/runner/utils.cpp new file mode 100644 index 000000000..d19bdbbcc --- /dev/null +++ b/game_template/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/game_template/windows/runner/utils.h b/game_template/windows/runner/utils.h new file mode 100644 index 000000000..3879d5475 --- /dev/null +++ b/game_template/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/game_template/windows/runner/win32_window.cpp b/game_template/windows/runner/win32_window.cpp new file mode 100644 index 000000000..c10f08dc7 --- /dev/null +++ b/game_template/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/game_template/windows/runner/win32_window.h b/game_template/windows/runner/win32_window.h new file mode 100644 index 000000000..17ba43112 --- /dev/null +++ b/game_template/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/tool/flutter_ci_script_beta.sh b/tool/flutter_ci_script_beta.sh index e10f50be2..1f4fc0ede 100755 --- a/tool/flutter_ci_script_beta.sh +++ b/tool/flutter_ci_script_beta.sh @@ -24,6 +24,9 @@ declare -ar PROJECT_NAMES=( "experimental/linting_tool" "flutter_maps_firestore" "form_app" + # TODO: Re-enable once WidgetBinding.instance is non-null + # in stable Flutter. + # "game_template" "infinite_list" "ios_app_clip" "isolate_example" diff --git a/tool/flutter_ci_script_stable.sh b/tool/flutter_ci_script_stable.sh index 62fbe3570..666ba7a72 100755 --- a/tool/flutter_ci_script_stable.sh +++ b/tool/flutter_ci_script_stable.sh @@ -22,6 +22,7 @@ declare -ar PROJECT_NAMES=( "experimental/linting_tool" "flutter_maps_firestore" "form_app" + "game_template" "infinite_list" "ios_app_clip" "isolate_example"