First round of animation improvements

This commit is contained in:
2026-07-20 16:07:38 -05:00
parent 7ceb9e571d
commit 0826908068
20 changed files with 1049 additions and 49 deletions
+2
View File
@@ -6,5 +6,7 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
easing.c
keyframe.c
keyframeset.c
animation.c
)
+27 -32
View File
@@ -5,48 +5,43 @@
#include "animation.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
#include "time/time.h"
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
keyframe_t **tracks,
uint16_t *trackCounts,
uint16_t trackCount
) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
anim->keyframes = keyframes;
anim->keyframeCount = keyframeCount;
keyframeSetInit(&anim->keyframes, tracks, trackCounts, trackCount);
anim->time = 0.0f;
anim->speed = 1.0f;
anim->loop = false;
anim->playing = false;
}
float_t animationGetValue(animation_t *anim, const float_t time) {
void animationUpdate(animation_t *anim) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
assertTrue(time >= 0, "Time must be non-negative.");
keyframe_t *start;
keyframe_t *end;
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
keyframe_t *current = anim->keyframes;
start = current;
if(!anim->playing) return;
do {
if(current->time > time) {
end = current;
break;
}
start = current;
current++;
float_t duration = keyframeSetGetDuration(&anim->keyframes);
anim->time += TIME.delta * anim->speed;
if(current > last) {
end = start;
break;
}
} while(true);
if(anim->loop) {
if(duration > 0.0f) anim->time = mathModFloat(anim->time, duration);
return;
}
float_t t = (time - start->time) / (end->time - start->time);
return mathLerp(start->value, end->value, easingApply(start->easing, t));
}
if(anim->time >= duration) {
anim->time = duration;
anim->playing = false;
}
}
float_t animationGetValue(animation_t *anim, const uint16_t trackIndex) {
assertNotNull(anim, "Animation pointer cannot be null.");
return keyframeSetGetValue(&anim->keyframes, trackIndex, anim->time);
}
+44 -15
View File
@@ -4,31 +4,60 @@
// https://opensource.org/licenses/MIT
#pragma once
#include "keyframe.h"
#include "keyframeset.h"
typedef struct {
keyframe_t *keyframes;
uint16_t keyframeCount;
/** The animation's tracks/channels and their raw keyframe data. */
keyframeset_t keyframes;
/** Current playback position, in seconds. */
float_t time;
/** Playback rate multiplier; 1.0 = normal speed. */
float_t speed;
/** True if animationUpdate() should wrap time back to 0 on reaching the
* final keyframe, rather than holding there and clearing playing. */
bool_t loop;
/** True while animationUpdate() should advance time each call. */
bool_t playing;
} animation_t;
/**
* Initializes an animation.
*
* Initializes an animation: time 0, speed 1.0, not looping, not playing.
* See keyframeSetInit() -- tracks/trackCounts and the keyframe_t arrays
* they point to are not copied, and must outlive this animation.
*
* @param anim The animation to initialize.
* @param keyframes The keyframes to use for the animation.
* @param keyframeCount The number of keyframes in the animation.
* @param tracks Array of trackCount keyframe_t arrays.
* @param trackCounts Array of trackCount keyframe counts, matching tracks.
* @param trackCount The number of tracks/channels in this animation.
*/
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
keyframe_t **tracks,
uint16_t *trackCounts,
uint16_t trackCount
);
/**
* Gets the value of the animation at a given time.
*
* @param anim The animation to get the value from.
* @param time The time at which to get the value, in seconds.
* @return The value of the animation at the given time.
* Advances an animation's time by TIME.delta * speed. No-op if not
* playing. Duration is the latest of every track's final keyframe time
* (see keyframeSetGetDuration()). If loop is set, time wraps back into
* [0, duration) on reaching it; otherwise time is clamped there and
* playing is cleared.
*
* @param anim The animation to update.
*/
float_t animationGetValue(animation_t *anim, const float_t time);
void animationUpdate(animation_t *anim);
/**
* Gets the value of one of the animation's tracks at its current
* playback time.
*
* @param anim The animation to get the value from.
* @param trackIndex The track to evaluate, in [0, trackCount).
* @return The interpolated value of that track at the current time.
*/
float_t animationGetValue(animation_t *anim, const uint16_t trackIndex);
+43
View File
@@ -0,0 +1,43 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "keyframe.h"
#include "assert/assert.h"
#include "util/math.h"
float_t keyframeGetValue(
keyframe_t *keyframes,
const uint16_t keyframeCount,
const float_t time
) {
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
assertTrue(time >= 0, "Time must be non-negative.");
keyframe_t *first = keyframes;
keyframe_t *last = keyframes + keyframeCount - 1;
// Clamp to the boundary keyframes' values directly rather than
// interpolating -- start == end at either boundary would otherwise
// divide by zero.
if(time <= first->time) return first->value;
if(time >= last->time) return last->value;
keyframe_t *start = first;
keyframe_t *end;
keyframe_t *current = first;
do {
if(current->time > time) {
end = current;
break;
}
start = current;
current++;
} while(true);
float_t t = (time - start->time) / (end->time - start->time);
return mathLerp(start->value, end->value, easingApply(start->easing, t));
}
+15 -1
View File
@@ -10,4 +10,18 @@ typedef struct {
float_t time;
float_t value;
easingtype_t easing;
} keyframe_t;
} keyframe_t;
/**
* Gets the eased, interpolated value of a keyframe array at a given time.
*
* @param keyframes The keyframes to evaluate, ascending by time.
* @param keyframeCount The number of keyframes.
* @param time The time at which to get the value, in seconds.
* @return The interpolated value at the given time.
*/
float_t keyframeGetValue(
keyframe_t *keyframes,
const uint16_t keyframeCount,
const float_t time
);
+60
View File
@@ -0,0 +1,60 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "keyframeset.h"
#include "assert/assert.h"
void keyframeSetInit(
keyframeset_t *set,
keyframe_t **tracks,
uint16_t *trackCounts,
uint16_t trackCount
) {
assertNotNull(set, "Keyframe set pointer cannot be null.");
assertNotNull(tracks, "Tracks pointer cannot be null.");
assertNotNull(trackCounts, "Track counts pointer cannot be null.");
assertTrue(trackCount > 0, "Track count must be more than 0.");
set->tracks = tracks;
set->trackCounts = trackCounts;
set->trackCount = trackCount;
}
float_t keyframeSetGetValue(
keyframeset_t *set,
const uint16_t trackIndex,
const float_t time
) {
assertNotNull(set, "Keyframe set pointer cannot be null.");
assertTrue(trackIndex < set->trackCount, "Track index out of bounds.");
return keyframeGetValue(
set->tracks[trackIndex], set->trackCounts[trackIndex], time
);
}
void keyframeSetGetValues(
keyframeset_t *set,
const float_t time,
float_t *outValues
) {
assertNotNull(set, "Keyframe set pointer cannot be null.");
assertNotNull(outValues, "Output values pointer cannot be null.");
for(uint16_t i = 0; i < set->trackCount; i++) {
outValues[i] = keyframeGetValue(set->tracks[i], set->trackCounts[i], time);
}
}
float_t keyframeSetGetDuration(keyframeset_t *set) {
assertNotNull(set, "Keyframe set pointer cannot be null.");
float_t duration = 0.0f;
for(uint16_t i = 0; i < set->trackCount; i++) {
float_t trackDuration = set->tracks[i][set->trackCounts[i] - 1].time;
if(trackDuration > duration) duration = trackDuration;
}
return duration;
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "keyframe.h"
/**
* A group of N parallel keyframe tracks (e.g. one per animated channel --
* position.x/y/z, bone rotations, etc.) sharing a single timeline. Each
* track is independent: its own keyframe array and count, evaluated at
* whatever time is asked for.
*/
typedef struct {
/** Caller-owned array of trackCount keyframe_t arrays. */
keyframe_t **tracks;
/** Caller-owned array of trackCount counts, one per entry in tracks. */
uint16_t *trackCounts;
uint16_t trackCount;
} keyframeset_t;
/**
* Initializes a keyframe set. Neither tracks, trackCounts, nor the
* keyframe_t arrays they point to are copied -- they must outlive this
* keyframeset_t.
*
* @param set The keyframe set to initialize.
* @param tracks Array of trackCount keyframe_t arrays.
* @param trackCounts Array of trackCount keyframe counts, matching tracks.
* @param trackCount The number of tracks.
*/
void keyframeSetInit(
keyframeset_t *set,
keyframe_t **tracks,
uint16_t *trackCounts,
uint16_t trackCount
);
/**
* Gets the value of a single track at a given time.
*
* @param set The keyframe set to evaluate.
* @param trackIndex The track to evaluate, in [0, set->trackCount).
* @param time The time at which to get the value, in seconds.
* @return The interpolated value of that track at the given time.
*/
float_t keyframeSetGetValue(
keyframeset_t *set,
const uint16_t trackIndex,
const float_t time
);
/**
* Gets the value of every track at a given time.
*
* @param set The keyframe set to evaluate.
* @param time The time at which to get the values, in seconds.
* @param outValues Destination array of at least set->trackCount floats.
*/
void keyframeSetGetValues(
keyframeset_t *set,
const float_t time,
float_t *outValues
);
/**
* Gets the set's duration: the latest final-keyframe time across every
* track, i.e. how long it takes for every track to finish.
*
* @param set The keyframe set to measure.
* @return The set's duration, in seconds.
*/
float_t keyframeSetGetDuration(keyframeset_t *set);
+1
View File
@@ -7,3 +7,4 @@
add_subdirectory(display)
add_subdirectory(physics)
add_subdirectory(trigger)
add_subdirectory(animation)
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
entityanimation.c
)
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityanimation.h"
#include "entity/entitymanager.h"
#include "util/memory.h"
void entityAnimationInit(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
memoryZero(animComp, sizeof(entityanimation_t));
entityUpdateAdd(mgr, entityId, entityAnimationUpdate, componentId, NULL);
}
entityanimation_t *entityAnimationGet(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
return componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_ANIMATION
);
}
void entityAnimationSetKeyframes(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
keyframe_t **channelTracks,
uint16_t *channelTrackCounts,
const uint16_t channelCount
) {
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
animationInit(
&animComp->anim, channelTracks, channelTrackCounts, channelCount
);
}
void entityAnimationPlay(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
animComp->anim.time = 0.0f;
animComp->anim.playing = true;
}
void entityAnimationStop(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
animComp->anim.playing = false;
}
bool_t entityAnimationIsPlaying(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
return animComp->anim.playing;
}
void entityAnimationSetLoop(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const bool_t loop
) {
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
animComp->anim.loop = loop;
}
void entityAnimationSetSpeed(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const float_t speed
) {
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
animComp->anim.speed = speed;
}
float_t entityAnimationGetValue(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const uint16_t channelIndex
) {
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
return animationGetValue(&animComp->anim, channelIndex);
}
void entityAnimationUpdate(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
void *user
) {
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
animationUpdate(&animComp->anim);
}
@@ -0,0 +1,176 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
#include "animation/animation.h"
typedef struct {
animation_t anim;
} entityanimation_t;
/**
* Initializes the animation component: no keyframes set, and registers
* entityAnimationUpdate as an update callback. Call
* entityAnimationSetKeyframes() before entityAnimationPlay().
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
*/
void entityAnimationInit(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Gets the underlying animation structure (temporarily) for the given
* entity. Prefer the dedicated getters/setters where possible.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @return The animation component data for the given entity and
* component ID.
*/
entityanimation_t *entityAnimationGet(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Sets the entity's keyframes, stopped, at speed 1.0, non-looping. See
* keyframeSetInit() -- channelTracks/channelTrackCounts and the
* keyframe_t arrays they point to are not copied, and must outlive this
* component (e.g. static/const arrays owned by the caller).
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param channelTracks Array of channelCount keyframe_t arrays -- one per
* animated channel (e.g. position.x/y/z), sharing this animation's
* timeline.
* @param channelTrackCounts Array of channelCount keyframe counts,
* matching channelTracks.
* @param channelCount The number of channels.
*/
void entityAnimationSetKeyframes(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
keyframe_t **channelTracks,
uint16_t *channelTrackCounts,
const uint16_t channelCount
);
/**
* Starts (or restarts) playback from time 0.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
*/
void entityAnimationPlay(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Stops playback without resetting the current time.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
*/
void entityAnimationStop(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Checks whether the animation is currently playing.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @return True if playing.
*/
bool_t entityAnimationIsPlaying(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Sets whether the animation loops on reaching its final keyframe, rather
* than stopping there.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param loop True to loop, false to stop at the end.
*/
void entityAnimationSetLoop(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const bool_t loop
);
/**
* Sets the animation's playback rate multiplier.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param speed The new playback rate; 1.0 = normal speed.
*/
void entityAnimationSetSpeed(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const float_t speed
);
/**
* Evaluates one of the animation's channels at its current playback
* time, regardless of whether it's currently playing.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param channelIndex The channel to evaluate, in [0, channelCount) as
* passed to entityAnimationSetKeyframes().
* @return That channel's value at the current time.
*/
float_t entityAnimationGetValue(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const uint16_t channelIndex
);
/**
* Per-tick update for the animation component: calls animationUpdate()
* (a no-op if not playing). Registered automatically as an update
* callback by entityAnimationInit.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param user Unused.
*/
void entityAnimationUpdate(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
void *user
);
+6
View File
@@ -10,6 +10,7 @@
#include "entity/component/display/entityrenderable.h"
#include "entity/component/physics/entityphysics.h"
#include "entity/component/trigger/entitytrigger.h"
#include "entity/component/animation/entityanimation.h"
// Name (Uppercase)
// Structure
@@ -31,6 +32,11 @@ X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, NULL, NULL,
entityPhysicsSerialize, entityPhysicsDeserialize)
X(TRIGGER, entitytrigger_t, trigger, entityTriggerInit, NULL, NULL,
entityTriggerSerialize, entityTriggerDeserialize)
// No serialize/deserialize: the animation's keyframes are caller-owned
// (see animationInit()), not data this component owns, so there's
// nothing meaningful to persist to/from JSON.
X(ANIMATION, entityanimation_t, animation, entityAnimationInit, NULL, NULL,
NULL, NULL)
// Game-specific components
#include "entity/gamecomponentlist.h"
@@ -17,7 +17,7 @@
#define ENTITY_PLAYER_MOVE_SPEED_DEFAULT 4.0f
#define ENTITY_PLAYER_JUMP_IMPULSE_DEFAULT 6.0f
#define ENTITY_PLAYER_TURN_SPEED_DEFAULT 10.0f
#define ENTITY_PLAYER_TURN_SPEED_DEFAULT 15.0f
// Below this squared magnitude, movement input is treated as "not moving"
// and the player keeps facing whichever way it last faced, rather than
+1
View File
@@ -3,6 +3,7 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
add_subdirectory(animation)
add_subdirectory(assert)
add_subdirectory(asset)
add_subdirectory(error)
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_keyframe.c)
dusktest(test_keyframeset.c)
dusktest(test_animation.c)
+111
View File
@@ -0,0 +1,111 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "time/time.h"
#include "animation/animation.h"
static void test_animationInitDefaults(void **state) {
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);
assert_float_equal(anim.time, 0.0f, 0.0001f);
assert_float_equal(anim.speed, 1.0f, 0.0001f);
assert_false(anim.loop);
assert_false(anim.playing);
assert_float_equal(animationGetValue(&anim, 0), 0.0f, 0.0001f);
}
static void test_animationUpdateNoopWhenNotPlaying(void **state) {
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);
TIME.delta = 1.0f;
animationUpdate(&anim);
assert_float_equal(anim.time, 0.0f, 0.0001f);
}
static void test_animationUpdateStopsAtEndWhenNotLooping(void **state) {
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;
TIME.delta = 0.5f;
animationUpdate(&anim);
assert_true(anim.playing);
assert_float_equal(animationGetValue(&anim, 0), 5.0f, 0.0001f);
animationUpdate(&anim);
assert_false(anim.playing);
assert_float_equal(animationGetValue(&anim, 0), 10.0f, 0.0001f);
// Stays clamped/held once stopped, even if updated again.
animationUpdate(&anim);
assert_float_equal(anim.time, 1.0f, 0.0001f);
}
static void test_animationUpdateLoopsWithMultipleChannels(void **state) {
keyframe_t xKeyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
};
keyframe_t yKeyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 100.0f, .easing = EASING_LINEAR }
};
keyframe_t *tracks[] = { xKeyframes, yKeyframes };
uint16_t trackCounts[] = { 2, 2 };
animation_t anim;
animationInit(&anim, tracks, trackCounts, 2);
anim.loop = true;
anim.playing = true;
// Duration is the longer (Y) channel's 2s -- looping wraps against
// that, not X's shorter 1s.
TIME.delta = 1.5f;
animationUpdate(&anim);
assert_true(anim.playing);
assert_float_equal(anim.time, 1.5f, 0.0001f);
animationUpdate(&anim);
// 1.5 + 1.5 = 3.0 -> wraps to 1.0 against a 2.0s duration.
assert_true(anim.playing);
assert_float_equal(anim.time, 1.0f, 0.0001f);
}
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),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+66
View File
@@ -0,0 +1,66 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "animation/keyframe.h"
static void test_keyframeGetValueInterpolatesLinear(void **state) {
keyframe_t keyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
};
assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 5.0f, 0.0001f);
}
static void test_keyframeGetValueClampsAtBoundaries(void **state) {
keyframe_t keyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
};
// Exactly on, and beyond, the final keyframe: holds its value rather
// than dividing by zero (start == end at that boundary).
assert_float_equal(keyframeGetValue(keyframes, 2, 1.0f), 10.0f, 0.0001f);
assert_float_equal(keyframeGetValue(keyframes, 2, 5.0f), 10.0f, 0.0001f);
// Exactly on the first keyframe: same boundary case at the start.
assert_float_equal(keyframeGetValue(keyframes, 2, 0.0f), 0.0f, 0.0001f);
}
static void test_keyframeGetValueRespectsEasing(void **state) {
keyframe_t keyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_IN_QUAD },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_IN_QUAD }
};
// easingInQuad(0.5) = 0.25 -> lerp(0, 10, 0.25) = 2.5, not the linear
// midpoint (5.0).
assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 2.5f, 0.0001f);
}
static void test_keyframeGetValueWithThreeKeyframes(void **state) {
keyframe_t keyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 0.0f, .easing = EASING_LINEAR }
};
assert_float_equal(keyframeGetValue(keyframes, 3, 1.0f), 10.0f, 0.0001f);
assert_float_equal(keyframeGetValue(keyframes, 3, 1.5f), 5.0f, 0.0001f);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_keyframeGetValueInterpolatesLinear),
cmocka_unit_test(test_keyframeGetValueClampsAtBoundaries),
cmocka_unit_test(test_keyframeGetValueRespectsEasing),
cmocka_unit_test(test_keyframeGetValueWithThreeKeyframes),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+77
View File
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "animation/keyframeset.h"
static void test_keyframeSetGetValuePerTrack(void **state) {
keyframe_t xKeyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
};
keyframe_t yKeyframes[] = {
{ .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 200.0f, .easing = EASING_LINEAR }
};
keyframe_t *tracks[] = { xKeyframes, yKeyframes };
uint16_t trackCounts[] = { 2, 2 };
keyframeset_t set;
keyframeSetInit(&set, tracks, trackCounts, 2);
assert_float_equal(keyframeSetGetValue(&set, 0, 0.5f), 5.0f, 0.0001f);
assert_float_equal(keyframeSetGetValue(&set, 1, 0.5f), 150.0f, 0.0001f);
}
static void test_keyframeSetGetValuesFillsAllTracks(void **state) {
keyframe_t xKeyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
};
keyframe_t yKeyframes[] = {
{ .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 200.0f, .easing = EASING_LINEAR }
};
keyframe_t *tracks[] = { xKeyframes, yKeyframes };
uint16_t trackCounts[] = { 2, 2 };
keyframeset_t set;
keyframeSetInit(&set, tracks, trackCounts, 2);
float_t values[2];
keyframeSetGetValues(&set, 0.5f, values);
assert_float_equal(values[0], 5.0f, 0.0001f);
assert_float_equal(values[1], 150.0f, 0.0001f);
}
static void test_keyframeSetGetDurationIsLongestTrack(void **state) {
keyframe_t shortKeyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
};
keyframe_t longKeyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 3.5f, .value = 10.0f, .easing = EASING_LINEAR }
};
keyframe_t *tracks[] = { shortKeyframes, longKeyframes };
uint16_t trackCounts[] = { 2, 2 };
keyframeset_t set;
keyframeSetInit(&set, tracks, trackCounts, 2);
assert_float_equal(keyframeSetGetDuration(&set), 3.5f, 0.0001f);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_keyframeSetGetValuePerTrack),
cmocka_unit_test(test_keyframeSetGetValuesFillsAllTracks),
cmocka_unit_test(test_keyframeSetGetDurationIsLongestTrack),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+1
View File
@@ -9,3 +9,4 @@ include(dusktest)
dusktest(test_entitymanager.c)
dusktest(test_entityposition.c)
dusktest(test_entitytrigger.c)
dusktest(test_entityanimation.c)
+209
View File
@@ -0,0 +1,209 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "time/time.h"
#include "entity/entitymanager.h"
#include "entity/component/animation/entityanimation.h"
static void test_entityAnimationSetKeyframesDefaults(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t entity = entityManagerAdd(&mgr);
componentid_t anim = entityAddComponent(
&mgr, entity, COMPONENT_TYPE_ANIMATION
);
static keyframe_t keyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
};
keyframe_t *channels[] = { keyframes };
uint16_t channelCounts[] = { 2 };
entityAnimationSetKeyframes(&mgr, entity, anim, channels, channelCounts, 1);
assert_false(entityAnimationIsPlaying(&mgr, entity, anim));
assert_float_equal(
entityAnimationGetValue(&mgr, entity, anim, 0), 0.0f, 0.0001f
);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityAnimationPlayAdvancesAndStopsNonLooping(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t entity = entityManagerAdd(&mgr);
componentid_t anim = entityAddComponent(
&mgr, entity, COMPONENT_TYPE_ANIMATION
);
static keyframe_t keyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
};
keyframe_t *channels[] = { keyframes };
uint16_t channelCounts[] = { 2 };
entityAnimationSetKeyframes(&mgr, entity, anim, channels, channelCounts, 1);
entityAnimationPlay(&mgr, entity, anim);
assert_true(entityAnimationIsPlaying(&mgr, entity, anim));
TIME.delta = 0.5f;
entityUpdate(&mgr, entity);
assert_true(entityAnimationIsPlaying(&mgr, entity, anim));
assert_float_equal(
entityAnimationGetValue(&mgr, entity, anim, 0), 5.0f, 0.0001f
);
// Reaches the final keyframe exactly: stops, holds the final value
// (rather than dividing by zero evaluating exactly at the last
// keyframe -- see keyframeGetValue()).
entityUpdate(&mgr, entity);
assert_false(entityAnimationIsPlaying(&mgr, entity, anim));
assert_float_equal(
entityAnimationGetValue(&mgr, entity, anim, 0), 10.0f, 0.0001f
);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityAnimationLoopsAndWrapsTime(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t entity = entityManagerAdd(&mgr);
componentid_t anim = entityAddComponent(
&mgr, entity, COMPONENT_TYPE_ANIMATION
);
static keyframe_t keyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
};
keyframe_t *channels[] = { keyframes };
uint16_t channelCounts[] = { 2 };
entityAnimationSetKeyframes(&mgr, entity, anim, channels, channelCounts, 1);
entityAnimationSetLoop(&mgr, entity, anim, true);
entityAnimationPlay(&mgr, entity, anim);
TIME.delta = 0.7f;
entityUpdate(&mgr, entity);
assert_true(entityAnimationIsPlaying(&mgr, entity, anim));
assert_float_equal(
entityAnimationGetValue(&mgr, entity, anim, 0), 7.0f, 0.0001f
);
// Wraps back around instead of stopping: 0.7 + 0.7 = 1.4 -> 0.4.
entityUpdate(&mgr, entity);
assert_true(entityAnimationIsPlaying(&mgr, entity, anim));
assert_float_equal(
entityAnimationGetValue(&mgr, entity, anim, 0), 4.0f, 0.0001f
);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityAnimationStopAndSpeed(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t entity = entityManagerAdd(&mgr);
componentid_t anim = entityAddComponent(
&mgr, entity, COMPONENT_TYPE_ANIMATION
);
static keyframe_t keyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
};
keyframe_t *channels[] = { keyframes };
uint16_t channelCounts[] = { 2 };
entityAnimationSetKeyframes(&mgr, entity, anim, channels, channelCounts, 1);
entityAnimationSetSpeed(&mgr, entity, anim, 2.0f);
entityAnimationPlay(&mgr, entity, anim);
TIME.delta = 0.25f;
entityUpdate(&mgr, entity);
// 0.25s of real time at 2x speed = 0.5 track-seconds -> halfway.
assert_float_equal(
entityAnimationGetValue(&mgr, entity, anim, 0), 5.0f, 0.0001f
);
entityAnimationStop(&mgr, entity, anim);
assert_false(entityAnimationIsPlaying(&mgr, entity, anim));
// Stopping doesn't reset time -- value is held where it was.
assert_float_equal(
entityAnimationGetValue(&mgr, entity, anim, 0), 5.0f, 0.0001f
);
entityUpdate(&mgr, entity);
assert_float_equal(
entityAnimationGetValue(&mgr, entity, anim, 0), 5.0f, 0.0001f
);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityAnimationMultipleChannels(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t entity = entityManagerAdd(&mgr);
componentid_t anim = entityAddComponent(
&mgr, entity, COMPONENT_TYPE_ANIMATION
);
// X goes 0 -> 10 over 1s; Y goes 0 -> 100 over 2s -- different lengths,
// sharing one timeline.
static keyframe_t xKeyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }
};
static keyframe_t yKeyframes[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
{ .time = 2.0f, .value = 100.0f, .easing = EASING_LINEAR }
};
keyframe_t *channels[] = { xKeyframes, yKeyframes };
uint16_t channelCounts[] = { 2, 2 };
entityAnimationSetKeyframes(&mgr, entity, anim, channels, channelCounts, 2);
entityAnimationPlay(&mgr, entity, anim);
TIME.delta = 1.0f;
entityUpdate(&mgr, entity);
// Still playing: the set's duration is the longer (Y's) 2s.
assert_true(entityAnimationIsPlaying(&mgr, entity, anim));
// X reached/passed its own end -- held at its final value.
assert_float_equal(
entityAnimationGetValue(&mgr, entity, anim, 0), 10.0f, 0.0001f
);
// Y is halfway through its own 2s span.
assert_float_equal(
entityAnimationGetValue(&mgr, entity, anim, 1), 50.0f, 0.0001f
);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityAnimationSetKeyframesDefaults),
cmocka_unit_test(test_entityAnimationPlayAdvancesAndStopsNonLooping),
cmocka_unit_test(test_entityAnimationLoopsAndWrapsTime),
cmocka_unit_test(test_entityAnimationStopAndSpeed),
cmocka_unit_test(test_entityAnimationMultipleChannels),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}