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