From 0852b8463b961015a822ce7a505cf84d8ce8cbab Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Tue, 21 Jul 2026 18:55:23 -0500 Subject: [PATCH] ANIM --- src/dusk/animation/animation.c | 33 ++++++- src/dusk/animation/animation.h | 41 +++++++- src/dusk/asset/loader/dmf/assetmeshloader.c | 8 -- test/animation/test_animation.c | 100 ++++++++++++++++++++ 4 files changed, 169 insertions(+), 13 deletions(-) diff --git a/src/dusk/animation/animation.c b/src/dusk/animation/animation.c index 11cd23b7..2f21d2db 100644 --- a/src/dusk/animation/animation.c +++ b/src/dusk/animation/animation.c @@ -23,6 +23,9 @@ void animationInit( anim->speed = 1.0f; anim->loop = false; anim->playing = false; + anim->eventTime = 0.0f; + anim->eventCallback = NULL; + anim->eventUser = NULL; } void animationUpdate(animation_t *anim) { @@ -30,17 +33,39 @@ void animationUpdate(animation_t *anim) { if(!anim->playing) return; float_t duration = keyframeSetGetDuration(&anim->keyframes); + float_t prevTime = anim->time; anim->time += TIME.delta * anim->speed; + // Loops that overshoot duration wrap back into [0, duration) -- an event + // near the end must still be checked across that wrap, as two segments: + // (prevTime, duration] then [0, newTime]. + bool_t wrapped = anim->loop && duration > 0.0f && anim->time >= duration; + if(anim->loop) { if(duration > 0.0f) anim->time = mathModFloat(anim->time, duration); - return; - } - - if(anim->time >= duration) { + } else if(anim->time >= duration) { anim->time = duration; anim->playing = false; } + + if(!anim->eventCallback) return; + + bool_t crossed = wrapped + ? (prevTime < anim->eventTime || anim->time >= anim->eventTime) + : (prevTime < anim->eventTime && anim->time >= anim->eventTime); + if(crossed) anim->eventCallback(anim, anim->eventUser); +} + +void animationSetEvent( + animation_t *anim, + const float_t time, + const animationeventcallback_t callback, + void *user +) { + assertNotNull(anim, "Animation pointer cannot be null."); + anim->eventTime = time; + anim->eventCallback = callback; + anim->eventUser = user; } float_t animationGetValue(animation_t *anim, const uint16_t trackIndex) { diff --git a/src/dusk/animation/animation.h b/src/dusk/animation/animation.h index dd0a740d..fddd683f 100644 --- a/src/dusk/animation/animation.h +++ b/src/dusk/animation/animation.h @@ -6,7 +6,18 @@ #pragma once #include "keyframeset.h" -typedef struct { +typedef struct animation_t animation_t; + +/** + * Callback fired once when animationUpdate() advances time past + * eventTime (see animationSetEvent()). + * + * @param anim The animation whose event fired. + * @param user The user pointer passed to animationSetEvent(). + */ +typedef void (*animationeventcallback_t)(animation_t *anim, void *user); + +typedef struct animation_t { /** The animation's tracks/channels and their raw keyframe data. */ keyframeset_t keyframes; @@ -22,6 +33,13 @@ typedef struct { /** True while animationUpdate() should advance time each call. */ bool_t playing; + + /** Time (seconds) at which eventCallback fires; see animationSetEvent(). */ + float_t eventTime; + /** Callback fired once when time advances past eventTime, or NULL. */ + animationeventcallback_t eventCallback; + /** User pointer passed to eventCallback unchanged. */ + void *eventUser; } animation_t; /** @@ -48,10 +66,31 @@ void animationInit( * [0, duration) on reaching it; otherwise time is clamped there and * playing is cleared. * + * If eventCallback is set, it fires once when this call advances time + * from at or before eventTime to after it (checked across the loop wrap + * point too, so an event near the end of a looping animation still fires + * every loop). + * * @param anim The animation to update. */ void animationUpdate(animation_t *anim); +/** + * Sets (or clears, with callback NULL) the single time-triggered event + * fired by animationUpdate(). + * + * @param anim The animation to set the event on. + * @param time The time, in seconds, at which callback fires. + * @param callback The callback to invoke, or NULL to clear any event. + * @param user Arbitrary pointer forwarded to callback unchanged. + */ +void animationSetEvent( + animation_t *anim, + const float_t time, + const animationeventcallback_t callback, + void *user +); + /** * Gets the value of one of the animation's tracks at its current * playback time. diff --git a/src/dusk/asset/loader/dmf/assetmeshloader.c b/src/dusk/asset/loader/dmf/assetmeshloader.c index de40a622..caa3af86 100644 --- a/src/dusk/asset/loader/dmf/assetmeshloader.c +++ b/src/dusk/asset/loader/dmf/assetmeshloader.c @@ -127,14 +127,6 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) { errorChain(ret); } - #if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY) - // VBO owns the data now; CPU copy is no longer needed. The platform - // mesh object itself still needs meshDispose later - tracked via - // out->meshInitialized, independent of the CPU buffer's lifetime. - memoryFree(out->vertices); - out->vertices = NULL; - #endif - loading->entry->state = ASSET_ENTRY_STATE_LOADED; errorOk(); } diff --git a/test/animation/test_animation.c b/test/animation/test_animation.c index 41e58893..09bfaa0d 100644 --- a/test/animation/test_animation.c +++ b/test/animation/test_animation.c @@ -99,12 +99,112 @@ static void test_animationUpdateLoopsWithMultipleChannels(void **state) { assert_float_equal(anim.time, 1.0f, 0.0001f); } +static uint32_t TEST_EVENT_FIRE_COUNT; +static animation_t *TEST_EVENT_LAST_ANIM; +static void *TEST_EVENT_LAST_USER; + +static void test_resetEventCounters(void) { + TEST_EVENT_FIRE_COUNT = 0; + TEST_EVENT_LAST_ANIM = NULL; + TEST_EVENT_LAST_USER = NULL; +} + +static void test_onAnimationEvent(animation_t *anim, void *user) { + TEST_EVENT_FIRE_COUNT++; + TEST_EVENT_LAST_ANIM = anim; + TEST_EVENT_LAST_USER = user; +} + +static void test_animationEventFiresOnceWhenCrossed(void **state) { + test_resetEventCounters(); + + keyframe_t keyframes[] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR } + }; + keyframe_t *tracks[] = { keyframes }; + uint16_t trackCounts[] = { 2 }; + + animation_t anim; + animationInit(&anim, tracks, trackCounts, 1); + anim.playing = true; + + int32_t userValue = 7; + animationSetEvent(&anim, 0.5f, test_onAnimationEvent, &userValue); + + TIME.delta = 0.3f; + animationUpdate(&anim); + assert_int_equal(TEST_EVENT_FIRE_COUNT, 0); + + // 0.3 -> 0.6 crosses the 0.5 event time. + animationUpdate(&anim); + assert_int_equal(TEST_EVENT_FIRE_COUNT, 1); + assert_ptr_equal(TEST_EVENT_LAST_ANIM, &anim); + assert_ptr_equal(TEST_EVENT_LAST_USER, &userValue); + + // Doesn't refire on later ticks past the event time. + animationUpdate(&anim); + assert_int_equal(TEST_EVENT_FIRE_COUNT, 1); +} + +static void test_animationEventFiresEachLoopAcrossWrap(void **state) { + test_resetEventCounters(); + + keyframe_t keyframes[] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR } + }; + keyframe_t *tracks[] = { keyframes }; + uint16_t trackCounts[] = { 2 }; + + animation_t anim; + animationInit(&anim, tracks, trackCounts, 1); + anim.loop = true; + anim.playing = true; + animationSetEvent(&anim, 0.9f, test_onAnimationEvent, NULL); + + TIME.delta = 0.95f; + animationUpdate(&anim); + assert_int_equal(TEST_EVENT_FIRE_COUNT, 1); + assert_float_equal(anim.time, 0.95f, 0.0001f); + + // 0.95 + 0.95 = 1.9 -> wraps to 0.9 against a 1.0s duration, crossing + // the event time again on the way around. + animationUpdate(&anim); + assert_int_equal(TEST_EVENT_FIRE_COUNT, 2); + assert_float_equal(anim.time, 0.9f, 0.0001f); +} + +static void test_animationEventClearedByNullCallback(void **state) { + test_resetEventCounters(); + + keyframe_t keyframes[] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR } + }; + keyframe_t *tracks[] = { keyframes }; + uint16_t trackCounts[] = { 2 }; + + animation_t anim; + animationInit(&anim, tracks, trackCounts, 1); + anim.playing = true; + animationSetEvent(&anim, 0.5f, test_onAnimationEvent, NULL); + animationSetEvent(&anim, 0.5f, NULL, NULL); + + TIME.delta = 1.0f; + animationUpdate(&anim); + assert_int_equal(TEST_EVENT_FIRE_COUNT, 0); +} + int main(int argc, char **argv) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_animationInitDefaults), cmocka_unit_test(test_animationUpdateNoopWhenNotPlaying), cmocka_unit_test(test_animationUpdateStopsAtEndWhenNotLooping), cmocka_unit_test(test_animationUpdateLoopsWithMultipleChannels), + cmocka_unit_test(test_animationEventFiresOnceWhenCrossed), + cmocka_unit_test(test_animationEventFiresEachLoopAcrossWrap), + cmocka_unit_test(test_animationEventClearedByNullCallback), }; return cmocka_run_group_tests(tests, NULL, NULL);