Getting Started Coding Floating Combat Text

If I wanted to scroll some text across the screen, e.g., “Hello World.”, how would I get started. I’ve looked through NameplateSCT and MIK’s and tried a number of experiments so stupid that I will not admit to trying them.

Anyway, from what I’ve seen so far, scrolling text seems more complicated than I had originally thought. But, any help getting started would be much appreciated.

Thanks,

There are several ways. This should be fairly easy to see what’s happening.

Paste the following into the website addon.bool.no to create/download the example as an addon:

local UpdateSpeed = 0.01
local ScrollMax = (UIParent:GetWidth() * UIParent:GetEffectiveScale()) -- max scroll width
local xmove = 1.5 -- move this much each update
local f = CreateFrame("Frame")
f.Text1 = f:CreateFontString()
f.Text1:SetFontObject(GameFontNormal)
f.Text1:SetText("Some Floating Across Text!")
f.Text1:SetPoint("LEFT", UIParent)

local xos = -f.Text1:GetWidth() -- Set to start offscreen by the width of the text
f.UpdateSpeed = UpdateSpeed
f:SetScript("OnUpdate", function(self, elapsed)
	self.UpdateSpeed = self.UpdateSpeed - elapsed
	if self.UpdateSpeed > 0 then
		return
	end
	self.UpdateSpeed = UpdateSpeed
	xos = xos + xmove
	if xos > ScrollMax then -- we're offscreen to the right so...
		xos =  -self.Text1:GetWidth() -- reset to the left again
	end
	f.Text1:SetPoint("LEFT", UIParent, xos, 0) -- reposition the text to its new location
end)

It’s not very well documented, but AnimationGroups are fun to play with.

The following creates a /float command to send text up the center of the screen, so you can do:

/float Hello, world
-- frame that contains the text
local f = CreateFrame("Frame","ScT",UIParent)
f:SetSize(200,20)
-- animations will revert to this starting position/alpha
f:SetPoint("CENTER")
f:SetAlpha(0)
-- create fontstring to be set that fills frame
f.text = f:CreateFontString(nil,"ARTWORK","GameFontNormalHuge")
f.text:SetAllPoints(true)

-- set up an AnimationGroup for a related group of animations
f.anim = f:CreateAnimationGroup()
-- order 1: fadein alpha 0 to 1
f.anim.fadein = f.anim:CreateAnimation("alpha")
f.anim.fadein:SetFromAlpha(0)
f.anim.fadein:SetToAlpha(1)
f.anim.fadein:SetDuration(0.5)
f.anim.fadein:SetOrder(1)
f.anim.fadein:SetEndDelay(0.5) -- wait half a second while faded in
-- order 1: translation 150px up
f.anim.move = f.anim:CreateAnimation("translation")
f.anim.move:SetOffset(0,150)
f.anim.move:SetDuration(1) -- 0.5 fadein + 0.5 delay
f.anim.move:SetOrder(1)
-- order 2: fadeout alpha 1 to 0
f.anim.fadeout = f.anim:CreateAnimation("alpha")
f.anim.fadeout:SetFromAlpha(1)
f.anim.fadeout:SetToAlpha(0)
f.anim.fadeout:SetDuration(0.25)
f.anim.fadeout:SetOrder(2)

-- create slash command to set text and start animation
-- try: /float Hello, world!
SLASH_FLOAT1 = "/float"
SlashCmdList["FLOAT"] = function(msg)
  f.text:SetText(msg)
  f.anim:Play()
end

Ideally the fadeout alpha would happen during the translation, and it seems like this should be possible but I’ve not stumbled on a way yet. So this cheats and does a 0.25-second fadeout in order 2.

Good resources for this are this thread:
https://www.wowinterface.com/forums/showthread.php?t=35104
And wowpedia:
https://wow.gamepedia.com/Widget_API#AnimationGroup

And of course the game’s default code to see how it’s used by Blizzard.

1 Like

Thanks to both of you. Much simpler than I had thought. But now I have a starting place.

Cheers,

EDIT:
Fizzle,

In your example, ‘elapsed’ doesn’t appear to be initialized. Nevertheless, the text scrolls happily as intended. So, what sets the value of ‘elapsed’ (on my system ‘elapsed’ varies from around 0.016 to 0.017),

Thanks,

elapsed is a parameter that is passed to the OnUpdate script handler by the game (see the SetScript line). It signifies the time elapsed since that frame was last updated (refreshed).

The lines:

self.UpdateSpeed = self.UpdateSpeed - elapsed
	if self.UpdateSpeed > 0 then
		return
	end
	self.UpdateSpeed = UpdateSpeed

limits how often the code below it is executed. self.UpdateSpeed counts down. Once it gets below zero, it is reset to the designated “time to wait” (UpdateSpeed) and the rest of the move code is run.

Thanks. That makes perfect sense.

Cheers,

The throttle (limiter) is not really needed in this instance but habit.

Gello,

You’re right, they are fun to play with. So, I’ve been trying to figure out how to get lines of text to scroll sequentially. The code I’m using (pretty much your example) creates an animation group for each line of text and then “plays” that line as in

for orderNum = 1, 5 do
    local msg = "Hello #"..tostring(i)
    floatingText( orderNum, msg)  -- wrapper around Gello's code
end

This doesn’t work, of course. One of the lines scrolls up as expected, but the other 4 all scroll at once overlapping each other. Perhaps you could point me in the right direction.

Also, is Animation:SetDuration() the way to control the rate at which the lines of text would scroll?

Cheers,

You need a delay between floatingText(). In that loop all 5 animations start simultaneously. Assuming each of the 5 has its own frame, it can be:

for orderNum = 1, 5 do
    local msg = "Hello #"..tostring(i)
    C_Timer.After((orderNum-1)/2,function() floatingText( orderNum, msg) end)
end

You may find working with OnUpdate is a better method to tightly control when stuff is starting. Fizzlemizz’s code above is a good example of OnUpdate usage, especially the elapsed throttling bit. (It’s bad practice to do stuff every frame when it’s not necessary–which for animation it’s kind of necessary–but doing computationally expensive stuff every frame for no reason can lower your users’ fps.)

btw to work around the lack of “spanning” animations, I’ve adjusted the previous code to break it into 3 orders. order 1 is alpha/translation, order 2 is translation only, order 3 is alpha/translation.

And for your purposes you may want to work with a frame pool if you’re not already. The following uses a simple frame pool and changes the code a bit so /float by itself will send up Hello #index in a random position/duration/distance on the screen, roughly around the center.

If you make a macro with /float and spam it, it will create a ton of frames but only as many as it needs. When an animation finishes it will become available in the frame pool for the next time one is needed, recycling the frames.

-- creates f's animation with no durations/distances
local function addAnimation(f)
  -- set up an AnimationGroup for a related group of animations
  f.anim = f:CreateAnimationGroup()
  -- order 1: fadein alpha 0 to 1 and translation start
  f.anim.fadein = f.anim:CreateAnimation("alpha")
  f.anim.fadein:SetFromAlpha(0)
  f.anim.fadein:SetToAlpha(1)
  f.anim.fadein:SetOrder(1)
  f.anim.movein = f.anim:CreateAnimation("translation")
  f.anim.movein:SetOrder(1)
  -- order 2: translation most of the distance
  f.anim.move = f.anim:CreateAnimation("translation")
  f.anim.move:SetOrder(2)
  -- order 3: fadeout alpha 1 to 0 and translation remaining
  f.anim.fadeout = f.anim:CreateAnimation("alpha")
  f.anim.fadeout:SetFromAlpha(1)
  f.anim.fadeout:SetToAlpha(0)
  f.anim.fadeout:SetOrder(3)
  -- order3: translation 
  f.anim.moveout = f.anim:CreateAnimation("translation")
  f.anim.moveout:SetOrder(3)
  -- hide frame when animation ends
  f.anim:SetScript("OnFinished",function(self) self:GetParent():Hide() end)
end

-- updates f's animation to given duration/distance
local function updateAnimation(f,duration,distance)
  f.anim.fadein:SetDuration(duration/4)
  f.anim.movein:SetOffset(0,distance/4)
  f.anim.movein:SetDuration(duration/4)
  f.anim.move:SetOffset(0,distance/2)
  f.anim.move:SetDuration(duration/2)
  f.anim.fadeout:SetDuration(duration/4)
  f.anim.moveout:SetOffset(0,distance/4)
  f.anim.moveout:SetDuration(duration/4)
end

local pool = {} -- simple frame pool

-- gets available unused frame from the pool or creates one if needed
local function getFrame()
  for _,f in pairs(pool) do
    if not f:IsVisible() then
      f:Show()
      return f
    end
  end
  -- no available frames, create one
  local f = CreateFrame("Frame",nil,UIParent)
  f:SetSize(200,20)
  f:SetPoint("CENTER")
  f:SetAlpha(0)
  f.text = f:CreateFontString(nil,"ARTWORK","GameFontNormalHuge")
  f.text:SetPoint("CENTER")
  addAnimation(f)
  tinsert(pool,f)
  return f
end

-- /float command to send up 5 copies of a message or if no message then
-- send up 5 "Hello"s at random position/duration/distances
SLASH_FLOAT1 = "/float"
SlashCmdList["FLOAT"] = function(msg)
  local isRandom = msg:lower()=="random" or msg==""
  for i=1,5 do
    local f = getFrame()
    f.text:SetText(isRandom and "Hello #"..i or msg)
    if isRandom then
      f.text:SetText("Hello #"..i)
      f:SetPoint("CENTER",random(400)-200,random(200)-100)
      updateAnimation(f,random(20)/10+0.25,random(100)+50)
    else
      f.text:SetText(msg.." #"..i)
      f:SetPoint("CENTER")
      updateAnimation(f,1,150)
    end
    -- wait half a second between each of the 5 floated texts
    C_Timer.After((i-1)/2,function() f.anim:Play() end)
  end
end

edit: and rate as distance/duration is controlled by both distance and duration. So you can adjust either in a translation to affect the speed that it moves.

Thanks again, Gello. This is getting to be a lot of fun. Got most of my addon working and am learning a lot. I couldn’t be more grateful.

And yes, I’ve been using home-grown frame pools (not the mixin) for awhile now. But I do want to learn more about the mixin - but that’s for another project. In any case, my recycle needs are handled perfectly by code very similar to yours.

Cheers,

Alaric, here. I’m Having to post under another name because the forum software won’t let me reply.

I’m making progress but have reached a point where I need some additional help. So, what I’m trying to do is to produce code that will print lines of floating text (floating combat text is a good example). So, for example, I’d like my code to produce output like this:

Hello #1
Hello #2 
Hello #3

where the three lines scroll up in unison. Currently, my code produces this:

Hello #3
<8 blank lines>
Hello #3
<8 blank lines>
Hello #3

The basic code flow goes like this: three frames are created, each with a different string ("Hello #1, Hello #2, Hello #3). After each frame is created and passed into the scrollText() function, the code waits for 3 seconds, loops back and repeats. Here’s the slash command that starts it off:

SLASH_SCROLL_TESTS1 = "/scroll"
SlashCmdList["SCROLL_TESTS"] = function( num )
    local f = {}
    for i = 1, 3 do
        f[i] = getFrame( "Hello #" ..tostring(i))
        scrollText( f[i] )
        C_Timer.After(3.0, function() 
            -- do nothing )
        end)
    end
end

The getFrame() code uses Blizzard’s frame pool service. Apart from that getFrame() is unremarkable.

local framePool = nil
local function getFrame( displayStr )
    if framePool == nil then
        framePool = CreateFramePool("Frame", UIParent, "BackdropTemplate")
    end
    local f = framePool:GetNextActive()
    if f == nil then
        f = framePool:Acquire()
    end

    f.Text1 = f:CreateFontString()
    f.Text1:SetFontObject(GameFontNormal)
    f.Text1:SetPoint( REGION, STARTING_XPOS, STARTING_YPOS )
    f.Text1:SetText( displayStr )
    f.ScrollXMax = (UIParent:GetWidth() * UIParent:GetEffectiveScale())/2
    f.ScrollYMax = (UIParent:GetHeight() * UIParent:GetEffectiveScale())/2 

    f:Show()
    return f 
end

And finally the code that scrolls the text. Note that I use C_Timer.NewTicker() to control the rate at which the string moves upwards. It doesn’t repeat. As soon as the string exceeds the upper boundary, the timer is cancelled and the frame recycled.

local function scrollText( f )

    local scrollHeightMax = f.ScrollYMax/2

    local yDelta = 30      
    local yPos = -f.ScrollYMax

    local count = 1
    f.handle = C_Timer.NewTicker( 0.5, function (self)
        if count == 1 then
            yPos = STARTING_YPOS
            count = count + 1
        end
        yPos = yPos + yDelta
        f.Text1:SetPoint( REGION, UIParent, STARTING_XPOS, yPos  )

        if yPos > scrollHeightMax then
            f.handle:Cancel()
            framePool:Release(f)
        end
    end)
end

Any thoughts?

Gello,

Oh, I forgot to mention that I really appreciate your updated animation example. It works great. One other kinda sorta unrelated question: do you think pool implementations like yours are lighter weight than the mixin? Given how simple the home-grown pool is, I would certainly think so.

All of which begs the question: what are the advantages of using the mixin over your approach?

Cheers,

imho “weight” as in code footprint has too much weight. If it achieves the goal without including 25k lines of bloated Ace libraries that instantiate an extra 3MB every time it wants to do some trivial task; and if it achieves the goal without creating a lot of needless garbage lua has to frequently collect; and if it achieves the goal with a straightforward approach that doesn’t needlessly waste cpu cycles every frame, and a dialog doesn’t have tens of thousands of frames and regions to render, it’s fine.

Where “weight” becomes serious consideration is doing cpu-bound tasks like sorting, or computationally expensive tasks on every frame in an OnUpdates, or responding to all 18 BAG_UPDATE events in one frame by rescanning all bags, etc.

I think Blizzard’s mixins are likely harmless to use, especially for straightforward stuff if it doesn’t reuse static variables that can taint Blizzard’s stuff. There’s some advantage to reusing code that does its intended job well. For very simple stuff, it may be overkill but no reason to avoid their use.

Well, thanks Gello. As it happens I’ve got everything working … sort of. After a short time the spacing between the lines of text begins to decay and the lines to overlap. Here’s the code snippet that plays the animation. The str variable is a straightforward array of strings each element of which is to be ‘floated’ on the screen:

for i = 1, #str do
        local f = getFrame()
        f.Text:SetText( str[i] )

        local duration = 5     -- seconds
        local xDistance = 0
        local yDistance = f.ScrollYMax/2
        updateAnimation(f, duration, xDistance, yDistance )
        C_Timer.After( (i-1)/2, function()
                f.animGroup:Play()
            end)
 end

I guess I’m not sure how the Play() function works. I’ve been assuming that, for an array of 5 frames, this configuration of Play() will animate the first frame at 0 seconds, the second after 0.5 seconds, the third after 1 second, and so forth.

Still, no matter how I alter the duration and/or the distances, the lines of text quickly start to overlap. Any thoughts?

Thanks, as always.

Are you running the loop just once or more than once?

There’s not enough in that code snippet to debug the issue. f.ScrollMax is not defined (you should be getting lua errors if it’s attempting to do math on nil values, so something else was cut out of there) and I need to see the updateAnimation function.

Here it is…

local function updateAnimation(f, duration, xPos, travelDistance )
  
    local fadeDuration = duration/4
    local moveDuration = duration/2       

    f.animGroup.fadein:SetDuration(  fadeDuration )
    f.animGroup.movein:SetOffset( xPos, travelDistance )
    f.animGroup.movein:SetDuration( moveDuration )

    f.animGroup.move:SetOffset( xPos,travelDistance )
    f.animGroup.move:SetDuration( moveDuration)

    f.animGroup.fadeout:SetDuration( fadeDuration )
    f.animGroup.moveout:SetOffset( xPos, travelDistance )
    f.animGroup.moveout:SetDuration( moveDuration )
end

The movein, move and moveout all have the same duration. The movein and moveout durations are going to be twice the fadein/fadeout durations. You probably want the movein/moveout durations to be fadeDuration.

And if you want the distance parameter to be the total distance travelled, you want to divide up the travelDistance by 4 for the movein/moveout and by 2 for the move:

local function updateAnimation(f, duration, xPos, travelDistance )
  
    local fadeDuration = duration/4
    local moveDuration = duration/2       

    f.anim.fadein:SetDuration(  fadeDuration )
    f.anim.movein:SetOffset( xPos/4, travelDistance/4 )
    f.anim.movein:SetDuration( fadeDuration )

    f.anim.move:SetOffset( xPos/2,travelDistance/2 )
    f.anim.move:SetDuration( moveDuration)

    f.anim.fadeout:SetDuration( fadeDuration )
    f.anim.moveout:SetOffset( xPos/4, travelDistance/4 )
    f.anim.moveout:SetDuration( fadeDuration )
end

Gello,

Here is the complete code that, from using your code as a template, I’ve implemented to display floating combat text. These functions and variables that support them are defined in FloatingText.lua. Of course, any fubar code is mine, not yours.

The last problem (I think) to be solved is how to setup updateAnimation() and C_Timer:After() such that the text will print individually and not on top of one another.

Oh, and thanks for the explanation of the duration and distance parameters to updateAnimation(). Makes perfect sense, now. But, now I have a question about the Animation:Play() function. When f.animGroup:Play() is invoked, I presume that it accesses the animation configuration to determine how the animation is to proceed. What I don’t understand is how does the function (:Play()) know to animate the f.Text:GetText() string?

Here are the variables global within the module (FloatingText.lua):

local DEFAULT_STARTING_REGION = ft.DEFAULT_STARTING_REGION
local DEFAULT_STARTING_XPOS = ft.DEFAULT_STARTING_XPOS
local DEFAULT_STARTING_YPOS = ft.DEFAULT_STARTING_YPOS

local framePool = CreateFramePool(“Frame”, UIParent, “BackdropTemplate”)

Here is the configAnimation() function (essentially your animate() function renamed):

local function configAnimation(f)
    f.animGroup = f:CreateAnimationGroup()

    f.animGroup.fadein = f.animGroup:CreateAnimation("alpha")
    f.animGroup.fadein:SetFromAlpha(0)
    f.animGroup.fadein:SetToAlpha(1)
    f.animGroup.fadein:SetOrder(1)

    f.animGroup.movein = f.animGroup:CreateAnimation("translation")
    f.animGroup.movein:SetOrder(1)

    f.animGroup.move = f.animGroup:CreateAnimation("translation")
    f.animGroup.move:SetOrder(2)

    f.animGroup.fadeout = f.animGroup:CreateAnimation("alpha")
    f.animGroup.fadeout:SetFromAlpha(1)
    f.animGroup.fadeout:SetToAlpha(0)
    f.animGroup.fadeout:SetOrder(3)

    f.animGroup.moveout = f.animGroup:CreateAnimation("translation")
    f.animGroup.moveout:SetOrder(3)
 
    -- hide and release the frame when animation ends
    f.animGroup:SetScript("OnFinished",
    	function(self) 
        	self:GetParent():Hide() 
        	framePool:Release(f)
    	end)
end

And now the getframe() function:

function ft:getFrame( region, startingXpos, startingYpos)
    f = framePool:Acquire()
    f.Text = f:CreateFontString(nil,"ARTWORK","GameFontNormalLarge")
    f.Text:SetPoint("LEFT", 0, 0 )
    f:SetSize(400,30)

    -- When the frame is created f.SetPoint() is the starting position
    if startingXpos == nil then startingXpos = DEFAULT_STARTING_XPOS end
    if startingYpos == nil then startingYpos = DEFAULT_STARTING_YPOS end

    f:SetPoint(region, startingXpos, startingYpos )
    f:SetAlpha(0)
    f.ScrollXMax = (UIParent:GetWidth() * UIParent:GetEffectiveScale())/2
    f.ScrollYMax = (UIParent:GetHeight() * UIParent:GetEffectiveScale())/2

    configAnimation(f)
    f:Show()
    return f
end

And finally the function that displays the text.

function ft:displayText(f, threatTable )
    local str = {}
    for i, entry in ipairs( threatTable ) do
	      f.Text:SetText( entry[2] )
	      local duration = 12
	      local Xdistance = 0
	      local Ydistance = f.ScrollYMax   -- ScrollYMax = 384
         updateAnimation(f, duration, Xdistance, Ydistance )
         local delay = (i - 1)/2
	    C_Timer.After( delay,
	        function()
	            f.animGroup:Play()
           end)
    end
end

Finally, to display the text, ft:displayThreatTable called from the “UNIT_THREAT_LIST_UPDATE” handler after all the relevant data has been collected.

local f = ft:getFrame( "CENTER", DEFAULT_STARTING_XPOS, DEFAULT_STARTING_YPOS )
ft:displayThreatTable(f, threatTable )

One final note: I test this by taking a party of 2 of my 60s into Draenor and letting the elites bang on 'em. The team generates lots of threat (they’re disarmed and hit with their fists), Even a 60 takes a long time to kill a 40 elite with just its fists, so it’s a good test.

cheers,

Edit: I placed some logging code into the C_Timer.After() function:

C_Timer.After( (i - 1)/2 + 1, 
            function()
                msg:postMsg( sprintf("%0.1f : %s\n", delay, f.Text:GetText() ))
                f.animGroup:Play()
        end)

Here were the results:

1.0 : Shadowraîth: 36235 threat generated (89.4% of 40541)
1.0 : Shadowraîth: 72470 threat generated (89.4% of 81082)
1.5 : Zippora: 4306 threat generated (10.6% of 40541)
1.5 : Zippora: 8612 threat generated (10.6% of 81082)
2.0 : Shadowraîth: 36235 threat generated (89.4% of 40541)
2.0 : Shadowraîth: 72470 threat generated (89.4% of 81082)
2.5 : Zippora: 8612 threat generated (10.6% of 81082)
2.5 : Zippora: 4306 threat generated (10.6% of 40541)

I’m curious why 2 lines of text get written at each time interval? Wouldn’t this explain the overlapping lines of text?

ddd

It applies the animation to the whole frame. You can attach icons and such and you’ll see the whole frame animates. To animate specific elements of a frame you need to target it with animation:SetTarget().

Your doubling up is likely due to your event handler (which is absent in your code above) and that you’re sending multiple texts in response to each event. I suspect you need to study the behavior of the event more. It’s very possible for it to fire twice in a frame, causing two sets of loops to trigger in the same frame. Use print() liberally to study how events behave.

OK, got it and thank you for your patience.

So, I was in the process of studying the handler just as you suggested when I saw your response. So, Just for completeness, here’s my handler:

if event == "UNIT_THREAT_LIST_UPDATE" then

        -- Get threat values for each party member
        local partyMembers = grp:getAddonPartyMembers()
        for _, entry in ipairs( partyMembers ) do
            local unitId = entry[VT_UNIT_ID]
            local memberName = UnitName( unitId )

            local _, _, threatValue = UnitDetailedThreatSituation( unitId, arg1 )
            if threatValue ~= nil then
                if threatValue > 0 then
                    grp:setThreatValues( memberName, threatValue )
                end
            end
        end
        local threatStats, healsTakenStats, dmgTakenStats = metrics:getThreatStats()
        ft:displayThreatTable( threatStats )
end

In the code above, threatStats, healingStats, and dmgTakenStats are simple tables of strings each of which logs the member’s name, the member’s total threat, and the group’s total threat.

---------------------------------------------- EDIT -------------------------------------------------------
Gave up for the moment on solving the overlapping frame problem. Have now turned to coroutines. My initial implementation uses a table of coroutines (one coroutine per member of the party), each of which polls the addon’s database for stats. My thinking is that by using coroutines for polling, the data reporting is disconnected from frame rate at the expense of real-time reporting. So far it seems to be working. Fingers crossed.

Coroutines, Animation, Framepools - what could be more fun?