• Editor
  • Custom Triggers Feature Request

In practice, I'd like to be able to trigger a sound or spawn a particle based on key points in the animation.

Let's say that I'm ripping off Plants Vs Zombies and I want to shoot a Pea out when the head cocks to a certain angle. Now in this example I can simply play the sound the same time I emit the pea particle, but there are several other instances when I want the sound generation separate from particle triggering or script triggering, etc...etc..etc..

Instead of having to create new types for sound triggers, particle triggers, script triggers, etc... All you really need to do is create a single new Timeline Type for 'trigger'.

The timeline would have a instance type, read as a string, and each frame would have a simple string. That's it.

The timeline will need to keep track of the previous frame that it executed so that we only receive the edges of the events. When an edge is encountered, then a global callback is made, similar to texture page creation, only optional, like overriding the free() and malloc() functions. Something like:

void _onTrigger(void* theId (Skeleton*? AnimationState*?), char * theTriggerType, char * theTriggerValue)

voide _setOnTrigger(func* MyCustomTriggerCallback)

In the Editor, we only need to add Trigger Types to the Root Bone. The New Trigger gets a single instance name, and we are allowed to put any text we want on a keyframe. The custom text can be anything from a single word to an entire script (e.g. lua) to execute.

Related Discussions
...
  • Đã chỉnh sửa

Yep, that is this task:
https://trello.com/card/event-timeline/ ... c0059d9/44

One implementation might have an "Events" node under the skeleton in the tree. Here you would define named events. Each one would have a key button, clicking it adds a key to an event timeline in the dopesheet. At runtime you'd get a callback. Each named event in the tree could have some properties. Eg, a string payload. They could have a type, eg "string", "audio", etc. Audio could let you specify an audio file so you can hear the sound in the editor and better synchronize your animations.

I like this because it's similar to how image changes are keyed. It has the drawback (or is it an advantage?) that you need to define all your events on the skeleton. If you often have an event you only use once in a single animation, the Events node in the tree might become cluttered.

What do you guys think?

That seems to fit the workflow perfectly, and if the events are part of the skeleton, then we can iterate over them in code. (change them procedurally)

As long as I get the [Instance Id], the [event Type] and [string Payload] on the callback then I'm not really concerned if the event types are hard coded {sound, string, etc...}. I guess if the editor wants to play a sound then internally it will probably have a hard type, but I'll just lookup the audio filename in a map to play the right sound.

The [Instance Id] is important, I think [Skeleton* ] will be fine, I'm not seeing a single Skeleton being shared among several instances (again, fine for me). This is so I can lookup the instance in a map to make sure I spawn particles correctly.

I was thinking the same, but mostly because I don't want to write a multi-line text edit control, which is a huge PITA. :p All of Spine is rendered with OpenGL, so all the UI widgets are custom built. Users can always look up a game script by name and execute it.

Your screenshot looks good except I don't think there should be a visibility dot, only a key dot (green if not key is set on current frame, red if keyed on current frame). I'm not yet sure about grouping the events node.

In the dopesheet we currently have a number of bone groups, and each has a number of timelines, one for each bone property that has a key. For events, we'd have a single "Events" group which would have a timeline under it for each named event that has a key. A timeline for each is needed to allow two events to be keyed on the same frame.

I think that about covers it for the whole feature from start to finish. 🙂 The dopesheet is the hardest part, it is quite complex.

Oh, I forgot that all the runtimes need callbacks. Animations are stateless and currently just take a time and pose the skeleton at that time. This isn't enough to fire callbacks. 🙁 We could change the API to take a "last time" and "new time" so that we know to fire callbacks between those. But, how does the animation fire the callback? It knows the skeleton (which you noted was important), animation, event, time the event was keyed, and time the event was fired.

Runtimes like spine-libgdx, spine-as3, and spine-csharp could store a listener list directly on Skeleton. For spine-c, since it is generic and using C function pointers is inconvenient for some implementations, I guess there could be a single, global callback function pointer. Implementations can use this to route the event appropriately. Skeleton should probably have a "void* rendererObject", eg so cococs2d can find the scene graph node from the Skeleton* in order to fire ObjC listeners stored on the node.

I see what you're saying about time and lasttime in the animation state. Perhaps you could have a forward declaration of the AnimationState struct in the Animation header and instead of passing in {time, lasttime, loop, etc...} you can pass in a pointer to the AnimationState. Then, you'd never have to change the parameters again if you needed to pass extra state data.

As for firing the event, I figured you'd do 2 binary searches for frameIndex and prevFrameIndex. If they are not equal, then the event fires. That should work forwards and backwards. I think there might be some minor annoyances of at event being on the first (or last) frame because binsearch returns 0 if not found, but then you'd have to check is (lasttime < self->frames[0] && time >= self->frames[0]), etc... and probably "adjust" lasttime when you loop.

I was thinking a global callback function pointer that is initially set to null. Then the EventTimeline_apply method could check that first and ignore the functionality completely if the user doesn't set it. The callback could take a const ref or pointer to a spine::Event{} and you wouldn't need 3+ parameters on the callback. The event would live on the stack so no need to NEW() it up.

I would route the events on my side. I'd have a map registered for each of the skeletons to index into my game objects. My SoundManager can lookup a sound from it's filename, or from an "export" name.

Currently Animation doesn't know about AnimationState. AnimationState is solely for convenience to do animation mixing and queuing, I don't want to force it to be used.

Detecting when to fire the event may be a little tricky for some edges cases. Eg, animations can loop.

I wonder if we could use the callback mechanism for AnimationState to fire events for animation start/end.

If Skeleton has a void* rendererObject (as some structs already do, eg RegionAttachment) then you wouldn't need a Skeleton -> game object map.

Everything else you mentioned sounds fine. 🙂 I just need to get started implementing some of this stuff!

Nate đã viết

Currently Animation doesn't know about AnimationState. AnimationState is solely for convenience to do animation mixing and queuing, I don't want to force it to be used.

I would recommend a structure, something like AnimParams{}, pass it through as a const ref or pointer, but let it live on the stack.

typedef struct _AnimParams{
     float _time;
	 float _lastime;
	 bool _loop;
} AnimParams;

AnimationState_apply()
AnimParams params;
params._time = time; etc...; etc...;
Animation_apply(animation, skeleton, &params);
Nate đã viết

I wonder if we could use the callback mechanism for AnimationState to fire events for animation start/end.

Yeah, system defined events. That sounds cool, actually.

Nate đã viết

If Skeleton has a void* rendererObject (as some structs already do, eg RegionAttachment) then you wouldn't need a Skeleton -> game object map.

That would be really handy.

jpoag đã viết

I would recommend a structure, something like AnimParams{}, pass it through as a const ref or pointer, but let it live on the stack.

That would be fine for spine-c, but other languages (Java) don't have stack allocation or incur some (small) overhead (JavaScript, Lua) for passing a parameter object containing the parameters. It would be nice if the method signatures were similar across languages, at least for the core of the API. I have a pretty good feeling that after adding lastTime that we won't need more state for an animation. 4 parameters isn't too bad.

Lol, yeah I guess I can be a little myopic when it comes to platform support. Just trying to save a little trouble down the road if you ever need to add a 5th parameter.

17 ngày sau

Animation shot, the bullet in the middle of a frame sent, how fired bullets in that frame trigger event, draw a new bullet?(google translate)

dfw10011, Google translate didn't do a great job conveying what you wanted to ask. 🙁 You would use an event in your animation to trigger code in your game to do whatever is necessary to spawn a bullet.