Keep this file current. When a session changes something described here — a convention, a tool, a gotcha — update it in the same session, and add an entry for anything non-obvious learned the hard way. Prune entries that stop being true. Be concise: the rule and why, and history only when the history is the warning.
(If copying these tools into a project that already has a CLAUDE.md, merge these sections into it.)
Three independent UGUI tools, each a self-contained folder with its own README.md. Read it before changing a tool (for DOTweenAnimationPlayer, read the wiki). No tool references another.
| Folder | What it does | Namespace |
|---|---|---|
DOTweenAnimationPlayer/ |
Inspector-authored DOTween animations, played by name | rmf_claude.DOTweenUI |
FlipbookAnimation/ |
Sprite-frame flipbooks on UGUI Images |
rmf_claude.FlipbookAnimation |
SelectableEvents/ |
UnityEvents for Selectable pointer/selection callbacks | none — global |
SelectableEvents is the author's hand-written code: don't restructure, namespace or modernise it unasked.
Common to all: runtime types are public, editor drawers and editors internal. The .meta files carry the script GUIDs — a folder copied without them loses every component on existing prefabs. If Animation Sets "break" after copying the folder anywhere, compare script GUIDs first (the jam's copy once had three that had drifted from the library's; they match now).
https://github.com/rmfandyplayz/UnityUIAnimationTools.wiki.git, pushable with the author's git login. The README holds only an install note, the Code API signature block and a link. A public API change updates both the README's block and the wiki's Code-API.md; any user-visible behaviour change goes into the relevant wiki page.
Wiki conventions: plain language for non-programmers first; technical detail in a collapsed <details><summary><b>Under the hood</b></summary> (blank line after </summary>, or its markdown doesn't render); GitHub alerts (> [!TIP], > [!WARNING], > [!NOTE]); no # Title line; step types by Inspector name (Anchored Position); links as [text](Page-Name#heading-slug), each checked to resolve before pushing. _Sidebar.md has six sections (Installation, Code API, Basic usage, Advanced usage, Nitty-gritties, Warnings and Gotchas) under a "UI Animation Player" heading, leaving room for the other tools.
The other two are still loose folders. Projects install from the release branch — https://github.com/rmfandyplayz/UnityUIAnimationTools.git?path=/DOTweenAnimationPlayer#release — and update with Package Manager's Update button. The author chose a branch over tags: one-click updates, no need for old versions.
Publishing is the author's job: merging main into release through a PR on GitHub, per RELEASING.md. What it asks of a session:
- Bump
versioninpackage.jsonwith the first change to the package after each release. If it still equalsgit show origin/release:DOTweenAnimationPlayer/package.json, bump it in the same commit: patch for fixes, minor for additions, major for anything breaking saved data or the public API. The author doesn't bump it. - Never commit to or rewrite
release. It only receives merge commits frommain. A squash merge would leave the same changes under different commits and the next merge conflicting with itself. releaseis GitHub's default branch, butmainis where work happens. Branch frommain, and open every PR with--base main(gh pr createotherwise targetsrelease). The author chose this so the repo page shows released code. After a release,mainmay be fast-forwarded toorigin/release(merge --ff-only) to absorb the merge commit; that's the only wayreleaseflows back.- Commit every new file's and folder's
.meta. Unity ignores a meta-less asset in a read-only git package, so a missing one only shows up in projects that install it. - The install URL never changes, so a release needs no README or wiki edit.
The Update button (6000.3's GitUpdateAction) re-installs the package's git URL, ignoring the hash locked in packages-lock.json, so it fetches whatever #release points at now; the refresh button ignores git packages. Tested end to end on P2. Two traps: an Editor in the background downloads the new version but only switches when the project is reopened, and calling Client.Resolve() from eval hung the Editor on a modal for 7+ minutes until killed. Reopen the project instead.
What the package needs, and why:
- Two asmdefs:
rmf_claude.DOTweenUIat the root,rmf_claude.DOTweenUI.EditorinEditor/(Editor platform only). A package's scripts only compile inside an asmdef, where a folder namedEditoris no longer special, so the second asmdef is what keeps editor code out of builds. BothautoReferenced, soAssembly-CSharpsees the tool unchanged. - Both reference
DOTween.Modules, which exists only after DOTween Utility Panel → Create ASMDEF. DOTween's UI shortcuts (DOAnchorPos,DOFade,DOColor,DOSizeDelta, punch/shake anchor pos) are generated intoAssets/Pluginsand otherwise compile intoAssembly-CSharp-firstpass, which no asmdef can reference. Forgetting the click gives exactly 7 errors (CS1061 / CS1929) inUIAnimationStep.csthat don't name the cause, so the README does. A setup check inside the package can't help: a package that fails to compile loads no new code. For the same reason, never add an asmdef to a copy that isn't the package. Swapping the 7 shortcuts for DOTween core calls was declined:DOShakeAnchorPosusesSetSpecialStartupMode, a module-only helper DOTween could rename. package.jsonsays"unity": "2022.3"(SerializedProperty.boxedValueneeds 2022.1+).- Moving into the asmdef did not break UnityEvent listeners storing
m_TargetAssemblyTypeName: UIAnimationPlayer, Assembly-CSharp(all 32 in the jam resolve throughUnityEventBase.FindMethod).
FlipbookAnimation/ and SelectableEvents/ have no asmdef; the Editor/ folder name separates their editor code.
Requires DOTween Pro, then Tools → Demigiant → DOTween Utility Panel → Setup DOTween… and Create ASMDEF. Skipping either produces errors pointing at this code, not the cause.
Layout (the author's reorganisation; every file moved with its .meta): UIAnimationPlayer.cs and UIMaterialInstance.cs at the root, the two components you add. Data/: UIAnimation, UIAnimationStep, UIAnimationAsset. Helpers/: UIAnimationAudio, UIAnimationProperty, UIAnimationSteppedEase, UIAnimationLog. Attributes/: the PropertyAttributes. Editor/: UIAnimationTargets.cs at its root; Inspectors/ (player and asset editors, Shared Animations, preview, the pop-out window, recording), Drawers/, SceneView/ (gizmos, the drawing they share with the path editor, path editor, simulation), Menus/ (context menu, mirror, Save as Animation Set).
UIAnimationPlayer holds named animations, each a list of steps: player.Play("Show"). A step drives one property on one target — anchored position, scale, rotation, colour, alpha, size delta, offset min/max, material float/colour, SetActive, punch or shake (position, rotation, scale), a sound, or CustomProperty (any public float / int / Vector2 / Vector3 / Color / string member of any component, via DOTween.To). Steps run in sequence or joined to the previous one, with per-step delay, duration, ease and endpoint mode (Absolute / Baseline / Current). It is deliberately generic — do not add game-specific logic.
Every addition is back-compatible. It is either optional and invisible when unused, or additive: a new enum number, new fields other step types ignore, new bools whose false (what old data reads) is the old behaviour. An existing player must behave exactly as before; prove it with the play-path fingerprint (see Verifying). Optional today: a shared UIAnimationAsset in the Shared slot, a step's TargetPath, Play(string, Action<UIAnimationEndReason>), per-animation Snapping, custom movement paths, SetFrom / SetTo / ClearOverrides, and Shared Animations with each player's own On Complete (a hidden Id on UIAnimation, SharedOnComplete on the player, [NonSerialized] PlayerOnComplete on the clone). Additive: CustomProperty (enum 20), Interrupt Others' STOP / COMPLETE and REPORT / SILENT (CompleteInterrupted / CompleteSilently; false/false is the old path). Editor-only additions (gizmos, the type check, the pop-out window and recording) can't change playback.
Two deliberate behaviour changes, on request: Stop(name, complete: true) and StopAll(true) finish through Finish, so they now run the SetActive steps they used to skip; and a punch or shake that Kill stops part way is put back where it started.
- Reversing is an authoring operation, not a playback mode.
Mirror Animation/Duplicate as Mirroredrewrite the data once. There is no runtimePlayReverseand there should not be — having both was tried and one cut. - Mirroring never touches easing (preset or custom curve): the house style decelerates into place in both directions. Ease inversion was built, then removed on request.
PlayAnimation/StopAnimationaren't redundant. UnityEvent's dropdown only lists void methods with at most one argument, which excludesPlay(returns aSequence) andStop(optional parameter).WithPrevioussteps are groups, not rows. Anything reordering steps must keep each group contiguous, or it silently re-parents which steps are joined.AnchoredPosition,SizeDelta,OffsetMinandOffsetMaxall write one rect, so two on one target overwrite each other. A new rect-driving step type must be added toCaptureBaseline, orTo: Baselinecollapses the target to zero.- A step's
Delayis applied only in the insert positionBuildSequencecomputes.BuildTweenmust neverSetDelay: DOTween adds it on top, doubling every delay. This regressed once. - Stepped playback ("Play At Custom FPS") quantises the time handed to the ease, not the clock, on one grid keyed off the absolute sequence position (per-tween grids read as jitter). Punch and shake are excluded.
UIAnimationAudio.Sharedis the one global. For a mixer, callUIAnimationAudio.SetSharedonce.- Snapping resolves as
step.Snapping || (animation.Snapping && !animation.SnapPerStep)(UIAnimation.SnapsEveryStep, passed toBuildTweenassnapAll). The animation-wide box adds to a step's own, which is what leaves old data unchanged with no migration; the drawer shows a ticked step box even with Snap Per Step off. Don't "simplify" it into Snap Per Step choosing between the two. ShakeRotation/ShakeScaletake a per-axis strength (ToVector);ShakeAnchoredPositiontakes one number (ToFloat). Z-only rotation must be expressible (X/Y tips a flat element over in 3D), and the older field can't move.CompleteInterrupteddefaults to true only for a never-filled animation (Loops == 0 && Nameempty, inFillUnsetDefaults), not through its initializer: Unity gives a field missing from old data its initializer, and false must mean "authored before this existed".+on an empty list zero-fills every field, bools included, so that branch also setsInterruptOthersandApplyFromValuesImmediatelyto true.+on a non-empty list copies the last element.
- A player deep-clones every animation it takes from a shared
UIAnimationAsset, for correctness. Runtime state (RuntimeSequence,RuntimeCallback, each step's resolved targets) lives onUIAnimation/UIAnimationStep, so two players reading one asset would drive each other's objects.CloneForRuntimeround-trips throughJsonUtilityand re-pointsOnCompleteat the originalUnityEvent. So: editing an asset at runtime doesn't reach initialised players, and assigningSharedafterAwakedoes nothing. TargetPathreaches upward with..(transform.Findaccepts it; segments resolve one at a time, so../Selfis a sibling named Self). The common layout — the player on anAnimationschild driving the button above it — depends on it. Don't restrictResolveHostto descendants.- A
TargetPaththat matches nothing resolves to null, not the owner — a step that does nothing is easier to diagnose than one scaling the wrong object.Resolvewarns once and setstargetPathMissedsoHasTargetdoesn't warn again everyPlay. - A filled target slot skips the Target Path entirely — it isn't even looked up. The drawer greys the path to show it.
- The end-callback fire-once flag is a local captured by each play's closure, never a field. An
onEndthat replays the same animation would otherwise let the old play's teardown fire the new play's callback. LikewiseKillonly clearsRuntimeSequence/RuntimeEndCallbackif they stillReferenceEqualsthe ones it started with. - The player has no
Update. A kill it didn't issue is caught by the sequence'sOnKill, withPendingEndReason(armed asStopped, overwritten byKill) carrying the reason. Every end path has two routes to the callback; the fire-once flag drops the second. A pollingUpdatewas once added on an edit-mode measurement — see the DOTween table. - Destroying an enabled player reports
Disabled, notDestroyed:OnDisableruns first and Unity gives no way to know a destroy is coming. No heuristics. UIAnimationEndReason.NotFoundis a reason, not a silent no-callback — a caller is never left waiting on a mistyped name.- From / To set from code live in
[NonSerialized]slots beside the authored value, one per value kind (EndpointOverride), never written over it. A local animation's runtime step is the serialized one, so overwriting would show in the Inspector and leaveClearOverridesnothing to restore. Every playback read goes throughFromVectorValue/ToFloatValue/ …. The value keeps the step's mode.SetFromrefuses a step whose From is off. Steps are addressed by index (the README warns reordering changes them). The edit-mode preview re-clones shared animations, dropping overrides; nothing sets one in edit mode. - Every "end it at its end state" path goes through
Finish, never DOTween'sKill(complete: true), which skips a Sequence's internal callbacks (measured: aSetActivestep still ahead never runs).FinishcallsComplete(true)and setsfinishingso sounds stay quiet. SILENT nulls the sequence'sOnCompletefirst, soOnKillreports the caller's reason. The player's ownKillonly stops where it stands. FinishRunningsnapshots who is running before finishing any, then sweeps with a plain stop. REPORT'sOn Completecan chain into another animation on the same player; finishing whatever is live during the loop would make chains depend on list order.- Replaying an animation that's already running always stops it where it stands, whatever its own STOP / COMPLETE says (those are about the others). Completing first would snap a step with no From to its end, so the replay would show nothing. Questioned once, when a counter test read its half-counted number as the total; the fix was the caller keeping the real total and
SetTo-ing it (tooltip and README say so). - A punch or shake that
Killstops part way is put back where it started (UIAnimationStep.SettleImpulse). DOTween's punch and shake are relative to their start, so one stopped mid-wobble stayed displaced and the next wobbled around that (spam left a scale at 2.19x). The start is read in the nested tween'sOnStart, which runs before any write and once per lifetime.Killwrites it back only whenElapsedPercentage(false)is strictly between 0 and 1, last step first so the earliest start wins. Target and type are pinned at build time;BuildTweenforgets the tracked tween first, since DOTween reuses dead tween objects.Finishneeds nothing. - Punch and shake capture a baseline anyway, under the property they drive, so the preview can restore one stopped mid-oscillation.
RestoreBaselineswitches onbaselineType, the Type it was captured under, not the current one — changing Type mid-preview otherwise wrote a scale into a position.baselineMember/snapshotMemberdo the same for Custom Property.- Every warning reads "who: CONSEQUENCE IN CAPITALS. details" through
UIAnimationLog.Warn, which ispublicbecause the editor assembly can't see the runtime'sinternals. Steps say "Animation 'X' step n". The flipbook formats its own inline. A step's resolve-time warnings appear atAwake.
- Built on DOTween's generic
PathPlugin—DOTween.To(PathPlugin.Get(), getter, setter, new Path(...), duration)— so one path can driveanchoredPosition,sizeDeltaorlocalScale. Measured traps (1.3.030): it throws a NullReferenceException at startup withoutSetTarget(transform); it ignoresFrom(), so the getter returns the authored From; it prepends the start as a point unless the first waypoint equals it.Rotationis excluded (quaternion). An empty point list falls back to the ordinary tween. - Waypoints follow
To's mode (SetRelativeoffsets every point). When a mirror changes To's mode, the points can't be converted without the runtime baseline, soUIAnimationMirror.PathSpaceChangesreports them instead. UIAnimationPathEditor.Evaluatereimplements DOTween's Catmull-Rom (Path.GetPointisinternal) with its non-textbook ends: the control point before the first point is the second point, and after the last it's the last mirrored through the one before. It matches a real tween within 0.001. "Fixing" the ends draws a curve the object doesn't follow. It's also why a mirrored Curved path retraces only approximately; Linear is exact.- The path is created with a transparent gizmo colour, since DOTween would draw raw
anchoredPositionvalues as world positions. - The Scene-view path editor follows Unity's Edit Collider pattern: hides
Tools(restoring it, also frombeforeAssemblyReload, sinceTools.hiddensurvives a reload), registers a passive default control so empty clicks don't deselect, writes only through aSerializedObject. It edits onlyAnchoredPosition/LocalPositionoutside play mode, draws from rest plus the step's simulated start, and keeps drawing through a preview. The gizmos skip the step it is editing. Its caps, lit ring, labels and line styles are the gizmos' own (UIAnimationHandles), asked for so the two look like one tool.
- The member is stored as three plain values — the component's
Type.FullName, the member name, aUIAnimationPropertyKind— and resolved once inResolve. The component is found by matchingGetType().FullNameon the object, notType.GetType, so a script changing assembly doesn't break it. The kind is stored because a shared asset with no Preview On can't derive it. A type mismatch is a warning and a skipped step, never a guess. All lookup rules live inUIAnimationProperty.cs(TryLocate/FindMember/CollectMembers), shared by playback, the dropdown and the type check. FindMemberwalks the type hierarchy oneDeclaredOnlylevel at a time. A flatGetPropertycalls anew-redeclared name ambiguous, and a getter-only override has no setter; the walk binds the base declaration, whose delegate still dispatches to the override. It stops atUnityEngine.Object/Component/Behaviour/MonoBehaviour, which keepsname,tagandenabledout.- Accessors are bound with
Delegate.CreateDelegate, as UnityEvent does, so there's no per-frame reflection; fields go throughFieldInfo. IL2CPP stripping above Minimal can remove a setter only this tool calls — the README documentslink.xml. - Nothing in the Inspector calls a getter except a recording's Finish (and the preview's own captures, on a Play or record click or a save mid-preview). A getter is arbitrary code, and everything else runs every repaint, so a recording Custom Property row shows "read on Finish".
CapturePropertyBaseline/RestorePropertyBaselinecatch exceptions; no other step type does. One throwing member mustn't leave the player half-initialised or the scene half-restored. It's warned once and the step skipped.- A counted-number text (
NumberText) keeps two baselines: the parsed number (baselineFloat, whatBaselinemeasures from) and the original string (baselineText, what's restored — elseScore: 120becomes0).ParseNumberreads only a plain number (current culture, then invariant), anything else as 0.FormatNumberfalls back to the plain number on aFormatException. - Typewriter text offers
AbsoluteandBaseline, neverCurrent: a mirror can't take appended characters back off. It usesSetOptions(richText: true), so tags never show half-written. - An
intmember's values live in the float fields, drawn with anIntField.FromText/ToTextare the only value fields the type added. - The edit-mode preview runs the member's real setter, side effects and all; the README says so.
- The lifecycle and button rows (
DrawRow/DrawFooter) live there, not in an inspector: two inspectors drive it, and a second copy would drift. - Within one player it chains rather than restoring between Plays —
CloseCreditsafterOpenCreditsmust start from open. A different animation pressed mid-play first finishes the running one (Complete(true), firing SetActive callbacks; an endless loop is left where it stands). The preview never reaches the interrupt code (it callsStopAll()beforePlay); play mode only matches it under COMPLETE, and the README says so. - Every preview
Playis Reset-then-Play:ApplyFromState, thenEditorMarkPreviewStartre-captures the start snapshot. Deliberately unlike play mode whenApplyFromValuesImmediatelyis off:CloseCreditsplayed from rest looked broken, reported as a bug. - Chaining stays safe two ways. (1) Baselines are still captured at rest before every Play:
EditorContinuePreviewsnapshots the current state (throughCaptureBaseline/RestoreBaselinethemselves, swapped into the baseline fields, so no step type can be covered by one and missed by the other), restores rest, re-initialises there (picking up Inspector edits) and puts the snapshot back. (2) Playing the same animation again restores its start snapshot first. A snapshot is only written back if the step still resolves to the same object. EditorSuppressSoundis on for the whole preview, since sounds fire from callbacks during playback. Out of play modeDontDestroyOnLoadthrows after the GameObject exists, leaking aUI Animation Audiointo the scene per call;Sharedalso returns null out of play mode.- It unwinds from
[InitializeOnLoad]hooks:playModeStateChangedatExitingEditMode(before Unity backs up the scene),beforeAssemblyReloadandEditorSceneManager.sceneClosing(the player's scene), plusOnDisableandSelection.selectionChanged.EndraisesEndedlast, whoever called it. - A scene saved mid-preview is written at rest, and the preview carries on (
OnSceneSaving/OnSceneSaved, andPrefabStage.prefabSaving/prefabSaved, for the previewed player's scene).EditorParknotes every value in a third per-step capture (parked*, apart from the snapshot Play-again needs), restores baselines and puts each Graphic's own material back without letting go of the instance (UIMaterialInstance.EditorShowSource);EditorUnparkreverses it, with adelayCallfallback in casesceneSavednever comes. Plain setters only, so no Undo entry and the scene isn't dirty again after the save (measured). TMP keeps its instance in a second serialized field,m_fontMaterial, which the save andEditorReleaseboth put back as it was; without that the instance is embedded through it. - It survives selecting the objects it animates, because posing means selecting them.
EndIfOwnedByleaves it running, owned by nobody, while the selection is on the player, its asset or anything it animates;Selection.selectionChangedends it once the selection leaves. An owner that still exists (a locked inspector, a pop-out window, a harness'sobject) ends its own, so a window's preview survives any selection - and closing the window ends it whatever is selected (EndIfOwnedBy(this, true)): a close is deliberate, a selection change isn't. A scripted test missed this because it never changed the selection. Holdkeeps it open for a recording: no other owner, selection change or Stop and Restore button ends it (that button reads "Recording - Finish or Cancel in its window"), andPlayfrom an inspector doesn't take ownership while it's held. OnlyEndgets past it. Holding a preview already runningRefreshes it (what a second Play does before playing: back to rest, re-initialise, current state back), so steps added since are captured too.- Nothing in the Inspector may read
UIMaterialInstance.Material: the first read instances a material onto the Graphic. ReadUIAnimationTargets.SourceMaterialOf. The preview's material steps do use.Material, soEndgives every Graphic its own material back (EditorReleaseMaterials→UIMaterialInstance.EditorRelease, which writes back the serializedm_Material/ TMP'sm_sharedMaterialas it was, not the getter's default-material fallback). Before that, a scene saved after any material preview kept a copy of the instance embedded in the scene file, cut off from the material asset (measured withRecordTestFill.mat). The Inspector's material editor at the bottom of a Graphic is built for whatever material it had at the time, soBeginandEndrebuild the tracker (delayCall→ForceRebuild) when a selected object's material was swapped; otherwise it kept editing the asset while the instance showed, which looked like recording a material did nothing. Only when a selected object is affected, so the player's own editor isn't rebuilt (and itsOnDisablerun) under a click. - In play mode the rows call the player's API, and the footer adds Stop and Restore (
StopAll+EditorRestoreBaselines, back to the values captured atInitialize). Reported as "the buttons do nothing": a one-way animation (no From) sits at its end after one Play, so Play, Reset and Stop had nothing left to show. A note under the buttons says so.
- Editor-side target resolution is
Editor/UIAnimationTargets.cs, mirroringResolvewithout side effects. A shared asset's steps resolve against the asset inspector's Preview On player, kept per asset in a static map (PreviewPlayerFor) until the next domain reload — not a serialized field, since an asset can't hold a scene reference. - The player inspector draws its own fields (
DrawFields), iterating likeDrawDefaultInspector, to labelShared"Shared Anim. Asset" (the serialized name stays; renaming would empty saved slots) and put Shared Animations after Animations.UIAnimationDrawerdraws every animation: Unity's layout plus a grey header note ("overrides shared", "overridden locally", …) or the Override button, drawn before the foldout so the foldout doesn't take the click. - Type and Ease dropdowns are
GenericMenus, not enum popups. Type is grouped (IsImpulse/TargetKindOf) and sorted in the drawer; Ease is grouped by direction (Linear and Flash, In, Out, In Out) with the enum number as tie-break, sinceList.Sortisn't stable. The stored numbers never move for menu order.Custom Curveis an Ease entry setting the sameUseCustomCurvebool. Menu callbacks write through.Copy()'d properties andApplyModifiedProperties, since they run after the repaint. - Several rows are one control over several fields, each binding
BeginPropertyto whichever field decides what's shown (so override bold and Revert follow what's visible), and nothing is rewritten until clicked: the Ease row overEase/UseCustomCurve; the Snapping button overSnapping+ hiddenSnapPerStep(SnapPerStepwins, as at runtime); the Custom Path dropdown overUseCustomPath+PathShape(Disabledkeeps shape and points); Start as AFTER / WITH PREVIOUS over the enum (anything butAfterPreviousreads WITH); Play At Custom FPS withFPSinline (UIAnimationInlineValueAttribute; theBeginPropertyrect stops where the field starts).UIAnimationShowIfAttributeis unused but kept as a public type. - Start and Interrupt Others' buttons are sized from their words (
CalcSizeof both labels + 8); a fixed 72 clippedCOMPLETEat 125% scaling. Snapping's fixed 84 fits. - The drawer reads enums with
intValue, notenumValueIndex— only the number is the contract. - The mode dropdown's tooltip is an empty
GUI.Labelover the popup (a popup doesn't show its label's tooltip), built per dropdown byModeTooltipfrom the options that dropdown offers. - The From dropdown doesn't offer
Currentbut still shows it when stored.HasAuthoredStartexcludes it, so it behaves like TO. It's never rewritten: changing what stored data means is the back-compat line. By contrast the To row with From on rewrites a storedCurrenttoBaselineon draw — left alone, flagged here. - The step list's row shading comes from ReorderableList's source:
BandLeft = 28(drag handle 20 + foldout margin 8),BandRight = 6, and the drawer's rect is the whole row, so the band is aty - 1,position.heighttall.ReorderableList.Defaultsgives the constants by reflection. - A step the type check finds dead is greyed out below its target (
DisabledScope, header faded byDimFoldout). The fields that fix it — Start, Type, slot, Target Path,Clip,ShaderProperty— are drawn outside the scope, since nestedDisabledScopes AND together. A sound whose path misses is warned about, not greyed: it still plays. The warning row wraps (seeGetPropertyHeightunder shared conventions). - IMGUI drops images from
EditorGUI.Foldoutwith the foldout style, and from a label whose text overflows, so the warning icons are separateGUI.Labels. - Save as Animation Set (
Editor/Menus/UIAnimationSetExport.cs) turns each filled target slot into a Target Path, since an empty slot means the player's own object. The path goes up with..to the nearest shared ancestor and down by name, and is kept only ifUIAnimationTargets.Resolvewithout the slot returns the same object. Otherwise (another root, a same-named sibling, a second component of its type) it's written from the scene root, misses and warns at play, and is listed in the Console. On Complete listeners targeting a scene object are removed (an asset can't reference one). The player is untouched. Saving over an existing set writes into it;CreateAssetwould change its GUID and empty every Shared slot on it. The session folder isSessionState, and a second menu entry saves there without a dialog. To check it: save every player inUITester2toAssets/_Check, compare each step's resolved target against the saved one's with no slot, and expect everything else's JSON to match. - Shared Animations (
Editor/Inspectors/UIAnimationSharedSection.cs) draws a private copy of the set:Object.InstantiatewithDontSave— notHideAndDontSave, whoseNotEditablegreys out the whole section, Override and On Complete included. Fields that mustn't change are greyed byUIAnimationDrawer's ownDisabledScope. The copy is refreshed byEditorUtility.CopySerializedwhenEditorUtility.GetDirtyCount(asset)changes or agenerationcounter moves. Saving resets the dirty count to zero, so Add On Complete (which gives the Id and saves in one go) looked dead while hidden duplicates piled up.generationis bumped byAddOnCompleteand by any import (a nestedAssetPostprocessor), andAddOnCompleterefuses a second entry per Id. Unity's right-click Duplicate / Delete Array Element apply themselves despite greying, soKeepOnlyCopyingremoves them (leaving Copy and Override). A static map from the copy'sSerializedObjectto its player (PlayerOf) tells everything else it's drawing the section:UIAnimationTargets.OwnerOfasks it first (so the type check runs against the viewing player), the drawer's margin cache is keyed by object, viewer and path (MarginKey; one path is on screen twice at different widths), and the gizmos read expansion from it (ViewOf). Foldouts still open inside aDisabledScope. - A player's own On Complete finds a set's animation by a hidden
Id, so renaming it in the set keeps it. The Id is given at the first Add On Complete On This Player, and the set is saved there and then (SaveAssetIfDirty) so a scene never points at an unsaved Id. A copy must not keep an Id:UIAnimationAsset.OnValidateclears one matching an animation above it; the clipboard'sRebuild, Duplicate as Mirrored and Save as Animation Set clear it (Paste over an animation, and overwriting a set, carry the old one back by name).boxedValuerefuses aPersistentCall, so Override copies listeners value by value (CopyValues), bounded by property path sinceGetEndPropertyon a list's last element runs past it. Entries matching nothing are listed with a Remove button. An overridden shared animation offers no Add button; an entry already on one is shown greyed as unused. - Gizmos draw from
UIAnimationPlayerEditor.OnSceneGUI, which only runs for editors the Inspector built; "Expanded" isisExpandedon that editor'sserializedObject.OnSceneGUImust not touchserializedObjectortargets— Unity logs an error every Scene view repaint. It usessceneSerialized, taken inOnEnable, refreshed inOnInspectorGUI, null when multi-editing. Rainbow colours apply only inExpanded Steps(EffectiveColors). Harness traps: after changing selection in the background callActiveEditorTracker.sharedTracker.ForceRebuild(), andisExpandedset through a differentSerializedObjectdoesn't reach an existing editor. - The pop-out window (
Editor/Inspectors/UIAnimationWindow.cs) shows one animation of a player or a set, opened by the Pop Out buttonUIAnimationDrawerputs on every header (not a player's Shared Animations, not multi-edit). It remembers the animation by index and name: found under its name if moved, taken as renamed if not found and the list kept its length (listLength), "gone" otherwise. The target is kept by reference andGlobalObjectId, so reopening its scene finds it again. While it's open, the Inspector draws that animation as one line with Focus (UIAnimationWindow.Find), so its fields are only edited in one place, and the Inspector's gizmos skip it: the window draws it fromSceneView.duringSceneGuiwhatever is selected (UIAnimationGizmos.Draw(..., only); the hover bookkeeping is per caller, since two callers draw in one pass). ItsSerializedObjectis made lazily (Serialized), and a static map (Of) tells the step drawer it's drawing in a window, which is the only place record buttons appear. The path editor's selection-change stop is skipped for a step whose animation is in a window. Closing the window finishes a recording; a recompile has already cancelled it by then. - Recording (
Editor/Inspectors/UIAnimationRecording.cs): the first record clickHolds a preview (a new one starts where things stand, so nothing moves), which captures every resting value before anything is posed; Finish reads every row, writes them through the window'sSerializedObject, thenEnds; Cancel only ends. A row is kept by step index, From/To and a fingerprint of the step's type and target, and dropped if the fingerprint changes. Conversion keeps the mode: Absolute as read, Baseline minus the rest (UIAnimationSimulation.TryRestOf), Current minus the step's simulated start (TryStartOf); a To with From on and a stored Current is treated as Baseline, which is what the drawer shows and rewrites it to. Rotation wraps withMathf.DeltaAngle(Rotation isFastBeyond360, so350would spin the long way); a snapping position or size rounds halves away from zero, as dragging does; typewriter Baseline is the suffix after the resting text, refused if the text no longer starts with it. Finish and Cancel collapse Undo fromUIAnimationPreview.UndoGroup:Beginrecords rest in a fresh group, andHoldon a running preview records rest again in a new group from insideEditorContinuePreview'satRestcallback, the one moment the targets are at rest - so one Ctrl+Z undoes the recording and leaves the objects at rest, without taking edits from before it. Without the collapse, undoing a single pose after the restore moves that object back to its posed position, outside any preview. A preview ended from outside (Ended) cancels the recording with a Console warning, and so does dropping the last row (× or its record button). Rows are read in order byReadAll, once per OnGUI in the panel (shown, which the step drawer's armed fields reuse) and once in Finish: a Current To is measured on aJsonUtilitycopy of the animation with the earlier rows already written in, so two rows recording the same object both land on the pose (the later one stores 0) and the live value shows what Finish writes. Rows reading the same value (TryValueOf: object plus position/size/scale/rotation/offset, CanvasGroup alpha, Graphic colour incl. alpha, material property, member) get a warning, not a refusal - a step starting where another ends wants exactly that. Each row's crosshair (d_Animation.FilterBySelection) selectsEditableOf- the component, or for a material the instance on screen - viaUIAnimationReveal, which unfolds the component and scrolls every Inspector to it throughPropertyEditor.m_ScrollView/m_EditorsElementby reflection (nothing public does it), giving up silently after a second. A material row remembers its asset's value (UIMaterialInstance.EditorSource) and the panel warns if the asset changes, the R4 trap. Finish closes in afinally, so a throwing getter can't leave the objects posed. - Line style is an endpoint's mode: solid Absolute, dashed Baseline, dotted Current (
UIAnimationHandles.PolyLine, dashes measured on screen). A position route and the path editor's line follow To's effective mode (relative = dotted; a Current To with a From is read as typed, so solid); a box step's first and last outlines follow From's and To's; the trail between stays solid, and skips any moment within 4 points of an end (TrailGap), since an Out ease stacks its last moments on the To outline and fills its gaps. A Baseline end draws a faint dashed ghost of rest (the outline with the property atTrack.Rest, or a cross plus a dashed line for position, skipped where it would lie on the route). - The selected step is the last one clicked, tracked by the tool (
UIAnimationGizmos.Select, from the step drawer'sNoteSelectionon anyMouseDownin the row, drag-handle margin included, before any field takes it): Unity keeps a highlighted row per step list, readable only throughPropertyHandler.s_reorderableLists, so there's no single selection to read. Kept as instance ID + property path inSessionState. It is drawn whatever Show says (except Disabled), last, with a dark halo (UIAnimationHandles.Highlighted), and even when it goes nowhere; a blue bar marks it in the Inspector. Show'sSelected Step(enum 4) draws it alone. The halo replaced the requested colour inversion, which vanishes on mid-grey; auto-switching Show on adding a scale step was dropped for always drawing the selected step. - The gizmos' endpoint handles (a position step's To dot, and its From ring when
HasAuthoredStart) convert a drop back through the simulation's ownTrack.FromOrigin/ToOriginandUIAnimationRectState.WorldToPosition, keeping Z, so the object lands where it's let go (checked: 0 error over every position step inUITesterandUITester2).Drawtherefore runs on every event, not just Repaint, and takes both control IDs per position step before the "goes nowhere" filter, which also keeps a held step: a filter that changed mid-drag would shift the IDs and hand the drag to another handle. A snapping step's dragged value is rounded on release (RoundOff, halves away from zero, notMathf.Round's to-even, as asked), not during the drag, and it's the typed number that's rounded. Look-only for a player's Shared Animations, while the path editor is active (UIAnimationPathEditor.IsActive), and under a parent scaled to zero on X or Y (HasPlane), where a drop maps back to garbage. - Box steps (Size Delta, Offset Min/Max, Scale) have corner and edge handles on the To outline (dots) and, with
HasFrom, the From outline (rings): 16 IDs per box, taken like the position ones. A drop is solved by finite differences:OutlineAtthe value, the value + 1 X and + 1 Y gives each handle's motion per unit, and every rect view and scale is affine in its value, so that's exact for any pivot, anchor, rotation or parent scale (checked: 0.00 pt miss on corners, throughSceneView.SendEvent). An edge takes only the mouse movement along its own motion. A handle that doesn't move on screen per unit isn't drawn: edges on the pivot, the far side of an offset. Edges are picked along their length with a 0.5 bias so a corner on top wins. Shift keeps proportions of the real rect size (Scale: the scale), captured at press; a box pressed with no size on a side grows positive along it whatever way it's pulled (Unfold), since its stacked corners are picked at random and the wrong one gave a negative scale. Boxes resize around the pivot because the step can't move the element. Scale Z is kept. The outlines come fromTrack.StartFrame/EndFrame(the rect once every step acting at that moment has acted), captured even when no Moments are. UIAnimationSimulationplays position, size, scale and rotation steps forward on paper (Run), for the gizmos, the path editor's start point and recording'sCurrent, and alpha, colour, material and Custom Property steps (RunValues, keyed byUIAnimationChannel: object plus shader property or member; a Graphic's alpha steps write part of its colour channel), for recording'sCurrenton those.RunValuesfollows the same rules and matches the real preview exactly (29 value steps acrossUITesterandUITester2); it skips typewriter text. A material's rest comes from the preview or the source material; a Custom Property's only from the preview, which reads it from the runtime steps' baselines (UIAnimationStep.EditorTryGetBaseline,NoteRest(player)) rather than calling a getter. It shares code with playback wherever it can (UIAnimationPlayer.LayOutSteps, a realUIAnimationSteppedEase,UIAnimationPathEditor.Evaluate) and copies the rest: From state applied first in step order; steps act in(start time, index)order; a step's start is read at its start time; paths at constant speed (ConvertToConstantPathPerc); snapping rounds. It matches the real preview within 0.07 units. It works from rest, which during a preview is whatUIAnimationPreviewcaptured (NoteRest/ForgetRest).
SetLinkis a no-op on a tween inside a Sequence; apply it to the outerSequence.- Configure a tween fully before
Append/Join:DoInsertsetscreationLocked, after whichFrom/SetEase/SetRelativedo nothing. .From(value)defaults tosetImmediately: trueand writes during construction, not at t=0.- A stencil
Maskdefeats material instancing (StencilMaterial.Addcaches a copy that never re-syncs).RectMask2Dis fine;MaterialPropertyBlockdoesn't work with UGUI at all. TextMeshProUGUIignoresGraphic.material: useTMP_Text.fontMaterial, and never destroy it.SetUpdate(true)is required for UI animating whileTime.timeScale == 0.DOTween.Kill(x)matches a tween's target or id, soDOTween.Kill(sequence)is a no-op — not the same assequence.Kill().
Kill semantics differ between play mode and edit mode (DOTween 1.3.030, Unity 6000.3):
tween.Kill() |
DOTween.Kill(id) / KillAll() / Clear() |
Pause() |
Complete() |
|
|---|---|---|---|---|
| Play mode | immediate, OnKill fires |
immediate, OnKill fires |
works | OnComplete then OnKill, immediately |
| Edit mode | nothing — stays active, no OnKill |
work | works | works, both fire |
DOTween never initialises out of play mode, and Tween.Kill() returns early when uninitialised; the filtered kills don't check. A test that kills a tween in edit mode is measuring the Editor, not the game — check tween lifetimes in play mode. Also, DOTweenEditorPreview.Stop() doesn't kill (it only stops calling ManualUpdate), so an old preview resumed on the next Play. That's why UIAnimationPreview tags each sequence with an id, pauses it and kills it with DOTween.Kill(id).
No dependencies, not even DOTween. It only writes Image.sprite, so DOTween can move, scale and fade the same object; the split is intentional.
UISpriteFlipbook cycles an Image through sprites. UIFlipbookSelectable sits beside a Selectable and picks the clip for its state (Disabled > Pressed > Selected > Highlighted > Normal, Unity's priority), leaving onClick, navigation and transitions alone. UIFlipbookClipAsset optionally shares a clip. Per clip: frames, FPS, loop mode (Once / Loop / PingPong), timing mode (Constant FPS / Random Offset / Per Frame Durations).
Intentional:
stoppedExplicitlytells aStop()ped clip from one that ran out, soOnEnablerestarts a finishedOnceclip rather than a re-shown panel sitting frozen.Stop()sets it,Play()clears it.- Randomised timing and
RandomStartFramelive on the player, never the clip — one clip asset can drive hundreds of objects. - Ping-pong turns at
count - 2, so end frames aren't held twice:ABCDCBABCDCB. Selecteddoesn't fall back toHighlighted: a clicked button keeps selection, and borrowing the hover look reads as stuck. EmptySelectedmeans no focus look.- Don't set the Selectable's Transition to
Sprite Swap— it fights the flipbook.OnValidatewarns. LoopModereplaced a serializedbool Loop;MigrateLoop()reads it once, usingdefaultsFilledto tell authored data from a new element. Keep both hidden fields.- Thumbnails use
GUI.DrawTextureWithTexCoordsoversprite.textureRect;AssetPreviewloads async and returns null at first. - The frame sort is numeric-aware:
frame_2beforeframe_10,frame_007besideframe_7.
The author's own code: UnityEvents for hover, unhover, press, release, click, select, deselect and submit on anything deriving from Selectable. It implements the same handler interfaces as UIFlipbookSelectable, which is fine — the EventSystem delivers to every handler. OnHover / OnUnhover fire regardless of IsInteractable(); press, click and submit don't.
- Inspector data classes need a
FillUnsetDefaults()called from the owner'sOnValidate. Unity zero-fills elements added with+on aList<T>, so initialisers don't apply. Only write fields still at zero; where false is ambiguous, a hiddendefaultsFilledbool tells "never set" from "unticked". - Serialized enums are stored as integers, and the integers are the contract. Assign every value explicitly. Changing or reusing a number silently repoints every asset: alphabetising
UIAnimationStepTypeonce turned 16Scalesteps intoGraphicAlphaand faded a menu away. If animations "disappear", check the enum's history first. Fix by remapping stored integers, never by reordering back. - Edit-mode preview animates real scene objects, and three things are load-bearing: capture and restore per property (never a blanket
EditorJsonUtilityround-trip, which blanks references like anImage's sprite); re-capture baselines before every preview; clear callbacks, since On Complete runs game code. Plus: one preview at a time, oneUndoentry,SetDirtyon each touched object, an unwind on selection change, play-mode change and assembly reload registered from an[InitializeOnLoad]static constructor, and a stopped preview that is actually dead (see the DOTween table). - Right-click menus are scoped by target object, not type name.
SerializedProperty.type/propertyPathare unqualified, so require your own component asserializedObject.targetObject..Copy()the property for the callback. RegistercontextualPropertyMenuonce per folder. Start a handler's entries withSeparate(menu), never a bareAddSeparator: Unity 6000.3 already ends its part with a divider, which drew two on Windows.Separatereads the privatem_MenuItems. To see a real menu, call the internalEditorGUI.FillPropertyContextMenu(property, null, null)by reflection from a utility window'sOnGUI(it throws outside one) and readm_MenuItems. - Copy Inspector data with
JsonUtility, notEditorJsonUtility. The plain one keeps object references ({"instanceID":-228892}); the Editor one zeroes them. [Range]clamps typed input too. To let users type past the slider, draw the slider and field separately and only adopt the slider when it moved.GetPropertyHeightisn't given the draw width. For wrapping, the step drawer'sOnGUIrecordscurrentViewWidth - position.widthper property path (skippingLayoutevents) and the measure lays out atcurrentViewWidthminus that, callingHandleUtility.Repaint()when the margin changes. Anything that doesn't wrap uses fixed rows and truncates.- Testing enable-time logic needs reflection. Edit mode doesn't deliver
Awake/OnEnable/OnDisablewithout[ExecuteAlways], so togglingSetActivetests nothing — invoke them directly. In play modeAddComponentrunsAwakeat once, so reset the lazy-init flag after filling fields. - A namespaced tool in an un-namespaced project needs a
usingin game code that references it. Serialization only cares about the.metaGUID.
This repo is loose folders, not a Unity project. Nothing here can be claimed to compile without compiling it somewhere.
GameCraftSep2026Jam links this repo directly: its Packages/manifest.json has "com.rmfandyplayz.dotween-ui-animation": "file:../../UnityAnimationTools/DOTweenAnimationPlayer" (on the jam's package-migration branch, not yet merged). Every edit compiles there on the next AssetDatabase.Refresh(). Unity writes .meta files into this repo through the link, so check git status and commit them. GameCraftSep2026P2Jam installs by git URL (#release) and only sees what's published.
For the other two tools: copy the folder without its .meta files into a real project with DOTween Pro under Assets/_CompileCheck/, compile, delete, recompile. The jam's older un-namespaced copies don't conflict. Ask before using another project.
- Open the jam with
unity open <path> --editor-version 6000.3.9f1. It blocks: run it in the background and wait forunity status --format jsonto showready. - The jam is
GameCraftSep2026Jam, notGameCraftSep2026P2Jam, and only one should be open at a time. Both editors take port 7800, andunity cmdtalks to whichever holds it (--project-pathreturned 401). Checkeval 'return Application.dataPath;'before anything that writes.unity commandrejects--caller/--skill. unity cmd recompileusually reports a network error because the domain reload drops the connection — expected. Pollunity cmd recompile_status, which is authoritative; the console buffer replays stale errors.unity cmd evalruns C# in the live Editor, in its own assembly, so it can't see the tool'sinternaltypes (the editor classes) - call them through a harness. It mangles long scripts and can't takeusingdirectives, so call DOTween extensions statically (DG.Tweening.TweenExtensions.Goto(t, 0.5f)) or put the code in a compiled harness in a temp folder likeAssets/_PathCheck/Editor/, deleted with its.metaand recompiled away afterwards.- An
evalholding the main thread over 5 s returns a 400 "timed out" but still runs to the end. For anything long (opening scenes additively, a full fingerprint), have the harness store its result in a static and read it with a secondeval. unity cmd editor_play/editor_stopswitch modes. Anything about tween lifetimes must be checked in play mode.
The play-path fingerprint is the strongest test, and needs no play mode. In an additive scratch scene (NewScene(..., Additive)), instantiate Assets/Prefabs/UI/UI.prefab; copy the tester scenes to Assets/_Check/ with AssetDatabase.CopyAsset and open those additively, so the open scene is never touched. For every player and animation, at six sample points: EditorPrepareForPreview(), Random.InitState, player.Play(name), DOTweenEditorPreview.PrepareTweenForPreview(seq, true, true, false), one DOTween.ManualUpdate(t, t), dump, then DOTween.Kill(id) / StopAll() / EditorRestoreBaselines(). The dump: EditorJsonUtility.ToJson of every component plus activeSelf, and material floats/colours for anything with UIMaterialInstance, writing only what differs from rest. Currently 71 animations, 426 samples. Build a fresh sequence per sample (rewinding one accumulates on relative and To: Current steps), run it twice to prove it's deterministic, and enumerate names through EditorCollectAnimationNames, not EditorAnimations. TextMeshPro's JSON never returns to rest after an animation — stable "not restored" lines, not a bug.
Before and after are two states of this repo: git stash push -u -- DOTweenAnimationPlayer (the -u matters: an untracked file using new APIs breaks the old code's compile), refresh the jam, fingerprint, git stash pop, refresh, fingerprint, diff. Keep harnesses compatible with both versions (reflection only).
- For target resolution or baseline changes, a cheaper edit-mode dump is enough:
EditorPrepareForPreview()on every player, then each runtime step's resolved-target fields,targetPathMissed, baseline fields and any warnings, by reflection. - Through the preview:
UIAnimationPreview.End(),Random.InitState,UIAnimationPreview.Play, read the private staticrunningby reflection,DOTween.ManualUpdatein five slices. It differs from play mode forApplyFromValuesImmediatelyoff, and doesn't cover play-mode lifetimes. - For recording, round-trip every row: in a copy of
UITester, for each From and To row of each station,UIAnimationWindow.Open,UIAnimationRecording.Toggle, pose the target (throughUndo.RecordObject, as the Move tool would, withUndo.IncrementCurrentGroup()between "events"),Finish, then preview-play to the step's end (ManualUpdate(end + 1e-6)) and compare with the pose. Last run: 39 rows, plus Undo scope, Cancel, closing the window, snapping, a set via Preview On, joining a played preview, selection changes, refused typewriter text, three rows at once, dropping the last row, two rows on one value, the shared-value warning, a material asset edit, and saving mid-preview and mid-recording (position, Image material, TMP material, Custom Property: read the saved YAML document byGlobalObjectId.targetObjectId, count--- !u!21documents). CheckRunValuesagainst the real preview at each step's start plus 1e-6 s: a 1e-4 read is already measurably into an Out ease. - Preview behaviour can be driven synchronously: preview sequences are
UpdateType.Manual, soUIAnimationPreview.PlaythenDOTween.ManualUpdate(dt, dt)in a loop inside oneevalis exact, where sleeping betweenevalcalls isn't. A plainobjectas the requester keeps inspectors'OnDisableout of it.
GameCraftSep2026Jam's Assets/Scenes/UI/UITester.unity and UITester2.unity hold labelled hand-test stations under UI/UIAnimation Tests, one player per station, so the author tests by selecting objects. Fixtures are in Assets/Testing/UI/.
UITester's bottom row has G1–G4 for the gizmo handles (October 2026): G1 Size Delta in each mode, a From, snapping; G2 Scale, a pop-in from 0, a tilted block, and a five-step Crowd for Selected Step; G3 Offset Min / Max and a bottom-left pivot; G4 movement line styles, Baseline ghosts, and a curved path for the path editor's handles. Same Notes convention.UITester: R1–R12 for recording, rebuilt on request in October 2026 (the old Shared Animations, punch spam, warning and Ease stations are in git history). Each animation's Notes say what to do and what to expect. R1 position in every mode and a From, R2 size / scale / rotation / Offset Max, R3 Canvas Group alpha, Graphic alpha and colour (Current chained after an Absolute step, so the start isn't rest), R4 material float and colour (RecordTestFill.shader/.mat: UI/Default plus a_Progressfill), R5 a C# float and int property (CustomPropertyTestGauge), R6 Vector3 / Color / typewriter fields (CustomPropertyTestFields), R7 a counted TMP number and a Vector2 member (RectTransform.anchoredPositionas a Custom Property), R8 three rows at once, R9 a set (UITester Record Set.asset) via Preview On, R10 the window drawing without selection, R11 snapping and close-to-keep, R12 Undo, Cancel and play mode.SharedSetTest,PunchSpamTest,WarningsTest,CustomPropertyTestThrower,CP Counter.prefab,UITester Shared.assetandUITester Shared Set.assetare now unused.UITester2: gizmos, the type check, U1 / U2 (named for the record button's old Use Current flow; recorded from a pop-out window now), the CloseCredits replica, and a shared asset. Its bottom row has two play-mode stations with on-screen buttons:N1for Interrupt Others andStop/Stop(complete)/StopAll(complete),N2forSetTo/SetFrom/ClearOverrides(fixturesInterruptOthersTest,SetToFromTest), each showing what happened in white and what should have in yellow. The Canvas Scaler is Expand so that row fits a 16:9 Game view. They were checked byEditorSceneManager.LoadSceneInPlayMode(path, Additive)and a coroutine pressing each button withonClick.Invoke().- A station name must not contain
/—Transform.Findreads it as a path. - Harnesses open scenes additively (
OpenScene(path, Additive), build,SaveScene,CloseScene);Singlediscards the open scene's unsaved changes without asking. Check the scene isn't already open first: an additive open then returns the open scene, andCloseScene(scene, true)closes it, unsaved changes and all. With it open, entering play mode andLoadSceneInPlayModegives two copies. - A re-saved tester scene makes a huge, alarming diff (Unity reorders the YAML, and old players gain new fields at their defaults). Compare per YAML document (
--- !u!<class> &<fileID>) againstHEAD: expect no document removed and only added lines in changed ones.
-
Assets/Prefabs/UI/UI.prefabinstantiated into an empty additive scene has a zero-scale Canvas (nothing has sized it), so world-space checks on it are degenerate. Use the tester scenes for anything converting to or from world positions. -
Never call
AssetDatabase.SaveAssets(): it flushes every dirty asset, including the jam's TMP dynamic fontAssets/Fonts/TMPro/GeistPixel Regular.asset, whose committed copy is empty. Save your own withAssetDatabase.SaveAssetIfDirty(asset). If it happens,git checkoutthe font and reimport; the openUI.unitymay then show layout-only unsaved changes, which are harmless — tell the user rather than saving. -
After a builder that creates assets, check
git statusfor assets it didn't mean to touch (one once re-saved UITester2'sUI Animation Set.asset, cause unknown); revert withgit checkout. -
Selection.selectionChangeddoesn't fire with the Editor in the background. Invoke the delegate yourself (typeof(Selection).GetField("selectionChanged", …)→DynamicInvoke), thenActiveEditorTracker.sharedTracker.ForceRebuild(). The Inspector's UI Toolkit tree then only lays out after aRepaintImmediatelyon theInspectorWindow. -
Play mode stands still with the Editor in the background (
runInBackgroundis off in the jam):Time.frameCountstops, and a tween played fromevalnever moves. SetApplication.runInBackground = truefromevalonce in play mode; it resets when play mode ends. -
To check a drawer for exceptions without a picture, open a utility
EditorWindowwhoseOnGUIcallsEditor.CreateEditor(player).OnInspectorGUI()in a try/catch, setisExpandedon everything, and call the window's non-publicRepaintImmediately()by reflection. -
The Bash tool turns
\\$2into$2and a\\nin a heredoc into a line break. Build Windows paths withcygpath -w, and write scripts containing backslashes with the Write tool.
capture_scene_viewrenders only the camera (no handles, gizmos or overlays) and saves underAssets/.- What works is Win32
PrintWindow(hwnd, hdc, 2)from PowerShell, which captures the DX12 window without bringing it forward. CallSetProcessDPIAware()first (125% scaling clips it otherwise) and find the window withFindWindow([NullString]::Value, title)— PowerShell turns$nullinto"". For a drawer, draw it in a temporary utilityEditorWindowwithEditorGUILayout.PropertyFieldand capture that window by title. - Scene-view gizmos: a floating
SceneView(CreateInstance<SceneView>(), a uniquetitleContent,ShowUtility(),Frame(bounds)) captured by title. Every Screen Space Overlay canvas sits at the same world position, soSceneVisibilityManager.instance.Isolate(go, true), thenExitIsolation(). - Scene-view interaction via
SceneView.SendEvent: window coordinates arrive shifted (about 50 points in y on Windows, 26 on the Mac), so calibrate with oneMouseMoveand aduringSceneGuiprobe first. A world point's GUI position comes fromHandleUtility.WorldToGUIPointinside aduringSceneGuicallback during aRepaintImmediately. SendMouseDragwith a matchingdelta:Slider2Daccumulates it. - A hover can't be pictured:
RepaintImmediatelylays out at the real mouse, not a sent one. Send aMouseDownto hold the handle (lit while hot), grab, thenMouseUp. - On the Mac, any editor window can be pictured from inside Unity:
RepaintImmediatelyit (non-public, by reflection), then call the internalGrabPixels(RenderTexture, Rect)on itsm_Parent(found by walking up from the parent's type),ReadPixelsthe texture andEncodeToPNG. The image comes out upside down:sips -f vertical. Size it in pixels withEditorGUIUtility.pixelsPerPoint. For an Inspector, drawEditor.CreateEditor(x).OnInspectorGUI()from a utility window'sOnGUIand grab that.
On the author's Mac the jam's main branch holds a plain copy of the tool in Assets/Scripts/UI/Utility/DOTweenAnimationPlayer/ (Assembly-CSharp, no asmdef) instead of the package link, so testing in the Editor means copying the changed .cs files over (and a new file's .meta, so the GUIDs match). Offline, compile Library/Bee/artifacts/200b0aE.dag/Assembly-CSharp.rsp then Assembly-CSharp-Editor.rsp, each with the copy's files dropped and this repo's (runtime or Editor/) added, an -out: in the scratchpad, no -refout, and the editor one's Assembly-CSharp.ref.dll reference pointed at the runtime DLL just built. Unity's compiler there is Unity.app/Contents/Resources/Scripting/NetCoreRuntime/dotnet and .../DotNetSdkRoslyn/csc.dll. Both take about two seconds, with the Editor open.
On the author's Windows machine, Editor closed (about a minute). Unity's compiler response files are in GameCraftSep2026Jam/Library/Bee/artifacts/2000b0aE.dag/ (2000b0aP.dag/ is the player build, without UNITY_EDITOR); the animator's are rmf_claude.DOTweenUI.rsp and rmf_claude.DOTweenUI.Editor.rsp, whose source lines are already this repo's files. Copy them to the scratchpad, replace -out: / -refout: with an -out: there, and point the editor one's runtime reference at the DLL just built. Run from the jam root: "<Unity>/Editor/Data/NetCoreRuntime/dotnet.exe" exec <csc.dll> -nologo @<rsp>, with csc.dll from Editor/Data/DotNetSdkRoslyn/. In Git Bash, prefix MSYS2_ARG_CONV_EXCL='*' and pass csc.dll and the @rsp as cygpath -w paths. This recipe was worked out on the pre-package Assembly-CSharp rsps and hasn't been rerun since. It proves compilation against real 6000.3, DOTween, TMP and UGUI, not behaviour.
Offline, in a cloud container, no Unity. Proves compilation against real assemblies and that pure logic (reflection, baselines, DOTween tweens) behaves. It can't cover the native engine: GetComponent, transform.Find, IMGUI, Undo, the preview's editor hooks.
- Toolchain:
apt-get install dotnet-sdk-8.0 mono-devel. The Ubuntu archive is reachable; thedot.netinstaller, NuGet's search host andcodeload.github.comare blocked. - Unity references from
https://api.nuget.org/v3-flatcontainer/<id>/<version>/<id>.<version>.nupkg:unity3d.sdk2021.1.14.1 (UnityEditor.dlland a real monolithicUnityEngine.dll),unityengine.modules2021.3.33 (reference-only module DLLs, every bodythrow null),unity3d.unityengine.ui2020.3.21. Compile against the modules, not the monolithic DLL, so UnityEngine.UI's types unify. - DOTween:
git clone --depth 1 -b develop https://github.com/Demigiant/dotween;UnityTests.Unity6000.3/Assets/Plugins/Demigiant/DOTween/hasDOTween.dll,Editor/DOTweenEditor.dlland the generatedModules/*.cs(1.3.045 then, against the jam's 1.3.030). - Three
net48projects, split as Unity does, withMicrosoft.NETFramework.ReferenceAssemblies.net48: firstpass (Modules/*.cs); runtime (the tool's.csoutsideEditor/, plus a TMP stubTMP_Text : MaskableGraphicwithfontSharedMaterial/fontMaterial); editor (Editor/**/*.cs+UnityEditor+DOTweenEditor). DefineUNITY_EDITOR;UNITY_2018_1_OR_NEWER;…;UNITY_2021_3_OR_NEWER;NET_4_6, and build the runtime again withoutUNITY_EDITOR. - One known false error:
SerializedProperty.boxedValue(2022.1+) isn't in the 2021.1UnityEditor.dll. Shim it in a scratch copy, not the real files. - Running tests needs Mono: build one facade assembly per module name that
[TypeForwardedTo]s every public type into the monolithicUnityEngine.dll(dropping[Obsolete(error: true)]types); patch that DLL with Mono.Cecil so everyInternalCallreturnsdefaultandUnityEngine.Object's static constructor is a bareret; put everything in one folder and runmono tests.exe. - Faking a live component:
FormatterServices.GetUninitializedObject, then setUnityEngine.Object.m_CachedPtrnon-zero (look it up withPublic | NonPublic). Inject resolved members into a step by reflection, sinceResolveneedsGetComponents. - DOTween runs uninitialised here, as in edit mode.
SetUpdate(UpdateType.Manual)plusDOTween.ManualUpdate(dt, dt)steps a tween or sequence exactly.