• 11/12/07 09:57 PM
    Sign in to follow this  

    Visual Studio Macros for the Game Programmer

    General and Gameplay Programming

    Introduction

    Coding is all about flow. If you are like me, you are probably most productive when you get 'in the zone' -- concentrated on your task, without any distractions.

    Repetitive tasks can create distractions that take you out of the zone and impede your progress. Visual Studio is a good piece of software because it contains a macro programming language that helps you automate repetitive tasks. It does this by giving you access to a lot of its internal state, and letting you command the IDE programmatically. Most of what you can do with the keyboard and mouse in Visual Studio can also be done by writing a macro.

    One good example of a repetitive task that is easily automated is switching back and forth between the .cpp file and .h file for the same class. If you are writing a new class, you'll probably be toggling back and forth between these two files dozens of times. Why waste time searching for the file in the Solution Explorer or in a cluttered bar of tabs? That will surely take you out of the zone. Going back and forth between .h and .cpp is such a common thing to want to do that it's a wonder that they haven't built this function into Visual Studio (though Whole Tomato's Visual Assist add-in and a number of other add-ins have this feature).

    You don't need to go and pay for an expensive 3rd party add-in just to get this feature though. A very short macro will take care of it.

    We'll use this .cpp/.h swapping as a first example of a macro:

    ' if this file is a .cpp then open up the .h 
    '  or vice versa
    Sub OpenPartnerFile()
      Dim filename As String
      Dim partnerFilename As String
      filename = DTE.ActiveDocument.FullName
      If (filename.EndsWith(".h")) Then
        partnerFilename = filename.Substring(0, filename.Length() - 2) + ".cpp"
      End If
      If (filename.EndsWith(".cpp")) Then
        partnerFilename = filename.Substring(0, filename.Length() - 4) + ".h"
      End If
      DTE.ItemOperations.OpenFile(partnerFilename)
    End Sub
    
    The 'DTE' object is Visual Studio's automation object that you talk to through Visual Basic.

    In the case of the macro above, the only 2 operations we are doing with the DTE object are:

    • Getting the name of the current active document that is open in the editor (DTE.ActiveDocument.FullName)
    • Opening up a file (DTE.ItemOperations.OpenFile)
    The rest of the code here is just string manipulation to get the name of the file to open.

    Here are the steps that you follow to add this macro to your copy of Visual Studio:

    1. go to Tools->Macros->Macro IDE
    2. in the project explorer, go to MyMacros
    3. right click on MyMacros, go to Add, go to Add New Item, create a Module and name it whatever you want. Name it after yourself.
    4. Add the source code above to the module
    5. Save it and close the Macros IDE
    6. You can test it out by double clicking on the name of the macro in the Macro Explorer (Tools->Options->Macro Exlorer)
    The next thing you'll probably want to do, if it's a macro that you are planning to use a lot, is map it to a function key or a key-combination:
    1. go to Tools->Options->Keyboard
    2. in the Show Commands Containing text field, enter your macro name, or part of it
    3. Click on your macro's name in the list
    4. Click in the Press Shortcut Key field
    5. Press your key combination
    6. Press the Assign button
    Note: if the key combination is already assigned to something else, you might have to remove that mapping before you can use that key for your own macro.

    Macros for Calling External Tools

    Often, when you are working with a source file you will want to do something with the file that would normally require going outside the IDE. For example, you may want to perform some source control action on the file like doing a diff against the previous revision so you can see what changes you have made so far. The problem with this is that once you leave the IDE you lose the context that you are working in, and you have to navigate around to locate that file again. If you are working on a project that has a huge source tree, it can take some time to navigate through windows explorer and find your cpp file again. This is all unnecessary and takes you out of the zone once again. Here's a macro that lets you diff the current file against the depot, without leaving the zone:

    ' Diffs the current file against the latest revision.
    Sub DiffCurrentFile()
      Dim filename As String = DTE.ActiveWindow.Document.FullName
      Dim commandString = "diffCommand " + filename
      ChDir("d:\project")
      Shell("cmd /c """ + commandString + """", AppWinStyle.Hide, True, 100)
      'MsgBox(commandString)
    End Sub
    
    In the above example 'diffCommand' needs to be replaced with the command-line syntax that you would use to talk to whatever source control system you are using.

    If the macro does not work at first, you can uncomment the last line to pop up a dialog box showing exactly what the full command string is that the macro is creating and trying to run.

    Macros that Communicate with the Code Editor

    Another thing that Visual Studio macros can do is talk to the editor -- cut and paste code around for you, insert bits of text, manipulate a selected piece of text, etc.

    Here's an example that places braces around a selected chunk of code, then indents the block properly. Activating this macro with a single hotkey feels so much smoother than having to put in the braces manually.

    ' Takes the current selection, indents it, and surrounds with braces.
    Sub CreateCodeBlock()
      CreateCodeBlock_Helper("{", "}")
    End Sub
    
    ' Takes the current selection, indents it, and surrounds it with startOfBlock and endOfBlock
    Sub CreateCodeBlock_Helper(ByVal startOfBlock As String, ByVal endOfBlock As String)
      Dim sel As TextSelection = DTE.ActiveDocument.Selection
      Dim objAnchor As VirtualPoint = sel.AnchorPoint
      Dim bottomPoint As VirtualPoint = sel.BottomPoint
    
      Dim trimmedString As String = sel.Text.TrimStart()
      Dim numChars As Integer = sel.Text.Length - trimmedString.Length
      Dim spaceString As String = sel.Text.Substring(0, numChars)
    
      objAnchor.CreateEditPoint().Insert(spaceString + startOfBlock + vbCrLf)
      bottomPoint.CreateEditPoint().Insert(spaceString + endOfBlock + vbCrLf)
      sel.Indent()
      
      
      'sel.LineUp()
      'sel.Text = "{" + vbCrLf + sel.Text + "}" + vbCrLf
    End Sub
    
    Another useful thing to do with macros is pasting some kind of a template into the current document. I use the following macro quite often when I want to leave a comment in the code that includes my name and the date that the comment was made:
    ' Inserts name and date
    Sub Signature()
      Dim sel As TextSelection = DTE.ActiveDocument.Selection
      sel.Insert("MikeCline ")
      sel.Insert(Format(Date.Today, "MMM dd yyyy"))
      sel.Insert(".")
    End Sub
    
    It's really a simple thing, but it saves you from having to fumble around looking at the clock and if you have a lot of these comments it's good to have a consistent date format.

    Along the same vein, here's one that inserts a horizontal line in the code. Lots of people like to put these in between each function, and having a macro to do this saves you from having to cut and paste your horizontal line around all the time. I have this mapped in the editor to CTRL+ENTER, which is the hotkey that some word processors use for "new page".

    Sub InsertHorizontalLine()
      Dim lineString = "//--------------------------------------" + vbCrLf
      Dim sel As TextSelection = DTE.ActiveDocument().Selection()
      sel.Text() = lineString
    End Sub
    

    Macros that Take Advantage Of Visual Studio's Knowledge Of the Code Structure

    Visual Studio knows a lot about the structure of your code. The DTE object that you access through the macro language lets you have access to a lot of this knowledge when writing a macro.

    One of the more useful things that it knows about is where each symbol is defined. Right-clicking on a symbol and selecting "Go To Definition" is a powerful way of navigating around your code base, especially when you are unfamiliar with the layout of a project.

    When creating a brand new cpp file, you probably don't know which header files you need to #include until the compiler starts complaining about undefined identifiers. Even when the compiler complains about SomeClassXYZ being undefined, you might not have any clue where to find the definition yourself. The logical thing to do here is click on "Go To Definition" and have Visual Studio tell you where that symbol is defined.

    But wait... If Visual Studio knows where it is defined, why can't it go the extra mile and fix the problem for you? This functionality is not built in to Visual Studio, but with a little bit of macro trickery, you can connect the dots and get Visual Studio to write #include statements for you based on its own internal database of where everything is defined.

    The macro below essentially just piggybacks on the "Edit.GoToDeclaration" command built in to Visual Studio. The steps it takes are:

    1. Remember the name of the current file (the one we are editing)
    2. Call Edit.GoToDeclaration
    3. Remember the name of the current file (the one with the declaration)
    4. Return to the original file (the one we are editing)
    5. Go to the start of the file and search for the last #include statement
    6. Insert #include with the name of the file with the declaration
    As a macro, these steps look like this:
    ' Finds the definition/declaration of the currently selected word, then pastes an "#include" 
    ' near the top of your file with the correct include (hopefully) of the file
    ' that that thing is defined in.
    Sub IncludeFileForSelected()
      'Get the name of the current file, so we can come back to it later.
      Dim currentFile As String = DTE.ActiveDocument.Path + DTE.ActiveDocument.Name
      'Get the word underneath the cursor.
      Dim objSel As TextSelection = DTE.ActiveDocument.Selection
      Dim objAnchor As VirtualPoint = objSel.AnchorPoint
      Dim s As String = objSel.Text
      DTE.ExecuteCommand("Edit.GoToDeclaration", s)
      'Put the name of the include file into the clipboard ring
      Dim includeFileName As String = DTE.ActiveDocument.Path + DTE.ActiveDocument.Name
      includeFileName = ReplaceBackSlashWithForward(includeFileName)
    
      includeFileName = MakeIncludeRelative(includeFileName)
    
      Dim includeString As String = "#include " & Chr(34) & includeFileName & Chr(34) & vbCrLf
    
      'Go back to the file you were originally in.
      DTE.ItemOperations().OpenFile(currentFile)
    
      objSel = DTE.ActiveDocument.Selection
      objSel.StartOfDocument()
    
      'Move to the line after the last #include
      While (objSel.FindText("#include"))
      End While
      objSel.LineDown()
      'objSel.NewLine()
    
      objSel.Insert(includeString)
    End Sub
    
    The macro above uses the following string-manipulation function to chop off the beginning of the .h file's path, so that the #include statement is relative to the root of your source tree, rather than being an absolute path with your drive letter and everything else.
    ' Takes an absolute path of a .h file and makes it relative to the project include directories
    Function MakeIncludeRelative(ByVal includeFileName As String)
    
      Dim projectName As String = GetProjectName()
    
      'Replace baseDir with the base directory that all of your include are
      'relative to.
      Dim baseDir As String = "c:/MyProject/code/"
      If (includeFileName.ToLower().StartsWith(baseDir)) Then
        includeFileName = includeFileName.Substring(baseDir.Length())
      End If
      
      Return includeFileName
    
    End Function
    
    This function is also used since DTE.ActiveDocument.Name returns something with backslashes, but the C++ standard expects a path with forward slashes in #include statements.
            
    Public Function ReplaceBackSlashWithForward(ByVal s As String)
      Dim result As String = s
    
      Dim m As Match
      Dim ex As Regex
      ex = New Regex("\\")
    
      m = ex.Match(result)
    
      If (m.Success) Then
        result = ex.Replace(result, "\\", "/")
        m = ex.Match(result)
      End If
    
      Return result
    End Function
    

    Controlling External Applications Through a COM Interface

    At the game development studios I've worked at, CodeWarrior is used as debugger for PlayStation2 and Wii applications, but many people switch over to Visual Studio for writing their code because they are more familiar with it as an editing environment. I've found that I would often navigate through code in Visual Studio and then want to put a breakpoint in, requiring me to switch over to CodeWarrior and open the same file, scroll to the same line, and press F9 in the other window. This is another great example of where using a macro in Visual Studio to take advantage of the current working context can save time. Visual Basic makes it easy to talk to other applications that expose a COM interface. CodeWarrior is an example of an application that has an extensive COM interface. You can use the program OLEView to explore the interface that an application exposes. Looking at the ICodeWarriorApp interface in OLEView, I see that it has a function called Debugger which returns an ICodeWarriorDebugger, and looking at the definition of ICodeWarriorDebugger, I see that it has a function called SetBreakpointInSource. Based on this, it's pretty easy to whip up a macro that tells CodeWarrior to set a breakpoint at the current file and line that the cursor is on in Visual Studio:

            
    Sub CWSetBreakpointHere()
      Dim objSel As TextSelection = DTE.ActiveDocument.Selection
      Dim objAnchor As VirtualPoint = objSel.AnchorPoint
      Dim line As Long = objAnchor.Line
      Dim fileName As String = DTE.ActiveDocument.FullName()
    
      Dim CW = CreateObject("CodeWarrior.CodeWarriorApp")
      CW.Debugger.SetBreakpointInSource(fileName, line, 0)
    
      DTE.ExecuteCommand("Debug.ToggleBreakpoint")
      MsgBox("Breakpoint set in CodeWarrior: " + vbCrLf + fileName.ToString() + ":" + line.ToString())
    End Sub
    

    Conclusion

    Visual Studio provides a very flexible macro language that can accomplish just about anything you dream up. It really shines when used to take advantage of the IDE's internal knowledge of the file and project that you are currently working in, so that you don't have to be constantly taking your attention away.

    If you have any VS macros or tricks that you think are valuable to other game developers, I invite you to share them on my blog, which contains other macros as well.



      Report Article
    Sign in to follow this  


    User Feedback

    Create an account or sign in to leave a review

    You need to be a member in order to leave a review

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

    There are no reviews to display.