Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/alternative animation queuing mechanism #16

Open
wants to merge 2 commits into
base: custom_animation_engine
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions SpineImporter/SGG_Spine.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ typedef enum {
-(SGG_SpineBone*)findBoneNamed:(NSString*)boneName;


// these methods enqueue given animation(s) to be run after the current animation (if any) completes its run. any call to enqueue does not stop the existing animation
// if an animation is scheduled to run indefinitely, and another animation is enqueued; the indefinite animation continues to run after the new animation
-(void)enqueueAnimation:(NSString*)animationName;
-(void)enqueueAnimations:(NSArray<NSString*>*)animationNames;
-(void)enqueueIndefiniteAnimation:(NSString*)animationName;
-(void)enqueueAnimation:(NSString*)animationName forNumberOfRuns:(NSInteger)numberOfRuns;
-(void)cancelAllInstancesOfEnqueuedAnimationNamed:(NSString*)animationName;


@end
141 changes: 140 additions & 1 deletion SpineImporter/SGG_Spine.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,23 @@ @interface SGG_Spine () {
NSInteger repeatAnimationCount;

NSString* previousAnimation;

}

@property (strong, nonatomic) NSMutableArray<NSDictionary*> *animationQueue;

@end

@implementation SGG_Spine

-(NSArray<NSDictionary*> *)animationQueue
{
if (!_animationQueue) {
_animationQueue = [NSMutableArray<NSDictionary*> array];
}

return _animationQueue;
}

-(id)init {

if (self = [super init]) {
Expand Down Expand Up @@ -72,6 +82,10 @@ -(void)skeletonFromFileNamed:(NSString*)name andAtlasNamed:(NSString*)atlasName

#pragma mark PLAYBACK CONTROLS

-(void)runAnimation:(NSString*)animationName {
[self runAnimation:animationName andCount:1];
}

-(void)runAnimation:(NSString*)animationName andCount:(NSInteger)count {


Expand Down Expand Up @@ -334,6 +348,14 @@ -(void)jumpToPreviousFrame {
}

-(void)endOfAnimation {
NSString *topOfAnimationQueue = [self dequeueNextAnimation];
if (topOfAnimationQueue) {
// If we have ant animation enqueued, run them and return without running legacy code
[self runAnimation:topOfAnimationQueue];
return;
}


if ([_currentAnimation isEqualToString:@"INTRO_ANIMATION"]) { //clear out intro animation after it's been used
if (self.debugMode) {
NSLog(@"finished intro");
Expand Down Expand Up @@ -1211,6 +1233,123 @@ -(void)setPlaybackSpeed:(CGFloat)playbackSpeed {

}

#pragma mark - Animation enqueing
-(void)pushAnimationDictionaryToQueue:(NSDictionary*)dict
{
@synchronized (self) {
NSString *name = dict[@"name"];
NSInteger indefiniteAnimation = [dict[@"indefinitely"] boolValue];

// if the one and only waiting animation is indefinite animation, it can be there because of its rescheduling.
// lets not wait another turn of its
for (int i=0; i<self.animationQueue.count; i++) {
NSDictionary *d = self.animationQueue[0];
if ([d[@"indefinitely"] boolValue]) {
// insert it in front of the indefinite animation
[self.animationQueue insertObject:dict atIndex:i];

// it is pointless to have another indefinite animation waiting in the queue
// just log a warning
if (indefiniteAnimation)
NSLog(@"WARN: have two animations ('%@' and '%@') in the queue for indefinite run", name, d[@"name"]);

return;
}
}

[self.animationQueue addObject:dict];
}
}

-(NSDictionary*)popAnimationDictionaryFromQueue
{
NSDictionary *dict = nil;

@synchronized (self) {
if (self.animationQueue.count) {
dict = self.animationQueue[0];
[self.animationQueue removeObjectAtIndex:0];

NSString *name = dict[@"name"];
NSInteger indefiniteAnimation = [dict[@"indefinitely"] boolValue];

// if this animation was scheduled to run indefinetely, add back to the queue.
if (indefiniteAnimation) {
// but if there is another animation is scheduled to run indefinetely, let it run instead of us.
BOOL hasAnotherIndefiniteScheduleAnimation = NO;
for (NSDictionary *dict in self.animationQueue) {
if ([dict[@"indefinitely"] boolValue]) {
hasAnotherIndefiniteScheduleAnimation = YES;
break;
}
}

if (!hasAnotherIndefiniteScheduleAnimation) {
// we are adding to the end, so that other animations can be inserted between our indefinite run
[self.animationQueue addObject:dict];
} else {
NSLog(@"WARN: We already have another indefinite animation. Ignoring '%@'", name);
}
}
}
}

return dict;
}


-(void)enqueueAnimation:(NSString*)animationName
{
[self enqueueAnimation:animationName forNumberOfRuns:1];
}

-(void)enqueueAnimations:(NSArray<NSString*>*)animationNames
{
for (NSString *animationName in animationNames)
[self enqueueAnimation:animationName];
}

-(void)enqueueIndefiniteAnimation:(NSString*)animationName
{
[self enqueueAnimation:animationName forNumberOfRuns:-1];
}


-(void)enqueueAnimation:(NSString*)animationName forNumberOfRuns:(NSInteger)numberOfRuns
{
BOOL indefiniteAnimation = (numberOfRuns < 0);
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:animationName, @"name", (indefiniteAnimation ? @YES : @NO), @"indefinitely", nil];

for (int i=0; i<(indefiniteAnimation ? 1 : numberOfRuns); i++)
[self pushAnimationDictionaryToQueue:dict];

// TODO: there is a risk of race case here, as the check for runningAnimation & starting one is not synchronized for parallel threads
if (![self isRunningAnimation])
[self runAnimation:[self dequeueNextAnimation]];
}

-(NSString *)dequeueNextAnimation
{
return [self popAnimationDictionaryFromQueue][@"name"];
}

-(void)cancelAllInstancesOfEnqueuedAnimationNamed:(NSString*)animationName
{
@synchronized (self) {
NSMutableIndexSet *discardedItems = [NSMutableIndexSet indexSet];
NSUInteger index = 0;

for (NSDictionary *dict in self.animationQueue) {
if ([dict[@"name"] isEqualToString:animationName])
[discardedItems addIndex:index];
index++;
}

[self.animationQueue removeObjectsAtIndexes:discardedItems];
}
}




@end
77 changes: 33 additions & 44 deletions SpineTesting/SGG_MyScene.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,34 @@ @implementation SGG_MyScene {

}

-(id)initWithSize:(CGSize)size {
static const BOOL tryAlternativeQueuingMethods = YES;

-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
/* Setup your scene here */


boy = [SGG_Spine node];
// boy.debugMode = YES;
// boy.timeResolution = 1.0 / 1200.0; // this is typically overkill, 1/120 will normally be MORE than enough, but this demo can go to some VERY slow motion. 1/120 is also the default.
[boy skeletonFromFileNamed:@"spineboy" andAtlasNamed:@"spineboy" andUseSkinNamed:Nil];
boy.position = CGPointMake(self.size.width/4, self.size.height/4);
// [boy runAnimationSequence:@[@"walk", @"jump", @"walk", @"walk", @"jump"] andUseQueue:NO]; //uncomment to see how a sequence works (commment the other animation calls)
boy.queuedAnimation = @"walk";
boy.name = @"boy";
boy.queueIntro = 0.1;
[boy runAnimation:@"walk" andCount:0 withIntroPeriodOf:0.1 andUseQueue:YES];

boy = [SGG_Spine node];
[boy skeletonFromFileNamed:@"spineboy" andAtlasNamed:@"spineboy" andUseSkinNamed:Nil];
boy.position = CGPointMake(self.size.width/4, self.size.height/4);
if (tryAlternativeQueuingMethods) {
[boy enqueueIndefiniteAnimation:@"walk"]; // walk indefinitely

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[boy enqueueAnimation:@"jump"]; // jump once, and then continue walking
});

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[boy enqueueAnimations:@[@"jump", @"walk", @"jump", @"jump"]]; // try a sequence of animations, and then continue walking
});
} else {
// boy.debugMode = YES;
// boy.timeResolution = 1.0 / 1200.0; // this is typically overkill, 1/120 will normally be MORE than enough, but this demo can go to some VERY slow motion. 1/120 is also the default.
// [boy runAnimationSequence:@[@"walk", @"jump", @"walk", @"walk", @"jump"] andUseQueue:NO]; //uncomment to see how a sequence works (commment the other animation calls)
boy.queuedAnimation = @"walk";
boy.name = @"boy";
boy.queueIntro = 0.1;
[boy runAnimation:@"walk" andCount:0 withIntroPeriodOf:0.1 andUseQueue:YES];
}
boy.zPosition = 0;
[self addChild:boy];

Expand Down Expand Up @@ -151,9 +164,13 @@ -(void)keyDown:(NSEvent *)theEvent {
unichar character = [characters characterAtIndex:s];
switch (character) {
case ' ':{
if (![boy.currentAnimation isEqualToString:@"jump"]) {
[boy runAnimation:@"jump" andCount:0 withIntroPeriodOf:0.1 andUseQueue:YES];
}
if (tryAlternativeQueuingMethods) {
[boy enqueueAnimation:@"jump"]; // jump once, and then continue walking
} else {
if (![boy.currentAnimation isEqualToString:@"jump"]) {
[boy runAnimation:@"jump" andCount:0 withIntroPeriodOf:0.1 andUseQueue:YES];
}
}

}
break;
Expand Down Expand Up @@ -263,32 +280,4 @@ -(void)update:(CFTimeInterval)currentTime {
}



//NSValue testing
/*
CGFloat xa = arc4random() % 100;
CGFloat xb = arc4random() % 100;
CGFloat ya = arc4random() % 100;
CGFloat yb = arc4random() % 100;

xb = xb / 100;
yb = yb / 100;

CGFloat x = xa + xb;
CGFloat y = ya + yb;

bool xc = arc4random() % 2;
bool yc = arc4random() % 2;

if (xc) {
x *= -1;
}
if (yc) {
y *= -1;
}

[[[SGG_SpineBoneAction alloc] init] addTranslationAtTime:0 withPoint:CGPointMake(x, y) andCurveInfo:@[@"1.2, 2.1"]];*/



@end