Topic 13 · Shipping
Platform build pipeline
Android/iOS/IL2CPP, artefacts, build modes and device testing.
Android/iOS/IL2CPP, scripting defines, Gradle and vendor SDK ports. Mostly failures that no editor check can catch because they only exist at build or launch time.
- PRODUCT-VERSION-IS-THREE-SEGMENT — product version is
MAJOR.MINOR.PATCHin repo-rootVERSION.txt, and that string must equalPlayerSettings.bundleVersion/Application.version. Start development at0.0.1; increment PATCH for every product change; PATCH 0–99 rolls into MINOR (0.0.99→0.1.0) and never persists as 100. Keep MAJOR at 0 throughout development. Advance only to1.0.0when the developer explicitly requests the production release; do not roll into MAJOR automatically. AndroidbundleVersionCode/ platformbuildNumberare monotonic and never go backward. EditbundleVersionthrough the PlayerSettings API (PROJECTSETTINGS-EDIT-VIA-API) — a disk edit ofProjectSettings.assetwhile the editor is open reverts. The file isVERSION.txt, never bareVERSION(next bullet). Project spec lives atdocs/GAME-VERSION.mdwhen the title has one. - VERSION-BUMPS-AT-THE-MERGE — bump PATCH where changes serialize, in the merge into the integration branch, never per commit in parallel branches. Per-commit bumps in parallel worktrees collide (two branches both claim
x.y.12), so they get skipped, and the number stops meaning anything: one project found 627 product commits behind a version that never moved. Rule: every merge into the integration branch that touches product files (Assets, ProjectSettings, Packages) bumps PATCH in the merge commit, reading the number from the remote branch AFTER the merge; a rejected push re-merges and re-reads, so no two merges share a number. A repair counts the missed commits (git rev-list --no-merges --count <last-bump>..origin/<branch> -- Assets ProjectSettings Packages) and adds one per commit. Write each active build profile'sbundleVersionoverride too: an active profile overrides the global PlayerSettings value. - SERVER-VERSION-LINE + BUILD-STAMP — a dedicated server keeps its own version line and every build names its commit.
SERVER-VERSION.txtstarts at the client version it diverged from; server-only changes bump it, client-only changes bumpVERSION.txt, shared contracts (RPCs, replicated components) bump both. The build step writesBUILD.txtbeside the exe (client version, server version,git rev-parse --short HEADplus+dirtywhen product files were uncommitted, UTC time); each client sends that stamp once on join and the server logs and lists any that differ. Two version numbers say THAT builds differ; the commit pair says HOW (git log <a>..<b>). - Android IL2CPP: never put a bare file named
VERSIONat the project root. NDK libc++ does#include <version>(C++20 header). On Windows the FS is case-insensitive, so the project-rootVERSION(game semver text) is picked up instead and every translation unit dies withversion(1,1): expected unqualified-id. UseVERSION.txt/GAME_VERSION/ a non-root path. (Seen in a mobile strategy game: Android build.) - Generalize the check: ANY extensionless file at the compiler's include root whose lowercase
name matches a C++ standard header is poison —
version,ratio,string,array,list,map,set,vector,memory,new,limits,numeric,optional,atomic,chrono,format,span,bit,concepts,ranges,future,thread,mutex,regex,random,complex,bitset,functional,iterator,tuple,utility,variant,any,filesystem.LICENSE,README,CHANGELOG,Dockerfileare all safe. Audit is one line —ls -la | awk '$0 ~ /^-/ {print $NF}' | grep -v '\.'— and it is worth running once per repo rather than waiting to recognise the one name that already burned you. - DELETE-AND-IGNORE, never just add the replacement — a poison file left TRACKED IN GIT is a
time bomb that re-arms on the next checkout. This is the half that gets skipped: the
fix correctly introduced
VERSION.txt, but nobody rangit rmon the bareVERSION, so it sat committed and clean for 18 days and a routine checkout restored it — costing a 37-minute Android build that died inPostprocess built playerwith 19 identicalversion(1,1): error: expected unqualified-idfailures. Nothing had changed in the build config; the repo simply handed the file back. So the fix is three parts, and the third is the one that makes it stick: (1) create the replacement, (2)git rmthe poison file, (3) add it to.gitignore(anchored —/VERSION, notVERSION, or you also ignore every nested one) with a comment naming the failure, and verify withgit check-ignore -v <file>. Rule of thumb beyond this instance: when the remedy for a landmine is "rename it", the rename is not done until version control can no longer produce the old name. Same shape as the vendor-residue trap where the generated Gradle project keeps handing back a deleted vendor file. -
The error names the STDLIB header, not your file — read the include chain, not the last line. Clang reports the chain (
ratio→<version>) and only the final frame carries your repo path, so the failure reads as a broken NDK/toolchain rather than a stray file, and the-ferror-limitfatal error: too many errors emittedat the bottom buries it further. (a mobile strategy game.) -
MOBILE-TEXTURE-AUDIT-RESOLUTION-NOT-FORMAT — when a mobile build is too big, measure before optimizing: compression is usually already correct and RESOLUTION is the lever. The reflex is to hunt uncompressed textures; on a real project the importer defaults have almost always handled that. Measured (a mobile strategy game, 771 textures): compression was ETC2 across the board with only 7.7 MB uncompressed — and that 7.7 MB was data textures (splat, walkability mask, colormap, a LUT) that MUST stay raw. Meanwhile 115 textures over 1024px were 413 MB of a 495 MB payload. Capping units/buildings at 1024 and VFX at 512 took the shipped total to 121.6 MB with formats byte-for-byte unchanged. Method notes that matter: (1) source file size is NOT shipped size — a 64 MB 4096² TGA ships as ~10 MB ETC2, so rank by
min(source, maxTextureSize)² × bits-per-pixel × (1.333 if mipmapped), not by bytes on disk; (2) a cap above a texture's native size is a no-op — 515 icons at 256² carrying a 2048 cap looked like an easy win and yielded 0.2 MB, so exclude already-small art from the projected saving instead of counting it; (3) exclude data textures, terrain, sky and font atlases by path BEFORE the pass, and say which were excluded; (4) hold SHARED atlases (a clothing atlas serving ~20 units) one tier higher than single-object textures — halving the one texture every unit samples is where softness actually shows; (5) always DRY RUN the classification and print per-category before/after, because an importer sweep's blast radius is the whole matched set (see the AssetPostprocessor blast-radius rule in the assets serialization page); (6)SaveAndReimportalso migrates the.metaserialization (serializedVersion,ignorePlatformSupport, materialized platform blocks), so the diff is wider than the property you set — verify no FORMAT changed by re-reading the format distribution afterwards, rather than assuming only your field moved. -
PLAYER-COOK-SWEEPS-UNUSED-ASMDEFS (HARD) — before every player cook, drop unused runtime asmdef
referencesand leftover packages that no player script names. Managed stripping (next bullet) cannot save you from a reference you still declare: the assembly is considered used and ships. Runtools/asmdef-unused-refs.shfrom the project root, confirm each hit, remove the ref, then cook. Same slice as the cook, not a later cleanup. Detail: the compile assemblies page UNUSED-ASMDEF-REFERENCE-SHIPS-THE-LIBRARY. A keep-all shader-variant vote is the opposite of this rule — HDRP already keeps the lighting keywords the HDRP asset uses; blocking that strip compiles tens of thousands of unused Forward variants. -
MANAGED-STRIPPING-MINIMAL-IS-THE-DEFAULT — Unity strips unreferenced ASSETS, not CODE. At the IL2CPP default (
Minimal) every runtime assembly underAssets/andPackages/ships whole. The developer's model ("Unity strips what scenes don't use") is true for textures and audio and false for code: Minimal trims only UnityEngine and the .NET class libraries, so every asset-store plugin, every unused Mirror transport and every demo assembly is compiled to C++ and linked. Measured (a mobile card game, a uGUI card game): 942 MB of generated C++, 68 MB of it BouncyCastle from an Encryption transport nothing referenced; arm64libil2cpp.so114.6 MB; App Store install 315 MB. Two PlayerSettings, no code, took it to 373 MB of C++ / 46.3 MB / AAB 216 -> 144 MB:ManagedStrippingLevel.MediumandIl2CppCodeGeneration.OptimizeSize("Faster (smaller) builds" — generics were 46% of the C++). Rank what to expect by the generated-C++ size per assembly inLibrary/Bee/artifacts/<Platform>/il2cppOutput/cppand the survivors in.../ManagedStripped; both are free to read after any build. The price is alink.xml, and it is the whole contract: the linker keeps only what static references reach, so anything resolved by NAME at runtime is listed there — your own assembly (JsonUtility DTOs, save snapshots, weaver output), Newtonsoft, DOTween, attribute-discovered effect libraries, native bridges with string callbacks. UGS packages self-preserve with[Preserve]; Unity's Newtonsoft package ships a link.xml that only coversSystem.ComponentModelconverters. Deliberately leave dead plugins OFF the list so they strip, and still delete them — the linker keeps a stub of each. Never answer aMissingMethodExceptionon device by going back to Minimal; add the line. It compiles, plays in the editor, and dies only in a release build. -
NPOT-MULTIPLE-OF-4-SHIPS-RAW-RGBA32 — the exception to "compression is usually already correct": a sprite whose size after the max-size downscale is not a multiple of 4 cannot be ETC2 (or crunched ETC2) and Unity silently ships it RGBA32. The importer still reports the compressed format; only the built texture is raw, and
Texture2D.formaton the imported asset for the active target shows it (1024x775 -> RGBA32, next to a512x512 -> ETC2_RGBA8). Measured (a mobile card game, 477 textures): 169 shipped raw, 114 of 135 MB, including 80 the previous audit had "crunched" to no effect. ASTC has no such rule (verified: a 42x64 sprite compresses with an ASTC override), so iOS and ASTC-default Android are exempt and the bug hides on the platform you test. Fix shape: sprite atlases (pages are POT) or sources resized to multiples of 4; a max-size cap alone just picks a new non-multiple. Audit by joining the build log's per-asset sizes to imported dimensions and flaggingsize ~= w*h*4. -
BUILD-WINDOW-COMPRESSION-IS-A-BUILDOPTION — the Build Settings "Compression Method" is not project state; the window adds
BuildOptions.CompressWithLz4[HC]per build. A scriptedBuildPlayer(or an MCP build tool) that passesBuildOptions.Noneships loose uncompressed data — on Androiddata.unity3dvanishes and the Data folder went 84 -> 204 MB — so its size numbers are not comparable to the developer's builds. Read the window's choice with the internalEditorUserBuildSettings.GetCompressionType(BuildTargetGroup)and pass the matching flag. -
IL2CPP-MASTER-IS-SHIP-ONLY —
Il2CppCompilerConfiguration.Masterleft in PlayerSettings turns EVERY manual File ▸ Build into a ~30 min whole-program link, and nothing in the UI says so. Master enables whole-program optimization: the link is not incremental, so a one-line change pays the same cost as a cold build, and the IL2CPP cache bucket is keyed on the configuration so it shares nothing with Release builds. The trap is that the setting is correct — a ship build wants Master — andBuildPipeline.BuildPlayer/ the Build Settings window simply consume whatever PlayerSettings holds, with no warning, no progress hint distinguishing link from compile, and no per-build override in the UI. So the project is configured right and every iteration build is silently a ship build. The tell is a build whose Unity progress bar sits on the linker for tens of minutes while the editor is otherwise idle; checkPlayerSettings.GetIl2CppCompilerConfiguration(NamedBuildTarget…)(YAMLil2cppCompilerConfiguration:—Debug=0, Release=1, Master=2, and an ABSENT platform key means Release, so only the platforms actually listed are slow). Fix shape — an iteration build path overrides the configuration per-build and restores it in afinally: set Release,BuildPlayer, restore the project value infinally(not after the call) so a thrownBuildFailedExceptionfrom a preprocess/postprocess guard cannot strand the project on the iteration configuration and silently downgrade the next ship build. Surface the cost next to the picker rather than in a doc nobody opens, and predict cache reuse BEFORE building — afterwards the fingerprint is already overwritten. Corollary: obfuscation / signing / symbol postprocessors usually gate on!BuildOptions.Development, so a manual non-dev build also pays those; run iteration builds as Development, or as Release-non-dev only when you need the real shipping surface. (a mobile strategy game: Standalone pinned to Master, a manual Windows build ran 30+ min; the project's own Build & Ship tab already offered a Release iteration path.) -
Android Active Input Handling must be Input System Package only — not Both. Player setting
activeInputHandler: 2(Both) logs "unsupported on Android" and breaks touch/perf. PC mouse/keyboard + mobile EnhancedTouch all live on the new Input System; useInputSystemUIInputModule(not StandaloneInputModule). (Seen in a mobile strategy game: Android build.) -
MOBILE-TIER-FROM-HARDWARE — derive the graphics platform tier from the device at boot; never let it default to a desktop pref. And every UI panel host resolves its device scale through ONE shared helper. Two paired mobile-ship traps: (1) a settings system whose platform-tier pref defaults to the desktop band boots phones on desktop defaults (long shadows, HQ post-FX, uncapped FPS → slideshow); force tier from
Application.isMobilePlatformevery boot and seed the mobile performance band ONCE so later player tuning survives. (2) When compact layouts key offisMobilePlatform, every PanelSettings host must re-reference through the same central resolve — one screen cloning a mobile reference locally while the rest use the raw desktop asset ships mixed UI scales (menu correct, HUD ~2.5× too small to tap). (Seen in a mobile strategy game: Android build: 15 fps + tiny in-game UI.) -
STANDALONE-NATIVE-RESOLUTION —
defaultIsNativeResolutionis ignored when the player starts Windowed, andScreen.currentResolutionis not the monitor. IfPlayerSettings.fullScreenModeisWindowed, Unity usesdefaultScreenWidth/Height(often leftover 1024×768) and never reads native. Native-at-launch needsFullScreenWindow/ExclusiveFullScreenplusdefaultIsNativeResolution. At runtime, "this monitor" isDisplay.main.systemWidth/Height.Screen.currentResolutionin fullscreen is the current swap size — a 1080p pick on a 1440p display rewrites "native" if you seed from it. Persist width/height yourself; first-run seed fromDisplay.main, thenScreen.SetResolution(w, h, mode). Edit those PlayerSettings fields via the API (PROJECTSETTINGS-EDIT-VIA-API, the editor tooling page). (Seen in a mobile strategy game: settings had display mode only, PlayerSettings was Windowed 1024×768 with native checked, and boot never calledSetResolution.)
Major editor-version upgrades
BATCHMODE-CANNOT-UPGRADE-A-BROKEN-PROJECT (HARD). Unity.exe -batchmode -quit will not finish
an in-place editor upgrade while any script fails to compile. It logs Scripts have compiler errors,
terminates with return code 1, and does not stamp ProjectVersion.txt — so the upgrade silently
did not happen. This is a chicken-and-egg: the upgrade is precisely what breaks vendor scripts.
Sequence that works:
- Open the project once in the GUI with the target editor. It enters Safe Mode, but it stamps the version and migrates packages.
- Fix compile errors from the shell.
- Drive every subsequent compile headless:
Unity.exe -projectPath <p> -batchmode -quit -nographics -logFile <log>.
A background shell wrapper's exit code is not Unity's. Read the log's last lines — Shut down.
after a clean run, versus Scripts have compiler errors ... return code 1.
UNITY65-ENTITYID-DEPRECATIONS. Unity 6.5 promotes a family of deprecations to CS0619
errors, and they land almost entirely in asset-store code rather than yours. The InstanceID
-> EntityId rename table, and the "prefer the overload that was never deprecated" fix, are
UNITY65-INSTANCEID-OBSOLETE-ERROR in the compile assemblies page; the two members of the
family that are NOT renames:
| Deprecated | Replacement |
|---|---|
TreeView / TreeViewState / TreeViewItem |
the <int> generics |
HierarchyProperty |
HierarchyIterator — a redesign, not a rename |
The TreeView trap: an asset that declares its own TreeViewItem<T> : TreeViewItem cannot take
the prescribed <int> fix — TreeViewItem<int> then binds to the vendor's generic and violates
its where T : ... constraint. Fixing it properly means renaming a vendor generic across every use.
For an editor-only tool the game never calls, park it instead of forking it: move the folder out
of Assets/ with its .meta (GUIDs survive, restore is a move back), and record why in a README
next to it. Patching vendor code is debt that the next asset-store update silently reverts.
- EDITORBUILDSETTINGS-MUST-BE-VERSIONED — an absent
ProjectSettings/EditorBuildSettings.assetships a BLANK player, and on iOS may not even emit an Xcode project; the Build Settings window shows zero scenes after the first open on that checkout. Version the asset, and have the build script refuse unless the exact scene order is present. (a card game, Mac checkout.) - ANDROID-MIN-API-FOLLOWS-EDITOR — never copy an Android API floor from a project on an older
editor. 6000.5 raised the minimum to 26;
AndroidApiLevel24isCS0618today and an error on the next release. (a card game, copied from a 6000.3 title's 25.)
PRODUCT-RENAME-IS-A-SWEEP (HARD)
productName and companyName are
keys, not labels. A rename, in global Player Settings or in any build profile override, lands in ONE
commit with all of:
- Vendor state keyed on the name. Carlos Wilkes'
CwShaderBundle(Space Graphics Toolkit and siblings) storesprojectHash = Application.productName.GetHashCode. On a mismatch itsAutoSwitch, queued withdelayCallafter every script reload and so run when the Editor next gets focus, rewrites every generated.shaderfrom the bundle and drops local shader patches. Measured: 28 bundles stale for seven weeks after a rename; one focus rewrote nine shaders and brought back 8 build errors. Set every bundle's hash, and add an EditMode test comparing each one to the current name. - Build profiles. A profile that overrides
productNamemust equal the global one; while it is active,Application.productNamein the Editor is the profile's value. - Paths and prefs move.
persistentDataPath(LocalLow/<company>/<product>: saves,Player.log, tool inboxes) and the PlayerPrefs registry key (HKCU\Software\<company>\<product>) follow the name. Every setting resets unless migrated; say so in the release notes. - Grep for the old name across docs, tools, recipes and agent memory.
RELEASE-NOTES-DESCRIBE-THE-FIX (HARD)
- Before distributing a build, write release/fix notes for its exact version and build number. Each material fix states the previous symptom and the new user-visible behavior; add a concrete What to Test action and expected result.
- Use specific outcomes, not standalone "bug fixes", "improvements", or internal filenames. Include only changes present in that build; exclude plans and unfinished work, and do not claim verification that was not performed.
- Keep the project changelog and destination notes (TestFlight, store, or release page) consistent with that shipped scope. Respect the destination's length and locale requirements without silently dropping the description.
- After publishing, read back the notes attached to the exact remote version/build and locale. Verify their content, not just the binary upload result; report metadata as pending if it cannot yet be confirmed. A successful upload does not prove testers received the fix description.
SERVER-BUILD-SHIPS-COMPILED-OUT-COMPONENTS
A dedicated server build logged about 80 The referenced script (Unknown) ... is missing lines, 6 different
serialization layout errors, 20+ Trying to access a shader but no shaders were included ... Dedicated Server
Optimizations warnings and an ArgumentNullException ... shader from FontAsset.CreateFontAssetInstance, although
Multiplayer Roles stripped the scene's UIDocuments. Role stripping removes only the engine component types listed per
role. Scene components from an assembly with defineConstraints: ["!UNITY_SERVER"] still ship as missing scripts, and
every client asset they reference (PanelSettings, fonts, VFX, item models) rides into the server build; a PanelSettings
that reaches the server then lays out text with a stripped shader.
- Strip them at build time: an Editor
IProcessSceneWithReportthat runs for the Server subtarget only and destroys every component whose assembly is compiled out underUNITY_SERVER, plus every component holding a managed reference to such a type. - Never catch the font/shader exception or add shaders back to the server; the UI asset should not be there.
- Gate: a fresh server log carries zero missing-script and serialization-layout lines. Prefabs still reached from shared server data log on; trace their owner next.
- Amended: the scene processor never reaches ASSETS. After it, one server still logged 46 missing-script and 14
layout lines per boot: ScriptableObject definitions the server loads (item, content references, building part, world
screen) held client view prefabs as direct fields, so every prefab and its presentation scripts shipped in and loaded.
Wrap each view-prefab field and its accessors in
#if UNITY_EDITOR || !UNITY_SERVER. The Editor keeps the data across build-profile switches, client players keep it, and the server player neither serializes nor loads the reference. Then compile the runtime assemblies with the server player's defines (UNITY_SERVER, no UNITY_EDITOR): every server-compiled reader fails to compile, which is the reader list. Gate out the client-only systems and components it names, whole files where they only run in client worlds. An offline server compile must also drop assemblies whose constraint is"!UNITY_SERVER || UNITY_EDITOR": true in the Editor, false in the player.
HEADLESS-NULL-DEVICE-LOGS-ARE-BENIGN
- A
UNITY_SERVERplayer (null graphics device) logs once per boot: Entities GraphicsNo SRP present ... disabled,Warning: No Graphics Device found. Deformation systems will not run.andNo texture data is available to upload.. Engine systems created in the server's local world self-disable. Record them as benign in the log inventory and count them out of error budgets; do not suppress them or chase them as defects.
ENTITIES-CONTENT-FOLLOWS-THE-ACTIVE-TARGET
BuildPipeline.BuildPlayer(BuildPlayerWithProfileOptions)with an Android profile, started while the Editor's active build target was still StandaloneWindows64, reported Succeeded, but its Entities runtime content (the entity scene files behind the subscenes) was built for the active target (build target 19, standalone). On the device the log said "file created for another build target ... File's Build target is: 19", the subscene never loaded, and the game's readiness gate timed out on boot. Measured: one APK broke this way; the next build, made after the switch, booted on the same phone.- Switch the active build target to the profile's platform first (
EditorUserBuildSettings.SwitchActiveBuildTarget, and wait for the reimport), or build a second time after the switch. "Succeeded" and a valid APK prove nothing about baked content. - Gate: on the first device boot, grep logcat for
another build target, then start a real match on the device. See ACTIVE-BUILD-PROFILE-REDEFINES-EDITOR-COMPILE in the compile assemblies page for the compile side of the same active-target coupling.
ONE-BUILD-MODE-PER-TEST-CYCLE
- The player build cache lives in
Library/, not in the output folder. Bee, ShaderCache, Burst and the generated Gradle project are all underLibrary/, so building into the same folder saves nothing. Keep one fixed dev folder anyway (e.g.Builds/<Platform>/Dev/) so testers always know where to look. Corrected: the file name carries the version,<Product>-<version>-dev.apk(ARTIFACT-NAME-CARRIES-THE-VERSION below). - Release builds only on the developer's explicit request (corrected; A batch close, a version bump or a finished lane is not that request.
- The real costs are the first-ever import for a platform and switching build modes. Measured on one Android project: the first Android import took 1:05:55. Alternating Development and Release builds recompiled shader variants and IL2CPP from scratch: a dev build after a release build compiled its 2304-variant batches anew, about 60 min. Pick one mode per test cycle and stay in it; see IL2CPP-MASTER-IS-SHIP-ONLY for the compiler configuration side.
BUILD-FOLDER-SHIPS-DEBUG-ARTEFACTS (HARD)
A client patch measured 22.8 MiB where the game change was 7.6 MiB: the rest was debug output.
- A Development build writes one PDB per assembly beside the player (measured 219 files, 92 MB, the Burst library's
PDB alone 77 MB). EVERY build, Release included, writes
<product>_BurstDebugInformation_DoNotShip(2.2 MB) beside the exe; IL2CPP adds*_BackUpThisFolder_ButDontShipItWithYourGame. Unity writes them into the output folder and nothing removes them, so zipping or uploading the folder ships them. - After
BuildPipeline.BuildPlayersucceeds, move them to a private symbol archive keyed by folder and version (crash symbolication reads them from there), then validate the folder against ONE rule list and leave the build unstamped on any finding. Run the same list from a menu/tool tab, a unit test and a CLI, and again at pack/upload time with the run-data rules (saves, logs, archives written by running the exe) that a rebuild of a used folder would trip. - Moving a Development build's PDBs away removes file:line from its log stack traces; archive them anyway if dev builds leave the PC, and read crashes against the archive.
- Gate: a validator exit code on the exact folder being packed, not a remembered pattern list per tool.
DEVELOPMENT-PLAYER-IS-NOT-SHAREABLE (HARD)
- A
BuildOptions.Developmentplayer writesboot.configwithplayer-connection-mode=Listenandplayer-connection-ip=<the build PC's LAN address>, shipsWinPixEventRuntime.dllon Windows, and compiles in everyDEVELOPMENT_BUILDblock (dev command inboxes, scripted input, debug probes). Measured: dev builds were zipped and sent to testers with all of it. - Builds that leave the build PC are Release. A validator flags
player-connection-*inboot.configand the PIX runtime when the build claims Release flavour.
EDITOR-TOOLING-PACKAGE-SHIPS-IN-EVERY-PLAYER
- An Editor tooling package whose runtime asmdef has no Editor-only platform lands in every player and server build.
Measured: an experimental compile-pipeline package shipped Roslyn (
Microsoft.CodeAnalysis*) plus its IL interpreter, 9.2 MB per build, with a dormantRuntimeInitializeOnLoadMethodbootstrap and, in Development players, a PlayerConnection receiver. Sample assemblies (*DocCodeExamples,*DemoScripts*) ride along the same way. - List every non-engine DLL in
<Player>_Data/Managedafter the first build. Remove tooling with an EditorIFilterBuildAssemblies(keep the Editor copy), then prove the player boots with noRuntimeInitializeOnLoador missing-type error in the player log before the filter enters shared builds; grep project code for the assembly first, since an empty grep does not cover the package's own bootstrap.
DEVICE-TEST-RESTORES-PLAYER-DATA
- Playing real matches on the developer's phone changes their saved progress. On a Development (debuggable) build, back up
adb shell run-as <pkg> cat shared_prefs/<pkg>.v2.playerprefs.xmland/sdcard/Android/data/<pkg>/files/*before the session, push them back afterwards (run-as <pkg> cp /data/local/tmp/<file> shared_prefs/<file>), andcmpto prove they are byte-identical. Git Bash rewrites/sdcardpaths unlessMSYS_NO_PATHCONV=1is set.run-asworks only on debuggable builds, so a release build needs the developer's consent or a separate test profile.
DEV-BUILD-OVERRIDES-IL2CPP-MASTER
- Amends IL2CPP-MASTER-IS-SHIP-ONLY above. A Development build does not change
il2cppCompilerConfiguration: with Master saved in PlayerSettings, every dev APK paid the whole-program link. The dev build script setsIl2CppCompilerConfiguration.Releasefor the build, restores the saved value infinally, and recordsconfigRestored=in its receipt. - Measured dev APK times on one project: 1:05:55 (first platform import), 1:00:41 (first build after a target
switch), 0:25:05 (warm
Library/Bee, a few changed files). The first Release-configuration build is cold, because the cache is keyed by configuration; time the second one before claiming a gain (see ONE-BUILD-MODE-PER-TEST-CYCLE).
EDITOR-CAPTURE-IS-THE-DESKTOP-TIER
Application.isMobilePlatformis false in the Editor even with the Android build target active. So any tier chosen from it (see MOBILE-TIER-FROM-HARDWARE above) resolves to Desktop in every Editor capture.- An Editor capture of a tiered effect or preset proves the Desktop tier only. Say so in the evidence class, and prove the mobile tier on a device, or through an explicit Editor preview override when the project has one.
- If mobile tiers need Editor review, add that override as one authored setting. Never add a second detection path.
ARTIFACT-NAME-CARRIES-THE-VERSION (HARD)
- Every build artifact's file name carries the product version:
<Product>-<version>-dev.apkfor a development build, and<Product>-<version>.apkfor a release build. The same applies to other platforms' artifacts. - The build script derives the version from the project's version source of truth, the same file that sets the player version. Never type it by hand or copy it from the previous build. A name that disagrees with the running game's version is a failed build.
- The fixed dev folder stays, so testers know where to look (see ONE-BUILD-MODE-PER-TEST-CYCLE). A rebuild at the same version overwrites its file, and a version bump writes a new one.
- Gate: the build receipt prints the output path, and its version segment equals the version file and the installed game's reported version.