Dota 2 - Custom UI

The aim of this tutorial is giving an introduction into making custom UIs for dota 2 using flash/scaleform. This is a very flexible way of making UIs, so there are lots of possibilities. This tutorial will go over the files needed for making UIs work ingame, and give a small flash tutorial on how to get your UI working. We will also have a look at letting your UI and vsript work together. Remember: flash is very flexible, so your imagination and creativity is the limit here.
Basic OOP programming knowledge is assumed.

For this tutorial I will use Adobe Flash CS6, but any program that compiles swf files will do.

Console command to enable scaleform output to console: 'scaleform_spew 1'

A list of scaleform functionality can be found on the wiki.

Credits for figuring out pretty much everything we know about this topic go to Ash47

Table of contents

  1. The basics
  2. Compiling your UI
  3. Adding modules
    1. Creating a movieclip
    2. Adding a background
    3. Adding a button
    4. Test run
    5. Actionscript I
    6. Actionscript II
  4. UI - vscript interaction
  5. Example UI code
  6. Flash tips and tricks

The basics

We've seen the possibilities of the server sided vscript - which is cool - but to add more functionality, the ingame mechanics are not always enough. To remedy this the dota 2 engine presents us with the possibility of adding custom panels and functionality to the user interface. This custom UI exposes a big list of scaleform functions,a full reference can be found here.

So where can we find the UI files? A custom UI belongs to an addon, so is placed inside its folder. For a custom UI you only need two files, in resource/flash3/ you will first of all need custom_ui.txt. This file tells the engine what file to load as custom UI. Most of the file is commented out, it is just information. The only imporant lines in custom_ui.txt are the upper lines, which look like this:

"CustomUI"
{
    "1"
    {
        "File"      "CustomUIFileName1" //for example: "UI1", if your swf file is called UI1.swf
        "Depth"     "51"
    }
}
As you can see it would be possible to add multiple different UI files by adding multiple entries. This is usually not needed however, you can add multiple elements in one file, and al your panels in one file might be a good way to keep your work organised. If you want your elements at different depths you will have to add more entries though.

The next file you need is a compiled *.swf file. The name does not matter, as long as you put it inside your custom_ui.txt. This file is a compiled flash file. Not every file will work though, you need some specific code to let the game display the file correctly. I have provided a skeleton with the minimum amount of code that is required to let the dota 2 engine show your interface ingame. This skeleton allows you to compile (and possibly edit) the *.swf file.

Custom UI Skeleton

You can find the full skeleton here, it contains everything you need to compile your UI: https://github.com/Perryvw/SkeletonFlashUI.
This file also contains the folders ValveLib and scaleform. Without these two folders, your UI will not compile!

For reference: here is the code of the main class of the skeleton file, remember this is absolutely essential.

package {
    import flash.display.MovieClip;

    //import some stuff from the valve lib
    import ValveLib.Globals;
    import ValveLib.ResizeManager;
    
    public class CustomUI extends MovieClip{
        
        //these three variables are required by the engine
        public var gameAPI:Object;
        public var globals:Object;
        public var elementName:String;
        
        //constructor, you usually will use onLoaded() instead
        public function CustomUI() : void {
        }
        
        //this function is called
        public function onLoaded() : void {            
            //make this UI visible
            visible = true;
            
            //let the client rescale the UI
            Globals.instance.resizeManager.AddListener(this);
            
            //this is not needed, but it shows you your UI has loaded (needs 'scaleform_spew 1' in console)
            trace("Custom UI loaded!");
        }
        
        //this handles the resizes, it sets the UI dimensions to your screen dimensions, credits to Nullscope
        public function onResize(re:ResizeManager) : * {
            //Calculate scale ratio
			var scaleRatioY:Number = re.ScreenHeight/900;
			
			//If the screen is bigger than our stage, keep elements the same size (you can remove this)
			if (re.ScreenHeight > 900){
				scaleRatioY = 1;
			}
                    
            //You will probably want to scale your elements by here, they keep the same width and height by default.
            
            //The engine keeps elements at the same X and Y coordinates even after resizing, you will probably want to adjust that here.
        }
    }
}

Compiling your UI

To compile your UI you need to open it in Flash. For people not familiar with Flash here's a short rundown of the important bits:

flash overview

As you can see the back square is your stage, this covers your entire Dota 2 interface, so anything you place on here will appear on that spot ingame. On the right you can see the two tabs. The library is very important, it contains all resources you are using in this .fla file.
Below that you can see the 'Class:' box. This contains the name of your main class, which is taken from a *.as file in your *.fla's directory and has the same name as your main class, so MainClass will be in MainClass.as.
At the stage properties you can see the fps and size. You can just leave the fps as it is, and for the size you can take whatever you like, as long as your resizing code works properly. I prefer having it at my native resolution, but I could also work on a slightly smaller stage.
At the right bottom corner I have placed the Align window. This is very useful for positioning elements, which I will demonstrate later.

To compile your UI just go to Debug > Debug Movie > Debug (or press the shortcut CTRL + SHIFT + ENTER). This places a .swf file in the same directory as your .fla file. You can put this file in your addon directory. Make sure it has the right name!

Important: Every time you change your .swf you HAVE to restart your dota 2 client!

Ingame type into the console: 'scaleform_spew 1' (without the quotes). Then load your addon. If you have done it correctly you will see success! somewhere in your console. You will not see anything ingame, because nothing that shows ingame was added yet, you can try drawing something on the stage and it will show up.

Adding modules

For the sake of this tutorial I will explain things by example. I will add a small panel with a button, that prints a message in console when the button is clicked. For demonstration purposes I will also give the dialogue a background image, to show how to import resources into your UI. I will put a link to the end result at the end of this page.

So let's get started:

UI <-> vscript communication

To make use of your custom UI panel in your agame mode you will need some way of communicating its results to your server vscript, and you will possibly also want that UI to get some data from the server script to display.

For this part of the tutorial we will add some functionality to the button we created before. For this example we will make the button 'buy' ability points. This means we will deduct the player's money and give him one extra ability point to spend. Once the player is out of money we would like to remove the button and display a message that there is not enough money anymore.

Upon inspecting function we can see there are two parts. Firstly communicating when the player clicks the button from the UI to your vscript. Secondly we want to send back if the player has enough money left. We will go over both ways of communication separately.

UI -> vscript

Flash
First up is sending a message from our UI to our server script. This is done by registering a command in our vscript, and executing that command when the button is pressed in our UI.
Recall the click handler in our module. This is what it currently looks like:

private function onButtonClicked(event:MouseEvent) : void {
    trace("click!");
}
We would like to call the command instead of the trace, so we change it to this:
private function onButtonClicked(event:MouseEvent) : void {
    //Send the 'BuyAbilityPoint' command to the server. We do not need the 1, but I left it in as a parameter you can pass with your command.
    If you want to multiple ability points with one click for example, you can pass the number of points instead of this 1.
    this.gameAPI.SendServerCommand("BuyAbilityPoint 1");
}
Now this modification causes a bit of a problem, because we do not have access to the gameAPI object inside our module. To solve this by adding a variable to store the gameAPI object in, and a setup() function to put the right value into that variable. After changing this, the top of our module file will look like this (I left out some parts that didn't change and replaced them with ...):
...
public class ExampleModule extends MovieClip {
    
    //hold the gameAPI
    var gameAPI:Object;
    
    public function ExampleModule() {
        this.button1.addEventListener(MouseEvent.CLICK, onButtonClicked);
    }
    
    //set initialise this instance's gameAPI
    public function setup(api:Object) {
        this.gameAPI = api;
    }
    
    private function onButtonClicked(event:MouseEvent) : void {
        this.gameAPI.SendServerCommand("BuyAbilityPoint 1");
    }
    
    ...
We will have to call the setup( ) ourselves from our mainclass, because that is where we get the gameAPI from the engine. We do this in the onLoaded function, because we know for sure the gameAPI object is available there. I just modified it to look like this:
//this function is called when the UI is loaded
public function onLoaded() : void {            
    //make this UI visible
    visible = true;
    
    //let the client rescale the UI
    Globals.instance.resizeManager.AddListener(this);
    
    //this is not needed, but it shows you your UI has loaded (needs 'scaleform_spew 1' in console)
    trace("Custom UI loaded!");
    
    //pass the gameAPI on to the module
    this.myModule.setup(this.gameAPI);
}
Vscript
The UI file is completely ready now, we just have to make sure our server script actually accepts this message from our UI. So open up your lua script and add the following to the InitGameMode() function:
--register the 'BuyAbilityPoint' command in our console
Convars:RegisterCommand( "BuyAbilityPoint", function(name, p)
    --get the player that sent the command
    local cmdPlayer = Convars:GetCommandClient()
    if cmdPlayer then 
        --if the player is valid, execute PlayerBuyAbilityPoint
        return self:PlayerBuyAbilityPoint( cmdPlayer, p ) 
    end
end, "A player buys an ability point", 0 )
It would actually be neater to make one function that registers all commands, so you only have to call that one function from your init, but I am trying to minimize the pseudo-code in this tutorial.

So now we also need a PlayerBuyAbilityPoint( ) function, if we want to do something like buying ability points it should look a bit like this:
function CustomGameMode:PlayerBuyAbilityPoint( player, p)
    --NOTE: p contains our parameter (the '1') now (as a string not a number), we just don't use it
    
    --determine a price for the ability point, you probably should do this globally
    local price = 200;
    
    --get the player's ID
    local pID = player:GetPlayerID()
    
    --get the players current gold
    local playerGold = PlayerResource:GetGold( pID )
    
    --check if the player has enough gold, checking extra doesn't hurt
    if playerGold >= price then
        --spend the gold
        PlayerResource:SpendGold( pID, price, 0 )
        --add the ability point to the player
        local playerHero = player:GetAssignedHero()
        playerHero:SetAbilityPoints(playerHero:GetAbilityPoints() + 1)
    end
end
Once you have these functions in place you should be all set to test and see if it actually works. Note that you do need to have selected a hero! So: jointeam good

vscript -> UI

Now we can communicate from the UI to the server script we can have any functionality we want, it is however not very responsive. To remedy this we would also like to send messages from our vscript to the UI. Doing this we can dynamically add/remove modules on the stage, or just make existing modules visible or invisible. In our case we could use it to let the UI know when we can not buy ability points anymore. This means we could disable the button or let the player know he does not have enough money anymore.

Custom events
This communication is not very straightforward however. The only way to get data from our vscript to the UI is to listen to game events in the UI. Now we are lucky we can make our own events, allowing us to send whatever we want to the UI. In this case I would like an event called player_gold_changed which contains the player_ID and gold_amount.
These custom events are defined in scripts/custom_events.txt in your addon directory. So open up this file and add the following event:

//cgm stands for customGameMode, it is a good idea to give your events a custom prefix so you don't accidentally cause collisions with existing events.
"cgm_player_gold_changed"
{
    //define the name of the parameter and the type, we use a short for these values (16 bit integer)
    "player_ID"        "short"
    "gold_amount"    "short"
}
I feel I have to clarify my design decision here: I could have just passed a boolean signifying if the player has enough gold. Instead I chose to pass the actual gold amount, which means that if I would have multiple buttons, all with different 'prices', we could disable them judging on the gold amount the player still has.

Vscript
So now we have defined our custom event, we have to fire it in lua. If we define our event like this, it should be called every time the player's gold changes. For now, we only modify the user's gold with this button, so we add it to the PlayerBuyAbilityPoint( ) function. You could imagine that when you have a shop it would be a good idea to also fire this event at different times. We modify our PlayerBuyAbilityPoint() like this:
function CvHGameMode:PlayerBuyAbilityPoint( player, p)
    local price = 200;
    local pID = player:GetPlayerID()
    local playerGold = PlayerResource:GetGold( pID )
    
    if playerGold >= price then
        PlayerResource:SpendGold( pID, price, 0 )
        local playerHero = player:GetAssignedHero()
        playerHero:SetAbilityPoints(playerHero:GetAbilityPoints() + 1)
    end
    
    --Fire the event. The second parameter is an object with all the event's parameters as properties
    --We have to get the player's gold again, because we have deducted the price from it since the last time we got it.
    FireGameEvent('cgm_player_gold_changed', { player_ID = pID, gold_amount = PlayerResource:GetGold( pID ) })
end
Flash
Now our lua fires the event correctly, we have to change our UI code to listen for it and actually handle the event. I want to start listening as soon as the module appears, so I'm going to add the listener on our setup( ) function. We modify our setup( ) function slightly, so it looks like this:
public function setup(api:Object, globals:Object) {
    this.gameAPI = api;
    //added globals to this module too (don't forget the variable!), we need it now
    this.globals = globals;
    
    //this is our listener for the event, onGoldUpdate() is the handler
    this.gameAPI.SubscribeToGameEvent("cgm_player_gold_changed", this.onGoldUpdate);
}
We will keep the onGoldUpdate( ) function that handles the event simple for now, but you can do whatever you want here, animate elements in and out, add filters, whatever you come up with is probably possible. We will however just remove the button when the user does not have enough money anymore. Usually you would just disable it temporarily, but for now this is nice and simple. onGoldUpdate( ) looks like this:
public function onGoldUpdate(args:Object) : void {
    //get the ID of the player this UI belongs to, here we use a scaleform function from globals
    var pID:int = globals.Players.GetLocalPlayer();
    
    //check of the player in the event is the owner of this UI. Note that args are the parameters of the event
    if (args.player_ID == pID) {
        //if we can not afford another ability point, we will remove the button
        if (args.gold_amount < 200) {
            this.removeChild(this.button1);
        }
    }
}
You are done now, compile this and test it. Once your gold drops below 200 you will see the button disappear, which means the communication was succesful!

Example UI code

I have put the code of the UI that has been talked about in this tutorial in this github repository

Flash Tips and Tricks

Some tips and tricks for flash: