Flutter Game Sample Devlog
by Filip Hracek, February—May 2022
This is a "devlog" following Filip's work on the Tic Tac Toe sample game (iOS, Android) and the associated game_template project in flutter/samples, all of which were created solo in the span of 40.5 days (part-time) between February and May 2022.
Here, I recorded problems, pain points and confusions that I encountered along the way.
NOTE: When the earlier log entries talk about a "private repo", they mean the repository that is currently public. Since the game is to "launch" at around I/O 2022, I have to make it a bit of the secret until then.
2022-02-03 Start
- We're going with the tic-tac-toe game proposed earlier
- Specifically, we agree that the game should be an "app-like" game, not an action game. App-like games (puzzles, board games, card games, "infinite clickers", idle games) are a fantastic fit for Flutter, and are also super popular on mobile right now.
- AI(Filip): Apart from that, I also need to figure out how to introduce some text input into the game. That is the last missing component that will be useful to most game developers.
- I will use this document, the devlog, to:
- Provide progress reports
- Record pain points
- List resources I've found useful
- AI(Filip): start a private github repository.
- Filip will be using his company's accounts on Google Play Dev Console and App Store Connect in order to fully test integrations with services such as leaderboards, Game Center, and so on.
- The game will never be published on either store. But it might be accessible through things like TestFlight and Google Play Internal Testing.
- AI(Filip): Start implementing the skeleton of the game as a Flutter project.
- This first version will be ugly and won't have the platform features yet, but:
- We'll have a sound foundation for the code.
- I'll be able to put the game on a (secret) URL so the team can check it out and give me guidance. It'll be much faster than giving you a wireframe, since the devil is in the details, and if I work in Flutter from the get go, I can work out the details much sooner. (I mean, for example, what happens right after the player wins a game? Does she first linger on the screen with some form of fanfare, and then automatically goes to a leaderboard screen? Or do we give her a chance to replay? Stuff like that.)
2022-02-07 Initial implementation
Days worked: 1
Trying to get to a working game skeleton as soon as possible, since the meat of the problem will be with the integrations and the polish.
Here's a "playable" demo of what I have right now:
- The goal of this demo is to allow a more direct feedback loop later this week. If you already have something major for feedback, of course let me know, but right now, I'm just glad that it even runs on the web like that and I have everything set up more or less correctly. :)
- I will actively ask for feedback later this week.
Code is here (private repo, please ask for access):
Next AIs:
- Fix: vertical sets don't win?
- Ah, it's because we ignore AI wins, and so if we detect an AI win before a player win, we do nothing.
- Sounds / Music
- "Remove ads" (later an in-app purchase)
- Reset progress (later removes Cloud Save in Play Games Services / Games Center)
- Add Achievements skeleton
- Add at least one more "Level"
- Show interstitial ad before every level (except first)
- Responsiveness (tablet / mobile)
- Experiment with messages bubbling up (instead of tight coupling of, say, BoardTile with BoardState), inspired by Unity. Probably some PubSub object at the top
- Show some kind of an animation before player wins
- Think: how does GameState lock the screen, and how does it show an animation? Does
- Think: where to put a text input? Leaderboard?
- Idea: use Firebase / Cloud to share screenshots (viral)
- Idea: tutorial on level 1, something like https://pub.dev/packages/overlay_tutorialÂ
- Idea: The lowest AI level actually actively goes out of your way. A higher level just tries to score ASAP, and will block lines, but will actively stay away from crosses (so that tactic works). We end up with a really good, aggressive AI.
- Idea: Game broadcasts achievements messages
- Ask for input
2022-02-08 Getting to something feedback-worthy
Days worked: 2
Going through the AI list from yesterday. Things achieved today:
- Fix: win sometimes undetected
- Sounds / Music
- "Remove ads" (later an in-app purchase)
- Reset progress (later removes Cloud Save in Play Games Services / Games Center)
- DONE
- Add Achievements skeleton
- Add at least one more "Level"
- Implement a "player state" (which level won etc.)
- Which levels were finished
- Which one is (are?) available to attempt
- DONE: Always the one just above the "highestReachedLevel"
- Possibly something like a "star rating" for each level, or "time to win"
- Show interstitial ad before every level (except first)
- Responsiveness (tablet / mobile)
- Experiment with messages bubbling up (instead of tight coupling of, say, BoardTile with BoardState), inspired by Unity. Probably some PubSub object at the top
- Meh, maybe later. We are fine with a ChangeNotifier.
- Show some kind of an animation before player wins
- Think: how does GameState lock the screen, and how does it show an animation?
- DONE: ignores pointer and paints the winning line red
- AI: make it prettier and more exciting
Summary:
- Prototype now much closer to something I can get feedback on.
- Still need to implement all the "placeholders", though, so no feedback requested yet.
- Namely, at the very least, I want to implement the "interstitial ad" placeholder.
- Ideally, also some kind of "sharing" functionality (for text input and Firebase integration).
2022-02-09 First prototype worthy of some feedback
Days worked: 3
Okay, I feel that the current state of the sample is far enough that I can ask for feedback.
Playable demo:
https://filiph.github.io/flutter_game_sample/mobile.html (access this on desktop for best results)
Of course, most of the functionality isn't there yet.
Here's the kind of feedback I'm looking for:
- Am I working on the wrong stuff? Like, did I completely misunderstand something about the requirements? Possible, and it's still okay to pivot to something else.
- Is the sample likely to "hit" all the boxes?
- Here's what I think the sample will easily demonstrate:
- Integration with Mobile Ads (both interstitial and banner/text)
- Integration with Apple Game Center and/or Play Games.
- Leaderboard
- Cloud save
- Achievements
- (stretch goal) Multiplayer
- Integration with In-app purchases (to remove ads, in our case)
- Integration with Firebase / Google Cloud (for saving and sharing game screenshots)
- Sound & music
- Internationalization
- Text input
- Responsiveness (mobile - tablet)
- Are there other boxes I should absolutely be trying to hit?
Assuming there's no feedback to the questions above, we can talk about details of the implementation. Of course, the current implementation is terrible, but I wonder:
- Is the minimalist / organic drawing style something we want to keep even beyond the prototype? I imagine animations of drawing and ink that will be both original, pleasing, and in the theme of the game (played on paper).
- How long should I try to keep the game running on the web? I can imagine keeping it there even after launch, for people to try the sample before reading it. This wasn't in the initial requirements, though, and will make things a bit harder/slower (will need to make sure things work on 3 platforms instead of 2, and web doesn't support many of the integrations we're trying to highlight).
2022-02-09 Consolidating feedback, going forward with prototype
Days worked: 4
Now that people had a chance to give initial feedback on the early prototype, Â asked me to codify the design in a written format (as opposed to just having it be an ephemeral prototype on the web).
Initial design
This is a single-player mobile game with mobile ads, in-app payments, a leaderboard, achievements, sound and music. The player progresses through a number of levels, in which she is pitted against increasingly tougher AI, playing increasingly complex versions of tic tac toe.
The player's performance in each level is scored, and she can attempt to retry each level for a better score. The game also awards achievements. Both the scores and the achievements are managed through each platform's SDK (Game Center on iOS and Play Games on Android).
There are both banner and interstitial ads in the game, served via Google Mobile Ads SDK. The player can pay to remove these ads via an in-app payment.
The game looks great on phones (with notches or without) and tablets.
Goals
The fact that we have the design down means we can finalize the P0-P2 features.
- P0: Looks great (in order to demonstrate Flutter's ability to build beautiful UIs)
- P0: Actual working game from start to finish, as opposed to a tech demo (this is a requirement for being included in the App Store)
- P0: Integration with Mobile Ads (both interstitial and banner/text)
- P0: Integration with Apple Game Center and Play Games
- P0: Leaderboard
- P0: Achievements
- P0: Integration with In-app purchases (to remove ads)
- P0: Music
- P0: Sound
- P0: Responsive design ​​(phone - tablet)
- -----
- P1: A web-based 'Lite' demo
- P1: Integration with Firebase / Google Cloud (for saving and sharing game screenshots)
- P1: A desktop-based ‘Lite’ demo. Ultimately, we want to show a picture where the same game can be played on multiple platforms.
- -----
- P2: Internationalization
- P2: Use Firebase's Remote Config to test if ads should be shown or not
DevLog for today
Today was all about trying to disprove my hypothesis that I can implement the P0-P1 features in time, while also documenting what I found.
The final verdict is that, despite trying, I wasn't able to find any show stoppers to any of the P0s. I have a good idea of what packages I will use, and how they will interact with each other (e.g. sound vs music, in-app vs AdMob). I also played a bit with graphics to see if they can be sketch-like without seeming "cheap", and I already have a few ideas.
Task: Check that there is no show stopper for adding Mobile Ads, get the lay of the land
- Banner ads have a nice API.
- The documentation warns about slow performance when using Banner ads on Android 9 and lower inside a scrolling view. This is great, as it prevents later suprises. In our case, we're keeping the banner anchored, so no biggie.
- Interstitial ads actually don't get added to the widget tree by the developer. The developer just loads the app, then shows it. The SDK will put the ad into the Overlay.
- AI: move interstitial ads _after_ the level is complete, not before. Better placement, according to this.
- "Native" ads are not a good fit since they are clearly meant for Android SDK / iOS UI kit. The "native" in the name means that the ad unit is a NativeAdView (a Java class) on Android and GADNativeAdView (an Objective-C class) on iOS. These can be wrapped into a Flutter app, of course, and the AdMob SDK makes it easy, but that just seems like jumping through hoops.
- Rewarded ads are a great fit for free-to-play games with some form of game economy. They allow the player to earn in-game rewards for watching ads. Our game doesn't have an in-game economy, so we're skipping this. But implementing them is not any more complex than implementing interstitial ads, at least on paper.
- There's some (understandable) bureaucracy involved in creating an AdMob account. I didn't encounter anything that would be Flutter-specific. But I want to make it clear that setting this up is not as 1-2-3 as the marketing might make it sound. You need to verify yourself, add a bank account, etc. There's something called a "Publisher ID" which is attached to a Google Account, so (I presume) you need to create a separate Google Account for an organization if you want the money to go to a company and not an individual. Little things like that. Nothing insurmountable, but still takes time to figure out.
- Test devices
- You need to tell AdMob about your test (development) device in AdMob's UI. You have to find the device's advertising ID (IDFA). Here's how.
- When you have the ID, adding the test device is easy in the UI. Go to Settings > Test devices > Add test device.
- On a test device, you don't need to set up test ads (below). But if you want to check that ads are showing on a variety of devices, you need test ads.
- On the other hand, a test device will show production-looking ads (it's just that the views and clicks won't count).
Task: Check that there is no show stopper for adding Game Center / Play Games support, get the lay of the land
- There are a few unofficial packages that include both Game Center and Play Services/Games support:
- 82% popularity, 125 pub points, updated 6 days ago, min Dart SDK 2.12
- Unverified, by Abedalkareem Omreyh
- AI: we might want to nudge Abedalkareem to verify his pub account. It's a blemish on an otherwise solid package.
- API looks very nice (maybe too nice? as in, maybe it's too simple?)
- 53% popularity, 120 pub points, updated 6 months ago, min Dart SDK 2.12
- 59% popularity, 120 pub points, updated 6 months ago, min Dart SDK 2.12
- Unverified and version 0.1.1
- "only" links Firebase Auth with Game Center and Play Games
- Packages that only support Apple Game Center (and not Google Play Games)
- 11% popularity, 100 pub points, no null safety
- Packages that only support Google Play Games (and not Apple Game Center)
- 38% popularity, 110 pub points, updated 4 months ago, min Dart SDK 2.12
- pretty nice API
- 52% popularity, 100 pub points, no null safety
- since we're showing achievements in the native UI (as we should), the implementation looks easy. We just need a leaderboard / achievement ID, and we call a simple static method.
- API
- signIn()
- showAchievements()
- showLeaderboards()
- submitScore() - to leaderboard
- unlock() - an achievement
- increment() - an achievement
- getPlayerID()
Task: Check that there is no show stopper for adding In-App purchases, get the lay of the land
- Official plugin
- Long documentation with plenty of examples
- some of the examples might do with a bit of a syntax cleanup. For example, `const Set<String> _kIds = <String>{'product1', 'product2'};` is just `const _kIds = {'product1', 'product2'};` in modern Dart.
- AI: submit a PR if time permits
- The API is a bit hard to use. (But hey, it's generalizing over two different stores, so I'm not complaining.)
- You must listen to a stream as soon as possible (they say "your app's initState", so I guess providing through a ChangeNotifierProvider is too late.
- This Stream (purchaseStream) seems to be your only way of getting updates. It's not `Future<Result> doSomething()`, it's "listen to all updates from the start of the app and figure out how to respond yourself".
- API (through InAppPurchase.instance)
- purchaseStream - the only "out" from the API
- you get PurchaseDetails with things like purchased / restored / error
- restored purchases should be validated (iOS, Android)
- completePurchase() - to mark the purchase as finished (otherwise it will be refunded after 3 days)
- isAvailable() - can connect to the store
- queryProductDetails(Set<String> ids) - load products for sale
- buyNonConsumable()
Task: Get the lay of the land of audio plugins
- Packages that provide the kind of audio play we want
- configures things like "I want to pause the sound when the navigation app starts speaking directions"
- 99% popularity, 120 pub points, updated 5 months ago
- "play multiple simultaneously audio files, works for Android, iOS, macOS and web"
- works on Android, iOS/macOS and web
- web only supports some audio formats (see here). The safest ones:
- mp3
- AAC (Advanced Audio Coding)
- 95% popularity, 120 pub points, updated 5 months ago
- "caches audio tracks in memory"
- NOT for playing music, but might be great for one-off shots
- AI: see if sounds are too slow to play in other packages. Only if so, use this package.
- some convenience on top of pkg:audioplayers. It has a dependency on flame, so probably not going to use this, but might be a good inspiration, code-wise.
- 81% popularity, 120 pub points, updated 4 months ago
- "audio play from local file"
- zero dependencies
- Packages that are not what we want (provide music/audio playback with native controls)
- Audio packages that are unmaintained
- Summary:Â Looks like I'll start with audioplayers. I have experience with this package. It's a bit unwieldy but is probably the only real option, especially if we want to also support web. I'll possibly use pkg:audio_session and pkg:sound_pool.
Task: See if we can make the game look nice while keeping the minimalist / sketch-like style
- The worry is that the game might look cheap.
- I only worked in Affinity Designer today, and only shortly, but I'm pretty confident I can make this look great.
- Here's a sketch that takes it a bit too far, but shows the "organic" feel more clearly than the web demo:
Task: Explore music options
- Worse comes to worst, I can contribute my own music, in order to make 100% sure there are no legal problems or loss of goodwill.
- That said, it would be great to have some actually good music maybe? :) For that, the best approach is to go with one of two options:
- Purchased license to a song or two, with the express permission to use in an open source project.
- Go with a CC-BY or CC0 lincensed music.
- Jason Shaw is providing just that: https://audionautix.com/free-music/blues/. I would stll want to check with him whether he's fine with his song being on GitHub, for example. Same for the folks below.
- There are free game music packs with very permissive licenses, too.
- There's CC0 music in FMA. For example:
- looks like I can make an arrangement with Mr Smith, whose music is CC0, but will also give express permission to use it in the project.
- Mr Smith's music is fantastic for the game, and for mobile gaming in general. It sounds great even on tiny speakers, but also sounds fantastic when using headphones.
- pre-selection:
- The Mariachi, The Introduction, Reflector, Azul, This Could Get Dark, The Get Away, Black Top, Sunday Solitude, Sonorus, Pequeñas Guitarras, Lamento
- Alt (voice, perc): Trash Town Boogie, Discovery, Crunchy Funk, Blood in the Water, Big Sky Spy, Action, Ether, Poor Man's Groove, 3DJC
- Summary:Â We'll have good music, with no strings attached.
2022-02-12 Making the AI better (by making it worse)
Days worked so far: 4½
- If the game is to be actually fun to play, the AI must be better than random, but definitely worse than perfect. Today, I first fixed a bug in the AI that made it perform in a weird way (for example, the AI was less interested in some positions even if they were clearly important - once the player learns this, it's painfully easy to win every time). When the AI was fixed, I realized that playing a perfect opponent is no fun. I then made a version of the AI that emulates "an eager child" -- only attacks, and defends when it's often too late.
- I have ideas on how to make the AI even more fun to play.
- It should, like a poor human player, pursue doomed places. Like when there's only room for 4 tiles in a 5-in-a-row game, it should still strive for it. Currently, even the intentionally poor AI will (correctly) ignore those tiles.
- It should focus on the obvious next move (this requires implementing history - at least "what was the latest move by opponent/me?" but possibly also the last two moves by the opponent, in order to see the direction). This makes it possible for the player to "feint" - and the computer gets fooled on lower difficulties. Most human players do focus on the "obvious" next tile.
- The AI should focus on attacking more than defending. (Already implemented, in a way.)
- It should not see places that are intersections. The current AI (correctly) sees these places as important by the nature of its scoring. The "worse" AI should be more human-like at it, and not see non-obvious intersections.
2022-02-16 Animated marks
Days worked so far: 5
- The last big thing to validate was that we can make the minimalist, sketch-y graphics look good.
- I experimented with Shaders and Canvas today. This is the result:
- Now, it's still very early (marks too uniform, grid lines too straight, clash with the rest of the UI), but with enough iteration, it will look nice.
- Unfortunately, the "drawing" animation is achieved through shaders, and those are not available on the web (I think?). So we'll need to either recreate a similar effect using "frames", or use some other way to animate on the web (e.g. fade in, which looked fine in my limited testing).
- I also removed pkg:rough (no longer needed for the grid) so the game is now fully null safe. (This was always the plan, of course, I just didn't bother till I knew we're on the same board in regards to the design.)
2022-02-17 High quality images versus performance
Days worked so far: 5½
- Today, I continued making progress on the graphics. Visually, the marks (X and O) are still more or less prototype-quality, but I implemented the following:
- Marks are bundled in device-pixel-density-appropriate sizes, so they look crisp on every screen.
- The game has several variants to choose from, so a line of crosses looks more natural than if they were all the same.
- This effect is still somewhat ruined by the fact that the grid is uniform, but I'll get to that.
- I'm using ResizeImage to make sure the game only deals with data that it needs and not more. Since the game needs to sometimes render the marks at almost the size of a third of the screen, and sometimes at the size of just one tenth of the screen, I can't leave it to Flutter's automatic asset management.
2022-02-18 Flavors, plus making it prettier
Days worked: 6½
- Looking at flavors for a bit, to see if they could help me test better. (For example, having a 'Lite' version for the web, or having a debug version that completely loses some of the functionality such as ad serving.)
- We don't actually need Android flavors.
- DONE: I implemented a simple String.fromEnvironment constant that lets me define, compile time, what features should be included.
- Currently, the only difference between 'full' and 'lite' version is that the lite version says "Lite" on the main screen. But later, we can easily remove content from the lite version, and redirect people to a URL / store page.
- The thing that still makes the game look cheap, in my opinion, is the inconsistency between the human-like marks and the uniformity of the grid.
- DONE:
- It may be subtle from a distance but it totally works for me. The lines are now a tiny bit "rough".
- I experimented with a ton of color schemes. Finally, I arrived at a color scheme extracted from an actual photo of a paper notebook, combined with some additional help from coolors.co and the Material Design theme maker. I'm going for a subdued, realistic color scheme, with a deep red accent.
- I also added an appearing effect to the grid, so it "draws itself" just before play. And I made the buttons in the game a little nicer.
- After today, we have this:
- Next, I want to start working on mobile-only features, like in-app purchases. If y'all want to follow my progress there, I can add you to the "Internal testers" group.
- Testing on a mobile phone also shows the full beauty of the design. The web experience is a lot slower and has less effects (no ShaderMask, see above).
- I can do this on Android or on iOS (or both). At this point, I'd prefer Android, as it's easier to set up and manage than TestFlight. But I'm open to both.
- Questions:
- How important is it for the team to follow my progress?
- If not very important, I would still prefer if someone was my "tester". Someone I can throw builds at and ask questions. If nobody is interested, that's also fine (I have a wife).
- In case anyone's interested, I'll need their email address associated with their Google Play (or, alternatively, Apple) account, so I can add them to the tester list.
- Follow-up question (Feb 21)
- Ok, it looks like we'll have several Googler testers, on both Android and iOS, which is great!
- This week, I'll create the App Store / Play Store game records (among other things, of course). For that, I need to come up with a good app identifier. This is really hard to change later, especially after the app has been published to the store (even the first, non-public version).
- This identifier is not visible to users*, but it's visible to developers and to Google and Apple employees reviewing the app/game. In theory, it shouldn't matter what the identifier is (as long as it's unique), but I still stress over it a little bit.
- Right now, the identifier for the Flutter game sample is com.example.flutter_game_sample. This is not good, imho, since it might lead to a knee-jerk reaction from an Apple employee (they don't approve tech demos into their store; our game will be a lot more than a tech demo, but I wouldn't blame them from thinking that, looking at the app ID).
- I propose the following alternatives:
- dev.flutter.tictactoe -- generic enough, tied to Flutter itself. Immediately recognizable.
- com.example.tictactoe -- example.com is the standard "example" domain. Might have a bit of the same vibe as the current ID, but at least it doesn't say "sample" in it.
- com.raindead.tictactoe -- Tied to Raindead (my company). This is in suit of past samples, like the History of Everything app from Flutter Live 2018, which has the id "com.twodimensions.timeline". This makes sense if we go the safe route and release the game under Raindead.
- net.filiph.tictactoe -- Tied to my person. Might be a better look than the company?
- If there is no reply from the team, I'll default to "com.raindead.tictactoe", since that seems the most natural to me, given the plan. But I'm listening.
- DONE: After discussion here in the doc comments, it's "dev.flutter.tictactoe", in the similar vein to "dev.flutter.veggieseasons" (here).
- It will be possible to change this in the future, but it'll be quite a hassle, so if we can get a decision now, it'll save time.
2022-02-22 Responsive design
Days worked: 7
- Implemented responsiveness, so that the game looks good even in portrait mode or on a tablet. Not perfect by any means, but it's something I wanted to do now so that I don't need to duct-tape it on later.
- On very narrow or wide screens, the grid "stretches" a little bit to fill the screen better. I'm not sure if I want to keep this, but I wanted to try it:
- Hmm, now that I see it after a while, I'm pretty sure I don't want this. Welp, at least it's very easy to remove.
- To a point, it's okay, and actually looks fine, but when stretched too far, it looks pretty bad. I might make an "adaptive aspect ratio" that isn't as strict as AspectRatio but still prevents the grid from going crazy.
- You can now use the web experience in two modes:
- For desktop. Forces a portrait, mobile-like viewport.
- For mobile and desktop. Fills the whole broser.
- Both modes are super slow, especially on mobile. I'm not wasting time on optimizing the web experience at this point, but I plan to do it later.
- Experimented with the level selection screen. Still too early, though. We don't even know how many levels, and if one of the levels will be "special" in some way.
2022-02-23 Full screen, confetti and AI
Days worked: 8
Today, I am focusing on getting the game ready for feedback. That means that all parts of the game should fit together. Things will still behave and look unfinished, of course, but at least there shouldn't be times when a user has no idea what just happened or why.
- Fixed the stretched grid from yesterday.
- Made sure the game plays nice with notches (using SafeArea in the ResponsiveLayout widget).
- Implementing "full screen"
- Implemented a very naive confetti animation after winning a level. This will take time to get right, but at least now it's clear what the point of the wait after winning the game is. Later, I can make this both look better and be more performant.
- Implemented a more humanlike AI. Still needs tuning, but at level 6, the AI gives me a good challenge while staying beatable.
- The good thing about this AI is that it's based on a sliding scale. I can say "strength = 0.4" or "strength = 0.35". This allows me to tune the "humanlike-ness" of the play.
- The AI is still completely deterministic, which helps with the puzzle nature of the game. When faced with the same situation, the AI always plays exactly the same.
- This is easy to change. For example, for the "final boss", the last level, we can introduce randomness so even the expert player gets a surprise.
- Also implemented unit tests for the AI, to show how that can be done.
- TODO: Remove some of the more puzzling moves, such as putting a tile in the middle of nowhere.
- TODO: Introduce a more gradual difficulty curve. Right now, we go from very poor play to pretty good all at once.
2022-02-24 Bugs bugs bugs
Days worked: 8½
- I played the game today as if I was an actual player and found some bugs, especially in the AI. I'm fixing the most egregious bugs today.
- Tweaking took a lot of time and I'm still not completely happy, but at least the first 5 levels are playable and winnable, without completely braindead mistakes by the computer.
- I'm about ready to publish a test version to the internal testing (e.g. TestFlight) services. I do need to have a decision on the id.
2022-02-25 Publish publish publish (to internal testing)
Days worked: 9½
- We agreed on the game's "app id": dev.flutter.tictactoe. It follows suit of dev.flutter.veggieseasons. I have updated the code. (Must call `flutter clean`, otherwise you might lose time trying to find where that last old string is coming from, like I did.)
- Switched the web version to use canvaskit. This makes it much smoother, and also enables the shaders. Can't believe I didn't catch that before.
- Running the game on a very old device with a small screen:
- The main screen needs improvement - logo should be the largest element on the screen.
- Padding of marks (X and O) should be proportional to tile size. Now it's not, so on small screens the marks are tiny.
- Provisioning profile. It was missing, and the Flutter tool told me exactly what I needed to do! I haven't been this delighted with DevExp in a long time. I guess a big reason for that is that, in the past, I almost pulled out my hair trying to figure out what to do in exactly this situation. Huge kudos to whoever's responsible for this feature.
- Forgot how unintuitive Xcode is. (I'm using Codemagic for CI publishing.) For about the third time in the last few years, I had to look up how to "Archive". Archive, for the uninitiated, is a code word for publishing, basically.
- The app name: it must be globally unique. Every single app name I tried was already taken (Tic Tac Toe, The Tic Tac Toe Game, Tic Tac Toe: The Game). I ended up using "Tic Tac Toe Puzzle Game", because that's what it is. (I was really tempted to use something like "Tic Tac Two: The Reckoning", but I realize it would only be trouble. Better to stick with generic terms.)
- Forgot the huge "Flutter Sample Game" title on the main screen. That would be hilarious if it went to the apple reviewers, after going through all the trouble with the app id.
- I may be holding it wrong, but it seems that if I want to upload a new version to App Store, I have to first `flutter build ios` and then go to Xcode to do Product → Archive. Because Product → Archive also takes a long time, it seems like it's building the project. But it's not? Because the version never updates in Xcode. I have to run `flutter build ios` for the version to update.
- Publishing to Google Play
- I got cocky and thought I can just upload the app bundle to Google Play. Not possible (https://stackoverflow.com/a/51689069/1416886). My bad, I should have read the docs again. It works once you do the one-time setup, but it's been a while that I created a new app. The docs are clear. It's just that Google Play Developer Console says "You uploaded an APK or Android App Bundle that was signed in debug mode.", and I just saw "debug" and automatically assumed it's a problem with my `flutter build` invocation, and not a problem in the build.gradle file.
- Testers (extracted from a doc comment above)
- Implemented the first stab at audio
- Reacts to app lifecycle changes
- Plays only one music track at this point
- Able to switch sound off from the main screen
- Nothing is saved at this point, so even the sound will restart after you've killed the app. I'm on it, no worries.
2022-02-28 Finishing first test release
Days worked: 10
- In debug and profile mode, things work, but for some reason, release mode doesn't. I wasn't aware of this until the release was rejected by Apple.
- "Your app, Tic Tac Toe Puzzle Game, was not approved for beta testing"
- "We discovered one or more bugs in your app. Specifically, a blank screen is displayed indefinitely after launch. Please review the details below and complete the next steps."
- Screenshot
- Reviewing the Build and release an iOS app page, I'm learning that we don't need to go via XCode Product → Archive anymore. I can just `flutter build ipa` now. This reminds me how often I keep doing things the old way despite there being a new way.
- Weirdly, the release through TestFlight doesn't work, but when I build the app locally in release mode and upload it to the same device, it does work.
- FIXED. Ok, this was a foot-gun. I forgot to define a flavor when building the app for TestFlight, and so it crashed on startup. My own code did this. :facepalm:
- flutter build ipa --dart-define flavor=full
- Audio now correctly pauses when game not in foreground.
- Y'all should be getting the first test release to play with today.
- March 15
- Docs might be more important than some P2 features
- How to use this sample to build upon
- TODO: Shell of a game - starting point
- audio
- leaderboard
- ads
- in-app
- settings menu
- level selection
- "start editing lib/game.dart to make your game"
- TODO: make the folder structure so that the game is in a separate folder from the shell. So we have:
- TODO: tag XYZ for issues that would be great to fix by engineering
- TODO: by next monday, is Filip able to deliver a 30 minute how to video using the shell, including basic game concepts. This is for people who are existing flutter devs interested in gamedev. Deadline: before I/O? April 15th.
- if not, then I need to deliver the shell sooner, in order for someone else to start working on the video
2022-03-01 Ads
Days worked: 11
- By the way, this got buried before, but I'm happy to add anyone who asks to the private repo. In fact, I'd welcome if someone gives me a general feedback on the code — not a full code review, but more like "hey, this looks kind of stupid" or "why don't you do X instead?" The code is obviously WIP, but it shouldn't be straying too far from best practices at this point. Just give me your github IDs and I'll add you.
- Ready to add mobile ads! 💰🤑 Here we go!
- The package:google_mobile_ads README shows a big failing badge at the top. I of course understand this, and assume it's just a failure of the CI and not an actually broken package, but a newcomer could get easily discouraged.
- It appears the build has been broken for 14 days at this point? Here.
- The AdMob getting started guide asks me to make sure that I compile for target Android API level of 19+ and compileSdkVersion of 28+. I'm pretty sure that's the case but I wanted to check and found out that these numbers are no longer in app/build.gradle. Or, they are, but they forward to somewhere else.
- I searched for compileSdkVersion and expected a line like this: `compileSdkVersion 28`. Instead, I got this line: `compileSdkVersion flutter.compileSdkVersion`. There's no  compileSdkVersion key in local.properties.
- I don't know gradle syntax, but I think the `flutter.compileSdkVersion` symbol comes from this line: `apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"`. But looking at "External libraries" in Android Studio, no such package is included.
- Finally, I went straight to github.com/flutter/flutter, navigated to packages/flutter_tools/gradle, and found the file. But I'm afraid this will not occur to the regular developer. Possible solution: add a line comment to the generated build.gradle file that tells people where to find this stuff. Something like this:
- Anyway, turns out we're on target level 31 and compile version 31. Good to go with Ads.
- I need an app ID (to add to AndroidManifest.xml). Let's "add" an app in AdMob.
- AdMob doesn't let me create the same app ID for both iOS and Android. Not terribly surprising, just a bit of a let down.
- Next question from AdMob: "Is the app listed on a supported app store?" Meaning, is it listed on Google Play?
- Through quite a few clicks and searching I got to this section of AdMob docs, about adding apps that are not yet listed in any store.
- I'm sorry, AdMob, but what's the percentage of people who are adding AdMob after they've published their app? Shouldn't this be listed a bit more prominently?
- "Choose this option if your app isn’t listed to an app store."
- It's unclear if internal testing / TestFlight might constitute "listed", but I assume not.
- Maybe I just got thrown off by the question. It makes it seem almost like I'm doing something wrong adding an app this soon.
- The list of supported app stores in AdMob's documentation doesn't include Google Play nor App Store.
It's a small thing, but, yeah.
- I created the app with "not listed" and found the app ID in "App settings". Back to the Getting Started guide!
- Initializing the plugin is easy, and follows convention.
- Starting with a banner ad, as I think it's the most obvious use of ads. (Later, I plan to implement the interstitial as well.) Quick start continues here, then.
- Loading and showing a banner is a bit of a manual process, but I actually welcome it since it reminds me that this is a platform view, after all. I'm implementing a MyBannerAd that contains the bare minimum boilerplate to make this work.
- The official guide code is helpful. I could also use a prominent link to a full example code.
- I decided to check if there's something like that in the Flutter package itself, and there is. Maybe add a link there from the quick start guide? On the other hand, the example in the package tries to (understandably!) show a lot. I was looking for sample code that just does a single type of ads (a banner).
- Ah, here we go with the single example, on the package's github repo.
- The official guide also mentions getSmartBanner, which is a function that doesn't exist in Flutter-land, to my knowledge.
- But the example above shows the use of `AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize()`
- I decided to make it so that my banner ad widget will straight out break the game if you try to add it on a platform that AdMob doesn't support. (To be more precise, the behavior is undefined for such cases.) So you have to do something like `if (Platform.isIOS || Platform.isAndroid) return MyBannerAd()`.
- I think it's a fair API since most people will want to do something completely different on platforms that can't show ads. Like, you don't just want a blank space when there are no ads. You want a completely different layout, possibly even skipping a whole page.
- I could imagine an API that defines some kind of fallback. Something like `MyBannerAd(fallback: SomethingElseHere())`. But I think it's silly in any real app.
- Running in debug after implementing all this leads to an error:
- Execution failed for task ':app:processDebugMainManifest'.
> Manifest merger failed : uses-sdk:minSdkVersion 16 cannot be smaller than version 19 declared in library [:google_mobile_ads] /Users/filiph/dev/flutter_game_sample/build/google_mobile_ads/intermediates/library_manifest/debug/AndroidManifest.xml as the library might be using APIs not available in 16
         Suggestion: use a compatible library with a minSdk of at most 16,
                 or increase this project's minSdk version to at least 19,
                 or use tools:overrideLibrary="io.flutter.plugins.googlemobileads" to force usage (may lead to runtime failures)
- Once again, Flutter Fix translates this into normal language ("The plugin google_mobile_ads requires a higher Android SDK version.") and suggests an edit. Have I mentioned how I love this feature and that whoever is even remotely responsible for adding it to the flutter tool should be immediately promoted?
- Here we go again with flutter.* in gradle files
- But this time, I'm ready, and I just put a 19 there. (I wonder if some developers might go on a hunt at this point, trying to change `flutter.minSdkVersion` somewhere else instead of just changing their app's build.gradle file directly.)
- Ha! After fixing the above, I get to see another error and Flutter Fix. This time, it's our good old friend Multidex.
- "The number of method references in a .dex file cannot exceed 64K."
- This time, the Flutter Fix message is a little less readable:
- "Flutter multidex handling is disabled. If you wish to let the tool configure multidex, use the --mutidex flag."
- I'm not sure if everyone understand what "the tool" means in this context. I assume it's the flutter CLI tool.
- And so I open a terminal (I'm using Android Studio otherwise) and run `flutter --multidex` first (doesn't work), then `flutter run --multidex`. This starts building, and stops at the same error:
- But it asks if I want to automatically add multidex support, which is fantastic! I say yes, and … it works!
- But then, when I want to build back in Android Studio, I get another exception:
- /Users/filiph/dev/flutter_game_sample/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java:8: error: package androidx.multidex does not exist
import androidx.multidex.MultiDex;
            ^
/Users/filiph/dev/flutter_game_sample/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java:18: error: cannot find symbol
  MultiDex.install(this);
  ^
 symbol:  variable MultiDex
 location: class FlutterMultiDexApplication
2 errors
FAILURE: Build failed with an exception.
- I have no idea why it works on the CLI and not in Android Studio. Must I use the `--multidex` flag every time now? (Adding flags to Android Studio runs is less straightforward than doing it in the terminal.)
- I try adding `--multidex` to the run configuration:
- But no luck.
- Weird. Running in the terminal actually says "Building with Flutter multidex support enabled." even when not using the `--multidex` flag. But in Android Studio, it's not there. I'm pretty sure I'm using the same Flutter installation…
- I verified that I'm using the same Flutter.
- `flutter clean` doesn't help
- Restarting Android Studio doesn't help
- It's like the --multidex flag is ignored by the Android Studio
- Running in --verbose, the first mention of multidex is with the error itself. This is incredibly frustrating. Why isn't `--multidex` being picked up from the "additional run args"? Everything else is, including the --verbose flag.
- I don't know how to inspect what Android Studio is really launching.
- For the record, I'm using Flutter version 2.10.2 and Android Studio Flutter plugin 64.1.2.
- Searching StackOverflow
- Digging in the code, looks like this conditional block somehow fails to run when running from Android Studio. Thankfully, I was able to understand that there's a "secret" flag, `-Pmultidex-enabled=true`. So I added it to the run configuration in Android Studio:
- And now the game compiles and runs even in Android Studio.
- Not fixed, but at least there's a workaround.
- I should say that I'm using FVMÂ to manage Flutter installations on my computer. Maybe that's the problem? But, like, I do point Android Studio at the right directory.
- Ok, back to work.
- I/Ads   (  390): Use RequestConfiguration.Builder().setTestDeviceIds(Arrays.asList("04DD38B1CD91DDB03282FDE81BBCD861")) to get test ads on this device.
- This is helpful. Though of course, one needs to "translate" this from Java to Dart. The salient part is, of course, the ID.
- I/Ads   (  390): Ad failed to load : 0
- Not very helpful. This is from the plugin side.
- Thankfully, I also get this from the Dart side:
- I/flutter ( Â 390): WARNING: 2022-03-01 13:31:41.414089: MyBannerAd: Banner failedToLoad: LoadAdError(code: 0, domain: com.google.android.gms.ads, message: Unable to obtain a JavascriptEngine., responseInfo: ResponseInfo(responseId: null, mediationAdapterClassName: , adapterResponses: []))
- W/Ads   ( 2395): Error while pinging URL: https://csi.gstatic.com/csi?app=dev.flutter.tictactoe&os=11&e=&network_coarse=-1&task.request-parcel=s.3&task.renderer=f.1&task.webview-cookie=s.39&task.server-transaction=f.10696&task.signals=s.832&s=gmob_sdk&is_lite_sdk=0&v=3&action=ftl&api_v=30&sdkVersion=afma-sdk-a-v212910000.212910000.0&network_fine=0&label.ttc=f.11534&device=HMD%20Global%20Nokia%201.3&ftl=0&ed=com.google.android.gms.ads. Unable to resolve host "csi.gstatic.com": No address associated with hostname
- This makes a lot more sense, since my development device is, by default, not connected to the internet. :facepalm:
- Fixed by logging onto the wifi with the dev device.
- It took _several seconds_ (in debug mode). I do use a slow device, and it probably needed to initialize everything first. Still. I was ready to assume it didn't work at all.
- Wierd log:
- E/chromium( 2395): [ERROR:cookie_manager.cc(129)] Strict Secure Cookie policy does not allow setting a secure cookie for http://googleads.g.doubleclick.net/ for apps targeting >= R. Please either use the 'https:' scheme for this URL or omit the 'Secure' directive in the cookie value.
- The plugin is using an API in a non-standard or obsolete way, it seems?
- Ads change by their own, about once a minute.
- When going from the page that shows the ad to a different page, at least in slow/debug mode, the ad stays on screen for a fraction of a second after it should have gone.
- There seems to be a race condition in my code. And I basically copied the code from the AdMob sample. Let me investigate.
- When showing the banner ad widget, I see that the ad is being loaded twice in a quick succession (like, a few milliseconds apart at most).
- Yep, after sprinkling the code with some logging, I found out that the _loadAd() is first called from `didChangeDependencies()`, and then immediately called from the OrientationBuilder callback in `build()`. And the same thing happens in the official sample.
- Fixed by moving from a single _isLoading boolean to an enum: _LoadingState that goes through initial → loading → loaded [→ disposing → loaded]. No more double-calls to a platform channel, and double-loads of expensive platformviews!
- I'm not sure if I want to put this change upstream, though, in its current form. I may have over-engineered it.
- DONE: Uploaded the Android version to Play Store.
- Next: iOS
- Back to the top of the Quick start guide.
- Something I noticed before with the Android code and now with the iOS code: after modifying the AndroidManifest.xml / Info.plist file, the guide says: "You must pass the same value when you initialize the plugin in your Dart code." But then, I never really need to use the value again. Maybe this was true for a previous version of the plugin?
- The guide looks promising. After changing Info.plist with two lines of code, I don't have anything else to do than to test the app on an iPhone.
- I spoke too soon. Pod installation fails.
- [!] `GoogleAppMeasurement` requires CocoaPods version `>= 1.10.2`, which is not satisfied by your current version, `1.10.1`.
- I have to say it took me some time to find the error. The one at the bottom was a red herring ("Automatically assigning platform `iOS` with version `9.0`..."), and there was a huge number of irrelevant log messages at the top. Not Flutter's fault, this is just `pod install` output.
- StackOverflow says: run sudo gem install cocoapods. So that's what I do.
- Cocoapods 1.11.2 gets installed on my machine. There's only one warning, which says "Ignoring ffi-1.14.2 because its extensions are not built. Try: gem pristine ffi --version 1.14.2". I hope that's not going to be a problem.
- Compiling again, and this time it's a success.
- One info log from the ads plugin just after initialization is a bit discouraging:
- - <Google>[I-ACS012056] Transaction failed. Transaction failed
- What transaction? No idea.
- The ad shows! Yay!
- The plugin also tells me the identifier of the test device:
- <Google> To get test ads on this device, set:
Objective-C
GADMobileAds.sharedInstance.requestConfiguration.testDeviceIdentifiers = @[ @"949193f10482181c062b166586b3e111" ];
Swift
GADMobileAds.sharedInstance().requestConfiguration.testDeviceIdentifiers = [ @"949193f10482181c062b166586b3e111" ]
- Some errors from the plugin:
- [assertion] Error acquiring assertion: <Error Domain=RBSServiceErrorDomain Code=1 "target is not running or doesn't have entitlement com.apple.runningboard.assertions.webkit" UserInfo={NSLocalizedFailureReason=target is not running or doesn't have entitlement com.apple.runningboard.assertions.webkit}>
- [ProcessSuspension] 0x14fffb1e0 - ProcessAssertion: Failed to acquire RBS assertion 'WebProcess Background Assertion' for process with PID=764, error: Error Domain=RBSServiceErrorDomain Code=1 "target is not running or doesn't have entitlement com.apple.runningboard.assertions.webkit" UserInfo={NSLocalizedFailureReason=target is not running or doesn't have entitlement com.apple.runningboard.assertions.webkit}
- Could not signal service com.apple.WebKit.WebContent: 113: Could not find specified service
- But the ad shows. And clicking it also works.
- It does seem sligtly cut off from the side. Like, a pixel from the right border is seemingly missing. Will investigate.
- After changing orientation, I get an error message while loading the new banner. It might be merely that the size is too small (I haven't had time to optimize the landscape layout to show ads). But the message is wierd:
- <Google> Cannot find an ad network adapter with the name(s): com.google.DummyAdapter. Remember to link all required ad network adapters and SDKs, and set -ObjC in the 'Other Linker Flags' setting of your build target.
- flutter: WARNING: 2022-03-01 16:02:27.838631: MyBannerAd: Banner failedToLoad: LoadAdError(code: 8, domain: com.google.admob, message: Cannot find an ad network adapter with the name(s): com.google.DummyAdapter. Remember to link all required ad network adapters and SDKs, and set -ObjC in the 'Other Linker Flags' setting of your build target., responseInfo: ResponseInfo(responseId: gzUeYvSKG5CT7_UP3pu7sAg, mediationAdapterClassName: null, adapterResponses: [AdapterResponseInfo(adapterClassName: com.google.DummyAdapter, latencyMillis: 1)
- Ok, StackOverflow to the rescue (here). As I assumed, it's just that there are any units to show in the small area. The error message leaves room for improvement.
- Now I see the same error even with a completely clean run of the app. So it's not device orientation change.
- I'm going to assume it's a glitch for now, and will release the app to TestFlight.
- Bug with audio: music start when resuming the app on iOS even when I set sound to off at the start. This might be just the fact that iOS killed the app in the background (and I still haven't implemented any persistence). But worth looking into tomorrow.
- Hmm, upload to AppStoreConnect failed the first time, citing probable network error. Trying again.
- Ah! Second time's the charm!
- The 'ads' version is now on both stores for internal testing.
- Yeah, looking at the release version on Android now:
- The ad takes a second or two to load. Not fun.
- The ad unit can't be transparent, so going from the page with the ad unit to another one using the default Material design transition looks bad. The whole departing screen makes a nice "down and fade out" transition, but the ad unit just goes down and then suddenly doesn't exist anymore.
2022-03-02 Giving access, adding sounds
Days worked: 12
- Another build was rejected by Apple because they only see a blank white screen. The problem is, they don't send any logs, and I can't reproduce. I'm starting to think, a little bit, that they might be testing the old build unwittingly, but of course I'm assuming the problem is on my side.
- I wrote down the definition of "app-like games" into the README, too. Quote: ----
- This sample focuses on what I call "app-like games". It's a very popular genre of (often mobile) games that do not have a normal game loop (where the screen changes almost every frame). Instead, these "app-like games" spend most of their time waiting for the player's input, just like regular apps. The games may be real-time, but they seldom redraw the whole screen.
- Examples of such "app-like games":
- These types of games are a great fit for Flutter, since they can use Flutter's widget layer for building UIs and still have powerful pixel-perfect control of visuals on all platforms.
- Just to be clear, Flutter also supports more traditional videogames (with a 60 FPS game loop). A great example of this is Flame, a 2D game engine made on top of Flutter. It's just that this sample game is focused on "app-like games".
- --- End quote.
- I added sound effects. You can hear them in the web version, though expect higher latency than in the mobile product.
- I tested the SFX version on my kid and my wife and I think it's going to work.
- I realized something I never thought about before. Since `dart:io` is not available on the web, you can't check `Platform.isIOS` or anything similar on the web.
- Thankfully, this is easy to work around. You just prepend the isIOS check with kIsWeb:
- final platformSupportsAds = !kIsWeb && (Platform.isIOS || Platform.isAndroid);
- I learned this the hard way: the web version just didn't load, and the culprit was me calling `Platform.isIOS || Platform.isAndroid` to check if we should show ads.
2022-03-03 Debugging an audio / performance issue on iOS
Days worked: 12½
- Pleasantly fast.
- The responsive design still clearly needs work but isn't as terrible as I assumed.
- There's an error with the audioplayers plugin
- "iOS => call startHeadlessService, playerId c3d98bf5-5262-4f1a-97a7-e586abf43d76"
- "calling start headless service (\n   \"-6035574777829228503\"\n)"
- "iOS => call play, playerId c3d98bf5-5262-4f1a-97a7-e586abf43d76"
- [default] AudioSessionSetProperty_Priv posting message to kill mediaserverd (916)
- "Error configuring audio session: Error Domain=NSOSStatusErrorDomain Code=2003329396 \"(null)\""
- "Error configuring audio session: Error Domain=NSOSStatusErrorDomain Code=1836282486 \"(null)\""
- This completely stops the game for at least a second. It's probably something to do with the `AudioPlayer` being created and destroyed too fast. This happens by default in pkg:audioplayers because every new sound from `AudioCache`, by default, creates its own player.
- On the other hand, if you use a "fixedPlayer" so that the players aren't created for every sound, there can't be two SFX sounds playing at the same time.
- The solution I'll try is to create maybe 2-3 SFX AudioPlayers at the start of the game and just use the ones that aren't playing something (rotating with an "index").
- Good news, Apple has approved the latest testing version!
- This means that folks who've gotten the invite to Raindead Company's internal testing can ignore that unless they want access to "master" channel.
2022-03-04 Adding Google Play Games Services (GPGS)
Days worked: 13½
- Got feedback from XYZ about game getting stuck. Could reproduce at home. I think it's still the audio problem.
- Well, I just found a bug in the AI that I introduced recently and that just straight out crashes one of the AIs when close to endgame. Fixed!
- I also implemented the caching of audioplayers and I haven't seen the error from before yet. More testing needed.
- DONE. I'm pretty sure I fixed it all.
- One thing that is frustrating about Flutter is that even fatal bugs won't show up in TestFlight / Google Play crash reports. Flutter just silently fails.
- The above is an example. XYZ's game should have crashed and I should have seen the crash in TestFlight. Instead, XYZ just couldn't do anything with the game. Thankfully, she sent a feedback using TestFlight. But the feedback only has a screenshot and her explanation, not any stacktrace or even a single error.
- Folks on StackOverflow have workarounds (example) but they all need Sentry or Crashlytics. Which means, the app needs internet permissions.
- Added all the music, so testing is not as annoying (with the only one song on loop).
- I have explicit permission from the artist to use the songs and to have the audiofiles in the repository. The tracks are all either CC-BY or CC0, so sharing is okay even without explicit permission, but I wanted to have this absolutely clear.
- If you like Mr Smith's music, consider buying him a cup of coffee.
- Adding Games Services now
- Weird "annoyance"-level issue: After adding `games_services: ^2.0.7` to pubspec.yaml, the package is downloaded and shows up in "External Libraries", but importing it in dart files doesn't work.
- Error: "error: Target of URI doesn't exist: 'package:games_services/games_services.dart'. (uri_does_not_exist at [tictactoe] lib/main.dart:3)"
- running `flutter clean` doesn't work
- restarting Android Studio worked (!!!)
- now that I think about it, I might have upgraded the Flutter plugin in Android Studio and then neglected to restart Android Studio. Maybe.
- Okay, back to adding Games Services
- The README of pkg:games_services makes it look like the only change required is adding code, but it's not enough. Fortunately, the linked How to goes through the steps of enabling Games Center, for example. But it should be more prominent, I think.
- The iOS part of the How to is slightly outdated:
- Different iconography in Xcode
- Still refers to iTunes Connect (i.e. App Store Connect)
- "Features" (in iTunes Connect) is now "Services"
- The Android part of the How to is also slightly outdated
- Nowadays, you open the app and go to the Play Games Services submenu
- Adding Google Play Games Services is a bit more involved than the equivalent on iOS
- You are creating a Google Cloud Platform project
- You have to create a new "OAuth consent screen"
- It already wants the app's logo (which I don't have) - TODO: add logo later
- Lots of modals like the following one, which ask you to go to a different Google product and fill in information there.
- I_have_no_idea_what_I'm_doing.gif
- All this to get a number
- You have to actually "publish" you Play Games Services (whatever that means). You get a scary message. But it's not like you're actually publishing your game. Just the "services" (?).
- Implementation of games_services done. Now, to test it.
- Trying to run the game on iOS
- CocoaPods could not find compatible versions for pod "games_services":
- Â Â Â In Podfile:
- games_services (from `.symlinks/plugins/games_services/ios`)
- Â Â Specs satisfying the `games_services (from `.symlinks/plugins/games_services/ios`)` dependency were found, but they required a higher minimum deployment target.
- I get that games_services needs some iOS version higher than 9.0 (the default), but which version?
- I went to github and found a file that I think is saying the lowest target is iOS 11.
- The ability to change the deployment target is well hidden in Xcode, even if you know where you should look:
- Those little arrows next to 9.0 signify that you can change the number.
- After changing to 11.0, it builds!
- iOS leaderboard works, although it kind of calls me out on not having any friends:
- Trying to run the game on Android
- W/ConnectionStatusConfig( 3021): Dynamic lookup for intent failed for action: com.google.android.gms.leibniz.events.service.START
- W/GmsClient( 3021): unable to connect to service: com.google.android.gms.leibniz.events.service.START on com.google.android.gms
- W/utter.tictacto( 3021): Accessing hidden method Landroid/os/Trace;->isTagEnabled(J)Z (greylist, reflection, allowed)
- W/cr_media( 3021): Requires BLUETOOTH permission
- E/Error  ( 3021): signInError
- E/Error  ( 3021): com.google.android.gms.common.api.ApiException: 12501: 12501:
- E/Error  ( 3021):         at com.google.android.gms.common.internal.ApiExceptionUtil.fromStatus(com.google.android.gms:play-services-base@@18.0.1:3)
- E/Error  ( 3021):         at com.google.android.gms.common.internal.zap.onComplete(com.google.android.gms:play-services-base@@18.0.1:4)
- [...]
- I/ExplicitSignIn( 3021): Trying explicit sign in
- I/silentSignIn( 3021): error
- E/flutter ( 3021): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: PlatformException(error, Something went wrong Status{statusCode=unknown status code: 12501, resolution=null}, null, null)
- [...]
- E/flutter ( 3021): #2 Â Â Â MethodChannelGamesServices.signIn (package:games_services_platform_interface/method_channel_games_services.dart:48:14)
- Ok, something's wrong with signing in, but StackOverflow doesn't help, at least at first. I'm not any wiser after reading this, for example.
- Calling it a day. There's either something wrong with how I set up my app, or something wrong with how I set up the OAuth above.
2022-03-07 Trying to get Google Play Game Services to work
Days worked: 14½
- Continuing with debugging the Google Play Game Services Sign in issue from Friday.
- I just realized my development device (Nokia 1.3) wasn't logged into any Google Account. Doing it now.
- I created a test Google account (just a regular account which I'll only use for testing). This is good practice: you don't want to accidentally have something work just because your development device is logged under the "right" account.
- Don't forget to, at the very least, open Play on the device once, so that you can agree to terms of service and so on.
- Still getting the same "silent sign in error". Here's a stab at cherry picking just the potentially salient errors, in chronological order.
- W/ConnectionStatusConfig( 2223): Dynamic lookup for intent failed for action: com.google.android.gms.leibniz.events.service.START
- W/GmsClient( 2223): unable to connect to service: com.google.android.gms.leibniz.events.service.START on com.google.android.gms
- E/Error  ( 2223): signInError
- E/Error  ( 2223): com.google.android.gms.common.api.ApiException: 10: 10:
- E/Error  ( 2223):         at com.google.android.gms.common.internal.ApiExceptionUtil.fromStatus(com.google.android.gms:play-services-base@@18.0.1:3)
- I/ExplicitSignIn( 2223): Trying explicit sign in
- I/silentSignIn( 2223): error
- D/SharedPreferencesImpl( 2223): Time required to fsync /data/user/0/dev.flutter.tictactoe/shared_prefs/com.google.android.gms.measurement.prefs
- E/flutter ( 2223): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: PlatformException(error, Something went wrong Status{statusCode=DEVELOPER_ERROR, resolution=null}, null, null)
- *shakes fist at "Something went wrong" as an error message*
- I found a 3 months old StackOverflow question that seems to be exactly my problem: here. No replies. Added a bounty.
- It just occurred to me that I should probably search for DEVELOPER_ERROR as a code. I found it here, in GMS ConnectionResult.
- It says: "The application is misconfigured. This error is not recoverable and will be treated as fatal. The developer should look at the logs after this to determine more actionable information."
- I'm guessing I misconfigured something when setting up the OAuth / GMS / app glue.
- Ok, back to basics. Going to Play Games Services configuration in Play Console.
- I think I can skip modifying build.gradle since that's done by the plugin, right?
- I am linked to another step, about OAuth. Here. I thought I did this, but maybe I skipped something.
- Now I'm thinking whether where I got the fingerprint and whether I should have used the keytool to get a new one.
- Running keytool tells me "The operation couldn’t be completed. Unable to locate a Java Runtime. Please visit http://www.java.com for information on installing Java."
- I'm downloading JDK 17 now.
- So, my keystore file has a different fingerprint than what Google Play Game Services asked me to use. I'm guessing Google Play Game Services knows what it's doing. At the same time, one FAQÂ on the developers.google.com site says "Sign your APK with the correct certificate". Maybe I should use a different "credential" when debugging?
- Ahh, getting back to the Flutter plugin's How to. I noticed there's a part I skipped in the confusion.
- It's the part where I fingerprint the debug key: keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
- Trying to update the Cloud Platform OAuth client now.
- Good news. The new fingerprint is shown immediately in Google Play Console.
- Trying to build and run the game anew.
- Nope. Still getting the same error.
- I temporarily removed apps because the ads plugin is quite chatty in the debug console, and uses cryptic codenames like "leibnitz" (which I wasn't sure until now whether it was Ad-related or GPGS-related.
- Now focusing on the following part of the Flutter plugin How To. Quote ---
- I don't know what  "OAuth APIs" I should be enabling and how. The API manager doesn't even show any OAuth APIs.
- I enabled Google People API.
- All required fileds.
- Still the same error.
- Running in verbose mode didn't add any new information from the plugin.
- I have all required fields in the OAuth consent screen set up.
- Whoa. Actually, after moving to another screen in console.could.google.com and coming back, I can see that two more fields are now required: Application home page and Application privacy policy link.
- Ok, making a "homepage" in the Github Pages.
- Actually, the domain needs to be "verified", so I'm moving the homepage and the privacy page to filiph.net
- Any app/game that is pushed to production needs this sooner or later, so I'm not complaining.
- Unfortunately, even after doing this, I still see the error.
- Is it possible I'm seeing the same thing?
- I'm going back to Google Cloud Platform console's Oauth consent screen, and reverting the screen "Back to testing". Maybe the problem was that the OAuth consent screen was published already?
- Also adding my test account to testers. Maybe that was the problem?
- On one of the screens, I noticed that it says something about a long delay (24 hours?). So I'm going to try again tomorrow. I'm out of ideas today.
- Tomorrow, if it doesn't work, I'm going to try to implement the example first. This means a lot of work but at least it's isolated.
- Trying to also add google-services.json as per this SO answer. No luck.
- Tried flutter clean, no luck.
- AAAAARRGH, I finally figured it out.
- If you enter the app ID in AndroidManifest.xml directly, it won't work:
- <meta-data
- Â Â Â Â Â Â android:name="com.google.android.gms.games.APP_ID"
- Â Â Â Â Â Â android:value="123456789012" />
- ^^^^ won't work
- <meta-data
- Â Â Â Â Â Â android:name="com.google.android.gms.games.APP_ID"
- Â Â Â Â Â Â android:value="@string/app_id" />
- (and then put a file in res/values that includes the app_id key)
- This is how they do it in the GPGS basic sample:
- The games_services plugin's How to didn't mention it, and made it look like you can just put the string there.
- Maybe this worked in some previous version of Android?
- What's frustrating is that I seem to remember that I had a similar problem before (Android failing to load a resource from AndroidManifest.xml directly, without actually letting me know there's a problem) and I successfully forgot all about it.
- I'm realizing that if I used Android SDK more, I would have caught this problem ages ago. I'm pretty sure it's a "noob mistake". And again, I already knew this!
- Honestly, setting up Google Play Games Services (GPGS) with Flutter is a pain. I realize there are many moving parts (the app itself, GMS, Oauth2 in Cloud Platform, etc.), so I understand. But there's definitely room for improvement.
- Most of the frustration comes from the GPGS+GMS+Cloud+Android side, not Flutter itself. It's just that using a Flutter plugin does some things in the background for you, so you don't know what to skip and what not to skip.
- More importantly, there is no single guide to implement this stuff. All the guides link to parts of other guides. Often, I felt completely lost as to which guide I'm reading and why, and when I should stop and go back to the previous guide. In isolation, each guide is good, but they're all for a different purpose than what I'd like to achieve (set up GPGS in my game).
- I got in deep trouble at least one time by having to juggle two or more guides. As the plugin guide was clearly outdated, I had to search for another guide, and then I didn't return to the right place in the Flutter guide, thinking I've already done a step, and skipping past it.
- It doesn't help that the failure has a very vague error message ("Something went wrong", "DEVELOPER_ERROR") and seemingly no useful logging information. All I know from that message is that there's an error and that it's my fault. And from the context, I am pretty sure it's an error with authentication (signIn).
- Now that it works, the integration works nicely.
- The game asks for creating a Google Play Games account
- Showing the leaderboard asks you to install the Google Play Games app.
- But it seems that score wasn't submitted correctly. Or at least I'm not seeing it on the leaderboard.
- Ah, it works correctly. I just had my scores privacy set to "Only me", and, apparently, they won't therefore show (even for me) on global public leaderboards.
- Now that we know how the integration works, time to maybe rethink achievements and leaderboards and scoring a bit
- We have several levels with raising difficulty. It's weird to have a single leaderboard where we put scores from any level (leaderboards can't have additional information: just the one number — so it's not clear from which level the score comes). Either create a leaderboard for each level, or create a leaderboard that sums scores from all levels. Or, make a leaderboard only for the last, hardest level. Maybe the score is the number of turns to beat the AI?
- Both on Android and iOS, leaderboard and achievements have their own UI. There is no need to render the leaderboard ourselves, unless we want to have a "local" leaderboard (which doesn't make much sense on mobile, a deeply personal device). Maybe, instead of the "Achievements" page, we just have two buttons in the main menu that lead to the native UIs of GameKit and GPGS.
- TODO: create a list of interesting achievements we can track
- Since GameKit and GPGS take care of the text entry on their side (you can't provide a custom name in the leaderboard, for example), we can't use that. Other options for text entry:
- Social sharing - write a short message to the social image
- Chatting in multiplayer
- Maybe game saves?
- Customize the name of the player in Settings? "Player" can become "Filip". Game Sessions would then say things like "Player vs Chicken AI" or "Filip vs Kraken AI".
2022-03-08 In-app purchase
Days worked: 15½
- I was able to verify that Google Play Games Services works even on my day-to-day device.
- There's still a bug with music not resuming when it should. TODO.
- This is how I track progress towards the March 15 milestone (breadth-first, complete). Looking pretty good, although, of course, the devil's in the details, so I expect things to slow down significantly as we approach completion.
- It's also clear from the above what to do today: In-app purchases.
- As explained above, going with the official plugin, of course: pkg:in_app_purchase
- Ok, pretty soon I realize that the example's README is high level and I do need to go through the more detailed, per-store, guides.
- Once again, I'm not sure if the google play billing library dependency (explained here) is already added through the plugin.
- Checking the plugin's code: it is!
- It would be useful for plugins to advertise this information. Just say something like: "Go through this codelab. The only thing you can skip is X and Y."
- Ok, so, basically, after reading the Getting ready guide, I'm realizing that all I need to do is to add in_app_puchase as a dependency of the Flutter project, and upload to Play Store.
- Very cool! After adding in_app_puchase to pubspec.yaml, building the game, and uploading the new version to Play Store, I can now add In-app products (in Google Play Console → (the app) → Monetize → In-app products).
- Creating a product is straightforward: come up with a product ID ("remove_ads"), the user-facing name and description, a price (can be done through "Pricing templates"), and click Save.
- Implementing the dart code for initialization
- Plus, for example, this line will lose the generic type information for the Stream, which leads to a confusing and unnecessary compile-time error:
- final Stream purchaseUpdated =
- Â Â Â Â InAppPurchase.instance.purchaseStream;
- Just remove `Stream`, or make it explicitly Stream<List<PurchaseDetails>>, and it's fixed.
- TODO: revisit the code and use the codelab to make it more robust
- the authors of the codelab clearly put more thought into this, and they have things like testing implemented nicely.
- The usual dilemma: where to put the logic? We don't want go all Enterprise on this, with InAppSubscriptionManagerFactory. Yet, we also don't want
- When testing initially, things didn't work. The product wasn't found. Only then I noticed that after you create the in-app product in Google Play Console, you have to Activate it.
- Ok, after taking the time to read through the codelab, I think that's the best resource by far. It seems like the plugin's example or the official Google Play docs should be the default guides to follow, but the codelab is much better. Yay codelabs!
- Another problem was that I wasn't releasing to Closed testing, and instead to Internal testing. Despite the warnings not to do so. I guess "closed testing" seemed similar to "internal testing" in my skimming? Once again, the codelab makes it much clearer than the other guides:
- Ok, since we need to publish to Alpha, we absolutely need to finish the app's information in Google Play Console:
- A reminder that you need to go through these kinds of forms when publishing a game. I remember doing this for my game, but it seems like the UI is different (and better! more straightforward) now.
- Hell yeah! E for Everyone.
- Caveat: while tic tac toe is a kid game, I had to designate the game as 13+. Because Google (rightly) asks whether the ads that we're showing to the kids are family friendly. Since I can't (currently?) be 100% sure that the ads are okay to kids, I'm going to leave the game as 13+ only.
- Once again, I don't remember this explained as well as it is now. Kudos to the Google Play team for making it easier for developers to understand why their games might not be suitable for under-13. As a parent, I like this even more.
- Ok, I managed to complete the submission to the Google Play Store today.
- The app is under review now.
- Code for the shell is tucked away in lib/src/shell so that the developer has the rest of the lib/src/ folder for the actual game — the main thing.
- Start without integrations.
- Normal developers will not want to spend the first day of game development by setting up stores, ad units, and the like. They'll want to start implementing some simple game.
- Let's give them the main menu and the basics first (like a main menu, a settings screen, shared_preferences, audio).
- pubspec.yaml has all integration dependencies (in_app_purchases, ads) commented out, with additional comments leading to README files
- Each feature (like ads) should have a README in the corresponding folder in lib/. (Or just a section in the main README?)
- There should be a config.dart file which has bool.fromEnvironment (or just regular) consts that default to false at first. The developer can turn on feature by feature (or use --dart-define when building).
2022-03-09 iOS In-app purchase
Days worked: 16
- Alpha release still in review, which means I can't make progress on the Android side of In-app purchases.
- Let's do iOS!
- This time, I have learned my lesson and I'm going straight with the excellent Adding in-app purchases to your Flutter app codelab.
- Paid apps agreement is something I already have with Apple. I remember it was major pain to set up in my case. For example, I remember that when I was inputting my address, the App Store Connect web app kept "correcting" my address (even though the correction was a no-op — it was already correct). I spent hours trying to figure out what was wrong. I read forums, wrote Apple support, all of it. Then, somewhere, I found about a simple solution to this. Click the submit button 3 to 5 times in a row. I'm not kidding. The solution was to just let the web app "correct" my address several times in a row, and then everything went smoothly. WAT.
- The part where the codelab asks me to "Register App ID"
- It doesn't link to the Apple Developer Portal. Confusingly, the Developer Portal is not the same as App Store Connect.
- Since I already published the app to TestFlight, my App ID already exists. Something to think about when people build their games. I think it's safe to say that most developers will already have their games published to App Store Connect before starting to set up In-App Payments. (But of course, in the context of the codelab, it makes perfect sense.)
- Interestingly, my game already has "In-App Purchase" checked. I guess it's because I selected "Game Center" in Xcode a few days back.
- Creating a Sandbox account
- Creating the account itself is pretty easy using the codelab instructions. Especially since you can use something like your.normal.email+sandbox@gmail.com.
- Adding the sandbox account to an iOS device is not as easy. The part where this happens in the iOS Settings app is missing on my iPad.
- StackOverflow to the rescue again: it looks like, nowadays, you don't need to create a separate sandbox account. And, if you do, you don't input it in advance. Instead, you try to make an In-app purchase in your TestFlight app, and it will work like a sandbox.
- Moving on with creating a new in-app purchase. Going to In-App purchases and looking for a way to create the new purchase. The UI is not very helpful (try to find where to add a new purchase):
- Here, silly!
- Filling the actual In-app purchase form is straightforward.
- UNTIL, you save and it says "Missing metadata" and doesn't tell you which ones. It shows a little yellow dot next to the "Missing metadata" warning, and also next to the English localization strings. But those are not missing. I've included them.
- Arghhh, it's because I haven't included a screenshot, StackOverflow reports.
- Confirmed, when I uploaded a screenshot, "Missing Metadata" was replaced with "Ready to Submit". But I still can't submit. That button is grayed out.
- Next idea: add review notes as well.
- Ah, there's a new message at the top of the form:
- """ Your first in-app purchase must be submitted with a new app version. Create your in-app purchase, then select it from the app’s In-App Purchases section under App Store and click Submit. Learn More
- Once your binary has been uploaded and your first-in app purchase has been submitted for review, additional in-app purchases can be submitted from the In-App Purchases section. Learn More """
- Ok, maybe I went a bit ahead of myself. The In-app purchase is set up, let's try to test.
- It works!
- Correctly identified as "Sandbox"
- No money changed hands.
- Uploaded the new version to both stores. Test away!
- This takes me to "everything is at least partly done" territory in my high-tech project tracker:
- The next 4 workdays will be in the name of bringing all the features to completeness, and polish.
2022-03-10 Persistence
Days worked: 17
- Only 4 workdays left before first, breadth-first but "final", version. Identifying the most pressing issues of the current version and what I can work on.
- The priorities for this "sprint":
- 1. Obvious bugs
- 2. Looks
- 3. Integrations
- Starting with persistence
- the game now persist settings and player progress
- mute all sound (both music & sfx)
- turn music on/off
- turn sound effects on/off
- Fixed a lot of small but obvious bugs
- Audio starting after app resumed from background - FIXED (this required a refactoring of the WidgetBindingObserver wrapper)
- Ads show in a small banner instead of covering a larger available area - FIXED
- Starting the In-app purchase seems like it's not working (no immediate change of UI) - FIXED
- Now designing the final* layout of the play session screen.
- *) There'll be changes, I'm sure. But I think we now know what elements should be there, and where they should be located, roughly.
- Swag from Flutter Europe is really really helpful here:
- Similarly, getting a bit more "final" about the graphics:
- Performance
- I'm testing on a very low-end device (Nokia 1.3, Android Go) and I'm getting pretty low framerates. Still too soon to optimize, but assuming there is a hard limit to what low end devices can muster in regards to custom shaders (which I use heavily to make the game look good and simple at the same time), here's an idea:
- Adaptive performance. Something I've been thinking about for some time. A natural idea, really. Since we're in full control of the rendering, we can dynamically lower fidelity on lower end phones (and on the web).
- Implementation idea:
- Some common animation (such as the cross mark paiting) has "telemetry" attached. It counts the number of frames it was called with. Or, it keeps track of the last N durations between build times. If this number seems bad (e.g. low framerate), the telemetry sends a message to the `Settings` object (something like `Settings.reportFramerate(x)`, and settings can decide to lower some (hidden) quality setting.
- Of course, this is just an idea at this point. Just wanted this to be recorded in case I need it later.
2022-03-11 Layout and new assets
Days worked: 18
- As hinted above, I'm going to redesign the layout today, at least the one on the play session screen. This screen needs quick access to settings (or at least a mute button, or both), some kind of "Player vs X" title, a back button, a restart button, and of course the playing field. Ideally, there's also space for strategy tips, but that may be too much.
- First stab at the updated layout. (No new graphics, just layout.)
- The name of the player, which defaults to "Player" but will allow customization. This is how we get to show work with text and forms in Flutter.
- The name of the AI, which makes it more personal and shareable. It also allows me to "explain" some of the AI's personalities (random, attacking, etc.)
- A way to access settings.
- A gradient background instead of the paper texture. Still not entirely sure about this. Just a way to give the screen a bit more.
- Same but in landscape:
- Nah, I'm pretty sure I like the previous background (but without the paper texture).
- More neutral means I can get more aggressive about the red later.
- Looks better on the device, too. Plus, this is easy to tweak.
- Adding new assets and reworking the grid:
2022-03-12 Icon
Days worked: 18½
- Wierdly, the official docs don't have keylines. It can only be found in Nick's medium article (above).
- Without keylines, it's very hard to design the adaptive icon. Here's my first try:
- Keylines in use:
just overlaying the asset with a semi-transparent layer with the keylines, and locking it. Then lining it up. - Even with the keylines, it took me about 5 uploads to the Play Store to get the icon look just right.
- Much better:
- Which reminds me I need to change the label from "tictactoe" to "Tic Tac Toe"
- Getting there:
2022-03-14 Transitions
Days worked: 19
- MOVED forward to next day
- Working on assets and implementation of a nicer transition
- DONE!
- Current implementation is simple in both assets and implementation. But it looks pretty good even now:
- Still TODO:
- A bit more "appearing" of the page that is being transitioned into.
- Use Canvas.drawImageRect or Canvas.drawAtlas() to optimize the loading and the performance of this effect. Potentially just load the PNGs in advance.
- Bigger "brush" so that the effect doesn't look so much like a linear swipe. "Sell" the fact that this is
2022-03-15 Deadline
Days worked: 20
- In testing (with my wife), the scribble transition looked more like a simple linear wipe. So I'm going with a more pronounced look.
- The app has been approved for Google Play alpha testing over the weekend. Which means we can continue with in-app purchase work.
- "Achievements" is basically empty now. All the functionality is "outsourced" to Game Center / Play Games
- add leaderboard and achievements as direct links to the Game Center / Play Games overlays
- Showin an ad takes way too much time.
- We should start pre-loading the ad during play, only to show it later.
- This was quite a refactoring but it was worth it. Ad now shows immediately.
- The app-y default transitions don't look good.
- implement better transitions
- Maybe "scratch over" with black ink, then "unscratch"
- TODO: Last move should be highlighted (also adds color)
- TODO: Restart button should do something when tapped
- TODO: Restart button should "pop" when round was lost
- TODO: Tapping the "celebration" should immediately jump to /play/win
- TODO: Consistent back button
- TODO: Main menu should look better
- Too conservative (monochromatic, basically) palette
- Maybe try to make each page and each level a slightly different (paper) color?
- DONE
- TODO: Strike-through a winning set
- Grid should paint itself slower on 3x3 grid
- An error/warning I just noticed while building iOS in Profile mode:
- /Users/filiph/.pub-cache/hosted/pub.dartlang.org/games_services-2.0.7/macos/Classes/SwiftGamesServicesPlugin.swift:58:25: warning: 'GKScore' was deprecated in macOS 11.0: Replaced by GKLeaderboardEntry
- Â Â let reportedScore = GKScore(leaderboardIdentifier: leaderboardID)
- Â Â Â Â Â Â Â Â Â Â Â Â ^
- /Users/filiph/.pub-cache/hosted/pub.dartlang.org/games_services-2.0.7/macos/Classes/SwiftGamesServicesPlugin.swift:60:5: warning: 'GKScore' was deprecated in macOS 11.0: Replaced by GKLeaderboardEntry
- Â Â GKScore.report([reportedScore]) { (error) in
- Â Â ^
- This is coming from pkg:games_services, and complains about an obsolete API being used. No biggie, just something to be cognizant of.
- Also added new sound effects, so the experience sounds better.
- Expanded the palette
- After today:
- Recoded a brief update to show the game in motion.
- Start the publishing process on both stores (We don't want any surprises in April.)
- Add the only missing P0 feature:
- text input (I know where it goes, I just need to implement it)
- Clean the code
- Hunt for bugs
- Extract the "shell"
- Write the script for the "shell" video, and get it reviewed by you
- Record the video
- Return to the full game and
- Add as much polish as possible
- Make it fun from start to finish (work on level design etc.)
2022-03-16 Script writing
Days worked: 21
- Started a script for the accompanying video.
- Started setting up the AdMob house ads (i.e. ads that don't generate revenue, and that promote something of "our own").
- I need Flutter ads to upload to admob. With the latest design, I converged onto a single format: a 300x250 banner (a.k.a. "medium rectangle"). Can I get something like that? Here are the specs. For a 300x250 banner, Google accepts a double-resolution image (600x500).
- I guess the ad will link to flutter.dev and say something like "Learn how to build Flutter apps" or similar.
- It can feature Dash and stuff like that, but it should be an actual ad for something actual. People will be reviewing it, so I'd like to avoid a silly ad.
- I have it set up the house ads to run 100% of the time. I'm hoping this means that there's no revenue to worry about.
- I'm of course also happy to _not_ have house ads all the time, and send all proceeds to charity. It might make the app look more "real".
2022-03-17 User testing
Days worked: 22
- Start the publishing process on both stores (We don't want any surprises in April.)
- Add the only missing P0 feature:
- text input (I know where it goes, I just need to implement it)
- put the shell in lib/shell/ ?
- Hunt for bugs
- Extract the "shell"
- Write the script for the "shell" video, and get it reviewed by you
- Record the video
- Return to the full game and
- Add as much polish as possible
- Make it fun from start to finish (work on level design etc.)
- Getting feedback from random strangers via usertesting.com
- This, to me, is a must have for catching problems "in the wild". Strangers will always be more direct than friends, and they will have a more diverse set of expectations, capabilities, and devices.
- Yes, it's "too early". The game is in an embarrassing state still. It's painful to watch. But it's really important. Waiting for the game to be "better" often means you do a bunch of work on things that ultimately don't matter.
- usertesting.com isn't free. I paid $140 for the three sessions below. If I was on a tighter budget, I'd go to a coffee shop, invite people to play the game, and gift them a cup of coffee for the trouble. Either way, it's great value for the money. Imagine spending $50 instead of a month of futile work. Imagine spending $50 to prevent your game from getting abysmal early reviews (and thus becoming a failure).
- Below are my notes. Sentences in quotes are direct quotes.
- Test session 1Â (rheaphil95)
- The automatic logging in (?) takes a long time, several seconds, and only shows a gray overlay and a circular progress indicator.
- TODO: Maybe let the player log themselves in, via a button on the main screen?
- Playing with sound, we just don't hear it
- "I really like the audio. The sound effects."
- Ad shows immediately.
- Win screen
- Laughs at the "yay!" sound effect. (Delight the user!)
- About the You Won text: "I think this could be more centralized. Because it's something I might miss."
- "I really like the layout of this."
- Starts level 2 and sees a bigger field: "Ooooh, I wonder what this is."
- I'm realizing I don't explain that this one is 4-in-a-row.
- Player seems to struggle. Not trying to win?
- When all tiles are taken: "I would expect a popup over here." It is weird that we don't do anything.
- TODO: at least "pop" the restart button when everything is filled.
- It seems really really hard for the player. Some moves that would seem clear to me, she doesn't play at all. Seemingly places tiles at random.
- TODO: second level should be really easy, such as a random thing, or "AttackOnlyOpponentAI"
- TODO: give tips?
- start in the middle
- put Xs close to each other
- make opportunities
- later: try to place X in places where they could be part of 2 different winning lines.
- later: don't forget to foil the opponent's plans. Don't just attack.
- later: don't pursue lines that are already doomed (there's no place for a winning line there anymore).
- This is mentioned by the player later: "Maybe some hints would be nice at this point? If there were some strategies provided, tips, trick, it would be really nice."
- Maybe a button you can click?
- I totally underestimated the difficulty. I realize that if one doesn't ever play "four in a row" or gomoku, this is actually really hard.
- The player does say "this is fun", though!
- "I don't think I'm really going to pass this level. I'm going to keep trying" -- a normal casual player would probably close the app at this point!
- "This is harder than I though. But really fun."
- "This reminds me a little of 'connect four'."
- Takes
- Performance: game is suddenly choppy!
- And the audio is not there anymore.
- Damn! This might be because we're running on a phone that's also recoding the screen. But it's not good.
- TODO: before next test, I need some way to send logs!
- After some time, the audio recording stops. At about the same time, the performance goes to normal again!
- So it's probably something with pkg:audio_players! I've seen performance issues with audio_players on iOS. I fixed that one by reusing a small set of AudioCache instances. Maybe there's something more to fix.
- TODO: limit the number of times we play SFX?
- Tries to turn of sound but clicks to the left of the icon
- TODO: make the icon larger, or — even better — make the whole row clickable
- Error: The item you requested is not available for purchase.
- That's right, I need to make it "public", or release the app first.
- Clicking on the "Ok" button in the In-app overlay does nothing. This is outside my reach. Something wrong with the Google Play overlay.
- Oh, it just took a long time.
- Clicking leaderboard does nothing.
- TODO: It should somehow show that things are loading!
- TODO: It should prevent launching the leaderboard several times in a row!
- Unfortunately, I can hear the final verdict. :(
- Test session 2Â (abg8897)
- This one actually shows the "Sign in with Google Play Games" UI correctly. The player can Cancel.
- The player goes through the whole Games flow in the overlay, no problem.
- No audio?
- The grid looks wierd. It's way too long to the side and the bottom.
- Completely misses an easy way to win the first round of the tic-tac-toe.
- Since there's no UI to say "you lost", it's a bit underwhelming I guess?
- Player goes back to level selection screen at this point.
- I don't think the player is enjoying this.
- Yeah, level 2 is way too hard for being the second level!
- TODO: make it much easier!
- Does not press restart at all. Instead, goes back.
- Turns music off. Keeps sounds. No problem there.
- Completely loses interest of the game 5 minutes in. The level 2 difficulty spike is brutal.
- Does try to tap "Remove ads" (the text) instead of the icon
- TODO: make the whoe row tappable
- Same error as before, with the item not available for purchase. And, once again, tapping OK in the overlay does nothing. Tapping back works, though.
- Leaderboard:
- Nothing happens when tapped
- "I was a bit annoyed about the sound. In the middle of the game I tried to mute it."
- Okay. Can't please everyone. I do think the sounds should be quieter, though, at the very least.
- TODO: maybe make the "drawX" and "drawO" sounds something more normal, like clicking or notes, or just not a mouth-sound. The player will hear it a thousand times, and they will be frustrated by losing, so it'll get old pretty quickly.
- The user is trying to be nice but I think it's clear she didn't have much time with the game itself.
- "I like the interface, it's really straightforward and simple."
- "I kept losing at level 2 and that was a bit frustrating."
- TODO: maybe allow player to skip a level?
- Acknoledges that she wasn't able to focus on playing 100% since she was cognizant of the 10 minute time limit. It's true. A normal player that's trying to kill the time has a different approach than a person who's trying to test an app. Though I still think that a good game should keep the player engaged. (My commercial game had no problem keeping even the testers engaged.)
- Mentions the "hushed" sound and how it can be muted. Yeah, she didn't like the SFX. See TODO above.
- Test session #3 had problem with installation (not a problem on our side) — waiting for a replacement session
- Actually, I decided I have plenty of things to fix already. I'll use the credit for this session for the next set of sessions.
2022-03-21 Asset stores
Days worked: 22½
- Searching for good 2D asset stores and free download sites
- found a few good music and sfx sites along the way
- DONE: I added all of the findings under the Assets heading
2022-03-23 Text input & bugs
Days worked: 23½
- Not styled yet, but it works, is accessible from both Settings and the Play Session pages, and shows reactivity of Flutter (changing the name in the text field immediatelly changes it in the background).
- TODO: Settings- make the icon larger, or — even better — make the whole row clickable
- duplicate TODO: make the whole row tappable
- DONE!
- TODO: maybe make the "drawX" and "drawO" sounds something more normal, like clicking or notes, or just not a mouth-sound. The player will hear it a thousand times, and they will be frustrated by losing, so it'll get old pretty quickly.
- TODO: at least "pop" the restart button when everything is filled.
- start in the middle
- put Xs close to each other
- make opportunities
- later: try to place X in places where they could be part of 2 different winning lines.
- later: don't forget to foil the opponent's plans. Don't just attack.
- later: don't pursue lines that are already doomed (there's no place for a winning line there anymore).
- This is mentioned by the player later: "Maybe some hints would be nice at this point? If there were some strategies provided, tips, trick, it would be really nice."
- Maybe a button you can click?
- TODO: second level should be really easy, such as a random thing, or "AttackOnlyOpponentAI"
- duplicate TODO: make it much easier!
- TODO: maybe allow player to skip a level?
- ----
- TODO: before next test, I need some way to send logs!
- TODO: Maybe let the player log themselves in, via a button on the main screen?
- TODO: Error: The item you requested is not available for purchase.
- That's right, I need to make it "public", or release the app first.
- TODO: It should somehow show that Leaderboard is loading! Or that it failed.
- TODO: It should prevent launching the leaderboard several times in a row!
- TODO: fix bug with grid proportions
2022-03-24 Planning
Days worked: 23½ (i.e. no actual work today, just some admin — not incrementing)
- I want to make a "launchable" version as early as possible, so that no amount of snafu can stand between me and launching on May 11 on both stores.
- I am targetting to have a build on April 8, and put it on stores with "managed" publishing (the game goes through the full review process and then waits for us to actually launch it).
- There are 11 workdays between now and then.
- I need to deliver a HowTo video on April 18, 2022. Due to a planned holiday (Easter) and a weekend, this translates to April 13.
- This means I have to have the "shell" ready by cca April 8, too.
- To be reasonably (95%) sure that a game is reviewed and ready to launch for I/O (May 11), I want to submit a "golden" version on April 27.
- This is, again, counting with all sorts of snafu. In fact, App Store says that 90% of apps are reviewed in 48 hours. The real number is probably somewhere around 5 days for iOS and 7 days for Android. Plus, updates often take shorter than initial versions. I'm being conservative and pushing the golden version 13 days ahead of I/O.
- We can always make yet another release closer to I/O.
- Priorities in chronological order:
- First, fix only things that are absolutely embarrassing, and only those that are user-facing, not developer-facing:
- The game itself falls apart after about level 5.
- Graphical glitch with grid
- No achievements
- We currently pretty much ignore errors coming from Leaderboard/Achievements.
- Level selection screen should look better.
- Publish (or, if there's still time till April 8, mark a commit as "launchable").
- This involves a bunch of "marketing materials" work, too.
- Just the parts I will be showing in the video:
- AudioSystem should know that it's muted so that UI code doesn't need to check it.
- Extract the "shell"
- Record the video
- Hunt for bugs
- Add Firebase Crashlytics
- Do another round of UserTesting
- Everything must be clean and documented
- But resist over-engineering this! It's still a solo game dev sample / template.
- Write long-form documentation (one-pagers, READMEs)
- Return to the full game and
- Add as much polish as possible
- Make it fun from start to finish (work on level design etc.)
- Work on performance
- Copy changes to "shell" where appropriate
- Submit the "golden" version
- Hunt for bugs
- Launch at I/O
2022-03-29 Fixes
Days worked: 24
- As planned, I start by fixing only things that are absolutely embarrassing, and only those that are user-facing, not developer-facing:
- The game itself falls apart after about level 5.
- Graphical glitch with grid
- No achievements
- We currently pretty much ignore errors coming from Leaderboard/Achievements.
- Level selection screen should look better.
- IN PROGRESS (drew some UIs today)
2022-03-30 Making the game finishable
Days worked: 24½
- As planned, I continue fixing only things that are absolutely embarrassing, and only those that are user-facing, not developer-facing:
- The game itself falls apart after about level 5.
- DONE! The game is playable and has a roughly increasing complexity.
- Okay, what do we need for achievements for the "initial" version? Something that shows up pretty early. Something that people actually have to fight for. And then the game's completion.
- first_win: First win - Bested the first opponent. - 10 points
- half_way: Half-way there - Bested half the opponents. 80 points
- finished: Mastered - Mastered the art of Gomoku - 100 points
- Alright, let's add those, according to pkg:game_services' how to. iOS First:
- We get to choose the point value of each achievement.
- The web UI is clearly old, but the adding of achievements is relatively painless.
- On Android, the ID is generated (I can't customize it). That's the only difference. Icons are the same (512x512 JPG/PNG).
- Google Play makes a bit more noise about publishing the changes (they can't be reverted, so it's a good reality check):
- In parallel (while waiting for App Store Connect and Play Console to do their stuff, I implemented the code in the game. Nothing robust, for sure, but it should give me an idea.
- We currently pretty much ignore errors coming from Leaderboard/Achievements.
- Level selection screen should look better.
2022-03-31 Video script
Days worked: 25
- Working on a script for the How To video
- Some prep work for the video recording & editing, to make sure I have everything I need. Test footage looking decent.
2022-04-01 Last changes before "launchable" version
Days worked: 26
- I am now hiding the Leaderboard and Achievements items in the main menu unless / until we're signed into Game Center / Google Play Games Services.
- Working on the level selection screen.
- DONE. Not fantastic, but looks a lot better now. (As in, I'm not completely embarrassed.)
- Okay, here we go. Version 1.7.0 (build 27) is our "launchable version"Â release candidate (see 2022-03-24 Planning). Uploading to the stores.
- Test the release candidate.
- Go through the motions of actually releasing the game (except, of course, not releasing the game). The goal is to have this version ready to release if everything else goes pear shaped.
2022-04-05 "Publish"
Days worked: 27
- First, I prepare the "launchable version"
- Apple (App Store Connect)
- Need screenshots.
- Since I don't have most of the devices listed, I'm going with Simulator. Must not forget to put `debugShowCheckedModeBanner: false` to MaterialApp.
- Apple doesn't make it easy to know which Simulator device to use for these screenshots. This is also changing, as old devices get phased out from Simulator and new ones are added.
- I finally figured it out and posted it below a question on Stack Overflow: https://stackoverflow.com/a/71748686/1416886.
- Ok, screenshots uploaded. This took over an hour? Ugh.
- We need a description:
- Start with the kids game Tic-Tac-Toe and end with the ancient strategy board game Gomoku.
- This games throws increasingly tougher AI at you, with increasingly more complex game rules, in the span of 9 levels. If you can't beat level 1, you might be a child. If you can beat level 9, you are Galaxy Brain.
- board game, tic tac toe, gomoku
- Submitting for review now, with "Manually release this version" checked.
- Here's a handy list of things that MUST be done before doing review:
- Name
- Subtitle
- Category
- Content Rights (basically just needs confirming that you have the rights to the content you show)
- Age rating (need to fill out a form)
- Pricing and availability
- Tax Category (app / game / book / etc.)
- Availability (which countries)
- if you want to offer your game in mainland China, for example, you have to obtain some form of approval number from the Chinese government.
- I have this, originally drafted with help of the Termly.io Privacy Policy Generator (here, but be warned, it's not as "free" as it tries to look).
- just select what kind of information you collect
- don't forget to push "Publish" when you're done filling out the App Privacy section. This "publishes" your privacy responses (not the game).
- Screenshots
- Description
- keywords
- Support URL
- A build in TestFlight to "promote"
- In-App Purchases / Leaderboard / Achievements (if you want them, of course) to "promote"
- Your contact information
- Plus:
- You must set up Content Rights Information in App Information.
- You must enter a Privacy Policy URL in App Privacy.
- You must select a primary category for your app.
- You must choose a price tier in Pricing.
- You must select the level of frequency for each Apple content description in the Age Rating section.
- Before you can submit this app for review, an Admin must provide information about the app’s privacy practices in the App Privacy section. Learn More
- Submitted
- Make sure Managed Publishing is turned on
- This lets you submit an app or game for review without automatically releasing it when it's approved.
- Play Console must say: "Managed publishing on - You control when approved updates are published"
- The caveat is that you now have to remember to actually publish changes to your app (even updates) in the "Publishing overview"
- Go to Internal testing (or whatever other track you're developing on) and tap on "Promote release" and then "Production"
- Must do before release:
- Select countries / regions
- Go to Release → Production, then select the Countries / regions tab. Then, "Add countries / regioins"
- Now you can "Start rollout to production"
- A scary dialog appears:
- This looks like clicking the button will actually roll out immediately. But it's really just submitting for approval. Plus, we have the Managed Publishing turned on. So the dialog is quite a bit over-dramatic. (I learned this the hard way when I published Knights of San Francisco. I thought "that's it" when I saw this dialog, and waited until the day before release day. Then I finally clicked the "Rollout" button and, to my dismay, realized this is just the submission to approval.)
- DONE! (In review)
- Starting to organize code into more managable and testable parts. Also, making the naming a lot more consistent and standard. Not a huge rework, just things like:
- `AdController` that manages MobileAds
- inAppPurchaseNotifier → inAppPurchaseController
- `GamesServicesController` for achievements and leaderboards
- `AudioSystem` → `AudioController`
- `Settings` to `SettingsController`
- Added a line to the main menu that says "Music by Mr Smith". I hope to add more information to the the Settings / About page, but this is good enough for now. Just in case I forget, at least there's this.
2022-04-06 Code quality
Days worked: 28
- Apple already approved the "realeasable" MVP version of the game (pending developer release). They made it in less than 10 hours.
- Code quality improvements
- Ads should have a controller
- Everything that looks like a controller should be called a controller
- InAppPurchaseNotifier → controller
- AudioSystem → controller
- Settings → SettingsController
- all DONE
- simplify InAppPuchase API
- `GamesServicesController` for achievements and leaderboards
- Palette should be a ChangeNotifier, or should be const, so that changes to palette immediately show up.
- DONE - palette has getters now
- Consisten naming for persistent stores - Service? Persistence?
2022-04-08 Code quality part II
Days worked: 28½
- AudioController should depend on SettingsController, not the other way around?
- that way, audiocontroller can deal with its own lifecycle
- buttons and such can just call audioController.play() and let it deal with muted etc.
- settings just change a value (ValueNotifier), and save the new value to persistence
- audiocontroller now must subscribe to applifecycle (settings doesn't need to anymore)
- DONE!
- Remove dependency on IAP from Ads - DONE
- Later
- On launch, "It both thinks I’m logged in and asks me for login. Some kind of race?"
- On launch, "does not respect hardware silent toggle on iOS"
- Merge preloaded_banner_ad into ads_controller
2022-04-10 Extracting the template
Days worked: 29½
- Preparing the "shell" template
- Removing everything extra - DONE
- Adding a smoke test - DONE
- Getting the template to run
- crashes on startup, no logs (flutter run hangs before showing any logs)
- "The Google Mobile Ads SDK was initialized incorrectly."
- Had to add a sample AdMob app ID
- Chrome - works as expected
- macOS - works
2022-04-11 Recoding and editing
Days worked: 30½
- Recording the video
- First very rough edit.
2022-04-12 Recording and editing, part II
Days worked: 31½
- Recording the code walkthrough part of the video
- Continuing with the edit
2022-04-13 Editing
Days worked: 32½
- Just throwing myself at the video editing today
- Video length is 53 minutes. Checking that this is okay (we talked about something around 30 minutes before).
- There are several days off (Easter) coming up in which I'm needed out of office, and the deadline for the video is April 18. So I'm hoping to upload the video today and consider it delivered. I will have some time to address feedback tomorrow but that's the last half-day of work for me before Tuesday, April 19.
YouTube chapters
Must be offset after adding official intro.
00:00 - Intro
00:57 - Why games?
01:31 - App-like games versus videogames
03:57 - Flame Engine
04:23 - Tic Tac Toe Sample
05:23 - The Template
06:23 - General Approach
07:25 - Shallow Structure
09:45 - Code walkthrough
43:32 - Assets
52:20 - Conclusion
2022-04-19 Editing, part 2
Days worked: 33
- Here are the proposed changes, ordered roughly by impact/work ratio.
- Adding more chapters on YouTube
- easy and will make the "meat" of the content a lot more accessible
- TODO
- Adding the public source of the sales data (Wikipedia) into the video
- Making the Chapter cards a bit more energetic
- Medium amount of work
- Might give a pinch of additional energy to the video
- Design
- DONE!
- Uses the actual code, scrolling by, as a backdrop
- Much bolder font.
- Adding more Chapter cards into the video
- Medium amount of work
- Might make the code walkthrough more digestible?
- Trimming down Shallow structure
- Trimming down existing video is orders of magnitude harder than trimming down text (script), and runs a high risk of creating more problems than it solves.
- Re-arranging the structure of the video (putting the sections after the code reading)
- Same as above — this could take a lot of time.
- Making this into a series of shorter videos
- This would require re-scripting, I think. I totally get what you're getting at but I'm not sure we have the time before I/O.
2022-04-21 Editing, part 3
Days worked: 34
- Video work
- I created a new edit
- With this, I'm considering my work on the video finished (at least until something else comes up, of course).
2022-04-22 Crashlytics
Days worked: 35
- Starting with `firebase login`. I have firebase-tools from earlier. It just needs updating. Unfortunately, npm update fails.
- Looks like I installed using homebrew. Running `brew update` and `brew upgrade`, getting coffee.
- Ok, now with coffee, `brew upgrade` is still running. I guess I have time to explain that I wanted everything to be at latest version. Not just because I don't want to deal with outdated bugs, but — maybe more importantly — because I want to see new bugs. I've seen many times that something worked for me because I was using an older version of stuff, and then it didn't work with a newer version. The upgrade is, technically speaking, a minor semver update, so theoretically speaking nothing should break. But, alas, we don't live in theory. This is real life, baby. Coffee is good. Still waiting.
- Ugh, after about 15 minutes of upgrading, `firebase login` stops working.
- Weird. I ran `brew cleanup` and then `brew upgrade firebase-cli` (suggested here) and now all works fine.
- Installed flutterfire_cli via `dart pub global activate flutterfire_cli`
- Creating a new project via CLI doesn't work. I can't input any characters here:
- Running `flutterfire configure --help`, I find the `-p` flag to input the project name before the command runs.
- Except this doesn't work since no such project exists. This flag only gets existing firebase projects.
- Ok, I guess I have to go to Firebase Console and create the new Firebase project there.
- In step 1: Naming my project tic-tac-toe-flutter-game.
- In step 2: Disabling Google Analytics for the project.
- This comes with a pretty scary-looking list of disabled features. At first glance, it looks like Crashlytics is disabled if you disable Analytics. But I don't think that's the case, actually.
- I'm not changing anything about the project in the console for now. I want to see if Crashlytics will "just work", without setting them up.
- Re-running `flutterfire configure`, and the new project is already listed.
- The CLI lets me select which platforms to support. Since I really only want to support iOS and Android, I'm deselecting macos and web for now.
- Yay! It worked.
- The tool puts a configuration .dart file into a hard-coded location, lib/firebase_options.dart. I would have preferred to have it somewhere like `lib/src/crashlytics/firebase_options.dart`. But for ease of updating (especially for the template users), I'll keep it where it is.
- Looks like these configuration files are non-secret, so I'm adding them to source control. Quote: "This Firebase config file contains unique, but non-secret identifiers for each platform you selected."
- `flutter pub add firebase_core`
- `flutter pub add firebase_crashlytics`
- I notice this in the docs: "For the FlutterFire CLI to add the appropriate Gradle plugin, the product's Flutter plugin must already be imported into your Flutter app." So, after adding the two Flutter plugins (firebase_core and firebase_crashlytics), I rerun `flutterfire configure`.
- Finally adding the Dart code to handle problems.
- Crashlytics have a rotating log that it attaches to error reports, which is lovely! When using Sentry.io, I roll my own rotating log, and I thought I would have to do the same here. Instead, I just `FirebaseCrashlytics.instance.log(message);`.
- Performance? This seems to go through a method channel for every single log message. This could be a problem, and if it is, I ought to revisit it (maybe rolling my own Dart-based solution, then calling `FirebaseCrashlytics.instance.log()` with the saved logs only when encountering an error?
- After adding everything and running on Android, I get the multidex error again. I though I fixed this? Going with this flutter/flutter GitHub comment.
- TIL that you need to WidgetsFlutterBinding.ensureInitialized() even before using any kind of method channels
- Putting all the logging / crash-catching code into lib/src/crashlytics/.
- It works!
- Something to note: before I reloaded the firebase console, it showed something like "Get started with Crashlytics", and it looked like I do need to set up something in the Firebase Console. I didin't! Reloading the page changed the UI to what you see above.
- Firebase is pretty aggressive about getting you to turn on Analytics. I get it, I get it. But I'd rather it does this later and more delicately. The big red message is a little heavy handed, imho. It makes me _not_ want to turn it on.
- Ok, features of my implementation:
- All the runZoned/isolate/flutterError code is neatly tugged away in lib/src/crashlytics. In main.dart, you only attempt initializing crashlytics and then running the `guardWithCrashlytics` function whether or not you succeed:
- Errors are reported from Flutter itself (flutterError), from callbacks (runZonedGuarded), and from anywhere else (Isolate.current.addErrorListener).
- Errors are reported with:
- Stacktrace
- Metadata (including app version, RAM free, etc.)
- And logs!
- Crashlytics DONE for Android for the game sample.
- validate iOS - DONE
- push versions to stores
- translate the changes to the template
- LoadError - dlopen(/Library/Ruby/Gems/2.6.0/gems/ffi-1.14.2/lib/ffi_c.bundle, 0x0009)
- [...]
- Error: To set up CocoaPods for ARM macOS, run:
- Â arch -x86_64 sudo gem install ffi
- Running `arch -x86_64 sudo gem install ffi` as suggested.
- This fixed it
- Encountering the "double sign in" problem. Looking at logs, it looks that while Games Services logs me in, InAppPurchases doesn't and requires a sign-in.
- TODO: only sign into In-App purchases after player goes to settings and clicks "Remove ads". Save this in Settings so we can check on startup.
- iOS errors are shown separately from Android reports in Crashlytics. I think I like the Sentry.io model better here, where errors in a project are all reported in one big heap so that cross-platform problems can be dealt with.
- iOS gets a long time to "get" the error:
- I've triggeren an error a few minutes ago, and still "waiting".
- Looking at logs, I'm getting
- [Firebase/Core][I-COR000012] Could not locate configuration file: 'GoogleService-Info.plist'.
- [Firebase/Core][I-COR000003] The default Firebase app has not yet been configured. Add `FirebaseApp.configure()` to your application initialization. This can be done in in the App Delegate's application(_:didFinishLaunchingWithOptions:)` (or the `@main` struct's initializer in SwiftUI). Read more: https://goo.gl/ctyzm8.
- [Firebase/Core][I-COR000005] No app has been configured yet.
- …
- and later, after triggering an error:
- flutter: Another exception was thrown: Instance of 'ErrorSummary'
- Oh! It just took its time. Without changing anything, I looked back at Firebase Console and found the crashes there.
- So the log failures above (e.g. Could not locate configuration file: 'GoogleService-Info.plist') are red herrings.
- Pushing new version to stores
- New version 1.7.3+30 in both Android and iOS internal testing tracks
- Transfer the changes to the template
- Finish editing (or re-shoot?) video
- Implement Firebase Crashlytics
- Write long-form documentation (README)
- Re-view the Privacy Policy and other documents of the game
- Document code in template
- Make changes to the game before launch version submission
- More performant confetti
- Nicer main screen
- Nicer text input
- Fix all TODOs
- Prepare app store listings before launch version submission
- Document code in game sample
- Clean out the dev log (remove names and confidential stuff, but it can stay raw, bullet-pointy Google Doc)
2022-04-25 Preparing for final launch of Tic Tac Toe
Days worked: 35½
- Re-view the Privacy Policy and other documents of the game
- Make changes to the game before launch version submission
- More performant confetti
- Nicer main screen
- Nicer text input
- Fix all TODOs
- Prepare app store listings before launch version submission
- Was definitely needed. The new version might not be fantastic, but it is definitely bold, and better than the bland old one:
- Also, it helps "sell" the transition to the level selection screen.
- It's also animated:
- Found a crash from in_app_purchases
- Solved by try/catching it
- Now sending severe log messages (and above) to Crashlytics
- Spent some time on the "in app purchase sign in" bug on iOS reported orignially by Eric. It seems this doesn't have a clean solution:
- It looks like it only happens on testing devices where the user isn't fully signed in, or where they have a sandbox device? NEED CONFIRMING
- Here's what's happening: The user is signed into Games Services (this is "automatic"). And they're also being signed into In-app purchases. The second thing is what shows the "sign in" form.
- I thought I could avoid this by only signing into In-app purchases _after_ the user tries to buy. BUT, in_app_purchase requires (?) that you sign in and subscribe to purchaseStream as soon as possible, ideally in main().
2022-04-26 Last game user test before submitting
Days worked: 36
- Another UserTesting round + testing on my own
- On Android, music now starts only after the first SFX is played. I don't remember changing anything about the music-playing logic.
- Liked the sound effects
- Was pleasantly surprised by m,n,k-game going beyond tic tac toe
- Integration didn't work, but I think this is because of APK being sideloaded.
- signs the user into Google Play Games
- likes the graphics and intuitive interface
- gets a bit hung up on "beyond tic tac toe" — maybe we should say "connect 4 in a row"?
- misunderstands the "don't slow down for them" - thinks it's really about speed.
- TODO: remove the "don't slow down for them"
- All in all, I'm happy. I found a few small issues that will be easy to fix tomorrow. We'll be ready to submit for launch.
2022-04-27 Submit launch version
Days worked: 37
- Bunch of work on getting the game ready for launch
- keeping some less important TODOs in the interest of submitting early and having a buffer. None of the remaining TODOs is breaking the game.
- setting up AdMob "app ID" for the iOS version
- tune scoring so that leaderboards make sense
- "final" version is 1.7.6+33
- Cache shaders for better performance
- Revisiting the app stores to make sure everything is set up for launch
- Re-uploading newer screenshots (the old ones were there from the time of early alpha)
- Once again, hard to know what sizes App Store wants, so I'm glad I documented that on Stack Overflow.
- (Re?)encountered an issue in which Simulator completely crashes when trying to subscribe to In App Purchases. Found the issue on Stack Overflow, too. Since I don't really need in-app purchases to make screenshots, I'm just disabling the integration for the simulator.
- Looking good:
- Since we are using Crashlytics now, I'm revisiting the Privacy parts of app stores
- App Store Connect's "App Privacy" section. Clicking "Edit" next to "Dat Types"
- DONE
- Google Play Store's "Privacy" section (in the left menu, all the way down), then "Data Safety" → "Manage"
- DONE
- Submitting the 1.7.6+33 version
- I got a confusing error at first in App Store Correct (try again later). Then, checking, it said the game was "added to review", so I thought all is good. But it turned out that "added for review" is not the same as "submitted for review". I had to click on a "for review" link and click "Submit" there.
- DONE
- TIL that if you have a version ready to launch, you have to "reject" it first, and then ask for another review. This is not great, since I hoped that the "worse case scenario" version would still be ready to launch if everything else goes pear shaped. Here's hoping the approval process is as breezy with the newer version as it was with the old one.
- Ugh, a minute after uploading, I noticed that Crashlytics groups error messages too eagerly. See here. This is not a huge deal, and is relatively easy to work around, but didn't get to the "golden" version. Oh well.
- Ugh ugh! I my have found an issue with the rendering. Will have to investigate tomorrow or later today. Not sure what happened there.
- Oh no, this looks like a serious issue with SkSL shaders. Filed: https://github.com/flutter/flutter/issues/102655Â (Building Android app with SkSL warmup introduces rendering bug #102655).
- Looks like I can work around the issue by preparing the SkSL shader warmup files on my Pixel 5 (instead of my Nokia 1.3).
- Ok, so since 1.7.6 had this obvious bug, I'm re-releasing with a newer version, with the shaders (hopefully!) fixed: 1.7.7.
- Re-build and upload for both stores
- I think the title screen now looks pretty decent, even if I say so myself.
- These are just quick visualizations in Rotato. This kind of UI does look much nicer when viewed on a device, rather than just as a rectangle.
2022-04-28 Document things
Days worked: 37½
- Inviting the first beta-tester of the game_template — Indy
- Transfering the improvements I made to the game to the template
- Move the Crashlytics documentation to main README
- Making the READMEÂ easier on the eyes by adding things like code snippets, lists, ASCII art:
- Basics are DONE
- Ideally, I'll be able to add pictures and even movies, too, but that won't work properly until I start landing this to the final repo.
- Provided a nicer animation to the folks working on flutter.dev/games
2022-04-29 Document things, part 2
Days worked: 38
- Good news, the "golden" version of the game is now ready to launch on both stores!
- Documenting Games Services now
- I actually found a an issue with the implementation of Play Games Services in the game_template. Yay for writing documentation!
- DONE!
- Documenting in-app purchases
- Adding API docs where it makes sense
- Creating the pull request:
2022-05-02 Landing the PR
Days worked: 38½
- Addressed feedback by Brett and Parker, fixed a silly bug with the web version along the way.
2022-05-03 Tutorial title card
Days worked: 39
- Improving the title card of the video
2022-05-04 YouTube metadata
Days worked: 39½
- Working on YouTube metadata
2022-05-06 Making sure everything is ready for next week
Days worked: 40½
- Acting upon the meeting from yesterday.
- Creating a macOS icon
- Here's an online tool that helps make the different sizes: https://appicon.co/#app-icon. But I ended up using my vector editor's (Affinity Designer's) built-in functionality for exporting in different resolutions.
- Contrary to other platforms, Mac app icons must do their own rounded-rectangles (or whatever they need; some are circular or skueuomorphic). There's some confusion around it.
- For lolz, I tried to run the iOS app on my M1 macbook (via Mac Catalyst, a relatively new feature) and it runs perfectly well!
- It even does achievements & leaderboard!
- And ads (because it's an iOS app, for all intents and purposes):
- No fishiness whatsoever, contrary to what I expected based on my previous experimentation with the feature.
- I might actually allow App Store Connect to publish the app on the Mac App Store after all!
- Note that this is separate from being able to build and run the game on macOS natively, using Flutter's `build macos` capability. All other things being equal, it's of course better to build the game directly for macos from Flutter, and not relying on Apple's "Catalyst" feature.
- Also implemented a proper confetti animation so that I don't need to use a gif anymore. This should also be a lot more performant on most devices.
- Got the right format for the house ads, and uploaded it to AdMob.
2022-05-10 Launch day
Days worked: 40½
- Publishing github.com/filiph/tictactoe - DONE
- Hah! About 2 hours in, and I already have an issue filed on that repo… It's about the store link not working. :)
- Publish game on Android - DONE
- Weirdly, the game listing showed really old screenshots at first, but after about 10 minutes, all is good
- Publish game on iOS - DONE
- AdMob - get app reviewed
- AdMob is not finding it yet on Google Play
- UPDATE: About half an hour later, it's there
- linking the iOS app works right out of the bat
- Implementing suggestion to the template's README - DONE
- Re-basing the game_template with newest changes - DONE
- Add licensing info to top of all Dart files - DONE
- "Verify web demos", an unrelated suite of tests, is failing. Otherwise, all is green.
- Ignoring that failure, as suggested.
2022-05-12 Upgrade to Flutter 3
Days worked: 41
- Upgrade the sample game to Flutter 3
- Includes new warm-up shaders
- Includes new confetti animation (instead of the stand-in gif): more performant, and in color scheme
- Includes improvements to Crashlytics
- Android release: DONE
- iOS problem: "CocoaPods's specs repository is too out-of-date to satisfy dependencies"
- Another iOS problem is that App Store Connect isn't working today.
- A few hours later, it did start to work.
- iOS release: IN REVIEW
- Upgrade game_template to Flutter 3
2022-09-09 Upgrade audioplayers
Days worked: 41½
- Unfortunately, a bunch of underlying libraries have moved under the template
- Went ahead and removed ios/ and macos/ runners, and created them anew with `flutter create .`.
- There's been a change in how CocoaPods work, maybe? I had to run `flutter clean`, as suggested here. The error was: The linked framework 'Pods_Runner.framework' is missing one or more architectures required by this target: x86_64
- The swift part of GamesServices seems to have changed:
- /Users/filiph/.pub-cache/hosted/pub.dartlang.org/games_services-2.2.2/macos/Classes/SwiftGamesServicesPlugin.swift:115:76: error: value of type 'NSImage' has no member 'pngData'
- Â Â Â Â Â Â let imageData = try? await item.player.loadPhoto(for: .normal).pngData()
- Had to upgrade to games_services: ^3.0.0. Thankfully, no breaking change is affecting the template (at least from the first few minutes of testing).
- After getting macos to work, I'm seeing another error in ios/:
- Searching for inspections failed: undefined method `map' for nil:NilClass
- This seems to be Apple M1 (arch = arm64) related. Fixed this with reinstalling ffi via Rosetta (here):
- sudo arch -x86_64 gem install ffi
- # Then, in ios folder.
- arch -x86_64 pod install
- Another problem was with ios provisioning profile. I neglected to agree with some updated TOS, and had to go to https://developer.apple.com/account/Â to do that, then open the ios project in Xcode and push "Try again" in Signing & Capabilities.
- Next, had a problem running on an iPad, but got a nice error from Flutter
- Could not run build/ios/iphoneos/Runner.app on 00008101-00063DA63C85001E.
- Try launching Xcode and selecting "Product > Run" to fix the problem:
- Â open ios/Runner.xcworkspace
- Turns out I had to delete the old app from the testing device first because Xcode refused to update an app with a different signing.
- Going to slowly upgrade all dependencies to latest version:
- go_router — needs upgrade to a major new version, with major changes (migration guide).
- Punting for later, as navigation is not the main value of this sample.
- flutter_launcher_icons — from 0.9.3 to 0.10.0
- This is easy, since this is only a dev dependency, and we're not running it (the user of the template is running it once the change the icon). I confirmed that the tool works even with the new version.
- now audioplayers — from 0.20.1 to 1.0.1
- The main thing to upgrade.
- This was a bit hairy since they changed AudioCache to be a different class and moved things around. But it got done, and, on the plus side, the code looks a lot cleaner.
- google_mobile_ads — from 1.3.0 to 2.0.1
- This just worked. Seems to work on device, too.
- Ok, going back to go_router
2022-12-03 Fixing go_router breakage
Days worked: 42
- I couldn't reproduce this at first, but then it occurred to me to upgrade dependencies (with go_router still pinned at major version 5 with ^5.0.5). After upgrading, navigation indeed broke.
- This is quite frustrating. I'm guessing the new behavior was always the intended behavior, and game_template worked correctly until now despite me holding it wrong. I should have double checked that pop() indeed does what I thought it did (remove a level of navigation, i.e. go to the previous /).
- Fixed with https://github.com/flutter/samples/pull/1515Â
- Mixing declarative (.go()) and imperative (.pop()) navigation no longer works.
- This CL changes all navigation to the declarative go() wherever possible. The only imperative navigation still standing is the /settings route, which needs to be accessed (pushed) from both the main screen and the play session screen, and therefore there is no single URL to go back to. So we’re keeping push() / pop() there.
- I’m sure it’s possible to implement even the /settings route with declarative navigation, but it seems way beyond the scope of this sample.