Collectibles pickups scoring and win condition
CollectibleComponent, CollectorComponent, or SpawnPoolComponent (all stripped). This is the canonical AI-authored collect-count-win loop, mirrored by the engine's own shooter-collector-v2 eval case.Use this for
Not for
Pairs with: World build document authoring compile apply loop, Game loop assembly entities scripts HUD win shippable loop, UI HUD panels widgets and world nameplates, Saves persistence player progress and data stores, Physics colliders rigid bodies triggers and joints
The one-document path (default): batch + collectible cluster
Do not create the pickups one call at a time, and do not hand-build the loop. ONE world build document (see authoring-with-the-world-build-document) carries BOTH: an entity.primitive_batch.create whose payload.items place one body per coin (write the required exact key and the query tags on each item), and a gameplay.collectible_cluster.create with tag, entityKeys, scriptKey, score, victory. The cluster composes the whole proven loop: the onTriggerEnter collect handler (despawn + score increment), the onTick tag scan with the won flag and win condition, the HUD wire when the document carries a panel, the ScriptComponent attached to EVERY entity in entityKeys, the static trigger collider completed onto every collectible body the same document creates (never add RigidBody to a static sensor), and the referenced typed-state store auto-created. The sections below teach the same loop as script-patch ops - use them to UNDERSTAND or to repair/extend a loop, not to rebuild what the cluster already landed.
The scoring recipe (the script pattern)
Author with project_apply_script_semantic_patch using the ai_safe op pack. Ops inside a handler take scope: { "kind":"handler", "handlerRef": <key> }.
- Tag every collectible entity (a tag on the entity, for example
coin) so a query can find them, and give each a triggerColliderComponent(collision:"trigger") so contact is detectable. The document already does both when you author batch items withtagsand a collectible cluster withentityKeys; hand-attach them (one atomicproject_apply_transaction) only for entities the document did not create. - Seed the score:
writeTypedStateValue{ "field":"score", "valueType":"number", "value": 0 }(storeKey defaultsprogress, recordKeyplayer). - On pickup, react with
addTriggerHandler{ "handlerKey":"onCollect" }. Inside it:despawnEntity{ "scope":{...}, "reason":"collected" }removes the item.incrementNumericState{ "scope":{...}, "field":"score", "by": 1 }adds to the score.
- To evaluate a win each tick, add
addTickHandler{ "handlerKey":"onTick" }and inside it:findEntitiesByTag{ "scope":{...}, "tag":"coin", "assignTo":"coins", "limit": 64 }collects the remaining targets.compareRemainingCount{ "scope":{...}, "arrayRef":"coins", "operator":"lte", "count":0, "assignTo":"allGone" }, or go straight todeclareWinCondition{ "scope":{...}, "arrayRef":"coins", "remainingCount":0, "phaseKey":"won", "reason":"all coins collected" }which transitions the match phase when the count reaches the threshold.
- Show the score with
wireHudStateBinding(see ui-hud-panels-and-widgets) or aTextRenderableComponentbound to the stat.
Pitfalls
findEntitiesByTaglimitcaps at 1000;forEachEntityInResultmaxIterationscaps at 1000. Size them to your item count.forEachEntityInResult.bodyaccepts only the loop-intent ops (despawnEntity,emitCustomEvent,incrementNumericState,attachComponentWithValidatedDefaults); it is not arbitrary code.declareWinConditioncarrieswriteState/emitEvent/requiresAuthority; it runs on the authority. Do not also flip the phase client-side.- The one-shot
addShooterCollectorLoopmacro is NOT AI-expressible; build from the primitives above. - The document places the bodies AND can carry the tag, the trigger collider (component.put operations), and the behavior patch in the same apply; per mutation calls are the precision tier for targeted repairs, not for bulk placement or the initial loop.
Verify
simulation_run(qa.run.start) to prove the score rises and the win phase fires when the last item is collected.world_playtest_scenarioto PLAY it: an after state with the coins gone and the score raised is the proof; an unchanged digest means the loop did not run.project_get_graph_snapshotto confirm the tick and trigger handlers landed.