We are launching a game with playcanvas with a lot of very small animated characters. We have built a pipeline where animation assets are loaded from the registry and we are also bypassing creating a state graph asse. Instead we are using code to do state assignment.
This works very well for us but I’m having some confusion about the best way to stop and animation.
In our cloudtester code we find the Open glb asset in the registery and prebind the animation asset to the model/rig.
this.animComponent.assignAnimation(“Open”, asset.resource);
This starts the animation looping.
When I hit “O” on the keyboard to play the animation with loop false it plays but continues to loop. Loop false has no effect. Here’s the code I’m using:
this.animComponent.speed = 1;
this.animComponent.playing = true;
layer.loop = false;
layer.playing = true;
layer.play("Open");
});
Is this behavior expected? My expectation is that setting layer.loop to false and layer.play(Open) it would play and then stop. Does it need an initial state or transition defined for it to not loop?
The workaround I’ve found is to just check activeStateProgress and use layer.stop() . The keyboard “S” runs this code:
if ( !layer.transitioning && layer.activeStateProgress >= 0.98) {
layer.playing = false;
}
This works fine but I am wondering if there is a better way to setup the states so that loop false is the same as doing a layer.stop().
https://playcanvas.com/editor/project/1475545
Thank you.
Hi @noah_mizrahi!
As far as I can see, the loop property is not available on the anim layer itself. The loop property belongs to the animation state inside the state graph, so not to the layer.
Thanks for the heads up. I was. not able to get the track to “stop” on it’s own using the loop attr on the state. However, I was able to define a start state in code using an empty track and a freeze event defined in code to stop the animation.
import * as pc from "playcanvas";
export class cloudstester extends pc.Script {
static scriptName = "cloudstester";
/** @attribute @type {pc.Asset} @resource animation */
openAsset = null;
/** @attribute */
openAssetName = "Open.glb";
animComponent = null;
initialize() {
this.findAnimComponent();
this.createSimpleGraph();
this.assignTracksAndEvents();
}
createSimpleGraph() {
const stateGraph = {
"parameters": {},
"layers": [{
"name": "Base",
"states": [
{ "name": "START" },
{ "name": "Empty", "speed": 1 },
{ "name": "Open", "speed": 1, "loop": false }
],
"transitions": [
{ "from": "START", "to": "Empty", "time": 0 }
]
}]
};
this.animComponent.loadStateGraph(stateGraph);
// Listen for the specific event we're about to add to the track
this.animComponent.on('freeze', () => {
if (this.animComponent.baseLayer) {
this.animComponent.baseLayer.playing = false;
console.log("[cloudstester] Event 'freeze' received. Layer paused.");
}
});
}
assignTracksAndEvents() {
const silentTrack = new pc.AnimTrack("Silent", 0, [], [], []);
this.animComponent.assignAnimation("Empty", silentTrack);
const asset = this.openAsset || this.app.assets.find(this.openAssetName);
if (asset) {
const onReady = () => {
const track = (asset.type === "container") ? asset.resource.animations[0] : asset.resource;
if (track) {
// --- THE FIX: Inject an Event at the end of the track ---
// We place it at (duration - a tiny margin) to ensure it fires
// before the engine resets the playhead.
const eventTime = Math.max(0, track.duration - 0.01);
track.events = new pc.AnimEvents([
{
time: eventTime,
name: 'freeze'
}
]);
this.animComponent.assignAnimation("Open", track);
console.log(`[cloudstester] Track assigned with 'freeze' event at ${eventTime}s`);
}
};
if (asset.resource) onReady();
else {
asset.ready(onReady);
this.app.assets.load(asset);
}
}
}
update(dt) {
if (this.app.keyboard.wasPressed(pc.KEY_O)) {
const layer = this.animComponent.baseLayer;
if (layer) {
layer.playing = true; // Unpause
layer.play("Open");
console.log("[cloudstester] Playing 'Open'...");
}
}
}
findAnimComponent() {
this.animComponent = this.entity.anim || this.entity.findComponent("anim");
if (!this.animComponent) {
this.entity.addComponent("anim");
this.animComponent = this.entity.anim;
}
}
}
That’s weird. That workaround shouldn’t be necessary though. When I have some time, I’ll run a quick test myself. Thanks!