Skip to content

Port to Unity 6 (6000.5): 2026 Play Store compliance, level-data fix, tests & CI - #3

Open
kauenet wants to merge 17 commits into
Ordyns:mainfrom
agentalabs:port/unity-6.x
Open

Port to Unity 6 (6000.5): 2026 Play Store compliance, level-data fix, tests & CI#3
kauenet wants to merge 17 commits into
Ordyns:mainfrom
agentalabs:port/unity-6.x

Conversation

@kauenet

@kauenet kauenet commented Jul 11, 2026

Copy link
Copy Markdown

Hi! We loved this game and wanted it to keep living on modern toolchains, so here is a complete, verified port from Unity 2020.3.11f1 to Unity 6 (6000.5.3f1) — plus fixes for two latent bugs on main and a quality/testing/CI baseline. Play testing was done throughout on the ported build.

Why

  • Google Play requires app updates to target API 36 by Aug 31, 2026 (plus 16 KB page-size support); Unity 2020.3 cannot produce a compliant build.
  • The legacy com.unity.ads direct integration was discontinued by Unity in Jan 2026 and its SDK breaks Gradle builds on Unity 6.

What's inside (reviewable commit-by-commit)

  1. Engine port — URP 10.5 → 17.5 (renderer auto-upgrade verified), TextMesh Pro → uGUI 2, DOTween → 1.3.030, Zenject 9.1 kept (its full vendored test suite passes on Unity 6; two FindObjectsOfType call sites migrated, [RuntimeInitializeOnLoadMethod] removed from generic pool classes where Unity 6 rejects it).
  2. Android chain — IL2CPP, ARM64+ARMv7, target SDK 36, min SDK 26 (Unity 6.5 floor), Gradle 9/AGP 9 templates, Play In-App Review plugin 1.7 → 1.8.4 (drops the deprecated monolithic Play Core that Play now rejects). Google-Play-compliant AAB verified with bundletool/readelf: both ABIs, 16 KB alignment, only INTERNET permission.
  3. Unity Ads removed — the network no longer serves the legacy SDK, ads were shipped soft-disabled (empty game ID), and the SDK breaks Unity 6 Android builds. Hints are now free; the reward gate was already non-functional on Android 9+ due to a cleartext-HTTP reachability check.
  4. Lighting — all 24 levels re-baked (2020 LightingData is incompatible with Unity 6, which is GPU-lightmapper-only now).
  5. Bug fixes for issues that exist on main today:
    • Island.cs/IslandEnergy.cs referenced UnityEditor APIs in runtime code — player builds could not compile, and island materials would never initialize in a build. (Application.isPlaying replaces the editor-only check.)
    • The IslandEnergy refactor left the island prefabs without the IslandEnergy component, so RequireComponent auto-added it with default values and every scene's serialized direction overrides silently dangled — no level could be completed. Restored the authored data from git history + the dangling prefab-instance overrides across all prefabs, 24 levels, Level Template and Tutorial; verified 151 islands with zero degenerate direction pairs and a full start-to-finish solve of Level 1.
    • Render textures (HintTexture, Guide) lacked a depth-stencil format and carried a corrupt volume depth after upgrade — Unity 6's Render Graph errors on every frame otherwise.
    • SaveSystem now falls back to defaults on corrupt save files instead of throwing.
  6. Tests & CI — 36 EditMode tests (save round-trips, legacy save migration, MVVM primitives, direction math; fixtures back up local editor saves), plus GitHub Actions: no-secret static checks on every push, EditMode tests and a manual Android build via game-ci that self-skip until a UNITY_LICENSE secret exists.
  7. Docs — README updated for Unity 6 setup/build/test (GUI + command line via the new Assets/Editor/BuildTools entry points), CHANGELOG added.

Notes for review

  • The diff is large because of one-time mechanical changes: asset reserialization from the version jump and the 24 lightmap re-bakes. The hand-written changes are concentrated in Assets/Scripts, Assets/Hint, Assets/Islands, Assets/Editor, gradle templates and Packages/manifest.json — reviewing commit-by-commit keeps each step small.
  • Release signing is untouched (builds are debug-signed until a keystore is configured).
  • Nothing gameplay-visible changed except the fixes above; the campaign, tutorial, hints, localization and review prompt behave as before.

Happy to split this into smaller PRs, adjust anything to your preferences, or keep maintaining the port — thanks for open-sourcing such a lovely game!

kauenet added 17 commits July 11, 2026 20:00
- manifest.json: drop device-simulator (preview, unresolvable on 6000.x),
  ide.vscode, collab-proxy, recorder, timeline (unused), textmeshpro
  (merged into ugui 2.x on Unity 6); com.unity.ads stays until ads strip
- delete packages-lock.json for clean resolve
- DOTween: replace 2018-era DLL install with 1.3.030 (official Unity 6
  support); DLL GUID unchanged so DOTweenSettings.asset reference holds
- delete orphaned AdMob iOS bridge (Plugins/iOS) and NaughtyAttributes
  sample tests (asmdef compiled into player builds)
First clean open on Unity 6.5: package auto-migration (URP 10.5->17.5,
TMP merged into ugui 2.5, ads auto-bumped 4.16.4), full reserialization,
URP global settings + default volume profile created.

Manual fixes required by the 6.5 Tech stream:
- NaughtyAttributes editor: GetInstanceID() is obsolete-as-error on
  6000.5 -> GetEntityId() (2 sites, string cache keys)
- TMP Examples & Extras deleted: 3.0.6-era sample scripts don't compile
  against UGUI-2 TMP (Vector4 UV API); zero game references (verified)
- TMP Essential Resources refreshed GUID-matched in place at
  Assets/Plugins/TextMesh Pro (33 updated, 6 new)

gitignore: UserSettings/
Unity Ads direct integration is unsupported since Jan 2026 and the game
shipped with an empty game ID (rewarded hint gate was already broken on
Android 9+ by its cleartext reachability check). Hints are now free.

- delete AdsManager.cs; remove com.unity.ads from manifest
- HintUI: hint button shown whenever steps exist; ad button stays
  serialized but permanently hidden (avoids scene edits)
- HintViewModel: drop AdsManager dep, IsAdViewed persistence and the
  hint_data save file (old files are simply unread)
- ProjectInstaller: BindInstance(levelsInfoProvider) only
- ProjectContext.prefab: AdsManager GameObject removed via editor script
- UnityConnectSettings: engine-level ads init disabled
- add Assets/Editor/PortTools (headless port helpers)
- replace GooglePlayPlugins 1.7.0 (deprecated Play Core 1.10.3 monolith)
  with Play In-App Review 1.8.4 (+ common 1.9.2, core 1.8.6, EDM4U
  1.2.185); native deps are now review:2.0.2 + core-common:2.0.4;
  appbundle plugin dropped - Unity builds the AAB natively
- regenerate gradle templates from 6000.5.3f1 (Gradle 9.1/AGP 9,
  Java 17); old 2020 templates carried dead Firebase deps and removed
  AGP flags
- EDM4U force-resolve: deps injected, jetifier off, resolver state
  regenerated (stale Firebase/PlayCore references gone)
- player settings: IL2CPP, ARM64+ARMv7, minSdk 26 (Unity 6.5 floor,
  clamped from 25), targetSdk 36 pinned, versionCode 7
2020.3 LightingData assets are incompatible with Unity 6 (and the CPU
lightmapper they were baked with was removed). Each level re-baked
against its existing per-scene .lighting settings; scenes reserialized
to Unity 6 format in the same pass.
Island.cs / IslandEnergy.cs (from the recent IslandEnergy refactor) used
UnityEditor.EditorApplication.isPlaying in runtime code - player builds
could not compile, and IslandEnergy would have left its material null in
any build. Application.isPlaying is the portable equivalent and fixes
both.

Verified on Builds/MakeNewWay.aab (dev-signed, 71.5 MB):
- targetSdk 36, minSdk 26, versionCode 7
- arm64-v8a + armeabi-v7a, all LOAD segments 16 KB-aligned
- permissions: INTERNET only (no AD_ID)
- play review 2.0.2 + core-common 2.0.4 in dex; no Play Core monolith,
  no Firebase SDK, no Unity Ads classes

gitignore: /Builds/
- delete BaseSceneContext (pre-DI service locator, no references) and the
  hidden ViewAd button left in Base.unity after the ads removal
- drop write-only state fields (AnimatedPanel.isOpened,
  MoveIslandTutorialView.isShowing, Screenshot.showPreview/lastTime)
- Screenshot window: EditorWindow.title -> titleContent
- Analytics: replace commented-out Firebase code with a plain stub
- SaveSystem: corrupt save files load as defaults instead of throwing
- fix localization error message wording
- replace one-shot PortTools with durable BuildTools entry points
  (ConfigureAndroid, ResolveAndroidDependencies, BakeLevelScenes, BuildAab)
36 tests covering SaveSystem round-trips and corrupt-file recovery, the
legacy data.sv migration (with backup/restore of local editor saves),
ObservableProperty/DelegateCommand, PlayerData's monotonic level unlock,
and Direction angle math. Zenject's vendored suite runs alongside them;
457 tests total pass on 6000.5.3f1.
- CI: static checks on every push (manifest validation, guard against
  UnityEditor references in runtime code), EditMode tests via game-ci,
  manual Android AAB build; Unity jobs self-skip without license secrets
- README: Unity 6000.5.3f1 setup, run/test/build guides for first-time
  Unity users, CLI recipes, project structure map; stale Firebase/ads
  sections replaced
- CHANGELOG: 2.1.0 port notes
Unity 6's Render Graph rejects camera output textures without a depth
buffer; HintTexture and Guide were 2020-era assets with depth None,
spamming per-frame execution errors. Both now use 24-bit depth+stencil.

Zenject: migrate the two live FindObjectsOfType calls to
FindObjectsByType (InstanceID order preserved) and drop the bundled
integration-test fixtures that shipped serialization warnings.
Diagnosed live via the Unity MCP bridge:
- HintTexture upgraded from its 2020 format with volumeDepth=100 on a
  Tex2D; Render Graph rejected the descriptor on every editor repaint
  ("slices larger than one"). Reset to 1 and force-reserialized both
  render textures to the modern asset format.
- Zenject's generic pool classes (ListPool, DictionaryPool, HashSetPool)
  carried [RuntimeInitializeOnLoadMethod] resets, which Unity 6.5
  rejects in generic classes (3 errors per domain reload). The resets
  only served domain-reload-disabled play mode, which this project does
  not use; removed.

Console verified clean through a domain reload and live repaints.
The pre-port refactor (2ed678f) moved the energy direction fields from
Island to a new IslandEnergy component but never added that component
to the island prefabs. RequireComponent silently auto-added it with
default values at load, and every scene's authored direction overrides
kept targeting the fields removed from Island - dangling and ignored.
Result: every island reported input==output==UpperLeft and no level
could complete. Unnoticed because the game was never played after the
refactor.

Recovery, scripted through the editor:
- prefabs: IslandEnergy added with the pre-refactor defaults recovered
  from git history (Start out=DownRight, Movable/Rotatable in=DownRight,
  Finish input-only); complex prefabs' dangling child overrides
  reapplied (14 values)
- scenes: dangling per-instance overrides read via
  PrefabUtility.GetPropertyModifications and written onto IslandEnergy
  across 24 levels + Level Template + Tutorial (live overrides win over
  stale ones)

Verified: 151 energy islands across 26 scenes, zero with degenerate
input==output; Level 1 walked start-to-finish in play mode.
Base declares a Zenject 'Level' scene parent contract, so pressing Play
with only Base open aborted with a contract exception - a recurring trap
during level iteration. A DefaultSceneContractConfig in Resources now
fills the contract with Level 1 automatically. Editor-only behavior;
runtime scene flow is unchanged.
The transition curtain parks its parts off-screen when idle, but the
panel stayed active - and its invisible graphics are raycast targets,
so every UI click died on the shield. The shipped flow masked this
(the first menu transition closes the panel before any interaction);
booting Base directly via the scene-contract config exposed it.

The panel now deactivates at the end of Init and is reactivated by
CreateNewTransition. Verified in play mode: panel inactive at boot,
all visible buttons reachable by EventSystem raycast.
@kauenet

kauenet commented Jul 11, 2026

Copy link
Copy Markdown
Author

Added two follow-up commits from continued play-testing:

  • Play-from-Base auto-load: pressing Play with only Base.unity open used to abort with a Zenject scene-contract exception — a recurring trap during level iteration. A DefaultSceneContractConfig in Resources now auto-loads Level 1 to fill the contract (editor-only; runtime flow unchanged).
  • Idle transition panel swallowed UI clicks: the scene-transition curtain keeps its panel active while idle, and its invisible graphics are raycast targets — any boot path that doesn't immediately run a transition leaves a full-screen click shield. The panel now deactivates when idle. Latent in the original code; exposed by the new boot path, fixed at the source.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant