Visual Studio Macros for the Game Programmer

Published November 12, 2007 by Mike Cline, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement

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.

Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement