Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Problem with using tweens on a class of object

edited December 2014 in Questions Posts: 2,020

Hi there

I'm having issues with tweens on objects set up by a class for animating text. For some reasons all the tutorials I find assume that the tweens are defined by a function, rather than a class.

In Main when I want to print to screen I do this:

table.insert(writing, Write ("game over"))
tween (2, writing[#writing], {y=50}, tween.easing.backOut, writing[#writing]:erase())

in the Write class:

function Write:init(str)
self.x=WIDTH/2
self.y=HEIGHT/2
self.str=str
self.col=color(0, 244, 255, 255)
--etc
end

function Write:draw()
--draws the text
end

function Write:erase()
 print "callback"  --ie this is just for debugging. 
end

A few questions:
1. Is it possible to put the tween command into the Write:init function, something like this:

tween(2, self, {y=50})

every time I try this, or variations on it, I get an error. I can't define the tween inside the class, is that correct?

  1. I can't seem to tween the colour (I want the alpha to go down to 0, so that the text fades). This:

tween (2, writing[#writing], {y=50, col.a=0}, tween.easing.backOut, writing[#writing]:erase())

produces an error, as does adding any kind of colour variable

  1. the callback function seems to get called at the very bginning of the tween, rather than at the end of the tween.
    Also I don't know how or whether a table.remove would work if the erase() function is inside the Write class

It would be great if someone could tell me where I'm going wrong. Perhaps there's a reason why none of the tweening tutorials I've seen are animating objects defined by a class, and I need to convert the Write class into a set of functions in the Main tab. I'm new to Codea/Lua (my last programming experience was Blitz Basic on the Amiga a long tme ago), and don't have a sense yet of when class or a function is the best way of defining the objects in the game.

thanks for reading

Tagged:

Comments

  • IgnatzIgnatz Mod
    edited December 2014 Posts: 5,396

    @yojimbo2000 - I suspect your problem is because you can't use self as the item to be tweened, because self is not a normal table. If you define a table inside your class and tween that, you should be OK.

    I'm not sure about the colour variable but that's easy to get around by just tweening an ordinary number and then setting the colour value to that.

    I don't understand the last question about the table remove, can you provide more detail?

  • edited December 2014 Posts: 2,020

    @Ignatz, thanks for the speedy reply.

    OK, I'll investigate putting a table inside the class.

    I tried your suggestion of tweening a separate alpha variable, it worked, thanks for that.

    the callback is meant to erase the writing object after the tween has finished.

    Before I thought about using callback, I had a self.timer variable set in Write:init(), which would manually count down inside the draw loop. When it reached 0 I'd erase that entry. THis is the code from the main draw loop:

    for i,v in ipairs(writing) do
    v:draw()
    if v.timer==0 then table.remove(writing, i) else v.timer = v.timer -1 end
    end
    

    Rather than this manual method, I thought it would be more elegant to use the callback to kill the writing when the tween finished. However, I don't know how I would get the correct position in table.remove for this, and also the erase() function seems to get called at the very start of the tween, not the end of the motion

  • IgnatzIgnatz Mod
    Posts: 5,396

    You can't kill a class instance from within itself

    Better is to have a table where you keep objects to be deleted, and then empty it in draw, eg something like

    function Write:erase()
        table.insert(ObjectsForDeletion,self)
    end
    
    --then at the top of draw
    for _,d in pairs(ObjectsForDeletion) do
        d=nil
    end
    
  • Posts: 2,020

    Great idea, I'll try that.

    Any thoughts on why the callback function is being called at the very start of the tween though?

  • dave1707dave1707 Mod
    edited December 2014 Posts: 7,879

    Try this. Tap screen to start the tween.


    function setup() writing={} table.insert(writing, Write ("game over",200,200,300,500)) table.insert(writing, Write ("game start",400,200,300,520)) end function draw() background(0) for a,b in pairs(writing) do b:draw() end end function touched(t) if t.state==BEGAN then for a,b in pairs(writing) do b:touched(t) end end end function erase() print("tween done") end Write=class() function Write:init(str,xs,ys,xe,ye) self.str=str self.col=color(0, 244, 255, 255) self.starts={x=xs,y=ys} self.ends={x=xe,y=ye} end function Write:tw() tween(2,self.starts,self.ends,tween.easing.sineOutIn,erase) end function Write:draw() fill(self.col) text(self.str,self.starts.x,self.starts.y) end function Write:touched(t) self:tw() end
  • IgnatzIgnatz Mod
    Posts: 5,396

    There's no real need for a class for this. When I did it, I put messages into a table, and as each one expired, I removed it and tweened the next. That's simpler to manage.

  • dave1707dave1707 Mod
    Posts: 7,879

    I changed my code above so the erase function gets called when the tween is done.

  • edited December 2014 Posts: 2,020

    @dave1707 thanks for that example, the tween motion works great. However, I see the "callback" debug text being printed straightaway, not at the end of the motion. Are you seeing that behaviour too?

    UPDATED: Great, it works now! So the callback cannot be inside the class? That's interesting. I think @Ignatz is right, maybe it is better in a table/ function

  • dave1707dave1707 Mod
    edited December 2014 Posts: 7,879

    @yojimbo2000 Here's a version with the callback in the class. I have it clear the string when the tween is done.


    function setup() writing={} table.insert(writing, Write ("game over",8,200,200,300,500)) table.insert(writing, Write ("game start",3,400,200,300,520)) end function draw() background(0) for a,b in pairs(writing) do b:draw() end end function touched(t) if t.state==BEGAN then for a,b in pairs(writing) do b:touched(t) end end end Write=class() function Write:init(str,t,xs,ys,xe,ye) self.str=str self.col=color(0, 244, 255, 255) self.starts={x=xs,y=ys} self.ends={x=xe,y=ye} self.time=t end function Write:tw() tween(self.time,self.starts,self.ends,tween.easing.sineOutIn, function() self.str="" print("tween done") end) end function Write:draw() fill(self.col) text(self.str,self.starts.x,self.starts.y) end function Write:touched(t) self:tw() end
  • dave1707dave1707 Mod
    Posts: 7,879

    @yojimbo2000 I modified the code above to allow different times for different Tweens. again, just tap the screen to start the Tweens.

  • Posts: 2,020

    Ok, combining @dave1707 and @Ignatz's suggestions. I've also placed the tween in the unit function and commented out the touch function. The tween animates, the callback happens when it should, but, setting the value to nil does not kill the object. I've found this previously with objects stored in tables, I had to use remove.table to kill the object. Any ideas how to fix this?

    function setup()
        writing={}
        kill={}
        table.insert(writing, Write ("game over",200,200,300,500))
        table.insert(writing, Write ("game start",400,200,300,520))
    end
    
    function draw()
        background(0)
        for a,b in pairs(writing) do
            b:draw()   
        end
        for _,b in ipairs(kill) do
            b=nil  -- why doesnt this work?
    
        end
    end
    
    --[[function touched(t)
        if t.state==BEGAN then
            for a,b in pairs(writing) do
                b:touched(t)   
            end
        end
    end]]--
    
    Write=class()
    
    function Write:init(str,xs,ys,xe,ye)
        self.str=str
        self.col=color(0, 244, 255, 255)
        self.starts={x=xs,y=ys}
        self.ends={x=xe,y=ye}
        tween(2,self.starts,self.ends,tween.easing.sineOutIn,erase)
    end
    
    function Write:tw()
        tween(2,self.starts,self.ends,tween.easing.sineOutIn,self:erase())
    end
    
    function Write:draw()
        fill(self.col)
        text(self.str,self.starts.x,self.starts.y)
    end
    
    function Write:touched(t)
        self:tw()
    end
    
    function erase()
       table.insert(kill,self)
    print "callback" --debug
    end
    
  • Posts: 2,020

    Oh I've just realised, erase is no longer inside the class, so there's no self variable here

  • dave1707dave1707 Mod
    Posts: 7,879

    @yojimbo2000 See my version above that has the callback in the class.

  • Posts: 2,020

    But when the callback was in the class, it didn't work properly. It was called as soon as the tween started.

  • dave1707dave1707 Mod
    edited December 2014 Posts: 7,879

    @yojimbo2000 You're not looking at my last example where it's in the class and it's correct.

  • Posts: 2,020

    @dave1707

    You're right, sorry I didn't see the second version that you posted.

    And it works!

    I had no idea that you could define a null function in a single line like that with just spaces inbetween each command. That is fantastically useful. Is there a tutorial anywhere explaining this syntax?

    Thank you

    I guess though that I should have the object stored in a table inside the class though, because the self.str="" isn't killing the object as such, just making it invisible, right? ie, as the game went on, the pairs(writing) loop would continue to get longer (I'll be using this every time the player scores points). Although maybe the processor overhead will be minimal if things aren't actually being drawn.

  • dave1707dave1707 Mod
    Posts: 7,879

    @yojimbo2000 I was just clearing the str variable because I didn't know what you really want to do. You can add as much code as you want in that function to do whatever.

  • dave1707dave1707 Mod
    Posts: 7,879

    @yojimbo2000 Here's a version where I increased the table count and then I delete the table entry when the tween is done.


    function setup() parameter.watch("#writing") writing={} table.insert(writing, Write ("game over 3",3,100,200,300,560)) table.insert(writing, Write ("game start 4",8,200,200,300,600)) table.insert(writing, Write ("game over 5",5,300,200,300,540)) table.insert(writing, Write ("game start 6",7,400,200,300,520)) table.insert(writing, Write ("game over 7",6,500,200,300,580)) table.insert(writing, Write ("game start 8",4,600,200,300,500)) end function draw() background(0) for a,b in pairs(writing) do b:draw() if b.remove then table.remove(writing,a) end end end function touched(t) if t.state==BEGAN then for a,b in pairs(writing) do b:touched(t) end end end Write=class() function Write:init(str,t,xs,ys,xe,ye) self.str=str self.col=color(0, 244, 255, 255) self.starts={x=xs,y=ys} self.ends={x=xe,y=ye} self.time=t self.remove=false end function Write:tw() tween(self.time,self.starts,self.ends,tween.easing.sineOutIn, function() self.remove=true print("tween complete") end ) end function Write:draw() fill(self.col) text(self.str,self.starts.x,self.starts.y) end function Write:touched(t) self:tw() end
  • Posts: 2,020

    Ok, I figured it out. I made the callback function set the string to nil, and this is what I changed the main draw command to:

    for a,b in ipairs(writing) do
     if b.str~=nil then b:draw() else table.remove(writing, a) end
    end
    

    So the actual act of deletion occurs outside the class in the main loop.

    Thanks to both of you for your help, I've learned a lot!

  • Posts: 2,020

    Sorry I was typing my comment at the same time as yours, I think our solutions are the same, aren't they?

  • IgnatzIgnatz Mod
    Posts: 5,396

    @yojimbo2000 - IMHO, a simple table will be better than a class - and efficient - because it works like a queue, which is exactly what you want. Using a class and setting messages to blanks without killing them when they finish is pretty ugly.

    I've written about anonymous functions here
    https://coolcodea.wordpress.com/2013/09/13/111-anonymous-functions-and-the-mysterious-_g/

  • dave1707dave1707 Mod
    Posts: 7,879

    @yojimbo2000 They both work the same. I like using seperate self variables for different conditions.

  • Posts: 2,020

    @Ignatz you could be right there. I might have a go at recoding it as a table/ function in the main tab just to see what the difference is. But if you look at mine and @dave1707 's most recent code, we do have an efficient way of properly killing the instance in the draw loop, after it has been set to nil.

  • IgnatzIgnatz Mod
    Posts: 5,396

    @yojimbo2000 - below is a table of functions I use in my 3D dungeon, to show messages in sequence, one after the other, for variable times.

    to add a message: Messages.Add{text=a.text,length=a.length}
    (note I send a table of named parameters through, enabling me to define only the message elements I need)

    to show messages in draw: Messages.draw()
    (which also manages deletion)

    Messages={}
        Messages.text={}
        Messages.defaultLength=5
        Messages.phaseOutTime=2 --seconds
        Messages.defaultColour=color(255)
        Messages.defaultSize=32
        --Messages.defaultPosition=vec2(WIDTH/2, HEIGHT/2)
        Messages.defaultFont="Noteworthy-Bold"
    
        function Messages.Add(settings)       
        --messageText,durationInSeconds=Messages.defaultLength,size=64,colour)
        Messages.text[#Messages.text+1]={
            ["message"]=settings.text,
            ["length"]=settings.length or Messages.defaultLength,
            ["size"]=settings.size or Messages.defaultSize,
            ["colour"]=settings.colour or Messages.defaultColour,
            ["position"]=settings.position or vec2(WIDTH/2, HEIGHT/2),                
            ["font"]=settings.font or Messages.defaultFont
        }
    end
    
    function Messages.draw()
        if #Messages.text==0 then return end
        m=Messages.text[1]
        if not m.endTime then
            m.endTime=ElapsedTime+m.length
        elseif ElapsedTime>m.endTime then
            table.remove(Messages.text,1)
            return
        end
        Messages.WriteMessage(m)
    end
    
    function Messages.WriteMessage(m)
        pushStyle()
        font(m.font)
        textWrapWidth(WIDTH*.75)
        fontSize(m.size)
        local t=m.endTime-ElapsedTime
        m.colour.a=math.min(1,t/Messages.phaseOutTime)*255
        fill(m.colour)
        textMode(CENTER)
        text(m.message,WIDTH/2,HEIGHT/2)
        popStyle()
    end 
    

    I have a variation on this in my 2D sidescroller, which shows multiple messages. scrolling them upwards and fading them, so they can stack on each other.

  • Posts: 2,020

    @Ignatz oh, you're the coolcodea author! I love that blog! It's an incredible resource

  • IgnatzIgnatz Mod
    Posts: 5,396

    Below is my scrolling message function set, you can see the result here
    https://coolcodea.wordpress.com/2014/09/16/162-2d-platform-game-6-extras/

    Message.draw needs to be given a player object in this case so it can figure out where to position the message (just above the player's head)

    SS_Message={}
    SS_Message.text={}
    
    function SS_Message.Add(txt,t) --txt is message, t is time
        t= (t or 2) + ElapsedTime --time when message ends
        SS_Message.text[#SS_Message.text+1]={txt,t}
        SS_Message.startTime=nil
    end
    
    function SS_Message.draw(p)
        if #SS_Message.text==0 then return end
        pushStyle()
        textWrapWidth(400)
        fontSize(18)
        local pos=p:GetPos()
        --starting height
        local ww,hh=pos.x*SS_w,pos.y*SS_h+150 --WIDTH/2, HEIGHT*2/3 
        for i=#SS_Message.text,1,-1 do
            if ElapsedTime>SS_Message.text[i][2] then
                table.remove(SS_Message.text,i)
            else
                local t=math.min(1,SS_Message.text[i][2]-ElapsedTime)*255
                fill(124, 167, 210, t)
                local w,h=textSize(SS_Message.text[i][1])
                local w2=math.max(ww,w/2+30)
                rect(w2-w/2-20,hh-h/2-20,w+40,h+40)
                fill(255,255,255,t)
                text(SS_Message.text[i][1],w2,hh)
                hh=hh+h+50
            end
        end
        popStyle()
    end 
    
  • Posts: 2,020

    @Ignatz I saw a version of the code for the 2d platform game, I was the guy that asked that question today on that page of the blog. It looks very different from a class, when you store it in a table like that. I still don't understand the first 2 lines though. Is the messages.text {} table nested inside (like a dimension) the messages {} table? Are the functions also part of the messages {} table? You have to forgive me, object oriented stuff (if that's the correct word for classes and functions) is still new to me, and I don't entirely get the .suffix convention

  • IgnatzIgnatz Mod
    Posts: 5,396

    See if this helps
    https://coolcodea.wordpress.com/2013/04/12/31-a-lesson-from-the-roller-coaster-demo/
    https://coolcodea.wordpress.com/2014/10/01/169-why-tables-and-classes-are-so-useful/
    https://coolcodea.wordpress.com/2013/06/14/84-a-practical-example-showing-the-value-of-classes/

    Essentially, you can put functions in a table, which just means you add a prefix with the table name. The same goes for variables. This enables you to keep this stuff completely separate from your other code and not risk duplicating variable names etc.

  • Posts: 2,042

    Just replying to something @Ignatz said waaay above: you can use self as the table for a tween.

  • Posts: 2,020

    @JakAttak but you can't just make "self" by itself the object of the tween though can you, like I was trying to do right at the top of the thread? It has to be self.something, whether that something is a position vector, like @Dave1707 came up with here, or a table inside the table, like something={str=srt, x=xs, y=ys, col=col} etc.

    Or have you managed it with just self as the object?

  • edited December 2014 Posts: 2,042

    tween(1, self, { x = 100 }, tween.easing.linear) works perfectly fine

    Here's a small example:

    function setup()
        ex = Example()
        ex2 = Example(HEIGHT / 4, WIDTH / 3, WIDTH * 2/3)
    end
    
    function draw()
        background(255)
    
        ex:draw()
        ex2:draw()
    end
    
    Example = class()
    
    function Example:init(y, x, x2)
        self.y = y or HEIGHT / 2
        self.x = x or 0
    
        tween(2, self, { x = x2 or WIDTH }, { loop = tween.loop.pingpong })
    end
    
    function Example:draw()
       ellipse(self.x, self.y, 30)
    end
    
  • Posts: 2,020

    @JakAttak you're right. I went back to have another look at my original code. Apart from not using the callback function correctly, the destination was a variable I'd defined locally somewhere else (it was from my first Codea project, and the code was a mess basically). I thought I'd tried every combo (obviously not). I fixed all of that, and it now works. i.e. I have a class for animating text on the screen, the tween in the init of the class with self as object, and a callback that tells the draw loop to kill the text once the animation is done. Thank you everyone, I learnt a lot.

    Slightly off-topic follow-on question, my FPS drops from 60 to 30 or lower when there's lots of action on screen (ie explosions plus this text routine displaying points, multipliers, and so on). Is it faster to create an image of the text, and then draw it as a mesh, than using text() for the frame-by-frame drawing? Or is it the tweens that are the processor drag? I also have the same question for my explosion sparks, which are currently drawn with ellipse(). Would an image mesh be faster? Generally, I'm very impressed with the speed of codea. I have 50 physical bodies bouncing around, all drawn with meshes, and FPS is a consistent 60 on the iPad Air.

  • IgnatzIgnatz Mod
    Posts: 5,396

    Tweens are just number interpolations, and very fast. Ellipses (in great numbers) can be slow, I recall from previous threads.

    But why don't you try it different ways and see what happens? That is the best way to discover the fastest approach.

Sign In or Register to comment.