Animation Helper

Discussion in 'Scripting & Mechanics' started by Dan, Jun 20, 2012.

  1. Dan Member

    When working with animations I noticed I was writing a few variables to track which animation was playing, if an animation was playing at all and the a count of (roughly) how long the animation was playing.

    So I thought I'd encapsulate it in a metatable. Figured it could be useful for others. Put this in a script file in your project. It does not have to be included on a gameobject.

    This is also on the Community Hub: /open 21541

    The Code (Example below):
    Code:
    --------------------------------------------------------------------
    --  AnimationSet class (meta-table)                              --
    --  By: AnnoyedGuy                                                --
    --Note: Use this however you want! My only request, is please add --
    --      improvements/fixes to the Community Hub. Also, saying    --
    --      thanks might be nice.                                    --
    --------------------------------------------------------------------
    --HowTo: To use this, create a variable on your Behavior:Awake.  --
    --      like:                                                    --
    --self.animations = AnimationSet:new(self.gameObject:GetComponent("ModelRenderer")) --
    --      to add animations you fill out the args:                --
    --self.animations:Add(friendlyName, assetPath, start_frame, end_frame, loop)        --
    --      then when you want to play an animation:                --
    --self.animations:Play("friendlyName")                            --
    --      you can then check if the animation is still playing:    --
    --self.animations:IsPlaying()                                    --
    --------------------------------------------------------------------
     
    AnimationSet = { }
    AnimationSet_mt = { __index = AnimationSet }
     
    function AnimationSet:new(a_model)
        return setmetatable( { model = a_model, current="none", timer=0, anims = {} }, AnimationSet_mt)
    end
     
    function AnimationSet:Add(friendlyName, assetPath, start_frame, end_frame, loop)
        -- Prep the table
        self.anims[friendlyName] = { }
        -- Get the asset table and check if it exists. Pass off the error to the designer.
        local asset = CraftStudio.FindAsset(assetPath)
        if (asset == nil) then print("ERROR: Animation does not exist: "..assetPath) return end
        -- Store variables needed
        self.anims[friendlyName].asset = asset
        self.anims[friendlyName].startFrame = start_frame
        self.anims[friendlyName].endFrame = end_frame
        -- Multiply by 2 as an approximation of frames per sec, pending real animation timing from CS
        self.anims[friendlyName].length = (end_frame-start_frame)*2
        self.anims[friendlyName].looping = loop
        self.anims[friendlyName].isPlaying = false
    end
     
    function AnimationSet:IsPlaying()
        if (self.current) then return self.anims[self.current].isPlaying
        else return false
        end
    end
     
    function AnimationSet:Play(name)
        -- Check if it's already playing the animation, avoid restarting at the first frame
        if (self.current == name) then return end
        self.current = name
        -- Check on animation existence
        if (self.anims[self.current] == nil) then
            print("ERROR: Animation is not added: '"..self.current.."' use :Add(friendlyName, assetPath, start_time, end_time, loop[optional])")
            return
        end
        -- Start up animation and when to start
        self.model:SetAnimation(self.anims[self.current].asset)
        self.model:SetAnimationTime(self.anims[self.current].startFrame)
        -- If this is not a looping animation...
        self.model:StartAnimationPlayback(self.anims[self.current].looping)
        if (not self.anims[self.current].looping) then
            -- Set the timer
            self.timer = self.anims[self.current].length
            if (self.timer == 0) then
                -- zero length, stay at the same frame
                self.model:StopAnimationPlayback()
                self.model:SetAnimationTime(self.anims[self.current].startFrame)
            end
        else
            self.timer = self.anims[self.current].length
        end
        -- This animation is now playing
        self.anims[self.current].isPlaying = true
    end
     
    function AnimationSet:Update()
        if (self.timer > 0) then
            self.timer = self.timer - 1
            if (self.timer == 0) then
                self.model:StopAnimationPlayback()
                self.anims[self.current].isPlaying = false
            end
        end
    end
    
    Example of usage:
    Throw this onto a gameobject with model that can animation.
    Code:
    function Behavior:Awake()
        self.animations = AnimationSet:new(self.gameObject:GetComponent( "ModelRenderer" ))
        --  Arguments are: name, assetName, start frame, end frame, looping
        -- Idle is non looping and stays on 0
        self.animations:Add("Idle","Creatures/NPCs/idle", 0, 0, false)
        -- Attack is non-looping and is a one shot of 30 frames
        self.animations:Add("Attack1","Creatures/NPCs/strike1",0,30,false)
        -- Starting animation
        self.animations:Play("Idle")
    end
     
    function Behavior:Update()
        -- If animation is idle
        if self.animations.current == "Idle" then
            if CraftStudio.Input.WasButtonJustPressed("attack1") then
                print("Attack")
                self.animations:Play("Attack1")
            end
        -- Otherwise if it's an attack
        elseif self.animations.current == "Attack1" then
            -- If it finished playing
            if not self.animations:IsPlaying() then
                -- Go back to idle
                self.animations:Play("Idle")
            end
        end
        -- Do update for timed animations
        self.animations:Update()
    end
    
    htdreams, elisee and micahleevincent like this.
  2. XTender Moderator

    Nice thing :)
  3. armag3ddon New Member

    Yep, very nice :)

    AnimationSet:Update() doesn't seem to work proper with looped animations. I would expect that an object calls this function regardless of its state and the animation playing. But with your default, even looping animations stop after one cycle because the timer runs down.

    I modified it like this, the timer still runs down and then starts again. What I'm not sure about is whether the restarting should happens in the next call of :Update() or if this is right. Anyway, I suggest altering :Update() in any way that won't make looped animations stop.

    Code:
    function AnimationSet:Update()
        if (self.timer > 0) then
            self.timer = self.timer - 1
            if (self.timer == 0 and not self.anims[self.current].looping) then
                self.model:StopAnimationPlayback()
                self.anims[self.current].isPlaying = false
            elseif (self.timer == 0) then
                self.timer = self.anims[self.current].length
            end
        end
    end
  4. micahleevincent Funder !

    Dan doesn't check the forums regularly, but you are correct about this. We noticed this bug a few days ago.

    :] good eye!
    armag3ddon likes this.
  5. Dan Member

    It's true, I'm a horrible forum go-er. Thanks for the correction! :)
  6. Darkhog Active Member

    This soooo should go into official API...
    micahleevincent likes this.
  7. MiniMatt Active Member

    Im having some issues changing the code, I wanted to make it so if W key is down the animation will play and when I let go of it then it would go straight to the idle animation and not finish up the rest of it. Here is my code...

    Code:
    --------------------------------------------------------------------
    --HowTo: To use this, create a variable on your Behavior:Awake.  --
    --      like:                                                    --
    --self.animations = AnimationSet:new(self.gameObject:GetComponent("ModelRenderer")) --
    --      to add animations you fill out the args:                --
    --self.animations:Add(friendlyName, assetPath, start_frame, end_frame, loop)        --
    --      then when you want to play an animation:                --
    --self.animations:Play("friendlyName")                            --
    --      you can then check if the animation is still playing:    --
    --self.animations:IsPlaying()                                    --
    --------------------------------------------------------------------
    local walking = false;
    local moveSpeed = 0.2;
     
    AnimationSet = { }
    AnimationSet_mt = { __index = AnimationSet }
     
    function AnimationSet:new(a_model)
        return setmetatable( { model = a_model, current="none", timer=0, anims = {} }, AnimationSet_mt)
    end
     
    function AnimationSet:Add(friendlyName, assetPath, start_frame, end_frame, loop)
        -- Prep the table
        self.anims[friendlyName] = { }
        -- Get the asset table and check if it exists. Pass off the error to the designer.
        local asset = CraftStudio.FindAsset(assetPath)
        if (asset == nil) then print("ERROR: Animation does not exist: "..assetPath) return end
        -- Store variables needed
        self.anims[friendlyName].asset = asset
        self.anims[friendlyName].startFrame = start_frame
        self.anims[friendlyName].endFrame = end_frame
        -- Multiply by 2 as an approximation of frames per sec, pending real animation timing from CS
        self.anims[friendlyName].length = (end_frame-start_frame)*2
        self.anims[friendlyName].looping = loop
        self.anims[friendlyName].isPlaying = false
    end
     
    function AnimationSet:IsPlaying()
        if (self.current) then return self.anims[self.current].isPlaying
        else return false
        end
    end
     
    function AnimationSet:Play(name)
        -- Check if it's already playing the animation, avoid restarting at the first frame
        if (self.current == name) then return end
        self.current = name
        -- Check on animation existence
        if (self.anims[self.current] == nil) then
            print("ERROR: Animation is not added: '"..self.current.."' use :Add(friendlyName, assetPath, start_time, end_time, loop[optional])")
            return
        end
        -- Start up animation and when to start
        self.model:SetAnimation(self.anims[self.current].asset)
        self.model:SetAnimationTime(self.anims[self.current].startFrame)
        -- If this is not a looping animation...
        self.model:StartAnimationPlayback(self.anims[self.current].looping)
        if (not self.anims[self.current].looping) then
            -- Set the timer
            self.timer = self.anims[self.current].length
            if (self.timer == 0) then
                -- zero length, stay at the same frame
                self.model:StopAnimationPlayback()
                self.model:SetAnimationTime(self.anims[self.current].startFrame)
            end
        else
            self.timer = self.anims[self.current].length
        end
        -- This animation is now playing
        self.anims[self.current].isPlaying = true
    end
     
    function AnimationSet:Update()
        if (self.timer > 0) then
            self.timer = self.timer - 1
            if (self.timer == 0) then
                self.model:StopAnimationPlayback()
                self.anims[self.current].isPlaying = false
            end
        end
    end
     
    function Behavior:Awake()
      self.animations = AnimationSet:new(self.gameObject:GetComponent( "ModelRenderer" ))
      --  Arguments are: name, assetName, start frame, end frame, looping
      -- Idle is non looping and stays on 0
      self.animations:Add("Idle","Idle",0,0,true)
      -- Attack is non-looping and is a one shot of 30 frames
      self.animations:Add("Punch","Punch",0,10,false)
      -- Starting animation
      self.animations:Add("Walk","Walk",0,40,false)
      -- Starting animation
      self.animations:Play("Idle")
    end
     
    function Behavior:Update()
        -- If animation is idle
        if self.animations.current == "Idle" then
            if CraftStudio.Input.WasButtonJustPressed("Fire") then
                print("Attack")
                self.animations:Play("Punch")
            end
        -- Otherwise if it's an attack
        elseif self.animations.current == "Punch" then
            -- If it finished playing
            if not self.animations:IsPlaying() then
                -- Go back to idle
                self.animations:Play("Idle")
            end
        end
       
        if self.animations.current == "Idle" then
            if CraftStudio.Input.IsButtonDown("W") then
                print("W")
                self.animations:Play("Walk")
              end
             
        elseif self.animations.current == "Walk" then
            if not self.animations:IsPlaying() then
                self.animations:Play("Idle")
            end
        end
        -- Do update for timed animations
        self.animations:Update()
    en
  8. Dan Member

    Well, just in case... you probably want Walk to be a looping animation?
    Code:
    self.animations:Add("Walk","Walk",0,40,true)
    To answer your question, you might want to change this:
    Code:
    if self.animations.current == "Idle" then
            if CraftStudio.Input.IsButtonDown("W") then
                print("W")
                self.animations:Play("Walk")
              end
       
        elseif self.animations.current == "Walk" then
            if not self.animations:IsPlaying() then
                self.animations:Play("Idle")
            end
        end
    
    To this:
    Code:
            if (CraftStudio.Input.IsButtonDown("W") and self.animations.current == "Idle") then
                self.animations:Play("Walk")
            elseif self.animations.current == "Walk" then
                self.animations:Play("Idle")
            end
    
    I'm pretty sure that should work: if you are providing input and the animations is Idle (I.E. not "Punch"), Play walk. Otherwise if the animation is "Walk" (and no input is provided, else part), play "Idle".

    I'm not sure what kind of game you are doing, so this might not apply, but: The way we do it in Curse of the Iron Keep is the character has a speed. So the check is actually on his speed and the input is handled earlier in the routine and allows for gravity and collisions to override the speed and thus make him Idle if he's colliding with something.

    Hope this helps!

    EDIT: Funky font tag from my careless copying! Oops, fixed!
  9. Darkhog Active Member

    But really, Elisee should merge it into CraftStudio's API. How about it, Dan?
  10. Dan Member

    Well, I don't think it's quite good enough to be useful to everyone. It's also not completely accurate on the frame timing (because I haven't needed it to be yet). There were a couple bugs and a few new features I've added since that version.
  11. Dan Member

    Sorry, I was wrong. You'll still have to do the button check again. It's been a long day...
    Code:
     
        if (CraftStudio.Input.IsButtonDown("W") and self.animations.current == "Idle") then
            self.animations:Play("Walk")
        elseif (not CraftStudio.Input.IsButtonDown("W") and self.animations.current == "Walk") then
            self.animations:Play("Idle")
        end
    

Share This Page