pixijs / spine

Pixi.js plugin that enables Spine support.
Other
564 stars 217 forks source link

Let animation moves along a path? #188

Open lirhtc opened 7 years ago

lirhtc commented 7 years ago

Hi, I am having problems with moving an animation along a path. I want to make an animation series, for example, move a monster close to an actor, attack the actor and then cast a skill. Finally, the monster should go back to its original position. The animation series could be achieved by using state.addAnimation so it will play animation one by one. But I have a problem with the moving part. I tried to change .x and .y of a spine object and the moving will start together with animation. So the monster is moving while attacking and casting. I also tried to have an update function and a callback function for the moving but the logic becomes really complex when an animation series contains more than one moving action.

So is there a better solution to move the animation? Or another suggestion to make an animation series like (moving, animation1, animation2, moving, animation3.....)

ivanpopelyshev commented 7 years ago

There's no synchronization between spine animation and pixi coordinates change. That's not a pixi problem, there's no that kind of synchronization for other engines.

May be you can just hack the way spine object is updated and check if there's specific animation in specific track. https://github.com/pixijs/pixi-spine/blob/master/src/Spine.ts#L186

You can change specific bone in spine animation and then look at that bone in pixi, modyfying something on pixi side depending on bone position.

You can also try to use GSAP/tweenlight with pixi if you really need tweens.

Whatever solution you find, please post it here, if its good enough I'll add special hacks for other people to use.

ivanpopelyshev commented 7 years ago

I just remembered that one of possible solutions is to attach model to another model, but pixi-spine does not support it yet. If you ask how to do that for spine-ts on esotericsoftware forums or in their issues (https://github.com/EsotericSoftware/spine-runtimes/issues), and get answer from them, then I can impolement the same thing in pixi-spine.

lirhtc commented 7 years ago

I manage a way to achieve this but it might be not the best solution.

I added the following to AnimationState class:

AnimationState.stepMoveMode = false;
AnimationState.stepMoveTime = 0;
AnimationState.stepMoves= [];
AnimationState.prototype.cleanSteps = function () {
    if (this.stepMoves.length > 0) {
        var i = 0;
        var num = this.stepMoves.length;
        while (i < num) {
            var step = this.stepMoves[i];
            if (step.stepMoveCount == 0 && step.animations.length == 0) {
                this.stepMoves.splice(i, 1);
                num--;
            } else {
                i++;
            }
        }
    }
};

And change the existed functions to

AnimationState.prototype.addAnimation = function (trackIndex, animationName, loop, delay, stepMoveFlag) {
    var animation = this.data.skeletonData.findAnimation(animationName);
    if (animation == null)
        throw new Error("Animation not found: " + animationName);
    if (stepMoveFlag == undefined) {
        stepMoveFlag = 1
    };
    return this.addAnimationWith(trackIndex, animation, loop, delay, stepMoveFlag);
};
AnimationState.prototype.addAnimationWith = function (trackIndex, animation, loop, delay, stepMoveFlag) {
    if (animation == null)
        throw new Error("animation cannot be null.");
    /* Flag:
     *     0:  Animation will be added in to animation queue
     *         Duration is counted
     *     1:  Default value. The animation will be added in to <step> cache wait for step move finished
     *         Duration is counted
     *     2:  The animation will be added in to <step> cache wait for step move finished
     *         Duration is not counted
     */
    if (this.stepMoveMode && stepMoveFlag < 3) {
        if (stepMoveFlag == 0) {
            this.stepMoveTime += animation.duration;
        } else {
            this.cleanSteps();
            if (stepMoveFlag == 1) {
                this.stepMoveTime += animation.duration;
            }
            if (this.stepMoves.length > 0) {
                var lastStep = this.stepMoves[this.stepMoves.length - 1];
                lastStep.animations.push({
                    "name": animation.name,
                    "loop": loop,
                    "delay": delay,
                    "trackIndex": trackIndex
                });
                return "animation add to cache " + animation.name;
            } else {
                this.addAnimationWith(trackIndex, animation, loop, delay, 0);
                return "animation add to cache " + animation.name;
            }
        }
    }
};
AnimationState.prototype.setAnimation = function (trackIndex, animationName, loop, flag) {
    var animation = this.data.skeletonData.findAnimation(animationName);
    if (flag == undefined) {
        flag = 1;
    }
    if (animation == null)
        throw new Error("Animation not found: " + animationName);
    return this.setAnimationWith(trackIndex, animation, loop, flag);
};
AnimationState.prototype.setAnimationWith = function (trackIndex, animation, loop, flag) {
    if (animation == null)
        throw new Error("animation cannot be null.");
    if (this.stepMoveMode && flag == 1) {
        this.cleanSteps();
        this.stepMoveTimes += animation.duration;
        if (this.stepMoves.length > 0) {
            this.stepMoves[this.stepMoves.length - 1].setAnimation = {
                "trackIndex": trackIndex,
                "animation": animation.name,
                "loop": loop
            }
            return "add setAnimation to cache";
        }
        this.setAnimationWith(trackIndex, animation, loop, 0);
        return "directly added";
    }
    var interrupt = true;
    var current = this.expandToIndex(trackIndex);
    if (current != null) {
        if (current.nextTrackLast == -1) {
            this.tracks[trackIndex] = current.mixingFrom;
            this.queue.interrupt(current);
            this.queue.end(current);
            this.disposeNext(current);
            current = current.mixingFrom;
            interrupt = false;
        } else
            this.disposeNext(current);
    }
    var entry = this.trackEntry(trackIndex, animation, loop, current);
    this.setCurrent(trackIndex, entry, interrupt);
    this.queue.drain();
    return entry;
};

Also two new functions in Spine class:

Spine.prototype.moveTo = function (x, y, speed, flag) {
    speed = speed || 60;
    var step = {
        "targetPosition": {
            "x": x,
            "y": y
        },
        "delay": (flag == 1) ? 0 : this.state.stepMoveTime,
        "stepMoveCount": speed,
        "setAnimation": null,
        "animations": []
    };
    this.state.stepMoveTime += speed / 60;
    this.state.stepMoves.push(step);
};

Spine.prototype.updateStepMove = function (dt) {
    if (this.state.stepMoveMode) {
        this.state.stepMoveTime -= dt;
        if (this.state.stepMoveTime < 0) {
            this.state.stepMoveTime = 0;
        }
        this.state.stepMoves.forEach(function (step) {
            if (step.delay > 0) {
                step.delay -= dt;
            }
            if (step.delay <= 0 && step.stepMoveCount > 0) {
                var deltaX = (step.targetPosition.x - this.x) / step.stepMoveCount;
                var deltaY = (step.targetPosition.y - this.y) / step.stepMoveCount;
                this.x += deltaX;
                this.y += deltaY;
                step.stepMoveCount--;
                if (step.stepMoveCount == 0 && step.setAnimation != null) {
                    var ani = step.setAnimation;
                    this.state.setAnimation(ani.trackIndex, ani.animation, ani.loop, 0);
                    step.setAnimation = null;
                }
                if (step.stepMoveCount == 0 && step.animations.length > 0) {
                    step.animations.forEach(function (ani) {
                        this.state.addAnimation(ani.trackIndex, ani.name, ani.loop, ani.delay, 0);
                    }
                        .bind(this));
                    step.animations = [];
                }
            }
        }
            .bind(this));
    }
};

And finally, the one more in spine.update function:

Spine.prototype.update = function (dt) {
    this.updateStepMove(dt);
.....
};

By doing this. The second timeline for coordinate changes is maintained by spine.update function. The changes to animation (.setAnimation and .addAnimation) will be sent to cache, waiting for the coordinate to be finished. This could be controlled by using a different flag. I can provide a small demo of how to using this. But I don't know how to put it online.