Keep behavior variables private to their own graph (#257)
game-maker / gate (push) Successful in 3m40s
game-maker / suite (push) Successful in 10m56s

A behavior often needs working state such as a timer or a counter.
Until now every variable showed up on each placed object and in the
Properties menu, so the inspector filled up with values nobody should
touch. A private variable works like any other inside its graph but
is not offered anywhere outside.

- Each behavior variable gets a lock switch next to Keep. It starts
  off, so existing projects stay as they are (`privateVariables`).
- A placed object shows no private variable, neither in the
  inspector nor while the game runs. If all are private, it says so.
- Switching a variable to private drops its overrides on every
  placed object in one undo step. Switching back does not restore
  them.
- A running game does not write a private value back as an
  override, and a project change during the run does not reset it.
- `+ Property` no longer offers private variables. A port that
  names one by hand still reads and writes it without a warning.
- The default value of a private variable is read-only in the asset
  inspector. Its shape (name, fields, type, delete) stays editable.
- Keep is now a pin icon instead of a word. With the lock added, the
  header of a list variable had no room left and the lock covered
  `+ Item`.

Not done: the animation track picker still offers private variables.

Tests: ./check.sh -> 186 files, 2711 tests, CHECK EXIT 0.
e2e 1,19,20,21,56 -> ALL PASSED, 119 checks. New module 56 has
7 checks with a real mouse. Counter-checks fail as expected: unit
without the write-back filter, the sync guard, dropping overrides or
loading the flag. Browser with all variables on the object, an
unfiltered menu, or no value lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-28 13:54:57 +02:00
1 parent cf6565d108
commit e8b3ac0a3b
16 files changed
+455 -49

No files matched your search

+10 -3
View File
@@ -1,6 +1,6 @@
import { ENTITY_HANDLE, isHandleEnabled, type PointerState, worldMatrixOf } from '../core/graph';
import { newId } from '../core/ids';
import type { Asset, TilemapAsset } from '../core/model/Asset';
import { type Asset, isPrivateVariable, type TilemapAsset } from '../core/model/Asset';
import { withoutObjects } from '../core/graph/liveObjects';
import { bagWithoutItemTypes, type PropertyBag, sameValue, type Vec2 } from '../core/model/Properties';
import { isPlaceableType } from '../core/model/AssetTypes';
@@ -252,8 +252,11 @@ export class World {
const resolved = bagWithoutItemTypes(resolveSceneObjectProps(this.project, asset, object));
for (const key of Object.keys(entity.props)) if (!(key in resolved)) delete entity.props[key];
// The scene only ever got the objects' empty places, so a value that differs by them alone stays.
for (const [key, value] of Object.entries(resolved))
// A private value never reaches the scene, so the scene's is only where it starts.
for (const [key, value] of Object.entries(resolved)) {
if (isPrivateVariable(asset, key) && key in entity.props) continue;
if (!sameValue(value, withoutObjects(entity.props[key]))) entity.props[key] = value;
}
return entity;
}
@@ -322,9 +325,13 @@ export class World {
const moved = p.x !== entity.x || p.y !== entity.y || p.z !== entity.z;
const base = bagWithoutItemTypes(resolveSceneObjectProps(this.project, asset, object));
// What a run holds of an object stays in the run; the scene gets the empty one.
// A private variable (#257) has no override to hold its value: that stays in the run.
const drifted = Object.keys(entity.props).filter(
(k) =>
k in base && base[k] !== entity.props[k] && !sameValue(base[k], withoutObjects(entity.props[k])),
k in base &&
!isPrivateVariable(asset, k) &&
base[k] !== entity.props[k] &&
!sameValue(base[k], withoutObjects(entity.props[k])),
);
if (!moved && drifted.length === 0) continue;
// Only now, because a ref for resolved content costs a resolve of its own – and because a